Skip to content

Commit af20b15

Browse files
committed
Add datetime, timedelta, date support for numpy arrays
Signed-off-by: Arham Chopra <arham.chopra@cubistsystematic.com>
1 parent 126fdd8 commit af20b15

4 files changed

Lines changed: 631 additions & 0 deletions

File tree

cpp/csp/python/adapters/ArrowCppNodes.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,14 @@ static std::vector<int64_t> numpyShape( DialectGenericType ndarray )
7878
static int npyTypeFromPyType( DialectGenericType pyTypeObj )
7979
{
8080
auto & cspType = csp::python::pyTypeAsCspType( csp::python::toPythonBorrowed( pyTypeObj ) );
81+
auto cspTypeEnum = cspType -> type();
82+
83+
// Temporal types don't have NPY_TYPE<> specializations — handle them explicitly
84+
if( cspTypeEnum == CspType::Type::DATETIME || cspTypeEnum == CspType::Type::DATE )
85+
return NPY_DATETIME;
86+
if( cspTypeEnum == CspType::Type::TIMEDELTA )
87+
return NPY_TIMEDELTA;
88+
8189
return csp::PartialSwitchCspType<csp::CspType::Type::DOUBLE, csp::CspType::Type::INT64,
8290
csp::CspType::Type::BOOL, csp::CspType::Type::STRING>::invoke(
8391
cspType.get(),

cpp/csp/python/adapters/ArrowNumpyListReader.h

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
#include <arrow/array.h>
1515
#include <arrow/type.h>
1616

17+
#include <csp/core/Time.h>
18+
1719
#include <codecvt>
1820
#include <cstring>
1921
#include <functional>
@@ -140,6 +142,117 @@ makeStringListReadValue()
140142
};
141143
}
142144

