Skip to content

Commit

Permalink
Support century when parsing/formatting year
Browse files Browse the repository at this point in the history
  • Loading branch information
jhpratt committed Oct 15, 2024
1 parent a6c3243 commit 19120ac
Show file tree
Hide file tree
Showing 12 changed files with 275 additions and 31 deletions.
1 change: 1 addition & 0 deletions tests/formatting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ fn format_date() -> time::Result<()> {
(fd!("[year base:iso_week]"), "2020"),
(fd!("[year sign:mandatory]"), "+2019"),
(fd!("[year base:iso_week sign:mandatory]"), "+2020"),
(fd!("[year repr:century]"), "20"),
(fd!("[year repr:last_two]"), "19"),
(fd!("[year base:iso_week repr:last_two]"), "20"),
];
Expand Down
2 changes: 1 addition & 1 deletion tests/meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ fn size() {
assert_size!(iso8601::FormattedComponents, 1, 1);
assert_size!(iso8601::OffsetPrecision, 1, 1);
assert_size!(iso8601::TimePrecision, 2, 2);
assert_size!(Parsed, 56, 56);
assert_size!(Parsed, 64, 64);
assert_size!(Month, 1, 1);
assert_size!(Weekday, 1, 1);
assert_size!(Error, 56, 56);
Expand Down
1 change: 1 addition & 0 deletions tests/parse_format_description.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ fn modifiers(
week_number_repr: _,
#[values(
(YearRepr::Full, "repr:full"),
(YearRepr::Century, "repr:century"),
(YearRepr::LastTwo, "repr:last_two"),
)]
year_repr: _,
Expand Down
30 changes: 26 additions & 4 deletions tests/parsing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -867,8 +867,8 @@ fn parse_date() -> time::Result<()> {
"[year padding:space]-W[week_number repr:sunday padding:none]-[weekday \
repr:sunday]",
)?,
" 2018-W01-2",
date!(2018 - 01 - 02),
" 201-W01-2",
date!(201 - 01 - 06),
),
];

