use oxide_compiler::compiler::Compiler; use oxide_vm::vm::Vm; fn eval(vm: &mut Vm, source: &str) -> Result { let allocator = oxide_parser::Allocator::default(); let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?; let module = Compiler::new().compile(&program).map_err(|e| format!("Compile error: {}", e))?; vm.run(&module) } fn str_val(vm: &Vm, val: oxide_types::value::JsValue) -> String { vm.lookup_str(val).unwrap_or_default() } fn num(vm: &mut Vm, source: &str) -> f64 { let r = eval(vm, source).unwrap(); if r.is_double() { r.as_double() } else { r.as_int() as f64 } } // -- Temporal 命名空间 -- #[test] fn temporal_is_object() { let mut vm = Vm::new(); let r = eval(&mut vm, "typeof Temporal").unwrap(); assert_eq!(str_val(&vm, r), "object"); } #[test] fn temporal_now_time_zone_id_is_utc() { let mut vm = Vm::new(); let r = eval(&mut vm, "Temporal.Now.timeZoneId()").unwrap(); assert_eq!(str_val(&vm, r), "UTC"); } #[test] fn temporal_now_instant_returns_instant() { let mut vm = Vm::new(); let r = eval(&mut vm, "Temporal.Now.instant() instanceof Temporal.Instant").unwrap(); assert!(r.as_bool()); } #[test] fn temporal_now_instant_epoch_is_positive() { let mut vm = Vm::new(); let r = num(&mut vm, "Temporal.Now.instant().epochMilliseconds"); assert!(r > 1_500_000_000_000.0); } #[test] fn temporal_now_is_namespace_not_constructor() { let mut vm = Vm::new(); let r = eval(&mut vm, "typeof Temporal.Now").unwrap(); assert_eq!(str_val(&vm, r), "object"); } // -- Temporal.Instant -- #[test] fn instant_from_iso_string() { let mut vm = Vm::new(); let r = eval(&mut vm, "Temporal.Instant.from('2024-01-01T00:00:00Z').toString()").unwrap(); assert_eq!(str_val(&vm, r), "2024-01-01T00:00:00Z"); } #[test] fn instant_epoch_seconds() { let mut vm = Vm::new(); assert_eq!(num(&mut vm, "Temporal.Instant.from('2024-01-01T00:00:00Z').epochSeconds"), 1704067200.0); } #[test] fn instant_epoch_milliseconds() { let mut vm = Vm::new(); assert_eq!( num(&mut vm, "Temporal.Instant.from('2024-01-01T00:00:00Z').epochMilliseconds"), 1704067200000.0 ); } #[test] fn instant_epoch_nanoseconds() { let mut vm = Vm::new(); let r = eval( &mut vm, "Temporal.Instant.from('2024-01-01T00:00:00Z').epochNanoseconds === 1704067200000000000n", ) .unwrap(); assert!(r.as_bool()); } #[test] fn instant_from_epoch_milliseconds_is_exact() { let mut vm = Vm::new(); let r = eval( &mut vm, "Temporal.Instant.fromEpochMilliseconds(-217175010876).epochNanoseconds === -217175010876000000n", ) .unwrap(); assert!(r.as_bool()); } #[test] fn instant_from_epoch_nanoseconds_is_exact() { let mut vm = Vm::new(); let r = eval( &mut vm, "Temporal.Instant.fromEpochNanoseconds(217175010123456789n).epochNanoseconds === 217175010123456789n && Temporal.Instant.fromEpochNanoseconds(-217175010876543211n).epochMilliseconds === -217175010877", ) .unwrap(); assert!(r.as_bool()); } #[test] fn instant_equals_compares_exact_epoch_nanoseconds() { let mut vm = Vm::new(); let r = eval( &mut vm, "Temporal.Instant.fromEpochNanoseconds(217175010123456789n) .equals(Temporal.Instant.fromEpochMilliseconds(217175010123))", ) .unwrap(); assert!(!r.as_bool()); } #[test] fn instant_compare_accepts_instances_strings_and_annotations() { let mut vm = Vm::new(); let r = eval( &mut vm, "Temporal.Instant.compare('1970-01-01T00:00Z[UTC]', new Temporal.Instant(0n)) === 0 && Temporal.Instant.compare('1969-12-31T23:00+00:00', new Temporal.Instant(0n)) === -1 && Temporal.Instant.compare('1970-01-01T01:00+00:00', new Temporal.Instant(0n)) === 1", ) .unwrap(); assert!(r.as_bool()); } #[test] fn instant_add_and_subtract_are_exact() { let mut vm = Vm::new(); let r = eval( &mut vm, "let i = Temporal.Instant.fromEpochNanoseconds(1582966647747612578n); i.add({hours: 1, microseconds: 9007199254740991}).epochNanoseconds === 10590169502488603578n && i.subtract('PT1.03125H').epochNanoseconds === 1582962935247612578n", ) .unwrap(); assert!(r.as_bool()); } #[test] fn instant_add_ignores_receiver_subclass_for_result() { let mut vm = Vm::new(); let r = eval( &mut vm, "class Sub extends Temporal.Instant { constructor(value) { super(value); } } let result = new Sub(10n).add({nanoseconds: 5}); result instanceof Temporal.Instant && !(result instanceof Sub) && result.epochNanoseconds === 15n", ) .unwrap(); assert!(r.as_bool()); } #[test] fn instant_arithmetic_rejects_date_units_and_out_of_range_results() { let mut vm = Vm::new(); let r = eval( &mut vm, "let max = Temporal.Instant.fromEpochNanoseconds(8640000000000000000000n); (() => { try { max.add({nanoseconds: 1}); return false } catch (e) { return e instanceof RangeError } })() && (() => { try { max.subtract({days: 1}); return false } catch (e) { return e instanceof RangeError } })()", ) .unwrap(); assert!(r.as_bool()); } #[test] fn instant_round_supports_all_rounding_directions() { let mut vm = Vm::new(); let r = eval( &mut vm, "let i = new Temporal.Instant(-1500n); i.round({smallestUnit:'microsecond', roundingMode:'floor'}).epochNanoseconds === -2000n && i.round({smallestUnit:'microsecond', roundingMode:'ceil'}).epochNanoseconds === -1000n && i.round({smallestUnit:'microsecond', roundingMode:'halfFloor'}).epochNanoseconds === -2000n && i.round({smallestUnit:'microsecond', roundingMode:'halfCeil'}).epochNanoseconds === -1000n && i.round({smallestUnit:'microsecond', roundingMode:'halfEven'}).epochNanoseconds === -2000n", ) .unwrap(); assert!(r.as_bool()); } #[test] fn instant_round_validates_increment_and_accepts_shorthand() { let mut vm = Vm::new(); let r = eval( &mut vm, "let i = new Temporal.Instant(123456789n); i.round('microseconds').epochNanoseconds === 123457000n && i.round({smallestUnit:'nanosecond', roundingIncrement:2.5, roundingMode:'expand'}).epochNanoseconds === 123456790n && (() => { try { i.round({smallestUnit:'hour', roundingIncrement:7}); return false } catch (e) { return e instanceof RangeError } })()", ) .unwrap(); assert!(r.as_bool()); } #[test] fn instant_round_accepts_plural_units_inside_callbacks() { let mut vm = Vm::new(); let r = eval( &mut vm, "let i = new Temporal.Instant(1000000000123456789n), ok = true; ['hour','minute','second','millisecond','microsecond','nanosecond'].forEach(unit => { ok = ok && i.round({smallestUnit:unit}).equals(i.round({smallestUnit:`${unit}s`})); }); ok", ) .unwrap(); assert!(r.as_bool()); } #[test] fn instant_round_handles_shorthand_callback_parameters() { let mut vm = Vm::new(); let r = eval( &mut vm, "let i = new Temporal.Instant(1n), shorthand = true; ['hour'].forEach(smallestUnit => { shorthand = i.round({smallestUnit}).equals(i.round(smallestUnit)); }); shorthand", ) .unwrap(); assert!(r.as_bool()); } #[test] fn instant_round_handles_nested_destructured_callback_captures() { let mut vm = Vm::new(); let r = eval( &mut vm, "let i = new Temporal.Instant(1n); let pairs = [['hour', [1, 2]]], captured = true; pairs.forEach(([unit, increments]) => { increments.forEach(increment => { captured = captured && i.round({smallestUnit: unit, roundingIncrement: increment}) instanceof Temporal.Instant; }); }); captured", ) .unwrap(); assert!(r.as_bool()); } #[test] fn instant_until_and_since_balance_exact_differences() { let mut vm = Vm::new(); let r = eval( &mut vm, "let earlier = new Temporal.Instant(1000000000000000000n); let later = new Temporal.Instant(1000090061987654321n); let exact = earlier.until(later); let hours = later.since(earlier, { largestUnit: 'hours' }); exact.seconds === 90061 && exact.milliseconds === 987 && exact.microseconds === 654 && exact.nanoseconds === 321 && hours.hours === 25 && hours.minutes === 1 && hours.seconds === 1 && hours.milliseconds === 987 && hours.microseconds === 654 && hours.nanoseconds === 321", ) .unwrap(); assert!(r.as_bool()); } #[test] fn instant_difference_rounds_signed_values_and_validates_units() { let mut vm = Vm::new(); let r = eval( &mut vm, "let zero = new Temporal.Instant(0n), value = new Temporal.Instant(7199000000000n); let positive = zero.until(value, { largestUnit: 'hours', smallestUnit: 'minutes', roundingMode: 'expand' }); let negative = zero.since(value, { largestUnit: 'hours', smallestUnit: 'minutes', roundingMode: 'expand' }); positive.hours === 2 && positive.minutes === 0 && negative.hours === -2 && negative.minutes === 0 && (() => { try { zero.until(value, { largestUnit: 'seconds', smallestUnit: 'hours' }); return false } catch (e) { return e instanceof RangeError } })() && (() => { try { zero.until(value, { smallestUnit: 'minutes', roundingIncrement: 60 }); return false } catch (e) { return e instanceof RangeError } })()", ) .unwrap(); assert!(r.as_bool()); } #[test] fn instant_to_string_supports_precision_rounding_and_offsets() { let mut vm = Vm::new(); let r = eval( &mut vm, "let instant = new Temporal.Instant(1000000000123987500n); instant.toString({smallestUnit:'microsecond', roundingMode:'halfEven'}) === '2001-09-09T01:46:40.123988Z' && instant.toString({fractionalSecondDigits:3}) === '2001-09-09T01:46:40.123Z' && new Temporal.Instant(0n).toString({timeZone:'-05:00'}) === '1969-12-31T19:00:00-05:00' && new Temporal.Instant(999999960000000000n).toString({smallestUnit:'minute'}) === '2001-09-09T01:46Z'", ) .unwrap(); assert!(r.as_bool()); } #[test] fn instant_json_and_locale_string_use_default_iso_output() { let mut vm = Vm::new(); let r = eval( &mut vm, "let instant = new Temporal.Instant(30123400000n); instant.toJSON({get ignored(){throw new Error()}}) === '1970-01-01T00:00:30.1234Z' && typeof instant.toLocaleString('en', {dateStyle:'short'}) === 'string'", ) .unwrap(); assert!(r.as_bool()); } #[test] fn instant_to_string_tag_has_temporal_descriptor() { let mut vm = Vm::new(); let r = eval( &mut vm, "let descriptor = Object.getOwnPropertyDescriptor(Temporal.Instant.prototype, Symbol.toStringTag); Object.prototype.toString.call(Temporal.Instant.prototype) === '[object Temporal.Instant]' && descriptor.value === 'Temporal.Instant' && descriptor.writable === false && descriptor.enumerable === false && descriptor.configurable === true", ) .unwrap(); assert!(r.as_bool()); } #[test] fn instant_to_zoned_date_time_iso_preserves_epoch_and_time_zone() { let mut vm = Vm::new(); let r = eval( &mut vm, "let instant = new Temporal.Instant(1000000000000000000n); let utc = instant.toZonedDateTimeISO('uTc'); let offset = instant.toZonedDateTimeISO('2021-08-19T17:30-07:00'); let constructed = new Temporal.ZonedDateTime(0n, '+01:30'); utc.epochNanoseconds === instant.epochNanoseconds && utc.timeZoneId === 'UTC' && utc.calendarId === 'iso8601' && offset.timeZoneId === '-07:00' && constructed.epochNanoseconds === 0n && constructed.timeZoneId === '+01:30' && Object.prototype.toString.call(constructed) === '[object Temporal.ZonedDateTime]'", ) .unwrap(); assert!(r.as_bool()); } #[test] fn instant_epoch_factories_validate_input_and_range() { let mut vm = Vm::new(); let r = eval( &mut vm, "(() => { try { Temporal.Instant.fromEpochMilliseconds(1.5); return false } catch (e) { return e instanceof RangeError } })() && (() => { try { Temporal.Instant.fromEpochMilliseconds(1n); return false } catch (e) { return e instanceof TypeError } })() && (() => { try { Temporal.Instant.fromEpochNanoseconds(1); return false } catch (e) { return e instanceof TypeError } })() && (() => { try { Temporal.Instant.fromEpochNanoseconds(8640000000000000000001n); return false } catch (e) { return e instanceof RangeError } })()", ) .unwrap(); assert!(r.as_bool()); } #[test] fn instant_constructor_requires_new() { let mut vm = Vm::new(); let r = eval(&mut vm, "try { Temporal.Instant(0); 'no' } catch (e) { e.constructor.name }").unwrap(); assert_eq!(str_val(&vm, r), "TypeError"); } #[test] fn instant_value_of_throws() { let mut vm = Vm::new(); let r = eval( &mut vm, "try { Temporal.Instant.prototype.valueOf.call(Temporal.Now.instant()) } catch (e) { e.constructor.name }", ) .unwrap(); assert_eq!(str_val(&vm, r), "TypeError"); } #[test] fn instant_method_on_non_instant_throws() { let mut vm = Vm::new(); let r = eval( &mut vm, "try { Temporal.Instant.prototype.toString.call({}) } catch (e) { e.constructor.name }", ) .unwrap(); assert_eq!(str_val(&vm, r), "TypeError"); } #[test] fn instant_from_invalid_throws_range_error() { let mut vm = Vm::new(); let r = eval(&mut vm, "try { Temporal.Instant.from('nope') } catch (e) { e.constructor.name }").unwrap(); assert_eq!(str_val(&vm, r), "RangeError"); } // -- Temporal.PlainDate -- #[test] fn plain_date_from_string() { let mut vm = Vm::new(); assert_eq!(num(&mut vm, "Temporal.PlainDate.from('2024-01-15').year"), 2024.0); assert_eq!(num(&mut vm, "Temporal.PlainDate.from('2024-01-15').month"), 1.0); assert_eq!(num(&mut vm, "Temporal.PlainDate.from('2024-01-15').day"), 15.0); } #[test] fn plain_date_from_object() { let mut vm = Vm::new(); let r = eval(&mut vm, "Temporal.PlainDate.from({year:2023, month:3, day:5}).toString()").unwrap(); assert_eq!(str_val(&vm, r), "2023-03-05"); } #[test] fn plain_date_constructor_month_day() { let mut vm = Vm::new(); assert_eq!(num(&mut vm, "new Temporal.PlainDate(2024, 2, 29).month"), 2.0); assert_eq!(num(&mut vm, "new Temporal.PlainDate(2024, 2, 29).day"), 29.0); assert_eq!(num(&mut vm, "new Temporal.PlainDate(2024, 2, 29).year"), 2024.0); } #[test] fn plain_date_leap_year_validation() { let mut vm = Vm::new(); let r = eval(&mut vm, "try { new Temporal.PlainDate(2024, 2, 30) } catch (e) { e.constructor.name }").unwrap(); assert_eq!(str_val(&vm, r), "RangeError"); } #[test] fn plain_date_invalid_month() { let mut vm = Vm::new(); let r = eval(&mut vm, "try { new Temporal.PlainDate(2024, 13, 1) } catch (e) { e.constructor.name }").unwrap(); assert_eq!(str_val(&vm, r), "RangeError"); } #[test] fn plain_date_to_string() { let mut vm = Vm::new(); let r = eval(&mut vm, "new Temporal.PlainDate(2024, 1, 5).toString()").unwrap(); assert_eq!(str_val(&vm, r), "2024-01-05"); } #[test] fn plain_date_requires_new() { let mut vm = Vm::new(); let r = eval(&mut vm, "try { Temporal.PlainDate(2024, 1, 1); 'no' } catch (e) { e.constructor.name }").unwrap(); assert_eq!(str_val(&vm, r), "TypeError"); } #[test] fn plain_date_method_on_non_date_throws() { let mut vm = Vm::new(); let r = eval( &mut vm, "try { Temporal.PlainDate.prototype.year.get.call({}) } catch (e) { e.constructor.name }", ) .unwrap(); assert_eq!(str_val(&vm, r), "TypeError"); } #[test] fn plain_date_add_and_subtract_support_negative_offsets() { let mut vm = Vm::new(); let r = eval(&mut vm, "new Temporal.PlainDate(2024, 1, 31).add({months:1}).toString()").unwrap(); assert_eq!(str_val(&vm, r), "2024-02-29"); let r = eval(&mut vm, "new Temporal.PlainDate(2024, 1, 5).subtract({days:10}).toString()").unwrap(); assert_eq!(str_val(&vm, r), "2023-12-26"); } #[test] fn plain_date_until_and_since_return_duration() { let mut vm = Vm::new(); let r = eval(&mut vm, "new Temporal.PlainDate(2024, 1, 1).until('2024-01-06').toString()").unwrap(); assert_eq!(str_val(&vm, r), "P5D"); assert_eq!( num( &mut vm, "new Temporal.PlainDate(2024, 1, 6).since(new Temporal.PlainDate(2024, 1, 11)).days", ), -5.0 ); assert_eq!( num( &mut vm, "new Temporal.PlainDate(1997, 7, 16).since(new Temporal.PlainDate(2021, 7, 15), {largestUnit:'years'}).days", ), -29.0 ); } #[test] fn duration_constructor_exposes_components() { let mut vm = Vm::new(); let r = eval(&mut vm, "new Temporal.Duration(1,2,3,4,5,6,7,8,9,10).toString()").unwrap(); assert_eq!(str_val(&vm, r), "P1Y2M3W4DT5H6M7.00800901S"); assert_eq!(num(&mut vm, "new Temporal.Duration(1,2,3,4,5,6,7,8,9,10).nanoseconds"), 10.0); let r = eval(&mut vm, "Temporal.Duration.from('-PT24.567890123H').toString()").unwrap(); assert_eq!(str_val(&vm, r), "-PT24H34M4.4044428S"); assert_eq!(num(&mut vm, "Temporal.Duration.from('P43Y').years"), 43.0); let r = eval(&mut vm, "new Temporal.Duration(1,2,3,4).negated().abs().toString()").unwrap(); assert_eq!(str_val(&vm, r), "P1Y2M3W4D"); } #[test] fn duration_from_validates_property_bags_and_preserves_precision() { let mut vm = Vm::new(); let r = eval(&mut vm, "try { Temporal.Duration.from({}) } catch (e) { e.constructor.name }").unwrap(); assert_eq!(str_val(&vm, r), "TypeError"); let r = eval( &mut vm, "try { Temporal.Duration.from({hours:1,minutes:-1}) } catch (e) { e.constructor.name }", ) .unwrap(); assert_eq!(str_val(&vm, r), "RangeError"); let r = eval( &mut vm, "Temporal.Duration.from({milliseconds:4503599627370497000,microseconds:4503599627370495000000}).toString()", ) .unwrap(); assert_eq!(str_val(&vm, r), "PT9007199254740991.975424S"); } #[test] fn duration_from_propagates_getter_errors_and_checks_range() { let mut vm = Vm::new(); let r = eval( &mut vm, "try { Temporal.Duration.from({get years(){throw new TypeError('sentinel')}}) } catch (e) { e.message }", ) .unwrap(); assert_eq!(str_val(&vm, r), "sentinel"); let r = eval( &mut vm, "try { Temporal.Duration.from({seconds:9007199254740992}) } catch (e) { e.constructor.name }", ) .unwrap(); assert_eq!(str_val(&vm, r), "RangeError"); let r = eval(&mut vm, "Temporal.Duration.from('p1y1m1dt1h1m1s').toString()").unwrap(); assert_eq!(str_val(&vm, r), "P1Y1M1DT1H1M1S"); } #[test] fn plain_date_balances_duration_time_units_into_days() { let mut vm = Vm::new(); let r = eval(&mut vm, "new Temporal.PlainDate(2000,5,2).add('P1DT24H1440M86400S').toString()").unwrap(); assert_eq!(str_val(&vm, r), "2000-05-06"); let r = eval(&mut vm, "new Temporal.PlainDate(2000,5,2).add('-PT24.567890123H').toString()").unwrap(); assert_eq!(str_val(&vm, r), "2000-05-01"); } // -- Temporal.PlainTime -- #[test] fn plain_time_constructor_components() { let mut vm = Vm::new(); assert_eq!(num(&mut vm, "new Temporal.PlainTime(12, 30, 5).hour"), 12.0); assert_eq!(num(&mut vm, "new Temporal.PlainTime(12, 30, 5).minute"), 30.0); assert_eq!(num(&mut vm, "new Temporal.PlainTime(12, 30, 5).second"), 5.0); } #[test] fn plain_time_full_precision_components() { let mut vm = Vm::new(); let r = eval( &mut vm, "let t = new Temporal.PlainTime(23, 59, 59, 999, 999, 999); t.hour + ',' + t.minute + ',' + t.second + ',' + t.millisecond + ',' + t.microsecond + ',' + t.nanosecond", ) .unwrap(); assert_eq!(str_val(&vm, r), "23,59,59,999,999,999"); } #[test] fn plain_time_to_string() { let mut vm = Vm::new(); let r = eval(&mut vm, "new Temporal.PlainTime(12, 30, 5).toString()").unwrap(); assert_eq!(str_val(&vm, r), "12:30:05"); } #[test] fn plain_time_to_string_with_subseconds() { let mut vm = Vm::new(); let r = eval(&mut vm, "new Temporal.PlainTime(12, 30, 5, 500, 250, 1).toString()").unwrap(); assert_eq!(str_val(&vm, r), "12:30:05.500250001"); } #[test] fn plain_time_missing_components_default_zero() { let mut vm = Vm::new(); let r = eval(&mut vm, "new Temporal.PlainTime().toString()").unwrap(); assert_eq!(str_val(&vm, r), "00:00:00"); let r = eval(&mut vm, "new Temporal.PlainTime(12).toString()").unwrap(); assert_eq!(str_val(&vm, r), "12:00:00"); } #[test] fn plain_time_range_validation() { let mut vm = Vm::new(); let r = eval(&mut vm, "try { new Temporal.PlainTime(24, 0, 0) } catch (e) { e.constructor.name }").unwrap(); assert_eq!(str_val(&vm, r), "RangeError"); let r = eval(&mut vm, "try { new Temporal.PlainTime(0, 60, 0) } catch (e) { e.constructor.name }").unwrap(); assert_eq!(str_val(&vm, r), "RangeError"); } #[test] fn plain_time_equals_same_and_different_fields() { let mut vm = Vm::new(); let r = eval(&mut vm, "new Temporal.PlainTime(12, 30, 5).equals(new Temporal.PlainTime(12, 30, 5))").unwrap(); assert!(r.as_bool()); let r = eval(&mut vm, "new Temporal.PlainTime(12, 30, 5).equals(new Temporal.PlainTime(12, 30, 6))").unwrap(); assert!(!r.as_bool()); let r = eval(&mut vm, "new Temporal.PlainTime(12, 30, 5).equals('12:30:05')").unwrap(); assert!(r.as_bool()); } #[test] fn plain_time_equals_invalid_argument_throws() { let mut vm = Vm::new(); let r = eval(&mut vm, "try { new Temporal.PlainTime(12).equals(42) } catch (e) { e.constructor.name }").unwrap(); assert_eq!(str_val(&vm, r), "TypeError"); let r = eval( &mut vm, "try { Temporal.PlainTime.prototype.equals.call({}) } catch (e) { e.constructor.name }", ) .unwrap(); assert_eq!(str_val(&vm, r), "TypeError"); } #[test] fn plain_time_compare_epoch_order() { let mut vm = Vm::new(); assert_eq!( num( &mut vm, "Temporal.PlainTime.compare(new Temporal.PlainTime(12, 30), new Temporal.PlainTime(13, 30))" ), -1.0 ); assert_eq!( num( &mut vm, "Temporal.PlainTime.compare(new Temporal.PlainTime(12, 30), new Temporal.PlainTime(12, 30))" ), 0.0 ); assert_eq!( num( &mut vm, "Temporal.PlainTime.compare(new Temporal.PlainTime(13, 30), new Temporal.PlainTime(12, 30))" ), 1.0 ); assert_eq!(num(&mut vm, "Temporal.PlainTime.compare('23:59', '00:00')"), 1.0); } #[test] fn plain_time_until_since_normal() { let mut vm = Vm::new(); let r = eval(&mut vm, "new Temporal.PlainTime(12, 30).until(new Temporal.PlainTime(13, 30)).toString()").unwrap(); assert_eq!(str_val(&vm, r), "PT1H"); let r = eval(&mut vm, "new Temporal.PlainTime(13, 30).since(new Temporal.PlainTime(12, 30)).toString()").unwrap(); assert_eq!(str_val(&vm, r), "PT1H"); let r = eval( &mut vm, "new Temporal.PlainTime(12, 30).until(new Temporal.PlainTime(13, 30, 5, 500)).toString()", ) .unwrap(); assert_eq!(str_val(&vm, r), "PT1H5.5S"); } #[test] fn plain_time_until_since_options_and_direction() { let mut vm = Vm::new(); let r = eval( &mut vm, "new Temporal.PlainTime(12, 30).until(new Temporal.PlainTime(13, 35), { largestUnit: 'minute' }).toString()", ) .unwrap(); assert_eq!(str_val(&vm, r), "PT65M"); let r = eval( &mut vm, "new Temporal.PlainTime(12, 30).since(new Temporal.PlainTime(13, 35), { largestUnit: 'minute' }).toString()", ) .unwrap(); assert_eq!(str_val(&vm, r), "-PT65M"); let r = eval( &mut vm, "new Temporal.PlainTime(12, 30).until(new Temporal.PlainTime(13, 30), { smallestUnit: 'hour' }).toString()", ) .unwrap(); assert_eq!(str_val(&vm, r), "PT1H"); } #[test] fn plain_time_add_subtract_normal() { let mut vm = Vm::new(); // 同日内小时加/减与跨分钟进位。 let r = eval(&mut vm, "new Temporal.PlainTime(12, 30).add({ hours: 1, minutes: 5 }).toString()").unwrap(); assert_eq!(str_val(&vm, r), "13:35:00"); let r = eval(&mut vm, "new Temporal.PlainTime(12, 30).subtract({ hours: 1, minutes: 5 }).toString()").unwrap(); assert_eq!(str_val(&vm, r), "11:25:00"); // 亚秒进位:秒进位到分钟。 let r = eval(&mut vm, "new Temporal.PlainTime(12, 30, 59, 500).add({ milliseconds: 500 }).toString()").unwrap(); assert_eq!(str_val(&vm, r), "12:31:00"); } #[test] fn plain_time_add_subtract_cross_midnight() { let mut vm = Vm::new(); // 时间溢出跨午夜:rem_euclid 回 0-24 域。 let r = eval(&mut vm, "new Temporal.PlainTime(23, 30).add({ hours: 1 }).toString()").unwrap(); assert_eq!(str_val(&vm, r), "00:30:00"); let r = eval(&mut vm, "new Temporal.PlainTime(00, 30).subtract({ hours: 1 }).toString()").unwrap(); assert_eq!(str_val(&vm, r), "23:30:00"); // 多天增量同样折叠回当日。 let r = eval(&mut vm, "new Temporal.PlainTime(12, 30).add({ hours: 26 }).toString()").unwrap(); assert_eq!(str_val(&vm, r), "14:30:00"); } #[test] fn plain_time_add_subtract_ignores_date_units_and_blank() { let mut vm = Vm::new(); // 日期字段(days 及以上)对 PlainTime 忽略,不报错。 let r = eval(&mut vm, "new Temporal.PlainTime(12, 30).add({ days: 3, hours: 1 }).toString()").unwrap(); assert_eq!(str_val(&vm, r), "13:30:00"); // blank duration(Duration 对象全零):值不变的新对象。 let r = eval(&mut vm, "new Temporal.PlainTime(12, 30).add(new Temporal.Duration()).toString()").unwrap(); assert_eq!(str_val(&vm, r), "12:30:00"); let r = eval(&mut vm, "new Temporal.PlainTime(12, 30).subtract(new Temporal.Duration()).toString()").unwrap(); assert_eq!(str_val(&vm, r), "12:30:00"); // 空对象无 duration 字段 → TypeError(符合 duration-like 字段要求)。 let r = eval(&mut vm, "try { new Temporal.PlainTime(12, 30).add({}) } catch (e) { e.constructor.name }").unwrap(); assert_eq!(str_val(&vm, r), "TypeError"); } #[test] fn plain_time_add_subtract_invalid_argument() { let mut vm = Vm::new(); // 非 duration-like 原始值抛 TypeError。 let r = eval(&mut vm, "try { new Temporal.PlainTime(12).add(42) } catch (e) { e.constructor.name }").unwrap(); assert_eq!(str_val(&vm, r), "TypeError"); } #[test] fn plain_time_round_units() { let mut vm = Vm::new(); // 亚秒舍入到秒(halfExpand 默认)。 let r = eval( &mut vm, "new Temporal.PlainTime(12, 30, 5, 800).round({ smallestUnit: 'second' }).toString()", ) .unwrap(); assert_eq!(str_val(&vm, r), "12:30:06"); // 舍入到分钟:30:59 进位到 31。 let r = eval(&mut vm, "new Temporal.PlainTime(12, 30, 59).round({ smallestUnit: 'minute' }).toString()").unwrap(); assert_eq!(str_val(&vm, r), "12:31:00"); // 舍入到小时:30:30 进位到 13。 let r = eval(&mut vm, "new Temporal.PlainTime(12, 30, 30).round({ smallestUnit: 'hour' }).toString()").unwrap(); assert_eq!(str_val(&vm, r), "13:00:00"); // 字符串简写形式同效。 let r = eval(&mut vm, "new Temporal.PlainTime(12, 30, 30).round('hour').toString()").unwrap(); assert_eq!(str_val(&vm, r), "13:00:00"); } #[test] fn plain_time_round_cross_midnight() { let mut vm = Vm::new(); // 23:59:59.9 舍入到秒 → 24:00:00 → 取模回 00:00:00。 let r = eval( &mut vm, "new Temporal.PlainTime(23, 59, 59, 900).round({ smallestUnit: 'second' }).toString()", ) .unwrap(); assert_eq!(str_val(&vm, r), "00:00:00"); // 23:30 舍入到小时 → 24:00 → 00:00。 let r = eval(&mut vm, "new Temporal.PlainTime(23, 30).round({ smallestUnit: 'hour' }).toString()").unwrap(); assert_eq!(str_val(&vm, r), "00:00:00"); } #[test] fn plain_time_round_increment_and_mode() { let mut vm = Vm::new(); // 15 分钟增量舍入:23:07:30 → 23:00(floor)。 let r = eval( &mut vm, "new Temporal.PlainTime(23, 7, 30).round({ smallestUnit: 'minute', roundingIncrement: 15, roundingMode: 'floor' }).toString()", ) .unwrap(); assert_eq!(str_val(&vm, r), "23:00:00"); // 真因子校验:increment 必须整除最大增量且严格小于之。 let r = eval( &mut vm, "try { new Temporal.PlainTime(12).round({ smallestUnit: 'minute', roundingIncrement: 60 }) } catch (e) { e.constructor.name }", ) .unwrap(); assert_eq!(str_val(&vm, r), "RangeError"); let r = eval( &mut vm, "try { new Temporal.PlainTime(12).round({ smallestUnit: 'hour', roundingIncrement: 24 }) } catch (e) { e.constructor.name }", ) .unwrap(); assert_eq!(str_val(&vm, r), "RangeError"); } #[test] fn plain_time_round_invalid_round_to() { let mut vm = Vm::new(); // roundTo undefined → TypeError。 let r = eval(&mut vm, "try { new Temporal.PlainTime(12).round() } catch (e) { e.constructor.name }").unwrap(); assert_eq!(str_val(&vm, r), "TypeError"); // 对象缺 smallestUnit → RangeError。 let r = eval(&mut vm, "try { new Temporal.PlainTime(12).round({}) } catch (e) { e.constructor.name }").unwrap(); assert_eq!(str_val(&vm, r), "RangeError"); // 非法最小单位 → RangeError。 let r = eval( &mut vm, "try { new Temporal.PlainTime(12).round({ smallestUnit: 'day' }) } catch (e) { e.constructor.name }", ) .unwrap(); assert_eq!(str_val(&vm, r), "RangeError"); } // -- Temporal.PlainDateTime -- #[test] fn plain_date_time_constructor_and_components() { let mut vm = Vm::new(); let r = eval( &mut vm, "let dt = new Temporal.PlainDateTime(2024, 2, 29, 23, 58, 57, 987, 654, 321); dt.year + ',' + dt.month + ',' + dt.day + ',' + dt.hour + ',' + dt.minute + ',' + dt.second + ',' + dt.millisecond + ',' + dt.microsecond + ',' + dt.nanosecond + ',' + dt.calendarId", ) .unwrap(); assert_eq!(str_val(&vm, r), "2024,2,29,23,58,57,987,654,321,iso8601"); } #[test] fn plain_date_time_defaults_time_to_midnight() { let mut vm = Vm::new(); assert_eq!( num( &mut vm, "let dt = new Temporal.PlainDateTime(2024, 7, 6); dt.hour + dt.minute + dt.second + dt.nanosecond", ), 0.0 ); } #[test] fn plain_date_time_splits_into_plain_date_and_time() { let mut vm = Vm::new(); let r = eval( &mut vm, "let dt = new Temporal.PlainDateTime(2000, 5, 2, 12, 34, 56, 789, 123, 456); dt.toPlainDate().toString() + 'T' + dt.toPlainTime().toString()", ) .unwrap(); assert_eq!(str_val(&vm, r), "2000-05-02T12:34:56.789123456"); } #[test] fn plain_date_time_validates_brand_and_ranges() { let mut vm = Vm::new(); let r = eval( &mut vm, "let getter = Object.getOwnPropertyDescriptor(Temporal.PlainDateTime.prototype, 'year').get; let a; let b; let c; try { getter.call({}) } catch (e) { a = e.constructor.name } try { new Temporal.PlainDateTime(2023, 2, 29) } catch (e) { b = e.constructor.name } try { new Temporal.PlainDateTime(2024, 1, 1, 24) } catch (e) { c = e.constructor.name } a + ',' + b + ',' + c", ) .unwrap(); assert_eq!(str_val(&vm, r), "TypeError,RangeError,RangeError"); } #[test] fn plain_date_time_metadata_and_value_of() { let mut vm = Vm::new(); let r = eval( &mut vm, "let dt = new Temporal.PlainDateTime(2024, 1, 2); let error; try { dt.valueOf() } catch (e) { error = e.constructor.name } Temporal.PlainDateTime.length + ',' + (dt instanceof Temporal.PlainDateTime) + ',' + Object.prototype.toString.call(dt) + ',' + error", ) .unwrap(); assert_eq!(str_val(&vm, r), "3,true,[object Temporal.PlainDateTime],TypeError"); } #[test] fn plain_date_time_from_instances_strings_and_objects() { let mut vm = Vm::new(); let r = eval( &mut vm, "let original = new Temporal.PlainDateTime(1976, 11, 18, 15, 23, 30, 1, 123, 456); let copy = Temporal.PlainDateTime.from(original); let parsed = Temporal.PlainDateTime.from('1976-11-18T15:23:30.001123456'); let bag = Temporal.PlainDateTime.from({year: 1976, monthCode: 'M11', day: 18, hour: 15}); (copy !== original) + ',' + parsed.toString() + ',' + bag.toString()", ) .unwrap(); assert_eq!(str_val(&vm, r), "true,1976-11-18T15:23:30.001123456,1976-11-18T15:00:00"); } #[test] fn plain_date_time_compare_uses_internal_slots() { let mut vm = Vm::new(); let r = eval( &mut vm, "let earlier = new Temporal.PlainDateTime(2000, 5, 2, 12, 34, 56); let later = new Temporal.PlainDateTime(2000, 5, 2, 12, 34, 57); Object.defineProperty(earlier, 'year', {get() { throw new Error('getter') }}); Object.defineProperty(later, 'year', {get() { throw new Error('getter') }}); Temporal.PlainDateTime.compare(earlier, later) + ',' + Temporal.PlainDateTime.compare(later, earlier) + ',' + Temporal.PlainDateTime.compare(earlier, earlier)", ) .unwrap(); assert_eq!(str_val(&vm, r), "-1,1,0"); } #[test] fn plain_date_time_from_accepts_variants_and_full_range() { let mut vm = Vm::new(); let r = eval( &mut vm, "const lower = Temporal.PlainDateTime.from('1976-11-18t15:23').toString(); const space = Temporal.PlainDateTime.from('1976-11-18 15:23').toString(); const offset = Temporal.PlainDateTime.from('1976-11-18T15:23+00:00:00,0[UTC]').toString(); const criticalZone = Temporal.PlainDateTime.from('1976-11-18T15:23[!Europe/Vienna]').toString(); const secondCalendar = Temporal.PlainDateTime.from('1976-11-18T15:23[u-ca=iso8601][u-ca=discord]').toString(); const minOk = Temporal.PlainDateTime.from('-271821-04-19T00:00:00.000000001').toString(); const maxOk = Temporal.PlainDateTime.from('+275760-09-13').toString(); const minBad = (() => { try { Temporal.PlainDateTime.from('-271821-04-19T00:00'); return false } catch (e) { return e instanceof RangeError } })(); const maxBad = (() => { try { Temporal.PlainDateTime.from('+275760-09-14'); return false } catch (e) { return e instanceof RangeError } })(); const dateOffsetBad = (() => { try { Temporal.PlainDateTime.from('2022-09-15+00:00'); return false } catch (e) { return e instanceof RangeError } })(); const badCalendar = (() => { try { Temporal.PlainDateTime.from('1997-12-04[u-ca=notacal]'); return false } catch (e) { return e instanceof RangeError } })(); const duplicateCritical = (() => { try { Temporal.PlainDateTime.from('1970-01-01[u-ca=iso8601][!u-ca=iso8601]'); return false } catch (e) { return e instanceof RangeError } })(); [lower, space, offset, criticalZone, secondCalendar, minOk, maxOk, minBad, maxBad, dateOffsetBad, badCalendar, duplicateCritical].join('|')", ) .unwrap(); assert_eq!( str_val(&vm, r), concat!( "1976-11-18T15:23:00|1976-11-18T15:23:00|1976-11-18T15:23:00|", "1976-11-18T15:23:00|1976-11-18T15:23:00|-271821-04-19T00:00:00.000000001|", "+275760-09-13T00:00:00|true|true|true|true|true" ) ); } #[test] fn plain_date_time_to_string_honors_options() { let mut vm = Vm::new(); let r = eval( &mut vm, "(() => { const dt = new Temporal.PlainDateTime(2000, 5, 2, 12, 34, 56, 987, 650, 0); const minute = dt.toString({ smallestUnit: 'minute' }); const seconds = dt.toString({ smallestUnit: 'second' }); const micros = dt.toString({ smallestUnit: 'microsecond' }); const two = dt.toString({ fractionalSecondDigits: 2.5 }); const nine = dt.toString({ fractionalSecondDigits: 9.7 }); const midnight = new Temporal.PlainDateTime(1999, 12, 31, 23, 59, 59, 999, 999, 999) .toString({ fractionalSecondDigits: 8, roundingMode: 'ceil' }); const always = dt.toString({ calendarName: 'always' }); const critical = dt.toString({ calendarName: 'critical' }); const never = dt.toString({ calendarName: 'never' }); const badCalendar = (() => { try { dt.toString({ calendarName: 'ALWAYS' }); return false } catch (e) { return e instanceof RangeError } })(); const badUnit = (() => { try { dt.toString({ smallestUnit: 'hour' }); return false } catch (e) { return e instanceof RangeError } })(); const badDigits = (() => { try { dt.toString({ fractionalSecondDigits: -0.6 }); return false } catch (e) { return e instanceof RangeError } })(); const badOptions = (() => { try { dt.toString(null); return false } catch (e) { return e instanceof TypeError } })(); return [minute, seconds, micros, two, nine, midnight, always, critical, never, badCalendar, badUnit, badDigits, badOptions].join('|'); })()", ) .unwrap(); assert_eq!( str_val(&vm, r), concat!( "2000-05-02T12:34|2000-05-02T12:34:56|2000-05-02T12:34:56.987650|", "2000-05-02T12:34:56.98|2000-05-02T12:34:56.987650000|2000-01-01T00:00:00.00000000|", "2000-05-02T12:34:56.98765[u-ca=iso8601]|2000-05-02T12:34:56.98765[!u-ca=iso8601]|", "2000-05-02T12:34:56.98765|true|true|true|true" ) ); } #[test] fn plain_date_time_serializes_extended_years() { let mut vm = Vm::new(); let r = eval( &mut vm, "new Temporal.PlainDateTime(-1, 8, 7, 6, 54, 32, 100).toString() + ',' + new Temporal.PlainDateTime(10000, 6, 7, 8, 9, 10, 987).toJSON()", ) .unwrap(); assert_eq!(str_val(&vm, r), "-000001-08-07T06:54:32.1,+010000-06-07T08:09:10.987"); } #[test] fn zdt_subclass_compare_debug() { let mut vm = Vm::new(); let r = eval( &mut vm, "class AvoidGettersDateTime extends Temporal.PlainDateTime { get year() { throw new Error('getter'); } } const one = new AvoidGettersDateTime(2000, 5, 2, 12, 34, 56, 987, 654, 321); const two = new AvoidGettersDateTime(2006, 3, 25, 6, 54, 32, 123, 456, 789); typeof Temporal.PlainDateTime.compare(one, two) + ':' + Temporal.PlainDateTime.compare(one, two)", ) .unwrap(); assert_eq!(str_val(&vm, r), "number:-1"); } #[test] fn plain_date_time_add_balances_months_days_and_time() { let mut vm = Vm::new(); let r = eval( &mut vm, "const a = Temporal.PlainDateTime.from('1997-12-01T12:34'); const b = a.add(new Temporal.Duration(3, 6, 0, 17)); const c = a.add(new Temporal.Duration(0, 1, 0, 0, 36)); const d = a.subtract(new Temporal.Duration(0, 0, 0, 1)); const constrained = new Temporal.PlainDateTime(2020, 1, 31, 15, 0).add({ months: 1 }); const rejected = (() => { try { new Temporal.PlainDateTime(2020, 1, 31, 15, 0).add({ months: 1 }, { overflow: 'reject' }); return false; } catch (e) { return e instanceof RangeError; } })(); [b.year, b.month, b.day, b.hour, b.minute, b.second, c.day, c.hour, d.year, d.month, d.day, constrained.month, constrained.day, rejected].join(':')", ) .unwrap(); assert_eq!(str_val(&vm, r), "2001:6:18:12:34:0:3:0:1997:11:30:2:29:true"); } #[test] fn plain_date_constructor_stores_real_calendar() { let mut vm = Vm::new(); // 规范日历 ID 存槽:大小写折叠为规范小写。 let r = eval( &mut vm, "new Temporal.PlainDate(2000, 5, 2, 'gregory').calendarId + ',' + new Temporal.PlainDate(2000, 5, 2, 'iSo8601').calendarId + ',' + new Temporal.PlainDate(2000, 5, 2).calendarId", ) .unwrap(); assert_eq!(str_val(&vm, r), "gregory,iso8601,iso8601"); } #[test] fn plain_date_constructor_rejects_invalid_calendar() { let mut vm = Vm::new(); // 未在白名单 → RangeError;ISO 串拒绝;对象(非 Temporal 实例)→ TypeError。 let r = eval( &mut vm, "(() => { try { new Temporal.PlainDate(2000, 5, 2, 'notacal'); return false; } catch (e) { return e instanceof RangeError; } })() + ',' + (() => { try { new Temporal.PlainDate(2000, 5, 2, '1997-12-04[u-ca=iso8601]'); return false; } catch (e) { return e instanceof RangeError; } })() + ',' + (() => { try { new Temporal.PlainDate(2000, 5, 2, {}); return false; } catch (e) { return e instanceof TypeError; } })()", ) .unwrap(); assert_eq!(str_val(&vm, r), "true,true,true"); } #[test] fn plain_date_time_constructor_stores_real_calendar() { let mut vm = Vm::new(); let r = eval( &mut vm, "new Temporal.PlainDateTime(2000, 5, 2, 12, 0, 0, 0, 0, 0, 'hebrew').calendarId + ',' + new Temporal.PlainDateTime(2000, 5, 2).calendarId", ) .unwrap(); assert_eq!(str_val(&vm, r), "hebrew,iso8601"); } #[test] fn plain_date_time_constructor_wrong_calendar_is_type_error() { let mut vm = Vm::new(); // 规范要求 `{}` 日历 → TypeError(strict 走日历 ID 校验而非宽松 to-option-string)。 let r = eval( &mut vm, "(() => { try { new Temporal.PlainDateTime(2000, 5, 2, 12, 0, 0, 0, 0, 0, {}); return false; } catch (e) { return e instanceof TypeError; } })()", ) .unwrap(); assert!(r.as_bool()); } #[test] fn zoned_date_time_constructor_stores_real_calendar() { let mut vm = Vm::new(); let r = eval( &mut vm, "new Temporal.ZonedDateTime(0n, 'UTC', 'hebrew').calendarId + ',' + new Temporal.ZonedDateTime(0n, 'UTC').calendarId", ) .unwrap(); assert_eq!(str_val(&vm, r), "hebrew,iso8601"); } #[test] fn zoned_date_time_constructor_wrong_calendar_is_type_error() { let mut vm = Vm::new(); let r = eval( &mut vm, "(() => { try { new Temporal.ZonedDateTime(0n, 'UTC', {}); return false; } catch (e) { return e instanceof TypeError; } })()", ) .unwrap(); assert!(r.as_bool()); } #[test] fn zoned_date_time_epoch_getters() { let mut vm = Vm::new(); // 正负 epoch 的秒/毫秒/微秒除法:负值向下取整(floor)。 let r = eval( &mut vm, "const z = new Temporal.ZonedDateTime(217175010123456789n, 'UTC'); const n = new Temporal.ZonedDateTime(-217175010876543211n, 'UTC'); z.epochSeconds + ',' + z.epochMilliseconds + ',' + z.epochMicroseconds + ',' + n.epochSeconds + ',' + n.epochMilliseconds + ',' + n.epochMicroseconds", ) .unwrap(); assert_eq!( str_val(&vm, r), "217175010,217175010123,217175010123456,-217175011,-217175010877,-217175010876544" ); } #[test] fn zoned_date_time_calendar_getters() { let mut vm = Vm::new(); let r = eval( &mut vm, "const z = new Temporal.ZonedDateTime(0n, 'UTC'); z.year + ',' + z.month + ',' + z.day + ',' + z.hour + ',' + z.minute + ',' + z.second + ',' + z.millisecond + ',' + z.microsecond + ',' + z.nanosecond", ) .unwrap(); assert_eq!(str_val(&vm, r), "1970,1,1,0,0,0,0,0,0"); } #[test] fn zoned_date_time_offset_balance_negative_time_units() { let mut vm = Vm::new(); // 负偏移跨日:60_000_000_001n + "-00:02" → 本地 23:59:00.000000001。 let r = eval( &mut vm, "const z = new Temporal.ZonedDateTime(60_000_000_001n, '-00:02'); z.minute + ',' + z.second + ',' + z.millisecond + ',' + z.microsecond + ',' + z.nanosecond", ) .unwrap(); assert_eq!(str_val(&vm, r), "59,0,0,0,1"); } #[test] fn zoned_date_time_balance_negative_day() { let mut vm = Vm::new(); // 负偏移跨日到前一天:86_400_000_000_001n + "-00:02" → day 1(1970-01-01)/ 23:58。 let r = eval( &mut vm, "const z = new Temporal.ZonedDateTime(86_400_000_000_001n, '-00:02'); z.day + ',' + z.hour + ',' + z.minute", ) .unwrap(); assert_eq!(str_val(&vm, r), "1,23,58"); } #[test] fn zoned_date_time_negative_epoch_millisecond() { let mut vm = Vm::new(); // 负 epoch floor 语义(镜像 epochMilliseconds/basic.js)→ 本地时分使 ms 为 0。 let r = eval(&mut vm, "new Temporal.ZonedDateTime(-13_849_764_999_999_999n, 'UTC').millisecond === 0").unwrap(); assert!(r.as_bool()); } #[test] fn zoned_date_time_week_fields() { let mut vm = Vm::new(); let r = eval( &mut vm, "const a = new Temporal.ZonedDateTime(217178610123456789n, 'UTC'); const z = new Temporal.ZonedDateTime(0n, 'UTC'); const y = new Temporal.ZonedDateTime(-4n*864000000000000n, 'UTC'); a.dayOfWeek + ',' + a.dayOfYear + ',' + a.daysInMonth + ',' + a.inLeapYear + ',' + z.weekOfYear + ',' + z.yearOfWeek + ',' + y.yearOfWeek", ) .unwrap(); assert_eq!(str_val(&vm, r), "4,323,30,true,1,1970,1969"); } #[test] fn zoned_date_time_offset_and_offset_nanoseconds() { let mut vm = Vm::new(); let r = eval( &mut vm, "new Temporal.ZonedDateTime(0n, '+01:00').offset + ',' + new Temporal.ZonedDateTime(0n, 'UTC').offset + ',' + new Temporal.ZonedDateTime(0n, '-05:00').offset + ',' + new Temporal.ZonedDateTime(0n, '+01:00').offsetNanoseconds + ',' + new Temporal.ZonedDateTime(0n, '-05:00').offsetNanoseconds", ) .unwrap(); assert_eq!(str_val(&vm, r), "+01:00,+00:00,-05:00,3600000000000,-18000000000000"); } #[test] fn zoned_date_time_constants_and_month_code() { let mut vm = Vm::new(); let r = eval( &mut vm, "const z = new Temporal.ZonedDateTime(0n, 'UTC'); z.era + ',' + z.eraYear + ',' + z.monthCode + ',' + z.daysInWeek + ',' + z.monthsInYear", ) .unwrap(); assert_eq!(str_val(&vm, r), "undefined,undefined,M01,7,12"); } #[test] fn zoned_date_time_m0_plus_hh_offset_and_hours_in_day() { let mut vm = Vm::new(); // M0:+01 基本偏移可构造,且 offset 仍规范化、hoursInDay 恒 24。 let r = eval( &mut vm, "const z = new Temporal.ZonedDateTime(0n, '+01'); z.timeZoneId + ',' + z.offset + ',' + z.hoursInDay", ) .unwrap(); assert_eq!(str_val(&vm, r), "+01,+01:00,24"); } #[test] fn zoned_date_time_hours_in_day_out_of_range() { let mut vm = Vm::new(); // 今日/明日当地午夜越 Instant 界 → hoursInDay 抛 RangeError。 let r = eval( &mut vm, "(() => { try { new Temporal.ZonedDateTime(-864n*10n**19n, '-01').hoursInDay; return false; } catch (e) { return e instanceof RangeError; } })() && (() => { try { new Temporal.ZonedDateTime(864n*10n**19n, 'UTC').hoursInDay; return false; } catch (e) { return e instanceof RangeError; } })()", ) .unwrap(); assert!(r.as_bool()); } #[test] fn zoned_date_time_getters_branding_type_error() { let mut vm = Vm::new(); // 非 ZDT receiver 调 getter 一律抛 TypeError(branding)。 let r = eval( &mut vm, "(() => { try { Temporal.ZonedDateTime.prototype.year.call({}); return false; } catch (e) { return e instanceof TypeError; } })()", ) .unwrap(); assert!(r.as_bool()); } #[test] fn temporal_instance_as_calendar_fast_path_reads_slot() { let mut vm = Vm::new(); // Temporal 实例作 property bag 的 calendar:直接读内部槽,不触发 calendar 属性 getter。 let r = eval( &mut vm, "const pd = new Temporal.PlainDate(2000, 5, 2, 'gregory'); const pdt = new Temporal.PlainDateTime(2000, 5, 2, 12, 0, 0, 0, 0, 0, 'hebrew'); const zdt = new Temporal.ZonedDateTime(0n, 'UTC', 'japanese'); Object.defineProperty(pd, 'calendar', { get() { throw new Error('getter'); } }); Object.defineProperty(pdt, 'calendar', { get() { throw new Error('getter'); } }); Object.defineProperty(zdt, 'calendar', { get() { throw new Error('getter'); } }); const a = Temporal.PlainDate.from({ year: 2000, month: 5, day: 2, calendar: pd }).calendarId; const b = Temporal.PlainDateTime.from({ year: 2000, month: 5, day: 2, hour: 12, calendar: pdt }).calendarId; const c = Temporal.PlainDateTime.from({ year: 2000, month: 5, day: 2, hour: 12, calendar: zdt }).calendarId; a + ',' + b + ',' + c", ) .unwrap(); assert_eq!(str_val(&vm, r), "gregory,hebrew,japanese"); } #[test] fn plain_date_from_instance_preserves_calendar() { let mut vm = Vm::new(); // from(实例) 复制日历槽;add/subtract 与 toPlainDate 传播日历。 let r = eval( &mut vm, "const pd = new Temporal.PlainDate(2000, 5, 2, 'gregory'); const pdt = new Temporal.PlainDateTime(2000, 5, 2, 12, 0, 0, 0, 0, 0, 'hebrew'); Temporal.PlainDate.from(pd).calendarId + ',' + Temporal.PlainDateTime.from(pd).calendarId + ',' + Temporal.PlainDateTime.from(pdt).calendarId + ',' + pdt.toPlainDate().calendarId + ',' + pd.add({ days: 1 }).calendarId + ',' + pd.subtract({ days: 1 }).calendarId + ',' + pdt.add({ days: 1 }).calendarId", ) .unwrap(); assert_eq!(str_val(&vm, r), "gregory,gregory,hebrew,hebrew,gregory,gregory,hebrew"); } #[test] fn plain_date_brand_only_getters_require_plain_date_receiver() { let mut vm = Vm::new(); // 4 个品牌-only getter 对普通对象 receiver 必须抛 TypeError。 // 经 property descriptor 取真实 getter 函数(原型上的 `.get` 访问不可靠, // 会拿到 undefined),修复前只查 is_object 不查品牌 → 返回常量不抛,断言失败。 for getter in ["daysInWeek", "monthsInYear", "era", "eraYear"] { let r = eval( &mut vm, &format!( "var g = Object.getOwnPropertyDescriptor(Temporal.PlainDate.prototype, '{getter}').get; \ try {{ g.call({{}}); 'no-throw' }} catch (e) {{ e.constructor.name }}" ), ) .unwrap(); assert_eq!(str_val(&vm, r), "TypeError", "{getter} on plain object must throw"); } // 正常 PlainDate receiver 不回归。 let r = eval( &mut vm, "var p = new Temporal.PlainDate(2000, 5, 2); \ [p.daysInWeek, p.monthsInYear, p.era === undefined, p.eraYear === undefined].join(',')", ) .unwrap(); assert_eq!(str_val(&vm, r), "7,12,true,true"); } #[test] fn zoned_date_time_to_string_default() { let mut vm = Vm::new(); // 默认无参输出:offset 恒显示 + timeZoneName auto 显示,calendarName auto 省略。 let r = eval( &mut vm, "new Temporal.ZonedDateTime(0n, 'UTC').toString() + ',' + new Temporal.ZonedDateTime(0n, '+01:00').toString() + ',' + new Temporal.ZonedDateTime(0n, '-05:00').toString()", ) .unwrap(); assert_eq!( str_val(&vm, r), "1970-01-01T00:00:00+00:00[UTC],1970-01-01T01:00:00+01:00[+01:00],1969-12-31T19:00:00-05:00[-05:00]" ); } #[test] fn zoned_date_time_to_string_options() { let mut vm = Vm::new(); // 各 options 组合:offset 省略 / calendarName always / timeZoneName never / critical 前缀。 let r = eval( &mut vm, "const z = new Temporal.ZonedDateTime(0n, '+01:00'); z.toString({timeZoneName:'never'}) + '|' + z.toString({calendarName:'always'}) + '|' + z.toString({timeZoneName:'critical'}) + '|' + z.toString({calendarName:'critical'}) + '|' + z.toString({offset:'never'}) + '|' + z.toString({offset:'critical'}) + '|' + z.toString({timeZoneName:'never', calendarName:'never', offset:'never'})", ) .unwrap(); assert_eq!( str_val(&vm, r), "1970-01-01T01:00:00+01:00|1970-01-01T01:00:00+01:00[+01:00][u-ca=iso8601]|1970-01-01T01:00:00+01:00[!+01:00]|1970-01-01T01:00:00+01:00[+01:00][!u-ca=iso8601]|1970-01-01T01:00:00[+01:00]|1970-01-01T01:00:00!+01:00[+01:00]|1970-01-01T01:00:00" ); } #[test] fn zoned_date_time_to_string_epoch_rounding_cross_midnight() { let mut vm = Vm::new(); // 舍入在 epoch 域:2000-01-01 前 1ns + fractionalSecondDigits:8 + halfExpand → 跨午夜进位。 let r = eval( &mut vm, "new Temporal.ZonedDateTime(946_684_799_999_999_999n, 'UTC') .toString({fractionalSecondDigits:8, roundingMode:'halfExpand'})", ) .unwrap(); assert_eq!(str_val(&vm, r), "2000-01-01T00:00:00.00000000+00:00[UTC]"); } #[test] fn zoned_date_time_to_string_negative_epoch_rounding() { let mut vm = Vm::new(); // 负 epoch 舍入:halfCeil 上取整到毫秒。 let r = eval( &mut vm, "new Temporal.ZonedDateTime(-999999999999999990n, 'UTC') .toString({smallestUnit:'millisecond', roundingMode:'halfCeil'})", ) .unwrap(); assert_eq!(str_val(&vm, r), "1938-04-24T22:13:20.000+00:00[UTC]"); } #[test] fn zoned_date_time_to_string_smallest_unit() { let mut vm = Vm::new(); // smallestUnit minute 省略秒段。 let r = eval( &mut vm, "new Temporal.ZonedDateTime(3661_000_000_000n, 'UTC').toString({smallestUnit:'minute'})", ) .unwrap(); assert_eq!(str_val(&vm, r), "1970-01-01T01:01+00:00[UTC]"); } #[test] fn zoned_date_time_to_json_and_locale_and_value_of() { let mut vm = Vm::new(); // toJSON === toString();toLocaleString === toString();valueOf 抛 TypeError。 let r = eval( &mut vm, "const z = new Temporal.ZonedDateTime(0n, '+01:00'); z.toJSON() + '|' + z.toLocaleString() + '|' + (z.toJSON() === z.toString()) + '|' + (() => { try { z.valueOf(); return 'no-throw'; } catch (e) { return e instanceof TypeError; } })()", ) .unwrap(); assert_eq!( str_val(&vm, r), "1970-01-01T01:00:00+01:00[+01:00]|1970-01-01T01:00:00+01:00[+01:00]|true|true" ); } #[test] fn zoned_date_time_to_string_tag_and_branding() { let mut vm = Vm::new(); // @@toStringTag 数据属性 + 非 ZDT receiver 调 toString 抛 TypeError。 let r = eval( &mut vm, "Object.prototype.toString.call(new Temporal.ZonedDateTime(0n, 'UTC')) + '|' + (() => { try { Temporal.ZonedDateTime.prototype.toString.call({}); return 'no-throw'; } catch (e) { return e instanceof TypeError; } })()", ) .unwrap(); assert_eq!(str_val(&vm, r), "[object Temporal.ZonedDateTime]|true"); } #[test] fn zoned_date_time_to_string_options_validation() { let mut vm = Vm::new(); // 非法 options 值抛 RangeError;options 非对象抛 TypeError。 let r = eval( &mut vm, "const z = new Temporal.ZonedDateTime(0n, 'UTC'); (() => { try { z.toString({offset:'bad'}); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })() + '|' + (() => { try { z.toString({timeZoneName:'always'}); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })() + '|' + (() => { try { z.toString({calendarName:'bad'}); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })() + '|' + (() => { try { z.toString(42); return 'no-throw'; } catch (e) { return e instanceof TypeError; } })()", ) .unwrap(); assert_eq!(str_val(&vm, r), "true|true|true|true"); } #[test] fn zoned_date_time_with_time_zone_swaps_zone_keeps_instant() { let mut vm = Vm::new(); // 换时区后 instant 保留、返回新对象、timeZoneId 更新;大小写不敏感 + ±HH 均可用。 let r = eval( &mut vm, "const z = new Temporal.ZonedDateTime(0n, 'UTC'); const w = z.withTimeZone('+01:00'); w.timeZoneId + '|' + w.epochNanoseconds + '|' + (w !== z) + '|' + z.timeZoneId + '|' + new Temporal.ZonedDateTime(0n, 'uTc').withTimeZone('+01').timeZoneId", ) .unwrap(); assert_eq!(str_val(&vm, r), "+01:00|0|true|UTC|+01"); } #[test] fn zoned_date_time_with_time_zone_bad_receiver_or_zone() { let mut vm = Vm::new(); // 非 ZDT receiver 抛 TypeError;非法时区串抛 RangeError;非字符串抛 TypeError。 let r = eval( &mut vm, "( () => { try { Temporal.ZonedDateTime.prototype.withTimeZone.call({}, 'UTC'); return 'no-throw'; } catch (e) { return e instanceof TypeError; } })() + '|' + ( () => { try { new Temporal.ZonedDateTime(0n, 'UTC').withTimeZone('Not/AZone'); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })() + '|' + ( () => { try { new Temporal.ZonedDateTime(0n, 'UTC').withTimeZone(42); return 'no-throw'; } catch (e) { return e instanceof TypeError; } })()", ) .unwrap(); assert_eq!(str_val(&vm, r), "true|true|true"); } #[test] fn zoned_date_time_equals_compares_epoch_zone_calendar() { let mut vm = Vm::new(); // 同 instant/时区/日历 true;异 instant false;异时区 false;异日历 false。 let r = eval( &mut vm, "const a = new Temporal.ZonedDateTime(0n, 'UTC'); const b = new Temporal.ZonedDateTime(0n, 'UTC'); const c = new Temporal.ZonedDateTime(1n, 'UTC'); const d = new Temporal.ZonedDateTime(0n, '-05:00'); const e = new Temporal.ZonedDateTime(0n, 'UTC', 'japanese'); a.equals(b) + '|' + a.equals(c) + '|' + a.equals(d) + '|' + a.equals(e)", ) .unwrap(); assert_eq!(str_val(&vm, r), "true|false|false|false"); } #[test] fn zoned_date_time_equals_receiver_branding_and_non_zoned_arg() { let mut vm = Vm::new(); // 非 ZDT receiver 抛 TypeError;非 ZDT 对象参数返回 false。 let r = eval( &mut vm, "( () => { try { Temporal.ZonedDateTime.prototype.equals.call({}, new Temporal.ZonedDateTime(0n, 'UTC')); return 'no-throw'; } catch (e) { return e instanceof TypeError; } })() + '|' + new Temporal.ZonedDateTime(0n, 'UTC').equals({}) + '|' + new Temporal.ZonedDateTime(0n, 'UTC').equals('2021-01-01T00:00:00+00:00[UTC]')", ) .unwrap(); assert_eq!(str_val(&vm, r), "true|false|false"); } #[test] fn zoned_date_time_from_string_wall_and_exact_time() { let mut vm = Vm::new(); // 无偏移注解:墙钟按注解时区换算;带偏移与注解一致:精确时刻(墙钟 - 偏移)。 let r = eval( &mut vm, "Temporal.ZonedDateTime.from('1976-11-18T15:23:30[UTC]').toString() + '|' + Temporal.ZonedDateTime.from('1976-11-18T15:23:30[+01:00]').epochNanoseconds + '|' + Temporal.ZonedDateTime.from('1976-11-18T15:23:30+01:00[+01:00]').epochNanoseconds + '|' + Temporal.ZonedDateTime.from('1976-11-18T15:23:30+01:00[+01:00]').timeZoneId", ) .unwrap(); assert_eq!( str_val(&vm, r), "1976-11-18T15:23:30+00:00[UTC]|217175010000000000|217175010000000000|+01:00" ); } #[test] fn zoned_date_time_from_string_utc_designator_pins_exact_time() { let mut vm = Vm::new(); // Z 使 offsetBehaviour 为 exact:epoch 恒为墙钟时刻,注解时区只改 timeZoneId。 let r = eval( &mut vm, "Temporal.ZonedDateTime.from('1970-01-01T00:00Z[+01:00]').epochNanoseconds + '|' + Temporal.ZonedDateTime.from('1970-01-01T00:00Z[+01:00]').timeZoneId + '|' + Temporal.ZonedDateTime.from('1970-01-01T00:00Z[+01:00]').toString()", ) .unwrap(); assert_eq!(str_val(&vm, r), "0|+01:00|1970-01-01T01:00:00+01:00[+01:00]"); } #[test] fn zoned_date_time_from_string_requires_time_zone_annotation() { let mut vm = Vm::new(); // 无时区注解(裸日期时间 / Z / 纯偏移)均抛 RangeError。 let r = eval( &mut vm, "['1970-01-01T00:00', '1970-01-01T00:00Z', '1970-01-01T00:00+01:00'].every( s => { try { Temporal.ZonedDateTime.from(s); return false; } catch (e) { return e instanceof RangeError; } })", ) .unwrap(); assert!(r.as_bool()); } #[test] fn zoned_date_time_from_string_offset_options() { let mut vm = Vm::new(); // offset 选项覆盖 critical 标记:use 保留字符串偏移、ignore/prefer 保留墙钟。 let r = eval( &mut vm, "const s = '2022-10-07T18:37-07:00[!UTC]'; Temporal.ZonedDateTime.from(s, { offset: 'use' }).epochNanoseconds + '|' + Temporal.ZonedDateTime.from(s, { offset: 'ignore' }).epochNanoseconds + '|' + Temporal.ZonedDateTime.from(s, { offset: 'prefer' }).epochNanoseconds + '|' + (() => { try { Temporal.ZonedDateTime.from(s); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })()", ) .unwrap(); assert_eq!(str_val(&vm, r), "1665193020000000000|1665167820000000000|1665167820000000000|true"); } #[test] fn zoned_date_time_from_property_bag() { let mut vm = Vm::new(); // 纯日期 bag 与完整分量 bag 的墙钟换算与 toString 对拍。 let r = eval( &mut vm, "Temporal.ZonedDateTime.from({ year: 2000, month: 5, day: 2, timeZone: 'UTC' }).toString() + '|' + Temporal.ZonedDateTime.from({ year: 2000, month: 5, day: 2, hour: 12, minute: 34, second: 56, millisecond: 987, microsecond: 654, nanosecond: 321, timeZone: 'UTC' }).toString() + '|' + Temporal.ZonedDateTime.from({ year: 2000, month: 5, day: 2, hour: 12, timeZone: '-05:00' }).toString()", ) .unwrap(); assert_eq!( str_val(&vm, r), "2000-05-02T00:00:00+00:00[UTC]|2000-05-02T12:34:56.987654321+00:00[UTC]|2000-05-02T12:00:00-05:00[-05:00]" ); } #[test] fn zoned_date_time_from_property_bag_offset_conflict() { let mut vm = Vm::new(); // bag 内 offset 与 timeZone 不一致:默认与显式 reject 均抛 RangeError。 let r = eval( &mut vm, "const props = { year: 2021, month: 10, day: 28, offset: '-07:00', timeZone: '+01:00' }; (() => { try { Temporal.ZonedDateTime.from(props); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })() + '|' + (() => { try { Temporal.ZonedDateTime.from(props, { offset: 'reject' }); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })()", ) .unwrap(); assert_eq!(str_val(&vm, r), "true|true"); } #[test] fn zoned_date_time_from_zdt_object_copies_slots() { let mut vm = Vm::new(); // 复制 ZDT 对象:三槽相等且返回新对象;非法选项值校验仍执行。 let r = eval( &mut vm, "const z = new Temporal.ZonedDateTime(217175010123456789n, '+01:00', 'hebrew'); const c = Temporal.ZonedDateTime.from(z); (c !== z) + '|' + c.epochNanoseconds + '|' + c.timeZoneId + '|' + c.calendarId + '|' + (() => { try { Temporal.ZonedDateTime.from(z, { offset: 'bad' }); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })()", ) .unwrap(); assert_eq!(str_val(&vm, r), "true|217175010123456789|+01:00|hebrew|true"); } #[test] fn zoned_date_time_from_error_paths() { let mut vm = Vm::new(); // 缺 timeZone 的 bag / 非字符串原始值 → TypeError;越界日期 → RangeError。 let r = eval( &mut vm, "( () => { try { Temporal.ZonedDateTime.from({ year: 2000, month: 5, day: 2 }); return 'no-throw'; } catch (e) { return e instanceof TypeError; } })() + '|' + [123, null, undefined, 1n].every(v => { try { Temporal.ZonedDateTime.from(v); return false; } catch (e) { return e instanceof TypeError; } }) + '|' + (() => { try { Temporal.ZonedDateTime.from({ year: -271821, month: 4, day: 19, timeZone: 'UTC' }); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })()", ) .unwrap(); assert_eq!(str_val(&vm, r), "true|true|true"); } #[test] fn zoned_date_time_from_options_validation() { let mut vm = Vm::new(); // offset/disambiguation 非法 → RangeError;options 非对象 → TypeError。 let r = eval( &mut vm, "const s = '1970-01-01T00:00[UTC]'; (() => { try { Temporal.ZonedDateTime.from(s, { offset: 'garbage' }); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })() + '|' + (() => { try { Temporal.ZonedDateTime.from(s, { disambiguation: 'garbage' }); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })() + '|' + (() => { try { Temporal.ZonedDateTime.from(s, null); return 'no-throw'; } catch (e) { return e instanceof TypeError; } })()", ) .unwrap(); assert_eq!(str_val(&vm, r), "true|true|true"); } #[test] fn zoned_date_time_compare_orders_by_epoch() { let mut vm = Vm::new(); // 按 epoch 大小比较而非墙钟:epoch 大者胜,返回 -1/0/1。 let r = eval( &mut vm, "const a = new Temporal.ZonedDateTime(217175010123456789n, 'UTC'); const b = new Temporal.ZonedDateTime(217175010223456789n, 'UTC'); Temporal.ZonedDateTime.compare(a, a) + '|' + Temporal.ZonedDateTime.compare(a, b) + '|' + Temporal.ZonedDateTime.compare(b, a)", ) .unwrap(); assert_eq!(str_val(&vm, r), "0|-1|1"); } #[test] fn zoned_date_time_compare_ignores_time_zone_and_calendar() { let mut vm = Vm::new(); // 同 epoch 不同时区/日历 → 0(只按 epoch 比较)。 let r = eval( &mut vm, "const a = new Temporal.ZonedDateTime(0n, '+01:00', 'hebrew'); const b = new Temporal.ZonedDateTime(0n, 'UTC'); const c = new Temporal.ZonedDateTime(1n, '-05:00'); Temporal.ZonedDateTime.compare(a, b) + '|' + Temporal.ZonedDateTime.compare(b, c)", ) .unwrap(); assert_eq!(str_val(&vm, r), "0|-1"); } #[test] fn zoned_date_time_compare_accepts_string_and_bag() { let mut vm = Vm::new(); // 字符串 / property bag 参数走 from 解析器归一为 epoch 后比较。 let r = eval( &mut vm, "const zdt = new Temporal.ZonedDateTime(0n, 'UTC'); Temporal.ZonedDateTime.compare('1970-01-01T00:00Z[UTC]', zdt) + '|' + Temporal.ZonedDateTime.compare( { year: 1969, month: 12, day: 31, hour: 19, minute: 0, timeZone: '-05:00' }, zdt) + '|' + Temporal.ZonedDateTime.compare(zdt, '1970-01-01T00:00:01Z[UTC]')", ) .unwrap(); assert_eq!(str_val(&vm, r), "0|0|-1"); } #[test] fn zoned_date_time_compare_error_paths() { let mut vm = Vm::new(); // 非法字符串 / 缺 timeZone 的 bag / 非转换原始值均抛错(RangeError/TypeError)。 let r = eval( &mut vm, "( () => { try { Temporal.ZonedDateTime.compare('garbage', '1970-01-01T00:00Z[UTC]'); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })() + '|' + (() => { try { Temporal.ZonedDateTime.compare( { year: 2000, month: 1, day: 1 }, '1970-01-01T00:00Z[UTC]'); return 'no-throw'; } catch (e) { return e instanceof TypeError; } })() + '|' + (() => { try { Temporal.ZonedDateTime.compare(123, 456); return 'no-throw'; } catch (e) { return e instanceof TypeError; } })()", ) .unwrap(); assert_eq!(str_val(&vm, r), "true|true|true"); } #[test] fn zoned_date_time_until_defaults_to_hours() { let mut vm = Vm::new(); // 默认 largest = hour:epoch 差 217175010123456789n 分解为 60326h 23m 30.123456789s。 let r = eval( &mut vm, "const a = new Temporal.ZonedDateTime(0n, 'UTC'); const b = new Temporal.ZonedDateTime(217175010123456789n, 'UTC'); a.until(b).toString() + '|' + a.until(b).hours + '|' + a.until(b, { largestUnit: 'auto' }).toString()", ) .unwrap(); assert_eq!(str_val(&vm, r), "PT60326H23M30.123456789S|60326|PT60326H23M30.123456789S"); } #[test] fn zoned_date_time_until_since_are_negatives() { let mut vm = Vm::new(); // since 为 until 的精确取反(同 tz 下对称)。 let r = eval( &mut vm, "const a = new Temporal.ZonedDateTime(0n, '+01:00'); const b = new Temporal.ZonedDateTime(217175010123456789n, '+01:00'); b.until(a).toString() + '|' + a.since(b).toString() + '|' + b.since(a).toString()", ) .unwrap(); assert_eq!( str_val(&vm, r), "-PT60326H23M30.123456789S|-PT60326H23M30.123456789S|PT60326H23M30.123456789S" ); } #[test] fn zoned_date_time_until_same_epoch_blank() { let mut vm = Vm::new(); // 同 epoch 不同 tz:epoch 相等快速路径返回空时长。 let r = eval( &mut vm, "const a = new Temporal.ZonedDateTime(0n, 'UTC'); const b = new Temporal.ZonedDateTime(0n, '+01:00'); (a.until(b).toString() === 'PT0S') + '|' + a.until(b).toString()", ) .unwrap(); assert_eq!(str_val(&vm, r), "true|PT0S"); } #[test] fn zoned_date_time_until_largest_unit_days() { let mut vm = Vm::new(); // 显式 largestUnit:90 天 + 1 小时 1 秒按 days / years 分解。 let r = eval( &mut vm, "const a = new Temporal.ZonedDateTime(0n, 'UTC'); const b = new Temporal.ZonedDateTime(7779601000000000n, 'UTC'); a.until(b, { largestUnit: 'days' }).toString() + '|' + a.until(b, { largestUnit: 'years' }).toString()", ) .unwrap(); assert_eq!(str_val(&vm, r), "P90DT1H1S|P3MT1H1S"); } #[test] fn zoned_date_time_until_casts_argument() { let mut vm = Vm::new(); // bag 缺时分秒字段默认 0;字符串带注解;均按 +01:00 墙钟换算 epoch(对拍 casts-argument.js)。 let r = eval( &mut vm, "const zdt = Temporal.ZonedDateTime.from('1976-11-18T15:23:30.123456789+01:00[+01:00]'); zdt.until({ year: 2019, month: 10, day: 29, hour: 10, timeZone: '+01:00' }).toString() + '|' + zdt.until('2019-10-29T10:46:38.271986102+01:00[+01:00]').toString()", ) .unwrap(); assert_eq!(str_val(&vm, r), "PT376434H36M29.876543211S|PT376435H23M8.148529313S"); } #[test] fn zoned_date_time_until_string_annotation_wall_and_exact() { let mut vm = Vm::new(); // 注解无偏移 → 墙钟按注解时区;Z → exact;偏移与注解一致 → 精确时刻(对拍 zoneddatetime-string.js)。 let r = eval( &mut vm, "const instance = new Temporal.ZonedDateTime(0n, 'UTC'); instance.until('1970-01-01T00:00[+01:00]').toString() + '|' + instance.until('1970-01-01T00:00Z[+01:00]').toString() + '|' + instance.until('1970-01-01T00:00+01:00[+01:00]').toString() + '|' + (instance.until('1970-01-01T00:00[UTC]').toString() === 'PT0S')", ) .unwrap(); assert_eq!(str_val(&vm, r), "-PT1H|PT0S|-PT1H|true"); } #[test] fn zoned_date_time_until_rounding_options() { let mut vm = Vm::new(); // roundingMode/roundingIncrement/smallestUnit 生效;非法增量抛 RangeError。 let r = eval( &mut vm, "const a = new Temporal.ZonedDateTime(0n, 'UTC'); const b = new Temporal.ZonedDateTime(3601000000000n, 'UTC'); a.until(b, { smallestUnit: 'hours', roundingMode: 'ceil' }).toString() + '|' + a.until(b, { smallestUnit: 'hours', roundingMode: 'floor' }).toString() + '|' + a.until(b, { smallestUnit: 'hours', roundingIncrement: 2, roundingMode: 'halfExpand' }).toString() + '|' + (() => { try { a.until(b, { smallestUnit: 'hours', roundingIncrement: 24 }); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })() + '|' + (() => { try { a.until(b, { smallestUnit: 'hours', roundingIncrement: 11 }); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })()", ) .unwrap(); assert_eq!(str_val(&vm, r), "PT2H|PT1H|PT2H|true|true"); } #[test] fn zoned_date_time_until_error_paths() { let mut vm = Vm::new(); // 裸日期时间/纯偏移字符串 RangeError;空对象 TypeError;bag offset 冲突 RangeError。 let r = eval( &mut vm, "const instance = new Temporal.ZonedDateTime(0n, 'UTC'); ['1970-01-01T00:00', '1970-01-01T00:00Z', '1970-01-01T00:00+01:00', '-271821-04-19T23:00-01:00[-01:00]'] .every(s => { try { instance.until(s); return false; } catch (e) { return e instanceof RangeError; } }) + '|' + (() => { try { instance.until({}); return 'no-throw'; } catch (e) { return e instanceof TypeError; } })() + '|' + (() => { try { instance.until({ year: 2021, month: 10, day: 28, offset: '-07:00', timeZone: '+01:00' }); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })()", ) .unwrap(); assert_eq!(str_val(&vm, r), "true|true|true"); } #[test] fn zoned_date_time_until_string_limits() { let mut vm = Vm::new(); // 边界字符串:Instant 界内通过,墙钟日越界 / epoch 越界抛 RangeError(argument-string-limits.js)。 let r = eval( &mut vm, "const instance = new Temporal.ZonedDateTime(0n, 'UTC'); ['-271821-04-20T00:00Z[UTC]', '+275760-09-13T00:00Z[UTC]', '+275760-09-13T01:00+01:00[+01:00]'] .every(s => { try { instance.until(s); return true; } catch (e) { return false; } }) + '|' + ['-271821-04-19T23:59:59.999999999Z[UTC]', '+275760-09-14T00:00+23:59[+23:59]', '+275760-09-13T00:00:00.000000001Z[UTC]'] .every(s => { try { instance.until(s); return false; } catch (e) { return e instanceof RangeError; } })", ) .unwrap(); assert_eq!(str_val(&vm, r), "true|true"); } #[test] fn zoned_date_time_until_bag_default_time_zone() { let mut vm = Vm::new(); // bag 缺 timeZone → 按 receiver 时区解释墙钟(+01:00 下 02:00 本地 = 01:00 UTC);timeZone 非字符串 → TypeError。 let r = eval( &mut vm, "const instance = new Temporal.ZonedDateTime(0n, '+01:00'); instance.until({ year: 1970, month: 1, day: 1, hour: 2 }).toString() + '|' + (() => { try { instance.until({ year: 2021, month: 10, day: 28, timeZone: 42 }); return 'no-throw'; } catch (e) { return e instanceof TypeError; } })()", ) .unwrap(); assert_eq!(str_val(&vm, r), "PT1H|true"); } // -- ZonedDateTime.prototype.with / withCalendar / withPlainTime -- #[test] fn zoned_date_time_with_partial_merge() { let mut vm = Vm::new(); // 单字段覆盖与多字段合并:未给字段沿用 receiver 本地分量。 let r = eval( &mut vm, "const z = new Temporal.ZonedDateTime(0n, 'UTC'); z.with({ year: 2019 }).toString() + '|' + z.with({ hour: 12, minute: 34, nanosecond: 5 }).toString() + '|' + z.with({ month: 5, second: 15 }).toString()", ) .unwrap(); assert_eq!( str_val(&vm, r), "2019-01-01T00:00:00+00:00[UTC]|1970-01-01T12:34:00.000000005+00:00[UTC]|1970-05-01T00:00:15+00:00[UTC]" ); } #[test] fn zoned_date_time_with_undefined_fields_not_copied() { let mut vm = Vm::new(); // year: undefined 不覆盖,其余字段覆盖(copy-properties-not-undefined 语义)。 let r = eval( &mut vm, "const d1 = new Temporal.ZonedDateTime(1_000_000_000_000_000_789n, 'UTC'); const d2 = d1.with({ day: 1, hour: 10, year: undefined }); d2.year === 2001 && d2.month === 9 && d2.day === 1 && d2.hour === 10 && d2.minute === 46 && d2.second === 40 && d2.nanosecond === 789", ) .unwrap(); assert!(r.as_bool()); } #[test] fn zoned_date_time_with_rejects_calendar_time_zone_and_temporal_objects() { let mut vm = Vm::new(); // calendar/timeZone 字段、Temporal 实例参数、字符串参数均 TypeError。 let r = eval( &mut vm, "const z = new Temporal.ZonedDateTime(0n, 'UTC'); (() => { try { z.with({ month: 2, calendar: 'iso8601' }); return 'no-throw'; } catch (e) { return e instanceof TypeError; } })() + '|' + (() => { try { z.with({ month: 2, timeZone: 'UTC' }); return 'no-throw'; } catch (e) { return e instanceof TypeError; } })() + '|' + (() => { try { z.with(new Temporal.PlainDate(1976, 11, 18)); return 'no-throw'; } catch (e) { return e instanceof TypeError; } })() + '|' + (() => { try { z.with('1976-11-18T12:00+00:00[UTC]'); return 'no-throw'; } catch (e) { return e instanceof TypeError; } })() + '|' + (() => { try { z.with({}); return 'no-throw'; } catch (e) { return e instanceof TypeError; } })()", ) .unwrap(); assert_eq!(str_val(&vm, r), "true|true|true|true|true"); } #[test] fn zoned_date_time_with_offset_option() { let mut vm = Vm::new(); // offset 默认 prefer;use 用 bag 偏移;reject 冲突 RangeError;非法值各类型错误。 let r = eval( &mut vm, "const z = new Temporal.ZonedDateTime(0n, 'UTC'); const dt = new Temporal.ZonedDateTime(1572757201_000_000_000n, '-03:30'); (dt.with({ minute: 31 }).epochNanoseconds === 1572757261_000_000_000n) + '|' + (dt.with({ minute: 31 }, {}).epochNanoseconds === 1572757261_000_000_000n) + '|' + (z.with({ offset: '+01:00' }, { offset: 'use' }).epochNanoseconds === -3_600_000_000_000n) + '|' + (z.with({ offset: '+01:00' }, { offset: 'prefer' }).epochNanoseconds === 0n) + '|' + (() => { try { z.with({ offset: '+01:00' }, { offset: 'reject' }); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })() + '|' + (() => { try { z.with({ offset: 0 }); return 'no-throw'; } catch (e) { return e instanceof TypeError; } })() + '|' + (() => { try { z.with({ offset: '00:00' }); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })()", ) .unwrap(); assert_eq!(str_val(&vm, r), "true|true|true|true|true|true|true"); } #[test] fn zoned_date_time_with_overflow_constrain_and_reject() { let mut vm = Vm::new(); // constrain 钳制月/日/时/亚秒;reject 越界 RangeError;monthCode 冲突/闰月 RangeError。 let r = eval( &mut vm, "const z = new Temporal.ZonedDateTime(0n, 'UTC'); z.with({ month: 29 }).toString() + '|' + z.with({ hour: 29 }).toString() + '|' + z.with({ nanosecond: 9000 }).toString() + '|' + (() => { try { z.with({ month: 29 }, { overflow: 'reject' }); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })() + '|' + (() => { try { z.with({ month: 5, monthCode: 'M06' }); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })() + '|' + (() => { try { z.with({ monthCode: 'M08L' }); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })() + '|' + (z.with({ monthCode: 'M05' }).toString() === '1970-05-01T00:00:00+00:00[UTC]')", ) .unwrap(); assert_eq!( str_val(&vm, r), "1970-12-01T00:00:00+00:00[UTC]|1970-01-01T23:00:00+00:00[UTC]|1970-01-01T00:00:00.000000999+00:00[UTC]|true|true|true|true" ); } #[test] fn zoned_date_time_with_month_code_constrain_day() { let mut vm = Vm::new(); // monthCode 换月后日钳制:1 月 31 日 → 2 月 28 日(constrain);reject → RangeError。 let r = eval( &mut vm, "const z = Temporal.ZonedDateTime.from({ year: 2019, monthCode: 'M01', day: 31, hour: 12, minute: 34, timeZone: 'UTC' }); z.with({ monthCode: 'M02' }).toString() + '|' + (() => { try { z.with({ monthCode: 'M02' }, { overflow: 'reject' }); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })()", ) .unwrap(); assert_eq!(str_val(&vm, r), "2019-02-28T12:34:00+00:00[UTC]|true"); } #[test] fn zoned_date_time_with_range_errors_and_options_order() { let mut vm = Vm::new(); // 越界墙钟 / offset use 越界 → RangeError;字段先于 options 校验。 let r = eval( &mut vm, "( () => { try { new Temporal.ZonedDateTime(0n, 'UTC').with({ year: -271821, month: 4, day: 19, hour: 1 }); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })() + '|' + ( () => { try { new Temporal.ZonedDateTime(-864n * 10n**19n, 'UTC').with({ offset: '+01' }, { offset: 'use' }); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })() + '|' + ( () => { try { new Temporal.ZonedDateTime(0n, 'UTC').with({ day: 5 }, null); return 'no-throw'; } catch (e) { return e instanceof TypeError; } })() + '|' + ( () => { try { new Temporal.ZonedDateTime(0n, 'UTC').with({ day: -1 }, null); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })()", ) .unwrap(); assert_eq!(str_val(&vm, r), "true|true|true|true"); } #[test] fn zoned_date_time_with_calendar_swaps_calendar_slot() { let mut vm = Vm::new(); // 换日历槽:epoch/时区不变、返回新对象;大小写不敏感;ISO 串与时间串接受。 let r = eval( &mut vm, "const c = new Temporal.ZonedDateTime(0n, 'UTC', 'hebrew'); const w = c.withCalendar('iso8601'); (w !== c) + '|' + w.calendarId + '|' + w.epochNanoseconds + '|' + w.timeZoneId + '|' + c.withCalendar('iSo8601').calendarId + '|' + c.withCalendar('2020-01-01').calendarId + '|' + c.withCalendar('15:23').calendarId + '|' + c.withCalendar(new Temporal.ZonedDateTime(0n, 'UTC', 'japanese')).calendarId", ) .unwrap(); assert_eq!(str_val(&vm, r), "true|iso8601|0|UTC|iso8601|iso8601|iso8601|japanese"); } #[test] fn zoned_date_time_with_calendar_errors() { let mut vm = Vm::new(); // 缺参/undefined/非字符串非对象 → TypeError;非法串 → RangeError。 let r = eval( &mut vm, "const c = new Temporal.ZonedDateTime(0n, 'UTC'); (() => { try { c.withCalendar(); return 'no-throw'; } catch (e) { return e instanceof TypeError; } })() + '|' + (() => { try { c.withCalendar(undefined); return 'no-throw'; } catch (e) { return e instanceof TypeError; } })() + '|' + (() => { try { c.withCalendar(42); return 'no-throw'; } catch (e) { return e instanceof TypeError; } })() + '|' + (() => { try { c.withCalendar('notacal'); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })()", ) .unwrap(); assert_eq!(str_val(&vm, r), "true|true|true|true"); } #[test] fn zoned_date_time_with_plain_time_defaults_and_bag() { let mut vm = Vm::new(); // undefined → 午夜;bag 缺省字段为 0;second=60 按 59 钳制。 let r = eval( &mut vm, "const p = new Temporal.ZonedDateTime(957270896_987_654_321n, 'UTC'); p.withPlainTime().toString() + '|' + p.withPlainTime(undefined).toString() + '|' + p.withPlainTime({ minute: 30 }).toString() + '|' + p.withPlainTime({ hour: 23, minute: 59, second: 60 }).toString() + '|' + p.withPlainTime(new Temporal.PlainTime(11, 22)).hour", ) .unwrap(); assert_eq!( str_val(&vm, r), "2000-05-02T00:00:00+00:00[UTC]|2000-05-02T00:00:00+00:00[UTC]|2000-05-02T00:30:00+00:00[UTC]|2000-05-02T23:59:59+00:00[UTC]|11" ); } #[test] fn zoned_date_time_with_plain_time_strings() { let mut vm = Vm::new(); // 时间串 / 日期+时间 / 歧义串须 T 前缀 / Z 与纯日期拒绝 / offset 忽略。 let r = eval( &mut vm, "const p = new Temporal.ZonedDateTime(957270896_987_654_321n, 'UTC'); p.withPlainTime('12:34').toString() + '|' + p.withPlainTime('1976-11-18T15:23:30.123456789+00:00').toString() + '|' + p.withPlainTime('T2021-12').hour + '|' + (() => { try { p.withPlainTime('2021-12'); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })() + '|' + (() => { try { p.withPlainTime('09:00:00Z'); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })() + '|' + (() => { try { p.withPlainTime('2019-10-01'); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })() + '|' + (() => { try { p.withPlainTime({}); return 'no-throw'; } catch (e) { return e instanceof TypeError; } })()", ) .unwrap(); assert_eq!( str_val(&vm, r), "2000-05-02T12:34:00+00:00[UTC]|2000-05-02T15:23:30.123456789+00:00[UTC]|20|true|true|true|true" ); } #[test] fn zoned_date_time_with_plain_time_zoned_date_time_argument() { let mut vm = Vm::new(); // ZDT 参数用其自身时区取本地时间(负偏移平衡负时间单位);负 epoch 模运算正确。 let r = eval( &mut vm, "const dtz = new Temporal.ZonedDateTime(3661_001_001_001n, '-00:02'); const r1 = new Temporal.ZonedDateTime(86400_000_000_000n, 'UTC').withPlainTime(dtz); (r1.hour === 0 && r1.minute === 59) + '|' + (new Temporal.ZonedDateTime(0n, 'UTC') .withPlainTime(new Temporal.ZonedDateTime(-13849764_999_999_999n, 'UTC')) .epochNanoseconds === 60635_000_000_001n)", ) .unwrap(); assert_eq!(str_val(&vm, r), "true|true"); } #[test] fn zoned_date_time_with_plain_time_out_of_range() { let mut vm = Vm::new(); // 本地分量越界(±MAX 边界)→ RangeError(start-of-day 与 epoch 越界两条路径)。 let r = eval( &mut vm, "( () => { try { new Temporal.ZonedDateTime(-864n * 10n**19n, '-01').withPlainTime(); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })() + '|' + ( () => { try { new Temporal.ZonedDateTime(-864n * 10n**19n, '+01').withPlainTime('00:00'); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })() + '|' + ( () => { try { new Temporal.ZonedDateTime(864n * 10n**19n, 'UTC').withPlainTime('01:00'); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })()", ) .unwrap(); assert_eq!(str_val(&vm, r), "true|true|true"); } #[test] fn zoned_date_time_with_family_branding() { let mut vm = Vm::new(); // 非 ZDT receiver 调三个方法均 TypeError。 let r = eval( &mut vm, "( () => { try { Temporal.ZonedDateTime.prototype.with.call({}, { year: 2019 }); return 'no-throw'; } catch (e) { return e instanceof TypeError; } })() + '|' + ( () => { try { Temporal.ZonedDateTime.prototype.withCalendar.call({}, 'iso8601'); return 'no-throw'; } catch (e) { return e instanceof TypeError; } })() + '|' + ( () => { try { Temporal.ZonedDateTime.prototype.withPlainTime.call({}); return 'no-throw'; } catch (e) { return e instanceof TypeError; } })()", ) .unwrap(); assert_eq!(str_val(&vm, r), "true|true|true"); } #[test] fn zoned_date_time_add_duration_object() { let mut vm = Vm::new(); // add-duration.js 对拍:240h + 800ns 叠加到负 epoch。 let r = eval( &mut vm, "const z = new Temporal.ZonedDateTime(-560174321098766n, 'UTC'); z.add(new Temporal.Duration(0, 0, 0, 0, 240, 0, 0, 0, 0, 800)).epochNanoseconds === 303825678902034n", ) .unwrap(); assert!(r.as_bool()); } #[test] fn zoned_date_time_add_constrain_and_month_boundary() { let mut vm = Vm::new(); // 月末 + 1 月 constrain 钳制到 2 月 28;reject 抛 RangeError;月份/日叠加顺序一致。 let r = eval( &mut vm, "const z = new Temporal.ZonedDateTime(0n, 'UTC'); z.with({ day: 31 }).add({ months: 1 }).toString() + '|' + (() => { try { z.with({ day: 31 }).add({ months: 1 }, { overflow: 'reject' }); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })() + '|' + (z.add({ months: 1 }).add({ days: 1 }).epochNanoseconds === z.add({ months: 1, days: 1 }).epochNanoseconds)", ) .unwrap(); assert_eq!(str_val(&vm, r), "1970-02-28T00:00:00+00:00[UTC]|true|true"); } #[test] fn zoned_date_time_add_subtract_are_inverse() { let mut vm = Vm::new(); // 同一 duration 符号反转互逆;blank duration 返回等值对象。 let r = eval( &mut vm, "const z = new Temporal.ZonedDateTime(1_700_000_000_123_456_789n, '+05:30'); (z.add({ days: 5, hours: 3 }).subtract({ days: 5, hours: 3 }).epochNanoseconds === z.epochNanoseconds) + '|' + (z.subtract({ months: 1 }).add({ months: 1 }).epochNanoseconds === z.epochNanoseconds) + '|' + (z.add(new Temporal.Duration()).epochNanoseconds === z.epochNanoseconds) + '|' + (z.add('PT0S').toString() === z.toString())", ) .unwrap(); assert_eq!(str_val(&vm, r), "true|true|true|true"); } #[test] fn zoned_date_time_add_subtract_intermediate_check() { let mut vm = Vm::new(); // ±MAX instant 的 {days:∓1}:中间日期越 PlainDateTime 范围 → RangeError。 let r = eval( &mut vm, "const min = new Temporal.ZonedDateTime(-8640000000000000000000n, 'UTC'); const max = new Temporal.ZonedDateTime(8640000000000000000000n, 'UTC'); (() => { try { min.add({ days: -1 }); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })() + '|' + (() => { try { min.subtract({ days: 1 }); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })() + '|' + (() => { try { max.add({ days: 1 }); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })() + '|' + (() => { try { max.subtract({ days: -1 }); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })()", ) .unwrap(); assert_eq!(str_val(&vm, r), "true|true|true|true"); } #[test] fn zoned_date_time_add_slots_preserved() { let mut vm = Vm::new(); // 运算结果保留 receiver 时区/日历槽。 let r = eval( &mut vm, "const z = new Temporal.ZonedDateTime(0n, 'UTC', 'gregory'); const a = z.add({ years: 1 }); (a.timeZoneId === 'UTC') + '|' + (a.calendarId === 'gregory') + '|' + (a.toString() === '1971-01-01T00:00:00+00:00[UTC]') + '|' + (z.subtract({ years: 1 }).toString() === '1969-01-01T00:00:00+00:00[UTC]')", ) .unwrap(); assert_eq!(str_val(&vm, r), "true|true|true|true"); } #[test] fn zoned_date_time_add_string_and_time_fields() { let mut vm = Vm::new(); // duration 字符串与时间各字段(含跨天进位)合成正确。 let r = eval( &mut vm, "const z = new Temporal.ZonedDateTime(0n, 'UTC'); z.add('PT2H30M').toString() + '|' + z.add({ hours: 24, minutes: 30 }).toString() + '|' + z.add({ milliseconds: 1500, microseconds: 2, nanoseconds: 3 }).toString() + '|' + z.subtract('PT1H').toString()", ) .unwrap(); assert_eq!( str_val(&vm, r), "1970-01-01T02:30:00+00:00[UTC]|1970-01-02T00:30:00+00:00[UTC]|1970-01-01T00:00:01.500002003+00:00[UTC]|1969-12-31T23:00:00+00:00[UTC]" ); } #[test] fn zoned_date_time_round_hour_increment() { let mut vm = Vm::new(); // 217175010123456789n +01:00 本地 15:23:30.123456789,hour/4 舍到 16:00 // (rounding-increments.js 期望 epoch 217177200000000000n)。 let r = eval( &mut vm, "const z = new Temporal.ZonedDateTime(217175010123456789n, '+01:00'); z.round({ smallestUnit: 'hour', roundingIncrement: 4 }).toString() + '|' + z.round({ smallestUnit: 'hour', roundingIncrement: 4 }).epochNanoseconds", ) .unwrap(); assert_eq!(str_val(&vm, r), "1976-11-18T16:00:00+01:00[+01:00]|217177200000000000"); } #[test] fn zoned_date_time_round_day_path_cross_midnight() { let mut vm = Vm::new(); // day 双路径:23:59:59.999999999 本地舍到次日 00:00(对象与字符串简写同效)。 let r = eval( &mut vm, "const z = Temporal.ZonedDateTime.from('1976-11-18T23:59:59.999999999+01:00[+01:00]'); z.round({ smallestUnit: 'day' }).toString() + '|' + z.round('day').toString() + '|' + Temporal.ZonedDateTime.from('1976-11-18T11:59:59.999999999+01:00[+01:00]').round('day').toString()", ) .unwrap(); assert_eq!( str_val(&vm, r), "1976-11-19T00:00:00+01:00[+01:00]|1976-11-19T00:00:00+01:00[+01:00]|1976-11-18T00:00:00+01:00[+01:00]" ); } #[test] fn zoned_date_time_round_time_unit_epoch_values() { let mut vm = Vm::new(); // minute/15 与 nanosecond 舍入的 epoch 对拍(rounding-increments.js 合法表)。 let r = eval( &mut vm, "const z = new Temporal.ZonedDateTime(217175010123456789n, '+01:00'); z.round({ smallestUnit: 'minute', roundingIncrement: 15 }).epochNanoseconds + '|' + z.round({ smallestUnit: 'nanosecond' }).epochNanoseconds + '|' + z.round({ smallestUnit: 'hour', roundingIncrement: 12 }).epochNanoseconds", ) .unwrap(); assert_eq!(str_val(&vm, r), "217175400000000000|217175010123456789|217162800000000000"); } #[test] fn zoned_date_time_round_true_factor_increment_validation() { let mut vm = Vm::new(); // 真因子校验:increment==units_per_day(hour/24、minute/60)与 day>1 均 RangeError; // 合法因子放行(hour/12、minute/15、day/1)。 let r = eval( &mut vm, "const z = new Temporal.ZonedDateTime(0n, 'UTC'); const tryRound = (o) => { try { z.round(o); return 'ok'; } catch (e) { return e instanceof RangeError; } }; tryRound({ smallestUnit: 'hour', roundingIncrement: 24 }) + '|' + tryRound({ smallestUnit: 'minute', roundingIncrement: 60 }) + '|' + tryRound({ smallestUnit: 'day', roundingIncrement: 2 }) + '|' + tryRound({ smallestUnit: 'hour', roundingIncrement: 12 }) + '|' + tryRound({ smallestUnit: 'minute', roundingIncrement: 15 }) + '|' + tryRound({ smallestUnit: 'day' })", ) .unwrap(); assert_eq!(str_val(&vm, r), "true|true|true|ok|ok|ok"); } #[test] fn zoned_date_time_round_out_of_range_errors() { let mut vm = Vm::new(); // 越界三例:day 路径 startNs/endNs 越 MAX,else 路径进位越 MAX,均 RangeError。 let r = eval( &mut vm, "const tryR = (z, o) => { try { z.round(o); return 'ok'; } catch (e) { return e instanceof RangeError; } }; tryR(new Temporal.ZonedDateTime(-8640000000000000000000n, '-01:00'), { smallestUnit: 'days' }) + '|' + tryR(new Temporal.ZonedDateTime(8640000000000000000000n, 'UTC'), { smallestUnit: 'day' }) + '|' + tryR(new Temporal.ZonedDateTime(8640000000000000000000n, '+23:59'), { smallestUnit: 'minutes', roundingIncrement: 10, roundingMode: 'ceil' })", ) .unwrap(); assert_eq!(str_val(&vm, r), "true|true|true"); } #[test] fn zoned_date_time_round_branding_and_arg_validation() { let mut vm = Vm::new(); // branding TypeError、round() 无参 TypeError、对象无 smallestUnit RangeError、 // 非法单位串 RangeError、非法舍入模式 RangeError。 let r = eval( &mut vm, "const z = new Temporal.ZonedDateTime(0n, 'UTC'); (() => { try { Temporal.ZonedDateTime.prototype.round.call({}, { smallestUnit: 'day' }); return 'no-throw'; } catch (e) { return e instanceof TypeError; } })() + '|' + (() => { try { z.round(); return 'no-throw'; } catch (e) { return e instanceof TypeError; } })() + '|' + (() => { try { z.round({}); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })() + '|' + (() => { try { z.round({ smallestUnit: 'years' }); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })() + '|' + (() => { try { z.round({ smallestUnit: 'hour', roundingMode: 'bogus' }); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })()", ) .unwrap(); assert_eq!(str_val(&vm, r), "true|true|true|true|true"); } #[test] fn zoned_date_time_add_error_paths() { let mut vm = Vm::new(); // 缺参 TypeError、混合符号 duration RangeError、epoch 越界 RangeError、branding TypeError。 let r = eval( &mut vm, "const z = new Temporal.ZonedDateTime(0n, 'UTC'); (() => { try { z.add(); return 'no-throw'; } catch (e) { return e instanceof TypeError; } })() + '|' + (() => { try { z.add({ days: 1, hours: -1 }); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })() + '|' + (() => { try { new Temporal.ZonedDateTime(8640000000000000000000n, 'UTC').add({ hours: 1 }); return 'no-throw'; } catch (e) { return e instanceof RangeError; } })() + '|' + (() => { try { Temporal.ZonedDateTime.prototype.add.call({}, {}); return 'no-throw'; } catch (e) { return e instanceof TypeError; } })() + '|' + (() => { try { Temporal.ZonedDateTime.prototype.subtract.call({}, {}); return 'no-throw'; } catch (e) { return e instanceof TypeError; } })()", ) .unwrap(); assert_eq!(str_val(&vm, r), "true|true|true|true|true"); }