Skip to content

Commit e069247

Browse files
committed
Allow constructing non-ISO monthdays
1 parent 2a04310 commit e069247

5 files changed

Lines changed: 219 additions & 22 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/builtins/core/calendar.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -241,9 +241,24 @@ impl Calendar {
241241
);
242242
}
243243

244-
// TODO: This may get complicated...
245-
// For reference: https://github.com/tc39/proposal-temporal/blob/main/polyfill/lib/calendar.mjs#L1275.
246-
Err(TemporalError::range().with_message("Not yet implemented/supported."))
244+
// We trust ResolvedCalendarFields to have calculated an appropriate reference year for us
245+
let calendar_date = self
246+
.0
247+
.from_codes(
248+
resolved_fields.era_year.era.as_ref().map(|e| e.0.as_str()),
249+
resolved_fields.era_year.year,
250+
IcuMonthCode(resolved_fields.month_code.0),
251+
resolved_fields.day,
252+
)
253+
.map_err(TemporalError::from_icu4x)?;
254+
let iso = self.0.to_iso(&calendar_date);
255+
PlainMonthDay::new_with_overflow(
256+
Iso.month(&iso).ordinal,
257+
Iso.day_of_month(&iso).0,
258+
self.clone(),
259+
overflow,
260+
Some(Iso.extended_year(&iso)),
261+
)
247262
}
248263

249264
/// `CalendarPlainYearMonthFromFields`

src/builtins/core/calendar/types.rs

Lines changed: 104 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,24 @@ impl EraYear {
135135
resolution_type: ResolutionType,
136136
) -> TemporalResult<Self> {
137137
match (partial.year, partial.era, partial.era_year) {
138+
_ if resolution_type == ResolutionType::MonthDay => {
139+
let day = partial
140+
.day
141+
.ok_or(TemporalError::assert().with_message("MonthDay must specify day"))?;
142+
let arithmetic_year = Self::reference_arithmetic_year_for_month_day(
143+
&partial.calendar,
144+
partial.month_code,
145+
partial.month,
146+
day,
147+
)?;
148+
Ok(Self {
149+
// We should just specify these as arithmetic years, no need
150+
// to muck with eras
151+
era: None,
152+
arithmetic_year,
153+
year: arithmetic_year,
154+
})
155+
}
138156
(maybe_year, Some(era), Some(era_year)) => {
139157
let Some(era_info) = partial.calendar.get_era_info(&era) else {
140158
return Err(TemporalError::range().with_message("Invalid era provided."));
@@ -169,15 +187,96 @@ impl EraYear {
169187
year,
170188
arithmetic_year: year,
171189
}),
172-
(None, None, None) if resolution_type == ResolutionType::MonthDay => Ok(Self {
173-
era: None,
174-
year: 1972,
175-
arithmetic_year: 1972,
176-
}),
177190
_ => Err(TemporalError::r#type()
178191
.with_message("Required fields missing to determine an era and year.")),
179192
}
180193
}
194+
195+
fn reference_arithmetic_year_for_month_day(
196+
calendar: &Calendar,
197+
month_code: Option<MonthCode>,
198+
ordinal_month: Option<u8>,
199+
day: u8,
200+
) -> TemporalResult<i32> {
201+
let ordinal_month_in_solar = || {
202+
ordinal_month
203+
.or_else(|| month_code.map(|c| MonthCode::to_month_integer(&c)))
204+
.ok_or(TemporalError::assert().with_message("Neither month nor monthCode provided"))
205+
};
206+
// The reference date is the latest ISO 8601 date corresponding to the calendar date, that is also earlier than
207+
// or equal to the ISO 8601 date December 31, 1972. If that calendar date never occurs on or before the ISO 8601 date December 31, 1972,
208+
// then the reference date is the earliest ISO 8601 date corresponding to that calendar date.
209+
// The reference year is almost always 1972 (the first ISO 8601 leap year after the epoch), with exceptions
210+
// for calendars where some dates (e.g. leap days or days in leap months) didn't occur during that ISO 8601 year.
211+
// For example, Hebrew calendar leap month Adar I occurred in calendar years 5730 and 5733 (respectively overlapping
212+
// ISO 8601 February/March 1970 and February/March 1973), but did not occur between them, so the reference year for days of that month is 1970.
213+
214+
Ok(match calendar.kind() {
215+
AnyCalendarKind::Iso | AnyCalendarKind::Gregorian => 1972,
216+
// These calendars just wrap Gregorian with a different epoch
217+
AnyCalendarKind::Buddhist => 1972 + 543,
218+
AnyCalendarKind::Roc => 1972 - 1911,
219+
220+
AnyCalendarKind::Indian => {
221+
let month = ordinal_month_in_solar()?;
222+
// 1972-12-31 is y=1894 saka, m=10, d=10
223+
// 1894 happens to be an Indian leap year (which happens at m=1 d=30)
224+
if (month == 10 && day > 10) || month > 10 {
225+
// 1894 will produce dates after 1972. Return 1893 instead
226+
1893
227+
} else {
228+
1894
229+
}
230+
}
231+
AnyCalendarKind::Persian => {
232+
let month = ordinal_month_in_solar()?;
233+
// 1972-12-31 is y=1351 ap, m=10, d=10
234+
// 1350 happens to be a Persian leap year (which happens at m=12 d=30)
235+
if (month == 10 && day > 10) || month > 10 {
236+
// 1351 will produce dates after 1972. Return 1350 instead
237+
// This also handles the leap year
238+
1350
239+
} else {
240+
1351
241+
}
242+
}
243+
AnyCalendarKind::Hebrew => {
244+
let Some(month_code) = month_code else {
245+
return Err(TemporalError::r#type().with_message(
246+
"Must specify month codes with MonthDay for lunar calendars",
247+
));
248+
};
249+
250+
// 1972-12-31 is y=5733 am, m=4, d=26. We must produce year 5723 or lower
251+
if month_code.is_leap_month() {
252+
// 5730 is a leap year
253+
5730
254+
} else {
255+
let month = month_code.to_month_integer();
256+
if (month == 4 && day == 26) || month > 4 {
257+
// 5733 will produce dates after 1972, return 5722 instead
258+
5732
259+
} else {
260+
// All months have 29 days
261+
if day <= 29 {
262+
5733
263+
// Ḥeshvan/Kislev only have 30 days sometimes
264+
// Fortunately 5732 has 30 days for both
265+
} else if month == 2 || month == 3 {
266+
5732
267+
} else {
268+
// Some other month, we don't actually need to check
269+
5733
270+
}
271+
}
272+
}
273+
}
274+
_ => {
275+
return Err(TemporalError::range()
276+
.with_message("Do not currently support MonthDay with this calendar"))
277+
}
278+
})
279+
}
181280
}
182281

