Skip to content

Commit 784737e

Browse files
authored
Fix Persian/Gregorian calendar conversion (#6118)
1 parent 049c672 commit 784737e

File tree

3 files changed

+55
-40
lines changed

3 files changed

+55
-40
lines changed

packages/@internationalized/date/src/calendars/PersianCalendar.ts

Lines changed: 32 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -17,30 +17,23 @@ import {AnyCalendarDate, Calendar} from '../types';
1717
import {CalendarDate} from '../CalendarDate';
1818
import {mod} from '../utils';
1919

20-
const PERSIAN_EPOCH = 1948321; // 622/03/19 Julian C.E.
21-
22-
function isLeapYear(year: number): boolean {
23-
let y0 = year > 0 ? year - 474 : year - 473;
24-
let y1 = mod(y0, 2820) + 474;
25-
26-
return mod((y1 + 38) * 31, 128) < 31;
27-
}
28-
29-
function persianToJulianDay(year: number, month: number, day: number): number {
30-
let y0 = year > 0 ? year - 474 : year - 473;
31-
let y1 = mod(y0, 2820) + 474;
32-
let offset = month <= 7 ? 31 * (month - 1) : 30 * (month - 1) + 6;
33-
34-
return (
35-
PERSIAN_EPOCH -
36-
1 +
37-
1029983 * Math.floor(y0 / 2820) +
38-
365 * (y1 - 1) +
39-
Math.floor((31 * y1 - 5) / 128) +
40-
offset +
41-
day
42-
);
43-
}
20+
const PERSIAN_EPOCH = 1948320;
21+
22+
// Number of days from the start of the year to the start of each month.
23+
const MONTH_START = [
24+
0, // Farvardin
25+
31, // Ordibehesht
26+
62, // Khordad
27+
93, // Tir
28+
124, // Mordad
29+
155, // Shahrivar
30+
186, // Mehr
31+
216, // Aban
32+
246, // Azar
33+
276, // Dey
34+
306, // Bahman
35+
336 // Esfand
36+
];
4437

4538
/**
4639
* The Persian calendar is the main calendar used in Iran and Afghanistan. It has 12 months
@@ -52,24 +45,22 @@ export class PersianCalendar implements Calendar {
5245
identifier = 'persian';
5346

5447
fromJulianDay(jd: number): CalendarDate {
55-
let d0 = jd - persianToJulianDay(475, 1, 1);
56-
let n2820 = Math.floor(d0 / 1029983);
57-
let d1 = mod(d0, 1029983);
58-
let y2820 = d1 === 1029982 ? 2820 : Math.floor((128 * d1 + 46878) / 46751);
59-
let year = 474 + 2820 * n2820 + y2820;
60-
if (year <= 0) {
61-
year--;
62-
}
63-
64-
let yDay = jd - persianToJulianDay(year, 1, 1) + 1;
65-
let month = yDay <= 186 ? Math.ceil(yDay / 31) : Math.ceil((yDay - 6) / 31);
66-
let day = jd - persianToJulianDay(year, month, 1) + 1;
67-
68-
return new CalendarDate(this, year, month, day);
48+
let daysSinceEpoch = jd - PERSIAN_EPOCH;
49+
let year = 1 + Math.floor((33 * daysSinceEpoch + 3) / 12053);
50+
let farvardin1 = 365 * (year - 1) + Math.floor((8 * year + 21) / 33);
51+
let dayOfYear = daysSinceEpoch - farvardin1;
52+
let month = dayOfYear < 216
53+
? Math.floor(dayOfYear / 31)
54+
: Math.floor((dayOfYear - 6) / 30);
55+
let day = dayOfYear - MONTH_START[month] + 1;
56+
return new CalendarDate(this, year, month + 1, day);
6957
}
7058

7159
toJulianDay(date: AnyCalendarDate): number {
72-
return persianToJulianDay(date.year, date.month, date.day);
60+
let jd = PERSIAN_EPOCH - 1 + 365 * (date.year - 1) + Math.floor((8 * date.year + 21) / 33);
61+
jd += MONTH_START[date.month - 1];
62+
jd += date.day;
63+
return jd;
7364
}
7465

7566
getMonthsInYear(): number {
@@ -85,7 +76,8 @@ export class PersianCalendar implements Calendar {
8576
return 30;
8677
}
8778

88-
return isLeapYear(date.year) ? 30 : 29;
79+
let isLeapYear = mod(25 * date.year + 11, 33) < 8;
80+
return isLeapYear ? 30 : 29;
8981
}
9082

9183
getEras() {

packages/@internationalized/date/tests/conversion.test.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,10 +348,25 @@ describe('CalendarDate conversion', function () {
348348
expect(toCalendar(date, new GregorianCalendar())).toEqual(new CalendarDate(2020, 9, 2));
349349
});
350350

351+
it('persian to gregorian for months greater than 6', function () {
352+
let date = new CalendarDate(new PersianCalendar(), 1403, 12, 1);
353+
expect(toCalendar(date, new GregorianCalendar())).toEqual(new CalendarDate(2025, 2, 19));
354+
});
355+
351356
it('gregorian to persian', function () {
352357
let date = new CalendarDate(2020, 9, 2);
353358
expect(toCalendar(date, new PersianCalendar())).toEqual(new CalendarDate(new PersianCalendar(), 1399, 6, 12));
354359
});
360+
361+
it('gregorian to persian for months lower than 6', function () {
362+
let date = new CalendarDate(2025, 3, 21);
363+
expect(toCalendar(date, new PersianCalendar())).toEqual(new CalendarDate(new PersianCalendar(), 1404, 1, 1));
364+
});
365+
366+
it('persian to gregorian in leap years', function () {
367+
let date = new CalendarDate(new PersianCalendar(), 1403, 12, 30);
368+
expect(toCalendar(date, new GregorianCalendar())).toEqual(new CalendarDate(2025, 3, 20));
369+
});
355370
});
356371

357372
describe('hebrew', function () {

packages/@internationalized/date/tests/queries.test.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
JapaneseCalendar,
3131
maxDate,
3232
minDate,
33+
PersianCalendar,
3334
startOfMonth,
3435
startOfWeek,
3536
startOfYear,
@@ -56,6 +57,13 @@ describe('queries', function () {
5657
expect(isSameDay(new CalendarDate(2021, 4, 17), new CalendarDate(new IslamicUmalquraCalendar(), 1442, 9, 4))).toBe(false);
5758
expect(isSameDay(new CalendarDate(2021, 4, 16), new CalendarDate(new IslamicUmalquraCalendar(), 1442, 9, 3))).toBe(false);
5859
});
60+
61+
it('works in Persian calendar', function () {
62+
const persian = new CalendarDate(new PersianCalendar(), 1401, 12, 8);
63+
const gregorian = new CalendarDate(2023, 2, 27);
64+
expect(isSameDay(gregorian, persian)).toBe(true);
65+
expect(isSameDay(persian, gregorian)).toBe(true);
66+
});
5967
});
6068

6169
describe('isSameMonth', function () {

0 commit comments

Comments
 (0)