`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<Tz>` 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.
This commit is contained in:
Kang Seonghoon 2017-02-07 03:43:59 +09:00
parent 2b5553ee76
commit 7ea1ce5080
No known key found for this signature in database
GPG Key ID: 82440FABA6709020
9 changed files with 146 additions and 112 deletions

View File

@ -9,7 +9,7 @@ use std::ops::{Add, Sub};
use oldtime::Duration as OldDuration; use oldtime::Duration as OldDuration;
use {Weekday, Datelike}; use {Weekday, Datelike};
use offset::{TimeZone, Offset}; use offset::TimeZone;
use offset::utc::UTC; use offset::utc::UTC;
use naive; use naive;
use naive::date::NaiveDate; use naive::date::NaiveDate;
@ -256,9 +256,13 @@ impl<Tz: TimeZone> Date<Tz> {
} }
/// Returns a view to the naive local 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] #[inline]
pub fn naive_local(&self) -> NaiveDate { pub fn naive_local(&self) -> NaiveDate {
self.date + self.offset.local_minus_utc() self.date
} }
} }
@ -398,62 +402,3 @@ impl<Tz: TimeZone> fmt::Display for Date<Tz> 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<OneYear> {
LocalResult::Single(OneYear)
}
fn offset_from_local_datetime(&self, _local: &NaiveDateTime) -> LocalResult<OneYear> {
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);
}
}

View File

