Skip to content

Commit ba2ee61

Browse files
author
Christopher Doris
committed
refactor(numpydates): simplify date and time delta conversions with rescale function
- Add pyconvert rules for InlineDateTime64 and InlineTimeDelta64 types - Refactor DateTime64 and TimeDelta64 constructors to use rescale for unit conversions - Introduce unitscale and rescale functions in Unit.jl for efficient scaling between units - Add convert methods for DatesInstant and DatesPeriod types - Update common.jl with new type aliases and helper functions - Enhance tests for new conversion capabilities and error handling These changes streamline the conversion logic, reduce code duplication, and improve performance by centralizing unit scaling operations. No breaking changes to public APIs.
1 parent de9e29f commit ba2ee61

File tree

10 files changed

+550
-230
lines changed

10 files changed

+550
-230
lines changed

src/Convert/numpy.jl

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ function pyconvert_rule_datetime64(::Type{DateTime64}, x::Py)
1313
pyconvert_return(C.PySimpleObject_GetValue(DateTime64, x))
1414
end
1515

16+
function pyconvert_rule_datetime64(::Type{T}, x::Py) where {T<:InlineDateTime64}
17+
pyconvert_return(T(C.PySimpleObject_GetValue(DateTime64, x)))
18+
end
19+
1620
function pyconvert_rule_datetime64(::Type{DateTime}, x::Py)
1721
d = C.PySimpleObject_GetValue(DateTime64, x)
1822
if isnan(d)
@@ -44,6 +48,10 @@ function pyconvert_rule_timedelta64(::Type{TimeDelta64}, x::Py)
4448
pyconvert_return(C.PySimpleObject_GetValue(TimeDelta64, x))
4549
end
4650

51+
function pyconvert_rule_datetime64(::Type{T}, x::Py) where {T<:InlineTimeDelta64}
52+
pyconvert_return(T(C.PySimpleObject_GetValue(TimeDelta64, x)))
53+
end
54+
4755
function pyconvert_rule_timedelta64(::Type{Missing}, x::Py)
4856
d = C.PySimpleObject_GetValue(TimeDelta64, x)
4957
if isnan(d)
@@ -116,6 +124,7 @@ function init_numpy()
116124
pyconvert_rule_datetime64,
117125
PYCONVERT_PRIORITY_ARRAY,
118126
)
127+
pyconvert_add_rule("numpy:datetime64", InlineDateTime64, pyconvert_rule_datetime64)
119128
pyconvert_add_rule("numpy:datetime64", DateTime, pyconvert_rule_datetime64)
120129
pyconvert_add_rule("numpy:datetime64", Missing, pyconvert_rule_datetime64)
121130
pyconvert_add_rule("numpy:datetime64", Nothing, pyconvert_rule_datetime64)
@@ -127,6 +136,7 @@ function init_numpy()
127136
pyconvert_rule_timedelta64,
128137
PYCONVERT_PRIORITY_ARRAY,
129138
)
139+
pyconvert_add_rule("numpy:timedelta64", InlineTimeDelta64, pyconvert_rule_timedelta64)
130140
pyconvert_add_rule("numpy:timedelta64", Missing, pyconvert_rule_timedelta64)
131141
pyconvert_add_rule("numpy:timedelta64", Nothing, pyconvert_rule_timedelta64)
132142
end

