Skip to content

Commit a7fc946

Browse files
authored
Implement MonthCode, PartialDate, and Date::with (#89)
So the title basically says it all as far as implementation. That being said, there's a lot of interpretation going into this that I'd like feedback on if possible, and it could be the right direction or it could be the wrong direction (although I'm leaning the former over the latter). The main culprit is basically [PrepareTemporalFields](https://tc39.es/proposal-temporal/#sec-temporal-preparetemporalfields). Background for discussion/sanity check: Up until recently, I basically thought it would be an implementation detail on the engine/interpreter side, but the thing that always bugged me was the `requiredFields` parameter, being either a List or PARTIAL. We could probably do that to specification, but we might be providing something like `Some(Vec::default())` as an argument, and it basically just felt clunky. After the recent `TemporalFields` update, I went to implement the `TemporalFields` portion of the `toX` abstract ops in Boa and realized that PARTIAL is never called in the `toX` operations, and it's actually exclusively called in `with` methods. We already have a sort of precedence for partials with `PartialDuration`. There's some benefits to this: we can have a with method on the native rust side, ideally the complexity that exists in `PrepareTemporalFields` can be made a bit easier to reason about. Potential negatives: we might end up deviating from the specification as far as the order of when errors are thrown and observability (TBD...potentially a total non-issue) and this is probably opening up a can of worms around what would be the ideal API for a `PartialDate`, `PartialDateTime`, and `PartialTime`. That all being said, I think the benefits do most likely outweigh any negatives, and it would be really cool to have `with` method implementations. I'm just not entirely sure around the API. Also, there's an addition of a `MonthCode` enum to make `From<X> for TemporalFields` implementations easier.
1 parent af94bbc commit a7fc946

File tree

4 files changed

+325
-73
lines changed

4 files changed

+325
-73
lines changed

src/components/calendar.rs

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use crate::{
1010
duration::{DateDuration, TimeDuration},
1111
Date, DateTime, Duration, MonthDay, YearMonth,
1212
},
13-
fields::{TemporalFieldKey, TemporalFields},
13+
fields::{FieldMap, TemporalFields},
1414
iso::{IsoDate, IsoDateSlots},
1515
options::{ArithmeticOverflow, TemporalUnit},
1616
TemporalError, TemporalResult,
@@ -310,6 +310,10 @@ impl Calendar {
310310
let month_code = MonthCode(
311311
fields
312312
.month_code
313+
.map(|mc| {
314+
TinyAsciiStr::from_bytes(mc.as_str().as_bytes())
315+
.expect("MonthCode as_str is always valid.")
316+
})
313317
.ok_or(TemporalError::range().with_message("No MonthCode provided."))?,
314318
);
315319
// NOTE: This might preemptively throw as `ICU4X` does not support constraining.
@@ -373,6 +377,10 @@ impl Calendar {
373377
let month_code = MonthCode(
374378
fields
375379
.month_code
380+
.map(|mc| {
381+
TinyAsciiStr::from_bytes(mc.as_str().as_bytes())
382+
.expect("MonthCode as_str is always valid.")
383+
})
376384
.ok_or(TemporalError::range().with_message("No MonthCode provided."))?,
377385
);
378386

@@ -626,10 +634,22 @@ impl Calendar {
626634
}
627635

628636
/// Provides field keys to be ignored depending on the calendar.
629-
pub fn field_keys_to_ignore(
630-
&self,
631-
_keys: &[TemporalFieldKey],
632-
) -> TemporalResult<Vec<TemporalFieldKey>> {
637+
pub fn field_keys_to_ignore(&self, keys: FieldMap) -> TemporalResult<FieldMap> {
638+
let mut ignored_keys = FieldMap::empty();
639+
if self.is_iso() {
640+
// NOTE: It is okay for ignored keys to have duplicates?
641+
for key in keys.iter() {
642+
ignored_keys.set(key, true);
643+
if key == FieldMap::MONTH {
644+
ignored_keys.set(FieldMap::MONTH_CODE, true);
645+
} else if key == FieldMap::MONTH_CODE {
646+
ignored_keys.set(FieldMap::MONTH, true);
647+
}
648+
}
649+
650+
return Ok(ignored_keys);
651+
}
652+
633653
// TODO: Research and implement the appropriate KeysToIgnore for all `BuiltinCalendars.`
634654
Err(TemporalError::range().with_message("FieldKeysToIgnore is not yet implemented."))
635655
}
@@ -677,8 +697,9 @@ impl From<YearMonth> for Calendar {
677697

678698
#[cfg(test)]
679699
mod tests {
700+
use crate::{components::Date, iso::IsoDate, options::TemporalUnit};
680701

681-
use super::*;
702+
use super::Calendar;
682703

683704
#[test]
684705
fn date_until_largest_year() {
@@ -925,7 +946,7 @@ mod tests {
925946
),
926947
];
927948

928-
let calendar = Calendar::from_str("iso8601").unwrap();
949+
let calendar = Calendar::default();
929950

930951
for test in tests {
931952
let first = Date::new_unchecked(

src/components/date.rs

Lines changed: 133 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,49 @@ use std::str::FromStr;
2121

2222
use super::{
2323
duration::{normalized::NormalizedDurationRecord, TimeDuration},
24-
MonthDay, Time, YearMonth,
24+
MonthCode, MonthDay, Time, YearMonth,
2525
};
2626

27+
// TODO: PrepareTemporalFields expects a type error to be thrown when all partial fields are None/undefined.
28+
/// A partial Date that may or may not be complete.
29+
#[derive(Debug, Default, Clone, Copy)]
30+
pub struct PartialDate {
31+
pub(crate) year: Option<i32>,
32+
pub(crate) month: Option<i32>,
33+
pub(crate) month_code: Option<MonthCode>,
34+
pub(crate) day: Option<i32>,
35+
pub(crate) era: Option<TinyAsciiStr<16>>,
36+
pub(crate) era_year: Option<i32>,
37+
}
38+
39+
impl PartialDate {
40+
/// Create a new `PartialDate`
41+
pub fn new(
42+
year: Option<i32>,
43+
month: Option<i32>,
44+
month_code: Option<MonthCode>,
45+
day: Option<i32>,
46+
era: Option<TinyAsciiStr<16>>,
47+
era_year: Option<i32>,
48+
) -> TemporalResult<Self> {
49+
if !(day.is_some()
50+
&& (month.is_some() || month_code.is_some())
51+
&& (year.is_some() || (era.is_some() && era_year.is_some())))
52+
{
53+
return Err(TemporalError::r#type()
54+
.with_message("A partial date must have at least one defined field."));
55+
}
56+
Ok(Self {
57+
year,
58+
month,
59+
month_code,
60+
day,
61+
era,
62+
era_year,
63+
})
64+
}
65+
}
66+
2767
/// The native Rust implementation of `Temporal.PlainDate`.
2868
#[non_exhaustive]
2969
#[derive(Debug, Default, Clone, PartialEq, Eq)]
@@ -210,6 +250,28 @@ impl Date {
210250
Ok(Self::new_unchecked(iso, calendar))
211251
}
212252

253+
/// Creates a date time with values from a `PartialDate`.
254+
pub fn with(
255+
&self,
256+
partial: PartialDate,
257+
overflow: Option<ArithmeticOverflow>,
258+
) -> TemporalResult<Self> {
259+
// 6. Let fieldsResult be ? PrepareCalendarFieldsAndFieldNames(calendarRec, temporalDate, « "day", "month", "monthCode", "year" »).
260+
let fields = TemporalFields::from(self);
261+
// 7. Let partialDate be ? PrepareTemporalFields(temporalDateLike, fieldsResult.[[FieldNames]], partial).
262+
let partial_fields = TemporalFields::from(partial);
263+
264+
// 8. Let fields be ? CalendarMergeFields(calendarRec, fieldsResult.[[Fields]], partialDate).
265+
let mut merge_result = fields.merge_fields(&partial_fields, self.calendar())?;
266+
267+
// 9. Set fields to ? PrepareTemporalFields(fields, fieldsResult.[[FieldNames]], «»).
268+
// 10. Return ? CalendarDateFromFields(calendarRec, fields, resolvedOptions).
269+
self.calendar.date_from_fields(
270+
&mut merge_result,
271+
overflow.unwrap_or(ArithmeticOverflow::Constrain),
272+
)
273+
}
274+
213275
/// Creates a new `Date` from the current `Date` and the provided calendar.
214276
pub fn with_calendar(&self, calendar: Calendar) -> TemporalResult<Self> {
215277
Self::new(
@@ -396,15 +458,15 @@ impl Date {
396458
/// Converts the current `Date<C>` into a `YearMonth<C>`
397459
#[inline]
398460
pub fn to_year_month(&self) -> TemporalResult<YearMonth> {
399-
let mut fields: TemporalFields = self.iso_date().into();
461+
let mut fields: TemporalFields = self.into();
400462
self.get_calendar()
401463
.year_month_from_fields(&mut fields, ArithmeticOverflow::Constrain)
402464
}
403465

404466
/// Converts the current `Date<C>` into a `MonthDay<C>`
405467
#[inline]
406468
pub fn to_month_day(&self) -> TemporalResult<MonthDay> {
407-
let mut fields: TemporalFields = self.iso_date().into();
469+
let mut fields: TemporalFields = self.into();
408470
self.get_calendar()
409471
.month_day_from_fields(&mut fields, ArithmeticOverflow::Constrain)
410472
}
@@ -577,6 +639,74 @@ mod tests {
577639
assert_eq!(result.days(), 9719.0,);
578640
}
579641

642+
#[test]
643+
fn basic_date_with() {
644+
let base = Date::new(
645+
1976,
646+
11,
647+
18,
648+
Calendar::default(),
649+
ArithmeticOverflow::Constrain,
650+
)
651+
.unwrap();
652+
653+
// Year
654+
let partial = PartialDate {
655+
year: Some(2019),
656+
..Default::default()
657+
};
658+
let with_year = base.with(partial, None).unwrap();
659+
assert_eq!(with_year.year().unwrap(), 2019);
660+
assert_eq!(with_year.month().unwrap(), 11);
661+
assert_eq!(
662+
with_year.month_code().unwrap(),
663+
TinyAsciiStr::<4>::from_str("M11").unwrap()
664+
);
665+
assert_eq!(with_year.day().unwrap(), 18);
666+
667+
// Month
668+
let partial = PartialDate {
669+
month: Some(5),
670+
..Default::default()
671+
};
672+
let with_month = base.with(partial, None).unwrap();
673+
assert_eq!(with_month.year().unwrap(), 1976);
674+
assert_eq!(with_month.month().unwrap(), 5);
675+
assert_eq!(
676+
with_month.month_code().unwrap(),
677+
TinyAsciiStr::<4>::from_str("M05").unwrap()
678+
);
679+
assert_eq!(with_month.day().unwrap(), 18);
680+
681+
// Month Code
682+
let partial = PartialDate {
683+
month_code: Some(MonthCode::Five),
684+
..Default::default()
685+
};
686+
let with_mc = base.with(partial, None).unwrap();
687+
assert_eq!(with_mc.year().unwrap(), 1976);
688+
assert_eq!(with_mc.month().unwrap(), 5);
689+
assert_eq!(
690+
with_mc.month_code().unwrap(),
691+
TinyAsciiStr::<4>::from_str("M05").unwrap()
692+
);
693+
assert_eq!(with_mc.day().unwrap(), 18);
694+
695+
// Day
696+
let partial = PartialDate {
697+
day: Some(17),
698+
..Default::default()
699+
};
700+
let with_day = base.with(partial, None).unwrap();
701+
assert_eq!(with_day.year().unwrap(), 1976);
702+
assert_eq!(with_day.month().unwrap(), 11);
703+
assert_eq!(
704+
with_day.month_code().unwrap(),
705+
TinyAsciiStr::<4>::from_str("M11").unwrap()
706+
);
707+
assert_eq!(with_day.day().unwrap(), 17);
708+
}
709+
580710
// test262/test/built-ins/Temporal/Calendar/prototype/month/argument-string-invalid.js
581711
#[test]
582712
fn invalid_strings() {

src/components/mod.rs

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ mod time;
2828
mod year_month;
2929
mod zoneddatetime;
3030

31+
use std::str::FromStr;
32+
3133
#[doc(inline)]
32-
pub use date::Date;
34+
pub use date::{Date, PartialDate};
3335
#[doc(inline)]
3436
pub use datetime::DateTime;
3537
#[doc(inline)]
@@ -45,3 +47,91 @@ pub use year_month::YearMonth;
4547
pub use year_month::YearMonthFields;
4648
#[doc(inline)]
4749
pub use zoneddatetime::ZonedDateTime;
50+
51+
use crate::TemporalError;
52+
53+
// TODO: Update to account for https://tc39.es/proposal-intl-era-monthcode/
54+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
55+
#[repr(u8)]
56+
pub enum MonthCode {
57+
One = 1,
58+
Two,
59+
Three,
60+
Four,
61+
Five,
62+
Six,
63+
Seven,
64+
Eight,
65+
Nine,
66+
Ten,
67+
Eleven,
68+
Twelve,
69+
Thirteen,
70+
}
71+
72+
impl MonthCode {
73+
pub fn as_str(&self) -> &str {
74+
match self {
75+
Self::One => "M01",
76+
Self::Two => "M02",
77+
Self::Three => "M03",
78+
Self::Four => "M04",
79+
Self::Five => "M05",
80+
Self::Six => "M06",
81+
Self::Seven => "M07",
82+
Self::Eight => "M08",
83+
Self::Nine => "M09",
84+
Self::Ten => "M10",
85+
Self::Eleven => "M11",
86+
Self::Twelve => "M12",
87+
Self::Thirteen => "M13",
88+
}
89+
}
90+
}
91+
92+
impl FromStr for MonthCode {
93+
type Err = TemporalError;
94+
fn from_str(s: &str) -> Result<Self, Self::Err> {
95+
match s {
96+
"M01" => Ok(Self::One),
97+
"M02" => Ok(Self::Two),
98+
"M03" => Ok(Self::Three),
99+
"M04" => Ok(Self::Four),
100+
"M05" => Ok(Self::Five),
101+
"M06" => Ok(Self::Six),
102+
"M07" => Ok(Self::Seven),
103+
"M08" => Ok(Self::Eight),
104+
"M09" => Ok(Self::Nine),
105+
"M10" => Ok(Self::Ten),
106+
"M11" => Ok(Self::Eleven),
107+
"M12" => Ok(Self::Twelve),
108+
"M13" => Ok(Self::Thirteen),
109+
_ => {
110+
Err(TemporalError::range()
111+
.with_message("monthCode is not within the valid values."))
112+
}
113+
}
114+
}
115+
}
116+
117+
impl TryFrom<u8> for MonthCode {
118+
type Error = TemporalError;
119+
fn try_from(value: u8) -> Result<Self, Self::Error> {
120+
match value {
121+
1 => Ok(Self::One),
122+
2 => Ok(Self::Two),
123+
3 => Ok(Self::Three),
124+
4 => Ok(Self::Four),
125+
5 => Ok(Self::Five),
126+
6 => Ok(Self::Six),
127+
7 => Ok(Self::Seven),
128+
8 => Ok(Self::Eight),
129+
9 => Ok(Self::Nine),
130+
10 => Ok(Self::Ten),
131+
11 => Ok(Self::Eleven),
132+
12 => Ok(Self::Twelve),
133+
13 => Ok(Self::Thirteen),
134+
_ => Err(TemporalError::range().with_message("Invalid MonthCode value.")),
135+
}
136+
}
137+
}

0 commit comments

Comments
 (0)