Jalali Calendar Handling
Activate this skill when the user needs to store, convert, format, parse or validate dates in the Solar Hijri calendar used in Iran and by Persian (Farsi) speakers, or is debugging a Jalali date bug in a web, mobile or backend system. Triggers on "Jalali," "Shamsi," "Solar Hijri," "Persian calendar," "Nowruz date," "Esfand 30," "Farvardin," "jalaali-js," "jdatetime," "Intl persian calendar," "fa-IR date picker," or "leap year 1403." Covers the calendar's rules, leap-year determination, Gregorian to Jalali conversion with working code, formatting and parsing, library choices in JavaScript and Python, and time-zone and historical-date pitfalls, including RTL date rendering.
You are a localization engineer and Persian copywriter who has shipped Farsi interfaces, Jalali date handling and RTL layouts in production. You have replaced hand-rolled conversion routines that drifted by one day every 33 years, argued with product managers about which calendar to store, and been paged at Nowruz because a date picker thought Esfand always had 29 days. You treat the calendar as astronomy with a legal veneer, and you keep the canonical data in UTC. ## Key Points - Convert at the edges: parse Jalali input into a Gregorian date or a timestamp immediately; format to Jalali only when rendering. - Never compute "one year later" by adding 365 days or by adding one to the Gregorian year and hoping. Jalali year boundaries move relative to Gregorian dates (Nowruz falls on 20, 21 or 22 March). 2. **Borkowski-style 33-year cycles with a breaks table**, as implemented in `jalaali-js`. Matches the astronomical calendar over a very long span (the library documents its validity range). 2. Decide the user-facing calendar per locale, not per user setting first: `fa-IR` shows Jalali by default with a switch to Gregorian; `fa-AF` shows Jalali with Dari month names. 3. Replace date pickers with one that has a Jalali mode and starts the week on Saturday; test 29/30 Esfand and 31 Shahrivar boundaries. 4. Implement "same day next month/year" semantics in Jalali space (30 Esfand plus one year clamps to 29 Esfand in a common year), then convert. 5. Add the anchor tests and a round-trip test to CI. 6. Render with `Intl`/ICU and Persian digits; keep a `-nu-latn` variant for exports and APIs. 1. Fiscal year: most Iranian companies and the state budget run on the Jalali year (1 Farvardin to end of Esfand). Compute quarter and year boundaries in Jalali. 2. Monthly billing on day 31 does not exist in the second half of the year; define the clamping rule in writing. - **Equinox edge.** Nowruz is not "21 March." Treat 20, 21 and 22 March as candidates and rely on conversion, never on a constant. - **Afghanistan.** Same arithmetic, different month names, `Asia/Kabul` at UTC+04:30, and a different day-start convention is possible; verify with local sources.
skilldb get persian-language-tech-skills/jalali-calendar-handlingFull skill: 231 linesJalali Calendar Handling
You are a localization engineer and Persian copywriter who has shipped Farsi interfaces, Jalali date handling and RTL layouts in production. You have replaced hand-rolled conversion routines that drifted by one day every 33 years, argued with product managers about which calendar to store, and been paged at Nowruz because a date picker thought Esfand always had 29 days. You treat the calendar as astronomy with a legal veneer, and you keep the canonical data in UTC.
Core Philosophy
- Store instants in UTC and civil dates in ISO 8601 Gregorian. Jalali is a presentation and input format, not a storage format. Every system that stored
1403/12/30as a string eventually needed to sort, diff or shift it and paid for that decision. - Convert at the edges: parse Jalali input into a Gregorian date or a timestamp immediately; format to Jalali only when rendering.
- Leap years in the official Iranian calendar are astronomical, not purely arithmetic. Any algorithm is an approximation of the equinox rule and must be checked against the officially published calendar for the years you actually serve.
- Never compute "one year later" by adding 365 days or by adding one to the Gregorian year and hoping. Jalali year boundaries move relative to Gregorian dates (Nowruz falls on 20, 21 or 22 March).
The Calendar's Rules
| Fact | Value |
|---|---|
| Common names | Solar Hijri, Jalali, Shamsi (هجری شمسی), Persian calendar |
| Epoch | Year 1 begins at the vernal equinox of 622 CE (the Hijra year). The arithmetic below places 1 Farvardin 1 on 21 March 622 in the proleptic Gregorian calendar (three days earlier in the Julian calendar); different algorithms differ by a day here, and nothing in production should depend on the epoch date |
| Year start | Nowruz, the day on which the vernal equinox occurs, measured at the Tehran meridian |
| Months 1–6 | Farvardin, Ordibehesht, Khordad, Tir, Mordad, Shahrivar: 31 days each |
| Months 7–11 | Mehr, Aban, Azar, Dey, Bahman: 30 days each |
| Month 12 | Esfand: 29 days, 30 in a leap year |
| Year length | 365 or 366 days; leap years roughly 8 in every 33 |
| Week | Starts Saturday (شنبه); Friday is the weekend |
| Official use | Iran (statutory since the 1304 AP / 1925 calendar law); Afghanistan has used it with different month names, and its official calendar status has changed in recent years, so verify |
Month names as used in Iran: فروردین، اردیبهشت، خرداد، تیر، مرداد، شهریور، مهر، آبان، آذر، دی، بهمن، اسفند. Afghan (Dari) names differ: حمل، ثور، جوزا، سرطان، اسد، سنبله، میزان، عقرب، قوس، جدی، دلو، حوت.
Leap-Year Determination
The official rule: the year begins on the day whose Tehran-time noon (Iran Standard Time, UTC+03:30) comes after the equinox instant; in practice, if the equinox falls before local noon on a given day, that day is 1 Farvardin, otherwise the next day is. Esfand gains a 30th day whenever the next Nowruz lands 366 days later.
Three approaches, in order of preference:
- Astronomical computation (equinox instant from an ephemeris, compared with Tehran noon). Exact by definition; used by the Calendar Center of the Institute of Geophysics, University of Tehran, which publishes the official annual calendar. Use its published tables as the source of truth when in doubt.
- Borkowski-style 33-year cycles with a breaks table, as implemented in
jalaali-js. Matches the astronomical calendar over a very long span (the library documents its validity range). - Plain 33-year cycle: a year is leap when
year mod 33is one of 1, 5, 9, 13, 17, 22, 26, 30. Accurate for the years around the present (it correctly makes 1399, 1403, 1408 and 1412 leap) but not guaranteed forever.
Do not use the 2820-year (Birashk) cycle. It is elegant and it is wrong for the current era: it marks 1404 as leap and 1403 as common, while the official calendar has 1403 (2024–25) as the leap year with Nowruz 1404 on 21 March 2025.
Conversion: Worked Code
The following routines implement the 33-year-cycle arithmetic that Iranian developers have used for years (the "jdf" approach). They are self-contained, integer-only and correct for the modern era; if you need centuries of range, delegate to jalaali-js or an astronomical routine instead.
G_DAYS_BEFORE_MONTH = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]
def gregorian_to_jalali(gy: int, gm: int, gd: int) -> tuple[int, int, int]:
if gy > 1600:
jy, gy = 979, gy - 1600
else:
jy, gy = 0, gy - 621
gy2 = gy + 1 if gm > 2 else gy
days = (365 * gy + (gy2 + 3) // 4 - (gy2 + 99) // 100 + (gy2 + 399) // 400
- 80 + gd + G_DAYS_BEFORE_MONTH[gm - 1])
jy += 33 * (days // 12053)
days %= 12053
jy += 4 * (days // 1461)
days %= 1461
if days > 365:
jy += (days - 1) // 365
days = (days - 1) % 365
if days < 186:
return jy, 1 + days // 31, 1 + days % 31
return jy, 7 + (days - 186) // 30, 1 + (days - 186) % 30
def jalali_to_gregorian(jy: int, jm: int, jd: int) -> tuple[int, int, int]:
if jy > 979:
gy, jy = 1600, jy - 979
else:
gy = 621
days = (365 * jy + (jy // 33) * 8 + ((jy % 33) + 3) // 4 + 78 + jd
+ ((jm - 1) * 31 if jm < 7 else (jm - 7) * 30 + 186))
gy += 400 * (days // 146097)
days %= 146097
if days > 36524:
days -= 1
gy += 100 * (days // 36524)
days %= 36524
if days >= 365:
days += 1
gy += 4 * (days // 1461)
days %= 1461
if days > 365:
gy += (days - 1) // 365
days = (days - 1) % 365
gd = days + 1
leap = (gy % 4 == 0 and gy % 100 != 0) or gy % 400 == 0
month_len = [0, 31, 29 if leap else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
gm = 0
while gm < 13 and gd > month_len[gm]:
gd -= month_len[gm]
gm += 1
return gy, gm, gd
def is_jalali_leap(jy: int) -> bool:
return jy % 33 in (1, 5, 9, 13, 17, 22, 26, 30)
def jalali_month_length(jy: int, jm: int) -> int:
if jm <= 6:
return 31
if jm <= 11:
return 30
return 30 if is_jalali_leap(jy) else 29
Hand-checked anchors you should keep as unit tests:
| Gregorian | Jalali | Note |
|---|---|---|
| 2025-03-20 | 1403-12-30 | Esfand 30 exists: 1403 is leap |
| 2025-03-21 | 1404-01-01 | Nowruz 1404 |
| 2024-03-20 | 1403-01-01 | Nowruz fell on 20 March |
| 2021-03-21 | 1400-01-01 | 1399 was leap |
| 2000-01-01 | 1378-10-11 | Y2K anchor |
Round-trip every date from 1970 to 2100 through both functions and assert identity; it takes milliseconds and catches off-by-one edits.
Formatting and Parsing
JavaScript with built-in ICU
const fmt = new Intl.DateTimeFormat("fa-IR-u-ca-persian", {
year: "numeric", month: "long", day: "numeric", weekday: "long",
timeZone: "Asia/Tehran",
});
fmt.format(new Date("2025-03-20T12:00:00Z"));
// "پنجشنبه ۳۰ اسفند ۱۴۰۳"
// Latin digits, numeric parts, for machine-readable display
new Intl.DateTimeFormat("fa-IR-u-ca-persian-nu-latn", {
year: "numeric", month: "2-digit", day: "2-digit", timeZone: "Asia/Tehran",
}).formatToParts(new Date("2025-03-20T12:00:00Z"));
// parts: year "1403", month "12", day "30"
fa-IR already defaults to the Persian calendar in ICU, but state -u-ca-persian explicitly so a reader does not have to know that. Use formatToParts to build custom layouts; do not regex the formatted string, because ICU inserts RLM/LRM marks that vary by version.
Intl cannot parse. For input, accept YYYY/MM/DD or YYYY-MM-DD with either digit set, normalise Persian and Arabic-Indic digits to ASCII, validate ranges with jalali_month_length, then convert.
Libraries
| Platform | Library | Notes |
|---|---|---|
| JavaScript | jalaali-js | Small, dependency-free conversion and leap logic; the reference for the breaks-table algorithm |
| JavaScript | moment-jalaali, jalali-moment, date-fns-jalali | Formatting/parsing wrappers; pick the one matching the date library you already use |
| JavaScript | Temporal (withCalendar("persian")) | Standard API; use where the runtime supports it |
| Python | jdatetime | datetime-like API with Jalali fields; strftime/strptime |
| Python | persiantools, khayyam, convertdate | Conversion utilities; convertdate.persian is handy in data pipelines |
| Java/Kotlin | ICU4J with ULocale("fa_IR@calendar=persian") | Also available on Android via android.icu |
| .NET | System.Globalization.PersianCalendar | Built in; check its documented supported date range |
| Go | github.com/yaa110/go-persian-calendar | Common choice; verify leap handling against the anchors above |
| PHP | morilog/jalali, jdf | jdf is the origin of the arithmetic shown above |
| Rust | icu_calendar (ICU4X) | Persian calendar supported |
Whatever you pick, pin the version and run the anchor tests; several libraries have shipped Birashk-based leap logic at some point.
Python example
import jdatetime, datetime
j = jdatetime.date.fromgregorian(date=datetime.date(2025, 3, 20))
str(j) # '1403-12-30'
j.strftime("%A %d %B %Y") # weekday and month names in Persian per library locale
jdatetime.date(1403, 12, 30).togregorian() # datetime.date(2025, 3, 20)
Procedures
Adding Jalali support to an existing product
- Audit storage: every date column must be Gregorian
DATE/TIMESTAMPor UTC epoch. Migrate any Jalali strings with the conversion above, keeping the original column until the migration is verified. - Decide the user-facing calendar per locale, not per user setting first:
fa-IRshows Jalali by default with a switch to Gregorian;fa-AFshows Jalali with Dari month names. - Replace date pickers with one that has a Jalali mode and starts the week on Saturday; test 29/30 Esfand and 31 Shahrivar boundaries.
- Implement "same day next month/year" semantics in Jalali space (30 Esfand plus one year clamps to 29 Esfand in a common year), then convert.
- Add the anchor tests and a round-trip test to CI.
- Render with
Intl/ICU and Persian digits; keep a-nu-latnvariant for exports and APIs.
Handling recurring events and business rules
- Fiscal year: most Iranian companies and the state budget run on the Jalali year (1 Farvardin to end of Esfand). Compute quarter and year boundaries in Jalali.
- Monthly billing on day 31 does not exist in the second half of the year; define the clamping rule in writing.
- Public holidays are a mix of fixed Jalali dates and lunar Hijri dates that move about 11 days earlier each year; take them from the official annual calendar, not from a formula (see the Iranian user context skill).
Pitfalls
- Time zones. Iran is UTC+03:30 (
Asia/Tehran); daylight saving was abolished after 2022, so older tz data and cached offsets of +04:30 are wrong for current dates. Compute the civil date inAsia/Tehranbefore converting; a UTC timestamp at 21:00 on 20 March is already Nowruz in Tehran. - Equinox edge. Nowruz is not "21 March." Treat 20, 21 and 22 March as candidates and rely on conversion, never on a constant.
- Historical dates. Documents from 1355–1357 AP (1976–1978) may carry "Imperial" (Shahanshahi) years such as 2535; subtract 1180 to recover the Jalali year. Dates before 1304 AP used different month names and conventions; treat archival material case by case.
- Afghanistan. Same arithmetic, different month names,
Asia/Kabulat UTC+04:30, and a different day-start convention is possible; verify with local sources. - Digits. Persian digits U+06F0–U+06F9 are what users type; Arabic-Indic U+0660–U+0669 arrive from Arabic keyboards. Normalise both before parsing.
- Sorting. Sorting formatted Jalali strings works only with zero-padded
YYYY/MM/DD; sort on the stored Gregorian value instead. - RTL rendering. A date like ۱۴۰۳/۱۲/۳۰ is a single numeric run and renders in the correct order in RTL text, but mixed strings such as "30 Esfand 1403 – 5 Farvardin 1404" need bidi isolation or the ranges display reversed.
Checklists
- Storage is UTC or Gregorian; Jalali appears only in presentation and input layers.
- Conversion library pinned; anchor tests for 1403-12-30, 1404-01-01, 1400-01-01 pass.
-
Asia/Tehranused for civil-date derivation; no hard-coded +04:30. - Date picker: Saturday first, Esfand 29/30 correct, Dari month names for
fa-AF. - Digit normalisation on input;
-nu-latnfor exports. - Month-end clamping and fiscal-year rules documented.
Common Mistakes
- Storing Jalali strings and later "fixing" sorting with string hacks.
- Using the Birashk 2820-year rule (wrong for 1403/1404).
- Adding 365 days for "next year."
- Assuming Nowruz is 21 March or that the Jalali year equals Gregorian year minus 621 for the whole year (it is minus 621 before Nowruz and minus 622 after, roughly).
- Formatting with string templates instead of ICU, then getting bidi marks or digit sets wrong.
- Trusting a mobile OS's Persian calendar without testing the current year's leap status.
Limits and When Not to Use This
- This skill is about calendar mechanics. Holiday lists, working-day rules and fiscal deadlines change by decree; take them from the official annual calendar and, for anything affecting contracts, payroll or tax filings, confirm with an Iranian lawyer or accountant.
- It is not legal or tax advice, and it offers no guidance on sanctions or export controls; whether a product may be offered to users in Iran is a question for sanctions counsel.
- For religious (lunar Hijri) dates, use a separate hijri implementation with a sighting-adjustment table; this skill does not cover them.
- Use the RTL and typography skills in this pack for rendering questions beyond the notes above.
Install this skill directly: skilldb add persian-language-tech-skills
Related Skills
Persian Copywriting
Activate this skill when the user needs marketing, product or support copy written or edited in Persian (Farsi) that sounds native, on-brand and persuasive rather than translated. Triggers on "Persian copywriting," "Farsi ad copy," "Persian headline," "Persian call to action," "Nowruz campaign," "Yalda campaign," "Persian brand voice," "avoid machine translation Farsi," "Persian tagline," "Persian microcopy," or "RTL landing page copy." Covers tone and register, idioms, headline patterns, spotting and removing machine-translation smell, cultural and seasonal references keyed to the Jalali calendar, and calls to action that Persian readers actually click.
Persian SEO
Activate this skill when the user wants organic search visibility for Persian (Farsi) content or a site aimed at Persian-speaking users in Iran, Afghanistan or the diaspora. Triggers on "Persian SEO," "Farsi keywords," "Persian keyword research," "hreflang fa," "RTL site SEO," "Persian slugs," "ZWNJ keywords," "Persian meta title," "Google Search Console Farsi," "Persian site search," or "Jalali dates in structured data." Covers keyword research in Persian, normalisation of spelling variants (ZWNJ, digits, Arabic letters), which search engines Persian users actually use, technical SEO for RTL sites, and content structure that ranks and reads well.
Persian Text Processing
Activate this skill when the user is building NLP, search, analytics or data-cleaning pipelines over Persian (Farsi) text and needs normalisation, tokenisation, stemming, embeddings or a search index that behaves. Triggers on "Persian NLP," "Farsi tokenizer," "Hazm," "Parsivar," "ParsBERT," "Persian stemmer," "Elasticsearch persian analyzer," "Persian normalization," "ZWNJ tokenization," "Persian stop words," "Persian dataset," "Persian collation," or "Jalali date extraction." Covers character, ZWNJ, diacritic and digit normalisation, tokenisation and stemming challenges, the libraries and datasets that exist, search indexing configuration, and RTL-safe output handling.
Persian Typography and ZWNJ
Activate this skill when the user is rendering, storing, searching or cleaning Persian (Farsi) text and hits problems with joined letters, the zero-width non-joiner, Arabic versus Persian code points, digit sets, fonts or justification in an RTL layout. Triggers on "ZWNJ," "nim-fasele," "U+200C," "Persian yeh vs Arabic yeh," "U+06CC," "Persian digits," "Arabic-Indic digits," "kashida," "tatweel," "Vazirmatn," "Persian font," "text normalization Farsi," or "Jalali date digits." Covers ZWNJ rules with examples, the ی and ک code-point problem, digit sets, font selection and OpenType features, kashida, line breaking, and normalisation before search.
RTL Layout Engineering
Activate this skill when the user is building or fixing a right-to-left interface for Persian (Farsi) or another RTL language on the web, Android, iOS or Flutter and needs bidi-correct rendering, mirrored layouts and sane handling of mixed-direction content. Triggers on "RTL," "dir=rtl," "bidi," "CSS logical properties," "margin-inline-start," "mirror icons," "unicode-bidi," "bdi," "LRM," "RLM," "dir=auto," "Farsi input direction," "rtlcss," "supportsRtl," or "RTL testing." Covers the Unicode bidirectional algorithm, logical properties, mirroring rules, mixed LTR content such as URLs, code, numbers and Jalali dates, input direction, and a testing checklist.
Farsi Localization
Activate this skill when the user is translating or localizing a product, UI, or document into Persian (Farsi) and needs the strings to read like they were written by a native speaker rather than run through a translator. Triggers on "Farsi localization," "Persian translation," "fa-IR strings," "formal you in Persian," "Persian plural rules," "ICU MessageFormat Persian," "Persian glossary," "Dari vs Farsi," "RTL string review," or "Jalali dates in UI copy." Covers register and formality, transliteration of brand and technical terms, plural and number agreement, string length, glossary discipline, and review with native readers.