1use crate::calendars::Calendar;
2use crate::datetime::CFDatetime;
3use crate::duration::CFDuration;
4use crate::encoder::CFEncoder;
5use crate::{constants, decoder::*};
6use pyo3::exceptions::PyValueError;
7use pyo3::prelude::*;
8use pyo3::types::PyDateTime;
9use std::str::FromStr;
10use std::sync::Arc;
11#[pyclass]
12#[derive(Clone)]
13pub struct PyCFCalendar {
14 pub calendar: Calendar,
15}
16
17#[pyclass]
24pub struct PyCFDuration {
25 pub duration: CFDuration,
26}
27
28#[pymethods]
29impl PyCFCalendar {
30 #[staticmethod]
31 pub fn from_str(s: String) -> PyResult<Self> {
32 let calendar = Calendar::from_str(s.as_str())
33 .map_err(|e| PyValueError::new_err(format!("Could not parse calendar: {}", e)))?;
34 Ok(Self { calendar })
35 }
36}
37
38#[pymethods]
39impl PyCFDuration {
40 #[new]
42 pub fn new(seconds: i64, nanoseconds: i64, calendar: PyCFCalendar) -> Self {
43 Self {
44 duration: CFDuration::new(seconds, nanoseconds, calendar.calendar),
45 }
46 }
47 #[staticmethod]
49 pub fn from_years(years: i64, calendar: PyCFCalendar) -> PyCFDuration {
50 Self {
51 duration: CFDuration::from_years(years, calendar.calendar),
52 }
53 }
54 #[staticmethod]
56 pub fn from_months(months: i64, calendar: PyCFCalendar) -> PyCFDuration {
57 Self {
58 duration: CFDuration::from_months(months, calendar.calendar),
59 }
60 }
61 #[staticmethod]
63 pub fn from_weeks(weeks: i64, calendar: PyCFCalendar) -> PyCFDuration {
64 Self {
65 duration: CFDuration::from_weeks(weeks, calendar.calendar),
66 }
67 }
68 #[staticmethod]
70 pub fn from_days(days: i64, calendar: PyCFCalendar) -> PyCFDuration {
71 Self {
72 duration: CFDuration::from_days(days, calendar.calendar),
73 }
74 }
75 #[staticmethod]
77 pub fn from_hours(hours: i64, calendar: PyCFCalendar) -> PyCFDuration {
78 Self {
79 duration: CFDuration::from_hours(hours, calendar.calendar),
80 }
81 }
82 #[staticmethod]
84 pub fn from_minutes(minutes: i64, calendar: PyCFCalendar) -> PyCFDuration {
85 Self {
86 duration: CFDuration::from_minutes(minutes, calendar.calendar),
87 }
88 }
89 #[staticmethod]
91 pub fn from_seconds(seconds: i64, calendar: PyCFCalendar) -> PyCFDuration {
92 Self {
93 duration: CFDuration::from_seconds(seconds, calendar.calendar),
94 }
95 }
96 #[staticmethod]
98 pub fn from_milliseconds(milliseconds: i64, calendar: PyCFCalendar) -> PyCFDuration {
99 Self {
100 duration: CFDuration::from_milliseconds(milliseconds, calendar.calendar),
101 }
102 }
103 #[staticmethod]
105 pub fn from_microseconds(microseconds: i64, calendar: PyCFCalendar) -> PyCFDuration {
106 Self {
107 duration: CFDuration::from_microseconds(microseconds, calendar.calendar),
108 }
109 }
110 #[staticmethod]
112 pub fn from_nanoseconds(nanoseconds: i64, calendar: PyCFCalendar) -> PyCFDuration {
113 Self {
114 duration: CFDuration::from_nanoseconds(nanoseconds, calendar.calendar),
115 }
116 }
117 pub fn num_years(&self) -> f64 {
119 self.duration.num_years()
120 }
121 pub fn num_months(&self) -> f64 {
123 self.duration.num_months()
124 }
125 pub fn num_weeks(&self) -> f64 {
127 self.duration.num_weeks()
128 }
129 pub fn num_days(&self) -> f64 {
131 self.duration.num_days()
132 }
133 pub fn num_hours(&self) -> f64 {
135 self.duration.num_hours()
136 }
137 pub fn num_minutes(&self) -> f64 {
139 self.duration.num_minutes()
140 }
141 pub fn num_seconds(&self) -> f64 {
143 self.duration.num_seconds()
144 }
145 pub fn num_milliseconds(&self) -> f64 {
147 self.duration.num_milliseconds()
148 }
149 pub fn num_microseconds(&self) -> f64 {
151 self.duration.num_microseconds()
152 }
153 pub fn num_nanoseconds(&self) -> f64 {
155 self.duration.num_nanoseconds()
156 }
157 pub fn __repr__(&self) -> String {
159 format!("{}", self.duration)
160 }
161 pub fn __str__(&self) -> String {
163 self.duration.to_string()
164 }
165
166 pub fn __sub__(&self, other: &PyCFDuration) -> PyResult<PyCFDuration> {
167 Ok(PyCFDuration {
168 duration: (&self.duration - &other.duration)
169 .map_err(|e| PyValueError::new_err(format!("{}", e)))?,
170 })
171 }
172
173 pub fn __add__(&self, other: &PyCFDuration) -> PyResult<PyCFDuration> {
174 Ok(PyCFDuration {
175 duration: (&self.duration + &other.duration)
176 .map_err(|e| PyValueError::new_err(format!("{}", e)))?,
177 })
178 }
179
180 pub fn __neg__(&self) -> PyCFDuration {
181 let duration = -&self.duration;
182 PyCFDuration { duration: duration }
183 }
184}
185
186#[pyclass]
191#[derive(Clone, PartialEq)]
192pub struct PyCFDatetime {
193 pub dt: Arc<CFDatetime>,
194}
195
196#[pymethods]
197impl PyCFDatetime {
198 #[new]
200 pub fn new(
201 year: i64,
202 month: u8,
203 day: u8,
204 hour: u8,
205 minute: u8,
206 second: f32,
207 calendar: PyCFCalendar,
208 ) -> PyResult<Self> {
209 let dt =
210 CFDatetime::from_ymd_hms(year, month, day, hour, minute, second, calendar.calendar)
211 .map_err(|e| PyValueError::new_err(e.to_string()))?;
212 Ok(Self { dt: dt.into() })
213 }
214 pub fn ymd(&self) -> PyResult<(i64, u8, u8)> {
216 let (year, month, day, _, _, _) = self
217 .ymd_hms()
218 .map_err(|e| PyValueError::new_err(e.to_string()))?;
219 Ok((year, month, day))
220 }
221 pub fn hms(&self) -> PyResult<(u8, u8, u8)> {
223 let (_, _, _, hour, min, sec) = self
224 .ymd_hms()
225 .map_err(|e| PyValueError::new_err(e.to_string()))?;
226 Ok((hour, min, sec))
227 }
228 pub fn ymd_hms(&self) -> PyResult<(i64, u8, u8, u8, u8, u8)> {
230 self.dt
231 .ymd_hms()
232 .map_err(|e| PyValueError::new_err(e.to_string()))
233 }
234 #[staticmethod]
236 pub fn from_ymd_hms(
237 year: i64,
238 month: u8,
239 day: u8,
240 hour: u8,
241 minute: u8,
242 second: f32,
243 calendar: PyCFCalendar,
244 ) -> PyResult<Self> {
245 let dt =
246 CFDatetime::from_ymd_hms(year, month, day, hour, minute, second, calendar.calendar)
247 .map_err(|e| PyValueError::new_err(e.to_string()))?;
248 Ok(Self { dt: dt.into() })
249 }
250 #[staticmethod]
253 pub fn from_hms(hour: u8, minute: u8, second: f32, calendar: PyCFCalendar) -> PyResult<Self> {
254 let dt = CFDatetime::from_ymd_hms(
255 constants::UNIX_DEFAULT_YEAR,
256 constants::UNIX_DEFAULT_MONTH,
257 constants::UNIX_DEFAULT_DAY,
258 hour,
259 minute,
260 second,
261 calendar.calendar,
262 )
263 .map_err(|e| PyValueError::new_err(e.to_string()))?;
264 Ok(Self { dt: dt.into() })
265 }
266 #[staticmethod]
269 pub fn from_ymd(year: i64, month: u8, day: u8, calendar: PyCFCalendar) -> PyResult<Self> {
270 let dt = CFDatetime::from_ymd_hms(year, month, day, 0, 0, 0.0, calendar.calendar)
271 .map_err(|e| PyValueError::new_err(e.to_string()))?;
272 Ok(Self { dt: dt.into() })
273 }
274 #[staticmethod]
276 pub fn from_timestamp(
277 timestamp: i64,
278 nanoseconds: u32,
279 calendar: PyCFCalendar,
280 ) -> PyResult<Self> {
281 let dt = CFDatetime::from_timestamp(timestamp, nanoseconds, calendar.calendar)
282 .map_err(|e| PyValueError::new_err(e.to_string()))?;
283 Ok(Self { dt: dt.into() })
284 }
285 pub fn hours(&self) -> PyResult<u8> {
287 let (hour, _, _) = self.hms()?;
288 Ok(hour)
289 }
290 pub fn minutes(&self) -> PyResult<u8> {
292 let (_, min, _) = self.hms()?;
293 Ok(min)
294 }
295 pub fn seconds(&self) -> PyResult<u8> {
297 let (_, _, sec) = self.hms()?;
298 Ok(sec)
299 }
300 pub fn nanoseconds(&self) -> u32 {
302 self.dt.nanoseconds()
303 }
304 pub fn change_calendar(&self, calendar: PyCFCalendar) -> PyResult<Self> {
314 let new_dt = self
315 .dt
316 .change_calendar(calendar.calendar)
317 .map_err(|e| PyValueError::new_err(e.to_string()))?;
318 Ok(Self { dt: new_dt.into() })
319 }
320
321 pub fn change_calendar_from_timestamp(&self, calendar: PyCFCalendar) -> PyResult<Self> {
335 let new_dt = self
337 .dt
338 .change_calendar_from_timestamp(calendar.calendar)
339 .map_err(|e| PyValueError::new_err(e.to_string()))?;
340
341 Ok(Self { dt: new_dt.into() })
343 }
344
345 fn to_pydatetime<'a>(&self, py: Python<'a>) -> PyResult<&'a PyDateTime> {
346 let (year, month, day, hour, minute, second) = self
347 .ymd_hms()
348 .map_err(|e| PyValueError::new_err(format!("Could not convert to datetime: {}", e)))?;
349 let nanoseconds = self.nanoseconds();
350 let microsecond = nanoseconds / 1_000;
351 PyDateTime::new(
352 py,
353 year as i32,
354 month,
355 day,
356 hour,
357 minute,
358 second,
359 microsecond as u32,
360 None,
361 )
362 }
363 fn to_pydatetime_from_timestamp<'a>(&self, py: Python<'a>) -> PyResult<&'a PyDateTime> {
364 PyDateTime::from_timestamp(
365 py,
366 self.dt.timestamp() as f64 + self.dt.nanoseconds() as f64 / 1e9,
367 None,
368 )
369 }
370 fn __repr__(&self) -> String {
371 format!("PyCFDatetime({}, {})", self.dt, self.dt.calendar())
372 }
373 fn __str__(&self) -> String {
374 self.dt.to_string()
375 }
376 fn __sub__(&self, other: &PyCFDatetime) -> PyResult<PyCFDuration> {
377 let duration =
378 (&*self.dt - &*other.dt).map_err(|e| PyValueError::new_err(e.to_string()))?;
379 Ok(PyCFDuration { duration: duration })
380 }
381 fn __add__(&self, other: &PyCFDuration) -> PyResult<PyCFDatetime> {
382 let dt = (&*self.dt + &other.duration).map_err(|e| PyValueError::new_err(e.to_string()))?;
383 Ok(PyCFDatetime { dt: dt.into() })
384 }
385 fn __eq__(&self, other: &PyCFDatetime) -> PyResult<bool> {
386 Ok(self.dt == other.dt)
387 }
388}
389
390impl std::fmt::Display for PyCFDatetime {
391 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
392 self.dt.fmt(f)
393 }
394}
395
396macro_rules! decode_numbers {
397 ($numbers:expr, $units:expr, $calendar:expr, $($t:ty),+) => {
398 {
399 $(
400 if let Ok(numbers) = $numbers.extract::<Vec<$t>>() {
401 numbers.decode_cf($units.as_str(), $calendar)
402 .map_err(|e| PyValueError::new_err(format!("Could not decode numbers {} into PyCFDatetime: {}", $numbers, e)))?
403 } else
404 )+
405 {
406 let supported_types = stringify!($($t),+);
407 return Err(PyValueError::new_err(format!(
408 "Could not convert array to supported types. \
409 Needs an one-dimensional array of one the following types: {}",supported_types)))
410 }
411 }
412 };
413}
414
415#[pyfunction]
416fn num2date(numbers: &PyAny, units: String, calendar: String) -> PyResult<Vec<PyCFDatetime>> {
417 let calendar = Calendar::from_str(calendar.as_str())
418 .map_err(|e| PyValueError::new_err(format!("Could not parse calendar: {}", e)))?;
419 let datetimes = decode_numbers!(numbers, units, calendar, i64, i32, f64, f32);
424 Ok(datetimes
425 .into_iter()
426 .map(|dt| PyCFDatetime { dt: dt.into() })
427 .collect())
428}
429
430#[pyfunction]
431#[pyo3(signature = (numbers, units, calendar, from_timestamp=false))]
432fn num2pydate<'a>(
433 py: Python<'a>,
434 numbers: &'a PyAny,
435 units: String,
436 calendar: String,
437 from_timestamp: Option<bool>,
438) -> PyResult<Vec<&'a PyDateTime>> {
439 match from_timestamp {
440 Some(true) => num2date(numbers, units, calendar)?
441 .iter()
442 .map(|dt| dt.to_pydatetime_from_timestamp(py))
443 .collect::<Result<Vec<_>, _>>(),
444 _ => num2date(numbers, units, calendar)?
445 .iter()
446 .map(|dt| dt.to_pydatetime(py))
447 .collect::<Result<Vec<_>, _>>(),
448 }
449}
450enum DType {
451 Int32,
452 Int64,
453 Float32,
454 Float64,
455 Unknown,
456}
457
458const INT_32_TYPES: &[&str] = &["i32"];
459const INT_64_TYPES: &[&str] = &["i64", "i", "integer", "int"];
460const FLOAT_32_TYPES: &[&str] = &["f32"];
461const FLOAT_64_TYPES: &[&str] = &["f64", "f", "float"];
462
463impl FromStr for DType {
464 type Err = String;
465 fn from_str(s: &str) -> Result<Self, Self::Err> {
466 match s.to_lowercase().as_str() {
467 s if INT_32_TYPES.iter().any(|&x| x == s) => Ok(DType::Int32),
468 s if INT_64_TYPES.iter().any(|&x| x == s) => Ok(DType::Int64),
469 s if FLOAT_32_TYPES.iter().any(|&x| x == s) => Ok(DType::Float32),
470 s if FLOAT_64_TYPES.iter().any(|&x| x == s) => Ok(DType::Float64),
471 _ => Ok(DType::Unknown),
472 }
473 }
474}
475
476#[pyfunction]
477fn date2num(
478 py: Python,
479 datetimes: Vec<PyCFDatetime>,
480 units: String,
481 calendar: String,
482 dtype: String,
483) -> PyResult<PyObject> {
484 let calendar = Calendar::from_str(calendar.as_str())
485 .map_err(|e| PyValueError::new_err(format!("Could not parse calendar: {}", e)))?;
486 let dtype_enum = DType::from_str(dtype.as_str())
487 .map_err(|e| PyValueError::new_err(format!("Could not parse dtype: {}", e)))?;
488 let dts: Vec<&CFDatetime> = datetimes.iter().map(|pydatetime| &*pydatetime.dt).collect();
489 match dtype_enum {
490 DType::Int32 => {
491 let numbers: Vec<i32> = dts
492 .encode_cf(units.as_str(), calendar)
493 .map_err(|e| PyValueError::new_err(format!("Could not encode datetimes: {}", e)))?;
494 Ok(numbers.into_py(py))
495 }
496 DType::Int64 => {
497 let numbers: Vec<i64> = dts
498 .encode_cf(units.as_str(), calendar)
499 .map_err(|e| PyValueError::new_err(format!("Could not encode datetimes: {}", e)))?;
500 Ok(numbers.into_py(py))
501 }
502 DType::Float32 => {
503 let numbers: Vec<f32> = dts
504 .encode_cf(units.as_str(), calendar)
505 .map_err(|e| PyValueError::new_err(format!("Could not encode datetimes: {}", e)))?;
506 Ok(numbers.into_py(py))
507 }
508 DType::Float64 => {
509 let numbers: Vec<f64> = dts
510 .encode_cf(units.as_str(), calendar)
511 .map_err(|e| PyValueError::new_err(format!("Could not encode datetimes: {}", e)))?;
512 Ok(numbers.into_py(py))
513 }
514 DType::Unknown => Err(PyValueError::new_err(format!(
515 "Invalid dtype `{}`. For i32 use {}. For i64 use {}. For f32 use {}. For f64 use {}.",
516 dtype,
517 INT_32_TYPES.join(", "),
518 INT_64_TYPES.join(", "),
519 FLOAT_32_TYPES.join(", "),
520 FLOAT_64_TYPES.join(", ")
521 ))),
522 }
523}
524pub struct PyDateTimeList<'a> {
527 datetimes: Vec<&'a PyDateTime>,
528}
529
530impl<'a> pyo3::FromPyObject<'a> for PyDateTimeList<'a> {
531 fn extract(obj: &'a PyAny) -> pyo3::PyResult<Self> {
532 let py_list = obj.downcast::<pyo3::types::PyList>()?;
533 let mut datetimes = Vec::with_capacity(py_list.len());
534 for elem in py_list {
535 let py_dt = elem.extract::<&PyDateTime>()?;
536 datetimes.push(py_dt);
537 }
538 Ok(PyDateTimeList {
539 datetimes: datetimes,
540 })
541 }
542}
543
544#[pyfunction]
545fn pydate2num(
546 py: Python,
547 datetimes: PyDateTimeList,
548 units: String,
549 calendar: String,
550 dtype: String,
551) -> PyResult<PyObject> {
552 let calendar = Calendar::from_str(calendar.as_str())
553 .map_err(|e| PyValueError::new_err(format!("Could not parse calendar: {}", e)))?;
554 let dtype_enum = DType::from_str(dtype.as_str())
555 .map_err(|e| PyValueError::new_err(format!("Could not parse dtype: {}", e)))?;
556 let mut dts: Vec<CFDatetime> = Vec::with_capacity(datetimes.datetimes.len());
557
558 for pydt in datetimes.datetimes.iter() {
559 let year = pydt.getattr("year")?.extract::<i64>()?;
560 let month = pydt.getattr("month")?.extract::<u8>()?;
561 let day = pydt.getattr("day")?.extract::<u8>()?;
562 let hour = pydt.getattr("hour")?.extract::<u8>()?;
563 let minute = pydt.getattr("minute")?.extract::<u8>()?;
564 let second = pydt.getattr("second")?.extract::<u8>()?;
565 let microsecond = pydt.getattr("microsecond")?.extract::<u32>()?;
566 let new_second = second as f32 + (microsecond / 1_000_000) as f32;
567 dts.push(
568 CFDatetime::from_ymd_hms(year, month, day, hour, minute, new_second, calendar)
569 .map_err(|e| {
570 PyValueError::new_err(format!(
571 "Could not convert datetime to CFDatetime: {}",
572 e
573 ))
574 })?,
575 );
576 }
577
578 match dtype_enum {
579 DType::Int32 => {
580 let numbers: Vec<i32> = dts
581 .encode_cf(units.as_str(), calendar)
582 .map_err(|e| PyValueError::new_err(format!("Could not encode datetimes: {}", e)))?;
583 Ok(numbers.into_py(py))
584 }
585 DType::Int64 => {
586 let numbers: Vec<i64> = dts
587 .encode_cf(units.as_str(), calendar)
588 .map_err(|e| PyValueError::new_err(format!("Could not encode datetimes: {}", e)))?;
589 Ok(numbers.into_py(py))
590 }
591 DType::Float32 => {
592 let numbers: Vec<f32> = dts
593 .encode_cf(units.as_str(), calendar)
594 .map_err(|e| PyValueError::new_err(format!("Could not encode datetimes: {}", e)))?;
595 Ok(numbers.into_py(py))
596 }
597 DType::Float64 => {
598 let numbers: Vec<f64> = dts
599 .encode_cf(units.as_str(), calendar)
600 .map_err(|e| PyValueError::new_err(format!("Could not encode datetimes: {}", e)))?;
601 Ok(numbers.into_py(py))
602 }
603 DType::Unknown => Err(PyValueError::new_err(format!(
604 "Invalid dtype `{}`. For i32 use {}. For i64 use {}. For f32 use {}. For f64 use {}.",
605 dtype,
606 INT_32_TYPES.join(", "),
607 INT_64_TYPES.join(", "),
608 FLOAT_32_TYPES.join(", "),
609 FLOAT_64_TYPES.join(", ")
610 ))),
611 }
612}
613
614#[pymodule]
616fn cftime_rs(_py: Python, m: &PyModule) -> PyResult<()> {
617 m.add_function(wrap_pyfunction!(num2date, m)?)?;
618 m.add_function(wrap_pyfunction!(date2num, m)?)?;
619 m.add_function(wrap_pyfunction!(num2pydate, m)?)?;
620 m.add_function(wrap_pyfunction!(pydate2num, m)?)?;
621 m.add_class::<PyCFCalendar>()?;
622 m.add_class::<PyCFDuration>()?;
623 m.add_class::<PyCFDatetime>()?;
624
625 Ok(())
626}