145+
// Helper: compute nanosecond multiplier for a given arrow::TimeUnit
146+
inline int64_t listTimeUnitMultiplier( ::arrow::TimeUnit::type unit )
147+
{
148+
switch( unit )
149+
{
150+
case ::arrow::TimeUnit::SECOND: return csp::NANOS_PER_SECOND;
151+
case ::arrow::TimeUnit::MILLI: return csp::NANOS_PER_MILLISECOND;
152+
case ::arrow::TimeUnit::MICRO: return csp::NANOS_PER_MICROSECOND;
153+
case ::arrow::TimeUnit::NANO: return 1LL;
154+
}
155+
CSP_THROW( TypeError, "Unexpected arrow TimeUnit: " << static_cast<int>( unit ) );
156+
}
157+
158+
// Create a readValue lambda for datetime64[ns] from Arrow Timestamp/Date columns
159+
// ArrowValueArrayT is the typed Arrow array (TimestampArray, Date32Array, Date64Array)
160+
// multiplier converts from Arrow units to nanoseconds
161+
template<typename ArrowValueArrayT>
162+
std::function<DialectGenericType( const ::arrow::Array &, int64_t )>
163+
makeTimestampListReadValue( int64_t multiplier )
164+
{
165+
// Lazily create and cache the datetime64[ns] descriptor
166+
static PyArray_Descr * s_dtype = nullptr;
167+
if( !s_dtype )
168+
{
169+
auto dtypeStr = PyPtr<PyObject>::own( PyUnicode_FromString( "<M8[ns]" ) );
170+
PyArray_DescrConverter( dtypeStr.get(), &s_dtype );
171+
}
172+
173+
return [multiplier]( const ::arrow::Array & arr, int64_t row ) -> DialectGenericType
174+
{
175+
auto & listArr = static_cast<const ::arrow::ListArray &>( arr );
176+
auto values = std::static_pointer_cast<ArrowValueArrayT>( listArr.value_slice( row ) );
177+
178+
npy_intp size = values -> length();
179+
Py_INCREF( s_dtype );
180+
PyObject * pyArr = PyArray_SimpleNewFromDescr( 1, &size, s_dtype );
181+
PyObjectPtr arrOwner{ PyObjectPtr::own( pyArr ) };
182+
183+
auto * buf = reinterpret_cast<int64_t *>( PyArray_DATA( reinterpret_cast<PyArrayObject *>( pyArr ) ) );
184+
185+
// Fast path: nanosecond units (multiplier==1), no nulls, AND 64-bit storage — bulk memcpy.
186+
// Date32Array/Time32Array have 32-bit storage so memcpy is invalid for them;
187+
// the constexpr sizeof check prevents this at compile time.
188+
constexpr bool is64bit = sizeof( typename ArrowValueArrayT::value_type ) == sizeof( int64_t );
189+
if constexpr( is64bit )
190+
{
191+
if( values -> null_count() == 0 && multiplier == 1 )
192+
{
193+
std::memcpy( buf, values -> raw_values(), sizeof( int64_t ) * size );
194+
return fromPython<DialectGenericType>( pyArr );
195+
}
196+
}
197+
198+
for( int64_t i = 0; i < size; ++i )
199+
{
200+
if( values -> IsValid( i ) )
201+
buf[i] = static_cast<int64_t>( values -> Value( i ) ) * multiplier;
202+
else
203+
buf[i] = NPY_DATETIME_NAT;
204+
}
205+
206+
return fromPython<DialectGenericType>( pyArr );
207+
};
208+
}
209+
210+
// Create a readValue lambda for timedelta64[ns] from Arrow Duration/Time columns
211+
template<typename ArrowValueArrayT>
212+
std::function<DialectGenericType( const ::arrow::Array &, int64_t )>
213+
makeDurationListReadValue( int64_t multiplier )
214+
{
215+
static PyArray_Descr * s_dtype = nullptr;
216+
if( !s_dtype )
217+
{
218+
auto dtypeStr = PyPtr<PyObject>::own( PyUnicode_FromString( "<m8[ns]" ) );
219+
PyArray_DescrConverter( dtypeStr.get(), &s_dtype );
220+
}
221+
222+
return [multiplier]( const ::arrow::Array & arr, int64_t row ) -> DialectGenericType
223+
{
224+
auto & listArr = static_cast<const ::arrow::ListArray &>( arr );
225+
auto values = std::static_pointer_cast<ArrowValueArrayT>( listArr.value_slice( row ) );
226+
227+
npy_intp size = values -> length();
228+
Py_INCREF( s_dtype );
229+
PyObject * pyArr = PyArray_SimpleNewFromDescr( 1, &size, s_dtype );
230+
PyObjectPtr arrOwner{ PyObjectPtr::own( pyArr ) };
231+
232+
auto * buf = reinterpret_cast<int64_t *>( PyArray_DATA( reinterpret_cast<PyArrayObject *>( pyArr ) ) );
233+
234+
constexpr bool is64bit = sizeof( typename ArrowValueArrayT::value_type ) == sizeof( int64_t );
235+
if constexpr( is64bit )
236+
{
237+
if( values -> null_count() == 0 && multiplier == 1 )
238+
{
239+
std::memcpy( buf, values -> raw_values(), sizeof( int64_t ) * size );
240+
return fromPython<DialectGenericType>( pyArr );
241+
}
242+
}
243+
244+
for( int64_t i = 0; i < size; ++i )
245+
{
246+
if( values -> IsValid( i ) )
247+
buf[i] = static_cast<int64_t>( values -> Value( i ) ) * multiplier;
248+
else
249+
buf[i] = NPY_DATETIME_NAT;
250+
}
251+
252+
return fromPython<DialectGenericType>( pyArr );
253+
};
254+
}
255+
143256
// Dispatch on arrow list element type to create the appropriate readValue lambda
144257
inline std::function<DialectGenericType( const ::arrow::Array &, int64_t )>
145258
dispatchListReadValue( const std::shared_ptr<::arrow::ListType> & listType, const std::string & columnName )
@@ -162,6 +275,35 @@ dispatchListReadValue( const std::shared_ptr<::arrow::ListType> & listType, cons
162275
return makeStringListReadValue<::arrow::LargeStringArray>();
163276
case ::arrow::Type::LARGE_BINARY:
164277
return makeStringListReadValue<::arrow::LargeBinaryArray>();
278+
279+
// --- Temporal: datetime64[ns] ---
280+
case ::arrow::Type::TIMESTAMP:
281+
{
282+
auto tsType = std::static_pointer_cast<::arrow::TimestampType>( listType -> value_type() );
283+
return makeTimestampListReadValue<::arrow::TimestampArray>( listTimeUnitMultiplier( tsType -> unit() ) );
284+
}
285+
case ::arrow::Type::DATE32:
286+
return makeTimestampListReadValue<::arrow::Date32Array>( csp::NANOS_PER_DAY );
287+
case ::arrow::Type::DATE64:
288+
return makeTimestampListReadValue<::arrow::Date64Array>( csp::NANOS_PER_MILLISECOND );
289+
290+
// --- Temporal: timedelta64[ns] ---
291+
case ::arrow::Type::DURATION:
292+
{
293+
auto durType = std::static_pointer_cast<::arrow::DurationType>( listType -> value_type() );
294+
return makeDurationListReadValue<::arrow::DurationArray>( listTimeUnitMultiplier( durType -> unit() ) );
295+
}
296+
case ::arrow::Type::TIME32:
297+
{
298+
auto timeType = std::static_pointer_cast<::arrow::Time32Type>( listType -> value_type() );
299+
return makeDurationListReadValue<::arrow::Time32Array>( listTimeUnitMultiplier( timeType -> unit() ) );
300+
}
301+
case ::arrow::Type::TIME64:
302+
{
303+
auto timeType = std::static_pointer_cast<::arrow::Time64Type>( listType -> value_type() );
304+
return makeDurationListReadValue<::arrow::Time64Array>( listTimeUnitMultiplier( timeType -> unit() ) );
305+
}
306+
165307
default:
166308
CSP_THROW( TypeError, "Unsupported list element type " << listType -> value_type() -> ToString()
167309
<< " for list column '" << columnName << "'" );

cpp/csp/python/adapters/ArrowNumpyListWriter.h

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#define _IN_CSP_PYTHON_ADAPTERS_ArrowNumpyListWriter_H
1010

1111
#include <csp/adapters/arrow/ArrowFieldWriter.h>
12+
#include <csp/core/Time.h>
1213
#include <csp/python/Conversions.h>
1314
#include <csp/python/NumpyConversions.h>
1415

@@ -174,6 +175,150 @@ class StringListWriter final : public csp::adapters::arrow::FieldWriter
174175
std::string m_utf8Buf; // reused buffer
175176
};
176177

