diff --git a/CHANGELOG.md b/CHANGELOG.md index 030158b9..360703c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,13 +12,13 @@ Versions with only mechanical changes will be omitted from the following list. ### Features -* Added day and week iterators for `NaiveDate` (@gnzlbg & @robyoung) +* Add day and week iterators for `NaiveDate` (@gnzlbg & @robyoung) +* Add a `Month` enum (@hhamana) ### Improvements * Added MIN and MAX values for `NaiveTime`, `NaiveDateTime` and `DateTime`. - ## 0.4.13 ### Features diff --git a/src/format/mod.rs b/src/format/mod.rs index b634b094..a1f197ec 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -28,15 +28,14 @@ use core::str::FromStr; #[cfg(any(feature = "std", test))] use std::error::Error; -#[cfg(any(feature = "alloc", feature = "std", test))] use div::{div_floor, mod_floor}; #[cfg(any(feature = "alloc", feature = "std", test))] use naive::{NaiveDate, NaiveTime}; #[cfg(any(feature = "alloc", feature = "std", test))] use offset::{FixedOffset, Offset}; #[cfg(any(feature = "alloc", feature = "std", test))] -use {Datelike, Timelike}; -use {ParseWeekdayError, Weekday}; +use {Datelike, Month, Timelike, Weekday}; +use {ParseMonthError, ParseWeekdayError}; pub use self::parse::parse; pub use self::parsed::Parsed; @@ -759,3 +758,54 @@ impl FromStr for Weekday { } } } + +/// Parsing a `str` into a `Month` uses the format [`%W`](./format/strftime/index.html). +/// +/// # Example +/// +/// ~~~~ +/// use chrono::Month; +/// +/// assert_eq!("January".parse::(), Ok(Month::January)); +/// assert!("any day".parse::().is_err()); +/// ~~~~ +/// +/// The parsing is case-insensitive. +/// +/// ~~~~ +/// # use chrono::Month; +/// assert_eq!("fEbruARy".parse::(), Ok(Month::February)); +/// ~~~~ +/// +/// Only the shortest form (e.g. `jan`) and the longest form (e.g. `january`) is accepted. +/// +/// ~~~~ +/// # use chrono::Month; +/// assert!("septem".parse::().is_err()); +/// assert!("Augustin".parse::().is_err()); +/// ~~~~ +impl FromStr for Month { + type Err = ParseMonthError; + + fn from_str(s: &str) -> Result { + if let Ok(("", w)) = scan::short_or_long_month0(s) { + match w { + 0 => Ok(Month::January), + 1 => Ok(Month::February), + 2 => Ok(Month::March), + 3 => Ok(Month::April), + 4 => Ok(Month::May), + 5 => Ok(Month::June), + 6 => Ok(Month::July), + 7 => Ok(Month::August), + 8 => Ok(Month::September), + 9 => Ok(Month::October), + 10 => Ok(Month::November), + 11 => Ok(Month::December), + _ => Err(ParseMonthError { _dummy: () }), + } + } else { + Err(ParseMonthError { _dummy: () }) + } + } +} diff --git a/src/lib.rs b/src/lib.rs index f09d682d..fe59547c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -892,6 +892,286 @@ mod weekday_serde { } } +/// The month of the year. +/// +/// This enum is just a convenience implementation. +/// The month in dates created by DateLike objects does not return this enum. +/// +/// It is possible to convert from a date to a month independently +/// ``` +/// use num_traits::FromPrimitive; +/// let date = Utc.ymd(2019, 10, 28).and_hms(9, 10, 11); +/// // `2019-10-28T09:10:11Z` +/// let month = Month::from_u32((date.month()); +/// assert_eq!(month, Some(Month::October)) +/// ``` +/// Or from a Month to an integer usable by dates +/// ``` +/// let month = Month::January; +/// let dt = Utc.ymd(2019, month.number_from_month(), 28).and_hms(9, 10, 11); +/// assert_eq!((dt.year(), dt.month(), dt.day()), (2019, 1, 28)); +/// ``` +/// Allows mapping from and to month, from 1-January to 12-December. +/// Can be Serialized/Deserialized with serde +// Actual implementation is zero-indexed, API intended as 1-indexed for more intuitive behavior. +#[derive(PartialEq, Eq, Copy, Clone, Debug, Hash)] +#[cfg_attr(feature = "rustc-serialize", derive(RustcEncodable, RustcDecodable))] +pub enum Month { + /// January + January = 0, + /// February + February = 1, + /// March + March = 2, + /// April + April = 3, + /// May + May = 4, + /// June + June = 5, + /// July + July = 6, + /// August + August = 7, + /// September + September = 8, + /// October + October = 9, + /// November + November = 10, + /// December + December = 11 +} + + +impl Month { + /// The next month. + /// + /// `m`: | `January` | `February` | `...` | `December` + /// ----------- | --------- | ---------- | --- | --------- + /// `m.succ()`: | `February` | `March` | `...` | `January` + #[inline] + pub fn succ(&self) -> Month { + match *self { + Month::January => Month::February, + Month::February => Month::March, + Month::March => Month::April, + Month::April => Month::May, + Month::May => Month::June, + Month::June => Month::July, + Month::July => Month::August, + Month::August => Month::September, + Month::September => Month::October, + Month::October => Month::November, + Month::November => Month::December, + Month::December => Month::January, + } + } + + /// The previous month. + /// + /// `m`: | `January` | `February` | `...` | `December` + /// ----------- | --------- | ---------- | --- | --------- + /// `m.succ()`: | `December` | `January` | `...` | `November` + #[inline] + pub fn pred(&self) -> Month { + match *self { + Month::January => Month::December, + Month::February => Month::January, + Month::March => Month::February, + Month::April => Month::March, + Month::May => Month::April, + Month::June => Month::May, + Month::July => Month::June, + Month::August => Month::July, + Month::September => Month::August, + Month::October => Month::September, + Month::November => Month::October, + Month::December => Month::November, + } + } + + /// Returns a month-of-year number starting from January = 1. + /// + /// `m`: | `January` | `February` | `...` | `December` + /// -------------------------| --------- | ---------- | --- | ----- + /// `m.number_from_month()`: | 1 | 2 | `...` | 12 + #[inline] + pub fn number_from_month(&self) -> u32 { + match *self { + Month::January => 1, + Month::February => 2, + Month::March => 3, + Month::April => 4, + Month::May => 5, + Month::June => 6, + Month::July => 7, + Month::August => 8, + Month::September => 9, + Month::October => 10, + Month::November => 11, + Month::December => 12, + } + } +} + +impl num_traits::FromPrimitive for Month { + /// Returns an Option from a i64, assuming a 1-index, January = 1. + /// + /// `Month::from_i64(n: i64)`: | `1` | `2` | ... | `12` + /// ---------------------------| -------------------- | --------------------- | ... | ----- + /// ``: | Some(Month::January) | Some(Month::February) | ... | Some(Month::December) + + #[inline] + fn from_u64(n: u64) -> Option { + Self::from_u32(n as u32) + } + + #[inline] + fn from_i64(n: i64) -> Option { + Self::from_u32(n as u32) + } + + #[inline] + fn from_u32(n: u32) -> Option { + match n { + 1 => Some(Month::January), + 2 => Some(Month::February), + 3 => Some(Month::March), + 4 => Some(Month::April), + 5 => Some(Month::May), + 6 => Some(Month::June), + 7 => Some(Month::July), + 8 => Some(Month::August), + 9 => Some(Month::September), + 10 => Some(Month::October), + 11 => Some(Month::November), + 12 => Some(Month::December), + _ => None, + } + } +} + +/// An error resulting from reading `` value with `FromStr`. +#[derive(Clone, PartialEq)] +pub struct ParseMonthError { + _dummy: (), +} + +impl fmt::Debug for ParseMonthError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "ParseMonthError {{ .. }}") + } +} +#[cfg(feature = "serde")] +mod month_serde { + use super::Month; + use std::fmt; + use serdelib::{ser, de}; + + impl ser::Serialize for Month { + fn serialize(&self, serializer: S) -> Result + where S: ser::Serializer + { + serializer.serialize_str(&format!("{:?}", self)) + } + } + + struct MonthVisitor; + + impl<'de> de::Visitor<'de> for MonthVisitor { + type Value = Month; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Month") + } + + fn visit_str(self, value: &str) -> Result + where E: de::Error + { + value.parse().map_err(|_| E::custom("short (3-letter) or full month names expected")) + } + } + + impl<'de> de::Deserialize<'de> for Month { + fn deserialize(deserializer: D) -> Result + where D: de::Deserializer<'de> + { + deserializer.deserialize_str(MonthVisitor) + } + } + + #[cfg(test)] + extern crate serde_json; + + #[test] + fn test_serde_serialize() { + use self::serde_json::to_string; + use Month::*; + + let cases: Vec<(Month, &str)> = vec![ + (January, "\"January\""), + (February, "\"February\""), + (March, "\"March\""), + (April, "\"April\""), + (May, "\"May\""), + (June, "\"June\""), + (July, "\"July\""), + (August, "\"August\""), + (September, "\"September\""), + (October, "\"October\""), + (November, "\"November\""), + (December, "\"December\""), + ]; + + for (month, expected_str) in cases { + let string = to_string(&month).unwrap(); + assert_eq!(string, expected_str); + } + } + + #[test] + fn test_serde_deserialize() { + use self::serde_json::from_str; + use Month::*; + + let cases: Vec<(&str, Month)> = vec![ + ("\"january\"", January), + ("\"jan\"", January), + ("\"FeB\"", February), + ("\"MAR\"", March), + ("\"mar\"", March), + ("\"april\"", April), + ("\"may\"", May), + ("\"june\"", June), + ("\"JULY\"", July), + ("\"august\"", August), + ("\"september\"", September), + ("\"October\"", October), + ("\"November\"", November), + ("\"DECEmbEr\"", December), + ]; + + for (string, expected_month) in cases { + let month = from_str::(string).unwrap(); + assert_eq!(month, expected_month); + } + + let errors: Vec<&str> = vec![ + "\"not a month\"", + "\"ja\"", + "\"Dece\"", + "Dec", + "\"Augustin\"" + ]; + + for string in errors { + from_str::(string).unwrap_err(); + } + } +} + + /// The common set of methods for date component. pub trait Datelike: Sized { /// Returns the year number in the [calendar date](./naive/struct.NaiveDate.html#calendar-date). @@ -1072,30 +1352,57 @@ pub trait Timelike: Sized { #[cfg(test)] extern crate num_iter; -#[test] -fn test_readme_doomsday() { - use num_iter::range_inclusive; +mod test { + #[allow(unused_imports)] + use super::*; - for y in range_inclusive(naive::MIN_DATE.year(), naive::MAX_DATE.year()) { - // even months - let d4 = NaiveDate::from_ymd(y, 4, 4); - let d6 = NaiveDate::from_ymd(y, 6, 6); - let d8 = NaiveDate::from_ymd(y, 8, 8); - let d10 = NaiveDate::from_ymd(y, 10, 10); - let d12 = NaiveDate::from_ymd(y, 12, 12); + #[test] + fn test_readme_doomsday() { + use num_iter::range_inclusive; - // nine to five, seven-eleven - let d59 = NaiveDate::from_ymd(y, 5, 9); - let d95 = NaiveDate::from_ymd(y, 9, 5); - let d711 = NaiveDate::from_ymd(y, 7, 11); - let d117 = NaiveDate::from_ymd(y, 11, 7); + for y in range_inclusive(naive::MIN_DATE.year(), naive::MAX_DATE.year()) { + // even months + let d4 = NaiveDate::from_ymd(y, 4, 4); + let d6 = NaiveDate::from_ymd(y, 6, 6); + let d8 = NaiveDate::from_ymd(y, 8, 8); + let d10 = NaiveDate::from_ymd(y, 10, 10); + let d12 = NaiveDate::from_ymd(y, 12, 12); - // "March 0" - let d30 = NaiveDate::from_ymd(y, 3, 1).pred(); + // nine to five, seven-eleven + let d59 = NaiveDate::from_ymd(y, 5, 9); + let d95 = NaiveDate::from_ymd(y, 9, 5); + let d711 = NaiveDate::from_ymd(y, 7, 11); + let d117 = NaiveDate::from_ymd(y, 11, 7); - let weekday = d30.weekday(); - let other_dates = [d4, d6, d8, d10, d12, d59, d95, d711, d117]; - assert!(other_dates.iter().all(|d| d.weekday() == weekday)); + // "March 0" + let d30 = NaiveDate::from_ymd(y, 3, 1).pred(); + + let weekday = d30.weekday(); + let other_dates = [d4, d6, d8, d10, d12, d59, d95, d711, d117]; + assert!(other_dates.iter().all(|d| d.weekday() == weekday)); + } + } + + + #[test] + fn test_month_enum_primitive_parse() { + use num_traits::FromPrimitive; + + let jan_opt = Month::from_u32(1); + let feb_opt = Month::from_u64(2); + let dec_opt = Month::from_i64(12); + let no_month = Month::from_u32(13); + assert_eq!(jan_opt, Some(Month::January)); + assert_eq!(feb_opt, Some(Month::February)); + assert_eq!(dec_opt, Some(Month::December)); + assert_eq!(no_month, None); + + let date = Utc.ymd(2019, 10, 28).and_hms(9, 10, 11); + assert_eq!(Month::from_u32(date.month()), Some(Month::October)); + + let month = Month::January; + let dt = Utc.ymd(2019, month.number_from_month(), 28).and_hms(9, 10, 11); + assert_eq!((dt.year(), dt.month(), dt.day()), (2019, 1, 28)); } } @@ -1149,4 +1456,12 @@ fn test_num_days_from_ce_against_alternative_impl() { let mid_year = jan1_year + Duration::days(133); assert_eq!(mid_year.num_days_from_ce(), num_days_from_ce(&mid_year), "on {:?}", mid_year); } + + #[test] + fn test_month_enum_succ_pred() { + assert_eq!(Month::January.succ(), Month::February); + assert_eq!(Month::December.succ(), Month::January); + assert_eq!(Month::January.pred(), Month::December); + assert_eq!(Month::February.pred(), Month::January); + } }