2015-01-26 12:14:50 +00:00
|
|
|
// This is a part of rust-chrono.
|
|
|
|
// Copyright (c) 2015, Kang Seonghoon.
|
|
|
|
// See README.md and LICENSE.txt for details.
|
|
|
|
|
|
|
|
/*!
|
|
|
|
`strftime`/`strptime`-inspired date and time formatting syntax.
|
|
|
|
|
|
|
|
## Specifiers
|
|
|
|
|
|
|
|
The following specifiers are available both to formatting and parsing.
|
|
|
|
|
|
|
|
```plain
|
|
|
|
Spec. Example Description
|
|
|
|
----- ------- -----------
|
|
|
|
|
|
|
|
DATE SPECIFIERS:
|
|
|
|
|
|
|
|
%Y 2001 The full proleptic Gregorian year, zero-padded to 4 digits.
|
|
|
|
Negative years are allowed in formatting but not in parsing.
|
|
|
|
%C 20 The proleptic Gregorian year divided by 100, zero-padded to 2 digits.
|
|
|
|
This is floor division, so 100 BCE (year number -99) will print `-1`.
|
|
|
|
%y 01 The proleptic Gregorian year modulo 100, zero-padded to 2 digits.
|
|
|
|
This is floor division, so 100 BCE (year number -99) will print `99`.
|
|
|
|
|
|
|
|
%m 07 Month number (01--12), zero-padded to 2 digits.
|
|
|
|
%b Jul Abbreviated month name. Always 3 letters.
|
2015-02-04 17:37:54 +00:00
|
|
|
%B July Full month name. Also accepts corresponding abbreviation in parsing.
|
2015-01-26 12:14:50 +00:00
|
|
|
%h Jul Same to `%b`.
|
|
|
|
|
|
|
|
%d 08 Day number (01--31), zero-padded to 2 digits.
|
|
|
|
%e 8 Same to `%d` but space-padded.
|
|
|
|
|
|
|
|
%a Sun Abbreviated weekday name. Always 3 letters.
|
2015-02-04 17:37:54 +00:00
|
|
|
%A Sunday Full weekday name. Also accepts corresponding abbreviation in parsing.
|
2015-01-26 12:14:50 +00:00
|
|
|
%w 0 Sunday = 0, Monday = 1, ..., Saturday = 6.
|
|
|
|
%u 7 Monday = 1, Tuesday = 2, ..., Sunday = 7. (ISO 8601)
|
|
|
|
|
|
|
|
%U 28 Week number (00--53), zero-padded to 2 digits.
|
|
|
|
Week 1 starts with the first Sunday in that year.
|
|
|
|
It is possible to have week 0 for days before the first Sunday.
|
|
|
|
%W 27 Same to `%U`, but week 1 starts with the first Monday in that year.
|
|
|
|
|
|
|
|
%G 2001 Same to `%Y` but uses the year number in ISO 8601 week date.
|
|
|
|
%g 01 Same to `%y` but uses the year number in ISO 8601 week date.
|
|
|
|
%V 27 Same to `%U` but uses the week number in ISO 8601 week date (01--53).
|
|
|
|
Week 1 is the first week with at least 4 days in that year.
|
|
|
|
Week 0 does not exist, so this should be used with `%G` or `%g`.
|
|
|
|
|
|
|
|
%j 189 Day of the year (001--366), zero-padded to 3 digits.
|
|
|
|
|
|
|
|
%D 08/07/2001 Month-day-year format. Same to `%m/%d/%Y`.
|
|
|
|
%x 08/07/2001 Same to `%D`.
|
|
|
|
%F 2001-07-08 Year-month-day format (ISO 8601). Same to `%Y-%m-%d`.
|
|
|
|
%v 7-Jul-2001 Day-month-year format. Same to `%e-%b-%Y`.
|
|
|
|
|
|
|
|
TIME SPECIFIERS:
|
|
|
|
|
|
|
|
%H 00 Hour number (00--23), zero-padded to 2 digits.
|
|
|
|
%k 0 Same to `%H` but space-padded.
|
|
|
|
%I 12 Hour number in 12-hour clocks (01--12), zero-padded to 2 digits.
|
|
|
|
%l 12 Same to `%I` but space-padded.
|
|
|
|
|
|
|
|
%P am `am` or `pm` in 12-hour clocks.
|
|
|
|
%p AM `AM` or `PM` in 12-hour clocks.
|
|
|
|
|
|
|
|
%M 34 Minute number (00--59), zero-padded to 2 digits.
|
|
|
|
%S 60 Second number (00--60), zero-padded to 2 digits.
|
|
|
|
It accounts for leap seconds, so `60` is possible.
|
|
|
|
%f 026413966 The number of nanoseconds since the last whole second,
|
|
|
|
zero-padded to 9 digits.
|
|
|
|
|
|
|
|
%R 00:34 Hour-minute format. Same to `%H:%M`.
|
|
|
|
%T 00:34:60 Hour-minute-second format. Same to `%H:%M:%S`.
|
|
|
|
%x 00:34:60 Same to `%T`.
|
|
|
|
%r 12:34:60 AM Hour-minute-second format in 12-hour clocks. Same to `%I:%M:%S %p`.
|
|
|
|
|
|
|
|
TIME ZONE SPECIFIERS:
|
|
|
|
|
|
|
|
%Z ACST Local time zone name.
|
|
|
|
%z +09:30 Offset from the local time to UTC (with UTC being `+00:00`).
|
|
|
|
|
|
|
|
DATE & TIME SPECIFIERS:
|
|
|
|
|
|
|
|
%c Sun Jul 8 00:34:60 2001
|
|
|
|
`ctime` date & time format. Same to `%a %b %e %T %Y`. (No newline!)
|
|
|
|
%+ 2001-07-08T00:34:60+09:30
|
2015-02-15 12:26:11 +00:00
|
|
|
ISO 8601 date & time format. Almost same to `%Y-%m-%dT%H:%M:%S%z`.
|
2015-01-26 12:14:50 +00:00
|
|
|
|
|
|
|
%s 994485899 UNIX timestamp, the number of seconds since 1970-01-01 00:00 UTC.
|
|
|
|
This is not padded and can be negative.
|
|
|
|
For the purpose of Chrono, it only accounts for non-leap seconds
|
|
|
|
so it slightly differs from ISO C `strftime` behavior.
|
|
|
|
|
|
|
|
SPECIAL SPECIFIERS:
|
|
|
|
|
|
|
|
%t Literal tab (`\t`).
|
|
|
|
%n Literal newline (`\n`).
|
|
|
|
%% Literal percent sign.
|
|
|
|
```
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
use super::{Item, Numeric, Fixed, Pad};
|
|
|
|
|
|
|
|
/// Parsing iterator for `strftime`-like format strings.
|
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct StrftimeItems<'a> {
|
|
|
|
/// Remaining portion of the string.
|
|
|
|
remainder: &'a str,
|
|
|
|
/// If the current specifier is composed of multiple formatting items (e.g. `%+`),
|
|
|
|
/// parser refers to the statically reconstructed slice of them.
|
|
|
|
/// If `recons` is not empty they have to be returned earlier than the `remainder`.
|
|
|
|
recons: &'static [Item<'static>],
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> StrftimeItems<'a> {
|
|
|
|
/// Creates a new parsing iterator from the `strftime`-like format string.
|
|
|
|
pub fn new(s: &'a str) -> StrftimeItems<'a> {
|
|
|
|
static FMT_NONE: [Item<'static>; 0] = [];
|
|
|
|
StrftimeItems { remainder: s, recons: &FMT_NONE }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> Iterator for StrftimeItems<'a> {
|
|
|
|
type Item = Item<'a>;
|
|
|
|
|
|
|
|
fn next(&mut self) -> Option<Item<'a>> {
|
|
|
|
// we have some reconstructed items to return
|
|
|
|
if !self.recons.is_empty() {
|
|
|
|
let item = self.recons[0];
|
|
|
|
self.recons = &self.recons[1..];
|
|
|
|
return Some(item);
|
|
|
|
}
|
|
|
|
|
|
|
|
match self.remainder.slice_shift_char() {
|
|
|
|
// we are done
|
|
|
|
None => return None,
|
|
|
|
|
|
|
|
// the next item is a specifier
|
|
|
|
Some(('%', remainder)) => {
|
|
|
|
self.remainder = remainder;
|
|
|
|
|
|
|
|
let (spec, remainder) = match self.remainder.slice_shift_char() {
|
|
|
|
Some(x) => x,
|
|
|
|
None => return Some(Item::Error), // premature end of string
|
|
|
|
};
|
|
|
|
self.remainder = remainder;
|
|
|
|
|
|
|
|
macro_rules! recons {
|
|
|
|
[$head:expr, $($tail:expr),+] => ({
|
|
|
|
const RECONS: &'static [Item<'static>] = &[$($tail),+];
|
|
|
|
self.recons = RECONS;
|
|
|
|
$head
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
match spec {
|
|
|
|
'A' => Some(fix!(LongWeekdayName)),
|
|
|
|
'B' => Some(fix!(LongMonthName)),
|
|
|
|
'C' => Some(num0!(YearDiv100)),
|
|
|
|
'D' => Some(recons![num0!(Month), lit!("/"), num0!(Day), lit!("/"),
|
|
|
|
num0!(YearMod100)]),
|
2015-02-17 15:00:30 +00:00
|
|
|
'F' => Some(recons![num0!(Year), lit!("-"), num0!(Month), lit!("-"),
|
2015-01-26 12:14:50 +00:00
|
|
|
num0!(Day)]),
|
2015-02-17 15:00:30 +00:00
|
|
|
'G' => Some(num0!(IsoYear)),
|
2015-01-26 12:14:50 +00:00
|
|
|
'H' => Some(num0!(Hour)),
|
|
|
|
'I' => Some(num0!(Hour12)),
|
|
|
|
'M' => Some(num0!(Minute)),
|
|
|
|
'P' => Some(fix!(LowerAmPm)),
|
|
|
|
'R' => Some(recons![num0!(Hour), lit!(":"), num0!(Minute)]),
|
|
|
|
'S' => Some(num0!(Second)),
|
|
|
|
'T' => Some(recons![num0!(Hour), lit!(":"), num0!(Minute), lit!(":"),
|
|
|
|
num0!(Second)]),
|
|
|
|
'U' => Some(num0!(WeekFromSun)),
|
|
|
|
'V' => Some(num0!(IsoWeek)),
|
|
|
|
'W' => Some(num0!(WeekFromMon)),
|
|
|
|
'X' => Some(recons![num0!(Hour), lit!(":"), num0!(Minute), lit!(":"),
|
|
|
|
num0!(Second)]),
|
2015-02-17 15:00:30 +00:00
|
|
|
'Y' => Some(num0!(Year)),
|
2015-01-26 12:14:50 +00:00
|
|
|
'Z' => Some(fix!(TimezoneName)),
|
|
|
|
'a' => Some(fix!(ShortWeekdayName)),
|
|
|
|
'b' => Some(fix!(ShortMonthName)),
|
|
|
|
'c' => Some(recons![fix!(ShortWeekdayName), sp!(" "), fix!(ShortMonthName),
|
|
|
|
sp!(" "), nums!(Day), sp!(" "), num0!(Hour), lit!(":"),
|
|
|
|
num0!(Minute), lit!(":"), num0!(Second), sp!(" "),
|
2015-02-17 15:00:30 +00:00
|
|
|
num0!(Year)]),
|
2015-01-26 12:14:50 +00:00
|
|
|
'd' => Some(num0!(Day)),
|
|
|
|
'e' => Some(nums!(Day)),
|
|
|
|
'f' => Some(num0!(Nanosecond)),
|
|
|
|
'g' => Some(num0!(IsoYearMod100)),
|
|
|
|
'h' => Some(fix!(ShortMonthName)),
|
|
|
|
'j' => Some(num0!(Ordinal)),
|
|
|
|
'k' => Some(nums!(Hour)),
|
|
|
|
'l' => Some(nums!(Hour12)),
|
|
|
|
'm' => Some(num0!(Month)),
|
|
|
|
'n' => Some(sp!("\n")),
|
|
|
|
'p' => Some(fix!(UpperAmPm)),
|
|
|
|
'r' => Some(recons![num0!(Hour12), lit!(":"), num0!(Minute), lit!(":"),
|
|
|
|
num0!(Second), sp!(" "), fix!(UpperAmPm)]),
|
|
|
|
's' => Some(num!(Timestamp)),
|
|
|
|
't' => Some(sp!("\t")),
|
|
|
|
'u' => Some(num!(WeekdayFromMon)),
|
|
|
|
'v' => Some(recons![nums!(Day), lit!("-"), fix!(ShortMonthName), lit!("-"),
|
2015-02-17 15:00:30 +00:00
|
|
|
num0!(Year)]),
|
2015-01-26 12:14:50 +00:00
|
|
|
'w' => Some(num!(NumDaysFromSun)),
|
|
|
|
'x' => Some(recons![num0!(Month), lit!("/"), num0!(Day), lit!("/"),
|
|
|
|
num0!(YearMod100)]),
|
|
|
|
'y' => Some(num0!(YearMod100)),
|
|
|
|
'z' => Some(fix!(TimezoneOffset)),
|
2015-02-15 12:26:11 +00:00
|
|
|
'+' => Some(fix!(RFC3339)),
|
2015-01-26 12:14:50 +00:00
|
|
|
'%' => Some(lit!("%")),
|
|
|
|
_ => Some(Item::Error), // no such specifier
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
// the next item is space
|
|
|
|
Some((c, _)) if c.is_whitespace() => {
|
|
|
|
// `%` is not a whitespace, so `c != '%'` is redundant
|
2015-03-02 11:50:03 +00:00
|
|
|
let nextspec = self.remainder.find(|c: char| !c.is_whitespace())
|
2015-01-26 12:14:50 +00:00
|
|
|
.unwrap_or(self.remainder.len());
|
|
|
|
assert!(nextspec > 0);
|
|
|
|
let item = sp!(&self.remainder[..nextspec]);
|
|
|
|
self.remainder = &self.remainder[nextspec..];
|
|
|
|
Some(item)
|
|
|
|
},
|
|
|
|
|
|
|
|
// the next item is literal
|
|
|
|
_ => {
|
2015-03-02 11:50:03 +00:00
|
|
|
let nextspec = self.remainder.find(|c: char| c.is_whitespace() || c == '%')
|
2015-01-26 12:14:50 +00:00
|
|
|
.unwrap_or(self.remainder.len());
|
|
|
|
assert!(nextspec > 0);
|
|
|
|
let item = lit!(&self.remainder[..nextspec]);
|
|
|
|
self.remainder = &self.remainder[nextspec..];
|
|
|
|
Some(item)
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
#[test]
|
|
|
|
fn test_strftime_items() {
|
|
|
|
fn parse_and_collect<'a>(s: &'a str) -> Vec<Item<'a>> {
|
|
|
|
// map any error into `[Item::Error]`. useful for easy testing.
|
|
|
|
let items = StrftimeItems::new(s);
|
|
|
|
let items = items.map(|spec| if spec == Item::Error {None} else {Some(spec)});
|
|
|
|
items.collect::<Option<Vec<_>>>().unwrap_or(vec![Item::Error])
|
|
|
|
}
|
|
|
|
|
|
|
|
assert_eq!(parse_and_collect(""), []);
|
|
|
|
assert_eq!(parse_and_collect(" \t\n\r "), [sp!(" \t\n\r ")]);
|
|
|
|
assert_eq!(parse_and_collect("hello?"), [lit!("hello?")]);
|
|
|
|
assert_eq!(parse_and_collect("a b\t\nc"), [lit!("a"), sp!(" "), lit!("b"), sp!("\t\n"),
|
|
|
|
lit!("c")]);
|
|
|
|
assert_eq!(parse_and_collect("100%%"), [lit!("100"), lit!("%")]);
|
|
|
|
assert_eq!(parse_and_collect("100%% ok"), [lit!("100"), lit!("%"), sp!(" "), lit!("ok")]);
|
|
|
|
assert_eq!(parse_and_collect("%%PDF-1.0"), [lit!("%"), lit!("PDF-1.0")]);
|
2015-02-17 15:00:30 +00:00
|
|
|
assert_eq!(parse_and_collect("%Y-%m-%d"), [num0!(Year), lit!("-"), num0!(Month), lit!("-"),
|
2015-01-26 12:14:50 +00:00
|
|
|
num0!(Day)]);
|
|
|
|
assert_eq!(parse_and_collect("[%F]"), parse_and_collect("[%Y-%m-%d]"));
|
|
|
|
assert_eq!(parse_and_collect("%m %d"), [num0!(Month), sp!(" "), num0!(Day)]);
|
|
|
|
assert_eq!(parse_and_collect("%"), [Item::Error]);
|
|
|
|
assert_eq!(parse_and_collect("%%"), [lit!("%")]);
|
|
|
|
assert_eq!(parse_and_collect("%%%"), [Item::Error]);
|
|
|
|
assert_eq!(parse_and_collect("%%%%"), [lit!("%"), lit!("%")]);
|
|
|
|
assert_eq!(parse_and_collect("foo%?"), [Item::Error]);
|
|
|
|
assert_eq!(parse_and_collect("bar%42"), [Item::Error]);
|
|
|
|
assert_eq!(parse_and_collect("quux% +"), [Item::Error]);
|
|
|
|
}
|
|
|
|
|