src/NumpyDates/AbstractDateTime64.jl

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,44 +5,17 @@ Supertype for [`DateTime64`](@ref) and [`InlineDateTime64`](@ref).
55
"""
66
abstract type AbstractDateTime64 <: Dates.TimeType end
77

8-
function Dates.value(d::AbstractDateTime64)
9-
d.value
10-
end
11-
128
function Dates.DateTime(d::AbstractDateTime64)
139
isnan(d) && error("Cannot convert NaT to DateTime")
1410
v = value(d)
15-
u, s = unitpair(d)
16-
v = v * s # TODO: check overflow
11+
u, _ = unit = unitpair(d)
1712
b = Dates.DateTime(1970)
18-
if u == YEARS
19-
b + Dates.Year(v)
20-
elseif u == MONTHS
21-
b + Dates.Month(v)
22-
elseif u == WEEKS
23-
b + Dates.Week(v)
24-
elseif u == DAYS
25-
b + Dates.Day(v)
26-
elseif u == HOURS
27-
b + Dates.Hour(v)
28-
elseif u == MINUTES
29-
b + Dates.Minute(v)
30-
elseif u == SECONDS
31-
b + Dates.Second(v)
32-
elseif u == MILLISECONDS
13+
if u > MONTHS
14+
v, _ = rescale(v, unit, MILLISECONDS)
3315
b + Dates.Millisecond(v)
34-
elseif u == MICROSECONDS
35-
b + Dates.Microsecond(v)
36-
elseif u == NANOSECONDS
37-
b + Dates.Nanosecond(v)
38-
elseif u == PICOSECONDS
39-
b + Dates.Nanosecond(fld(v, 1_000))
40-
elseif u == FEMTOSECONDS
41-
b + Dates.Nanosecond(fld(v, 1_000_000))
42-
elseif u == ATTOSECONDS
43-
b + Dates.Nanosecond(fld(v, 1_000_000_000))
4416
else
45-
error("Unsupported units: $unit_base")
17+
v, _ = rescale(v, unit, MONTHS)
18+
b + Dates.Month(v)
4619
end
4720
end
4821

@@ -51,6 +24,9 @@ function Dates.Date(d::AbstractDateTime64)
5124
Dates.Date(Dates.DateTime(d))
5225
end
5326

27+
Base.convert(::Type{Dates.DateTime}, d::AbstractDateTime64) = Dates.DateTime(d)
28+
Base.convert(::Type{Dates.Date}, d::AbstractDateTime64) = Dates.Date(d)
29+
5430
function Base.isnan(d::AbstractDateTime64)
5531
value(d) == typemin(Int64)
5632
end

src/NumpyDates/AbstractTimeDelta64.jl

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,32 @@ Supertype for [`TimeDelta64`](@ref) and [`InlineTimeDelta64`](@ref).
55
"""
66
abstract type AbstractTimeDelta64 <: Dates.Period end
77

8+
function construct(::Type{T}, d::AbstractTimeDelta64) where {T<:DatesPeriod}
9+
v, r = rescale(value(d), unitpair(d), Unit(T))
10+
iszero(r) || throw(InexactError(nameof(T), T, d))
11+
T(v)
12+
end
13+
14+
Dates.Year(d::AbstractTimeDelta64) = construct(Dates.Year, d)
15+
Dates.Month(d::AbstractTimeDelta64) = construct(Dates.Month, d)
16+
Dates.Day(d::AbstractTimeDelta64) = construct(Dates.Day, d)
17+
Dates.Hour(d::AbstractTimeDelta64) = construct(Dates.Hour, d)
18+
Dates.Minute(d::AbstractTimeDelta64) = construct(Dates.Minute, d)
19+
Dates.Second(d::AbstractTimeDelta64) = construct(Dates.Second, d)
20+
Dates.Millisecond(d::AbstractTimeDelta64) = construct(Dates.Millisecond, d)
21+
Dates.Microsecond(d::AbstractTimeDelta64) = construct(Dates.Microsecond, d)
22+
Dates.Nanosecond(d::AbstractTimeDelta64) = construct(Dates.Nanosecond, d)
23+
24+
Base.convert(::Type{Dates.Year}, d::AbstractTimeDelta64) = Dates.Year(d)
25+
Base.convert(::Type{Dates.Month}, d::AbstractTimeDelta64) = Dates.Month(d)
26+
Base.convert(::Type{Dates.Day}, d::AbstractTimeDelta64) = Dates.Day(d)
27+
Base.convert(::Type{Dates.Hour}, d::AbstractTimeDelta64) = Dates.Hour(d)
28+
Base.convert(::Type{Dates.Minute}, d::AbstractTimeDelta64) = Dates.Minute(d)
29+
Base.convert(::Type{Dates.Second}, d::AbstractTimeDelta64) = Dates.Second(d)
30+
Base.convert(::Type{Dates.Millisecond}, d::AbstractTimeDelta64) = Dates.Millisecond(d)
31+
Base.convert(::Type{Dates.Microsecond}, d::AbstractTimeDelta64) = Dates.Microsecond(d)
32+
Base.convert(::Type{Dates.Nanosecond}, d::AbstractTimeDelta64) = Dates.Nanosecond(d)
33+
834
function Base.isnan(d::AbstractTimeDelta64)
935
value(d) == NAT
1036
end

