Skip to content

Commit

Permalink
Add support for week-of-month. (#1468)
Browse files Browse the repository at this point in the history
Contrary to UTS 35 this always uses min_days = 1 as there's no month of
week-of-month field so there would be inconsistencies otherwise (e.g. in
the ISO calendar 2021-01-01 is the last week of December but 'MMMMW' would
have it formatted as 'week 5 of January').
  • Loading branch information
mildgravitas authored Mar 21, 2022
1 parent 0fe7bae commit d3cbc28
Show file tree
Hide file tree
Showing 30 changed files with 179 additions and 36 deletions.
50 changes: 50 additions & 0 deletions components/calendar/src/arithmetic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,31 @@ pub mod week_of {
}
}

/// Computes & returns the week of given month or year accoding to a calendar with min_week_days = 1.
///
/// # Arguments
/// - first_weekday: The first day of a week.
/// - day: 1-based day of the month or year.
/// - week_day: The weekday of `day`.
pub fn simple_week_of(first_weekday: IsoWeekday, day: u16, week_day: IsoWeekday) -> u16 {
let calendar = CalendarInfo {
first_weekday,
min_week_days: 1,
};

week_of(
&calendar,
// The duration of the current and previous unit does not influence the result if min_week_days = 1
// so we only need to use a valid value.
MIN_UNIT_DAYS,
MIN_UNIT_DAYS,
day,
week_day,
)
.expect("week_of should can't fail with MIN_UNIT_DAYS")
.week
}

#[cfg(test)]
mod tests {
use super::{week_of, CalendarInfo, RelativeUnit, RelativeWeek, UnitInfo, WeekOf};
Expand Down Expand Up @@ -422,4 +447,29 @@ pub mod week_of {
Ok(())
}
}

