Skip to content

Commit 2f119a7

Browse files
committed
Make float and date of ITime not overflow
1 parent d290623 commit 2f119a7

File tree

2 files changed

+61
-8
lines changed

2 files changed

+61
-8
lines changed

src/ITime.jl

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,30 @@ end
101101
"""
102102
date(t::ITime)
103103
104-
Return the date associated with `t`. If the time is fractional round it to millisecond.
104+
Return the date associated with `t`. If the time is fractional, round it to
105+
millisecond.
105106
106-
For this to work, `t` has to have a `epoch`
107+
For this to work, `t` has to have a `epoch`.
107108
"""
108109
function date(t::ITime)
109-
return epoch(t) + counter(t) * period(t)
110+
current_date = epoch(t)
111+
elapsed_period = counter(t) * period(t)
112+
113+
# We only check if `elapsed_period` overflows.
114+
# It is reasonable to assume that the date will not overflow since the
115+
# maximum value for the date is 292277025-08-17T07:12:55.807
116+
# which is computed by
117+
# `DateTime(Dates.UTInstant(Millisecond(typemax(Int64))))`.
118+
# Meanwhile, it is possible for `elapsed_period` to overflow if the period
119+
# is extremely small (e.g. nanosecond) and Int64 is used for the counter.
120+
# Although, this will not happen for the nanosecond and Int64 case for ~294
121+
# years
122+
period_is_zero = period(t) == zero(period(t))
123+
no_overflow = period_is_zero || elapsed_period / period(t) == counter(t)
124+
no_overflow && return current_date + elapsed_period
125+
error(
126+
"Overflow with counter(t) * period(t) in computing the date; try to use a bigger value for the period if possible",
127+
)
110128
end
111129

112130
"""
@@ -159,7 +177,7 @@ function Base.show(io::IO, time::ITime)
159177
# them because they cannot be nicely converted to Periods, instead of
160178
# reconstruct the string from the type name and the value (obtained my
161179
# multiplying the counter and the number of units in the period)
162-
value = counter(time) * period(time).value
180+
value = counter(time) * float(period(time).value)
163181
plural_s = abs(value) != 1 ? "s" : ""
164182
unit = lowercase(string(nameof(typeof(period(time))))) * plural_s
165183

@@ -348,7 +366,7 @@ Convert an `ITime` to a floating-point number representing the time in seconds.
348366
"""
349367
function Base.float(t::T) where {T <: ITime}
350368
if VERSION >= v"1.11"
351-
return float(Dates.seconds(t.period) * t.counter)
369+
return float(Dates.seconds(t.period)) * t.counter
352370
else
353371
return Dates.tons(t.period) / 1_000_000_000 * t.counter
354372
end

test/itime.jl

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,19 +143,20 @@ using Test, Dates
143143

144144
@testset "Show method" begin
145145
t1 = ITime(10)
146-
@test sprint(show, t1) == "10 seconds [counter = 10, period = 1 second]"
146+
@test sprint(show, t1) ==
147+
"10.0 seconds [counter = 10, period = 1 second]"
147148

148149
t2 = ITime(10, epoch = Dates.DateTime(2024, 1, 1))
149150
@test sprint(show, t2) ==
150-
"10 seconds (2024-01-01T00:00:10) [counter = 10, period = 1 second, epoch = 2024-01-01T00:00:00]"
151+
"10.0 seconds (2024-01-01T00:00:10) [counter = 10, period = 1 second, epoch = 2024-01-01T00:00:00]"
151152

152153
t3 = ITime(
153154
10,
154155
period = Dates.Hour(1),
155156
epoch = Dates.DateTime(2024, 1, 1),
156157
)
157158
@test sprint(show, t3) ==
158-
"10 hours (2024-01-01T10:00:00) [counter = 10, period = 1 hour, epoch = 2024-01-01T00:00:00]"
159+
"10.0 hours (2024-01-01T10:00:00) [counter = 10, period = 1 hour, epoch = 2024-01-01T00:00:00]"
159160
end
160161

161162
@testset "Find common epoch" begin
@@ -290,4 +291,38 @@ using Test, Dates
290291
@test typeof(zero(t1).counter) == Int32
291292
@test typeof(mod(t3, t2).counter) == Int32
292293
end
294+
295+
@testset "Overflow test" begin
296+
t1 = ITime(typemax(Int64), period = Second(2))
297+
# Dates.seconds(t.period) returns an integer if t.period is in seconds
298+
# Multiplying this by t.counter, an integer, results in an integer that
299+
# could overflow
300+
@test isapprox(float(t1), float(typemax(Int64)) * 2.0)
301+
302+
# The computation epoch(t) + counter(t) * period(t) could overflow since
303+
# counter(t) * period(t) could overflow. In this case, the maximum value
304+
# of period(t) is 2^63 - 1 nanoseconds. Multiplying 2 nanoseconds by the
305+
# counter overflow and give -2 nanoseconds. Here, we decide to throw an
306+
# error instead of circumventing the overflow. One way to circumvent the
307+
# overflow is to add counter(t) * oneunit(period(t)) n times, where n is
308+
# the value of period.
309+
t2 = ITime(
310+
typemax(Int64),
311+
period = Nanosecond(2),
312+
epoch = Dates.DateTime(2010),
313+
)
314+
@test_throws ErrorException date(t2)
315+
316+
# Same tests as above, but with Int32
317+
t3 = ITime(typemax(Int32), period = Second(2))
318+
@test isapprox(float(t3), float(typemax(Int32)) * 2.0)
319+
320+
t4 = ITime(
321+
typemax(Int32),
322+
period = Nanosecond(2),
323+
epoch = Dates.DateTime(2010),
324+
)
325+
# We do not overflow because Period uses Int64 internally
326+
@test date(t4) == Dates.DateTime(2010) + typemax(Int32) * Nanosecond(2)
327+
end
293328
end

0 commit comments

Comments
 (0)