diff --git a/Cargo.toml b/Cargo.toml index 4401441..6b45c5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,5 +15,5 @@ license = "MIT/Apache-2.0" name = "chrono" [dependencies] -time = "0.1.15" +time = "0.1.16" diff --git a/README.md b/README.md index 333ad09..1e1eebf 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ chrono = "0.2" And this in your crate root: -```toml +```rust extern crate chrono; ``` @@ -53,14 +53,14 @@ Chrono simply reexports it. Chrono provides a `DateTime` type for the combined date and time. `DateTime`, among others, is timezone-aware and -must be constructed from the timezone object (`Offset`). -`DateTime`s with different offsets do not mix, but can be converted to each other. +must be constructed from the `TimeZone` object. +`DateTime`s with different time zones do not mix, but can be converted to each other. You can get the current date and time in the UTC timezone (`UTC::now()`) or in the local timezone (`Local::now()`). ~~~~ {.rust} -use chrono::{UTC, Local, DateTime}; +use chrono::*; let utc: DateTime = UTC::now(); // e.g. `2014-11-28T12:45:59.324310806Z` let local: DateTime = Local::now(); // e.g. `2014-11-28T21:45:59.324310806+09:00` @@ -71,7 +71,7 @@ This is a bit verbose due to Rust's lack of function and method overloading, but in turn we get a rich combination of initialization methods. ~~~~ {.rust} -use chrono::{UTC, Offset, Weekday, LocalResult}; +use chrono::*; let dt = UTC.ymd(2014, 7, 8).and_hms(9, 10, 11); // `2014-07-08T09:10:11Z` // July 8 is 188th day of the year 2014 (`o` for "ordinal") @@ -96,7 +96,7 @@ Addition and subtraction is also supported. The following illustrates most supported operations to the date and time: ~~~~ {.rust} -use chrono::{UTC, Local, Datelike, Timelike, Weekday, Duration}; +use chrono::*; // assume this returned `2014-11-28T21:45:59.324310806+09:00`: let dt = Local::now(); @@ -110,9 +110,10 @@ assert_eq!(dt.weekday().number_from_monday(), 5); // Mon=1, ..., Sat=7 assert_eq!(dt.ordinal(), 332); // the day of year assert_eq!(dt.num_days_from_ce(), 735565); // the number of days from and including Jan 1, 1 -// offset accessor and manipulation +// time zone accessor and manipulation assert_eq!(dt.offset().local_minus_utc(), Duration::hours(9)); -assert_eq!(dt.with_offset(UTC), UTC.ymd(2014, 11, 28).and_hms_nano(12, 45, 59, 324310806)); +assert_eq!(dt.timezone(), FixedOffset::east(9 * 3600)); +assert_eq!(dt.with_timezone(&UTC), UTC.ymd(2014, 11, 28).and_hms_nano(12, 45, 59, 324310806)); // a sample of property manipulations (validates dynamically) assert_eq!(dt.with_day(29).unwrap().weekday(), Weekday::Sat); // 2014-11-29 is Saturday @@ -134,7 +135,7 @@ which format is equivalent to the familiar `strftime` format. The default `to_string` method and `{:?}` specifier also give a reasonable representation. ~~~~ {.rust} -use chrono::{UTC, Offset}; +use chrono::*; let dt = UTC.ymd(2014, 11, 28).and_hms(12, 0, 9); assert_eq!(dt.format("%Y-%m-%d %H:%M:%S").to_string(), "2014-11-28 12:00:09"); @@ -145,27 +146,40 @@ assert_eq!(dt.to_string(), "2014-11-28 12:00:09 UTC"); assert_eq!(format!("{:?}", dt), "2014-11-28T12:00:09Z"); ~~~~ -Parsing can be done with two methods: +Parsing can be done with three methods: -- `DateTime::parse_from_str` parses a date and time with offsets and - returns `DateTime`. - This should be used when the offset is a part of input and the caller cannot guess that. - It *cannot* be used when the offset can be missing. +1. The standard `FromStr` trait (and `parse` method on a string) can be used for + parsing `DateTime` and `DateTime` values. + This parses what the `{:?}` (`std::fmt::Debug`) format specifier prints, + and requires the offset to be present. -- `Offset::datetime_from_str` is similar but returns `DateTime` of given offset. - When the explicit offset is missing from the input, it simply uses given offset. - It issues an error when the input contains an explicit offset different from the current offset. +2. `DateTime::parse_from_str` parses a date and time with offsets and + returns `DateTime`. + This should be used when the offset is a part of input and the caller cannot guess that. + It *cannot* be used when the offset can be missing. + +3. `Offset::datetime_from_str` is similar but returns `DateTime` of given offset. + When the explicit offset is missing from the input, it simply uses given offset. + It issues an error when the input contains an explicit offset different from the current offset. More detailed control over the parsing process is available via `format` module. ~~~~ {.rust} -use chrono::{UTC, Offset, DateTime}; +use chrono::*; let dt = UTC.ymd(2014, 11, 28).and_hms(12, 0, 9); + +// method 1 +assert_eq!("2014-11-28T12:00:09Z".parse::>(), Ok(dt.clone())); +assert_eq!("2014-11-28T21:00:09+09:00".parse::>(), Ok(dt.clone())); + +// method 2 assert_eq!(UTC.datetime_from_str("2014-11-28 12:00:09", "%Y-%m-%d %H:%M:%S"), Ok(dt.clone())); assert_eq!(UTC.datetime_from_str("Fri Nov 28 12:00:09 2014", "%a %b %e %T %Y"), Ok(dt.clone())); -assert_eq!(DateTime::parse_from_str("2014-11-28 21:00:09 +09:00", - "%Y-%m-%d %H:%M:%S %z").map(|dt| dt.with_offset(UTC)), Ok(dt)); + +// method 3 +assert_eq!(DateTime::parse_from_str("2014-11-28 21:00:09 +09:00", "%Y-%m-%d %H:%M:%S %z"), + Ok(dt.with_timezone(&FixedOffset::east(9*3600)))); // oops, the year is missing! assert!(UTC.datetime_from_str("Fri Nov 28 12:00:09", "%a %b %e %T %Y").is_err()); @@ -178,12 +192,12 @@ assert!(UTC.datetime_from_str("Sat Nov 28 12:00:09 2014", "%a %b %e %T %Y").is_e ### Individual date and time Chrono also provides an individual date type (`Date`) and time type (`Time`). -They also have offsets attached, and have to be constructed via offsets. +They also have time zones attached, and have to be constructed via time zones. Most operations available to `DateTime` are also available to `Date` and `Time` whenever appropriate. ~~~~ {.rust} -use chrono::{UTC, Local, Offset, LocalResult, Datelike, Weekday}; +use chrono::*; assert_eq!(UTC::today(), UTC::now().date()); assert_eq!(Local::today(), Local::now().date()); @@ -202,7 +216,7 @@ Chrono provides naive counterparts to `Date`, `Time` and `DateTime` as `NaiveDate`, `NaiveTime` and `NaiveDateTime` respectively. They have almost equivalent interfaces as their timezone-aware twins, -but are not associated to offsets obviously and can be quite low-level. +but are not associated to time zones obviously and can be quite low-level. They are mostly useful for building blocks for higher-level types. ## Limitations @@ -223,4 +237,5 @@ Any operation that can be ambiguous will return `None` in such cases. For example, "a month later" of 2014-01-30 is not well-defined and consequently `UTC.ymd(2014, 1, 30).with_month(2)` returns `None`. -Advanced offset handling is not yet supported (but is planned in 0.3). +Advanced time zone handling is not yet supported (but is planned in 0.3). + diff --git a/src/date.rs b/src/date.rs index 6a86989..06a8e54 100644 --- a/src/date.rs +++ b/src/date.rs @@ -3,7 +3,7 @@ // See README.md and LICENSE.txt for details. /*! - * ISO 8601 calendar date with timezone. + * ISO 8601 calendar date with time zone. */ use std::{fmt, hash}; @@ -12,18 +12,19 @@ use std::ops::{Add, Sub}; use {Weekday, Datelike}; use duration::Duration; -use offset::{Offset, UTC}; +use offset::{TimeZone, Offset}; +use offset::utc::UTC; use naive; use naive::date::NaiveDate; use naive::time::NaiveTime; use datetime::DateTime; use format::{Item, DelayedFormat, StrftimeItems}; -/// ISO 8601 calendar date with timezone. +/// ISO 8601 calendar date with time zone. #[derive(Clone)] -pub struct Date { +pub struct Date { date: NaiveDate, - offset: Off, + offset: Tz::Offset, } /// The minimum possible `Date`. @@ -31,11 +32,13 @@ pub const MIN: Date = Date { date: naive::date::MIN, offset: UTC }; /// The maximum possible `Date`. pub const MAX: Date = Date { date: naive::date::MAX, offset: UTC }; -impl Date { +impl Date { /// Makes a new `Date` with given *UTC* date and offset. - /// The local date should be constructed via the `Offset` trait. + /// The local date should be constructed via the `TimeZone` trait. + // + // note: this constructor is purposedly not named to `new` to discourage the direct usage. #[inline] - pub fn from_utc(date: NaiveDate, offset: Off) -> Date { + pub fn from_utc(date: NaiveDate, offset: Tz::Offset) -> Date { Date { date: date, offset: offset } } @@ -44,9 +47,9 @@ impl Date { /// /// Fails on invalid datetime. #[inline] - pub fn and_time(&self, time: NaiveTime) -> Option> { - let localdt = self.offset.to_local_date(&self.date).and_time(time); - self.offset.from_local_datetime(&localdt).single() + pub fn and_time(&self, time: NaiveTime) -> Option> { + let localdt = self.naive_local().and_time(time); + self.timezone().from_local_datetime(&localdt).single() } /// Makes a new `DateTime` from the current date, hour, minute and second. @@ -54,7 +57,7 @@ impl Date { /// /// Fails on invalid hour, minute and/or second. #[inline] - pub fn and_hms(&self, hour: u32, min: u32, sec: u32) -> DateTime { + pub fn and_hms(&self, hour: u32, min: u32, sec: u32) -> DateTime { self.and_hms_opt(hour, min, sec).expect("invalid time") } @@ -63,7 +66,7 @@ impl Date { /// /// Returns `None` on invalid hour, minute and/or second. #[inline] - pub fn and_hms_opt(&self, hour: u32, min: u32, sec: u32) -> Option> { + pub fn and_hms_opt(&self, hour: u32, min: u32, sec: u32) -> Option> { NaiveTime::from_hms_opt(hour, min, sec).and_then(|time| self.and_time(time)) } @@ -73,7 +76,7 @@ impl Date { /// /// Fails on invalid hour, minute, second and/or millisecond. #[inline] - pub fn and_hms_milli(&self, hour: u32, min: u32, sec: u32, milli: u32) -> DateTime { + pub fn and_hms_milli(&self, hour: u32, min: u32, sec: u32, milli: u32) -> DateTime { self.and_hms_milli_opt(hour, min, sec, milli).expect("invalid time") } @@ -84,7 +87,7 @@ impl Date { /// Returns `None` on invalid hour, minute, second and/or millisecond. #[inline] pub fn and_hms_milli_opt(&self, hour: u32, min: u32, sec: u32, - milli: u32) -> Option> { + milli: u32) -> Option> { NaiveTime::from_hms_milli_opt(hour, min, sec, milli).and_then(|time| self.and_time(time)) } @@ -94,7 +97,7 @@ impl Date { /// /// Fails on invalid hour, minute, second and/or microsecond. #[inline] - pub fn and_hms_micro(&self, hour: u32, min: u32, sec: u32, micro: u32) -> DateTime { + pub fn and_hms_micro(&self, hour: u32, min: u32, sec: u32, micro: u32) -> DateTime { self.and_hms_micro_opt(hour, min, sec, micro).expect("invalid time") } @@ -105,7 +108,7 @@ impl Date { /// Returns `None` on invalid hour, minute, second and/or microsecond. #[inline] pub fn and_hms_micro_opt(&self, hour: u32, min: u32, sec: u32, - micro: u32) -> Option> { + micro: u32) -> Option> { NaiveTime::from_hms_micro_opt(hour, min, sec, micro).and_then(|time| self.and_time(time)) } @@ -115,7 +118,7 @@ impl Date { /// /// Fails on invalid hour, minute, second and/or nanosecond. #[inline] - pub fn and_hms_nano(&self, hour: u32, min: u32, sec: u32, nano: u32) -> DateTime { + pub fn and_hms_nano(&self, hour: u32, min: u32, sec: u32, nano: u32) -> DateTime { self.and_hms_nano_opt(hour, min, sec, nano).expect("invalid time") } @@ -126,7 +129,7 @@ impl Date { /// Returns `None` on invalid hour, minute, second and/or nanosecond. #[inline] pub fn and_hms_nano_opt(&self, hour: u32, min: u32, sec: u32, - nano: u32) -> Option> { + nano: u32) -> Option> { NaiveTime::from_hms_nano_opt(hour, min, sec, nano).and_then(|time| self.and_time(time)) } @@ -134,7 +137,7 @@ impl Date { /// /// Fails when `self` is the last representable date. #[inline] - pub fn succ(&self) -> Date { + pub fn succ(&self) -> Date { self.succ_opt().expect("out of bound") } @@ -142,7 +145,7 @@ impl Date { /// /// Returns `None` when `self` is the last representable date. #[inline] - pub fn succ_opt(&self) -> Option> { + pub fn succ_opt(&self) -> Option> { self.date.succ_opt().map(|date| Date::from_utc(date, self.offset.clone())) } @@ -150,7 +153,7 @@ impl Date { /// /// Fails when `self` is the first representable date. #[inline] - pub fn pred(&self) -> Date { + pub fn pred(&self) -> Date { self.pred_opt().expect("out of bound") } @@ -158,28 +161,34 @@ impl Date { /// /// Returns `None` when `self` is the first representable date. #[inline] - pub fn pred_opt(&self) -> Option> { + pub fn pred_opt(&self) -> Option> { self.date.pred_opt().map(|date| Date::from_utc(date, self.offset.clone())) } - /// Retrieves an associated offset. + /// Retrieves an associated offset from UTC. #[inline] - pub fn offset<'a>(&'a self) -> &'a Off { + pub fn offset<'a>(&'a self) -> &'a Tz::Offset { &self.offset } - /// Changes the associated offset. + /// Retrieves an associated time zone. + #[inline] + pub fn timezone(&self) -> Tz { + TimeZone::from_offset(&self.offset) + } + + /// Changes the associated time zone. /// This does not change the actual `Date` (but will change the string representation). #[inline] - pub fn with_offset(&self, offset: Off2) -> Date { - Date::from_utc(self.date, offset) + pub fn with_timezone(&self, tz: &Tz2) -> Date { + tz.from_utc_date(&self.date) } /// Adds given `Duration` to the current date. /// /// Returns `None` when it will result in overflow. #[inline] - pub fn checked_add(self, rhs: Duration) -> Option> { + pub fn checked_add(self, rhs: Duration) -> Option> { let date = try_opt!(self.date.checked_add(rhs)); Some(Date { date: date, offset: self.offset }) } @@ -188,23 +197,36 @@ impl Date { /// /// Returns `None` when it will result in overflow. #[inline] - pub fn checked_sub(self, rhs: Duration) -> Option> { + pub fn checked_sub(self, rhs: Duration) -> Option> { let date = try_opt!(self.date.checked_sub(rhs)); Some(Date { date: date, offset: self.offset }) } - /// Returns a view to the local date. - fn local(&self) -> NaiveDate { - self.offset.to_local_date(&self.date) + /// Returns a view to the naive UTC date. + #[inline] + pub fn naive_utc(&self) -> NaiveDate { + self.date + } + + /// Returns a view to the naive local date. + #[inline] + pub fn naive_local(&self) -> NaiveDate { + self.date + self.offset.local_minus_utc() } } -impl Date { +/// Maps the local date to other date with given conversion function. +fn map_local(d: &Date, mut f: F) -> Option> + where F: FnMut(NaiveDate) -> Option { + f(d.naive_local()).and_then(|date| d.timezone().from_local_date(&date).single()) +} + +impl Date where Tz::Offset: fmt::Display { /// Formats the date with the specified formatting items. #[inline] pub fn format_with_items<'a, I>(&'a self, items: I) -> DelayedFormat<'a, I> where I: Iterator> + Clone { - DelayedFormat::new_with_offset(Some(self.local()), None, &self.offset, items) + DelayedFormat::new_with_offset(Some(self.naive_local()), None, &self.offset, items) } /// Formats the date with the specified format string. @@ -215,115 +237,108 @@ impl Date { } } -impl Datelike for Date { - #[inline] fn year(&self) -> i32 { self.local().year() } - #[inline] fn month(&self) -> u32 { self.local().month() } - #[inline] fn month0(&self) -> u32 { self.local().month0() } - #[inline] fn day(&self) -> u32 { self.local().day() } - #[inline] fn day0(&self) -> u32 { self.local().day0() } - #[inline] fn ordinal(&self) -> u32 { self.local().ordinal() } - #[inline] fn ordinal0(&self) -> u32 { self.local().ordinal0() } - #[inline] fn weekday(&self) -> Weekday { self.local().weekday() } - #[inline] fn isoweekdate(&self) -> (i32, u32, Weekday) { self.local().isoweekdate() } +impl Datelike for Date { + #[inline] fn year(&self) -> i32 { self.naive_local().year() } + #[inline] fn month(&self) -> u32 { self.naive_local().month() } + #[inline] fn month0(&self) -> u32 { self.naive_local().month0() } + #[inline] fn day(&self) -> u32 { self.naive_local().day() } + #[inline] fn day0(&self) -> u32 { self.naive_local().day0() } + #[inline] fn ordinal(&self) -> u32 { self.naive_local().ordinal() } + #[inline] fn ordinal0(&self) -> u32 { self.naive_local().ordinal0() } + #[inline] fn weekday(&self) -> Weekday { self.naive_local().weekday() } + #[inline] fn isoweekdate(&self) -> (i32, u32, Weekday) { self.naive_local().isoweekdate() } #[inline] - fn with_year(&self, year: i32) -> Option> { - self.local().with_year(year) - .and_then(|date| self.offset.from_local_date(&date).single()) + fn with_year(&self, year: i32) -> Option> { + map_local(self, |date| date.with_year(year)) } #[inline] - fn with_month(&self, month: u32) -> Option> { - self.local().with_month(month) - .and_then(|date| self.offset.from_local_date(&date).single()) + fn with_month(&self, month: u32) -> Option> { + map_local(self, |date| date.with_month(month)) } #[inline] - fn with_month0(&self, month0: u32) -> Option> { - self.local().with_month0(month0) - .and_then(|date| self.offset.from_local_date(&date).single()) + fn with_month0(&self, month0: u32) -> Option> { + map_local(self, |date| date.with_month0(month0)) } #[inline] - fn with_day(&self, day: u32) -> Option> { - self.local().with_day(day) - .and_then(|date| self.offset.from_local_date(&date).single()) + fn with_day(&self, day: u32) -> Option> { + map_local(self, |date| date.with_day(day)) } #[inline] - fn with_day0(&self, day0: u32) -> Option> { - self.local().with_day0(day0) - .and_then(|date| self.offset.from_local_date(&date).single()) + fn with_day0(&self, day0: u32) -> Option> { + map_local(self, |date| date.with_day0(day0)) } #[inline] - fn with_ordinal(&self, ordinal: u32) -> Option> { - self.local().with_ordinal(ordinal) - .and_then(|date| self.offset.from_local_date(&date).single()) + fn with_ordinal(&self, ordinal: u32) -> Option> { + map_local(self, |date| date.with_ordinal(ordinal)) } #[inline] - fn with_ordinal0(&self, ordinal0: u32) -> Option> { - self.local().with_ordinal0(ordinal0) - .and_then(|date| self.offset.from_local_date(&date).single()) + fn with_ordinal0(&self, ordinal0: u32) -> Option> { + map_local(self, |date| date.with_ordinal0(ordinal0)) } } -impl PartialEq> for Date { - fn eq(&self, other: &Date) -> bool { self.date == other.date } +impl PartialEq> for Date { + fn eq(&self, other: &Date) -> bool { self.date == other.date } } -impl Eq for Date { +impl Eq for Date { } -impl PartialOrd for Date { - fn partial_cmp(&self, other: &Date) -> Option { +impl PartialOrd for Date { + fn partial_cmp(&self, other: &Date) -> Option { self.date.partial_cmp(&other.date) } } -impl Ord for Date { - fn cmp(&self, other: &Date) -> Ordering { self.date.cmp(&other.date) } +impl Ord for Date { + fn cmp(&self, other: &Date) -> Ordering { self.date.cmp(&other.date) } } -impl hash::Hash for Date { +impl hash::Hash for Date { fn hash(&self, state: &mut H) { self.date.hash(state) } } -impl Add for Date { - type Output = Date; +impl Add for Date { + type Output = Date; #[inline] - fn add(self, rhs: Duration) -> Date { + fn add(self, rhs: Duration) -> Date { self.checked_add(rhs).expect("`Date + Duration` overflowed") } } -impl Sub> for Date { +impl Sub> for Date { type Output = Duration; #[inline] - fn sub(self, rhs: Date) -> Duration { self.date - rhs.date } + fn sub(self, rhs: Date) -> Duration { self.date - rhs.date } } -impl Sub for Date { - type Output = Date; +impl Sub for Date { + type Output = Date; #[inline] - fn sub(self, rhs: Duration) -> Date { + fn sub(self, rhs: Duration) -> Date { self.checked_sub(rhs).expect("`Date - Duration` overflowed") } } -impl fmt::Debug for Date { +impl fmt::Debug for Date { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?}{:?}", self.local(), self.offset) + write!(f, "{:?}{:?}", self.naive_local(), self.offset) } } -impl fmt::Display for Date { +impl fmt::Display for Date where Tz::Offset: fmt::Display { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}{}", self.local(), self.offset) + write!(f, "{}{}", self.naive_local(), self.offset) } } @@ -335,35 +350,39 @@ mod tests { use naive::date::NaiveDate; use naive::time::NaiveTime; use naive::datetime::NaiveDateTime; - use super::Date; - use time::Time; - use datetime::DateTime; - use offset::{Offset, LocalResult}; + use offset::{TimeZone, Offset, LocalResult}; #[derive(Copy, Clone, PartialEq, Eq)] struct UTC1y; // same to UTC but with an offset of 365 days - impl Offset for UTC1y { - fn local_minus_utc(&self) -> Duration { Duration::zero() } + #[derive(Copy, Clone, PartialEq, Eq)] + struct OneYear; - fn from_local_date(&self, local: &NaiveDate) -> LocalResult> { - LocalResult::Single(Date::from_utc(*local - Duration::days(365), UTC1y)) + impl TimeZone for UTC1y { + type Offset = OneYear; + + fn from_offset(_offset: &OneYear) -> UTC1y { UTC1y } + + fn offset_from_local_date(&self, _local: &NaiveDate) -> LocalResult { + LocalResult::Single(OneYear) } - fn from_local_time(&self, local: &NaiveTime) -> LocalResult> { - LocalResult::Single(Time::from_utc(local.clone(), UTC1y)) + fn offset_from_local_time(&self, _local: &NaiveTime) -> LocalResult { + LocalResult::Single(OneYear) } - fn from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult> { - LocalResult::Single(DateTime::from_utc(*local - Duration::days(365), UTC1y)) + fn offset_from_local_datetime(&self, _local: &NaiveDateTime) -> LocalResult { + LocalResult::Single(OneYear) } - fn to_local_date(&self, utc: &NaiveDate) -> NaiveDate { *utc + Duration::days(365) } - fn to_local_time(&self, utc: &NaiveTime) -> NaiveTime { utc.clone() } - fn to_local_datetime(&self, utc: &NaiveDateTime) -> NaiveDateTime { - *utc + Duration::days(365) - } + fn offset_from_utc_date(&self, _utc: &NaiveDate) -> OneYear { OneYear } + fn offset_from_utc_time(&self, _utc: &NaiveTime) -> OneYear { OneYear } + fn offset_from_utc_datetime(&self, _utc: &NaiveDateTime) -> OneYear { OneYear } } - impl fmt::Debug for UTC1y { + impl Offset for OneYear { + fn local_minus_utc(&self) -> Duration { Duration::days(365) } + } + + impl fmt::Debug for OneYear { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "+8760:00") } } diff --git a/src/datetime.rs b/src/datetime.rs index c597e31..06692ff 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -3,7 +3,7 @@ // See README.md and LICENSE.txt for details. /*! - * ISO 8601 date and time. + * ISO 8601 date and time with time zone. */ use std::{str, fmt, hash}; @@ -11,7 +11,9 @@ use std::cmp::Ordering; use std::ops::{Add, Sub}; use {Weekday, Timelike, Datelike}; -use offset::{Offset, FixedOffset, UTC}; +use offset::{TimeZone, Offset}; +use offset::utc::UTC; +use offset::fixed::FixedOffset; use duration::Duration; use naive::datetime::NaiveDateTime; use time::Time; @@ -19,30 +21,32 @@ use date::Date; use format::{Item, Numeric, Pad, Fixed}; use format::{parse, Parsed, ParseError, ParseResult, DelayedFormat, StrftimeItems}; -/// ISO 8601 combined date and time with timezone. +/// ISO 8601 combined date and time with time zone. #[derive(Clone)] -pub struct DateTime { +pub struct DateTime { datetime: NaiveDateTime, - offset: Off, + offset: Tz::Offset, } -impl DateTime { +impl DateTime { /// Makes a new `DateTime` with given *UTC* datetime and offset. - /// The local datetime should be constructed via the `Offset` trait. + /// The local datetime should be constructed via the `TimeZone` trait. + // + // note: this constructor is purposedly not named to `new` to discourage the direct usage. #[inline] - pub fn from_utc(datetime: NaiveDateTime, offset: Off) -> DateTime { + pub fn from_utc(datetime: NaiveDateTime, offset: Tz::Offset) -> DateTime { DateTime { datetime: datetime, offset: offset } } /// Retrieves a date component. #[inline] - pub fn date(&self) -> Date { + pub fn date(&self) -> Date { Date::from_utc(self.datetime.date().clone(), self.offset.clone()) } /// Retrieves a time component. #[inline] - pub fn time(&self) -> Time { + pub fn time(&self) -> Time { Time::from_utc(self.datetime.time().clone(), self.offset.clone()) } @@ -52,24 +56,30 @@ impl DateTime { self.datetime.num_seconds_from_unix_epoch() } - /// Retrieves an associated offset. + /// Retrieves an associated offset from UTC. #[inline] - pub fn offset<'a>(&'a self) -> &'a Off { + pub fn offset<'a>(&'a self) -> &'a Tz::Offset { &self.offset } - /// Changes the associated offset. + /// Retrieves an associated time zone. + #[inline] + pub fn timezone(&self) -> Tz { + TimeZone::from_offset(&self.offset) + } + + /// Changes the associated time zone. /// This does not change the actual `DateTime` (but will change the string representation). #[inline] - pub fn with_offset(&self, offset: Off2) -> DateTime { - DateTime::from_utc(self.datetime, offset) + pub fn with_timezone(&self, tz: &Tz2) -> DateTime { + tz.from_utc_datetime(&self.datetime) } /// Adds given `Duration` to the current date and time. /// /// Returns `None` when it will result in overflow. #[inline] - pub fn checked_add(self, rhs: Duration) -> Option> { + pub fn checked_add(self, rhs: Duration) -> Option> { let datetime = try_opt!(self.datetime.checked_add(rhs)); Some(DateTime { datetime: datetime, offset: self.offset }) } @@ -78,15 +88,28 @@ impl DateTime { /// /// Returns `None` when it will result in overflow. #[inline] - pub fn checked_sub(self, rhs: Duration) -> Option> { + pub fn checked_sub(self, rhs: Duration) -> Option> { let datetime = try_opt!(self.datetime.checked_sub(rhs)); Some(DateTime { datetime: datetime, offset: self.offset }) } - /// Returns a view to the local datetime. - fn local(&self) -> NaiveDateTime { - self.offset.to_local_datetime(&self.datetime) + /// Returns a view to the naive UTC datetime. + #[inline] + pub fn naive_utc(&self) -> NaiveDateTime { + self.datetime } + + /// Returns a view to the naive local datetime. + #[inline] + pub fn naive_local(&self) -> NaiveDateTime { + self.datetime + self.offset.local_minus_utc() + } +} + +/// Maps the local datetime to other datetime with given conversion function. +fn map_local(dt: &DateTime, mut f: F) -> Option> + where F: FnMut(NaiveDateTime) -> Option { + f(dt.naive_local()).and_then(|datetime| dt.timezone().from_local_datetime(&datetime).single()) } impl DateTime { @@ -102,12 +125,12 @@ impl DateTime { } } -impl DateTime { +impl DateTime where Tz::Offset: fmt::Display { /// Formats the combined date and time with the specified formatting items. #[inline] pub fn format_with_items<'a, I>(&'a self, items: I) -> DelayedFormat<'a, I> where I: Iterator> + Clone { - let local = self.local(); + let local = self.naive_local(); DelayedFormat::new_with_offset(Some(local.date()), Some(local.time()), &self.offset, items) } @@ -119,146 +142,135 @@ impl DateTime { } } -impl Datelike for DateTime { - #[inline] fn year(&self) -> i32 { self.local().year() } - #[inline] fn month(&self) -> u32 { self.local().month() } - #[inline] fn month0(&self) -> u32 { self.local().month0() } - #[inline] fn day(&self) -> u32 { self.local().day() } - #[inline] fn day0(&self) -> u32 { self.local().day0() } - #[inline] fn ordinal(&self) -> u32 { self.local().ordinal() } - #[inline] fn ordinal0(&self) -> u32 { self.local().ordinal0() } - #[inline] fn weekday(&self) -> Weekday { self.local().weekday() } - #[inline] fn isoweekdate(&self) -> (i32, u32, Weekday) { self.local().isoweekdate() } +impl Datelike for DateTime { + #[inline] fn year(&self) -> i32 { self.naive_local().year() } + #[inline] fn month(&self) -> u32 { self.naive_local().month() } + #[inline] fn month0(&self) -> u32 { self.naive_local().month0() } + #[inline] fn day(&self) -> u32 { self.naive_local().day() } + #[inline] fn day0(&self) -> u32 { self.naive_local().day0() } + #[inline] fn ordinal(&self) -> u32 { self.naive_local().ordinal() } + #[inline] fn ordinal0(&self) -> u32 { self.naive_local().ordinal0() } + #[inline] fn weekday(&self) -> Weekday { self.naive_local().weekday() } + #[inline] fn isoweekdate(&self) -> (i32, u32, Weekday) { self.naive_local().isoweekdate() } #[inline] - fn with_year(&self, year: i32) -> Option> { - self.local().with_year(year) - .and_then(|datetime| self.offset.from_local_datetime(&datetime).single()) + fn with_year(&self, year: i32) -> Option> { + map_local(self, |datetime| datetime.with_year(year)) } #[inline] - fn with_month(&self, month: u32) -> Option> { - self.local().with_month(month) - .and_then(|datetime| self.offset.from_local_datetime(&datetime).single()) + fn with_month(&self, month: u32) -> Option> { + map_local(self, |datetime| datetime.with_month(month)) } #[inline] - fn with_month0(&self, month0: u32) -> Option> { - self.local().with_month0(month0) - .and_then(|datetime| self.offset.from_local_datetime(&datetime).single()) + fn with_month0(&self, month0: u32) -> Option> { + map_local(self, |datetime| datetime.with_month0(month0)) } #[inline] - fn with_day(&self, day: u32) -> Option> { - self.local().with_day(day) - .and_then(|datetime| self.offset.from_local_datetime(&datetime).single()) + fn with_day(&self, day: u32) -> Option> { + map_local(self, |datetime| datetime.with_day(day)) } #[inline] - fn with_day0(&self, day0: u32) -> Option> { - self.local().with_day0(day0) - .and_then(|datetime| self.offset.from_local_datetime(&datetime).single()) + fn with_day0(&self, day0: u32) -> Option> { + map_local(self, |datetime| datetime.with_day0(day0)) } #[inline] - fn with_ordinal(&self, ordinal: u32) -> Option> { - self.local().with_ordinal(ordinal) - .and_then(|datetime| self.offset.from_local_datetime(&datetime).single()) + fn with_ordinal(&self, ordinal: u32) -> Option> { + map_local(self, |datetime| datetime.with_ordinal(ordinal)) } #[inline] - fn with_ordinal0(&self, ordinal0: u32) -> Option> { - self.local().with_ordinal0(ordinal0) - .and_then(|datetime| self.offset.from_local_datetime(&datetime).single()) + fn with_ordinal0(&self, ordinal0: u32) -> Option> { + map_local(self, |datetime| datetime.with_ordinal0(ordinal0)) } } -impl Timelike for DateTime { - #[inline] fn hour(&self) -> u32 { self.local().hour() } - #[inline] fn minute(&self) -> u32 { self.local().minute() } - #[inline] fn second(&self) -> u32 { self.local().second() } - #[inline] fn nanosecond(&self) -> u32 { self.local().nanosecond() } +impl Timelike for DateTime { + #[inline] fn hour(&self) -> u32 { self.naive_local().hour() } + #[inline] fn minute(&self) -> u32 { self.naive_local().minute() } + #[inline] fn second(&self) -> u32 { self.naive_local().second() } + #[inline] fn nanosecond(&self) -> u32 { self.naive_local().nanosecond() } #[inline] - fn with_hour(&self, hour: u32) -> Option> { - self.local().with_hour(hour) - .and_then(|datetime| self.offset.from_local_datetime(&datetime).single()) + fn with_hour(&self, hour: u32) -> Option> { + map_local(self, |datetime| datetime.with_hour(hour)) } #[inline] - fn with_minute(&self, min: u32) -> Option> { - self.local().with_minute(min) - .and_then(|datetime| self.offset.from_local_datetime(&datetime).single()) + fn with_minute(&self, min: u32) -> Option> { + map_local(self, |datetime| datetime.with_minute(min)) } #[inline] - fn with_second(&self, sec: u32) -> Option> { - self.local().with_second(sec) - .and_then(|datetime| self.offset.from_local_datetime(&datetime).single()) + fn with_second(&self, sec: u32) -> Option> { + map_local(self, |datetime| datetime.with_second(sec)) } #[inline] - fn with_nanosecond(&self, nano: u32) -> Option> { - self.local().with_nanosecond(nano) - .and_then(|datetime| self.offset.from_local_datetime(&datetime).single()) + fn with_nanosecond(&self, nano: u32) -> Option> { + map_local(self, |datetime| datetime.with_nanosecond(nano)) } } -impl PartialEq> for DateTime { - fn eq(&self, other: &DateTime) -> bool { self.datetime == other.datetime } +impl PartialEq> for DateTime { + fn eq(&self, other: &DateTime) -> bool { self.datetime == other.datetime } } -impl Eq for DateTime { +impl Eq for DateTime { } -impl PartialOrd for DateTime { - fn partial_cmp(&self, other: &DateTime) -> Option { +impl PartialOrd for DateTime { + fn partial_cmp(&self, other: &DateTime) -> Option { self.datetime.partial_cmp(&other.datetime) } } -impl Ord for DateTime { - fn cmp(&self, other: &DateTime) -> Ordering { self.datetime.cmp(&other.datetime) } +impl Ord for DateTime { + fn cmp(&self, other: &DateTime) -> Ordering { self.datetime.cmp(&other.datetime) } } -impl hash::Hash for DateTime { +impl hash::Hash for DateTime { fn hash(&self, state: &mut H) { self.datetime.hash(state) } } -impl Add for DateTime { - type Output = DateTime; +impl Add for DateTime { + type Output = DateTime; #[inline] - fn add(self, rhs: Duration) -> DateTime { + fn add(self, rhs: Duration) -> DateTime { self.checked_add(rhs).expect("`DateTime + Duration` overflowed") } } -impl Sub> for DateTime { +impl Sub> for DateTime { type Output = Duration; #[inline] - fn sub(self, rhs: DateTime) -> Duration { self.datetime - rhs.datetime } + fn sub(self, rhs: DateTime) -> Duration { self.datetime - rhs.datetime } } -impl Sub for DateTime { - type Output = DateTime; +impl Sub for DateTime { + type Output = DateTime; #[inline] - fn sub(self, rhs: Duration) -> DateTime { + fn sub(self, rhs: Duration) -> DateTime { self.checked_sub(rhs).expect("`DateTime - Duration` overflowed") } } -impl fmt::Debug for DateTime { +impl fmt::Debug for DateTime { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?}{:?}", self.local(), self.offset) + write!(f, "{:?}{:?}", self.naive_local(), self.offset) } } -impl fmt::Display for DateTime { +impl fmt::Display for DateTime where Tz::Offset: fmt::Display { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{} {}", self.local(), self.offset) + write!(f, "{} {}", self.naive_local(), self.offset) } } @@ -295,7 +307,7 @@ impl str::FromStr for DateTime { fn from_str(s: &str) -> ParseResult> { // we parse non-UTC time zones then convert them into UTC let dt: DateTime = try!(s.parse()); - Ok(dt.with_offset(UTC)) + Ok(dt.with_timezone(&UTC)) } } @@ -306,7 +318,10 @@ mod tests { use super::DateTime; use Datelike; use duration::Duration; - use offset::{Offset, UTC, Local, FixedOffset}; + use offset::TimeZone; + use offset::utc::UTC; + use offset::local::Local; + use offset::fixed::FixedOffset; #[test] #[allow(non_snake_case)] @@ -366,7 +381,7 @@ mod tests { fn test_datetime_format_with_local() { // if we are not around the year boundary, local and UTC date should have the same year let dt = Local::now().with_month(5).unwrap(); - assert_eq!(dt.format("%Y").to_string(), dt.with_offset(UTC).format("%Y").to_string()); + assert_eq!(dt.format("%Y").to_string(), dt.with_timezone(&UTC).format("%Y").to_string()); } } diff --git a/src/format/parse.rs b/src/format/parse.rs index b5f7512..ceb21b9 100644 --- a/src/format/parse.rs +++ b/src/format/parse.rs @@ -590,7 +590,7 @@ fn test_parse() { #[test] fn test_rfc2822() { use datetime::DateTime; - use offset::FixedOffset; + use offset::fixed::FixedOffset; use super::*; use super::NOT_ENOUGH; @@ -640,7 +640,7 @@ fn test_rfc2822() { #[test] fn test_rfc3339() { use datetime::DateTime; - use offset::FixedOffset; + use offset::fixed::FixedOffset; use super::*; // Test data - (input, Ok(expected result after parse and format) or Err(error code)) diff --git a/src/format/parsed.rs b/src/format/parsed.rs index f75bcf7..840973f 100644 --- a/src/format/parsed.rs +++ b/src/format/parsed.rs @@ -13,7 +13,8 @@ use {Datelike, Timelike}; use Weekday; use div::div_rem; use duration::Duration; -use offset::{Offset, FixedOffset, LocalResult}; +use offset::{TimeZone, Offset, LocalResult}; +use offset::fixed::FixedOffset; use naive::date::NaiveDate; use naive::time::NaiveTime; use naive::datetime::NaiveDateTime; @@ -537,21 +538,55 @@ impl Parsed { } /// Returns a parsed timezone-aware date and time out of given fields, - /// with an additional `Offset` used to interpret and validate the local date. + /// with an additional `TimeZone` used to interpret and validate the local date. /// /// This method is able to determine the combined date and time /// from date and time fields or a single `timestamp` field, plus a time zone offset. /// Either way those fields have to be consistent to each other. /// If parsed fields include an UTC offset, it also has to be consistent to `offset`. - pub fn to_datetime_with_offset(&self, offset: Off) -> ParseResult> { - let delta = offset.local_minus_utc().num_seconds(); - let delta = try!(delta.to_i32().ok_or(OUT_OF_RANGE)); - if self.offset.unwrap_or(delta) != delta { return Err(IMPOSSIBLE); } - let datetime = try!(self.to_naive_datetime_with_offset(delta)); - match offset.from_local_datetime(&datetime) { + pub fn to_datetime_with_timezone(&self, tz: &Tz) -> ParseResult> { + // if we have `timestamp` specified, guess an offset from that. + let mut guessed_offset = 0; + if let Some(timestamp) = self.timestamp { + // make a naive `DateTime` from given timestamp and (if any) nanosecond. + // an empty `nanosecond` is always equal to zero, so missing nanosecond is fine. + let nanosecond = self.nanosecond.unwrap_or(0); + let dt = NaiveDateTime::from_num_seconds_from_unix_epoch_opt(timestamp, nanosecond); + let dt = try!(dt.ok_or(OUT_OF_RANGE)); + + // we cannot handle offsets larger than i32 at all. give up if so. + // we can instead make `to_naive_datetime_with_offset` to accept i64, but this makes + // the algorithm too complex and tons of edge cases. i32 should be enough for all. + let offset = tz.offset_from_utc_datetime(&dt).local_minus_utc().num_seconds(); + guessed_offset = try!(offset.to_i32().ok_or(OUT_OF_RANGE)); + } + + // checks if the given `DateTime` has a consistent `Offset` with given `self.offset`. + let check_offset = |dt: &DateTime| { + if let Some(offset) = self.offset { + let delta = dt.offset().local_minus_utc().num_seconds(); + // if `delta` does not fit in `i32`, it cannot equal to `self.offset` anyway. + delta.to_i32() == Some(offset) + } else { + true + } + }; + + // `guessed_offset` should be correct when `self.timestamp` is given. + // it will be 0 otherwise, but this is fine as the algorithm ignores offset for that case. + let datetime = try!(self.to_naive_datetime_with_offset(guessed_offset)); + match tz.from_local_datetime(&datetime) { LocalResult::None => Err(IMPOSSIBLE), - LocalResult::Single(t) => Ok(t), - LocalResult::Ambiguous(..) => Err(NOT_ENOUGH), + LocalResult::Single(t) => if check_offset(&t) {Ok(t)} else {Err(IMPOSSIBLE)}, + LocalResult::Ambiguous(min, max) => { + // try to disambiguate two possible local dates by offset. + match (check_offset(&min), check_offset(&max)) { + (false, false) => Err(IMPOSSIBLE), + (false, true) => Ok(max), + (true, false) => Ok(min), + (true, true) => Err(NOT_ENOUGH), + } + } } } } @@ -564,7 +599,9 @@ mod tests { use Weekday::*; use naive::date::{self, NaiveDate}; use naive::time::NaiveTime; - use offset::{Offset, FixedOffset}; + use offset::TimeZone; + use offset::utc::UTC; + use offset::fixed::FixedOffset; #[test] fn test_parsed_set_fields() { @@ -964,5 +1001,45 @@ mod tests { minute: 26, second: 40, nanosecond: 12_345_678, offset: 86400), Err(OUT_OF_RANGE)); // `FixedOffset` does not support such huge offset } + + #[test] + fn test_parsed_to_datetime_with_timezone() { + macro_rules! parse { + ($tz:expr; $($k:ident: $v:expr),*) => ( + Parsed { $($k: Some($v),)* ..Parsed::new() }.to_datetime_with_timezone(&$tz) + ) + } + + // single result from ymdhms + assert_eq!(parse!(UTC; + year: 2014, ordinal: 365, hour_div_12: 0, hour_mod_12: 4, + minute: 26, second: 40, nanosecond: 12_345_678, offset: 0), + Ok(UTC.ymd(2014, 12, 31).and_hms_nano(4, 26, 40, 12_345_678))); + assert_eq!(parse!(UTC; + year: 2014, ordinal: 365, hour_div_12: 1, hour_mod_12: 1, + minute: 26, second: 40, nanosecond: 12_345_678, offset: 32400), + Err(IMPOSSIBLE)); + assert_eq!(parse!(FixedOffset::east(32400); + year: 2014, ordinal: 365, hour_div_12: 0, hour_mod_12: 4, + minute: 26, second: 40, nanosecond: 12_345_678, offset: 0), + Err(IMPOSSIBLE)); + assert_eq!(parse!(FixedOffset::east(32400); + year: 2014, ordinal: 365, hour_div_12: 1, hour_mod_12: 1, + minute: 26, second: 40, nanosecond: 12_345_678, offset: 32400), + Ok(FixedOffset::east(32400).ymd(2014, 12, 31) + .and_hms_nano(13, 26, 40, 12_345_678))); + + // single result from timestamp + assert_eq!(parse!(UTC; timestamp: 1_420_000_000, offset: 0), + Ok(UTC.ymd(2014, 12, 31).and_hms(4, 26, 40))); + assert_eq!(parse!(UTC; timestamp: 1_420_000_000, offset: 32400), + Err(IMPOSSIBLE)); + assert_eq!(parse!(FixedOffset::east(32400); timestamp: 1_420_000_000, offset: 0), + Err(IMPOSSIBLE)); + assert_eq!(parse!(FixedOffset::east(32400); timestamp: 1_420_000_000, offset: 32400), + Ok(FixedOffset::east(32400).ymd(2014, 12, 31).and_hms(13, 26, 40))); + + // TODO test with a variable time zone (for None and Ambiguous cases) + } } diff --git a/src/lib.rs b/src/lib.rs index 553c0c0..02b1f11 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,7 +32,7 @@ chrono = "0.2" And this in your crate root: -```toml +```rust extern crate chrono; ``` @@ -49,14 +49,14 @@ Chrono simply reexports it. Chrono provides a `DateTime` type for the combined date and time. `DateTime`, among others, is timezone-aware and -must be constructed from the timezone object (`Offset`). -`DateTime`s with different offsets do not mix, but can be converted to each other. +must be constructed from the `TimeZone` object. +`DateTime`s with different time zones do not mix, but can be converted to each other. You can get the current date and time in the UTC timezone (`UTC::now()`) or in the local timezone (`Local::now()`). ~~~~ {.rust} -use chrono::{UTC, Local, DateTime}; +use chrono::*; let utc: DateTime = UTC::now(); // e.g. `2014-11-28T12:45:59.324310806Z` let local: DateTime = Local::now(); // e.g. `2014-11-28T21:45:59.324310806+09:00` @@ -68,7 +68,7 @@ This is a bit verbose due to Rust's lack of function and method overloading, but in turn we get a rich combination of initialization methods. ~~~~ {.rust} -use chrono::{UTC, Offset, Weekday, LocalResult}; +use chrono::*; let dt = UTC.ymd(2014, 7, 8).and_hms(9, 10, 11); // `2014-07-08T09:10:11Z` // July 8 is 188th day of the year 2014 (`o` for "ordinal") @@ -93,13 +93,12 @@ Addition and subtraction is also supported. The following illustrates most supported operations to the date and time: ~~~~ {.rust} -# /* we intentionally fake the datetime... -use chrono::{UTC, Local, Datelike, Timelike, Weekday, Duration}; +use chrono::*; +# /* we intentionally fake the datetime... // assume this returned `2014-11-28T21:45:59.324310806+09:00`: let dt = Local::now(); # */ // up to here. we now define a fixed datetime for the illustrative purpose. -# use chrono::{UTC, FixedOffset, Offset, Datelike, Timelike, Weekday, Duration}; # let dt = FixedOffset::east(9*3600).ymd(2014, 11, 28).and_hms_nano(21, 45, 59, 324310806); // property accessors @@ -111,9 +110,10 @@ assert_eq!(dt.weekday().number_from_monday(), 5); // Mon=1, ..., Sat=7 assert_eq!(dt.ordinal(), 332); // the day of year assert_eq!(dt.num_days_from_ce(), 735565); // the number of days from and including Jan 1, 1 -// offset accessor and manipulation +// time zone accessor and manipulation assert_eq!(dt.offset().local_minus_utc(), Duration::hours(9)); -assert_eq!(dt.with_offset(UTC), UTC.ymd(2014, 11, 28).and_hms_nano(12, 45, 59, 324310806)); +assert_eq!(dt.timezone(), FixedOffset::east(9 * 3600)); +assert_eq!(dt.with_timezone(&UTC), UTC.ymd(2014, 11, 28).and_hms_nano(12, 45, 59, 324310806)); // a sample of property manipulations (validates dynamically) assert_eq!(dt.with_day(29).unwrap().weekday(), Weekday::Sat); // 2014-11-29 is Saturday @@ -135,7 +135,7 @@ which format is equivalent to the familiar `strftime` format. The default `to_string` method and `{:?}` specifier also give a reasonable representation. ~~~~ {.rust} -use chrono::{UTC, Offset}; +use chrono::*; let dt = UTC.ymd(2014, 11, 28).and_hms(12, 0, 9); assert_eq!(dt.format("%Y-%m-%d %H:%M:%S").to_string(), "2014-11-28 12:00:09"); @@ -165,7 +165,7 @@ Parsing can be done with three methods: More detailed control over the parsing process is available via `format` module. ~~~~ {.rust} -use chrono::{UTC, Offset, DateTime}; +use chrono::*; let dt = UTC.ymd(2014, 11, 28).and_hms(12, 0, 9); @@ -178,8 +178,8 @@ assert_eq!(UTC.datetime_from_str("2014-11-28 12:00:09", "%Y-%m-%d %H:%M:%S"), Ok assert_eq!(UTC.datetime_from_str("Fri Nov 28 12:00:09 2014", "%a %b %e %T %Y"), Ok(dt.clone())); // method 3 -assert_eq!(DateTime::parse_from_str("2014-11-28 21:00:09 +09:00", - "%Y-%m-%d %H:%M:%S %z").map(|dt| dt.with_offset(UTC)), Ok(dt)); +assert_eq!(DateTime::parse_from_str("2014-11-28 21:00:09 +09:00", "%Y-%m-%d %H:%M:%S %z"), + Ok(dt.with_timezone(&FixedOffset::east(9*3600)))); // oops, the year is missing! assert!(UTC.datetime_from_str("Fri Nov 28 12:00:09", "%a %b %e %T %Y").is_err()); @@ -192,12 +192,12 @@ assert!(UTC.datetime_from_str("Sat Nov 28 12:00:09 2014", "%a %b %e %T %Y").is_e ### Individual date and time Chrono also provides an individual date type (`Date`) and time type (`Time`). -They also have offsets attached, and have to be constructed via offsets. +They also have time zones attached, and have to be constructed via time zones. Most operations available to `DateTime` are also available to `Date` and `Time` whenever appropriate. ~~~~ {.rust} -use chrono::{UTC, Local, Offset, LocalResult, Datelike, Weekday}; +use chrono::*; # // these *may* fail, but only very rarely. just rerun the test if you were that unfortunate ;) assert_eq!(UTC::today(), UTC::now().date()); @@ -217,7 +217,7 @@ Chrono provides naive counterparts to `Date`, `Time` and `DateTime` as `NaiveDate`, `NaiveTime` and `NaiveDateTime` respectively. They have almost equivalent interfaces as their timezone-aware twins, -but are not associated to offsets obviously and can be quite low-level. +but are not associated to time zones obviously and can be quite low-level. They are mostly useful for building blocks for higher-level types. ## Limitations @@ -238,7 +238,7 @@ Any operation that can be ambiguous will return `None` in such cases. For example, "a month later" of 2014-01-30 is not well-defined and consequently `UTC.ymd(2014, 1, 30).with_month(2)` returns `None`. -Advanced offset handling is not yet supported (but is planned in 0.3). +Advanced time zone handling is not yet supported (but is planned in 0.3). */ @@ -251,8 +251,10 @@ Advanced offset handling is not yet supported (but is planned in 0.3). extern crate "time" as stdtime; pub use duration::Duration; -pub use offset::{Offset, LocalResult}; -pub use offset::{UTC, FixedOffset, Local}; +pub use offset::{TimeZone, Offset, LocalResult}; +pub use offset::utc::UTC; +pub use offset::fixed::FixedOffset; +pub use offset::local::Local; pub use naive::date::NaiveDate; pub use naive::time::NaiveTime; pub use naive::datetime::NaiveDateTime; @@ -278,7 +280,7 @@ pub mod offset; pub mod naive { //! Date and time types which do not concern about the timezones. //! - //! They are primarily building blocks for other types (e.g. `Offset`), + //! They are primarily building blocks for other types (e.g. `TimeZone`), //! but can be also used for the simpler date and time handling. pub mod date; pub mod time; diff --git a/src/naive/time.rs b/src/naive/time.rs index e70a605..f0abb8e 100644 --- a/src/naive/time.rs +++ b/src/naive/time.rs @@ -12,7 +12,6 @@ use std::ops::{Add, Sub}; use Timelike; use div::div_mod_floor; -use offset::Offset; use duration::Duration; use format::{Item, Numeric, Pad, Fixed}; use format::{parse, Parsed, ParseError, ParseResult, DelayedFormat, StrftimeItems}; diff --git a/src/offset/fixed.rs b/src/offset/fixed.rs new file mode 100644 index 0000000..e6528b5 --- /dev/null +++ b/src/offset/fixed.rs @@ -0,0 +1,107 @@ +// This is a part of rust-chrono. +// Copyright (c) 2015, Kang Seonghoon. +// See README.md and LICENSE.txt for details. + +/*! + * The time zone which has a fixed offset from UTC. + */ + +use std::fmt; + +use div::div_mod_floor; +use duration::Duration; +use naive::date::NaiveDate; +use naive::time::NaiveTime; +use naive::datetime::NaiveDateTime; +use super::{TimeZone, Offset, LocalResult}; + +/// The time zone with fixed offset, from UTC-23:59:59 to UTC+23:59:59. +#[derive(Copy, Clone, PartialEq, Eq)] +pub struct FixedOffset { + local_minus_utc: i32, +} + +impl FixedOffset { + /// Makes a new `FixedOffset` for the Eastern Hemisphere with given timezone difference. + /// The negative `secs` means the Western Hemisphere. + /// + /// Fails on the out-of-bound `secs`. + pub fn east(secs: i32) -> FixedOffset { + FixedOffset::east_opt(secs).expect("FixedOffset::east out of bounds") + } + + /// Makes a new `FixedOffset` for the Eastern Hemisphere with given timezone difference. + /// The negative `secs` means the Western Hemisphere. + /// + /// Returns `None` on the out-of-bound `secs`. + pub fn east_opt(secs: i32) -> Option { + if -86400 < secs && secs < 86400 { + Some(FixedOffset { local_minus_utc: secs }) + } else { + None + } + } + + /// Makes a new `FixedOffset` for the Western Hemisphere with given timezone difference. + /// The negative `secs` means the Eastern Hemisphere. + /// + /// Fails on the out-of-bound `secs`. + pub fn west(secs: i32) -> FixedOffset { + FixedOffset::west_opt(secs).expect("FixedOffset::west out of bounds") + } + + /// Makes a new `FixedOffset` for the Western Hemisphere with given timezone difference. + /// The negative `secs` means the Eastern Hemisphere. + /// + /// Returns `None` on the out-of-bound `secs`. + pub fn west_opt(secs: i32) -> Option { + if -86400 < secs && secs < 86400 { + Some(FixedOffset { local_minus_utc: -secs }) + } else { + None + } + } +} + +impl TimeZone for FixedOffset { + type Offset = FixedOffset; + + fn from_offset(offset: &FixedOffset) -> FixedOffset { offset.clone() } + + fn offset_from_local_date(&self, _local: &NaiveDate) -> LocalResult { + LocalResult::Single(self.clone()) + } + fn offset_from_local_time(&self, _local: &NaiveTime) -> LocalResult { + LocalResult::Single(self.clone()) + } + fn offset_from_local_datetime(&self, _local: &NaiveDateTime) -> LocalResult { + LocalResult::Single(self.clone()) + } + + fn offset_from_utc_date(&self, _utc: &NaiveDate) -> FixedOffset { self.clone() } + fn offset_from_utc_time(&self, _utc: &NaiveTime) -> FixedOffset { self.clone() } + fn offset_from_utc_datetime(&self, _utc: &NaiveDateTime) -> FixedOffset { self.clone() } +} + +impl Offset for FixedOffset { + fn local_minus_utc(&self) -> Duration { Duration::seconds(self.local_minus_utc as i64) } +} + +impl fmt::Debug for FixedOffset { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let offset = self.local_minus_utc; + let (sign, offset) = if offset < 0 {('-', -offset)} else {('+', offset)}; + let (mins, sec) = div_mod_floor(offset, 60); + let (hour, min) = div_mod_floor(mins, 60); + if sec == 0 { + write!(f, "{}{:02}:{:02}", sign, hour, min) + } else { + write!(f, "{}{:02}:{:02}:{:02}", sign, hour, min, sec) + } + } +} + +impl fmt::Display for FixedOffset { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Debug::fmt(self, f) } +} + diff --git a/src/offset/local.rs b/src/offset/local.rs new file mode 100644 index 0000000..23e51aa --- /dev/null +++ b/src/offset/local.rs @@ -0,0 +1,122 @@ +// This is a part of rust-chrono. +// Copyright (c) 2015, Kang Seonghoon. +// See README.md and LICENSE.txt for details. + +/*! + * The local (system) time zone. + */ + +use stdtime; + +use {Datelike, Timelike}; +use duration::Duration; +use naive::date::NaiveDate; +use naive::time::NaiveTime; +use naive::datetime::NaiveDateTime; +use date::Date; +use time::Time; +use datetime::DateTime; +use super::{TimeZone, LocalResult}; +use super::fixed::FixedOffset; + +/// Converts a `time::Tm` struct into the timezone-aware `DateTime`. +/// This assumes that `time` is working correctly, i.e. any error is fatal. +fn tm_to_datetime(mut tm: stdtime::Tm) -> DateTime { + if tm.tm_sec >= 60 { + tm.tm_sec = 59; + tm.tm_nsec += (tm.tm_sec - 59) * 1_000_000_000; + } + + // from_yo is more efficient than from_ymd (since it's the internal representation). + let date = NaiveDate::from_yo(tm.tm_year + 1900, tm.tm_yday as u32 + 1); + let time = NaiveTime::from_hms_nano(tm.tm_hour as u32, tm.tm_min as u32, + tm.tm_sec as u32, tm.tm_nsec as u32); + let offset = FixedOffset::east(tm.tm_utcoff); + DateTime::from_utc(date.and_time(time) + Duration::seconds(-tm.tm_utcoff as i64), offset) +} + +/// Converts a local `NaiveDateTime` to the `time::Timespec`. +fn datetime_to_timespec(d: &NaiveDateTime) -> stdtime::Timespec { + let tm = stdtime::Tm { + tm_sec: d.second() as i32, + tm_min: d.minute() as i32, + tm_hour: d.hour() as i32, + tm_mday: d.day() as i32, + tm_mon: d.month0() as i32, // yes, C is that strange... + tm_year: d.year() - 1900, // this doesn't underflow, we know that d is `NaiveDateTime`. + tm_wday: 0, // to_local ignores this + tm_yday: 0, // and this + tm_isdst: -1, + tm_utcoff: 1, // this is arbitrary but should be nonzero + // in order to make `to_timespec` use `rust_mktime` internally. + tm_nsec: d.nanosecond() as i32, + }; + tm.to_timespec() +} + +/// The local timescale. This is implemented via the standard `time` crate. +#[derive(Copy, Clone)] +pub struct Local; + +impl Local { + /// Returns a `Date` which corresponds to the current date. + pub fn today() -> Date { + Local::now().date() + } + + /// Returns a `DateTime` which corresponds to the current date. + pub fn now() -> DateTime { + tm_to_datetime(stdtime::now()) + } +} + +impl TimeZone for Local { + type Offset = FixedOffset; + + fn from_offset(_offset: &FixedOffset) -> Local { Local } + + // they are easier to define in terms of the finished date and time unlike other offsets + fn offset_from_local_date(&self, local: &NaiveDate) -> LocalResult { + self.from_local_date(local).map(|&: date| *date.offset()) + } + fn offset_from_local_time(&self, local: &NaiveTime) -> LocalResult { + self.from_local_time(local).map(|&: time| *time.offset()) + } + fn offset_from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult { + self.from_local_datetime(local).map(|&: datetime| *datetime.offset()) + } + + fn offset_from_utc_date(&self, utc: &NaiveDate) -> FixedOffset { + *self.from_utc_date(utc).offset() + } + fn offset_from_utc_time(&self, utc: &NaiveTime) -> FixedOffset { + *self.from_utc_time(utc).offset() + } + fn offset_from_utc_datetime(&self, utc: &NaiveDateTime) -> FixedOffset { + *self.from_utc_datetime(utc).offset() + } + + // override them for avoiding redundant works + fn from_local_date(&self, local: &NaiveDate) -> LocalResult> { + self.from_local_datetime(&local.and_hms(0, 0, 0)).map(|datetime| datetime.date()) + } + fn from_local_time(&self, _local: &NaiveTime) -> LocalResult> { + LocalResult::None // we have no information about this time + } + fn from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult> { + let timespec = datetime_to_timespec(local); + LocalResult::Single(tm_to_datetime(stdtime::at(timespec))) + } + + fn from_utc_date(&self, utc: &NaiveDate) -> Date { + self.from_utc_datetime(&utc.and_hms(0, 0, 0)).date() + } + fn from_utc_time(&self, _utc: &NaiveTime) -> Time { + unimplemented!() // we have no information about this time + } + fn from_utc_datetime(&self, utc: &NaiveDateTime) -> DateTime { + let timespec = datetime_to_timespec(utc); + tm_to_datetime(stdtime::at_utc(timespec)) + } +} + diff --git a/src/offset.rs b/src/offset/mod.rs similarity index 56% rename from src/offset.rs rename to src/offset/mod.rs index 5b23202..0e20768 100644 --- a/src/offset.rs +++ b/src/offset/mod.rs @@ -3,14 +3,26 @@ // See README.md and LICENSE.txt for details. /*! - * Offsets from the local time to UTC. + * The time zone, which calculates offsets from the local time to UTC. + * + * There are three operations provided by the `TimeZone` trait: + * + * 1. Converting the local `NaiveDateTime` to `DateTime` + * 2. Converting the UTC `NaiveDateTime` to `DateTime` + * 3. Converting `DateTime` to the local `NaiveDateTime` + * + * 1 is used for constructors. 2 is used for the `with_timezone` method of date and time types. + * 3 is used for other methods, e.g. `year()` or `format()`, and provided by an associated type + * which implements `Offset` (which then passed to `TimeZone` for actual implementations). + * Technically speaking `TimeZone` has a total knowledge about given timescale, + * but `Offset` is used as a cache to avoid the repeated conversion + * and provides implementations for 1 and 3. + * An `TimeZone` instance can be reconstructed from the corresponding `Offset` instance. */ use std::fmt; -use stdtime; -use {Weekday, Datelike, Timelike}; -use div::div_mod_floor; +use Weekday; use duration::Duration; use naive::date::NaiveDate; use naive::time::NaiveTime; @@ -48,15 +60,24 @@ impl LocalResult { pub fn latest(self) -> Option { match self { LocalResult::Single(t) | LocalResult::Ambiguous(_,t) => Some(t), _ => None } } + + /// Maps a `LocalResult` into `LocalResult` with given function. + pub fn map U>(self, mut f: F) -> LocalResult { + match self { + LocalResult::None => LocalResult::None, + LocalResult::Single(v) => LocalResult::Single(f(v)), + LocalResult::Ambiguous(min, max) => LocalResult::Ambiguous(f(min), f(max)), + } + } } -impl LocalResult> { +impl LocalResult> { /// Makes a new `DateTime` from the current date and given `NaiveTime`. /// The offset in the current date is preserved. /// /// Propagates any error. Ambiguous result would be discarded. #[inline] - pub fn and_time(self, time: NaiveTime) -> LocalResult> { + pub fn and_time(self, time: NaiveTime) -> LocalResult> { match self { LocalResult::Single(d) => d.and_time(time) .map_or(LocalResult::None, LocalResult::Single), @@ -69,7 +90,7 @@ impl LocalResult> { /// /// Propagates any error. Ambiguous result would be discarded. #[inline] - pub fn and_hms_opt(self, hour: u32, min: u32, sec: u32) -> LocalResult> { + pub fn and_hms_opt(self, hour: u32, min: u32, sec: u32) -> LocalResult> { match self { LocalResult::Single(d) => d.and_hms_opt(hour, min, sec) .map_or(LocalResult::None, LocalResult::Single), @@ -84,7 +105,7 @@ impl LocalResult> { /// Propagates any error. Ambiguous result would be discarded. #[inline] pub fn and_hms_milli_opt(self, hour: u32, min: u32, sec: u32, - milli: u32) -> LocalResult> { + milli: u32) -> LocalResult> { match self { LocalResult::Single(d) => d.and_hms_milli_opt(hour, min, sec, milli) .map_or(LocalResult::None, LocalResult::Single), @@ -99,7 +120,7 @@ impl LocalResult> { /// Propagates any error. Ambiguous result would be discarded. #[inline] pub fn and_hms_micro_opt(self, hour: u32, min: u32, sec: u32, - micro: u32) -> LocalResult> { + micro: u32) -> LocalResult> { match self { LocalResult::Single(d) => d.and_hms_micro_opt(hour, min, sec, micro) .map_or(LocalResult::None, LocalResult::Single), @@ -114,7 +135,7 @@ impl LocalResult> { /// Propagates any error. Ambiguous result would be discarded. #[inline] pub fn and_hms_nano_opt(self, hour: u32, min: u32, sec: u32, - nano: u32) -> LocalResult> { + nano: u32) -> LocalResult> { match self { LocalResult::Single(d) => d.and_hms_nano_opt(hour, min, sec, nano) .map_or(LocalResult::None, LocalResult::Single), @@ -138,11 +159,19 @@ impl LocalResult { } /// The offset from the local time to UTC. -pub trait Offset: Clone + fmt::Debug { - /// Makes a new `Date` from year, month, day and the current offset. +pub trait Offset: Sized + Clone + fmt::Debug { + /// Returns the offset from UTC to the local time stored. + fn local_minus_utc(&self) -> Duration; +} + +/// The time zone. +pub trait TimeZone: Sized { + type Offset: Offset; + + /// Makes a new `Date` from year, month, day and the current time zone. /// This assumes the proleptic Gregorian calendar, with the year 0 being 1 BCE. /// - /// The offset normally does not affect the date (unless it is between UTC-24 and UTC+24), + /// The time zone normally does not affect the date (unless it is between UTC-24 and UTC+24), /// but it will propagate to the `DateTime` values constructed via this date. /// /// Fails on the out-of-range date, invalid month and/or day. @@ -150,10 +179,10 @@ pub trait Offset: Clone + fmt::Debug { self.ymd_opt(year, month, day).unwrap() } - /// Makes a new `Date` from year, month, day and the current offset. + /// Makes a new `Date` from year, month, day and the current time zone. /// This assumes the proleptic Gregorian calendar, with the year 0 being 1 BCE. /// - /// The offset normally does not affect the date (unless it is between UTC-24 and UTC+24), + /// The time zone normally does not affect the date (unless it is between UTC-24 and UTC+24), /// but it will propagate to the `DateTime` values constructed via this date. /// /// Returns `None` on the out-of-range date, invalid month and/or day. @@ -164,10 +193,10 @@ pub trait Offset: Clone + fmt::Debug { } } - /// Makes a new `Date` from year, day of year (DOY or "ordinal") and the current offset. + /// Makes a new `Date` from year, day of year (DOY or "ordinal") and the current time zone. /// This assumes the proleptic Gregorian calendar, with the year 0 being 1 BCE. /// - /// The offset normally does not affect the date (unless it is between UTC-24 and UTC+24), + /// The time zone normally does not affect the date (unless it is between UTC-24 and UTC+24), /// but it will propagate to the `DateTime` values constructed via this date. /// /// Fails on the out-of-range date and/or invalid DOY. @@ -175,10 +204,10 @@ pub trait Offset: Clone + fmt::Debug { self.yo_opt(year, ordinal).unwrap() } - /// Makes a new `Date` from year, day of year (DOY or "ordinal") and the current offset. + /// Makes a new `Date` from year, day of year (DOY or "ordinal") and the current time zone. /// This assumes the proleptic Gregorian calendar, with the year 0 being 1 BCE. /// - /// The offset normally does not affect the date (unless it is between UTC-24 and UTC+24), + /// The time zone normally does not affect the date (unless it is between UTC-24 and UTC+24), /// but it will propagate to the `DateTime` values constructed via this date. /// /// Returns `None` on the out-of-range date and/or invalid DOY. @@ -190,11 +219,11 @@ pub trait Offset: Clone + fmt::Debug { } /// Makes a new `Date` from ISO week date (year and week number), day of the week (DOW) and - /// the current offset. + /// the current time zone. /// This assumes the proleptic Gregorian calendar, with the year 0 being 1 BCE. /// The resulting `Date` may have a different year from the input year. /// - /// The offset normally does not affect the date (unless it is between UTC-24 and UTC+24), + /// The time zone normally does not affect the date (unless it is between UTC-24 and UTC+24), /// but it will propagate to the `DateTime` values constructed via this date. /// /// Fails on the out-of-range date and/or invalid week number. @@ -203,11 +232,11 @@ pub trait Offset: Clone + fmt::Debug { } /// Makes a new `Date` from ISO week date (year and week number), day of the week (DOW) and - /// the current offset. + /// the current time zone. /// This assumes the proleptic Gregorian calendar, with the year 0 being 1 BCE. /// The resulting `Date` may have a different year from the input year. /// - /// The offset normally does not affect the date (unless it is between UTC-24 and UTC+24), + /// The time zone normally does not affect the date (unless it is between UTC-24 and UTC+24), /// but it will propagate to the `DateTime` values constructed via this date. /// /// Returns `None` on the out-of-range date and/or invalid week number. @@ -218,14 +247,14 @@ pub trait Offset: Clone + fmt::Debug { } } - /// Makes a new `Time` from hour, minute, second and the current offset. + /// Makes a new `Time` from hour, minute, second and the current time zone. /// /// Fails on invalid hour, minute and/or second. fn hms(&self, hour: u32, min: u32, sec: u32) -> Time { self.hms_opt(hour, min, sec).unwrap() } - /// Makes a new `Time` from hour, minute, second and the current offset. + /// Makes a new `Time` from hour, minute, second and the current time zone. /// /// Returns `None` on invalid hour, minute and/or second. fn hms_opt(&self, hour: u32, min: u32, sec: u32) -> LocalResult> { @@ -235,7 +264,7 @@ pub trait Offset: Clone + fmt::Debug { } } - /// Makes a new `Time` from hour, minute, second, millisecond and the current offset. + /// Makes a new `Time` from hour, minute, second, millisecond and the current time zone. /// The millisecond part can exceed 1,000 in order to represent the leap second. /// /// Fails on invalid hour, minute, second and/or millisecond. @@ -243,7 +272,7 @@ pub trait Offset: Clone + fmt::Debug { self.hms_milli_opt(hour, min, sec, milli).unwrap() } - /// Makes a new `Time` from hour, minute, second, millisecond and the current offset. + /// Makes a new `Time` from hour, minute, second, millisecond and the current time zone. /// The millisecond part can exceed 1,000 in order to represent the leap second. /// /// Returns `None` on invalid hour, minute, second and/or millisecond. @@ -254,7 +283,7 @@ pub trait Offset: Clone + fmt::Debug { } } - /// Makes a new `Time` from hour, minute, second, microsecond and the current offset. + /// Makes a new `Time` from hour, minute, second, microsecond and the current time zone. /// The microsecond part can exceed 1,000,000 in order to represent the leap second. /// /// Fails on invalid hour, minute, second and/or microsecond. @@ -262,7 +291,7 @@ pub trait Offset: Clone + fmt::Debug { self.hms_micro_opt(hour, min, sec, micro).unwrap() } - /// Makes a new `Time` from hour, minute, second, microsecond and the current offset. + /// Makes a new `Time` from hour, minute, second, microsecond and the current time zone. /// The microsecond part can exceed 1,000,000 in order to represent the leap second. /// /// Returns `None` on invalid hour, minute, second and/or microsecond. @@ -273,7 +302,7 @@ pub trait Offset: Clone + fmt::Debug { } } - /// Makes a new `Time` from hour, minute, second, nanosecond and the current offset. + /// Makes a new `Time` from hour, minute, second, nanosecond and the current time zone. /// The nanosecond part can exceed 1,000,000,000 in order to represent the leap second. /// /// Fails on invalid hour, minute, second and/or nanosecond. @@ -281,7 +310,7 @@ pub trait Offset: Clone + fmt::Debug { self.hms_nano_opt(hour, min, sec, nano).unwrap() } - /// Makes a new `Time` from hour, minute, second, nanosecond and the current offset. + /// Makes a new `Time` from hour, minute, second, nanosecond and the current time zone. /// The nanosecond part can exceed 1,000,000,000 in order to represent the leap second. /// /// Returns `None` on invalid hour, minute, second and/or nanosecond. @@ -304,261 +333,71 @@ pub trait Offset: Clone + fmt::Debug { fn datetime_from_str(&self, s: &str, fmt: &str) -> ParseResult> { let mut parsed = Parsed::new(); try!(parse(&mut parsed, s, StrftimeItems::new(fmt))); - parsed.to_datetime_with_offset(self.clone()) + parsed.to_datetime_with_timezone(self) } - /// Returns the *current* offset from UTC to the local time. - fn local_minus_utc(&self) -> Duration; + /// Reconstructs the time zone from the offset. + fn from_offset(offset: &Self::Offset) -> Self; + + /// Creates the offset(s) for given local `NaiveDate` if possible. + fn offset_from_local_date(&self, local: &NaiveDate) -> LocalResult; + + /// Creates the offset(s) for given local `NaiveTime` if possible. + fn offset_from_local_time(&self, local: &NaiveTime) -> LocalResult; + + /// Creates the offset(s) for given local `NaiveDateTime` if possible. + fn offset_from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult; /// Converts the local `NaiveDate` to the timezone-aware `Date` if possible. - fn from_local_date(&self, local: &NaiveDate) -> LocalResult>; + fn from_local_date(&self, local: &NaiveDate) -> LocalResult> { + self.offset_from_local_date(local).map(|offset| { + Date::from_utc(*local - offset.local_minus_utc(), offset) + }) + } /// Converts the local `NaiveTime` to the timezone-aware `Time` if possible. - fn from_local_time(&self, local: &NaiveTime) -> LocalResult>; + fn from_local_time(&self, local: &NaiveTime) -> LocalResult> { + self.offset_from_local_time(local).map(|offset| { + Time::from_utc(*local - offset.local_minus_utc(), offset) + }) + } /// Converts the local `NaiveDateTime` to the timezone-aware `DateTime` if possible. - fn from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult>; + fn from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult> { + self.offset_from_local_datetime(local).map(|offset| { + DateTime::from_utc(*local - offset.local_minus_utc(), offset) + }) + } + + /// Creates the offset for given UTC `NaiveDate`. This cannot fail. + fn offset_from_utc_date(&self, utc: &NaiveDate) -> Self::Offset; + + /// Creates the offset for given UTC `NaiveTime`. This cannot fail. + fn offset_from_utc_time(&self, utc: &NaiveTime) -> Self::Offset; + + /// Creates the offset for given UTC `NaiveDateTime`. This cannot fail. + fn offset_from_utc_datetime(&self, utc: &NaiveDateTime) -> Self::Offset; /// Converts the UTC `NaiveDate` to the local time. /// The UTC is continuous and thus this cannot fail (but can give the duplicate local time). - fn to_local_date(&self, utc: &NaiveDate) -> NaiveDate; + fn from_utc_date(&self, utc: &NaiveDate) -> Date { + Date::from_utc(utc.clone(), self.offset_from_utc_date(utc)) + } /// Converts the UTC `NaiveTime` to the local time. /// The UTC is continuous and thus this cannot fail (but can give the duplicate local time). - fn to_local_time(&self, utc: &NaiveTime) -> NaiveTime; + fn from_utc_time(&self, utc: &NaiveTime) -> Time { + Time::from_utc(utc.clone(), self.offset_from_utc_time(utc)) + } /// Converts the UTC `NaiveDateTime` to the local time. /// The UTC is continuous and thus this cannot fail (but can give the duplicate local time). - fn to_local_datetime(&self, utc: &NaiveDateTime) -> NaiveDateTime; -} - -/// The UTC timescale. This is the most efficient offset when you don't need the local time. -#[derive(Copy, Clone, PartialEq, Eq)] -pub struct UTC; - -impl UTC { - /// Returns a `Date` which corresponds to the current date. - pub fn today() -> Date { UTC::now().date() } - - /// Returns a `DateTime` which corresponds to the current date. - pub fn now() -> DateTime { - let spec = stdtime::get_time(); - let naive = NaiveDateTime::from_num_seconds_from_unix_epoch(spec.sec, spec.nsec as u32); - DateTime::from_utc(naive, UTC) + fn from_utc_datetime(&self, utc: &NaiveDateTime) -> DateTime { + DateTime::from_utc(utc.clone(), self.offset_from_utc_datetime(utc)) } } -impl Offset for UTC { - fn local_minus_utc(&self) -> Duration { Duration::zero() } - - fn from_local_date(&self, local: &NaiveDate) -> LocalResult> { - LocalResult::Single(Date::from_utc(local.clone(), UTC)) - } - fn from_local_time(&self, local: &NaiveTime) -> LocalResult> { - LocalResult::Single(Time::from_utc(local.clone(), UTC)) - } - fn from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult> { - LocalResult::Single(DateTime::from_utc(local.clone(), UTC)) - } - - fn to_local_date(&self, utc: &NaiveDate) -> NaiveDate { utc.clone() } - fn to_local_time(&self, utc: &NaiveTime) -> NaiveTime { utc.clone() } - fn to_local_datetime(&self, utc: &NaiveDateTime) -> NaiveDateTime { utc.clone() } -} - -impl fmt::Debug for UTC { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Z") } -} - -impl fmt::Display for UTC { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "UTC") } -} - -/// The fixed offset, from UTC-23:59:59 to UTC+23:59:59. -#[derive(Copy, Clone, PartialEq, Eq)] -pub struct FixedOffset { - local_minus_utc: i32, -} - -impl FixedOffset { - /// Makes a new `FixedOffset` for the Eastern Hemisphere with given timezone difference. - /// The negative `secs` means the Western Hemisphere. - /// - /// Fails on the out-of-bound `secs`. - pub fn east(secs: i32) -> FixedOffset { - FixedOffset::east_opt(secs).expect("FixedOffset::east out of bounds") - } - - /// Makes a new `FixedOffset` for the Eastern Hemisphere with given timezone difference. - /// The negative `secs` means the Western Hemisphere. - /// - /// Returns `None` on the out-of-bound `secs`. - pub fn east_opt(secs: i32) -> Option { - if -86400 < secs && secs < 86400 { - Some(FixedOffset { local_minus_utc: secs }) - } else { - None - } - } - - /// Makes a new `FixedOffset` for the Western Hemisphere with given timezone difference. - /// The negative `secs` means the Eastern Hemisphere. - /// - /// Fails on the out-of-bound `secs`. - pub fn west(secs: i32) -> FixedOffset { - FixedOffset::west_opt(secs).expect("FixedOffset::west out of bounds") - } - - /// Makes a new `FixedOffset` for the Western Hemisphere with given timezone difference. - /// The negative `secs` means the Eastern Hemisphere. - /// - /// Returns `None` on the out-of-bound `secs`. - pub fn west_opt(secs: i32) -> Option { - if -86400 < secs && secs < 86400 { - Some(FixedOffset { local_minus_utc: -secs }) - } else { - None - } - } -} - -impl Offset for FixedOffset { - fn local_minus_utc(&self) -> Duration { Duration::seconds(self.local_minus_utc as i64) } - - fn from_local_date(&self, local: &NaiveDate) -> LocalResult> { - LocalResult::Single(Date::from_utc(local.clone(), self.clone())) - } - fn from_local_time(&self, local: &NaiveTime) -> LocalResult> { - let t = Time::from_utc(*local + Duration::seconds(-self.local_minus_utc as i64), - self.clone()); - LocalResult::Single(t) - } - fn from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult> { - let dt = DateTime::from_utc(*local + Duration::seconds(-self.local_minus_utc as i64), - self.clone()); - LocalResult::Single(dt) - } - - fn to_local_date(&self, utc: &NaiveDate) -> NaiveDate { - utc.clone() - } - fn to_local_time(&self, utc: &NaiveTime) -> NaiveTime { - *utc + Duration::seconds(self.local_minus_utc as i64) - } - fn to_local_datetime(&self, utc: &NaiveDateTime) -> NaiveDateTime { - *utc + Duration::seconds(self.local_minus_utc as i64) - } -} - -impl fmt::Debug for FixedOffset { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let offset = self.local_minus_utc; - let (sign, offset) = if offset < 0 {('-', -offset)} else {('+', offset)}; - let (mins, sec) = div_mod_floor(offset, 60); - let (hour, min) = div_mod_floor(mins, 60); - if sec == 0 { - write!(f, "{}{:02}:{:02}", sign, hour, min) - } else { - write!(f, "{}{:02}:{:02}:{:02}", sign, hour, min, sec) - } - } -} - -impl fmt::Display for FixedOffset { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Debug::fmt(self, f) } -} - -/// The local timescale. This is implemented via the standard `time` crate. -#[derive(Copy, Clone)] -pub struct Local { - cached: FixedOffset, -} - -impl Local { - /// Converts a `time::Tm` struct into the timezone-aware `DateTime`. - /// This assumes that `time` is working correctly, i.e. any error is fatal. - fn tm_to_datetime(mut tm: stdtime::Tm) -> DateTime { - if tm.tm_sec >= 60 { - tm.tm_sec = 59; - tm.tm_nsec += (tm.tm_sec - 59) * 1_000_000_000; - } - - // from_yo is more efficient than from_ymd (since it's the internal representation). - let date = NaiveDate::from_yo(tm.tm_year + 1900, tm.tm_yday as u32 + 1); - let time = NaiveTime::from_hms_nano(tm.tm_hour as u32, tm.tm_min as u32, - tm.tm_sec as u32, tm.tm_nsec as u32); - let offset = Local { cached: FixedOffset::east(tm.tm_utcoff) }; - DateTime::from_utc(date.and_time(time) + Duration::seconds(-tm.tm_utcoff as i64), offset) - } - - /// Converts a local `NaiveDateTime` to the `time::Timespec`. - fn datetime_to_timespec(d: &NaiveDateTime) -> stdtime::Timespec { - let tm = stdtime::Tm { - tm_sec: d.second() as i32, - tm_min: d.minute() as i32, - tm_hour: d.hour() as i32, - tm_mday: d.day() as i32, - tm_mon: d.month0() as i32, // yes, C is that strange... - tm_year: d.year() - 1900, // this doesn't underflow, we know that d is `NaiveDateTime`. - tm_wday: 0, // to_local ignores this - tm_yday: 0, // and this - tm_isdst: -1, - tm_utcoff: 1, // this is arbitrary but should be nonzero - // in order to make `to_timespec` use `rust_mktime` internally. - tm_nsec: d.nanosecond() as i32, - }; - tm.to_timespec() - } - - /// Returns a `Date` which corresponds to the current date. - pub fn today() -> Date { - Local::now().date() - } - - /// Returns a `DateTime` which corresponds to the current date. - pub fn now() -> DateTime { - Local::tm_to_datetime(stdtime::now()) - } -} - -impl Offset for Local { - 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)) { - LocalResult::None => LocalResult::None, - LocalResult::Single(dt) => LocalResult::Single(dt.date()), - LocalResult::Ambiguous(min, max) => { - let min = min.date(); - let max = max.date(); - if min == max {LocalResult::Single(min)} else {LocalResult::Ambiguous(min, max)} - } - } - } - - fn from_local_time(&self, local: &NaiveTime) -> LocalResult> { - // XXX we don't have enough information here, so we assume that the timezone remains same - LocalResult::Single(Time::from_utc(local.clone(), self.clone())) - } - - fn from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult> { - let timespec = Local::datetime_to_timespec(local); - LocalResult::Single(Local::tm_to_datetime(stdtime::at(timespec))) - } - - fn to_local_date(&self, utc: &NaiveDate) -> NaiveDate { self.cached.to_local_date(utc) } - fn to_local_time(&self, utc: &NaiveTime) -> NaiveTime { self.cached.to_local_time(utc) } - fn to_local_datetime(&self, utc: &NaiveDateTime) -> NaiveDateTime { - self.cached.to_local_datetime(utc) - } -} - -impl fmt::Debug for Local { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.cached.fmt(f) } -} - -impl fmt::Display for Local { - // TODO this should be a tz name whenever available - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.cached.fmt(f) } -} +pub mod utc; +pub mod fixed; +pub mod local; diff --git a/src/offset/utc.rs b/src/offset/utc.rs new file mode 100644 index 0000000..e741202 --- /dev/null +++ b/src/offset/utc.rs @@ -0,0 +1,68 @@ +// This is a part of rust-chrono. +// Copyright (c) 2015, Kang Seonghoon. +// See README.md and LICENSE.txt for details. + +/*! + * The UTC (Coordinated Universal Time) time zone. + */ + +use std::fmt; +use stdtime; + +use duration::Duration; +use naive::date::NaiveDate; +use naive::time::NaiveTime; +use naive::datetime::NaiveDateTime; +use date::Date; +use datetime::DateTime; +use super::{TimeZone, Offset, LocalResult}; + +/// The UTC time zone. This is the most efficient time zone when you don't need the local time. +/// It is also used as an offset (which is also a dummy type). +#[derive(Copy, Clone, PartialEq, Eq)] +pub struct UTC; + +impl UTC { + /// Returns a `Date` which corresponds to the current date. + pub fn today() -> Date { UTC::now().date() } + + /// Returns a `DateTime` which corresponds to the current date. + pub fn now() -> DateTime { + let spec = stdtime::get_time(); + let naive = NaiveDateTime::from_num_seconds_from_unix_epoch(spec.sec, spec.nsec as u32); + DateTime::from_utc(naive, UTC) + } +} + +impl TimeZone for UTC { + type Offset = UTC; + + fn from_offset(_state: &UTC) -> UTC { UTC } + + fn offset_from_local_date(&self, _local: &NaiveDate) -> LocalResult { + LocalResult::Single(UTC) + } + fn offset_from_local_time(&self, _local: &NaiveTime) -> LocalResult { + LocalResult::Single(UTC) + } + fn offset_from_local_datetime(&self, _local: &NaiveDateTime) -> LocalResult { + LocalResult::Single(UTC) + } + + fn offset_from_utc_date(&self, _utc: &NaiveDate) -> UTC { UTC } + fn offset_from_utc_time(&self, _utc: &NaiveTime) -> UTC { UTC } + fn offset_from_utc_datetime(&self, _utc: &NaiveDateTime) -> UTC { UTC} +} + +impl Offset for UTC { + fn local_minus_utc(&self) -> Duration { Duration::zero() } +} + +impl fmt::Debug for UTC { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Z") } +} + +impl fmt::Display for UTC { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "UTC") } +} + diff --git a/src/time.rs b/src/time.rs index 0f5b5ba..36f4ad6 100644 --- a/src/time.rs +++ b/src/time.rs @@ -3,7 +3,7 @@ // See README.md and LICENSE.txt for details. /*! - * ISO 8601 time with timezone. + * ISO 8601 time with time zone. */ use std::{fmt, hash}; @@ -11,51 +11,72 @@ use std::cmp::Ordering; use std::ops::{Add, Sub}; use Timelike; -use offset::Offset; +use offset::{TimeZone, Offset}; use duration::Duration; use naive::time::NaiveTime; use format::{Item, DelayedFormat, StrftimeItems}; /// ISO 8601 time with timezone. #[derive(Clone)] -pub struct Time { +pub struct Time { time: NaiveTime, - offset: Off, + offset: Tz::Offset, } -impl Time { +impl Time { /// Makes a new `Time` with given *UTC* time and offset. - /// The local time should be constructed via the `Offset` trait. + /// The local time should be constructed via the `TimeZone` trait. + // + // note: this constructor is purposedly not named to `new` to discourage the direct usage. #[inline] - pub fn from_utc(time: NaiveTime, offset: Off) -> Time { + pub fn from_utc(time: NaiveTime, offset: Tz::Offset) -> Time { Time { time: time, offset: offset } } - /// Retrieves an associated offset. + /// Retrieves an associated offset from UTC. #[inline] - pub fn offset<'a>(&'a self) -> &'a Off { + pub fn offset<'a>(&'a self) -> &'a Tz::Offset { &self.offset } - /// Changes the associated offset. - /// This does not change the actual `Time` (but will change the string representation). + /// Retrieves an associated time zone. #[inline] - pub fn with_offset(&self, offset: Off2) -> Time { - Time::from_utc(self.time, offset) + pub fn timezone(&self) -> Tz { + TimeZone::from_offset(&self.offset) } - /// Returns a view to the local time. - fn local(&self) -> NaiveTime { - self.offset.to_local_time(&self.time) + /// Changes the associated time zone. + /// This does not change the actual `Time` (but will change the string representation). + #[inline] + pub fn with_timezone(&self, tz: &Tz2) -> Time { + tz.from_utc_time(&self.time) + } + + /// Returns a view to the naive UTC time. + #[inline] + pub fn naive_utc(&self) -> NaiveTime { + self.time + } + + /// Returns a view to the naive local time. + #[inline] + pub fn naive_local(&self) -> NaiveTime { + self.time + self.offset.local_minus_utc() } } -impl Time { +/// Maps the local time to other time with given conversion function. +fn map_local(t: &Time, mut f: F) -> Option> + where F: FnMut(NaiveTime) -> Option { + f(t.naive_local()).and_then(|time| t.timezone().from_local_time(&time).single()) +} + +impl Time where Tz::Offset: fmt::Display { /// Formats the time with the specified formatting items. #[inline] pub fn format_with_items<'a, I>(&'a self, items: I) -> DelayedFormat<'a, I> where I: Iterator> + Clone { - DelayedFormat::new_with_offset(None, Some(self.local()), &self.offset, items) + DelayedFormat::new_with_offset(None, Some(self.naive_local()), &self.offset, items) } /// Formats the time with the specified format string. @@ -66,91 +87,87 @@ impl Time { } } -impl Timelike for Time { - #[inline] fn hour(&self) -> u32 { self.local().hour() } - #[inline] fn minute(&self) -> u32 { self.local().minute() } - #[inline] fn second(&self) -> u32 { self.local().second() } - #[inline] fn nanosecond(&self) -> u32 { self.local().nanosecond() } +impl Timelike for Time { + #[inline] fn hour(&self) -> u32 { self.naive_local().hour() } + #[inline] fn minute(&self) -> u32 { self.naive_local().minute() } + #[inline] fn second(&self) -> u32 { self.naive_local().second() } + #[inline] fn nanosecond(&self) -> u32 { self.naive_local().nanosecond() } #[inline] - fn with_hour(&self, hour: u32) -> Option> { - self.local().with_hour(hour) - .and_then(|time| self.offset.from_local_time(&time).single()) + fn with_hour(&self, hour: u32) -> Option> { + map_local(self, |time| time.with_hour(hour)) } #[inline] - fn with_minute(&self, min: u32) -> Option> { - self.local().with_minute(min) - .and_then(|time| self.offset.from_local_time(&time).single()) + fn with_minute(&self, min: u32) -> Option> { + map_local(self, |time| time.with_minute(min)) } #[inline] - fn with_second(&self, sec: u32) -> Option> { - self.local().with_second(sec) - .and_then(|time| self.offset.from_local_time(&time).single()) + fn with_second(&self, sec: u32) -> Option> { + map_local(self, |time| time.with_second(sec)) } #[inline] - fn with_nanosecond(&self, nano: u32) -> Option> { - self.local().with_nanosecond(nano) - .and_then(|time| self.offset.from_local_time(&time).single()) + fn with_nanosecond(&self, nano: u32) -> Option> { + map_local(self, |time| time.with_nanosecond(nano)) } #[inline] - fn num_seconds_from_midnight(&self) -> u32 { self.local().num_seconds_from_midnight() } + fn num_seconds_from_midnight(&self) -> u32 { self.naive_local().num_seconds_from_midnight() } } -impl PartialEq> for Time { - fn eq(&self, other: &Time) -> bool { self.time == other.time } +impl PartialEq> for Time { + fn eq(&self, other: &Time) -> bool { self.time == other.time } } -impl Eq for Time { +impl Eq for Time { } -impl PartialOrd for Time { - fn partial_cmp(&self, other: &Time) -> Option { +impl PartialOrd for Time { + fn partial_cmp(&self, other: &Time) -> Option { self.time.partial_cmp(&other.time) } } -impl Ord for Time { - fn cmp(&self, other: &Time) -> Ordering { self.time.cmp(&other.time) } +impl Ord for Time { + fn cmp(&self, other: &Time) -> Ordering { self.time.cmp(&other.time) } } -impl hash::Hash for Time { +impl hash::Hash for Time { fn hash(&self, state: &mut H) { self.time.hash(state) } } -impl Add for Time { - type Output = Time; +impl Add for Time { + type Output = Time; - fn add(self, rhs: Duration) -> Time { + fn add(self, rhs: Duration) -> Time { Time { time: self.time + rhs, offset: self.offset } } } -impl Sub> for Time { +impl Sub> for Time { type Output = Duration; - fn sub(self, rhs: Time) -> Duration { self.time - rhs.time } + fn sub(self, rhs: Time) -> Duration { self.time - rhs.time } } -impl Sub for Time { - type Output = Time; +impl Sub for Time { + type Output = Time; #[inline] - fn sub(self, rhs: Duration) -> Time { self.add(-rhs) } + fn sub(self, rhs: Duration) -> Time { self.add(-rhs) } } -impl fmt::Debug for Time { +impl fmt::Debug for Time { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?}{:?}", self.local(), self.offset) + write!(f, "{:?}{:?}", self.naive_local(), self.offset) } } -impl fmt::Display for Time { +impl fmt::Display for Time where Tz::Offset: fmt::Display { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}{}", self.local(), self.offset) + write!(f, "{}{}", self.naive_local(), self.offset) } }