Skip to content

Commit 373a64b

Browse files
committed
Add quarter (%q) date string specifier
GNU date supports %q as a date string specifier. This adds support for that in chrono. This is needed by uutils/coreutils for compability.
1 parent 1f345fd commit 373a64b

File tree

13 files changed

+116
-7
lines changed

13 files changed

+116
-7
lines changed

src/date.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,10 @@ impl<Tz: TimeZone> Datelike for Date<Tz> {
395395
self.naive_local().year()
396396
}
397397
#[inline]
398+
fn quarter(&self) -> u32 {
399+
self.naive_local().quarter()
400+
}
401+
#[inline]
398402
fn month(&self) -> u32 {
399403
self.naive_local().month()
400404
}

src/datetime/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1175,6 +1175,10 @@ impl<Tz: TimeZone> Datelike for DateTime<Tz> {
11751175
self.overflowing_naive_local().year()
11761176
}
11771177
#[inline]
1178+
fn quarter(&self) -> u32 {
1179+
self.overflowing_naive_local().quarter()
1180+
}
1181+
#[inline]
11781182
fn month(&self) -> u32 {
11791183
self.overflowing_naive_local().month()
11801184
}

src/format/formatting.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ impl<'a, I: Iterator<Item = B> + Clone, B: Borrow<Item<'a>>> DelayedFormat<I> {
185185
(IsoYearMod100, Some(d), _) => {
186186
write_two(w, d.iso_week().year().rem_euclid(100) as u8, pad)
187187
}
188+
(Quarter, Some(d), _) => write_one(w, d.quarter() as u8),
188189
(Month, Some(d), _) => write_two(w, d.month() as u8, pad),
189190
(Day, Some(d), _) => write_two(w, d.day() as u8, pad),
190191
(WeekFromSun, Some(d), _) => write_two(w, d.weeks_from(Weekday::Sun) as u8, pad),
@@ -657,6 +658,7 @@ mod tests {
657658
let d = NaiveDate::from_ymd_opt(2012, 3, 4).unwrap();
658659
assert_eq!(d.format("%Y,%C,%y,%G,%g").to_string(), "2012,20,12,2012,12");
659660
assert_eq!(d.format("%m,%b,%h,%B").to_string(), "03,Mar,Mar,March");
661+
assert_eq!(d.format("%q").to_string(), "1");
660662
assert_eq!(d.format("%d,%e").to_string(), "04, 4");
661663
assert_eq!(d.format("%U,%W,%V").to_string(), "10,09,09");
662664
assert_eq!(d.format("%a,%A,%w,%u").to_string(), "Sun,Sunday,0,7");

src/format/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ pub enum Numeric {
115115
IsoYearDiv100,
116116
/// Year in the ISO week date, modulo 100 (FW=PW=2). Cannot be negative.
117117
IsoYearMod100,
118+
/// Quarter (FW=PW=1).
119+
Quarter,
118120
/// Month (FW=PW=2).
119121
Month,
120122
/// Day of the month (FW=PW=2).

src/format/parse.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ where
343343
IsoYear => (4, true, Parsed::set_isoyear),
344344
IsoYearDiv100 => (2, false, Parsed::set_isoyear_div_100),
345345
IsoYearMod100 => (2, false, Parsed::set_isoyear_mod_100),
346+
Quarter => (1, false, Parsed::set_quarter),
346347
Month => (2, false, Parsed::set_month),
347348
Day => (2, false, Parsed::set_day),
348349
WeekFromSun => (2, false, Parsed::set_week_from_sun),
@@ -819,9 +820,9 @@ mod tests {
819820
parsed!(year_div_100: 12, year_mod_100: 34, isoyear_div_100: 56, isoyear_mod_100: 78),
820821
);
821822
check(
822-
"1 2 3 4 5",
823-
&[num(Month), num(Day), num(WeekFromSun), num(NumDaysFromSun), num(IsoWeek)],
824-
parsed!(month: 1, day: 2, week_from_sun: 3, weekday: Weekday::Thu, isoweek: 5),
823+
"1 1 2 3 4 5",
824+
&[num(Quarter), num(Month), num(Day), num(WeekFromSun), num(NumDaysFromSun), num(IsoWeek)],
825+
parsed!(quarter: 1, month: 1, day: 2, week_from_sun: 3, weekday: Weekday::Thu, isoweek: 5),
825826
);
826827
check(
827828
"6 7 89 01",

src/format/parsed.rs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ pub struct Parsed {
140140
#[doc(hidden)]
141141
pub isoyear_mod_100: Option<i32>,
142142
#[doc(hidden)]
143+
pub quarter: Option<u32>,
144+
#[doc(hidden)]
143145
pub month: Option<u32>,
144146
#[doc(hidden)]
145147
pub week_from_sun: Option<u32>,
@@ -304,6 +306,23 @@ impl Parsed {
304306
set_if_consistent(&mut self.isoyear_mod_100, value as i32)
305307
}
306308

309+
/// Set the [`quarter`](Parsed::quarter) field to the given value.
310+
///
311+
/// Quarter 1 starts in January.
312+
///
313+
/// # Errors
314+
///
315+
/// Returns `OUT_OF_RANGE` if `value` is not in the range 1-4.
316+
///
317+
/// Returns `IMPOSSIBLE` if this field was already set to a different value.
318+
#[inline]
319+
pub fn set_quarter(&mut self, value: i64) -> ParseResult<()> {
320+
if !(1..=4).contains(&value) {
321+
return Err(OUT_OF_RANGE);
322+
}
323+
set_if_consistent(&mut self.quarter, value as u32)
324+
}
325+
307326
/// Set the [`month`](Parsed::month) field to the given value.
308327
///
309328
/// # Errors
@@ -698,7 +717,13 @@ impl Parsed {
698717
(_, _, _) => return Err(NOT_ENOUGH),
699718
};
700719

701-
if verified { Ok(parsed_date) } else { Err(IMPOSSIBLE) }
720+
// verify the quarter if we have a good parsed date so far
721+
let verify_quarter = |date: NaiveDate| {
722+
let quarter = date.quarter();
723+
self.quarter.unwrap_or(quarter) == quarter
724+
};
725+
726+
if verified && verify_quarter(parsed_date) { Ok(parsed_date) } else { Err(IMPOSSIBLE) }
702727
}
703728

704729
/// Returns a parsed naive time out of given fields.
@@ -1013,6 +1038,14 @@ impl Parsed {
10131038
self.isoyear_mod_100
10141039
}
10151040

1041+
/// Get the `quarter` field if set.
1042+
///
1043+
/// See also [`set_quarter()`](Parsed::set_quarter).
1044+
#[inline]
1045+
pub fn quarter(&self) -> Option<u32> {
1046+
self.quarter
1047+
}
1048+
10161049
/// Get the `month` field if set.
10171050
///
10181051
/// See also [`set_month()`](Parsed::set_month).
@@ -1267,6 +1300,11 @@ mod tests {
12671300
assert!(Parsed::new().set_isoyear_mod_100(99).is_ok());
12681301
assert_eq!(Parsed::new().set_isoyear_mod_100(100), Err(OUT_OF_RANGE));
12691302

1303+
assert_eq!(Parsed::new().set_quarter(0), Err(OUT_OF_RANGE));
1304+
assert!(Parsed::new().set_quarter(1).is_ok());
1305+
assert!(Parsed::new().set_quarter(4).is_ok());
1306+
assert_eq!(Parsed::new().set_quarter(5), Err(OUT_OF_RANGE));
1307+
12701308
assert_eq!(Parsed::new().set_month(0), Err(OUT_OF_RANGE));
12711309
assert!(Parsed::new().set_month(1).is_ok());
12721310
assert!(Parsed::new().set_month(12).is_ok());
@@ -1425,6 +1463,17 @@ mod tests {
14251463
assert_eq!(parse!(year: -1, year_div_100: 0, month: 1, day: 1), Err(IMPOSSIBLE));
14261464
assert_eq!(parse!(year: -1, year_mod_100: 99, month: 1, day: 1), Err(IMPOSSIBLE));
14271465

1466+
// quarters
1467+
assert_eq!(parse!(year: 2000, quarter: 1), Err(NOT_ENOUGH));
1468+
assert_eq!(parse!(year: 2000, quarter: 1, month: 1, day: 1), ymd(2000, 1, 1));
1469+
assert_eq!(parse!(year: 2000, quarter: 2, month: 4, day: 1), ymd(2000, 4, 1));
1470+
assert_eq!(parse!(year: 2000, quarter: 3, month: 7, day: 1), ymd(2000, 7, 1));
1471+
assert_eq!(parse!(year: 2000, quarter: 4, month: 10, day: 1), ymd(2000, 10, 1));
1472+
1473+
// quarter: conflicting inputs
1474+
assert_eq!(parse!(year: 2000, quarter: 2, month: 3, day: 31), Err(IMPOSSIBLE));
1475+
assert_eq!(parse!(year: 2000, quarter: 4, month: 3, day: 31), Err(IMPOSSIBLE));
1476+
14281477
// weekdates
14291478
assert_eq!(parse!(year: 2000, week_from_mon: 0), Err(NOT_ENOUGH));
14301479
assert_eq!(parse!(year: 2000, week_from_sun: 0), Err(NOT_ENOUGH));

src/format/strftime.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ The following specifiers are available both to formatting and parsing.
1515
| `%C` | `20` | The proleptic Gregorian year divided by 100, zero-padded to 2 digits. [^1] |
1616
| `%y` | `01` | The proleptic Gregorian year modulo 100, zero-padded to 2 digits. [^1] |
1717
| | | |
18+
| `%q` | `1` | Quarter of year (1-4) |
1819
| `%m` | `07` | Month number (01--12), zero-padded to 2 digits. |
1920
| `%b` | `Jul` | Abbreviated month name. Always 3 letters. |
2021
| `%B` | `July` | Full month name. Also accepts corresponding abbreviation in parsing. |
@@ -538,6 +539,7 @@ impl<'a> StrftimeItems<'a> {
538539
'm' => num0(Month),
539540
'n' => Space("\n"),
540541
'p' => fixed(Fixed::UpperAmPm),
542+
'q' => num(Quarter),
541543
#[cfg(not(feature = "unstable-locales"))]
542544
'r' => queue_from_slice!(T_FMT_AMPM),
543545
#[cfg(feature = "unstable-locales")]
@@ -866,6 +868,7 @@ mod tests {
866868
assert_eq!(dt.format("%Y").to_string(), "2001");
867869
assert_eq!(dt.format("%C").to_string(), "20");
868870
assert_eq!(dt.format("%y").to_string(), "01");
871+
assert_eq!(dt.format("%q").to_string(), "3");
869872
assert_eq!(dt.format("%m").to_string(), "07");
870873
assert_eq!(dt.format("%b").to_string(), "Jul");
871874
assert_eq!(dt.format("%B").to_string(), "July");

src/naive/date/mod.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1457,6 +1457,23 @@ impl Datelike for NaiveDate {
14571457
self.year()
14581458
}
14591459

1460+
/// Returns the quarter number starting from 1
1461+
///
1462+
/// The return value ranges from 1 to 4.
1463+
///
1464+
/// # Example
1465+
///
1466+
/// ```
1467+
/// use chrono::{Datelike, NaiveDate};
1468+
///
1469+
/// assert_eq!(NaiveDate::from_ymd_opt(2015, 9, 8).unwrap().quarter(), 3);
1470+
/// assert_eq!(NaiveDate::from_ymd_opt(-308, 3, 14).unwrap().quarter(), 1);
1471+
/// ```
1472+
#[inline]
1473+
fn quarter(&self) -> u32 {
1474+
(self.month() - 1).div_euclid(3) + 1
1475+
}
1476+
14601477
/// Returns the month number starting from 1.
14611478
///
14621479
/// The return value ranges from 1 to 12.

src/naive/date/tests.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -666,14 +666,16 @@ fn test_date_parse_from_str() {
666666
Ok(ymd(2014, 5, 7))
667667
); // ignore time and offset
668668
assert_eq!(
669-
NaiveDate::parse_from_str("2015-W06-1=2015-033", "%G-W%V-%u = %Y-%j"),
669+
NaiveDate::parse_from_str("2015-W06-1=2015-033 Q1", "%G-W%V-%u = %Y-%j Q%q"),
670670
Ok(ymd(2015, 2, 2))
671671
);
672672
assert_eq!(NaiveDate::parse_from_str("Fri, 09 Aug 13", "%a, %d %b %y"), Ok(ymd(2013, 8, 9)));
673673
assert!(NaiveDate::parse_from_str("Sat, 09 Aug 2013", "%a, %d %b %Y").is_err());
674674
assert!(NaiveDate::parse_from_str("2014-57", "%Y-%m-%d").is_err());
675675
assert!(NaiveDate::parse_from_str("2014", "%Y").is_err()); // insufficient
676676

677+
assert!(NaiveDate::parse_from_str("2014-5-7 Q3", "%Y-%m-%d Q%q").is_err()); // mismatched quarter
678+
677679
assert_eq!(
678680
NaiveDate::parse_from_str("2020-01-0", "%Y-%W-%w").ok(),
679681
NaiveDate::from_ymd_opt(2020, 1, 12),

src/naive/datetime/mod.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -984,6 +984,26 @@ impl Datelike for NaiveDateTime {
984984
self.date.year()
985985
}
986986

987+
/// Returns the quarter number starting from 1
988+
///
989+
/// The return value ranges from 1 to 4.
990+
///
991+
/// See also the [`NaiveDate::quarter`](./struct.NaiveDate.html#method.quarter) method.
992+
///
993+
/// # Example
994+
///
995+
/// ```
996+
/// use chrono::{Datelike, NaiveDate, NaiveDateTime};
997+
///
998+
/// let dt: NaiveDateTime =
999+
/// NaiveDate::from_ymd_opt(2015, 9, 25).unwrap().and_hms_opt(12, 34, 56).unwrap();
1000+
/// assert_eq!(dt.quarter(), 3);
1001+
/// ```
1002+
#[inline]
1003+
fn quarter(&self) -> u32 {
1004+
self.date.quarter()
1005+
}
1006+
9871007
/// Returns the month number starting from 1.
9881008
///
9891009
/// The return value ranges from 1 to 12.

0 commit comments

Comments
 (0)