Skip to content

Commit 5740b59

Browse files
committed
support timedelta, timedelta64, datetime64 and respective conversions
1 parent 5a65a52 commit 5740b59

File tree

7 files changed

+115
-0
lines changed

7 files changed

+115
-0
lines changed

src/Convert/Convert.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ module Convert
88
using ..Core
99
using ..Core: C, Utils, @autopy, getptr, incref, pynew, PyNULL, pyisnull, pydel!, pyisint, iserrset_ambig, pyisnone, pyisTrue, pyisFalse, pyfloat_asdouble, pycomplex_ascomplex, pyisstr, pystr_asstring, pyisbytes, pybytes_asvector, pybytes_asUTF8string, pyisfloat, pyisrange, pytuple_getitem, unsafe_pynext, pyistuple, pydatetimetype, pytime_isaware, pydatetime_isaware, _base_pydatetime, _base_datetime, errmatches, errclear, errset, pyiscomplex, pythrow, pybool_asbool
1010
using Dates: Date, Time, DateTime, Second, Millisecond, Microsecond, Nanosecond
11+
using Dates: Year, Month, Day, Hour, Minute, Week, Period, CompoundPeriod, canonicalize
1112

1213
import ..Core: pyconvert
1314

src/Convert/numpy.jl

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,70 @@ const NUMPY_SIMPLE_TYPES = [
2727
("complex128", ComplexF64),
2828
]
2929

