Skip to content

Commit

Permalink
Add quarter (%q) date string specifier
Browse files Browse the repository at this point in the history
GNU date supports %q as a date string specifier. This adds
support for that in chrono.

This is needed by uutils/coreutils for compability.
  • Loading branch information
drinkcat authored and djc committed Feb 25, 2025
1 parent 07216ae commit 6d29c8a
Show file tree
Hide file tree
Showing 9 changed files with 83 additions and 7 deletions.
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 @@ pub struct Parsed {
#[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 @@ impl Parsed {
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 @@ impl Parsed {
(_, _, _) => 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 @@ impl Parsed {
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
}

/// Get the `month` field if set.
///
/// See also [`set_month()`](Parsed::set_month).
Expand Down Expand Up @@ -1267,6 +1302,11 @@ mod tests {
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 @@ mod tests {
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 {
(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

0 comments on commit 6d29c8a

Please sign in to comment.