diff --git a/src/date.rs b/src/date.rs index 2144a46..3b927d6 100644 --- a/src/date.rs +++ b/src/date.rs @@ -15,6 +15,7 @@ use naive; use naive::date::NaiveDate; use naive::time::NaiveTime; use datetime::DateTime; +use format::DelayedFormat; /// ISO 8601 calendar date with timezone. #[deriving(Clone)] @@ -158,6 +159,13 @@ impl Date { self.date.pred_opt().map(|date| Date::from_utc(date, self.offset.clone())) } + /// Formats the date in the specified format string. + /// See the `format` 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) + } + /// Returns a view to the local date. fn local(&self) -> NaiveDate { self.offset.to_local_date(&self.date) diff --git a/src/datetime.rs b/src/datetime.rs index 9d10c5f..8a9068d 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -14,6 +14,7 @@ use duration::Duration; use naive::datetime::NaiveDateTime; use time::Time; use date::Date; +use format::DelayedFormat; /// ISO 8601 combined date and time with timezone. #[deriving(Clone)] @@ -48,6 +49,14 @@ impl DateTime { self.datetime.num_seconds_from_unix_epoch() } + /// Formats the combined date and time in the specified format string. + /// See the `format` module on the supported escape sequences. + #[inline] + pub fn format<'a>(&'a self, fmt: &'a str) -> DelayedFormat<'a> { + let local = self.local(); + DelayedFormat::new_with_offset(Some(local.date()), Some(local.time()), &self.offset, fmt) + } + /// Returns a view to the local datetime. fn local(&self) -> NaiveDateTime { self.offset.to_local_datetime(&self.datetime) diff --git a/src/format.rs b/src/format.rs new file mode 100644 index 0000000..de4d407 --- /dev/null +++ b/src/format.rs @@ -0,0 +1,215 @@ +// This is a part of rust-chrono. +// Copyright (c) 2014, Kang Seonghoon. +// See README.md and LICENSE.txt for details. + +/*! + * Formatting utilities for date and time. + */ + +use std::fmt; +use std::str::MaybeOwned; +use std::io::{IoResult, IoError, InvalidInput}; + +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 Writer, date: Option<&NaiveDate>, time: Option<&NaiveTime>, + off: Option<&(MaybeOwned<'static>, Duration)>, fmt: &str) -> IoResult<()> { + 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) = part.slice_shift_char(); + 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().val0())), + (Some('g'), Some(d), _, _) => try!(write!(w, "{:02}", d.isoweekdate().val0() % 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 uint])), + (Some('B'), Some(d), _, _) => + try!(write!(w, "{}", LONG_MONTHS[d.month0() as uint])), + + // 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().val1())), + + // day of week + (Some('a'), Some(d), _, _) => + try!(write!(w, "{}", SHORT_WEEKDAYS[d.weekday().num_days_from_monday() as uint])), + (Some('A'), Some(d), _, _) => + try!(write!(w, "{}", LONG_WEEKDAYS[d.weekday().num_days_from_monday() as uint])), + (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 uint], + 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().val1())), + (Some('l'), _, Some(t), _) => try!(write!(w, "{:2}", t.hour12().val1())), + (Some('P'), _, Some(t), _) => + try!(write!(w, "{}", if t.hour12().val0() {"pm"} else {"am"})), + (Some('p'), _, Some(t), _) => + try!(write!(w, "{}", if t.hour12().val0() {"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 uint], + SHORT_MONTHS[d.month0() as uint], 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")), + + (Some(c), _, _, _) => { + return Err(IoError { kind: InvalidInput, desc: "invalid date/time format", + detail: Some(format!("unsupported escape sequence %{}", c)) }); + } + + (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 `%` + Err(IoError { kind: InvalidInput, + desc: "invalid date/time format: stray `%`", detail: None }) + } 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. +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<(MaybeOwned<'static>, 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> { + let name_and_diff = (offset.name(), offset.local_minus_utc()); + DelayedFormat { date: date, time: time, off: Some(name_and_diff), fmt: fmt } + } +} + +impl<'a> fmt::Show 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::WriteError) // XXX + } +} + diff --git a/src/lib.rs b/src/lib.rs index bd4b953..3902bf7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,6 +39,7 @@ pub mod naive { pub mod date; pub mod time; pub mod datetime; +pub mod format; /// The day of week (DOW). /// diff --git a/src/naive/date.rs b/src/naive/date.rs index 7c34727..42995d9 100644 --- a/src/naive/date.rs +++ b/src/naive/date.rs @@ -13,6 +13,7 @@ use {Weekday, Datelike}; use duration::Duration; use naive::time::NaiveTime; use naive::datetime::NaiveDateTime; +use format::DelayedFormat; use self::internals::{DateImpl, Of, Mdf, YearFlags}; @@ -296,6 +297,13 @@ impl NaiveDate { pub fn pred_opt(&self) -> Option { self.with_of(self.of().pred()).or_else(|| NaiveDate::from_ymd_opt(self.year() - 1, 12, 31)) } + + /// Formats the date in the specified format string. + /// See the `format` 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) + } } impl Datelike for NaiveDate { @@ -736,6 +744,27 @@ mod tests { assert_eq!(format!("{:+30}", NaiveDate::from_ymd(1234, 5, 6)), "1234-05-06".to_string()); assert_eq!(format!("{:30}", NaiveDate::from_ymd(12345, 6, 7)), "+12345-06-07".to_string()); } + + #[test] + fn test_date_format() { + let d = NaiveDate::from_ymd(2012, 3, 4); + assert_eq!(d.format("%Y,%C,%y,%G,%g").to_string(), "2012,20,12,2012,12".to_string()); + assert_eq!(d.format("%m,%b,%h,%B").to_string(), "03,Mar,Mar,March".to_string()); + assert_eq!(d.format("%d,%e").to_string(), "04, 4".to_string()); + assert_eq!(d.format("%U,%W,%V").to_string(), "10,09,09".to_string()); + assert_eq!(d.format("%a,%A,%w,%u").to_string(), "Sun,Sunday,0,7".to_string()); + assert_eq!(d.format("%j").to_string(), "064".to_string()); // since 2012 is a leap year + assert_eq!(d.format("%D,%x").to_string(), "03/04/12,03/04/12".to_string()); + assert_eq!(d.format("%F").to_string(), "2012-03-04".to_string()); + assert_eq!(d.format("%v").to_string(), " 4-Mar-2012".to_string()); + assert_eq!(d.format("%t%n%%%n%t").to_string(), "\t\n%\n\t".to_string()); + + // corner cases + assert_eq!(NaiveDate::from_ymd(2007, 12, 31).format("%G,%g,%U,%W,%V").to_string(), + "2008,08,53,53,01".to_string()); + assert_eq!(NaiveDate::from_ymd(2010, 1, 3).format("%G,%g,%U,%W,%V").to_string(), + "2009,09,01,00,53".to_string()); + } } /** diff --git a/src/naive/datetime.rs b/src/naive/datetime.rs index b6726f5..95570d4 100644 --- a/src/naive/datetime.rs +++ b/src/naive/datetime.rs @@ -13,6 +13,7 @@ use {Weekday, Timelike, Datelike}; use duration::Duration; use naive::time::NaiveTime; use naive::date::NaiveDate; +use format::DelayedFormat; /// ISO 8601 combined date and time without timezone. #[deriving(PartialEq, Eq, PartialOrd, Ord, Clone, Hash)] @@ -77,6 +78,13 @@ impl NaiveDateTime { let nseconds = self.time.num_seconds_from_midnight() as i64; (ndays - 719163) * 86400 + nseconds } + + /// Formats the combined date and time in the specified format string. + /// See the `format` 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) + } } impl Datelike for NaiveDateTime { @@ -247,5 +255,12 @@ mod tests { assert_eq!(to_timestamp(2001, 9, 9, 1, 46, 40), 1_000_000_000); assert_eq!(to_timestamp(2038, 1, 19, 3, 14, 7), 0x7fffffff); } + + #[test] + 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".to_string()); + assert_eq!(dt.format("%t%n%%%n%t").to_string(), "\t\n%\n\t".to_string()); + } } diff --git a/src/naive/time.rs b/src/naive/time.rs index 6b1a4ab..3f5627f 100644 --- a/src/naive/time.rs +++ b/src/naive/time.rs @@ -12,6 +12,7 @@ use num::Integer; use Timelike; use offset::Offset; use duration::Duration; +use format::DelayedFormat; /// ISO 8601 time without timezone. /// Allows for the nanosecond precision and optional leap second representation. @@ -115,6 +116,13 @@ impl NaiveTime { Some(NaiveTime { secs: secs, frac: nano }) } + /// Formats the time in the specified format string. + /// See the `format` 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) + } + /// Returns a triple of the hour, minute and second numbers. fn hms(&self) -> (u32, u32, u32) { let (mins, sec) = self.secs.div_mod_floor(&60); @@ -350,5 +358,21 @@ mod tests { assert_eq!(format!("{:30}", NaiveTime::from_hms_milli(3, 5, 7, 9)), "03:05:07.009".to_string()); } + + #[test] + fn test_time_format() { + let t = NaiveTime::from_hms_nano(3, 5, 7, 98765432); + assert_eq!(t.format("%H,%k,%I,%l,%P,%p").to_string(), "03, 3,03, 3,am,AM".to_string()); + assert_eq!(t.format("%M").to_string(), "05".to_string()); + assert_eq!(t.format("%S,%f").to_string(), "07,098765432".to_string()); + assert_eq!(t.format("%R").to_string(), "03:05".to_string()); + assert_eq!(t.format("%T,%X").to_string(), "03:05:07,03:05:07".to_string()); + assert_eq!(t.format("%r").to_string(), "03:05:07 AM".to_string()); + assert_eq!(t.format("%t%n%%%n%t").to_string(), "\t\n%\n\t".to_string()); + + // corner cases + assert_eq!(NaiveTime::from_hms(13, 57, 9).format("%r").to_string(), + "01:57:09 PM".to_string()); + } } diff --git a/src/offset.rs b/src/offset.rs index 928a407..931f291 100644 --- a/src/offset.rs +++ b/src/offset.rs @@ -7,6 +7,7 @@ */ use std::fmt; +use std::str::MaybeOwned; use stdtime; use num::Integer; @@ -214,6 +215,12 @@ pub trait Offset: Clone + fmt::Show { } } + /// Returns a name or abbreviation of this offset. + fn name(&self) -> MaybeOwned<'static>; + + /// Returns the *current* offset from UTC to the local time. + fn local_minus_utc(&self) -> Duration; + /// Converts the local `NaiveDate` to the timezone-aware `Date` if possible. fn from_local_date(&self, local: &NaiveDate) -> LocalResult>; @@ -253,6 +260,9 @@ impl UTC { } impl Offset for UTC { + fn name(&self) -> MaybeOwned<'static> { "UTC".into_maybe_owned() } + fn local_minus_utc(&self) -> Duration { Duration::zero() } + fn from_local_date(&self, local: &NaiveDate) -> LocalResult> { Single(Date::from_utc(local.clone(), UTC)) } @@ -321,6 +331,9 @@ impl FixedOffset { } impl Offset for FixedOffset { + fn name(&self) -> MaybeOwned<'static> { "UTC".into_maybe_owned() } // XXX + fn local_minus_utc(&self) -> Duration { Duration::seconds(self.local_minus_utc) } + fn from_local_date(&self, local: &NaiveDate) -> LocalResult> { Single(Date::from_utc(local.clone(), self.clone())) } @@ -359,7 +372,7 @@ impl fmt::Show for FixedOffset { /// The local timescale. This is implemented via the standard `time` crate. #[deriving(Clone)] pub struct Local { - cached: FixedOffset, + cached: FixedOffset, } impl Local { @@ -410,6 +423,9 @@ impl Local { } impl Offset for Local { + fn name(&self) -> MaybeOwned<'static> { "LMT".into_maybe_owned() } // XXX XXX + fn local_minus_utc(&self) -> Duration { self.cached.local_minus_utc() } + fn from_local_date(&self, local: &NaiveDate) -> LocalResult> { match self.from_local_datetime(&local.and_hms(0, 0, 0)) { NoResult => NoResult, diff --git a/src/time.rs b/src/time.rs index 2a07a50..68266f2 100644 --- a/src/time.rs +++ b/src/time.rs @@ -12,6 +12,7 @@ use Timelike; use offset::Offset; use duration::Duration; use naive::time::NaiveTime; +use format::DelayedFormat; /// ISO 8601 time with timezone. #[deriving(Clone)] @@ -28,6 +29,13 @@ impl Time { Time { time: time, offset: offset } } + /// Formats the time in the specified format string. + /// See the `format` 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) + } + /// Returns a view to the local time. fn local(&self) -> NaiveTime { self.offset.to_local_time(&self.time)