30+
function pydatetime64(
31+
_year::Int=0, _month::Int=1, _day::Int=1, _hour::Int=0, _minute::Int=0,_second::Int=0, _millisecond::Int=0, _microsecond::Int=0, _nanosecond::Int=0;
32+
year::Int=_year, month::Int=_month, day::Int=_day, hour::Int=_hour, minute::Int=_minute, second::Int=_second,
33+
millisecond::Int=_millisecond, microsecond::Int=_microsecond, nanosecond::Int=_nanosecond
34+
)
35+
pyimport("numpy").datetime64("$(DateTime(year, month, day, hour, minute, second))") + pytimedelta64(;millisecond, microsecond, nanosecond)
36+
end
37+
function pydatetime64(@nospecialize(x::T)) where T <: Period
38+
T <: Union{Week, Day, Hour, Minute, Second, Millisecond, Microsecond} ||
39+
error("Unsupported Period type: ", "Year, Month and Nanosecond are not supported, consider using pytimedelta64 instead.")
40+
args = T .== (Day, Second, Millisecond, Microsecond, Minute, Hour, Week)
41+
pydatetime64(x.value .* args...)
42+
end
43+
function pydatetime64(x::CompoundPeriod)
44+
x = canonicalize(x)
45+
isempty(x.periods) ? pydatetime64(Second(0)) : sum(pydatetime64.(x.periods))
46+
end
47+
export pydatetime64
48+
49+
function pytimedelta64(
50+
_year::Int=0, _month::Int=0, _day::Int=0, _hour::Int=0, _minute::Int=0, _second::Int=0, _millisecond::Int=0, _microsecond::Int=0, _nanosecond::Int=0, _week::Int=0;
51+
year::Int=_year, month::Int=_month, day::Int=_day, hour::Int=_hour, minute::Int=_minute, second::Int=_second, microsecond::Int=_microsecond, millisecond::Int=_millisecond, nanosecond::Int=_nanosecond, week::Int=_week)
52+
pytimedelta64(sum((
53+
Year(year), Month(month), # you cannot mix year or month with any of the below units in python, the error will be thrown by `pytimedelta64(::CompoundPeriod)`
54+
Day(day), Hour(hour), Minute(minute), Second(second), Millisecond(millisecond), Microsecond(microsecond), Nanosecond(nanosecond))
55+
))
56+
end
57+
function pytimedelta64(@nospecialize(x::T)) where T <: Period
58+
index = findfirst(==(T), (Year, Month, Week, Day, Hour, Minute, Second, Millisecond, Microsecond, Nanosecond, T))
59+
unit = ("Y", "M", "W", "D", "h", "m", "s", "ms", "us", "ns", "")[index]
60+
pyimport("numpy").timedelta64(x.value, unit)
61+
end
62+
function pytimedelta64(x::CompoundPeriod)
63+
x = canonicalize(x)
64+
isempty(x.periods) ? pytimedelta64(Second(0)) : sum(pytimedelta64.(x.periods))
65+
end
66+
export pytimedelta64
67+
68+
function pyconvert_rule_datetime64(::Type{DateTime}, x::Py)
69+
unit, value = pyconvert(Tuple, pyimport("numpy").datetime_data(x))
70+
# strangely, datetime_data does not return the value correctly
71+
# so we retrieve the value from the byte representation
72+
value = reinterpret(Int64, pyconvert(Vector, x))[1]
73+
units = ("Y", "M", "W", "D", "h", "m", "s", "ms", "us", "ns")
74+
types = (Year, Month, Week, Day, Hour, Minute, Second, Millisecond, Microsecond, Nanosecond)
75+
T = types[findfirst(==(unit), units)]
76+
pyconvert_return(DateTime(_base_datetime) + T(value))
77+
end
78+
79+
function pyconvert_rule_timedelta64(::Type{CompoundPeriod}, x::Py)
80+
unit, value = pyconvert(Tuple, pyimport("numpy").datetime_data(x))
81+
# strangely, datetime_data does not return the value correctly
82+
# so we retrieve the value from the byte representation
83+
value = reinterpret(Int64, pyconvert(Vector, x))[1]
84+
units = ("Y", "M", "W", "D", "h", "m", "s", "ms", "us", "ns")
85+
types = (Year, Month, Week, Day, Hour, Minute, Second, Millisecond, Microsecond, Nanosecond)
86+
T = types[findfirst(==(unit), units)]
87+
pyconvert_return(CompoundPeriod(T(value)) |> canonicalize)
88+
end
89+
90+
function pyconvert_rule_timedelta64(::Type{T}, x::Py) where T<:Period
91+
pyconvert_return(convert(T, pyconvert_rule_timedelta64(CompoundPeriod, x)))
92+
end
93+
3094
function init_numpy()
3195
for (t, T) in NUMPY_SIMPLE_TYPES
3296
isbool = occursin("bool", t)
@@ -52,4 +116,14 @@ function init_numpy()
52116
iscomplex && pyconvert_add_rule(name, Complex, rule)
53117
isnumber && pyconvert_add_rule(name, Number, rule)
54118
end
119+
120+
priority = PYCONVERT_PRIORITY_ARRAY
121+
pyconvert_add_rule("numpy:datetime64", DateTime, pyconvert_rule_datetime64, priority)
122+
for T in (CompoundPeriod, Year, Month, Day, Hour, Minute, Second, Millisecond, Microsecond, Nanosecond, Week)
123+
pyconvert_add_rule("numpy:timedelta64", T, pyconvert_rule_timedelta64, priority)
124+
end
125+
126+
priority = PYCONVERT_PRIORITY_CANONICAL
127+
pyconvert_add_rule("numpy:datetime64", DateTime, pyconvert_rule_datetime64, priority)
128+
pyconvert_add_rule("numpy:timedelta64", Nanosecond, pyconvert_rule_timedelta, priority)
55129
end

src/Convert/pyconvert.jl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,12 @@ function init_pyconvert()
391391
push!(PYCONVERT_EXTRATYPES, pyimport("numbers" => ("Number", "Complex", "Real", "Rational", "Integral"))...)
392392
push!(PYCONVERT_EXTRATYPES, pyimport("collections.abc" => ("Iterable", "Sequence", "Set", "Mapping"))...)
393393

394+
priority = PYCONVERT_PRIORITY_ARRAY
395+
pyconvert_add_rule("datetime:datetime", DateTime, pyconvert_rule_datetime, priority)
396+
for T in (Millisecond, Second, Nanosecond, Day, Hour, Minute, Second, Millisecond, Week, CompoundPeriod)
397+
pyconvert_add_rule("datetime:timedelta", T, pyconvert_rule_timedelta, priority)
398+
end
399+
394400
priority = PYCONVERT_PRIORITY_CANONICAL
395401
pyconvert_add_rule("builtins:NoneType", Nothing, pyconvert_rule_none, priority)
396402
pyconvert_add_rule("builtins:bool", Bool, pyconvert_rule_bool, priority)