#[test]
fn test_simple_week_of() {
// The 1st is a Monday and the week starts on Mondays.
assert_eq!(
simple_week_of(IsoWeekday::Monday, 2, IsoWeekday::Tuesday),
1
);
assert_eq!(simple_week_of(IsoWeekday::Monday, 7, IsoWeekday::Sunday), 1);
assert_eq!(simple_week_of(IsoWeekday::Monday, 8, IsoWeekday::Monday), 2);

// The 1st is a Wednesday and the week starts on Tuesdays.
assert_eq!(
simple_week_of(IsoWeekday::Tuesday, 1, IsoWeekday::Wednesday),
1
);
assert_eq!(
simple_week_of(IsoWeekday::Tuesday, 6, IsoWeekday::Monday),
1
);
assert_eq!(
simple_week_of(IsoWeekday::Tuesday, 7, IsoWeekday::Tuesday),
2
);
}
}
50 changes: 44 additions & 6 deletions components/datetime/src/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,10 @@ pub trait LocalizedDateTimeInput<T: DateTimeInput> {
/// For example, December 31, 2020 is part of the first week of 2021.
fn year_week(&self) -> Result<Year, DateTimeError>;

/// The week of the month according to UTS 35.
fn week_of_month(&self) -> WeekOfMonth;
/// The week of the month.
///
/// For example, January 1, 2021 is part of the first week of January.
fn week_of_month(&self) -> Result<WeekOfMonth, DateTimeError>;

/// The week number of the year.
///
Expand Down Expand Up @@ -163,6 +165,32 @@ fn week_of_year<T: DateInput>(
Ok(WeekOfYear(u32::from(week.week)))
}

/// Returns the week of month according to a calendar with min_week_days = 1.
///
/// This is different from what the UTS35 spec describes [1] but the latter is
/// missing a month of week-of-month field so following the spec would result
/// in inconsistencies (e.g. in the ISO calendar 2021-01-01 is the last week
/// of December but 'MMMMW' would have it formatted as 'week 5 of January').
///
/// 1: https://www.unicode.org/reports/tr35/tr35-55/tr35-dates.html#Date_Patterns_Week_Of_Year
fn week_of_month<T: DateInput>(
datetime: &T,
first_weekday: IsoWeekday,
) -> Result<WeekOfMonth, DateTimeError> {
let day_of_month = datetime
.day_of_month()
.ok_or(DateTimeError::MissingInput("DateTimeInput::day_of_month"))?;

let week = week_of::simple_week_of(
first_weekday,
day_of_month.0 as u16,
datetime
.iso_weekday()
.ok_or(DateTimeError::MissingInput("DateTimeInput::iso_weekday"))?,
);
Ok(WeekOfMonth(u32::from(week)))
}

impl<'data, T: DateTimeInput> DateTimeInputWithLocale<'data, T> {
pub fn new(
data: &'data T,
Expand Down Expand Up @@ -201,8 +229,13 @@ impl<'data, T: DateTimeInput> LocalizedDateTimeInput<T> for DateTimeInputWithLoc
)
}

fn week_of_month(&self) -> WeekOfMonth {
todo!("#488")
fn week_of_month(&self) -> Result<WeekOfMonth, DateTimeError> {
week_of_month(
self.data,
self.calendar
.expect("calendar must be provided when using week of methods")
.first_weekday,
)
}

fn week_of_year(&self) -> Result<WeekOfYear, DateTimeError> {
Expand Down Expand Up @@ -233,8 +266,13 @@ impl<'data, T: ZonedDateTimeInput> LocalizedDateTimeInput<T>
)
}

fn week_of_month(&self) -> WeekOfMonth {
todo!("#488")
fn week_of_month(&self) -> Result<WeekOfMonth, DateTimeError> {
week_of_month(
self.data,
self.calendar
.expect("calendar must be provided when using week of methods")
.first_weekday,
)
}

fn week_of_year(&self) -> Result<WeekOfYear, DateTimeError> {
Expand Down
9 changes: 1 addition & 8 deletions components/datetime/src/fields/symbols.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,14 +231,7 @@ impl TryFrom<char> for FieldSymbol {
})
.or_else(|_| Year::try_from(ch).map(Self::Year))
.or_else(|_| Month::try_from(ch).map(Self::Month))
.or_else(|_| {
if ch == 'w' {
Week::try_from(ch).map(Self::Week)
} else {
// TODO(#488): Add support for 'W'.
Err(SymbolError::Unknown(ch))
}
})
.or_else(|_| Week::try_from(ch).map(Self::Week))
.or_else(|_| Day::try_from(ch).map(Self::Day))
.or_else(|_| Weekday::try_from(ch).map(Self::Weekday))
.or_else(|_| DayPeriod::try_from(ch).map(Self::DayPeriod))
Expand Down
12 changes: 8 additions & 4 deletions components/datetime/src/format/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,10 +215,14 @@ where
w.write_str(symbol)?
}
},
FieldSymbol::Week(Week::WeekOfYear) => {
format_number(w, datetime.week_of_year()?.0 as isize, field.length)?
}
field @ FieldSymbol::Week(_) => return Err(Error::UnsupportedField(field)),
FieldSymbol::Week(week) => match week {
Week::WeekOfYear => {
format_number(w, datetime.week_of_year()?.0 as isize, field.length)?
}
Week::WeekOfMonth => {
format_number(w, datetime.week_of_month()?.0 as isize, field.length)?
}
},
FieldSymbol::Weekday(weekday) => {
let dow = datetime
.datetime()
Expand Down
20 changes: 9 additions & 11 deletions components/datetime/src/options/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,7 @@ pub struct Bag {
pub year: Option<Year>,
/// Include the month, such as "April" or "Apr".
pub month: Option<Month>,
/// Include the week, such as "1st" or "1".
#[doc(hidden)]
// TODO(#488): make visible once fully supported.
/// Include the week number, such as "51st" or "51" for week 51.
pub week: Option<Week>,
/// Include the day, such as "07" or "7".
pub day: Option<Numeric>,
Expand Down Expand Up @@ -390,7 +388,7 @@ pub enum Month {
Numeric,
/// The two-digit value of the month, such as "04".
TwoDigit,
/// The two-digit value of the month, such as "April".
/// The long value of the month, such as "April".
Long,
/// The short value of the month, such as "Apr".
Short,
Expand All @@ -400,22 +398,22 @@ pub enum Month {

// Each enum variant is documented with the UTS 35 field information from:
// https://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table
//
/// Options for displaying the current week for the `components::`[`Bag`].
#[doc(hidden)]
// TODO(#488): make visible once fully supported.

/// Options for displaying the current week number for the `components::`[`Bag`].
///
/// Week numbers are relative to either a month or year, e.g. 'week 3 of January' or 'week 40 of 2000'.
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(
feature = "serialize",
derive(Serialize, Deserialize),
serde(rename_all = "kebab-case")
)]
pub enum Week {
/// The week of the month, such as "3".
/// The week of the month, such as the "3" in "week 3 of January".
WeekOfMonth,
/// The numeric value of the week of the year, such as "8".
/// The numeric value of the week of the year, such as the "8" in "week 8 of 2000".
NumericWeekOfYear,
/// The two-digit value of the week of the year, such as "08".
/// The two-digit value of the week of the year, such as the "08" in "2000-W08".
TwoDigitWeekOfYear,
}

Expand Down
2 changes: 1 addition & 1 deletion components/datetime/src/pattern/runtime/plural.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ impl<'data> PatternPlurals<'data> {
Self::SinglePattern(pattern) => Ok(pattern),
Self::MultipleVariants(plural_pattern) => {
let week_number = match plural_pattern.pivot_field() {
Week::WeekOfMonth => loc_datetime.week_of_month().0,
Week::WeekOfMonth => loc_datetime.week_of_month()?.0,
Week::WeekOfYear => loc_datetime.week_of_year()?.0,
};
let category = ordinal_rules
Expand Down
2 changes: 0 additions & 2 deletions components/datetime/src/skeleton/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,6 @@ impl From<fields::SymbolError> for SkeletonError {
match ch {
// TODO(#487) - Flexible day periods
'B'
// TODO(#502) - Week of month
| 'W'
// TODO(#501) - Quarters
| 'Q'
=> Self::SymbolUnimplemented(ch),
Expand Down
5 changes: 2 additions & 3 deletions components/datetime/src/skeleton/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,8 @@ mod test {
#[rustfmt::skip]
const SUPPORTED_STRING_SKELETONS: &[&str] = &[
"E", "dEEEE", "EHm", "EHms", "dE", "Ehm", "Ehms", "H", "HHmm", "HHmmss", "Hm", "Hms", "M",
"MdEEEE", "MdE", "MMM", "MMMdEEEE", "MMMdE", "MMMM", "MMMMdEEEE", "MMMMdE", "MMMMd",
"MdEEEE", "MdE", "MMM", "MMMdEEEE", "MMMdE", "MMMM", "MMMMW",
"MMMMdEEEE", "MMMMdE", "MMMMd",
"MMMMdd", "MMMd", "MMMdd", "MMd", "MMdd", "Md", "Mdd", "d", "h", "hm", "hms", "mmss", "ms",
"y", "yM", "yMdEEEE", "yMdE", "yMM", "yMMM", "yMMMdEEEE", "yMMMdE", "yMMMM", "yMMMMdEEEE",
"yMMMMdE", "yMMMMdcccc", "yMMMMd", "yMMMd", "yMMdd", "yMd", "yw",
Expand All @@ -221,8 +222,6 @@ mod test {
const UNSUPPORTED_STRING_SKELETONS: &[&str] = &[
// TODO(#487) - Flexible day periods
"Bh", "Bhm", "Bhms", "EBhm", "EBhms",
// TODO(#502) - Week of month
"MMMMW",
// TODO(#501) - Quarters
"yQ", "yQQQ", "yQQQQ",
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,7 @@
}
},
{
"description": "Exact match for: yw => week' w 'of' Y",
"description": "Exact match for: yw => 'week' w 'of' Y",
"input": {
"locale": "en",
"value": "2016-04-17T08:25:07.000",
Expand All @@ -530,5 +530,23 @@
"en-ZA": "week 17 of 2016"
}
}
},
{
"description": "Exact match for: MMMMW -> 'week' W 'of' MMMM",
"input": {
"locale": "en",
"value": "2022-01-01T08:25:07.000",
"options": {
"components": {
"month": "long",
"week": "week-of-month"
}
}
},
"output": {
"values": {
"en": "week 1 of January"
}
}
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,23 @@
"en": "week 53 of 2002"
}
}
},
{
"description": "Partial match for: MMMMWEEEE -> MMMMW -> 'week' W 'of' MMMM",
"input": {
"value": "2002-12-31T08:25:07.000",
"options": {
"components": {
"month": "long",
"week": "week-of-month",
"weekday": "long"
}
}
},
"output": {
"values": {
"en": "week 1 of December"
}
}
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"MMM": "LLL",
"MMMd": "d MMM",
"MMMdE": "E، d MMM",
"MMMMW": "الأسبوع W من MMMM",
"MMMMd": "d MMMM",
"MMMMdE": "E، d MMMM",
"d": "d",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"MMM": "LLL",
"MMMd": "d MMM",
"MMMdE": "E، d MMM",
"MMMMW": "الأسبوع W من MMMM",
"MMMMd": "d MMMM",
"MMMMdE": "E، d MMMM",
"d": "d",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"MMM": "LLL",
"MMMd": "d MMM",
"MMMdE": "E d MMM",
"MMMMW": "MMMM এর Wয় সপ্তাহ",
"MMMMd": "d MMMM",
"MMMMdE": "E d MMMM",
"d": "d",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"MMM": "LLL",
"MMMd": "d MMM",
"MMMdE": "E d MMM",
"MMMMW": "MMMM 𑄃𑄬𑄢𑄴 𑄠𑄴 𑄥𑄛𑄴𑄖 W",
"MMMMd": "d MMMM",
"MMMMdE": "E d MMMM",
"d": "d",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"MMM": "LLL",
"MMMd": "d MMM",
"MMMdE": "E, d MMM",
"MMMMW": "'week' W 'of' MMMM",
"MMMMd": "d MMMM",
"d": "d",
"dE": "E d",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"MMM": "LLL",
"MMMd": "dd MMM",
"MMMdE": "E, dd MMM",
"MMMMW": "'week' W 'of' MMMM",
"MMMMd": "d MMMM",
"d": "d",
"dE": "E d",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"MMM": "LLL",
"MMMd": "MMM d",
"MMMdE": "E, MMM d",
"MMMMW": "'week' W 'of' MMMM",
"MMMMd": "MMMM d",
"d": "d",
"dE": "d E",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"MMMd": "d MMM",
"MMMdE": "E, d MMM",
"MMMdd": "dd-MMM",
"MMMMW": "'semana' W 'de' MMMM",
"MMMMd": "d 'de' MMMM",
"MMMMdE": "E, d 'de' MMMM",
"d": "d",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"MMM": "LLL",
"MMMd": "d MMM",
"MMMdE": "E, d MMM",
"MMMMW": "'semana' W 'de' MMMM",
"MMMMd": "d 'de' MMMM",
"MMMMdE": "E, d 'de' MMMM",
"d": "d",
Expand Down
Loading

1 comment on commit d3cbc28

@github-actions
Copy link

Choose a reason for hiding this comment

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

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.

Benchmark suite Current: d3cbc28 Previous: 0fe7bae Ratio
provider/testdata/data/testdata.postcard 4505794 bytes 4504771 bytes 1.00

This comment was automatically generated by workflow using github-action-benchmark.

CC: @gnrunge @sffc @zbraniecki @echeran

Please sign in to comment.