Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add timedelta, timedelta64 and datetime64 plus respective conversions #509

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions src/Convert/Convert.jl
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ using ..Core:
pythrow,
pybool_asbool
using Dates: Date, Time, DateTime, Second, Millisecond, Microsecond, Nanosecond
using Dates: Year, Month, Day, Hour, Minute, Week, Period, CompoundPeriod, canonicalize

import ..Core: pyconvert

Expand Down
72 changes: 72 additions & 0 deletions src/Convert/numpy.jl
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,68 @@ const NUMPY_SIMPLE_TYPES = [
("complex128", ComplexF64),
]

function pydatetime64(
_year::Integer=0, _month::Integer=1, _day::Integer=1, _hour::Integer=0, _minute::Integer=0,_second::Integer=0, _millisecond::Integer=0, _microsecond::Integer=0, _nanosecond::Integer=0;
year::Integer=_year, month::Integer=_month, day::Integer=_day, hour::Integer=_hour, minute::Integer=_minute, second::Integer=_second,
millisecond::Integer=_millisecond, microsecond::Integer=_microsecond, nanosecond::Integer=_nanosecond
)
pyimport("numpy").datetime64("$(DateTime(year, month, day, hour, minute, second))") + pytimedelta64(;millisecond, microsecond, nanosecond)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is pyimport("numpy") the correct API call, or is that just to be used in user packages?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw similar calls at different places in the package, so I took this approach. But I also wouldn't know how to code a timedelta64 without calling pyimport.
Please let me know if there's a better solution.

end
function pydatetime64(@nospecialize(x::T)) where T <: Period
T <: Union{Week, Day, Hour, Minute, Second, Millisecond, Microsecond} ||
error("Unsupported Period type: `$x::$T`. Consider using pytimedelta64 instead.")
args = map(Base.Fix1(isa, x), (Day, Second, Millisecond, Microsecond, Minute, Hour, Week))
pydatetime64(map(Base.Fix1(*, x.value), args)...)
end
function pydatetime64(x::CompoundPeriod)
x = canonicalize(x)
isempty(x.periods) ? pydatetime64(Second(0)) : sum(pydatetime64, x.periods)
end
export pydatetime64

function pytimedelta64(
_year::Integer=0, _month::Integer=0, _day::Integer=0, _hour::Integer=0, _minute::Integer=0, _second::Integer=0, _millisecond::Integer=0, _microsecond::Integer=0, _nanosecond::Integer=0, _week::Integer=0;
year::Integer=_year, month::Integer=_month, day::Integer=_day, hour::Integer=_hour, minute::Integer=_minute, second::Integer=_second, microsecond::Integer=_microsecond, millisecond::Integer=_millisecond, nanosecond::Integer=_nanosecond, week::Integer=_week)
pytimedelta64(sum((
Year(year), Month(month),
# you cannot mix year or month with any of the below units in python
# in case of wrong usage a descriptive error message will by thrown by the underlying python function
Day(day), Hour(hour), Minute(minute), Second(second), Millisecond(millisecond), Microsecond(microsecond), Nanosecond(nanosecond), Week(week))
))
end
function pytimedelta64(@nospecialize(x::T)) where T <: Period
index = findfirst(==(T), (Year, Month, Week, Day, Hour, Minute, Second, Millisecond, Microsecond, Nanosecond, T))::Int
unit = ("Y", "M", "W", "D", "h", "m", "s", "ms", "us", "ns", "")[index]
pyimport("numpy").timedelta64(x.value, unit)
end
function pytimedelta64(x::CompoundPeriod)
x = canonicalize(x)
isempty(x.periods) ? pytimedelta64(Second(0)) : sum(pytimedelta64.(x.periods))
end
export pytimedelta64