@ -9,7 +9,7 @@ use std::ops::{Add, Sub};
use oldtime::Duration as OldDuration; use oldtime::Duration as OldDuration;
use {Weekday, Timelike, Datelike}; use {Weekday, Timelike, Datelike};
use offset::{TimeZone, Offset, add_with_leapsecond}; use offset::{TimeZone, Offset};
use offset::utc::UTC; use offset::utc::UTC;
use offset::local::Local; use offset::local::Local;
use offset::fixed::FixedOffset; use offset::fixed::FixedOffset;
@ -59,7 +59,7 @@ impl<Tz: TimeZone> DateTime<Tz> {
/// Unlike `date`, this is not associated to the time zone. /// Unlike `date`, this is not associated to the time zone.
#[inline] #[inline]
pub fn time(&self) -> NaiveTime { 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 /// Returns the number of non-leap seconds since January 1, 1970 0:00:00 UTC
@ -152,7 +152,7 @@ impl<Tz: TimeZone> DateTime<Tz> {
/// Returns a view to the naive local datetime. /// Returns a view to the naive local datetime.
#[inline] #[inline]
pub fn naive_local(&self) -> NaiveDateTime { pub fn naive_local(&self) -> NaiveDateTime {
add_with_leapsecond(&self.datetime, &self.offset.local_minus_utc()) self.datetime + self.offset.fix()
} }
} }

View File

@ -5,11 +5,11 @@
use std::fmt; use std::fmt;
use std::error::Error; use std::error::Error;
use oldtime::Duration as OldDuration;
use {Datelike, Timelike}; use {Datelike, Timelike};
use div::{div_floor, mod_floor}; 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::date::NaiveDate;
use naive::time::NaiveTime; use naive::time::NaiveTime;
@ -252,7 +252,7 @@ const BAD_FORMAT: ParseError = ParseError(ParseErrorKind::BadFormat);
/// Tries to format given arguments with given formatting items. /// Tries to format given arguments with given formatting items.
/// Internally used by `DelayedFormat`. /// Internally used by `DelayedFormat`.
pub fn format<'a, I>(w: &mut fmt::Formatter, date: Option<&NaiveDate>, time: Option<&NaiveTime>, 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<Item=Item<'a>> { where I: Iterator<Item=Item<'a>> {
// full and abbreviated month and weekday names // full and abbreviated month and weekday names
static SHORT_MONTHS: [&'static str; 12] = 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), Some(t), None) =>
Some(d.and_time(*t).timestamp()), Some(d.and_time(*t).timestamp()),
(Some(d), Some(t), Some(&(_, off))) => (Some(d), Some(t), Some(&(_, off))) =>
Some(add_with_leapsecond(&d.and_time(*t), &-off).timestamp()), Some((d.and_time(*t) - off).timestamp()),
(_, _, _) => None (_, _, _) => 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`. /// 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. /// `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 { allow_zulu: bool, use_colon: bool) -> fmt::Result {
let off = off.num_minutes(); let off = off.local_minus_utc();
if !allow_zulu || off != 0 { if !allow_zulu || off != 0 {
let (sign, off) = if off < 0 {('-', -off)} else {('+', off)}; let (sign, off) = if off < 0 {('-', -off)} else {('+', off)};
if use_colon { if use_colon {
write!(w, "{}{:02}:{:02}", sign, off / 60, off % 60) write!(w, "{}{:02}:{:02}", sign, off / 3600, off / 60 % 60)
} else { } else {
write!(w, "{}{:02}{:02}", sign, off / 60, off % 60) write!(w, "{}{:02}{:02}", sign, off / 3600, off / 60 % 60)
} }
} else { } else {
write!(w, "Z") write!(w, "Z")
@ -452,7 +452,7 @@ pub struct DelayedFormat<I> {
/// The time view, if any. /// The time view, if any.
time: Option<NaiveTime>, time: Option<NaiveTime>,
/// The name and local-to-UTC difference for the offset (timezone), if any. /// 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. /// An iterator returning formatting items.
items: I, items: I,
} }
@ -467,7 +467,7 @@ impl<'a, I: Iterator<Item=Item<'a>> + Clone> DelayedFormat<I> {
pub fn new_with_offset<Off>(date: Option<NaiveDate>, time: Option<NaiveTime>, pub fn new_with_offset<Off>(date: Option<NaiveDate>, time: Option<NaiveTime>,
offset: &Off, items: I) -> DelayedFormat<I> offset: &Off, items: I) -> DelayedFormat<I>
where Off: Offset + fmt::Display { 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 } DelayedFormat { date: date, time: time, off: Some(name_and_diff), items: items }
} }
} }

View File

@ -604,20 +604,13 @@ impl Parsed {
let nanosecond = self.nanosecond.unwrap_or(0); let nanosecond = self.nanosecond.unwrap_or(0);
let dt = NaiveDateTime::from_timestamp_opt(timestamp, nanosecond); let dt = NaiveDateTime::from_timestamp_opt(timestamp, nanosecond);
let dt = try!(dt.ok_or(OUT_OF_RANGE)); let dt = try!(dt.ok_or(OUT_OF_RANGE));
guessed_offset = tz.offset_from_utc_datetime(&dt).fix().local_minus_utc();
// 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`. // checks if the given `DateTime` has a consistent `Offset` with given `self.offset`.
let check_offset = |dt: &DateTime<Tz>| { let check_offset = |dt: &DateTime<Tz>| {
if let Some(offset) = self.offset { if let Some(offset) = self.offset {
let delta = dt.offset().local_minus_utc().num_seconds(); dt.offset().fix().local_minus_utc() == offset
// if `delta` does not fit in `i32`, it cannot equal to `self.offset` anyway.
delta.to_i32() == Some(offset)
} else { } else {
true true
} }

View File

@ -177,7 +177,7 @@
//! assert_eq!(dt.num_days_from_ce(), 735565); // the number of days from and including Jan 1, 1 //! assert_eq!(dt.num_days_from_ce(), 735565); // the number of days from and including Jan 1, 1
//! //!
//! // time zone accessor and manipulation //! // 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.timezone(), FixedOffset::east(9 * 3600));
//! assert_eq!(dt.with_timezone(&UTC), UTC.ymd(2014, 11, 28).and_hms_nano(12, 45, 59, 324310806)); //! assert_eq!(dt.with_timezone(&UTC), UTC.ymd(2014, 11, 28).and_hms_nano(12, 45, 59, 324310806));
//! //!

View File

@ -3,12 +3,16 @@
//! The time zone which has a fixed offset from UTC. //! The time zone which has a fixed offset from UTC.
use std::ops::{Add, Sub};
use std::fmt; use std::fmt;
use oldtime::Duration as OldDuration; use oldtime::Duration as OldDuration;
use Timelike;
use div::div_mod_floor; use div::div_mod_floor;
use naive::time::NaiveTime;
use naive::date::NaiveDate; use naive::date::NaiveDate;
use naive::datetime::NaiveDateTime; use naive::datetime::NaiveDateTime;
use datetime::DateTime;
use super::{TimeZone, Offset, LocalResult}; use super::{TimeZone, Offset, LocalResult};
/// The time zone with fixed offset, from UTC-23:59:59 to UTC+23:59:59. /// The time zone with fixed offset, from UTC-23:59:59 to UTC+23:59:59.
@ -82,6 +86,16 @@ impl FixedOffset {
None 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 { impl TimeZone for FixedOffset {
@ -101,7 +115,7 @@ impl TimeZone for FixedOffset {
} }
impl Offset 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 { 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) } 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<T>(lhs: &T, rhs: i32) -> T
where T: Timelike + Add<OldDuration, Output=T>
{
// 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<FixedOffset> for NaiveTime {
type Output = NaiveTime;
#[inline]
fn add(self, rhs: FixedOffset) -> NaiveTime {
add_with_leapsecond(&self, rhs.local_minus_utc)
}
}
impl Sub<FixedOffset> for NaiveTime {
type Output = NaiveTime;
#[inline]
fn sub(self, rhs: FixedOffset) -> NaiveTime {
add_with_leapsecond(&self, -rhs.local_minus_utc)
}
}
impl Add<FixedOffset> for NaiveDateTime {
type Output = NaiveDateTime;
#[inline]
fn add(self, rhs: FixedOffset) -> NaiveDateTime {
add_with_leapsecond(&self, rhs.local_minus_utc)
}
}
impl Sub<FixedOffset> for NaiveDateTime {
type Output = NaiveDateTime;
#[inline]
fn sub(self, rhs: FixedOffset) -> NaiveDateTime {
add_with_leapsecond(&self, -rhs.local_minus_utc)
}
}
impl<Tz: TimeZone> Add<FixedOffset> for DateTime<Tz> {
type Output = DateTime<Tz>;
#[inline]
fn add(self, rhs: FixedOffset) -> DateTime<Tz> {
add_with_leapsecond(&self, rhs.local_minus_utc)
}
}
impl<Tz: TimeZone> Sub<FixedOffset> for DateTime<Tz> {
type Output = DateTime<Tz>;
#[inline]
fn sub(self, rhs: FixedOffset) -> DateTime<Tz> {
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());
}
}

View File

@ -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);
}
}

View File

@ -21,31 +21,15 @@
*/ */
use std::fmt; use std::fmt;
use std::ops::Add;
use oldtime::Duration as OldDuration;
use Weekday; use Weekday;
use Timelike;
use naive::date::NaiveDate; use naive::date::NaiveDate;
use naive::time::NaiveTime; use naive::time::NaiveTime;
use naive::datetime::NaiveDateTime; use naive::datetime::NaiveDateTime;
use date::Date; use date::Date;
use datetime::DateTime; use datetime::DateTime;
use format::{parse, Parsed, ParseResult, StrftimeItems}; use format::{parse, Parsed, ParseResult, StrftimeItems};
use self::fixed::FixedOffset;
/// 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<T>(lhs: &T, rhs: &OldDuration) -> T
where T: Timelike + Add<OldDuration, Output=T>
{
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()
}
/// The conversion result from the local time to the timezone-aware datetime types. /// The conversion result from the local time to the timezone-aware datetime types.
#[derive(Clone, PartialEq, Debug)] #[derive(Clone, PartialEq, Debug)]
@ -175,8 +159,8 @@ impl<T: fmt::Debug> LocalResult<T> {
/// The offset from the local time to UTC. /// The offset from the local time to UTC.
pub trait Offset: Sized + Clone + fmt::Debug { pub trait Offset: Sized + Clone + fmt::Debug {
/// Returns the offset from UTC to the local time stored. /// Returns the fixed offset from UTC to the local time stored.
fn local_minus_utc(&self) -> OldDuration; fn fix(&self) -> FixedOffset;
} }
/// The time zone. /// The time zone.
@ -358,15 +342,15 @@ pub trait TimeZone: Sized + Clone {
/// Converts the local `NaiveDate` to the timezone-aware `Date` if possible. /// Converts the local `NaiveDate` to the timezone-aware `Date` if possible.
fn from_local_date(&self, local: &NaiveDate) -> LocalResult<Date<Self>> { fn from_local_date(&self, local: &NaiveDate) -> LocalResult<Date<Self>> {
self.offset_from_local_date(local).map(|offset| { 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. /// Converts the local `NaiveDateTime` to the timezone-aware `DateTime` if possible.
fn from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult<DateTime<Self>> { fn from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult<DateTime<Self>> {
self.offset_from_local_datetime(local).map(|offset| { self.offset_from_local_datetime(local).map(|offset| {
let utc = add_with_leapsecond(local, &-offset.local_minus_utc()); DateTime::from_utc(*local - offset.fix(), offset)
DateTime::from_utc(utc, offset)
}) })
} }

View File

@ -1,19 +1,17 @@
// This is a part of Chrono. // This is a part of Chrono.
// See README.md and LICENSE.txt for details. // 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 std::fmt;
use oldtime; use oldtime;
use oldtime::Duration as OldDuration;
use naive::date::NaiveDate; use naive::date::NaiveDate;
use naive::datetime::NaiveDateTime; use naive::datetime::NaiveDateTime;
use date::Date; use date::Date;
use datetime::DateTime; use datetime::DateTime;
use super::{TimeZone, Offset, LocalResult}; 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. /// 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). /// 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 { impl Offset for UTC {
fn local_minus_utc(&self) -> OldDuration { OldDuration::zero() } fn fix(&self) -> FixedOffset { FixedOffset::east(0) }
} }
impl fmt::Debug for UTC { impl fmt::Debug for UTC {