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

Issue:
Advanced date arithmetic functionality
First of all, thank you for maintaining a crate that has proven very useful many times in my projects.

However, I think what this crate lacks is more advanced and high-level date arithmetic that can make writing code related to time calculations easier.

# Examples
- advance several months using one method or something simpler in general instead of having to manually calculate the target month and year
- calculate the first/nth occurence of weekday for a given month
- get the next/previous/nth weekday relative to a given point in time
- being able to get the time when a day/month/year starts (so basically rounding to given time units, although I see this has been mentioned in #524).

I believe this would be a great addition for this crate because operations on dates can be quite daunting because of all the inconsistencies in time units, like month lengths and leap years.

Also, this idea is quite general and would make for a very large contribution, so if you are interested, I could elaborate on some arithmetic features more specifically.

If this sounds like a good idea, I could try to contribute to it.


Discussion / hints:
Overall, this is something I absolutely want to support.

> advance several months using one method or something simpler in general instead of having to manually calculate the target month and year

Unquestionably useful. The current problem is that `Duration` is represented as seconds and nanosecond and months vary in length, preventing their representation. My intent is to introduce another type that is more general, though I'm not yet set on the name.

> calculate the first/nth occurence of weekday for a given month

Interestingly, this is something I have ported from C not too long ago. I haven't pushed it to any repository, but the code already exists!

> get the next/previous/nth weekday relative to a given point in time

Definitely doable, and shouldn't involve a ton of effort, relatively speaking.

> being able to get the time when a day/month/year starts (so basically rounding to given time units

This can be done now using the various `replace_X` methods. The linked PR is quite a bit different than what you're saying.

---

If you are interested in working on this, I'd start with the next/previous/nth weekday. That should serve as a decent introduction to the repository and would be uncontroversial. The API would be something like

```rust
impl Date {
    fn next_occurrence(self, weekday: Weekday) -> Self;
    fn prev_occurrence(self, weekday: Weekday) -> Self;
    fn nth_next_occurrence(self, weekday: Weekday, n: u8) -> Self;
    fn nth_prev_occurrence(self, weekday: Weekday, n: u8) -> Self;
}
```

Not sure on the order of `weekday` and `n` for the last two.

Let me know if you'd like any assistance with this!
That's awesome! I'm going to work on my pr, and also inttroduce several other ideas:
- `Weekday` has a `nth_next` method. Why not introduce it to `Month`, too?
- Get a number of occurences of a given weekday in a given year (and/or year + month)
- Get the weekday of the first day in a year (and/or month)

I think that the order of parameters in the `nth_next_occurence`/`nth_prev_occurence` is subjective. Both seem natural to me, and I'll go with the suggested order.

I also have several questions. Could `nth_next occurence` and similar methods be implemented in `PrimitiveDateTime` and `OffsetDateTime`, with their implementation preserving their time and offset, or should I stick to `Date` only?
And will these methods have their checked counterparts? Because adding or subtracting something from a `Date` can result in an overflow.
Finally, which region these methods belong to? I haven't found any region that would fully suit these methods.
> `Weekday` has a `nth_next` method. Why not introduce it to `Month`, too?

Go for it. It's probably best for organization if introducing `nth_next` and `nth_prev` on `Month` are in a separate PR. Although now I see that there is no `nth_prev` on `Weekday`, which is definitely an omission.

> * Get a number of occurences of a given weekday in a given year (and/or year + month)

Let's hold off on this. While it may be good information to have in certain circumstances, it seems like it could be a bit niche. I also don't immediately think of (or know of) a way to calculate this directly.

> * Get the weekday of the first day in a year (and/or month)

This can be done today with `Date::weekday`, no?

> Could `nth_next occurence` and similar methods be implemented in `PrimitiveDateTime` and `OffsetDateTime`, with their implementation preserving their time and offset, or should I stick to `Date` only?

I would stick with `Date` only, as the behavior on other types could be somewhat confusing.

> And will these methods have their checked counterparts? Because adding or subtracting something from a `Date` can result in an overflow.

I'm torn on this. Perhaps introduce a checked method, just using `.expect()` (or realistically the macro to do the same thing in const context). This would make it trivial to export if it is ultimately desired.

> Finally, which region these methods belong to?

For the methods on `Month`, I'd place them immediately after the existing `next` and `prev` methods, just as they are with `Weekday`. For `Date`, I think the best place would be immediately after `next_day` and `previous_day`, as those are the closest equivalents that exist today.

Fix patch:
diff --git a/time/src/month.rs b/time/src/month.rs
index 0c657e982..8d8c164b2 100644
--- a/time/src/month.rs
+++ b/time/src/month.rs
@@ -97,6 +97,60 @@ impl Month {
             December => January,
         }
     }
+
+    /// Get n-th next month.
+    ///
+    /// ```rust
+    /// # use time::Month;
+    /// assert_eq!(Month::January.nth_next(4), Month::May);
+    /// assert_eq!(Month::July.nth_next(9), Month::April);
+    /// ```
+    pub const fn nth_next(self, n: u8) -> Self {
+        match (self as u8 - 1 + n % 12) % 12 {
+            0 => January,
+            1 => February,
+            2 => March,
+            3 => April,
+            4 => May,
+            5 => June,
+            6 => July,
+            7 => August,
+            8 => September,
+            9 => October,
+            10 => November,
+            val => {
+                debug_assert!(val == 11);
+                December
+            }
+        }
+    }
+
+    /// Get n-th previous month.
+    ///
+    /// ```rust
+    /// # use time::Month;
+    /// assert_eq!(Month::January.nth_prev(4), Month::September);
+    /// assert_eq!(Month::July.nth_prev(9), Month::October);
+    /// ```
+    pub const fn nth_prev(self, n: u8) -> Self {
+        match self as i8 - 1 - (n % 12) as i8 {
+            1 | -11 => February,
+            2 | -10 => March,
+            3 | -9 => April,
+            4 | -8 => May,
+            5 | -7 => June,
+            6 | -6 => July,
+            7 | -5 => August,
+            8 | -4 => September,
+            9 | -3 => October,
+            10 | -2 => November,
+            11 | -1 => December,
+            val => {
+                debug_assert!(val == 0);
+                January
+            }
+        }
+    }
 }
 
 impl fmt::Display for Month {
diff --git a/time/src/weekday.rs b/time/src/weekday.rs
index d530a2e4d..761107e69 100644
--- a/time/src/weekday.rs
+++ b/time/src/weekday.rs
@@ -88,6 +88,28 @@ impl Weekday {
         }
     }
 
