Skip to main content
Countries & MarketsPersian Language Tech231 lines

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.

Quick Summary18 lines
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 lines
Paste into your CLAUDE.md or agent config

Jalali 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/30 as 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

FactValue
Common namesSolar Hijri, Jalali, Shamsi (هجری شمسی), Persian calendar
EpochYear 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 startNowruz, the day on which the vernal equinox occurs, measured at the Tehran meridian
Months 1–6Farvardin, Ordibehesht, Khordad, Tir, Mordad, Shahrivar: 31 days each
Months 7–11Mehr, Aban, Azar, Dey, Bahman: 30 days each
Month 12Esfand: 29 days, 30 in a leap year
Year length365 or 366 days; leap years roughly 8 in every 33
WeekStarts Saturday (شنبه); Friday is the weekend
Official useIran (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:

  1. 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.
  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).
  3. Plain 33-year cycle: a year is leap when year mod 33 is 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:

GregorianJalaliNote
2025-03-201403-12-30Esfand 30 exists: 1403 is leap
2025-03-211404-01-01Nowruz 1404
2024-03-201403-01-01Nowruz fell on 20 March
2021-03-211400-01-011399 was leap
2000-01-011378-10-11Y2K 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

PlatformLibraryNotes
JavaScriptjalaali-jsSmall, dependency-free conversion and leap logic; the reference for the breaks-table algorithm
JavaScriptmoment-jalaali, jalali-moment, date-fns-jalaliFormatting/parsing wrappers; pick the one matching the date library you already use
JavaScriptTemporal (withCalendar("persian"))Standard API; use where the runtime supports it
Pythonjdatetimedatetime-like API with Jalali fields; strftime/strptime
Pythonpersiantools, khayyam, convertdateConversion utilities; convertdate.persian is handy in data pipelines
Java/KotlinICU4J with ULocale("fa_IR@calendar=persian")Also available on Android via android.icu
.NETSystem.Globalization.PersianCalendarBuilt in; check its documented supported date range
Gogithub.com/yaa110/go-persian-calendarCommon choice; verify leap handling against the anchors above
PHPmorilog/jalali, jdfjdf is the origin of the arithmetic shown above
Rusticu_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

  1. Audit storage: every date column must be Gregorian DATE/TIMESTAMP or UTC epoch. Migrate any Jalali strings with the conversion above, keeping the original column until the migration is verified.
  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.

Handling recurring events and business rules

  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.
  3. 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 in Asia/Tehran before 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/Kabul at 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/Tehran used 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-latn for 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

Get CLI access →

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 Language Tech181L

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 Language Tech182L

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 Language Tech196L

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.

Persian Language Tech199L

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.

Persian Language Tech186L

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.

Persian Language Tech199L