From 43ee68b5223ee4deb3e5e5cb8eb8b8cda741eaf4 Mon Sep 17 00:00:00 2001 From: Kang Seonghoon Date: Mon, 26 Jan 2015 21:14:50 +0900 Subject: [PATCH] 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. --- src/date.rs | 9 +- src/datetime.rs | 9 +- src/div.rs | 14 ++- src/format.rs | 216 -------------------------------- src/format/mod.rs | 276 +++++++++++++++++++++++++++++++++++++++++ src/format/strftime.rs | 272 ++++++++++++++++++++++++++++++++++++++++ src/naive/date.rs | 8 +- src/naive/datetime.rs | 15 ++- src/naive/time.rs | 10 +- src/time.rs | 9 +- 10 files changed, 592 insertions(+), 246 deletions(-) delete mode 100644 src/format.rs create mode 100644 src/format/mod.rs create mode 100644 src/format/strftime.rs diff --git a/src/date.rs b/src/date.rs index 7580899..4d6d0fe 100644 --- a/src/date.rs +++ b/src/date.rs @@ -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 Date { impl Date { /// 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)) } } diff --git a/src/datetime.rs b/src/datetime.rs index 25b00a6..18070b6 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -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 DateTime { impl DateTime { /// 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)) } } diff --git a/src/div.rs b/src/div.rs index 750714f..d04933a 100644 --- a/src/div.rs +++ b/src/div.rs @@ -16,14 +16,16 @@ pub fn div_rem(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(a: T, b: T) -> T { + div_mod_floor(a, b).0 +} + +/// Calculates a floored modulo. #[inline] pub fn mod_floor(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. diff --git a/src/format.rs b/src/format.rs deleted file mode 100644 index 1417e10..0000000 --- a/src/format.rs +++ /dev/null @@ -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 { // `%%` - 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, - /// The time view, if any. - time: Option, - /// 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, time: Option, - 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(date: Option, time: Option, - 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... - } -} - diff --git a/src/format/mod.rs b/src/format/mod.rs new file mode 100644 index 0000000..f7f13a7 --- /dev/null +++ b/src/format/mod.rs @@ -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> { + 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> + Clone> { + /// The date view, if any. + date: Option, + /// The time view, if any. + time: Option, + /// 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> + Clone> DelayedFormat<'a, I> { + /// Makes a new `DelayedFormat` value out of local date and time. + pub fn new(date: Option, time: Option, 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(date: Option, time: Option, + 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> + 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; + diff --git a/src/format/strftime.rs b/src/format/strftime.rs new file mode 100644 index 0000000..0848949 --- /dev/null +++ b/src/format/strftime.rs @@ -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> { + // 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> { + // 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::>>().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]); +} + diff --git a/src/naive/date.rs b/src/naive/date.rs index 71c07f2..9f89199 100644 --- a/src/naive/date.rs +++ b/src/naive/date.rs @@ -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)) } } diff --git a/src/naive/datetime.rs b/src/naive/datetime.rs index 9fdfc9e..e9856a8 100644 --- a/src/naive/datetime.rs +++ b/src/naive/datetime.rs @@ -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. } } diff --git a/src/naive/time.rs b/src/naive/time.rs index a235edb..5488e59 100644 --- a/src/naive/time.rs +++ b/src/naive/time.rs @@ -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"); } } diff --git a/src/time.rs b/src/time.rs index ded3b95..84df8e1 100644 --- a/src/time.rs +++ b/src/time.rs @@ -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 Time { impl Time { /// 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)) } }