Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8e6a7dc
add NumpyDates module with DateTime64 and TimeDelta64 types, plus con…
Sep 7, 2025
2a21d64
feat(numpydates): add constructors and conversions for delta64 types
Sep 7, 2025
4ad1812
fix(numpydates): correct year/month extraction in DateTime64(Date)
Sep 7, 2025
b66d37d
test(numpydates): add tests for Date to DateTime64 unit mapping
Sep 13, 2025
4f78f7e
fix(numpydates): use floor div and fix atto scale
Sep 13, 2025
34adabc
test(numpydates): test more constructors
Sep 13, 2025
b949573
test(numpydates): add test for show(datetime64)
Sep 13, 2025
7f2a0b5
fix(numpydates): resolve method ambiguity by splitting unions
Sep 13, 2025
34c5662
test(numpydates): add tests for timedelta64
Sep 13, 2025
955ebee
test(numpydates): add test for conversion to Date/DateTime
Sep 13, 2025
2f7ccb4
test(numpydates): add tests for defaultunit and add more methods for …
Sep 13, 2025
2c89e08
test(numpydates): code shuffle
Sep 13, 2025
a461e3b
test(numpydates): add tests for isnan
Sep 13, 2025
be62b3d
docs(numpydates): add comprehensive docstrings for NumpyDates types
Sep 14, 2025
350171e
docs(numpydates): add NumpyDates reference and clarify unit docs
Sep 14, 2025
de9e29f
docs(numpydates): add numpy.datetime64 and numpy.timedelta64 conversions
Sep 17, 2025
ba2ee61
refactor(numpydates): simplify date and time delta conversions with r…
Sep 17, 2025
4f3195a
refactor(numpydates): add rescale support and expand convert methods …
Sep 17, 2025
c8c411a
refactor(numpydates): simplify accessors and add inline datetime/time…
Sep 17, 2025
0ecb038
refactor(numpydates): add concrete type check for inline datetime/tim…
Sep 17, 2025
873f398
docs(numpydates): update release notes with NumpyDates features
Sep 17, 2025
58ed2a8
refactor(numpydates): remove unbound units export
Sep 17, 2025
60de18b
Merge remote-tracking branch 'origin/main' into numpy-dates
Sep 17, 2025
eca7adb
test(numpydates): fixes for julia 1.9
Sep 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/src/conversion-to-julia.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ From Python, the arguments to a Julia function will be converted according to th
| `datetime.date`/`datetime.time`/`datetime.datetime` | `Date`/`Time`/`DateTime` |
| `datetime.timedelta` | `Microsecond` (or `Millisecond` or `Second` on overflow) |
| `numpy.intXX`/`numpy.uintXX`/`numpy.floatXX` | `IntXX`/`UIntXX`/`FloatXX` |
| `numpy.datetime64` | `NumpyDates.DateTime64` |
| `numpy.timedelta64` | `NumpyDates.TimeDelta64` |
| **Standard priority (other reasonable conversions).** | |
| `None` | `Missing` |
| `bytes` | `Vector{UInt8}`, `Vector{Int8}`, `String` |
Expand All @@ -48,6 +50,8 @@ From Python, the arguments to a Julia function will be converted according to th
| `ctypes.c_char_p` | `Cstring`, `Ptr{Cchar}`, `Ptr` |
| `ctypes.c_wchar_p` | `Cwstring`, `Ptr{Cwchar}`, `Ptr` |
| `numpy.bool_`/`numpy.intXX`/`numpy.uintXX`/`numpy.floatXX` | `Bool`, `Integer`, `Rational`, `Real`, `Number` |
| `numpy.datetime64` | `NumpyDates.InlineDateTime64`, `Dates.DateTime` |
| `numpy.timedelta64` | `NumpyDates.InlineTimeDelta64`, `Dates.Period` |
| Objects satisfying the buffer or array interface | `Array`, `AbstractArray` |
| **Low priority (fallback to `Py`).** | |
| Anything | `Py` |
Expand Down
15 changes: 15 additions & 0 deletions docs/src/pythoncall-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,18 @@ PythonCall.getptr
PythonCall.pydel!
PythonCall.unsafe_pynext
```

## NumpyDates

The submodule `PythonCall.NumpyDates` provides types corresponding to Numpy's `datetime64` and `timedelta64` types. Enables conversion of these Numpy types (either as scalars or in arrays) to native Julia types.

```@docs
PythonCall.NumpyDates
PythonCall.NumpyDates.AbstractDateTime64
PythonCall.NumpyDates.InlineDateTime64
PythonCall.NumpyDates.DateTime64
PythonCall.NumpyDates.AbstractTimeDelta64
PythonCall.NumpyDates.InlineTimeDelta64
PythonCall.NumpyDates.TimeDelta64
PythonCall.NumpyDates.Unit
```
4 changes: 4 additions & 0 deletions docs/src/releasenotes.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Release Notes

## Unreleased
* Added `NumpyDates`: NumPy-compatible DateTime64/TimeDelta64 types and units.
* Added `pyconvert` rules for NumpyDates types.
* Added `PyArray` support for NumPy arrays of `datetime64` and `timedelta64`.
* Added `juliacall.ArrayValue` support for Julia arrays of `InlineDateTime64` and `InlineTimeDelta64`.
* Bug fixes.
* Internal: switch from Requires.jl to package extensions.

Expand Down
4 changes: 2 additions & 2 deletions src/Convert/Convert.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ using ..PythonCall
using ..Utils
using ..C
using ..Core
using ..NumpyDates

using Dates: Date, Time, DateTime, Second, Millisecond, Microsecond, Nanosecond

Expand All @@ -20,8 +21,7 @@ import ..PythonCall:
pyconvert,
PyConvertPriority

export
pyconvert_isunconverted,
export pyconvert_isunconverted,
pyconvert_result,
pyconvert_result,
pyconvert_tryconvert,
Expand Down
103 changes: 103 additions & 0 deletions src/Convert/numpy.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,76 @@ function (::pyconvert_rule_numpysimplevalue{R,SAFE})(::Type{T}, x::Py) where {R,
end
end

function pyconvert_rule_datetime64(::Type{DateTime64}, x::Py)
pyconvert_return(C.PySimpleObject_GetValue(DateTime64, x))
end

function pyconvert_rule_datetime64(::Type{T}, x::Py) where {T<:InlineDateTime64}
pyconvert_tryconvert(T, C.PySimpleObject_GetValue(DateTime64, x))
end

function pyconvert_rule_datetime64(::Type{T}, x::Py) where {T<:NumpyDates.DatesInstant}
d = C.PySimpleObject_GetValue(DateTime64, x)
if isnan(d)
pyconvert_unconverted()
else
pyconvert_tryconvert(T, d)
end
end

function pyconvert_rule_datetime64(::Type{Missing}, x::Py)
d = C.PySimpleObject_GetValue(DateTime64, x)
if isnan(d)
pyconvert_return(missing)
else
pyconvert_unconverted()
end
end

function pyconvert_rule_datetime64(::Type{Nothing}, x::Py)
d = C.PySimpleObject_GetValue(DateTime64, x)
if isnan(d)
pyconvert_return(nothing)
else
pyconvert_unconverted()
end
end

function pyconvert_rule_timedelta64(::Type{TimeDelta64}, x::Py)
pyconvert_return(C.PySimpleObject_GetValue(TimeDelta64, x))
end

function pyconvert_rule_timedelta64(::Type{T}, x::Py) where {T<:InlineTimeDelta64}
pyconvert_tryconvert(T, C.PySimpleObject_GetValue(TimeDelta64, x))
end

function pyconvert_rule_timedelta64(::Type{T}, x::Py) where {T<:NumpyDates.DatesPeriod}
d = C.PySimpleObject_GetValue(TimeDelta64, x)
if isnan(d)
pyconvert_unconverted()
else
pyconvert_tryconvert(T, d)
end
end

function pyconvert_rule_timedelta64(::Type{Missing}, x::Py)
d = C.PySimpleObject_GetValue(TimeDelta64, x)
if isnan(d)
pyconvert_return(missing)
else
pyconvert_unconverted()
end
end

function pyconvert_rule_timedelta64(::Type{Nothing}, x::Py)
d = C.PySimpleObject_GetValue(TimeDelta64, x)
if isnan(d)
pyconvert_return(missing)
else
pyconvert_unconverted()
end
end

const NUMPY_SIMPLE_TYPES = [
("bool_", Bool),
("int8", Int8),
Expand All @@ -28,6 +98,7 @@ const NUMPY_SIMPLE_TYPES = [
]

function init_numpy()
# simple numeric scalar types
for (t, T) in NUMPY_SIMPLE_TYPES
isbool = occursin("bool", t)
isint = occursin("int", t) || isbool
Expand All @@ -54,4 +125,36 @@ function init_numpy()
iscomplex && pyconvert_add_rule(name, Complex, rule)
isnumber && pyconvert_add_rule(name, Number, rule)
end

# datetime64
pyconvert_add_rule(
"numpy:datetime64",
DateTime64,
pyconvert_rule_datetime64,
PYCONVERT_PRIORITY_ARRAY,
)
pyconvert_add_rule("numpy:datetime64", InlineDateTime64, pyconvert_rule_datetime64)
pyconvert_add_rule(
"numpy:datetime64",
NumpyDates.DatesInstant,
pyconvert_rule_datetime64,
)
pyconvert_add_rule("numpy:datetime64", Missing, pyconvert_rule_datetime64)
pyconvert_add_rule("numpy:datetime64", Nothing, pyconvert_rule_datetime64)

# timedelta64
pyconvert_add_rule(
"numpy:timedelta64",
TimeDelta64,
pyconvert_rule_timedelta64,
PYCONVERT_PRIORITY_ARRAY,
)
pyconvert_add_rule("numpy:timedelta64", InlineTimeDelta64, pyconvert_rule_timedelta64)
pyconvert_add_rule(
"numpy:timedelta64",
NumpyDates.DatesPeriod,
pyconvert_rule_timedelta64,
)
pyconvert_add_rule("numpy:timedelta64", Missing, pyconvert_rule_timedelta64)
pyconvert_add_rule("numpy:timedelta64", Nothing, pyconvert_rule_timedelta64)
end
1 change: 1 addition & 0 deletions src/JlWrap/JlWrap.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module JlWrap

using ..PythonCall
using ..Utils
using ..NumpyDates: NumpyDates
using ..C
using ..Core
using ..Convert
Expand Down
57 changes: 41 additions & 16 deletions src/JlWrap/array.jl
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,8 @@ pyjlarray_isarrayabletype(::Type{T}) where {T} = T in (
Complex{Float32},
Complex{Float64},
)
pyjlarray_isarrayabletype(::Type{NumpyDates.InlineDateTime64{U}}) where {U} = true
pyjlarray_isarrayabletype(::Type{NumpyDates.InlineTimeDelta64{U}}) where {U} = true
pyjlarray_isarrayabletype(::Type{T}) where {T<:Tuple} =
isconcretetype(T) &&
Base.allocatedinline(T) &&
Expand All @@ -235,22 +237,45 @@ const PYTYPESTRDESCR = IdDict{Type,Tuple{String,Py}}()
pytypestrdescr(::Type{T}) where {T} =
get!(PYTYPESTRDESCR, T) do
c = Utils.islittleendian() ? '<' : '>'
T == Bool ? ("$(c)b$(sizeof(Bool))", PyNULL) :
T == Int8 ? ("$(c)i1", PyNULL) :
T == UInt8 ? ("$(c)u1", PyNULL) :
T == Int16 ? ("$(c)i2", PyNULL) :
T == UInt16 ? ("$(c)u2", PyNULL) :
T == Int32 ? ("$(c)i4", PyNULL) :
T == UInt32 ? ("$(c)u4", PyNULL) :
T == Int64 ? ("$(c)i8", PyNULL) :
T == UInt64 ? ("$(c)u8", PyNULL) :
T == Float16 ? ("$(c)f2", PyNULL) :
T == Float32 ? ("$(c)f4", PyNULL) :
T == Float64 ? ("$(c)f8", PyNULL) :
T == Complex{Float16} ? ("$(c)c4", PyNULL) :
T == Complex{Float32} ? ("$(c)c8", PyNULL) :
T == Complex{Float64} ? ("$(c)c16", PyNULL) :
if isstructtype(T) && isconcretetype(T) && Base.allocatedinline(T)
if T == Bool
("$(c)b$(sizeof(Bool))", PyNULL)
elseif T == Int8
("$(c)i1", PyNULL)
elseif T == UInt8
("$(c)u1", PyNULL)
elseif T == Int16
("$(c)i2", PyNULL)
elseif T == UInt16
("$(c)u2", PyNULL)
elseif T == Int32
("$(c)i4", PyNULL)
elseif T == UInt32
("$(c)u4", PyNULL)
elseif T == Int64
("$(c)i8", PyNULL)
elseif T == UInt64
("$(c)u8", PyNULL)
elseif T == Float16
("$(c)f2", PyNULL)
elseif T == Float32
("$(c)f4", PyNULL)
elseif T == Float64
("$(c)f8", PyNULL)
elseif T == Complex{Float16}
("$(c)c4", PyNULL)
elseif T == Complex{Float32}
("$(c)c8", PyNULL)
elseif T == Complex{Float64}
("$(c)c16", PyNULL)
elseif isconcretetype(T) &&
T <: Union{NumpyDates.InlineDateTime64,NumpyDates.InlineTimeDelta64}
u, m = NumpyDates.unitpair(T)
tc = T <: NumpyDates.InlineDateTime64 ? 'M' : 'm'
us =
u == NumpyDates.UNBOUND_UNITS ? "" :
m == 1 ? "[$(Symbol(u))]" : "[$(m)$(Symbol(u))]"
("$(c)$(tc)8$(us)", PyNULL)
elseif isstructtype(T) && isconcretetype(T) && Base.allocatedinline(T)
n = fieldcount(T)
flds = []
for i = 1:n
Expand Down
58 changes: 58 additions & 0 deletions src/NumpyDates/AbstractDateTime64.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""
abstract type AbstractDateTime64 <: Dates.TimeType

Supertype for [`DateTime64`](@ref) and [`InlineDateTime64`](@ref).
"""
abstract type AbstractDateTime64 <: Dates.TimeType end

function Dates.DateTime(d::AbstractDateTime64)
isnan(d) && error("Cannot convert NaT to DateTime")
v = value(d)
u, _ = unit = unitpair(d)
b = Dates.DateTime(1970)
if u > MONTHS
v, _ = rescale(v, unit, MILLISECONDS)
b + Dates.Millisecond(v)
else
v, _ = rescale(v, unit, MONTHS)
b + Dates.Month(v)
end
end

function Dates.Date(d::AbstractDateTime64)
isnan(d) && error("Cannot convert NaT to Date")
Dates.Date(Dates.DateTime(d))
end

Base.convert(::Type{Dates.DateTime}, d::AbstractDateTime64) = Dates.DateTime(d)
Base.convert(::Type{Dates.Date}, d::AbstractDateTime64) = Dates.Date(d)

function Base.isnan(d::AbstractDateTime64)
value(d) == typemin(Int64)
end

function showvalue(io::IO, d::AbstractDateTime64)
u, m = unit = unitpair(d)
if isnan(d)
show(io, "NaT")
elseif u ≤ DAYS
d2 = Dates.Date(d)
if value(DateTime64(d2, unit)) == value(d)
show(io, string(d2))
else
show(io, value(d))
end
else
d2 = Dates.DateTime(d)
if value(DateTime64(d2, unit)) == value(d)
show(io, string(d2))
else
show(io, value(d))
end
end
nothing
end

function defaultunit(d::AbstractDateTime64)
unitpair(d)
end
50 changes: 50 additions & 0 deletions src/NumpyDates/AbstractTimeDelta64.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""
abstract type AbstractTimeDelta64 <: Dates.Period

Supertype for [`TimeDelta64`](@ref) and [`InlineTimeDelta64`](@ref).
"""
abstract type AbstractTimeDelta64 <: Dates.Period end

function construct(::Type{T}, d::AbstractTimeDelta64) where {T<:DatesPeriod}
v, r = rescale(value(d), unitpair(d), Unit(T))
iszero(r) || throw(InexactError(:convert, T, d))
T(v)
end

Dates.Year(d::AbstractTimeDelta64) = construct(Dates.Year, d)
Dates.Month(d::AbstractTimeDelta64) = construct(Dates.Month, d)
Dates.Day(d::AbstractTimeDelta64) = construct(Dates.Day, d)
Dates.Hour(d::AbstractTimeDelta64) = construct(Dates.Hour, d)
Dates.Minute(d::AbstractTimeDelta64) = construct(Dates.Minute, d)
Dates.Second(d::AbstractTimeDelta64) = construct(Dates.Second, d)
Dates.Millisecond(d::AbstractTimeDelta64) = construct(Dates.Millisecond, d)
Dates.Microsecond(d::AbstractTimeDelta64) = construct(Dates.Microsecond, d)
Dates.Nanosecond(d::AbstractTimeDelta64) = construct(Dates.Nanosecond, d)

Base.convert(::Type{Dates.Year}, d::AbstractTimeDelta64) = Dates.Year(d)
Base.convert(::Type{Dates.Month}, d::AbstractTimeDelta64) = Dates.Month(d)
Base.convert(::Type{Dates.Day}, d::AbstractTimeDelta64) = Dates.Day(d)
Base.convert(::Type{Dates.Hour}, d::AbstractTimeDelta64) = Dates.Hour(d)
Base.convert(::Type{Dates.Minute}, d::AbstractTimeDelta64) = Dates.Minute(d)
Base.convert(::Type{Dates.Second}, d::AbstractTimeDelta64) = Dates.Second(d)
Base.convert(::Type{Dates.Millisecond}, d::AbstractTimeDelta64) = Dates.Millisecond(d)
Base.convert(::Type{Dates.Microsecond}, d::AbstractTimeDelta64) = Dates.Microsecond(d)
Base.convert(::Type{Dates.Nanosecond}, d::AbstractTimeDelta64) = Dates.Nanosecond(d)

function Base.isnan(d::AbstractTimeDelta64)
value(d) == NAT
end

function showvalue(io::IO, d::AbstractTimeDelta64)
u, m = unitpair(d)
if isnan(d)
show(io, "NaT")
else
show(io, value(d))
end
nothing
end

function defaultunit(d::AbstractTimeDelta64)
unitpair(d)
end
Loading
Loading