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

Issue:
OffsetDateTime lacks an obvious constructor
As evidenced by posts like this: https://www.reddit.com/r/rust/comments/18i8y39/learning_rust_my_experience_so_far_has_been_mixed/, as well as my own experience converting code from chrono to time, the lack of an obvious `OffsetDateTime::new` makes it more difficult to figure out how to work with the time crate.  

Some current workarounds:

Using macros.  This is how the docs tend to show creating an OffsetDateTime, but it only works if you have a literal datetime to plug in.  You can't use it with a list of integers representing different components, which is how you would want to, say, convert from a datetime gotten from another library or language.

Constructing a PrimitiveDateTime, and adding an offset to it.  This is an unsatisfying learning experience, because the user is trying to create an OffsetDateTime, so they aren't expecting the constructor to live on a different type.
```rust
PrimitiveDateTime::new(
    Date::from_calendar_date(2023, Month::December, 14)?, 
    Time::from_hms(12, 30, 59)?
).assume_utc()
```

Create an arbitrary OffsetDateTime, and replace all the components.  This also doesn't feel right, because you have to construct the wrong datetime to get to the right one, and you involve a syscall where it isn't necessary.  Also, you can get funky results but no error if you forget a component.  For instance, if you forget the nanoseconds, you could end up with two values that you expect to be equal that are not.

```rust
let dt1 = OffsetDateTime::now_utc()
    .replace_date(date)
    .replace_time(time)
    .replace_offset(offset);
let dt2 = OffsetDateTime::now_utc()
    .replace_year(year)?
    .replace_month(month)?
    .replace_day(day)?
    .replace_hour(hour)?
    .replace_minute(minute)?
    .replace_second(second)?
    .replace_nanosecond(nano)?
    .replace_offset(offset);
```

I propose adding a constructor:

```rust
fn new(date: Date, time: Time, offset: UtcOffset) -> OffsetDateTime {
     PrimitiveDateTime::new(date, time).assume_offset(offset)
}
```
and perhaps also a shortcut for utc, since that seems useful and common in time's api:

```rust
fn new_utc(date: Date, time: Time) -> OffsetDateTime {
     PrimitiveDateTime::new(date, time).assume_utc()
}
```

This still requires the user to know how to create a Date and a Time, but at least the references to those types are present in the `new` documentation, which is where they would expect it, and example code on the `OffsetDateTime` constuctors could show how to create them without requiring the user to navigate to the other types.




Discussion / hints:

Fix patch:
diff --git a/time/src/offset_date_time.rs b/time/src/offset_date_time.rs
index 79d91a61b..dcdd2b2f9 100644
--- a/time/src/offset_date_time.rs
+++ b/time/src/offset_date_time.rs
@@ -71,6 +71,39 @@ impl OffsetDateTime {
     }
     // endregion now
 
+    /// Create a new `OffsetDateTime` with the given [`Date`], [`Time`], and [`UtcOffset`].
+    ///
+    /// ```
+    /// # use time::{Date, Month, OffsetDateTime, Time, UtcOffset};
+    /// # use time_macros::datetime;
+    /// let dt = OffsetDateTime::new_in_offset(
+    ///     Date::from_calendar_date(2024, Month::January, 1)?,
+    ///     Time::from_hms_nano(12, 59, 59, 500_000_000)?,
+    ///     UtcOffset::from_hms(-5, 0, 0)?,
+    /// );
+    /// assert_eq!(dt, datetime!(2024-01-01 12:59:59.5 -5));
+    /// # Ok::<_, time::error::Error>(())
+    /// ```
+    pub const fn new_in_offset(date: Date, time: Time, offset: UtcOffset) -> Self {
+        PrimitiveDateTime::new(date, time).assume_offset(offset)
+    }
+
+    /// Create a new `OffsetDateTime` with the given [`Date`] and [`Time`] in the UTC timezone.
+    ///
+    /// ```
+    /// # use time::{Date, Month, OffsetDateTime, Time};
+    /// # use time_macros::datetime;
+    /// let dt = OffsetDateTime::new_utc(
+    ///     Date::from_calendar_date(2024, Month::January, 1)?,
+    ///     Time::from_hms_nano(12, 59, 59, 500_000_000)?,
+    /// );
+    /// assert_eq!(dt, datetime!(2024-01-01 12:59:59.5 UTC));
+    /// # Ok::<_, time::error::Error>(())
+    /// ```
+    pub const fn new_utc(date: Date, time: Time) -> Self {
+        PrimitiveDateTime::new(date, time).assume_utc()
+    }
+
     /// Convert the `OffsetDateTime` from the current [`UtcOffset`] to the provided [`UtcOffset`].
     ///
     /// ```rust


