From 7ea1ce50801abf66867a100704946ea007c0ee7a Mon Sep 17 00:00:00 2001 From: Kang Seonghoon Date: Tue, 7 Feb 2017 03:43:59 +0900 Subject: [PATCH] `FixedOffset` is now the official "fixed offset value" type. This may sound strange, but the final type for the offset "value" was originally `time::Duration` (returned by `Offset::local_minus_utc`). This caused a lot of problems becaus adding `Duration` fully interacts with leap seconds and `Duration` itself is somewhat deprecated. This commit entirely replaces this role of `Duration` with `FixedOffset`. So if we had `Offset` and `Duration` to represent the "storage" offset type and the offset "value" in the past, we now have `Offset` and `FixedOffset`. Storage-to-value conversion is called to "fix" the offset---an apt term for the type. The list of actual changes: - The time zone offset is now restricted to UTC-23:59:59 through UTC+23:59:59, and no subsecond value is allowed. As described above, `FixedOffset` is now fully used for this purpose. - One can now add and subtract `FixedOffset` to/from timelike values. Replaces a temporary `chrono::offset::add_with_leapsecond` function. Datelike & non-timelike values are never affected by the offset. - UTC and local views to `Date` are now identical. We keep relevant methods for the consistency right now. - `chrono::format::format` now receives `FixedOffset` in place of `(Old)Duration`. - `Offset` now has a `fix` method to resolve, or to "fix" the "storage" offset (`Offset`) to the offset "value" (`FixedOffset`). - `FixedOffset::{local_minus_utc, utc_minus_local}` methods are added. They no longer depend on `Duration` as well. --- src/date.rs | 67 +++------------------------- src/datetime.rs | 6 +-- src/format/mod.rs | 20 ++++----- src/format/parsed.rs | 11 +---- src/lib.rs | 2 +- src/offset/fixed.rs | 104 ++++++++++++++++++++++++++++++++++++++++++- src/offset/local.rs | 12 +++++ src/offset/mod.rs | 28 +++--------- src/offset/utc.rs | 8 ++-- 9 files changed, 146 insertions(+), 112 deletions(-) diff --git a/src/date.rs b/src/date.rs index 16374c4..aa63706 100644 --- a/src/date.rs +++ b/src/date.rs @@ -9,7 +9,7 @@ use std::ops::{Add, Sub}; use oldtime::Duration as OldDuration; use {Weekday, Datelike}; -use offset::{TimeZone, Offset}; +use offset::TimeZone; use offset::utc::UTC; use naive; use naive::date::NaiveDate; @@ -256,9 +256,13 @@ impl Date { } /// Returns a view to the naive local date. + /// + /// This is technically same to [`naive_utc`](#method.naive_utc) + /// because the offset is restricted to never exceed one day, + /// but provided for the consistency. #[inline] pub fn naive_local(&self) -> NaiveDate { - self.date + self.offset.local_minus_utc() + self.date } } @@ -398,62 +402,3 @@ impl fmt::Display for Date where Tz::Offset: fmt::Display { } } -#[cfg(test)] -mod tests { - use std::fmt; - use oldtime::Duration; - - use Datelike; - use naive::date::NaiveDate; - use naive::datetime::NaiveDateTime; - use offset::{TimeZone, Offset, LocalResult}; - use offset::local::Local; - - #[derive(Copy, Clone, PartialEq, Eq)] - struct UTC1y; // same to UTC but with an offset of 365 days - - #[derive(Copy, Clone, PartialEq, Eq)] - struct OneYear; - - 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 offset_from_local_datetime(&self, _local: &NaiveDateTime) -> LocalResult { - LocalResult::Single(OneYear) - } - - fn offset_from_utc_date(&self, _utc: &NaiveDate) -> OneYear { OneYear } - fn offset_from_utc_datetime(&self, _utc: &NaiveDateTime) -> OneYear { OneYear } - } - - 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") } - } - - #[test] - fn test_date_weird_offset() { - assert_eq!(format!("{:?}", UTC1y.ymd(2012, 2, 29)), - "2012-02-29+8760:00".to_string()); - assert_eq!(format!("{:?}", UTC1y.ymd(2012, 2, 29).and_hms(5, 6, 7)), - "2012-02-29T05:06:07+8760:00".to_string()); - assert_eq!(format!("{:?}", UTC1y.ymd(2012, 3, 4)), - "2012-03-04+8760:00".to_string()); - assert_eq!(format!("{:?}", UTC1y.ymd(2012, 3, 4).and_hms(5, 6, 7)), - "2012-03-04T05:06:07+8760:00".to_string()); - } - - #[test] - fn test_local_date_sanity_check() { // issue #27 - assert_eq!(Local.ymd(2999, 12, 28).day(), 28); - } -} - diff --git a/src/datetime.rs b/src/datetime.rs index 7fddf5f..c88d4c9 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -9,7 +9,7 @@ use std::ops::{Add, Sub}; use oldtime::Duration as OldDuration; use {Weekday, Timelike, Datelike}; -use offset::{TimeZone, Offset, add_with_leapsecond}; +use offset::{TimeZone, Offset}; use offset::utc::UTC; use offset::local::Local; use offset::fixed::FixedOffset; @@ -59,7 +59,7 @@ impl DateTime { /// Unlike `date`, this is not associated to the time zone. #[inline] pub fn time(&self) -> NaiveTime { - add_with_leapsecond(&self.datetime.time(), &self.offset.local_minus_utc()) + self.datetime.time() + self.offset.fix() } /// Returns the number of non-leap seconds since January 1, 1970 0:00:00 UTC @@ -152,7 +152,7 @@ impl DateTime { /// Returns a view to the naive local datetime. #[inline] pub fn naive_local(&self) -> NaiveDateTime { - add_with_leapsecond(&self.datetime, &self.offset.local_minus_utc()) + self.datetime + self.offset.fix() } } diff --git a/src/format/mod.rs b/src/format/mod.rs index 3f36251..74f9fb5 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -5,11 +5,11 @@ use std::fmt; use std::error::Error; -use oldtime::Duration as OldDuration; use {Datelike, Timelike}; use div::{div_floor, mod_floor}; -use offset::{Offset, add_with_leapsecond}; +use offset::Offset; +use offset::fixed::FixedOffset; use naive::date::NaiveDate; use naive::time::NaiveTime; @@ -252,7 +252,7 @@ const BAD_FORMAT: ParseError = ParseError(ParseErrorKind::BadFormat); /// Tries to format given arguments with given formatting items. /// Internally used by `DelayedFormat`. pub fn format<'a, I>(w: &mut fmt::Formatter, date: Option<&NaiveDate>, time: Option<&NaiveTime>, - off: Option<&(String, OldDuration)>, items: I) -> fmt::Result + off: Option<&(String, FixedOffset)>, items: I) -> fmt::Result where I: Iterator> { // full and abbreviated month and weekday names static SHORT_MONTHS: [&'static str; 12] = @@ -302,7 +302,7 @@ pub fn format<'a, I>(w: &mut fmt::Formatter, date: Option<&NaiveDate>, time: Opt (Some(d), Some(t), None) => Some(d.and_time(*t).timestamp()), (Some(d), Some(t), Some(&(_, off))) => - Some(add_with_leapsecond(&d.and_time(*t), &-off).timestamp()), + Some((d.and_time(*t) - off).timestamp()), (_, _, _) => None }), }; @@ -332,15 +332,15 @@ pub fn format<'a, I>(w: &mut fmt::Formatter, date: Option<&NaiveDate>, time: Opt /// Prints an offset from UTC in the format of `+HHMM` or `+HH:MM`. /// `Z` instead of `+00[:]00` is allowed when `allow_zulu` is true. - fn write_local_minus_utc(w: &mut fmt::Formatter, off: OldDuration, + fn write_local_minus_utc(w: &mut fmt::Formatter, off: FixedOffset, allow_zulu: bool, use_colon: bool) -> fmt::Result { - let off = off.num_minutes(); + let off = off.local_minus_utc(); if !allow_zulu || off != 0 { let (sign, off) = if off < 0 {('-', -off)} else {('+', off)}; if use_colon { - write!(w, "{}{:02}:{:02}", sign, off / 60, off % 60) + write!(w, "{}{:02}:{:02}", sign, off / 3600, off / 60 % 60) } else { - write!(w, "{}{:02}{:02}", sign, off / 60, off % 60) + write!(w, "{}{:02}{:02}", sign, off / 3600, off / 60 % 60) } } else { write!(w, "Z") @@ -452,7 +452,7 @@ pub struct DelayedFormat { /// The time view, if any. time: Option, /// The name and local-to-UTC difference for the offset (timezone), if any. - off: Option<(String, OldDuration)>, + off: Option<(String, FixedOffset)>, /// An iterator returning formatting items. items: I, } @@ -467,7 +467,7 @@ impl<'a, I: Iterator> + Clone> DelayedFormat { pub fn new_with_offset(date: Option, time: Option, offset: &Off, items: I) -> DelayedFormat where Off: Offset + fmt::Display { - let name_and_diff = (offset.to_string(), offset.local_minus_utc()); + let name_and_diff = (offset.to_string(), offset.fix()); DelayedFormat { date: date, time: time, off: Some(name_and_diff), items: items } } } diff --git a/src/format/parsed.rs b/src/format/parsed.rs index 423f19f..7eb3a3e 100644 --- a/src/format/parsed.rs +++ b/src/format/parsed.rs @@ -604,20 +604,13 @@ impl Parsed { let nanosecond = self.nanosecond.unwrap_or(0); let dt = NaiveDateTime::from_timestamp_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)); + guessed_offset = tz.offset_from_utc_datetime(&dt).fix().local_minus_utc(); } // 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) + dt.offset().fix().local_minus_utc() == offset } else { true } diff --git a/src/lib.rs b/src/lib.rs index 6badb30..87bd9cd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -177,7 +177,7 @@ //! assert_eq!(dt.num_days_from_ce(), 735565); // the number of days from and including Jan 1, 1 //! //! // time zone accessor and manipulation -//! assert_eq!(dt.offset().local_minus_utc(), Duration::hours(9)); +//! assert_eq!(dt.offset().fix().local_minus_utc(), 9 * 3600); //! 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)); //! diff --git a/src/offset/fixed.rs b/src/offset/fixed.rs index 885741a..884da6c 100644 --- a/src/offset/fixed.rs +++ b/src/offset/fixed.rs @@ -3,12 +3,16 @@ //! The time zone which has a fixed offset from UTC. +use std::ops::{Add, Sub}; use std::fmt; use oldtime::Duration as OldDuration; +use Timelike; use div::div_mod_floor; +use naive::time::NaiveTime; use naive::date::NaiveDate; use naive::datetime::NaiveDateTime; +use datetime::DateTime; use super::{TimeZone, Offset, LocalResult}; /// The time zone with fixed offset, from UTC-23:59:59 to UTC+23:59:59. @@ -82,6 +86,16 @@ impl FixedOffset { None } } + + /// Returns the number of seconds to add to convert from UTC to the local time. + pub fn local_minus_utc(&self) -> i32 { + self.local_minus_utc + } + + /// Returns the number of seconds to add to convert from the local time to UTC. + pub fn utc_minus_local(&self) -> i32 { + -self.local_minus_utc + } } impl TimeZone for FixedOffset { @@ -101,7 +115,7 @@ impl TimeZone for FixedOffset { } impl Offset for FixedOffset { - fn local_minus_utc(&self) -> OldDuration { OldDuration::seconds(self.local_minus_utc as i64) } + fn fix(&self) -> FixedOffset { *self } } impl fmt::Debug for FixedOffset { @@ -122,3 +136,91 @@ impl fmt::Display for FixedOffset { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Debug::fmt(self, f) } } +// addition or subtraction of FixedOffset to/from Timelike values is same to +// adding or subtracting the offset's local_minus_utc value +// but keep keeps the leap second information. +// this should be implemented more efficiently, but for the time being, this is generic right now. + +fn add_with_leapsecond(lhs: &T, rhs: i32) -> T + where T: Timelike + Add +{ + // extract and temporarily remove the fractional part and later recover it + let nanos = lhs.nanosecond(); + let lhs = lhs.with_nanosecond(0).unwrap(); + (lhs + OldDuration::seconds(rhs as i64)).with_nanosecond(nanos).unwrap() +} + +impl Add for NaiveTime { + type Output = NaiveTime; + + #[inline] + fn add(self, rhs: FixedOffset) -> NaiveTime { + add_with_leapsecond(&self, rhs.local_minus_utc) + } +} + +impl Sub for NaiveTime { + type Output = NaiveTime; + + #[inline] + fn sub(self, rhs: FixedOffset) -> NaiveTime { + add_with_leapsecond(&self, -rhs.local_minus_utc) + } +} + +impl Add for NaiveDateTime { + type Output = NaiveDateTime; + + #[inline] + fn add(self, rhs: FixedOffset) -> NaiveDateTime { + add_with_leapsecond(&self, rhs.local_minus_utc) + } +} + +impl Sub for NaiveDateTime { + type Output = NaiveDateTime; + + #[inline] + fn sub(self, rhs: FixedOffset) -> NaiveDateTime { + add_with_leapsecond(&self, -rhs.local_minus_utc) + } +} + +impl Add for DateTime { + type Output = DateTime; + + #[inline] + fn add(self, rhs: FixedOffset) -> DateTime { + add_with_leapsecond(&self, rhs.local_minus_utc) + } +} + +impl Sub for DateTime { + type Output = DateTime; + + #[inline] + fn sub(self, rhs: FixedOffset) -> DateTime { + add_with_leapsecond(&self, -rhs.local_minus_utc) + } +} + +#[cfg(test)] +mod tests { + use offset::TimeZone; + use super::FixedOffset; + + #[test] + fn test_date_extreme_offset() { + // starting from 0.3 we don't have an offset exceeding one day. + // this makes everything easier! + assert_eq!(format!("{:?}", FixedOffset::east(86399).ymd(2012, 2, 29)), + "2012-02-29+23:59:59".to_string()); + assert_eq!(format!("{:?}", FixedOffset::east(86399).ymd(2012, 2, 29).and_hms(5, 6, 7)), + "2012-02-29T05:06:07+23:59:59".to_string()); + assert_eq!(format!("{:?}", FixedOffset::west(86399).ymd(2012, 3, 4)), + "2012-03-04-23:59:59".to_string()); + assert_eq!(format!("{:?}", FixedOffset::west(86399).ymd(2012, 3, 4).and_hms(5, 6, 7)), + "2012-03-04T05:06:07-23:59:59".to_string()); + } +} + diff --git a/src/offset/local.rs b/src/offset/local.rs index 8f8c7ca..c470d9a 100644 --- a/src/offset/local.rs +++ b/src/offset/local.rs @@ -137,3 +137,15 @@ impl TimeZone for Local { } } +#[cfg(test)] +mod tests { + use Datelike; + use offset::TimeZone; + use super::Local; + + #[test] + fn test_local_date_sanity_check() { // issue #27 + assert_eq!(Local.ymd(2999, 12, 28).day(), 28); + } +} + diff --git a/src/offset/mod.rs b/src/offset/mod.rs index cf27da9..965497a 100644 --- a/src/offset/mod.rs +++ b/src/offset/mod.rs @@ -21,31 +21,15 @@ */ use std::fmt; -use std::ops::Add; -use oldtime::Duration as OldDuration; use Weekday; -use Timelike; use naive::date::NaiveDate; use naive::time::NaiveTime; use naive::datetime::NaiveDateTime; use date::Date; use datetime::DateTime; use format::{parse, Parsed, ParseResult, StrftimeItems}; - -/// Same to `*lhs + *rhs`, but keeps the leap second information. -/// `rhs` should *not* have a fractional second. -// TODO this should be replaced by the addition with FixedOffset in 0.3! -pub fn add_with_leapsecond(lhs: &T, rhs: &OldDuration) -> T - where T: Timelike + Add -{ - debug_assert!(*rhs == OldDuration::seconds(rhs.num_seconds())); - - // extract and temporarily remove the fractional part and later recover it - let nanos = lhs.nanosecond(); - let lhs = lhs.with_nanosecond(0).unwrap(); - (lhs + *rhs).with_nanosecond(nanos).unwrap() -} +use self::fixed::FixedOffset; /// The conversion result from the local time to the timezone-aware datetime types. #[derive(Clone, PartialEq, Debug)] @@ -175,8 +159,8 @@ impl LocalResult { /// The offset from the local time to UTC. pub trait Offset: Sized + Clone + fmt::Debug { - /// Returns the offset from UTC to the local time stored. - fn local_minus_utc(&self) -> OldDuration; + /// Returns the fixed offset from UTC to the local time stored. + fn fix(&self) -> FixedOffset; } /// The time zone. @@ -358,15 +342,15 @@ pub trait TimeZone: Sized + Clone { /// Converts the local `NaiveDate` to the timezone-aware `Date` if possible. 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) + // since FixedOffset is within +/- 1 day, the date is never affected + Date::from_utc(*local, offset) }) } /// Converts the local `NaiveDateTime` to the timezone-aware `DateTime` if possible. fn from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult> { self.offset_from_local_datetime(local).map(|offset| { - let utc = add_with_leapsecond(local, &-offset.local_minus_utc()); - DateTime::from_utc(utc, offset) + DateTime::from_utc(*local - offset.fix(), offset) }) } diff --git a/src/offset/utc.rs b/src/offset/utc.rs index 2a0c9d4..aa028fc 100644 --- a/src/offset/utc.rs +++ b/src/offset/utc.rs @@ -1,19 +1,17 @@ // This is a part of Chrono. // See README.md and LICENSE.txt for details. -/*! - * The UTC (Coordinated Universal Time) time zone. - */ +//! The UTC (Coordinated Universal Time) time zone. use std::fmt; use oldtime; -use oldtime::Duration as OldDuration; use naive::date::NaiveDate; use naive::datetime::NaiveDateTime; use date::Date; use datetime::DateTime; use super::{TimeZone, Offset, LocalResult}; +use super::fixed::FixedOffset; /// 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). @@ -64,7 +62,7 @@ impl TimeZone for UTC { } impl Offset for UTC { - fn local_minus_utc(&self) -> OldDuration { OldDuration::zero() } + fn fix(&self) -> FixedOffset { FixedOffset::east(0) } } impl fmt::Debug for UTC {