@@ -499,11 +499,13 @@ impl<'gc> VM<'gc> {
499499 Ok ( value) => value,
500500 Err ( err) => return self . temporal_throw ( ctx, err) ,
501501 } ;
502- // Use to_plain_date_time to correctly handle non-ISO calendars:
503- // value.year() is the calendar year (e.g. Buddhist 0 for ISO -543), not ISO year.
504- // to_plain_date_time() works from the internal IsoDate directly.
505- let date_time = value. to_plain_date_time ( plain_time) ;
506- match date_time. and_then ( |value| value. to_zoned_date_time ( time_zone, Disambiguation :: Compatible ) ) {
502+ // Use PlainDate::to_zoned_date_time which handles two cases correctly:
503+ // 1. plain_time = None → uses get_start_of_day (spec-correct start-of-day)
504+ // 2. plain_time = Some → uses Compatible disambiguation for the given local time
505+ // Using to_plain_date_time() + to_zoned_date_time() would always go via midnight,
506+ // breaking start-of-day for timezones where midnight is in a DST gap.
507+ // PlainDate internally uses IsoDate so non-ISO calendar years are handled correctly.
508+ match value. to_zoned_date_time ( time_zone, plain_time) {
507509 Ok ( value) => {
508510 let ctor_value = self . temporal_intrinsic_ctor_value ( "ZonedDateTime" ) ;
509511 self . temporal_wrap_zoned_date_time ( ctx, ctor_value. as_ref ( ) , & value)
@@ -1863,25 +1865,25 @@ impl<'gc> VM<'gc> {
18631865 Ok ( value) => value,
18641866 Err ( err) => return self . temporal_throw ( ctx, err) ,
18651867 } ;
1866- let self_ref = self
1867- . temporal_plain_year_month_reference_day_slot ( receiver )
1868- . unwrap_or_else ( || value . reference_day ( ) ) ;
1869- let other_ref = self
1870- . temporal_plain_year_month_reference_day_slot ( Some ( arg ) )
1871- . unwrap_or_else ( || other . reference_day ( ) ) ;
1872- Value :: Boolean (
1873- (
1874- value . year ( ) ,
1875- value . month ( ) ,
1876- self_ref ,
1877- Self :: temporal_canonical_calendar_id ( value . calendar ( ) . identifier ( ) ) ,
1878- ) == (
1879- other . year ( ) ,
1880- other . month ( ) ,
1881- other_ref ,
1882- Self :: temporal_canonical_calendar_id ( other . calendar ( ) . identifier ( ) ) ,
1883- ) ,
1884- )
1868+ // Spec: PlainYearMonth.equals compares [[ISODate]] + calendar identifier.
1869+ // Use to_ixdtf_string(Always) to get the canonical ISO date representation
1870+ // (includes the ISO day for all calendars). Canonicalize calendar IDs before
1871+ // comparing so aliases like "islamicc" == "islamic-civil" are treated as equal.
1872+ let canonicalize_pym_repr = | s : String | -> String {
1873+ // Replace calendar ID in "[u-ca=ID]" annotation with canonical form
1874+ if let Some ( bracket_pos ) = s . find ( '[' ) {
1875+ let date_part = & s [ ..bracket_pos ] ;
1876+ let inner = & s [ bracket_pos + 1 ..s . len ( ) . saturating_sub ( 1 ) ] ;
1877+ let cal_id = inner . strip_prefix ( "u-ca=" ) . unwrap_or ( inner ) ;
1878+ let canonical = Self :: temporal_canonical_calendar_id ( cal_id ) ;
1879+ format ! ( "{date_part}[u-ca={canonical}]" )
1880+ } else {
1881+ s
1882+ }
1883+ } ;
1884+ let self_str = canonicalize_pym_repr ( value . to_ixdtf_string ( DisplayCalendar :: Always ) ) ;
1885+ let other_str = canonicalize_pym_repr ( other . to_ixdtf_string ( DisplayCalendar :: Always ) ) ;
1886+ Value :: Boolean ( self_str == other_str )
18851887 }
18861888 "temporal.plainYearMonth.toString" => self . temporal_plain_year_month_to_string ( ctx, receiver, args. first ( ) ) ,
18871889 "temporal.plainYearMonth.toLocaleString" => {
@@ -2052,7 +2054,8 @@ impl<'gc> VM<'gc> {
20522054 let Some ( value) = self . temporal_expect_plain_month_day ( ctx, receiver) else {
20532055 return Value :: Undefined ;
20542056 } ;
2055- let fields = match self . temporal_plain_month_day_to_plain_date_arg ( ctx, args. first ( ) ) {
2057+ let pmd_calendar = value. calendar ( ) . clone ( ) ;
2058+ let fields = match self . temporal_plain_month_day_to_plain_date_arg ( ctx, args. first ( ) , & pmd_calendar) {
20562059 Ok ( value) => value,
20572060 Err ( err) => return self . temporal_throw ( ctx, err) ,
20582061 } ;
@@ -4322,22 +4325,28 @@ impl<'gc> VM<'gc> {
43224325 & mut self ,
43234326 ctx : & GcContext < ' gc > ,
43244327 value : Option < & Value < ' gc > > ,
4328+ pmd_calendar : & Calendar ,
43254329 ) -> Result < CalendarFields , TemporalError > {
43264330 let Some ( value) = value else {
43274331 return Err ( TemporalError :: r#type ( ) . with_message ( "Temporal.PlainMonthDay.prototype.toPlainDate requires an object" ) ) ;
43284332 } ;
43294333 if !self . temporal_is_object_like ( value) {
43304334 return Err ( TemporalError :: r#type ( ) . with_message ( "Temporal.PlainMonthDay.prototype.toPlainDate requires an object" ) ) ;
43314335 }
4332- // Read era/eraYear first (before year) so that eraYear: Infinity throws RangeError,
4333- // not TypeError from the missing-year check below.
4334- let dummy_era_calendar = Calendar :: try_from_utf8 ( b"gregory" ) . unwrap_or_default ( ) ;
4335- let ( era, era_year) = self . temporal_read_era_fields_if_needed ( ctx, value, & dummy_era_calendar, "Invalid Temporal input" ) ?;
4336+ // For ISO calendars, spec says only `year` is read. Reading era/eraYear changes
4337+ // the operation order vs the spec (which reads only get year, year.valueOf).
4338+ if * pmd_calendar == Calendar :: ISO {
4339+ let year = self
4340+ . temporal_optional_trunc_i32_property ( ctx, value, "year" ) ?
4341+ . ok_or_else ( || TemporalError :: r#type ( ) . with_message ( "year is required" ) ) ?;
4342+ return Ok ( CalendarFields :: new ( ) . with_year ( year) ) ;
4343+ }
4344+ // Non-ISO: read era/eraYear FIRST so that eraYear: Infinity throws RangeError
4345+ // before the missing-year TypeError.
4346+ let ( era, era_year) = self . temporal_read_era_fields_if_needed ( ctx, value, pmd_calendar, "Invalid Temporal input" ) ?;
43364347 let year = self . temporal_optional_trunc_i32_property ( ctx, value, "year" ) ?;
4337- // If era+eraYear provided, build a fields object so the calendar resolves the ISO year.
43384348 if era. is_some ( ) || era_year. is_some ( ) {
4339- let fields = CalendarFields :: new ( ) . with_era ( era) . with_era_year ( era_year) . with_optional_year ( year) ;
4340- return Ok ( fields) ;
4349+ return Ok ( CalendarFields :: new ( ) . with_era ( era) . with_era_year ( era_year) . with_optional_year ( year) ) ;
43414350 }
43424351 let year = year. ok_or_else ( || TemporalError :: r#type ( ) . with_message ( "year is required" ) ) ?;
43434352 Ok ( CalendarFields :: new ( ) . with_year ( year) )
@@ -5610,11 +5619,13 @@ impl<'gc> VM<'gc> {
56105619 let valid = if let Some ( rest) = value. strip_prefix ( '+' ) . or_else ( || value. strip_prefix ( '-' ) ) {
56115620 let base = rest. split ( '.' ) . next ( ) . unwrap_or ( rest) ;
56125621 let fraction = rest. strip_prefix ( base) . unwrap_or ( "" ) ;
5622+ // Fractions (.N) are only allowed when seconds are present (HH:MM:SS or HHMMSS formats).
5623+ let has_seconds = matches ! ( base. as_bytes( ) , [ _, _, b':' , _, _, b':' , _, _] | [ _, _, _, _, _, _] ) ;
56135624 let valid_basic = matches ! ( base. as_bytes( ) , [ a, b, b':' , c, d] if a. is_ascii_digit( ) && b. is_ascii_digit( ) && c. is_ascii_digit( ) && d. is_ascii_digit( ) )
56145625 || matches ! ( base. as_bytes( ) , [ a, b, c, d] if a. is_ascii_digit( ) && b. is_ascii_digit( ) && c. is_ascii_digit( ) && d. is_ascii_digit( ) )
56155626 || matches ! ( base. as_bytes( ) , [ a, b, b':' , c, d, b':' , e, f] if a. is_ascii_digit( ) && b. is_ascii_digit( ) && c. is_ascii_digit( ) && d. is_ascii_digit( ) && * e == b'0' && * f == b'0' )
56165627 || matches ! ( base. as_bytes( ) , [ a, b, c, d, e, f] if a. is_ascii_digit( ) && b. is_ascii_digit( ) && c. is_ascii_digit( ) && d. is_ascii_digit( ) && * e == b'0' && * f == b'0' ) ;
5617- valid_basic && fraction. chars ( ) . all ( |ch| ch == '.' || ch == '0' )
5628+ valid_basic && ( fraction. is_empty ( ) || ( has_seconds && fraction . chars ( ) . all ( |ch| ch == '.' || ch == '0' ) ) )
56185629 } else {
56195630 false
56205631 } ;
@@ -6586,9 +6597,19 @@ impl<'gc> VM<'gc> {
65866597
65876598 if let Some ( reference_iso_date) = reference_iso_date
65886599 && let Ok ( reference_date) = PlainDate :: from_utf8 ( reference_iso_date. as_bytes ( ) )
6589- && let Ok ( reconstructed) = reference_date. with_calendar ( calendar. clone ( ) ) . to_plain_year_month ( )
65906600 {
6591- return Some ( reconstructed) ;
6601+ // reference_date is in ISO calendar; its year/month/day ARE the ISO components.
6602+ // Build PYM directly with these ISO components so iso.day is preserved
6603+ // (going through to_plain_year_month() drops the day, normalizing it to 1).
6604+ if let Ok ( reconstructed) = PlainYearMonth :: new_with_overflow (
6605+ reference_date. year ( ) , // ISO year (ISO calendar: year() == iso.year)
6606+ reference_date. month ( ) , // ISO month
6607+ Some ( reference_date. day ( ) ) , // ISO day preserved
6608+ calendar. clone ( ) ,
6609+ Overflow :: Constrain ,
6610+ ) {
6611+ return Some ( reconstructed) ;
6612+ }
65926613 }
65936614
65946615 let partial = PartialYearMonth {
0 commit comments