Test patch:
diff --git a/tests/offset_date_time.rs b/tests/offset_date_time.rs
index 86db74a89..a9593d7d3 100644
--- a/tests/offset_date_time.rs
+++ b/tests/offset_date_time.rs
@@ -5,6 +5,23 @@ use time::ext::{NumericalDuration, NumericalStdDuration};
 use time::macros::{date, datetime, offset, time};
 use time::{Date, Duration, Month, OffsetDateTime, PrimitiveDateTime, Result, Weekday};
 
+#[test]
+fn new_utc() {
+    let dt = OffsetDateTime::new_utc(date!(2023 - 12 - 18), time!(10:13:44.250 AM));
+    assert_eq!(dt.year(), 2023);
+    assert_eq!(dt.millisecond(), 250);
+    assert_eq!(dt.offset(), offset!(UTC));
+}
+
+#[test]
+fn new_in_offset() {
+    let dt =
+        OffsetDateTime::new_in_offset(date!(2023 - 12 - 18), time!(10:13:44.250 AM), offset!(-4));
+    assert_eq!(dt.year(), 2023);
+    assert_eq!(dt.millisecond(), 250);
+    assert_eq!(dt.offset().whole_hours(), -4);
+}
+
 #[test]
 fn now_utc() {
     assert!(OffsetDateTime::now_utc().year() >= 2019);
@@ -433,16 +450,12 @@ fn replace_year() -> Result<()> {
         datetime!(2022 - 02 - 18 12:00 +01).replace_year(2019),
         Ok(datetime!(2019 - 02 - 18 12:00 +01))
     );
-    assert!(
-        datetime!(2022 - 02 - 18 12:00 +01)
-            .replace_year(-1_000_000_000)
-            .is_err()
-    ); // -1_000_000_000 isn't a valid year
-    assert!(
-        datetime!(2022 - 02 - 18 12:00 +01)
-            .replace_year(1_000_000_000)
-            .is_err()
-    ); // 1_000_000_000 isn't a valid year
+    assert!(datetime!(2022 - 02 - 18 12:00 +01)
+        .replace_year(-1_000_000_000)
+        .is_err()); // -1_000_000_000 isn't a valid year
+    assert!(datetime!(2022 - 02 - 18 12:00 +01)
+        .replace_year(1_000_000_000)
+        .is_err()); // 1_000_000_000 isn't a valid year
     Ok(())
 }
 
@@ -452,11 +465,9 @@ fn replace_month() -> Result<()> {
         datetime!(2022 - 02 - 18 12:00 +01).replace_month(Month::January),
         Ok(datetime!(2022 - 01 - 18 12:00 +01))
     );
-    assert!(
-        datetime!(2022 - 01 - 30 12:00 +01)
-            .replace_month(Month::February)
-            .is_err()
-    ); // 30 isn't a valid day in February
+    assert!(datetime!(2022 - 01 - 30 12:00 +01)
+        .replace_month(Month::February)
+        .is_err()); // 30 isn't a valid day in February
     Ok(())
 }
 
@@ -477,11 +488,9 @@ fn replace_hour() -> Result<()> {
         datetime!(2022 - 02 - 18 01:02:03.004_005_006 +01).replace_hour(7),
         Ok(datetime!(2022 - 02 - 18 07:02:03.004_005_006 +01))
     );
