From 246b064fe13e8ad418191423e7baea70f5c9aef0 Mon Sep 17 00:00:00 2001 From: GHTaarn <62629455+GHTaarn@users.noreply.github.com> Date: Sat, 25 Jan 2025 12:49:05 +0100 Subject: [PATCH 01/15] ZonedDateTime can parse its own String output (#470) --- src/parse.jl | 16 +++++++++++++++- test/parse.jl | 11 ++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/parse.jl b/src/parse.jl index 2ef08fd0c..ec00e797a 100644 --- a/src/parse.jl +++ b/src/parse.jl @@ -15,6 +15,12 @@ const ISOZonedDateTimeFormat = let DateFormat("yyyy-mm-ddTHH:MM:SS.ssszzz") end +const NoMillisecondFormat = let + init_dates_extension() + DateFormat("yyyy-mm-ddTHH:MM:SSzzz") +end + +Base.parse(::Type{ZonedDateTime}, str::AbstractString) = ZonedDateTime(str) Dates.default_format(::Type{ZonedDateTime}) = ISOZonedDateTimeFormat function tryparsenext_fixedtz(str, i, len, min_width::Int=1, max_width::Int=0) @@ -97,7 +103,15 @@ function Dates.format(io::IO, d::DatePart{'Z'}, zdt, locale) write(io, string(zdt.zone)) # In most cases will be an abbreviation. end -function ZonedDateTime(str::AbstractString, df::DateFormat=ISOZonedDateTimeFormat) +function ZonedDateTime(str::AbstractString) + if length(str) < 20 || str[20] == '.' + ZonedDateTime(str, ISOZonedDateTimeFormat) + else + ZonedDateTime(str, NoMillisecondFormat) + end +end + +function ZonedDateTime(str::AbstractString, df::DateFormat) try parse(ZonedDateTime, str, df) catch e diff --git a/test/parse.jl b/test/parse.jl index 100aaebd8..5eb7aef0b 100644 --- a/test/parse.jl +++ b/test/parse.jl @@ -1,4 +1,4 @@ -using Dates: parse_components, default_format +using Dates: parse_components, default_format, Millisecond using TimeZones: ParseNextError, _parsesub_tzabbr, _parsesub_offset, _parsesub_time, _parsesub_tzdate, _parsesub_tz @testset "parse" begin @@ -78,6 +78,15 @@ end end end +@testset "self parseable" begin + tt = [ZonedDateTime(2025, 2, 28, 14, 22, tz"UTC")] + push!(tt, tt[1] + Millisecond(55)) + for t in tt + @test t == ZonedDateTime(string(t)) + @test t == parse(ZonedDateTime, string(t)) + end +end + @testset "_parsesub_tzabbr" begin empty_msg = "Time zone abbreviation must start with a letter or the less-than (<) character" not_closed_msg = "Expected expanded time zone abbreviation end with the greater-than sign (>)" From 8e682eda05516ff0b9e38aa7a53aa043817e238c Mon Sep 17 00:00:00 2001 From: GHTaarn <62629455+GHTaarn@users.noreply.github.com> Date: Sat, 25 Jan 2025 12:56:28 +0100 Subject: [PATCH 02/15] Removed Dates.default_format(::Type{ZonedDateTime}) The previous commit makes it unnecessary --- src/parse.jl | 1 - test/parse.jl | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/parse.jl b/src/parse.jl index ec00e797a..ca4679da2 100644 --- a/src/parse.jl +++ b/src/parse.jl @@ -21,7 +21,6 @@ const NoMillisecondFormat = let end Base.parse(::Type{ZonedDateTime}, str::AbstractString) = ZonedDateTime(str) -Dates.default_format(::Type{ZonedDateTime}) = ISOZonedDateTimeFormat function tryparsenext_fixedtz(str, i, len, min_width::Int=1, max_width::Int=0) i == len && str[i] === 'Z' && return ("Z", i+1) diff --git a/test/parse.jl b/test/parse.jl index 5eb7aef0b..0eef6ce71 100644 --- a/test/parse.jl +++ b/test/parse.jl @@ -1,4 +1,4 @@ -using Dates: parse_components, default_format, Millisecond +using Dates: parse_components, Millisecond using TimeZones: ParseNextError, _parsesub_tzabbr, _parsesub_offset, _parsesub_time, _parsesub_tzdate, _parsesub_tz @testset "parse" begin @@ -49,10 +49,6 @@ end @test parse_components(test...) == expected end -@testset "default format" begin - @test default_format(ZonedDateTime) === TimeZones.ISOZonedDateTimeFormat -end - @testset "parse constructor" begin @test isequal( ZonedDateTime("2000-01-02T03:04:05.006+0700"), From 0a067de8868e116d5e193fa2d0b4f90b47f31c58 Mon Sep 17 00:00:00 2001 From: GHTaarn <62629455+GHTaarn@users.noreply.github.com> Date: Fri, 31 Jan 2025 20:26:41 +0100 Subject: [PATCH 03/15] Handle >4 digit year in ZonedDateTime(::AbstractString) --- src/parse.jl | 7 ++----- test/parse.jl | 9 ++++++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/parse.jl b/src/parse.jl index ca4679da2..badcb61de 100644 --- a/src/parse.jl +++ b/src/parse.jl @@ -103,11 +103,8 @@ function Dates.format(io::IO, d::DatePart{'Z'}, zdt, locale) end function ZonedDateTime(str::AbstractString) - if length(str) < 20 || str[20] == '.' - ZonedDateTime(str, ISOZonedDateTimeFormat) - else - ZonedDateTime(str, NoMillisecondFormat) - end + res = tryparse(ZonedDateTime, str, ISOZonedDateTimeFormat) + isnothing(res) ? ZonedDateTime(str, NoMillisecondFormat) : res end function ZonedDateTime(str::AbstractString, df::DateFormat) diff --git a/test/parse.jl b/test/parse.jl index 0eef6ce71..31ce6115c 100644 --- a/test/parse.jl +++ b/test/parse.jl @@ -70,13 +70,16 @@ end catch e @test e isa ArgumentError @test occursin(str, e.msg) - @test occursin(string(TimeZones.ISOZonedDateTimeFormat), e.msg) + @test occursin(string(TimeZones.ISOZonedDateTimeFormat), e.msg) || + occursin(string(TimeZones.NoMillisecondFormat), e.msg) end end @testset "self parseable" begin - tt = [ZonedDateTime(2025, 2, 28, 14, 22, tz"UTC")] - push!(tt, tt[1] + Millisecond(55)) + tt = [ZonedDateTime(2025, 2, 28, 14, 22, tz"UTC"), + ZonedDateTime(20025, 2, 28, 14, tz"UTC+05"), + ] + push!(tt, tt[1] + Millisecond(55), tt[2] + Millisecond(4)) for t in tt @test t == ZonedDateTime(string(t)) @test t == parse(ZonedDateTime, string(t)) From c885cab8dfc2690d0961e17f6aacf25935aab0f1 Mon Sep 17 00:00:00 2001 From: Curtis Vogt Date: Fri, 31 Jan 2025 13:54:42 -0600 Subject: [PATCH 04/15] Call `init_dates_extension` once from top-level --- src/parse.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/parse.jl b/src/parse.jl index badcb61de..8c881bbdd 100644 --- a/src/parse.jl +++ b/src/parse.jl @@ -10,14 +10,14 @@ function init_dates_extension() ) end -const ISOZonedDateTimeFormat = let +begin + # Needs to be initialized to construct formats init_dates_extension() - DateFormat("yyyy-mm-ddTHH:MM:SS.ssszzz") -end -const NoMillisecondFormat = let - init_dates_extension() - DateFormat("yyyy-mm-ddTHH:MM:SSzzz") + # Follows the ISO 8601 standard for date and time with an offset. See + # `Dates.ISODateTimeFormat` for the `DateTime` equivalent. + const ISOZonedDateTimeFormat = DateFormat("yyyy-mm-ddTHH:MM:SS.ssszzz") + const NoMillisecondFormat = DateFormat("yyyy-mm-ddTHH:MM:SSzzz") end Base.parse(::Type{ZonedDateTime}, str::AbstractString) = ZonedDateTime(str) From b5814faf02df0ef90c46dd60eca0b880663026ec Mon Sep 17 00:00:00 2001 From: Curtis Vogt Date: Fri, 31 Jan 2025 13:56:42 -0600 Subject: [PATCH 05/15] Ensure parsing finds a literal `T` --- src/parse.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/parse.jl b/src/parse.jl index 8c881bbdd..eeac46f9d 100644 --- a/src/parse.jl +++ b/src/parse.jl @@ -16,8 +16,8 @@ begin # Follows the ISO 8601 standard for date and time with an offset. See # `Dates.ISODateTimeFormat` for the `DateTime` equivalent. - const ISOZonedDateTimeFormat = DateFormat("yyyy-mm-ddTHH:MM:SS.ssszzz") - const NoMillisecondFormat = DateFormat("yyyy-mm-ddTHH:MM:SSzzz") + const ISOZonedDateTimeFormat = DateFormat("yyyy-mm-dd\\THH:MM:SS.ssszzz") + const NoMillisecondFormat = DateFormat("yyyy-mm-dd\\THH:MM:SSzzz") end Base.parse(::Type{ZonedDateTime}, str::AbstractString) = ZonedDateTime(str) From 6dd1df1ac894145d1da5aa5a4ead9d75af1431ec Mon Sep 17 00:00:00 2001 From: Curtis Vogt Date: Fri, 31 Jan 2025 14:02:09 -0600 Subject: [PATCH 06/15] Rename `NoMillisecondFormat` to `ISOZonedDateTimeNoMillisecondFormat` --- src/parse.jl | 4 ++-- test/parse.jl | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/parse.jl b/src/parse.jl index eeac46f9d..553aa8842 100644 --- a/src/parse.jl +++ b/src/parse.jl @@ -17,7 +17,7 @@ begin # Follows the ISO 8601 standard for date and time with an offset. See # `Dates.ISODateTimeFormat` for the `DateTime` equivalent. const ISOZonedDateTimeFormat = DateFormat("yyyy-mm-dd\\THH:MM:SS.ssszzz") - const NoMillisecondFormat = DateFormat("yyyy-mm-dd\\THH:MM:SSzzz") + const ISOZonedDateTimeNoMillisecondFormat = DateFormat("yyyy-mm-dd\\THH:MM:SSzzz") end Base.parse(::Type{ZonedDateTime}, str::AbstractString) = ZonedDateTime(str) @@ -104,7 +104,7 @@ end function ZonedDateTime(str::AbstractString) res = tryparse(ZonedDateTime, str, ISOZonedDateTimeFormat) - isnothing(res) ? ZonedDateTime(str, NoMillisecondFormat) : res + return isnothing(res) ? ZonedDateTime(str, ISOZonedDateTimeNoMillisecondFormat) : res end function ZonedDateTime(str::AbstractString, df::DateFormat) diff --git a/test/parse.jl b/test/parse.jl index 31ce6115c..e5bb8e5bb 100644 --- a/test/parse.jl +++ b/test/parse.jl @@ -71,7 +71,7 @@ end @test e isa ArgumentError @test occursin(str, e.msg) @test occursin(string(TimeZones.ISOZonedDateTimeFormat), e.msg) || - occursin(string(TimeZones.NoMillisecondFormat), e.msg) + occursin(string(TimeZones.ISOZonedDateTimeNoMillisecondFormat), e.msg) end end From 9e413e2d9f205fda9ead0ea1f4aa1cfb2ec52211 Mon Sep 17 00:00:00 2001 From: Curtis Vogt Date: Fri, 31 Jan 2025 14:03:40 -0600 Subject: [PATCH 07/15] Add exhaustive self-parse tests --- test/parse.jl | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/test/parse.jl b/test/parse.jl index e5bb8e5bb..f8c5dfe07 100644 --- a/test/parse.jl +++ b/test/parse.jl @@ -21,7 +21,6 @@ using TimeZones: ParseNextError, _parsesub_tzabbr, _parsesub_offset, _parsesub_t parse(ZonedDateTime, Test.GenericString("2018-01-01 00:00 UTC"), dateformat"yyyy-mm-dd HH:MM ZZZ"), ZonedDateTime(2018, 1, 1, 0, tz"UTC"), ) - end @testset "tryparse" begin @@ -76,13 +75,20 @@ end end @testset "self parseable" begin - tt = [ZonedDateTime(2025, 2, 28, 14, 22, tz"UTC"), - ZonedDateTime(20025, 2, 28, 14, tz"UTC+05"), - ] - push!(tt, tt[1] + Millisecond(55), tt[2] + Millisecond(4)) - for t in tt - @test t == ZonedDateTime(string(t)) - @test t == parse(ZonedDateTime, string(t)) + zdt_args = Iterators.product( + [0, 1, 10, 100, 1000, 10000], # Year + [1, 12], # Month + [3, 31], # Day + [0, 4, 23], # Hour + [0, 5, 55], # Minute + [0, 6, 56], # Seconds + [0, 7, 77, 777], # Milliseconds + [tz"UTC"], # Time zones + ) + for args in zdt_args + zdt = ZonedDateTime(args...) + @test zdt == parse(ZonedDateTime, string(zdt)) + @test zdt == ZonedDateTime(string(zdt)) end end From 68a90ec49f927f9c47bca1c5fe61fcf05d9aa3ff Mon Sep 17 00:00:00 2001 From: Curtis Vogt Date: Fri, 31 Jan 2025 14:08:08 -0600 Subject: [PATCH 08/15] Treat `parse` as primary parsing functions --- src/parse.jl | 44 ++++++++++++++++---------------------- src/types/zoneddatetime.jl | 9 ++++++++ test/parse.jl | 34 +++++++++++++++++++---------- 3 files changed, 51 insertions(+), 36 deletions(-) diff --git a/src/parse.jl b/src/parse.jl index 553aa8842..9b479fc66 100644 --- a/src/parse.jl +++ b/src/parse.jl @@ -20,7 +20,25 @@ begin const ISOZonedDateTimeNoMillisecondFormat = DateFormat("yyyy-mm-dd\\THH:MM:SSzzz") end -Base.parse(::Type{ZonedDateTime}, str::AbstractString) = ZonedDateTime(str) +function Base.parse(::Type{ZonedDateTime}, str::AbstractString) + res = tryparse(ZonedDateTime, str, ISOZonedDateTimeFormat) + return isnothing(res) ? parse(ZonedDateTime, str, ISOZonedDateTimeNoMillisecondFormat) : res +end + +function Base.parse(::Type{ZonedDateTime}, str::AbstractString, df::DateFormat) + argtypes = Tuple{Type{<:TimeType},AbstractString,DateFormat} + try + invoke(parse, argtypes, ZonedDateTime, str, df) + catch e + if e isa ArgumentError + rethrow(ArgumentError( + "Unable to parse string \"$str\" using format $df. $(e.msg)" + )) + else + rethrow() + end + end +end function tryparsenext_fixedtz(str, i, len, min_width::Int=1, max_width::Int=0) i == len && str[i] === 'Z' && return ("Z", i+1) @@ -102,30 +120,6 @@ function Dates.format(io::IO, d::DatePart{'Z'}, zdt, locale) write(io, string(zdt.zone)) # In most cases will be an abbreviation. end -function ZonedDateTime(str::AbstractString) - res = tryparse(ZonedDateTime, str, ISOZonedDateTimeFormat) - return isnothing(res) ? ZonedDateTime(str, ISOZonedDateTimeNoMillisecondFormat) : res -end - -function ZonedDateTime(str::AbstractString, df::DateFormat) - try - parse(ZonedDateTime, str, df) - catch e - if e isa ArgumentError - rethrow(ArgumentError( - "Unable to parse string \"$str\" using format $df. $(e.msg)" - )) - else - rethrow() - end - end -end - -function ZonedDateTime(str::AbstractString, format::AbstractString; locale::AbstractString="english") - ZonedDateTime(str, DateFormat(format, locale)) -end - - """ _parsesub_tzabbr(str, [i, len]) -> Union{Tuple{AbstractString, Integer}, Exception} diff --git a/src/types/zoneddatetime.jl b/src/types/zoneddatetime.jl index 87503e4ec..65f3cfaad 100644 --- a/src/types/zoneddatetime.jl +++ b/src/types/zoneddatetime.jl @@ -166,6 +166,15 @@ function ZonedDateTime(date::Date, args...; kwargs...) return ZonedDateTime(DateTime(date), args...; kwargs...) end +# Parsing constructors + +ZonedDateTime(str::AbstractString) = parse(ZonedDateTime, str) +ZonedDateTime(str::AbstractString, df::DateFormat) = parse(ZonedDateTime, str, df) + +function ZonedDateTime(str::AbstractString, format::AbstractString; locale::AbstractString="english") + return parse(ZonedDateTime, str, DateFormat(format, locale)) +end + # Promotion # Because of the promoting fallback definitions for TimeType, we need a special case for diff --git a/test/parse.jl b/test/parse.jl index f8c5dfe07..dcfabf2cd 100644 --- a/test/parse.jl +++ b/test/parse.jl @@ -61,17 +61,6 @@ end ZonedDateTime("2018-11-01-0600", dateformat"yyyy-mm-ddzzzz"), ZonedDateTime(2018, 11, 1, tz"UTC-06"), ) - - # Validate that error message contains the original string and the format used - str = "2018-11-01" - try - ZonedDateTime(str) - catch e - @test e isa ArgumentError - @test occursin(str, e.msg) - @test occursin(string(TimeZones.ISOZonedDateTimeFormat), e.msg) || - occursin(string(TimeZones.ISOZonedDateTimeNoMillisecondFormat), e.msg) - end end @testset "self parseable" begin @@ -92,6 +81,29 @@ end end end +# Validate that error message contains the original string and the format used +@testset "contextual error" begin + str = "2018-11-01" + + try + parse(ZonedDateTime, str) + @test false + catch e + @test e isa ArgumentError + @test occursin(str, e.msg) + @test occursin(string(TimeZones.ISOZonedDateTimeNoMillisecondFormat), e.msg) + end + + try + ZonedDateTime(str) + @test false + catch e + @test e isa ArgumentError + @test occursin(str, e.msg) + @test occursin(string(TimeZones.ISOZonedDateTimeNoMillisecondFormat), e.msg) + end +end + @testset "_parsesub_tzabbr" begin empty_msg = "Time zone abbreviation must start with a letter or the less-than (<) character" not_closed_msg = "Expected expanded time zone abbreviation end with the greater-than sign (>)" From 3e4ead3f4b86b8927d8af4624362cfd071a89739 Mon Sep 17 00:00:00 2001 From: Curtis Vogt Date: Fri, 31 Jan 2025 14:22:12 -0600 Subject: [PATCH 09/15] Improve parsing heuristic --- src/parse.jl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/parse.jl b/src/parse.jl index 9b479fc66..6f477b4ee 100644 --- a/src/parse.jl +++ b/src/parse.jl @@ -21,8 +21,12 @@ begin end function Base.parse(::Type{ZonedDateTime}, str::AbstractString) - res = tryparse(ZonedDateTime, str, ISOZonedDateTimeFormat) - return isnothing(res) ? parse(ZonedDateTime, str, ISOZonedDateTimeNoMillisecondFormat) : res + # Works as the format should only contain a period when milliseconds are included + return if contains(str, '.') + parse(ZonedDateTime, str, ISOZonedDateTimeFormat) + else + parse(ZonedDateTime, str, ISOZonedDateTimeNoMillisecondFormat) + end end function Base.parse(::Type{ZonedDateTime}, str::AbstractString, df::DateFormat) From 8b42414ffd76c59601a318345a8bbefa8aad71e1 Mon Sep 17 00:00:00 2001 From: Curtis Vogt Date: Fri, 31 Jan 2025 14:40:38 -0600 Subject: [PATCH 10/15] Deprecate `Dates.default_format` --- src/deprecated.jl | 10 ++++++++++ test/deprecated.jl | 3 +++ test/runtests.jl | 1 + 3 files changed, 14 insertions(+) create mode 100644 test/deprecated.jl diff --git a/src/deprecated.jl b/src/deprecated.jl index be1383384..5d219eee0 100644 --- a/src/deprecated.jl +++ b/src/deprecated.jl @@ -13,4 +13,14 @@ const TransitionTimeInfo = TZFile.TransitionTimeInfo @deprecate build(; force=false) build(TZJData.TZDATA_VERSION; force) +function Dates.default_format(::Type{ZonedDateTime}) + depwarn( + "`$(Dates.default_format)(ZonedDateTime)` is deprecated and has direct " * + "replacement. Consider using refactoring to use " * + "`parse(::Type{ZonedDateTime}, ::AbstractString)` as an alternative.", + :default_format, + ) + return ISOZonedDateTimeFormat +end + # END TimeZones 1.0 deprecations diff --git a/test/deprecated.jl b/test/deprecated.jl new file mode 100644 index 000000000..8dee8ec9a --- /dev/null +++ b/test/deprecated.jl @@ -0,0 +1,3 @@ +@testset "default format" begin + @test default_format(ZonedDateTime) === TimeZones.ISOZonedDateTimeFormat +end diff --git a/test/runtests.jl b/test/runtests.jl index e38eb8c53..b04df5cc5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -77,4 +77,5 @@ include("helpers.jl") include("rounding.jl") include("parse.jl") include("plotting.jl") + include("deprecated.jl") end From 4f6add72dd406bff0a22a370a752b448fffe2c38 Mon Sep 17 00:00:00 2001 From: Curtis Vogt Date: Fri, 31 Jan 2025 14:55:31 -0600 Subject: [PATCH 11/15] fixup! Deprecate `Dates.default_format` --- src/deprecated.jl | 4 ++-- test/deprecated.jl | 2 +- test/parse.jl | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/deprecated.jl b/src/deprecated.jl index 5d219eee0..cbc24c8d5 100644 --- a/src/deprecated.jl +++ b/src/deprecated.jl @@ -1,4 +1,4 @@ -using Base: @deprecate +using Base: @deprecate, depwarn # BEGIN TimeZones 1.0 deprecations @@ -15,7 +15,7 @@ const TransitionTimeInfo = TZFile.TransitionTimeInfo function Dates.default_format(::Type{ZonedDateTime}) depwarn( - "`$(Dates.default_format)(ZonedDateTime)` is deprecated and has direct " * + "`Dates.default_format(ZonedDateTime)` is deprecated and has no direct " * "replacement. Consider using refactoring to use " * "`parse(::Type{ZonedDateTime}, ::AbstractString)` as an alternative.", :default_format, diff --git a/test/deprecated.jl b/test/deprecated.jl index 8dee8ec9a..1c41c1b92 100644 --- a/test/deprecated.jl +++ b/test/deprecated.jl @@ -1,3 +1,3 @@ @testset "default format" begin - @test default_format(ZonedDateTime) === TimeZones.ISOZonedDateTimeFormat + @test Dates.default_format(ZonedDateTime) === TimeZones.ISOZonedDateTimeFormat end diff --git a/test/parse.jl b/test/parse.jl index dcfabf2cd..d944b63af 100644 --- a/test/parse.jl +++ b/test/parse.jl @@ -1,4 +1,4 @@ -using Dates: parse_components, Millisecond +using Dates: parse_components using TimeZones: ParseNextError, _parsesub_tzabbr, _parsesub_offset, _parsesub_time, _parsesub_tzdate, _parsesub_tz @testset "parse" begin From 18176293123846b974c221cf55178f0fb849de91 Mon Sep 17 00:00:00 2001 From: GHTaarn <62629455+GHTaarn@users.noreply.github.com> Date: Sat, 1 Feb 2025 18:17:54 +0100 Subject: [PATCH 12/15] Docstrings for ZonedDateTime(::AbstractString,...) --- src/types/zoneddatetime.jl | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/types/zoneddatetime.jl b/src/types/zoneddatetime.jl index 65f3cfaad..ef63b771a 100644 --- a/src/types/zoneddatetime.jl +++ b/src/types/zoneddatetime.jl @@ -168,7 +168,21 @@ end # Parsing constructors +""" + ZonedDateTime(str::AbstractString) + +Construct a `ZonedDateTime` by parsing `str`. This method is designed so that +`zdt == ZonedDateTime(string(zdt))` where `zdt` can be any `ZonedDateTime` +object. +""" ZonedDateTime(str::AbstractString) = parse(ZonedDateTime, str) + +""" + ZonedDateTime(str::AbstractString, df::DateFormat) + +Construct a `ZonedDateTime` by parsing `str` according to the format specified +in `df`. +""" ZonedDateTime(str::AbstractString, df::DateFormat) = parse(ZonedDateTime, str, df) function ZonedDateTime(str::AbstractString, format::AbstractString; locale::AbstractString="english") From e43de3522c149e3996d651b6d4a9400e477b8d6d Mon Sep 17 00:00:00 2001 From: GHTaarn <62629455+GHTaarn@users.noreply.github.com> Date: Sat, 1 Feb 2025 18:58:17 +0100 Subject: [PATCH 13/15] Extra dosctring for Dates.DateFormat --- src/parse.jl | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/parse.jl b/src/parse.jl index 6f477b4ee..81529a5ee 100644 --- a/src/parse.jl +++ b/src/parse.jl @@ -20,6 +20,18 @@ begin const ISOZonedDateTimeNoMillisecondFormat = DateFormat("yyyy-mm-dd\\THH:MM:SSzzz") end +@doc """ + DateFormat(format::AbstractString, locale="english") --> DateFormat + +When the `TimeZones` package is loaded, 2 extra character codes are available +for constructing the `format` string: + +| Code | Matches | Comment | +|:-----------|:----------|:--------------------------------------------------------------| +| `z` | +02:00 | Numeric UTC offset | +| `Z` | GST, UTC | Time zone abbreviation | +""" DateFormat + function Base.parse(::Type{ZonedDateTime}, str::AbstractString) # Works as the format should only contain a period when milliseconds are included return if contains(str, '.') From f66c6dffdf29cd010bf0ccc03a4859bcf221ddec Mon Sep 17 00:00:00 2001 From: GHTaarn <62629455+GHTaarn@users.noreply.github.com> Date: Sat, 1 Feb 2025 19:01:00 +0100 Subject: [PATCH 14/15] More self-parse tests --- test/parse.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/parse.jl b/test/parse.jl index d944b63af..bf1709a12 100644 --- a/test/parse.jl +++ b/test/parse.jl @@ -65,14 +65,14 @@ end @testset "self parseable" begin zdt_args = Iterators.product( - [0, 1, 10, 100, 1000, 10000], # Year + [0, 1, 10, 100, 1000, 2025, 10000], # Year [1, 12], # Month [3, 31], # Day [0, 4, 23], # Hour [0, 5, 55], # Minute [0, 6, 56], # Seconds - [0, 7, 77, 777], # Milliseconds - [tz"UTC"], # Time zones + [0, 7, 50, 77, 777], # Milliseconds + [tz"UTC-06", tz"UTC", tz"UTC+08:45", tz"UTC+14"], # Time zones ) for args in zdt_args zdt = ZonedDateTime(args...) From b51e9034458259b941fca229a70bb274dc6862ff Mon Sep 17 00:00:00 2001 From: Curtis Vogt Date: Mon, 3 Feb 2025 14:16:21 -0600 Subject: [PATCH 15/15] Documentation update --- src/parse.jl | 8 ++++---- src/types/zoneddatetime.jl | 24 +++++++++++++++++++++++- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/parse.jl b/src/parse.jl index 81529a5ee..2e17f95ad 100644 --- a/src/parse.jl +++ b/src/parse.jl @@ -26,10 +26,10 @@ end When the `TimeZones` package is loaded, 2 extra character codes are available for constructing the `format` string: -| Code | Matches | Comment | -|:-----------|:----------|:--------------------------------------------------------------| -| `z` | +02:00 | Numeric UTC offset | -| `Z` | GST, UTC | Time zone abbreviation | +| Code | Matches | Comment | +|:-----------|:-------------------|:------------------------------------------------------| +| `z` | +02:00, -0100, +14 | Parsing matches a fixed numeric UTC offset `±hh:mm`, `±hhmm`, or `±hh`. Formatting outputs `±hh:mm` | +| `Z` | UTC, GMT, America/New_York | Name of a time zone as specified in the IANA tz database | """ DateFormat function Base.parse(::Type{ZonedDateTime}, str::AbstractString) diff --git a/src/types/zoneddatetime.jl b/src/types/zoneddatetime.jl index ef63b771a..5191f8304 100644 --- a/src/types/zoneddatetime.jl +++ b/src/types/zoneddatetime.jl @@ -173,7 +173,29 @@ end Construct a `ZonedDateTime` by parsing `str`. This method is designed so that `zdt == ZonedDateTime(string(zdt))` where `zdt` can be any `ZonedDateTime` -object. +object. Take note that this method will always create a `ZonedDateTime` with a +`FixedTimeZone` which can result in different results with date/time arithmetic. + +## Examples +```jltest +julia> zdt = ZonedDateTime(2025, 3, 8, 9, tz"America/New_York") +2025-03-08T09:00:00-05:00 + +julia> timezone(zdt) +America/New_York (UTC-5/UTC-4) + +julia> zdt + Day(1) +2025-03-09T09:00:00-04:00 + +julia> pzdt = ZonedDateTime(string(zdt)) +2025-03-08T09:00:00-05:00 + +julia> timezone(pzdt) +UTC-05:00 + +julia> pzdt + Day(1) +2025-03-09T09:00:00-05:00 +``` """ ZonedDateTime(str::AbstractString) = parse(ZonedDateTime, str)