Skip to content

Commit 3f7c511

Browse files
feat: custom calendar support (#7803)
* Add createCalendar prop to Calendar/RangeCalendar * Update date math for adding/subtracting months * Add doc comments * Simplify isEqualDay/Month/Year * Display the calendar month name based on the result of getCurrentMonth() * Format aria labels based on current month * Refactor to return weeksInMonth from useCalendarGrid hook * Fix month's year display in calendar header for custom calendars * Update Calendar and RangeCalendar docs to talk about custom calendars * Add custom calendar stories to Calendar and RangeCalendar * Fix range selection styles for custom calendar * Write tests for custom calendar date math * Write tests for changes to queries.ts * Use customCalendarImpl in stories * Write tests for visible range description with custom calendar * Update calendar docs to use jsx syntax * Update calendar docs to fully implement Calendar interface to satisfy the type checker * Update calendar docs Finally learned how to run `make check-examples` myself * wip * fix * feat: use suggestion from @devongovett to implement custom cal w/ julian dates * chore: remove unnecessary code in queries & manipulation * feat: zero-index the week pattern in 454 calendar impl * chore: remove unused code in utils * test: update useCalendar tests for custom calendar * test: verify conversion for end of big year * feat: update createCalendar prop types * feat: add createCalendar prop to DatePicker and DateRangePicker * Update docs * update examples * ts * docs: update docs for DatePicker and DateRangePicker * docs: remove custom calendar system section in useCalendar hooks * chore: more code cleanup * fix: mdx example * Add isEqualCalendar function * Update docs --------- Co-authored-by: Devon Govett <[email protected]>
1 parent fdf48d2 commit 3f7c511

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1016
-111
lines changed

packages/@internationalized/date/docs/Calendar.mdx

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,3 +226,69 @@ function createCalendar(identifier) {
226226
## Interface
227227

228228
<ClassAPI links={docs.links} class={docs.exports.Calendar} />
229+
230+
## Custom calendars
231+
232+
You can create your own custom calendar system by implementing the `Calendar` interface shown above. This enables calendars that follow custom business rules. An example would be a fiscal year calendar that follows a [4-5-4 format](https://nrf.com/resources/4-5-4-calendar), where month ranges don't follow the usual Gregorian calendar.
233+
234+
To implement a calendar, either extend an existing implementation (e.g. `GregorianCalendar`) or implement the `Calendar` interface from scratch. The most important methods are `fromJulianDay` and `toJulianDay`, which convert between the calendar's year/month/day numbering system and a [Julian Day Number](https://en.wikipedia.org/wiki/Julian_day). This allows converting dates between calendar systems. Other methods such as `getDaysInMonth` and `getMonthsInYear` can be implemented to define how dates are organized in your calendar system.
235+
236+
The following code is an example of how you might implement a custom 4-5-4 calendar (though implementing a true 4-5-4 calendar would be more nuanced than this).
237+
238+
```tsx
239+
import type {AnyCalendarDate, Calendar} from '@internationalized/date';
240+
import {CalendarDate, GregorianCalendar, startOfWeek} from '@internationalized/date';
241+
242+
const weekPattern = [4, 5, 4, 4, 5, 4, 4, 5, 4, 4, 5, 4];
243+
244+
class Custom454 extends GregorianCalendar {
245+
// Months always have either 4 or 5 full weeks.
246+
getDaysInMonth(date) {
247+
return weekPattern[date.month - 1] * 7;
248+
}
249+
250+
// Enable conversion between calendar systems.
251+
fromJulianDay(jd: number): CalendarDate {
252+
let gregorian = super.fromJulianDay(jd);
253+
254+
// Start from the beginning of the first week of the gregorian year
255+
// and add weeks until we find the month.
256+
let monthStart = startOfWeek(new CalendarDate(gregorian.year, 1, 1), 'en');
257+
for (let months = 0; months < weekPattern.length; months++) {
258+
let weeksInMonth = weekPattern[months];
259+
let monthEnd = monthStart.add({weeks: weeksInMonth});
260+
if (monthEnd.compare(gregorian) > 0) {
261+
let days = gregorian.compare(monthStart);
262+
return new CalendarDate(this, monthStart.year, months + 1, days + 1);
263+
}
264+
monthStart = monthEnd;
265+
}
266+
267+
throw Error('Date is not in any month somehow!');
268+
}
269+
270+
toJulianDay(date: AnyCalendarDate): number {
271+
let monthStart = startOfWeek(new CalendarDate(date.year, 1, 1), 'en');
272+
for (let month = 1; month < date.month; month++) {
273+
monthStart = monthStart.add({weeks: weekPattern[month - 1]});
274+
}
275+
276+
let gregorian = monthStart.add({days: date.day - 1});
277+
return super.toJulianDay(gregorian);
278+
}
279+
280+
isEqual(other: Calendar) {
281+
return other instanceof Custom454;
282+
}
283+
}
284+
```
285+
286+
This enables dates to be converted between calendar systems.
287+
288+
```tsx
289+
import {GregorianCalendar, toCalendar} from '@internationalized/date';
290+
291+
let date = new CalendarDate(new Custom454(), 2024, 2, 1);
292+
let gregorianDate = toCalendar(date, new GregorianCalendar());
293+
// => new CalendarDate(new GregorianCalendar(), 2024, 1, 29);
294+
```

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
// Portions of the code in this file are based on code from ICU.
1414
// Original licensing can be found in the NOTICE file in the root directory of this source tree.
1515

16-
import {AnyCalendarDate} from '../types';
16+
import {AnyCalendarDate, CalendarIdentifier} from '../types';
1717
import {CalendarDate} from '../CalendarDate';
1818
import {fromExtendedYear, getExtendedYear, GregorianCalendar} from './GregorianCalendar';
1919

@@ -25,7 +25,7 @@ const BUDDHIST_ERA_START = -543;
2525
* era, identified as 'BE'.
2626
*/
2727
export class BuddhistCalendar extends GregorianCalendar {
28-
identifier = 'buddhist';
28+
identifier: CalendarIdentifier = 'buddhist';
2929

3030
fromJulianDay(jd: number): CalendarDate {
3131
let gregorianDate = super.fromJulianDay(jd);

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
// Portions of the code in this file are based on code from ICU.
1414
// Original licensing can be found in the NOTICE file in the root directory of this source tree.
1515

16-
import {AnyCalendarDate, Calendar} from '../types';
16+
import {AnyCalendarDate, Calendar, CalendarIdentifier} from '../types';
1717
import {CalendarDate} from '../CalendarDate';
1818
import {Mutable} from '../utils';
1919

@@ -66,7 +66,7 @@ function getDaysInMonth(year: number, month: number) {
6666
* on whether it is a leap year. Two eras are supported: 'AA' and 'AM'.
6767
*/
6868
export class EthiopicCalendar implements Calendar {
69-
identifier = 'ethiopic';
69+
identifier: CalendarIdentifier = 'ethiopic';
7070

7171
fromJulianDay(jd: number): CalendarDate {
7272
let [year, month, day] = julianDayToCE(ETHIOPIC_EPOCH, jd);
@@ -117,7 +117,7 @@ export class EthiopicCalendar implements Calendar {
117117
* except years were measured from a different epoch. Only one era is supported: 'AA'.
118118
*/
119119
export class EthiopicAmeteAlemCalendar extends EthiopicCalendar {
120-
identifier = 'ethioaa'; // also known as 'ethiopic-amete-alem' in ICU
120+
identifier: CalendarIdentifier = 'ethioaa'; // also known as 'ethiopic-amete-alem' in ICU
121121

122122
fromJulianDay(jd: number): CalendarDate {
123123
let [year, month, day] = julianDayToCE(ETHIOPIC_EPOCH, jd);
@@ -141,7 +141,7 @@ export class EthiopicAmeteAlemCalendar extends EthiopicCalendar {
141141
* on whether it is a leap year. Two eras are supported: 'BCE' and 'CE'.
142142
*/
143143
export class CopticCalendar extends EthiopicCalendar {
144-
identifier = 'coptic';
144+
identifier: CalendarIdentifier = 'coptic';
145145

146146
fromJulianDay(jd: number): CalendarDate {
147147
let [year, month, day] = julianDayToCE(COPTIC_EPOCH, jd);

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
// Portions of the code in this file are based on code from ICU.
1414
// Original licensing can be found in the NOTICE file in the root directory of this source tree.
1515

16-
import {AnyCalendarDate, Calendar} from '../types';
16+
import {AnyCalendarDate, Calendar, CalendarIdentifier} from '../types';
1717
import {CalendarDate} from '../CalendarDate';
1818
import {mod, Mutable} from '../utils';
1919

@@ -68,7 +68,7 @@ const daysInMonth = {
6868
* Years always contain 12 months, and 365 or 366 days depending on whether it is a leap year.
6969
*/
7070
export class GregorianCalendar implements Calendar {
71-
identifier = 'gregory';
71+
identifier: CalendarIdentifier = 'gregory';
7272

7373
fromJulianDay(jd: number): CalendarDate {
7474
let jd0 = jd;

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
// Portions of the code in this file are based on code from ICU.
1414
// Original licensing can be found in the NOTICE file in the root directory of this source tree.
1515

16-
import {AnyCalendarDate, Calendar} from '../types';
16+
import {AnyCalendarDate, Calendar, CalendarIdentifier} from '../types';
1717
import {CalendarDate} from '../CalendarDate';
1818
import {mod, Mutable} from '../utils';
1919

@@ -128,7 +128,7 @@ function getDaysInMonth(year: number, month: number): number {
128128
* In leap years, an extra month is inserted at month 6.
129129
*/
130130
export class HebrewCalendar implements Calendar {
131-
identifier = 'hebrew';
131+
identifier: CalendarIdentifier = 'hebrew';
132132

133133
fromJulianDay(jd: number): CalendarDate {
134134
let d = jd - HEBREW_EPOCH;

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
// Portions of the code in this file are based on code from ICU.
1414
// Original licensing can be found in the NOTICE file in the root directory of this source tree.
1515

16-
import {AnyCalendarDate} from '../types';
16+
import {AnyCalendarDate, CalendarIdentifier} from '../types';
1717
import {CalendarDate} from '../CalendarDate';
1818
import {fromExtendedYear, GregorianCalendar, gregorianToJulianDay, isLeapYear} from './GregorianCalendar';
1919

@@ -29,7 +29,7 @@ const INDIAN_YEAR_START = 80;
2929
* in each year, with either 30 or 31 days. Only one era identifier is supported: 'saka'.
3030
*/
3131
export class IndianCalendar extends GregorianCalendar {
32-
identifier = 'indian';
32+
identifier: CalendarIdentifier = 'indian';
3333

3434
fromJulianDay(jd: number): CalendarDate {
3535
// Gregorian date for Julian day

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
// Portions of the code in this file are based on code from ICU.
1414
// Original licensing can be found in the NOTICE file in the root directory of this source tree.
1515

16-
import {AnyCalendarDate, Calendar} from '../types';
16+
import {AnyCalendarDate, Calendar, CalendarIdentifier} from '../types';
1717
import {CalendarDate} from '../CalendarDate';
1818

1919
const CIVIL_EPOC = 1948440; // CE 622 July 16 Friday (Julian calendar) / CE 622 July 19 (Gregorian calendar)
@@ -50,7 +50,7 @@ function isLeapYear(year: number): boolean {
5050
* Learn more about the available Islamic calendars [here](https://cldr.unicode.org/development/development-process/design-proposals/islamic-calendar-types).
5151
*/
5252
export class IslamicCivilCalendar implements Calendar {
53-
identifier = 'islamic-civil';
53+
identifier: CalendarIdentifier = 'islamic-civil';
5454

5555
fromJulianDay(jd: number): CalendarDate {
5656
return julianDayToIslamic(this, CIVIL_EPOC, jd);
@@ -95,7 +95,7 @@ export class IslamicCivilCalendar implements Calendar {
9595
* Learn more about the available Islamic calendars [here](https://cldr.unicode.org/development/development-process/design-proposals/islamic-calendar-types).
9696
*/
9797
export class IslamicTabularCalendar extends IslamicCivilCalendar {
98-
identifier = 'islamic-tbla';
98+
identifier: CalendarIdentifier = 'islamic-tbla';
9999

100100
fromJulianDay(jd: number): CalendarDate {
101101
return julianDayToIslamic(this, ASTRONOMICAL_EPOC, jd);
@@ -145,7 +145,7 @@ function umalquraYearLength(year: number): number {
145145
* Learn more about the available Islamic calendars [here](https://cldr.unicode.org/development/development-process/design-proposals/islamic-calendar-types).
146146
*/
147147
export class IslamicUmalquraCalendar extends IslamicCivilCalendar {
148-
identifier = 'islamic-umalqura';
148+
identifier: CalendarIdentifier = 'islamic-umalqura';
149149

150150
constructor() {
151151
super();

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
// Portions of the code in this file are based on code from the TC39 Temporal proposal.
1414
// Original licensing can be found in the NOTICE file in the root directory of this source tree.
1515

16-
import {AnyCalendarDate} from '../types';
16+
import {AnyCalendarDate, CalendarIdentifier} from '../types';
1717
import {CalendarDate} from '../CalendarDate';
1818
import {GregorianCalendar} from './GregorianCalendar';
1919
import {Mutable} from '../utils';
@@ -70,7 +70,7 @@ function toGregorian(date: AnyCalendarDate) {
7070
* Note that eras before 1868 (Gregorian) are not currently supported by this implementation.
7171
*/
7272
export class JapaneseCalendar extends GregorianCalendar {
73-
identifier = 'japanese';
73+
identifier: CalendarIdentifier = 'japanese';
7474

7575
fromJulianDay(jd: number): CalendarDate {
7676
let date = super.fromJulianDay(jd);

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
// Portions of the code in this file are based on code from ICU.
1414
// Original licensing can be found in the NOTICE file in the root directory of this source tree.
1515

16-
import {AnyCalendarDate, Calendar} from '../types';
16+
import {AnyCalendarDate, Calendar, CalendarIdentifier} from '../types';
1717
import {CalendarDate} from '../CalendarDate';
1818
import {mod} from '../utils';
1919

@@ -42,7 +42,7 @@ const MONTH_START = [
4242
* around the March equinox.
4343
*/
4444
export class PersianCalendar implements Calendar {
45-
identifier = 'persian';
45+
identifier: CalendarIdentifier = 'persian';
4646

4747
fromJulianDay(jd: number): CalendarDate {
4848
let daysSinceEpoch = jd - PERSIAN_EPOCH;

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
// Portions of the code in this file are based on code from ICU.
1414
// Original licensing can be found in the NOTICE file in the root directory of this source tree.
1515

16-
import {AnyCalendarDate} from '../types';
16+
import {AnyCalendarDate, CalendarIdentifier} from '../types';
1717
import {CalendarDate} from '../CalendarDate';
1818
import {fromExtendedYear, getExtendedYear, GregorianCalendar} from './GregorianCalendar';
1919
import {Mutable} from '../utils';
@@ -41,7 +41,7 @@ function gregorianToTaiwan(year: number): [string, number] {
4141
* 'before_minguo' and 'minguo'.
4242
*/
4343
export class TaiwanCalendar extends GregorianCalendar {
44-
identifier = 'roc'; // Republic of China
44+
identifier: CalendarIdentifier = 'roc'; // Republic of China
4545

4646
fromJulianDay(jd: number): CalendarDate {
4747
let date = super.fromJulianDay(jd);

0 commit comments

Comments
 (0)