Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement MonthCode, PartialDate, and Date::with #89

Merged
merged 6 commits into from
Aug 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will these not change in different calendars? Aren't there more month codes in the chinese calendar? such as https://tc39.es/proposal-intl-era-monthcode/

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I realized that earlier. I was sort of thinking we could just expand the enum, but that's probably not sufficient either.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this structure is fine for now and we can refactor it to support more in future? What do you think?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if you don't have a "leap" flag in the month code then it probably doesn't make sense to have M13... AFAIK no calendars have an M13, if there is a thirteenth month it's always one of M00L through M12L (or maybe M01L through M11L; it's unclear whether M00L and M12L actually exist)

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