Skip to content
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
2 changes: 2 additions & 0 deletions src/format/formatting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ impl<'a, I: Iterator<Item = B> + Clone, B: Borrow<Item<'a>>> DelayedFormat<I> {
(IsoYearMod100, Some(d), _) => {
write_two(w, d.iso_week().year().rem_euclid(100) as u8, pad)
}
(Quarter, Some(d), _) => write_one(w, d.quarter() as u8),
(Month, Some(d), _) => write_two(w, d.month() as u8, pad),
(Day, Some(d), _) => write_two(w, d.day() as u8, pad),
(WeekFromSun, Some(d), _) => write_two(w, d.weeks_from(Weekday::Sun) as u8, pad),
Expand Down Expand Up @@ -657,6 +658,7 @@ mod tests {
let d = NaiveDate::from_ymd_opt(2012, 3, 4).unwrap();
assert_eq!(d.format("%Y,%C,%y,%G,%g").to_string(), "2012,20,12,2012,12");
assert_eq!(d.format("%m,%b,%h,%B").to_string(), "03,Mar,Mar,March");
assert_eq!(d.format("%q").to_string(), "1");
assert_eq!(d.format("%d,%e").to_string(), "04, 4");
assert_eq!(d.format("%U,%W,%V").to_string(), "10,09,09");
assert_eq!(d.format("%a,%A,%w,%u").to_string(), "Sun,Sunday,0,7");
Expand Down
2 changes: 2 additions & 0 deletions src/format/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ pub enum Numeric {
IsoYearDiv100,
/// Year in the ISO week date, modulo 100 (FW=PW=2). Cannot be negative.
IsoYearMod100,
/// Quarter (FW=PW=1).
Quarter,
/// Month (FW=PW=2).
Month,
/// Day of the month (FW=PW=2).
Expand Down
14 changes: 11 additions & 3 deletions src/format/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ where
IsoYear => (4, true, Parsed::set_isoyear),
IsoYearDiv100 => (2, false, Parsed::set_isoyear_div_100),
IsoYearMod100 => (2, false, Parsed::set_isoyear_mod_100),
Quarter => (1, false, Parsed::set_quarter),
Month => (2, false, Parsed::set_month),
Day => (2, false, Parsed::set_day),
WeekFromSun => (2, false, Parsed::set_week_from_sun),
Expand Down Expand Up @@ -819,9 +820,16 @@ mod tests {
parsed!(year_div_100: 12, year_mod_100: 34, isoyear_div_100: 56, isoyear_mod_100: 78),
);
check(
"1 2 3 4 5",
&[num(Month), num(Day), num(WeekFromSun), num(NumDaysFromSun), num(IsoWeek)],
parsed!(month: 1, day: 2, week_from_sun: 3, weekday: Weekday::Thu, isoweek: 5),
"1 1 2 3 4 5",
&[
num(Quarter),
num(Month),
num(Day),
num(WeekFromSun),
num(NumDaysFromSun),
num(IsoWeek),
],
parsed!(quarter: 1, month: 1, day: 2, week_from_sun: 3, weekday: Weekday::Thu, isoweek: 5),
);
check(
"6 7 89 01",
Expand Down
53 changes: 52 additions & 1 deletion src/format/parsed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@
#[doc(hidden)]
pub isoyear_mod_100: Option<i32>,
#[doc(hidden)]
pub quarter: Option<u32>,
#[doc(hidden)]
pub month: Option<u32>,
#[doc(hidden)]
pub week_from_sun: Option<u32>,
Expand Down Expand Up @@ -304,6 +306,23 @@
set_if_consistent(&mut self.isoyear_mod_100, value as i32)
}

/// Set the [`quarter`](Parsed::quarter) field to the given value.
///
/// Quarter 1 starts in January.
///
/// # Errors
///
/// Returns `OUT_OF_RANGE` if `value` is not in the range 1-4.
///
/// Returns `IMPOSSIBLE` if this field was already set to a different value.
#[inline]
pub fn set_quarter(&mut self, value: i64) -> ParseResult<()> {
if !(1..=4).contains(&value) {
return Err(OUT_OF_RANGE);
}
set_if_consistent(&mut self.quarter, value as u32)
}

/// Set the [`month`](Parsed::month) field to the given value.
///
/// # Errors
Expand Down Expand Up @@ -698,7 +717,15 @@
(_, _, _) => return Err(NOT_ENOUGH),
};

if verified { Ok(parsed_date) } else { Err(IMPOSSIBLE) }
if !verified {
return Err(IMPOSSIBLE);
} else if let Some(parsed) = self.quarter {
if parsed != parsed_date.quarter() {
return Err(IMPOSSIBLE);
}
}

Ok(parsed_date)
}

/// Returns a parsed naive time out of given fields.
Expand Down Expand Up @@ -1013,6 +1040,14 @@
self.isoyear_mod_100
}

