new formatter design!

- Format string is internally represented as a series of formatting
  items. Items can be directly given to now-public `format::format`
  function as well.

- Format string parser is separated to `format::strftime` module.
  This is to allow for potentional alternative formatting syntaxes.

- `DelayedFormat` now receives an iterator for formatting items.
This commit is contained in:
Kang Seonghoon 2015-01-26 21:14:50 +09:00
parent 98d1ce01c9
commit 43ee68b522
10 changed files with 592 additions and 246 deletions

View File

@ -17,7 +17,7 @@ use naive;
use naive::date::NaiveDate;
use naive::time::NaiveTime;
use datetime::DateTime;
use format::DelayedFormat;
use format::{DelayedFormat, StrftimeItems};
/// ISO 8601 calendar date with timezone.
#[derive(Clone)]
@ -183,10 +183,11 @@ impl<Off:Offset> Date<Off> {
impl<Off: Offset + fmt::Display> Date<Off> {
/// Formats the date in the specified format string.
/// See the `format` module on the supported escape sequences.
/// See the `format::strftime` module on the supported escape sequences.
#[inline]
pub fn format<'a>(&'a self, fmt: &'a str) -> DelayedFormat<'a> {
DelayedFormat::new_with_offset(Some(self.local()), None, &self.offset, fmt)
pub fn format<'a>(&'a self, fmt: &'a str) -> DelayedFormat<'a, StrftimeItems<'a>> {
DelayedFormat::new_with_offset(Some(self.local()), None, &self.offset,
StrftimeItems::new(fmt))
}
}

View File

@ -16,7 +16,7 @@ use duration::Duration;
use naive::datetime::NaiveDateTime;
use time::Time;
use date::Date;
use format::DelayedFormat;
use format::{DelayedFormat, StrftimeItems};
/// ISO 8601 combined date and time with timezone.
#[derive(Clone)]
@ -72,11 +72,12 @@ impl<Off:Offset> DateTime<Off> {
impl<Off: Offset + fmt::Display> DateTime<Off> {
/// Formats the combined date and time in the specified format string.
/// See the `format` module on the supported escape sequences.
/// See the `format::strftime` module on the supported escape sequences.
#[inline]
pub fn format<'a>(&'a self, fmt: &'a str) -> DelayedFormat<'a> {
pub fn format<'a>(&'a self, fmt: &'a str) -> DelayedFormat<'a, StrftimeItems<'a>> {
let local = self.local();
DelayedFormat::new_with_offset(Some(local.date()), Some(local.time()), &self.offset, fmt)
DelayedFormat::new_with_offset(Some(local.date()), Some(local.time()), &self.offset,
StrftimeItems::new(fmt))
}
}

View File

@ -16,14 +16,16 @@ pub fn div_rem<T: Int>(a: T, b: T) -> (T, T) {
(a / b, a % b)
}
/// Same as `let (q, r) = div_mod_floor(a, b); r`.
/// Calculates a floored integer quotient.
#[inline]
pub fn div_floor<T: Int>(a: T, b: T) -> T {
div_mod_floor(a, b).0
}
/// Calculates a floored modulo.
#[inline]
pub fn mod_floor<T: Int>(a: T, b: T) -> T {
let zero = Int::zero();
match a % b {
r if (r > zero && b < zero) || (r < zero && b > zero) => r + b,
r => r,
}
div_mod_floor(a, b).1
}
/// Calculates a floored integer quotient and modulo.

View File

@ -1,216 +0,0 @@
// This is a part of rust-chrono.
// Copyright (c) 2014-2015, Kang Seonghoon.
// See README.md and LICENSE.txt for details.
/*!
* Formatting utilities for date and time.
*/
use std::fmt;
use {Datelike, Timelike};
use duration::Duration;
use offset::Offset;
use naive::date::NaiveDate;
use naive::time::NaiveTime;
/// The internal workhouse for `DelayedFormat`.
fn format(w: &mut fmt::Formatter, date: Option<&NaiveDate>, time: Option<&NaiveTime>,
off: Option<&(String, Duration)>, fmt: &str) -> fmt::Result {
static SHORT_MONTHS: [&'static str; 12] =
["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
static LONG_MONTHS: [&'static str; 12] =
["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"];
static SHORT_WEEKDAYS: [&'static str; 7] =
["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
static LONG_WEEKDAYS: [&'static str; 7] =
["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
let mut parts = fmt.split('%');
match parts.next() {
Some(first) => try!(write!(w, "{}", first)),
None => return Ok(()),
}
let mut last_was_percent = false;
for part in parts {
if last_was_percent { // `%%<part>`
last_was_percent = false;
try!(write!(w, "%{}", part));
continue;
}
let (head, tail) = match part.slice_shift_char() {
Some((head, tail)) => (Some(head), tail),
None => (None, ""),
};
match (head, date, time, off) {
// year
(Some('Y'), Some(d), _, _) => try!(write!(w, "{}", d.year())),
(Some('C'), Some(d), _, _) => try!(write!(w, "{:02}", d.year() / 100)),
(Some('y'), Some(d), _, _) => try!(write!(w, "{:02}", d.year() % 100)),
(Some('G'), Some(d), _, _) => try!(write!(w, "{:04}", d.isoweekdate().0)),
(Some('g'), Some(d), _, _) => try!(write!(w, "{:02}", d.isoweekdate().0 % 100)),
// month
(Some('m'), Some(d), _, _) => try!(write!(w, "{:02}", d.month())),
(Some('b'), Some(d), _, _) | (Some('h'), Some(d), _, _) =>
try!(write!(w, "{}", SHORT_MONTHS[d.month0() as usize])),
(Some('B'), Some(d), _, _) =>
try!(write!(w, "{}", LONG_MONTHS[d.month0() as usize])),
// day of month
(Some('d'), Some(d), _, _) => try!(write!(w, "{:02}", d.day())),
(Some('e'), Some(d), _, _) => try!(write!(w, "{:2}", d.day())),
// week
(Some('U'), Some(d), _, _) =>
try!(write!(w, "{:02}", (d.ordinal() - d.weekday().num_days_from_sunday()
+ 7) / 7)),
(Some('W'), Some(d), _, _) =>
try!(write!(w, "{:02}", (d.ordinal() - d.weekday().num_days_from_monday()
+ 7) / 7)),
(Some('V'), Some(d), _, _) => try!(write!(w, "{:02}", d.isoweekdate().1)),
// day of week
(Some('a'), Some(d), _, _) =>
try!(write!(w, "{}", SHORT_WEEKDAYS[d.weekday().num_days_from_monday() as usize])),
(Some('A'), Some(d), _, _) =>
try!(write!(w, "{}", LONG_WEEKDAYS[d.weekday().num_days_from_monday() as usize])),
(Some('w'), Some(d), _, _) => try!(write!(w, "{}", d.weekday().num_days_from_sunday())),
(Some('u'), Some(d), _, _) => try!(write!(w, "{}", d.weekday().number_from_monday())),
// day of year
(Some('j'), Some(d), _, _) => try!(write!(w, "{:03}", d.ordinal())),
// combined date
(Some('D'), Some(d), _, _) | (Some('x'), Some(d), _, _) => // `%m/%d/%y`
try!(write!(w, "{:02}/{:02}/{:02}", d.month(), d.day(), d.year() % 100)),
(Some('F'), Some(d), _, _) => // `%Y-%m-%d'
try!(write!(w, "{:04}-{:02}-{:02}", d.year(), d.month(), d.day())),
(Some('v'), Some(d), _, _) => // `%e-%b-%Y'
try!(write!(w, "{:2}-{}-{:04}", d.day(), SHORT_MONTHS[d.month0() as usize],
d.year())),
// hour
(Some('H'), _, Some(t), _) => try!(write!(w, "{:02}", t.hour())),
(Some('k'), _, Some(t), _) => try!(write!(w, "{:2}", t.hour())),
(Some('I'), _, Some(t), _) => try!(write!(w, "{:02}", t.hour12().1)),
(Some('l'), _, Some(t), _) => try!(write!(w, "{:2}", t.hour12().1)),
(Some('P'), _, Some(t), _) =>
try!(write!(w, "{}", if t.hour12().0 {"pm"} else {"am"})),
(Some('p'), _, Some(t), _) =>
try!(write!(w, "{}", if t.hour12().0 {"PM"} else {"AM"})),
// minute
(Some('M'), _, Some(t), _) => try!(write!(w, "{:02}", t.minute())),
// second and below
(Some('S'), _, Some(t), _) => try!(write!(w, "{:02}", t.second())),
(Some('f'), _, Some(t), _) => try!(write!(w, "{:09}", t.nanosecond())),
// combined time
(Some('R'), _, Some(t), _) => // `%H:%M`
try!(write!(w, "{:02}:{:02}", t.hour(), t.minute())),
(Some('T'), _, Some(t), _) | (Some('X'), _, Some(t), _) => // `%H:%M:%S`
try!(write!(w, "{:02}:{:02}:{:02}", t.hour(), t.minute(), t.second())),
(Some('r'), _, Some(t), _) => { // `%I:%M:%S %p`
let (is_pm, hour12) = t.hour12();
try!(write!(w, "{:02}:{:02}:{:02} {}", hour12, t.minute(), t.second(),
if is_pm {"PM"} else {"AM"}))
},
// timezone
(Some('Z'), _, _, Some(&(ref name, _))) => try!(write!(w, "{}", *name)),
(Some('z'), _, _, Some(&(_, ref local_minus_utc))) => {
let off = local_minus_utc.num_minutes();
let (sign, off) = if off < 0 {('-', -off)} else {('+', off)};
try!(write!(w, "{}{:02}{:02}", sign, off / 60, off % 60))
},
/*
// timestamp
(Some('s'), Some(d), Some(t), Some(o)) => { // XXX
let datetime = o.from_local_datetime(&d.and_time(t.clone())).unwrap();
try!(write!(w, "{}", datetime.num_seconds_from_unix_epoch()))
},
*/
// combined date and time
(Some('c'), Some(d), Some(t), _) => // `%a %b %e %T %Y`
try!(write!(w, "{} {} {:2} {:02}:{:02}:{:02} {:04}",
SHORT_WEEKDAYS[d.weekday().num_days_from_monday() as usize],
SHORT_MONTHS[d.month0() as usize], d.day(),
t.hour(), t.minute(), t.second(), d.year())),
(Some('+'), Some(d), Some(t),
Some(&(_, ref local_minus_utc))) => { // `%Y-%m-%dT%H:%M:%S` plus tz
let off = local_minus_utc.num_minutes();
let (sign, off) = if off < 0 {('-', -off)} else {('+', off)};
try!(write!(w, "{}-{:02}-{:02}T{:02}:{:02}:{:02}{}{:02}:{:02}",
d.year(), d.month(), d.day(), t.hour(), t.minute(), t.second(),
sign, off / 60, off % 60))
},
// special characters
(Some('t'), _, _, _) => try!(write!(w, "\t")),
(Some('n'), _, _, _) => try!(write!(w, "\n")),
// TODO issue a detailed error if possible
(Some(_), _, _, _) => return Err(fmt::Error),
(None, _, _, _) => {
// if there is the next part, a single `%` and that part should be printed
// in verbatim. otherwise it is an error (the stray `%`).
last_was_percent = true;
}
}
try!(write!(w, "{}", tail));
}
if last_was_percent { // a stray `%`
// TODO issue a detailed error if possible
Err(fmt::Error)
} else {
Ok(())
}
}
/// A *temporary* object which can be used as an argument to `format!` or others.
/// This is normally constructed via `format` methods of each date and time type.
#[derive(Debug)]
pub struct DelayedFormat<'a> {
/// The date view, if any.
date: Option<NaiveDate>,
/// The time view, if any.
time: Option<NaiveTime>,
/// The name and local-to-UTC difference for the offset (timezone), if any.
off: Option<(String, Duration)>,
/// The format string.
fmt: &'a str,
}
impl<'a> DelayedFormat<'a> {
/// Makes a new `DelayedFormat` value out of local date and time.
pub fn new(date: Option<NaiveDate>, time: Option<NaiveTime>,
fmt: &'a str) -> DelayedFormat<'a> {
DelayedFormat { date: date, time: time, off: None, fmt: fmt }
}
/// Makes a new `DelayedFormat` value out of local date and time and UTC offset.
pub fn new_with_offset<Off>(date: Option<NaiveDate>, time: Option<NaiveTime>,
offset: &Off, fmt: &'a str) -> DelayedFormat<'a>
where Off: Offset + fmt::Display {
let name_and_diff = (offset.to_string(), offset.local_minus_utc());
DelayedFormat { date: date, time: time, off: Some(name_and_diff), fmt: fmt }
}
}
impl<'a> fmt::Display for DelayedFormat<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let ret = format(f, self.date.as_ref(), self.time.as_ref(), self.off.as_ref(), self.fmt);
ret.map_err(|_| fmt::Error) // we don't have any good means to pass detailed errors...
}
}

276
src/format/mod.rs Normal file
View File

@ -0,0 +1,276 @@
// This is a part of rust-chrono.
// Copyright (c) 2014-2015, Kang Seonghoon.
// See README.md and LICENSE.txt for details.
/*!
* Formatting utilities for date and time.
*/
use std::fmt;
use {Datelike, Timelike};
use div::{div_floor, mod_floor};
use duration::Duration;
use offset::Offset;
use naive::date::NaiveDate;
use naive::time::NaiveTime;
pub use self::strftime::StrftimeItems;
/// Padding characters for numeric items.
#[derive(Copy, PartialEq, Eq, Debug)]
pub enum Pad {
/// No padding.
None,
/// Zero (`0`) padding.
Zero,
/// Space padding.
Space,
}
/// Numeric item types.
#[derive(Copy, PartialEq, Eq, Debug)]
pub enum Numeric {
/// Full Gregorian year.
Year,
/// Gregorian year divided by 100 (century number). Always rounds towards minus infinity.
YearDiv100,
/// Gregorian year modulo 100. Cannot be negative.
YearMod100,
/// Year in the ISO week date.
IsoYear,
/// Year in the ISO week date, divided by 100. Always rounds towards minus infinity.
IsoYearDiv100,
/// Year in the ISO week date, modulo 100. Cannot be negative.
IsoYearMod100,
/// Month.
Month,
/// Day of the month.
Day,
/// Week number, where the week 1 starts at the first Sunday of January.
WeekFromSun,
/// Week number, where the week 1 starts at the first Monday of January.
WeekFromMon,
/// Week number in the ISO week date.
IsoWeek,
/// Day of the week, where Sunday = 0 and Saturday = 6.
NumDaysFromSun,
/// Day of the week, where Monday = 1 and Sunday = 7.
WeekdayFromMon,
/// Day of the year.
Ordinal,
/// Hour number in the 24-hour clocks.
Hour,
/// Hour number in the 12-hour clocks.
Hour12,
/// The number of minutes since the last whole hour.
Minute,
/// The number of seconds since the last whole minute.
Second,
/// The number of nanoseconds since the last whole second.
Nanosecond,
/// The number of non-leap seconds since January 1, 1970 0:00:00 UTC.
Timestamp,
}
/// Fixed-format item types.
#[derive(Copy, PartialEq, Eq, Debug)]
pub enum Fixed {
/// Abbreviated month names.
ShortMonthName,
/// Full month names.
LongMonthName,
/// Abbreviated day of the week names.
ShortWeekdayName,
/// FUll day of the week names.
LongWeekdayName,
/// AM/PM in upper cases.
LowerAmPm,
/// AM/PM in lower cases.
UpperAmPm,
/// Timezone name.
TimezoneName,
/// Offset from the local time to UTC (`+09:00` or `-04:00` or `+00:00`).
TimezoneOffset,
/// Offset from the local time to UTC (`+09:00` or `-04:00` or `Z`).
TimezoneOffsetZ,
}
/// A single formatting item. This is used for both formatting and parsing.
#[derive(Copy, PartialEq, Eq, Debug)]
pub enum Item<'a> {
/// A literally printed and parsed text.
Literal(&'a str),
/// Whitespace. Prints literally but parses zero or more whitespace.
Space(&'a str),
/// Numeric item. Can be optionally padded to the maximal length (if any) when formatting;
/// the parser simply ignores any padded whitespace and zeroes.
Numeric(Numeric, Pad),
/// Fixed-format item.
Fixed(Fixed),
/// Issues a formatting error. Used to signal an invalid format string.
Error,
}
macro_rules! lit { ($x:expr) => (Item::Literal($x)) }
macro_rules! sp { ($x:expr) => (Item::Space($x)) }
macro_rules! num { ($x:ident) => (Item::Numeric(Numeric::$x, Pad::None)) }
macro_rules! num0 { ($x:ident) => (Item::Numeric(Numeric::$x, Pad::Zero)) }
macro_rules! nums { ($x:ident) => (Item::Numeric(Numeric::$x, Pad::Space)) }
macro_rules! fix { ($x:ident) => (Item::Fixed(Fixed::$x)) }
/// Abbreviated month names.
static SHORT_MONTHS: [&'static str; 12] =
["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
/// Full month names.
static LONG_MONTHS: [&'static str; 12] =
["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"];
/// Abbreviated weekday names.
static SHORT_WEEKDAYS: [&'static str; 7] =
["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
/// FUll weekday names.
static LONG_WEEKDAYS: [&'static str; 7] =
["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
/// Tries to format given arguments with given formatting items.
/// Internally used by `DelayedFormat`.
pub fn format<'a, I>(w: &mut fmt::Formatter, date: Option<&NaiveDate>, time: Option<&NaiveTime>,
off: Option<&(String, Duration)>, items: I) -> fmt::Result
where I: Iterator<Item=Item<'a>> {
for item in items {
match item {
Item::Literal(s) | Item::Space(s) => try!(write!(w, "{}", s)),
Item::Numeric(spec, pad) => {
use self::Numeric::*;
let week_from_sun =
|&: d: &NaiveDate| (d.ordinal() - d.weekday().num_days_from_sunday() + 7) / 7;
let week_from_mon =
|&: d: &NaiveDate| (d.ordinal() - d.weekday().num_days_from_monday() + 7) / 7;
let (width, v) = match spec {
Year => (4, date.map(|d| d.year() as i64)),
YearDiv100 => (2, date.map(|d| div_floor(d.year() as i64, 100))),
YearMod100 => (2, date.map(|d| mod_floor(d.year() as i64, 100))),
IsoYear => (4, date.map(|d| d.isoweekdate().0 as i64)),
IsoYearDiv100 => (2, date.map(|d| div_floor(d.isoweekdate().0 as i64, 100))),
IsoYearMod100 => (2, date.map(|d| mod_floor(d.isoweekdate().0 as i64, 100))),
Month => (2, date.map(|d| d.month() as i64)),
Day => (2, date.map(|d| d.day() as i64)),
WeekFromSun => (2, date.map(|d| week_from_sun(d) as i64)),
WeekFromMon => (2, date.map(|d| week_from_mon(d) as i64)),
IsoWeek => (2, date.map(|d| d.isoweekdate().1 as i64)),
NumDaysFromSun => (1, date.map(|d| d.weekday().num_days_from_sunday() as i64)),
WeekdayFromMon => (1, date.map(|d| d.weekday().number_from_monday() as i64)),
Ordinal => (3, date.map(|d| d.ordinal() as i64)),
Hour => (2, time.map(|t| t.hour() as i64)),
Hour12 => (2, time.map(|t| t.hour12().1 as i64)),
Minute => (2, time.map(|t| t.minute() as i64)),
Second => (2, time.map(|t| (t.second() +
t.nanosecond() / 1_000_000_000) as i64)),
Nanosecond => (9, time.map(|t| (t.nanosecond() % 1_000_000_000) as i64)),
Timestamp => (1, match (date, time) {
(Some(d), Some(t)) => Some(d.and_time(*t).num_seconds_from_unix_epoch()),
(_, _) => None
}),
};
if let Some(v) = v {
match pad {
Pad::None => try!(write!(w, "{}", v)),
Pad::Zero => try!(write!(w, "{:01$}", v, width)),
Pad::Space => try!(write!(w, "{:1$}", v, width)),
}
} else {
return Err(fmt::Error); // insufficient arguments for given format
}
},
Item::Fixed(spec) => {
use self::Fixed::*;
let ret = match spec {
ShortMonthName =>
date.map(|d| write!(w, "{}", SHORT_MONTHS[d.month0() as usize])),
LongMonthName =>
date.map(|d| write!(w, "{}", LONG_MONTHS[d.month0() as usize])),
ShortWeekdayName =>
date.map(|d| write!(w, "{}",
SHORT_WEEKDAYS[d.weekday() .num_days_from_monday() as usize])),
LongWeekdayName =>
date.map(|d| write!(w, "{}",
LONG_WEEKDAYS[d.weekday().num_days_from_monday() as usize])),
LowerAmPm =>
time.map(|t| write!(w, "{}", if t.hour12().0 {"pm"} else {"am"})),
UpperAmPm =>
time.map(|t| write!(w, "{}", if t.hour12().0 {"PM"} else {"AM"})),
TimezoneName =>
off.map(|&(ref name, _)| write!(w, "{}", *name)),
TimezoneOffset =>
off.map(|&(_, ref local_minus_utc)| {
let off = local_minus_utc.num_minutes();
let (sign, off) = if off < 0 {('-', -off)} else {('+', off)};
write!(w, "{}{:02}{:02}", sign, off / 60, off % 60)
}),
TimezoneOffsetZ =>
off.map(|&(_, ref local_minus_utc)| {
let off = local_minus_utc.num_minutes();
if off != 0 {
let (sign, off) = if off < 0 {('-', -off)} else {('+', off)};
write!(w, "{}{:02}{:02}", sign, off / 60, off % 60)
} else {
write!(w, "Z")
}
}),
};
match ret {
Some(ret) => try!(ret),
None => return Err(fmt::Error), // insufficient arguments for given format
}
},
Item::Error => return Err(fmt::Error),
}
}
Ok(())
}
/// A *temporary* object which can be used as an argument to `format!` or others.
/// This is normally constructed via `format` methods of each date and time type.
#[derive(Debug)]
pub struct DelayedFormat<'a, I: Iterator<Item=Item<'a>> + Clone> {
/// The date view, if any.
date: Option<NaiveDate>,
/// The time view, if any.
time: Option<NaiveTime>,
/// The name and local-to-UTC difference for the offset (timezone), if any.
off: Option<(String, Duration)>,
/// An iterator returning formatting items.
items: I,
}
impl<'a, I: Iterator<Item=Item<'a>> + Clone> DelayedFormat<'a, I> {
/// Makes a new `DelayedFormat` value out of local date and time.
pub fn new(date: Option<NaiveDate>, time: Option<NaiveTime>, items: I) -> DelayedFormat<'a, I> {
DelayedFormat { date: date, time: time, off: None, items: items }
}
/// Makes a new `DelayedFormat` value out of local date and time and UTC offset.
pub fn new_with_offset<Off>(date: Option<NaiveDate>, time: Option<NaiveTime>,
offset: &Off, items: I) -> DelayedFormat<'a, I>
where Off: Offset + fmt::Display {
let name_and_diff = (offset.to_string(), offset.local_minus_utc());
DelayedFormat { date: date, time: time, off: Some(name_and_diff), items: items }
}
}
impl<'a, I: Iterator<Item=Item<'a>> + Clone> fmt::Display for DelayedFormat<'a, I> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
format(f, self.date.as_ref(), self.time.as_ref(), self.off.as_ref(), self.items.clone())
}
}
pub mod strftime;

272
src/format/strftime.rs Normal file
View File

@ -0,0 +1,272 @@
// 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.
%B July Full month name.
%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.
%A Sunday Full weekday name.
%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
ISO 8601 date & time format. Same to `%Y-%m-%dT%H:%M:%S%z`.
%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)]),
'F' => Some(recons![num!(Year), lit!("-"), num0!(Month), lit!("-"),
num0!(Day)]),
'G' => Some(num!(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(num!(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!(" "),
num!(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!("-"),
num!(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(recons![num!(Year), lit!("-"), num0!(Month), lit!("-"), num0!(Day),
lit!("T"), num0!(Hour), lit!(":"), num0!(Minute), lit!(":"),
num0!(Second), fix!(TimezoneOffset)]),
'%' => 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
let nextspec = self.remainder.find(|&: c: char| !c.is_whitespace())
.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
_ => {
let nextspec = self.remainder.find(|&: c: char| c.is_whitespace() || c == '%')
.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")]);
assert_eq!(parse_and_collect("%Y-%m-%d"), [num!(Year), lit!("-"), num0!(Month), lit!("-"),
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]);
}

View File

@ -15,7 +15,7 @@ use div::div_mod_floor;
use duration::Duration;
use naive::time::NaiveTime;
use naive::datetime::NaiveDateTime;
use format::DelayedFormat;
use format::{DelayedFormat, StrftimeItems};
use self::internals::{DateImpl, Of, Mdf, YearFlags};
@ -303,10 +303,10 @@ impl NaiveDate {
}
/// Formats the date in the specified format string.
/// See the `format` module on the supported escape sequences.
/// See the `format::strftime` module on the supported escape sequences.
#[inline]
pub fn format<'a>(&'a self, fmt: &'a str) -> DelayedFormat<'a> {
DelayedFormat::new(Some(self.clone()), None, fmt)
pub fn format<'a>(&'a self, fmt: &'a str) -> DelayedFormat<'a, StrftimeItems<'a>> {
DelayedFormat::new(Some(self.clone()), None, StrftimeItems::new(fmt))
}
}

View File

@ -15,7 +15,7 @@ use div::div_mod_floor;
use duration::Duration;
use naive::time::NaiveTime;
use naive::date::NaiveDate;
use format::DelayedFormat;
use format::{DelayedFormat, StrftimeItems};
/// ISO 8601 combined date and time without timezone.
#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone)]
@ -82,10 +82,11 @@ impl NaiveDateTime {
}
/// Formats the combined date and time in the specified format string.
/// See the `format` module on the supported escape sequences.
/// See the `format::strftime` module on the supported escape sequences.
#[inline]
pub fn format<'a>(&'a self, fmt: &'a str) -> DelayedFormat<'a> {
DelayedFormat::new(Some(self.date.clone()), Some(self.time.clone()), fmt)
pub fn format<'a>(&'a self, fmt: &'a str) -> DelayedFormat<'a, StrftimeItems<'a>> {
DelayedFormat::new(Some(self.date.clone()), Some(self.time.clone()),
StrftimeItems::new(fmt))
}
}
@ -281,7 +282,13 @@ mod tests {
fn test_datetime_format() {
let dt = NaiveDate::from_ymd(2010, 9, 8).and_hms_milli(7, 6, 54, 321);
assert_eq!(dt.format("%c").to_string(), "Wed Sep 8 07:06:54 2010");
assert_eq!(dt.format("%s").to_string(), "1283929614");
assert_eq!(dt.format("%t%n%%%n%t").to_string(), "\t\n%\n\t");
// a horror of leap second: coming near to you.
let dt = NaiveDate::from_ymd(2012, 6, 30).and_hms_milli(23, 59, 59, 1_000);
assert_eq!(dt.format("%c").to_string(), "Sat Jun 30 23:59:60 2012");
assert_eq!(dt.format("%s").to_string(), "1341100799"); // not 1341100800, it's intentional.
}
}

View File

@ -14,7 +14,7 @@ use Timelike;
use div::div_mod_floor;
use offset::Offset;
use duration::Duration;
use format::DelayedFormat;
use format::{DelayedFormat, StrftimeItems};
/// ISO 8601 time without timezone.
/// Allows for the nanosecond precision and optional leap second representation.
@ -119,10 +119,10 @@ impl NaiveTime {
}
/// Formats the time in the specified format string.
/// See the `format` module on the supported escape sequences.
/// See the `format::strftime` module on the supported escape sequences.
#[inline]
pub fn format<'a>(&'a self, fmt: &'a str) -> DelayedFormat<'a> {
DelayedFormat::new(None, Some(self.clone()), fmt)
pub fn format<'a>(&'a self, fmt: &'a str) -> DelayedFormat<'a, StrftimeItems<'a>> {
DelayedFormat::new(None, Some(self.clone()), StrftimeItems::new(fmt))
}
/// Returns a triple of the hour, minute and second numbers.
@ -381,6 +381,8 @@ mod tests {
// corner cases
assert_eq!(NaiveTime::from_hms(13, 57, 9).format("%r").to_string(), "01:57:09 PM");
assert_eq!(NaiveTime::from_hms_milli(23, 59, 59, 1_000).format("%X").to_string(),
"23:59:60");
}
}

View File

@ -14,7 +14,7 @@ use Timelike;
use offset::Offset;
use duration::Duration;
use naive::time::NaiveTime;
use format::DelayedFormat;
use format::{DelayedFormat, StrftimeItems};
/// ISO 8601 time with timezone.
#[derive(Clone)]
@ -52,10 +52,11 @@ impl<Off:Offset> Time<Off> {
impl<Off: Offset + fmt::Display> Time<Off> {
/// Formats the time in the specified format string.
/// See the `format` module on the supported escape sequences.
/// See the `format::strftime` module on the supported escape sequences.
#[inline]
pub fn format<'a>(&'a self, fmt: &'a str) -> DelayedFormat<'a> {
DelayedFormat::new_with_offset(None, Some(self.local()), &self.offset, fmt)
pub fn format<'a>(&'a self, fmt: &'a str) -> DelayedFormat<'a, StrftimeItems<'a>> {
DelayedFormat::new_with_offset(None, Some(self.local()), &self.offset,
StrftimeItems::new(fmt))
}
}