-    assert!(
-        datetime!(2022 - 02 - 18 01:02:03.004_005_006 +01)
-            .replace_hour(24)
-            .is_err()
-    ); // 24 isn't a valid hour
+    assert!(datetime!(2022 - 02 - 18 01:02:03.004_005_006 +01)
+        .replace_hour(24)
+        .is_err()); // 24 isn't a valid hour
     Ok(())
 }
 
@@ -491,11 +500,9 @@ fn replace_minute() -> Result<()> {
         datetime!(2022 - 02 - 18 01:02:03.004_005_006 +01).replace_minute(7),
         Ok(datetime!(2022 - 02 - 18 01:07:03.004_005_006 +01))
     );
-    assert!(
-        datetime!(2022 - 02 - 18 01:02:03.004_005_006 +01)
-            .replace_minute(60)
-            .is_err()
-    ); // 60 isn't a valid minute
+    assert!(datetime!(2022 - 02 - 18 01:02:03.004_005_006 +01)
+        .replace_minute(60)
+        .is_err()); // 60 isn't a valid minute
     Ok(())
 }
 
@@ -505,11 +512,9 @@ fn replace_second() -> Result<()> {
         datetime!(2022 - 02 - 18 01:02:03.004_005_006 +01).replace_second(7),
         Ok(datetime!(2022 - 02 - 18 01:02:07.004_005_006 +01))
     );
-    assert!(
-        datetime!(2022 - 02 - 18 01:02:03.004_005_006 +01)
-            .replace_second(60)
-            .is_err()
-    ); // 60 isn't a valid second
+    assert!(datetime!(2022 - 02 - 18 01:02:03.004_005_006 +01)
+        .replace_second(60)
+        .is_err()); // 60 isn't a valid second
     Ok(())
 }
 
@@ -519,11 +524,9 @@ fn replace_millisecond() -> Result<()> {
         datetime!(2022 - 02 - 18 01:02:03.004_005_006 +01).replace_millisecond(7),
         Ok(datetime!(2022 - 02 - 18 01:02:03.007 +01))
     );
-    assert!(
-        datetime!(2022 - 02 - 18 01:02:03.004_005_006 +01)
-            .replace_millisecond(1_000)
-            .is_err()
-    ); // 1_000 isn't a valid millisecond
+    assert!(datetime!(2022 - 02 - 18 01:02:03.004_005_006 +01)
+        .replace_millisecond(1_000)
+        .is_err()); // 1_000 isn't a valid millisecond
     Ok(())
 }
 
@@ -533,11 +536,9 @@ fn replace_microsecond() -> Result<()> {
         datetime!(2022 - 02 - 18 01:02:03.004_005_006 +01).replace_microsecond(7_008),
         Ok(datetime!(2022 - 02 - 18 01:02:03.007_008 +01))
     );
-    assert!(
-        datetime!(2022 - 02 - 18 01:02:03.004_005_006 +01)
-            .replace_microsecond(1_000_000)
-            .is_err()
-    ); // 1_000_000 isn't a valid microsecond
+    assert!(datetime!(2022 - 02 - 18 01:02:03.004_005_006 +01)
+        .replace_microsecond(1_000_000)
+        .is_err()); // 1_000_000 isn't a valid microsecond
     Ok(())
 }
 
@@ -547,11 +548,9 @@ fn replace_nanosecond() -> Result<()> {
         datetime!(2022 - 02 - 18 01:02:03.004_005_006 +01).replace_nanosecond(7_008_009),
         Ok(datetime!(2022 - 02 - 18 01:02:03.007_008_009 +01))
     );
-    assert!(
-        datetime!(2022 - 02 - 18 01:02:03.004_005_006 +01)
-            .replace_nanosecond(1_000_000_000)
-            .is_err()
-    ); // 1_000_000_000 isn't a valid nanosecond
+    assert!(datetime!(2022 - 02 - 18 01:02:03.004_005_006 +01)
+        .replace_nanosecond(1_000_000_000)
+        .is_err()); // 1_000_000_000 isn't a valid nanosecond
     Ok(())
 }