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

0 comments on commit 19120ac

Please sign in to comment.