Expand Down Expand Up @@ -1124,10 +1124,10 @@ fn parse_offset_date_time_err() -> time::Result<()> {
#[test]
fn parse_components() -> time::Result<()> {
macro_rules! parse_component {
($component:expr, $input:expr,_. $property:ident() == $expected:expr) => {
($component:expr, $input:expr, $(_. $property:ident() == $expected:expr);+ $(;)?) => {
let mut parsed = Parsed::new();
parsed.parse_component($input, $component)?;
assert_eq!(parsed.$property(), $expected);
$(assert_eq!(parsed.$property(), $expected);)+
};
}

Expand All @@ -1141,6 +1141,17 @@ fn parse_components() -> time::Result<()> {
b"2021",
_.year() == Some(2021)
);
parse_component!(
Component::Year(modifier!(Year {
padding: modifier::Padding::Zero,
repr: modifier::YearRepr::Century,
iso_week_based: false,
sign_is_mandatory: false,
})),
b"20",
_.year_century() == Some(20);
_.year_century_is_negative() == Some(false);
);
parse_component!(
Component::Year(modifier!(Year {
padding: modifier::Padding::Zero,
Expand All @@ -1161,6 +1172,17 @@ fn parse_components() -> time::Result<()> {
b"2021",
_.iso_year() == Some(2021)
);
parse_component!(
Component::Year(modifier!(Year {
padding: modifier::Padding::Zero,
repr: modifier::YearRepr::Century,
iso_week_based: true,
sign_is_mandatory: false,
})),
b"20",
_.iso_year_century() == Some(20);
_.iso_year_century_is_negative() == Some(false);
);
parse_component!(
Component::Year(modifier!(Year {
padding: modifier::Padding::Zero,
Expand Down
28 changes: 27 additions & 1 deletion tests/quickcheck.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use num_conv::prelude::*;
use quickcheck::{Arbitrary, TestResult};
use quickcheck_macros::quickcheck;
use time::macros::time;
use time::macros::{format_description, time};
use time::Weekday::*;
use time::{Date, Duration, Month, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset, Weekday};

Expand Down Expand Up @@ -56,6 +56,32 @@ fn date_ywd_roundtrip(d: Date) -> bool {
Date::from_iso_week_date(year, week, weekday) == Ok(d)
}

#[quickcheck]
fn date_format_century_last_two_equivalent(d: Date) -> bool {
let split_format = format_description!("[year repr:century][year repr:last_two]-[month]-[day]");
let split = d.format(&split_format).expect("formatting failed");

let combined_format = format_description!("[year]-[month]-[day]");
let combined = d.format(&combined_format).expect("formatting failed");

split == combined
}

#[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.
if !matches!(d.year().unsigned_abs().to_string().len(), 6) {
return TestResult::discard();
}

let split_format = format_description!("[year repr:century][year repr:last_two]-[month]-[day]");
let combined_format = format_description!("[year]-[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
1 change: 1 addition & 0 deletions time-macros/src/format_description/format_item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ modifier! {
enum YearRepr {
#[default]
Full = b"full",
Century = b"century",
LastTwo = b"last_two",
}
}
Expand Down
1 change: 1 addition & 0 deletions time-macros/src/format_description/public/modifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ to_tokens! {
to_tokens! {
pub(crate) enum YearRepr {
Full,
Century,
LastTwo,
}
}
Expand Down
2 changes: 2 additions & 0 deletions time/src/format_description/modifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ pub struct WeekNumber {
pub enum YearRepr {
/// The full value of the year.
Full,
/// All digits except the last two. Includes the sign, if any.
Century,
/// Only the last two digits of the year.
LastTwo,
}
Expand Down
1 change: 1 addition & 0 deletions time/src/format_description/parse/format_item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,7 @@ modifier! {
enum YearRepr {
#[default]
Full = b"full",
Century = b"century",
LastTwo = b"last_two",
}
}
Expand Down
7 changes: 6 additions & 1 deletion time/src/formatting/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ fn fmt_year(
};
let value = match repr {
modifier::YearRepr::Full => full_year,
modifier::YearRepr::Century => full_year / 100,
modifier::YearRepr::LastTwo => (full_year % 100).abs(),
};
let format_number = match repr {
Expand All @@ -316,7 +317,11 @@ fn fmt_year(
#[cfg(feature = "large-dates")]
modifier::YearRepr::Full if value.abs() >= 10_000 => format_number::<5>,
modifier::YearRepr::Full => format_number::<4>,
modifier::YearRepr::LastTwo => format_number::<2>,
#[cfg(feature = "large-dates")]
modifier::YearRepr::Century if value.abs() >= 1_000 => format_number::<4>,
#[cfg(feature = "large-dates")]
modifier::YearRepr::Century if value.abs() >= 100 => format_number::<3>,
modifier::YearRepr::Century | modifier::YearRepr::LastTwo => format_number::<2>,
};
let mut bytes = 0;
if repr != modifier::YearRepr::LastTwo {
Expand Down
62 changes: 50 additions & 12 deletions time/src/parsing/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,62 @@ use crate::{Month, Weekday};

// region: date components
/// Parse the "year" component of a `Date`.
pub(crate) fn parse_year(input: &[u8], modifiers: modifier::Year) -> Option<ParsedItem<'_, i32>> {
pub(crate) fn parse_year(
input: &[u8],
modifiers: modifier::Year,
) -> Option<ParsedItem<'_, (i32, bool)>> {
match modifiers.repr {
modifier::YearRepr::Full => {
let ParsedItem(input, sign) = opt(sign)(input);
#[cfg(not(feature = "large-dates"))]
let ParsedItem(input, year) =
exactly_n_digits_padded::<4, u32>(modifiers.padding)(input)?;
#[cfg(feature = "large-dates")]
let ParsedItem(input, year) =
n_to_m_digits_padded::<4, 6, u32>(modifiers.padding)(input)?;
match sign {
Some(b'-') => Some(ParsedItem(input, -year.cast_signed())),
None if modifiers.sign_is_mandatory || year >= 10_000 => None,
_ => Some(ParsedItem(input, year.cast_signed())),

if let Some(sign) = sign {
#[cfg(not(feature = "large-dates"))]
let ParsedItem(input, year) =
exactly_n_digits_padded::<4, u32>(modifiers.padding)(input)?;
#[cfg(feature = "large-dates")]
let ParsedItem(input, year) =
n_to_m_digits_padded::<4, 6, u32>(modifiers.padding)(input)?;

Some(if sign == b'-' {
ParsedItem(input, (-year.cast_signed(), true))
} else {
ParsedItem(input, (year.cast_signed(), false))
})
} else if modifiers.sign_is_mandatory {
None
} else {
let ParsedItem(input, year) =
exactly_n_digits_padded::<4, u32>(modifiers.padding)(input)?;
Some(ParsedItem(input, (year.cast_signed(), false)))
}
}
modifier::YearRepr::Century => {
let ParsedItem(input, sign) = opt(sign)(input);

if let Some(sign) = sign {
#[cfg(not(feature = "large-dates"))]
let ParsedItem(input, year) =
exactly_n_digits_padded::<2, u32>(modifiers.padding)(input)?;
#[cfg(feature = "large-dates")]
let ParsedItem(input, year) =
n_to_m_digits_padded::<2, 4, u32>(modifiers.padding)(input)?;

Some(if sign == b'-' {
ParsedItem(input, (-year.cast_signed(), true))
} else {
ParsedItem(input, (year.cast_signed(), false))
})
} else if modifiers.sign_is_mandatory {
None
} else {
let ParsedItem(input, year) =
exactly_n_digits_padded::<2, u32>(modifiers.padding)(input)?;
Some(ParsedItem(input, (year.cast_signed(), false)))
}
}
modifier::YearRepr::LastTwo => Some(
exactly_n_digits_padded::<2, u32>(modifiers.padding)(input)?.map(|v| v.cast_signed()),
exactly_n_digits_padded::<2, u32>(modifiers.padding)(input)?
.map(|v| (v.cast_signed(), false)),
),
}
}
Expand Down
Loading

3 comments on commit 19120ac

@dennisorlando
Copy link

@dennisorlando dennisorlando commented on 19120ac Oct 26, 2024

Choose a reason for hiding this comment

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

Sooooooooo
Apparently,
this commit fixes #683. I can't figure out if this breaks large-dates, I'm very confused about this.
The "minimal example" presented in #683 doesn't panic anymore due to a ParseFromDescription(InvalidComponent("year")) error. The previous commit, a6c3243, crashes like expected.
I can't figure out why, thus I don't know if this implements a breaking change or not.
This snippet is supposed to panic:

use time::{macros::format_description, PrimitiveDateTime};

fn main() {
    let format = format_description!("[year][month][day][hour][minute][second]");
    let datetime = PrimitiveDateTime::parse("2024240602205731", format).unwrap();
    println!("{}", datetime);
}

@dennisorlando
Copy link

Choose a reason for hiding this comment

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

Edit: this commit apparently removes support for large dates without a sign, is that correct?

...
            } else if modifiers.sign_is_mandatory {
                None
            } else {
                let ParsedItem(input, year) =
                    exactly_n_digits_padded::<4, u32>(modifiers.padding)(input)?;
                Some(ParsedItem(input, (year.cast_signed(), false)))
            }
...

@jhpratt
Copy link
Member Author

Choose a reason for hiding this comment

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

There are still some ambiguities, which is why this comment is present. This commit does improve the behavior, though, even if it is does cause minor breakage (the previous behavior was unintended).

Please sign in to comment.