183282
// MonthCode constants.

src/builtins/core/date.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,18 @@ impl PartialDate {
7070
crate::impl_with_fallback_method!(with_fallback_date, (with_day: day) PlainDate);
7171
crate::impl_with_fallback_method!(with_fallback_datetime, (with_day:day) PlainDateTime);
7272
crate::impl_field_keys_to_ignore!((with_day:day));
73+
74+
pub(crate) fn try_from_month_day(month_day: &PlainMonthDay) -> TemporalResult<Self> {
75+
Ok(Self {
76+
year: None,
77+
month: None,
78+
month_code: Some(month_day.month_code()),
79+
era: None,
80+
era_year: None,
81+
day: Some(month_day.day()),
82+
calendar: month_day.calendar().clone(),
83+
})
84+
}
7385
}
7486

7587
/// The return value of CalendarFieldKeysToIgnore

src/builtins/core/month_day.rs

Lines changed: 83 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -220,20 +220,13 @@ impl PlainMonthDay {
220220
iso.check_validity()?;
221221

222222
// 13. Set result to ISODateToFields(calendar, isoDate, month-day).
223+
224+
let intermediate = Self::new_unchecked(iso, Calendar::new(parsed.calendar));
225+
let partial = PartialDate::try_from_month_day(&intermediate)?;
223226
// 14. NOTE: The following operation is called with constrain regardless of the value of overflow, in
224227
// order for the calendar to store a canonical value in the [[Year]] field of the [[ISODate]] internal slot of the result.
225228
// 15. Set isoDate to ? CalendarMonthDayFromFields(calendar, result, constrain).
226-
227-
// TODO(Manishearth) this must tweak the year to something valid
228-
// https://github.com/boa-dev/temporal/issues/450
229-
// https://github.com/tc39/proposal-intl-era-monthcode/issues/60
230-
Self::new_with_overflow(
231-
parsed.record.month,
232-
parsed.record.day,
233-
calendar,
234-
ArithmeticOverflow::Reject,
235-
Some(parsed.record.year),
236-
)
229+
PlainMonthDay::from_partial(partial, Some(ArithmeticOverflow::Constrain))
237230
}
238231

239232
/// Create a `PlainYearMonth` from a `PartialDate`
@@ -334,6 +327,12 @@ impl PlainMonthDay {
334327
self.calendar.day(&self.iso)
335328
}
336329

