diff --git a/Project.toml b/Project.toml index b7770094..ecf1f953 100644 --- a/Project.toml +++ b/Project.toml @@ -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"] diff --git a/src/Py.jl b/src/Py.jl index 13fbdb02..b815ca9a 100644 --- a/src/Py.jl +++ b/src/Py.jl @@ -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) ? "" : pystr(String, x) diff --git a/src/concrete/datetime.jl b/src/concrete/datetime.jl index 1e0c41a8..76fbe50a 100644 --- a/src/concrete/datetime.jl +++ b/src/concrete/datetime.jl @@ -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) @@ -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 \ No newline at end of file diff --git a/src/convert.jl b/src/convert.jl index 498293a5..402b01fd 100644 --- a/src/convert.jl +++ b/src/convert.jl @@ -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("", PyArray, pyconvert_rule_array_nocopy, priority) pyconvert_add_rule("", PyArray, pyconvert_rule_array_nocopy, priority) pyconvert_add_rule("", PyArray, pyconvert_rule_array_nocopy, priority) pyconvert_add_rule("", 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) @@ -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 diff --git a/src/pywrap/PyArray.jl b/src/pywrap/PyArray.jl index af7933cf..32ca236a 100644 --- a/src/pywrap/PyArray.jl +++ b/src/pywrap/PyArray.jl @@ -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 diff --git a/test/pywrap.jl b/test/pywrap.jl index 13b4d3a1..4c53b1dc 100644 --- a/test/pywrap.jl +++ b/test/pywrap.jl @@ -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:" + @test PythonCall.pyconvert_typename(pdf.y[0]) == "pandas._libs.tslibs.timedeltas:" + jdf2 = DataFrame(PyTable(pdf)) + @test all((jdf .== jdf2).x) + @test all((jdf .== jdf2).y) end @testitem "PySet" begin