/// Get the `quarter` field if set.
///
/// See also [`set_quarter()`](Parsed::set_quarter).
#[inline]
pub fn quarter(&self) -> Option<u32> {
self.quarter
}

Check warning on line 1049 in src/format/parsed.rs

View check run for this annotation

Codecov / codecov/patch

src/format/parsed.rs#L1047-L1049

Added lines #L1047 - L1049 were not covered by tests

/// Get the `month` field if set.
///
/// See also [`set_month()`](Parsed::set_month).
Expand Down Expand Up @@ -1267,6 +1302,11 @@
assert!(Parsed::new().set_isoyear_mod_100(99).is_ok());
assert_eq!(Parsed::new().set_isoyear_mod_100(100), Err(OUT_OF_RANGE));

assert_eq!(Parsed::new().set_quarter(0), Err(OUT_OF_RANGE));
assert!(Parsed::new().set_quarter(1).is_ok());
assert!(Parsed::new().set_quarter(4).is_ok());
assert_eq!(Parsed::new().set_quarter(5), Err(OUT_OF_RANGE));

assert_eq!(Parsed::new().set_month(0), Err(OUT_OF_RANGE));
assert!(Parsed::new().set_month(1).is_ok());
assert!(Parsed::new().set_month(12).is_ok());
Expand Down Expand Up @@ -1425,6 +1465,17 @@
assert_eq!(parse!(year: -1, year_div_100: 0, month: 1, day: 1), Err(IMPOSSIBLE));
assert_eq!(parse!(year: -1, year_mod_100: 99, month: 1, day: 1), Err(IMPOSSIBLE));

// quarters
assert_eq!(parse!(year: 2000, quarter: 1), Err(NOT_ENOUGH));
assert_eq!(parse!(year: 2000, quarter: 1, month: 1, day: 1), ymd(2000, 1, 1));
assert_eq!(parse!(year: 2000, quarter: 2, month: 4, day: 1), ymd(2000, 4, 1));
assert_eq!(parse!(year: 2000, quarter: 3, month: 7, day: 1), ymd(2000, 7, 1));
assert_eq!(parse!(year: 2000, quarter: 4, month: 10, day: 1), ymd(2000, 10, 1));

// quarter: conflicting inputs
assert_eq!(parse!(year: 2000, quarter: 2, month: 3, day: 31), Err(IMPOSSIBLE));
assert_eq!(parse!(year: 2000, quarter: 4, month: 3, day: 31), Err(IMPOSSIBLE));