function pyconvert_rule_datetime64(::Type{DateTime}, x::Py)
unit, count = pyconvert(Tuple, pyimport("numpy").datetime_data(x))
value = reinterpret(Int64, pyconvert(Vector, x))[1]
units = ("Y", "M", "W", "D", "h", "m", "s", "ms", "us", "ns")
types = (Year, Month, Week, Day, Hour, Minute, Second, Millisecond, Microsecond, Nanosecond)
T = types[findfirst(==(unit), units)::Int]
pyconvert_return(DateTime(_base_datetime) + T(value * count))
end

function pyconvert_rule_timedelta64(::Type{CompoundPeriod}, x::Py)
unit, count = pyconvert(Tuple, pyimport("numpy").datetime_data(x))
value = reinterpret(Int64, pyconvert(Vector, x))[1]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is reinterpret safe here? Is there a better alternative to use?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought, pyconvert creates a new Julia Vector which is not mapped onto Python data. If that would be the case, we'd need to wrap the vector by a copy().

units = ("Y", "M", "W", "D", "h", "m", "s", "ms", "us", "ns")
types = (Year, Month, Week, Day, Hour, Minute, Second, Millisecond, Microsecond, Nanosecond)
T = types[findfirst(==(unit), units)::Int]
pyconvert_return(CompoundPeriod(T(value * count)) |> canonicalize)
end

function pyconvert_rule_timedelta64(::Type{T}, x::Py) where T<:Period
pyconvert_return(convert(T, pyconvert_rule_timedelta64(CompoundPeriod, x)))
end

function init_numpy()
for (t, T) in NUMPY_SIMPLE_TYPES
isbool = occursin("bool", t)
Expand Down Expand Up @@ -54,4 +116,14 @@ function init_numpy()
iscomplex && pyconvert_add_rule(name, Complex, rule)
isnumber && pyconvert_add_rule(name, Number, rule)
end

priority = PYCONVERT_PRIORITY_ARRAY
pyconvert_add_rule("numpy:datetime64", DateTime, pyconvert_rule_datetime64, priority)
let TT = (CompoundPeriod, Year, Month, Day, Hour, Minute, Second, Millisecond, Microsecond, Nanosecond, Week)
Base.Cartesian.@nexprs 11 i -> pyconvert_add_rule("numpy:timedelta64", TT[i], pyconvert_rule_timedelta64, priority)
end

priority = PYCONVERT_PRIORITY_CANONICAL
pyconvert_add_rule("numpy:datetime64", DateTime, pyconvert_rule_datetime64, priority)
pyconvert_add_rule("numpy:timedelta64", Nanosecond, pyconvert_rule_timedelta, priority)
end
6 changes: 6 additions & 0 deletions src/Convert/pyconvert.jl
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,12 @@ function init_pyconvert()
pyimport("collections.abc" => ("Iterable", "Sequence", "Set", "Mapping"))...,
)

priority = PYCONVERT_PRIORITY_ARRAY
pyconvert_add_rule("datetime:datetime", DateTime, pyconvert_rule_datetime, priority)
for T in (Millisecond, Second, Nanosecond, Day, Hour, Minute, Second, Millisecond, Week, CompoundPeriod)
pyconvert_add_rule("datetime:timedelta", T, pyconvert_rule_timedelta, priority)
end

priority = PYCONVERT_PRIORITY_CANONICAL
pyconvert_add_rule("builtins:NoneType", Nothing, pyconvert_rule_none, priority)
pyconvert_add_rule("builtins:bool", Bool, pyconvert_rule_bool, priority)
Expand Down
13 changes: 13 additions & 0 deletions src/Convert/rules.jl
Original file line number Diff line number Diff line change
Expand Up @@ -512,3 +512,16 @@ function pyconvert_rule_timedelta(::Type{Second}, x::Py)
end
return Second(days * 3600 * 24 + seconds)
end

