From 4520c5a4bdaa9f366fc567ccf7d347c69da33aa7 Mon Sep 17 00:00:00 2001 From: Michael Doherty Date: Sun, 29 Jun 2025 16:26:32 +1000 Subject: [PATCH 1/5] Introduce method to convert to Time from TimeType This is to satisfy the expectation that `Time(t::Time) == t`, similar to other subtypes of TimeType and Period. This also resolves issues ranges of Time, such as given by `range(Time(0), step=Hour(1), length=24)`. --- stdlib/Dates/src/conversions.jl | 2 +- stdlib/Dates/test/conversions.jl | 14 ++++++++++++++ stdlib/Dates/test/ranges.jl | 4 +++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/stdlib/Dates/src/conversions.jl b/stdlib/Dates/src/conversions.jl index 65df4c06b64db..fbc2ca9854f54 100644 --- a/stdlib/Dates/src/conversions.jl +++ b/stdlib/Dates/src/conversions.jl @@ -25,7 +25,7 @@ DateTime(dt::TimeType) = convert(DateTime, dt) Convert a `DateTime` to a `Time`. The hour, minute, second, and millisecond parts of the `DateTime` are used to create the new `Time`. Microsecond and nanoseconds are zero by default. """ -Time(dt::DateTime) = convert(Time, dt) +Time(dt::TimeType) = convert(Time, dt) Base.convert(::Type{DateTime}, dt::Date) = DateTime(UTM(value(dt) * 86400000)) Base.convert(::Type{Date}, dt::DateTime) = Date(UTD(days(dt))) diff --git a/stdlib/Dates/test/conversions.jl b/stdlib/Dates/test/conversions.jl index 99572b41b4f90..660542bbbf11a 100644 --- a/stdlib/Dates/test/conversions.jl +++ b/stdlib/Dates/test/conversions.jl @@ -129,4 +129,18 @@ end @test Dates.nanosecond(t) == 0 end +@testset "idempotency of conversion for TimeType subtypes" begin + for T in [DateTime, Date, Time] + @test T(T(0)) == T(0) + end +end + +@testset "idempotency of conversion for Period subtypes" begin + for T in [Nanosecond, Millisecond, + Second, Minute, Hour, + Day, Week, Month, Quarter, Year] + @test T(T(0)) == T(0) + end +end + end diff --git a/stdlib/Dates/test/ranges.jl b/stdlib/Dates/test/ranges.jl index d4339dcde51d4..525af73d2d113 100644 --- a/stdlib/Dates/test/ranges.jl +++ b/stdlib/Dates/test/ranges.jl @@ -539,6 +539,8 @@ dr = Dates.Time(23, 1, 1):Dates.Second(1):Dates.Time(23, 2, 1) dr1 = Dates.Time(23, 1, 1):Dates.Second(1):Dates.Time(23, 1, 1) dr2 = Dates.Time(23, 1, 1):Dates.Second(1):Dates.Time(22, 2, 1) # empty range dr3 = Dates.Time(23, 1, 1):Dates.Minute(-1):Dates.Time(22, 1, 1) # negative step +dr4 = range(Dates.Time(0), step=Dates.Hour(1), length=23) # by step, length + # Big ranges dr8 = typemin(Dates.Time):Dates.Second(1):typemax(Dates.Time) dr9 = typemin(Dates.Time):Dates.Nanosecond(1):typemax(Dates.Time) @@ -555,7 +557,7 @@ dr18 = typemax(Dates.Time):Dates.Minute(-100):typemin(Dates.Time) dr19 = typemax(Dates.Time):Dates.Hour(-10):typemin(Dates.Time) dr20 = typemin(Dates.Time):Dates.Microsecond(2):typemax(Dates.Time) -drs = Any[dr, dr1, dr2, dr3, dr8, dr9, dr10, +drs = Any[dr, dr1, dr2, dr3, dr4, dr8, dr9, dr10, dr11, dr12, dr13, dr14, dr15, dr16, dr17, dr18, dr19, dr20] @test map(length, drs) == map(x->size(x)[1], drs) From abcb6c95d78f31fe641f8678b0b01d430442d7ad Mon Sep 17 00:00:00 2001 From: Michael Doherty Date: Mon, 30 Jun 2025 15:40:58 +1000 Subject: [PATCH 2/5] address feedback by dropping the generic conversion constructor in favor of explicity copy constructors. Additional test to show the periodicity of StepRangeLen{Time} --- stdlib/Dates/src/conversions.jl | 8 ++-- stdlib/Dates/src/types.jl | 5 ++ stdlib/Dates/test/ranges.jl | 82 ++++++++++++++++++++------------- 3 files changed, 59 insertions(+), 36 deletions(-) diff --git a/stdlib/Dates/src/conversions.jl b/stdlib/Dates/src/conversions.jl index fbc2ca9854f54..473d7dea6936a 100644 --- a/stdlib/Dates/src/conversions.jl +++ b/stdlib/Dates/src/conversions.jl @@ -9,15 +9,15 @@ Convert a `DateTime` to a `Date`. The hour, minute, second, and millisecond part the `DateTime` are truncated, so only the year, month and day parts are used in construction. """ -Date(dt::TimeType) = convert(Date, dt) +Date(dt::DateTime) = convert(Date, dt) """ - DateTime(dt::Date) + DateTime(d::Date) Convert a `Date` to a `DateTime`. The hour, minute, second, and millisecond parts of the new `DateTime` are assumed to be zero. """ -DateTime(dt::TimeType) = convert(DateTime, dt) +DateTime(d::Date) = convert(DateTime, d) """ Time(dt::DateTime) @@ -25,7 +25,7 @@ DateTime(dt::TimeType) = convert(DateTime, dt) Convert a `DateTime` to a `Time`. The hour, minute, second, and millisecond parts of the `DateTime` are used to create the new `Time`. Microsecond and nanoseconds are zero by default. """ -Time(dt::TimeType) = convert(Time, dt) +Time(dt::DateTime) = convert(Time, dt) Base.convert(::Type{DateTime}, dt::Date) = DateTime(UTM(value(dt) * 86400000)) Base.convert(::Type{Date}, dt::DateTime) = Date(UTD(days(dt))) diff --git a/stdlib/Dates/src/types.jl b/stdlib/Dates/src/types.jl index 29b25eef087de..c1ca38d5d7274 100644 --- a/stdlib/Dates/src/types.jl +++ b/stdlib/Dates/src/types.jl @@ -417,6 +417,11 @@ function DateTime(dt::Date, t::Time) return DateTime(y, m, d, hour(t), minute(t), second(t), millisecond(t)) end +# explicit copy constructors +# (to avoid the Fallbacks which attempt to convert single param to Int64) +DateTime(dt::DateTime) = dt +Date(d::Date) = d +Time(t::Time) = t # Fallback constructors DateTime(y, m=1, d=1, h=0, mi=0, s=0, ms=0, ampm::AMPM=TWENTYFOURHOUR) = DateTime(Int64(y), Int64(m), Int64(d), Int64(h), Int64(mi), Int64(s), Int64(ms), ampm) Date(y, m=1, d=1) = Date(Int64(y), Int64(m), Int64(d)) diff --git a/stdlib/Dates/test/ranges.jl b/stdlib/Dates/test/ranges.jl index 525af73d2d113..d4b8716dbebe8 100644 --- a/stdlib/Dates/test/ranges.jl +++ b/stdlib/Dates/test/ranges.jl @@ -534,43 +534,54 @@ let d = Dates.Day(1) @test (Dates.Date(2000):d:Dates.Date(2001)) - d == (Dates.Date(2000) - d:d:Dates.Date(2001) - d) end -# Time ranges -dr = Dates.Time(23, 1, 1):Dates.Second(1):Dates.Time(23, 2, 1) -dr1 = Dates.Time(23, 1, 1):Dates.Second(1):Dates.Time(23, 1, 1) -dr2 = Dates.Time(23, 1, 1):Dates.Second(1):Dates.Time(22, 2, 1) # empty range -dr3 = Dates.Time(23, 1, 1):Dates.Minute(-1):Dates.Time(22, 1, 1) # negative step -dr4 = range(Dates.Time(0), step=Dates.Hour(1), length=23) # by step, length - -# Big ranges -dr8 = typemin(Dates.Time):Dates.Second(1):typemax(Dates.Time) -dr9 = typemin(Dates.Time):Dates.Nanosecond(1):typemax(Dates.Time) -# Other steps -dr10 = typemax(Dates.Time):Dates.Microsecond(-1):typemin(Dates.Time) -dr11 = typemin(Dates.Time):Dates.Millisecond(1):typemax(Dates.Time) -dr12 = typemin(Dates.Time):Dates.Minute(1):typemax(Dates.Time) -dr13 = typemin(Dates.Time):Dates.Hour(1):typemax(Dates.Time) -dr14 = typemin(Dates.Time):Dates.Millisecond(10):typemax(Dates.Time) -dr15 = typemin(Dates.Time):Dates.Minute(100):typemax(Dates.Time) -dr16 = typemin(Dates.Time):Dates.Hour(1000):typemax(Dates.Time) -dr17 = typemax(Dates.Time):Dates.Millisecond(-10000):typemin(Dates.Time) -dr18 = typemax(Dates.Time):Dates.Minute(-100):typemin(Dates.Time) -dr19 = typemax(Dates.Time):Dates.Hour(-10):typemin(Dates.Time) -dr20 = typemin(Dates.Time):Dates.Microsecond(2):typemax(Dates.Time) - -drs = Any[dr, dr1, dr2, dr3, dr4, dr8, dr9, dr10, - dr11, dr12, dr13, dr14, dr15, dr16, dr17, dr18, dr19, dr20] +# small Time ranges +small_ranges_with = Any[ + Dates.Time(23, 1, 1):Dates.Second(1):Dates.Time(23, 2, 1), + Dates.Time(23, 1, 1):Dates.Second(1):Dates.Time(23, 1, 1), + Dates.Time(23, 1, 1):Dates.Minute(-1):Dates.Time(22, 1, 1) # negative step +] +empty_range = Dates.Time(23, 1, 1):Dates.Second(1):Dates.Time(22, 2, 1) +small_ranges_without = Any[ + empty_range, + range(Dates.Time(0), step=Dates.Hour(1), length=23) # by step, length +] +small_ranges = Any[small_ranges_with; small_ranges_without] + +# Big ranges of various steps, increasing and decreasing +big_ranges = Any[ + [typemin(Dates.Time):step:typemax(Dates.Time) for step in [ + Dates.Second(1) + Dates.Nanosecond(1) + Dates.Microsecond(2) + Dates.Millisecond(1) + Dates.Minute(1) + Dates.Hour(1) + Dates.Millisecond(10) + Dates.Minute(100) + Dates.Hour(1000) + ]] + [typemax(Dates.Time):step:typemin(Dates.Time) for step in [ + Dates.Microsecond(-1) + Dates.Millisecond(-10000) + Dates.Minute(-100) + Dates.Hour(-10) + ]] + ] + + +drs = Any[small_ranges; big_ranges] @test map(length, drs) == map(x->size(x)[1], drs) -@test all(x->findall(in(x), x) == [1:length(x);], drs[1:4]) -@test isempty(dr2) +@test all(x->findall(in(x), x) == [1:length(x);], small_ranges) +@test isempty(empty_range) @test all(x->reverse(x) == last(x): - step(x):first(x), drs) -@test all(x->minimum(x) == (step(x) < zero(step(x)) ? last(x) : first(x)), drs[4:end]) -@test all(x->maximum(x) == (step(x) < zero(step(x)) ? first(x) : last(x)), drs[4:end]) -@test_throws MethodError dr .+ 1 +@test all(x->minimum(x) == (step(x) < zero(step(x)) ? last(x) : first(x)), big_ranges) +@test all(x->maximum(x) == (step(x) < zero(step(x)) ? first(x) : last(x)), big_ranges) +@test_throws MethodError drs[1] .+ 1 a = Dates.Time(23, 1, 1) -@test map(x->a in x, drs[1:4]) == [true, true, false, true] -@test a in dr +@test all(x->a in x, small_ranges_with) +@test all(x->!(a in x), small_ranges_without) @test all(x->sort(x) == (step(x) < zero(step(x)) ? reverse(x) : x), drs) @test all(x->step(x) < zero(step(x)) ? issorted(reverse(x)) : issorted(x), drs) @@ -613,4 +624,11 @@ end @test_throws OverflowError StepRange(dmin, Day(1), dmax) end +@testset "StepRangeLen{Time} periodicity, indexing" begin + r = range(Time(0), step = Hour(9), length = 5) + @test length(r) == 5 + @test r[begin:end] == [Time(0), Time(9), Time(18), Time(3), Time(12)] +end + + end # RangesTest module From b4fe696eaa1e28fa934bcff130efe72b528e7b6d Mon Sep 17 00:00:00 2001 From: Michael Doherty Date: Tue, 1 Jul 2025 22:53:40 +1000 Subject: [PATCH 3/5] fix Date Functions docs references; refresh doco for --- stdlib/Dates/docs/src/index.md | 28 +++++++++++++++++++++++----- stdlib/Dates/src/types.jl | 2 +- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/stdlib/Dates/docs/src/index.md b/stdlib/Dates/docs/src/index.md index 38b4f7ae86d29..6ce5ba35a515f 100644 --- a/stdlib/Dates/docs/src/index.md +++ b/stdlib/Dates/docs/src/index.md @@ -4,8 +4,10 @@ DocTestSetup = :(using Dates) ``` -The `Dates` module provides two types for working with dates: [`Date`](@ref) and [`DateTime`](@ref), -representing day and millisecond precision, respectively; both are subtypes of the abstract [`TimeType`](@ref). +The `Dates` module provides three types for representing dates and times: +[`Date`](@ref), [`DateTime`](@ref), and [`Time`](@ref) representing +day, millisecond and nanosecond precision, respectively; +all are subtypes of the abstract [`TimeType`](@ref). The motivation for distinct types is simple: some operations are much simpler, both in terms of code and mental reasoning, when the complexities of greater precision don't have to be dealt with. For example, since the [`Date`](@ref) type only resolves to the precision of a single date (i.e. @@ -14,7 +16,7 @@ time, and leap seconds are unnecessary and avoided. Both [`Date`](@ref) and [`DateTime`](@ref) are basically immutable [`Int64`](@ref) wrappers. The single `instant` field of either type is actually a `UTInstant{P}` type, which -represents a continuously increasing machine timeline based on the UT second [^1]. The +represents a monotonically increasing machine timeline based on the UT second [^1]. The [`DateTime`](@ref) type is not aware of time zones (*naive*, in Python parlance), analogous to a *LocalDateTime* in Java 8. Additional time zone functionality can be added through the [TimeZones.jl package](https://github.com/JuliaTime/TimeZones.jl/), which @@ -26,6 +28,10 @@ The ISO standard, however, states that 1 BC/BCE is year zero, so `0000-12-31` is `0001-01-01`, and year `-0001` (yes, negative one for the year) is 2 BC/BCE, year `-0002` is 3 BC/BCE, etc. +The [`Time`](@ref) is also an immutable [`Int64`](@ref) wrapper, also based on the UT second [^1], +but constrained to represent the periodic (cyclic) time of the 24-hour day starting at midnight. +Note that midnight is represented as 0 hour - 24 hour is out of range. + [^1]: The notion of the UT second is actually quite fundamental. There are basically two different notions of time generally accepted, one based on the physical rotation of the earth (one full rotation @@ -457,6 +463,18 @@ julia> collect(dr) 2014-05-29 2014-06-29 2014-07-29 + +julia> r = range(Time(0), step = Hour(9), length = 5) +Time(0):Hour(9):Time(12) + +julia> collect(r) +5-element Vector{Time}: + 00:00:00 + 09:00:00 + 18:00:00 + 03:00:00 + 12:00:00 + ``` ## Adjuster Functions @@ -707,7 +725,7 @@ Dates.UTC Dates.DateTime(::Int64, ::Int64, ::Int64, ::Int64, ::Int64, ::Int64, ::Int64) Dates.DateTime(::Dates.Period) Dates.DateTime(::Function, ::Any...) -Dates.DateTime(::Dates.TimeType) +Dates.DateTime(::Dates.Date) Dates.DateTime(::AbstractString, ::AbstractString) Dates.format(::Dates.TimeType, ::AbstractString) Dates.DateFormat @@ -716,7 +734,7 @@ Dates.DateTime(::AbstractString, ::Dates.DateFormat) Dates.Date(::Int64, ::Int64, ::Int64) Dates.Date(::Dates.Period) Dates.Date(::Function, ::Any, ::Any, ::Any) -Dates.Date(::Dates.TimeType) +Dates.Date(::Dates.DateTime) Dates.Date(::AbstractString, ::AbstractString) Dates.Date(::AbstractString, ::Dates.DateFormat) Dates.Time(::Int64::Int64, ::Int64, ::Int64, ::Int64, ::Int64) diff --git a/stdlib/Dates/src/types.jl b/stdlib/Dates/src/types.jl index c1ca38d5d7274..228f36da54a84 100644 --- a/stdlib/Dates/src/types.jl +++ b/stdlib/Dates/src/types.jl @@ -132,7 +132,7 @@ struct UTC <: TimeZone end """ TimeType -`TimeType` types wrap `Instant` machine instances to provide human representations of the +`TimeType` types wrap `Instant` machine instants to provide human representations of the machine instant. `Time`, `DateTime` and `Date` are subtypes of `TimeType`. """ abstract type TimeType <: AbstractTime end From 4e5679ba7503964be72619d9081eb1437455bc7f Mon Sep 17 00:00:00 2001 From: Michael Doherty Date: Wed, 2 Jul 2025 09:44:41 +1000 Subject: [PATCH 4/5] two sentences more clear Co-authored-by: Lilith Orion Hafner --- stdlib/Dates/docs/src/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stdlib/Dates/docs/src/index.md b/stdlib/Dates/docs/src/index.md index 6ce5ba35a515f..af3a2fc7c32e1 100644 --- a/stdlib/Dates/docs/src/index.md +++ b/stdlib/Dates/docs/src/index.md @@ -30,7 +30,7 @@ BC/BCE, etc. The [`Time`](@ref) is also an immutable [`Int64`](@ref) wrapper, also based on the UT second [^1], but constrained to represent the periodic (cyclic) time of the 24-hour day starting at midnight. -Note that midnight is represented as 0 hour - 24 hour is out of range. +Note that midnight is represented as 0 hour. 24 hour is out of range. [^1]: The notion of the UT second is actually quite fundamental. There are basically two different notions From 1f4b0939d302859cd19e8c3c72b325272b276567 Mon Sep 17 00:00:00 2001 From: Michael Doherty Date: Thu, 17 Jul 2025 14:41:01 +1000 Subject: [PATCH 5/5] better phrasing about periodic time --- stdlib/Dates/docs/src/index.md | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/stdlib/Dates/docs/src/index.md b/stdlib/Dates/docs/src/index.md index af3a2fc7c32e1..3ef1429be111f 100644 --- a/stdlib/Dates/docs/src/index.md +++ b/stdlib/Dates/docs/src/index.md @@ -5,7 +5,7 @@ DocTestSetup = :(using Dates) ``` The `Dates` module provides three types for representing dates and times: -[`Date`](@ref), [`DateTime`](@ref), and [`Time`](@ref) representing +[`Date`](@ref), [`DateTime`](@ref), and [`Time`](@ref) measured with day, millisecond and nanosecond precision, respectively; all are subtypes of the abstract [`TimeType`](@ref). The motivation for distinct types is simple: some operations are much simpler, both in terms of @@ -29,8 +29,10 @@ The ISO standard, however, states that 1 BC/BCE is year zero, so `0000-12-31` is BC/BCE, etc. The [`Time`](@ref) is also an immutable [`Int64`](@ref) wrapper, also based on the UT second [^1], -but constrained to represent the periodic (cyclic) time of the 24-hour day starting at midnight. -Note that midnight is represented as 0 hour. 24 hour is out of range. +and represents the time of day according to the conventional 24-hour clock, starting at midnight, +and ending the instant one nanosecond prior to midnight. Time is periodic, and wraps around at +midnight (see [TimeType-Period arithmetic](#TimeType-Period-Arithmetic)). + [^1]: The notion of the UT second is actually quite fundamental. There are basically two different notions @@ -45,7 +47,7 @@ Note that midnight is represented as 0 hour. 24 hour is out of range. ## Constructors -[`Date`](@ref) and [`DateTime`](@ref) types can be constructed by integer or [`Period`](@ref) +[`Date`](@ref), [`DateTime`](@ref) and [`Time`](@ref) types can be constructed by integer or [`Period`](@ref) types, by parsing, or through adjusters (more on those later): ```jldoctest @@ -84,6 +86,16 @@ julia> Date(Dates.Year(2013),Dates.Month(7),Dates.Day(1)) julia> Date(Dates.Month(7),Dates.Year(2013)) 2013-07-01 + +julia> Time(12) +12:00:00 + +julia> Time(12, 30, 59, 1, 0, 2) +12:30:59.001000002 + +julia> Time(Hour(12), Minute(30), Second(59), Millisecond(1), Nanosecond(2)) +12:30:59.001000002 + ``` [`Date`](@ref) or [`DateTime`](@ref) parsing is accomplished by the use of format strings. Format @@ -463,6 +475,13 @@ julia> collect(dr) 2014-05-29 2014-06-29 2014-07-29 +``` + +Time is periodic, and wraps around at midnight: + +```jldoctest +julia> Time(23) + Hour(1) +00:00:00 julia> r = range(Time(0), step = Hour(9), length = 5) Time(0):Hour(9):Time(12)