330+
/// Returns the internal reference year used by this MonthDay.
331+
#[inline]
332+
pub fn reference_year(&self) -> i32 {
333+
self.calendar.year(&self.iso)
334+
}
335+
337336
/// Create a [`PlainDate`] from the current `PlainMonthDay`.
338337
pub fn to_plain_date(&self, year: Option<PartialDate>) -> TemporalResult<PlainDate> {
339338
let year_partial = match &year {
@@ -409,7 +408,6 @@ impl FromStr for PlainMonthDay {
409408
mod tests {
410409
use super::*;
411410
use crate::builtins::core::PartialDate;
412-
use crate::Calendar;
413411
use tinystr::tinystr;
414412

415413
#[test]
@@ -560,4 +558,77 @@ mod tests {
560558
assert!(PlainMonthDay::from_utf8(test.as_bytes()).is_err());
561559
}
562560
}
561+
562+
#[test]
563+
fn test_reference_year() {
564+
// monthCode, day, ISO string, expectedReferenceYear
565+
// Some of these parsed strings are deliberately not using the reference year
566+
// so that we can test all code paths. By default, when adding new strings, it is easier
567+
// to just add the reference year
568+
const TESTS: &[(&str, u8, &str, i32)] = &[
569+
("M10", 30, "1868-10-30[u-ca=gregory]", 1972),
570+
("M08", 8, "1868-10-30[u-ca=indian]", 1894),
571+
("M09", 21, "2000-12-12[u-ca=indian]", 1894),
572+
// Dates in the earlier half of the year get pushed back a year
573+
("M10", 22, "2000-01-12[u-ca=indian]", 1893),
574+
("M01", 11, "2000-03-30[u-ca=persian]", 1351),
575+
("M09", 22, "2000-12-12[u-ca=persian]", 1351),
576+
("M12", 29, "2025-03-19[u-ca=persian]", 1350),
577+
// Leap day
578+
("M12", 30, "2025-03-20[u-ca=persian]", 1350),
579+
("M01", 1, "2025-03-21[u-ca=persian]", 1351),
580+
("M01", 1, "2025-03-21[u-ca=persian]", 1351),
581+
("M01", 1, "1972-01-01[u-ca=roc]", 61),
582+
("M02", 29, "2024-02-29[u-ca=roc]", 61),
583+
("M12", 1, "1972-12-01[u-ca=roc]", 61),
584+
("M01", 1, "1972-09-09[u-ca=hebrew]", 5733),
585+
("M02", 29, "1972-11-06[u-ca=hebrew]", 5733),
586+
("M03", 29, "1972-12-05[u-ca=hebrew]", 5733),
587+
("M03", 30, "1971-12-18[u-ca=hebrew]", 5732),
588+
("M05L", 29, "1970-03-07[u-ca=hebrew]", 5730),
589+
("M07", 1, "1972-03-16[u-ca=hebrew]", 5732),
590+
];
591+
let reference_iso = IsoDate::new_unchecked(1972, 12, 31);
592+
for (month_code, day, string, year) in TESTS {
593+
let md = PlainMonthDay::from_str(string).expect(string);
594+
595+
let partial = PartialDate {
596+
month_code: Some(month_code.parse().unwrap()),
597+
day: Some(*day),
598+
calendar: md.calendar.clone(),
599+
..Default::default()
600+
};
601+
602+
let md_from_partial =
603+
PlainMonthDay::from_partial(partial, Some(ArithmeticOverflow::Reject)).unwrap();
604+
605+
assert_eq!(
606+
md,
607+
md_from_partial,
608+
"Parsed {string}, compared with {}: Expected {}-{}, Found {}-{}",
609+
md_from_partial.to_ixdtf_string(Default::default()),
610+
md_from_partial.month_code().0,
611+
md_from_partial.day(),
612+
md.month_code().0,
613+
md.day()
614+
);
615+
616+
assert_eq!(
617+
md_from_partial.reference_year(),
618+
*year,
619+
"Reference year for {string} ({}-{}) must be {year}",
620+
month_code,
621+
day
622+
);
623+
624+
assert!(
625+
md.iso <= reference_iso && md_from_partial.iso <= reference_iso,
626+
"Reference ISO for {string} ({}-{}) must be before 1972-12-31, found, {:?} / {:?}",
627+
md.month_code().0,
628+
md.day(),
629+
md.iso,
630+
md_from_partial.iso
631+
);
632+
}
633+
}
563634
}

0 commit comments

Comments
 (0)