1#![forbid(unsafe_code)]
2#![warn(rustdoc::broken_intra_doc_links)]
3
4use serde::{Deserialize, Serialize};
65use thiserror::Error;
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
68#[serde(rename_all = "snake_case")]
69pub enum DType {
70 Null,
71 Bool,
72 #[serde(rename = "boolean")]
74 BoolNullable,
75 Int64,
76 #[serde(rename = "Int64")]
78 Int64Nullable,
79 Float64,
80 #[serde(alias = "string", alias = "str")]
81 Utf8,
82 Categorical,
83 Timedelta64,
84 Datetime64,
86 Period,
88 Interval,
90 Sparse,
91}
92
93impl DType {
94 #[must_use]
96 pub const fn is_numeric(&self) -> bool {
97 matches!(self, Self::Int64 | Self::Int64Nullable | Self::Float64)
98 }
99
100 #[must_use]
102 pub const fn is_integer(&self) -> bool {
103 matches!(self, Self::Int64 | Self::Int64Nullable)
104 }
105
106 #[must_use]
108 pub const fn is_floating(&self) -> bool {
109 matches!(self, Self::Float64)
110 }
111
112 #[must_use]
114 pub const fn is_bool(&self) -> bool {
115 matches!(self, Self::Bool | Self::BoolNullable)
116 }
117
118 #[must_use]
120 pub const fn is_object(&self) -> bool {
121 matches!(self, Self::Utf8)
122 }
123
124 #[must_use]
126 pub const fn is_datetime(&self) -> bool {
127 matches!(self, Self::Datetime64)
128 }
129
130 #[must_use]
132 pub const fn is_timedelta(&self) -> bool {
133 matches!(self, Self::Timedelta64)
134 }
135
136 #[must_use]
138 pub const fn is_categorical(&self) -> bool {
139 matches!(self, Self::Categorical)
140 }
141
142 #[must_use]
144 pub const fn is_sparse(&self) -> bool {
145 matches!(self, Self::Sparse)
146 }
147
148 #[must_use]
150 pub const fn is_period(&self) -> bool {
151 matches!(self, Self::Period)
152 }
153
154 #[must_use]
156 pub const fn is_interval(&self) -> bool {
157 matches!(self, Self::Interval)
158 }
159
160 #[must_use]
164 pub const fn name(&self) -> &'static str {
165 match self {
166 Self::Bool => "bool",
167 Self::BoolNullable => "boolean",
168 Self::Int64 => "int64",
169 Self::Int64Nullable => "Int64",
170 Self::Float64 => "float64",
171 Self::Utf8 => "object",
172 Self::Datetime64 => "datetime64[ns]",
173 Self::Timedelta64 => "timedelta64[ns]",
174 Self::Categorical => "category",
175 Self::Period => "period",
176 Self::Interval => "interval",
177 Self::Sparse => "Sparse",
178 Self::Null => "object",
179 }
180 }
181
182 #[must_use]
186 pub const fn kind(&self) -> char {
187 match self {
188 Self::Bool | Self::BoolNullable => 'b',
189 Self::Int64 | Self::Int64Nullable => 'i',
190 Self::Float64 => 'f',
191 Self::Utf8 => 'O',
192 Self::Datetime64 => 'M',
193 Self::Timedelta64 => 'm',
194 Self::Categorical => 'O',
195 Self::Period => 'O',
196 Self::Interval => 'O',
197 Self::Sparse => 'O',
198 Self::Null => 'O',
199 }
200 }
201
202 #[must_use]
206 pub const fn itemsize(&self) -> usize {
207 match self {
208 Self::Bool | Self::BoolNullable => 1,
209 Self::Int64
210 | Self::Int64Nullable
211 | Self::Float64
212 | Self::Datetime64
213 | Self::Timedelta64
214 | Self::Period => 8,
215 Self::Utf8 | Self::Categorical | Self::Interval | Self::Sparse | Self::Null => 8,
216 }
217 }
218
219 #[must_use]
223 pub const fn is_extension(&self) -> bool {
224 matches!(
225 self,
226 Self::Categorical
227 | Self::Sparse
228 | Self::Period
229 | Self::Interval
230 | Self::Int64Nullable
231 | Self::BoolNullable
232 )
233 }
234
235 #[must_use]
240 pub const fn is_nullable(&self) -> bool {
241 matches!(self, Self::Int64Nullable | Self::BoolNullable)
242 }
243
244 #[must_use]
249 pub const fn to_non_nullable(&self) -> Self {
250 match self {
251 Self::Int64Nullable => Self::Int64,
252 Self::BoolNullable => Self::Bool,
253 other => *other,
254 }
255 }
256
257 #[must_use]
262 pub const fn to_nullable(&self) -> Self {
263 match self {
264 Self::Int64 => Self::Int64Nullable,
265 Self::Bool => Self::BoolNullable,
266 other => *other,
267 }
268 }
269
270 #[must_use]
274 pub const fn is_signed_integer(&self) -> bool {
275 matches!(self, Self::Int64 | Self::Int64Nullable)
276 }
277
278 #[must_use]
282 pub const fn is_string_dtype(&self) -> bool {
283 matches!(self, Self::Utf8)
284 }
285
286 #[must_use]
290 pub const fn is_any_real_numeric(&self) -> bool {
291 self.is_numeric()
292 }
293
294 #[must_use]
298 pub const fn is_datetime_like(&self) -> bool {
299 matches!(self, Self::Datetime64 | Self::Timedelta64 | Self::Period)
300 }
301
302 #[must_use]
306 pub const fn char(&self) -> char {
307 match self {
308 Self::Bool | Self::BoolNullable => '?',
309 Self::Int64 | Self::Int64Nullable => 'l',
310 Self::Float64 => 'd',
311 Self::Utf8 => 'O',
312 Self::Datetime64 => 'M',
313 Self::Timedelta64 => 'm',
314 Self::Categorical | Self::Period | Self::Interval | Self::Sparse | Self::Null => 'O',
315 }
316 }
317
318 #[must_use]
322 pub const fn num(&self) -> i32 {
323 match self {
324 Self::Bool | Self::BoolNullable => 0,
325 Self::Int64 | Self::Int64Nullable => 7,
326 Self::Float64 => 12,
327 Self::Utf8 => 17,
328 Self::Datetime64 => 21,
329 Self::Timedelta64 => 22,
330 Self::Categorical | Self::Period | Self::Interval | Self::Sparse | Self::Null => 17,
331 }
332 }
333
334 #[must_use]
338 pub const fn byteorder(&self) -> char {
339 '='
340 }
341
342 #[must_use]
346 pub const fn str_repr(&self) -> &'static str {
347 match self {
348 Self::Bool | Self::BoolNullable => "|b1",
349 Self::Int64 | Self::Int64Nullable => "<i8",
350 Self::Float64 => "<f8",
351 Self::Utf8 => "|O8",
352 Self::Datetime64 => "<M8[ns]",
353 Self::Timedelta64 => "<m8[ns]",
354 Self::Categorical | Self::Period | Self::Interval | Self::Sparse | Self::Null => "|O8",
355 }
356 }
357}
358
359#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
360pub struct SparseDType {
361 pub value_dtype: DType,
362 pub fill_value: Scalar,
363}
364
365impl SparseDType {
366 pub fn new(value_dtype: DType, fill_value: Scalar) -> Result<Self, TypeError> {
372 if matches!(value_dtype, DType::Null | DType::Sparse) {
373 return Err(TypeError::InvalidSparseValueDType { dtype: value_dtype });
374 }
375
376 let fill_value = if fill_value.is_missing() {
377 Scalar::missing_for_dtype(value_dtype)
378 } else {
379 cast_scalar_owned(fill_value, value_dtype)?
380 };
381
382 Ok(Self {
383 value_dtype,
384 fill_value,
385 })
386 }
387}
388
389#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
390#[serde(rename_all = "snake_case")]
391pub enum NullKind {
392 Null,
393 NaN,
394 NaT,
395}
396
397#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
398#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
399pub enum Scalar {
400 Null(NullKind),
401 Bool(bool),
402 Int64(i64),
403 Float64(f64),
404 #[serde(alias = "string", alias = "str")]
405 Utf8(String),
406 Timedelta64(i64),
407 Datetime64(i64),
410 Period(i64),
412 Interval(Interval),
414}
415
416impl std::fmt::Display for Scalar {
417 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
418 match self {
419 Self::Null(NullKind::NaN) => write!(f, "NaN"),
420 Self::Null(NullKind::NaT) => write!(f, "NaT"),
421 Self::Null(NullKind::Null) => write!(f, "None"),
422 Self::Bool(b) => write!(f, "{}", if *b { "True" } else { "False" }),
423 Self::Int64(v) => write!(f, "{v}"),
424 Self::Float64(v) => write!(f, "{v}"),
425 Self::Utf8(s) => write!(f, "{s}"),
426 Self::Timedelta64(nanos) => write!(f, "{}", Timedelta::format(*nanos)),
427 Self::Datetime64(nanos) => {
428 if *nanos == Timestamp::NAT {
429 write!(f, "NaT")
430 } else {
431 write!(f, "Timestamp[{nanos}]")
432 }
433 }
434 Self::Period(ordinal) => {
435 if *ordinal == i64::MIN {
436 write!(f, "NaT")
437 } else {
438 write!(f, "Period[{ordinal}]")
439 }
440 }
441 Self::Interval(interval) => write!(f, "{interval}"),
442 }
443 }
444}
445
446impl From<bool> for Scalar {
456 fn from(value: bool) -> Self {
457 Self::Bool(value)
458 }
459}
460
461impl From<i64> for Scalar {
462 fn from(value: i64) -> Self {
463 Self::Int64(value)
464 }
465}
466
467impl From<f64> for Scalar {
468 fn from(value: f64) -> Self {
469 Self::Float64(value)
470 }
471}
472
473impl From<&str> for Scalar {
474 fn from(value: &str) -> Self {
475 Self::Utf8(value.to_owned())
476 }
477}
478
479impl From<String> for Scalar {
480 fn from(value: String) -> Self {
481 Self::Utf8(value)
482 }
483}
484
485impl Scalar {
486 #[must_use]
487 pub fn dtype(&self) -> DType {
488 match self {
489 Self::Null(_) => DType::Null,
490 Self::Bool(_) => DType::Bool,
491 Self::Int64(_) => DType::Int64,
492 Self::Float64(_) => DType::Float64,
493 Self::Utf8(_) => DType::Utf8,
494 Self::Timedelta64(_) => DType::Timedelta64,
495 Self::Datetime64(_) => DType::Datetime64,
496 Self::Period(_) => DType::Period,
497 Self::Interval(_) => DType::Interval,
498 }
499 }
500
501 #[must_use]
502 pub fn is_missing(&self) -> bool {
503 match self {
504 Self::Null(_) => true,
505 Self::Float64(v) => v.is_nan(),
506 Self::Timedelta64(v) => *v == Timedelta::NAT,
507 Self::Datetime64(v) => *v == Timestamp::NAT,
508 Self::Period(v) => *v == i64::MIN,
509 _ => false,
510 }
511 }
512
513 #[must_use]
514 pub fn is_nan(&self) -> bool {
515 matches!(self, Self::Null(NullKind::NaN)) || matches!(self, Self::Float64(v) if v.is_nan())
516 }
517
518 #[must_use]
520 pub const fn is_bool(&self) -> bool {
521 matches!(self, Self::Bool(_))
522 }
523
524 #[must_use]
526 pub const fn is_integer(&self) -> bool {
527 matches!(self, Self::Int64(_))
528 }
529
530 #[must_use]
532 pub const fn is_float(&self) -> bool {
533 matches!(self, Self::Float64(_))
534 }
535
536 #[must_use]
538 pub const fn is_numeric(&self) -> bool {
539 matches!(self, Self::Int64(_) | Self::Float64(_))
540 }
541
542 #[must_use]
544 pub const fn is_string(&self) -> bool {
545 matches!(self, Self::Utf8(_))
546 }
547
548 #[must_use]
550 pub const fn is_datetime(&self) -> bool {
551 matches!(self, Self::Datetime64(_))
552 }
553
554 #[must_use]
556 pub const fn is_timedelta(&self) -> bool {
557 matches!(self, Self::Timedelta64(_))
558 }
559
560 #[must_use]
562 pub const fn is_period(&self) -> bool {
563 matches!(self, Self::Period(_))
564 }
565
566 #[must_use]
568 pub const fn is_interval(&self) -> bool {
569 matches!(self, Self::Interval(_))
570 }
571
572 #[must_use]
573 pub fn missing_for_dtype(dtype: DType) -> Self {
574 match dtype {
575 DType::Float64 => Self::Null(NullKind::NaN),
576 DType::Timedelta64 => Self::Timedelta64(Timedelta::NAT),
577 DType::Datetime64 => Self::Datetime64(Timestamp::NAT),
578 DType::Period => Self::Period(i64::MIN),
579 DType::Null => Self::Null(NullKind::Null),
580 DType::Bool
581 | DType::BoolNullable
582 | DType::Int64
583 | DType::Int64Nullable
584 | DType::Utf8
585 | DType::Categorical
586 | DType::Interval
587 | DType::Sparse => Self::Null(NullKind::Null),
588 }
589 }
590
591 #[must_use]
592 pub fn semantic_eq(&self, other: &Self) -> bool {
593 match (self, other) {
594 (Self::Float64(a), Self::Float64(b)) => {
595 if a.is_nan() && b.is_nan() {
596 return true;
597 }
598 if *a == *b {
599 return true;
600 }
601 let diff = (*a - *b).abs();
602 let max_abs = a.abs().max(b.abs());
603 if max_abs == 0.0 {
604 diff < f64::EPSILON
605 } else {
606 diff / max_abs < 1e-14
607 }
608 }
609 (Self::Null(_), Self::Float64(v)) | (Self::Float64(v), Self::Null(_)) => v.is_nan(),
610 (Self::Null(_), Self::Null(_)) => true,
617 _ => self == other,
618 }
619 }
620
621 #[must_use]
622 pub fn semantic_le(&self, other: &Self) -> bool {
623 match self.semantic_cmp(other) {
624 std::cmp::Ordering::Less | std::cmp::Ordering::Equal => true,
625 std::cmp::Ordering::Greater => false,
626 }
627 }
628
629 #[must_use]
630 pub fn semantic_ge(&self, other: &Self) -> bool {
631 match self.semantic_cmp(other) {
632 std::cmp::Ordering::Greater | std::cmp::Ordering::Equal => true,
633 std::cmp::Ordering::Less => false,
634 }
635 }
636
637 #[must_use]
638 pub fn is_null(&self) -> bool {
639 matches!(self, Self::Null(_))
640 }
641
642 #[must_use]
643 pub fn is_na(&self) -> bool {
644 self.is_missing()
645 }
646
647 #[must_use]
648 pub fn coalesce(&self, other: &Self) -> Self {
649 if self.is_missing() {
650 other.clone()
651 } else {
652 self.clone()
653 }
654 }
655
656 #[must_use]
657 pub fn semantic_cmp(&self, other: &Self) -> std::cmp::Ordering {
658 match (self, other) {
659 (Self::Int64(a), Self::Int64(b)) => a.cmp(b),
660 (Self::Float64(a), Self::Float64(b)) => {
661 a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
662 }
663 (Self::Utf8(a), Self::Utf8(b)) => a.cmp(b),
664 (Self::Bool(a), Self::Bool(b)) => a.cmp(b),
665 (Self::Null(a), Self::Null(b)) => a.cmp(b),
666 (Self::Timedelta64(a), Self::Timedelta64(b)) => {
667 if *a == Timedelta::NAT || *b == Timedelta::NAT {
668 std::cmp::Ordering::Equal
669 } else {
670 a.cmp(b)
671 }
672 }
673 (Self::Datetime64(a), Self::Datetime64(b)) => {
674 if *a == Timestamp::NAT || *b == Timestamp::NAT {
675 std::cmp::Ordering::Equal
676 } else {
677 a.cmp(b)
678 }
679 }
680 (Self::Period(a), Self::Period(b)) => {
681 if *a == i64::MIN || *b == i64::MIN {
682 std::cmp::Ordering::Equal
683 } else {
684 a.cmp(b)
685 }
686 }
687 (Self::Interval(a), Self::Interval(b)) => a
688 .left
689 .partial_cmp(&b.left)
690 .unwrap_or(std::cmp::Ordering::Equal)
691 .then_with(|| {
692 a.right
693 .partial_cmp(&b.right)
694 .unwrap_or(std::cmp::Ordering::Equal)
695 })
696 .then_with(|| a.closed.cmp(&b.closed)),
697 (Self::Int64(a), Self::Float64(b)) => (*a as f64)
699 .partial_cmp(b)
700 .unwrap_or(std::cmp::Ordering::Equal),
701 (Self::Float64(a), Self::Int64(b)) => a
702 .partial_cmp(&(*b as f64))
703 .unwrap_or(std::cmp::Ordering::Equal),
704 (a, b) => format!("{a:?}").cmp(&format!("{b:?}")),
706 }
707 }
708
709 pub fn to_f64(&self) -> Result<f64, TypeError> {
710 match self {
711 Self::Bool(v) => Ok(if *v { 1.0 } else { 0.0 }),
712 Self::Int64(v) => Ok(*v as f64),
713 Self::Float64(v) => Ok(*v),
714 Self::Null(kind) => Err(TypeError::ValueIsMissing { kind: *kind }),
715 Self::Utf8(v) => Err(TypeError::NonNumericValue {
716 value: v.clone(),
717 dtype: DType::Utf8,
718 }),
719 Self::Timedelta64(v) if *v == Timedelta::NAT => Err(TypeError::ValueIsMissing {
720 kind: NullKind::NaT,
721 }),
722 Self::Timedelta64(v) => Err(TypeError::NonNumericValue {
723 value: Timedelta::format(*v),
724 dtype: DType::Timedelta64,
725 }),
726 Self::Datetime64(v) if *v == Timestamp::NAT => Err(TypeError::ValueIsMissing {
727 kind: NullKind::NaT,
728 }),
729 Self::Datetime64(v) => Err(TypeError::NonNumericValue {
730 value: format!("Timestamp[{v}]"),
731 dtype: DType::Datetime64,
732 }),
733 Self::Period(v) if *v == i64::MIN => Err(TypeError::ValueIsMissing {
734 kind: NullKind::NaT,
735 }),
736 Self::Period(v) => Err(TypeError::NonNumericValue {
737 value: format!("Period[{v}]"),
738 dtype: DType::Period,
739 }),
740 Self::Interval(v) => Err(TypeError::NonNumericValue {
741 value: v.to_string(),
742 dtype: DType::Interval,
743 }),
744 }
745 }
746
747 pub fn to_i64(&self) -> Result<i64, TypeError> {
749 match self {
750 Self::Bool(v) => Ok(if *v { 1 } else { 0 }),
751 Self::Int64(v) => Ok(*v),
752 Self::Float64(v) => Ok(*v as i64),
753 Self::Null(kind) => Err(TypeError::ValueIsMissing { kind: *kind }),
754 Self::Utf8(v) => Err(TypeError::NonNumericValue {
755 value: v.clone(),
756 dtype: DType::Utf8,
757 }),
758 Self::Timedelta64(v) if *v == Timedelta::NAT => Err(TypeError::ValueIsMissing {
759 kind: NullKind::NaT,
760 }),
761 Self::Timedelta64(v) => Ok(*v),
762 Self::Datetime64(v) if *v == Timestamp::NAT => Err(TypeError::ValueIsMissing {
763 kind: NullKind::NaT,
764 }),
765 Self::Datetime64(v) => Ok(*v),
766 Self::Period(v) if *v == i64::MIN => Err(TypeError::ValueIsMissing {
767 kind: NullKind::NaT,
768 }),
769 Self::Period(v) => Ok(*v),
770 Self::Interval(v) => Err(TypeError::NonNumericValue {
771 value: v.to_string(),
772 dtype: DType::Interval,
773 }),
774 }
775 }
776
777 pub fn to_bool(&self) -> Result<bool, TypeError> {
779 match self {
780 Self::Bool(v) => Ok(*v),
781 Self::Int64(v) => Ok(*v != 0),
782 Self::Float64(v) => Ok(*v != 0.0 && !v.is_nan()),
783 Self::Null(kind) => Err(TypeError::ValueIsMissing { kind: *kind }),
784 Self::Utf8(v) => Ok(!v.is_empty()),
785 Self::Timedelta64(v) if *v == Timedelta::NAT => Err(TypeError::ValueIsMissing {
786 kind: NullKind::NaT,
787 }),
788 Self::Timedelta64(v) => Ok(*v != 0),
789 Self::Datetime64(v) if *v == Timestamp::NAT => Err(TypeError::ValueIsMissing {
790 kind: NullKind::NaT,
791 }),
792 Self::Datetime64(v) => Ok(*v != 0),
793 Self::Period(v) if *v == i64::MIN => Err(TypeError::ValueIsMissing {
794 kind: NullKind::NaT,
795 }),
796 Self::Period(v) => Ok(*v != 0),
797 Self::Interval(_) => Ok(true),
798 }
799 }
800
801 pub fn to_str(&self) -> String {
803 match self {
804 Self::Bool(v) => if *v { "True" } else { "False" }.to_string(),
805 Self::Int64(v) => v.to_string(),
806 Self::Float64(v) => {
807 if v.is_nan() {
808 "nan".to_string()
809 } else if v.is_infinite() {
810 if *v > 0.0 { "inf" } else { "-inf" }.to_string()
811 } else {
812 v.to_string()
813 }
814 }
815 Self::Null(_) => "NaN".to_string(),
816 Self::Utf8(v) => v.clone(),
817 Self::Timedelta64(v) => Timedelta::format(*v),
818 Self::Datetime64(v) if *v == Timestamp::NAT => "NaT".to_string(),
819 Self::Datetime64(v) => Timestamp::from_nanos(*v).isoformat(),
820 Self::Period(v) if *v == i64::MIN => "NaT".to_string(),
821 Self::Period(v) => format!("Period[{}]", v),
822 Self::Interval(v) => v.to_string(),
823 }
824 }
825}
826
827#[derive(Debug, Error, Clone, PartialEq)]
828pub enum TypeError {
829 #[error("dtype coercion from {left:?} to {right:?} has no compatible common type")]
830 IncompatibleDtypes { left: DType, right: DType },
831 #[error("cannot cast scalar of dtype {from:?} to {to:?}")]
832 InvalidCast { from: DType, to: DType },
833 #[error("cannot cast float {value} to int64 without loss")]
834 LossyFloatToInt { value: f64 },
835 #[error("expected 0/1 for bool cast from int64 but found {value}")]
836 InvalidBoolInt { value: i64 },
837 #[error("expected 0.0/1.0 for bool cast from float64 but found {value}")]
838 InvalidBoolFloat { value: f64 },
839 #[error("value {value:?} has non-numeric dtype {dtype:?}")]
840 NonNumericValue { value: String, dtype: DType },
841 #[error("value is missing ({kind:?})")]
842 ValueIsMissing { kind: NullKind },
843 #[error("sparse value dtype cannot be {dtype:?}")]
844 InvalidSparseValueDType { dtype: DType },
845 #[error("interval_range step must be finite, positive, and not NaN (got {step})")]
846 InvalidIntervalStep { step: f64 },
847 #[error("interval_range step {step} does not evenly divide range end-start={span}")]
848 IntervalStepDoesNotDivide { step: f64, span: f64 },
849 #[error("cannot parse '{value}' as {target}")]
850 ValueNotParseable { value: String, target: String },
851}
852
853pub fn common_dtype(left: DType, right: DType) -> Result<DType, TypeError> {
854 use DType::{
855 Bool, BoolNullable, Categorical, Datetime64, Float64, Int64, Int64Nullable, Null, Sparse,
856 Timedelta64,
857 };
858
859 let out = match (left, right) {
860 (a, b) if a == b => a,
861 (Null, other) | (other, Null) => other,
862 (Categorical, Categorical) => Categorical,
863
864 (Bool, Int64) | (Int64, Bool) => Int64,
866 (Bool, Int64Nullable) | (Int64Nullable, Bool) => Int64Nullable,
867 (BoolNullable, Int64) | (Int64, BoolNullable) => Int64Nullable,
868 (BoolNullable, Int64Nullable) | (Int64Nullable, BoolNullable) => Int64Nullable,
869 (Bool, BoolNullable) | (BoolNullable, Bool) => BoolNullable,
870 (Bool, Float64) | (Float64, Bool) => Float64,
871 (BoolNullable, Float64) | (Float64, BoolNullable) => Float64,
872
873 (Int64, Float64) | (Float64, Int64) => Float64,
875 (Int64Nullable, Float64) | (Float64, Int64Nullable) => Float64,
876 (Int64, Int64Nullable) | (Int64Nullable, Int64) => Int64Nullable,
877
878 (Timedelta64, Timedelta64) => Timedelta64,
880 (Datetime64, Datetime64) => Datetime64,
881
882 (Sparse, _) | (_, Sparse) => return Err(TypeError::IncompatibleDtypes { left, right }),
883 _ => return Err(TypeError::IncompatibleDtypes { left, right }),
884 };
885
886 Ok(out)
887}
888
889pub fn infer_dtype(values: &[Scalar]) -> Result<DType, TypeError> {
890 let mut current = DType::Null;
891 let mut saw_utf8 = false;
892 let mut saw_timedelta = false;
893 let mut saw_datetime = false;
894 let mut saw_non_utf8_non_null = false;
895
896 for value in values {
897 match value.dtype() {
898 DType::Null => {}
899 DType::Utf8 => saw_utf8 = true,
900 DType::Timedelta64 => {
901 saw_timedelta = true;
902 if current == DType::Null {
903 current = DType::Timedelta64;
904 } else if current != DType::Timedelta64 {
905 return Err(TypeError::IncompatibleDtypes {
906 left: current,
907 right: DType::Timedelta64,
908 });
909 }
910 }
911 DType::Datetime64 => {
912 saw_datetime = true;
913 if current == DType::Null {
914 current = DType::Datetime64;
915 } else if current != DType::Datetime64 {
916 return Err(TypeError::IncompatibleDtypes {
917 left: current,
918 right: DType::Datetime64,
919 });
920 }
921 }
922 other => {
923 saw_non_utf8_non_null = true;
924 current = common_dtype(current, other)?;
925 }
926 }
927
928 if saw_utf8 && saw_non_utf8_non_null {
929 return Ok(DType::Utf8);
933 }
934 if saw_timedelta && saw_non_utf8_non_null {
935 return Err(TypeError::IncompatibleDtypes {
936 left: DType::Timedelta64,
937 right: current,
938 });
939 }
940 if saw_datetime && saw_non_utf8_non_null {
941 return Err(TypeError::IncompatibleDtypes {
942 left: DType::Datetime64,
943 right: current,
944 });
945 }
946 }
947
948 if saw_utf8 {
949 Ok(DType::Utf8)
950 } else {
951 Ok(current)
952 }
953}
954
955pub fn cast_scalar_owned(value: Scalar, target: DType) -> Result<Scalar, TypeError> {
958 let from = value.dtype();
959 if from == target {
960 return Ok(value);
961 }
962 if (from == DType::Int64 && target == DType::Int64Nullable)
964 || (from == DType::Int64Nullable && target == DType::Int64)
965 {
966 return Ok(value);
967 }
968 if (from == DType::Bool && target == DType::BoolNullable)
970 || (from == DType::BoolNullable && target == DType::Bool)
971 {
972 return Ok(value);
973 }
974 if target == DType::Utf8 {
975 return Ok(Scalar::Utf8(scalar_to_string_for_astype(value)));
976 }
977 if target == DType::Bool
983 && let Scalar::Float64(v) = &value
984 && v.is_nan()
985 {
986 return Ok(Scalar::Bool(true));
987 }
988 if value.is_missing() {
989 return Ok(Scalar::missing_for_dtype(target));
990 }
991
992 match target {
995 DType::Null => Ok(Scalar::Null(NullKind::Null)),
996 DType::Bool => match &value {
997 Scalar::Int64(v) => Ok(Scalar::Bool(*v != 0)),
1000 Scalar::Float64(v) => Ok(Scalar::Bool(*v != 0.0)),
1003 _ => Err(TypeError::InvalidCast { from, to: target }),
1004 },
1005 DType::BoolNullable => match &value {
1006 Scalar::Bool(b) => Ok(Scalar::Bool(*b)),
1011 Scalar::Int64(0) => Ok(Scalar::Bool(false)),
1012 Scalar::Int64(1) => Ok(Scalar::Bool(true)),
1013 Scalar::Int64(v) => Err(TypeError::InvalidBoolInt { value: *v }),
1014 Scalar::Float64(v) if *v == 0.0 => Ok(Scalar::Bool(false)),
1015 Scalar::Float64(v) if *v == 1.0 => Ok(Scalar::Bool(true)),
1016 Scalar::Float64(v) => Err(TypeError::InvalidBoolFloat { value: *v }),
1017 _ => Err(TypeError::InvalidCast { from, to: target }),
1018 },
1019 DType::Int64 | DType::Int64Nullable => match &value {
1020 Scalar::Bool(v) => Ok(Scalar::Int64(i64::from(*v))),
1021 Scalar::Float64(v) => {
1022 if !v.is_finite() {
1029 return Err(TypeError::LossyFloatToInt { value: *v });
1030 }
1031 if *v < i64::MIN as f64 || *v >= 9223372036854775808.0 {
1032 return Err(TypeError::LossyFloatToInt { value: *v });
1033 }
1034 Ok(Scalar::Int64(*v as i64))
1035 }
1036 Scalar::Utf8(s) => {
1037 if let Ok(v) = s.parse::<i64>() {
1040 return Ok(Scalar::Int64(v));
1041 }
1042 if let Ok(f) = s.parse::<f64>()
1043 && f.is_finite()
1044 && f.fract() == 0.0
1045 && f >= i64::MIN as f64
1046 && f < 9223372036854775808.0
1047 {
1048 return Ok(Scalar::Int64(f as i64));
1049 }
1050 Err(TypeError::InvalidCast { from, to: target })
1051 }
1052 _ => Err(TypeError::InvalidCast { from, to: target }),
1053 },
1054 DType::Float64 => match &value {
1055 Scalar::Bool(v) => Ok(Scalar::Float64(if *v { 1.0 } else { 0.0 })),
1056 Scalar::Int64(v) => Ok(Scalar::Float64(*v as f64)),
1057 Scalar::Utf8(s) => s
1058 .parse::<f64>()
1059 .map(Scalar::Float64)
1060 .map_err(|_| TypeError::InvalidCast { from, to: target }),
1061 _ => Err(TypeError::InvalidCast { from, to: target }),
1062 },
1063 DType::Utf8 => Ok(Scalar::Utf8(scalar_to_string_for_astype(value))),
1064 DType::Categorical => Err(TypeError::InvalidCast { from, to: target }),
1065 DType::Timedelta64 => match &value {
1066 Scalar::Int64(v) => Ok(Scalar::Timedelta64(*v)),
1067 Scalar::Utf8(s) => Timedelta::parse(s)
1068 .map(Scalar::Timedelta64)
1069 .map_err(|_| TypeError::InvalidCast { from, to: target }),
1070 _ => Err(TypeError::InvalidCast { from, to: target }),
1071 },
1072 DType::Datetime64 => match &value {
1073 Scalar::Int64(v) => Ok(Scalar::Datetime64(*v)),
1074 Scalar::Utf8(s) => Timestamp::parse(s)
1075 .map(|timestamp| Scalar::Datetime64(timestamp.nanos))
1076 .map_err(|_| TypeError::InvalidCast { from, to: target }),
1077 _ => Err(TypeError::InvalidCast { from, to: target }),
1078 },
1079 DType::Period => match &value {
1080 Scalar::Int64(v) => Ok(Scalar::Period(*v)),
1081 Scalar::Utf8(s) => Period::parse(s)
1082 .map(|period| Scalar::Period(period.ordinal))
1083 .map_err(|_| TypeError::InvalidCast { from, to: target }),
1084 _ => Err(TypeError::InvalidCast { from, to: target }),
1085 },
1086 DType::Interval => match &value {
1087 Scalar::Utf8(s) => Interval::parse(s)
1088 .map(Scalar::Interval)
1089 .map_err(|_| TypeError::InvalidCast { from, to: target }),
1090 _ => Err(TypeError::InvalidCast { from, to: target }),
1091 },
1092 DType::Sparse => Err(TypeError::InvalidCast { from, to: target }),
1093 }
1094}
1095
1096fn scalar_to_string_for_astype(value: Scalar) -> String {
1097 match value {
1098 Scalar::Null(NullKind::Null) => "None".to_owned(),
1099 Scalar::Null(NullKind::NaN) => "nan".to_owned(),
1100 Scalar::Null(NullKind::NaT) => "NaT".to_owned(),
1101 Scalar::Bool(true) => "True".to_owned(),
1102 Scalar::Bool(false) => "False".to_owned(),
1103 Scalar::Int64(v) => v.to_string(),
1104 Scalar::Float64(v) => float_to_string_for_astype(v),
1105 Scalar::Utf8(s) => s,
1106 Scalar::Timedelta64(v) if v == Timedelta::NAT => "NaT".to_owned(),
1107 Scalar::Timedelta64(v) => Timedelta::format(v),
1108 Scalar::Datetime64(v) if v == Timestamp::NAT => "NaT".to_owned(),
1109 Scalar::Datetime64(v) => format!("Timestamp[{v}]"),
1110 Scalar::Period(v) if v == i64::MIN => "NaT".to_owned(),
1111 Scalar::Period(v) => format!("Period[{v}]"),
1112 Scalar::Interval(v) => v.to_string(),
1113 }
1114}
1115
1116fn float_to_string_for_astype(value: f64) -> String {
1117 if value.is_nan() {
1118 return "nan".to_owned();
1119 }
1120 if value.is_infinite() {
1121 return value.to_string(); }
1123 let s = format!("{value:?}");
1131 match s.split_once('e') {
1132 None => s,
1133 Some((mantissa, exp)) => {
1134 let (sign, digits) = match exp.strip_prefix('-') {
1135 Some(d) => ('-', d),
1136 None => ('+', exp.strip_prefix('+').unwrap_or(exp)),
1137 };
1138 format!("{mantissa}e{sign}{digits:0>2}")
1139 }
1140 }
1141}
1142
1143pub fn cast_scalar(value: &Scalar, target: DType) -> Result<Scalar, TypeError> {
1145 cast_scalar_owned(value.clone(), target)
1146}
1147
1148#[derive(Debug, Error, Clone, PartialEq)]
1151pub enum TimedeltaError {
1152 #[error("invalid timedelta string: {0}")]
1153 InvalidFormat(String),
1154 #[error("overflow in timedelta computation")]
1155 Overflow,
1156}
1157
1158#[derive(Debug, Clone, Copy, Default)]
1159pub struct TimedeltaComponents {
1160 pub days: i64,
1161 pub hours: i64,
1162 pub minutes: i64,
1163 pub seconds: i64,
1164 pub milliseconds: i64,
1165 pub microseconds: i64,
1166 pub nanoseconds: i64,
1167}
1168
1169pub struct Timedelta;
1170
1171impl Timedelta {
1172 pub const NANOS_PER_MICRO: i64 = 1_000;
1173 pub const NANOS_PER_MILLI: i64 = 1_000_000;
1174 pub const NANOS_PER_SEC: i64 = 1_000_000_000;
1175 pub const NANOS_PER_MIN: i64 = 60 * Self::NANOS_PER_SEC;
1176 pub const NANOS_PER_HOUR: i64 = 60 * Self::NANOS_PER_MIN;
1177 pub const NANOS_PER_DAY: i64 = 24 * Self::NANOS_PER_HOUR;
1178 pub const NANOS_PER_WEEK: i64 = 7 * Self::NANOS_PER_DAY;
1179
1180 pub const NAT: i64 = i64::MIN;
1181
1182 pub fn parse(s: &str) -> Result<i64, TimedeltaError> {
1183 let s = s.trim();
1184
1185 if s.eq_ignore_ascii_case("nat") {
1186 return Ok(Self::NAT);
1187 }
1188
1189 let (negative, s) = if let Some(rest) = s.strip_prefix('-') {
1190 (true, rest.trim())
1191 } else {
1192 (false, s)
1193 };
1194
1195 if let Some(nanos) = Self::try_parse_time_format(s) {
1196 return Ok(if negative { -nanos } else { nanos });
1197 }
1198
1199 if let Some(nanos) = Self::try_parse_iso8601_duration(s) {
1200 return Ok(if negative { -nanos } else { nanos });
1201 }
1202
1203 let nanos = Self::parse_compound(s)?;
1204 Ok(if negative { -nanos } else { nanos })
1205 }
1206
1207 fn try_parse_iso8601_duration(s: &str) -> Option<i64> {
1215 let mut rest = s.strip_prefix('P')?;
1216 if rest.is_empty() {
1217 return None;
1218 }
1219 let mut total: i64 = 0;
1220 let mut saw_component = false;
1221 while !rest.is_empty() {
1222 if let Some(after_t) = rest.strip_prefix('T') {
1223 rest = after_t;
1224 continue;
1225 }
1226 let num_end = rest.find(|c: char| !c.is_ascii_digit() && c != '.')?;
1227 if num_end == 0 {
1228 return None;
1229 }
1230 let num_str = &rest[..num_end];
1231 let unit = rest.as_bytes()[num_end];
1232 let is_fractional = num_str.contains('.');
1233 rest = &rest[num_end + 1..];
1234
1235 let (multiplier, frac_ok) = match unit {
1236 b'W' => (Self::NANOS_PER_WEEK, false),
1237 b'D' => (Self::NANOS_PER_DAY, false),
1238 b'H' => (Self::NANOS_PER_HOUR, false),
1239 b'M' => (Self::NANOS_PER_MIN, false),
1240 b'S' => (Self::NANOS_PER_SEC, true),
1241 _ => return None,
1242 };
1243 if is_fractional {
1244 if !frac_ok {
1245 return None;
1246 }
1247 let value: f64 = num_str.parse().ok()?;
1248 let product = value * multiplier as f64;
1249 if !product.is_finite() || product.abs() >= 9223372036854775808.0 {
1250 return None;
1251 }
1252 total = total.checked_add(product.round() as i64)?;
1253 } else {
1254 let value: i64 = num_str.parse().ok()?;
1255 total = total.checked_add(value.checked_mul(multiplier)?)?;
1256 }
1257 saw_component = true;
1258 }
1259 saw_component.then_some(total)
1260 }
1261
1262 fn try_parse_time_format(s: &str) -> Option<i64> {
1263 let parts: Vec<&str> = s.split(':').collect();
1264 if parts.len() < 2 || parts.len() > 3 {
1265 return None;
1266 }
1267
1268 let hours: i64 = parts[0].parse().ok()?;
1269 let minutes: i64 = parts[1].parse().ok()?;
1270
1271 let (seconds, frac_nanos) = if parts.len() == 3 {
1272 if let Some((sec_str, frac_str)) = parts[2].split_once('.') {
1273 let sec: i64 = sec_str.parse().ok()?;
1274 if !frac_str.bytes().all(|byte| byte.is_ascii_digit()) {
1275 return None;
1276 }
1277 let mut frac = 0_i64;
1278 let taken = frac_str.len().min(9);
1279 for byte in frac_str.bytes().take(9) {
1280 frac = frac * 10 + i64::from(byte - b'0');
1281 }
1282 for _ in taken..9 {
1283 frac *= 10;
1284 }
1285 (sec, frac)
1286 } else {
1287 let sec: i64 = parts[2].parse().ok()?;
1288 (sec, 0)
1289 }
1290 } else {
1291 (0, 0)
1292 };
1293
1294 hours
1295 .checked_mul(Self::NANOS_PER_HOUR)?
1296 .checked_add(minutes.checked_mul(Self::NANOS_PER_MIN)?)?
1297 .checked_add(seconds.checked_mul(Self::NANOS_PER_SEC)?)?
1298 .checked_add(frac_nanos)
1299 }
1300
1301 fn parse_compound(s: &str) -> Result<i64, TimedeltaError> {
1302 let mut total: i64 = 0;
1303 let mut remaining = s;
1304
1305 while !remaining.is_empty() {
1306 remaining = remaining.trim_start();
1307 if remaining.is_empty() {
1308 break;
1309 }
1310
1311 if remaining.contains(':')
1314 && let Some(time_nanos) = Self::try_parse_time_format(remaining)
1315 {
1316 total = total
1317 .checked_add(time_nanos)
1318 .ok_or(TimedeltaError::Overflow)?;
1319 break;
1320 }
1321
1322 let num_end = remaining
1323 .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '-')
1324 .unwrap_or(remaining.len());
1325
1326 if num_end == 0 {
1327 return Err(TimedeltaError::InvalidFormat(s.to_string()));
1328 }
1329
1330 let num_str = &remaining[..num_end];
1331 let num: f64 = num_str
1332 .parse()
1333 .map_err(|_| TimedeltaError::InvalidFormat(s.to_string()))?;
1334
1335 remaining = remaining[num_end..].trim_start();
1336
1337 let unit_end = remaining
1338 .find(|c: char| c.is_ascii_digit() || c.is_whitespace())
1339 .unwrap_or(remaining.len());
1340
1341 let unit = &remaining[..unit_end];
1342 remaining = &remaining[unit_end..];
1343
1344 let multiplier = Self::unit_to_nanos(unit)
1345 .ok_or_else(|| TimedeltaError::InvalidFormat(s.to_string()))?;
1346
1347 let product = num * multiplier as f64;
1352 if !product.is_finite() || product.abs() >= 9223372036854775808.0 {
1353 return Err(TimedeltaError::Overflow);
1354 }
1355 let nanos = product.round() as i64;
1356 total = total.checked_add(nanos).ok_or(TimedeltaError::Overflow)?;
1357 }
1358
1359 if total == 0 && !s.trim().is_empty() && s.trim() != "0" {
1360 return Err(TimedeltaError::InvalidFormat(s.to_string()));
1361 }
1362
1363 Ok(total)
1364 }
1365
1366 #[must_use]
1380 pub fn unit_to_nanos(unit: &str) -> Option<i64> {
1381 match unit.to_lowercase().as_str() {
1382 "w" | "week" | "weeks" => Some(Self::NANOS_PER_WEEK),
1383 "d" | "day" | "days" => Some(Self::NANOS_PER_DAY),
1384 "h" | "hr" | "hour" | "hours" => Some(Self::NANOS_PER_HOUR),
1385 "m" | "min" | "minute" | "minutes" | "t" => Some(Self::NANOS_PER_MIN),
1386 "s" | "sec" | "second" | "seconds" => Some(Self::NANOS_PER_SEC),
1387 "ms" | "milli" | "millis" | "millisecond" | "milliseconds" | "l" => {
1388 Some(Self::NANOS_PER_MILLI)
1389 }
1390 "us" | "µs" | "micro" | "micros" | "microsecond" | "microseconds" | "u" => {
1391 Some(Self::NANOS_PER_MICRO)
1392 }
1393 "ns" | "nano" | "nanos" | "nanosecond" | "nanoseconds" | "n" => Some(1),
1394 "" => Some(Self::NANOS_PER_DAY),
1395 _ => None,
1396 }
1397 }
1398
1399 pub fn components(nanos: i64) -> TimedeltaComponents {
1400 if nanos == Self::NAT {
1401 return TimedeltaComponents::default();
1402 }
1403
1404 let days = nanos.div_euclid(Self::NANOS_PER_DAY);
1409 let rem = nanos.rem_euclid(Self::NANOS_PER_DAY);
1410
1411 let hours = rem / Self::NANOS_PER_HOUR;
1412 let rem = rem % Self::NANOS_PER_HOUR;
1413
1414 let minutes = rem / Self::NANOS_PER_MIN;
1415 let rem = rem % Self::NANOS_PER_MIN;
1416
1417 let seconds = rem / Self::NANOS_PER_SEC;
1418 let rem = rem % Self::NANOS_PER_SEC;
1419
1420 let milliseconds = rem / Self::NANOS_PER_MILLI;
1421 let rem = rem % Self::NANOS_PER_MILLI;
1422
1423 let microseconds = rem / Self::NANOS_PER_MICRO;
1424 let nanoseconds = rem % Self::NANOS_PER_MICRO;
1425
1426 TimedeltaComponents {
1427 days,
1428 hours,
1429 minutes,
1430 seconds,
1431 milliseconds,
1432 microseconds,
1433 nanoseconds,
1434 }
1435 }
1436
1437 pub fn total_seconds(nanos: i64) -> f64 {
1438 if nanos == Self::NAT {
1439 f64::NAN
1440 } else {
1441 nanos as f64 / Self::NANOS_PER_SEC as f64
1442 }
1443 }
1444
1445 #[must_use]
1449 pub fn as_unit(nanos: i64, unit: &str) -> f64 {
1450 if nanos == Self::NAT {
1451 return f64::NAN;
1452 }
1453 let nanos_f = nanos as f64;
1454 match unit {
1455 "ns" | "nanoseconds" => nanos_f,
1456 "us" | "microseconds" => nanos_f / Self::NANOS_PER_MICRO as f64,
1457 "ms" | "milliseconds" => nanos_f / Self::NANOS_PER_MILLI as f64,
1458 "s" | "seconds" => nanos_f / Self::NANOS_PER_SEC as f64,
1459 "m" | "minutes" => nanos_f / Self::NANOS_PER_MIN as f64,
1460 "h" | "hours" => nanos_f / Self::NANOS_PER_HOUR as f64,
1461 "D" | "days" => nanos_f / Self::NANOS_PER_DAY as f64,
1462 _ => f64::NAN,
1463 }
1464 }
1465
1466 #[must_use]
1468 pub fn days(nanos: i64) -> i64 {
1469 if nanos == Self::NAT {
1470 return 0; }
1472 nanos.div_euclid(Self::NANOS_PER_DAY)
1474 }
1475
1476 #[must_use]
1478 pub fn seconds(nanos: i64) -> i64 {
1479 if nanos == Self::NAT {
1480 return 0;
1481 }
1482 nanos.rem_euclid(Self::NANOS_PER_DAY) / Self::NANOS_PER_SEC
1484 }
1485
1486 #[must_use]
1488 pub fn microseconds(nanos: i64) -> i64 {
1489 if nanos == Self::NAT {
1490 return 0;
1491 }
1492 nanos.rem_euclid(Self::NANOS_PER_SEC) / Self::NANOS_PER_MICRO
1493 }
1494
1495 #[must_use]
1497 pub fn nanoseconds(nanos: i64) -> i64 {
1498 if nanos == Self::NAT {
1499 return 0;
1500 }
1501 nanos.rem_euclid(Self::NANOS_PER_MICRO)
1502 }
1503
1504 pub fn format(nanos: i64) -> String {
1505 if nanos == Self::NAT {
1506 return "NaT".to_string();
1507 }
1508
1509 let days = nanos.div_euclid(Self::NANOS_PER_DAY);
1515 let rem = nanos.rem_euclid(Self::NANOS_PER_DAY);
1516 let hours = rem / Self::NANOS_PER_HOUR;
1517 let minutes = (rem % Self::NANOS_PER_HOUR) / Self::NANOS_PER_MIN;
1518 let seconds = (rem % Self::NANOS_PER_MIN) / Self::NANOS_PER_SEC;
1519 let frac = rem % Self::NANOS_PER_SEC;
1520
1521 let time_part = format!("{hours:02}:{minutes:02}:{seconds:02}");
1522 let sep = if days < 0 { "+" } else { "" };
1524
1525 if frac > 0 {
1526 if frac % 1_000 == 0 {
1530 format!("{days} days {sep}{time_part}.{:06}", frac / 1_000)
1531 } else {
1532 format!("{days} days {sep}{time_part}.{frac:09}")
1533 }
1534 } else {
1535 format!("{days} days {sep}{time_part}")
1536 }
1537 }
1538
1539 pub fn from_unit(value: f64, unit: &str) -> Result<i64, TimedeltaError> {
1540 let multiplier = Self::unit_to_nanos(unit)
1541 .ok_or_else(|| TimedeltaError::InvalidFormat(unit.to_string()))?;
1542 Ok((value * multiplier as f64).round() as i64)
1543 }
1544
1545 #[must_use]
1554 pub fn add(a: i64, b: i64) -> i64 {
1555 if a == Self::NAT || b == Self::NAT {
1556 return Self::NAT;
1557 }
1558 a.saturating_add(b)
1559 }
1560
1561 #[must_use]
1563 pub fn sub(a: i64, b: i64) -> i64 {
1564 if a == Self::NAT || b == Self::NAT {
1565 return Self::NAT;
1566 }
1567 a.saturating_sub(b)
1568 }
1569
1570 #[must_use]
1573 pub fn neg(a: i64) -> i64 {
1574 if a == Self::NAT {
1575 return Self::NAT;
1576 }
1577 a.saturating_neg()
1578 }
1579
1580 #[must_use]
1582 pub fn abs(a: i64) -> i64 {
1583 if a == Self::NAT {
1584 return Self::NAT;
1585 }
1586 a.saturating_abs()
1587 }
1588
1589 #[must_use]
1594 pub fn mul_scalar(a: i64, factor: i64) -> i64 {
1595 if a == Self::NAT {
1596 return Self::NAT;
1597 }
1598 a.saturating_mul(factor)
1599 }
1600
1601 #[must_use]
1611 pub fn div_scalar(a: i64, divisor: i64) -> i64 {
1612 if a == Self::NAT || divisor == 0 {
1613 return Self::NAT;
1614 }
1615 let q = a / divisor;
1620 let r = a % divisor;
1621 if r != 0 && (r < 0) != (divisor < 0) {
1625 q - 1
1626 } else {
1627 q
1628 }
1629 }
1630
1631 #[must_use]
1635 pub fn div_timedelta(a: i64, b: i64) -> f64 {
1636 if a == Self::NAT || b == Self::NAT {
1637 return f64::NAN;
1638 }
1639 (a as f64) / (b as f64)
1640 }
1641
1642 #[must_use]
1648 pub fn isoformat(nanos: i64) -> String {
1649 if nanos == Self::NAT {
1650 return "NaT".to_string();
1651 }
1652
1653 let negative = nanos < 0;
1654 let abs_nanos = nanos.saturating_abs();
1655
1656 let days = abs_nanos / Self::NANOS_PER_DAY;
1657 let remaining = abs_nanos % Self::NANOS_PER_DAY;
1658
1659 let hours = remaining / Self::NANOS_PER_HOUR;
1660 let remaining = remaining % Self::NANOS_PER_HOUR;
1661
1662 let minutes = remaining / Self::NANOS_PER_MIN;
1663 let remaining = remaining % Self::NANOS_PER_MIN;
1664
1665 let seconds = remaining / Self::NANOS_PER_SEC;
1666 let sub_sec_nanos = remaining % Self::NANOS_PER_SEC;
1667
1668 let mut result = String::new();
1669 if negative {
1670 result.push('-');
1671 }
1672
1673 result.push_str(&format!("P{days}DT{hours}H{minutes}M"));
1674
1675 if sub_sec_nanos == 0 {
1676 result.push_str(&format!("{seconds}S"));
1677 } else {
1678 let frac = format!("{:09}", sub_sec_nanos);
1679 let trimmed = frac.trim_end_matches('0');
1680 result.push_str(&format!("{seconds}.{trimmed}S"));
1681 }
1682
1683 result
1684 }
1685
1686 #[must_use]
1690 pub fn floor(nanos: i64, freq: &str) -> i64 {
1691 if nanos == Self::NAT {
1692 return Self::NAT;
1693 }
1694 let Some(unit_nanos) = Self::unit_to_nanos(freq) else {
1695 return Self::NAT;
1696 };
1697 if unit_nanos == 0 {
1698 return Self::NAT;
1699 }
1700 let negative = nanos < 0;
1701 let abs_nanos = nanos.saturating_abs();
1702 let floored = (abs_nanos / unit_nanos) * unit_nanos;
1703 if negative { -floored } else { floored }
1704 }
1705
1706 #[must_use]
1710 pub fn ceil(nanos: i64, freq: &str) -> i64 {
1711 if nanos == Self::NAT {
1712 return Self::NAT;
1713 }
1714 let Some(unit_nanos) = Self::unit_to_nanos(freq) else {
1715 return Self::NAT;
1716 };
1717 if unit_nanos == 0 {
1718 return Self::NAT;
1719 }
1720 let negative = nanos < 0;
1721 let abs_nanos = nanos.saturating_abs();
1722 let ceiled = ((abs_nanos + unit_nanos - 1) / unit_nanos) * unit_nanos;
1723 if negative { -ceiled } else { ceiled }
1724 }
1725
1726 #[must_use]
1731 pub fn round(nanos: i64, freq: &str) -> i64 {
1732 if nanos == Self::NAT {
1733 return Self::NAT;
1734 }
1735 let Some(unit_nanos) = Self::unit_to_nanos(freq) else {
1736 return Self::NAT;
1737 };
1738 if unit_nanos == 0 {
1739 return Self::NAT;
1740 }
1741 let negative = nanos < 0;
1742 let abs_nanos = nanos.saturating_abs();
1743
1744 let quotient = abs_nanos / unit_nanos;
1745 let remainder = abs_nanos % unit_nanos;
1746 let half = unit_nanos / 2;
1747
1748 let rounded = if remainder > half {
1749 (quotient + 1) * unit_nanos
1750 } else if remainder < half {
1751 quotient * unit_nanos
1752 } else {
1753 if quotient % 2 == 0 {
1755 quotient * unit_nanos
1756 } else {
1757 (quotient + 1) * unit_nanos
1758 }
1759 };
1760
1761 if negative { -rounded } else { rounded }
1762 }
1763}
1764
1765fn days_in_month(year: i64, month: u32) -> Option<u32> {
1775 if !(1..=12).contains(&month) {
1776 return None;
1777 }
1778 let is_leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
1779 let days: [u32; 12] = [
1780 31,
1781 if is_leap { 29 } else { 28 },
1782 31,
1783 30,
1784 31,
1785 30,
1786 31,
1787 31,
1788 30,
1789 31,
1790 30,
1791 31,
1792 ];
1793 Some(days[(month - 1) as usize])
1794}
1795
1796fn iso_weeks_in_year(year: i64) -> i64 {
1803 fn p(y: i64) -> i64 {
1804 (y + y.div_euclid(4) - y.div_euclid(100) + y.div_euclid(400)).rem_euclid(7)
1805 }
1806 if p(year) == 4 || p(year - 1) == 3 {
1807 53
1808 } else {
1809 52
1810 }
1811}
1812
1813#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1819pub struct Timestamp {
1820 pub nanos: i64,
1822 #[serde(default, skip_serializing_if = "Option::is_none")]
1826 pub tz: Option<String>,
1827}
1828
1829impl Timestamp {
1830 pub const NAT: i64 = i64::MIN;
1832
1833 #[must_use]
1836 pub const fn from_nanos(nanos: i64) -> Self {
1837 Self { nanos, tz: None }
1838 }
1839
1840 #[must_use]
1845 pub fn from_nanos_tz(nanos: i64, tz_name: impl Into<String>) -> Self {
1846 Self {
1847 nanos,
1848 tz: Some(tz_name.into()),
1849 }
1850 }
1851
1852 #[must_use]
1856 pub fn now() -> Self {
1857 use std::time::{SystemTime, UNIX_EPOCH};
1858 let duration = SystemTime::now()
1859 .duration_since(UNIX_EPOCH)
1860 .unwrap_or_default();
1861 let nanos = duration.as_nanos() as i64;
1862 Self { nanos, tz: None }
1863 }
1864
1865 #[must_use]
1867 pub fn utcnow() -> Self {
1868 Self::now()
1869 }
1870
1871 #[must_use]
1875 pub fn today() -> Self {
1876 let now = Self::now();
1877 now.normalize()
1878 }
1879
1880 #[must_use]
1882 pub const fn nat() -> Self {
1883 Self {
1884 nanos: Self::NAT,
1885 tz: None,
1886 }
1887 }
1888
1889 #[must_use]
1891 pub const fn is_nat(&self) -> bool {
1892 self.nanos == Self::NAT
1893 }
1894
1895 #[must_use]
1897 pub const fn value(&self) -> i64 {
1898 self.nanos
1899 }
1900
1901 #[must_use]
1906 pub const fn unit(&self) -> Option<&'static str> {
1907 if self.is_nat() { None } else { Some("ns") }
1908 }
1909
1910 #[must_use]
1914 pub const fn resolution(&self) -> Option<&'static str> {
1915 if self.is_nat() { None } else { Some("ns") }
1916 }
1917
1918 #[must_use]
1920 pub const fn asm8(&self) -> i64 {
1921 self.value()
1922 }
1923
1924 #[must_use]
1926 pub const fn to_datetime64(&self) -> i64 {
1927 self.value()
1928 }
1929
1930 #[must_use]
1932 pub const fn to_numpy(&self) -> i64 {
1933 self.value()
1934 }
1935
1936 pub fn timestamp(&self) -> Result<f64, TypeError> {
1942 if self.is_nat() {
1943 return Err(TypeError::ValueIsMissing {
1944 kind: NullKind::NaT,
1945 });
1946 }
1947 let seconds = self.nanos as f64 / 1_000_000_000.0;
1948 let rounded = format!("{seconds:.6}").parse().unwrap_or(seconds);
1949 Ok(rounded)
1950 }
1951
1952 #[must_use]
1955 pub fn add_timedelta(&self, td_nanos: i64) -> Self {
1956 if self.is_nat() || td_nanos == Timedelta::NAT {
1957 return Self::nat();
1958 }
1959 Self {
1960 nanos: self.nanos.saturating_add(td_nanos),
1961 tz: self.tz.clone(),
1962 }
1963 }
1964
1965 #[must_use]
1967 pub fn sub_timedelta(&self, td_nanos: i64) -> Self {
1968 if self.is_nat() || td_nanos == Timedelta::NAT {
1969 return Self::nat();
1970 }
1971 Self {
1972 nanos: self.nanos.saturating_sub(td_nanos),
1973 tz: self.tz.clone(),
1974 }
1975 }
1976
1977 #[must_use]
1980 pub fn sub_timestamp(&self, other: &Self) -> i64 {
1981 if self.is_nat() || other.is_nat() {
1982 return Timedelta::NAT;
1983 }
1984 self.nanos.saturating_sub(other.nanos)
1985 }
1986
1987 #[must_use]
1992 pub fn semantic_eq(&self, other: &Self) -> bool {
1993 if self.is_nat() && other.is_nat() {
1994 return true;
1995 }
1996 if self.is_nat() || other.is_nat() {
1997 return false;
1998 }
1999 self.nanos == other.nanos && self.tz == other.tz
2000 }
2001
2002 #[must_use]
2015 pub fn floor_to(&self, unit_nanos: i64) -> Self {
2016 if self.is_nat() || unit_nanos <= 0 {
2017 return Self::nat();
2018 }
2019 Self {
2020 nanos: self.nanos.div_euclid(unit_nanos) * unit_nanos,
2021 tz: self.tz.clone(),
2022 }
2023 }
2024
2025 #[must_use]
2030 pub fn ceil_to(&self, unit_nanos: i64) -> Self {
2031 if self.is_nat() || unit_nanos <= 0 {
2032 return Self::nat();
2033 }
2034 let rem = self.nanos.rem_euclid(unit_nanos);
2035 let nanos = if rem == 0 {
2036 self.nanos
2037 } else {
2038 self.nanos.saturating_add(unit_nanos - rem)
2039 };
2040 Self {
2041 nanos,
2042 tz: self.tz.clone(),
2043 }
2044 }
2045
2046 #[must_use]
2052 pub fn round_to(&self, unit_nanos: i64) -> Self {
2053 if self.is_nat() || unit_nanos <= 0 {
2054 return Self::nat();
2055 }
2056 let floor = self.nanos.div_euclid(unit_nanos);
2057 let rem = self.nanos.rem_euclid(unit_nanos);
2058 let half = unit_nanos / 2;
2059 let chosen_floor = if rem < half {
2060 floor
2061 } else if rem > half {
2062 floor + 1
2063 } else if unit_nanos % 2 != 0 {
2064 floor + 1
2066 } else {
2067 if floor % 2 == 0 { floor } else { floor + 1 }
2069 };
2070 Self {
2071 nanos: chosen_floor.saturating_mul(unit_nanos),
2072 tz: self.tz.clone(),
2073 }
2074 }
2075
2076 #[must_use]
2088 pub fn floor_to_unit(&self, unit: &str) -> Self {
2089 match Timedelta::unit_to_nanos(unit) {
2090 Some(unit_nanos) => self.floor_to(unit_nanos),
2091 None => Self::nat(),
2092 }
2093 }
2094
2095 #[must_use]
2099 pub fn ceil_to_unit(&self, unit: &str) -> Self {
2100 match Timedelta::unit_to_nanos(unit) {
2101 Some(unit_nanos) => self.ceil_to(unit_nanos),
2102 None => Self::nat(),
2103 }
2104 }
2105
2106 #[must_use]
2110 pub fn round_to_unit(&self, unit: &str) -> Self {
2111 match Timedelta::unit_to_nanos(unit) {
2112 Some(unit_nanos) => self.round_to(unit_nanos),
2113 None => Self::nat(),
2114 }
2115 }
2116
2117 #[must_use]
2119 pub fn floor(&self, freq: &str) -> Self {
2120 self.floor_to_unit(freq)
2121 }
2122
2123 #[must_use]
2125 pub fn ceil(&self, freq: &str) -> Self {
2126 self.ceil_to_unit(freq)
2127 }
2128
2129 #[must_use]
2131 pub fn round(&self, freq: &str) -> Self {
2132 self.round_to_unit(freq)
2133 }
2134
2135 #[must_use]
2139 pub fn year(&self) -> Option<i64> {
2140 if self.is_nat() {
2141 return None;
2142 }
2143 let total_secs = self.nanos / Timedelta::NANOS_PER_SEC;
2144 let days_since_epoch = total_secs / 86400;
2145 let days = days_since_epoch + 719_468;
2146 let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
2147 let doe = days - era * 146_097;
2148 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
2149 let y = yoe + era * 400;
2150 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
2151 let mp = (5 * doy + 2) / 153;
2152 let m = if mp < 10 { mp + 3 } else { mp - 9 };
2153 Some(if m <= 2 { y + 1 } else { y })
2154 }
2155
2156 #[must_use]
2160 pub fn month(&self) -> Option<i64> {
2161 if self.is_nat() {
2162 return None;
2163 }
2164 let total_secs = self.nanos / Timedelta::NANOS_PER_SEC;
2165 let days_since_epoch = total_secs / 86400;
2166 let days = days_since_epoch + 719_468;
2167 let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
2168 let doe = days - era * 146_097;
2169 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
2170 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
2171 let mp = (5 * doy + 2) / 153;
2172 Some(if mp < 10 { mp + 3 } else { mp - 9 })
2173 }
2174
2175 #[must_use]
2179 pub fn day(&self) -> Option<i64> {
2180 if self.is_nat() {
2181 return None;
2182 }
2183 let total_secs = self.nanos / Timedelta::NANOS_PER_SEC;
2184 let days_since_epoch = total_secs / 86400;
2185 let days = days_since_epoch + 719_468;
2186 let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
2187 let doe = days - era * 146_097;
2188 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
2189 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
2190 let mp = (5 * doy + 2) / 153;
2191 Some(doy - (153 * mp + 2) / 5 + 1)
2192 }
2193
2194 #[must_use]
2198 pub fn hour(&self) -> Option<i64> {
2199 if self.is_nat() {
2200 return None;
2201 }
2202 let total_secs = self.nanos / Timedelta::NANOS_PER_SEC;
2203 let secs_of_day = (total_secs % 86400 + 86400) % 86400;
2204 Some(secs_of_day / 3600)
2205 }
2206
2207 #[must_use]
2211 pub fn minute(&self) -> Option<i64> {
2212 if self.is_nat() {
2213 return None;
2214 }
2215 let total_secs = self.nanos / Timedelta::NANOS_PER_SEC;
2216 let secs_of_day = (total_secs % 86400 + 86400) % 86400;
2217 Some((secs_of_day % 3600) / 60)
2218 }
2219
2220 #[must_use]
2224 pub fn second(&self) -> Option<i64> {
2225 if self.is_nat() {
2226 return None;
2227 }
2228 let total_secs = self.nanos / Timedelta::NANOS_PER_SEC;
2229 let secs_of_day = (total_secs % 86400 + 86400) % 86400;
2230 Some(secs_of_day % 60)
2231 }
2232
2233 #[must_use]
2237 pub fn microsecond(&self) -> Option<i64> {
2238 if self.is_nat() {
2239 return None;
2240 }
2241 let sub_nanos = (self.nanos % Timedelta::NANOS_PER_SEC).unsigned_abs();
2242 Some((sub_nanos / 1000) as i64)
2243 }
2244
2245 #[must_use]
2249 pub fn nanosecond(&self) -> Option<i64> {
2250 if self.is_nat() {
2251 return None;
2252 }
2253 let sub_nanos = (self.nanos % Timedelta::NANOS_PER_SEC).unsigned_abs();
2254 Some((sub_nanos % 1000) as i64)
2255 }
2256
2257 #[must_use]
2261 pub fn dayofweek(&self) -> Option<i64> {
2262 if self.is_nat() {
2263 return None;
2264 }
2265 let days_since_epoch = self.nanos / Timedelta::NANOS_PER_DAY;
2266 let dow = ((days_since_epoch + 3) % 7 + 7) % 7;
2267 Some(dow)
2268 }
2269
2270 #[must_use]
2272 pub fn weekday(&self) -> Option<i64> {
2273 self.dayofweek()
2274 }
2275
2276 #[must_use]
2278 pub fn day_of_week(&self) -> Option<i64> {
2279 self.dayofweek()
2280 }
2281
2282 #[must_use]
2286 pub fn dayofyear(&self) -> Option<i64> {
2287 if self.is_nat() {
2288 return None;
2289 }
2290 let m = self.month()?;
2291 let d = self.day()?;
2292 let y = self.year()?;
2293 let is_leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
2294 let days_before: [i64; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
2295 let base = days_before[(m - 1) as usize] + d;
2296 if is_leap && m > 2 {
2297 Some(base + 1)
2298 } else {
2299 Some(base)
2300 }
2301 }
2302
2303 #[must_use]
2305 pub fn day_of_year(&self) -> Option<i64> {
2306 self.dayofyear()
2307 }
2308
2309 #[must_use]
2313 pub fn toordinal(&self) -> Option<i64> {
2314 if self.is_nat() {
2315 return None;
2316 }
2317 let y = self.year()?;
2318 let m = self.month()?;
2319 let d = self.day()?;
2320 let y_minus_1 = y - 1;
2324 let mut ordinal = y_minus_1 * 365 + y_minus_1 / 4 - y_minus_1 / 100 + y_minus_1 / 400;
2325 let is_leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
2326 let days_before: [i64; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
2327 ordinal += days_before[(m - 1) as usize];
2328 if is_leap && m > 2 {
2329 ordinal += 1;
2330 }
2331 ordinal += d;
2332 Some(ordinal)
2333 }
2334
2335 #[must_use]
2339 pub fn fromordinal(ordinal: i64) -> Self {
2340 if ordinal <= 0 {
2341 return Self {
2342 nanos: Self::NAT,
2343 tz: None,
2344 };
2345 }
2346 let days_since_epoch = ordinal - 719163;
2349 let nanos = days_since_epoch * 24 * 60 * 60 * 1_000_000_000_i64;
2350 Self { nanos, tz: None }
2351 }
2352
2353 #[must_use]
2359 pub fn to_julian_date(&self) -> f64 {
2360 if self.is_nat() {
2361 return f64::NAN;
2362 }
2363 let ordinal = match self.toordinal() {
2367 Some(o) => o,
2368 None => return f64::NAN,
2369 };
2370 let h = self.hour().unwrap_or(0) as f64;
2372 let m = self.minute().unwrap_or(0) as f64;
2373 let s = self.second().unwrap_or(0) as f64;
2374 let us = self.microsecond().unwrap_or(0) as f64;
2375 let ns = self.nanosecond().unwrap_or(0) as f64;
2376 let frac_day =
2377 (h + m / 60.0 + s / 3600.0 + us / 3_600_000_000.0 + ns / 3_600_000_000_000.0) / 24.0;
2378 1721424.5 + ordinal as f64 + frac_day
2380 }
2381
2382 #[must_use]
2386 pub fn quarter(&self) -> Option<i64> {
2387 self.month().map(|m| (m - 1) / 3 + 1)
2388 }
2389
2390 #[must_use]
2394 pub fn weekofyear(&self) -> Option<i64> {
2395 if self.is_nat() {
2396 return None;
2397 }
2398 let doy = self.dayofyear()?;
2399 let dow = self.dayofweek()?;
2400 let year = self.year()?;
2401 let iso_dow = if dow == 6 { 7 } else { dow + 1 };
2402 let week = (doy - iso_dow + 10) / 7;
2403 if week < 1 {
2409 Some(iso_weeks_in_year(year - 1))
2410 } else if week > iso_weeks_in_year(year) {
2411 Some(1)
2412 } else {
2413 Some(week)
2414 }
2415 }
2416
2417 #[must_use]
2419 pub fn week(&self) -> Option<i64> {
2420 self.weekofyear()
2421 }
2422
2423 #[must_use]
2428 pub fn to_unit(&self, unit: &str) -> Option<i64> {
2429 if self.is_nat() {
2430 return None;
2431 }
2432 match unit {
2433 "ns" | "nanosecond" | "nanoseconds" => Some(self.nanos),
2434 "us" | "microsecond" | "microseconds" => Some(self.nanos / 1_000),
2435 "ms" | "millisecond" | "milliseconds" => Some(self.nanos / 1_000_000),
2436 "s" | "second" | "seconds" => Some(self.nanos / 1_000_000_000),
2437 _ => None,
2438 }
2439 }
2440
2441 #[must_use]
2445 pub fn is_leap_year(&self) -> Option<bool> {
2446 self.year()
2447 .map(|y| (y % 4 == 0 && y % 100 != 0) || y % 400 == 0)
2448 }
2449
2450 #[must_use]
2454 pub fn is_month_start(&self) -> Option<bool> {
2455 self.day().map(|d| d == 1)
2456 }
2457
2458 #[must_use]
2462 pub fn is_month_end(&self) -> Option<bool> {
2463 let y = self.year()?;
2464 let m = self.month()?;
2465 let d = self.day()?;
2466 let is_leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
2467 let days_in_month: [i64; 12] = [
2468 31,
2469 if is_leap { 29 } else { 28 },
2470 31,
2471 30,
2472 31,
2473 30,
2474 31,
2475 31,
2476 30,
2477 31,
2478 30,
2479 31,
2480 ];
2481 Some(d == days_in_month[(m - 1) as usize])
2482 }
2483
2484 #[must_use]
2488 pub fn is_quarter_start(&self) -> Option<bool> {
2489 let m = self.month()?;
2490 let d = self.day()?;
2491 Some(d == 1 && (m == 1 || m == 4 || m == 7 || m == 10))
2492 }
2493
2494 #[must_use]
2498 pub fn is_quarter_end(&self) -> Option<bool> {
2499 let m = self.month()?;
2500 let d = self.day()?;
2501 Some(
2502 (m == 3 && d == 31)
2503 || (m == 6 && d == 30)
2504 || (m == 9 && d == 30)
2505 || (m == 12 && d == 31),
2506 )
2507 }
2508
2509 #[must_use]
2513 pub fn is_year_start(&self) -> Option<bool> {
2514 let m = self.month()?;
2515 let d = self.day()?;
2516 Some(m == 1 && d == 1)
2517 }
2518
2519 #[must_use]
2523 pub fn is_year_end(&self) -> Option<bool> {
2524 let m = self.month()?;
2525 let d = self.day()?;
2526 Some(m == 12 && d == 31)
2527 }
2528
2529 #[must_use]
2533 pub fn days_in_month(&self) -> Option<i64> {
2534 let y = self.year()?;
2535 let m = self.month()?;
2536 let is_leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
2537 let days: [i64; 12] = [
2538 31,
2539 if is_leap { 29 } else { 28 },
2540 31,
2541 30,
2542 31,
2543 30,
2544 31,
2545 31,
2546 30,
2547 31,
2548 30,
2549 31,
2550 ];
2551 Some(days[(m - 1) as usize])
2552 }
2553
2554 #[must_use]
2556 pub fn daysinmonth(&self) -> Option<i64> {
2557 self.days_in_month()
2558 }
2559
2560 #[must_use]
2562 pub fn normalize(&self) -> Self {
2563 self.floor_to_unit("D")
2564 }
2565
2566 #[must_use]
2570 #[allow(clippy::too_many_arguments)]
2571 pub fn replace(
2572 &self,
2573 year: Option<i64>,
2574 month: Option<i64>,
2575 day: Option<i64>,
2576 hour: Option<i64>,
2577 minute: Option<i64>,
2578 second: Option<i64>,
2579 microsecond: Option<i64>,
2580 nanosecond: Option<i64>,
2581 ) -> Self {
2582 if self.is_nat() {
2583 return self.clone();
2584 }
2585 let cur_year = self.year().unwrap_or(1970);
2586 let cur_month = self.month().unwrap_or(1);
2587 let cur_day = self.day().unwrap_or(1);
2588 let cur_hour = self.hour().unwrap_or(0);
2589 let cur_minute = self.minute().unwrap_or(0);
2590 let cur_second = self.second().unwrap_or(0);
2591 let cur_micro = self.microsecond().unwrap_or(0);
2592 let cur_nano = self.nanosecond().unwrap_or(0);
2593
2594 let y = year.unwrap_or(cur_year);
2595 let mo = month.unwrap_or(cur_month);
2596 let d = day.unwrap_or(cur_day);
2597 let h = hour.unwrap_or(cur_hour);
2598 let mi = minute.unwrap_or(cur_minute);
2599 let s = second.unwrap_or(cur_second);
2600 let us = microsecond.unwrap_or(cur_micro);
2601 let ns = nanosecond.unwrap_or(cur_nano);
2602
2603 let days_from_epoch = Self::days_from_ymd(y, mo, d);
2604 let secs = h * 3600 + mi * 60 + s;
2605 let total_nanos = days_from_epoch * Timedelta::NANOS_PER_DAY
2606 + secs * Timedelta::NANOS_PER_SEC
2607 + us * Timedelta::NANOS_PER_MICRO
2608 + ns;
2609
2610 Self {
2611 nanos: total_nanos,
2612 tz: self.tz.clone(),
2613 }
2614 }
2615
2616 fn days_from_ymd(year: i64, month: i64, day: i64) -> i64 {
2617 let y = if month <= 2 { year - 1 } else { year };
2618 let era = if y >= 0 { y } else { y - 399 } / 400;
2619 let yoe = y - era * 400;
2620 let doy = (153 * (if month > 2 { month - 3 } else { month + 9 }) + 2) / 5 + day - 1;
2621 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
2622 era * 146097 + doe - 719468
2623 }
2624
2625 #[must_use]
2629 pub fn isoformat(&self) -> String {
2630 if self.is_nat() {
2631 return "NaT".to_string();
2632 }
2633 let total_secs = self.nanos / Timedelta::NANOS_PER_SEC;
2634 let sub_nanos = (self.nanos % Timedelta::NANOS_PER_SEC).unsigned_abs();
2635 let days_since_epoch = total_secs / 86400;
2636 let secs_of_day = (total_secs % 86400 + 86400) % 86400;
2637
2638 let days = days_since_epoch + 719_468;
2639 let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
2640 let doe = days - era * 146_097;
2641 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
2642 let y = yoe + era * 400;
2643 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
2644 let mp = (5 * doy + 2) / 153;
2645 let d = doy - (153 * mp + 2) / 5 + 1;
2646 let m = if mp < 10 { mp + 3 } else { mp - 9 };
2647 let year = if m <= 2 { y + 1 } else { y };
2648
2649 let hour = secs_of_day / 3600;
2650 let minute = (secs_of_day % 3600) / 60;
2651 let second = secs_of_day % 60;
2652
2653 let base = if sub_nanos == 0 {
2654 format!("{year:04}-{m:02}-{d:02}T{hour:02}:{minute:02}:{second:02}")
2655 } else {
2656 let micros = sub_nanos / 1000;
2657 format!("{year:04}-{m:02}-{d:02}T{hour:02}:{minute:02}:{second:02}.{micros:06}")
2658 };
2659 match &self.tz {
2660 Some(tz) if tz == "UTC" => format!("{base}+00:00"),
2661 Some(tz) => format!("{base}[{tz}]"),
2662 None => base,
2663 }
2664 }
2665
2666 #[must_use]
2668 pub fn to_iso8601(&self) -> String {
2669 self.isoformat()
2670 }
2671
2672 pub fn parse(s: &str) -> Result<Self, TypeError> {
2685 let s = s.trim();
2686
2687 if s.eq_ignore_ascii_case("nat") {
2688 return Ok(Self::nat());
2689 }
2690
2691 let (datetime_part, tz) = Self::split_timezone(s);
2692
2693 let (date_part, time_part) = if datetime_part.contains('T') {
2694 datetime_part
2695 .split_once('T')
2696 .ok_or_else(|| TypeError::ValueNotParseable {
2697 value: s.to_string(),
2698 target: "Timestamp".to_string(),
2699 })?
2700 } else if datetime_part.contains(' ')
2701 && datetime_part.chars().filter(|&c| c == ' ').count() == 1
2702 {
2703 datetime_part
2704 .split_once(' ')
2705 .ok_or_else(|| TypeError::ValueNotParseable {
2706 value: s.to_string(),
2707 target: "Timestamp".to_string(),
2708 })?
2709 } else {
2710 (datetime_part, "00:00:00")
2711 };
2712
2713 let (year, month, day) =
2714 Self::parse_date(date_part).ok_or_else(|| TypeError::ValueNotParseable {
2715 value: s.to_string(),
2716 target: "Timestamp".to_string(),
2717 })?;
2718
2719 let (hour, minute, second, nanos) =
2720 Self::parse_time(time_part).ok_or_else(|| TypeError::ValueNotParseable {
2721 value: s.to_string(),
2722 target: "Timestamp".to_string(),
2723 })?;
2724
2725 let total_nanos = Self::ymd_hms_to_nanos(year, month, day, hour, minute, second, nanos);
2726
2727 Ok(if let Some(tz_name) = tz {
2728 Self::from_nanos_tz(total_nanos, tz_name)
2729 } else {
2730 Self::from_nanos(total_nanos)
2731 })
2732 }
2733
2734 fn split_timezone(s: &str) -> (&str, Option<String>) {
2735 if let Some(stripped) = s.strip_suffix('Z') {
2736 (stripped, Some("UTC".to_string()))
2737 } else if let Some(idx) = s.rfind('+') {
2738 if idx > 10 {
2739 (&s[..idx], Some(s[idx..].to_string()))
2740 } else {
2741 (s, None)
2742 }
2743 } else if let Some(idx) = s.rfind('-') {
2744 if idx > 10 && s[idx..].contains(':') {
2745 (&s[..idx], Some(s[idx..].to_string()))
2746 } else {
2747 (s, None)
2748 }
2749 } else {
2750 (s, None)
2751 }
2752 }
2753
2754 fn parse_date(s: &str) -> Option<(i64, u32, u32)> {
2755 let parts: Vec<&str> = s.split('-').collect();
2756 if parts.len() != 3 {
2757 return None;
2758 }
2759 let year: i64 = parts[0].parse().ok()?;
2760 let month: u32 = parts[1].parse().ok()?;
2761 let day: u32 = parts[2].parse().ok()?;
2762 if !(1..=days_in_month(year, month)?).contains(&day) {
2763 return None;
2764 }
2765 Some((year, month, day))
2766 }
2767
2768 fn parse_time(s: &str) -> Option<(u32, u32, u32, u64)> {
2769 let (time_str, frac_str) = s.split_once('.').unwrap_or((s, ""));
2770 let parts: Vec<&str> = time_str.split(':').collect();
2771 if parts.is_empty() || parts.len() > 3 {
2772 return None;
2773 }
2774 let hour: u32 = parts.first().and_then(|p| p.parse().ok())?;
2775 let minute: u32 = parts.get(1).and_then(|p| p.parse().ok()).unwrap_or(0);
2776 let second: u32 = parts.get(2).and_then(|p| p.parse().ok()).unwrap_or(0);
2777
2778 if hour > 23 || minute > 59 || second > 59 {
2779 return None;
2780 }
2781
2782 let nanos = if frac_str.is_empty() {
2783 0
2784 } else {
2785 let padded = format!("{:0<9}", &frac_str[..frac_str.len().min(9)]);
2786 padded.parse::<u64>().unwrap_or(0)
2787 };
2788
2789 Some((hour, minute, second, nanos))
2790 }
2791
2792 fn ymd_hms_to_nanos(
2793 year: i64,
2794 month: u32,
2795 day: u32,
2796 hour: u32,
2797 minute: u32,
2798 second: u32,
2799 sub_nanos: u64,
2800 ) -> i64 {
2801 let m = month as i64;
2802 let d = day as i64;
2803
2804 let y = if m <= 2 { year - 1 } else { year };
2805 let era = if y >= 0 { y } else { y - 399 } / 400;
2806 let yoe = y - era * 400;
2807 let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1;
2808 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
2809 let days_since_epoch = era * 146_097 + doe - 719_468;
2810
2811 let total_seconds = days_since_epoch * 86400
2812 + (hour as i64) * 3600
2813 + (minute as i64) * 60
2814 + (second as i64);
2815 total_seconds * Timedelta::NANOS_PER_SEC + sub_nanos as i64
2816 }
2817
2818 #[must_use]
2824 pub fn strftime(&self, format: &str) -> String {
2825 if self.is_nat() {
2826 return "NaT".to_string();
2827 }
2828 let total_secs = self.nanos / Timedelta::NANOS_PER_SEC;
2829 let sub_nanos = (self.nanos % Timedelta::NANOS_PER_SEC).unsigned_abs();
2830
2831 let days_since_epoch = total_secs / 86400;
2832 let secs_of_day = (total_secs % 86400 + 86400) % 86400;
2833
2834 let days = days_since_epoch + 719_468;
2835 let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
2836 let doe = days - era * 146_097;
2837 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
2838 let y = yoe + era * 400;
2839 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
2840 let mp = (5 * doy + 2) / 153;
2841 let d = doy - (153 * mp + 2) / 5 + 1;
2842 let m = if mp < 10 { mp + 3 } else { mp - 9 };
2843 let year = if m <= 2 { y + 1 } else { y };
2844
2845 let hour = secs_of_day / 3600;
2846 let minute = (secs_of_day % 3600) / 60;
2847 let second = secs_of_day % 60;
2848 let micros = sub_nanos / 1000;
2849
2850 format
2851 .replace("%Y", &format!("{year:04}"))
2852 .replace("%m", &format!("{m:02}"))
2853 .replace("%d", &format!("{d:02}"))
2854 .replace("%H", &format!("{hour:02}"))
2855 .replace("%M", &format!("{minute:02}"))
2856 .replace("%S", &format!("{second:02}"))
2857 .replace("%f", &format!("{micros:06}"))
2858 }
2859
2860 #[must_use]
2864 pub fn day_name(&self) -> String {
2865 const NAMES: [&str; 7] = [
2866 "Thursday",
2867 "Friday",
2868 "Saturday",
2869 "Sunday",
2870 "Monday",
2871 "Tuesday",
2872 "Wednesday",
2873 ];
2874 if self.is_nat() {
2875 return "NaT".to_string();
2876 }
2877 let days_since_epoch = self.nanos / Timedelta::NANOS_PER_DAY;
2878 let dow = ((days_since_epoch % 7) + 7) % 7;
2879 NAMES[dow as usize].to_string()
2880 }
2881
2882 #[must_use]
2886 pub fn month_name(&self) -> String {
2887 const NAMES: [&str; 12] = [
2888 "January",
2889 "February",
2890 "March",
2891 "April",
2892 "May",
2893 "June",
2894 "July",
2895 "August",
2896 "September",
2897 "October",
2898 "November",
2899 "December",
2900 ];
2901 if self.is_nat() {
2902 return "NaT".to_string();
2903 }
2904 let total_secs = self.nanos / Timedelta::NANOS_PER_SEC;
2905 let days_since_epoch = total_secs / 86400;
2906 let days = days_since_epoch + 719_468;
2907 let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
2908 let doe = days - era * 146_097;
2909 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
2910 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
2911 let mp = (5 * doy + 2) / 153;
2912 let m = if mp < 10 { mp + 3 } else { mp - 9 };
2913 NAMES[(m - 1) as usize].to_string()
2914 }
2915
2916 #[must_use]
2921 pub fn tz_localize(&self, tz: Option<&str>) -> Self {
2922 if self.is_nat() {
2923 return Self::nat();
2924 }
2925 Self {
2926 nanos: self.nanos,
2927 tz: tz.map(String::from),
2928 }
2929 }
2930
2931 #[must_use]
2937 pub fn tz_convert(&self, tz: &str) -> Self {
2938 if self.is_nat() {
2939 return Self::nat();
2940 }
2941 Self {
2942 nanos: self.nanos,
2943 tz: Some(tz.to_string()),
2944 }
2945 }
2946
2947 #[must_use]
2952 pub fn fromtimestamp(ts: f64, tz: Option<&str>) -> Self {
2953 if ts.is_nan() || ts.is_infinite() {
2954 return Self::nat();
2955 }
2956 let nanos_f64 = ts * 1_000_000_000.0;
2957 const MAX_NANOS: f64 = i64::MAX as f64;
2959 const MIN_NANOS: f64 = i64::MIN as f64;
2960 if !(MIN_NANOS..=MAX_NANOS).contains(&nanos_f64) {
2961 return Self::nat();
2962 }
2963 Self {
2964 nanos: nanos_f64 as i64,
2965 tz: tz.map(String::from),
2966 }
2967 }
2968
2969 #[must_use]
2973 pub fn from_millis(ms: i64, tz: Option<&str>) -> Self {
2974 Self {
2975 nanos: ms.saturating_mul(1_000_000),
2976 tz: tz.map(String::from),
2977 }
2978 }
2979
2980 #[must_use]
2984 pub fn from_micros(us: i64, tz: Option<&str>) -> Self {
2985 Self {
2986 nanos: us.saturating_mul(1_000),
2987 tz: tz.map(String::from),
2988 }
2989 }
2990
2991 #[must_use]
2993 pub fn tzinfo(&self) -> Option<&str> {
2994 self.tz.as_deref()
2995 }
2996
2997 #[must_use]
3001 pub fn tzname(&self) -> Option<&str> {
3002 self.tzinfo()
3003 }
3004}
3005
3006impl std::fmt::Display for Timestamp {
3007 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3010 if self.is_nat() {
3011 return f.write_str("NaT");
3012 }
3013 match &self.tz {
3014 Some(tz) => write!(f, "Timestamp[{}, {}]", self.nanos, tz),
3015 None => write!(f, "Timestamp[{}, UTC]", self.nanos),
3016 }
3017 }
3018}
3019
3020impl PartialOrd for Timestamp {
3021 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
3026 if self.is_nat() || other.is_nat() {
3027 return None;
3028 }
3029 Some(self.nanos.cmp(&other.nanos))
3030 }
3031}
3032
3033pub fn isna(values: &[Scalar]) -> Vec<bool> {
3036 values.iter().map(Scalar::is_missing).collect()
3037}
3038
3039pub fn isnull(values: &[Scalar]) -> Vec<bool> {
3040 isna(values)
3041}
3042
3043pub fn notna(values: &[Scalar]) -> Vec<bool> {
3044 values.iter().map(|v| !v.is_missing()).collect()
3045}
3046
3047pub fn notnull(values: &[Scalar]) -> Vec<bool> {
3048 notna(values)
3049}
3050
3051pub fn count_na(values: &[Scalar]) -> usize {
3052 values.iter().filter(|v| v.is_missing()).count()
3053}
3054
3055pub fn fill_na(values: &[Scalar], fill: &Scalar) -> Vec<Scalar> {
3056 values
3057 .iter()
3058 .map(|v| {
3059 if v.is_missing() {
3060 fill.clone()
3061 } else {
3062 v.clone()
3063 }
3064 })
3065 .collect()
3066}
3067
3068pub fn dropna(values: &[Scalar]) -> Vec<Scalar> {
3069 values.iter().filter(|v| !v.is_missing()).cloned().collect()
3070}
3071
3072fn collect_finite(values: &[Scalar]) -> Vec<f64> {
3075 values
3076 .iter()
3077 .filter(|v| !v.is_missing())
3078 .filter_map(|v| v.to_f64().ok())
3079 .collect()
3080}
3081
3082fn collect_timedelta_ns(values: &[Scalar]) -> Option<(i128, usize)> {
3088 let mut sum: i128 = 0;
3089 let mut count: usize = 0;
3090 let mut saw_timedelta = false;
3091 for v in values {
3092 if v.is_missing() {
3093 continue;
3094 }
3095 match v {
3096 Scalar::Timedelta64(ns) => {
3097 saw_timedelta = true;
3098 sum += i128::from(*ns);
3099 count += 1;
3100 }
3101 _ => return None,
3104 }
3105 }
3106 if saw_timedelta {
3107 Some((sum, count))
3108 } else {
3109 None
3110 }
3111}
3112
3113pub fn nansum(values: &[Scalar]) -> Scalar {
3114 if let Some((sum, _)) = collect_timedelta_ns(values) {
3115 let clamped = sum.clamp(i128::from(i64::MIN), i128::from(i64::MAX));
3116 return Scalar::Timedelta64(clamped as i64);
3117 }
3118 let mut sum = 0.0_f64;
3123 for v in values {
3124 if v.is_missing() {
3125 continue;
3126 }
3127 if let Ok(x) = v.to_f64() {
3128 sum += x;
3129 }
3130 }
3131 Scalar::Float64(sum)
3132}
3133
3134pub fn nanmean(values: &[Scalar]) -> Scalar {
3135 if let Some((sum, count)) = collect_timedelta_ns(values) {
3136 if count == 0 {
3137 return Scalar::Timedelta64(Timedelta::NAT);
3138 }
3139 let mean = sum / count as i128;
3140 let clamped = mean.clamp(i128::from(i64::MIN), i128::from(i64::MAX));
3141 return Scalar::Timedelta64(clamped as i64);
3142 }
3143 let mut sum = 0.0_f64;
3147 let mut count = 0usize;
3148 for v in values {
3149 if v.is_missing() {
3150 continue;
3151 }
3152 if let Ok(x) = v.to_f64() {
3153 sum += x;
3154 count += 1;
3155 }
3156 }
3157 if count == 0 {
3158 return Scalar::Null(NullKind::NaN);
3159 }
3160 Scalar::Float64(sum / count as f64)
3161}
3162
3163pub fn nanany(values: &[Scalar]) -> Scalar {
3164 for v in values {
3165 if v.is_missing() {
3166 continue;
3167 }
3168 match v {
3169 Scalar::Bool(flag) if *flag => return Scalar::Bool(true),
3170 Scalar::Int64(val) if *val != 0 => return Scalar::Bool(true),
3171 Scalar::Float64(val) if !val.is_nan() && *val != 0.0 => return Scalar::Bool(true),
3172 Scalar::Utf8(val) if !val.is_empty() => return Scalar::Bool(true),
3173 Scalar::Timedelta64(ns) if *ns != 0 => return Scalar::Bool(true),
3176 _ => continue,
3177 }
3178 }
3179 Scalar::Bool(false)
3180}
3181
3182pub fn nanall(values: &[Scalar]) -> Scalar {
3183 for v in values {
3184 if v.is_missing() {
3185 continue;
3186 }
3187 match v {
3188 Scalar::Bool(flag) if !*flag => return Scalar::Bool(false),
3189 Scalar::Int64(val) if *val == 0 => return Scalar::Bool(false),
3190 Scalar::Float64(val) if val.is_nan() || *val == 0.0 => return Scalar::Bool(false),
3191 Scalar::Utf8(val) if val.is_empty() => return Scalar::Bool(false),
3192 Scalar::Timedelta64(ns) if *ns == 0 => return Scalar::Bool(false),
3195 _ => continue,
3196 }
3197 }
3198 Scalar::Bool(true)
3199}
3200
3201pub fn nancount(values: &[Scalar]) -> Scalar {
3202 let n = values.iter().filter(|v| !v.is_missing()).count();
3203 Scalar::Int64(n as i64)
3204}
3205
3206pub fn nanmin(values: &[Scalar]) -> Scalar {
3207 let mut min: Option<&Scalar> = None;
3208 for v in values {
3209 if v.is_missing() {
3210 continue;
3211 }
3212 match (min, v) {
3213 (None, _) => min = Some(v),
3214 (Some(Scalar::Int64(a)), Scalar::Int64(b)) => {
3215 if b < a {
3216 min = Some(v)
3217 }
3218 }
3219 (Some(Scalar::Float64(a)), Scalar::Float64(b)) => {
3220 if *b < *a {
3221 min = Some(v)
3222 }
3223 }
3224 (Some(Scalar::Utf8(a)), Scalar::Utf8(b)) => {
3225 if b < a {
3226 min = Some(v)
3227 }
3228 }
3229 (Some(Scalar::Bool(a)), Scalar::Bool(b)) => {
3230 if b < a {
3231 min = Some(v)
3232 }
3233 }
3234 (Some(Scalar::Timedelta64(a)), Scalar::Timedelta64(b)) => {
3239 if b < a {
3240 min = Some(v)
3241 }
3242 }
3243 (Some(a), b) => match (a.to_f64(), b.to_f64()) {
3244 (Ok(af), Ok(bf)) if bf < af => min = Some(v),
3245 (Ok(_), Ok(_)) => {}
3246 _ => return Scalar::Null(NullKind::NaN),
3247 },
3248 }
3249 }
3250 match min {
3251 Some(v) => v.clone(),
3252 None => Scalar::Null(NullKind::NaN),
3253 }
3254}
3255
3256pub fn nanmax(values: &[Scalar]) -> Scalar {
3257 let mut max: Option<&Scalar> = None;
3258 for v in values {
3259 if v.is_missing() {
3260 continue;
3261 }
3262 match (max, v) {
3263 (None, _) => max = Some(v),
3264 (Some(Scalar::Int64(a)), Scalar::Int64(b)) => {
3265 if b > a {
3266 max = Some(v)
3267 }
3268 }
3269 (Some(Scalar::Float64(a)), Scalar::Float64(b)) => {
3270 if *b > *a {
3271 max = Some(v)
3272 }
3273 }
3274 (Some(Scalar::Utf8(a)), Scalar::Utf8(b)) => {
3275 if b > a {
3276 max = Some(v)
3277 }
3278 }
3279 (Some(Scalar::Bool(a)), Scalar::Bool(b)) => {
3280 if b > a {
3281 max = Some(v)
3282 }
3283 }
3284 (Some(Scalar::Timedelta64(a)), Scalar::Timedelta64(b)) => {
3288 if b > a {
3289 max = Some(v)
3290 }
3291 }
3292 (Some(a), b) => match (a.to_f64(), b.to_f64()) {
3293 (Ok(af), Ok(bf)) if bf > af => max = Some(v),
3294 (Ok(_), Ok(_)) => {}
3295 _ => return Scalar::Null(NullKind::NaN),
3296 },
3297 }
3298 }
3299 match max {
3300 Some(v) => v.clone(),
3301 None => Scalar::Null(NullKind::NaN),
3302 }
3303}
3304
3305fn collect_timedelta_ns_f64(values: &[Scalar]) -> Option<Vec<f64>> {
3311 let mut out = Vec::with_capacity(values.len());
3312 let mut saw_td = false;
3313 for v in values {
3314 if v.is_missing() {
3315 continue;
3316 }
3317 match v {
3318 Scalar::Timedelta64(ns) => {
3319 saw_td = true;
3320 out.push(*ns as f64);
3321 }
3322 _ => return None,
3323 }
3324 }
3325 if saw_td { Some(out) } else { None }
3326}
3327
3328fn float_ns_to_timedelta(value: f64) -> Scalar {
3330 if !value.is_finite() {
3331 return Scalar::Timedelta64(Timedelta::NAT);
3332 }
3333 let clamped = value.clamp(i64::MIN as f64, i64::MAX as f64);
3334 Scalar::Timedelta64(clamped as i64)
3335}
3336
3337pub fn nanmedian(values: &[Scalar]) -> Scalar {
3338 if let Some(mut td) = collect_timedelta_ns_f64(values) {
3340 if td.is_empty() {
3341 return Scalar::Timedelta64(Timedelta::NAT);
3342 }
3343 let n = td.len();
3348 let mid = n / 2;
3349 let cmp = |a: &f64, b: &f64| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal);
3350 let (left, mid_ref, _right) = td.select_nth_unstable_by(mid, cmp);
3351 let mid_val = *mid_ref;
3352 let median_ns = if n.is_multiple_of(2) {
3353 let lower = left.iter().copied().fold(f64::NEG_INFINITY, f64::max);
3354 (lower + mid_val) / 2.0
3355 } else {
3356 mid_val
3357 };
3358 return float_ns_to_timedelta(median_ns);
3359 }
3360 let mut nums = collect_finite(values);
3361 if nums.is_empty() {
3362 return Scalar::Null(NullKind::NaN);
3363 }
3364 let n = nums.len();
3371 let mid = n / 2;
3372 let cmp = |a: &f64, b: &f64| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal);
3373 let (left, mid_ref, _right) = nums.select_nth_unstable_by(mid, cmp);
3374 let mid_val = *mid_ref;
3375 if n.is_multiple_of(2) {
3376 let lower = left.iter().copied().fold(f64::NEG_INFINITY, f64::max);
3377 Scalar::Float64((lower + mid_val) / 2.0)
3378 } else {
3379 Scalar::Float64(mid_val)
3380 }
3381}
3382
3383pub fn nanvar(values: &[Scalar], ddof: usize) -> Scalar {
3384 if let Some(td) = collect_timedelta_ns_f64(values) {
3387 if td.len() <= ddof {
3388 return Scalar::Timedelta64(Timedelta::NAT);
3389 }
3390 let mean: f64 = td.iter().sum::<f64>() / td.len() as f64;
3391 let sum_sq: f64 = td.iter().map(|x| (x - mean).powi(2)).sum();
3392 return float_ns_to_timedelta(sum_sq / (td.len() - ddof) as f64);
3393 }
3394 let nums = collect_finite(values);
3395 if nums.len() <= ddof {
3396 return Scalar::Null(NullKind::NaN);
3397 }
3398 let mean: f64 = nums.iter().sum::<f64>() / nums.len() as f64;
3399 let sum_sq: f64 = nums.iter().map(|x| (x - mean).powi(2)).sum();
3400 Scalar::Float64(sum_sq / (nums.len() - ddof) as f64)
3401}
3402
3403pub fn nanstd(values: &[Scalar], ddof: usize) -> Scalar {
3404 if let Some(td) = collect_timedelta_ns_f64(values) {
3406 if td.len() <= ddof {
3407 return Scalar::Timedelta64(Timedelta::NAT);
3408 }
3409 let mean: f64 = td.iter().sum::<f64>() / td.len() as f64;
3410 let sum_sq: f64 = td.iter().map(|x| (x - mean).powi(2)).sum();
3411 let var = sum_sq / (td.len() - ddof) as f64;
3412 return float_ns_to_timedelta(var.sqrt());
3413 }
3414 match nanvar(values, ddof) {
3415 Scalar::Float64(v) => Scalar::Float64(v.sqrt()),
3416 other => other,
3417 }
3418}
3419
3420pub fn nansem(values: &[Scalar], ddof: usize) -> Scalar {
3426 if let Some(td) = collect_timedelta_ns_f64(values) {
3428 if td.len() <= ddof {
3429 return Scalar::Timedelta64(Timedelta::NAT);
3430 }
3431 let mean: f64 = td.iter().sum::<f64>() / td.len() as f64;
3432 let sum_sq: f64 = td.iter().map(|x| (x - mean).powi(2)).sum();
3433 let var = sum_sq / (td.len() - ddof) as f64;
3434 let std = var.sqrt();
3435 return float_ns_to_timedelta(std / (td.len() as f64).sqrt());
3436 }
3437 let nums = collect_finite(values);
3438 if nums.len() <= ddof {
3439 return Scalar::Null(NullKind::NaN);
3440 }
3441 match nanstd(values, ddof) {
3442 Scalar::Float64(s) => Scalar::Float64(s / (nums.len() as f64).sqrt()),
3443 other => other,
3444 }
3445}
3446
3447pub fn nanptp(values: &[Scalar]) -> Scalar {
3452 if let Some(td) = collect_timedelta_ns_f64(values) {
3456 if td.is_empty() {
3457 return Scalar::Timedelta64(Timedelta::NAT);
3458 }
3459 let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
3460 for x in &td {
3461 if *x < lo {
3462 lo = *x;
3463 }
3464 if *x > hi {
3465 hi = *x;
3466 }
3467 }
3468 return float_ns_to_timedelta(hi - lo);
3469 }
3470 let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
3475 let mut seen = false;
3476 for v in values {
3477 if v.is_missing() {
3478 continue;
3479 }
3480 if let Ok(x) = v.to_f64() {
3481 seen = true;
3482 if x < lo {
3483 lo = x;
3484 }
3485 if x > hi {
3486 hi = x;
3487 }
3488 }
3489 }
3490 if !seen {
3491 return Scalar::Null(NullKind::NaN);
3492 }
3493 Scalar::Float64(hi - lo)
3494}
3495
3496pub fn nanskew(values: &[Scalar]) -> Scalar {
3502 let nums = collect_finite(values);
3503 let n = nums.len() as f64;
3504 if n < 3.0 {
3505 return Scalar::Null(NullKind::NaN);
3506 }
3507 let mean = nums.iter().sum::<f64>() / n;
3508 let m2: f64 = nums.iter().map(|x| (x - mean).powi(2)).sum();
3509 let m3: f64 = nums.iter().map(|x| (x - mean).powi(3)).sum();
3510 let s2 = m2 / (n - 1.0);
3511 if s2 == 0.0 {
3512 return Scalar::Float64(0.0);
3513 }
3514 let s3 = s2.powf(1.5);
3515 Scalar::Float64((n / ((n - 1.0) * (n - 2.0))) * (m3 / s3))
3516}
3517
3518pub fn nankurt(values: &[Scalar]) -> Scalar {
3525 let nums = collect_finite(values);
3526 let n = nums.len() as f64;
3527 if n < 4.0 {
3528 return Scalar::Null(NullKind::NaN);
3529 }
3530 let mean = nums.iter().sum::<f64>() / n;
3531 let m2: f64 = nums.iter().map(|x| (x - mean).powi(2)).sum();
3532 let m4: f64 = nums.iter().map(|x| (x - mean).powi(4)).sum();
3533 let s2 = m2 / (n - 1.0);
3534 if s2 == 0.0 {
3535 return Scalar::Float64(0.0);
3536 }
3537 let adj = (n * (n + 1.0)) / ((n - 1.0) * (n - 2.0) * (n - 3.0));
3538 let sub = (3.0 * (n - 1.0).powi(2)) / ((n - 2.0) * (n - 3.0));
3539 Scalar::Float64(adj * (m4 / (s2 * s2)) - sub)
3540}
3541
3542pub fn nanprod(values: &[Scalar]) -> Scalar {
3544 if is_timedelta_input(values) {
3551 return Scalar::Null(NullKind::NaN);
3552 }
3553 let mut prod = 1.0_f64;
3558 for v in values {
3559 if v.is_missing() {
3560 continue;
3561 }
3562 if let Ok(x) = v.to_f64() {
3563 prod *= x;
3564 }
3565 }
3566 Scalar::Float64(prod)
3567}
3568
3569fn is_timedelta_input(values: &[Scalar]) -> bool {
3575 let mut saw_td = false;
3576 for v in values {
3577 if v.is_missing() {
3578 continue;
3579 }
3580 match v {
3581 Scalar::Timedelta64(_) => saw_td = true,
3582 _ => return false,
3583 }
3584 }
3585 saw_td
3586}
3587
3588fn timedelta_cumulative<F>(values: &[Scalar], init: i128, mut step: F) -> Vec<Scalar>
3593where
3594 F: FnMut(i128, i128) -> i128,
3595{
3596 let mut out = Vec::with_capacity(values.len());
3597 let mut running: i128 = init;
3598 for v in values {
3599 if v.is_missing() {
3600 out.push(Scalar::Null(NullKind::NaT));
3601 continue;
3602 }
3603 if let Scalar::Timedelta64(ns) = v {
3604 running = step(running, i128::from(*ns));
3605 let clamped = running.clamp(i128::from(i64::MIN), i128::from(i64::MAX));
3606 out.push(Scalar::Timedelta64(clamped as i64));
3607 } else {
3608 out.push(Scalar::Null(NullKind::NaT));
3609 }
3610 }
3611 out
3612}
3613
3614fn timedelta_cumulative_extrema<F>(values: &[Scalar], sentinel: i64, mut step: F) -> Vec<Scalar>
3619where
3620 F: FnMut(i64, i64) -> i64,
3621{
3622 let mut out = Vec::with_capacity(values.len());
3623 let mut running: Option<i64> = None;
3624 for v in values {
3625 if v.is_missing() {
3626 out.push(Scalar::Null(NullKind::NaT));
3627 continue;
3628 }
3629 if let Scalar::Timedelta64(ns) = v {
3630 let new_val = match running {
3631 Some(prev) => step(prev, *ns),
3632 None => *ns,
3633 };
3634 running = Some(new_val);
3635 out.push(Scalar::Timedelta64(new_val));
3636 } else {
3637 out.push(Scalar::Null(NullKind::NaT));
3638 }
3639 }
3640 let _ = sentinel; out
3642}
3643
3644pub fn nancumsum(values: &[Scalar]) -> Vec<Scalar> {
3648 if is_timedelta_input(values) {
3652 return timedelta_cumulative(values, 0_i128, |acc, x| acc.saturating_add(x));
3653 }
3654 let mut out = Vec::with_capacity(values.len());
3655 let mut running = 0.0_f64;
3656 for v in values {
3657 if v.is_missing() {
3658 out.push(Scalar::Null(NullKind::NaN));
3659 continue;
3660 }
3661 match v.to_f64() {
3662 Ok(x) if !x.is_nan() => {
3663 running += x;
3664 out.push(Scalar::Float64(running));
3665 }
3666 _ => out.push(Scalar::Null(NullKind::NaN)),
3667 }
3668 }
3669 out
3670}
3671
3672pub fn nancumprod(values: &[Scalar]) -> Vec<Scalar> {
3677 let mut out = Vec::with_capacity(values.len());
3678 let mut running = 1.0_f64;
3679 for v in values {
3680 if v.is_missing() {
3681 out.push(Scalar::Null(NullKind::NaN));
3682 continue;
3683 }
3684 match v.to_f64() {
3685 Ok(x) if !x.is_nan() => {
3686 running *= x;
3687 out.push(Scalar::Float64(running));
3688 }
3689 _ => out.push(Scalar::Null(NullKind::NaN)),
3690 }
3691 }
3692 out
3693}
3694
3695pub fn nancummax(values: &[Scalar]) -> Vec<Scalar> {
3701 if is_timedelta_input(values) {
3703 return timedelta_cumulative_extrema(values, i64::MAX, |acc, x| acc.max(x));
3704 }
3705 let mut out = Vec::with_capacity(values.len());
3706 let mut running: Option<f64> = None;
3707 for v in values {
3708 if v.is_missing() {
3709 out.push(Scalar::Null(NullKind::NaN));
3710 continue;
3711 }
3712 match v.to_f64() {
3713 Ok(x) if !x.is_nan() => {
3714 let new_val = match running {
3715 Some(prev) => prev.max(x),
3716 None => x,
3717 };
3718 running = Some(new_val);
3719 out.push(Scalar::Float64(new_val));
3720 }
3721 _ => out.push(Scalar::Null(NullKind::NaN)),
3722 }
3723 }
3724 out
3725}
3726
3727pub fn nancummin(values: &[Scalar]) -> Vec<Scalar> {
3731 if is_timedelta_input(values) {
3733 return timedelta_cumulative_extrema(values, i64::MIN, |acc, x| acc.min(x));
3734 }
3735 let mut out = Vec::with_capacity(values.len());
3736 let mut running: Option<f64> = None;
3737 for v in values {
3738 if v.is_missing() {
3739 out.push(Scalar::Null(NullKind::NaN));
3740 continue;
3741 }
3742 match v.to_f64() {
3743 Ok(x) if !x.is_nan() => {
3744 let new_val = match running {
3745 Some(prev) => prev.min(x),
3746 None => x,
3747 };
3748 running = Some(new_val);
3749 out.push(Scalar::Float64(new_val));
3750 }
3751 _ => out.push(Scalar::Null(NullKind::NaN)),
3752 }
3753 }
3754 out
3755}
3756
3757pub fn nanquantile(values: &[Scalar], q: f64) -> Scalar {
3763 if !(0.0..=1.0).contains(&q) {
3764 return Scalar::Null(NullKind::NaN);
3765 }
3766 if let Some(mut td) = collect_timedelta_ns_f64(values) {
3769 if td.is_empty() {
3770 return Scalar::Timedelta64(Timedelta::NAT);
3771 }
3772 let n = td.len();
3773 if n == 1 {
3774 return float_ns_to_timedelta(td[0]);
3775 }
3776 let pos = q * (n - 1) as f64;
3777 let lo = pos.floor() as usize;
3778 let hi = pos.ceil() as usize;
3779 let cmp = |a: &f64, b: &f64| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal);
3783 let (_left, lo_ref, right) = td.select_nth_unstable_by(lo, cmp);
3784 let lo_val = *lo_ref;
3785 let ns = if lo == hi {
3786 lo_val
3787 } else {
3788 let hi_val = right.iter().copied().fold(f64::INFINITY, f64::min);
3789 let weight = pos - lo as f64;
3790 lo_val + (hi_val - lo_val) * weight
3791 };
3792 return float_ns_to_timedelta(ns);
3793 }
3794 let mut nums = collect_finite(values);
3795 if nums.is_empty() {
3796 return Scalar::Null(NullKind::NaN);
3797 }
3798 let n = nums.len();
3799 if n == 1 {
3800 return Scalar::Float64(nums[0]);
3801 }
3802 let pos = q * (n - 1) as f64;
3803 let lo = pos.floor() as usize;
3804 let hi = pos.ceil() as usize;
3805 let cmp = |a: &f64, b: &f64| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal);
3810 let (_left, lo_ref, right) = nums.select_nth_unstable_by(lo, cmp);
3811 let lo_val = *lo_ref;
3812 if lo == hi {
3813 return Scalar::Float64(lo_val);
3814 }
3815 let hi_val = right.iter().copied().fold(f64::INFINITY, f64::min);
3816 let weight = pos - lo as f64;
3817 Scalar::Float64(lo_val + (hi_val - lo_val) * weight)
3818}
3819
3820pub fn nanargmax(values: &[Scalar]) -> Option<usize> {
3825 if is_timedelta_input(values) {
3830 let mut best: Option<(usize, i64)> = None;
3831 for (i, v) in values.iter().enumerate() {
3832 if v.is_missing() {
3833 continue;
3834 }
3835 if let Scalar::Timedelta64(ns) = v {
3836 match best {
3837 None => best = Some((i, *ns)),
3838 Some((_, cur)) if *ns > cur => best = Some((i, *ns)),
3839 _ => {}
3840 }
3841 }
3842 }
3843 return best.map(|(i, _)| i);
3844 }
3845 let mut best: Option<(usize, f64)> = None;
3846 for (i, v) in values.iter().enumerate() {
3847 if v.is_missing() {
3848 continue;
3849 }
3850 if let Ok(x) = v.to_f64() {
3851 if x.is_nan() {
3852 continue;
3853 }
3854 match best {
3855 None => best = Some((i, x)),
3856 Some((_, cur)) if x > cur => best = Some((i, x)),
3857 _ => {}
3858 }
3859 }
3860 }
3861 best.map(|(i, _)| i)
3862}
3863
3864pub fn nanargmin(values: &[Scalar]) -> Option<usize> {
3868 if is_timedelta_input(values) {
3870 let mut best: Option<(usize, i64)> = None;
3871 for (i, v) in values.iter().enumerate() {
3872 if v.is_missing() {
3873 continue;
3874 }
3875 if let Scalar::Timedelta64(ns) = v {
3876 match best {
3877 None => best = Some((i, *ns)),
3878 Some((_, cur)) if *ns < cur => best = Some((i, *ns)),
3879 _ => {}
3880 }
3881 }
3882 }
3883 return best.map(|(i, _)| i);
3884 }
3885 let mut best: Option<(usize, f64)> = None;
3886 for (i, v) in values.iter().enumerate() {
3887 if v.is_missing() {
3888 continue;
3889 }
3890 if let Ok(x) = v.to_f64() {
3891 if x.is_nan() {
3892 continue;
3893 }
3894 match best {
3895 None => best = Some((i, x)),
3896 Some((_, cur)) if x < cur => best = Some((i, x)),
3897 _ => {}
3898 }
3899 }
3900 }
3901 best.map(|(i, _)| i)
3902}
3903
3904pub fn nannunique(values: &[Scalar]) -> Scalar {
3906 use rustc_hash::FxHashSet;
3907 #[derive(Hash, PartialEq, Eq)]
3908 enum ScalarKey<'a> {
3909 Bool(bool),
3910 Int64(i64),
3911 FloatBits(u64),
3912 Utf8(&'a str),
3913 Timedelta64(i64),
3914 Datetime64(i64),
3915 Period(i64),
3916 Interval(u64, u64, IntervalClosed),
3917 }
3918
3919 let mut seen = FxHashSet::default();
3920 for val in values {
3921 if val.is_missing() {
3922 continue;
3923 }
3924 let key = match val {
3925 Scalar::Bool(v) => ScalarKey::Bool(*v),
3926 Scalar::Int64(v) => ScalarKey::Int64(*v),
3927 Scalar::Float64(v) => {
3928 let normalized = if *v == 0.0 { 0.0 } else { *v };
3929 ScalarKey::FloatBits(normalized.to_bits())
3930 }
3931 Scalar::Utf8(v) => ScalarKey::Utf8(v.as_str()),
3932 Scalar::Timedelta64(v) => ScalarKey::Timedelta64(*v),
3933 Scalar::Datetime64(v) => ScalarKey::Datetime64(*v),
3934 Scalar::Period(v) => ScalarKey::Period(*v),
3935 Scalar::Interval(v) => ScalarKey::Interval(
3936 normalized_float_bits(v.left),
3937 normalized_float_bits(v.right),
3938 v.closed,
3939 ),
3940 Scalar::Null(_) => continue,
3941 };
3942 seen.insert(key);
3943 }
3944 Scalar::Int64(seen.len() as i64)
3945}
3946
3947fn normalized_float_bits(value: f64) -> u64 {
3948 let normalized = if value == 0.0 { 0.0 } else { value };
3949 normalized.to_bits()
3950}
3951
3952#[derive(
3972 Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize,
3973)]
3974#[serde(rename_all = "snake_case")]
3975#[non_exhaustive]
3976pub enum IntervalClosed {
3977 Left,
3979 #[default]
3981 Right,
3982 Both,
3984 Neither,
3986}
3987
3988impl IntervalClosed {
3989 #[must_use]
3991 pub fn left_closed(self) -> bool {
3992 matches!(self, Self::Left | Self::Both)
3993 }
3994
3995 #[must_use]
3997 pub fn right_closed(self) -> bool {
3998 matches!(self, Self::Right | Self::Both)
3999 }
4000}
4001
4002impl std::fmt::Display for IntervalClosed {
4003 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4004 match self {
4005 Self::Left => write!(f, "left"),
4006 Self::Right => write!(f, "right"),
4007 Self::Both => write!(f, "both"),
4008 Self::Neither => write!(f, "neither"),
4009 }
4010 }
4011}
4012
4013#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
4019pub struct Interval {
4020 pub left: f64,
4021 pub right: f64,
4022 #[serde(default)]
4023 pub closed: IntervalClosed,
4024}
4025
4026impl Interval {
4027 #[must_use]
4030 pub const fn new(left: f64, right: f64, closed: IntervalClosed) -> Self {
4031 Self {
4032 left,
4033 right,
4034 closed,
4035 }
4036 }
4037
4038 #[must_use]
4040 pub fn length(&self) -> f64 {
4041 self.right - self.left
4042 }
4043
4044 #[must_use]
4046 pub fn mid(&self) -> f64 {
4047 (self.left + self.right) / 2.0
4048 }
4049
4050 #[must_use]
4053 pub fn is_empty(&self) -> bool {
4054 self.left == self.right && !matches!(self.closed, IntervalClosed::Both)
4055 }
4056
4057 #[must_use]
4062 pub fn contains(&self, value: f64) -> bool {
4063 if value.is_nan() {
4064 return false;
4065 }
4066 let left_ok = if self.closed.left_closed() {
4067 value >= self.left
4068 } else {
4069 value > self.left
4070 };
4071 let right_ok = if self.closed.right_closed() {
4072 value <= self.right
4073 } else {
4074 value < self.right
4075 };
4076 left_ok && right_ok
4077 }
4078
4079 #[must_use]
4086 pub fn overlaps(&self, other: &Self) -> bool {
4087 if self.left > other.right || other.left > self.right {
4088 return false;
4089 }
4090 if self.right == other.left {
4092 return self.closed.right_closed() && other.closed.left_closed();
4093 }
4094 if other.right == self.left {
4095 return other.closed.right_closed() && self.closed.left_closed();
4096 }
4097 true
4098 }
4099
4100 pub fn parse(s: &str) -> Result<Self, TypeError> {
4106 let s = s.trim();
4107 if s.len() < 5 {
4108 return Err(TypeError::ValueNotParseable {
4109 value: s.to_string(),
4110 target: "Interval".to_string(),
4111 });
4112 }
4113
4114 let first_char = s.chars().next().unwrap();
4115 let last_char = s.chars().last().unwrap();
4116
4117 let left_closed = match first_char {
4118 '[' => true,
4119 '(' => false,
4120 _ => {
4121 return Err(TypeError::ValueNotParseable {
4122 value: s.to_string(),
4123 target: "Interval".to_string(),
4124 });
4125 }
4126 };
4127
4128 let right_closed = match last_char {
4129 ']' => true,
4130 ')' => false,
4131 _ => {
4132 return Err(TypeError::ValueNotParseable {
4133 value: s.to_string(),
4134 target: "Interval".to_string(),
4135 });
4136 }
4137 };
4138
4139 let closed = match (left_closed, right_closed) {
4140 (true, true) => IntervalClosed::Both,
4141 (true, false) => IntervalClosed::Left,
4142 (false, true) => IntervalClosed::Right,
4143 (false, false) => IntervalClosed::Neither,
4144 };
4145
4146 let inner = &s[1..s.len() - 1];
4147 let parts: Vec<&str> = inner.split(',').collect();
4148 if parts.len() != 2 {
4149 return Err(TypeError::ValueNotParseable {
4150 value: s.to_string(),
4151 target: "Interval".to_string(),
4152 });
4153 }
4154
4155 let left: f64 = parts[0]
4156 .trim()
4157 .parse()
4158 .map_err(|_| TypeError::ValueNotParseable {
4159 value: s.to_string(),
4160 target: "Interval".to_string(),
4161 })?;
4162
4163 let right: f64 = parts[1]
4164 .trim()
4165 .parse()
4166 .map_err(|_| TypeError::ValueNotParseable {
4167 value: s.to_string(),
4168 target: "Interval".to_string(),
4169 })?;
4170
4171 Ok(Self::new(left, right, closed))
4172 }
4173}
4174
4175impl std::fmt::Display for Interval {
4176 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4178 let left_bracket = if self.closed.left_closed() { '[' } else { '(' };
4179 let right_bracket = if self.closed.right_closed() { ']' } else { ')' };
4180 write!(
4181 f,
4182 "{left_bracket}{}, {}{right_bracket}",
4183 self.left, self.right
4184 )
4185 }
4186}
4187
4188#[must_use]
4206pub fn interval_range_by_periods(
4207 start: f64,
4208 end: f64,
4209 periods: usize,
4210 closed: IntervalClosed,
4211) -> Vec<Interval> {
4212 if periods == 0 || !start.is_finite() || !end.is_finite() || start >= end {
4213 return Vec::new();
4214 }
4215 let step = (end - start) / (periods as f64);
4216 let mut out = Vec::with_capacity(periods);
4217 for i in 0..periods {
4218 let left = start + step * (i as f64);
4219 let right = if i + 1 == periods {
4221 end
4222 } else {
4223 start + step * ((i + 1) as f64)
4224 };
4225 out.push(Interval::new(left, right, closed));
4226 }
4227 out
4228}
4229
4230pub fn interval_range_by_step(
4241 start: f64,
4242 end: f64,
4243 step: f64,
4244 closed: IntervalClosed,
4245) -> Result<Vec<Interval>, TypeError> {
4246 if !step.is_finite() || !step.is_sign_positive() || step == 0.0 {
4247 return Err(TypeError::InvalidIntervalStep { step });
4248 }
4249 if !start.is_finite() || !end.is_finite() || start >= end {
4250 return Ok(Vec::new());
4251 }
4252 let span = end - start;
4253 let periods_f = span / step;
4254 let periods = periods_f.round() as i64;
4255 if periods <= 0 {
4256 return Ok(Vec::new());
4257 }
4258 let reconstructed = step * (periods as f64);
4259 if (span - reconstructed).abs() > span.abs() * 1e-9 + 1e-12 {
4261 return Err(TypeError::IntervalStepDoesNotDivide { step, span });
4262 }
4263 let periods = periods as usize;
4264 let mut out = Vec::with_capacity(periods);
4265 for i in 0..periods {
4266 let left = start + step * (i as f64);
4267 let right = if i + 1 == periods {
4268 end
4269 } else {
4270 start + step * ((i + 1) as f64)
4271 };
4272 out.push(Interval::new(left, right, closed));
4273 }
4274 Ok(out)
4275}
4276
4277#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
4296#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
4297#[non_exhaustive]
4298pub enum PeriodFreq {
4299 Annual,
4301 Quarterly,
4303 Monthly,
4305 Weekly,
4307 Daily,
4309 Business,
4311 Hourly,
4313 Minutely,
4315 Secondly,
4317}
4318
4319impl PeriodFreq {
4320 pub fn parse(alias: &str) -> Option<Self> {
4324 match alias.to_ascii_uppercase().as_str() {
4325 "A" | "Y" | "A-DEC" | "Y-DEC" | "ANNUAL" | "YEARLY" => Some(Self::Annual),
4326 "Q" | "Q-DEC" | "QUARTERLY" => Some(Self::Quarterly),
4327 "M" | "MONTHLY" => Some(Self::Monthly),
4328 "W" | "W-SUN" | "WEEKLY" => Some(Self::Weekly),
4329 "D" | "DAILY" => Some(Self::Daily),
4330 "B" | "BUSINESS" => Some(Self::Business),
4331 "H" | "HOURLY" => Some(Self::Hourly),
4332 "T" | "MIN" | "MINUTELY" => Some(Self::Minutely),
4333 "S" | "SECONDLY" => Some(Self::Secondly),
4334 _ => None,
4335 }
4336 }
4337
4338 #[must_use]
4340 pub const fn alias(self) -> &'static str {
4341 match self {
4342 Self::Annual => "Y-DEC",
4343 Self::Quarterly => "Q-DEC",
4344 Self::Monthly => "M",
4345 Self::Weekly => "W-SUN",
4346 Self::Daily => "D",
4347 Self::Business => "B",
4348 Self::Hourly => "h",
4349 Self::Minutely => "min",
4350 Self::Secondly => "s",
4351 }
4352 }
4353
4354 #[must_use]
4356 pub const fn resolution(self) -> &'static str {
4357 match self {
4358 Self::Annual => "A-DEC",
4359 Self::Quarterly => "Q-DEC",
4360 Self::Monthly => "M",
4361 Self::Weekly => "W-SUN",
4362 Self::Daily => "D",
4363 Self::Business => "B",
4364 Self::Hourly => "H",
4365 Self::Minutely => "T",
4366 Self::Secondly => "S",
4367 }
4368 }
4369}
4370
4371impl std::fmt::Display for PeriodFreq {
4372 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4373 f.write_str(self.alias())
4374 }
4375}
4376
4377#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4383pub struct Period {
4384 pub ordinal: i64,
4385 pub freq: PeriodFreq,
4386}
4387
4388impl Period {
4389 #[must_use]
4390 pub const fn new(ordinal: i64, freq: PeriodFreq) -> Self {
4391 Self { ordinal, freq }
4392 }
4393
4394 #[must_use]
4397 pub const fn ordinal(&self) -> i64 {
4398 self.ordinal
4399 }
4400
4401 #[must_use]
4403 pub const fn freq(&self) -> PeriodFreq {
4404 self.freq
4405 }
4406
4407 #[must_use]
4409 pub const fn freqstr(&self) -> &'static str {
4410 self.freq.alias()
4411 }
4412
4413 #[must_use]
4416 pub fn cmp_same_freq(&self, other: &Self) -> Option<std::cmp::Ordering> {
4417 if self.freq != other.freq {
4418 return None;
4419 }
4420 Some(self.ordinal.cmp(&other.ordinal))
4421 }
4422
4423 #[must_use]
4426 pub fn shift(&self, n: i64) -> Self {
4427 Self {
4428 ordinal: self.ordinal.saturating_add(n),
4429 freq: self.freq,
4430 }
4431 }
4432
4433 #[must_use]
4436 pub fn diff(&self, other: &Self) -> Option<i64> {
4437 if self.freq != other.freq {
4438 return None;
4439 }
4440 Some(self.ordinal.saturating_sub(other.ordinal))
4441 }
4442
4443 pub fn parse(s: &str) -> Result<Self, TypeError> {
4450 let trimmed = s.trim();
4451 if trimmed.eq_ignore_ascii_case("nat") {
4452 return Ok(Self::new(i64::MIN, PeriodFreq::Daily));
4453 }
4454
4455 if let Some((year, quarter)) = parse_quarter_period(trimmed) {
4456 let ordinal = year
4457 .checked_sub(1970)
4458 .and_then(|offset| offset.checked_mul(4))
4459 .and_then(|base| base.checked_add(i64::from(quarter) - 1))
4460 .ok_or_else(|| TypeError::ValueNotParseable {
4461 value: s.to_owned(),
4462 target: "Period".to_owned(),
4463 })?;
4464 return Ok(Self::new(ordinal, PeriodFreq::Quarterly));
4465 }
4466
4467 if let Some((year, month, day)) = parse_ymd_period(trimmed) {
4468 let ordinal = Timestamp::days_from_ymd(year, i64::from(month), i64::from(day));
4469 return Ok(Self::new(ordinal, PeriodFreq::Daily));
4470 }
4471
4472 if let Some((year, month)) = parse_year_month_period(trimmed) {
4473 let ordinal = year
4474 .checked_sub(1970)
4475 .and_then(|offset| offset.checked_mul(12))
4476 .and_then(|base| base.checked_add(i64::from(month) - 1))
4477 .ok_or_else(|| TypeError::ValueNotParseable {
4478 value: s.to_owned(),
4479 target: "Period".to_owned(),
4480 })?;
4481 return Ok(Self::new(ordinal, PeriodFreq::Monthly));
4482 }
4483
4484 if let Some(year) = parse_annual_period(trimmed) {
4485 let ordinal = year
4486 .checked_sub(1970)
4487 .ok_or_else(|| TypeError::ValueNotParseable {
4488 value: s.to_owned(),
4489 target: "Period".to_owned(),
4490 })?;
4491 return Ok(Self::new(ordinal, PeriodFreq::Annual));
4492 }
4493
4494 Err(TypeError::ValueNotParseable {
4495 value: s.to_owned(),
4496 target: "Period".to_owned(),
4497 })
4498 }
4499}
4500
4501fn parse_annual_period(value: &str) -> Option<i64> {
4502 (value.len() == 4 && value.chars().all(|ch| ch.is_ascii_digit()))
4503 .then(|| value.parse::<i64>().ok())
4504 .flatten()
4505}
4506
4507fn parse_year_month_period(value: &str) -> Option<(i64, u32)> {
4508 let (year, month) = value.split_once('-')?;
4509 if year.len() != 4 || month.len() != 2 {
4510 return None;
4511 }
4512 let year = year.parse::<i64>().ok()?;
4513 let month = month.parse::<u32>().ok()?;
4514 (1..=12).contains(&month).then_some((year, month))
4515}
4516
4517fn parse_ymd_period(value: &str) -> Option<(i64, u32, u32)> {
4518 let mut parts = value.split('-');
4519 let year = parts.next()?;
4520 let month = parts.next()?;
4521 let day = parts.next()?;
4522 if parts.next().is_some() || year.len() != 4 || month.len() != 2 || day.len() != 2 {
4523 return None;
4524 }
4525 let year = year.parse::<i64>().ok()?;
4526 let month = month.parse::<u32>().ok()?;
4527 let day = day.parse::<u32>().ok()?;
4528 (1..=days_in_month(year, month)?)
4529 .contains(&day)
4530 .then_some((year, month, day))
4531}
4532
4533fn parse_quarter_period(value: &str) -> Option<(i64, u32)> {
4534 let (year, quarter) = value.split_once('Q').or_else(|| value.split_once('q'))?;
4535 if year.len() != 4 || quarter.len() != 1 {
4536 return None;
4537 }
4538 let year = year.parse::<i64>().ok()?;
4539 let quarter = quarter.parse::<u32>().ok()?;
4540 (1..=4).contains(&quarter).then_some((year, quarter))
4541}
4542
4543impl std::fmt::Display for Period {
4544 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4548 write!(f, "Period[{}, {}]", self.freq, self.ordinal)
4549 }
4550}
4551
4552#[must_use]
4571pub fn period_range(start: Period, periods: usize) -> Vec<Period> {
4572 (0..periods).map(|i| start.shift(i as i64)).collect()
4573}
4574
4575#[cfg(test)]
4576mod tests {
4577 use super::{
4578 DType, Interval, IntervalClosed, NullKind, Period, PeriodFreq, Scalar, SparseDType,
4579 cast_scalar, common_dtype, infer_dtype,
4580 };
4581
4582 #[test]
4584 fn scalar_from_primitive_types() {
4585 assert_eq!(Scalar::from(true), Scalar::Bool(true));
4587 assert_eq!(Scalar::from(42i64), Scalar::Int64(42));
4588 assert_eq!(Scalar::from(1.5f64), Scalar::Float64(1.5));
4589 assert_eq!(Scalar::from("hi"), Scalar::Utf8("hi".to_owned()));
4590 assert_eq!(
4591 Scalar::from(String::from("world")),
4592 Scalar::Utf8("world".to_owned())
4593 );
4594
4595 let mixed: Vec<Scalar> = vec![1i64.into(), 2.0f64.into(), "three".into()];
4599 assert_eq!(mixed.len(), 3);
4600 assert_eq!(mixed[0], Scalar::Int64(1));
4601 assert_eq!(mixed[1], Scalar::Float64(2.0));
4602 assert_eq!(mixed[2], Scalar::Utf8("three".to_owned()));
4603 }
4604
4605 #[test]
4606 fn dtype_inference_coerces_numeric_values() {
4607 let values = vec![Scalar::Bool(true), Scalar::Int64(7), Scalar::Float64(3.5)];
4608 assert_eq!(
4609 infer_dtype(&values).expect("dtype should infer"),
4610 DType::Float64
4611 );
4612 }
4613
4614 #[test]
4615 fn interval_scalar_has_dtype_storage_and_unique_semantics_5g5uj() {
4616 let left = Scalar::Interval(Interval::new(0.0, 1.0, IntervalClosed::Right));
4617 let right = Scalar::Interval(Interval::new(1.0, 2.0, IntervalClosed::Right));
4618 assert_eq!(left.dtype(), DType::Interval);
4619 assert!(!left.is_missing());
4620 assert_eq!(
4621 infer_dtype(&[left.clone(), right.clone()]).expect("interval dtype"),
4622 DType::Interval
4623 );
4624 assert_eq!(
4625 common_dtype(DType::Interval, DType::Interval).expect("same interval dtype"),
4626 DType::Interval
4627 );
4628 assert_eq!(
4629 cast_scalar(&Scalar::Null(NullKind::Null), DType::Interval).expect("missing casts"),
4630 Scalar::Null(NullKind::Null)
4631 );
4632 assert_eq!(
4633 cast_scalar(&left, DType::Utf8).expect("interval string cast"),
4634 Scalar::Utf8("(0, 1]".to_owned())
4635 );
4636 assert_eq!(
4637 super::nannunique(&[left.clone(), right, left, Scalar::Null(NullKind::Null)]),
4638 Scalar::Int64(2)
4639 );
4640 }
4641
4642 #[test]
4643 fn cast_scalar_parses_temporal_extension_strings_avm08() {
4644 let expected_nanos = super::Timestamp::parse("2024-01-15T10:30:45")
4645 .expect("timestamp parse")
4646 .nanos;
4647 assert_eq!(
4648 cast_scalar(
4649 &Scalar::Utf8("2024-01-15T10:30:45".to_owned()),
4650 DType::Datetime64
4651 )
4652 .expect("datetime cast"),
4653 Scalar::Datetime64(expected_nanos)
4654 );
4655 assert_eq!(
4656 cast_scalar(&Scalar::Utf8("2024Q1".to_owned()), DType::Period).expect("period cast"),
4657 Scalar::Period(216)
4658 );
4659 assert_eq!(
4660 cast_scalar(&Scalar::Utf8("(0, 1]".to_owned()), DType::Interval)
4661 .expect("interval cast"),
4662 Scalar::Interval(Interval::new(0.0, 1.0, IntervalClosed::Right))
4663 );
4664 }
4665
4666 #[test]
4667 fn missing_values_get_target_missing_marker() {
4668 let missing = Scalar::Null(NullKind::Null);
4669 let cast = cast_scalar(&missing, DType::Float64).expect("missing casts");
4670 assert_eq!(cast, Scalar::Null(NullKind::NaN));
4671 }
4672
4673 #[test]
4674 fn cast_scalar_to_utf8_uses_pandas_string_spellings() {
4675 let cases = [
4676 (Scalar::Bool(true), "True"),
4677 (Scalar::Bool(false), "False"),
4678 (Scalar::Int64(-7), "-7"),
4679 (Scalar::Float64(1.0), "1.0"),
4680 (Scalar::Float64(1.5), "1.5"),
4681 (Scalar::Float64(f64::NAN), "nan"),
4682 (Scalar::Null(NullKind::Null), "None"),
4683 (Scalar::Null(NullKind::NaN), "nan"),
4684 (Scalar::Null(NullKind::NaT), "NaT"),
4685 ];
4686
4687 for (value, expected) in cases {
4688 assert_eq!(
4689 cast_scalar(&value, DType::Utf8).expect("cast"),
4690 Scalar::Utf8(expected.to_owned())
4691 );
4692 }
4693 }
4694
4695 #[test]
4696 fn semantic_eq_treats_nan_as_equal() {
4697 let left = Scalar::Float64(f64::NAN);
4698 let right = Scalar::Null(NullKind::NaN);
4699 assert!(left.semantic_eq(&right));
4700 }
4701
4702 #[test]
4703 fn semantic_eq_treats_nan_as_missing_null() {
4704 let left = Scalar::Float64(f64::NAN);
4705 let right = Scalar::Null(NullKind::Null);
4706 assert!(left.semantic_eq(&right));
4707 }
4708
4709 #[test]
4710 fn common_dtype_rejects_string_numeric_mix() {
4711 let err = common_dtype(DType::Utf8, DType::Int64).expect_err("must fail");
4712 assert_eq!(
4713 err.to_string(),
4714 "dtype coercion from Utf8 to Int64 has no compatible common type"
4715 );
4716 let err = common_dtype(DType::Float64, DType::Utf8).expect_err("must fail");
4717 assert_eq!(
4718 err.to_string(),
4719 "dtype coercion from Float64 to Utf8 has no compatible common type"
4720 );
4721 }
4722
4723 #[test]
4724 fn sparse_dtype_normalizes_fill_value_to_value_dtype() {
4725 let dtype = SparseDType::new(DType::Float64, Scalar::Int64(0)).expect("fill should cast");
4726
4727 assert_eq!(dtype.value_dtype, DType::Float64);
4728 assert_eq!(dtype.fill_value, Scalar::Float64(0.0));
4729 }
4730
4731 #[test]
4732 fn sparse_dtype_rejects_sparse_value_dtype() {
4733 let err = SparseDType::new(DType::Sparse, Scalar::Int64(0)).expect_err("must reject");
4734
4735 assert_eq!(err.to_string(), "sparse value dtype cannot be Sparse");
4736 }
4737
4738 #[test]
4739 fn common_dtype_rejects_sparse_dense_mix() {
4740 let err = common_dtype(DType::Sparse, DType::Int64).expect_err("must fail");
4741
4742 assert_eq!(
4743 err.to_string(),
4744 "dtype coercion from Sparse to Int64 has no compatible common type"
4745 );
4746 }
4747
4748 #[test]
4751 fn nullable_int64_promotion_matrix() {
4752 assert_eq!(
4754 common_dtype(DType::Int64, DType::Int64Nullable).unwrap(),
4755 DType::Int64Nullable
4756 );
4757 assert_eq!(
4758 common_dtype(DType::Int64Nullable, DType::Int64).unwrap(),
4759 DType::Int64Nullable
4760 );
4761
4762 assert_eq!(
4764 common_dtype(DType::Int64Nullable, DType::Float64).unwrap(),
4765 DType::Float64
4766 );
4767 assert_eq!(
4768 common_dtype(DType::Float64, DType::Int64Nullable).unwrap(),
4769 DType::Float64
4770 );
4771
4772 assert_eq!(
4774 common_dtype(DType::Int64Nullable, DType::Int64Nullable).unwrap(),
4775 DType::Int64Nullable
4776 );
4777
4778 assert_eq!(
4780 common_dtype(DType::Bool, DType::Int64Nullable).unwrap(),
4781 DType::Int64Nullable
4782 );
4783
4784 assert_eq!(
4786 common_dtype(DType::BoolNullable, DType::Int64).unwrap(),
4787 DType::Int64Nullable
4788 );
4789 }
4790
4791 #[test]
4792 fn nullable_bool_promotion_matrix() {
4793 assert_eq!(
4795 common_dtype(DType::Bool, DType::BoolNullable).unwrap(),
4796 DType::BoolNullable
4797 );
4798 assert_eq!(
4799 common_dtype(DType::BoolNullable, DType::Bool).unwrap(),
4800 DType::BoolNullable
4801 );
4802
4803 assert_eq!(
4805 common_dtype(DType::BoolNullable, DType::Float64).unwrap(),
4806 DType::Float64
4807 );
4808 }
4809
4810 #[test]
4811 fn dtype_is_nullable_helper() {
4812 assert!(DType::Int64Nullable.is_nullable());
4813 assert!(DType::BoolNullable.is_nullable());
4814 assert!(!DType::Int64.is_nullable());
4815 assert!(!DType::Bool.is_nullable());
4816 assert!(!DType::Float64.is_nullable());
4817 assert!(!DType::Utf8.is_nullable());
4818 }
4819
4820 #[test]
4821 fn dtype_to_nullable_conversions() {
4822 assert_eq!(DType::Int64.to_nullable(), DType::Int64Nullable);
4823 assert_eq!(DType::Bool.to_nullable(), DType::BoolNullable);
4824 assert_eq!(DType::Float64.to_nullable(), DType::Float64); assert_eq!(DType::Int64Nullable.to_nullable(), DType::Int64Nullable);
4826 }
4827
4828 #[test]
4829 fn dtype_to_non_nullable_conversions() {
4830 assert_eq!(DType::Int64Nullable.to_non_nullable(), DType::Int64);
4831 assert_eq!(DType::BoolNullable.to_non_nullable(), DType::Bool);
4832 assert_eq!(DType::Int64.to_non_nullable(), DType::Int64); assert_eq!(DType::Float64.to_non_nullable(), DType::Float64);
4834 }
4835
4836 #[test]
4837 fn nullable_dtype_name_reports_pandas_style() {
4838 assert_eq!(DType::Int64.name(), "int64");
4839 assert_eq!(DType::Int64Nullable.name(), "Int64"); assert_eq!(DType::Bool.name(), "bool");
4841 assert_eq!(DType::BoolNullable.name(), "boolean");
4842 }
4843
4844 #[test]
4845 fn cast_scalar_int64_nullable_identity() {
4846 let val = Scalar::Int64(42);
4847 let result = cast_scalar(&val, DType::Int64Nullable).unwrap();
4849 assert_eq!(result, Scalar::Int64(42));
4850
4851 let result2 = cast_scalar(&val, DType::Int64).unwrap();
4853 assert_eq!(result2, Scalar::Int64(42));
4854 }
4855
4856 #[test]
4857 fn cast_float_to_utf8_uses_pandas_str_float_with_scientific() {
4858 let cases: &[(f64, &str)] = &[
4863 (1.0, "1.0"),
4864 (2.5, "2.5"),
4865 (100.0, "100.0"),
4866 (0.1, "0.1"),
4867 (0.0001, "0.0001"),
4868 (1e16, "1e+16"),
4869 (1e20, "1e+20"),
4870 (1e-5, "1e-05"),
4871 (1e-7, "1e-07"),
4872 (f64::INFINITY, "inf"),
4873 (f64::NEG_INFINITY, "-inf"),
4874 ];
4875 for (v, expected) in cases {
4876 assert_eq!(
4877 cast_scalar(&Scalar::Float64(*v), DType::Utf8).unwrap(),
4878 Scalar::Utf8((*expected).to_owned()),
4879 "float {v} -> str"
4880 );
4881 }
4882 }
4883
4884 #[test]
4885 fn cast_to_bool_uses_pandas_nonzero_truthiness() {
4886 let cases_int: &[(i64, bool)] = &[(0, false), (1, true), (-3, true), (2, true)];
4890 for (v, expected) in cases_int {
4891 assert_eq!(
4892 cast_scalar(&Scalar::Int64(*v), DType::Bool).unwrap(),
4893 Scalar::Bool(*expected),
4894 "int {v} -> bool"
4895 );
4896 }
4897 let cases_float: &[(f64, bool)] = &[
4898 (0.0, false),
4899 (-0.0, false),
4900 (0.1, true),
4901 (2.5, true),
4902 (1.0, true),
4903 (f64::NAN, true),
4905 ];
4906 for (v, expected) in cases_float {
4907 assert_eq!(
4908 cast_scalar(&Scalar::Float64(*v), DType::Bool).unwrap(),
4909 Scalar::Bool(*expected),
4910 "float {v} -> bool"
4911 );
4912 }
4913 }
4914
4915 #[test]
4916 fn nullable_dtype_is_extension() {
4917 assert!(DType::Int64Nullable.is_extension());
4918 assert!(DType::BoolNullable.is_extension());
4919 assert!(!DType::Int64.is_extension());
4920 assert!(!DType::Bool.is_extension());
4921 }
4922
4923 #[test]
4924 fn infer_dtype_preserves_string_numeric_mix_as_utf8_bucket() {
4925 let values = vec![Scalar::Utf8("x".into()), Scalar::Int64(7)];
4926 assert_eq!(
4927 infer_dtype(&values).expect("dtype should infer"),
4928 DType::Utf8
4929 );
4930 }
4931
4932 #[test]
4935 fn is_null_detects_explicit_nulls() {
4936 assert!(Scalar::Null(NullKind::Null).is_null());
4937 assert!(Scalar::Null(NullKind::NaN).is_null());
4938 assert!(!Scalar::Int64(42).is_null());
4939 assert!(!Scalar::Float64(f64::NAN).is_null());
4940 }
4941
4942 #[test]
4943 fn is_na_matches_is_missing() {
4944 let vals = vec![
4945 Scalar::Null(NullKind::Null),
4946 Scalar::Float64(f64::NAN),
4947 Scalar::Int64(0),
4948 Scalar::Bool(false),
4949 ];
4950 for v in &vals {
4951 assert_eq!(v.is_na(), v.is_missing());
4952 }
4953 }
4954
4955 #[test]
4956 fn coalesce_picks_first_non_missing() {
4957 let null = Scalar::Null(NullKind::Null);
4958 let fill = Scalar::Int64(99);
4959 assert_eq!(null.coalesce(&fill), fill);
4960 assert_eq!(fill.coalesce(&null), fill);
4961 }
4962
4963 #[test]
4966 fn isna_notna_complement() {
4967 let vals = vec![
4968 Scalar::Int64(1),
4969 Scalar::Null(NullKind::Null),
4970 Scalar::Float64(f64::NAN),
4971 Scalar::Float64(3.0),
4972 ];
4973 let na = super::isna(&vals);
4974 let not = super::notna(&vals);
4975 assert_eq!(na, vec![false, true, true, false]);
4976 for (a, b) in na.iter().zip(not.iter()) {
4977 assert_ne!(a, b);
4978 }
4979 }
4980
4981 #[test]
4982 fn count_na_counts_missing() {
4983 let vals = vec![
4984 Scalar::Int64(1),
4985 Scalar::Null(NullKind::Null),
4986 Scalar::Float64(f64::NAN),
4987 ];
4988 assert_eq!(super::count_na(&vals), 2);
4989 }
4990
4991 #[test]
4992 fn fill_na_replaces_missing() {
4993 let vals = vec![
4994 Scalar::Int64(1),
4995 Scalar::Null(NullKind::Null),
4996 Scalar::Float64(f64::NAN),
4997 Scalar::Int64(4),
4998 ];
4999 let filled = super::fill_na(&vals, &Scalar::Int64(0));
5000 assert_eq!(filled[0], Scalar::Int64(1));
5001 assert_eq!(filled[1], Scalar::Int64(0));
5002 assert_eq!(filled[2], Scalar::Int64(0));
5003 assert_eq!(filled[3], Scalar::Int64(4));
5004 }
5005
5006 #[test]
5007 fn dropna_removes_missing() {
5008 let vals = vec![
5009 Scalar::Int64(1),
5010 Scalar::Null(NullKind::Null),
5011 Scalar::Int64(3),
5012 Scalar::Float64(f64::NAN),
5013 ];
5014 let kept = super::dropna(&vals);
5015 assert_eq!(kept.len(), 2);
5016 assert_eq!(kept[0], Scalar::Int64(1));
5017 assert_eq!(kept[1], Scalar::Int64(3));
5018 }
5019
5020 #[test]
5023 fn nansum_skips_nulls() {
5024 let vals = vec![
5025 Scalar::Float64(1.0),
5026 Scalar::Null(NullKind::Null),
5027 Scalar::Float64(2.0),
5028 Scalar::Float64(f64::NAN),
5029 Scalar::Int64(7),
5030 ];
5031 assert_eq!(super::nansum(&vals), Scalar::Float64(10.0));
5032 }
5033
5034 #[test]
5035 fn nansum_empty_returns_zero() {
5036 assert_eq!(super::nansum(&[]), Scalar::Float64(0.0));
5037 }
5038
5039 #[test]
5040 fn nannunique_merges_negative_zero_and_zero() {
5041 let vals = vec![
5042 Scalar::Float64(-0.0),
5043 Scalar::Float64(0.0),
5044 Scalar::Float64(1.0),
5045 ];
5046 assert_eq!(super::nannunique(&vals), Scalar::Int64(2));
5047 }
5048
5049 #[test]
5050 fn nanmean_basic() {
5051 let vals = vec![
5052 Scalar::Float64(2.0),
5053 Scalar::Null(NullKind::Null),
5054 Scalar::Float64(4.0),
5055 ];
5056 assert_eq!(super::nanmean(&vals), Scalar::Float64(3.0));
5057 }
5058
5059 #[test]
5060 fn nanmean_all_null_returns_nan() {
5061 let vals = vec![Scalar::Null(NullKind::Null), Scalar::Float64(f64::NAN)];
5062 assert!(super::nanmean(&vals).is_missing());
5063 }
5064
5065 #[test]
5066 fn nansum_nanmean_timedelta64_preserves_dtype_620mj() {
5067 let one_hour = 3_600 * 1_000_000_000_i64;
5071 let vals = vec![
5072 Scalar::Timedelta64(one_hour),
5073 Scalar::Timedelta64(2 * one_hour),
5074 Scalar::Timedelta64(3 * one_hour),
5075 ];
5076 assert_eq!(super::nansum(&vals), Scalar::Timedelta64(6 * one_hour));
5077 assert_eq!(super::nanmean(&vals), Scalar::Timedelta64(2 * one_hour));
5078 }
5079
5080 #[test]
5081 fn nansum_nanmean_timedelta64_skips_nat_620mj() {
5082 let one_hour = 3_600 * 1_000_000_000_i64;
5083 let vals = vec![
5084 Scalar::Timedelta64(Timedelta::NAT),
5085 Scalar::Timedelta64(one_hour),
5086 Scalar::Timedelta64(3 * one_hour),
5087 Scalar::Timedelta64(Timedelta::NAT),
5088 ];
5089 assert_eq!(super::nansum(&vals), Scalar::Timedelta64(4 * one_hour));
5091 assert_eq!(super::nanmean(&vals), Scalar::Timedelta64(2 * one_hour));
5092 }
5093
5094 #[test]
5095 fn nansum_nanmean_mixed_timedelta_other_falls_back_620mj() {
5096 let vals = vec![Scalar::Timedelta64(3600 * 1_000_000_000), Scalar::Int64(5)];
5100 assert_eq!(super::nansum(&vals), Scalar::Float64(5.0));
5102 }
5103
5104 #[test]
5105 fn nancount_counts_non_missing() {
5106 let vals = vec![
5107 Scalar::Int64(1),
5108 Scalar::Null(NullKind::Null),
5109 Scalar::Float64(3.0),
5110 ];
5111 assert_eq!(super::nancount(&vals), Scalar::Int64(2));
5112 }
5113
5114 #[test]
5115 fn nanmin_basic() {
5116 let vals = vec![
5117 Scalar::Float64(5.0),
5118 Scalar::Null(NullKind::Null),
5119 Scalar::Float64(2.0),
5120 Scalar::Float64(8.0),
5121 ];
5122 assert_eq!(super::nanmin(&vals), Scalar::Float64(2.0));
5123 }
5124
5125 #[test]
5126 fn nanmax_basic() {
5127 let vals = vec![
5128 Scalar::Float64(5.0),
5129 Scalar::Float64(f64::NAN),
5130 Scalar::Float64(8.0),
5131 ];
5132 assert_eq!(super::nanmax(&vals), Scalar::Float64(8.0));
5133 }
5134
5135 #[test]
5136 fn nanmin_nanmax_empty_returns_nan() {
5137 assert!(super::nanmin(&[]).is_missing());
5138 assert!(super::nanmax(&[]).is_missing());
5139 }
5140
5141 #[test]
5142 fn nanmin_nanmax_mixed_incompatible_types_returns_nan() {
5143 let vals = vec![Scalar::Int64(5), Scalar::Utf8("hello".into())];
5144 assert!(super::nanmin(&vals).is_missing());
5145 assert!(super::nanmax(&vals).is_missing());
5146
5147 let vals2 = vec![Scalar::Utf8("a".into()), Scalar::Float64(3.0)];
5148 assert!(super::nanmin(&vals2).is_missing());
5149 assert!(super::nanmax(&vals2).is_missing());
5150 }
5151
5152 #[test]
5153 fn nanmin_nanmax_compatible_numeric_types_ok() {
5154 let vals = vec![Scalar::Int64(5), Scalar::Float64(3.0), Scalar::Bool(true)];
5155 assert_eq!(super::nanmin(&vals), Scalar::Bool(true));
5156 assert_eq!(super::nanmax(&vals), Scalar::Int64(5));
5157 }
5158
5159 #[test]
5160 fn nanmin_nanmax_timedelta64_returns_timedelta_yic5m() {
5161 let one_hour = 3_600 * 1_000_000_000_i64;
5165 let vals = vec![
5166 Scalar::Timedelta64(3 * one_hour),
5167 Scalar::Timedelta64(one_hour),
5168 Scalar::Timedelta64(2 * one_hour),
5169 ];
5170 assert_eq!(super::nanmin(&vals), Scalar::Timedelta64(one_hour));
5171 assert_eq!(super::nanmax(&vals), Scalar::Timedelta64(3 * one_hour));
5172 }
5173
5174 #[test]
5175 fn nanmin_nanmax_timedelta64_skips_nat_yic5m() {
5176 let one_hour = 3_600 * 1_000_000_000_i64;
5177 let vals = vec![
5178 Scalar::Timedelta64(Timedelta::NAT),
5179 Scalar::Timedelta64(one_hour),
5180 Scalar::Timedelta64(2 * one_hour),
5181 Scalar::Timedelta64(Timedelta::NAT),
5182 ];
5183 assert_eq!(super::nanmin(&vals), Scalar::Timedelta64(one_hour));
5184 assert_eq!(super::nanmax(&vals), Scalar::Timedelta64(2 * one_hour));
5185 }
5186
5187 #[test]
5188 fn nanmedian_odd_count() {
5189 let vals = vec![
5190 Scalar::Float64(3.0),
5191 Scalar::Null(NullKind::Null),
5192 Scalar::Float64(1.0),
5193 Scalar::Float64(2.0),
5194 ];
5195 assert_eq!(super::nanmedian(&vals), Scalar::Float64(2.0));
5196 }
5197
5198 #[test]
5199 fn nanmedian_even_count() {
5200 let vals = vec![
5201 Scalar::Float64(1.0),
5202 Scalar::Float64(3.0),
5203 Scalar::Float64(2.0),
5204 Scalar::Float64(4.0),
5205 ];
5206 assert_eq!(super::nanmedian(&vals), Scalar::Float64(2.5));
5207 }
5208
5209 #[test]
5210 fn nanvar_population() {
5211 let vals = vec![
5212 Scalar::Float64(2.0),
5213 Scalar::Float64(4.0),
5214 Scalar::Float64(4.0),
5215 Scalar::Float64(4.0),
5216 Scalar::Float64(5.0),
5217 Scalar::Float64(5.0),
5218 Scalar::Float64(7.0),
5219 Scalar::Float64(9.0),
5220 ];
5221 let var = super::nanvar(&vals, 0);
5222 assert!(matches!(var, Scalar::Float64(_)), "expected Float64");
5223 if let Scalar::Float64(v) = var {
5224 assert!((v - 4.0).abs() < 1e-10);
5225 }
5226 }
5227
5228 #[test]
5229 fn nanvar_sample_ddof1() {
5230 let vals = vec![
5231 Scalar::Float64(2.0),
5232 Scalar::Float64(4.0),
5233 Scalar::Float64(4.0),
5234 Scalar::Float64(4.0),
5235 Scalar::Float64(5.0),
5236 Scalar::Float64(5.0),
5237 Scalar::Float64(7.0),
5238 Scalar::Float64(9.0),
5239 ];
5240 let var = super::nanvar(&vals, 1);
5241 assert!(matches!(var, Scalar::Float64(_)), "expected Float64");
5242 if let Scalar::Float64(v) = var {
5243 assert!((v - 32.0 / 7.0).abs() < 1e-10);
5244 }
5245 }
5246
5247 #[test]
5248 fn nanvar_insufficient_values_returns_nan() {
5249 let vals = vec![Scalar::Float64(5.0)];
5250 assert!(super::nanvar(&vals, 1).is_missing());
5251 }
5252
5253 #[test]
5254 fn nanstd_is_sqrt_of_var() {
5255 let vals = vec![
5256 Scalar::Float64(2.0),
5257 Scalar::Float64(4.0),
5258 Scalar::Float64(4.0),
5259 Scalar::Float64(4.0),
5260 Scalar::Float64(5.0),
5261 Scalar::Float64(5.0),
5262 Scalar::Float64(7.0),
5263 Scalar::Float64(9.0),
5264 ];
5265 let std = super::nanstd(&vals, 0);
5266 assert!(matches!(std, Scalar::Float64(_)), "expected Float64");
5267 if let Scalar::Float64(v) = std {
5268 assert!((v - 2.0).abs() < 1e-10);
5269 }
5270 }
5271
5272 #[test]
5273 fn nanmedian_timedelta64_preserves_dtype_j8ntk() {
5274 let one_hour = 3_600 * 1_000_000_000_i64;
5277 let vals = vec![
5278 Scalar::Timedelta64(one_hour),
5279 Scalar::Timedelta64(2 * one_hour),
5280 Scalar::Timedelta64(3 * one_hour),
5281 ];
5282 assert_eq!(super::nanmedian(&vals), Scalar::Timedelta64(2 * one_hour));
5283 }
5284
5285 #[test]
5286 fn nanstd_timedelta64_preserves_dtype_j8ntk() {
5287 let one_hour: i64 = 3_600 * 1_000_000_000;
5291 let vals = vec![
5292 Scalar::Timedelta64(one_hour),
5293 Scalar::Timedelta64(2 * one_hour),
5294 Scalar::Timedelta64(3 * one_hour),
5295 ];
5296 let std = super::nanstd(&vals, 0);
5297 match std {
5298 Scalar::Timedelta64(ns) => {
5299 let expected = (2.0_f64 / 3.0).sqrt() * one_hour as f64;
5300 assert!(
5301 (ns as f64 - expected).abs() < 1e6,
5302 "expected ~{expected} ns, got {ns}"
5303 );
5304 }
5305 other => panic!("expected Timedelta64, got {other:?}"),
5306 }
5307 }
5308
5309 #[test]
5310 fn nanstd_nansem_timedelta64_insufficient_returns_nat_j8ntk() {
5311 let one_hour = 3_600 * 1_000_000_000_i64;
5312 let vals = vec![Scalar::Timedelta64(one_hour)];
5313 match super::nanstd(&vals, 1) {
5315 Scalar::Timedelta64(v) => assert_eq!(v, Timedelta::NAT),
5316 other => panic!("expected Timedelta64 NAT, got {other:?}"),
5317 }
5318 match super::nansem(&vals, 1) {
5319 Scalar::Timedelta64(v) => assert_eq!(v, Timedelta::NAT),
5320 other => panic!("expected Timedelta64 NAT, got {other:?}"),
5321 }
5322 }
5323
5324 #[test]
5325 fn nanops_with_mixed_types() {
5326 let vals = vec![
5327 Scalar::Bool(true),
5328 Scalar::Int64(3),
5329 Scalar::Float64(6.0),
5330 Scalar::Null(NullKind::Null),
5331 ];
5332 assert_eq!(super::nansum(&vals), Scalar::Float64(10.0));
5333 assert_eq!(super::nancount(&vals), Scalar::Int64(3));
5334 }
5335
5336 #[test]
5337 fn nanops_all_missing_returns_identity() {
5338 let vals = vec![Scalar::Null(NullKind::Null), Scalar::Float64(f64::NAN)];
5339 assert_eq!(super::nansum(&vals), Scalar::Float64(0.0));
5340 assert!(super::nanmean(&vals).is_missing());
5341 assert!(super::nanmedian(&vals).is_missing());
5342 assert!(super::nanvar(&vals, 0).is_missing());
5343 assert!(super::nanstd(&vals, 0).is_missing());
5344 }
5345
5346 #[test]
5349 fn timedelta_parse_simple_units() {
5350 use super::Timedelta;
5351 assert_eq!(Timedelta::parse("1d").unwrap(), Timedelta::NANOS_PER_DAY);
5352 assert_eq!(
5353 Timedelta::parse("2h").unwrap(),
5354 2 * Timedelta::NANOS_PER_HOUR
5355 );
5356 assert_eq!(
5357 Timedelta::parse("30m").unwrap(),
5358 30 * Timedelta::NANOS_PER_MIN
5359 );
5360 assert_eq!(
5361 Timedelta::parse("45s").unwrap(),
5362 45 * Timedelta::NANOS_PER_SEC
5363 );
5364 assert_eq!(
5365 Timedelta::parse("100ms").unwrap(),
5366 100 * Timedelta::NANOS_PER_MILLI
5367 );
5368 assert_eq!(
5369 Timedelta::parse("500us").unwrap(),
5370 500 * Timedelta::NANOS_PER_MICRO
5371 );
5372 assert_eq!(Timedelta::parse("1000ns").unwrap(), 1000);
5373 }
5374
5375 #[test]
5376 fn timedelta_parse_compound() {
5377 use super::Timedelta;
5378 let expected = Timedelta::NANOS_PER_DAY
5379 + 2 * Timedelta::NANOS_PER_HOUR
5380 + 30 * Timedelta::NANOS_PER_MIN;
5381 assert_eq!(Timedelta::parse("1d 2h 30m").unwrap(), expected);
5382 assert_eq!(Timedelta::parse("1d2h30m").unwrap(), expected);
5383 }
5384
5385 #[test]
5386 fn timedelta_parse_iso8601_matches_pandas_tdiso() {
5387 use super::Timedelta;
5388 assert_eq!(Timedelta::parse("P1DT2H3M4S").unwrap(), 93_784_000_000_000);
5390 assert_eq!(Timedelta::parse("PT1H").unwrap(), 3_600_000_000_000);
5391 assert_eq!(Timedelta::parse("PT1H30M").unwrap(), 5_400_000_000_000);
5392 assert_eq!(Timedelta::parse("P1D").unwrap(), 86_400_000_000_000);
5393 assert_eq!(Timedelta::parse("P2W").unwrap(), 1_209_600_000_000_000);
5394 assert_eq!(Timedelta::parse("PT0.5S").unwrap(), 500_000_000);
5395 assert_eq!(Timedelta::parse("P1M").unwrap(), 60_000_000_000);
5397 assert_eq!(Timedelta::parse("P1H").unwrap(), 3_600_000_000_000);
5398 assert_eq!(Timedelta::parse("PT1D").unwrap(), 86_400_000_000_000);
5399 assert_eq!(Timedelta::parse("P1D1H").unwrap(), 90_000_000_000_000);
5400 assert_eq!(Timedelta::parse("-P1DT2H").unwrap(), -93_600_000_000_000);
5401 assert!(Timedelta::parse("P1Y").is_err());
5403 assert!(Timedelta::parse("p1d").is_err());
5404 assert!(Timedelta::parse("P").is_err());
5405 assert!(Timedelta::parse("PT").is_err());
5406 }
5407
5408 #[test]
5409 fn timedelta_parse_time_format() {
5410 use super::Timedelta;
5411 let expected = Timedelta::NANOS_PER_HOUR
5412 + 30 * Timedelta::NANOS_PER_MIN
5413 + 45 * Timedelta::NANOS_PER_SEC;
5414 assert_eq!(Timedelta::parse("01:30:45").unwrap(), expected);
5415 }
5416
5417 #[test]
5418 fn timedelta_parse_time_fraction_rejects_unicode_without_panic() {
5419 use super::{Timedelta, TimedeltaError};
5420 let err = Timedelta::parse("00:00:00.\u{00e9}\u{00e9}\u{00e9}\u{00e9}\u{00e9}")
5421 .expect_err("non-ASCII fractional seconds must reject");
5422 assert!(matches!(err, TimedeltaError::InvalidFormat(_)));
5423 }
5424
5425 #[test]
5426 fn timedelta_parse_time_format_rejects_overflow_without_panic() {
5427 use super::{Timedelta, TimedeltaError};
5428 let err = Timedelta::parse("9223372036854775807:00")
5429 .expect_err("oversized hour component must reject");
5430 assert!(matches!(err, TimedeltaError::InvalidFormat(_)));
5431 }
5432
5433 #[test]
5434 fn timedelta_parse_rejects_huge_value_overflow_zw3mg() {
5435 use super::{Timedelta, TimedeltaError};
5442 let huge = format!("{} days", "9".repeat(18));
5443 assert!(matches!(
5444 Timedelta::parse(&huge).expect_err("9...(18 9s) days must overflow"),
5445 TimedeltaError::Overflow
5446 ));
5447 }
5448
5449 #[test]
5450 fn timedelta_parse_nat() {
5451 use super::Timedelta;
5452 assert_eq!(Timedelta::parse("NaT").unwrap(), Timedelta::NAT);
5453 assert_eq!(Timedelta::parse("nat").unwrap(), Timedelta::NAT);
5454 }
5455
5456 #[test]
5457 fn timedelta_parse_negative() {
5458 use super::Timedelta;
5459 assert_eq!(Timedelta::parse("-1d").unwrap(), -Timedelta::NANOS_PER_DAY);
5460 }
5461
5462 #[test]
5463 fn timedelta_components() {
5464 use super::Timedelta;
5465 let nanos = Timedelta::NANOS_PER_DAY
5466 + Timedelta::NANOS_PER_HOUR
5467 + Timedelta::NANOS_PER_MIN
5468 + Timedelta::NANOS_PER_SEC
5469 + Timedelta::NANOS_PER_MILLI
5470 + 2 * Timedelta::NANOS_PER_MICRO
5471 + 3;
5472 let comp = Timedelta::components(nanos);
5473 assert_eq!(comp.days, 1);
5474 assert_eq!(comp.hours, 1);
5475 assert_eq!(comp.minutes, 1);
5476 assert_eq!(comp.seconds, 1);
5477 assert_eq!(comp.milliseconds, 1);
5478 assert_eq!(comp.microseconds, 2);
5479 assert_eq!(comp.nanoseconds, 3);
5480 }
5481
5482 #[test]
5483 fn timedelta_negative_components_floor_div() {
5484 use super::Timedelta;
5485 let neg_1s = -Timedelta::NANOS_PER_SEC;
5488 assert_eq!(Timedelta::days(neg_1s), -1);
5489 assert_eq!(Timedelta::seconds(neg_1s), 86399);
5490 assert_eq!(Timedelta::microseconds(neg_1s), 0);
5491 assert_eq!(Timedelta::nanoseconds(neg_1s), 0);
5492 let comp = Timedelta::components(neg_1s);
5493 assert_eq!(
5494 (
5495 comp.days,
5496 comp.hours,
5497 comp.minutes,
5498 comp.seconds,
5499 comp.milliseconds,
5500 comp.microseconds,
5501 comp.nanoseconds
5502 ),
5503 (-1, 23, 59, 59, 0, 0, 0)
5504 );
5505
5506 let neg = -86_401 * Timedelta::NANOS_PER_SEC;
5508 assert_eq!(Timedelta::days(neg), -2);
5509 assert_eq!(Timedelta::seconds(neg), 86399);
5510 }
5511
5512 #[test]
5513 fn timedelta_total_seconds() {
5514 use super::Timedelta;
5515 let nanos = 90_000_000_000i64; assert!((Timedelta::total_seconds(nanos) - 90.0).abs() < 1e-9);
5517 assert!(Timedelta::total_seconds(Timedelta::NAT).is_nan());
5518 }
5519
5520 #[test]
5521 fn timedelta_format_basic() {
5522 use super::Timedelta;
5523 assert_eq!(Timedelta::format(Timedelta::NAT), "NaT");
5524 assert_eq!(
5525 Timedelta::format(Timedelta::NANOS_PER_DAY),
5526 "1 days 00:00:00"
5527 );
5528 assert_eq!(
5529 Timedelta::format(Timedelta::NANOS_PER_DAY + 2 * Timedelta::NANOS_PER_HOUR),
5530 "1 days 02:00:00"
5531 );
5532 }
5533
5534 #[test]
5535 fn timedelta_format_subsecond_matches_pandas() {
5536 use super::Timedelta;
5537 assert_eq!(
5541 Timedelta::format(1_500_000_000), "0 days 00:00:01.500000"
5543 );
5544 assert_eq!(
5545 Timedelta::format(1_000_000), "0 days 00:00:00.001000"
5547 );
5548 assert_eq!(
5549 Timedelta::format(123_456_000), "0 days 00:00:00.123456"
5551 );
5552 assert_eq!(
5554 Timedelta::format(500), "0 days 00:00:00.000000500"
5556 );
5557 assert_eq!(Timedelta::format(123_456_789), "0 days 00:00:00.123456789");
5558 }
5559
5560 #[test]
5561 fn timedelta_format_negative_uses_python_borrow_form() {
5562 use super::Timedelta;
5563 assert_eq!(Timedelta::format(-1_000_000_000), "-1 days +23:59:59");
5567 assert_eq!(
5568 Timedelta::format(-Timedelta::NANOS_PER_DAY),
5569 "-1 days +00:00:00"
5570 );
5571 assert_eq!(
5572 Timedelta::format(-25 * Timedelta::NANOS_PER_HOUR),
5573 "-2 days +23:00:00"
5574 );
5575 assert_eq!(
5576 Timedelta::format(-1_500_000_000),
5577 "-1 days +23:59:58.500000"
5578 );
5579 assert_eq!(Timedelta::format(-500), "-1 days +23:59:59.999999500");
5580 assert_eq!(Timedelta::format(-1), "-1 days +23:59:59.999999999");
5581 }
5582
5583 #[test]
5584 fn timedelta_isoformat_basic() {
5585 use super::Timedelta;
5586 assert_eq!(Timedelta::isoformat(Timedelta::NAT), "NaT");
5587 assert_eq!(Timedelta::isoformat(0), "P0DT0H0M0S");
5588 assert_eq!(Timedelta::isoformat(Timedelta::NANOS_PER_DAY), "P1DT0H0M0S");
5589 assert_eq!(
5590 Timedelta::isoformat(
5591 Timedelta::NANOS_PER_DAY
5592 + 2 * Timedelta::NANOS_PER_HOUR
5593 + 30 * Timedelta::NANOS_PER_MIN
5594 + 45 * Timedelta::NANOS_PER_SEC
5595 ),
5596 "P1DT2H30M45S"
5597 );
5598 assert_eq!(
5599 Timedelta::isoformat(Timedelta::NANOS_PER_SEC + 500_000_000),
5600 "P0DT0H0M1.5S"
5601 );
5602 assert_eq!(
5603 Timedelta::isoformat(-(Timedelta::NANOS_PER_DAY + Timedelta::NANOS_PER_HOUR)),
5604 "-P1DT1H0M0S"
5605 );
5606 }
5607
5608 #[test]
5609 fn timedelta_floor_ceil_round() {
5610 use super::Timedelta;
5611 let nanos = Timedelta::NANOS_PER_HOUR + 30 * Timedelta::NANOS_PER_MIN;
5612
5613 assert_eq!(Timedelta::floor(nanos, "h"), Timedelta::NANOS_PER_HOUR);
5615 assert_eq!(Timedelta::floor(nanos, "d"), 0);
5616
5617 assert_eq!(Timedelta::ceil(nanos, "h"), 2 * Timedelta::NANOS_PER_HOUR);
5619 assert_eq!(Timedelta::ceil(nanos, "d"), Timedelta::NANOS_PER_DAY);
5620
5621 assert_eq!(Timedelta::round(nanos, "h"), 2 * Timedelta::NANOS_PER_HOUR);
5623
5624 assert_eq!(Timedelta::floor(Timedelta::NAT, "h"), Timedelta::NAT);
5626 assert_eq!(Timedelta::ceil(Timedelta::NAT, "h"), Timedelta::NAT);
5627 assert_eq!(Timedelta::round(Timedelta::NAT, "h"), Timedelta::NAT);
5628
5629 assert_eq!(Timedelta::floor(nanos, "invalid"), Timedelta::NAT);
5631 }
5632
5633 #[test]
5634 fn timedelta_scalar_dtype() {
5635 let td = Scalar::Timedelta64(86_400_000_000_000);
5636 assert_eq!(td.dtype(), DType::Timedelta64);
5637 }
5638
5639 #[test]
5640 fn timedelta_scalar_is_missing() {
5641 use super::Timedelta;
5642 let valid = Scalar::Timedelta64(1000);
5643 let nat = Scalar::Timedelta64(Timedelta::NAT);
5644 assert!(!valid.is_missing());
5645 assert!(nat.is_missing());
5646 }
5647
5648 #[test]
5649 fn dtype_utf8_deserializes_legacy_aliases() {
5650 let dtype: DType = serde_json::from_str("\"str\"").unwrap();
5651 assert_eq!(dtype, DType::Utf8);
5652
5653 let dtype: DType = serde_json::from_str("\"string\"").unwrap();
5654 assert_eq!(dtype, DType::Utf8);
5655 }
5656
5657 #[test]
5658 fn scalar_utf8_deserializes_legacy_aliases() {
5659 let scalar: Scalar = serde_json::from_str(r#"{"kind":"str","value":"x"}"#).unwrap();
5660 assert_eq!(scalar, Scalar::Utf8("x".to_owned()));
5661
5662 let scalar: Scalar = serde_json::from_str(r#"{"kind":"string","value":"y"}"#).unwrap();
5663 assert_eq!(scalar, Scalar::Utf8("y".to_owned()));
5664 }
5665
5666 #[test]
5667 fn nancumsum_skips_nulls_and_accumulates() {
5668 let values = vec![
5669 Scalar::Float64(1.0),
5670 Scalar::Null(NullKind::NaN),
5671 Scalar::Float64(2.0),
5672 Scalar::Float64(3.0),
5673 ];
5674 let out = super::nancumsum(&values);
5675 assert!(matches!(out[0], Scalar::Float64(v) if (v - 1.0).abs() < 1e-9));
5676 assert!(out[1].is_missing());
5677 assert!(matches!(out[2], Scalar::Float64(v) if (v - 3.0).abs() < 1e-9));
5678 assert!(matches!(out[3], Scalar::Float64(v) if (v - 6.0).abs() < 1e-9));
5679 }
5680
5681 #[test]
5682 fn nancumprod_skips_nulls_and_multiplies() {
5683 let values = vec![
5684 Scalar::Float64(2.0),
5685 Scalar::Null(NullKind::NaN),
5686 Scalar::Float64(3.0),
5687 Scalar::Float64(4.0),
5688 ];
5689 let out = super::nancumprod(&values);
5690 assert!(matches!(out[0], Scalar::Float64(v) if (v - 2.0).abs() < 1e-9));
5691 assert!(out[1].is_missing());
5692 assert!(matches!(out[2], Scalar::Float64(v) if (v - 6.0).abs() < 1e-9));
5693 assert!(matches!(out[3], Scalar::Float64(v) if (v - 24.0).abs() < 1e-9));
5694 }
5695
5696 #[test]
5697 fn nancummax_tracks_running_max() {
5698 let values = vec![
5699 Scalar::Float64(1.0),
5700 Scalar::Float64(3.0),
5701 Scalar::Null(NullKind::NaN),
5702 Scalar::Float64(2.0),
5703 Scalar::Float64(5.0),
5704 ];
5705 let out = super::nancummax(&values);
5706 assert_eq!(out[0], Scalar::Float64(1.0));
5707 assert_eq!(out[1], Scalar::Float64(3.0));
5708 assert!(out[2].is_missing());
5709 assert_eq!(out[3], Scalar::Float64(3.0));
5710 assert_eq!(out[4], Scalar::Float64(5.0));
5711 }
5712
5713 #[test]
5714 fn nancummin_tracks_running_min() {
5715 let values = vec![
5716 Scalar::Float64(5.0),
5717 Scalar::Float64(3.0),
5718 Scalar::Null(NullKind::NaN),
5719 Scalar::Float64(4.0),
5720 Scalar::Float64(1.0),
5721 ];
5722 let out = super::nancummin(&values);
5723 assert_eq!(out[0], Scalar::Float64(5.0));
5724 assert_eq!(out[1], Scalar::Float64(3.0));
5725 assert!(out[2].is_missing());
5726 assert_eq!(out[3], Scalar::Float64(3.0));
5727 assert_eq!(out[4], Scalar::Float64(1.0));
5728 }
5729
5730 #[test]
5731 fn nancumsum_timedelta64_preserves_dtype_x0x91() {
5732 let one_hour = 3_600 * 1_000_000_000_i64;
5735 let values = vec![
5736 Scalar::Timedelta64(one_hour),
5737 Scalar::Timedelta64(2 * one_hour),
5738 Scalar::Timedelta64(3 * one_hour),
5739 ];
5740 let out = super::nancumsum(&values);
5741 assert_eq!(out[0], Scalar::Timedelta64(one_hour));
5742 assert_eq!(out[1], Scalar::Timedelta64(3 * one_hour));
5743 assert_eq!(out[2], Scalar::Timedelta64(6 * one_hour));
5744 }
5745
5746 #[test]
5747 fn nancummax_nancummin_timedelta64_preserves_dtype_x0x91() {
5748 let one_hour = 3_600 * 1_000_000_000_i64;
5749 let values = vec![
5750 Scalar::Timedelta64(2 * one_hour),
5751 Scalar::Timedelta64(5 * one_hour),
5752 Scalar::Timedelta64(one_hour),
5753 Scalar::Timedelta64(3 * one_hour),
5754 ];
5755 let mx = super::nancummax(&values);
5756 assert_eq!(mx[0], Scalar::Timedelta64(2 * one_hour));
5757 assert_eq!(mx[1], Scalar::Timedelta64(5 * one_hour));
5758 assert_eq!(mx[2], Scalar::Timedelta64(5 * one_hour));
5759 assert_eq!(mx[3], Scalar::Timedelta64(5 * one_hour));
5760
5761 let mn = super::nancummin(&values);
5762 assert_eq!(mn[0], Scalar::Timedelta64(2 * one_hour));
5763 assert_eq!(mn[1], Scalar::Timedelta64(2 * one_hour));
5764 assert_eq!(mn[2], Scalar::Timedelta64(one_hour));
5765 assert_eq!(mn[3], Scalar::Timedelta64(one_hour));
5766 }
5767
5768 #[test]
5769 fn nancumulative_timedelta64_skips_nat_x0x91() {
5770 let one_hour = 3_600 * 1_000_000_000_i64;
5771 let values = vec![
5772 Scalar::Timedelta64(one_hour),
5773 Scalar::Timedelta64(Timedelta::NAT),
5774 Scalar::Timedelta64(2 * one_hour),
5775 ];
5776 let cs = super::nancumsum(&values);
5777 assert_eq!(cs[0], Scalar::Timedelta64(one_hour));
5778 assert!(cs[1].is_missing());
5779 assert_eq!(cs[2], Scalar::Timedelta64(3 * one_hour));
5780 }
5781
5782 #[test]
5783 fn nanquantile_linear_interpolation_matches_numpy() {
5784 let values = vec![
5785 Scalar::Float64(1.0),
5786 Scalar::Float64(2.0),
5787 Scalar::Float64(3.0),
5788 Scalar::Float64(4.0),
5789 Scalar::Float64(5.0),
5790 ];
5791 let q = super::nanquantile(&values, 0.5);
5793 assert!(matches!(q, Scalar::Float64(v) if (v - 3.0).abs() < 1e-9));
5794 let q25 = super::nanquantile(&values, 0.25);
5796 assert!(matches!(q25, Scalar::Float64(v) if (v - 2.0).abs() < 1e-9));
5797 }
5798
5799 #[test]
5800 fn nanquantile_ignores_nulls() {
5801 let values = vec![
5802 Scalar::Float64(1.0),
5803 Scalar::Null(NullKind::NaN),
5804 Scalar::Float64(3.0),
5805 ];
5806 let q = super::nanquantile(&values, 0.5);
5807 assert!(matches!(q, Scalar::Float64(v) if (v - 2.0).abs() < 1e-9));
5808 }
5809
5810 #[test]
5811 fn nanquantile_empty_and_out_of_range_yield_null() {
5812 assert!(super::nanquantile(&[], 0.5).is_missing());
5813 assert!(super::nanquantile(&[Scalar::Float64(1.0)], 1.5).is_missing());
5814 assert!(super::nanquantile(&[Scalar::Float64(1.0)], -0.1).is_missing());
5815 }
5816
5817 #[test]
5818 fn nanquantile_timedelta64_preserves_dtype_5djk7() {
5819 let one_hour: i64 = 3_600 * 1_000_000_000;
5822 let vals = vec![
5823 Scalar::Timedelta64(one_hour),
5824 Scalar::Timedelta64(2 * one_hour),
5825 Scalar::Timedelta64(3 * one_hour),
5826 Scalar::Timedelta64(4 * one_hour),
5827 Scalar::Timedelta64(5 * one_hour),
5828 ];
5829 assert_eq!(
5830 super::nanquantile(&vals, 0.5),
5831 Scalar::Timedelta64(3 * one_hour)
5832 );
5833 assert_eq!(
5834 super::nanquantile(&vals, 0.0),
5835 Scalar::Timedelta64(one_hour)
5836 );
5837 assert_eq!(
5838 super::nanquantile(&vals, 1.0),
5839 Scalar::Timedelta64(5 * one_hour)
5840 );
5841 }
5842
5843 #[test]
5844 fn nanquantile_timedelta64_linear_interpolation_5djk7() {
5845 let one_hour: i64 = 3_600 * 1_000_000_000;
5846 let vals = vec![
5847 Scalar::Timedelta64(one_hour),
5848 Scalar::Timedelta64(3 * one_hour),
5849 ];
5850 assert_eq!(
5852 super::nanquantile(&vals, 0.5),
5853 Scalar::Timedelta64(2 * one_hour)
5854 );
5855 }
5856
5857 #[test]
5858 fn nanargmax_returns_first_position() {
5859 let values = vec![
5860 Scalar::Float64(1.0),
5861 Scalar::Null(NullKind::NaN),
5862 Scalar::Float64(4.0),
5863 Scalar::Float64(4.0),
5864 Scalar::Float64(2.0),
5865 ];
5866 assert_eq!(super::nanargmax(&values), Some(2));
5867 }
5868
5869 #[test]
5870 fn nanargmin_returns_first_position() {
5871 let values = vec![
5872 Scalar::Float64(3.0),
5873 Scalar::Null(NullKind::NaN),
5874 Scalar::Float64(1.0),
5875 Scalar::Float64(1.0),
5876 ];
5877 assert_eq!(super::nanargmin(&values), Some(2));
5878 }
5879
5880 #[test]
5881 fn nanargmax_all_missing_returns_none() {
5882 let values = vec![Scalar::Null(NullKind::NaN), Scalar::Null(NullKind::Null)];
5883 assert_eq!(super::nanargmax(&values), None);
5884 assert_eq!(super::nanargmin(&values), None);
5885 }
5886
5887 #[test]
5888 fn nansem_matches_std_over_sqrt_n() {
5889 let values = vec![
5890 Scalar::Float64(2.0),
5891 Scalar::Float64(4.0),
5892 Scalar::Float64(4.0),
5893 Scalar::Float64(4.0),
5894 Scalar::Float64(5.0),
5895 Scalar::Float64(5.0),
5896 Scalar::Float64(7.0),
5897 Scalar::Float64(9.0),
5898 ];
5899 let sem = super::nansem(&values, 1);
5901 assert!(matches!(sem, Scalar::Float64(_)));
5902 let Scalar::Float64(v) = sem else {
5903 return;
5904 };
5905 assert!((v - 0.7559289460184544).abs() < 1e-9);
5906 }
5907
5908 #[test]
5909 fn nansem_empty_returns_null() {
5910 assert!(super::nansem(&[], 1).is_missing());
5911 assert!(super::nansem(&[Scalar::Float64(1.0)], 1).is_missing());
5912 }
5913
5914 #[test]
5915 fn nanptp_returns_max_minus_min() {
5916 let values = vec![
5917 Scalar::Float64(3.0),
5918 Scalar::Null(NullKind::NaN),
5919 Scalar::Float64(7.0),
5920 Scalar::Float64(1.0),
5921 ];
5922 assert_eq!(super::nanptp(&values), Scalar::Float64(6.0));
5923 }
5924
5925 #[test]
5926 fn nanptp_empty_returns_null() {
5927 assert!(super::nanptp(&[]).is_missing());
5928 assert!(super::nanptp(&[Scalar::Null(NullKind::NaN)]).is_missing());
5929 }
5930
5931 #[test]
5932 fn nanptp_timedelta64_preserves_dtype_u2g0r() {
5933 let one_hour: i64 = 3_600 * 1_000_000_000;
5935 let values = vec![
5936 Scalar::Timedelta64(one_hour),
5937 Scalar::Timedelta64(5 * one_hour),
5938 Scalar::Timedelta64(2 * one_hour),
5939 ];
5940 assert_eq!(super::nanptp(&values), Scalar::Timedelta64(4 * one_hour));
5941 }
5942
5943 #[test]
5944 fn nanargmax_nanargmin_timedelta64_compare_by_ns_ql1t5() {
5945 let one_hour: i64 = 3_600 * 1_000_000_000;
5948 let values = vec![
5949 Scalar::Timedelta64(2 * one_hour),
5950 Scalar::Timedelta64(5 * one_hour),
5951 Scalar::Timedelta64(one_hour),
5952 Scalar::Timedelta64(3 * one_hour),
5953 ];
5954 assert_eq!(super::nanargmax(&values), Some(1));
5955 assert_eq!(super::nanargmin(&values), Some(2));
5956 }
5957
5958 #[test]
5959 fn nanprod_timedelta64_returns_null_szq6a() {
5960 let one_hour: i64 = 3_600 * 1_000_000_000;
5964 let values = vec![
5965 Scalar::Timedelta64(2 * one_hour),
5966 Scalar::Timedelta64(3 * one_hour),
5967 ];
5968 assert!(super::nanprod(&values).is_missing());
5969 }
5970
5971 #[test]
5972 fn nanskew_symmetric_distribution_near_zero() {
5973 let values = vec![
5974 Scalar::Float64(1.0),
5975 Scalar::Float64(2.0),
5976 Scalar::Float64(3.0),
5977 Scalar::Float64(4.0),
5978 Scalar::Float64(5.0),
5979 ];
5980 let skew = super::nanskew(&values);
5982 assert!(matches!(skew, Scalar::Float64(_)));
5983 let Scalar::Float64(v) = skew else {
5984 return;
5985 };
5986 assert!(v.abs() < 1e-9);
5987 }
5988
5989 #[test]
5990 fn nanskew_too_few_values_returns_null() {
5991 assert!(super::nanskew(&[]).is_missing());
5992 assert!(super::nanskew(&[Scalar::Float64(1.0), Scalar::Float64(2.0)]).is_missing());
5993 }
5994
5995 #[test]
5996 fn nankurt_symmetric_uniform_distribution() {
5997 let values = vec![
5998 Scalar::Float64(1.0),
5999 Scalar::Float64(2.0),
6000 Scalar::Float64(3.0),
6001 Scalar::Float64(4.0),
6002 Scalar::Float64(5.0),
6003 ];
6004 let kurt = super::nankurt(&values);
6006 assert!(matches!(kurt, Scalar::Float64(_)));
6007 let Scalar::Float64(v) = kurt else {
6008 return;
6009 };
6010 assert!((v + 1.2).abs() < 1e-9);
6011 }
6012
6013 #[test]
6014 fn nankurt_too_few_values_returns_null() {
6015 let vals: Vec<Scalar> = (0..3).map(|i| Scalar::Float64(i as f64)).collect();
6016 assert!(super::nankurt(&vals).is_missing());
6017 }
6018
6019 #[test]
6020 fn nanskew_constant_series_returns_zero() {
6021 let values = vec![
6022 Scalar::Float64(5.0),
6023 Scalar::Float64(5.0),
6024 Scalar::Float64(5.0),
6025 ];
6026 assert_eq!(super::nanskew(&values), Scalar::Float64(0.0));
6027 assert_eq!(
6028 super::nankurt(&[
6029 Scalar::Float64(5.0),
6030 Scalar::Float64(5.0),
6031 Scalar::Float64(5.0),
6032 Scalar::Float64(5.0),
6033 ]),
6034 Scalar::Float64(0.0)
6035 );
6036 }
6037
6038 #[test]
6041 fn interval_default_closed_is_right() {
6042 assert_eq!(IntervalClosed::default(), IntervalClosed::Right);
6043 }
6044
6045 #[test]
6046 fn interval_left_and_right_closed_helpers() {
6047 assert!(IntervalClosed::Left.left_closed());
6048 assert!(!IntervalClosed::Left.right_closed());
6049 assert!(!IntervalClosed::Right.left_closed());
6050 assert!(IntervalClosed::Right.right_closed());
6051 assert!(IntervalClosed::Both.left_closed());
6052 assert!(IntervalClosed::Both.right_closed());
6053 assert!(!IntervalClosed::Neither.left_closed());
6054 assert!(!IntervalClosed::Neither.right_closed());
6055 }
6056
6057 #[test]
6058 fn interval_display_matches_pandas_notation() {
6059 assert_eq!(
6060 Interval::new(0.0, 5.0, IntervalClosed::Right).to_string(),
6061 "(0, 5]"
6062 );
6063 assert_eq!(
6064 Interval::new(0.0, 5.0, IntervalClosed::Left).to_string(),
6065 "[0, 5)"
6066 );
6067 assert_eq!(
6068 Interval::new(0.0, 5.0, IntervalClosed::Both).to_string(),
6069 "[0, 5]"
6070 );
6071 assert_eq!(
6072 Interval::new(0.0, 5.0, IntervalClosed::Neither).to_string(),
6073 "(0, 5)"
6074 );
6075 }
6076
6077 #[test]
6078 fn interval_length_and_mid() {
6079 let i = Interval::new(2.0, 10.0, IntervalClosed::Right);
6080 assert_eq!(i.length(), 8.0);
6081 assert_eq!(i.mid(), 6.0);
6082 }
6083
6084 #[test]
6085 fn interval_contains_matches_closed_policy() {
6086 let right = Interval::new(0.0, 5.0, IntervalClosed::Right);
6087 assert!(!right.contains(0.0));
6088 assert!(right.contains(2.5));
6089 assert!(right.contains(5.0));
6090
6091 let left = Interval::new(0.0, 5.0, IntervalClosed::Left);
6092 assert!(left.contains(0.0));
6093 assert!(left.contains(2.5));
6094 assert!(!left.contains(5.0));
6095
6096 let both = Interval::new(0.0, 5.0, IntervalClosed::Both);
6097 assert!(both.contains(0.0));
6098 assert!(both.contains(5.0));
6099
6100 let neither = Interval::new(0.0, 5.0, IntervalClosed::Neither);
6101 assert!(!neither.contains(0.0));
6102 assert!(!neither.contains(5.0));
6103 assert!(neither.contains(2.5));
6104 }
6105
6106 #[test]
6107 fn interval_contains_nan_returns_false() {
6108 let i = Interval::new(0.0, 10.0, IntervalClosed::Both);
6109 assert!(!i.contains(f64::NAN));
6110 }
6111
6112 #[test]
6113 fn interval_is_empty_matches_pandas() {
6114 assert!(Interval::new(3.0, 3.0, IntervalClosed::Right).is_empty());
6116 assert!(Interval::new(3.0, 3.0, IntervalClosed::Left).is_empty());
6117 assert!(Interval::new(3.0, 3.0, IntervalClosed::Neither).is_empty());
6118 assert!(!Interval::new(3.0, 3.0, IntervalClosed::Both).is_empty());
6120 assert!(!Interval::new(0.0, 5.0, IntervalClosed::Right).is_empty());
6122 }
6123
6124 #[test]
6125 fn interval_overlaps_disjoint_returns_false() {
6126 let a = Interval::new(0.0, 1.0, IntervalClosed::Right);
6127 let b = Interval::new(2.0, 3.0, IntervalClosed::Right);
6128 assert!(!a.overlaps(&b));
6129 assert!(!b.overlaps(&a));
6130 }
6131
6132 #[test]
6133 fn interval_overlaps_nested_returns_true() {
6134 let outer = Interval::new(0.0, 10.0, IntervalClosed::Right);
6135 let inner = Interval::new(3.0, 7.0, IntervalClosed::Right);
6136 assert!(outer.overlaps(&inner));
6137 assert!(inner.overlaps(&outer));
6138 }
6139
6140 #[test]
6141 fn interval_overlaps_touching_respects_closed_policy() {
6142 let right_right = (
6144 Interval::new(0.0, 1.0, IntervalClosed::Right),
6145 Interval::new(1.0, 2.0, IntervalClosed::Right),
6146 );
6147 assert!(!right_right.0.overlaps(&right_right.1));
6149
6150 let both_both = (
6152 Interval::new(0.0, 1.0, IntervalClosed::Both),
6153 Interval::new(1.0, 2.0, IntervalClosed::Both),
6154 );
6155 assert!(both_both.0.overlaps(&both_both.1));
6156 }
6157
6158 #[test]
6159 fn interval_roundtrips_through_serde_json() {
6160 let i = Interval::new(1.5, 3.25, IntervalClosed::Both);
6161 let json = serde_json::to_string(&i).expect("serialize");
6162 let back: Interval = serde_json::from_str(&json).expect("deserialize");
6163 assert_eq!(i, back);
6164 }
6165
6166 #[test]
6167 fn interval_serde_default_closed_is_right_when_missing() {
6168 let back: Interval =
6170 serde_json::from_str(r#"{"left":0.0,"right":5.0}"#).expect("deserialize");
6171 assert_eq!(back.closed, IntervalClosed::Right);
6172 }
6173
6174 #[test]
6177 fn period_freq_parses_canonical_aliases() {
6178 assert_eq!(PeriodFreq::parse("A"), Some(PeriodFreq::Annual));
6179 assert_eq!(PeriodFreq::parse("Y"), Some(PeriodFreq::Annual));
6180 assert_eq!(PeriodFreq::parse("Q"), Some(PeriodFreq::Quarterly));
6181 assert_eq!(PeriodFreq::parse("M"), Some(PeriodFreq::Monthly));
6182 assert_eq!(PeriodFreq::parse("W"), Some(PeriodFreq::Weekly));
6183 assert_eq!(PeriodFreq::parse("D"), Some(PeriodFreq::Daily));
6184 assert_eq!(PeriodFreq::parse("B"), Some(PeriodFreq::Business));
6185 assert_eq!(PeriodFreq::parse("H"), Some(PeriodFreq::Hourly));
6186 assert_eq!(PeriodFreq::parse("T"), Some(PeriodFreq::Minutely));
6187 assert_eq!(PeriodFreq::parse("min"), Some(PeriodFreq::Minutely));
6188 assert_eq!(PeriodFreq::parse("S"), Some(PeriodFreq::Secondly));
6189 }
6190
6191 #[test]
6192 fn period_freq_parse_is_case_insensitive() {
6193 assert_eq!(PeriodFreq::parse("quarterly"), Some(PeriodFreq::Quarterly));
6194 assert_eq!(PeriodFreq::parse("MONTHLY"), Some(PeriodFreq::Monthly));
6195 }
6196
6197 #[test]
6198 fn period_freq_rejects_unknown_aliases() {
6199 assert_eq!(PeriodFreq::parse("nanosec"), None);
6200 assert_eq!(PeriodFreq::parse(""), None);
6201 assert_eq!(PeriodFreq::parse("xyz"), None);
6202 }
6203
6204 #[test]
6205 fn period_freq_alias_roundtrip() {
6206 for freq in [
6207 PeriodFreq::Annual,
6208 PeriodFreq::Quarterly,
6209 PeriodFreq::Monthly,
6210 PeriodFreq::Weekly,
6211 PeriodFreq::Daily,
6212 PeriodFreq::Business,
6213 PeriodFreq::Hourly,
6214 PeriodFreq::Minutely,
6215 PeriodFreq::Secondly,
6216 ] {
6217 assert_eq!(PeriodFreq::parse(freq.alias()), Some(freq));
6218 }
6219 }
6220
6221 #[test]
6222 fn period_freq_anchored_aliases_are_pandas_canonical_h2wiv() {
6223 assert_eq!(PeriodFreq::Annual.alias(), "Y-DEC");
6224 assert_eq!(PeriodFreq::Quarterly.alias(), "Q-DEC");
6225 assert_eq!(PeriodFreq::Weekly.alias(), "W-SUN");
6226
6227 assert_eq!(PeriodFreq::parse("A"), Some(PeriodFreq::Annual));
6228 assert_eq!(PeriodFreq::parse("Y"), Some(PeriodFreq::Annual));
6229 assert_eq!(PeriodFreq::parse("Y-DEC"), Some(PeriodFreq::Annual));
6230 assert_eq!(PeriodFreq::parse("Q"), Some(PeriodFreq::Quarterly));
6231 assert_eq!(PeriodFreq::parse("Q-DEC"), Some(PeriodFreq::Quarterly));
6232 assert_eq!(PeriodFreq::parse("W"), Some(PeriodFreq::Weekly));
6233 assert_eq!(PeriodFreq::parse("W-SUN"), Some(PeriodFreq::Weekly));
6234 }
6235
6236 #[test]
6237 fn period_freq_intraday_aliases_are_pandas_canonical_8kfdo() {
6238 assert_eq!(PeriodFreq::Hourly.alias(), "h");
6239 assert_eq!(PeriodFreq::Minutely.alias(), "min");
6240 assert_eq!(PeriodFreq::Secondly.alias(), "s");
6241
6242 assert_eq!(PeriodFreq::parse("H"), Some(PeriodFreq::Hourly));
6243 assert_eq!(PeriodFreq::parse("T"), Some(PeriodFreq::Minutely));
6244 assert_eq!(PeriodFreq::parse("S"), Some(PeriodFreq::Secondly));
6245 }
6246
6247 #[test]
6248 fn period_scalar_accessors_match_pandas_star8() {
6249 let period = Period::new(600, PeriodFreq::Monthly);
6250
6251 assert_eq!(period.ordinal(), 600);
6252 assert_eq!(period.freq(), PeriodFreq::Monthly);
6253 assert_eq!(period.freqstr(), "M");
6254 }
6255
6256 #[test]
6257 fn period_parse_common_pandas_ordinals_avm08() {
6258 assert_eq!(
6259 Period::parse("2024").unwrap(),
6260 Period::new(54, PeriodFreq::Annual)
6261 );
6262 assert_eq!(
6263 Period::parse("2024Q1").unwrap(),
6264 Period::new(216, PeriodFreq::Quarterly)
6265 );
6266 assert_eq!(
6267 Period::parse("2024-01").unwrap(),
6268 Period::new(648, PeriodFreq::Monthly)
6269 );
6270 assert_eq!(
6271 Period::parse("2024-01-15").unwrap(),
6272 Period::new(19_737, PeriodFreq::Daily)
6273 );
6274 assert!(Period::parse("216").is_err());
6275 }
6276
6277 #[test]
6278 fn period_shift_advances_ordinal() {
6279 let q1 = Period::new(216, PeriodFreq::Quarterly);
6280 let q2 = q1.shift(1);
6281 assert_eq!(q2.ordinal, 217);
6282 assert_eq!(q2.freq, PeriodFreq::Quarterly);
6283 let q0 = q1.shift(-1);
6284 assert_eq!(q0.ordinal, 215);
6285 }
6286
6287 #[test]
6288 fn period_shift_saturates_on_overflow() {
6289 let p = Period::new(i64::MAX - 2, PeriodFreq::Daily);
6290 assert_eq!(p.shift(100).ordinal, i64::MAX);
6291 let p = Period::new(i64::MIN + 2, PeriodFreq::Daily);
6292 assert_eq!(p.shift(-100).ordinal, i64::MIN);
6293 }
6294
6295 #[test]
6296 fn period_diff_returns_period_count() {
6297 let a = Period::new(216, PeriodFreq::Quarterly);
6298 let b = Period::new(220, PeriodFreq::Quarterly);
6299 assert_eq!(b.diff(&a), Some(4));
6300 assert_eq!(a.diff(&b), Some(-4));
6301 }
6302
6303 #[test]
6304 fn period_diff_rejects_mismatched_freq() {
6305 let monthly = Period::new(100, PeriodFreq::Monthly);
6306 let quarterly = Period::new(100, PeriodFreq::Quarterly);
6307 assert_eq!(monthly.diff(&quarterly), None);
6308 assert_eq!(quarterly.diff(&monthly), None);
6309 }
6310
6311 #[test]
6312 fn period_cmp_same_freq_respects_ordinal_order() {
6313 use std::cmp::Ordering;
6314 let a = Period::new(10, PeriodFreq::Monthly);
6315 let b = Period::new(20, PeriodFreq::Monthly);
6316 assert_eq!(a.cmp_same_freq(&b), Some(Ordering::Less));
6317 assert_eq!(b.cmp_same_freq(&a), Some(Ordering::Greater));
6318 assert_eq!(a.cmp_same_freq(&a), Some(Ordering::Equal));
6319 }
6320
6321 #[test]
6322 fn period_cmp_cross_freq_returns_none() {
6323 let m = Period::new(1, PeriodFreq::Monthly);
6324 let q = Period::new(1, PeriodFreq::Quarterly);
6325 assert_eq!(m.cmp_same_freq(&q), None);
6326 }
6327
6328 #[test]
6329 fn period_display_carries_freq_and_ordinal() {
6330 let p = Period::new(216, PeriodFreq::Quarterly);
6331 assert_eq!(p.to_string(), "Period[Q-DEC, 216]");
6332 }
6333
6334 #[test]
6335 fn period_roundtrips_through_serde_json() {
6336 let p = Period::new(42, PeriodFreq::Weekly);
6337 let json = serde_json::to_string(&p).expect("serialize");
6338 let back: Period = serde_json::from_str(&json).expect("deserialize");
6339 assert_eq!(p, back);
6340 }
6341
6342 use super::period_range;
6345
6346 #[test]
6347 fn period_range_zero_periods_is_empty() {
6348 let start = Period::new(216, PeriodFreq::Quarterly);
6349 assert!(period_range(start, 0).is_empty());
6350 }
6351
6352 #[test]
6353 fn period_range_single_period_returns_start_only() {
6354 let start = Period::new(216, PeriodFreq::Quarterly);
6355 let r = period_range(start, 1);
6356 assert_eq!(r.len(), 1);
6357 assert_eq!(r[0], start);
6358 }
6359
6360 #[test]
6361 fn period_range_increments_ordinal_by_one_per_step() {
6362 let start = Period::new(216, PeriodFreq::Quarterly);
6363 let r = period_range(start, 4);
6364 assert_eq!(r.len(), 4);
6365 assert_eq!(r[0].ordinal, 216);
6366 assert_eq!(r[1].ordinal, 217);
6367 assert_eq!(r[2].ordinal, 218);
6368 assert_eq!(r[3].ordinal, 219);
6369 }
6370
6371 #[test]
6372 fn period_range_preserves_frequency() {
6373 let start = Period::new(0, PeriodFreq::Monthly);
6374 let r = period_range(start, 12);
6375 assert!(r.iter().all(|p| p.freq == PeriodFreq::Monthly));
6376 }
6377
6378 #[test]
6379 fn period_range_negative_starting_ordinal_works() {
6380 let start = Period::new(-3, PeriodFreq::Annual);
6382 let r = period_range(start, 5);
6383 assert_eq!(
6384 r.iter().map(|p| p.ordinal).collect::<Vec<_>>(),
6385 vec![-3, -2, -1, 0, 1]
6386 );
6387 }
6388
6389 #[test]
6390 fn period_range_large_n_does_not_panic() {
6391 let start = Period::new(0, PeriodFreq::Monthly);
6393 let r = period_range(start, 1024);
6394 assert_eq!(r.len(), 1024);
6395 assert_eq!(r[1023].ordinal, 1023);
6396 }
6397
6398 use super::{TypeError, interval_range_by_periods, interval_range_by_step};
6401
6402 #[test]
6403 fn interval_range_by_periods_matches_pandas_default_case() {
6404 let bins = interval_range_by_periods(0.0, 10.0, 5, IntervalClosed::Right);
6406 assert_eq!(bins.len(), 5);
6407 for (i, bin) in bins.iter().enumerate() {
6408 assert_eq!(bin.left, (i as f64) * 2.0);
6409 assert_eq!(bin.right, ((i + 1) as f64) * 2.0);
6410 assert_eq!(bin.closed, IntervalClosed::Right);
6411 }
6412 }
6413
6414 #[test]
6415 fn interval_range_by_periods_final_edge_is_exact_end() {
6416 let bins = interval_range_by_periods(0.0, 1.0, 7, IntervalClosed::Right);
6418 assert_eq!(bins.last().unwrap().right, 1.0);
6419 }
6420
6421 #[test]
6422 fn interval_range_by_periods_zero_periods_is_empty() {
6423 assert!(interval_range_by_periods(0.0, 10.0, 0, IntervalClosed::Right).is_empty());
6424 }
6425
6426 #[test]
6427 fn interval_range_by_periods_reversed_range_is_empty() {
6428 assert!(interval_range_by_periods(10.0, 0.0, 5, IntervalClosed::Right).is_empty());
6430 }
6431
6432 #[test]
6433 fn interval_range_by_periods_preserves_closed_policy() {
6434 for closed in [
6435 IntervalClosed::Left,
6436 IntervalClosed::Right,
6437 IntervalClosed::Both,
6438 IntervalClosed::Neither,
6439 ] {
6440 let bins = interval_range_by_periods(0.0, 4.0, 2, closed);
6441 assert!(bins.iter().all(|b| b.closed == closed));
6442 }
6443 }
6444
6445 #[test]
6446 fn interval_range_by_step_matches_pandas_default_case() {
6447 let bins = interval_range_by_step(0.0, 10.0, 2.0, IntervalClosed::Right).expect("ok");
6449 assert_eq!(bins.len(), 5);
6450 assert_eq!(bins[0].left, 0.0);
6451 assert_eq!(bins[4].right, 10.0);
6452 }
6453
6454 #[test]
6455 fn interval_range_by_step_rejects_non_positive_step() {
6456 assert!(matches!(
6457 interval_range_by_step(0.0, 10.0, 0.0, IntervalClosed::Right),
6458 Err(TypeError::InvalidIntervalStep { .. })
6459 ));
6460 assert!(matches!(
6461 interval_range_by_step(0.0, 10.0, -2.0, IntervalClosed::Right),
6462 Err(TypeError::InvalidIntervalStep { .. })
6463 ));
6464 assert!(matches!(
6465 interval_range_by_step(0.0, 10.0, f64::NAN, IntervalClosed::Right),
6466 Err(TypeError::InvalidIntervalStep { .. })
6467 ));
6468 assert!(matches!(
6469 interval_range_by_step(0.0, 10.0, f64::INFINITY, IntervalClosed::Right),
6470 Err(TypeError::InvalidIntervalStep { .. })
6471 ));
6472 }
6473
6474 #[test]
6475 fn interval_range_by_step_rejects_non_dividing_step() {
6476 assert!(matches!(
6479 interval_range_by_step(0.0, 10.0, 3.0, IntervalClosed::Right),
6480 Err(TypeError::IntervalStepDoesNotDivide { .. })
6481 ));
6482 }
6483
6484 #[test]
6485 fn interval_range_by_step_reversed_range_is_empty() {
6486 let bins = interval_range_by_step(10.0, 0.0, 2.0, IntervalClosed::Right).expect("ok");
6487 assert!(bins.is_empty());
6488 }
6489
6490 #[test]
6491 fn interval_range_by_step_degenerate_zero_span_is_empty() {
6492 let bins = interval_range_by_step(5.0, 5.0, 1.0, IntervalClosed::Right).expect("ok");
6493 assert!(bins.is_empty());
6494 }
6495
6496 #[test]
6497 fn interval_range_by_step_accepts_float_step_within_tolerance() {
6498 let bins = interval_range_by_step(0.0, 1.0, 0.1, IntervalClosed::Right).expect("ok");
6500 assert_eq!(bins.len(), 10);
6501 assert_eq!(bins.last().unwrap().right, 1.0);
6502 }
6503
6504 use super::Timedelta;
6507
6508 #[test]
6509 fn timedelta_add_sums_non_nat() {
6510 let one_hour = Timedelta::NANOS_PER_HOUR;
6511 let one_day = Timedelta::NANOS_PER_DAY;
6512 assert_eq!(Timedelta::add(one_hour, one_day), one_hour + one_day);
6513 }
6514
6515 #[test]
6516 fn timedelta_add_propagates_nat() {
6517 assert_eq!(Timedelta::add(Timedelta::NAT, 100), Timedelta::NAT);
6518 assert_eq!(Timedelta::add(100, Timedelta::NAT), Timedelta::NAT);
6519 assert_eq!(
6520 Timedelta::add(Timedelta::NAT, Timedelta::NAT),
6521 Timedelta::NAT
6522 );
6523 }
6524
6525 #[test]
6526 fn timedelta_add_saturates_on_overflow() {
6527 assert_eq!(Timedelta::add(i64::MAX - 10, 100), i64::MAX);
6528 assert_eq!(Timedelta::add(i64::MIN + 10, -100), i64::MIN);
6530 }
6531
6532 #[test]
6533 fn timedelta_sub_subtracts_non_nat() {
6534 let one_hour = Timedelta::NANOS_PER_HOUR;
6535 assert_eq!(
6536 Timedelta::sub(one_hour, Timedelta::NANOS_PER_MIN),
6537 one_hour - Timedelta::NANOS_PER_MIN
6538 );
6539 }
6540
6541 #[test]
6542 fn timedelta_sub_propagates_nat() {
6543 assert_eq!(Timedelta::sub(Timedelta::NAT, 100), Timedelta::NAT);
6544 assert_eq!(Timedelta::sub(100, Timedelta::NAT), Timedelta::NAT);
6545 }
6546
6547 #[test]
6548 fn timedelta_neg_flips_sign_non_nat() {
6549 assert_eq!(Timedelta::neg(5), -5);
6550 assert_eq!(Timedelta::neg(-5), 5);
6551 assert_eq!(Timedelta::neg(0), 0);
6552 }
6553
6554 #[test]
6555 fn timedelta_neg_preserves_nat() {
6556 assert_eq!(Timedelta::neg(Timedelta::NAT), Timedelta::NAT);
6557 }
6558
6559 #[test]
6560 fn timedelta_abs_returns_magnitude() {
6561 assert_eq!(Timedelta::abs(-5), 5);
6562 assert_eq!(Timedelta::abs(5), 5);
6563 assert_eq!(Timedelta::abs(0), 0);
6564 assert_eq!(Timedelta::abs(Timedelta::NAT), Timedelta::NAT);
6565 }
6566
6567 #[test]
6568 fn timedelta_mul_scalar_scales() {
6569 let three_hours = Timedelta::NANOS_PER_HOUR * 3;
6570 assert_eq!(
6571 Timedelta::mul_scalar(Timedelta::NANOS_PER_HOUR, 3),
6572 three_hours
6573 );
6574 assert_eq!(Timedelta::mul_scalar(100, 0), 0);
6575 assert_eq!(Timedelta::mul_scalar(100, -2), -200);
6576 }
6577
6578 #[test]
6579 fn timedelta_mul_scalar_saturates() {
6580 assert_eq!(Timedelta::mul_scalar(i64::MAX, 2), i64::MAX);
6581 assert_eq!(Timedelta::mul_scalar(i64::MIN + 1, 2), i64::MIN);
6583 }
6584
6585 #[test]
6586 fn timedelta_mul_scalar_propagates_nat() {
6587 assert_eq!(Timedelta::mul_scalar(Timedelta::NAT, 5), Timedelta::NAT);
6588 }
6589
6590 #[test]
6591 fn timedelta_div_scalar_floor_divides() {
6592 assert_eq!(Timedelta::div_scalar(100, 3), 33);
6594 assert_eq!(Timedelta::div_scalar(-100, 3), -34);
6595 assert_eq!(Timedelta::div_scalar(100, -3), -34);
6596 assert_eq!(Timedelta::div_scalar(-100, -3), 33);
6597 }
6598
6599 #[test]
6600 fn timedelta_div_scalar_zero_divisor_returns_nat() {
6601 assert_eq!(Timedelta::div_scalar(100, 0), Timedelta::NAT);
6602 }
6603
6604 #[test]
6605 fn timedelta_div_scalar_min_neg_one_propagates_nat() {
6606 assert_eq!(Timedelta::div_scalar(i64::MIN, -1), Timedelta::NAT);
6609 assert_eq!(Timedelta::div_scalar(i64::MIN + 1, -1), i64::MAX);
6611 }
6612
6613 #[test]
6614 fn timedelta_div_scalar_propagates_nat() {
6615 assert_eq!(Timedelta::div_scalar(Timedelta::NAT, 10), Timedelta::NAT);
6616 }
6617
6618 #[test]
6619 fn timedelta_div_timedelta_returns_float_ratio() {
6620 let two_hours = Timedelta::NANOS_PER_HOUR * 2;
6621 let one_hour = Timedelta::NANOS_PER_HOUR;
6622 assert!((Timedelta::div_timedelta(two_hours, one_hour) - 2.0).abs() < 1e-12);
6623 assert!((Timedelta::div_timedelta(one_hour, two_hours) - 0.5).abs() < 1e-12);
6624 }
6625
6626 #[test]
6627 fn timedelta_div_timedelta_nat_returns_nan() {
6628 assert!(Timedelta::div_timedelta(Timedelta::NAT, 100).is_nan());
6629 assert!(Timedelta::div_timedelta(100, Timedelta::NAT).is_nan());
6630 }
6631
6632 use super::Timestamp;
6635
6636 #[test]
6637 fn timestamp_from_nanos_is_naive_utc() {
6638 let ts = Timestamp::from_nanos(1_700_000_000_000_000_000);
6639 assert_eq!(ts.nanos, 1_700_000_000_000_000_000);
6640 assert_eq!(ts.tz, None);
6641 assert!(!ts.is_nat());
6642 }
6643
6644 #[test]
6645 fn timestamp_from_nanos_tz_carries_tz_name() {
6646 let ts = Timestamp::from_nanos_tz(1_700_000_000_000_000_000, "US/Eastern");
6647 assert_eq!(ts.tz.as_deref(), Some("US/Eastern"));
6648 }
6649
6650 #[test]
6651 fn timestamp_now_returns_current_time() {
6652 let before = std::time::SystemTime::now()
6653 .duration_since(std::time::UNIX_EPOCH)
6654 .unwrap()
6655 .as_nanos() as i64;
6656 let ts = Timestamp::now();
6657 let after = std::time::SystemTime::now()
6658 .duration_since(std::time::UNIX_EPOCH)
6659 .unwrap()
6660 .as_nanos() as i64;
6661 assert!(ts.nanos >= before);
6662 assert!(ts.nanos <= after);
6663 assert!(!ts.is_nat());
6664 }
6665
6666 #[test]
6667 fn timestamp_today_returns_midnight() {
6668 let ts = Timestamp::today();
6669 assert!(!ts.is_nat());
6670 assert_eq!(ts.hour(), Some(0));
6672 assert_eq!(ts.minute(), Some(0));
6673 assert_eq!(ts.second(), Some(0));
6674 }
6675
6676 #[test]
6677 fn timestamp_add_timedelta_shifts_nanos_and_preserves_tz() {
6678 let ts = Timestamp::from_nanos_tz(0, "US/Eastern");
6679 let one_day = Timedelta::NANOS_PER_DAY;
6680 let shifted = ts.add_timedelta(one_day);
6681 assert_eq!(shifted.nanos, one_day);
6682 assert_eq!(shifted.tz.as_deref(), Some("US/Eastern"));
6683 }
6684
6685 #[test]
6686 fn timestamp_add_timedelta_saturates_on_overflow() {
6687 let ts = Timestamp::from_nanos(i64::MAX - 10);
6688 let shifted = ts.add_timedelta(100);
6689 assert_eq!(shifted.nanos, i64::MAX);
6690 }
6691
6692 #[test]
6693 fn timestamp_add_timedelta_propagates_nat() {
6694 assert!(Timestamp::nat().add_timedelta(100).is_nat());
6696 assert!(
6698 Timestamp::from_nanos(0)
6699 .add_timedelta(Timedelta::NAT)
6700 .is_nat()
6701 );
6702 }
6703
6704 #[test]
6705 fn timestamp_sub_timedelta_shifts_backward() {
6706 let ts = Timestamp::from_nanos(1_000);
6707 let shifted = ts.sub_timedelta(Timedelta::NANOS_PER_MICRO);
6708 assert_eq!(shifted.nanos, 0);
6709 }
6710
6711 #[test]
6712 fn timestamp_sub_timestamp_returns_timedelta_nanos() {
6713 let t0 = Timestamp::from_nanos(0);
6714 let t1 = Timestamp::from_nanos(Timedelta::NANOS_PER_HOUR);
6715 assert_eq!(t1.sub_timestamp(&t0), Timedelta::NANOS_PER_HOUR);
6716 assert_eq!(t0.sub_timestamp(&t1), -Timedelta::NANOS_PER_HOUR);
6717 }
6718
6719 #[test]
6720 fn timestamp_sub_timestamp_nat_propagates() {
6721 let ts = Timestamp::from_nanos(1_000);
6722 assert_eq!(Timestamp::nat().sub_timestamp(&ts), Timedelta::NAT);
6723 assert_eq!(ts.sub_timestamp(&Timestamp::nat()), Timedelta::NAT);
6724 }
6725
6726 #[test]
6727 fn timestamp_semantic_eq_treats_two_nat_as_equal() {
6728 assert!(Timestamp::nat().semantic_eq(&Timestamp::nat()));
6729 assert!(!Timestamp::nat().semantic_eq(&Timestamp::from_nanos(0)));
6730 assert!(!Timestamp::from_nanos(0).semantic_eq(&Timestamp::nat()));
6731 }
6732
6733 #[test]
6734 fn timestamp_partial_cmp_orders_by_nanos_nat_is_incomparable() {
6735 use std::cmp::Ordering;
6736 let a = Timestamp::from_nanos(0);
6737 let b = Timestamp::from_nanos(100);
6738 assert_eq!(a.partial_cmp(&b), Some(Ordering::Less));
6739 assert_eq!(b.partial_cmp(&a), Some(Ordering::Greater));
6740 assert_eq!(a.partial_cmp(&a), Some(Ordering::Equal));
6741 assert_eq!(a.partial_cmp(&Timestamp::nat()), None);
6742 assert_eq!(Timestamp::nat().partial_cmp(&Timestamp::nat()), None);
6743 }
6744
6745 #[test]
6746 fn timestamp_display_matches_phase2_debug_format() {
6747 assert_eq!(Timestamp::from_nanos(42).to_string(), "Timestamp[42, UTC]");
6748 assert_eq!(
6749 Timestamp::from_nanos_tz(42, "US/Eastern").to_string(),
6750 "Timestamp[42, US/Eastern]"
6751 );
6752 assert_eq!(Timestamp::nat().to_string(), "NaT");
6753 }
6754
6755 #[test]
6756 fn timestamp_value_and_unit_match_pandas_l0edr() {
6757 let ts = Timestamp::from_nanos(1_000_000_123);
6758 assert_eq!(ts.value(), 1_000_000_123);
6759 assert_eq!(ts.unit(), Some("ns"));
6760
6761 let nat = Timestamp::nat();
6762 assert_eq!(nat.value(), Timestamp::NAT);
6763 assert_eq!(nat.unit(), None);
6764 }
6765
6766 #[test]
6767 fn timestamp_numpy_datetime64_materializers_match_value_twksi() {
6768 let ts = Timestamp::from_nanos(1_000_000_123);
6769 assert_eq!(ts.asm8(), ts.value());
6770 assert_eq!(ts.to_datetime64(), ts.value());
6771 assert_eq!(ts.to_numpy(), ts.value());
6772
6773 let nat = Timestamp::nat();
6774 assert_eq!(nat.asm8(), Timestamp::NAT);
6775 assert_eq!(nat.to_datetime64(), Timestamp::NAT);
6776 assert_eq!(nat.to_numpy(), Timestamp::NAT);
6777 }
6778
6779 #[test]
6780 fn timestamp_timestamp_accessor_matches_pandas_microsecond_rounding_py0h3() {
6781 assert_eq!(Timestamp::from_nanos(0).timestamp(), Ok(0.0));
6782 assert_eq!(Timestamp::from_nanos(1_500_000_000).timestamp(), Ok(1.5));
6783 assert_eq!(Timestamp::from_nanos(500).timestamp(), Ok(0.0));
6784 assert_eq!(Timestamp::from_nanos(501).timestamp(), Ok(0.000001));
6785 assert_eq!(Timestamp::from_nanos(2_500).timestamp(), Ok(0.000003));
6786
6787 assert!(matches!(
6788 Timestamp::from_nanos(-500).timestamp(),
6789 Ok(value) if value == -0.0 && value.is_sign_negative()
6790 ));
6791 assert_eq!(Timestamp::from_nanos(-2_500).timestamp(), Ok(-0.000003));
6792 assert_eq!(
6793 Timestamp::nat().timestamp(),
6794 Err(TypeError::ValueIsMissing {
6795 kind: NullKind::NaT,
6796 })
6797 );
6798 }
6799
6800 #[test]
6801 fn timestamp_roundtrips_through_serde_json() {
6802 let naive = Timestamp::from_nanos(1_700_000_000_000_000_000);
6803 let json = serde_json::to_string(&naive).expect("serialize");
6804 let back: Timestamp = serde_json::from_str(&json).expect("deserialize");
6805 assert_eq!(naive, back);
6806
6807 let tz_aware = Timestamp::from_nanos_tz(1_700_000_000_000_000_000, "US/Eastern");
6808 let json = serde_json::to_string(&tz_aware).expect("serialize");
6809 let back: Timestamp = serde_json::from_str(&json).expect("deserialize");
6810 assert_eq!(tz_aware, back);
6811 }
6812
6813 #[test]
6814 fn timestamp_is_send_and_sync() {
6815 fn assert_send_sync<T: Send + Sync>() {}
6816 assert_send_sync::<Timestamp>();
6817 }
6818
6819 #[test]
6822 fn timestamp_floor_to_rounds_down() {
6823 let h = Timedelta::NANOS_PER_HOUR;
6825 let twelve_h = h * 12;
6826 let twelve_thirty_four =
6827 twelve_h + Timedelta::NANOS_PER_MIN * 34 + Timedelta::NANOS_PER_SEC * 56;
6828 let ts = Timestamp::from_nanos(twelve_thirty_four);
6829 let floored = ts.floor_to(h);
6830 assert_eq!(floored.nanos, twelve_h);
6831 }
6832
6833 #[test]
6834 fn timestamp_floor_to_handles_already_aligned() {
6835 let h = Timedelta::NANOS_PER_HOUR;
6837 let twelve_h = h * 12;
6838 let ts = Timestamp::from_nanos(twelve_h);
6839 assert_eq!(ts.floor_to(h).nanos, twelve_h);
6840 }
6841
6842 #[test]
6843 fn timestamp_floor_to_handles_negative_nanos() {
6844 let ts = Timestamp::from_nanos(-100);
6848 assert_eq!(ts.floor_to(60).nanos, -120);
6849 }
6850
6851 #[test]
6852 fn timestamp_ceil_to_rounds_up() {
6853 let h = Timedelta::NANOS_PER_HOUR;
6855 let twelve_h = h * 12;
6856 let thirteen_h = h * 13;
6857 let twelve_thirty_four =
6858 twelve_h + Timedelta::NANOS_PER_MIN * 34 + Timedelta::NANOS_PER_SEC * 56;
6859 let ts = Timestamp::from_nanos(twelve_thirty_four);
6860 assert_eq!(ts.ceil_to(h).nanos, thirteen_h);
6861 }
6862
6863 #[test]
6864 fn timestamp_ceil_to_no_op_on_aligned() {
6865 let h = Timedelta::NANOS_PER_HOUR;
6866 let twelve_h = h * 12;
6867 let ts = Timestamp::from_nanos(twelve_h);
6868 assert_eq!(ts.ceil_to(h).nanos, twelve_h);
6869 }
6870
6871 #[test]
6872 fn timestamp_round_to_rounds_to_nearest() {
6873 let h = Timedelta::NANOS_PER_HOUR;
6875 let twelve_h = h * 12;
6876 let twelve_thirty_one_sec =
6877 twelve_h + Timedelta::NANOS_PER_MIN * 30 + Timedelta::NANOS_PER_SEC;
6878 let ts = Timestamp::from_nanos(twelve_thirty_one_sec);
6879 assert_eq!(ts.round_to(h).nanos, h * 13);
6880
6881 let twelve_twenty_nine_sec =
6883 twelve_h + Timedelta::NANOS_PER_MIN * 29 + Timedelta::NANOS_PER_SEC * 59;
6884 let ts = Timestamp::from_nanos(twelve_twenty_nine_sec);
6885 assert_eq!(ts.round_to(h).nanos, twelve_h);
6886 }
6887
6888 #[test]
6889 fn timestamp_round_to_bankers_tie_to_even() {
6890 assert_eq!(Timestamp::from_nanos(5).round_to(10).nanos, 0);
6896 assert_eq!(Timestamp::from_nanos(15).round_to(10).nanos, 20);
6897 assert_eq!(Timestamp::from_nanos(25).round_to(10).nanos, 20);
6898 assert_eq!(Timestamp::from_nanos(35).round_to(10).nanos, 40);
6899 }
6900
6901 #[test]
6902 fn timestamp_round_to_zero_unit_returns_nat() {
6903 let ts = Timestamp::from_nanos(100);
6904 assert!(ts.round_to(0).is_nat());
6905 assert!(ts.floor_to(0).is_nat());
6906 assert!(ts.ceil_to(0).is_nat());
6907 }
6908
6909 #[test]
6910 fn timestamp_round_to_negative_unit_returns_nat() {
6911 let ts = Timestamp::from_nanos(100);
6912 assert!(ts.round_to(-10).is_nat());
6913 assert!(ts.floor_to(-10).is_nat());
6914 assert!(ts.ceil_to(-10).is_nat());
6915 }
6916
6917 #[test]
6918 fn timestamp_rounding_propagates_nat() {
6919 let nat = Timestamp::nat();
6920 assert!(nat.floor_to(60).is_nat());
6921 assert!(nat.ceil_to(60).is_nat());
6922 assert!(nat.round_to(60).is_nat());
6923 }
6924
6925 #[test]
6926 fn timestamp_rounding_preserves_tz() {
6927 let ts = Timestamp::from_nanos_tz(100, "US/Eastern");
6928 assert_eq!(ts.floor_to(60).tz.as_deref(), Some("US/Eastern"));
6929 assert_eq!(ts.ceil_to(60).tz.as_deref(), Some("US/Eastern"));
6930 assert_eq!(ts.round_to(60).tz.as_deref(), Some("US/Eastern"));
6931 }
6932
6933 #[test]
6936 fn timestamp_floor_to_unit_h_rounds_to_hour() {
6937 let h = Timedelta::NANOS_PER_HOUR;
6938 let twelve_h = h * 12;
6939 let twelve_thirty_four =
6940 twelve_h + Timedelta::NANOS_PER_MIN * 34 + Timedelta::NANOS_PER_SEC * 56;
6941 let ts = Timestamp::from_nanos(twelve_thirty_four);
6942 assert_eq!(ts.floor_to_unit("H").nanos, twelve_h);
6943 assert_eq!(ts.floor_to_unit("h").nanos, twelve_h);
6944 assert_eq!(ts.floor_to_unit("hour").nanos, twelve_h);
6945 assert_eq!(ts.floor_to_unit("hours").nanos, twelve_h);
6946 assert_eq!(ts.floor_to_unit("hr").nanos, twelve_h);
6947 }
6948
6949 #[test]
6950 fn timestamp_ceil_to_unit_d_rounds_to_day() {
6951 let h = Timedelta::NANOS_PER_HOUR;
6953 let d = Timedelta::NANOS_PER_DAY;
6954 let twelve_thirty_four = h * 12 + Timedelta::NANOS_PER_MIN * 34;
6955 let ts = Timestamp::from_nanos(twelve_thirty_four);
6956 assert_eq!(ts.ceil_to_unit("D").nanos, d);
6957 assert_eq!(ts.ceil_to_unit("day").nanos, d);
6958 assert_eq!(ts.ceil_to_unit("days").nanos, d);
6959 }
6960
6961 #[test]
6962 fn timestamp_round_to_unit_min_rounds_to_minute() {
6963 let m = Timedelta::NANOS_PER_MIN;
6965 let twelve_thirty_four_thirty_one =
6966 Timedelta::NANOS_PER_HOUR * 12 + m * 34 + Timedelta::NANOS_PER_SEC * 31;
6967 let ts = Timestamp::from_nanos(twelve_thirty_four_thirty_one);
6968 let expected = Timedelta::NANOS_PER_HOUR * 12 + m * 35;
6969 assert_eq!(ts.round_to_unit("min").nanos, expected);
6970 assert_eq!(ts.round_to_unit("T").nanos, expected); assert_eq!(ts.round_to_unit("minute").nanos, expected);
6972 }
6973
6974 #[test]
6975 fn timestamp_floor_ceil_round_aliases_match_unit_methods_li897() {
6976 let ts = Timestamp::from_nanos(
6977 Timedelta::NANOS_PER_HOUR * 12
6978 + Timedelta::NANOS_PER_MIN * 34
6979 + Timedelta::NANOS_PER_SEC * 31,
6980 );
6981
6982 assert_eq!(ts.floor("H"), ts.floor_to_unit("H"));
6983 assert_eq!(ts.ceil("D"), ts.ceil_to_unit("D"));
6984 assert_eq!(ts.round("min"), ts.round_to_unit("min"));
6985 }
6986
6987 #[test]
6988 fn timestamp_normalize_floors_to_day_and_preserves_tz_455op() {
6989 let ts = Timestamp::from_nanos_tz(
6990 Timedelta::NANOS_PER_DAY * 3
6991 + Timedelta::NANOS_PER_HOUR * 12
6992 + Timedelta::NANOS_PER_MIN * 34,
6993 "US/Eastern",
6994 );
6995 let normalized = ts.normalize();
6996
6997 assert_eq!(normalized.nanos, Timedelta::NANOS_PER_DAY * 3);
6998 assert_eq!(normalized.tz.as_deref(), Some("US/Eastern"));
6999 assert!(Timestamp::nat().normalize().is_nat());
7000 }
7001
7002 #[test]
7003 fn timestamp_unit_rounding_unknown_unit_returns_nat() {
7004 let ts = Timestamp::from_nanos(100);
7005 assert!(ts.floor_to_unit("fortnight").is_nat());
7006 assert!(ts.ceil_to_unit("century").is_nat());
7007 assert!(ts.round_to_unit("xyz").is_nat());
7008 }
7009
7010 #[test]
7011 fn timestamp_unit_rounding_propagates_nat() {
7012 let nat = Timestamp::nat();
7013 assert!(nat.floor_to_unit("H").is_nat());
7014 assert!(nat.ceil_to_unit("H").is_nat());
7015 assert!(nat.round_to_unit("H").is_nat());
7016 }
7017
7018 #[test]
7019 fn timestamp_unit_rounding_preserves_tz() {
7020 let ts = Timestamp::from_nanos_tz(Timedelta::NANOS_PER_HOUR * 12 + 100, "US/Eastern");
7021 assert_eq!(ts.floor_to_unit("H").tz.as_deref(), Some("US/Eastern"));
7022 assert_eq!(ts.ceil_to_unit("H").tz.as_deref(), Some("US/Eastern"));
7023 assert_eq!(ts.round_to_unit("H").tz.as_deref(), Some("US/Eastern"));
7024 }
7025
7026 #[test]
7027 fn timedelta_unit_to_nanos_is_now_public_and_matches_pandas_aliases() {
7028 assert_eq!(
7030 Timedelta::unit_to_nanos("W"),
7031 Some(Timedelta::NANOS_PER_WEEK)
7032 );
7033 assert_eq!(
7034 Timedelta::unit_to_nanos("D"),
7035 Some(Timedelta::NANOS_PER_DAY)
7036 );
7037 assert_eq!(
7038 Timedelta::unit_to_nanos("H"),
7039 Some(Timedelta::NANOS_PER_HOUR)
7040 );
7041 assert_eq!(
7042 Timedelta::unit_to_nanos("min"),
7043 Some(Timedelta::NANOS_PER_MIN)
7044 );
7045 assert_eq!(
7046 Timedelta::unit_to_nanos("s"),
7047 Some(Timedelta::NANOS_PER_SEC)
7048 );
7049 assert_eq!(
7050 Timedelta::unit_to_nanos("ms"),
7051 Some(Timedelta::NANOS_PER_MILLI)
7052 );
7053 assert_eq!(
7054 Timedelta::unit_to_nanos("us"),
7055 Some(Timedelta::NANOS_PER_MICRO)
7056 );
7057 assert_eq!(Timedelta::unit_to_nanos("ns"), Some(1));
7058 assert_eq!(Timedelta::unit_to_nanos(""), Some(Timedelta::NANOS_PER_DAY));
7060 assert_eq!(Timedelta::unit_to_nanos("century"), None);
7062 }
7063
7064 #[test]
7065 fn timestamp_isoformat_basic() {
7066 let ts = Timestamp::from_nanos(0);
7067 assert_eq!(ts.isoformat(), "1970-01-01T00:00:00");
7068
7069 let ts_utc = Timestamp::from_nanos_tz(0, "UTC");
7070 assert_eq!(ts_utc.isoformat(), "1970-01-01T00:00:00+00:00");
7071
7072 let ts_tz = Timestamp::from_nanos_tz(
7073 Timedelta::NANOS_PER_DAY
7074 + Timedelta::NANOS_PER_HOUR * 14
7075 + Timedelta::NANOS_PER_MIN * 30,
7076 "America/New_York",
7077 );
7078 assert!(ts_tz.isoformat().contains("1970-01-02T14:30:00"));
7079 assert!(ts_tz.isoformat().contains("[America/New_York]"));
7080
7081 assert_eq!(Timestamp::nat().isoformat(), "NaT");
7082 }
7083
7084 #[test]
7085 fn timestamp_strftime_basic() {
7086 let ts = Timestamp::from_nanos(
7087 Timedelta::NANOS_PER_DAY * 365
7088 + Timedelta::NANOS_PER_HOUR * 9
7089 + Timedelta::NANOS_PER_MIN * 15,
7090 );
7091 assert_eq!(ts.strftime("%Y-%m-%d"), "1971-01-01");
7092 assert_eq!(ts.strftime("%H:%M:%S"), "09:15:00");
7093 assert_eq!(ts.strftime("%Y/%m/%d %H:%M"), "1971/01/01 09:15");
7094 assert_eq!(Timestamp::nat().strftime("%Y-%m-%d"), "NaT");
7095 }
7096
7097 #[test]
7098 fn timestamp_day_name_and_month_name() {
7099 let ts = Timestamp::from_nanos(0);
7100 assert_eq!(ts.day_name(), "Thursday");
7101 assert_eq!(ts.month_name(), "January");
7102
7103 let ts2 = Timestamp::from_nanos(Timedelta::NANOS_PER_DAY * 365);
7104 assert_eq!(ts2.day_name(), "Friday");
7105 assert_eq!(ts2.month_name(), "January");
7106
7107 assert_eq!(Timestamp::nat().day_name(), "NaT");
7108 assert_eq!(Timestamp::nat().month_name(), "NaT");
7109 }
7110
7111 #[test]
7112 fn timestamp_component_accessors() {
7113 let ts = Timestamp::from_nanos(0);
7114 assert_eq!(ts.year(), Some(1970));
7115 assert_eq!(ts.month(), Some(1));
7116 assert_eq!(ts.day(), Some(1));
7117 assert_eq!(ts.hour(), Some(0));
7118 assert_eq!(ts.minute(), Some(0));
7119 assert_eq!(ts.second(), Some(0));
7120 assert_eq!(ts.microsecond(), Some(0));
7121 assert_eq!(ts.nanosecond(), Some(0));
7122
7123 let ts2 = Timestamp::from_nanos(
7124 Timedelta::NANOS_PER_DAY * 365
7125 + Timedelta::NANOS_PER_HOUR * 14
7126 + Timedelta::NANOS_PER_MIN * 30
7127 + Timedelta::NANOS_PER_SEC * 45
7128 + 123_456_789,
7129 );
7130 assert_eq!(ts2.year(), Some(1971));
7131 assert_eq!(ts2.month(), Some(1));
7132 assert_eq!(ts2.day(), Some(1));
7133 assert_eq!(ts2.hour(), Some(14));
7134 assert_eq!(ts2.minute(), Some(30));
7135 assert_eq!(ts2.second(), Some(45));
7136 assert_eq!(ts2.microsecond(), Some(123456));
7137 assert_eq!(ts2.nanosecond(), Some(789));
7138
7139 assert_eq!(Timestamp::nat().year(), None);
7140 assert_eq!(Timestamp::nat().month(), None);
7141 assert_eq!(Timestamp::nat().day(), None);
7142 }
7143
7144 #[test]
7145 fn timestamp_dayofweek_dayofyear_quarter() {
7146 let ts = Timestamp::from_nanos(0);
7147 assert_eq!(ts.dayofweek(), Some(3));
7148 assert_eq!(ts.weekday(), Some(3));
7149 assert_eq!(ts.dayofyear(), Some(1));
7150 assert_eq!(ts.quarter(), Some(1));
7151
7152 let ts2 = Timestamp::from_nanos(Timedelta::NANOS_PER_DAY * 90);
7153 assert_eq!(ts2.quarter(), Some(2));
7154
7155 let ts3 = Timestamp::from_nanos(Timedelta::NANOS_PER_DAY * 365);
7156 assert_eq!(ts3.dayofyear(), Some(1));
7157 assert_eq!(ts3.dayofweek(), Some(4));
7158
7159 assert_eq!(Timestamp::nat().dayofweek(), None);
7160 assert_eq!(Timestamp::nat().dayofyear(), None);
7161 assert_eq!(Timestamp::nat().quarter(), None);
7162 }
7163
7164 #[test]
7165 fn timestamp_is_boundary_methods() {
7166 let jan1 = Timestamp::from_nanos(0);
7167 assert_eq!(jan1.is_leap_year(), Some(false));
7168 assert_eq!(jan1.is_month_start(), Some(true));
7169 assert_eq!(jan1.is_month_end(), Some(false));
7170 assert_eq!(jan1.is_quarter_start(), Some(true));
7171 assert_eq!(jan1.is_quarter_end(), Some(false));
7172 assert_eq!(jan1.is_year_start(), Some(true));
7173 assert_eq!(jan1.is_year_end(), Some(false));
7174
7175 let dec31 = Timestamp::from_nanos(Timedelta::NANOS_PER_DAY * 364);
7176 assert_eq!(dec31.is_month_start(), Some(false));
7177 assert_eq!(dec31.is_month_end(), Some(true));
7178 assert_eq!(dec31.is_quarter_end(), Some(true));
7179 assert_eq!(dec31.is_year_end(), Some(true));
7180
7181 assert_eq!(Timestamp::nat().is_leap_year(), None);
7182 assert_eq!(Timestamp::nat().is_month_start(), None);
7183 }
7184
7185 #[test]
7186 fn timestamp_days_in_month() {
7187 let jan = Timestamp::from_nanos(0);
7188 assert_eq!(jan.days_in_month(), Some(31));
7189 assert_eq!(jan.daysinmonth(), Some(31));
7190
7191 let feb_non_leap = Timestamp::from_nanos(Timedelta::NANOS_PER_DAY * 31);
7192 assert_eq!(feb_non_leap.days_in_month(), Some(28));
7193
7194 assert_eq!(Timestamp::nat().days_in_month(), None);
7195 }
7196
7197 #[test]
7198 fn timestamp_weekofyear() {
7199 let jan1 = Timestamp::from_nanos(0);
7200 assert_eq!(jan1.weekofyear(), Some(1));
7201 assert_eq!(jan1.week(), Some(1));
7202
7203 let jan8 = Timestamp::from_nanos(Timedelta::NANOS_PER_DAY * 7);
7204 assert_eq!(jan8.weekofyear(), Some(2));
7205
7206 assert_eq!(Timestamp::nat().weekofyear(), None);
7207 assert_eq!(Timestamp::nat().week(), None);
7208 }
7209
7210 #[test]
7211 fn timestamp_weekofyear_iso_53_week_boundaries() {
7212 fn week_of(date_days: i64) -> Option<i64> {
7215 Timestamp::from_nanos(date_days * Timedelta::NANOS_PER_DAY).weekofyear()
7216 }
7217 assert_eq!(week_of(18_628), Some(53)); assert_eq!(week_of(16_801), Some(53)); assert_eq!(week_of(20_818), Some(53)); assert_eq!(week_of(18_627), Some(53)); assert_eq!(week_of(19_358), Some(52)); assert_eq!(week_of(20_087), Some(1)); assert_eq!(week_of(18_260), Some(1)); }
7231
7232 #[test]
7233 fn iso_weeks_in_year_53_week_years() {
7234 use super::iso_weeks_in_year;
7235 for y in [2004, 2009, 2015, 2020, 2026] {
7237 assert_eq!(iso_weeks_in_year(y), 53, "{y} should have 53 ISO weeks");
7238 }
7239 for y in [2018, 2019, 2021, 2022, 2023, 2024] {
7240 assert_eq!(iso_weeks_in_year(y), 52, "{y} should have 52 ISO weeks");
7241 }
7242 }
7243
7244 #[test]
7245 fn timestamp_to_unit() {
7246 let ts = Timestamp::from_nanos(1_000_000_000);
7247 assert_eq!(ts.to_unit("ns"), Some(1_000_000_000));
7248 assert_eq!(ts.to_unit("us"), Some(1_000_000));
7249 assert_eq!(ts.to_unit("ms"), Some(1_000));
7250 assert_eq!(ts.to_unit("s"), Some(1));
7251 assert_eq!(ts.to_unit("invalid"), None);
7252
7253 assert_eq!(Timestamp::nat().to_unit("ns"), None);
7254 }
7255
7256 #[test]
7257 fn timestamp_toordinal() {
7258 let nanos_2026_01_01 = 19723_i64 * 24 * 60 * 60 * 1_000_000_000;
7261 let ts = Timestamp::from_nanos(nanos_2026_01_01);
7262 assert_eq!(ts.toordinal(), Some(738886));
7263
7264 assert_eq!(Timestamp::nat().toordinal(), None);
7266 }
7267
7268 #[test]
7269 fn timestamp_fromordinal() {
7270 let nanos_2026_01_01 = 19723_i64 * 24 * 60 * 60 * 1_000_000_000;
7273 let ts_orig = Timestamp::from_nanos(nanos_2026_01_01);
7274 let ordinal = ts_orig.toordinal().unwrap();
7275
7276 let ts = Timestamp::fromordinal(ordinal);
7278 assert_eq!(ts.year(), ts_orig.year());
7279 assert_eq!(ts.month(), ts_orig.month());
7280 assert_eq!(ts.day(), ts_orig.day());
7281
7282 let nat = Timestamp::fromordinal(0);
7284 assert!(nat.is_nat());
7285 }
7286
7287 #[test]
7288 fn timestamp_parse_iso8601_date_only() {
7289 let ts = Timestamp::parse("2024-01-15").unwrap();
7290 assert_eq!(ts.year(), Some(2024));
7291 assert_eq!(ts.month(), Some(1));
7292 assert_eq!(ts.day(), Some(15));
7293 assert_eq!(ts.hour(), Some(0));
7294 assert_eq!(ts.minute(), Some(0));
7295 assert_eq!(ts.second(), Some(0));
7296 }
7297
7298 #[test]
7299 fn timestamp_parse_iso8601_datetime() {
7300 let ts = Timestamp::parse("2024-01-15T10:30:45").unwrap();
7301 assert_eq!(ts.year(), Some(2024));
7302 assert_eq!(ts.month(), Some(1));
7303 assert_eq!(ts.day(), Some(15));
7304 assert_eq!(ts.hour(), Some(10));
7305 assert_eq!(ts.minute(), Some(30));
7306 assert_eq!(ts.second(), Some(45));
7307 }
7308
7309 #[test]
7310 fn timestamp_parse_space_separator() {
7311 let ts = Timestamp::parse("2024-01-15 10:30:45").unwrap();
7312 assert_eq!(ts.year(), Some(2024));
7313 assert_eq!(ts.hour(), Some(10));
7314 }
7315
7316 #[test]
7317 fn timestamp_parse_with_fractional_seconds() {
7318 let ts = Timestamp::parse("2024-01-15T10:30:45.123456789").unwrap();
7319 assert_eq!(ts.second(), Some(45));
7320 assert_eq!(ts.microsecond(), Some(123456));
7321 assert_eq!(ts.nanosecond(), Some(789));
7322 }
7323
7324 #[test]
7325 fn timestamp_parse_utc_timezone() {
7326 let ts = Timestamp::parse("2024-01-15T10:30:45Z").unwrap();
7327 assert_eq!(ts.tz, Some("UTC".to_string()));
7328 }
7329
7330 #[test]
7331 fn timestamp_parse_offset_timezone() {
7332 let ts = Timestamp::parse("2024-01-15T10:30:45+05:30").unwrap();
7333 assert_eq!(ts.tz, Some("+05:30".to_string()));
7334 }
7335
7336 #[test]
7337 fn timestamp_parse_nat() {
7338 let ts = Timestamp::parse("NaT").unwrap();
7339 assert!(ts.is_nat());
7340 let ts2 = Timestamp::parse("nat").unwrap();
7341 assert!(ts2.is_nat());
7342 }
7343
7344 #[test]
7345 fn timestamp_parse_invalid() {
7346 assert!(Timestamp::parse("not a date").is_err());
7347 assert!(Timestamp::parse("2024-13-01").is_err()); assert!(Timestamp::parse("2024-01-32").is_err()); }
7350
7351 #[test]
7352 fn period_parse_annual() {
7353 let p = Period::parse("2024").unwrap();
7354 assert_eq!(p.freq(), PeriodFreq::Annual);
7355 assert_eq!(p.ordinal(), 2024 - 1970);
7356 }
7357
7358 #[test]
7359 fn period_parse_quarterly() {
7360 let p = Period::parse("2024Q1").unwrap();
7361 assert_eq!(p.freq(), PeriodFreq::Quarterly);
7362 assert_eq!(p.ordinal(), (2024 - 1970) * 4);
7363
7364 let p2 = Period::parse("2024q3").unwrap();
7365 assert_eq!(p2.freq(), PeriodFreq::Quarterly);
7366 assert_eq!(p2.ordinal(), (2024 - 1970) * 4 + 2);
7367 }
7368
7369 #[test]
7370 fn period_parse_monthly() {
7371 let p = Period::parse("2024-01").unwrap();
7372 assert_eq!(p.freq(), PeriodFreq::Monthly);
7373 assert_eq!(p.ordinal(), (2024 - 1970) * 12);
7374
7375 let p2 = Period::parse("2024-12").unwrap();
7376 assert_eq!(p2.freq(), PeriodFreq::Monthly);
7377 assert_eq!(p2.ordinal(), (2024 - 1970) * 12 + 11);
7378 }
7379
7380 #[test]
7381 fn period_parse_nat() {
7382 let p = Period::parse("NaT").unwrap();
7383 assert_eq!(p.ordinal(), i64::MIN);
7384 }
7385
7386 #[test]
7387 fn period_parse_invalid() {
7388 assert!(Period::parse("not a period").is_err());
7389 assert!(Period::parse("2024Q5").is_err()); assert!(Period::parse("2024-13").is_err()); }
7392
7393 #[test]
7394 fn interval_parse_basic() {
7395 let i = Interval::parse("[0, 1]").unwrap();
7396 assert_eq!(i.left, 0.0);
7397 assert_eq!(i.right, 1.0);
7398 assert_eq!(i.closed, IntervalClosed::Both);
7399
7400 let i2 = Interval::parse("(0, 1)").unwrap();
7401 assert_eq!(i2.left, 0.0);
7402 assert_eq!(i2.right, 1.0);
7403 assert_eq!(i2.closed, IntervalClosed::Neither);
7404
7405 let i3 = Interval::parse("[0, 1)").unwrap();
7406 assert_eq!(i3.closed, IntervalClosed::Left);
7407
7408 let i4 = Interval::parse("(0, 1]").unwrap();
7409 assert_eq!(i4.closed, IntervalClosed::Right);
7410 }
7411
7412 #[test]
7413 fn interval_parse_floats() {
7414 let i = Interval::parse("[-1.5, 2.5)").unwrap();
7415 assert_eq!(i.left, -1.5);
7416 assert_eq!(i.right, 2.5);
7417 assert_eq!(i.closed, IntervalClosed::Left);
7418 }
7419
7420 #[test]
7421 fn interval_parse_invalid() {
7422 assert!(Interval::parse("invalid").is_err());
7423 assert!(Interval::parse("[0]").is_err());
7424 assert!(Interval::parse("0, 1").is_err()); }
7426}