src/Convert/rules.jl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,3 +439,16 @@ function pyconvert_rule_timedelta(::Type{Second}, x::Py)
439439
end
440440
return Second(days * 3600 * 24 + seconds)
441441
end
442+
443+
function pyconvert_rule_timedelta(::Type{<:CompoundPeriod}, x::Py)
444+
days = pyconvert(Int, x.days)
445+
seconds = pyconvert(Int, x.seconds)
446+
microseconds = pyconvert(Int, x.microseconds)
447+
nanoseconds = pyhasattr(x, "nanoseconds") ? pyconvert(Int, x.nanoseconds) : 0
448+
timedelta = Day(days) + Second(seconds) + Microsecond(microseconds) + Nanosecond(nanoseconds)
449+
return pyconvert_return(timedelta)
450+
end
451+
452+
function pyconvert_rule_timedelta(::Type{T}, x::Py) where T<:Period
453+
pyconvert_return(convert(T, pyconvert_rule_timedelta(CompoundPeriod, x)))
454+
end

src/Core/Core.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ using ..GC: GC
1414
using ..Utils: Utils
1515
using Base: @propagate_inbounds, @kwdef
1616
using Dates: Date, Time, DateTime, year, month, day, hour, minute, second, millisecond, microsecond, nanosecond
17+
using Dates: Day, Second, Millisecond, Microsecond, Minute, Hour, Week
18+
using Dates: Period, CompoundPeriod, canonicalize
1719
using MacroTools: MacroTools, @capture
1820
using Markdown: Markdown
1921

src/Core/Py.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ Py(x::AbstractRange{<:Union{Int8,Int16,Int32,Int64,Int128,UInt8,UInt16,UInt32,UI
149149
Py(x::Date) = pydate(x)
150150
Py(x::Time) = pytime(x)
151151
Py(x::DateTime) = pydatetime(x)
152+
Py(x::Union{Period, CompoundPeriod}) = pytimedelta(x)
152153

153154
Base.string(x::Py) = pyisnull(x) ? "<py NULL>" : pystr(String, x)
154155
Base.print(io::IO, x::Py) = print(io, string(x))

src/Core/builtins.jl

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,6 +1065,24 @@ end
10651065
pydatetime(x::Date) = pydatetime(year(x), month(x), day(x))
10661066
export pydatetime
10671067

1068+
function pytimedelta(
1069+
_day::Int=0, _second::Int=0, _microsecond::Int=0, _millisecond::Int=0, _minute::Int=0, _hour::Int=0, _week::Int=0;
1070+
day::Int=_day, second::Int=_second, microsecond::Int=_microsecond, millisecond::Int=_millisecond, minute::Int=_minute, hour::Int=_hour, week::Int=_week
1071+
)
1072+
pyimport("datetime").timedelta(day, second, microsecond, millisecond, minute, hour, week)
1073+
end
1074+
function pytimedelta(@nospecialize(x::T)) where T <: Period
1075+
T <: Union{Week, Day, Hour, Minute, Second, Millisecond, Microsecond} ||
1076+
error("Unsupported Period type: ", "Year, Month and Nanosecond are not supported, consider using pytimedelta64 instead.")
1077+
args = T .== (Day, Second, Millisecond, Microsecond, Minute, Hour, Week)
1078+
pytimedelta(x.value .* args...)
1079+
end
1080+
function pytimedelta(x::CompoundPeriod)
1081+
x = canonicalize(x)
1082+
isempty(x.periods) ? pytimedelta(Second(0)) : sum(pytimedelta.(x.periods))
1083+
end
1084+
export pytimedelta
1085+
10681086
function pytime_isaware(x)
10691087
tzinfo = pygetattr(x, "tzinfo")
10701088
if pyisnone(tzinfo)

0 commit comments

Comments
 (0)