From 9768b289f0f78dbb7d267986a350e9d6941bb58b Mon Sep 17 00:00:00 2001 From: Kang Seonghoon Date: Thu, 22 Jun 2017 00:21:24 +0900 Subject: [PATCH] New type: `chrono::naive::IsoWeek`. This removes `Datelike::isoweekdate` in favor of `Datelike::iso_week`. The original `isoweekdate` was not named in accordance with the style guide and also used to return the day of the week which is already provided by `Datelike::weekday`. The new design should be more reasonable. Note that we initially do not implement any public constructor for `IsoWeek`. That is, the only legitimate way to get a new `IsoWeek` is from `Datelike::iso_week`. This sidesteps the issue of boundary values (for example the year number in the maximal date will overflow in the week date) while giving the same power as the original API. Partially accounts for #139. We may add additional week types as necessary---this is the beginning. --- src/date.rs | 4 +- src/datetime.rs | 4 +- src/format/mod.rs | 8 +-- src/format/parsed.rs | 5 +- src/lib.rs | 37 +++++----- src/naive/date.rs | 53 +++++++------- src/naive/datetime.rs | 6 +- src/naive/isoweek.rs | 161 ++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 219 insertions(+), 59 deletions(-) create mode 100644 src/naive/isoweek.rs diff --git a/src/date.rs b/src/date.rs index f3cad10..e02380a 100644 --- a/src/date.rs +++ b/src/date.rs @@ -10,7 +10,7 @@ use oldtime::Duration as OldDuration; use {Weekday, Datelike}; use offset::{TimeZone, UTC}; -use naive::{self, NaiveDate, NaiveTime}; +use naive::{self, NaiveDate, NaiveTime, IsoWeek}; use DateTime; use format::{Item, DelayedFormat, StrftimeItems}; @@ -278,7 +278,7 @@ impl Datelike for Date { #[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 iso_week(&self) -> IsoWeek { self.naive_local().iso_week() } #[inline] fn with_year(&self, year: i32) -> Option> { diff --git a/src/datetime.rs b/src/datetime.rs index e478ea5..319c7b8 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -12,7 +12,7 @@ use oldtime::Duration as OldDuration; use {Weekday, Timelike, Datelike}; use offset::{TimeZone, Offset, UTC, Local, FixedOffset}; -use naive::{NaiveTime, NaiveDateTime}; +use naive::{NaiveTime, NaiveDateTime, IsoWeek}; use Date; use format::{Item, Numeric, Pad, Fixed}; use format::{parse, Parsed, ParseError, ParseResult, DelayedFormat, StrftimeItems}; @@ -258,7 +258,7 @@ impl Datelike for DateTime { #[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 iso_week(&self) -> IsoWeek { self.naive_local().iso_week() } #[inline] fn with_year(&self, year: i32) -> Option> { diff --git a/src/format/mod.rs b/src/format/mod.rs index fc05fcc..29ca87d 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -353,14 +353,14 @@ pub fn format<'a, I>(w: &mut fmt::Formatter, date: Option<&NaiveDate>, time: Opt Year => (4, date.map(|d| d.year() as i64)), YearDiv100 => (2, date.map(|d| div_floor(d.year() as i64, 100))), YearMod100 => (2, date.map(|d| mod_floor(d.year() as i64, 100))), - IsoYear => (4, date.map(|d| d.isoweekdate().0 as i64)), - IsoYearDiv100 => (2, date.map(|d| div_floor(d.isoweekdate().0 as i64, 100))), - IsoYearMod100 => (2, date.map(|d| mod_floor(d.isoweekdate().0 as i64, 100))), + IsoYear => (4, date.map(|d| d.iso_week().year() as i64)), + IsoYearDiv100 => (2, date.map(|d| div_floor(d.iso_week().year() as i64, 100))), + IsoYearMod100 => (2, date.map(|d| mod_floor(d.iso_week().year() as i64, 100))), Month => (2, date.map(|d| d.month() as i64)), Day => (2, date.map(|d| d.day() as i64)), WeekFromSun => (2, date.map(|d| week_from_sun(d) as i64)), WeekFromMon => (2, date.map(|d| week_from_mon(d) as i64)), - IsoWeek => (2, date.map(|d| d.isoweekdate().1 as i64)), + IsoWeek => (2, date.map(|d| d.iso_week().week() as i64)), NumDaysFromSun => (1, date.map(|d| d.weekday().num_days_from_sunday() as i64)), WeekdayFromMon => (1, date.map(|d| d.weekday().number_from_monday() as i64)), Ordinal => (3, date.map(|d| d.ordinal() as i64)), diff --git a/src/format/parsed.rs b/src/format/parsed.rs index 00686d8..fd1b723 100644 --- a/src/format/parsed.rs +++ b/src/format/parsed.rs @@ -332,7 +332,10 @@ impl Parsed { // verify the ISO week date. let verify_isoweekdate = |date: NaiveDate| { - let (isoyear, isoweek, weekday) = date.isoweekdate(); + let week = date.iso_week(); + let isoyear = week.year(); + let isoweek = week.week(); + let weekday = date.weekday(); let (isoyear_div_100, isoyear_mod_100) = if isoyear >= 0 { let (q, r) = div_rem(isoyear, 100); (Some(q), Some(r)) diff --git a/src/lib.rs b/src/lib.rs index fc60760..a4dd502 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -362,10 +362,10 @@ extern crate serde as serdelib; pub use oldtime::Duration; #[doc(no_inline)] pub use offset::{TimeZone, Offset, LocalResult, UTC, FixedOffset, Local}; -#[doc(no_inline)] pub use naive::{NaiveDate, NaiveTime, NaiveDateTime}; -pub use date_::{Date, MIN_DATE, MAX_DATE}; -pub use datetime_::DateTime; -#[cfg(feature = "rustc-serialize")] pub use datetime_::TsSeconds; +#[doc(no_inline)] pub use naive::{NaiveDate, IsoWeek, NaiveTime, NaiveDateTime}; +pub use date::{Date, MIN_DATE, MAX_DATE}; +pub use datetime::DateTime; +#[cfg(feature = "rustc-serialize")] pub use datetime::TsSeconds; pub use format::{ParseError, ParseResult}; /// A convenience module appropriate for glob imports (`use chrono::prelude::*;`). @@ -393,24 +393,24 @@ pub mod naive { //! but can be also used for the simpler date and time handling. mod internals; + mod date; + mod isoweek; + mod time; + mod datetime; - // avoid using them directly even in the crate itself - #[path = "date.rs"] mod date_; - #[path = "time.rs"] mod time_; - #[path = "datetime.rs"] mod datetime_; - - pub use self::date_::{NaiveDate, MIN_DATE, MAX_DATE}; - pub use self::time_::NaiveTime; - pub use self::datetime_::{NaiveDateTime, TsSeconds}; + pub use self::date::{NaiveDate, MIN_DATE, MAX_DATE}; + pub use self::isoweek::IsoWeek; + pub use self::time::NaiveTime; + pub use self::datetime::{NaiveDateTime, TsSeconds}; /// Tools to help serializing/deserializing naive types. #[cfg(feature = "serde")] pub mod serde { - pub use super::datetime_::serde::*; + pub use super::datetime::serde::*; } } -#[path = "date.rs"] mod date_; -#[path = "datetime.rs"] mod datetime_; +mod date; +mod datetime; pub mod format; /// Ser/de helpers @@ -419,7 +419,7 @@ pub mod format; /// annotation](https://serde.rs/attributes.html#field-attributes). #[cfg(feature = "serde")] pub mod serde { - pub use super::datetime_::serde::*; + pub use super::datetime::serde::*; } /// The day of week. @@ -761,9 +761,8 @@ pub trait Datelike: Sized { /// Returns the day of week. fn weekday(&self) -> Weekday; - /// Returns the ISO week date: an adjusted year, week number and day of week. - /// The adjusted year may differ from that of the calendar date. - fn isoweekdate(&self) -> (i32, u32, Weekday); + /// Returns the ISO week. + fn iso_week(&self) -> IsoWeek; /// Makes a new value with the year number changed. /// diff --git a/src/naive/date.rs b/src/naive/date.rs index 8acea8e..f333413 100644 --- a/src/naive/date.rs +++ b/src/naive/date.rs @@ -10,10 +10,11 @@ use oldtime::Duration as OldDuration; use {Weekday, Datelike}; use div::div_mod_floor; -use naive::{NaiveTime, NaiveDateTime}; +use naive::{NaiveTime, NaiveDateTime, IsoWeek}; use format::{Item, Numeric, Pad}; use format::{parse, Parsed, ParseError, ParseResult, DelayedFormat, StrftimeItems}; +use super::isoweek; use super::internals::{self, DateImpl, Of, Mdf, YearFlags}; const MAX_YEAR: i32 = internals::MAX_YEAR; @@ -81,7 +82,8 @@ const MAX_BITS: usize = 44; /// For example, January 3, 2016 (Sunday) was on the last (53rd) week of 2015. /// /// Chrono's date types default to the ISO 8601 [calendar date](#calendar-date), -/// but the [`Datelike::isoweekdate`](../trait.Datelike.html#tymethod.isoweekdate) method +/// but [`Datelike::iso_week`](../trait.Datelike.html#tymethod.iso_week) and +/// [`Datelike::weekday`](../trait.Datelike.html#tymethod.weekday) methods /// can be used to get the corresponding week date. /// /// # Ordinal Date @@ -151,7 +153,9 @@ impl NaiveDate { /// assert_eq!(d.month(), 3); /// assert_eq!(d.day(), 14); /// assert_eq!(d.ordinal(), 73); // day of year - /// assert_eq!(d.isoweekdate(), (2015, 11, Weekday::Sat)); // ISO week and weekday + /// assert_eq!(d.iso_week().year(), 2015); + /// assert_eq!(d.iso_week().week(), 11); + /// assert_eq!(d.weekday(), Weekday::Sat); /// assert_eq!(d.num_days_from_ce(), 735671); // days since January 1, 1 CE /// ~~~~ pub fn from_ymd(year: i32, month: u32, day: u32) -> NaiveDate { @@ -197,7 +201,9 @@ impl NaiveDate { /// assert_eq!(d.year(), 2015); /// assert_eq!(d.month(), 3); /// assert_eq!(d.day(), 14); - /// assert_eq!(d.isoweekdate(), (2015, 11, Weekday::Sat)); // ISO week and weekday + /// assert_eq!(d.iso_week().year(), 2015); + /// assert_eq!(d.iso_week().week(), 11); + /// assert_eq!(d.weekday(), Weekday::Sat); /// assert_eq!(d.num_days_from_ce(), 735671); // days since January 1, 1 CE /// ~~~~ pub fn from_yo(year: i32, ordinal: u32) -> NaiveDate { @@ -241,7 +247,9 @@ impl NaiveDate { /// use chrono::{NaiveDate, Datelike, Weekday}; /// /// let d = NaiveDate::from_isoywd(2015, 11, Weekday::Sat); - /// assert_eq!(d.isoweekdate(), (2015, 11, Weekday::Sat)); + /// assert_eq!(d.iso_week().year(), 2015); + /// assert_eq!(d.iso_week().week(), 11); + /// assert_eq!(d.weekday(), Weekday::Sat); /// assert_eq!(d.year(), 2015); /// assert_eq!(d.month(), 3); /// assert_eq!(d.day(), 14); @@ -338,7 +346,9 @@ impl NaiveDate { /// assert_eq!(d.month(), 3); /// assert_eq!(d.day(), 14); /// assert_eq!(d.ordinal(), 73); // day of year - /// assert_eq!(d.isoweekdate(), (2015, 11, Weekday::Sat)); // ISO week and weekday + /// assert_eq!(d.iso_week().year(), 2015); + /// assert_eq!(d.iso_week().week(), 11); + /// assert_eq!(d.weekday(), Weekday::Sat); /// ~~~~ /// /// While not directly supported by Chrono, @@ -1128,21 +1138,9 @@ impl Datelike for NaiveDate { self.of().weekday() } - fn isoweekdate(&self) -> (i32, u32, Weekday) { - let of = self.of(); - let year = self.year(); - let (rawweek, weekday) = of.isoweekdate_raw(); - if rawweek < 1 { // previous year - let prevlastweek = YearFlags::from_year(year - 1).nisoweeks(); - (year - 1, prevlastweek, weekday) - } else { - let lastweek = of.flags().nisoweeks(); - if rawweek > lastweek { // next year - (year + 1, 1, weekday) - } else { - (year, rawweek, weekday) - } - } + #[inline] + fn iso_week(&self) -> IsoWeek { + isoweek::iso_week_from_yof(self.year(), self.of()) } /// Makes a new `NaiveDate` with the year number changed. @@ -1720,7 +1718,7 @@ mod tests { } #[test] - fn test_date_from_isoywd_and_isoweekdate() { + fn test_date_from_isoywd_and_iso_week() { for year in 2000..2401 { for week in 1..54 { for &weekday in [Weekday::Mon, Weekday::Tue, Weekday::Wed, Weekday::Thu, @@ -1729,10 +1727,9 @@ mod tests { if d.is_some() { let d = d.unwrap(); assert_eq!(d.weekday(), weekday); - let (year_, week_, weekday_) = d.isoweekdate(); - assert_eq!(year_, year); - assert_eq!(week_, week); - assert_eq!(weekday_, weekday); + let w = d.iso_week(); + assert_eq!(w.year(), year); + assert_eq!(w.week(), week); } } } @@ -1744,8 +1741,8 @@ mod tests { let d = NaiveDate::from_ymd_opt(year, month, day); if d.is_some() { let d = d.unwrap(); - let (year_, week_, weekday_) = d.isoweekdate(); - let d_ = NaiveDate::from_isoywd(year_, week_, weekday_); + let w = d.iso_week(); + let d_ = NaiveDate::from_isoywd(w.year(), w.week(), d.weekday()); assert_eq!(d, d_); } } diff --git a/src/naive/datetime.rs b/src/naive/datetime.rs index bd13edd..4fc0a4b 100644 --- a/src/naive/datetime.rs +++ b/src/naive/datetime.rs @@ -10,7 +10,7 @@ use oldtime::Duration as OldDuration; use {Weekday, Timelike, Datelike}; use div::div_mod_floor; -use naive::{NaiveTime, NaiveDate}; +use naive::{NaiveTime, NaiveDate, IsoWeek}; use format::{Item, Numeric, Pad, Fixed}; use format::{parse, Parsed, ParseError, ParseResult, DelayedFormat, StrftimeItems}; @@ -781,8 +781,8 @@ impl Datelike for NaiveDateTime { } #[inline] - fn isoweekdate(&self) -> (i32, u32, Weekday) { - self.date.isoweekdate() + fn iso_week(&self) -> IsoWeek { + self.date.iso_week() } /// Makes a new `NaiveDateTime` with the year number changed. diff --git a/src/naive/isoweek.rs b/src/naive/isoweek.rs new file mode 100644 index 0000000..3587bed --- /dev/null +++ b/src/naive/isoweek.rs @@ -0,0 +1,161 @@ +// This is a part of Chrono. +// See README.md and LICENSE.txt for details. + +//! ISO 8601 week. + +use std::fmt; + +use super::internals::{DateImpl, Of, YearFlags}; + +/// ISO 8601 week. +/// +/// This type, combined with [`Weekday`](../enum.Weekday.html), +/// constitues the ISO 8601 [week date](./struct.NaiveDate.html#week-date). +/// One can retrieve this type from the existing [`Datelike`](../trait.Datelike.html) types +/// via the [`Datelike::iso_week`](../trait.Datelike.html#tymethod.iso_week) method. +#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone)] +pub struct IsoWeek { + // note that this allows for larger year range than `NaiveDate`. + // this is crucial because we have an edge case for the first and last week supported, + // which year number might not match the calendar year number. + ywf: DateImpl, // (year << 10) | (week << 4) | flag +} + +/// Returns the corresponding `IsoWeek` from the year and the `Of` internal value. +// +// internal use only. we don't expose the public constructor for `IsoWeek` for now, +// because the year range for the week date and the calendar date do not match and +// it is confusing to have a date that is out of range in one and not in another. +// currently we sidestep this issue by making `IsoWeek` fully dependent of `Datelike`. +pub fn iso_week_from_yof(year: i32, of: Of) -> IsoWeek { + let (rawweek, _) = of.isoweekdate_raw(); + let (year, week) = if rawweek < 1 { // previous year + let prevlastweek = YearFlags::from_year(year - 1).nisoweeks(); + (year - 1, prevlastweek) + } else { + let lastweek = of.flags().nisoweeks(); + if rawweek > lastweek { // next year + (year + 1, 1) + } else { + (year, rawweek) + } + }; + IsoWeek { ywf: (year << 10) | (week << 4) as DateImpl | of.flags().0 as DateImpl } +} + +impl IsoWeek { + /// Returns the year number for this ISO week. + /// + /// # Example + /// + /// ~~~~ + /// use chrono::{NaiveDate, Datelike, Weekday}; + /// + /// let d = NaiveDate::from_isoywd(2015, 1, Weekday::Mon); + /// assert_eq!(d.iso_week().year(), 2015); + /// ~~~~ + /// + /// This year number might not match the calendar year number. + /// Continuing the example... + /// + /// ~~~~ + /// # use chrono::{NaiveDate, Datelike, Weekday}; + /// # let d = NaiveDate::from_isoywd(2015, 1, Weekday::Mon); + /// assert_eq!(d.year(), 2014); + /// assert_eq!(d, NaiveDate::from_ymd(2014, 12, 29)); + /// ~~~~ + #[inline] + pub fn year(&self) -> i32 { + self.ywf >> 10 + } + + /// Returns the ISO week number starting from 1. + /// + /// The return value ranges from 1 to 53. (The last week of year differs by years.) + /// + /// # Example + /// + /// ~~~~ + /// use chrono::{NaiveDate, Datelike, Weekday}; + /// + /// let d = NaiveDate::from_isoywd(2015, 15, Weekday::Mon); + /// assert_eq!(d.iso_week().week(), 15); + /// ~~~~ + #[inline] + pub fn week(&self) -> u32 { + ((self.ywf >> 4) & 0x3f) as u32 + } + + /// Returns the ISO week number starting from 0. + /// + /// The return value ranges from 0 to 52. (The last week of year differs by years.) + /// + /// # Example + /// + /// ~~~~ + /// use chrono::{NaiveDate, Datelike, Weekday}; + /// + /// let d = NaiveDate::from_isoywd(2015, 15, Weekday::Mon); + /// assert_eq!(d.iso_week().week0(), 14); + /// ~~~~ + #[inline] + pub fn week0(&self) -> u32 { + ((self.ywf >> 4) & 0x3f) as u32 - 1 + } +} + +/// The `Debug` output of the ISO week `w` is same to +/// [`d.format("%G-W%V")`](../format/strftime/index.html) +/// where `d` is any `NaiveDate` value in that week. +/// +/// # Example +/// +/// ~~~~ +/// use chrono::{NaiveDate, Datelike}; +/// +/// assert_eq!(format!("{:?}", NaiveDate::from_ymd(2015, 9, 5).iso_week()), "2015-W36"); +/// assert_eq!(format!("{:?}", NaiveDate::from_ymd( 0, 1, 3).iso_week()), "0000-W01"); +/// assert_eq!(format!("{:?}", NaiveDate::from_ymd(9999, 12, 31).iso_week()), "9999-W52"); +/// ~~~~ +/// +/// ISO 8601 requires an explicit sign for years before 1 BCE or after 9999 CE. +/// +/// ~~~~ +/// # use chrono::{NaiveDate, Datelike}; +/// assert_eq!(format!("{:?}", NaiveDate::from_ymd( 0, 1, 2).iso_week()), "-0001-W52"); +/// assert_eq!(format!("{:?}", NaiveDate::from_ymd(10000, 12, 31).iso_week()), "+10000-W52"); +/// ~~~~ +impl fmt::Debug for IsoWeek { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let year = self.year(); + let week = self.week(); + if 0 <= year && year <= 9999 { + write!(f, "{:04}-W{:02}", year, week) + } else { + // ISO 8601 requires the explicit sign for out-of-range years + write!(f, "{:+05}-W{:02}", year, week) + } + } +} + +#[cfg(test)] +mod tests { + use naive::{internals, MIN_DATE, MAX_DATE}; + use Datelike; + + #[test] + fn test_iso_week_extremes() { + let minweek = MIN_DATE.iso_week(); + let maxweek = MAX_DATE.iso_week(); + + assert_eq!(minweek.year(), internals::MIN_YEAR); + assert_eq!(minweek.week(), 1); + assert_eq!(minweek.week0(), 0); + assert_eq!(format!("{:?}", minweek), MIN_DATE.format("%G-W%V").to_string()); + + assert_eq!(maxweek.year(), internals::MAX_YEAR + 1); + assert_eq!(maxweek.week(), 1); + assert_eq!(maxweek.week0(), 0); + assert_eq!(format!("{:?}", maxweek), MAX_DATE.format("%G-W%V").to_string()); + } +}