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
179324inline 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