new formatting specifiers to `strftime` syntax.

- Padding modifiers `%_?`, `%-?` and `%0?` are implemented.
  They are glibc extensions which seem to be reasonably widespread
  (e.g. Ruby).

- Added `%:z` specifier and corresponding formatting items
  which is essentially same to `%z` but with a colon.

- Added a new specifier `%.f` which precision adapts from the input.
  Also clearly documented the differences between `%f` and `%.f`. (#40)
This commit is contained in:
Kang Seonghoon 2015-07-05 15:00:11 +09:00
parent 5bff8170ff
commit 1f40b03ab9
3 changed files with 151 additions and 70 deletions

View File

@ -133,12 +133,16 @@ pub enum Fixed {
/// ///
/// In the parser, the colon can be omitted and/or surrounded with any amount of whitespaces. /// In the parser, the colon can be omitted and/or surrounded with any amount of whitespaces.
/// The offset is limited from `-24:00` to `+24:00`, which is same to `FixedOffset`'s range. /// The offset is limited from `-24:00` to `+24:00`, which is same to `FixedOffset`'s range.
TimezoneOffset, TimezoneOffsetColon,
/// Offset from the local time to UTC (`+09:00` or `-04:00` or `Z`). /// Offset from the local time to UTC (`+09:00` or `-04:00` or `Z`).
/// ///
/// In the parser, the colon can be omitted and/or surrounded with any amount of whitespaces, /// In the parser, the colon can be omitted and/or surrounded with any amount of whitespaces,
/// and `Z` can be either in upper case or in lower case. /// and `Z` can be either in upper case or in lower case.
/// The offset is limited from `-24:00` to `+24:00`, which is same to `FixedOffset`'s range. /// The offset is limited from `-24:00` to `+24:00`, which is same to `FixedOffset`'s range.
TimezoneOffsetColonZ,
/// Same to `TimezoneOffsetColon` but prints no colon. Parsing allows an optional colon.
TimezoneOffset,
/// Same to `TimezoneOffsetColonZ` but prints no colon. Parsing allows an optional colon.
TimezoneOffsetZ, TimezoneOffsetZ,
/// RFC 2822 date and time syntax. Commonly used for email and MIME date and time. /// RFC 2822 date and time syntax. Commonly used for email and MIME date and time.
RFC2822, RFC2822,
@ -364,6 +368,10 @@ pub fn format<'a, I>(w: &mut fmt::Formatter, date: Option<&NaiveDate>, time: Opt
}), }),
TimezoneName => TimezoneName =>
off.map(|&(ref name, _)| write!(w, "{}", *name)), off.map(|&(ref name, _)| write!(w, "{}", *name)),
TimezoneOffsetColon =>
off.map(|&(_, off)| write_local_minus_utc(w, off, false, true)),
TimezoneOffsetColonZ =>
off.map(|&(_, off)| write_local_minus_utc(w, off, true, true)),
TimezoneOffset => TimezoneOffset =>
off.map(|&(_, off)| write_local_minus_utc(w, off, false, false)), off.map(|&(_, off)| write_local_minus_utc(w, off, false, false)),
TimezoneOffsetZ => TimezoneOffsetZ =>

View File

