Skip to content

Commit 1e1af76

Browse files
omuslazarusA
authored andcommitted
Fix fixed-length negative year parsing (JuliaLang#54535)
Follow up to JuliaLang#53981. Fixes an issue introduced with negative years and fixed-with date formats: ```julia julia> Dates.DateTime("-20240521", "yyyymmdd") ERROR: ArgumentError: Month: 40 out of range (1:12) Stacktrace: [1] DateTime(y::Int64, m::Int64, d::Int64, h::Int64, mi::Int64, s::Int64, ms::Int64, ampm::Dates.AMPM) @ Dates ~/Development/Julia/aarch64/latest/usr/share/julia/stdlib/v1.12/Dates/src/types.jl:246 [2] parse(::Type{DateTime}, str::String, df::DateFormat{:yyyymmdd, Tuple{Dates.DatePart{'y'}, Dates.DatePart{'m'}, Dates.DatePart{'d'}}}) @ Dates ~/Development/Julia/aarch64/latest/usr/share/julia/stdlib/v1.12/Dates/src/parse.jl:294 [3] DateTime(dt::String, format::String; locale::Dates.DateLocale) @ Dates ~/Development/Julia/aarch64/latest/usr/share/julia/stdlib/v1.12/Dates/src/io.jl:555 [4] DateTime(dt::String, format::String) @ Dates ~/Development/Julia/aarch64/latest/usr/share/julia/stdlib/v1.12/Dates/src/io.jl:554 [5] top-level scope @ REPL[4]:1 ``` This PR makes it so that fixed-width formats require the specified number of digits. I also decided to only add the sign parsing for years to running into performance issues with parsing sign information where it isn't expected.
1 parent d40b54c commit 1e1af76

File tree

3 files changed

+69
-10
lines changed

3 files changed

+69
-10
lines changed

stdlib/Dates/src/io.jl

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,25 @@ end
111111

112112
### Parse tokens
113113

114-
for c in "yYmdHIMS"
114+
for c in "yY"
115+
@eval begin
116+
@inline function tryparsenext(d::DatePart{$c}, str, i, len)
117+
val = tryparsenext_sign(str, i, len)
118+
if val !== nothing
119+
coefficient, i = val
120+
else
121+
coefficient = 1
122+
end
123+
# The sign character does not affect fixed length `DatePart`s
124+
val = tryparsenext_base10(str, i, len, min_width(d), max_width(d))
125+
val === nothing && return nothing
126+
y, ii = val
127+
return y * coefficient, ii
128+
end
129+
end
130+
end
131+
132+
for c in "mdHIMS"
115133
@eval begin
116134
@inline function tryparsenext(d::DatePart{$c}, str, i, len)
117135
return tryparsenext_base10(str, i, len, min_width(d), max_width(d))

stdlib/Dates/src/parse.jl

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -156,18 +156,23 @@ If successful, returns a 2-element tuple `(values, pos)`:
156156
end
157157
end
158158

159+
@inline function tryparsenext_sign(str::AbstractString, i::Int, len::Int)
160+
i > len && return nothing
161+
c, ii = iterate(str, i)::Tuple{Char, Int}
162+
if c == '+'
163+
return 1, ii
164+
elseif c == '-'
165+
return -1, ii
166+
else
167+
return nothing
168+
end
169+
end
170+
159171
@inline function tryparsenext_base10(str::AbstractString, i::Int, len::Int, min_width::Int=1, max_width::Int=0)
160172
i > len && return nothing
161173
min_pos = min_width <= 0 ? i : i + min_width - 1
162174
max_pos = max_width <= 0 ? len : min(i + max_width - 1, len)
163175
d::Int64 = 0
164-
c, neg = iterate(str, i)::Tuple{Char, Int}
165-
if c == '-'
166-
i = neg
167-
neg = -1
168-
else
169-
neg = 1
170-
end
171176
@inbounds while i <= max_pos
172177
c, ii = iterate(str, i)::Tuple{Char, Int}
173178
if '0' <= c <= '9'
@@ -180,7 +185,7 @@ end
180185
if i <= min_pos
181186
return nothing
182187
else
183-
return d * neg, i
188+
return d, i
184189
end
185190
end
186191

@@ -207,10 +212,18 @@ function Base.parse(::Type{DateTime}, s::AbstractString, df::typeof(ISODateTimeF
207212
i, end_pos = firstindex(s), lastindex(s)
208213
i > end_pos && throw(ArgumentError("Cannot parse an empty string as a DateTime"))
209214

215+
coefficient = 1
210216
local dy
211217
dm = dd = Int64(1)
212218
th = tm = ts = tms = Int64(0)
213219

220+
# Optional sign
221+
let val = tryparsenext_sign(s, i, end_pos)
222+
if val !== nothing
223+
coefficient, i = val
224+
end
225+
end
226+
214227
let val = tryparsenext_base10(s, i, end_pos, 1)
215228
val === nothing && @goto error
216229
dy, i = val
@@ -279,7 +292,7 @@ function Base.parse(::Type{DateTime}, s::AbstractString, df::typeof(ISODateTimeF
279292
end
280293

281294
@label done
282-
return DateTime(dy, dm, dd, th, tm, ts, tms)
295+
return DateTime(dy * coefficient, dm, dd, th, tm, ts, tms)
283296

284297
@label error
285298
throw(ArgumentError("Invalid DateTime string"))

stdlib/Dates/test/io.jl

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,23 @@ end
325325
# From Matt Bauman
326326
f = "yyyy-mm-ddTHH:MM:SS"
327327
@test Dates.DateTime("2014-05-28T16:46:04", f) == Dates.DateTime(2014, 5, 28, 16, 46, 04)
328+
329+
f = "yyyymmdd"
330+
@test Dates.DateTime("20240521", f) == Dates.DateTime(2024, 5, 21)
331+
@test Dates.DateTime("-20240521", f) == Dates.DateTime(-2024, 5, 21)
332+
@test Dates.DateTime("+20240521", f) == Dates.DateTime(2024, 5, 21)
333+
f = "YYYYmmdd"
334+
@test Dates.DateTime("20240521", f) == Dates.DateTime(2024, 5, 21)
335+
@test Dates.DateTime("-20240521", f) == Dates.DateTime(-2024, 5, 21)
336+
@test Dates.DateTime("+20240521", f) == Dates.DateTime(2024, 5, 21)
337+
f = "-yyyymmdd"
338+
@test Dates.DateTime("-20240521", f) == Dates.DateTime(2024, 5, 21)
339+
@test_throws ArgumentError Dates.DateTime("+20240521", f)
340+
@test_throws ArgumentError Dates.DateTime("20240521", f)
341+
f = "-YYYYmmdd"
342+
@test Dates.DateTime("-20240521", f) == Dates.DateTime(2024, 5, 21)
343+
@test_throws ArgumentError Dates.DateTime("+20240521", f)
344+
@test_throws ArgumentError Dates.DateTime("20240521", f)
328345
end
329346

330347
@testset "Error handling" begin
@@ -403,6 +420,17 @@ end
403420
@test_throws ArgumentError parse(Date, "Foo, 12 Nov 2016 07:45:36", Dates.RFC1123Format)
404421
end
405422

423+
@testset "ISODateTimeFormat" begin
424+
dt = Dates.DateTime(2024, 5, 21, 10, 57, 22)
425+
neg_dt = Dates.DateTime(-2024, 5, 21, 10, 57, 22)
426+
@test parse(Dates.DateTime, "2024-05-21T10:57:22", Dates.ISODateTimeFormat) == dt
427+
@test parse(Dates.DateTime, "+2024-05-21T10:57:22", Dates.ISODateTimeFormat) == dt
428+
@test parse(Dates.DateTime, "-2024-05-21T10:57:22", Dates.ISODateTimeFormat) == neg_dt
429+
430+
@test_throws ArgumentError parse(Dates.DateTime, "-", Dates.ISODateTimeFormat)
431+
@test_throws ArgumentError parse(Dates.DateTime, "+", Dates.ISODateTimeFormat)
432+
end
433+
406434
@testset "Issue 15195" begin
407435
f = "YY"
408436
@test Dates.format(Dates.Date(1999), f) == "1999"

0 commit comments

Comments
 (0)