Skip to content

Commit

Permalink
Dates: Error on construction/parsing with empty strings (JuliaLang#47117
Browse files Browse the repository at this point in the history
)

When attempting to construct a `DateTime`, `Date` or `Time` from an
`AbstractString`, throw an `ArgumentError` if the string is empty.
Likewise, error when `parse`ing an empty string as one of these types,
and return `nothing` from `tryparse`.

This behavior differs from previously.  Before, `Date` and `Time` would
return default values of `Date(1)` and `Time(0)`, respectively, while
`DateTime` would error without a `format` argument.  With a `format`
argument, it would return `DateTime(1)`.  However, this appears not to
have been explicitly intended, but rather a consequence of the way
parsing was implemented; no tests for empty string parsing existed.

This addresses JuliaLang#28090 and JuliaLang#43883; see discussion therein.

Summary of changes:
- Check for empty string in `Base.parse(::DateTime)` and throw if so.
- Change documentation to mention this.
- Add a compat notice to the docs contrasting the old behavior.
  • Loading branch information
anowacki authored Nov 10, 2022
1 parent e0ba28a commit 317211a
Show file tree
Hide file tree
Showing 4 changed files with 44 additions and 10 deletions.
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ Standard library changes

#### Dates

* Empty strings are no longer incorrectly parsed as valid `DateTime`s, `Date`s or `Time`s and instead throw an
`ArgumentError` in constructors and `parse`, while `nothing` is returned by `tryparse` ([#47117]).

#### Downloads

#### Statistics
Expand Down
18 changes: 8 additions & 10 deletions stdlib/Dates/docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,7 @@ missing parts of dates and times so long as the preceding parts are given. The o
default values. For example, `Date("1981-03", dateformat"y-m-d")` returns `1981-03-01`, whilst
`Date("31/12", dateformat"d/m/y")` gives `0001-12-31`. (Note that the default year is
1 AD/CE.)
Consequently, an empty string will always return `0001-01-01` for `Date`s,
and `0001-01-01T00:00:00.000` for `DateTime`s.
An empty string, however, always throws an `ArgumentError`.

Fixed-width slots are specified by repeating the period character the number of times corresponding
to the width with no delimiter between characters. So `dateformat"yyyymmdd"` would correspond to a date
Expand Down Expand Up @@ -153,14 +152,13 @@ an optional third argument of type `DateFormat` specifying the format; for examp
`parse(Date, "06.23.2013", dateformat"m.d.y")`, or
`tryparse(DateTime, "1999-12-31T23:59:59")` which uses the default format.
The notable difference between the functions is that with [`tryparse`](@ref),
an error is not thrown if the string is in an invalid format;
instead `nothing` is returned. Note however that as with the constructors
above, empty date and time parts assume
default values and consequently an empty string (`""`) is valid
for _any_ `DateFormat`, giving for example a `Date` of `0001-01-01`. Code
relying on `parse` or `tryparse` for `Date` and `DateTime` parsing should
therefore also check whether parsed strings are empty before using the
result.
an error is not thrown if the string is empty or in an invalid format;
instead `nothing` is returned.

!!! compat "Julia 1.9"
Before Julia 1.9, empty strings could be passed to constructors and `parse`
without error, returning as appropriate `DateTime(1)`, `Date(1)` or `Time(0)`.
Likewise, `tryparse` did not return `nothing`.

A full suite of parsing and formatting tests and examples is available in [`stdlib/Dates/test/io.jl`](https://github.com/JuliaLang/julia/blob/master/stdlib/Dates/test/io.jl).

Expand Down
3 changes: 3 additions & 0 deletions stdlib/Dates/src/parse.jl
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ end

function Base.parse(::Type{DateTime}, s::AbstractString, df::typeof(ISODateTimeFormat))
i, end_pos = firstindex(s), lastindex(s)
i > end_pos && throw(ArgumentError("Cannot parse an empty string as a DateTime"))

local dy
dm = dd = Int64(1)
Expand Down Expand Up @@ -279,6 +280,7 @@ end

function Base.parse(::Type{T}, str::AbstractString, df::DateFormat=default_format(T)) where T<:TimeType
pos, len = firstindex(str), lastindex(str)
pos > len && throw(ArgumentError("Cannot parse an empty string as a Date or Time"))
val = tryparsenext_internal(T, str, pos, len, df, true)
@assert val !== nothing
values, endpos = val
Expand All @@ -287,6 +289,7 @@ end

function Base.tryparse(::Type{T}, str::AbstractString, df::DateFormat=default_format(T)) where T<:TimeType
pos, len = firstindex(str), lastindex(str)
pos > len && return nothing
res = tryparsenext_internal(T, str, pos, len, df, false)
res === nothing && return nothing
values, endpos = res
Expand Down
30 changes: 30 additions & 0 deletions stdlib/Dates/test/io.jl
Original file line number Diff line number Diff line change
Expand Up @@ -588,4 +588,34 @@ end
@test (@inferred Nothing g()) == datetime
end

@testset "Issue #43883: parsing empty strings" begin
for (T, name, fmt) in zip(
(DateTime, Date, Time),
("DateTime", "Date or Time", "Date or Time"),
("yyyy-mm-ddHHMMSS.s", "yyymmdd", "HHMMSS")
)
@test_throws ArgumentError T("")
@test_throws ArgumentError T("", fmt)
@test_throws ArgumentError T("", DateFormat(fmt))
try
T("")
@test false
catch err
@test err.msg == "Cannot parse an empty string as a $name"
end

@test_throws ArgumentError parse(T, "")
@test_throws ArgumentError parse(T, "", DateFormat(fmt))
try
parse(T, "")
@test false
catch err
@test err.msg == "Cannot parse an empty string as a $name"
end

@test tryparse(T, "") === nothing
@test tryparse(T, "", DateFormat(fmt)) === nothing
end
end

end

0 comments on commit 317211a

Please sign in to comment.