+    /// Get n-th previous day.
+    ///
+    /// ```rust
+    /// # use time::Weekday;
+    /// assert_eq!(Weekday::Monday.nth_prev(1), Weekday::Sunday);
+    /// assert_eq!(Weekday::Sunday.nth_prev(10), Weekday::Thursday);
+    /// ```
+    pub const fn nth_prev(self, n: u8) -> Self {
+        match self.number_days_from_monday() as i8 - (n % 7) as i8 {
+            1 | -6 => Tuesday,
+            2 | -5 => Wednesday,
+            3 | -4 => Thursday,
+            4 | -3 => Friday,
+            5 | -2 => Saturday,
+            6 | -1 => Sunday,
+            val => {
+                debug_assert!(val == 0);
+                Monday
+            }
+        }
+    }
+
     /// Get the one-indexed number of days from Monday.
     ///
     /// ```rust


Test patch:
diff --git a/tests/month.rs b/tests/month.rs
index 6e4a1cbb1..e5354efef 100644
--- a/tests/month.rs
+++ b/tests/month.rs
@@ -32,6 +32,74 @@ fn next() {
     assert_eq!(December.next(), January);
 }
 
+#[test]
+fn nth_next() {
+    assert_eq!(January.nth_next(0), January);
+    assert_eq!(January.nth_next(1), February);
+    assert_eq!(January.nth_next(2), March);
+    assert_eq!(January.nth_next(3), April);
+    assert_eq!(January.nth_next(4), May);
+    assert_eq!(January.nth_next(5), June);
+    assert_eq!(January.nth_next(6), July);
+    assert_eq!(January.nth_next(7), August);
+    assert_eq!(January.nth_next(8), September);
+    assert_eq!(January.nth_next(9), October);
+    assert_eq!(January.nth_next(10), November);
+    assert_eq!(January.nth_next(11), December);
+
+    assert_eq!(December.nth_next(0), December);
+    assert_eq!(December.nth_next(1), January);
+    assert_eq!(December.nth_next(2), February);
+    assert_eq!(December.nth_next(3), March);
+    assert_eq!(December.nth_next(4), April);
+    assert_eq!(December.nth_next(5), May);
+    assert_eq!(December.nth_next(6), June);
+    assert_eq!(December.nth_next(7), July);
+    assert_eq!(December.nth_next(8), August);
+    assert_eq!(December.nth_next(9), September);
+    assert_eq!(December.nth_next(10), October);
+    assert_eq!(December.nth_next(11), November);
+
+    assert_eq!(January.nth_next(12), January);
+    assert_eq!(January.nth_next(u8::MAX), April);
+    assert_eq!(December.nth_next(12), December);
+    assert_eq!(December.nth_next(u8::MAX), March);
+}
+
+#[test]
+fn nth_prev() {
+    assert_eq!(January.nth_prev(0), January);
+    assert_eq!(January.nth_prev(1), December);
+    assert_eq!(January.nth_prev(2), November);
+    assert_eq!(January.nth_prev(3), October);
+    assert_eq!(January.nth_prev(4), September);
+    assert_eq!(January.nth_prev(5), August);
+    assert_eq!(January.nth_prev(6), July);
+    assert_eq!(January.nth_prev(7), June);
+    assert_eq!(January.nth_prev(8), May);
+    assert_eq!(January.nth_prev(9), April);
+    assert_eq!(January.nth_prev(10), March);
+    assert_eq!(January.nth_prev(11), February);
+
+    assert_eq!(December.nth_prev(0), December);
+    assert_eq!(December.nth_prev(1), November);
+    assert_eq!(December.nth_prev(2), October);
+    assert_eq!(December.nth_prev(3), September);
+    assert_eq!(December.nth_prev(4), August);
+    assert_eq!(December.nth_prev(5), July);
+    assert_eq!(December.nth_prev(6), June);
+    assert_eq!(December.nth_prev(7), May);
+    assert_eq!(December.nth_prev(8), April);
+    assert_eq!(December.nth_prev(9), March);
+    assert_eq!(December.nth_prev(10), February);
+    assert_eq!(December.nth_prev(11), January);
+
+    assert_eq!(January.nth_prev(12), January);
+    assert_eq!(January.nth_prev(u8::MAX), October);
+    assert_eq!(December.nth_prev(12), December);
+    assert_eq!(December.nth_prev(u8::MAX), September);
+}
+
 #[test]
 fn display() {
     assert_eq!(January.to_string(), "January");
diff --git a/tests/weekday.rs b/tests/weekday.rs
index 7ed14e105..e6dd48a11 100644
--- a/tests/weekday.rs
+++ b/tests/weekday.rs
@@ -46,6 +46,30 @@ fn nth_next() {
     assert_eq!(Monday.nth_next(u8::MAX), Thursday);
 }
 
+#[test]
+fn nth_prev() {
+    assert_eq!(Sunday.nth_prev(0), Sunday);
+    assert_eq!(Sunday.nth_prev(1), Saturday);
+    assert_eq!(Sunday.nth_prev(2), Friday);
+    assert_eq!(Sunday.nth_prev(3), Thursday);
+    assert_eq!(Sunday.nth_prev(4), Wednesday);
+    assert_eq!(Sunday.nth_prev(5), Tuesday);
+    assert_eq!(Sunday.nth_prev(6), Monday);
+
+    assert_eq!(Monday.nth_prev(0), Monday);
+    assert_eq!(Monday.nth_prev(1), Sunday);
+    assert_eq!(Monday.nth_prev(2), Saturday);
+    assert_eq!(Monday.nth_prev(3), Friday);
+    assert_eq!(Monday.nth_prev(4), Thursday);
+    assert_eq!(Monday.nth_prev(5), Wednesday);
+    assert_eq!(Monday.nth_prev(6), Tuesday);
+
+    assert_eq!(Sunday.nth_prev(7), Sunday);
+    assert_eq!(Sunday.nth_prev(u8::MAX), Thursday);
+    assert_eq!(Monday.nth_prev(7), Monday);
+    assert_eq!(Monday.nth_prev(u8::MAX), Friday);
+}
+
 #[test]
 fn number_from_monday() {
     assert_eq!(Monday.number_from_monday(), 1);