Repository: BurntSushi/jiff (language: rust)

Issue:
A way to construct a const Timestamp
I have a certain timestamp that I would like to be `const`. I tried the following ways:

- `Timestamp::constant(...)` (following similar fn in `Date`) -- doesn't exist.
- `Timestamp::new(...)` - not const fn
- `Timestamp::MIN + SignedDuration::new(...)` - the `+` is not const fn

Maybe there is way that I missed? If not, would it be possible to add if it makes sense?


Discussion / hints:
Jiff doesn't have much `const` support because Rust's `const` system isn't expressive enough to support it. This is primarily because of Jiff's use of custom ranged integers internally. 

There is a way to make some things `const`, but it usually requires duplicating logic. A `Timestamp` constructor is probably worth doing. 

As a work around, you can consider using `std::sync::LazyLock`.

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e906afd..8137303 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
 # CHANGELOG
 
+0.2.1 (TBD)
+===========
+TODO
+
+Enhancements:
+
+* [#263](https://github.com/BurntSushi/jiff/issues/263):
+Add `Timestamp::constant` for constructing timestamps in `const` context.
+
+
 0.2.0 (2025-02-10)
 ==================
 This is a new semver incompatible release of Jiff. It contains several breaking
diff --git a/src/timestamp.rs b/src/timestamp.rs
index 611529c..7b601d1 100644
--- a/src/timestamp.rs
+++ b/src/timestamp.rs
@@ -416,7 +416,7 @@ impl Timestamp {
     /// # Errors
     ///
     /// This returns an error if the given components would correspond to
-    /// an instant outside the support ranged. Also, `nanosecond` is limited
+    /// an instant outside the supported range. Also, `nanosecond` is limited
     /// to the range `-999,999,999..=999,999,999`.
     ///
     /// # Example
@@ -495,6 +495,60 @@ impl Timestamp {
         )
     }
 
+    /// Creates a new `Timestamp` value in a `const` context.
+    ///
+    /// # Panics
+    ///
+    /// This routine panics when [`Timestamp::new`] would return an error.
+    /// That is, when the given components would correspond to
+    /// an instant outside the supported range. Also, `nanosecond` is limited
+    /// to the range `-999,999,999..=999,999,999`.
+    ///
+    /// # Example
+    ///
+    /// This example shows the instant in time 123,456,789 seconds after the
+    /// Unix epoch:
+    ///
+    /// ```
+    /// use jiff::Timestamp;
+    ///
+    /// assert_eq!(
+    ///     Timestamp::constant(123_456_789, 0).to_string(),
+    ///     "1973-11-29T21:33:09Z",
+    /// );
+    /// ```
+    #[inline]
+    pub const fn constant(mut second: i64, mut nanosecond: i32) -> Timestamp {
+        if second == UnixSeconds::MIN_REPR && nanosecond < 0 {
+            panic!("nanoseconds must be >=0 when seconds are minimal");
+        }
+        // We now normalize our seconds and nanoseconds such that they have
+        // the same sign (or where one is zero). So for example, when given
+        // `-1s 1ns`, then we should turn that into `-999,999,999ns`.
+        //
+        // But first, if we're already normalized, we're done!
+        if second.signum() as i8 == nanosecond.signum() as i8
+            || second == 0
+            || nanosecond == 0
+        {
+            return Timestamp {
+                second: UnixSeconds::new_unchecked(second),
+                nanosecond: FractionalNanosecond::new_unchecked(nanosecond),
+            };
+        }
+        if second < 0 && nanosecond > 0 {
+            second += 1;
+            nanosecond -= t::NANOS_PER_SECOND.value() as i32;
+        } else if second > 0 && nanosecond < 0 {
+            second -= 1;
+            nanosecond += t::NANOS_PER_SECOND.value() as i32;
+        }
+        Timestamp {
+            second: UnixSeconds::new_unchecked(second),
+            nanosecond: FractionalNanosecond::new_unchecked(nanosecond),
+        }
+    }
+
     /// Creates a new instant in time from the number of seconds elapsed since
     /// the Unix epoch.
     ///
@@ -3546,6 +3600,22 @@ mod tests {
             let got = Timestamp::from_nanosecond(nanos).unwrap();
             t == got
         }
+
+        fn timestamp_constant_and_new_are_same1(t: Timestamp) -> bool {
+            let got = Timestamp::constant(t.as_second(), t.subsec_nanosecond());
+            t == got
+        }
+
+        fn timestamp_constant_and_new_are_same2(
+            secs: i64,
+            nanos: i32
+        ) -> quickcheck::TestResult {
+            let Ok(ts) = Timestamp::new(secs, nanos) else {
+                return quickcheck::TestResult::discard();
+            };
+            let got = Timestamp::constant(secs, nanos);
+            quickcheck::TestResult::from_bool(ts == got)
+        }
     }
 
     /// A `serde` deserializer compatibility test.


Test patch: