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 support for datetime64 #334

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
3 changes: 2 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ julia = "1.6.1"

[extras]
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a"

[targets]
test = ["Aqua", "Test", "TestItemRunner"]
test = ["Aqua", "Test", "TestItemRunner", "DataFrames"]
1 change: 1 addition & 0 deletions src/Py.jl
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ Py(x::AbstractRange{<:Union{Int8,Int16,Int32,Int64,Int128,UInt8,UInt16,UInt32,UI
Py(x::Date) = pydate(x)
Py(x::Time) = pytime(x)
Py(x::DateTime) = pydatetime(x)
Py(x::Union{Period, Dates.CompoundPeriod}) = pytimedelta64(x)
Py(x) = ispy(x) ? throw(MethodError(Py, (x,))) : pyjl(x)

Base.string(x::Py) = pyisnull(x) ? "<py NULL>" : pystr(String, x)
Expand Down
59 changes: 59 additions & 0 deletions src/concrete/datetime.jl
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,40 @@ end
pydatetime(x::Date) = pydatetime(year(x), month(x), day(x))
export pydatetime

function pytimedelta64(_year=0, _month=0, _day=0, _hour=0, _minute=0, _second=0, _millisecond=0, _microsecond=0, _nanosecond=0; year=_year, month=_month, day=_day, hour=_hour, minute=_minute, second=_second, microsecond=_microsecond, millisecond=_millisecond, nanosecond=_nanosecond)
pytimedelta64(sum((
Year(year), Month(month), Day(day), Hour(hour),
Minute(minute), Second(second), Millisecond(millisecond), Microsecond(microsecond), Nanosecond(nanosecond))
))
end

function pytimedelta64(@nospecialize(x::T)) where T <: Period
unit = if T==Year
"Y"
elseif T==Month
"M"
elseif T==Day
"D"
elseif T==Hour
"h"
elseif T==Minute
"m"
elseif T==Second
"s"
elseif T==Millisecond
"ms"
elseif T==Microsecond
"us"
elseif T==Nanosecond
"ns"
else
""
end
pyimport("numpy").timedelta64(x.value, unit)
end
pytimedelta64(x::Dates.CompoundPeriod) = isempty(x.periods) ? pytimedelta64(Second(0)) : sum(pytimedelta64.(x.periods))
export pytimedelta64

function pytime_isaware(x)
tzinfo = pygetattr(x, "tzinfo")
if pyisnone(tzinfo)
Expand Down Expand Up @@ -93,3 +127,28 @@ function pyconvert_rule_datetime(::Type{DateTime}, x::Py)
iszero(mod(microseconds, 1000)) || return pyconvert_unconverted()
return pyconvert_return(_base_datetime + Millisecond(div(microseconds, 1000) + 1000 * (seconds + 60 * 60 * 24 * days)))
end

function pyconvert_rule_datetime64(::Type{DateTime}, x::Py)
pyconvert(DateTime, pyimport("pandas").to_datetime(x))
end

function pyconvert_rule_timedelta(::Type{<:Dates.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(Dates.CompoundPeriod, x)))
end

function pyconvert_rule_timedelta64(::Type{Dates.CompoundPeriod}, x::Py)
pyconvert_rule_timedelta(Dates.CompoundPeriod, pyimport("pandas").to_timedelta(x))
end

function pyconvert_rule_timedelta64(::Type{T}, x::Py) where T<:Period
pyconvert_return(convert(T, pyconvert_rule_timedelta64(Dates.CompoundPeriod, x)))
end
12 changes: 11 additions & 1 deletion src/convert.jl
Original file line number Diff line number Diff line change
Expand Up @@ -390,12 +390,21 @@ function init_pyconvert()

priority = PYCONVERT_PRIORITY_WRAP
pyconvert_add_rule("juliacall:ValueBase", Any, pyconvert_rule_jlvalue, priority)

priority = PYCONVERT_PRIORITY_ARRAY
pyconvert_add_rule("<arraystruct>", PyArray, pyconvert_rule_array_nocopy, priority)
pyconvert_add_rule("<arrayinterface>", PyArray, pyconvert_rule_array_nocopy, priority)
pyconvert_add_rule("<array>", PyArray, pyconvert_rule_array_nocopy, priority)
pyconvert_add_rule("<buffer>", PyArray, pyconvert_rule_array_nocopy, priority)
pyconvert_add_rule("numpy:datetime64", DateTime, pyconvert_rule_datetime64, priority)
pyconvert_add_rule("numpy:timedelta64", Dates.CompoundPeriod, pyconvert_rule_timedelta64, priority)
pyconvert_add_rule("numpy:timedelta64", Year, pyconvert_rule_timedelta64, priority)
pyconvert_add_rule("numpy:timedelta64", Month, pyconvert_rule_timedelta64, priority)
pyconvert_add_rule("numpy:timedelta64", Day, pyconvert_rule_timedelta64, priority)
pyconvert_add_rule("numpy:timedelta64", Second, pyconvert_rule_timedelta64, priority)
pyconvert_add_rule("numpy:timedelta64", Millisecond, pyconvert_rule_timedelta64, priority)
pyconvert_add_rule("numpy:timedelta64", Microsecond, pyconvert_rule_timedelta64, priority)
pyconvert_add_rule("numpy:timedelta64", Nanosecond, pyconvert_rule_timedelta64, priority)

priority = PYCONVERT_PRIORITY_CANONICAL
pyconvert_add_rule("builtins:NoneType", Nothing, pyconvert_rule_none, priority)
Expand All @@ -420,6 +429,7 @@ function init_pyconvert()
pyconvert_add_rule("datetime:datetime", DateTime, pyconvert_rule_datetime, priority)
pyconvert_add_rule("datetime:date", Date, pyconvert_rule_date, priority)
pyconvert_add_rule("datetime:time", Time, pyconvert_rule_time, priority)
pyconvert_add_rule("datetime:timedelta", Dates.CompoundPeriod, pyconvert_rule_timedelta, priority)
pyconvert_add_rule("builtins:BaseException", PyException, pyconvert_rule_exception, priority)

priority = PYCONVERT_PRIORITY_NORMAL
Expand Down
2 changes: 1 addition & 1 deletion src/pywrap/PyArray.jl
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ function pyarray_get_R(src::PyArraySource_ArrayStruct)
elseif kind == 109 # m = timedelta
error("timedelta not supported")
elseif kind == 77 # M = datetime
error("datetime not supported")
error("datetime64 not supported")
elseif kind == 79 # O = object
if size == sizeof(C.PyPtr)
return UnsafePyObject
Expand Down
12 changes: 12 additions & 0 deletions test/pywrap.jl
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,18 @@ end
end

@testitem "PyPandasDataFrame" begin
using Dates
using DataFrames
using CondaPkg
CondaPkg.add("pandas")
jdf = DataFrame(x = [now() + Second(rand(1:1000)) for _ in 1:100], y = [Second(n) for n in 1:100])
pdf = pytable(jdf)
@test PyTable(pdf) isa PyPandasDataFrame
@test PythonCall.pyconvert_typename(pdf.x[0]) == "pandas._libs.tslibs.timestamps:<name>"
@test PythonCall.pyconvert_typename(pdf.y[0]) == "pandas._libs.tslibs.timedeltas:<name>"
jdf2 = DataFrame(PyTable(pdf))
@test all((jdf .== jdf2).x)
@test all((jdf .== jdf2).y)
end

@testitem "PySet" begin
Expand Down