// weekdates
assert_eq!(parse!(year: 2000, week_from_mon: 0), Err(NOT_ENOUGH));
assert_eq!(parse!(year: 2000, week_from_sun: 0), Err(NOT_ENOUGH));
Expand Down
3 changes: 3 additions & 0 deletions src/format/strftime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ The following specifiers are available both to formatting and parsing.
| `%C` | `20` | The proleptic Gregorian year divided by 100, zero-padded to 2 digits. [^1] |
| `%y` | `01` | The proleptic Gregorian year modulo 100, zero-padded to 2 digits. [^1] |
| | | |
| `%q` | `1` | Quarter of year (1-4) |
| `%m` | `07` | Month number (01--12), zero-padded to 2 digits. |
| `%b` | `Jul` | Abbreviated month name. Always 3 letters. |
| `%B` | `July` | Full month name. Also accepts corresponding abbreviation in parsing. |
Expand Down Expand Up @@ -538,6 +539,7 @@ impl<'a> StrftimeItems<'a> {
'm' => num0(Month),
'n' => Space("\n"),
'p' => fixed(Fixed::UpperAmPm),
'q' => num(Quarter),
#[cfg(not(feature = "unstable-locales"))]
'r' => queue_from_slice!(T_FMT_AMPM),
#[cfg(feature = "unstable-locales")]
Expand Down Expand Up @@ -866,6 +868,7 @@ mod tests {
assert_eq!(dt.format("%Y").to_string(), "2001");
assert_eq!(dt.format("%C").to_string(), "20");
assert_eq!(dt.format("%y").to_string(), "01");
assert_eq!(dt.format("%q").to_string(), "3");
assert_eq!(dt.format("%m").to_string(), "07");
assert_eq!(dt.format("%b").to_string(), "Jul");
assert_eq!(dt.format("%B").to_string(), "July");
Expand Down
4 changes: 3 additions & 1 deletion src/naive/date/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -666,14 +666,16 @@ fn test_date_parse_from_str() {
Ok(ymd(2014, 5, 7))
); // ignore time and offset
assert_eq!(
NaiveDate::parse_from_str("2015-W06-1=2015-033", "%G-W%V-%u = %Y-%j"),
NaiveDate::parse_from_str("2015-W06-1=2015-033 Q1", "%G-W%V-%u = %Y-%j Q%q"),
Ok(ymd(2015, 2, 2))
);
assert_eq!(NaiveDate::parse_from_str("Fri, 09 Aug 13", "%a, %d %b %y"), Ok(ymd(2013, 8, 9)));
assert!(NaiveDate::parse_from_str("Sat, 09 Aug 2013", "%a, %d %b %Y").is_err());
assert!(NaiveDate::parse_from_str("2014-57", "%Y-%m-%d").is_err());
assert!(NaiveDate::parse_from_str("2014", "%Y").is_err()); // insufficient

assert!(NaiveDate::parse_from_str("2014-5-7 Q3", "%Y-%m-%d Q%q").is_err()); // mismatched quarter

assert_eq!(
NaiveDate::parse_from_str("2020-01-0", "%Y-%W-%w").ok(),
NaiveDate::from_ymd_opt(2020, 1, 12),
Expand Down
2 changes: 1 addition & 1 deletion src/naive/datetime/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ fn test_datetime_parse_from_str() {
NaiveDateTime::parse_from_str("Sat, 09 Aug 2013 23:54:35 GMT", "%a, %d %b %Y %H:%M:%S GMT")
.is_err()
);
assert!(NaiveDateTime::parse_from_str("2014-5-7 12:3456", "%Y-%m-%d %H:%M:%S").is_err());
assert!(NaiveDateTime::parse_from_str("2014-5-7 Q2 12:3456", "%Y-%m-%d Q%q %H:%M:%S").is_err());
assert!(NaiveDateTime::parse_from_str("12:34:56", "%H:%M:%S").is_err()); // insufficient
assert_eq!(
NaiveDateTime::parse_from_str("1441497364", "%s"),
Expand Down
8 changes: 8 additions & 0 deletions src/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ pub trait Datelike: Sized {
if year < 1 { (false, (1 - year) as u32) } else { (true, year as u32) }
}

/// Returns the quarter number starting from 1.
///
/// The return value ranges from 1 to 4.
#[inline]
fn quarter(&self) -> u32 {
Copy link

@tustvold tustvold Feb 26, 2025

Choose a reason for hiding this comment

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

Just as an FYI this resulted in our arrow-rs builds failing with the latest chrono patch release.

We have codepaths generic on ChronoDateExt + Datelike and this now results in trait ambiguity.

I don't really know if this counts as a breaking change or not... I feel it shouldn't, but it broke our build so maybe it is?? I don't know...

Edit: The official guidance on semver highlights this as a potential issue but says it is up to maintainers to decide - https://doc.rust-lang.org/cargo/reference/semver.html#trait-new-default-item

Copy link
Member

Choose a reason for hiding this comment

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

See https://doc.rust-lang.org/cargo/reference/semver.html#trait-new-default-item. In general, with extension traits you're going to be somewhat more likely to be broken, especially if you pick very generic method names.

Choose a reason for hiding this comment

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

So I am a little unsure how best to proceed here, in particular what changes arrow-rs should be making to ensure this doesn't occur again. Other than pinning chrono to a specific patch version, I can't see a tractable way to avoid a reoccurrence?

Switching all callsites to use explicit disambiguation syntax when a chrono trait is in scope is not really a viable option given the extent to which the codebase makes use of traits and generics, and that isn't even considering our downstream consumers...

Maybe it is a case of do nothing now and only do something if it happens again, but I also wonder if chrono might consider not adding new methods to traits in patch releases, especially given how broad its user base is?

Copy link
Member

Choose a reason for hiding this comment

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

I wouldn't worry about it too much, I think this will be relatively rare.

Copy link
Member

Choose a reason for hiding this comment

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

if chrono might consider not adding new methods to traits in patch releases, especially given how broad its user base is?

I'm not sure this is tenable. Unlike arrow which does regular incompatible releases, chrono is firmly entrenched as low-level library for which semver-incompatible bumps are pretty painful.

Copy link
Member

Choose a reason for hiding this comment

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

If you want to influence the chrono roadmap feel free to contact me for a commercial arrangement. I'm not going to yank this release because your team is unable to manage Cargo.lock files.

Choose a reason for hiding this comment

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

@djc why not just yank the release and push this as 0.5.0 like semver conventions would suggest?

Copy link
Member

Choose a reason for hiding this comment

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

@djc why not just yank the release and push this as 0.5.0 like semver conventions would suggest?

Your understanding of semver conventions as practiced in the crates.io ecosystem seems limited at best, and you seemingly have no idea of the impact of semver-incompatible releases for foundational libraries like chrono. Please take your uninformed "why don't you just" elsewhere.

Copy link

Choose a reason for hiding this comment

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

Sadly it is for legal reasons...

Hi, it is allowed by the ASF to publish hot fixes without a three-day voting period after a public discussion.

Copy link

Choose a reason for hiding this comment

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

@djc If chrono is "firmly entrenched as a low-level library", maybe it's time for it to graduate to version 1.x?

Regardless of whether publishing breaking changes to patch-level increments is considered OK by Cargo's semver specification, it displays a degree of immaturity for something as widely used as chrono to do that.

(self.month() - 1).div_euclid(3) + 1
}

/// Returns the month number starting from 1.
///
/// The return value ranges from 1 to 12.
Expand Down
2 changes: 1 addition & 1 deletion tests/dateutils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ fn try_verify_against_date_command() {
#[cfg(target_os = "linux")]
fn verify_against_date_command_format_local(path: &'static str, dt: NaiveDateTime) {
let required_format =
"d%d D%D F%F H%H I%I j%j k%k l%l m%m M%M S%S T%T u%u U%U w%w W%W X%X y%y Y%Y z%:z";
"d%d D%D F%F H%H I%I j%j k%k l%l m%m M%M q%q S%S T%T u%u U%U w%w W%W X%X y%y Y%Y z%:z";
// a%a - depends from localization
// A%A - depends from localization
// b%b - depends from localization
Expand Down
Loading