""" تبدیلِ تاریخِ شمسی ↔ میلادی برای تاریخِ تولد. الگوریتمِ jalaali (همان jalaali-js، بورکوفسکی): کبیسه‌ها از جدولِ نقطه‌های شکستِ چرخه حساب می‌شوند، نه از قاعده‌ی سی‌وسه‌ساله‌ی ساده، و برای سال‌های ۱۱۷۸ تا ۱۶۳۳ با تقویمِ رسمی یکی است. تست‌ها (`accounts/tests_birth_date.py`) چند تاریخِ شناخته‌شده‌ی دو طرفِ اسفندِ کبیسه را می‌سنجند. چرا در بک‌اند: فرانت فقط روز/ماه/سالِ شمسی می‌فرستد. اگر هر دو طرف تبدیل می‌کردند، دو پیاده‌سازی می‌توانست یک روز با هم اختلاف داشته باشد. """ from datetime import date _BREAKS = [-61, 9, 38, 199, 426, 686, 756, 818, 1111, 1181, 1210, 1635, 2060, 2097, 2192, 2262, 2324, 2394, 2456, 3178] def _cal(jy): """(leap, gy, march) — leap: سالِ مانده در چرخه؛ march: روزِ مارسِ نوروز.""" if jy < _BREAKS[0] or jy >= _BREAKS[-1]: raise ValueError(f'سالِ شمسیِ خارج از بازه: {jy}') gy = jy + 621 leap_j = -14 jp = _BREAKS[0] jump = 0 for jm in _BREAKS[1:]: jump = jm - jp if jy < jm: break leap_j += (jump // 33) * 8 + (jump % 33) // 4 jp = jm n = jy - jp leap_j += (n // 33) * 8 + ((n % 33) + 3) // 4 if jump % 33 == 4 and jump - n == 4: leap_j += 1 leap_g = gy // 4 - ((gy // 100 + 1) * 3) // 4 - 150 march = 20 + leap_j - leap_g if jump - n < 6: n = n - jump + ((jump + 4) // 33) * 33 leap = (((n + 1) % 33) - 1) % 4 if leap == -1: leap = 4 return leap, gy, march def is_leap(jy: int) -> bool: return _cal(jy)[0] == 0 def month_length(jy: int, jm: int) -> int: if jm <= 6: return 31 if jm <= 11: return 30 return 30 if is_leap(jy) else 29 def to_gregorian(jy: int, jm: int, jd: int) -> date: """ValueError برای تاریخِ ناموجود (مثلاً ۳۰ اسفندِ سالِ غیرکبیسه).""" if not 1 <= jm <= 12 or not 1 <= jd <= month_length(jy, jm): raise ValueError('چنین روزی در تقویمِ شمسی وجود ندارد.') _, gy, march = _cal(jy) nowruz = date(gy, 3, march) day_of_year = (jm - 1) * 31 - (jm - 7) * (jm >= 7) + jd - 1 return date.fromordinal(nowruz.toordinal() + day_of_year) def from_gregorian(d: date) -> tuple[int, int, int]: jy = d.year - 621 _, gy, march = _cal(jy) k = d.toordinal() - date(gy, 3, march).toordinal() if k < 0: jy -= 1 _, gy, march = _cal(jy) k = d.toordinal() - date(gy, 3, march).toordinal() if k < 186: return jy, k // 31 + 1, k % 31 + 1 k -= 186 return jy, k // 30 + 7, k % 30 + 1 def parse(value: str) -> date: """«1385-04-12» (ارقامِ فارسی هم) → تاریخِ میلادی.""" from accounts.phone import ascii_digits parts = ascii_digits(str(value)).strip().replace('/', '-').split('-') # isascii: «²» برای isdigit رقم است ولی int() آن را نمی‌پذیرد (پیامِ خطای انگلیسیِ پایتون). if len(parts) != 3 or not all(p.isascii() and p.isdigit() for p in parts): raise ValueError('تاریخ باید به شکلِ سال-ماه-روز باشد، مثلاً 1385-04-12.') return to_gregorian(int(parts[0]), int(parts[1]), int(parts[2])) def to_string(d: date) -> str: jy, jm, jd = from_gregorian(d) return f'{jy:04d}-{jm:02d}-{jd:02d}' def age_of(birth_date: date | None, birth_year: int | None, today: date | None = None) -> int | None: """سنِ کامل از تاریخِ تولد؛ برای کاربری که فقط سالِ شمسی داده، تقریبی (سال منهای سال).""" from django.utils import timezone today = today or timezone.localdate() if birth_date: return today.year - birth_date.year - ((today.month, today.day) < (birth_date.month, birth_date.day)) if birth_year: return from_gregorian(today)[0] - birth_year return None