Skip to content

Commit 6d29c8a

Browse files
drinkcatdjc
authored andcommitted
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 07216ae commit 6d29c8a

File tree

9 files changed

+83
-7
lines changed

9 files changed

+83
-7
lines changed

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: 11 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,16 @@ 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+
&[
825+
num(Quarter),
826+
num(Month),
827+
num(Day),
828+
num(WeekFromSun),
829+
num(NumDaysFromSun),
830+
num(IsoWeek),
831+
],
832+
parsed!(quarter: 1, month: 1, day: 2, week_from_sun: 3, weekday: Weekday::Thu, isoweek: 5),
825833
);
826834
check(
827835
"6 7 89 01",

src/format/parsed.rs

Lines changed: 52 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,15 @@ impl Parsed {
698717
(_, _, _) => return Err(NOT_ENOUGH),
699718
};
700719

701-
if verified { Ok(parsed_date) } else { Err(IMPOSSIBLE) }
720+
if !verified {
721+
return Err(IMPOSSIBLE);
722+
} else if let Some(parsed) = self.quarter {
723+
if parsed != parsed_date.quarter() {
724+
return Err(IMPOSSIBLE);
725+
}
726+
}
727+
728+
Ok(parsed_date)
702729
}
703730

704731
/// Returns a parsed naive time out of given fields.
@@ -1013,6 +1040,14 @@ impl Parsed {
10131040
self.isoyear_mod_100
10141041
}
10151042

1043+
/// Get the `quarter` field if set.
1044+
///
1045+
/// See also [`set_quarter()`](Parsed::set_quarter).
1046+
#[inline]
1047+
pub fn quarter(&self) -> Option<u32> {
1048+
self.quarter
1049+
}
1050+
10161051
/// Get the `month` field if set.
10171052
///
10181053
/// See also [`set_month()`](Parsed::set_month).
@@ -1267,6 +1302,11 @@ mod tests {
12671302
assert!(Parsed::new().set_isoyear_mod_100(99).is_ok());
12681303
assert_eq!(Parsed::new().set_isoyear_mod_100(100), Err(OUT_OF_RANGE));
12691304

1305+
assert_eq!(Parsed::new().set_quarter(0), Err(OUT_OF_RANGE));
1306+
assert!(Parsed::new().set_quarter(1).is_ok());
1307+
assert!(Parsed::new().set_quarter(4).is_ok());
1308+
assert_eq!(Parsed::new().set_quarter(5), Err(OUT_OF_RANGE));
1309+
12701310
assert_eq!(Parsed::new().set_month(0), Err(OUT_OF_RANGE));
12711311
assert!(Parsed::new().set_month(1).is_ok());
12721312
assert!(Parsed::new().set_month(12).is_ok());
@@ -1425,6 +1465,17 @@ mod tests {
14251465
assert_eq!(parse!(year: -1, year_div_100: 0, month: 1, day: 1), Err(IMPOSSIBLE));
14261466
assert_eq!(parse!(year: -1, year_mod_100: 99, month: 1, day: 1), Err(IMPOSSIBLE));
14271467

1468+
// quarters
1469+
assert_eq!(parse!(year: 2000, quarter: 1), Err(NOT_ENOUGH));
1470+
assert_eq!(parse!(year: 2000, quarter: 1, month: 1, day: 1), ymd(2000, 1, 1));
1471+
assert_eq!(parse!(year: 2000, quarter: 2, month: 4, day: 1), ymd(2000, 4, 1));
1472+
assert_eq!(parse!(year: 2000, quarter: 3, month: 7, day: 1), ymd(2000, 7, 1));
1473+
assert_eq!(parse!(year: 2000, quarter: 4, month: 10, day: 1), ymd(2000, 10, 1));
1474+
1475+
// quarter: conflicting inputs
1476+
assert_eq!(parse!(year: 2000, quarter: 2, month: 3, day: 31), Err(IMPOSSIBLE));
1477+
assert_eq!(parse!(year: 2000, quarter: 4, month: 3, day: 31), Err(IMPOSSIBLE));
1478+
14281479
// weekdates
14291480
assert_eq!(parse!(year: 2000, week_from_mon: 0), Err(NOT_ENOUGH));
14301481
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/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/tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ fn test_datetime_parse_from_str() {
203203
NaiveDateTime::parse_from_str("Sat, 09 Aug 2013 23:54:35 GMT", "%a, %d %b %Y %H:%M:%S GMT")
204204
.is_err()
205205
);
206-
assert!(NaiveDateTime::parse_from_str("2014-5-7 12:3456", "%Y-%m-%d %H:%M:%S").is_err());
206+
assert!(NaiveDateTime::parse_from_str("2014-5-7 Q2 12:3456", "%Y-%m-%d Q%q %H:%M:%S").is_err());
207207
assert!(NaiveDateTime::parse_from_str("12:34:56", "%H:%M:%S").is_err()); // insufficient
208208
assert_eq!(
209209
NaiveDateTime::parse_from_str("1441497364", "%s"),

src/traits.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ pub trait Datelike: Sized {
4040
if year < 1 { (false, (1 - year) as u32) } else { (true, year as u32) }
4141
}
4242

43+
/// Returns the quarter number starting from 1.
44+
///
45+
/// The return value ranges from 1 to 4.
46+
#[inline]
47+
fn quarter(&self) -> u32 {
48+
(self.month() - 1).div_euclid(3) + 1
49+
}
50+
4351
/// Returns the month number starting from 1.
4452
///
4553
/// The return value ranges from 1 to 12.

tests/dateutils.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ fn try_verify_against_date_command() {
110110
#[cfg(target_os = "linux")]
111111
fn verify_against_date_command_format_local(path: &'static str, dt: NaiveDateTime) {
112112
let required_format =
113-
"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";
113+
"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";
114114
// a%a - depends from localization
115115
// A%A - depends from localization
116116
// b%b - depends from localization

0 commit comments

Comments
 (0)