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()); + } +}