Skip to content

Commit

Permalink
Fix fixed-length negative year parsing (JuliaLang#54535)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
omus authored and lazarusA committed Jul 12, 2024
1 parent d40b54c commit 1e1af76
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 10 deletions.
20 changes: 19 additions & 1 deletion stdlib/Dates/src/io.jl
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,25 @@ end

### Parse tokens

for c in "yYmdHIMS"
for c in "yY"
@eval begin
@inline function tryparsenext(d::DatePart{$c}, str, i, len)
val = tryparsenext_sign(str, i, len)
if val !== nothing
coefficient, i = val
else
coefficient = 1
end
# The sign character does not affect fixed length `DatePart`s
val = tryparsenext_base10(str, i, len, min_width(d), max_width(d))
val === nothing && return nothing
y, ii = val
return y * coefficient, ii
end
end
end

for c in "mdHIMS"
@eval begin
@inline function tryparsenext(d::DatePart{$c}, str, i, len)
return tryparsenext_base10(str, i, len, min_width(d), max_width(d))
Expand Down
31 changes: 22 additions & 9 deletions stdlib/Dates/src/parse.jl
Original file line number Diff line number Diff line change
Expand Up @@ -156,18 +156,23 @@ If successful, returns a 2-element tuple `(values, pos)`:
end
end

@inline function tryparsenext_sign(str::AbstractString, i::Int, len::Int)
i > len && return nothing
c, ii = iterate(str, i)::Tuple{Char, Int}
if c == '+'
return 1, ii
elseif c == '-'
return -1, ii
else
return nothing
end
end

@inline function tryparsenext_base10(str::AbstractString, i::Int, len::Int, min_width::Int=1, max_width::Int=0)
i > len && return nothing
min_pos = min_width <= 0 ? i : i + min_width - 1
max_pos = max_width <= 0 ? len : min(i + max_width - 1, len)
d::Int64 = 0
c, neg = iterate(str, i)::Tuple{Char, Int}
if c == '-'
i = neg
neg = -1
else
neg = 1
end
@inbounds while i <= max_pos
c, ii = iterate(str, i)::Tuple{Char, Int}
if '0' <= c <= '9'
Expand All @@ -180,7 +185,7 @@ end
if i <= min_pos
return nothing
else
return d * neg, i
return d, i
end
end

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

coefficient = 1
local dy
dm = dd = Int64(1)
th = tm = ts = tms = Int64(0)

# Optional sign
let val = tryparsenext_sign(s, i, end_pos)
if val !== nothing
coefficient, i = val
end
end

let val = tryparsenext_base10(s, i, end_pos, 1)
val === nothing && @goto error
dy, i = val
Expand Down Expand Up @@ -279,7 +292,7 @@ function Base.parse(::Type{DateTime}, s::AbstractString, df::typeof(ISODateTimeF
end

@label done
return DateTime(dy, dm, dd, th, tm, ts, tms)
return DateTime(dy * coefficient, dm, dd, th, tm, ts, tms)

@label error
throw(ArgumentError("Invalid DateTime string"))
Expand Down
28 changes: 28 additions & 0 deletions stdlib/Dates/test/io.jl
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,23 @@ end
# From Matt Bauman
f = "yyyy-mm-ddTHH:MM:SS"
@test Dates.DateTime("2014-05-28T16:46:04", f) == Dates.DateTime(2014, 5, 28, 16, 46, 04)

f = "yyyymmdd"
@test Dates.DateTime("20240521", f) == Dates.DateTime(2024, 5, 21)
@test Dates.DateTime("-20240521", f) == Dates.DateTime(-2024, 5, 21)
@test Dates.DateTime("+20240521", f) == Dates.DateTime(2024, 5, 21)
f = "YYYYmmdd"
@test Dates.DateTime("20240521", f) == Dates.DateTime(2024, 5, 21)
@test Dates.DateTime("-20240521", f) == Dates.DateTime(-2024, 5, 21)
@test Dates.DateTime("+20240521", f) == Dates.DateTime(2024, 5, 21)
f = "-yyyymmdd"
@test Dates.DateTime("-20240521", f) == Dates.DateTime(2024, 5, 21)
@test_throws ArgumentError Dates.DateTime("+20240521", f)
@test_throws ArgumentError Dates.DateTime("20240521", f)
f = "-YYYYmmdd"
@test Dates.DateTime("-20240521", f) == Dates.DateTime(2024, 5, 21)
@test_throws ArgumentError Dates.DateTime("+20240521", f)
@test_throws ArgumentError Dates.DateTime("20240521", f)
end

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

@testset "ISODateTimeFormat" begin
dt = Dates.DateTime(2024, 5, 21, 10, 57, 22)
neg_dt = Dates.DateTime(-2024, 5, 21, 10, 57, 22)
@test parse(Dates.DateTime, "2024-05-21T10:57:22", Dates.ISODateTimeFormat) == dt
@test parse(Dates.DateTime, "+2024-05-21T10:57:22", Dates.ISODateTimeFormat) == dt
@test parse(Dates.DateTime, "-2024-05-21T10:57:22", Dates.ISODateTimeFormat) == neg_dt

@test_throws ArgumentError parse(Dates.DateTime, "-", Dates.ISODateTimeFormat)
@test_throws ArgumentError parse(Dates.DateTime, "+", Dates.ISODateTimeFormat)
end

@testset "Issue 15195" begin
f = "YY"
@test Dates.format(Dates.Date(1999), f) == "1999"
Expand Down

0 comments on commit 1e1af76

Please sign in to comment.