Skip to content

Commit

Permalink
Implement MonthCode, PartialDate, and Date::with (#89)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
nekevss authored Aug 10, 2024
1 parent af94bbc commit a7fc946
Show file tree
Hide file tree
Showing 4 changed files with 325 additions and 73 deletions.
35 changes: 28 additions & 7 deletions src/components/calendar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::{
duration::{DateDuration, TimeDuration},
Date, DateTime, Duration, MonthDay, YearMonth,
},
fields::{TemporalFieldKey, TemporalFields},
fields::{FieldMap, TemporalFields},
iso::{IsoDate, IsoDateSlots},
options::{ArithmeticOverflow, TemporalUnit},
TemporalError, TemporalResult,
Expand Down Expand Up @@ -310,6 +310,10 @@ impl Calendar {
let month_code = MonthCode(
fields
.month_code
.map(|mc| {
TinyAsciiStr::from_bytes(mc.as_str().as_bytes())
.expect("MonthCode as_str is always valid.")
})
.ok_or(TemporalError::range().with_message("No MonthCode provided."))?,
);
// NOTE: This might preemptively throw as `ICU4X` does not support constraining.
Expand Down Expand Up @@ -373,6 +377,10 @@ impl Calendar {
let month_code = MonthCode(
fields
.month_code
.map(|mc| {
TinyAsciiStr::from_bytes(mc.as_str().as_bytes())
.expect("MonthCode as_str is always valid.")
})
.ok_or(TemporalError::range().with_message("No MonthCode provided."))?,
);

Expand Down Expand Up @@ -626,10 +634,22 @@ impl Calendar {
}

/// Provides field keys to be ignored depending on the calendar.
pub fn field_keys_to_ignore(
&self,
_keys: &[TemporalFieldKey],
) -> TemporalResult<Vec<TemporalFieldKey>> {
pub fn field_keys_to_ignore(&self, keys: FieldMap) -> TemporalResult<FieldMap> {
let mut ignored_keys = FieldMap::empty();
if self.is_iso() {
// NOTE: It is okay for ignored keys to have duplicates?
for key in keys.iter() {
ignored_keys.set(key, true);
if key == FieldMap::MONTH {
ignored_keys.set(FieldMap::MONTH_CODE, true);
} else if key == FieldMap::MONTH_CODE {
ignored_keys.set(FieldMap::MONTH, true);
}
}

return Ok(ignored_keys);
}

// TODO: Research and implement the appropriate KeysToIgnore for all `BuiltinCalendars.`
Err(TemporalError::range().with_message("FieldKeysToIgnore is not yet implemented."))
}
Expand Down Expand Up @@ -677,8 +697,9 @@ impl From<YearMonth> for Calendar {

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

use super::*;
use super::Calendar;

#[test]
fn date_until_largest_year() {
Expand Down Expand Up @@ -925,7 +946,7 @@ mod tests {
),
];

let calendar = Calendar::from_str("iso8601").unwrap();
let calendar = Calendar::default();

for test in tests {
let first = Date::new_unchecked(
Expand Down
136 changes: 133 additions & 3 deletions src/components/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,49 @@ use std::str::FromStr;

use super::{
duration::{normalized::NormalizedDurationRecord, TimeDuration},
MonthDay, Time, YearMonth,
MonthCode, MonthDay, Time, YearMonth,
};

// TODO: PrepareTemporalFields expects a type error to be thrown when all partial fields are None/undefined.
/// A partial Date that may or may not be complete.
#[derive(Debug, Default, Clone, Copy)]
pub struct PartialDate {
pub(crate) year: Option<i32>,
pub(crate) month: Option<i32>,
pub(crate) month_code: Option<MonthCode>,
pub(crate) day: Option<i32>,
pub(crate) era: Option<TinyAsciiStr<16>>,
pub(crate) era_year: Option<i32>,
}

impl PartialDate {
/// Create a new `PartialDate`
pub fn new(
year: Option<i32>,
month: Option<i32>,
month_code: Option<MonthCode>,
day: Option<i32>,
era: Option<TinyAsciiStr<16>>,
era_year: Option<i32>,
) -> TemporalResult<Self> {
if !(day.is_some()
&& (month.is_some() || month_code.is_some())
&& (year.is_some() || (era.is_some() && era_year.is_some())))
{
return Err(TemporalError::r#type()
.with_message("A partial date must have at least one defined field."));
}
Ok(Self {
year,
month,
month_code,
day,
era,
era_year,
})
}
}

/// The native Rust implementation of `Temporal.PlainDate`.
#[non_exhaustive]
#[derive(Debug, Default, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -210,6 +250,28 @@ impl Date {
Ok(Self::new_unchecked(iso, calendar))
}

/// Creates a date time with values from a `PartialDate`.
pub fn with(
&self,
partial: PartialDate,
overflow: Option<ArithmeticOverflow>,
) -> TemporalResult<Self> {
// 6. Let fieldsResult be ? PrepareCalendarFieldsAndFieldNames(calendarRec, temporalDate, « "day", "month", "monthCode", "year" »).
let fields = TemporalFields::from(self);
// 7. Let partialDate be ? PrepareTemporalFields(temporalDateLike, fieldsResult.[[FieldNames]], partial).
let partial_fields = TemporalFields::from(partial);

// 8. Let fields be ? CalendarMergeFields(calendarRec, fieldsResult.[[Fields]], partialDate).
let mut merge_result = fields.merge_fields(&partial_fields, self.calendar())?;

// 9. Set fields to ? PrepareTemporalFields(fields, fieldsResult.[[FieldNames]], «»).
// 10. Return ? CalendarDateFromFields(calendarRec, fields, resolvedOptions).
self.calendar.date_from_fields(
&mut merge_result,
overflow.unwrap_or(ArithmeticOverflow::Constrain),
)
}

/// Creates a new `Date` from the current `Date` and the provided calendar.
pub fn with_calendar(&self, calendar: Calendar) -> TemporalResult<Self> {
Self::new(
Expand Down Expand Up @@ -396,15 +458,15 @@ impl Date {
/// Converts the current `Date<C>` into a `YearMonth<C>`
#[inline]
pub fn to_year_month(&self) -> TemporalResult<YearMonth> {
let mut fields: TemporalFields = self.iso_date().into();
let mut fields: TemporalFields = self.into();
self.get_calendar()
.year_month_from_fields(&mut fields, ArithmeticOverflow::Constrain)
}

/// Converts the current `Date<C>` into a `MonthDay<C>`
#[inline]
pub fn to_month_day(&self) -> TemporalResult<MonthDay> {
let mut fields: TemporalFields = self.iso_date().into();
let mut fields: TemporalFields = self.into();
self.get_calendar()
.month_day_from_fields(&mut fields, ArithmeticOverflow::Constrain)
}
Expand Down Expand Up @@ -577,6 +639,74 @@ mod tests {
assert_eq!(result.days(), 9719.0,);
}

#[test]
fn basic_date_with() {
let base = Date::new(
1976,
11,
18,
Calendar::default(),
ArithmeticOverflow::Constrain,
)
.unwrap();

// Year
let partial = PartialDate {
year: Some(2019),
..Default::default()
};
let with_year = base.with(partial, None).unwrap();
assert_eq!(with_year.year().unwrap(), 2019);
assert_eq!(with_year.month().unwrap(), 11);
assert_eq!(
with_year.month_code().unwrap(),
TinyAsciiStr::<4>::from_str("M11").unwrap()
);
assert_eq!(with_year.day().unwrap(), 18);

// Month
let partial = PartialDate {
month: Some(5),
..Default::default()
};
let with_month = base.with(partial, None).unwrap();
assert_eq!(with_month.year().unwrap(), 1976);
assert_eq!(with_month.month().unwrap(), 5);
assert_eq!(
with_month.month_code().unwrap(),
TinyAsciiStr::<4>::from_str("M05").unwrap()
);
assert_eq!(with_month.day().unwrap(), 18);

// Month Code
let partial = PartialDate {
month_code: Some(MonthCode::Five),
..Default::default()
};
let with_mc = base.with(partial, None).unwrap();
assert_eq!(with_mc.year().unwrap(), 1976);
assert_eq!(with_mc.month().unwrap(), 5);
assert_eq!(
with_mc.month_code().unwrap(),
TinyAsciiStr::<4>::from_str("M05").unwrap()
);
assert_eq!(with_mc.day().unwrap(), 18);

// Day
let partial = PartialDate {
day: Some(17),
..Default::default()
};
let with_day = base.with(partial, None).unwrap();
assert_eq!(with_day.year().unwrap(), 1976);
assert_eq!(with_day.month().unwrap(), 11);
assert_eq!(
with_day.month_code().unwrap(),
TinyAsciiStr::<4>::from_str("M11").unwrap()
);
assert_eq!(with_day.day().unwrap(), 17);
}

// test262/test/built-ins/Temporal/Calendar/prototype/month/argument-string-invalid.js
#[test]
fn invalid_strings() {
Expand Down
92 changes: 91 additions & 1 deletion src/components/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ mod time;
mod year_month;
mod zoneddatetime;

use std::str::FromStr;

#[doc(inline)]
pub use date::Date;
pub use date::{Date, PartialDate};
#[doc(inline)]
pub use datetime::DateTime;
#[doc(inline)]
Expand All @@ -45,3 +47,91 @@ pub use year_month::YearMonth;
pub use year_month::YearMonthFields;
#[doc(inline)]
pub use zoneddatetime::ZonedDateTime;

use crate::TemporalError;

// TODO: Update to account for https://tc39.es/proposal-intl-era-monthcode/
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[repr(u8)]
pub enum MonthCode {
One = 1,
Two,
Three,
Four,
Five,
Six,
Seven,
Eight,
Nine,
Ten,
Eleven,
Twelve,
Thirteen,
}

impl MonthCode {
pub fn as_str(&self) -> &str {
match self {
Self::One => "M01",
Self::Two => "M02",
Self::Three => "M03",
Self::Four => "M04",
Self::Five => "M05",
Self::Six => "M06",
Self::Seven => "M07",
Self::Eight => "M08",
Self::Nine => "M09",
Self::Ten => "M10",
Self::Eleven => "M11",
Self::Twelve => "M12",
Self::Thirteen => "M13",
}
}
}

impl FromStr for MonthCode {
type Err = TemporalError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"M01" => Ok(Self::One),
"M02" => Ok(Self::Two),
"M03" => Ok(Self::Three),
"M04" => Ok(Self::Four),
"M05" => Ok(Self::Five),
"M06" => Ok(Self::Six),
"M07" => Ok(Self::Seven),
"M08" => Ok(Self::Eight),
"M09" => Ok(Self::Nine),
"M10" => Ok(Self::Ten),
"M11" => Ok(Self::Eleven),
"M12" => Ok(Self::Twelve),
"M13" => Ok(Self::Thirteen),
_ => {
Err(TemporalError::range()
.with_message("monthCode is not within the valid values."))
}
}
}
}

impl TryFrom<u8> for MonthCode {
type Error = TemporalError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
1 => Ok(Self::One),
2 => Ok(Self::Two),
3 => Ok(Self::Three),
4 => Ok(Self::Four),
5 => Ok(Self::Five),
6 => Ok(Self::Six),
7 => Ok(Self::Seven),
8 => Ok(Self::Eight),
9 => Ok(Self::Nine),
10 => Ok(Self::Ten),
11 => Ok(Self::Eleven),
12 => Ok(Self::Twelve),
13 => Ok(Self::Thirteen),
_ => Err(TemporalError::range().with_message("Invalid MonthCode value.")),
}
}
}
Loading

0 comments on commit a7fc946

Please sign in to comment.