function pyconvert_rule_timedelta(::Type{<:CompoundPeriod}, x::Py)
days = pyconvert(Int, x.days)
seconds = pyconvert(Int, x.seconds)
microseconds = pyconvert(Int, x.microseconds)
nanoseconds = pyhasattr(x, "nanoseconds") ? pyconvert(Int, x.nanoseconds) : 0
timedelta = Day(days) + Second(seconds) + Microsecond(microseconds) + Nanosecond(nanoseconds)
return pyconvert_return(timedelta)
end

function pyconvert_rule_timedelta(::Type{T}, x::Py) where T<:Period
pyconvert_return(convert(T, pyconvert_rule_timedelta(CompoundPeriod, x)))
end
12 changes: 11 additions & 1 deletion src/Core/Core.jl
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,17 @@ using Dates:
second,
millisecond,
microsecond,
nanosecond
nanosecond,
Day,
Hour,
Week,
Minute,
Second,
Millisecond,
Microsecond,
Period,
CompoundPeriod,
canonicalize
using MacroTools: MacroTools, @capture
using Markdown: Markdown

Expand Down
1 change: 1 addition & 0 deletions src/Core/Py.jl
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ Py(
Py(x::Date) = pydate(x)
Py(x::Time) = pytime(x)
Py(x::DateTime) = pydatetime(x)
Py(x::Union{Period, CompoundPeriod}) = pytimedelta(x)

Base.string(x::Py) = pyisnull(x) ? "<py NULL>" : pystr(String, x)
Base.print(io::IO, x::Py) = print(io, string(x))
Expand Down
18 changes: 18 additions & 0 deletions src/Core/builtins.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1167,6 +1167,24 @@ end
pydatetime(x::Date) = pydatetime(year(x), month(x), day(x))
export pydatetime

function pytimedelta(
_day::Int=0, _second::Int=0, _microsecond::Int=0, _millisecond::Int=0, _minute::Int=0, _hour::Int=0, _week::Int=0;
day::Int=_day, second::Int=_second, microsecond::Int=_microsecond, millisecond::Int=_millisecond, minute::Int=_minute, hour::Int=_hour, week::Int=_week
)
pyimport("datetime").timedelta(day, second, microsecond, millisecond, minute, hour, week)
end
function pytimedelta(@nospecialize(x::T)) where T <: Period
T <: Union{Week, Day, Hour, Minute, Second, Millisecond, Microsecond} ||
error("Unsupported Period type: ", "Year, Month and Nanosecond are not supported, consider using pytimedelta64 instead.")
args = T .== (Day, Second, Millisecond, Microsecond, Minute, Hour, Week)
pytimedelta(x.value .* args...)
end
function pytimedelta(x::CompoundPeriod)
x = canonicalize(x)
isempty(x.periods) ? pytimedelta(Second(0)) : sum(pytimedelta.(x.periods))
end
export pytimedelta

function pytime_isaware(x)
tzinfo = pygetattr(x, "tzinfo")
if pyisnone(tzinfo)
Expand Down
16 changes: 16 additions & 0 deletions src/PyMacro/PyMacro.jl
Original file line number Diff line number Diff line change
Expand Up @@ -886,6 +886,9 @@ For example:
- `import x: f as g` is translated to `g = pyimport("x" => "f")` (`from x import f as g` in Python)

Compound statements such as `begin`, `if`, `while` and `for` are supported.
Import statements are supported, e.g.
- `import foo, bar`
- `from os.path import join as py_joinpath, exists`

See the online documentation for more details.

Expand All @@ -895,6 +898,19 @@ See the online documentation for more details.
macro py(ex)
esc(py_macro(ex, __module__, __source__))
end

macro py(keyword, modulename, ex)
keyword == :from || return :( nothing )

d = Dict(isa(a.args[1], Symbol) ? a.args[1] => a.args[1] : a.args[1].args[1] => a.args[2] for a in ex.args)
vars = Expr(:tuple, values(d)...)
imports = Tuple(keys(d))

esc(quote
$vars = pyimport($(string(modulename)) => $(string.(imports)))
end)
end

export @py

end
Loading