From a644c44824a42afaded930c52986ea67b2a55f34 Mon Sep 17 00:00:00 2001
From: "carpentry-heartbeat[bot]"
- (Fn [(Ref Datetime a), (Ref Datetime b)] Int)
+ (Fn [(Ref Datetime a), (Ref Datetime b)] Long)
- (Fn [(Ref Duration a)] Int)
+ (Fn [(Ref Duration a)] Long)
- (Fn [(Ref Duration a)] Int)
+ (Fn [(Ref Duration a)] Long)
- (Fn [(Ref Duration a)] Int)
+ (Fn [(Ref Duration a)] Long)
- (Fn [(Ref Duration a)] Int)
+ (Fn [(Ref Duration a)] Long)
adds a A
defn
(diff a b)
diff --git a/docs/Duration.html b/docs/Duration.html
index 76e043d..dfb88e8 100644
--- a/docs/Duration.html
+++ b/docs/Duration.html
@@ -490,7 +490,7 @@
defn
(to-days d)
@@ -511,7 +511,7 @@
defn
(to-hours d)
@@ -532,7 +532,7 @@
defn
(to-minutes d)
@@ -553,7 +553,7 @@
defn
(to-seconds d)
diff --git a/test/time.carp b/test/time.carp
index 262415d..3bc0fb9 100644
--- a/test/time.carp
+++ b/test/time.carp
@@ -1692,19 +1692,19 @@
"subtract 3661 seconds (1h1m1s) from 00:00:00")
(assert-equal test
- 0
+ 0l
(Datetime.diff &(Datetime.date 2024 3 15) &(Datetime.date 2024 3 15))
"diff: same date is 0")
(assert-equal test
- 86400
+ 86400l
(Datetime.diff &(Datetime.date 2024 3 16) &(Datetime.date 2024 3 15))
"diff: one day apart is 86400")
(assert-equal test
- -86400
+ -86400l
(Datetime.diff &(Datetime.date 2024 3 15) &(Datetime.date 2024 3 16))
"diff: reversed order is negative")
(assert-equal test
- 3600
+ 3600l
(Datetime.diff
&(Datetime.init 2024
3
@@ -1724,7 +1724,7 @@
(Maybe.Nothing)))
"diff: one hour apart is 3600")
(assert-equal test
- 90061
+ 90061l
(Datetime.diff
&(Datetime.init 2024
3
@@ -1744,13 +1744,21 @@
(Maybe.Nothing)))
"diff: 1 day 1 hour 1 minute 1 second = 90061")
(assert-equal test
- (* 365 86400)
+ (* 365l 86400l)
(Datetime.diff &(Datetime.date 2023 1 1) &(Datetime.date 2022 1 1))
"diff: non-leap year is 365 days")
(assert-equal test
- (* 366 86400)
+ (* 366l 86400l)
(Datetime.diff &(Datetime.date 2025 1 1) &(Datetime.date 2024 1 1))
"diff: leap year 2024 has 366 days")
+ (assert-equal test
+ (* 36525l 86400l)
+ (Datetime.diff &(Datetime.date 2100 1 1) &(Datetime.date 2000 1 1))
+ "diff: a century of seconds does not wrap")
+ (assert-equal test
+ (* -36525l 86400l)
+ (Datetime.diff &(Datetime.date 2000 1 1) &(Datetime.date 2100 1 1))
+ "diff: a negative century of seconds does not wrap")
(assert-equal test
&(Datetime.date 2024 2 15)
@@ -1900,33 +1908,51 @@
"between: sub-day difference in seconds")
(assert-equal test
- 3600
+ 3600l
(Duration.to-seconds &(Duration.hours 1))
"to-seconds: exact")
(assert-equal test
- 1
+ 1l
(Duration.to-minutes &(Duration.seconds 90))
"to-minutes: truncates positive remainder")
(assert-equal test
- 0
+ 0l
(Duration.to-minutes &(Duration.seconds 59))
"to-minutes: below one minute is zero")
(assert-equal test
- -1
+ -1l
(Duration.to-minutes &(Duration.seconds -90))
"to-minutes: truncates toward zero for negatives")
(assert-equal test
- 1
+ 1l
(Duration.to-hours &(Duration.minutes 90))
"to-hours: truncates remainder")
(assert-equal test
- 1
+ 1l
(Duration.to-days &(Duration.hours 25))
"to-days: truncates remainder")
(assert-equal test
- 7
+ 7l
(Duration.to-days &(Duration.weeks 1))
"to-days: one week is seven days")
+ (assert-equal test
+ (* 3551l 604800l)
+ (Duration.to-seconds &(Duration.weeks 3551))
+ "weeks: a count past the 32-bit second range does not wrap")
+ (assert-equal test
+ (* 24856l 86400l)
+ (Duration.to-seconds &(Duration.days 24856))
+ "days: a count past the 32-bit second range does not wrap")
+ (assert-equal test
+ (* 36525l 86400l)
+ (Duration.to-seconds
+ &(Duration.between &(Datetime.date 2100 1 1) &(Datetime.date 2000 1 1)))
+ "between: a century does not wrap")
+ (assert-equal test
+ 36525l
+ (Duration.to-days
+ &(Duration.between &(Datetime.date 2100 1 1) &(Datetime.date 2000 1 1)))
+ "to-days: a century is 36525 days")
(assert-equal test
&(Duration.seconds 90)
@@ -1964,6 +1990,9 @@
(assert-true test
(Duration.= &(Duration.minutes 1) &(Duration.seconds 60))
"=: equal durations from different units")
+ (assert-false test
+ (Duration.= &(Duration.days 49710) &(Duration.seconds -23296))
+ "=: deltas that collide when truncated to 32 bits are not equal")
(assert-false test
(Duration.= &(Duration.minutes 1) &(Duration.seconds 61))
"=: unequal durations")
@@ -1979,12 +2008,28 @@
(assert-false test
(Duration.> &(Duration.seconds 30) &(Duration.minutes 1))
">: shorter is not greater")
+ (assert-true test
+ (Duration.> &(Duration.days 30000) &(Duration.days 1))
+ ">: a delta past the 32-bit second range is still the longer one")
+ (assert-false test
+ (Duration.< &(Duration.days 30000) &(Duration.days 1))
+ "<: a delta past the 32-bit second range is not the shorter one")
(assert-true test
(Duration.zero? &(Duration.seconds 0))
"zero?: the zero delta")
(assert-false test
(Duration.zero? &(Duration.seconds 1))
"zero?: a non-zero delta")
+ (assert-false test
+ (Duration.zero?
+ &(Duration.plus &(Duration.days 49710) &(Duration.seconds 23296)))
+ "zero?: a delta of exactly 2^32 seconds is not the zero delta")
+ (assert-true test
+ (Duration.pos? &(Duration.days 30000))
+ "pos?: a delta past the 32-bit second range is positive")
+ (assert-false test
+ (Duration.neg? &(Duration.days 30000))
+ "neg?: a delta past the 32-bit second range is not negative")
(assert-true test (Duration.pos? &(Duration.seconds 1)) "pos?: positive delta")
(assert-false test
(Duration.pos? &(Duration.seconds -1))
@@ -2034,4 +2079,39 @@
(assert-equal test
&@"-1d 2h 3m 4s"
&(Duration.str &(Duration.seconds -93784))
- "str: negative multi-unit"))
+ "str: negative multi-unit")
+ (assert-equal test
+ &@"36525d 1h 1m 1s"
+ &(Duration.str
+ &(Duration.plus &(Duration.days 36525) &(Duration.seconds 3661)))
+ "str: multi-decade delta")
+ (assert-equal test
+ &@"-36525d"
+ &(Duration.str &(Duration.negate &(Duration.days 36525)))
+ "str: negative multi-decade delta")
+
+ (assert-equal test
+ &@"2100-01-01"
+ &(Datetime.isoformat
+ &(Duration.add &(Datetime.date 2000 1 1) &(Duration.days 36525)))
+ "add: a century-long delta lands on the right date")
+ (assert-equal test
+ &@"2000-01-01"
+ &(Datetime.isoformat
+ &(Duration.sub &(Datetime.date 2100 1 1) &(Duration.days 36525)))
+ "sub: a century-long delta lands on the right date")
+ (assert-equal test
+ &@"2124-03-16 13:30:45"
+ &(Datetime.strftime
+ &(Duration.add
+ &(Datetime.init 2024
+ 3
+ 15
+ (Maybe.Just 12)
+ (Maybe.Just 30)
+ (Maybe.Just 45)
+ (Maybe.Nothing)
+ (Maybe.Nothing))
+ &(Duration.plus &(Duration.days 36525) &(Duration.hours 1)))
+ "%Y-%m-%d %H:%M:%S")
+ "add: a century-long delta keeps the time of day"))
diff --git a/time.carp b/time.carp
index 0b19afe..216d754 100644
--- a/time.carp
+++ b/time.carp
@@ -1236,7 +1236,9 @@ earlier. This is a timezone-unaware comparison, similar to `>` and `<`.")
time-b (+ (* (Maybe.from @(hours b) 0) HOUR)
(+ (* (Maybe.from @(minutes b) 0) MINUTE)
(Maybe.from @(seconds b) 0)))]
- (+ (* (- ord-a ord-b) DAY) (- time-a time-b))))
+ (+
+ (* (Long.from-int (- ord-a ord-b)) (Long.from-int DAY))
+ (Long.from-int (- time-a time-b)))))
(doc add-months "adds `n` months to the `Datetime` `dt`.
@@ -1279,7 +1281,7 @@ clamped to February 28.
@(nanoseconds dt)
@(tz dt)))))
-(deftype Duration [seconds- Int])
+(deftype Duration [seconds- Long])
(doc Duration "this is a complementary module to be used with `Datetime` and
is concerned with adding and subtracting time values of the scale from seconds
@@ -1311,21 +1313,36 @@ Example:
(private init)
(doc seconds "creates a delta representing a number of seconds `n`.")
- (defn seconds [n] (init n))
+ (defn seconds [n] (init (Long.from-int n)))
(doc minutes "creates a delta representing a number of minutes `n`.")
- (defn minutes [n] (seconds (* n 60)))
+ (defn minutes [n] (init (* (Long.from-int n) 60l)))
(doc hours "creates a delta representing a number of hours `n`.")
- (defn hours [n] (minutes (* n 60)))
+ (defn hours [n] (init (* (Long.from-int n) 3600l)))
(doc days "creates a delta representing a number of days `n`.")
- (defn days [n] (hours (* n 24)))
+ (defn days [n] (init (* (Long.from-int n) 86400l)))
(doc weeks "creates a delta representing a number of weeks `n`.")
- (defn weeks [n] (days (* n 7)))
+ (defn weeks [n] (init (* (Long.from-int n) 604800l)))
+
+ (private shift)
+ (hidden shift)
+ ; a Datetime names its date by an Int ordinal, so clamp rather than wrap
+ (defn shift [dt total]
+ (let-do [nd @dt
+ ord (Long.clamp (Long.from-int Int.MIN)
+ (Long.from-int Int.MAX)
+ (+ (Long.from-int (Datetime.to-ordinal dt))
+ (/ total 86400l)))
+ d (Datetime.from-ordinal (Long.to-int ord))]
+ (Datetime.set-day! &nd @(Datetime.day &d))
+ (Datetime.set-month! &nd @(Datetime.month &d))
+ (Datetime.set-year! &nd @(Datetime.year &d))
+ (Datetime.add-seconds &nd (Long.to-int (mod total 86400l)))))
(doc add "adds a `Duration` `delta` to a `Datetime` `dt`.")
- (defn add [dt delta] (Datetime.add-seconds dt @(seconds- delta)))
+ (defn add [dt delta] (shift dt @(seconds- delta)))
(doc sub "subtracts a `Duration` `delta` from a `Datetime` `dt`.")
- (defn sub [dt delta] (Datetime.add-seconds dt (neg @(seconds- delta))))
+ (defn sub [dt delta] (shift dt (neg @(seconds- delta))))
(doc between
"returns the signed difference between the `Datetime`s `a` and `b`
@@ -1341,16 +1358,16 @@ earlier, mirroring `Datetime.diff`.")
(doc to-minutes "returns the total number of whole minutes represented by the
delta `d`, truncating any remaining seconds toward zero.")
- (defn to-minutes [d] (/ @(seconds- d) 60))
+ (defn to-minutes [d] (/ @(seconds- d) 60l))
(doc to-hours
"returns the total number of whole hours represented by the delta
`d`, truncating any remainder toward zero.")
- (defn to-hours [d] (/ @(seconds- d) 3600))
+ (defn to-hours [d] (/ @(seconds- d) 3600l))
(doc to-days "returns the total number of whole days represented by the delta
`d`, truncating any remainder toward zero.")
- (defn to-days [d] (/ @(seconds- d) 86400))
+ (defn to-days [d] (/ @(seconds- d) 86400l))
(doc plus "returns the sum of the deltas `a` and `b`.")
(defn plus [a b] (init (+ @(seconds- a) @(seconds- b))))
@@ -1362,32 +1379,32 @@ delta `d`, truncating any remaining seconds toward zero.")
(defn negate [d] (init (neg @(seconds- d))))
(doc scale "returns the delta `d` scaled by the integer factor `n`.")
- (defn scale [d n] (init (* @(seconds- d) n)))
+ (defn scale [d n] (init (* @(seconds- d) (Long.from-int n))))
(doc mul "returns the delta `d` scaled by the integer factor `n`. An alias for
`scale`.")
(defn mul [d n] (scale d n))
(doc = "checks whether the deltas `a` and `b` represent the same duration.")
- (defn = [a b] (Int.= @(seconds- a) @(seconds- b)))
+ (defn = [a b] (Long.= @(seconds- a) @(seconds- b)))
(implements = Duration.=)
(doc < "checks whether the delta `a` is shorter than the delta `b`.")
- (defn < [a b] (Int.< @(seconds- a) @(seconds- b)))
+ (defn < [a b] (Long.< @(seconds- a) @(seconds- b)))
(implements < Duration.<)
(doc > "checks whether the delta `a` is longer than the delta `b`.")
- (defn > [a b] (Int.> @(seconds- a) @(seconds- b)))
+ (defn > [a b] (Long.> @(seconds- a) @(seconds- b)))
(implements > Duration.>)
(doc zero? "checks whether the delta `d` represents no elapsed time.")
- (defn zero? [d] (Int.= @(seconds- d) 0))
+ (defn zero? [d] (Long.= @(seconds- d) 0l))
(doc pos? "checks whether the delta `d` represents a positive duration.")
- (defn pos? [d] (Int.> @(seconds- d) 0))
+ (defn pos? [d] (Long.> @(seconds- d) 0l))
(doc neg? "checks whether the delta `d` represents a negative duration.")
- (defn neg? [d] (Int.< @(seconds- d) 0))
+ (defn neg? [d] (Long.< @(seconds- d) 0l))
(doc str
"renders the delta `d` in a human-readable form such as `1d 2h 3m 4s`.
@@ -1396,18 +1413,19 @@ Components that are zero are omitted, a negative delta is prefixed with a `-`,
and the zero delta renders as `0s`.")
(defn str [d]
(let [total @(seconds- d)]
- (if (Int.= total 0)
+ (if (Long.= total 0l)
@"0s"
- (let-do [a (Int.abs total)
- days (/ a 86400)
- hrs (/ (mod a 86400) 3600)
- mins (/ (mod a 3600) 60)
- secs (mod a 60)
+ (let-do [a (Long.abs total)
+ days (/ a 86400l)
+ hrs (Long.to-int (/ (mod a 86400l) 3600l))
+ mins (Long.to-int (/ (mod a 3600l) 60l))
+ secs (Long.to-int (mod a 60l))
parts []]
- (when (> days 0) (Array.push-back! &parts (fmt "%dd" days)))
+ (when (> days 0l)
+ (Array.push-back! &parts (fmt "%sd" &(Long.str days))))
(when (> hrs 0) (Array.push-back! &parts (fmt "%dh" hrs)))
(when (> mins 0) (Array.push-back! &parts (fmt "%dm" mins)))
(when (> secs 0) (Array.push-back! &parts (fmt "%ds" secs)))
(String.concat
- &[(if (Int.< total 0) @"-" @"") (String.join " " &parts)])))))
+ &[(if (Long.< total 0l) @"-" @"") (String.join " " &parts)])))))
(implements str Duration.str))
From 2db7e70b7db80554fd6017d2e91c2c23488f1b66 Mon Sep 17 00:00:00 2001
From: Veit Heller
Duration delta to a Datetime dt.Duration can name a span far wider than the range of dates a Datetime can
+represent. A result that would fall outside that range saturates at the first
+or last representable date rather than wrapping.
+ (Fn [Long] Duration) +
++ (from-seconds n) ++
+
creates a delta from a raw second count n.
The unit constructors above take an Int count, which cannot name a delta
+longer than about 68 years in seconds. This is the counterpart to
+to-seconds, so a delta of any width round-trips through it.