Repository: time-rs/time (language: rust)

Issue:
Month conversion to/from integral numbers
**Preamble**

As of version 0.3.0, the `Month` enum is a complete black box. It has no safe way of converting to/from numbers, and it even intentionally doesn't implement `Ord` or `PartialOrd`.

In my experience, there are valid use cases where you want those features. Here's one example:

```rust
pub fn age_at(date: Date, birthday: Date) -> Year {
    let (year, month, day) = date.to_calendar_date();
    let (year_b, month_b, day_b) = birthday.to_calendar_date();

    if (month_b.ordinal(), day_b) <= (month.ordinal(), day) {
        year - year_b
    } else {
        year - year_b - 1
    }
}
```

Another use case is date arithmetic. I realize that "date + 10 months" is a giant can of worms, but sometimes you have no choice but to crack that can open.

**Proposal**

How about `impl From<Month> for u8` and `impl TryFrom<u8> for Month`? I feel like these are fairly reasonable things to add to the library. The error type for the latter would be `time::error::ComponentRange`.


Discussion / hints:
Going _to_ a number can already be done — `month as u8`. Months have their value explicitly assigned, so you'll get the value you expect. For getting the month from the number, I could certainly expose that. The reason I didn't initially is likely because I needed it to be `const`, and const trait impls aren't a thing yet. There is a `Month::from_number(NonZeroU8) -> Result<Month, error::ComponentRange>` that exists internally, though.

If you're looking to get "date + 10 months", that's planned as a `Period` type in the future. I intend on having it in the 0.3 series.
> Going _to_ a number can already be done — `month as u8`.

That's why I wrote "no _safe_ way" to get there. `as` is a ticking footgun, and while `#[repr(u8)]` shows up in the docs, it's unclear if it's 1-based unless you look at the source.

Glad to hear that `Period` is still on track to be added!

Fix patch:
diff --git a/src/month.rs b/src/month.rs
index 88c1c0648..ca9b49628 100644
--- a/src/month.rs
+++ b/src/month.rs
@@ -1,11 +1,10 @@
 //! The `Month` enum and its associated `impl`s.
 
+use core::convert::TryFrom;
 use core::fmt;
-#[cfg(feature = "parsing")]
 use core::num::NonZeroU8;
 
 use self::Month::*;
-#[cfg(feature = "parsing")]
 use crate::error;
 
 /// Months of the year.
@@ -29,7 +28,6 @@ pub enum Month {
 
 impl Month {
     /// Create a `Month` from its numerical value.
-    #[cfg(feature = "parsing")]
     pub(crate) const fn from_number(n: NonZeroU8) -> Result<Self, error::ComponentRange> {
         match n.get() {
             1 => Ok(January),
@@ -119,3 +117,26 @@ impl fmt::Display for Month {
         })
     }
 }
+
+impl From<Month> for u8 {
+    fn from(month: Month) -> Self {
+        month as _
+    }
+}
+
+impl TryFrom<u8> for Month {
+    type Error = error::ComponentRange;
+
+    fn try_from(value: u8) -> Result<Self, Self::Error> {
+        match NonZeroU8::new(value) {
+            Some(value) => Self::from_number(value),
+            None => Err(error::ComponentRange {
+                name: "month",
+                minimum: 1,
+                maximum: 12,
+                value: 0,
+                conditional_range: false,
+            }),
+        }
+    }
+}


Test patch:
diff --git a/src/tests.rs b/src/tests.rs
index 1f512d3d0..885e8debf 100644
--- a/src/tests.rs
+++ b/src/tests.rs
@@ -9,11 +9,9 @@
 //! This module should only be used when it is not possible to test the implementation in a
 //! reasonable manner externally.
 
-use std::num::NonZeroU8;
-
+use crate::duration;
 use crate::format_description::modifier::Modifiers;
 use crate::formatting::DigitCount;
-use crate::{duration, Month};
 
 #[test]
 fn digit_count() {
@@ -54,29 +52,6 @@ fn digit_count() {
     assert_eq!(1_000_000_000_u32.num_digits(), 10);
 }
 
-#[test]
-fn month_from_number() {
-    macro_rules! nz {
-        ($x:literal) => {
-            NonZeroU8::new($x).unwrap()
-        };
-    }
-
-    assert_eq!(Month::from_number(nz!(1)), Ok(Month::January));
-    assert_eq!(Month::from_number(nz!(2)), Ok(Month::February));
-    assert_eq!(Month::from_number(nz!(3)), Ok(Month::March));
-    assert_eq!(Month::from_number(nz!(4)), Ok(Month::April));
-    assert_eq!(Month::from_number(nz!(5)), Ok(Month::May));
-    assert_eq!(Month::from_number(nz!(6)), Ok(Month::June));
-    assert_eq!(Month::from_number(nz!(7)), Ok(Month::July));
-    assert_eq!(Month::from_number(nz!(8)), Ok(Month::August));
-    assert_eq!(Month::from_number(nz!(9)), Ok(Month::September));
-    assert_eq!(Month::from_number(nz!(10)), Ok(Month::October));
-    assert_eq!(Month::from_number(nz!(11)), Ok(Month::November));
-    assert_eq!(Month::from_number(nz!(12)), Ok(Month::December));
-    assert!(Month::from_number(nz!(13)).is_err());
-}
-
 #[test]
 fn default() {
     assert_eq!(
diff --git a/tests/integration/month.rs b/tests/integration/month.rs
index e0399b590..9e03f5ce1 100644
--- a/tests/integration/month.rs
+++ b/tests/integration/month.rs
@@ -1,4 +1,6 @@
-use time::Month::*;
+use std::convert::TryFrom;
+
+use time::Month::{self, *};
 
 #[test]
 fn previous() {
@@ -47,3 +49,37 @@ fn display() {
     assert_eq!(November.to_string(), "November");
     assert_eq!(December.to_string(), "December");
 }
+
+#[test]
+fn to_u8() {
+    assert_eq!(u8::from(January), 1);
+    assert_eq!(u8::from(February), 2);
+    assert_eq!(u8::from(March), 3);
+    assert_eq!(u8::from(April), 4);
+    assert_eq!(u8::from(May), 5);
+    assert_eq!(u8::from(June), 6);
+    assert_eq!(u8::from(July), 7);
+    assert_eq!(u8::from(August), 8);
+    assert_eq!(u8::from(September), 9);
+    assert_eq!(u8::from(October), 10);
+    assert_eq!(u8::from(November), 11);
+    assert_eq!(u8::from(December), 12);
+}
+
+#[test]
+fn try_from_u8() {
+    assert!(matches!(Month::try_from(0), Err(err) if err.name() == "month"));
+    assert_eq!(Month::try_from(1), Ok(January));
+    assert_eq!(Month::try_from(2), Ok(February));
+    assert_eq!(Month::try_from(3), Ok(March));
+    assert_eq!(Month::try_from(4), Ok(April));
+    assert_eq!(Month::try_from(5), Ok(May));
+    assert_eq!(Month::try_from(6), Ok(June));
+    assert_eq!(Month::try_from(7), Ok(July));
+    assert_eq!(Month::try_from(8), Ok(August));
+    assert_eq!(Month::try_from(9), Ok(September));
+    assert_eq!(Month::try_from(10), Ok(October));
+    assert_eq!(Month::try_from(11), Ok(November));
+    assert_eq!(Month::try_from(12), Ok(December));
+    assert!(matches!(Month::try_from(13), Err(err) if err.name() == "month"));
+}