If you've ever built an app for Iranian users, you've run into the Persian calendar problem. Iran uses the Solar Hijri (Jalali) calendar as its official calendar — not Gregorian. So when a user types their birthdate as 1370/06/15, you can't just store it as-is if your backend expects Gregorian dates.
In this post I'll show you a pure JavaScript implementation with zero external dependencies that handles both directions: Jalali ↔ Gregorian. This is the same algorithm powering تبدیل تاریخ — tabdiltarikh.ir, a free Persian date converter tool.
A Quick Background on the Jalali Calendar
The Jalali (Solar Hijri) calendar:
- Is the official calendar of Iran and Afghanistan
- Starts from the vernal equinox (Nowruz, March 20/21)
- Has 6 months of 31 days, 5 months of 30 days, and 1 month of 29 or 30 days (leap year)
- Leap years follow a 4-5 year pattern, not strictly every 4 years like Gregorian
- Is currently 621 or 622 years behind the Gregorian calendar (depending on the month)
The Algorithm
We use the astronomical Jalali algorithm — the same one used in Iran's official calendar. It works via Julian Day Numbers (JDN) as an intermediate step, which makes bidirectional conversion straightforward.
javascriptcopy// Helper: integer division
function div(a, b) {
return ~~(a / b);
}// Core: compute Jalali calendar properties for a given Jalali year
function jalCal(jy) {
const breaks = [
-61, 9, 38, 199, 426, 686, 756, 818, 1111, 1181,
1210, 1635, 2060, 2097, 2192, 2262, 2324, 2394, 2456, 3178
];
const bl = breaks.length;
const gy = jy + 621;
let leapJ = -14, jp = breaks[0], jm, jump, leap, n, i;if (jy < jp || jy >= breaks[bl - 1])
throw new Error('Invalid Jalaali year ' + jy);for (i = 1; i < bl; i++) {
jm = breaks[i];
jump = jm - jp;
if (jy < jm) break;
leapJ = leapJ + div(jump, 33) * 8 + div(jump % 33, 4);
jp = jm;
}n = jy - jp;
leapJ = leapJ + div(n, 33) * 8 + div((n % 33) + 3, 4);
if ((jump % 33) === 4 && jump - n === 4) leapJ++;const leapG = div(gy, 4) - div((div(gy, 100) + 1) * 3, 4) - 150;
const march = 20 + leapJ - leapG;if (jump - n < 6) n = n - jump + div(jump + 4, 33) * 33;
leap = (((n + 1) % 33) - 1) % 4;
if (leap === -1) leap = 4;return { leap, gy, march };
}// Gregorian date → Julian Day Number
function g2d(gy, gm, gd) {
let d = div((gy + div(gm - 8, 6) + 100100) * 1461, 4)
+ div(153 * ((gm + 9) % 12) + 2, 5)
+ gd - 34840408;
d = d - div(div(gy + 100100 + div(gm - 8, 6), 100) * 3, 4) + 752;
return d;
}// Julian Day Number → Gregorian date
function d2g(jdn) {
let j = 4 * jdn + 139361631;
j = j + div(div(4 * jdn + 183187720, 146097) * 3, 4) * 4 - 3908;
const i = div((j % 1461), 4) * 5 + 308;
const gd = div(i % 153, 5) + 1;
const gm = (div(i, 153) % 12) + 1;
const gy = div(j, 1461) - 100100 + div(8 - gm, 6);
return { gy, gm, gd };
}// Jalali date → Julian Day Number
function j2d(jy, jm, jd) {
const r = jalCal(jy);
return g2d(r.gy, 3, r.march)
+ (jm - 1) * 31
- div(jm, 7) * (jm - 7)
+ jd - 1;
}// Julian Day Number → Jalali date
function d2j(jdn) {
const gy = d2g(jdn).gy;
let jy = gy - 621;
let r = jalCal(jy);
let jdn1f = g2d(gy, 3, r.march);
let jd, jm, k;k = jdn - jdn1f;
if (k >= 0) {
if (k <= 185) {
jm = 1 + div(k, 31);
jd = (k % 31) + 1;
return { jy, jm, jd };
} else {
k -= 186;
}
} else {
jy -= 1;
k += 179;
if (r.leap === 1) k++;
}
jm = 7 + div(k, 30);
jd = (k % 30) + 1;
return { jy, jm, jd };
}
The Public API
Two clean functions on top of the algorithm above:
javascriptcopy/**
- Convert Jalali (Persian) date to Gregorian
- @param {number} jy - Jalali year (e.g. 1403)
- @param {number} jm - Jalali month (1–12)
- @param {number} jd - Jalali day (1–31)
- @returns {number[]} [gregorianYear, gregorianMonth, gregorianDay]
*/
function jalaaliToGregorian(jy, jm, jd) {
const d = d2g(j2d(jy, jm, jd));
return [d.gy, d.gm, d.gd];
}
/**
- Convert Gregorian date to Jalali (Persian)
- @param {number} gy - Gregorian year
- @param {number} gm - Gregorian month (1–12)
- @param {number} gd - Gregorian day
@returns {number[]} [jalaliYear, jalaliMonth, jalaliDay]
*/
function gregorianToJalaali(gy, gm, gd) {
const d = d2j(g2d(gy, gm, gd));
return [d.jy, d.jm, d.jd];
}
Usage Examples
javascriptcopy// Jalali → Gregorian
const [gy, gm, gd] = jalaaliToGregorian(1403, 1, 1);
console.log(${gy}-${gm}-${gd}); // 2024-3-20// Gregorian → Jalali
const [jy, jm, jd] = gregorianToJalaali(2024, 3, 20);
console.log(${jy}/${jm}/${jd}); // 1403/1/1
// Convert a user's Persian birthdate to Gregorian
const birthJalali = { y: 1370, m: 6, d: 15 };
const [by, bm, bd] = jalaaliToGregorian(birthJalali.y, birthJalali.m, birthJalali.d);
console.log(${by}-${String(bm).padStart(2,'0')}-${String(bd).padStart(2,'0')});
// → 1991-09-06
Leap Year Check
Jalali leap years don't follow the simple "divisible by 4" rule. Use this:
javascriptcopyfunction isLeapJalaliYear(jy) {
return jalCal(jy).leap === 0;
}
console.log(isLeapJalaliYear(1403)); // true (1403 is a leap year)
console.log(isLeapJalaliYear(1404)); // false
console.log(isLeapJalaliYear(1408)); // true (next leap year after 1403)
The pattern for recent leap years: 1399, 1403, 1408, 1412 — notice the 4-4-4-5 year gap.
Month Lengths
javascriptcopyfunction jalaliMonthLength(jy, jm) {
if (jm <= 6) return 31; // Farvardin–Shahrivar
if (jm <= 11) return 30; // Mehr–Bahman
return isLeapJalaliYear(jy) ? 30 : 29; // Esfand
}
Why Not Just Use a Library?
You can — jalaali-js and moment-jalaali are solid. But if you only need date conversion (no formatting, no locale, no timezone handling), this self-contained implementation is under 2KB minified and has zero npm dependencies. Useful for edge functions, service workers, or any environment where bundle size matters.
Real-World Usage
If you need a ready-to-use tool rather than implementing it yourself, تبدیل تاریخ provides a free Persian date converter that supports Jalali ↔ Gregorian, Jalali ↔ Hijri (lunar), and age calculation. It also offers a free embeddable widget — two lines of code to add it to any site:
htmlcopy<div data-ttkw></div>
<script src="https://tabdiltarikh.ir/widget.js"></script>
Summary
| Feature | This implementation |
|---|---|
| Jalali → Gregorian | ✅ |
| Gregorian → Jalali | ✅ |
| Leap year detection | ✅ |
| Month length | ✅ |
| Dependencies | Zero |
| Valid range | Year 1–1500 Jalali |
| Algorithm | Astronomical Jalali (zero error) |
Drop the full code into any JS project and you're done. No build step, no install, no configuration.
Built something with Persian dates? Share it in the comments — always curious to see what the dev community builds for Farsi-speaking users.
Top comments (0)