src/NumpyDates/DateTime64.jl

Lines changed: 14 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -70,75 +70,29 @@ function DateTime64(d::AbstractString, f::Dates.DateFormat, unit::UnitArg = defa
7070
end
7171

7272
function DateTime64(d::Dates.DateTime, unit::UnitArg = defaultunit(d))
73-
u, m = unit = unitpair(unit)
74-
if u DAYS
75-
return DateTime64(Dates.Date(d), unit)
76-
end
77-
v = value((d - Dates.DateTime(1970))::Dates.Millisecond)
78-
if u == HOURS
79-
m = mul(m, 1000 * 60 * 60)
80-
elseif u == MINUTES
81-
m = mul(m, 1000 * 60)
82-
elseif u == SECONDS
83-
m = mul(m, 1000)
84-
elseif u == MILLISECONDS
85-
# nothing
86-
elseif u == MICROSECONDS
87-
v = mul(v, 1000)
88-
elseif u == NANOSECONDS
89-
v = mul(v, 1000_000)
90-
elseif u == PICOSECONDS
91-
v = mul(v, 1000_000_000)
92-
elseif u == FEMTOSECONDS
93-
v = mul(v, 1000_000_000_000)
94-
elseif u == ATTOSECONDS
95-
v = mul(v, 1000_000_000_000_000)
73+
u, _ = unit = unitpair(unit)
74+
if u == YEARS
75+
v_yr = sub(Dates.year(d), 1970)
76+
v, _ = rescale(v_yr, YEARS, unit)
77+
elseif u == MONTHS
78+
yr, mn = Dates.yearmonth(d)
79+
v_mn = add(mul(12, sub(yr, 1970)), sub(mn, 1))
80+
v, _ = rescale(v_mn, MONTHS, unit)
9681
else
97-
error("unknown unit: $u")
82+
v_ms = d - Dates.DateTime(1970)
83+
v, _ = rescale(value(v_ms), Unit(v_ms), unit)
9884
end
99-
v = fld(v, m)
10085
DateTime64(v, unit)
10186
end
10287

10388

10489
function DateTime64(d::Dates.Date, unit::UnitArg = defaultunit(d))
105-
u, m = unit = unitpair(unit)
106-
if u == YEARS
107-
v = Dates.year(d) - 1970
108-
elseif u == MONTHS
109-
v = 12 * (Dates.year(d) - 1970) + (Dates.month(d) - 1)
110-
else
111-
v = value((d - Dates.Date(1970))::Dates.Day)
112-
if u == WEEKS
113-
m = mul(m, 7)
114-
elseif u == DAYS
115-
# nothing
116-
elseif u == HOURS
117-
v = mul(v, 24)
118-
elseif u == MINUTES
119-
v = mul(v, 24 * 60)
120-
elseif u == SECONDS
121-
v = mul(v, 24 * 60 * 60)
122-
elseif u == MILLISECONDS
123-
v = mul(v, 24 * 60 * 60 * 1000)
124-
elseif u == MICROSECONDS
125-
v = mul(v, 24 * 60 * 60 * 1000_000)
126-
elseif u == NANOSECONDS
127-
v = mul(v, 24 * 60 * 60 * 1000_000_000)
128-
elseif u == PICOSECONDS
129-
v = mul(v, 24 * 60 * 60 * 1000_000_000_000)
130-
elseif u == FEMTOSECONDS
131-
throw(OverflowError(""))
132-
elseif u == ATTOSECONDS
133-
throw(OverflowError(""))
134-
else
135-
error("unknown unit: $u")
136-
end
137-
end
138-
v = fld(v, m)
139-
DateTime64(v, unit)
90+
DateTime64(Dates.DateTime(d), unit)
14091
end
14192

93+
# convert
94+
95+
Base.convert(::Type{DateTime64}, x::DatesInstant) = DateTime64(x)
14296

14397
# show
14498

src/NumpyDates/InlineDateTime64.jl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ function InlineDateTime64(
9494
InlineTimeDelta64{unitparam(u)}(v, f)
9595
end
9696

97+
# convert
98+
99+
Base.convert(::Type{InlineDateTime64}, x::DatesInstant) = InlineDateTime64(x)
100+
Base.convert(::Type{InlineDateTime64{U}}, x::DatesInstant) where {U} =
101+
InlineDateTime64{U}(x)
102+
97103
# show
98104

99105
function Base.show(io::IO, d::InlineDateTime64)

src/NumpyDates/InlineTimeDelta64.jl

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ function InlineTimeDelta64{U}(v::AbstractString) where {U}
4242
InlineTimeDelta64{U}(value(TimeDelta64(v, U)))
4343
end
4444

45-
function InlineTimeDelta64{U}(v::Dates.Period) where {U}
45+
function InlineTimeDelta64{U}(v::DatesPeriod) where {U}
4646
InlineTimeDelta64{U}(value(TimeDelta64(v, U)))
4747
end
4848

@@ -58,14 +58,20 @@ function InlineTimeDelta64(v::AbstractString, u::UnitArg = defaultunit(v))
5858
InlineTimeDelta64{unitparam(u)}(v)
5959
end
6060

61-
function InlineTimeDelta64(v::Dates.Period, u::UnitArg = defaultunit(v))
61+
function InlineTimeDelta64(v::DatesPeriod, u::UnitArg = defaultunit(v))
6262
InlineTimeDelta64{unitparam(u)}(v)
6363
end
6464

6565
function InlineTimeDelta64(v::Integer, u::UnitArg)
6666
InlineTimeDelta64{unitparam(u)}(v)
6767
end
6868

69+
# convert
70+
71+
Base.convert(::Type{InlineTimeDelta64}, p::DatesPeriod) = InlineTimeDelta64(p)
72+
Base.convert(::Type{InlineTimeDelta64{U}}, p::DatesPeriod) where {U} =
73+
InlineTimeDelta64{U}(p)
74+
6975
# show
7076

7177
function Base.show(io::IO, d::InlineTimeDelta64)
@@ -79,3 +85,5 @@ function Base.show(io::IO, d::InlineTimeDelta64)
7985
end
8086
nothing
8187
end
88+
89+
Base.show(io::IO, ::MIME"text/plain", d::InlineTimeDelta64) = show(io, d)

src/NumpyDates/TimeDelta64.jl

Lines changed: 8 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -59,90 +59,15 @@ function TimeDelta64(s::AbstractString, unit::UnitArg = defaultunit(s))
5959
end
6060
end
6161

62-
# Convert Dates.Period to TimeDelta64
63-
function TimeDelta64(p::Dates.Period, unit::UnitArg = defaultunit(p))
64-
u, m = unitpair(unit)
65-
if u == YEARS
66-
if p isa Dates.Year
67-
v = value(p)
68-
return TimeDelta64(fld(v, m), unit)
69-
else
70-
error("cannot convert $(typeof(p)) to years")
71-
end
72-
elseif u == MONTHS
73-
if p isa Dates.Month
74-
v = value(p)
75-
return TimeDelta64(fld(v, m), unit)
76-
elseif p isa Dates.Year
77-
v = mul(value(p), 12)
78-
return TimeDelta64(fld(v, m), unit)
79-
else
80-
error("cannot convert $(typeof(p)) to months")
81-
end
82-
elseif u == PICOSECONDS || u == FEMTOSECONDS || u == ATTOSECONDS
83-
# sub-nanosecond units: expand from ns
84-
ns = _period_to_ns(p)
85-
scale = u == PICOSECONDS ? 1_000 : u == FEMTOSECONDS ? 1_000_000 : 1_000_000_000
86-
ns_scaled = mul(ns, scale)
87-
return TimeDelta64(fld(ns_scaled, m), unit)
88-
else
89-
# weeks..nanoseconds: convert via nanoseconds
90-
ns = _period_to_ns(p)
91-
unit_ns = _unit_to_ns(u)
92-
denom = mul(unit_ns, Int64(m))
93-
return TimeDelta64(fld(ns, denom), unit)
94-
end
62+
function TimeDelta64(p::DatesPeriod, unit::UnitArg = defaultunit(p))
63+
v, r = rescale(value(p), Unit(p), unit)
64+
iszero(r) || throw(InexactError(:TimeDelta64, TimeDelta64, p, unit))
65+
TimeDelta64(v, unit)
9566
end
9667

97-
# helpers
98-
99-
# number of nanoseconds per unit (except sub-ns which are handled separately)
100-
function _unit_to_ns(u::Unit)::Int64
101-
if u == WEEKS
102-
mul(mul(mul(mul(Int64(7), Int64(24)), Int64(60)), Int64(60)), Int64(1_000_000_000))
103-
elseif u == DAYS
104-
mul(mul(mul(Int64(24), Int64(60)), Int64(60)), Int64(1_000_000_000))
105-
elseif u == HOURS
106-
mul(mul(Int64(60), Int64(60)), Int64(1_000_000_000))
107-
elseif u == MINUTES
108-
mul(Int64(60), Int64(1_000_000_000))
109-
elseif u == SECONDS
110-
Int64(1_000_000_000)
111-
elseif u == MILLISECONDS
112-
Int64(1_000_000)
113-
elseif u == MICROSECONDS
114-
Int64(1_000)
115-
elseif u == NANOSECONDS
116-
Int64(1)
117-
else
118-
error("Unsupported or sub-nanosecond unit for ns mapping: $u")
119-
end
120-
end
68+
# convert
12169

122-
# convert a Dates.Period into total nanoseconds (disallow calendar Year/Month here)
123-
function _period_to_ns(p::Dates.Period)::Int64
124-
if p isa Dates.Week
125-
mul(value(p), _unit_to_ns(WEEKS))
126-
elseif p isa Dates.Day
127-
mul(value(p), _unit_to_ns(DAYS))
128-
elseif p isa Dates.Hour
129-
mul(value(p), _unit_to_ns(HOURS))
130-
elseif p isa Dates.Minute
131-
mul(value(p), _unit_to_ns(MINUTES))
132-
elseif p isa Dates.Second
133-
mul(value(p), _unit_to_ns(SECONDS))
134-
elseif p isa Dates.Millisecond
135-
mul(value(p), _unit_to_ns(MILLISECONDS))
136-
elseif p isa Dates.Microsecond
137-
mul(value(p), _unit_to_ns(MICROSECONDS))
138-
elseif p isa Dates.Nanosecond
139-
value(p)
140-
elseif p isa Dates.Month || p isa Dates.Year
141-
error("cannot convert $(typeof(p)) to time-based units")
142-
else
143-
error("unsupported period type: $(typeof(p))")
144-
end
145-
end
70+
Base.convert(::Type{TimeDelta64}, p::DatesPeriod) = TimeDelta64(p)
14671

14772
# show
14873

@@ -159,3 +84,5 @@ function Base.show(io::IO, d::TimeDelta64)
15984
end
16085
nothing
16186
end
87+
88+
Base.show(io::IO, ::MIME"text/plain", d::TimeDelta64) = show(io, d)

0 commit comments

Comments
 (0)