diff --git a/packages/@internationalized/date/src/CalendarDate.ts b/packages/@internationalized/date/src/CalendarDate.ts index 10b7ebe4e90..e036c08d86d 100644 --- a/packages/@internationalized/date/src/CalendarDate.ts +++ b/packages/@internationalized/date/src/CalendarDate.ts @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {add, addTime, addZoned, constrain, constrainTime, cycleDate, cycleTime, cycleZoned, set, setTime, setZoned, subtract, subtractTime, subtractZoned} from './manipulation'; +import {add, addTime, addZoned, constrain, constrainTime, cycleDate, cycleTime, cycleZoned, normalize, set, setTime, setZoned, subtract, subtractTime, subtractZoned} from './manipulation'; import {AnyCalendarDate, AnyTime, Calendar, CycleOptions, CycleTimeOptions, DateDuration, DateField, DateFields, DateTimeDuration, Disambiguation, TimeDuration, TimeField, TimeFields} from './types'; import {compareDate, compareTime} from './queries'; import {dateTimeToString, dateToString, timeToString, zonedDateTimeToString} from './string'; @@ -65,12 +65,22 @@ export class CalendarDate { constructor(calendar: Calendar, era: string, year: number, month: number, day: number); constructor(...args: any[]) { let [calendar, era, year, month, day] = shiftArgs(args); - this.calendar = calendar; - this.era = era; - this.year = year; - this.month = month; - this.day = day; - + const normalized = normalize({ + calendar, + era, + year, + month, + day, + hour: 0, + minute: 0, + second: 0, + millisecond: 0 + }); + this.calendar = normalized.calendar; + this.era = normalized.era; + this.year = normalized.year; + this.month = normalized.month; + this.day = normalized.day; constrain(this); } @@ -222,16 +232,32 @@ export class CalendarDateTime { constructor(calendar: Calendar, era: string, year: number, month: number, day: number, hour?: number, minute?: number, second?: number, millisecond?: number); constructor(...args: any[]) { let [calendar, era, year, month, day] = shiftArgs(args); - this.calendar = calendar; - this.era = era; - this.year = year; - this.month = month; - this.day = day; - this.hour = args.shift() || 0; - this.minute = args.shift() || 0; - this.second = args.shift() || 0; - this.millisecond = args.shift() || 0; - + let hour = args.shift() || 0; + let minute = args.shift() || 0; + let second = args.shift() || 0; + let millisecond = args.shift() || 0; + + const normalized = normalize({ + calendar, + era, + year, + month, + day, + hour, + minute, + second, + millisecond + }); + + this.calendar = normalized.calendar; + this.era = normalized.era; + this.year = normalized.year; + this.month = normalized.month; + this.day = normalized.day; + this.hour = normalized.hour; + this.minute = normalized.minute; + this.second = normalized.second; + this.millisecond = normalized.millisecond; constrain(this); } @@ -336,18 +362,34 @@ export class ZonedDateTime { let [calendar, era, year, month, day] = shiftArgs(args); let timeZone = args.shift(); let offset = args.shift(); - this.calendar = calendar; - this.era = era; - this.year = year; - this.month = month; - this.day = day; + let hour = args.shift() || 0; + let minute = args.shift() || 0; + let second = args.shift() || 0; + let millisecond = args.shift() || 0; + + const normalized = normalize({ + calendar, + era, + year, + month, + day, + hour, + minute, + second, + millisecond + }); + + this.calendar = normalized.calendar; + this.era = normalized.era; + this.year = normalized.year; + this.month = normalized.month; + this.day = normalized.day; this.timeZone = timeZone; this.offset = offset; - this.hour = args.shift() || 0; - this.minute = args.shift() || 0; - this.second = args.shift() || 0; - this.millisecond = args.shift() || 0; - + this.hour = normalized.hour; + this.minute = normalized.minute; + this.second = normalized.second; + this.millisecond = normalized.millisecond; constrain(this); } diff --git a/packages/@internationalized/date/src/calendars/BuddhistCalendar.ts b/packages/@internationalized/date/src/calendars/BuddhistCalendar.ts index b48bda29cc7..577f84f2ff2 100644 --- a/packages/@internationalized/date/src/calendars/BuddhistCalendar.ts +++ b/packages/@internationalized/date/src/calendars/BuddhistCalendar.ts @@ -27,15 +27,17 @@ const BUDDHIST_ERA_START = -543; export class BuddhistCalendar extends GregorianCalendar { identifier = 'buddhist'; - fromJulianDay(jd: number): CalendarDate { + fromJulianDay(jd: number): AnyCalendarDate { let gregorianDate = super.fromJulianDay(jd); let year = getExtendedYear(gregorianDate.era, gregorianDate.year); - return new CalendarDate( - this, - year - BUDDHIST_ERA_START, - gregorianDate.month, - gregorianDate.day - ); + + return { + calendar: this, + era: 'BE', // Buddhist Era + year: year - BUDDHIST_ERA_START, + month: gregorianDate.month, + day: gregorianDate.day + }; } toJulianDay(date: AnyCalendarDate) { diff --git a/packages/@internationalized/date/src/calendars/GregorianCalendar.ts b/packages/@internationalized/date/src/calendars/GregorianCalendar.ts index 4bc5442e608..efa95e2dd49 100644 --- a/packages/@internationalized/date/src/calendars/GregorianCalendar.ts +++ b/packages/@internationalized/date/src/calendars/GregorianCalendar.ts @@ -14,7 +14,6 @@ // Original licensing can be found in the NOTICE file in the root directory of this source tree. import {AnyCalendarDate, Calendar} from '../types'; -import {CalendarDate} from '../CalendarDate'; import {mod, Mutable} from '../utils'; const EPOCH = 1721426; // 001/01/03 Julian C.E. @@ -70,7 +69,7 @@ const daysInMonth = { export class GregorianCalendar implements Calendar { identifier = 'gregory'; - fromJulianDay(jd: number): CalendarDate { + fromJulianDay(jd: number): AnyCalendarDate { let jd0 = jd; let depoch = jd0 - EPOCH; let quadricent = Math.floor(depoch / 146097); @@ -93,7 +92,13 @@ export class GregorianCalendar implements Calendar { let month = Math.floor(((yearDay + leapAdj) * 12 + 373) / 367); let day = jd0 - gregorianToJulianDay(era, year, month, 1) + 1; - return new CalendarDate(era, year, month, day); + return { + calendar: this, + era, + year, + month, + day + }; } toJulianDay(date: AnyCalendarDate): number { diff --git a/packages/@internationalized/date/src/calendars/IndianCalendar.ts b/packages/@internationalized/date/src/calendars/IndianCalendar.ts index a598ca224d0..d64c4028037 100644 --- a/packages/@internationalized/date/src/calendars/IndianCalendar.ts +++ b/packages/@internationalized/date/src/calendars/IndianCalendar.ts @@ -14,7 +14,6 @@ // Original licensing can be found in the NOTICE file in the root directory of this source tree. import {AnyCalendarDate} from '../types'; -import {CalendarDate} from '../CalendarDate'; import {fromExtendedYear, GregorianCalendar, gregorianToJulianDay, isLeapYear} from './GregorianCalendar'; // Starts in 78 AD, @@ -31,7 +30,7 @@ const INDIAN_YEAR_START = 80; export class IndianCalendar extends GregorianCalendar { identifier = 'indian'; - fromJulianDay(jd: number): CalendarDate { + fromJulianDay(jd: number): AnyCalendarDate { // Gregorian date for Julian day let date = super.fromJulianDay(jd); @@ -71,8 +70,15 @@ export class IndianCalendar extends GregorianCalendar { indianDay = (mDay % 30) + 1; } } - - return new CalendarDate(this, indianYear, indianMonth, indianDay); + const eras = this.getEras(); + const era = eras[eras.length - 1]; + return { + calendar: this, + era: era, + year: indianYear, + month: indianMonth, + day: indianDay + }; } toJulianDay(date: AnyCalendarDate) { diff --git a/packages/@internationalized/date/src/calendars/JapaneseCalendar.ts b/packages/@internationalized/date/src/calendars/JapaneseCalendar.ts index da0d6abedad..0da11897ac8 100644 --- a/packages/@internationalized/date/src/calendars/JapaneseCalendar.ts +++ b/packages/@internationalized/date/src/calendars/JapaneseCalendar.ts @@ -57,11 +57,13 @@ function toGregorian(date: AnyCalendarDate) { throw new Error('Unknown era: ' + date.era); } - return new CalendarDate( - date.year + eraAddend, - date.month, - date.day - ); + return { + era: 'AD', + year: date.year + eraAddend, + month: date.month, + day: date.day, + calendar: new GregorianCalendar() + }; } /** diff --git a/packages/@internationalized/date/src/calendars/PersianCalendar.ts b/packages/@internationalized/date/src/calendars/PersianCalendar.ts index c8eb15665ab..19348afa920 100644 --- a/packages/@internationalized/date/src/calendars/PersianCalendar.ts +++ b/packages/@internationalized/date/src/calendars/PersianCalendar.ts @@ -14,7 +14,6 @@ // Original licensing can be found in the NOTICE file in the root directory of this source tree. import {AnyCalendarDate, Calendar} from '../types'; -import {CalendarDate} from '../CalendarDate'; import {mod} from '../utils'; const PERSIAN_EPOCH = 1948320; @@ -44,7 +43,7 @@ const MONTH_START = [ export class PersianCalendar implements Calendar { identifier = 'persian'; - fromJulianDay(jd: number): CalendarDate { + fromJulianDay(jd: number): AnyCalendarDate { let daysSinceEpoch = jd - PERSIAN_EPOCH; let year = 1 + Math.floor((33 * daysSinceEpoch + 3) / 12053); let farvardin1 = 365 * (year - 1) + Math.floor((8 * year + 21) / 33); @@ -53,7 +52,14 @@ export class PersianCalendar implements Calendar { ? Math.floor(dayOfYear / 31) : Math.floor((dayOfYear - 6) / 30); let day = dayOfYear - MONTH_START[month] + 1; - return new CalendarDate(this, year, month + 1, day); + + return { + calendar: this, + era: 'AP', // Anno Persico/Persian Era + year, + month: month + 1, + day + }; } toJulianDay(date: AnyCalendarDate): number { diff --git a/packages/@internationalized/date/src/calendars/TaiwanCalendar.ts b/packages/@internationalized/date/src/calendars/TaiwanCalendar.ts index 1858cd44a62..f17eba616a6 100644 --- a/packages/@internationalized/date/src/calendars/TaiwanCalendar.ts +++ b/packages/@internationalized/date/src/calendars/TaiwanCalendar.ts @@ -79,10 +79,11 @@ export class TaiwanCalendar extends GregorianCalendar { function toGregorian(date: AnyCalendarDate) { let [era, year] = fromExtendedYear(gregorianYear(date)); - return new CalendarDate( - era, - year, - date.month, - date.day - ); + return { + era: era, + year: year, + month: date.month, + day: date.day, + calendar: new GregorianCalendar() + }; } diff --git a/packages/@internationalized/date/src/conversion.ts b/packages/@internationalized/date/src/conversion.ts index 508d0e2b336..dde0f89ee78 100644 --- a/packages/@internationalized/date/src/conversion.ts +++ b/packages/@internationalized/date/src/conversion.ts @@ -13,14 +13,14 @@ // Portions of the code in this file are based on code from the TC39 Temporal proposal. // Original licensing can be found in the NOTICE file in the root directory of this source tree. -import {AnyCalendarDate, AnyDateTime, AnyTime, Calendar, DateFields, Disambiguation, TimeFields} from './types'; +import {AnyCalendarDate, AnyDateTime, AnyTime, Calendar, Copyable, DateFields, Disambiguation, TimeFields} from './types'; import {CalendarDate, CalendarDateTime, Time, ZonedDateTime} from './CalendarDate'; import {constrain} from './manipulation'; import {getExtendedYear, GregorianCalendar} from './calendars/GregorianCalendar'; import {getLocalTimeZone} from './queries'; import {Mutable} from './utils'; -export function epochFromDate(date: AnyDateTime) { +export function epochFromDate(date: AnyDateTime & Copyable) { date = toCalendar(date, new GregorianCalendar()); let year = getExtendedYear(date.era, date.year); return epochFromParts(year, date.month, date.day, date.hour, date.minute, date.second, date.millisecond); @@ -259,7 +259,7 @@ export function toTime(dateTime: CalendarDateTime | ZonedDateTime): Time { } /** Converts a date from one calendar system to another. */ -export function toCalendar(date: T, calendar: Calendar): T { +export function toCalendar(date: T, calendar: Calendar): T { if (date.calendar.identifier === calendar.identifier) { return date; } diff --git a/packages/@internationalized/date/src/manipulation.ts b/packages/@internationalized/date/src/manipulation.ts index d6f66ca5709..bfc1b73aaa2 100644 --- a/packages/@internationalized/date/src/manipulation.ts +++ b/packages/@internationalized/date/src/manipulation.ts @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {AnyCalendarDate, AnyDateTime, AnyTime, CycleOptions, CycleTimeOptions, DateDuration, DateField, DateFields, DateTimeDuration, Disambiguation, TimeDuration, TimeField, TimeFields} from './types'; +import {AnyCalendarDate, AnyDateTime, AnyTime, Calendar, CycleOptions, CycleTimeOptions, DateDuration, DateField, DateFields, DateTimeDuration, Disambiguation, TimeDuration, TimeField, TimeFields} from './types'; import {CalendarDate, CalendarDateTime, Time, ZonedDateTime} from './CalendarDate'; import {epochFromDate, fromAbsolute, toAbsolute, toCalendar, toCalendarDateTime} from './conversion'; import {GregorianCalendar} from './calendars/GregorianCalendar'; @@ -18,6 +18,10 @@ import {Mutable} from './utils'; const ONE_HOUR = 3600000; +export const copy = (obj: T): T => { + return {...obj}; +}; + export function add(date: CalendarDateTime, duration: DateTimeDuration): CalendarDateTime; export function add(date: CalendarDate, duration: DateDuration): CalendarDate; export function add(date: CalendarDate | CalendarDateTime, duration: DateTimeDuration): CalendarDate | CalendarDateTime; @@ -105,7 +109,6 @@ function balanceDay(date: Mutable) { balanceYearMonth(date); date.day += date.calendar.getDaysInMonth(date); } - while (date.day > date.calendar.getDaysInMonth(date)) { date.day -= date.calendar.getDaysInMonth(date); date.month++; @@ -127,6 +130,161 @@ export function constrain(date: Mutable) { constrainMonthDay(date); } +export function normalize(fields: { + calendar: Calendar, + era: string, + year: number, + month: number, + day: number, + hour: number, + minute: number, + second: number, + millisecond: number +}) { + // Create a new object with the normalized values + const normalizedFields = { + calendar: fields.calendar, + era: fields.era, + year: fields.year, + month: fields.month, + day: fields.day, + hour: fields.hour, + minute: fields.minute, + second: fields.second, + millisecond: fields.millisecond + }; + + // Normalize time components + const {normalizedTime, extraDays} = normalizeTime({ + hour: normalizedFields.hour, + minute: normalizedFields.minute, + second: normalizedFields.second, + millisecond: normalizedFields.millisecond + }); + + // Apply normalized time values + Object.assign(normalizedFields, normalizedTime); + + // Handle extra days from time normalization + if (extraDays !== 0) { + normalizedFields.day += extraDays; + } + + // Normalize date components + const normalizedDate = normalizeDate({ + calendar: normalizedFields.calendar, + era: normalizedFields.era, + year: normalizedFields.year, + month: normalizedFields.month, + day: normalizedFields.day + }); + + return {...normalizedFields, ...normalizedDate}; +} + +function normalizeTime(time: { + hour: number, + minute: number, + second: number, + millisecond: number +}): { normalizedTime: Partial, extraDays: number } { + let {hour, minute, second, millisecond} = time; + let days = 0; + + // Normalize milliseconds + const normalizedMillisecond = nonNegativeMod(millisecond, 1000); + second += Math.floor(millisecond / 1000); + + // Normalize seconds + const normalizedSecond = nonNegativeMod(second, 60); + minute += Math.floor(second / 60); + + // Normalize minutes + const normalizedMinute = nonNegativeMod(minute, 60); + hour += Math.floor(minute / 60); + + // Normalize hours + const normalizedHour = nonNegativeMod(hour, 24); + days = Math.floor(hour / 24); + + return { + normalizedTime: { + hour: normalizedHour, + minute: normalizedMinute, + second: normalizedSecond, + millisecond: normalizedMillisecond + }, + extraDays: days + }; +} + +function normalizeDate(date: { + calendar: Calendar, + era: string, + year: number, + month: number, + day: number +}): Partial { + let {calendar, era, year, month, day} = date; + + // Validate era + const eras = calendar.getEras(); + if (!eras.includes(era)) { + era = eras[0]; + } + + // Basic normalization + while (month < 1) { + year--; + month += calendar.getMonthsInYear({...date, era, year}); + } + + let monthsInYear; + while (month > (monthsInYear = calendar.getMonthsInYear({...date, era, year}))) { + month -= monthsInYear; + year++; + } + + while (day < 1) { + month--; + if (month < 1) { + year--; + month = calendar.getMonthsInYear({...date, era, year}); + } + day += calendar.getDaysInMonth({...date, era, year, month}); + } + + while (day > calendar.getDaysInMonth({...date, era, year, month})) { + day -= calendar.getDaysInMonth({...date, era, year, month}); + month++; + if (month > calendar.getMonthsInYear({...date, era, year})) { + month = 1; + year++; + } + } + + // Let the calendar handle specific era transitions and constraints + if (calendar.balanceDate) { + let mutableDate = {calendar, era, year, month, day}; + calendar.balanceDate(mutableDate); + ({era, year, month, day} = mutableDate); + } + + // Final constraints + let maxYear = calendar.getYearsInEra({...date, era}); + if (year < 1) { + year = 1; + } + if (year > maxYear) { + let isInverseEra = calendar.isInverseEra?.({...date, era}); + year = maxYear; + month = isInverseEra ? 1 : calendar.getMonthsInYear({...date, era, year}); + day = isInverseEra ? 1 : calendar.getDaysInMonth({...date, era, year, month}); + } + + return {era, year, month, day}; +} + export function invertDuration(duration: DateTimeDuration): DateTimeDuration { let inverseDuration = {}; for (let key in duration) { diff --git a/packages/@internationalized/date/src/types.ts b/packages/@internationalized/date/src/types.ts index 6c4627bdc05..5bca74120ef 100644 --- a/packages/@internationalized/date/src/types.ts +++ b/packages/@internationalized/date/src/types.ts @@ -10,7 +10,6 @@ * governing permissions and limitations under the License. */ -import {CalendarDate} from './CalendarDate'; /** An interface that is compatible with any object with date fields. */ export interface AnyCalendarDate { @@ -18,8 +17,7 @@ export interface AnyCalendarDate { readonly era: string, readonly year: number, readonly month: number, - readonly day: number, - copy(): this + readonly day: number } /** An interface that is compatible with any object with time fields. */ @@ -27,7 +25,10 @@ export interface AnyTime { readonly hour: number, readonly minute: number, readonly second: number, - readonly millisecond: number, + readonly millisecond: number +} + +export interface Copyable { copy(): this } @@ -44,7 +45,7 @@ export interface Calendar { identifier: string, /** Creates a CalendarDate in this calendar from the given Julian day number. */ - fromJulianDay(jd: number): CalendarDate, + fromJulianDay(jd: number): AnyCalendarDate, /** Converts a date in this calendar to a Julian day number. */ toJulianDay(date: AnyCalendarDate): number, diff --git a/packages/@internationalized/date/tests/manipulation.test.js b/packages/@internationalized/date/tests/manipulation.test.js index 96963f2495f..26748f3c846 100644 --- a/packages/@internationalized/date/tests/manipulation.test.js +++ b/packages/@internationalized/date/tests/manipulation.test.js @@ -228,6 +228,7 @@ describe('CalendarDate manipulation', function () { it('should constrain when reaching year 9377', function () { let date = new CalendarDate(new PersianCalendar(), 9377, 12, 10); + console.log(date); expect(date.add({months: 1})).toEqual(new CalendarDate(new PersianCalendar(), 9377, 12, 31)); }); }); diff --git a/packages/@internationalized/date/tests/normalize.test.js b/packages/@internationalized/date/tests/normalize.test.js new file mode 100644 index 00000000000..a7fc306483f --- /dev/null +++ b/packages/@internationalized/date/tests/normalize.test.js @@ -0,0 +1,132 @@ +import {CalendarDate, CalendarDateTime, IslamicUmalquraCalendar, ZonedDateTime} from '..'; + +describe('normalize', function () { + describe('CalendarDateTime', function () { + it('should handle hour overflow', function () { + let date = new CalendarDateTime(2024, 1, 1, 26, 0, 0, 0); + expect(date).toEqual(new CalendarDateTime(2024, 1, 2, 2, 0, 0, 0)); + }); + + it('should handle minute overflow', function () { + let date = new CalendarDateTime(2024, 1, 1, 1, 70, 0, 0); + expect(date).toEqual(new CalendarDateTime(2024, 1, 1, 2, 10, 0, 0)); + }); + + it('should handle second overflow', function () { + let date = new CalendarDateTime(2024, 1, 1, 1, 1, 90, 0); + expect(date).toEqual(new CalendarDateTime(2024, 1, 1, 1, 2, 30, 0)); + }); + + it('should handle millisecond overflow', function () { + let date = new CalendarDateTime(2024, 1, 1, 1, 1, 1, 1500); + expect(date).toEqual(new CalendarDateTime(2024, 1, 1, 1, 1, 2, 500)); + }); + + it('should handle hour underflow', function () { + let date = new CalendarDateTime(2024, 1, 2, -2, 0, 0, 0); + expect(date).toEqual(new CalendarDateTime(2024, 1, 1, 22, 0, 0, 0)); + }); + + it('should handle complex time overflow', function () { + let date = new CalendarDateTime(2024, 1, 1, 25, 61, 61, 1001); + expect(date).toEqual(new CalendarDateTime(2024, 1, 2, 2, 2, 2, 1)); + }); + + it('should handle day overflow', function () { + let date = new CalendarDateTime(2024, 1, 32, 0, 0, 0, 0); + expect(date).toEqual(new CalendarDateTime(2024, 2, 1, 0, 0, 0, 0)); + }); + + it('should handle month overflow', function () { + let date = new CalendarDateTime(2024, 13, 1, 0, 0, 0, 0); + expect(date).toEqual(new CalendarDateTime(2025, 1, 1, 0, 0, 0, 0)); + }); + + it('should handle leap year correctly', function () { + let date = new CalendarDateTime(2024, 2, 29, 24, 0, 0, 0); + expect(date).toEqual(new CalendarDateTime(2024, 3, 1, 0, 0, 0, 0)); + }); + + it('should handle non-leap year correctly', function () { + let date = new CalendarDateTime(2023, 2, 29, 0, 0, 0, 0); + expect(date).toEqual(new CalendarDateTime(2023, 3, 1, 0, 0, 0, 0)); + }); + }); + + describe('CalendarDate', function () { + it('should handle day overflow', function () { + let date = new CalendarDate(2024, 1, 32); + expect(date).toEqual(new CalendarDate(2024, 2, 1)); + }); + + it('should handle month overflow', function () { + let date = new CalendarDate(2024, 13, 1); + expect(date).toEqual(new CalendarDate(2025, 1, 1)); + }); + + it('should handle day underflow', function () { + let date = new CalendarDate(2024, 1, 0); + expect(date).toEqual(new CalendarDate(2023, 12, 31)); + }); + + it('should handle month underflow', function () { + let date = new CalendarDate(2024, 0, 1); + expect(date).toEqual(new CalendarDate(2023, 12, 1)); + }); + + it('should handle leap year', function () { + let date = new CalendarDate(2024, 2, 29); + expect(date).toEqual(new CalendarDate(2024, 2, 29)); + }); + + it('should handle non-leap year', function () { + let date = new CalendarDate(2023, 2, 29); + expect(date).toEqual(new CalendarDate(2023, 3, 1)); + }); + + it('should work with different calendar systems', function () { + let date = new CalendarDate(new IslamicUmalquraCalendar(), 1445, 13, 1); + expect(date.month).toBe(1); + expect(date.year).toBe(1446); + }); + }); + + describe('ZonedDateTime', function () { + it('should handle hour overflow', function () { + let date = new ZonedDateTime(2024, 1, 1, 'America/Los_Angeles', -28800000, 26, 0, 0, 0); + expect(date).toEqual(new ZonedDateTime(2024, 1, 2, 'America/Los_Angeles', -28800000, 2, 0, 0, 0)); + }); + + it('should handle minute overflow', function () { + let date = new ZonedDateTime(2024, 1, 1, 'America/Los_Angeles', -28800000, 1, 70, 0, 0); + expect(date).toEqual(new ZonedDateTime(2024, 1, 1, 'America/Los_Angeles', -28800000, 2, 10, 0, 0)); + }); + + it('should handle complex time overflow', function () { + let date = new ZonedDateTime(2024, 1, 1, 'America/Los_Angeles', -28800000, 25, 61, 61, 1001); + expect(date).toEqual(new ZonedDateTime(2024, 1, 2, 'America/Los_Angeles', -28800000, 2, 2, 2, 1)); + }); + + it('should handle day overflow', function () { + let date = new ZonedDateTime(2024, 1, 32, 'America/Los_Angeles', -28800000, 0, 0, 0, 0); + expect(date).toEqual(new ZonedDateTime(2024, 2, 1, 'America/Los_Angeles', -28800000, 0, 0, 0, 0)); + }); + + it('should handle month overflow', function () { + let date = new ZonedDateTime(2024, 13, 1, 'UTC', 0, 0, 0, 0, 0); + expect(date.year).toBe(2025); + expect(date.month).toBe(1); + expect(date.day).toBe(1); + expect(date.hour).toBe(0); + expect(date.minute).toBe(0); + expect(date.second).toBe(0); + expect(date.millisecond).toBe(0); + }); + + it('should handle different calendar systems', function () { + let date = new ZonedDateTime(new IslamicUmalquraCalendar(), 1445, 13, 1, 'America/Los_Angeles', -28800000, 0, 0, 0, 0); + expect(date.month).toBe(1); + expect(date.year).toBe(1446); + }); + }); +});