178+
// --- Temporal list writers (datetime64[ns] → Timestamp, timedelta64[ns] → Duration) ---
179+
180+
// Writes numpy datetime64/timedelta64 arrays into Arrow List<Timestamp(ns,"UTC")> or List<Duration(ns)> columns.
181+
// Detects the numpy datetime unit at write time and converts to nanoseconds using scalingFromNumpyDtUnit().
182+
template<typename ArrowBuilderT>
183+
class TemporalListWriter final : public csp::adapters::arrow::FieldWriter
184+
{
185+
public:
186+
TemporalListWriter( const std::string & columnName, const StructFieldPtr & structField,
187+
std::shared_ptr<::arrow::DataType> elementType )
188+
: FieldWriter( { columnName }, { ::arrow::list( elementType ) }, structField )
189+
{
190+
m_valueBuilder = std::make_shared<ArrowBuilderT>( elementType, ::arrow::default_memory_pool() );
191+
m_listBuilder = std::make_shared<::arrow::ListBuilder>( ::arrow::default_memory_pool(), m_valueBuilder );
192+
}
193+
194+
void reserve( int64_t numRows ) override
195+
{
196+
ARROW_OK_OR_THROW_WRITER( m_listBuilder -> Reserve( numRows ), "Failed to reserve temporal list builder" );
197+
}
198+
199+
void writeNull() override
200+
{
201+
ARROW_OK_OR_THROW_WRITER( m_listBuilder -> AppendNull(), "Failed to append null temporal list" );
202+
}
203+
204+
std::vector<std::shared_ptr<::arrow::Array>> finish() override
205+
{
206+
std::shared_ptr<::arrow::Array> arr;
207+
ARROW_OK_OR_THROW_WRITER( m_listBuilder -> Finish( &arr ), "Failed to finish temporal list array" );
208+
return { arr };
209+
}
210+
211+
protected:
212+
void doWrite( const Struct * s ) override
213+
{
214+
ARROW_OK_OR_THROW_WRITER( m_listBuilder -> Append(), "Failed to start temporal list" );
215+
auto & dgt = m_field -> value<DialectGenericType>( s );
216+
auto * pyArr = reinterpret_cast<PyArrayObject *>( csp::python::toPythonBorrowed( dgt ) );
217+
npy_intp len = PyArray_SIZE( pyArr );
218+
219+
// PyArray_DATA requires C-contiguous layout for correct bulk reads.
220+
// Non-contiguous arrays (slices, transposes) must be copied to contiguous form first.
221+
PyArrayObject * contiguousArr = pyArr;
222+
PyObjectPtr contiguousOwner;
223+
if( !PyArray_IS_C_CONTIGUOUS( pyArr ) )
224+
{
225+
contiguousOwner = PyObjectPtr::own(
226+
reinterpret_cast<PyObject *>( PyArray_GETCONTIGUOUS( pyArr ) ) );
227+
contiguousArr = reinterpret_cast<PyArrayObject *>( contiguousOwner.get() );
228+
}
229+
230+
auto * data = reinterpret_cast<const int64_t *>( PyArray_DATA( contiguousArr ) );
231+
232+
// Detect numpy datetime unit and compute scaling to nanoseconds
233+
auto unit = datetimeUnitFromDescr( PyArray_DESCR( pyArr ) );
234+
int64_t scaling = scalingFromNumpyDtUnit( unit );
235+
236+
if( scaling == 1 )
237+
{
238+
// Already nanoseconds — bulk append
239+
ARROW_OK_OR_THROW_WRITER(
240+
m_valueBuilder -> AppendValues( data, static_cast<int64_t>( len ) ),
241+
"Failed to append temporal list elements" );
242+
}
243+
else
244+
{
245+
ARROW_OK_OR_THROW_WRITER(
246+
m_valueBuilder -> Reserve( static_cast<int64_t>( len ) ),
247+
"Failed to reserve temporal value builder" );
248+
for( npy_intp i = 0; i < len; ++i )
249+
m_valueBuilder -> UnsafeAppend( data[i] * scaling );
250+
}
251+
}
252+
253+
private:
254+
std::shared_ptr<ArrowBuilderT> m_valueBuilder;
255+
std::shared_ptr<::arrow::ListBuilder> m_listBuilder;
256+
};
257+
258+
// Date writer: numpy datetime64 → Arrow List<Date32> (nanoseconds → days since epoch)
259+
class DateListWriter final : public csp::adapters::arrow::FieldWriter
260+
{
261+
public:
262+
DateListWriter( const std::string & columnName, const StructFieldPtr & structField )
263+
: FieldWriter( { columnName }, { ::arrow::list( ::arrow::date32() ) }, structField )
264+
{
265+
m_valueBuilder = std::make_shared<::arrow::Date32Builder>();
266+
m_listBuilder = std::make_shared<::arrow::ListBuilder>( ::arrow::default_memory_pool(), m_valueBuilder );
267+
}
268+
269+
void reserve( int64_t numRows ) override
270+
{
271+
ARROW_OK_OR_THROW_WRITER( m_listBuilder -> Reserve( numRows ), "Failed to reserve date list builder" );
272+
}
273+
274+
void writeNull() override
275+
{
276+
ARROW_OK_OR_THROW_WRITER( m_listBuilder -> AppendNull(), "Failed to append null date list" );
277+
}
278+
279+
std::vector<std::shared_ptr<::arrow::Array>> finish() override
280+
{
281+
std::shared_ptr<::arrow::Array> arr;
282+
ARROW_OK_OR_THROW_WRITER( m_listBuilder -> Finish( &arr ), "Failed to finish date list array" );
283+
return { arr };
284+
}
285+
286+
protected:
287+
void doWrite( const Struct * s ) override
288+
{
289+
ARROW_OK_OR_THROW_WRITER( m_listBuilder -> Append(), "Failed to start date list" );
290+
auto & dgt = m_field -> value<DialectGenericType>( s );
291+
auto * pyArr = reinterpret_cast<PyArrayObject *>( csp::python::toPythonBorrowed( dgt ) );
292+
npy_intp len = PyArray_SIZE( pyArr );
293+
294+
// PyArray_DATA requires C-contiguous layout for correct bulk reads.
295+
PyArrayObject * contiguousArr = pyArr;
296+
PyObjectPtr contiguousOwner;
297+
if( !PyArray_IS_C_CONTIGUOUS( pyArr ) )
298+
{
299+
contiguousOwner = PyObjectPtr::own(
300+
reinterpret_cast<PyObject *>( PyArray_GETCONTIGUOUS( pyArr ) ) );
301+
contiguousArr = reinterpret_cast<PyArrayObject *>( contiguousOwner.get() );
302+
}
303+
304+
auto * data = reinterpret_cast<const int64_t *>( PyArray_DATA( contiguousArr ) );
305+
306+
// Detect numpy datetime unit and compute scaling to nanoseconds
307+
auto unit = datetimeUnitFromDescr( PyArray_DESCR( pyArr ) );
308+
int64_t scaling = scalingFromNumpyDtUnit( unit );
309+
310+
ARROW_OK_OR_THROW_WRITER(
311+
m_valueBuilder -> Reserve( static_cast<int64_t>( len ) ),
312+
"Failed to reserve date value builder" );
313+
for( npy_intp i = 0; i < len; ++i )
314+
m_valueBuilder -> UnsafeAppend( static_cast<int32_t>( data[i] * scaling / csp::NANOS_PER_DAY ) );
315+
}
316+
317+
private:
318+
std::shared_ptr<::arrow::Date32Builder> m_valueBuilder;
319+
std::shared_ptr<::arrow::ListBuilder> m_listBuilder;
320+
};
321+
177322
// --- Dispatch by npy_type ---
178323

179324
inline std::unique_ptr<csp::adapters::arrow::FieldWriter> dispatchListWriter(
@@ -193,6 +338,12 @@ inline std::unique_ptr<csp::adapters::arrow::FieldWriter> dispatchListWriter(
193338
return std::make_unique<NativeListWriter<bool, ::arrow::BooleanBuilder>>( columnName, structField, ::arrow::boolean() );
194339
case NPY_UNICODE:
195340
return std::make_unique<StringListWriter>( columnName, structField );
341+
case NPY_DATETIME:
342+
return std::make_unique<TemporalListWriter<::arrow::TimestampBuilder>>(
343+
columnName, structField, std::make_shared<::arrow::TimestampType>( ::arrow::TimeUnit::NANO, "UTC" ) );
344+
case NPY_TIMEDELTA:
345+
return std::make_unique<TemporalListWriter<::arrow::DurationBuilder>>(
346+
columnName, structField, std::make_shared<::arrow::DurationType>( ::arrow::TimeUnit::NANO ) );
196347
default:
197348
CSP_THROW( TypeError, "Unsupported numpy type " << npyType << " for list column '" << columnName << "'" );
198349
}

0 commit comments

Comments
 (0)