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:
parent
5bff8170ff
commit
1f40b03ab9
|
@ -133,12 +133,16 @@ pub enum Fixed {
|
|||
///
|
||||
/// 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.
|
||||
TimezoneOffset,
|
||||
TimezoneOffsetColon,
|
||||
/// 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,
|
||||
/// 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.
|
||||
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,
|
||||
/// RFC 2822 date and time syntax. Commonly used for email and MIME date and time.
|
||||
RFC2822,
|
||||
|
@ -364,6 +368,10 @@ pub fn format<'a, I>(w: &mut fmt::Formatter, date: Option<&NaiveDate>, time: Opt
|
|||
}),
|
||||
TimezoneName =>
|
||||
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 =>
|
||||
off.map(|&(_, off)| write_local_minus_utc(w, off, false, false)),
|
||||
TimezoneOffsetZ =>
|
||||
|
|
|
@ -309,13 +309,13 @@ pub fn parse<'a, I>(parsed: &mut Parsed, mut s: &str, items: I) -> ParseResult<(
|
|||
|
||||
TimezoneName => return Err(BAD_FORMAT),
|
||||
|
||||
TimezoneOffset => {
|
||||
TimezoneOffsetColon | TimezoneOffset => {
|
||||
let offset = try_consume!(scan::timezone_offset(s.trim_left(),
|
||||
scan::colon_or_space));
|
||||
try!(parsed.set_offset(offset as i64));
|
||||
}
|
||||
|
||||
TimezoneOffsetZ => {
|
||||
TimezoneOffsetColonZ | TimezoneOffsetZ => {
|
||||
let offset = try_consume!(scan::timezone_offset_zulu(s.trim_left(),
|
||||
scan::colon_or_space));
|
||||
try!(parsed.set_offset(offset as i64));
|
||||
|
|
|
@ -22,7 +22,7 @@ Spec. | Example | Description
|
|||
`%h` | `Jul` | Same to `%b`.
|
||||
| |
|
||||
`%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` | `Sunday` | Full weekday name. Also accepts corresponding abbreviation in parsing.
|
||||
|
@ -45,16 +45,17 @@ Spec. | Example | Description
|
|||
| |
|
||||
| | **TIME SPECIFIERS:**
|
||||
`%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.
|
||||
`%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.
|
||||
| |
|
||||
`%M` | `34` | Minute number (00--59), zero-padded to 2 digits.
|
||||
`%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`.
|
||||
`%T` | `00:34:60` | Hour-minute-second format. Same to `%H:%M:%S`.
|
||||
|
@ -64,6 +65,7 @@ Spec. | Example | Description
|
|||
| | **TIME ZONE SPECIFIERS:**
|
||||
`%Z` | `ACST` | *Formatting only:* Local time zone name.
|
||||
`%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:**
|
||||
`%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`).
|
||||
`%%` | | 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:
|
||||
|
||||
1. `%Y`:
|
||||
|
@ -96,15 +107,27 @@ Notes:
|
|||
It accounts for leap seconds, so `60` is possible.
|
||||
|
||||
6. `%+`:
|
||||
This one is close to, but not identical to, `%Y-%m-%dT%H:%M:%S%z`.
|
||||
The main differences are a colon in `%z` and fractional seconds (which width adapts
|
||||
accordingly to the number of trailing zeroes).
|
||||
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`.
|
||||
|
||||
7. `%s`:
|
||||
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.
|
||||
|
||||
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};
|
||||
|
@ -147,11 +170,26 @@ impl<'a> Iterator for StrftimeItems<'a> {
|
|||
Some('%') => {
|
||||
self.remainder = &self.remainder[1..];
|
||||
|
||||
let spec = match self.remainder.chars().next() {
|
||||
Some(x) => x,
|
||||
None => return Some(Item::Error), // premature end of string
|
||||
macro_rules! next {
|
||||
() => (
|
||||
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 {
|
||||
[$head:expr, $($tail:expr),+] => ({
|
||||
|
@ -161,62 +199,77 @@ impl<'a> Iterator for StrftimeItems<'a> {
|
|||
})
|
||||
}
|
||||
|
||||
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)]),
|
||||
'F' => Some(recons![num0!(Year), lit!("-"), num0!(Month), lit!("-"),
|
||||
num0!(Day)]),
|
||||
'G' => Some(num0!(IsoYear)),
|
||||
'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)]),
|
||||
'Y' => Some(num0!(Year)),
|
||||
'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!(" "),
|
||||
num0!(Year)]),
|
||||
'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!("-"),
|
||||
num0!(Year)]),
|
||||
'w' => Some(num!(NumDaysFromSun)),
|
||||
'x' => Some(recons![num0!(Month), lit!("/"), num0!(Day), lit!("/"),
|
||||
num0!(YearMod100)]),
|
||||
'y' => Some(num0!(YearMod100)),
|
||||
'z' => Some(fix!(TimezoneOffset)),
|
||||
'+' => Some(fix!(RFC3339)),
|
||||
'%' => Some(lit!("%")),
|
||||
_ => Some(Item::Error), // no such specifier
|
||||
let item = match spec {
|
||||
'A' => fix!(LongWeekdayName),
|
||||
'B' => fix!(LongMonthName),
|
||||
'C' => num0!(YearDiv100),
|
||||
'D' => recons![num0!(Month), lit!("/"), num0!(Day), lit!("/"),
|
||||
num0!(YearMod100)],
|
||||
'F' => recons![num0!(Year), lit!("-"), num0!(Month), lit!("-"), num0!(Day)],
|
||||
'G' => num0!(IsoYear),
|
||||
'H' => num0!(Hour),
|
||||
'I' => num0!(Hour12),
|
||||
'M' => num0!(Minute),
|
||||
'P' => fix!(LowerAmPm),
|
||||
'R' => recons![num0!(Hour), lit!(":"), num0!(Minute)],
|
||||
'S' => num0!(Second),
|
||||
'T' => recons![num0!(Hour), lit!(":"), num0!(Minute), lit!(":"), num0!(Second)],
|
||||
'U' => num0!(WeekFromSun),
|
||||
'V' => num0!(IsoWeek),
|
||||
'W' => num0!(WeekFromMon),
|
||||
'X' => recons![num0!(Hour), lit!(":"), num0!(Minute), lit!(":"), num0!(Second)],
|
||||
'Y' => num0!(Year),
|
||||
'Z' => fix!(TimezoneName),
|
||||
'a' => fix!(ShortWeekdayName),
|
||||
'b' => fix!(ShortMonthName),
|
||||
'c' => recons![fix!(ShortWeekdayName), sp!(" "), fix!(ShortMonthName),
|
||||
sp!(" "), nums!(Day), sp!(" "), num0!(Hour), lit!(":"),
|
||||
num0!(Minute), lit!(":"), num0!(Second), sp!(" "), num0!(Year)],
|
||||
'd' => num0!(Day),
|
||||
'e' => nums!(Day),
|
||||
'f' => num0!(Nanosecond),
|
||||
'g' => num0!(IsoYearMod100),
|
||||
'h' => fix!(ShortMonthName),
|
||||
'j' => num0!(Ordinal),
|
||||
'k' => nums!(Hour),
|
||||
'l' => nums!(Hour12),
|
||||
'm' => num0!(Month),
|
||||
'n' => sp!("\n"),
|
||||
'p' => fix!(UpperAmPm),
|
||||
'r' => recons![num0!(Hour12), lit!(":"), num0!(Minute), lit!(":"),
|
||||
num0!(Second), sp!(" "), fix!(UpperAmPm)],
|
||||
's' => num!(Timestamp),
|
||||
't' => sp!("\t"),
|
||||
'u' => num!(WeekdayFromMon),
|
||||
'v' => recons![nums!(Day), lit!("-"), fix!(ShortMonthName), lit!("-"),
|
||||
num0!(Year)],
|
||||
'w' => num!(NumDaysFromSun),
|
||||
'x' => recons![num0!(Month), lit!("/"), num0!(Day), lit!("/"),
|
||||
num0!(YearMod100)],
|
||||
'y' => num0!(YearMod100),
|
||||
'z' => fix!(TimezoneOffset),
|
||||
'+' => fix!(RFC3339),
|
||||
':' => match next!() {
|
||||
'z' => fix!(TimezoneOffsetColon),
|
||||
_ => Item::Error,
|
||||
},
|
||||
'.' => match next!() {
|
||||
'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("bar%42"), [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)]
|
||||
|
@ -292,6 +360,7 @@ fn test_strftime_docs() {
|
|||
assert_eq!(dt.format("%h").to_string(), "Jul");
|
||||
assert_eq!(dt.format("%d").to_string(), "08");
|
||||
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(), "Sunday");
|
||||
assert_eq!(dt.format("%w").to_string(), "0");
|
||||
|
@ -310,13 +379,16 @@ fn test_strftime_docs() {
|
|||
// time specifiers
|
||||
assert_eq!(dt.format("%H").to_string(), "00");
|
||||
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("%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("%M").to_string(), "34");
|
||||
assert_eq!(dt.format("%S").to_string(), "60");
|
||||
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("%T").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
|
||||
//assert_eq!(dt.format("%Z").to_string(), "ACST");
|
||||
assert_eq!(dt.format("%z").to_string(), "+0930");
|
||||
assert_eq!(dt.format("%:z").to_string(), "+09:30");
|
||||
|
||||
// date & time specifiers
|
||||
assert_eq!(dt.format("%c").to_string(), "Sun Jul 8 00:34:60 2001");
|
||||
|
|
Loading…
Reference in New Issue