@ -309,13 +309,13 @@ pub fn parse<'a, I>(parsed: &mut Parsed, mut s: &str, items: I) -> ParseResult<(
TimezoneName => return Err(BAD_FORMAT), TimezoneName => return Err(BAD_FORMAT),
TimezoneOffset => { TimezoneOffsetColon | TimezoneOffset => {
let offset = try_consume!(scan::timezone_offset(s.trim_left(), let offset = try_consume!(scan::timezone_offset(s.trim_left(),
scan::colon_or_space)); scan::colon_or_space));
try!(parsed.set_offset(offset as i64)); try!(parsed.set_offset(offset as i64));
} }
TimezoneOffsetZ => { TimezoneOffsetColonZ | TimezoneOffsetZ => {
let offset = try_consume!(scan::timezone_offset_zulu(s.trim_left(), let offset = try_consume!(scan::timezone_offset_zulu(s.trim_left(),
scan::colon_or_space)); scan::colon_or_space));
try!(parsed.set_offset(offset as i64)); try!(parsed.set_offset(offset as i64));

View File

@ -22,7 +22,7 @@ Spec. | Example | Description
`%h` | `Jul` | Same to `%b`. `%h` | `Jul` | Same to `%b`.
| | | |
`%d` | `08` | Day number (01--31), zero-padded to 2 digits. `%d` | `08` | Day number (01--31), zero-padded to 2 digits.
`%e` | ` 8` | Same to `%d` but space-padded. `%e` | ` 8` | Same to `%d` but space-padded. Same to `%_d`.
| | | |
`%a` | `Sun` | Abbreviated weekday name. Always 3 letters. `%a` | `Sun` | Abbreviated weekday name. Always 3 letters.
`%A` | `Sunday` | Full weekday name. Also accepts corresponding abbreviation in parsing. `%A` | `Sunday` | Full weekday name. Also accepts corresponding abbreviation in parsing.
@ -45,16 +45,17 @@ Spec. | Example | Description
| | | |
| | **TIME SPECIFIERS:** | | **TIME SPECIFIERS:**
`%H` | `00` | Hour number (00--23), zero-padded to 2 digits. `%H` | `00` | Hour number (00--23), zero-padded to 2 digits.
`%k` | ` 0` | Same to `%H` but space-padded. `%k` | ` 0` | Same to `%H` but space-padded. Same to `%_H`.
`%I` | `12` | Hour number in 12-hour clocks (01--12), zero-padded to 2 digits. `%I` | `12` | Hour number in 12-hour clocks (01--12), zero-padded to 2 digits.
`%l` | `12` | Same to `%I` but space-padded. `%l` | `12` | Same to `%I` but space-padded. Same to `%_I`.
| | | |
`%P` | `am` | `am` or `pm` in 12-hour clocks. `%P` | `am` | `am` or `pm` in 12-hour clocks.
`%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. `%M` | `34` | Minute number (00--59), zero-padded to 2 digits.
`%S` | `60` | Second number (00--60), zero-padded to 2 digits. [5] `%S` | `60` | Second number (00--60), zero-padded to 2 digits. [5]
`%f` | `026490000` | The number of nanoseconds since last whole second, zero-padded to 9 digits. `%f` | `026490000` | The fractional seconds (in nanoseconds) since last whole second. [8]
`%.f` | `.026490` | Similar to `.%f` but left-aligned. [8]
| | | |
`%R` | `00:34` | Hour-minute format. Same to `%H:%M`. `%R` | `00:34` | Hour-minute format. Same to `%H:%M`.
`%T` | `00:34:60` | Hour-minute-second format. Same to `%H:%M:%S`. `%T` | `00:34:60` | Hour-minute-second format. Same to `%H:%M:%S`.
@ -64,6 +65,7 @@ Spec. | Example | Description
| | **TIME ZONE SPECIFIERS:** | | **TIME ZONE SPECIFIERS:**
`%Z` | `ACST` | *Formatting only:* Local time zone name. `%Z` | `ACST` | *Formatting only:* Local time zone name.
`%z` | `+0930` | Offset from the local time to UTC (with UTC being `+0000`). `%z` | `+0930` | Offset from the local time to UTC (with UTC being `+0000`).
`%:z` | `+09:30` | Same to `%z` but with a colon.
| | | |
| | **DATE & TIME SPECIFIERS:** | | **DATE & TIME SPECIFIERS:**
`%c` | `Sun Jul 8 00:34:60 2001` | `ctime` date & time format. Same to `%a %b %e %T %Y` sans `\n`. `%c` | `Sun Jul 8 00:34:60 2001` | `ctime` date & time format. Same to `%a %b %e %T %Y` sans `\n`.
@ -76,6 +78,15 @@ Spec. | Example | Description
`%n` | | Literal newline (`\n`). `%n` | | Literal newline (`\n`).
`%%` | | Literal percent sign. `%%` | | Literal percent sign.
It is possible to override the default padding behavior of numeric specifiers `%?`.
This is not allowed for other specifiers and will result in the `BAD_FORMAT` error.
Modifier | Description
-------- | -----------
`%-?` | Suppresses any padding including spaces and zeroes. (e.g. `%j` = `012`, `%-j` = `12`)
`%_?` | Uses spaces as a padding. (e.g. `%j` = `012`, `%_j` = ` 12`)
`%0?` | Uses zeroes as a padding. (e.g. `%e` = ` 9`, `%0e` = `09`)
Notes: Notes:
1. `%Y`: 1. `%Y`:
@ -96,15 +107,27 @@ Notes:
It accounts for leap seconds, so `60` is possible. It accounts for leap seconds, so `60` is possible.
6. `%+`: 6. `%+`:
This one is close to, but not identical to, `%Y-%m-%dT%H:%M:%S%z`. This one is close to, but not identical to, `%Y-%m-%dT%H:%M:%S%.f%z`.
The main differences are a colon in `%z` and fractional seconds (which width adapts The main differences are a colon in `%z`.
accordingly to the number of trailing zeroes).
7. `%s`: 7. `%s`:
This is not padded and can be negative. This is not padded and can be negative.
For the purpose of Chrono, it only accounts for non-leap seconds For the purpose of Chrono, it only accounts for non-leap seconds
so it slightly differs from ISO C `strftime` behavior. so it slightly differs from ISO C `strftime` behavior.
8. `%f`, `%.f`:
The default `%f` is right-aligned and always zero-padded to 9 digits
for the compatibility with glibc and others,
so it always counts the number of nanoseconds since the last whole second.
E.g. 7ms after the last second will print `007000000`, and parsing `7000000` will yield the same.
The variant `%.f` is left-aligned and print 0, 3, 6 or 9 fractional digits
according to the precision. E.g. 70ms after the last second under `%.f` will print `.070`
(note: not `.07`), and parsing `.07`, `.070000` etc. will yield the same.
Note that they can print or read nothing if the fractional part is zero or
the next character is not `.`.
*/ */
use super::{Item, Numeric, Fixed, Pad}; use super::{Item, Numeric, Fixed, Pad};
@ -147,11 +170,26 @@ impl<'a> Iterator for StrftimeItems<'a> {
Some('%') => { Some('%') => {
self.remainder = &self.remainder[1..]; self.remainder = &self.remainder[1..];
let spec = match self.remainder.chars().next() { macro_rules! next {
Some(x) => x, () => (
None => return Some(Item::Error), // premature end of string match self.remainder.chars().next() {
Some(x) => {
self.remainder = &self.remainder[x.len_utf8()..];
x
},
None => return Some(Item::Error), // premature end of string
}
)
}
let spec = next!();
let pad_override = match spec {
'-' => Some(Pad::None),
'0' => Some(Pad::Zero),
'_' => Some(Pad::Space),
_ => None,
}; };
self.remainder = &self.remainder[spec.len_utf8()..]; let spec = if pad_override.is_some() { next!() } else { spec };
macro_rules! recons { macro_rules! recons {
[$head:expr, $($tail:expr),+] => ({ [$head:expr, $($tail:expr),+] => ({
@ -161,62 +199,77 @@ impl<'a> Iterator for StrftimeItems<'a> {
}) })
} }
match spec { let item = match spec {
'A' => Some(fix!(LongWeekdayName)), 'A' => fix!(LongWeekdayName),
'B' => Some(fix!(LongMonthName)), 'B' => fix!(LongMonthName),
'C' => Some(num0!(YearDiv100)), 'C' => num0!(YearDiv100),
'D' => Some(recons![num0!(Month), lit!("/"), num0!(Day), lit!("/"), 'D' => recons![num0!(Month), lit!("/"), num0!(Day), lit!("/"),
num0!(YearMod100)]), num0!(YearMod100)],
'F' => Some(recons![num0!(Year), lit!("-"), num0!(Month), lit!("-"), 'F' => recons![num0!(Year), lit!("-"), num0!(Month), lit!("-"), num0!(Day)],
num0!(Day)]), 'G' => num0!(IsoYear),
'G' => Some(num0!(IsoYear)), 'H' => num0!(Hour),
'H' => Some(num0!(Hour)), 'I' => num0!(Hour12),
'I' => Some(num0!(Hour12)), 'M' => num0!(Minute),
'M' => Some(num0!(Minute)), 'P' => fix!(LowerAmPm),
'P' => Some(fix!(LowerAmPm)), 'R' => recons![num0!(Hour), lit!(":"), num0!(Minute)],
'R' => Some(recons![num0!(Hour), lit!(":"), num0!(Minute)]), 'S' => num0!(Second),
'S' => Some(num0!(Second)), 'T' => recons![num0!(Hour), lit!(":"), num0!(Minute), lit!(":"), num0!(Second)],
'T' => Some(recons![num0!(Hour), lit!(":"), num0!(Minute), lit!(":"), 'U' => num0!(WeekFromSun),
num0!(Second)]), 'V' => num0!(IsoWeek),
'U' => Some(num0!(WeekFromSun)), 'W' => num0!(WeekFromMon),
'V' => Some(num0!(IsoWeek)), 'X' => recons![num0!(Hour), lit!(":"), num0!(Minute), lit!(":"), num0!(Second)],
'W' => Some(num0!(WeekFromMon)), 'Y' => num0!(Year),
'X' => Some(recons![num0!(Hour), lit!(":"), num0!(Minute), lit!(":"), 'Z' => fix!(TimezoneName),
num0!(Second)]), 'a' => fix!(ShortWeekdayName),
'Y' => Some(num0!(Year)), 'b' => fix!(ShortMonthName),
'Z' => Some(fix!(TimezoneName)), 'c' => recons![fix!(ShortWeekdayName), sp!(" "), fix!(ShortMonthName),
'a' => Some(fix!(ShortWeekdayName)), sp!(" "), nums!(Day), sp!(" "), num0!(Hour), lit!(":"),
'b' => Some(fix!(ShortMonthName)), num0!(Minute), lit!(":"), num0!(Second), sp!(" "), num0!(Year)],
'c' => Some(recons![fix!(ShortWeekdayName), sp!(" "), fix!(ShortMonthName), 'd' => num0!(Day),
sp!(" "), nums!(Day), sp!(" "), num0!(Hour), lit!(":"), 'e' => nums!(Day),
num0!(Minute), lit!(":"), num0!(Second), sp!(" "), 'f' => num0!(Nanosecond),
num0!(Year)]), 'g' => num0!(IsoYearMod100),
'd' => Some(num0!(Day)), 'h' => fix!(ShortMonthName),
'e' => Some(nums!(Day)), 'j' => num0!(Ordinal),
'f' => Some(num0!(Nanosecond)), 'k' => nums!(Hour),
'g' => Some(num0!(IsoYearMod100)), 'l' => nums!(Hour12),
'h' => Some(fix!(ShortMonthName)), 'm' => num0!(Month),
'j' => Some(num0!(Ordinal)), 'n' => sp!("\n"),
'k' => Some(nums!(Hour)), 'p' => fix!(UpperAmPm),
'l' => Some(nums!(Hour12)), 'r' => recons![num0!(Hour12), lit!(":"), num0!(Minute), lit!(":"),
'm' => Some(num0!(Month)), num0!(Second), sp!(" "), fix!(UpperAmPm)],
'n' => Some(sp!("\n")), 's' => num!(Timestamp),
'p' => Some(fix!(UpperAmPm)), 't' => sp!("\t"),
'r' => Some(recons![num0!(Hour12), lit!(":"), num0!(Minute), lit!(":"), 'u' => num!(WeekdayFromMon),
num0!(Second), sp!(" "), fix!(UpperAmPm)]), 'v' => recons![nums!(Day), lit!("-"), fix!(ShortMonthName), lit!("-"),
's' => Some(num!(Timestamp)), num0!(Year)],
't' => Some(sp!("\t")), 'w' => num!(NumDaysFromSun),
'u' => Some(num!(WeekdayFromMon)), 'x' => recons![num0!(Month), lit!("/"), num0!(Day), lit!("/"),
'v' => Some(recons![nums!(Day), lit!("-"), fix!(ShortMonthName), lit!("-"), num0!(YearMod100)],
num0!(Year)]), 'y' => num0!(YearMod100),
'w' => Some(num!(NumDaysFromSun)), 'z' => fix!(TimezoneOffset),
'x' => Some(recons![num0!(Month), lit!("/"), num0!(Day), lit!("/"), '+' => fix!(RFC3339),
num0!(YearMod100)]), ':' => match next!() {
'y' => Some(num0!(YearMod100)), 'z' => fix!(TimezoneOffsetColon),
'z' => Some(fix!(TimezoneOffset)), _ => Item::Error,
'+' => Some(fix!(RFC3339)), },
'%' => Some(lit!("%")), '.' => match next!() {
_ => Some(Item::Error), // no such specifier 'f' => fix!(Nanosecond),
_ => Item::Error,
},
'%' => lit!("%"),
_ => Item::Error, // no such specifier
};
// adjust `item` if we have any padding modifier
if let Some(new_pad) = pad_override {
match item {
Item::Numeric(kind, _pad) if self.recons.is_empty() =>
Some(Item::Numeric(kind, new_pad)),
_ => Some(Item::Error), // no reconstructed or non-numeric item allowed
}
} else {
Some(item)
} }
}, },
@ -273,6 +326,21 @@ fn test_strftime_items() {
assert_eq!(parse_and_collect("foo%?"), [Item::Error]); assert_eq!(parse_and_collect("foo%?"), [Item::Error]);
assert_eq!(parse_and_collect("bar%42"), [Item::Error]); assert_eq!(parse_and_collect("bar%42"), [Item::Error]);
assert_eq!(parse_and_collect("quux% +"), [Item::Error]); assert_eq!(parse_and_collect("quux% +"), [Item::Error]);
assert_eq!(parse_and_collect("%.Z"), [Item::Error]);
assert_eq!(parse_and_collect("%:Z"), [Item::Error]);
assert_eq!(parse_and_collect("%-Z"), [Item::Error]);
assert_eq!(parse_and_collect("%0Z"), [Item::Error]);
assert_eq!(parse_and_collect("%_Z"), [Item::Error]);
assert_eq!(parse_and_collect("%.j"), [Item::Error]);
assert_eq!(parse_and_collect("%:j"), [Item::Error]);
assert_eq!(parse_and_collect("%-j"), [num!(Ordinal)]);
assert_eq!(parse_and_collect("%0j"), [num0!(Ordinal)]);
assert_eq!(parse_and_collect("%_j"), [nums!(Ordinal)]);
assert_eq!(parse_and_collect("%.e"), [Item::Error]);
assert_eq!(parse_and_collect("%:e"), [Item::Error]);
assert_eq!(parse_and_collect("%-e"), [num!(Day)]);
assert_eq!(parse_and_collect("%0e"), [num0!(Day)]);
assert_eq!(parse_and_collect("%_e"), [nums!(Day)]);
} }
#[cfg(test)] #[cfg(test)]
@ -292,6 +360,7 @@ fn test_strftime_docs() {
assert_eq!(dt.format("%h").to_string(), "Jul"); assert_eq!(dt.format("%h").to_string(), "Jul");
assert_eq!(dt.format("%d").to_string(), "08"); assert_eq!(dt.format("%d").to_string(), "08");
assert_eq!(dt.format("%e").to_string(), " 8"); assert_eq!(dt.format("%e").to_string(), " 8");
assert_eq!(dt.format("%e").to_string(), dt.format("%_d").to_string());
assert_eq!(dt.format("%a").to_string(), "Sun"); assert_eq!(dt.format("%a").to_string(), "Sun");
assert_eq!(dt.format("%A").to_string(), "Sunday"); assert_eq!(dt.format("%A").to_string(), "Sunday");
assert_eq!(dt.format("%w").to_string(), "0"); assert_eq!(dt.format("%w").to_string(), "0");
@ -310,13 +379,16 @@ fn test_strftime_docs() {
// time specifiers // time specifiers
assert_eq!(dt.format("%H").to_string(), "00"); assert_eq!(dt.format("%H").to_string(), "00");
assert_eq!(dt.format("%k").to_string(), " 0"); assert_eq!(dt.format("%k").to_string(), " 0");
assert_eq!(dt.format("%k").to_string(), dt.format("%_H").to_string());
assert_eq!(dt.format("%I").to_string(), "12"); assert_eq!(dt.format("%I").to_string(), "12");
assert_eq!(dt.format("%l").to_string(), "12"); assert_eq!(dt.format("%l").to_string(), "12");
assert_eq!(dt.format("%l").to_string(), dt.format("%_I").to_string());
assert_eq!(dt.format("%P").to_string(), "am"); assert_eq!(dt.format("%P").to_string(), "am");
assert_eq!(dt.format("%p").to_string(), "AM"); assert_eq!(dt.format("%p").to_string(), "AM");
assert_eq!(dt.format("%M").to_string(), "34"); assert_eq!(dt.format("%M").to_string(), "34");
assert_eq!(dt.format("%S").to_string(), "60"); assert_eq!(dt.format("%S").to_string(), "60");
assert_eq!(dt.format("%f").to_string(), "026490000"); assert_eq!(dt.format("%f").to_string(), "026490000");
assert_eq!(dt.format("%.f").to_string(), ".026490");
assert_eq!(dt.format("%R").to_string(), "00:34"); assert_eq!(dt.format("%R").to_string(), "00:34");
assert_eq!(dt.format("%T").to_string(), "00:34:60"); assert_eq!(dt.format("%T").to_string(), "00:34:60");
assert_eq!(dt.format("%X").to_string(), "00:34:60"); assert_eq!(dt.format("%X").to_string(), "00:34:60");
@ -325,6 +397,7 @@ fn test_strftime_docs() {
// time zone specifiers // time zone specifiers
//assert_eq!(dt.format("%Z").to_string(), "ACST"); //assert_eq!(dt.format("%Z").to_string(), "ACST");
assert_eq!(dt.format("%z").to_string(), "+0930"); assert_eq!(dt.format("%z").to_string(), "+0930");
assert_eq!(dt.format("%:z").to_string(), "+09:30");
// date & time specifiers // date & time specifiers
assert_eq!(dt.format("%c").to_string(), "Sun Jul 8 00:34:60 2001"); assert_eq!(dt.format("%c").to_string(), "Sun Jul 8 00:34:60 2001");