Skip to content

Commit

Permalink
Implement range modifier for year component
Browse files Browse the repository at this point in the history
  • Loading branch information
jhpratt committed Dec 11, 2024
1 parent 971e111 commit 2785c11
Show file tree
Hide file tree
Showing 19 changed files with 223 additions and 57 deletions.
15 changes: 15 additions & 0 deletions tests/formatting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,9 @@ fn format_date() -> time::Result<()> {
(fd!("[year repr:century]"), "20"),
(fd!("[year repr:last_two]"), "19"),
(fd!("[year base:iso_week repr:last_two]"), "20"),
(fd!("[year range:standard]"), "2019"),
(fd!("[year range:standard repr:century]"), "20"),
(fd!("[year range:standard repr:last_two]"), "19"),
];

for &(format_description, output) in &format_output {
Expand All @@ -407,6 +410,18 @@ fn format_date() -> time::Result<()> {
Ok(())
}

#[test]
fn format_date_err() {
assert!(matches!(
date!(+10_000-01-01).format(fd!("[year range:standard]")),
Err(time::error::Format::ComponentRange(cr)) if cr.name() == "year"
));
assert!(matches!(
date!(+10_000-01-01).format(fd!("[year repr:century range:standard]")),
Err(time::error::Format::ComponentRange(cr)) if cr.name() == "year"
));
}

#[test]
fn display_date() {
assert_eq!(date!(2019-01-01).to_string(), "2019-01-01");
Expand Down
10 changes: 5 additions & 5 deletions tests/meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ fn size() {
assert_size!(PrimitiveDateTime, 12, 12);
assert_size!(Time, 8, 8);
assert_size!(UtcOffset, 3, 4);
assert_size!(error::ComponentRange, 48, 48);
assert_size!(error::ComponentRange, 56, 56);
assert_size!(error::ConversionRange, 0, 1);
assert_size!(error::DifferentVariant, 0, 1);
assert_size!(error::IndeterminateOffset, 0, 1);
Expand All @@ -141,7 +141,7 @@ fn size() {
assert_size!(modifier::Subsecond, 1, 1);
assert_size!(modifier::WeekNumber, 2, 2);
assert_size!(modifier::Weekday, 3, 3);
assert_size!(modifier::Year, 4, 4);
assert_size!(modifier::Year, 5, 5);
assert_size!(well_known::Rfc2822, 0, 1);
assert_size!(well_known::Rfc3339, 0, 1);
assert_size!(
Expand All @@ -157,12 +157,12 @@ fn size() {
assert_size!(Parsed, 64, 64);
assert_size!(Month, 1, 1);
assert_size!(Weekday, 1, 1);
assert_size!(Error, 56, 56);
assert_size!(Error, 64, 64);
assert_size!(error::Format, 24, 24);
assert_size!(error::InvalidFormatDescription, 48, 48);
assert_size!(error::Parse, 48, 48);
assert_size!(error::Parse, 64, 64);
assert_size!(error::ParseFromDescription, 24, 24);
assert_size!(error::TryFromParsed, 48, 48);
assert_size!(error::TryFromParsed, 56, 64);
assert_size!(Component, 6, 6); // TODO Size is 4 starting with rustc 1.71.
assert_size!(BorrowedFormatItem<'_>, 24, 24);
assert_size!(modifier::MonthRepr, 1, 1);
Expand Down
8 changes: 8 additions & 0 deletions tests/parse_format_description.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ fn modifiers(
(YearRepr::LastTwo, "repr:last_two"),
)]
year_repr: _,
#[values(
(YearRange::Standard, "range:standard"),
(YearRange::Extended, "range:extended"),
)]
year_range: _,
#[values(
(false, "base:calendar"),
(true, "base:iso_week"),
Expand Down Expand Up @@ -380,6 +385,7 @@ fn offset_hour_component(padding: M<Padding>, sign_is_mandatory: M<bool>) {
fn year_component(
padding: M<Padding>,
year_repr: M<YearRepr>,
year_range: M<YearRange>,
year_is_iso_week_based: M<bool>,
sign_is_mandatory: M<bool>,
) {
Expand All @@ -388,13 +394,15 @@ fn year_component(
"year",
padding,
year_repr,
year_range,
year_is_iso_week_based,
sign_is_mandatory
),
Ok(vec![BorrowedFormatItem::Component(Component::Year(
modifier_m!(Year {
padding,
repr: year_repr,
range: year_range,
iso_week_based: year_is_iso_week_based,
sign_is_mandatory
})
Expand Down
11 changes: 11 additions & 0 deletions tests/parsing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,11 @@ fn parse_date() -> time::Result<()> {
"2021-01-02",
date!(2021-01-02),
),
(
fd::parse("[year repr:century range:standard][year repr:last_two]-[month]-[day]")?,
"2021-01-02",
date!(2021-01-02),
),
(fd::parse("[year]-[ordinal]")?, "2021-002", date!(2021-002)),
(
fd::parse("[year base:iso_week]-W[week_number]-[weekday repr:monday]")?,
Expand Down Expand Up @@ -1131,6 +1136,7 @@ fn parse_components() -> time::Result<()> {
Component::Year(modifier!(Year {
padding: modifier::Padding::Zero,
repr: modifier::YearRepr::Full,
range: modifier::YearRange::Extended,
iso_week_based: false,
sign_is_mandatory: false,
})),
Expand All @@ -1141,6 +1147,7 @@ fn parse_components() -> time::Result<()> {
Component::Year(modifier!(Year {
padding: modifier::Padding::Zero,
repr: modifier::YearRepr::Century,
range: modifier::YearRange::Extended,
iso_week_based: false,
sign_is_mandatory: false,
})),
Expand All @@ -1152,6 +1159,7 @@ fn parse_components() -> time::Result<()> {
Component::Year(modifier!(Year {
padding: modifier::Padding::Zero,
repr: modifier::YearRepr::LastTwo,
range: modifier::YearRange::Extended,
iso_week_based: false,
sign_is_mandatory: false,
})),
Expand All @@ -1162,6 +1170,7 @@ fn parse_components() -> time::Result<()> {
Component::Year(modifier!(Year {
padding: modifier::Padding::Zero,
repr: modifier::YearRepr::Full,
range: modifier::YearRange::Extended,
iso_week_based: true,
sign_is_mandatory: false,
})),
Expand All @@ -1172,6 +1181,7 @@ fn parse_components() -> time::Result<()> {
Component::Year(modifier!(Year {
padding: modifier::Padding::Zero,
repr: modifier::YearRepr::Century,
range: modifier::YearRange::Extended,
iso_week_based: true,
sign_is_mandatory: false,
})),
Expand All @@ -1183,6 +1193,7 @@ fn parse_components() -> time::Result<()> {
Component::Year(modifier!(Year {
padding: modifier::Padding::Zero,
repr: modifier::YearRepr::LastTwo,
range: modifier::YearRange::Extended,
iso_week_based: true,
sign_is_mandatory: false,
})),
Expand Down
23 changes: 20 additions & 3 deletions tests/quickcheck.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,10 @@ fn date_format_century_last_two_equivalent(d: Date) -> bool {
}

#[quickcheck]
fn date_parse_century_last_two_equivalent(d: Date) -> TestResult {
// There is an ambiguity when parsing a year with fewer than six digits, as the first four are
// consumed by the century, leaving at most one for the last two digits.
fn date_parse_century_last_two_equivalent_extended(d: Date) -> TestResult {
// With the extended range, there is an ambiguity when parsing a year with fewer than six
// digits, as the first four are consumed by the century, leaving at most one for the last
// two digits.
if !matches!(d.year().unsigned_abs().to_string().len(), 6) {
return TestResult::discard();
}
Expand All @@ -82,6 +83,22 @@ fn date_parse_century_last_two_equivalent(d: Date) -> TestResult {
TestResult::from_bool(Date::parse(&combined, &split_format).expect("parsing failed") == d)
}

#[quickcheck]
fn date_parse_century_last_two_equivalent_standard(d: Date) -> TestResult {
// With the standard range, the year must be at most four digits.
if !matches!(d.year(), -9999..=9999) {
return TestResult::discard();
}

let split_format = format_description!(
"[year repr:century range:standard][year repr:last_two range:standard]-[month]-[day]"
);
let combined_format = format_description!("[year range:standard]-[month]-[day]");
let combined = d.format(&combined_format).expect("formatting failed");

TestResult::from_bool(Date::parse(&combined, &split_format).expect("parsing failed") == d)
}

#[quickcheck]
fn julian_day_roundtrip(d: Date) -> bool {
Date::from_julian_day(d.to_julian_day()) == Ok(d)
Expand Down
7 changes: 7 additions & 0 deletions time-macros/src/format_description/format_item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ component_definition! {
Year = "year" {
padding = "padding": Option<Padding> => padding,
repr = "repr": Option<YearRepr> => repr,
range = "range": Option<YearRange> => range,
base = "base": Option<YearBase> => iso_week_based,
sign_behavior = "sign": Option<SignBehavior> => sign_is_mandatory,
},
Expand Down Expand Up @@ -428,6 +429,12 @@ modifier! {
Century = b"century",
LastTwo = b"last_two",
}

enum YearRange {
Standard = b"standard",
#[default]
Extended = b"extended",
}
}

fn parse_from_modifier_value<T: FromStr>(value: &Spanned<&[u8]>) -> Result<Option<T>, Error> {
Expand Down
8 changes: 8 additions & 0 deletions time-macros/src/format_description/public/modifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,18 @@ to_tokens! {
}
}

to_tokens! {
pub(crate) enum YearRange {
Standard,
Extended,
}
}

to_tokens! {
pub(crate) struct Year {
pub(crate) padding: Padding,
pub(crate) repr: YearRepr,
pub(crate) range: YearRange,
pub(crate) iso_week_based: bool,
pub(crate) sign_is_mandatory: bool,
}
Expand Down
12 changes: 6 additions & 6 deletions time/src/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ impl Date {
minimum: 1,
maximum: month.length(year) as _,
value: day as _,
conditional_range: true,
conditional_message: Some("for the given month and year"),
});
}
}
Expand Down Expand Up @@ -165,7 +165,7 @@ impl Date {
minimum: 1,
maximum: days_in_year(year) as _,
value: ordinal as _,
conditional_range: true,
conditional_message: Some("for the given year"),
});
}
}
Expand Down Expand Up @@ -202,7 +202,7 @@ impl Date {
minimum: 1,
maximum: weeks_in_year(year) as _,
value: week as _,
conditional_range: true,
conditional_message: Some("for the given year"),
});
}
}
Expand Down Expand Up @@ -1059,7 +1059,7 @@ impl Date {
value: 29,
minimum: 1,
maximum: 28,
conditional_range: true,
conditional_message: Some("for the given month and year"),
}),
// We're going from a common year to a leap year. Shift dates in March and later by
// one day.
Expand Down Expand Up @@ -1110,7 +1110,7 @@ impl Date {
minimum: 1,
maximum: self.month().length(self.year()) as _,
value: day as _,
conditional_range: true,
conditional_message: Some("for the given month and year"),
});
}
}
Expand Down Expand Up @@ -1143,7 +1143,7 @@ impl Date {
minimum: 1,
maximum: days_in_year(self.year()) as _,
value: ordinal as _,
conditional_range: true,
conditional_message: Some("for the given year"),
});
}
}
Expand Down
34 changes: 28 additions & 6 deletions time/src/error/component_range.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
//! Component range error
use core::fmt;
use core::{fmt, hash};

use crate::error;

/// An error type indicating that a component provided to a method was out of range, causing a
/// failure.
// i64 is the narrowest type fitting all use cases. This eliminates the need for a type parameter.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, Copy, Eq)]
pub struct ComponentRange {
/// Name of the component.
pub(crate) name: &'static str,
Expand All @@ -19,7 +19,7 @@ pub struct ComponentRange {
pub(crate) value: i64,
/// The minimum and/or maximum value is conditional on the value of other
/// parameters.
pub(crate) conditional_range: bool,
pub(crate) conditional_message: Option<&'static str>,
}

impl ComponentRange {
Expand All @@ -31,7 +31,29 @@ impl ComponentRange {
/// Whether the value's permitted range is conditional, i.e. whether an input with this
/// value could have succeeded if the values of other components were different.
pub const fn is_conditional(self) -> bool {
self.conditional_range
self.conditional_message.is_some()
}
}

impl PartialEq for ComponentRange {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
&& self.minimum == other.minimum
&& self.maximum == other.maximum
&& self.value == other.value
// Skip the contents of the message when comparing for equality.
&& self.conditional_message.is_some() == other.conditional_message.is_some()
}
}

impl hash::Hash for ComponentRange {
fn hash<H: hash::Hasher>(&self, state: &mut H) {
self.name.hash(state);
self.minimum.hash(state);
self.maximum.hash(state);
self.value.hash(state);
// Skip the contents of the message when comparing for equality.
self.conditional_message.is_some().hash(state);
}
}

Expand All @@ -43,8 +65,8 @@ impl fmt::Display for ComponentRange {
self.name, self.minimum, self.maximum
)?;

if self.conditional_range {
f.write_str(", given values of other parameters")?;
if let Some(message) = self.conditional_message {
write!(f, " {message}")?;
}

Ok(())
Expand Down
Loading

0 comments on commit 2785c11

Please sign in to comment.