diff --git a/packages/dev/s2-docs/pages/react-aria/Calendar.mdx b/packages/dev/s2-docs/pages/react-aria/Calendar.mdx index e0fd2497e3f..3941ebaa1d4 100644 --- a/packages/dev/s2-docs/pages/react-aria/Calendar.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Calendar.mdx @@ -83,8 +83,8 @@ import {Calendar} from 'vanilla-starter/Calendar'; ```tsx render "use client"; -import type {AnyCalendarDate} from '@internationalized/date'; -import {CalendarDate, startOfWeek, toCalendar, GregorianCalendar} from '@internationalized/date'; +import type {AnyCalendarDate, Calendar as ICalendar} from '@internationalized/date'; +import {CalendarDate, startOfWeek, GregorianCalendar} from '@internationalized/date'; import {Calendar} from 'vanilla-starter/Calendar'; export default function Example() { @@ -100,45 +100,66 @@ export default function Example() { // See @internationalized/date docs linked above. ///- begin collapse -/// class Custom454 extends GregorianCalendar { - weekPattern = [4, 5, 4, 4, 5, 4, 4, 5, 4, 4, 5, 4]; - getDaysInMonth(date) { - return this.weekPattern[date.month - 1] * 7; + // The anchor date, in Gregorian calendar. + // The anchor date is a date that occurs in the first week of the first month of every fiscal year. + anchorDate = new CalendarDate(2001, 2, 4); + + private getYear(year: number): [CalendarDate, number[]] { + let anchor = this.anchorDate.set({year}); + let startOfYear = startOfWeek(anchor, 'en', 'sun'); + let isBigYear = !startOfYear.add({weeks: 53}).compare(anchor.add({years: 1})); + let weekPattern = [4, 5, 4, 4, 5, 4, 4, 5, 4, 4, 5, isBigYear ? 5 : 4]; + return [startOfYear, weekPattern]; + } + + getDaysInMonth(date: AnyCalendarDate): number { + let [, weekPattern] = this.getYear(date.year); + return weekPattern[date.month - 1] * 7; } fromJulianDay(jd: number): CalendarDate { let gregorian = super.fromJulianDay(jd); + let year = gregorian.year; + + let [monthStart, weekPattern] = this.getYear(year); + if (gregorian.compare(monthStart) < 0) { + year--; + [monthStart, weekPattern] = this.getYear(year); + } - let monthStart = startOfWeek(new CalendarDate(gregorian.year, 1, 1), 'en'); - for (let months = 0; months < this.weekPattern.length; months++) { - let weeksInMonth = this.weekPattern[months]; - let monthEnd = monthStart.add({weeks: weeksInMonth}); - if (monthEnd.compare(gregorian) > 0) { + for (let month = 1; month <= 12; month++) { + let weeks = weekPattern[month - 1]; + let nextMonth = monthStart.add({weeks}); + if (nextMonth.compare(gregorian) > 0) { let days = gregorian.compare(monthStart); - return new CalendarDate(this, monthStart.year, months + 1, days + 1); + return new CalendarDate(this, year, month, days + 1); } - monthStart = monthEnd; + monthStart = nextMonth; } - throw Error('Date is not in any month somehow!'); + throw new Error('date not found'); } toJulianDay(date: AnyCalendarDate): number { - let monthStart = startOfWeek(new CalendarDate(date.year, 1, 1), 'en'); + let [monthStart, weekPattern] = this.getYear(date.year); for (let month = 1; month < date.month; month++) { - monthStart = monthStart.add({weeks: this.weekPattern[month - 1]}); + monthStart = monthStart.add({weeks: weekPattern[month - 1]}); } let gregorian = monthStart.add({days: date.day - 1}); return super.toJulianDay(gregorian); } - getFormattableMonth(date) { - let gregorian = toCalendar(date, new GregorianCalendar()); - return gregorian.set({month: date.month, day: 1}); + getFormattableMonth(date: AnyCalendarDate): CalendarDate { + let anchorMonth = this.anchorDate.month - 1; + let dateMonth = date.month - 1; + let month = ((anchorMonth + dateMonth) % 12) + 1; + let year = anchorMonth + dateMonth >= 12 ? date.year + 1 : date.year; + return new CalendarDate(year, month, 1); } - isEqual(other) { - return other instanceof Custom454; + isEqual(other: ICalendar): boolean { + return other instanceof Custom454 && other.anchorDate.compare(this.anchorDate) === 0; } } ///- end collapse -/// diff --git a/packages/dev/s2-docs/pages/react-aria/RangeCalendar.mdx b/packages/dev/s2-docs/pages/react-aria/RangeCalendar.mdx index bead2451c0d..a5cc8075980 100644 --- a/packages/dev/s2-docs/pages/react-aria/RangeCalendar.mdx +++ b/packages/dev/s2-docs/pages/react-aria/RangeCalendar.mdx @@ -93,8 +93,8 @@ import {RangeCalendar} from 'vanilla-starter/RangeCalendar'; ```tsx render "use client"; -import type {AnyCalendarDate} from '@internationalized/date'; -import {CalendarDate, startOfWeek, toCalendar, GregorianCalendar} from '@internationalized/date'; +import type {AnyCalendarDate, Calendar} from '@internationalized/date'; +import {CalendarDate, startOfWeek, GregorianCalendar} from '@internationalized/date'; import {RangeCalendar} from 'vanilla-starter/RangeCalendar'; export default function Example() { @@ -110,45 +110,66 @@ export default function Example() { // See @internationalized/date docs linked above. ///- begin collapse -/// class Custom454 extends GregorianCalendar { - weekPattern = [4, 5, 4, 4, 5, 4, 4, 5, 4, 4, 5, 4]; - getDaysInMonth(date) { - return this.weekPattern[date.month - 1] * 7; + // The anchor date, in Gregorian calendar. + // The anchor date is a date that occurs in the first week of the first month of every fiscal year. + anchorDate = new CalendarDate(2001, 2, 4); + + private getYear(year: number): [CalendarDate, number[]] { + let anchor = this.anchorDate.set({year}); + let startOfYear = startOfWeek(anchor, 'en', 'sun'); + let isBigYear = !startOfYear.add({weeks: 53}).compare(anchor.add({years: 1})); + let weekPattern = [4, 5, 4, 4, 5, 4, 4, 5, 4, 4, 5, isBigYear ? 5 : 4]; + return [startOfYear, weekPattern]; + } + + getDaysInMonth(date: AnyCalendarDate): number { + let [, weekPattern] = this.getYear(date.year); + return weekPattern[date.month - 1] * 7; } fromJulianDay(jd: number): CalendarDate { let gregorian = super.fromJulianDay(jd); + let year = gregorian.year; + + let [monthStart, weekPattern] = this.getYear(year); + if (gregorian.compare(monthStart) < 0) { + year--; + [monthStart, weekPattern] = this.getYear(year); + } - let monthStart = startOfWeek(new CalendarDate(gregorian.year, 1, 1), 'en'); - for (let months = 0; months < this.weekPattern.length; months++) { - let weeksInMonth = this.weekPattern[months]; - let monthEnd = monthStart.add({weeks: weeksInMonth}); - if (monthEnd.compare(gregorian) > 0) { + for (let month = 1; month <= 12; month++) { + let weeks = weekPattern[month - 1]; + let nextMonth = monthStart.add({weeks}); + if (nextMonth.compare(gregorian) > 0) { let days = gregorian.compare(monthStart); - return new CalendarDate(this, monthStart.year, months + 1, days + 1); + return new CalendarDate(this, year, month, days + 1); } - monthStart = monthEnd; + monthStart = nextMonth; } - throw Error('Date is not in any month somehow!'); + throw new Error('date not found'); } toJulianDay(date: AnyCalendarDate): number { - let monthStart = startOfWeek(new CalendarDate(date.year, 1, 1), 'en'); + let [monthStart, weekPattern] = this.getYear(date.year); for (let month = 1; month < date.month; month++) { - monthStart = monthStart.add({weeks: this.weekPattern[month - 1]}); + monthStart = monthStart.add({weeks: weekPattern[month - 1]}); } let gregorian = monthStart.add({days: date.day - 1}); return super.toJulianDay(gregorian); } - getFormattableMonth(date) { - let gregorian = toCalendar(date, new GregorianCalendar()); - return gregorian.set({month: date.month, day: 1}); + getFormattableMonth(date: AnyCalendarDate): CalendarDate { + let anchorMonth = this.anchorDate.month - 1; + let dateMonth = date.month - 1; + let month = ((anchorMonth + dateMonth) % 12) + 1; + let year = anchorMonth + dateMonth >= 12 ? date.year + 1 : date.year; + return new CalendarDate(year, month, 1); } - isEqual(other) { - return other instanceof Custom454; + isEqual(other: Calendar): boolean { + return other instanceof Custom454 && other.anchorDate.compare(this.anchorDate) === 0; } } ///- end collapse -/// diff --git a/packages/dev/s2-docs/pages/react-aria/internationalized/date/Calendar.mdx b/packages/dev/s2-docs/pages/react-aria/internationalized/date/Calendar.mdx index b057698d818..fae8a638eec 100644 --- a/packages/dev/s2-docs/pages/react-aria/internationalized/date/Calendar.mdx +++ b/packages/dev/s2-docs/pages/react-aria/internationalized/date/Calendar.mdx @@ -125,36 +125,55 @@ The following code is an example of how you might implement a custom 4-5-4 calen import type {AnyCalendarDate, Calendar} from '@internationalized/date'; import {CalendarDate, GregorianCalendar, startOfWeek} from '@internationalized/date'; -const weekPattern = [4, 5, 4, 4, 5, 4, 4, 5, 4, 4, 5, 4]; - +// This calendar gives each month a 4-5-4 week pattern, with February as the first month of the year. +// This means that in this calendar, 2024-01-01 translates to 2024-02-04 in the Gregorian calendar. +// Months begin on day 1, and go through 7*weeksInMonth days, ending on either the 28th or 35th day of the month. class Custom454 extends GregorianCalendar { - // Months always have either 4 or 5 full weeks. - getDaysInMonth(date) { + // The anchor date, in Gregorian calendar. + // The anchor date is a date that occurs in the first week of the first month of every fiscal year. + anchorDate = new CalendarDate(2001, 2, 4); + + private getYear(year: number): [CalendarDate, number[]] { + let anchor = this.anchorDate.set({year}); + let startOfYear = startOfWeek(anchor, 'en', 'sun'); + let isBigYear = !startOfYear.add({weeks: 53}).compare(anchor.add({years: 1})); + let weekPattern = [4, 5, 4, 4, 5, 4, 4, 5, 4, 4, 5, isBigYear ? 5 : 4]; + return [startOfYear, weekPattern]; + } + + getDaysInMonth(date: AnyCalendarDate): number { + // Months always have either 4 or 5 full weeks. + let [, weekPattern] = this.getYear(date.year); return weekPattern[date.month - 1] * 7; } - // Enable conversion between calendar systems. fromJulianDay(jd: number): CalendarDate { let gregorian = super.fromJulianDay(jd); + let year = gregorian.year; + + let [monthStart, weekPattern] = this.getYear(year); + if (gregorian.compare(monthStart) < 0) { + year--; + [monthStart, weekPattern] = this.getYear(year); + } // Start from the beginning of the first week of the gregorian year // and add weeks until we find the month. - let monthStart = startOfWeek(new CalendarDate(gregorian.year, 1, 1), 'en'); - for (let months = 0; months < weekPattern.length; months++) { - let weeksInMonth = weekPattern[months]; - let monthEnd = monthStart.add({weeks: weeksInMonth}); - if (monthEnd.compare(gregorian) > 0) { + for (let month = 1; month <= 12; month++) { + let weeks = weekPattern[month - 1]; + let nextMonth = monthStart.add({weeks}); + if (nextMonth.compare(gregorian) > 0) { let days = gregorian.compare(monthStart); - return new CalendarDate(this, monthStart.year, months + 1, days + 1); + return new CalendarDate(this, year, month, days + 1); } - monthStart = monthEnd; + monthStart = nextMonth; } - throw Error('Date is not in any month somehow!'); + throw new Error('date not found'); } toJulianDay(date: AnyCalendarDate): number { - let monthStart = startOfWeek(new CalendarDate(date.year, 1, 1), 'en'); + let [monthStart, weekPattern] = this.getYear(date.year); for (let month = 1; month < date.month; month++) { monthStart = monthStart.add({weeks: weekPattern[month - 1]}); } @@ -163,8 +182,16 @@ class Custom454 extends GregorianCalendar { return super.toJulianDay(gregorian); } - isEqual(other: Calendar) { - return other instanceof Custom454; + getFormattableMonth(date: AnyCalendarDate): CalendarDate { + let anchorMonth = this.anchorDate.month - 1; + let dateMonth = date.month - 1; + let month = ((anchorMonth + dateMonth) % 12) + 1; + let year = anchorMonth + dateMonth >= 12 ? date.year + 1 : date.year; + return new CalendarDate(year, month, 1); + } + + isEqual(other: Calendar): boolean { + return other instanceof Custom454 && other.anchorDate.compare(this.anchorDate) === 0; } } ``` diff --git a/packages/dev/s2-docs/pages/s2/Calendar.mdx b/packages/dev/s2-docs/pages/s2/Calendar.mdx index 00c8e6c0796..06ff974e152 100644 --- a/packages/dev/s2-docs/pages/s2/Calendar.mdx +++ b/packages/dev/s2-docs/pages/s2/Calendar.mdx @@ -74,8 +74,8 @@ import {parseDate} from '@internationalized/date'; ```tsx render "use client"; -import type {AnyCalendarDate} from '@internationalized/date'; -import {CalendarDate, startOfWeek, toCalendar, GregorianCalendar} from '@internationalized/date'; +import type {AnyCalendarDate, Calendar as ICalendar} from '@internationalized/date'; +import {CalendarDate, startOfWeek, GregorianCalendar} from '@internationalized/date'; import {Calendar} from '@react-spectrum/s2'; export default function Example() { @@ -91,45 +91,66 @@ export default function Example() { // See @internationalized/date docs linked above. ///- begin collapse -/// class Custom454 extends GregorianCalendar { - weekPattern = [4, 5, 4, 4, 5, 4, 4, 5, 4, 4, 5, 4]; - getDaysInMonth(date) { - return this.weekPattern[date.month - 1] * 7; + // The anchor date, in Gregorian calendar. + // The anchor date is a date that occurs in the first week of the first month of every fiscal year. + anchorDate = new CalendarDate(2001, 2, 4); + + private getYear(year: number): [CalendarDate, number[]] { + let anchor = this.anchorDate.set({year}); + let startOfYear = startOfWeek(anchor, 'en', 'sun'); + let isBigYear = !startOfYear.add({weeks: 53}).compare(anchor.add({years: 1})); + let weekPattern = [4, 5, 4, 4, 5, 4, 4, 5, 4, 4, 5, isBigYear ? 5 : 4]; + return [startOfYear, weekPattern]; + } + + getDaysInMonth(date: AnyCalendarDate): number { + let [, weekPattern] = this.getYear(date.year); + return weekPattern[date.month - 1] * 7; } fromJulianDay(jd: number): CalendarDate { let gregorian = super.fromJulianDay(jd); + let year = gregorian.year; + + let [monthStart, weekPattern] = this.getYear(year); + if (gregorian.compare(monthStart) < 0) { + year--; + [monthStart, weekPattern] = this.getYear(year); + } - let monthStart = startOfWeek(new CalendarDate(gregorian.year, 1, 1), 'en'); - for (let months = 0; months < this.weekPattern.length; months++) { - let weeksInMonth = this.weekPattern[months]; - let monthEnd = monthStart.add({weeks: weeksInMonth}); - if (monthEnd.compare(gregorian) > 0) { + for (let month = 1; month <= 12; month++) { + let weeks = weekPattern[month - 1]; + let nextMonth = monthStart.add({weeks}); + if (nextMonth.compare(gregorian) > 0) { let days = gregorian.compare(monthStart); - return new CalendarDate(this, monthStart.year, months + 1, days + 1); + return new CalendarDate(this, year, month, days + 1); } - monthStart = monthEnd; + monthStart = nextMonth; } - throw Error('Date is not in any month somehow!'); + throw new Error('date not found'); } toJulianDay(date: AnyCalendarDate): number { - let monthStart = startOfWeek(new CalendarDate(date.year, 1, 1), 'en'); + let [monthStart, weekPattern] = this.getYear(date.year); for (let month = 1; month < date.month; month++) { - monthStart = monthStart.add({weeks: this.weekPattern[month - 1]}); + monthStart = monthStart.add({weeks: weekPattern[month - 1]}); } let gregorian = monthStart.add({days: date.day - 1}); return super.toJulianDay(gregorian); } - getFormattableMonth(date) { - let gregorian = toCalendar(date, new GregorianCalendar()); - return gregorian.set({month: date.month, day: 1}); + getFormattableMonth(date: AnyCalendarDate): CalendarDate { + let anchorMonth = this.anchorDate.month - 1; + let dateMonth = date.month - 1; + let month = ((anchorMonth + dateMonth) % 12) + 1; + let year = anchorMonth + dateMonth >= 12 ? date.year + 1 : date.year; + return new CalendarDate(year, month, 1); } - isEqual(other) { - return other instanceof Custom454; + isEqual(other: ICalendar): boolean { + return other instanceof Custom454 && other.anchorDate.compare(this.anchorDate) === 0; } } ///- end collapse -/// diff --git a/packages/dev/s2-docs/pages/s2/RangeCalendar.mdx b/packages/dev/s2-docs/pages/s2/RangeCalendar.mdx index 8e10cecc38e..e3247d9fe6b 100644 --- a/packages/dev/s2-docs/pages/s2/RangeCalendar.mdx +++ b/packages/dev/s2-docs/pages/s2/RangeCalendar.mdx @@ -86,8 +86,8 @@ import {parseDate} from '@internationalized/date'; ```tsx render "use client"; -import type {AnyCalendarDate} from '@internationalized/date'; -import {CalendarDate, startOfWeek, toCalendar, GregorianCalendar} from '@internationalized/date'; +import type {AnyCalendarDate, Calendar} from '@internationalized/date'; +import {CalendarDate, startOfWeek, GregorianCalendar} from '@internationalized/date'; import {RangeCalendar} from '@react-spectrum/s2'; export default function Example() { @@ -103,45 +103,66 @@ export default function Example() { // See @internationalized/date docs linked above. ///- begin collapse -/// class Custom454 extends GregorianCalendar { - weekPattern = [4, 5, 4, 4, 5, 4, 4, 5, 4, 4, 5, 4]; - getDaysInMonth(date) { - return this.weekPattern[date.month - 1] * 7; + // The anchor date, in Gregorian calendar. + // The anchor date is a date that occurs in the first week of the first month of every fiscal year. + anchorDate = new CalendarDate(2001, 2, 4); + + private getYear(year: number): [CalendarDate, number[]] { + let anchor = this.anchorDate.set({year}); + let startOfYear = startOfWeek(anchor, 'en', 'sun'); + let isBigYear = !startOfYear.add({weeks: 53}).compare(anchor.add({years: 1})); + let weekPattern = [4, 5, 4, 4, 5, 4, 4, 5, 4, 4, 5, isBigYear ? 5 : 4]; + return [startOfYear, weekPattern]; + } + + getDaysInMonth(date: AnyCalendarDate): number { + let [, weekPattern] = this.getYear(date.year); + return weekPattern[date.month - 1] * 7; } fromJulianDay(jd: number): CalendarDate { let gregorian = super.fromJulianDay(jd); + let year = gregorian.year; + + let [monthStart, weekPattern] = this.getYear(year); + if (gregorian.compare(monthStart) < 0) { + year--; + [monthStart, weekPattern] = this.getYear(year); + } - let monthStart = startOfWeek(new CalendarDate(gregorian.year, 1, 1), 'en'); - for (let months = 0; months < this.weekPattern.length; months++) { - let weeksInMonth = this.weekPattern[months]; - let monthEnd = monthStart.add({weeks: weeksInMonth}); - if (monthEnd.compare(gregorian) > 0) { + for (let month = 1; month <= 12; month++) { + let weeks = weekPattern[month - 1]; + let nextMonth = monthStart.add({weeks}); + if (nextMonth.compare(gregorian) > 0) { let days = gregorian.compare(monthStart); - return new CalendarDate(this, monthStart.year, months + 1, days + 1); + return new CalendarDate(this, year, month, days + 1); } - monthStart = monthEnd; + monthStart = nextMonth; } - throw Error('Date is not in any month somehow!'); + throw new Error('date not found'); } toJulianDay(date: AnyCalendarDate): number { - let monthStart = startOfWeek(new CalendarDate(date.year, 1, 1), 'en'); + let [monthStart, weekPattern] = this.getYear(date.year); for (let month = 1; month < date.month; month++) { - monthStart = monthStart.add({weeks: this.weekPattern[month - 1]}); + monthStart = monthStart.add({weeks: weekPattern[month - 1]}); } let gregorian = monthStart.add({days: date.day - 1}); return super.toJulianDay(gregorian); } - getFormattableMonth(date) { - let gregorian = toCalendar(date, new GregorianCalendar()); - return gregorian.set({month: date.month, day: 1}); + getFormattableMonth(date: AnyCalendarDate): CalendarDate { + let anchorMonth = this.anchorDate.month - 1; + let dateMonth = date.month - 1; + let month = ((anchorMonth + dateMonth) % 12) + 1; + let year = anchorMonth + dateMonth >= 12 ? date.year + 1 : date.year; + return new CalendarDate(year, month, 1); } - isEqual(other) { - return other instanceof Custom454; + isEqual(other: Calendar): boolean { + return other instanceof Custom454 && other.anchorDate.compare(this.anchorDate) === 0; } } ///- end collapse -///