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(Period),
416 Interval(Interval),
418}
419
420impl std::fmt::Display for Scalar {
421 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
422 match self {
423 Self::Null(NullKind::NaN) => write!(f, "NaN"),
424 Self::Null(NullKind::NaT) => write!(f, "NaT"),
425 Self::Null(NullKind::Null) => write!(f, "None"),
426 Self::Bool(b) => write!(f, "{}", if *b { "True" } else { "False" }),
427 Self::Int64(v) => write!(f, "{v}"),
428 Self::Float64(v) => write!(f, "{v}"),
429 Self::Utf8(s) => write!(f, "{s}"),
430 Self::Timedelta64(nanos) => write!(f, "{}", Timedelta::format(*nanos)),
431 Self::Datetime64(nanos) => {
432 if *nanos == Timestamp::NAT {
433 write!(f, "NaT")
434 } else {
435 write!(f, "Timestamp[{nanos}]")
436 }
437 }
438 Self::Period(p) => {
439 if p.ordinal == i64::MIN {
440 write!(f, "NaT")
441 } else {
442 write!(f, "{}", p.calendar_string())
443 }
444 }
445 Self::Interval(interval) => write!(f, "{interval}"),
446 }
447 }
448}
449
450impl From<bool> for Scalar {
460 fn from(value: bool) -> Self {
461 Self::Bool(value)
462 }
463}
464
465impl From<i64> for Scalar {
466 fn from(value: i64) -> Self {
467 Self::Int64(value)
468 }
469}
470
471impl From<f64> for Scalar {
472 fn from(value: f64) -> Self {
473 Self::Float64(value)
474 }
475}
476
477impl From<&str> for Scalar {
478 fn from(value: &str) -> Self {
479 Self::Utf8(value.to_owned())
480 }
481}
482
483impl From<String> for Scalar {
484 fn from(value: String) -> Self {
485 Self::Utf8(value)
486 }
487}
488
489impl Scalar {
490 #[must_use]
491 pub fn dtype(&self) -> DType {
492 match self {
493 Self::Null(_) => DType::Null,
494 Self::Bool(_) => DType::Bool,
495 Self::Int64(_) => DType::Int64,
496 Self::Float64(_) => DType::Float64,
497 Self::Utf8(_) => DType::Utf8,
498 Self::Timedelta64(_) => DType::Timedelta64,
499 Self::Datetime64(_) => DType::Datetime64,
500 Self::Period(_) => DType::Period,
501 Self::Interval(_) => DType::Interval,
502 }
503 }
504
505 #[must_use]
506 pub fn is_missing(&self) -> bool {
507 match self {
508 Self::Null(_) => true,
509 Self::Float64(v) => v.is_nan(),
510 Self::Timedelta64(v) => *v == Timedelta::NAT,
511 Self::Datetime64(v) => *v == Timestamp::NAT,
512 Self::Period(p) => p.ordinal == i64::MIN,
513 _ => false,
514 }
515 }
516
517 #[must_use]
518 pub fn is_nan(&self) -> bool {
519 matches!(self, Self::Null(NullKind::NaN)) || matches!(self, Self::Float64(v) if v.is_nan())
520 }
521
522 #[must_use]
524 pub const fn is_bool(&self) -> bool {
525 matches!(self, Self::Bool(_))
526 }
527
528 #[must_use]
530 pub const fn is_integer(&self) -> bool {
531 matches!(self, Self::Int64(_))
532 }
533
534 #[must_use]
536 pub const fn is_float(&self) -> bool {
537 matches!(self, Self::Float64(_))
538 }
539
540 #[must_use]
542 pub const fn is_numeric(&self) -> bool {
543 matches!(self, Self::Int64(_) | Self::Float64(_))
544 }
545
546 #[must_use]
548 pub const fn is_string(&self) -> bool {
549 matches!(self, Self::Utf8(_))
550 }
551
552 #[must_use]
554 pub const fn is_datetime(&self) -> bool {
555 matches!(self, Self::Datetime64(_))
556 }
557
558 #[must_use]
560 pub const fn is_timedelta(&self) -> bool {
561 matches!(self, Self::Timedelta64(_))
562 }
563
564 #[must_use]
566 pub const fn is_period(&self) -> bool {
567 matches!(self, Self::Period(_))
568 }
569
570 #[must_use]
572 pub const fn is_interval(&self) -> bool {
573 matches!(self, Self::Interval(_))
574 }
575
576 #[must_use]
577 pub fn missing_for_dtype(dtype: DType) -> Self {
578 match dtype {
579 DType::Float64 => Self::Null(NullKind::NaN),
580 DType::Timedelta64 => Self::Timedelta64(Timedelta::NAT),
581 DType::Datetime64 => Self::Datetime64(Timestamp::NAT),
582 DType::Period => Self::Period(Period::new(i64::MIN, PeriodFreq::Daily)),
583 DType::Null => Self::Null(NullKind::Null),
584 DType::Bool
585 | DType::BoolNullable
586 | DType::Int64
587 | DType::Int64Nullable
588 | DType::Utf8
589 | DType::Categorical
590 | DType::Interval
591 | DType::Sparse => Self::Null(NullKind::Null),
592 }
593 }
594
595 #[must_use]
596 pub fn semantic_eq(&self, other: &Self) -> bool {
597 match (self, other) {
598 (Self::Float64(a), Self::Float64(b)) => {
599 if a.is_nan() && b.is_nan() {
600 return true;
601 }
602 if *a == *b {
603 return true;
604 }
605 let diff = (*a - *b).abs();
606 let max_abs = a.abs().max(b.abs());
607 if max_abs == 0.0 {
608 diff < f64::EPSILON
609 } else {
610 diff / max_abs < 1e-14
611 }
612 }
613 (Self::Null(_), Self::Float64(v)) | (Self::Float64(v), Self::Null(_)) => v.is_nan(),
614 (Self::Null(_), Self::Null(_)) => true,
621 _ => self == other,
622 }
623 }
624
625 #[must_use]
626 pub fn semantic_le(&self, other: &Self) -> bool {
627 match self.semantic_cmp(other) {
628 std::cmp::Ordering::Less | std::cmp::Ordering::Equal => true,
629 std::cmp::Ordering::Greater => false,
630 }
631 }
632
633 #[must_use]
634 pub fn semantic_ge(&self, other: &Self) -> bool {
635 match self.semantic_cmp(other) {
636 std::cmp::Ordering::Greater | std::cmp::Ordering::Equal => true,
637 std::cmp::Ordering::Less => false,
638 }
639 }
640
641 #[must_use]
642 pub fn is_null(&self) -> bool {
643 matches!(self, Self::Null(_))
644 }
645
646 #[must_use]
647 pub fn is_na(&self) -> bool {
648 self.is_missing()
649 }
650
651 #[must_use]
652 pub fn coalesce(&self, other: &Self) -> Self {
653 if self.is_missing() {
654 other.clone()
655 } else {
656 self.clone()
657 }
658 }
659
660 #[must_use]
661 pub fn semantic_cmp(&self, other: &Self) -> std::cmp::Ordering {
662 match (self, other) {
663 (Self::Int64(a), Self::Int64(b)) => a.cmp(b),
664 (Self::Float64(a), Self::Float64(b)) => {
665 a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
666 }
667 (Self::Utf8(a), Self::Utf8(b)) => a.cmp(b),
668 (Self::Bool(a), Self::Bool(b)) => a.cmp(b),
669 (Self::Null(a), Self::Null(b)) => a.cmp(b),
670 (Self::Timedelta64(a), Self::Timedelta64(b)) => {
671 if *a == Timedelta::NAT || *b == Timedelta::NAT {
672 std::cmp::Ordering::Equal
673 } else {
674 a.cmp(b)
675 }
676 }
677 (Self::Datetime64(a), Self::Datetime64(b)) => {
678 if *a == Timestamp::NAT || *b == Timestamp::NAT {
679 std::cmp::Ordering::Equal
680 } else {
681 a.cmp(b)
682 }
683 }
684 (Self::Period(a), Self::Period(b)) => {
685 if a.ordinal == i64::MIN || b.ordinal == i64::MIN {
686 std::cmp::Ordering::Equal
687 } else {
688 a.ordinal.cmp(&b.ordinal)
689 }
690 }
691 (Self::Interval(a), Self::Interval(b)) => a
692 .left
693 .partial_cmp(&b.left)
694 .unwrap_or(std::cmp::Ordering::Equal)
695 .then_with(|| {
696 a.right
697 .partial_cmp(&b.right)
698 .unwrap_or(std::cmp::Ordering::Equal)
699 })
700 .then_with(|| a.closed.cmp(&b.closed)),
701 (Self::Int64(a), Self::Float64(b)) => (*a as f64)
703 .partial_cmp(b)
704 .unwrap_or(std::cmp::Ordering::Equal),
705 (Self::Float64(a), Self::Int64(b)) => a
706 .partial_cmp(&(*b as f64))
707 .unwrap_or(std::cmp::Ordering::Equal),
708 (a, b) => format!("{a:?}").cmp(&format!("{b:?}")),
710 }
711 }
712
713 pub fn to_f64(&self) -> Result<f64, TypeError> {
714 match self {
715 Self::Bool(v) => Ok(if *v { 1.0 } else { 0.0 }),
716 Self::Int64(v) => Ok(*v as f64),
717 Self::Float64(v) => Ok(*v),
718 Self::Null(kind) => Err(TypeError::ValueIsMissing { kind: *kind }),
719 Self::Utf8(v) => Err(TypeError::NonNumericValue {
720 value: v.clone(),
721 dtype: DType::Utf8,
722 }),
723 Self::Timedelta64(v) if *v == Timedelta::NAT => Err(TypeError::ValueIsMissing {
724 kind: NullKind::NaT,
725 }),
726 Self::Timedelta64(v) => Err(TypeError::NonNumericValue {
727 value: Timedelta::format(*v),
728 dtype: DType::Timedelta64,
729 }),
730 Self::Datetime64(v) if *v == Timestamp::NAT => Err(TypeError::ValueIsMissing {
731 kind: NullKind::NaT,
732 }),
733 Self::Datetime64(v) => Err(TypeError::NonNumericValue {
734 value: format!("Timestamp[{v}]"),
735 dtype: DType::Datetime64,
736 }),
737 Self::Period(p) if p.ordinal == i64::MIN => Err(TypeError::ValueIsMissing {
738 kind: NullKind::NaT,
739 }),
740 Self::Period(p) => Err(TypeError::NonNumericValue {
741 value: p.calendar_string(),
742 dtype: DType::Period,
743 }),
744 Self::Interval(v) => Err(TypeError::NonNumericValue {
745 value: v.to_string(),
746 dtype: DType::Interval,
747 }),
748 }
749 }
750
751 pub fn to_i64(&self) -> Result<i64, TypeError> {
753 match self {
754 Self::Bool(v) => Ok(if *v { 1 } else { 0 }),
755 Self::Int64(v) => Ok(*v),
756 Self::Float64(v) => Ok(*v as i64),
757 Self::Null(kind) => Err(TypeError::ValueIsMissing { kind: *kind }),
758 Self::Utf8(v) => Err(TypeError::NonNumericValue {
759 value: v.clone(),
760 dtype: DType::Utf8,
761 }),
762 Self::Timedelta64(v) if *v == Timedelta::NAT => Err(TypeError::ValueIsMissing {
763 kind: NullKind::NaT,
764 }),
765 Self::Timedelta64(v) => Ok(*v),
766 Self::Datetime64(v) if *v == Timestamp::NAT => Err(TypeError::ValueIsMissing {
767 kind: NullKind::NaT,
768 }),
769 Self::Datetime64(v) => Ok(*v),
770 Self::Period(p) if p.ordinal == i64::MIN => Err(TypeError::ValueIsMissing {
771 kind: NullKind::NaT,
772 }),
773 Self::Period(p) => Ok(p.ordinal),
774 Self::Interval(v) => Err(TypeError::NonNumericValue {
775 value: v.to_string(),
776 dtype: DType::Interval,
777 }),
778 }
779 }
780
781 pub fn to_bool(&self) -> Result<bool, TypeError> {
783 match self {
784 Self::Bool(v) => Ok(*v),
785 Self::Int64(v) => Ok(*v != 0),
786 Self::Float64(v) => Ok(*v != 0.0 && !v.is_nan()),
787 Self::Null(kind) => Err(TypeError::ValueIsMissing { kind: *kind }),
788 Self::Utf8(v) => Ok(!v.is_empty()),
789 Self::Timedelta64(v) if *v == Timedelta::NAT => Err(TypeError::ValueIsMissing {
790 kind: NullKind::NaT,
791 }),
792 Self::Timedelta64(v) => Ok(*v != 0),
793 Self::Datetime64(v) if *v == Timestamp::NAT => Err(TypeError::ValueIsMissing {
794 kind: NullKind::NaT,
795 }),
796 Self::Datetime64(v) => Ok(*v != 0),
797 Self::Period(p) if p.ordinal == i64::MIN => Err(TypeError::ValueIsMissing {
798 kind: NullKind::NaT,
799 }),
800 Self::Period(p) => Ok(p.ordinal != 0),
801 Self::Interval(_) => Ok(true),
802 }
803 }
804
805 pub fn to_str(&self) -> String {
807 match self {
808 Self::Bool(v) => if *v { "True" } else { "False" }.to_string(),
809 Self::Int64(v) => v.to_string(),
810 Self::Float64(v) => {
811 if v.is_nan() {
812 "nan".to_string()
813 } else if v.is_infinite() {
814 if *v > 0.0 { "inf" } else { "-inf" }.to_string()
815 } else {
816 v.to_string()
817 }
818 }
819 Self::Null(_) => "NaN".to_string(),
820 Self::Utf8(v) => v.clone(),
821 Self::Timedelta64(v) => Timedelta::format(*v),
822 Self::Datetime64(v) if *v == Timestamp::NAT => "NaT".to_string(),
823 Self::Datetime64(v) => Timestamp::from_nanos(*v).isoformat(),
824 Self::Period(p) if p.ordinal == i64::MIN => "NaT".to_string(),
825 Self::Period(p) => p.calendar_string(),
826 Self::Interval(v) => v.to_string(),
827 }
828 }
829}
830
831#[derive(Debug, Error, Clone, PartialEq)]
832pub enum TypeError {
833 #[error("dtype coercion from {left:?} to {right:?} has no compatible common type")]
834 IncompatibleDtypes { left: DType, right: DType },
835 #[error("cannot cast scalar of dtype {from:?} to {to:?}")]
836 InvalidCast { from: DType, to: DType },
837 #[error("cannot cast float {value} to int64 without loss")]
838 LossyFloatToInt { value: f64 },
839 #[error("expected 0/1 for bool cast from int64 but found {value}")]
840 InvalidBoolInt { value: i64 },
841 #[error("expected 0.0/1.0 for bool cast from float64 but found {value}")]
842 InvalidBoolFloat { value: f64 },
843 #[error("value {value:?} has non-numeric dtype {dtype:?}")]
844 NonNumericValue { value: String, dtype: DType },
845 #[error("value is missing ({kind:?})")]
846 ValueIsMissing { kind: NullKind },
847 #[error("sparse value dtype cannot be {dtype:?}")]
848 InvalidSparseValueDType { dtype: DType },
849 #[error("interval_range step must be finite, positive, and not NaN (got {step})")]
850 InvalidIntervalStep { step: f64 },
851 #[error("interval_range step {step} does not evenly divide range end-start={span}")]
852 IntervalStepDoesNotDivide { step: f64, span: f64 },
853 #[error("cannot parse '{value}' as {target}")]
854 ValueNotParseable { value: String, target: String },
855}
856
857pub fn common_dtype(left: DType, right: DType) -> Result<DType, TypeError> {
858 use DType::{
859 Bool, BoolNullable, Categorical, Datetime64, Float64, Int64, Int64Nullable, Null, Sparse,
860 Timedelta64,
861 };
862
863 let out = match (left, right) {
864 (a, b) if a == b => a,
865 (Null, other) | (other, Null) => other,
866 (Categorical, Categorical) => Categorical,
867
868 (Bool, Int64) | (Int64, Bool) => Int64,
870 (Bool, Int64Nullable) | (Int64Nullable, Bool) => Int64Nullable,
871 (BoolNullable, Int64) | (Int64, BoolNullable) => Int64Nullable,
872 (BoolNullable, Int64Nullable) | (Int64Nullable, BoolNullable) => Int64Nullable,
873 (Bool, BoolNullable) | (BoolNullable, Bool) => BoolNullable,
874 (Bool, Float64) | (Float64, Bool) => Float64,
875 (BoolNullable, Float64) | (Float64, BoolNullable) => Float64,
876
877 (Int64, Float64) | (Float64, Int64) => Float64,
879 (Int64Nullable, Float64) | (Float64, Int64Nullable) => Float64,
880 (Int64, Int64Nullable) | (Int64Nullable, Int64) => Int64Nullable,
881
882 (Timedelta64, Timedelta64) => Timedelta64,
884 (Datetime64, Datetime64) => Datetime64,
885
886 (Sparse, _) | (_, Sparse) => return Err(TypeError::IncompatibleDtypes { left, right }),
887 _ => return Err(TypeError::IncompatibleDtypes { left, right }),
888 };
889
890 Ok(out)
891}
892
893pub fn infer_dtype(values: &[Scalar]) -> Result<DType, TypeError> {
894 let mut current = DType::Null;
895 let mut saw_utf8 = false;
896 let mut saw_timedelta = false;
897 let mut saw_datetime = false;
898 let mut saw_non_utf8_non_null = false;
899
900 for value in values {
901 match value.dtype() {
902 DType::Null => {}
903 DType::Utf8 => saw_utf8 = true,
904 DType::Timedelta64 => {
905 saw_timedelta = true;
906 if current == DType::Null {
907 current = DType::Timedelta64;
908 } else if current != DType::Timedelta64 {
909 return Err(TypeError::IncompatibleDtypes {
910 left: current,
911 right: DType::Timedelta64,
912 });
913 }
914 }
915 DType::Datetime64 => {
916 saw_datetime = true;
917 if current == DType::Null {
918 current = DType::Datetime64;
919 } else if current != DType::Datetime64 {
920 return Err(TypeError::IncompatibleDtypes {
921 left: current,
922 right: DType::Datetime64,
923 });
924 }
925 }
926 other => {
927 saw_non_utf8_non_null = true;
928 current = common_dtype(current, other)?;
929 }
930 }
931
932 if saw_utf8 && saw_non_utf8_non_null {
933 return Ok(DType::Utf8);
937 }
938 if saw_timedelta && saw_non_utf8_non_null {
939 return Err(TypeError::IncompatibleDtypes {
940 left: DType::Timedelta64,
941 right: current,
942 });
943 }
944 if saw_datetime && saw_non_utf8_non_null {
945 return Err(TypeError::IncompatibleDtypes {
946 left: DType::Datetime64,
947 right: current,
948 });
949 }
950 }
951
952 if saw_utf8 {
953 Ok(DType::Utf8)
954 } else {
955 Ok(current)
956 }
957}
958
959pub fn cast_scalar_owned(value: Scalar, target: DType) -> Result<Scalar, TypeError> {
962 let from = value.dtype();
963 if from == target {
964 return Ok(value);
965 }
966 if (from == DType::Int64 && target == DType::Int64Nullable)
968 || (from == DType::Int64Nullable && target == DType::Int64)
969 {
970 return Ok(value);
971 }
972 if (from == DType::Bool && target == DType::BoolNullable)
974 || (from == DType::BoolNullable && target == DType::Bool)
975 {
976 return Ok(value);
977 }
978 if target == DType::Utf8 {
979 return Ok(Scalar::Utf8(scalar_to_string_for_astype(value)));
980 }
981 if target == DType::Bool
987 && let Scalar::Float64(v) = &value
988 && v.is_nan()
989 {
990 return Ok(Scalar::Bool(true));
991 }
992 if value.is_missing() {
993 return Ok(Scalar::missing_for_dtype(target));
994 }
995
996 match target {
999 DType::Null => Ok(Scalar::Null(NullKind::Null)),
1000 DType::Bool => match &value {
1001 Scalar::Int64(v) => Ok(Scalar::Bool(*v != 0)),
1004 Scalar::Float64(v) => Ok(Scalar::Bool(*v != 0.0)),
1007 _ => Err(TypeError::InvalidCast { from, to: target }),
1008 },
1009 DType::BoolNullable => match &value {
1010 Scalar::Bool(b) => Ok(Scalar::Bool(*b)),
1015 Scalar::Int64(0) => Ok(Scalar::Bool(false)),
1016 Scalar::Int64(1) => Ok(Scalar::Bool(true)),
1017 Scalar::Int64(v) => Err(TypeError::InvalidBoolInt { value: *v }),
1018 Scalar::Float64(v) if *v == 0.0 => Ok(Scalar::Bool(false)),
1019 Scalar::Float64(v) if *v == 1.0 => Ok(Scalar::Bool(true)),
1020 Scalar::Float64(v) => Err(TypeError::InvalidBoolFloat { value: *v }),
1021 _ => Err(TypeError::InvalidCast { from, to: target }),
1022 },
1023 DType::Int64 | DType::Int64Nullable => match &value {
1024 Scalar::Bool(v) => Ok(Scalar::Int64(i64::from(*v))),
1025 Scalar::Float64(v) => {
1026 if !v.is_finite() {
1033 return Err(TypeError::LossyFloatToInt { value: *v });
1034 }
1035 if *v < i64::MIN as f64 || *v >= 9223372036854775808.0 {
1036 return Err(TypeError::LossyFloatToInt { value: *v });
1037 }
1038 Ok(Scalar::Int64(*v as i64))
1039 }
1040 Scalar::Utf8(s) => {
1041 if let Ok(v) = s.parse::<i64>() {
1044 return Ok(Scalar::Int64(v));
1045 }
1046 if let Ok(f) = s.parse::<f64>()
1047 && f.is_finite()
1048 && f.fract() == 0.0
1049 && f >= i64::MIN as f64
1050 && f < 9223372036854775808.0
1051 {
1052 return Ok(Scalar::Int64(f as i64));
1053 }
1054 Err(TypeError::InvalidCast { from, to: target })
1055 }
1056 _ => Err(TypeError::InvalidCast { from, to: target }),
1057 },
1058 DType::Float64 => match &value {
1059 Scalar::Bool(v) => Ok(Scalar::Float64(if *v { 1.0 } else { 0.0 })),
1060 Scalar::Int64(v) => Ok(Scalar::Float64(*v as f64)),
1061 Scalar::Utf8(s) => s
1062 .parse::<f64>()
1063 .map(Scalar::Float64)
1064 .map_err(|_| TypeError::InvalidCast { from, to: target }),
1065 _ => Err(TypeError::InvalidCast { from, to: target }),
1066 },
1067 DType::Utf8 => Ok(Scalar::Utf8(scalar_to_string_for_astype(value))),
1068 DType::Categorical => Err(TypeError::InvalidCast { from, to: target }),
1069 DType::Timedelta64 => match &value {
1070 Scalar::Int64(v) => Ok(Scalar::Timedelta64(*v)),
1071 Scalar::Utf8(s) => Timedelta::parse(s)
1072 .map(Scalar::Timedelta64)
1073 .map_err(|_| TypeError::InvalidCast { from, to: target }),
1074 _ => Err(TypeError::InvalidCast { from, to: target }),
1075 },
1076 DType::Datetime64 => match &value {
1077 Scalar::Int64(v) => Ok(Scalar::Datetime64(*v)),
1078 Scalar::Utf8(s) => Timestamp::parse(s)
1079 .map(|timestamp| Scalar::Datetime64(timestamp.nanos))
1080 .map_err(|_| TypeError::InvalidCast { from, to: target }),
1081 _ => Err(TypeError::InvalidCast { from, to: target }),
1082 },
1083 DType::Period => match &value {
1084 Scalar::Int64(v) => Ok(Scalar::Period(Period::new(*v, PeriodFreq::Daily))),
1087 Scalar::Utf8(s) => Period::parse(s)
1088 .map(Scalar::Period)
1089 .map_err(|_| TypeError::InvalidCast { from, to: target }),
1090 _ => Err(TypeError::InvalidCast { from, to: target }),
1091 },
1092 DType::Interval => match &value {
1093 Scalar::Utf8(s) => Interval::parse(s)
1094 .map(Scalar::Interval)
1095 .map_err(|_| TypeError::InvalidCast { from, to: target }),
1096 _ => Err(TypeError::InvalidCast { from, to: target }),
1097 },
1098 DType::Sparse => Err(TypeError::InvalidCast { from, to: target }),
1099 }
1100}
1101
1102fn scalar_to_string_for_astype(value: Scalar) -> String {
1103 match value {
1104 Scalar::Null(NullKind::Null) => "None".to_owned(),
1105 Scalar::Null(NullKind::NaN) => "nan".to_owned(),
1106 Scalar::Null(NullKind::NaT) => "NaT".to_owned(),
1107 Scalar::Bool(true) => "True".to_owned(),
1108 Scalar::Bool(false) => "False".to_owned(),
1109 Scalar::Int64(v) => v.to_string(),
1110 Scalar::Float64(v) => float_to_string_for_astype(v),
1111 Scalar::Utf8(s) => s,
1112 Scalar::Timedelta64(v) if v == Timedelta::NAT => "NaT".to_owned(),
1113 Scalar::Timedelta64(v) => Timedelta::format(v),
1114 Scalar::Datetime64(v) if v == Timestamp::NAT => "NaT".to_owned(),
1115 Scalar::Datetime64(v) => format!("Timestamp[{v}]"),
1116 Scalar::Period(p) if p.ordinal == i64::MIN => "NaT".to_owned(),
1117 Scalar::Period(p) => p.calendar_string(),
1118 Scalar::Interval(v) => v.to_string(),
1119 }
1120}
1121
1122fn float_to_string_for_astype(value: f64) -> String {
1123 if value.is_nan() {
1124 return "nan".to_owned();
1125 }
1126 if value.is_infinite() {
1127 return value.to_string(); }
1129 let s = format!("{value:?}");
1137 match s.split_once('e') {
1138 None => s,
1139 Some((mantissa, exp)) => {
1140 let (sign, digits) = match exp.strip_prefix('-') {
1141 Some(d) => ('-', d),
1142 None => ('+', exp.strip_prefix('+').unwrap_or(exp)),
1143 };
1144 format!("{mantissa}e{sign}{digits:0>2}")
1145 }
1146 }
1147}
1148
1149pub fn cast_scalar(value: &Scalar, target: DType) -> Result<Scalar, TypeError> {
1151 cast_scalar_owned(value.clone(), target)
1152}
1153
1154#[derive(Debug, Error, Clone, PartialEq)]
1157pub enum TimedeltaError {
1158 #[error("invalid timedelta string: {0}")]
1159 InvalidFormat(String),
1160 #[error("overflow in timedelta computation")]
1161 Overflow,
1162}
1163
1164#[derive(Debug, Clone, Copy, Default)]
1165pub struct TimedeltaComponents {
1166 pub days: i64,
1167 pub hours: i64,
1168 pub minutes: i64,
1169 pub seconds: i64,
1170 pub milliseconds: i64,
1171 pub microseconds: i64,
1172 pub nanoseconds: i64,
1173}
1174
1175pub struct Timedelta;
1176
1177impl Timedelta {
1178 pub const NANOS_PER_MICRO: i64 = 1_000;
1179 pub const NANOS_PER_MILLI: i64 = 1_000_000;
1180 pub const NANOS_PER_SEC: i64 = 1_000_000_000;
1181 pub const NANOS_PER_MIN: i64 = 60 * Self::NANOS_PER_SEC;
1182 pub const NANOS_PER_HOUR: i64 = 60 * Self::NANOS_PER_MIN;
1183 pub const NANOS_PER_DAY: i64 = 24 * Self::NANOS_PER_HOUR;
1184 pub const NANOS_PER_WEEK: i64 = 7 * Self::NANOS_PER_DAY;
1185
1186 pub const NAT: i64 = i64::MIN;
1187
1188 pub fn parse(s: &str) -> Result<i64, TimedeltaError> {
1189 let s = s.trim();
1190
1191 if s.eq_ignore_ascii_case("nat") {
1192 return Ok(Self::NAT);
1193 }
1194
1195 let (negative, s) = if let Some(rest) = s.strip_prefix('-') {
1196 (true, rest.trim())
1197 } else {
1198 (false, s)
1199 };
1200
1201 if let Some(nanos) = Self::try_parse_time_format(s) {
1202 return Ok(if negative { -nanos } else { nanos });
1203 }
1204
1205 if let Some(nanos) = Self::try_parse_iso8601_duration(s) {
1206 return Ok(if negative { -nanos } else { nanos });
1207 }
1208
1209 let nanos = Self::parse_compound(s)?;
1210 Ok(if negative { -nanos } else { nanos })
1211 }
1212
1213 fn try_parse_iso8601_duration(s: &str) -> Option<i64> {
1221 let mut rest = s.strip_prefix('P')?;
1222 if rest.is_empty() {
1223 return None;
1224 }
1225 let mut total: i64 = 0;
1226 let mut saw_component = false;
1227 while !rest.is_empty() {
1228 if let Some(after_t) = rest.strip_prefix('T') {
1229 rest = after_t;
1230 continue;
1231 }
1232 let num_end = rest.find(|c: char| !c.is_ascii_digit() && c != '.')?;
1233 if num_end == 0 {
1234 return None;
1235 }
1236 let num_str = &rest[..num_end];
1237 let unit = rest.as_bytes()[num_end];
1238 let is_fractional = num_str.contains('.');
1239 rest = &rest[num_end + 1..];
1240
1241 let (multiplier, frac_ok) = match unit {
1242 b'W' => (Self::NANOS_PER_WEEK, false),
1243 b'D' => (Self::NANOS_PER_DAY, false),
1244 b'H' => (Self::NANOS_PER_HOUR, false),
1245 b'M' => (Self::NANOS_PER_MIN, false),
1246 b'S' => (Self::NANOS_PER_SEC, true),
1247 _ => return None,
1248 };
1249 if is_fractional {
1250 if !frac_ok {
1251 return None;
1252 }
1253 let value: f64 = num_str.parse().ok()?;
1254 let product = value * multiplier as f64;
1255 if !product.is_finite() || product.abs() >= 9223372036854775808.0 {
1256 return None;
1257 }
1258 total = total.checked_add(product.round() as i64)?;
1259 } else {
1260 let value: i64 = num_str.parse().ok()?;
1261 total = total.checked_add(value.checked_mul(multiplier)?)?;
1262 }
1263 saw_component = true;
1264 }
1265 saw_component.then_some(total)
1266 }
1267
1268 fn try_parse_time_format(s: &str) -> Option<i64> {
1269 let parts: Vec<&str> = s.split(':').collect();
1270 if parts.len() < 2 || parts.len() > 3 {
1271 return None;
1272 }
1273
1274 let hours: i64 = parts[0].parse().ok()?;
1275 let minutes: i64 = parts[1].parse().ok()?;
1276
1277 let (seconds, frac_nanos) = if parts.len() == 3 {
1278 if let Some((sec_str, frac_str)) = parts[2].split_once('.') {
1279 let sec: i64 = sec_str.parse().ok()?;
1280 if !frac_str.bytes().all(|byte| byte.is_ascii_digit()) {
1281 return None;
1282 }
1283 let mut frac = 0_i64;
1284 let taken = frac_str.len().min(9);
1285 for byte in frac_str.bytes().take(9) {
1286 frac = frac * 10 + i64::from(byte - b'0');
1287 }
1288 for _ in taken..9 {
1289 frac *= 10;
1290 }
1291 (sec, frac)
1292 } else {
1293 let sec: i64 = parts[2].parse().ok()?;
1294 (sec, 0)
1295 }
1296 } else {
1297 (0, 0)
1298 };
1299
1300 hours
1301 .checked_mul(Self::NANOS_PER_HOUR)?
1302 .checked_add(minutes.checked_mul(Self::NANOS_PER_MIN)?)?
1303 .checked_add(seconds.checked_mul(Self::NANOS_PER_SEC)?)?
1304 .checked_add(frac_nanos)
1305 }
1306
1307 fn parse_compound(s: &str) -> Result<i64, TimedeltaError> {
1308 let mut total: i64 = 0;
1309 let mut remaining = s;
1310
1311 while !remaining.is_empty() {
1312 remaining = remaining.trim_start();
1313 if remaining.is_empty() {
1314 break;
1315 }
1316
1317 if remaining.contains(':')
1320 && let Some(time_nanos) = Self::try_parse_time_format(remaining)
1321 {
1322 total = total
1323 .checked_add(time_nanos)
1324 .ok_or(TimedeltaError::Overflow)?;
1325 break;
1326 }
1327
1328 let num_end = remaining
1329 .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '-')
1330 .unwrap_or(remaining.len());
1331
1332 if num_end == 0 {
1333 return Err(TimedeltaError::InvalidFormat(s.to_string()));
1334 }
1335
1336 let num_str = &remaining[..num_end];
1337 let num: f64 = num_str
1338 .parse()
1339 .map_err(|_| TimedeltaError::InvalidFormat(s.to_string()))?;
1340
1341 remaining = remaining[num_end..].trim_start();
1342
1343 let unit_end = remaining
1344 .find(|c: char| c.is_ascii_digit() || c.is_whitespace())
1345 .unwrap_or(remaining.len());
1346
1347 let unit = &remaining[..unit_end];
1348 remaining = &remaining[unit_end..];
1349
1350 let multiplier = Self::unit_to_nanos(unit)
1351 .ok_or_else(|| TimedeltaError::InvalidFormat(s.to_string()))?;
1352
1353 let product = num * multiplier as f64;
1358 if !product.is_finite() || product.abs() >= 9223372036854775808.0 {
1359 return Err(TimedeltaError::Overflow);
1360 }
1361 let nanos = product.round() as i64;
1362 total = total.checked_add(nanos).ok_or(TimedeltaError::Overflow)?;
1363 }
1364
1365 if total == 0 && !s.trim().is_empty() && s.trim() != "0" {
1366 return Err(TimedeltaError::InvalidFormat(s.to_string()));
1367 }
1368
1369 Ok(total)
1370 }
1371
1372 #[must_use]
1386 pub fn unit_to_nanos(unit: &str) -> Option<i64> {
1387 match unit.to_lowercase().as_str() {
1388 "w" | "week" | "weeks" => Some(Self::NANOS_PER_WEEK),
1389 "d" | "day" | "days" => Some(Self::NANOS_PER_DAY),
1390 "h" | "hr" | "hour" | "hours" => Some(Self::NANOS_PER_HOUR),
1391 "m" | "min" | "minute" | "minutes" | "t" => Some(Self::NANOS_PER_MIN),
1392 "s" | "sec" | "second" | "seconds" => Some(Self::NANOS_PER_SEC),
1393 "ms" | "milli" | "millis" | "millisecond" | "milliseconds" | "l" => {
1394 Some(Self::NANOS_PER_MILLI)
1395 }
1396 "us" | "µs" | "micro" | "micros" | "microsecond" | "microseconds" | "u" => {
1397 Some(Self::NANOS_PER_MICRO)
1398 }
1399 "ns" | "nano" | "nanos" | "nanosecond" | "nanoseconds" | "n" => Some(1),
1400 "" => Some(Self::NANOS_PER_DAY),
1401 _ => None,
1402 }
1403 }
1404
1405 pub fn components(nanos: i64) -> TimedeltaComponents {
1406 if nanos == Self::NAT {
1407 return TimedeltaComponents::default();
1408 }
1409
1410 let days = nanos.div_euclid(Self::NANOS_PER_DAY);
1415 let rem = nanos.rem_euclid(Self::NANOS_PER_DAY);
1416
1417 let hours = rem / Self::NANOS_PER_HOUR;
1418 let rem = rem % Self::NANOS_PER_HOUR;
1419
1420 let minutes = rem / Self::NANOS_PER_MIN;
1421 let rem = rem % Self::NANOS_PER_MIN;
1422
1423 let seconds = rem / Self::NANOS_PER_SEC;
1424 let rem = rem % Self::NANOS_PER_SEC;
1425
1426 let milliseconds = rem / Self::NANOS_PER_MILLI;
1427 let rem = rem % Self::NANOS_PER_MILLI;
1428
1429 let microseconds = rem / Self::NANOS_PER_MICRO;
1430 let nanoseconds = rem % Self::NANOS_PER_MICRO;
1431
1432 TimedeltaComponents {
1433 days,
1434 hours,
1435 minutes,
1436 seconds,
1437 milliseconds,
1438 microseconds,
1439 nanoseconds,
1440 }
1441 }
1442
1443 pub fn total_seconds(nanos: i64) -> f64 {
1444 if nanos == Self::NAT {
1445 f64::NAN
1446 } else {
1447 nanos as f64 / Self::NANOS_PER_SEC as f64
1448 }
1449 }
1450
1451 #[must_use]
1455 pub fn as_unit(nanos: i64, unit: &str) -> f64 {
1456 if nanos == Self::NAT {
1457 return f64::NAN;
1458 }
1459 let nanos_f = nanos as f64;
1460 match unit {
1461 "ns" | "nanoseconds" => nanos_f,
1462 "us" | "microseconds" => nanos_f / Self::NANOS_PER_MICRO as f64,
1463 "ms" | "milliseconds" => nanos_f / Self::NANOS_PER_MILLI as f64,
1464 "s" | "seconds" => nanos_f / Self::NANOS_PER_SEC as f64,
1465 "m" | "minutes" => nanos_f / Self::NANOS_PER_MIN as f64,
1466 "h" | "hours" => nanos_f / Self::NANOS_PER_HOUR as f64,
1467 "D" | "days" => nanos_f / Self::NANOS_PER_DAY as f64,
1468 _ => f64::NAN,
1469 }
1470 }
1471
1472 #[must_use]
1474 pub fn days(nanos: i64) -> i64 {
1475 if nanos == Self::NAT {
1476 return 0; }
1478 nanos.div_euclid(Self::NANOS_PER_DAY)
1480 }
1481
1482 #[must_use]
1484 pub fn seconds(nanos: i64) -> i64 {
1485 if nanos == Self::NAT {
1486 return 0;
1487 }
1488 nanos.rem_euclid(Self::NANOS_PER_DAY) / Self::NANOS_PER_SEC
1490 }
1491
1492 #[must_use]
1494 pub fn microseconds(nanos: i64) -> i64 {
1495 if nanos == Self::NAT {
1496 return 0;
1497 }
1498 nanos.rem_euclid(Self::NANOS_PER_SEC) / Self::NANOS_PER_MICRO
1499 }
1500
1501 #[must_use]
1503 pub fn nanoseconds(nanos: i64) -> i64 {
1504 if nanos == Self::NAT {
1505 return 0;
1506 }
1507 nanos.rem_euclid(Self::NANOS_PER_MICRO)
1508 }
1509
1510 pub fn format(nanos: i64) -> String {
1511 if nanos == Self::NAT {
1512 return "NaT".to_string();
1513 }
1514
1515 let days = nanos.div_euclid(Self::NANOS_PER_DAY);
1521 let rem = nanos.rem_euclid(Self::NANOS_PER_DAY);
1522 let hours = rem / Self::NANOS_PER_HOUR;
1523 let minutes = (rem % Self::NANOS_PER_HOUR) / Self::NANOS_PER_MIN;
1524 let seconds = (rem % Self::NANOS_PER_MIN) / Self::NANOS_PER_SEC;
1525 let frac = rem % Self::NANOS_PER_SEC;
1526
1527 let time_part = format!("{hours:02}:{minutes:02}:{seconds:02}");
1528 let sep = if days < 0 { "+" } else { "" };
1530
1531 if frac > 0 {
1532 if frac % 1_000 == 0 {
1536 format!("{days} days {sep}{time_part}.{:06}", frac / 1_000)
1537 } else {
1538 format!("{days} days {sep}{time_part}.{frac:09}")
1539 }
1540 } else {
1541 format!("{days} days {sep}{time_part}")
1542 }
1543 }
1544
1545 pub fn from_unit(value: f64, unit: &str) -> Result<i64, TimedeltaError> {
1546 let multiplier = Self::unit_to_nanos(unit)
1547 .ok_or_else(|| TimedeltaError::InvalidFormat(unit.to_string()))?;
1548 Ok((value * multiplier as f64).round() as i64)
1549 }
1550
1551 #[must_use]
1560 pub fn add(a: i64, b: i64) -> i64 {
1561 if a == Self::NAT || b == Self::NAT {
1562 return Self::NAT;
1563 }
1564 a.saturating_add(b)
1565 }
1566
1567 #[must_use]
1569 pub fn sub(a: i64, b: i64) -> i64 {
1570 if a == Self::NAT || b == Self::NAT {
1571 return Self::NAT;
1572 }
1573 a.saturating_sub(b)
1574 }
1575
1576 #[must_use]
1579 pub fn neg(a: i64) -> i64 {
1580 if a == Self::NAT {
1581 return Self::NAT;
1582 }
1583 a.saturating_neg()
1584 }
1585
1586 #[must_use]
1588 pub fn abs(a: i64) -> i64 {
1589 if a == Self::NAT {
1590 return Self::NAT;
1591 }
1592 a.saturating_abs()
1593 }
1594
1595 #[must_use]
1600 pub fn mul_scalar(a: i64, factor: i64) -> i64 {
1601 if a == Self::NAT {
1602 return Self::NAT;
1603 }
1604 a.saturating_mul(factor)
1605 }
1606
1607 #[must_use]
1617 pub fn div_scalar(a: i64, divisor: i64) -> i64 {
1618 if a == Self::NAT || divisor == 0 {
1619 return Self::NAT;
1620 }
1621 let q = a / divisor;
1626 let r = a % divisor;
1627 if r != 0 && (r < 0) != (divisor < 0) {
1631 q - 1
1632 } else {
1633 q
1634 }
1635 }
1636
1637 #[must_use]
1641 pub fn div_timedelta(a: i64, b: i64) -> f64 {
1642 if a == Self::NAT || b == Self::NAT {
1643 return f64::NAN;
1644 }
1645 (a as f64) / (b as f64)
1646 }
1647
1648 #[must_use]
1654 pub fn isoformat(nanos: i64) -> String {
1655 if nanos == Self::NAT {
1656 return "NaT".to_string();
1657 }
1658
1659 let negative = nanos < 0;
1660 let abs_nanos = nanos.saturating_abs();
1661
1662 let days = abs_nanos / Self::NANOS_PER_DAY;
1663 let remaining = abs_nanos % Self::NANOS_PER_DAY;
1664
1665 let hours = remaining / Self::NANOS_PER_HOUR;
1666 let remaining = remaining % Self::NANOS_PER_HOUR;
1667
1668 let minutes = remaining / Self::NANOS_PER_MIN;
1669 let remaining = remaining % Self::NANOS_PER_MIN;
1670
1671 let seconds = remaining / Self::NANOS_PER_SEC;
1672 let sub_sec_nanos = remaining % Self::NANOS_PER_SEC;
1673
1674 let mut result = String::new();
1675 if negative {
1676 result.push('-');
1677 }
1678
1679 result.push_str(&format!("P{days}DT{hours}H{minutes}M"));
1680
1681 if sub_sec_nanos == 0 {
1682 result.push_str(&format!("{seconds}S"));
1683 } else {
1684 let frac = format!("{:09}", sub_sec_nanos);
1685 let trimmed = frac.trim_end_matches('0');
1686 result.push_str(&format!("{seconds}.{trimmed}S"));
1687 }
1688
1689 result
1690 }
1691
1692 #[must_use]
1696 pub fn floor(nanos: i64, freq: &str) -> i64 {
1697 if nanos == Self::NAT {
1698 return Self::NAT;
1699 }
1700 let Some(unit_nanos) = Self::unit_to_nanos(freq) else {
1701 return Self::NAT;
1702 };
1703 if unit_nanos == 0 {
1704 return Self::NAT;
1705 }
1706 nanos.div_euclid(unit_nanos).saturating_mul(unit_nanos)
1707 }
1708
1709 #[must_use]
1713 pub fn ceil(nanos: i64, freq: &str) -> i64 {
1714 if nanos == Self::NAT {
1715 return Self::NAT;
1716 }
1717 let Some(unit_nanos) = Self::unit_to_nanos(freq) else {
1718 return Self::NAT;
1719 };
1720 if unit_nanos == 0 {
1721 return Self::NAT;
1722 }
1723 let remainder = nanos.rem_euclid(unit_nanos);
1724 if remainder == 0 {
1725 nanos
1726 } else {
1727 nanos.saturating_add(unit_nanos - remainder)
1728 }
1729 }
1730
1731 #[must_use]
1736 pub fn round(nanos: i64, freq: &str) -> i64 {
1737 if nanos == Self::NAT {
1738 return Self::NAT;
1739 }
1740 let Some(unit_nanos) = Self::unit_to_nanos(freq) else {
1741 return Self::NAT;
1742 };
1743 if unit_nanos == 0 {
1744 return Self::NAT;
1745 }
1746 let negative = nanos < 0;
1747 let abs_nanos = nanos.saturating_abs();
1748
1749 let quotient = abs_nanos / unit_nanos;
1750 let remainder = abs_nanos % unit_nanos;
1751 let half = unit_nanos / 2;
1752
1753 let rounded = if remainder > half {
1754 (quotient + 1) * unit_nanos
1755 } else if remainder < half {
1756 quotient * unit_nanos
1757 } else {
1758 if quotient % 2 == 0 {
1760 quotient * unit_nanos
1761 } else {
1762 (quotient + 1) * unit_nanos
1763 }
1764 };
1765
1766 if negative { -rounded } else { rounded }
1767 }
1768}
1769
1770fn days_in_month(year: i64, month: u32) -> Option<u32> {
1780 if !(1..=12).contains(&month) {
1781 return None;
1782 }
1783 let is_leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
1784 let days: [u32; 12] = [
1785 31,
1786 if is_leap { 29 } else { 28 },
1787 31,
1788 30,
1789 31,
1790 30,
1791 31,
1792 31,
1793 30,
1794 31,
1795 30,
1796 31,
1797 ];
1798 Some(days[(month - 1) as usize])
1799}
1800
1801fn iso_weeks_in_year(year: i64) -> i64 {
1808 fn p(y: i64) -> i64 {
1809 (y + y.div_euclid(4) - y.div_euclid(100) + y.div_euclid(400)).rem_euclid(7)
1810 }
1811 if p(year) == 4 || p(year - 1) == 3 {
1812 53
1813 } else {
1814 52
1815 }
1816}
1817
1818#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1824pub struct Timestamp {
1825 pub nanos: i64,
1827 #[serde(default, skip_serializing_if = "Option::is_none")]
1831 pub tz: Option<String>,
1832}
1833
1834impl Timestamp {
1835 pub const NAT: i64 = i64::MIN;
1837
1838 #[must_use]
1841 pub const fn from_nanos(nanos: i64) -> Self {
1842 Self { nanos, tz: None }
1843 }
1844
1845 #[must_use]
1850 pub fn from_nanos_tz(nanos: i64, tz_name: impl Into<String>) -> Self {
1851 Self {
1852 nanos,
1853 tz: Some(tz_name.into()),
1854 }
1855 }
1856
1857 #[must_use]
1861 pub fn now() -> Self {
1862 use std::time::{SystemTime, UNIX_EPOCH};
1863 let duration = SystemTime::now()
1864 .duration_since(UNIX_EPOCH)
1865 .unwrap_or_default();
1866 let nanos = duration.as_nanos() as i64;
1867 Self { nanos, tz: None }
1868 }
1869
1870 #[must_use]
1872 pub fn utcnow() -> Self {
1873 Self::now()
1874 }
1875
1876 #[must_use]
1880 pub fn today() -> Self {
1881 let now = Self::now();
1882 now.normalize()
1883 }
1884
1885 #[must_use]
1887 pub const fn nat() -> Self {
1888 Self {
1889 nanos: Self::NAT,
1890 tz: None,
1891 }
1892 }
1893
1894 #[must_use]
1896 pub const fn is_nat(&self) -> bool {
1897 self.nanos == Self::NAT
1898 }
1899
1900 #[must_use]
1902 pub const fn value(&self) -> i64 {
1903 self.nanos
1904 }
1905
1906 #[must_use]
1911 pub const fn unit(&self) -> Option<&'static str> {
1912 if self.is_nat() { None } else { Some("ns") }
1913 }
1914
1915 #[must_use]
1919 pub const fn resolution(&self) -> Option<&'static str> {
1920 if self.is_nat() { None } else { Some("ns") }
1921 }
1922
1923 #[must_use]
1925 pub const fn asm8(&self) -> i64 {
1926 self.value()
1927 }
1928
1929 #[must_use]
1931 pub const fn to_datetime64(&self) -> i64 {
1932 self.value()
1933 }
1934
1935 #[must_use]
1937 pub const fn to_numpy(&self) -> i64 {
1938 self.value()
1939 }
1940
1941 pub fn timestamp(&self) -> Result<f64, TypeError> {
1947 if self.is_nat() {
1948 return Err(TypeError::ValueIsMissing {
1949 kind: NullKind::NaT,
1950 });
1951 }
1952 let seconds = self.nanos as f64 / 1_000_000_000.0;
1953 let rounded = format!("{seconds:.6}").parse().unwrap_or(seconds);
1954 Ok(rounded)
1955 }
1956
1957 #[must_use]
1960 pub fn add_timedelta(&self, td_nanos: i64) -> Self {
1961 if self.is_nat() || td_nanos == Timedelta::NAT {
1962 return Self::nat();
1963 }
1964 Self {
1965 nanos: self.nanos.saturating_add(td_nanos),
1966 tz: self.tz.clone(),
1967 }
1968 }
1969
1970 #[must_use]
1972 pub fn sub_timedelta(&self, td_nanos: i64) -> Self {
1973 if self.is_nat() || td_nanos == Timedelta::NAT {
1974 return Self::nat();
1975 }
1976 Self {
1977 nanos: self.nanos.saturating_sub(td_nanos),
1978 tz: self.tz.clone(),
1979 }
1980 }
1981
1982 #[must_use]
1985 pub fn sub_timestamp(&self, other: &Self) -> i64 {
1986 if self.is_nat() || other.is_nat() {
1987 return Timedelta::NAT;
1988 }
1989 self.nanos.saturating_sub(other.nanos)
1990 }
1991
1992 #[must_use]
1997 pub fn semantic_eq(&self, other: &Self) -> bool {
1998 if self.is_nat() && other.is_nat() {
1999 return true;
2000 }
2001 if self.is_nat() || other.is_nat() {
2002 return false;
2003 }
2004 self.nanos == other.nanos && self.tz == other.tz
2005 }
2006
2007 #[must_use]
2020 pub fn floor_to(&self, unit_nanos: i64) -> Self {
2021 if self.is_nat() || unit_nanos <= 0 {
2022 return Self::nat();
2023 }
2024 let Some(nanos) = self.nanos.div_euclid(unit_nanos).checked_mul(unit_nanos) else {
2025 return Self::nat();
2026 };
2027 Self {
2028 nanos,
2029 tz: self.tz.clone(),
2030 }
2031 }
2032
2033 #[must_use]
2038 pub fn ceil_to(&self, unit_nanos: i64) -> Self {
2039 if self.is_nat() || unit_nanos <= 0 {
2040 return Self::nat();
2041 }
2042 let rem = self.nanos.rem_euclid(unit_nanos);
2043 let nanos = if rem == 0 {
2044 self.nanos
2045 } else {
2046 self.nanos.saturating_add(unit_nanos - rem)
2047 };
2048 Self {
2049 nanos,
2050 tz: self.tz.clone(),
2051 }
2052 }
2053
2054 #[must_use]
2060 pub fn round_to(&self, unit_nanos: i64) -> Self {
2061 if self.is_nat() || unit_nanos <= 0 {
2062 return Self::nat();
2063 }
2064 let floor = self.nanos.div_euclid(unit_nanos);
2065 let rem = self.nanos.rem_euclid(unit_nanos);
2066 let half = unit_nanos / 2;
2067 let chosen_floor = if rem < half {
2068 floor
2069 } else if rem > half {
2070 floor + 1
2071 } else if unit_nanos % 2 != 0 {
2072 floor + 1
2074 } else {
2075 if floor % 2 == 0 { floor } else { floor + 1 }
2077 };
2078 Self {
2079 nanos: chosen_floor.saturating_mul(unit_nanos),
2080 tz: self.tz.clone(),
2081 }
2082 }
2083
2084 #[must_use]
2096 pub fn floor_to_unit(&self, unit: &str) -> Self {
2097 match Timedelta::unit_to_nanos(unit) {
2098 Some(unit_nanos) => self.floor_to(unit_nanos),
2099 None => Self::nat(),
2100 }
2101 }
2102
2103 #[must_use]
2107 pub fn ceil_to_unit(&self, unit: &str) -> Self {
2108 match Timedelta::unit_to_nanos(unit) {
2109 Some(unit_nanos) => self.ceil_to(unit_nanos),
2110 None => Self::nat(),
2111 }
2112 }
2113
2114 #[must_use]
2118 pub fn round_to_unit(&self, unit: &str) -> Self {
2119 match Timedelta::unit_to_nanos(unit) {
2120 Some(unit_nanos) => self.round_to(unit_nanos),
2121 None => Self::nat(),
2122 }
2123 }
2124
2125 #[must_use]
2127 pub fn floor(&self, freq: &str) -> Self {
2128 self.floor_to_unit(freq)
2129 }
2130
2131 #[must_use]
2133 pub fn ceil(&self, freq: &str) -> Self {
2134 self.ceil_to_unit(freq)
2135 }
2136
2137 #[must_use]
2139 pub fn round(&self, freq: &str) -> Self {
2140 self.round_to_unit(freq)
2141 }
2142
2143 #[must_use]
2147 pub fn year(&self) -> Option<i64> {
2148 if self.is_nat() {
2149 return None;
2150 }
2151 let days_since_epoch = self.nanos.div_euclid(Timedelta::NANOS_PER_DAY);
2155 let days = days_since_epoch + 719_468;
2156 let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
2157 let doe = days - era * 146_097;
2158 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
2159 let y = yoe + era * 400;
2160 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
2161 let mp = (5 * doy + 2) / 153;
2162 let m = if mp < 10 { mp + 3 } else { mp - 9 };
2163 Some(if m <= 2 { y + 1 } else { y })
2164 }
2165
2166 #[must_use]
2170 pub fn month(&self) -> Option<i64> {
2171 if self.is_nat() {
2172 return None;
2173 }
2174 let days_since_epoch = self.nanos.div_euclid(Timedelta::NANOS_PER_DAY);
2178 let days = days_since_epoch + 719_468;
2179 let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
2180 let doe = days - era * 146_097;
2181 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
2182 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
2183 let mp = (5 * doy + 2) / 153;
2184 Some(if mp < 10 { mp + 3 } else { mp - 9 })
2185 }
2186
2187 #[must_use]
2191 pub fn day(&self) -> Option<i64> {
2192 if self.is_nat() {
2193 return None;
2194 }
2195 let days_since_epoch = self.nanos.div_euclid(Timedelta::NANOS_PER_DAY);
2199 let days = days_since_epoch + 719_468;
2200 let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
2201 let doe = days - era * 146_097;
2202 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
2203 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
2204 let mp = (5 * doy + 2) / 153;
2205 Some(doy - (153 * mp + 2) / 5 + 1)
2206 }
2207
2208 #[must_use]
2212 pub fn hour(&self) -> Option<i64> {
2213 if self.is_nat() {
2214 return None;
2215 }
2216 let secs_of_day =
2220 self.nanos.rem_euclid(Timedelta::NANOS_PER_DAY) / Timedelta::NANOS_PER_SEC;
2221 Some(secs_of_day / 3600)
2222 }
2223
2224 #[must_use]
2228 pub fn minute(&self) -> Option<i64> {
2229 if self.is_nat() {
2230 return None;
2231 }
2232 let secs_of_day =
2236 self.nanos.rem_euclid(Timedelta::NANOS_PER_DAY) / Timedelta::NANOS_PER_SEC;
2237 Some((secs_of_day % 3600) / 60)
2238 }
2239
2240 #[must_use]
2244 pub fn second(&self) -> Option<i64> {
2245 if self.is_nat() {
2246 return None;
2247 }
2248 let secs_of_day =
2252 self.nanos.rem_euclid(Timedelta::NANOS_PER_DAY) / Timedelta::NANOS_PER_SEC;
2253 Some(secs_of_day % 60)
2254 }
2255
2256 #[must_use]
2260 pub fn microsecond(&self) -> Option<i64> {
2261 if self.is_nat() {
2262 return None;
2263 }
2264 let sub_nanos = self.nanos.rem_euclid(Timedelta::NANOS_PER_SEC) as u64;
2267 Some((sub_nanos / 1000) as i64)
2268 }
2269
2270 #[must_use]
2274 pub fn nanosecond(&self) -> Option<i64> {
2275 if self.is_nat() {
2276 return None;
2277 }
2278 let sub_nanos = self.nanos.rem_euclid(Timedelta::NANOS_PER_SEC) as u64;
2281 Some((sub_nanos % 1000) as i64)
2282 }
2283
2284 #[must_use]
2288 pub fn dayofweek(&self) -> Option<i64> {
2289 if self.is_nat() {
2290 return None;
2291 }
2292 let days_since_epoch = self.nanos.div_euclid(Timedelta::NANOS_PER_DAY);
2294 let dow = ((days_since_epoch + 3) % 7 + 7) % 7;
2295 Some(dow)
2296 }
2297
2298 #[must_use]
2300 pub fn weekday(&self) -> Option<i64> {
2301 self.dayofweek()
2302 }
2303
2304 #[must_use]
2306 pub fn day_of_week(&self) -> Option<i64> {
2307 self.dayofweek()
2308 }
2309
2310 #[must_use]
2314 pub fn dayofyear(&self) -> Option<i64> {
2315 if self.is_nat() {
2316 return None;
2317 }
2318 let m = self.month()?;
2319 let d = self.day()?;
2320 let y = self.year()?;
2321 let is_leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
2322 let days_before: [i64; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
2323 let base = days_before[(m - 1) as usize] + d;
2324 if is_leap && m > 2 {
2325 Some(base + 1)
2326 } else {
2327 Some(base)
2328 }
2329 }
2330
2331 #[must_use]
2333 pub fn day_of_year(&self) -> Option<i64> {
2334 self.dayofyear()
2335 }
2336
2337 #[must_use]
2341 pub fn toordinal(&self) -> Option<i64> {
2342 if self.is_nat() {
2343 return None;
2344 }
2345 let y = self.year()?;
2346 let m = self.month()?;
2347 let d = self.day()?;
2348 let y_minus_1 = y - 1;
2352 let mut ordinal = y_minus_1 * 365 + y_minus_1 / 4 - y_minus_1 / 100 + y_minus_1 / 400;
2353 let is_leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
2354 let days_before: [i64; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
2355 ordinal += days_before[(m - 1) as usize];
2356 if is_leap && m > 2 {
2357 ordinal += 1;
2358 }
2359 ordinal += d;
2360 Some(ordinal)
2361 }
2362
2363 #[must_use]
2367 pub fn fromordinal(ordinal: i64) -> Self {
2368 if ordinal <= 0 {
2369 return Self {
2370 nanos: Self::NAT,
2371 tz: None,
2372 };
2373 }
2374 let days_since_epoch = ordinal - 719163;
2377 match days_since_epoch.checked_mul(Timedelta::NANOS_PER_DAY) {
2378 Some(nanos) => Self { nanos, tz: None },
2379 None => Self::nat(),
2380 }
2381 }
2382
2383 #[must_use]
2389 pub fn to_julian_date(&self) -> f64 {
2390 if self.is_nat() {
2391 return f64::NAN;
2392 }
2393 let ordinal = match self.toordinal() {
2397 Some(o) => o,
2398 None => return f64::NAN,
2399 };
2400 let h = self.hour().unwrap_or(0) as f64;
2402 let m = self.minute().unwrap_or(0) as f64;
2403 let s = self.second().unwrap_or(0) as f64;
2404 let us = self.microsecond().unwrap_or(0) as f64;
2405 let ns = self.nanosecond().unwrap_or(0) as f64;
2406 let frac_day =
2407 (h + m / 60.0 + s / 3600.0 + us / 3_600_000_000.0 + ns / 3_600_000_000_000.0) / 24.0;
2408 1721424.5 + ordinal as f64 + frac_day
2410 }
2411
2412 #[must_use]
2416 pub fn quarter(&self) -> Option<i64> {
2417 self.month().map(|m| (m - 1) / 3 + 1)
2418 }
2419
2420 #[must_use]
2424 pub fn weekofyear(&self) -> Option<i64> {
2425 if self.is_nat() {
2426 return None;
2427 }
2428 let doy = self.dayofyear()?;
2429 let dow = self.dayofweek()?;
2430 let year = self.year()?;
2431 let iso_dow = if dow == 6 { 7 } else { dow + 1 };
2432 let week = (doy - iso_dow + 10) / 7;
2433 if week < 1 {
2439 Some(iso_weeks_in_year(year - 1))
2440 } else if week > iso_weeks_in_year(year) {
2441 Some(1)
2442 } else {
2443 Some(week)
2444 }
2445 }
2446
2447 #[must_use]
2449 pub fn week(&self) -> Option<i64> {
2450 self.weekofyear()
2451 }
2452
2453 #[must_use]
2458 pub fn to_unit(&self, unit: &str) -> Option<i64> {
2459 if self.is_nat() {
2460 return None;
2461 }
2462 match unit {
2463 "ns" | "nanosecond" | "nanoseconds" => Some(self.nanos),
2464 "us" | "microsecond" | "microseconds" => Some(self.nanos / 1_000),
2465 "ms" | "millisecond" | "milliseconds" => Some(self.nanos / 1_000_000),
2466 "s" | "second" | "seconds" => Some(self.nanos / 1_000_000_000),
2467 _ => None,
2468 }
2469 }
2470
2471 #[must_use]
2475 pub fn is_leap_year(&self) -> Option<bool> {
2476 self.year()
2477 .map(|y| (y % 4 == 0 && y % 100 != 0) || y % 400 == 0)
2478 }
2479
2480 #[must_use]
2484 pub fn is_month_start(&self) -> Option<bool> {
2485 self.day().map(|d| d == 1)
2486 }
2487
2488 #[must_use]
2492 pub fn is_month_end(&self) -> Option<bool> {
2493 let y = self.year()?;
2494 let m = self.month()?;
2495 let d = self.day()?;
2496 let is_leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
2497 let days_in_month: [i64; 12] = [
2498 31,
2499 if is_leap { 29 } else { 28 },
2500 31,
2501 30,
2502 31,
2503 30,
2504 31,
2505 31,
2506 30,
2507 31,
2508 30,
2509 31,
2510 ];
2511 Some(d == days_in_month[(m - 1) as usize])
2512 }
2513
2514 #[must_use]
2518 pub fn is_quarter_start(&self) -> Option<bool> {
2519 let m = self.month()?;
2520 let d = self.day()?;
2521 Some(d == 1 && (m == 1 || m == 4 || m == 7 || m == 10))
2522 }
2523
2524 #[must_use]
2528 pub fn is_quarter_end(&self) -> Option<bool> {
2529 let m = self.month()?;
2530 let d = self.day()?;
2531 Some(
2532 (m == 3 && d == 31)
2533 || (m == 6 && d == 30)
2534 || (m == 9 && d == 30)
2535 || (m == 12 && d == 31),
2536 )
2537 }
2538
2539 #[must_use]
2543 pub fn is_year_start(&self) -> Option<bool> {
2544 let m = self.month()?;
2545 let d = self.day()?;
2546 Some(m == 1 && d == 1)
2547 }
2548
2549 #[must_use]
2553 pub fn is_year_end(&self) -> Option<bool> {
2554 let m = self.month()?;
2555 let d = self.day()?;
2556 Some(m == 12 && d == 31)
2557 }
2558
2559 #[must_use]
2563 pub fn days_in_month(&self) -> Option<i64> {
2564 let y = self.year()?;
2565 let m = self.month()?;
2566 let is_leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
2567 let days: [i64; 12] = [
2568 31,
2569 if is_leap { 29 } else { 28 },
2570 31,
2571 30,
2572 31,
2573 30,
2574 31,
2575 31,
2576 30,
2577 31,
2578 30,
2579 31,
2580 ];
2581 Some(days[(m - 1) as usize])
2582 }
2583
2584 #[must_use]
2586 pub fn daysinmonth(&self) -> Option<i64> {
2587 self.days_in_month()
2588 }
2589
2590 #[must_use]
2592 pub fn normalize(&self) -> Self {
2593 self.floor_to_unit("D")
2594 }
2595
2596 #[must_use]
2600 #[allow(clippy::too_many_arguments)]
2601 pub fn replace(
2602 &self,
2603 year: Option<i64>,
2604 month: Option<i64>,
2605 day: Option<i64>,
2606 hour: Option<i64>,
2607 minute: Option<i64>,
2608 second: Option<i64>,
2609 microsecond: Option<i64>,
2610 nanosecond: Option<i64>,
2611 ) -> Self {
2612 if self.is_nat() {
2613 return self.clone();
2614 }
2615 let cur_year = self.year().unwrap_or(1970);
2616 let cur_month = self.month().unwrap_or(1);
2617 let cur_day = self.day().unwrap_or(1);
2618 let cur_hour = self.hour().unwrap_or(0);
2619 let cur_minute = self.minute().unwrap_or(0);
2620 let cur_second = self.second().unwrap_or(0);
2621 let cur_micro = self.microsecond().unwrap_or(0);
2622 let cur_nano = self.nanosecond().unwrap_or(0);
2623
2624 let y = year.unwrap_or(cur_year);
2625 let mo = month.unwrap_or(cur_month);
2626 let d = day.unwrap_or(cur_day);
2627 let h = hour.unwrap_or(cur_hour);
2628 let mi = minute.unwrap_or(cur_minute);
2629 let s = second.unwrap_or(cur_second);
2630 let us = microsecond.unwrap_or(cur_micro);
2631 let ns = nanosecond.unwrap_or(cur_nano);
2632
2633 if !(1..=12).contains(&mo)
2634 || !(1..=days_in_month(y, mo as u32).unwrap_or(0) as i64).contains(&d)
2635 || !(0..=23).contains(&h)
2636 || !(0..=59).contains(&mi)
2637 || !(0..=59).contains(&s)
2638 || !(0..=999_999).contains(&us)
2639 || !(0..=999).contains(&ns)
2640 {
2641 return Self::nat();
2642 }
2643
2644 let days_from_epoch = Self::days_from_ymd(y, mo, d);
2645 let secs = h * 3600 + mi * 60 + s;
2646 let total_nanos = days_from_epoch * Timedelta::NANOS_PER_DAY
2647 + secs * Timedelta::NANOS_PER_SEC
2648 + us * Timedelta::NANOS_PER_MICRO
2649 + ns;
2650
2651 Self {
2652 nanos: total_nanos,
2653 tz: self.tz.clone(),
2654 }
2655 }
2656
2657 fn days_from_ymd(year: i64, month: i64, day: i64) -> i64 {
2658 let y = if month <= 2 { year - 1 } else { year };
2659 let era = if y >= 0 { y } else { y - 399 } / 400;
2660 let yoe = y - era * 400;
2661 let doy = (153 * (if month > 2 { month - 3 } else { month + 9 }) + 2) / 5 + day - 1;
2662 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
2663 era * 146097 + doe - 719468
2664 }
2665
2666 #[must_use]
2670 pub fn isoformat(&self) -> String {
2671 if self.is_nat() {
2672 return "NaT".to_string();
2673 }
2674 let days_since_epoch = self.nanos.div_euclid(Timedelta::NANOS_PER_DAY);
2677 let nanos_of_day = self.nanos.rem_euclid(Timedelta::NANOS_PER_DAY);
2678 let secs_of_day = nanos_of_day / Timedelta::NANOS_PER_SEC;
2679 let sub_nanos = nanos_of_day.rem_euclid(Timedelta::NANOS_PER_SEC) as u64;
2680
2681 let days = days_since_epoch + 719_468;
2682 let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
2683 let doe = days - era * 146_097;
2684 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
2685 let y = yoe + era * 400;
2686 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
2687 let mp = (5 * doy + 2) / 153;
2688 let d = doy - (153 * mp + 2) / 5 + 1;
2689 let m = if mp < 10 { mp + 3 } else { mp - 9 };
2690 let year = if m <= 2 { y + 1 } else { y };
2691
2692 let hour = secs_of_day / 3600;
2693 let minute = (secs_of_day % 3600) / 60;
2694 let second = secs_of_day % 60;
2695
2696 let base = if sub_nanos == 0 {
2697 format!("{year:04}-{m:02}-{d:02}T{hour:02}:{minute:02}:{second:02}")
2698 } else if sub_nanos.is_multiple_of(1_000) {
2699 format!(
2700 "{year:04}-{m:02}-{d:02}T{hour:02}:{minute:02}:{second:02}.{:06}",
2701 sub_nanos / 1_000
2702 )
2703 } else {
2704 format!("{year:04}-{m:02}-{d:02}T{hour:02}:{minute:02}:{second:02}.{sub_nanos:09}")
2705 };
2706 match &self.tz {
2707 Some(tz) if tz == "UTC" => format!("{base}+00:00"),
2708 Some(tz) => format!("{base}[{tz}]"),
2709 None => base,
2710 }
2711 }
2712
2713 #[must_use]
2715 pub fn to_iso8601(&self) -> String {
2716 self.isoformat()
2717 }
2718
2719 pub fn parse(s: &str) -> Result<Self, TypeError> {
2732 let s = s.trim();
2733
2734 if s.eq_ignore_ascii_case("nat") {
2735 return Ok(Self::nat());
2736 }
2737
2738 let Some((datetime_part, tz)) = Self::split_timezone(s) else {
2739 return Err(TypeError::ValueNotParseable {
2740 value: s.to_string(),
2741 target: "Timestamp".to_string(),
2742 });
2743 };
2744
2745 let (date_part, time_part) = if datetime_part.contains('T') {
2746 datetime_part
2747 .split_once('T')
2748 .ok_or_else(|| TypeError::ValueNotParseable {
2749 value: s.to_string(),
2750 target: "Timestamp".to_string(),
2751 })?
2752 } else if datetime_part.contains(' ')
2753 && datetime_part.chars().filter(|&c| c == ' ').count() == 1
2754 {
2755 datetime_part
2756 .split_once(' ')
2757 .ok_or_else(|| TypeError::ValueNotParseable {
2758 value: s.to_string(),
2759 target: "Timestamp".to_string(),
2760 })?
2761 } else {
2762 (datetime_part, "00:00:00")
2763 };
2764
2765 let (year, month, day) =
2766 Self::parse_date(date_part).ok_or_else(|| TypeError::ValueNotParseable {
2767 value: s.to_string(),
2768 target: "Timestamp".to_string(),
2769 })?;
2770
2771 let (hour, minute, second, nanos) =
2772 Self::parse_time(time_part).ok_or_else(|| TypeError::ValueNotParseable {
2773 value: s.to_string(),
2774 target: "Timestamp".to_string(),
2775 })?;
2776
2777 let total_nanos = Self::ymd_hms_to_nanos(year, month, day, hour, minute, second, nanos);
2778
2779 Ok(if let Some(tz_name) = tz {
2780 Self::from_nanos_tz(total_nanos, tz_name)
2781 } else {
2782 Self::from_nanos(total_nanos)
2783 })
2784 }
2785
2786 fn split_timezone(s: &str) -> Option<(&str, Option<String>)> {
2787 if let Some(stripped) = s.strip_suffix('Z') {
2788 Some((stripped, Some("UTC".to_string())))
2789 } else if let Some(idx) = s.rfind('+') {
2790 if idx > 10 && Self::is_timezone_offset(&s[idx..]) {
2791 Some((&s[..idx], Some(s[idx..].to_string())))
2792 } else if idx > 10 {
2793 None
2794 } else {
2795 Some((s, None))
2796 }
2797 } else if let Some(idx) = s.rfind('-') {
2798 if idx > 10 && Self::is_timezone_offset(&s[idx..]) {
2799 Some((&s[..idx], Some(s[idx..].to_string())))
2800 } else if idx > 10 {
2801 None
2802 } else {
2803 Some((s, None))
2804 }
2805 } else {
2806 Some((s, None))
2807 }
2808 }
2809
2810 fn is_timezone_offset(s: &str) -> bool {
2811 let bytes = s.as_bytes();
2812 if bytes.len() != 6 || !matches!(bytes[0], b'+' | b'-') || bytes[3] != b':' {
2813 return false;
2814 }
2815 if !bytes[1..3].iter().all(u8::is_ascii_digit)
2816 || !bytes[4..6].iter().all(u8::is_ascii_digit)
2817 {
2818 return false;
2819 }
2820 let hour = u32::from(bytes[1] - b'0') * 10 + u32::from(bytes[2] - b'0');
2821 let minute = u32::from(bytes[4] - b'0') * 10 + u32::from(bytes[5] - b'0');
2822 hour <= 23 && minute <= 59
2823 }
2824
2825 fn parse_date(s: &str) -> Option<(i64, u32, u32)> {
2826 let parts: Vec<&str> = s.split('-').collect();
2827 if parts.len() != 3 {
2828 return None;
2829 }
2830 let year: i64 = parts[0].parse().ok()?;
2831 let month: u32 = parts[1].parse().ok()?;
2832 let day: u32 = parts[2].parse().ok()?;
2833 if !(1..=days_in_month(year, month)?).contains(&day) {
2834 return None;
2835 }
2836 Some((year, month, day))
2837 }
2838
2839 fn parse_time(s: &str) -> Option<(u32, u32, u32, u64)> {
2840 let (time_str, frac_str) = match s.split_once('.') {
2841 Some((time, frac)) => (time, Some(frac)),
2842 None => (s, None),
2843 };
2844 let parts: Vec<&str> = time_str.split(':').collect();
2845 if parts.is_empty() || parts.len() > 3 {
2846 return None;
2847 }
2848 let hour: u32 = parts.first().and_then(|p| p.parse().ok())?;
2849 let minute: u32 = parts.get(1).and_then(|p| p.parse().ok()).unwrap_or(0);
2850 let second: u32 = parts.get(2).and_then(|p| p.parse().ok()).unwrap_or(0);
2851
2852 if hour > 23 || minute > 59 || second > 59 {
2853 return None;
2854 }
2855
2856 let nanos = match frac_str {
2857 None => 0,
2858 Some(frac) => {
2859 if frac.is_empty() || !frac.bytes().all(|b| b.is_ascii_digit()) {
2860 return None;
2861 }
2862 let truncated = &frac[..frac.len().min(9)];
2863 let padded = format!("{truncated:0<9}");
2864 padded.parse::<u64>().ok()?
2865 }
2866 };
2867
2868 Some((hour, minute, second, nanos))
2869 }
2870
2871 fn ymd_hms_to_nanos(
2872 year: i64,
2873 month: u32,
2874 day: u32,
2875 hour: u32,
2876 minute: u32,
2877 second: u32,
2878 sub_nanos: u64,
2879 ) -> i64 {
2880 let m = month as i64;
2881 let d = day as i64;
2882
2883 let y = if m <= 2 { year - 1 } else { year };
2884 let era = if y >= 0 { y } else { y - 399 } / 400;
2885 let yoe = y - era * 400;
2886 let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1;
2887 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
2888 let days_since_epoch = era * 146_097 + doe - 719_468;
2889
2890 let total_seconds = days_since_epoch * 86400
2891 + (hour as i64) * 3600
2892 + (minute as i64) * 60
2893 + (second as i64);
2894 total_seconds * Timedelta::NANOS_PER_SEC + sub_nanos as i64
2895 }
2896
2897 #[must_use]
2903 pub fn strftime(&self, format: &str) -> String {
2904 if self.is_nat() {
2905 return "NaT".to_string();
2906 }
2907 let total_secs = self.nanos / Timedelta::NANOS_PER_SEC;
2908 let sub_nanos = self.nanos.rem_euclid(Timedelta::NANOS_PER_SEC) as u64;
2911
2912 let days_since_epoch = total_secs / 86400;
2913 let secs_of_day = (total_secs % 86400 + 86400) % 86400;
2914
2915 let days = days_since_epoch + 719_468;
2916 let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
2917 let doe = days - era * 146_097;
2918 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
2919 let y = yoe + era * 400;
2920 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
2921 let mp = (5 * doy + 2) / 153;
2922 let d = doy - (153 * mp + 2) / 5 + 1;
2923 let m = if mp < 10 { mp + 3 } else { mp - 9 };
2924 let year = if m <= 2 { y + 1 } else { y };
2925
2926 let hour = secs_of_day / 3600;
2927 let minute = (secs_of_day % 3600) / 60;
2928 let second = secs_of_day % 60;
2929 let micros = sub_nanos / 1000;
2930
2931 format
2932 .replace("%Y", &format!("{year:04}"))
2933 .replace("%m", &format!("{m:02}"))
2934 .replace("%d", &format!("{d:02}"))
2935 .replace("%H", &format!("{hour:02}"))
2936 .replace("%M", &format!("{minute:02}"))
2937 .replace("%S", &format!("{second:02}"))
2938 .replace("%f", &format!("{micros:06}"))
2939 }
2940
2941 #[must_use]
2945 pub fn day_name(&self) -> String {
2946 const NAMES: [&str; 7] = [
2947 "Thursday",
2948 "Friday",
2949 "Saturday",
2950 "Sunday",
2951 "Monday",
2952 "Tuesday",
2953 "Wednesday",
2954 ];
2955 if self.is_nat() {
2956 return "NaT".to_string();
2957 }
2958 let days_since_epoch = self.nanos / Timedelta::NANOS_PER_DAY;
2959 let dow = ((days_since_epoch % 7) + 7) % 7;
2960 NAMES[dow as usize].to_string()
2961 }
2962
2963 #[must_use]
2967 pub fn month_name(&self) -> String {
2968 const NAMES: [&str; 12] = [
2969 "January",
2970 "February",
2971 "March",
2972 "April",
2973 "May",
2974 "June",
2975 "July",
2976 "August",
2977 "September",
2978 "October",
2979 "November",
2980 "December",
2981 ];
2982 if self.is_nat() {
2983 return "NaT".to_string();
2984 }
2985 let days_since_epoch = self.nanos.div_euclid(Timedelta::NANOS_PER_DAY);
2989 let days = days_since_epoch + 719_468;
2990 let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
2991 let doe = days - era * 146_097;
2992 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
2993 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
2994 let mp = (5 * doy + 2) / 153;
2995 let m = if mp < 10 { mp + 3 } else { mp - 9 };
2996 NAMES[(m - 1) as usize].to_string()
2997 }
2998
2999 #[must_use]
3004 pub fn tz_localize(&self, tz: Option<&str>) -> Self {
3005 if self.is_nat() {
3006 return Self::nat();
3007 }
3008 Self {
3009 nanos: self.nanos,
3010 tz: tz.map(String::from),
3011 }
3012 }
3013
3014 #[must_use]
3020 pub fn tz_convert(&self, tz: &str) -> Self {
3021 if self.is_nat() {
3022 return Self::nat();
3023 }
3024 Self {
3025 nanos: self.nanos,
3026 tz: Some(tz.to_string()),
3027 }
3028 }
3029
3030 #[must_use]
3035 pub fn fromtimestamp(ts: f64, tz: Option<&str>) -> Self {
3036 if ts.is_nan() || ts.is_infinite() {
3037 return Self::nat();
3038 }
3039 let nanos_f64 = ts * 1_000_000_000.0;
3040 const MAX_NANOS: f64 = i64::MAX as f64;
3042 const MIN_NANOS: f64 = i64::MIN as f64;
3043 if !(MIN_NANOS..=MAX_NANOS).contains(&nanos_f64) {
3044 return Self::nat();
3045 }
3046 Self {
3047 nanos: nanos_f64 as i64,
3048 tz: tz.map(String::from),
3049 }
3050 }
3051
3052 #[must_use]
3056 pub fn from_millis(ms: i64, tz: Option<&str>) -> Self {
3057 Self {
3058 nanos: ms.saturating_mul(1_000_000),
3059 tz: tz.map(String::from),
3060 }
3061 }
3062
3063 #[must_use]
3067 pub fn from_micros(us: i64, tz: Option<&str>) -> Self {
3068 Self {
3069 nanos: us.saturating_mul(1_000),
3070 tz: tz.map(String::from),
3071 }
3072 }
3073
3074 #[must_use]
3076 pub fn tzinfo(&self) -> Option<&str> {
3077 self.tz.as_deref()
3078 }
3079
3080 #[must_use]
3084 pub fn tzname(&self) -> Option<&str> {
3085 self.tzinfo()
3086 }
3087}
3088
3089impl std::fmt::Display for Timestamp {
3090 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3093 if self.is_nat() {
3094 return f.write_str("NaT");
3095 }
3096 match &self.tz {
3097 Some(tz) => write!(f, "Timestamp[{}, {}]", self.nanos, tz),
3098 None => write!(f, "Timestamp[{}, UTC]", self.nanos),
3099 }
3100 }
3101}
3102
3103impl PartialOrd for Timestamp {
3104 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
3109 if self.is_nat() || other.is_nat() {
3110 return None;
3111 }
3112 Some(self.nanos.cmp(&other.nanos))
3113 }
3114}
3115
3116pub fn isna(values: &[Scalar]) -> Vec<bool> {
3119 values.iter().map(Scalar::is_missing).collect()
3120}
3121
3122pub fn isnull(values: &[Scalar]) -> Vec<bool> {
3123 isna(values)
3124}
3125
3126pub fn notna(values: &[Scalar]) -> Vec<bool> {
3127 values.iter().map(|v| !v.is_missing()).collect()
3128}
3129
3130pub fn notnull(values: &[Scalar]) -> Vec<bool> {
3131 notna(values)
3132}
3133
3134pub fn count_na(values: &[Scalar]) -> usize {
3135 values.iter().filter(|v| v.is_missing()).count()
3136}
3137
3138pub fn fill_na(values: &[Scalar], fill: &Scalar) -> Vec<Scalar> {
3139 values
3140 .iter()
3141 .map(|v| {
3142 if v.is_missing() {
3143 fill.clone()
3144 } else {
3145 v.clone()
3146 }
3147 })
3148 .collect()
3149}
3150
3151pub fn dropna(values: &[Scalar]) -> Vec<Scalar> {
3152 values.iter().filter(|v| !v.is_missing()).cloned().collect()
3153}
3154
3155fn collect_finite(values: &[Scalar]) -> Vec<f64> {
3158 values
3159 .iter()
3160 .filter(|v| !v.is_missing())
3161 .filter_map(|v| v.to_f64().ok())
3162 .collect()
3163}
3164
3165fn collect_timedelta_ns(values: &[Scalar]) -> Option<(i128, usize)> {
3171 let mut sum: i128 = 0;
3172 let mut count: usize = 0;
3173 let mut saw_timedelta = false;
3174 for v in values {
3175 if v.is_missing() {
3176 continue;
3177 }
3178 match v {
3179 Scalar::Timedelta64(ns) => {
3180 saw_timedelta = true;
3181 sum += i128::from(*ns);
3182 count += 1;
3183 }
3184 _ => return None,
3187 }
3188 }
3189 if saw_timedelta {
3190 Some((sum, count))
3191 } else {
3192 None
3193 }
3194}
3195
3196pub fn nansum(values: &[Scalar]) -> Scalar {
3197 if let Some((sum, _)) = collect_timedelta_ns(values) {
3198 let clamped = sum.clamp(i128::from(i64::MIN), i128::from(i64::MAX));
3199 return Scalar::Timedelta64(clamped as i64);
3200 }
3201 let mut sum = 0.0_f64;
3206 for v in values {
3207 if v.is_missing() {
3208 continue;
3209 }
3210 if let Ok(x) = v.to_f64() {
3211 sum += x;
3212 }
3213 }
3214 Scalar::Float64(sum)
3215}
3216
3217pub fn nanmean(values: &[Scalar]) -> Scalar {
3218 if let Some((sum, count)) = collect_timedelta_ns(values) {
3219 if count == 0 {
3220 return Scalar::Timedelta64(Timedelta::NAT);
3221 }
3222 let mean = sum / count as i128;
3223 let clamped = mean.clamp(i128::from(i64::MIN), i128::from(i64::MAX));
3224 return Scalar::Timedelta64(clamped as i64);
3225 }
3226 let mut sum = 0.0_f64;
3230 let mut count = 0usize;
3231 for v in values {
3232 if v.is_missing() {
3233 continue;
3234 }
3235 if let Ok(x) = v.to_f64() {
3236 sum += x;
3237 count += 1;
3238 }
3239 }
3240 if count == 0 {
3241 return Scalar::Null(NullKind::NaN);
3242 }
3243 Scalar::Float64(sum / count as f64)
3244}
3245
3246pub fn nanany(values: &[Scalar]) -> Scalar {
3247 for v in values {
3248 if v.is_missing() {
3249 continue;
3250 }
3251 match v {
3252 Scalar::Bool(flag) if *flag => return Scalar::Bool(true),
3253 Scalar::Int64(val) if *val != 0 => return Scalar::Bool(true),
3254 Scalar::Float64(val) if !val.is_nan() && *val != 0.0 => return Scalar::Bool(true),
3255 Scalar::Utf8(val) if !val.is_empty() => return Scalar::Bool(true),
3256 Scalar::Timedelta64(ns) if *ns != 0 => return Scalar::Bool(true),
3259 _ => continue,
3260 }
3261 }
3262 Scalar::Bool(false)
3263}
3264
3265pub fn nanall(values: &[Scalar]) -> Scalar {
3266 for v in values {
3267 if v.is_missing() {
3268 continue;
3269 }
3270 match v {
3271 Scalar::Bool(flag) if !*flag => return Scalar::Bool(false),
3272 Scalar::Int64(val) if *val == 0 => return Scalar::Bool(false),
3273 Scalar::Float64(val) if val.is_nan() || *val == 0.0 => return Scalar::Bool(false),
3274 Scalar::Utf8(val) if val.is_empty() => return Scalar::Bool(false),
3275 Scalar::Timedelta64(ns) if *ns == 0 => return Scalar::Bool(false),
3278 _ => continue,
3279 }
3280 }
3281 Scalar::Bool(true)
3282}
3283
3284pub fn nancount(values: &[Scalar]) -> Scalar {
3285 let n = values.iter().filter(|v| !v.is_missing()).count();
3286 Scalar::Int64(n as i64)
3287}
3288
3289pub fn nanmin(values: &[Scalar]) -> Scalar {
3290 let mut min: Option<&Scalar> = None;
3291 for v in values {
3292 if v.is_missing() {
3293 continue;
3294 }
3295 match (min, v) {
3296 (None, _) => min = Some(v),
3297 (Some(Scalar::Int64(a)), Scalar::Int64(b)) => {
3298 if b < a {
3299 min = Some(v)
3300 }
3301 }
3302 (Some(Scalar::Float64(a)), Scalar::Float64(b)) => {
3303 if *b < *a {
3304 min = Some(v)
3305 }
3306 }
3307 (Some(Scalar::Utf8(a)), Scalar::Utf8(b)) => {
3308 if b < a {
3309 min = Some(v)
3310 }
3311 }
3312 (Some(Scalar::Bool(a)), Scalar::Bool(b)) => {
3313 if b < a {
3314 min = Some(v)
3315 }
3316 }
3317 (Some(Scalar::Timedelta64(a)), Scalar::Timedelta64(b)) => {
3322 if b < a {
3323 min = Some(v)
3324 }
3325 }
3326 (Some(a), b) => match (a.to_f64(), b.to_f64()) {
3327 (Ok(af), Ok(bf)) if bf < af => min = Some(v),
3328 (Ok(_), Ok(_)) => {}
3329 _ => return Scalar::Null(NullKind::NaN),
3330 },
3331 }
3332 }
3333 match min {
3334 Some(v) => v.clone(),
3335 None => Scalar::Null(NullKind::NaN),
3336 }
3337}
3338
3339pub fn nanmax(values: &[Scalar]) -> Scalar {
3340 let mut max: Option<&Scalar> = None;
3341 for v in values {
3342 if v.is_missing() {
3343 continue;
3344 }
3345 match (max, v) {
3346 (None, _) => max = Some(v),
3347 (Some(Scalar::Int64(a)), Scalar::Int64(b)) => {
3348 if b > a {
3349 max = Some(v)
3350 }
3351 }
3352 (Some(Scalar::Float64(a)), Scalar::Float64(b)) => {
3353 if *b > *a {
3354 max = Some(v)
3355 }
3356 }
3357 (Some(Scalar::Utf8(a)), Scalar::Utf8(b)) => {
3358 if b > a {
3359 max = Some(v)
3360 }
3361 }
3362 (Some(Scalar::Bool(a)), Scalar::Bool(b)) => {
3363 if b > a {
3364 max = Some(v)
3365 }
3366 }
3367 (Some(Scalar::Timedelta64(a)), Scalar::Timedelta64(b)) => {
3371 if b > a {
3372 max = Some(v)
3373 }
3374 }
3375 (Some(a), b) => match (a.to_f64(), b.to_f64()) {
3376 (Ok(af), Ok(bf)) if bf > af => max = Some(v),
3377 (Ok(_), Ok(_)) => {}
3378 _ => return Scalar::Null(NullKind::NaN),
3379 },
3380 }
3381 }
3382 match max {
3383 Some(v) => v.clone(),
3384 None => Scalar::Null(NullKind::NaN),
3385 }
3386}
3387
3388fn collect_timedelta_ns_f64(values: &[Scalar]) -> Option<Vec<f64>> {
3394 let mut out = Vec::with_capacity(values.len());
3395 let mut saw_td = false;
3396 for v in values {
3397 if v.is_missing() {
3398 continue;
3399 }
3400 match v {
3401 Scalar::Timedelta64(ns) => {
3402 saw_td = true;
3403 out.push(*ns as f64);
3404 }
3405 _ => return None,
3406 }
3407 }
3408 if saw_td { Some(out) } else { None }
3409}
3410
3411fn float_ns_to_timedelta(value: f64) -> Scalar {
3413 if !value.is_finite() {
3414 return Scalar::Timedelta64(Timedelta::NAT);
3415 }
3416 let clamped = value.clamp(i64::MIN as f64, i64::MAX as f64);
3417 Scalar::Timedelta64(clamped as i64)
3418}
3419
3420pub fn nanmedian(values: &[Scalar]) -> Scalar {
3421 if let Some(mut td) = collect_timedelta_ns_f64(values) {
3423 if td.is_empty() {
3424 return Scalar::Timedelta64(Timedelta::NAT);
3425 }
3426 let n = td.len();
3431 let mid = n / 2;
3432 let cmp = |a: &f64, b: &f64| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal);
3433 let (left, mid_ref, _right) = td.select_nth_unstable_by(mid, cmp);
3434 let mid_val = *mid_ref;
3435 let median_ns = if n.is_multiple_of(2) {
3436 let lower = left.iter().copied().fold(f64::NEG_INFINITY, f64::max);
3437 (lower + mid_val) / 2.0
3438 } else {
3439 mid_val
3440 };
3441 return float_ns_to_timedelta(median_ns);
3442 }
3443 let mut nums = collect_finite(values);
3444 if nums.is_empty() {
3445 return Scalar::Null(NullKind::NaN);
3446 }
3447 let n = nums.len();
3454 let mid = n / 2;
3455 let cmp = |a: &f64, b: &f64| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal);
3456 let (left, mid_ref, _right) = nums.select_nth_unstable_by(mid, cmp);
3457 let mid_val = *mid_ref;
3458 if n.is_multiple_of(2) {
3459 let lower = left.iter().copied().fold(f64::NEG_INFINITY, f64::max);
3460 Scalar::Float64((lower + mid_val) / 2.0)
3461 } else {
3462 Scalar::Float64(mid_val)
3463 }
3464}
3465
3466pub fn nanvar(values: &[Scalar], ddof: usize) -> Scalar {
3467 if let Some(td) = collect_timedelta_ns_f64(values) {
3470 if td.len() <= ddof {
3471 return Scalar::Timedelta64(Timedelta::NAT);
3472 }
3473 let mean: f64 = td.iter().sum::<f64>() / td.len() as f64;
3474 let sum_sq: f64 = td.iter().map(|x| (x - mean).powi(2)).sum();
3475 return float_ns_to_timedelta(sum_sq / (td.len() - ddof) as f64);
3476 }
3477 let nums = collect_finite(values);
3478 if nums.len() <= ddof {
3479 return Scalar::Null(NullKind::NaN);
3480 }
3481 let mean: f64 = nums.iter().sum::<f64>() / nums.len() as f64;
3482 let sum_sq: f64 = nums.iter().map(|x| (x - mean).powi(2)).sum();
3483 Scalar::Float64(sum_sq / (nums.len() - ddof) as f64)
3484}
3485
3486pub fn nanstd(values: &[Scalar], ddof: usize) -> Scalar {
3487 if let Some(td) = collect_timedelta_ns_f64(values) {
3489 if td.len() <= ddof {
3490 return Scalar::Timedelta64(Timedelta::NAT);
3491 }
3492 let mean: f64 = td.iter().sum::<f64>() / td.len() as f64;
3493 let sum_sq: f64 = td.iter().map(|x| (x - mean).powi(2)).sum();
3494 let var = sum_sq / (td.len() - ddof) as f64;
3495 return float_ns_to_timedelta(var.sqrt());
3496 }
3497 match nanvar(values, ddof) {
3498 Scalar::Float64(v) => Scalar::Float64(v.sqrt()),
3499 other => other,
3500 }
3501}
3502
3503pub fn nansem(values: &[Scalar], ddof: usize) -> Scalar {
3509 if let Some(td) = collect_timedelta_ns_f64(values) {
3511 if td.len() <= ddof {
3512 return Scalar::Timedelta64(Timedelta::NAT);
3513 }
3514 let mean: f64 = td.iter().sum::<f64>() / td.len() as f64;
3515 let sum_sq: f64 = td.iter().map(|x| (x - mean).powi(2)).sum();
3516 let var = sum_sq / (td.len() - ddof) as f64;
3517 let std = var.sqrt();
3518 return float_ns_to_timedelta(std / (td.len() as f64).sqrt());
3519 }
3520 let nums = collect_finite(values);
3521 if nums.len() <= ddof {
3522 return Scalar::Null(NullKind::NaN);
3523 }
3524 match nanstd(values, ddof) {
3525 Scalar::Float64(s) => Scalar::Float64(s / (nums.len() as f64).sqrt()),
3526 other => other,
3527 }
3528}
3529
3530pub fn nanptp(values: &[Scalar]) -> Scalar {
3535 if let Some(td) = collect_timedelta_ns_f64(values) {
3539 if td.is_empty() {
3540 return Scalar::Timedelta64(Timedelta::NAT);
3541 }
3542 let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
3543 for x in &td {
3544 if *x < lo {
3545 lo = *x;
3546 }
3547 if *x > hi {
3548 hi = *x;
3549 }
3550 }
3551 return float_ns_to_timedelta(hi - lo);
3552 }
3553 let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
3558 let mut seen = false;
3559 for v in values {
3560 if v.is_missing() {
3561 continue;
3562 }
3563 if let Ok(x) = v.to_f64() {
3564 seen = true;
3565 if x < lo {
3566 lo = x;
3567 }
3568 if x > hi {
3569 hi = x;
3570 }
3571 }
3572 }
3573 if !seen {
3574 return Scalar::Null(NullKind::NaN);
3575 }
3576 Scalar::Float64(hi - lo)
3577}
3578
3579pub fn nanskew(values: &[Scalar]) -> Scalar {
3585 let nums = collect_finite(values);
3586 let n = nums.len() as f64;
3587 if n < 3.0 {
3588 return Scalar::Null(NullKind::NaN);
3589 }
3590 let mean = nums.iter().sum::<f64>() / n;
3591 let m2: f64 = nums.iter().map(|x| (x - mean).powi(2)).sum();
3592 let m3: f64 = nums.iter().map(|x| (x - mean).powi(3)).sum();
3593 let s2 = m2 / (n - 1.0);
3594 if s2 == 0.0 {
3595 return Scalar::Float64(0.0);
3596 }
3597 let s3 = s2.powf(1.5);
3598 Scalar::Float64((n / ((n - 1.0) * (n - 2.0))) * (m3 / s3))
3599}
3600
3601pub fn nankurt(values: &[Scalar]) -> Scalar {
3608 let nums = collect_finite(values);
3609 let n = nums.len() as f64;
3610 if n < 4.0 {
3611 return Scalar::Null(NullKind::NaN);
3612 }
3613 let mean = nums.iter().sum::<f64>() / n;
3614 let m2: f64 = nums.iter().map(|x| (x - mean).powi(2)).sum();
3615 let m4: f64 = nums.iter().map(|x| (x - mean).powi(4)).sum();
3616 let s2 = m2 / (n - 1.0);
3617 if s2 == 0.0 {
3618 return Scalar::Float64(0.0);
3619 }
3620 let adj = (n * (n + 1.0)) / ((n - 1.0) * (n - 2.0) * (n - 3.0));
3621 let sub = (3.0 * (n - 1.0).powi(2)) / ((n - 2.0) * (n - 3.0));
3622 Scalar::Float64(adj * (m4 / (s2 * s2)) - sub)
3623}
3624
3625pub fn nanprod(values: &[Scalar]) -> Scalar {
3627 if is_timedelta_input(values) {
3634 return Scalar::Null(NullKind::NaN);
3635 }
3636 let mut prod = 1.0_f64;
3641 for v in values {
3642 if v.is_missing() {
3643 continue;
3644 }
3645 if let Ok(x) = v.to_f64() {
3646 prod *= x;
3647 }
3648 }
3649 Scalar::Float64(prod)
3650}
3651
3652fn is_timedelta_input(values: &[Scalar]) -> bool {
3658 let mut saw_td = false;
3659 for v in values {
3660 if v.is_missing() {
3661 continue;
3662 }
3663 match v {
3664 Scalar::Timedelta64(_) => saw_td = true,
3665 _ => return false,
3666 }
3667 }
3668 saw_td
3669}
3670
3671fn timedelta_cumulative<F>(values: &[Scalar], init: i128, mut step: F) -> Vec<Scalar>
3676where
3677 F: FnMut(i128, i128) -> i128,
3678{
3679 let mut out = Vec::with_capacity(values.len());
3680 let mut running: i128 = init;
3681 for v in values {
3682 if v.is_missing() {
3683 out.push(Scalar::Null(NullKind::NaT));
3684 continue;
3685 }
3686 if let Scalar::Timedelta64(ns) = v {
3687 running = step(running, i128::from(*ns));
3688 let clamped = running.clamp(i128::from(i64::MIN), i128::from(i64::MAX));
3689 out.push(Scalar::Timedelta64(clamped as i64));
3690 } else {
3691 out.push(Scalar::Null(NullKind::NaT));
3692 }
3693 }
3694 out
3695}
3696
3697fn timedelta_cumulative_extrema<F>(values: &[Scalar], sentinel: i64, mut step: F) -> Vec<Scalar>
3702where
3703 F: FnMut(i64, i64) -> i64,
3704{
3705 let mut out = Vec::with_capacity(values.len());
3706 let mut running: Option<i64> = None;
3707 for v in values {
3708 if v.is_missing() {
3709 out.push(Scalar::Null(NullKind::NaT));
3710 continue;
3711 }
3712 if let Scalar::Timedelta64(ns) = v {
3713 let new_val = match running {
3714 Some(prev) => step(prev, *ns),
3715 None => *ns,
3716 };
3717 running = Some(new_val);
3718 out.push(Scalar::Timedelta64(new_val));
3719 } else {
3720 out.push(Scalar::Null(NullKind::NaT));
3721 }
3722 }
3723 let _ = sentinel; out
3725}
3726
3727pub fn nancumsum(values: &[Scalar]) -> Vec<Scalar> {
3731 if is_timedelta_input(values) {
3735 return timedelta_cumulative(values, 0_i128, |acc, x| acc.saturating_add(x));
3736 }
3737 let mut out = Vec::with_capacity(values.len());
3738 let mut running = 0.0_f64;
3739 for v in values {
3740 if v.is_missing() {
3741 out.push(Scalar::Null(NullKind::NaN));
3742 continue;
3743 }
3744 match v.to_f64() {
3745 Ok(x) if !x.is_nan() => {
3746 running += x;
3747 out.push(Scalar::Float64(running));
3748 }
3749 _ => out.push(Scalar::Null(NullKind::NaN)),
3750 }
3751 }
3752 out
3753}
3754
3755pub fn nancumprod(values: &[Scalar]) -> Vec<Scalar> {
3760 let mut out = Vec::with_capacity(values.len());
3761 let mut running = 1.0_f64;
3762 for v in values {
3763 if v.is_missing() {
3764 out.push(Scalar::Null(NullKind::NaN));
3765 continue;
3766 }
3767 match v.to_f64() {
3768 Ok(x) if !x.is_nan() => {
3769 running *= x;
3770 out.push(Scalar::Float64(running));
3771 }
3772 _ => out.push(Scalar::Null(NullKind::NaN)),
3773 }
3774 }
3775 out
3776}
3777
3778pub fn nancummax(values: &[Scalar]) -> Vec<Scalar> {
3784 if is_timedelta_input(values) {
3786 return timedelta_cumulative_extrema(values, i64::MAX, |acc, x| acc.max(x));
3787 }
3788 let mut out = Vec::with_capacity(values.len());
3789 let mut running: Option<f64> = None;
3790 for v in values {
3791 if v.is_missing() {
3792 out.push(Scalar::Null(NullKind::NaN));
3793 continue;
3794 }
3795 match v.to_f64() {
3796 Ok(x) if !x.is_nan() => {
3797 let new_val = match running {
3798 Some(prev) => prev.max(x),
3799 None => x,
3800 };
3801 running = Some(new_val);
3802 out.push(Scalar::Float64(new_val));
3803 }
3804 _ => out.push(Scalar::Null(NullKind::NaN)),
3805 }
3806 }
3807 out
3808}
3809
3810pub fn nancummin(values: &[Scalar]) -> Vec<Scalar> {
3814 if is_timedelta_input(values) {
3816 return timedelta_cumulative_extrema(values, i64::MIN, |acc, x| acc.min(x));
3817 }
3818 let mut out = Vec::with_capacity(values.len());
3819 let mut running: Option<f64> = None;
3820 for v in values {
3821 if v.is_missing() {
3822 out.push(Scalar::Null(NullKind::NaN));
3823 continue;
3824 }
3825 match v.to_f64() {
3826 Ok(x) if !x.is_nan() => {
3827 let new_val = match running {
3828 Some(prev) => prev.min(x),
3829 None => x,
3830 };
3831 running = Some(new_val);
3832 out.push(Scalar::Float64(new_val));
3833 }
3834 _ => out.push(Scalar::Null(NullKind::NaN)),
3835 }
3836 }
3837 out
3838}
3839
3840pub fn nanquantile(values: &[Scalar], q: f64) -> Scalar {
3846 if !(0.0..=1.0).contains(&q) {
3847 return Scalar::Null(NullKind::NaN);
3848 }
3849 if let Some(mut td) = collect_timedelta_ns_f64(values) {
3852 if td.is_empty() {
3853 return Scalar::Timedelta64(Timedelta::NAT);
3854 }
3855 let n = td.len();
3856 if n == 1 {
3857 return float_ns_to_timedelta(td[0]);
3858 }
3859 let pos = q * (n - 1) as f64;
3860 let lo = pos.floor() as usize;
3861 let hi = pos.ceil() as usize;
3862 let cmp = |a: &f64, b: &f64| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal);
3866 let (_left, lo_ref, right) = td.select_nth_unstable_by(lo, cmp);
3867 let lo_val = *lo_ref;
3868 let ns = if lo == hi {
3869 lo_val
3870 } else {
3871 let hi_val = right.iter().copied().fold(f64::INFINITY, f64::min);
3872 let weight = pos - lo as f64;
3873 lo_val + (hi_val - lo_val) * weight
3874 };
3875 return float_ns_to_timedelta(ns);
3876 }
3877 let mut nums = collect_finite(values);
3878 if nums.is_empty() {
3879 return Scalar::Null(NullKind::NaN);
3880 }
3881 let n = nums.len();
3882 if n == 1 {
3883 return Scalar::Float64(nums[0]);
3884 }
3885 let pos = q * (n - 1) as f64;
3886 let lo = pos.floor() as usize;
3887 let hi = pos.ceil() as usize;
3888 let cmp = |a: &f64, b: &f64| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal);
3893 let (_left, lo_ref, right) = nums.select_nth_unstable_by(lo, cmp);
3894 let lo_val = *lo_ref;
3895 if lo == hi {
3896 return Scalar::Float64(lo_val);
3897 }
3898 let hi_val = right.iter().copied().fold(f64::INFINITY, f64::min);
3899 let weight = pos - lo as f64;
3900 Scalar::Float64(lo_val + (hi_val - lo_val) * weight)
3901}
3902
3903pub fn nanargmax(values: &[Scalar]) -> Option<usize> {
3908 if is_timedelta_input(values) {
3913 let mut best: Option<(usize, i64)> = None;
3914 for (i, v) in values.iter().enumerate() {
3915 if v.is_missing() {
3916 continue;
3917 }
3918 if let Scalar::Timedelta64(ns) = v {
3919 match best {
3920 None => best = Some((i, *ns)),
3921 Some((_, cur)) if *ns > cur => best = Some((i, *ns)),
3922 _ => {}
3923 }
3924 }
3925 }
3926 return best.map(|(i, _)| i);
3927 }
3928 let mut best: Option<(usize, f64)> = None;
3929 for (i, v) in values.iter().enumerate() {
3930 if v.is_missing() {
3931 continue;
3932 }
3933 if let Ok(x) = v.to_f64() {
3934 if x.is_nan() {
3935 continue;
3936 }
3937 match best {
3938 None => best = Some((i, x)),
3939 Some((_, cur)) if x > cur => best = Some((i, x)),
3940 _ => {}
3941 }
3942 }
3943 }
3944 best.map(|(i, _)| i)
3945}
3946
3947pub fn nanargmin(values: &[Scalar]) -> Option<usize> {
3951 if is_timedelta_input(values) {
3953 let mut best: Option<(usize, i64)> = None;
3954 for (i, v) in values.iter().enumerate() {
3955 if v.is_missing() {
3956 continue;
3957 }
3958 if let Scalar::Timedelta64(ns) = v {
3959 match best {
3960 None => best = Some((i, *ns)),
3961 Some((_, cur)) if *ns < cur => best = Some((i, *ns)),
3962 _ => {}
3963 }
3964 }
3965 }
3966 return best.map(|(i, _)| i);
3967 }
3968 let mut best: Option<(usize, f64)> = None;
3969 for (i, v) in values.iter().enumerate() {
3970 if v.is_missing() {
3971 continue;
3972 }
3973 if let Ok(x) = v.to_f64() {
3974 if x.is_nan() {
3975 continue;
3976 }
3977 match best {
3978 None => best = Some((i, x)),
3979 Some((_, cur)) if x < cur => best = Some((i, x)),
3980 _ => {}
3981 }
3982 }
3983 }
3984 best.map(|(i, _)| i)
3985}
3986
3987pub fn nannunique(values: &[Scalar]) -> Scalar {
3989 use rustc_hash::FxHashSet;
3990 #[derive(Hash, PartialEq, Eq)]
3991 enum ScalarKey<'a> {
3992 Bool(bool),
3993 Int64(i64),
3994 FloatBits(u64),
3995 Utf8(&'a str),
3996 Timedelta64(i64),
3997 Datetime64(i64),
3998 Period(i64, PeriodFreq),
3999 Interval(u64, u64, IntervalClosed),
4000 }
4001
4002 let mut seen = FxHashSet::default();
4003 for val in values {
4004 if val.is_missing() {
4005 continue;
4006 }
4007 let key = match val {
4008 Scalar::Bool(v) => ScalarKey::Bool(*v),
4009 Scalar::Int64(v) => ScalarKey::Int64(*v),
4010 Scalar::Float64(v) => {
4011 let normalized = if *v == 0.0 { 0.0 } else { *v };
4012 ScalarKey::FloatBits(normalized.to_bits())
4013 }
4014 Scalar::Utf8(v) => ScalarKey::Utf8(v.as_str()),
4015 Scalar::Timedelta64(v) => ScalarKey::Timedelta64(*v),
4016 Scalar::Datetime64(v) => ScalarKey::Datetime64(*v),
4017 Scalar::Period(p) => ScalarKey::Period(p.ordinal, p.freq),
4018 Scalar::Interval(v) => ScalarKey::Interval(
4019 normalized_float_bits(v.left),
4020 normalized_float_bits(v.right),
4021 v.closed,
4022 ),
4023 Scalar::Null(_) => continue,
4024 };
4025 seen.insert(key);
4026 }
4027 Scalar::Int64(seen.len() as i64)
4028}
4029
4030fn normalized_float_bits(value: f64) -> u64 {
4031 let normalized = if value == 0.0 { 0.0 } else { value };
4032 normalized.to_bits()
4033}
4034
4035#[derive(
4055 Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize,
4056)]
4057#[serde(rename_all = "snake_case")]
4058#[non_exhaustive]
4059pub enum IntervalClosed {
4060 Left,
4062 #[default]
4064 Right,
4065 Both,
4067 Neither,
4069}
4070
4071impl IntervalClosed {
4072 #[must_use]
4074 pub fn left_closed(self) -> bool {
4075 matches!(self, Self::Left | Self::Both)
4076 }
4077
4078 #[must_use]
4080 pub fn right_closed(self) -> bool {
4081 matches!(self, Self::Right | Self::Both)
4082 }
4083}
4084
4085impl std::fmt::Display for IntervalClosed {
4086 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4087 match self {
4088 Self::Left => write!(f, "left"),
4089 Self::Right => write!(f, "right"),
4090 Self::Both => write!(f, "both"),
4091 Self::Neither => write!(f, "neither"),
4092 }
4093 }
4094}
4095
4096#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
4102pub struct Interval {
4103 pub left: f64,
4104 pub right: f64,
4105 #[serde(default)]
4106 pub closed: IntervalClosed,
4107}
4108
4109impl Interval {
4110 #[must_use]
4113 pub const fn new(left: f64, right: f64, closed: IntervalClosed) -> Self {
4114 Self {
4115 left,
4116 right,
4117 closed,
4118 }
4119 }
4120
4121 #[must_use]
4123 pub fn length(&self) -> f64 {
4124 self.right - self.left
4125 }
4126
4127 #[must_use]
4129 pub fn mid(&self) -> f64 {
4130 (self.left + self.right) / 2.0
4131 }
4132
4133 #[must_use]
4136 pub fn is_empty(&self) -> bool {
4137 self.left == self.right && !matches!(self.closed, IntervalClosed::Both)
4138 }
4139
4140 #[must_use]
4145 pub fn contains(&self, value: f64) -> bool {
4146 if value.is_nan() {
4147 return false;
4148 }
4149 let left_ok = if self.closed.left_closed() {
4150 value >= self.left
4151 } else {
4152 value > self.left
4153 };
4154 let right_ok = if self.closed.right_closed() {
4155 value <= self.right
4156 } else {
4157 value < self.right
4158 };
4159 left_ok && right_ok
4160 }
4161
4162 #[must_use]
4169 pub fn overlaps(&self, other: &Self) -> bool {
4170 if self.left > other.right || other.left > self.right {
4171 return false;
4172 }
4173 if self.right == other.left {
4175 return self.closed.right_closed() && other.closed.left_closed();
4176 }
4177 if other.right == self.left {
4178 return other.closed.right_closed() && self.closed.left_closed();
4179 }
4180 true
4181 }
4182
4183 pub fn parse(s: &str) -> Result<Self, TypeError> {
4189 let s = s.trim();
4190 if s.len() < 5 {
4191 return Err(TypeError::ValueNotParseable {
4192 value: s.to_string(),
4193 target: "Interval".to_string(),
4194 });
4195 }
4196
4197 let first_char = s.chars().next().unwrap();
4198 let last_char = s.chars().last().unwrap();
4199
4200 let left_closed = match first_char {
4201 '[' => true,
4202 '(' => false,
4203 _ => {
4204 return Err(TypeError::ValueNotParseable {
4205 value: s.to_string(),
4206 target: "Interval".to_string(),
4207 });
4208 }
4209 };
4210
4211 let right_closed = match last_char {
4212 ']' => true,
4213 ')' => false,
4214 _ => {
4215 return Err(TypeError::ValueNotParseable {
4216 value: s.to_string(),
4217 target: "Interval".to_string(),
4218 });
4219 }
4220 };
4221
4222 let closed = match (left_closed, right_closed) {
4223 (true, true) => IntervalClosed::Both,
4224 (true, false) => IntervalClosed::Left,
4225 (false, true) => IntervalClosed::Right,
4226 (false, false) => IntervalClosed::Neither,
4227 };
4228
4229 let inner = &s[1..s.len() - 1];
4230 let parts: Vec<&str> = inner.split(',').collect();
4231 if parts.len() != 2 {
4232 return Err(TypeError::ValueNotParseable {
4233 value: s.to_string(),
4234 target: "Interval".to_string(),
4235 });
4236 }
4237
4238 let left: f64 = parts[0]
4239 .trim()
4240 .parse()
4241 .map_err(|_| TypeError::ValueNotParseable {
4242 value: s.to_string(),
4243 target: "Interval".to_string(),
4244 })?;
4245
4246 let right: f64 = parts[1]
4247 .trim()
4248 .parse()
4249 .map_err(|_| TypeError::ValueNotParseable {
4250 value: s.to_string(),
4251 target: "Interval".to_string(),
4252 })?;
4253
4254 Ok(Self::new(left, right, closed))
4255 }
4256}
4257
4258impl std::fmt::Display for Interval {
4259 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4266 let left_bracket = if self.closed.left_closed() { '[' } else { '(' };
4267 let right_bracket = if self.closed.right_closed() { ']' } else { ')' };
4268 write!(
4269 f,
4270 "{left_bracket}{}, {}{right_bracket}",
4271 float_to_string_for_astype(self.left),
4272 float_to_string_for_astype(self.right)
4273 )
4274 }
4275}
4276
4277#[must_use]
4295pub fn interval_range_by_periods(
4296 start: f64,
4297 end: f64,
4298 periods: usize,
4299 closed: IntervalClosed,
4300) -> Vec<Interval> {
4301 if periods == 0 || !start.is_finite() || !end.is_finite() || start >= end {
4302 return Vec::new();
4303 }
4304 let step = (end - start) / (periods as f64);
4305 let mut out = Vec::with_capacity(periods);
4306 for i in 0..periods {
4307 let left = start + step * (i as f64);
4308 let right = if i + 1 == periods {
4310 end
4311 } else {
4312 start + step * ((i + 1) as f64)
4313 };
4314 out.push(Interval::new(left, right, closed));
4315 }
4316 out
4317}
4318
4319pub fn interval_range_by_step(
4330 start: f64,
4331 end: f64,
4332 step: f64,
4333 closed: IntervalClosed,
4334) -> Result<Vec<Interval>, TypeError> {
4335 if !step.is_finite() || !step.is_sign_positive() || step == 0.0 {
4336 return Err(TypeError::InvalidIntervalStep { step });
4337 }
4338 if !start.is_finite() || !end.is_finite() || start >= end {
4339 return Ok(Vec::new());
4340 }
4341 let span = end - start;
4342 let periods_f = span / step;
4343 let periods = periods_f.round() as i64;
4344 if periods <= 0 {
4345 return Ok(Vec::new());
4346 }
4347 let reconstructed = step * (periods as f64);
4348 if (span - reconstructed).abs() > span.abs() * 1e-9 + 1e-12 {
4350 return Err(TypeError::IntervalStepDoesNotDivide { step, span });
4351 }
4352 let periods = periods as usize;
4353 let mut out = Vec::with_capacity(periods);
4354 for i in 0..periods {
4355 let left = start + step * (i as f64);
4356 let right = if i + 1 == periods {
4357 end
4358 } else {
4359 start + step * ((i + 1) as f64)
4360 };
4361 out.push(Interval::new(left, right, closed));
4362 }
4363 Ok(out)
4364}
4365
4366#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
4385#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
4386#[non_exhaustive]
4387pub enum PeriodFreq {
4388 Annual,
4390 Quarterly,
4392 Monthly,
4394 Weekly,
4396 Daily,
4398 Business,
4400 Hourly,
4402 Minutely,
4404 Secondly,
4406}
4407
4408impl PeriodFreq {
4409 pub fn parse(alias: &str) -> Option<Self> {
4413 match alias.to_ascii_uppercase().as_str() {
4414 "A" | "Y" | "A-DEC" | "Y-DEC" | "ANNUAL" | "YEARLY" => Some(Self::Annual),
4415 "Q" | "Q-DEC" | "QUARTERLY" => Some(Self::Quarterly),
4416 "M" | "MONTHLY" => Some(Self::Monthly),
4417 "W" | "W-SUN" | "WEEKLY" => Some(Self::Weekly),
4418 "D" | "DAILY" => Some(Self::Daily),
4419 "B" | "BUSINESS" => Some(Self::Business),
4420 "H" | "HOURLY" => Some(Self::Hourly),
4421 "T" | "MIN" | "MINUTELY" => Some(Self::Minutely),
4422 "S" | "SECONDLY" => Some(Self::Secondly),
4423 _ => None,
4424 }
4425 }
4426
4427 #[must_use]
4429 pub const fn alias(self) -> &'static str {
4430 match self {
4431 Self::Annual => "Y-DEC",
4432 Self::Quarterly => "Q-DEC",
4433 Self::Monthly => "M",
4434 Self::Weekly => "W-SUN",
4435 Self::Daily => "D",
4436 Self::Business => "B",
4437 Self::Hourly => "h",
4438 Self::Minutely => "min",
4439 Self::Secondly => "s",
4440 }
4441 }
4442
4443 #[must_use]
4445 pub const fn resolution(self) -> &'static str {
4446 match self {
4447 Self::Annual => "A-DEC",
4448 Self::Quarterly => "Q-DEC",
4449 Self::Monthly => "M",
4450 Self::Weekly => "W-SUN",
4451 Self::Daily => "D",
4452 Self::Business => "B",
4453 Self::Hourly => "H",
4454 Self::Minutely => "T",
4455 Self::Secondly => "S",
4456 }
4457 }
4458}
4459
4460impl std::fmt::Display for PeriodFreq {
4461 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4462 f.write_str(self.alias())
4463 }
4464}
4465
4466#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4472pub struct Period {
4473 pub ordinal: i64,
4474 pub freq: PeriodFreq,
4475}
4476
4477impl Period {
4478 #[must_use]
4479 pub const fn new(ordinal: i64, freq: PeriodFreq) -> Self {
4480 Self { ordinal, freq }
4481 }
4482
4483 #[must_use]
4486 pub const fn ordinal(&self) -> i64 {
4487 self.ordinal
4488 }
4489
4490 #[must_use]
4492 pub const fn freq(&self) -> PeriodFreq {
4493 self.freq
4494 }
4495
4496 #[must_use]
4498 pub const fn freqstr(&self) -> &'static str {
4499 self.freq.alias()
4500 }
4501
4502 #[must_use]
4505 pub fn cmp_same_freq(&self, other: &Self) -> Option<std::cmp::Ordering> {
4506 if self.freq != other.freq {
4507 return None;
4508 }
4509 Some(self.ordinal.cmp(&other.ordinal))
4510 }
4511
4512 #[must_use]
4515 pub fn shift(&self, n: i64) -> Self {
4516 Self {
4517 ordinal: self.ordinal.saturating_add(n),
4518 freq: self.freq,
4519 }
4520 }
4521
4522 #[must_use]
4525 pub fn diff(&self, other: &Self) -> Option<i64> {
4526 if self.freq != other.freq {
4527 return None;
4528 }
4529 Some(self.ordinal.saturating_sub(other.ordinal))
4530 }
4531
4532 pub fn parse(s: &str) -> Result<Self, TypeError> {
4539 let trimmed = s.trim();
4540 if trimmed.eq_ignore_ascii_case("nat") {
4541 return Ok(Self::new(i64::MIN, PeriodFreq::Daily));
4542 }
4543
4544 if let Some((year, quarter)) = parse_quarter_period(trimmed) {
4545 let ordinal = year
4546 .checked_sub(1970)
4547 .and_then(|offset| offset.checked_mul(4))
4548 .and_then(|base| base.checked_add(i64::from(quarter) - 1))
4549 .ok_or_else(|| TypeError::ValueNotParseable {
4550 value: s.to_owned(),
4551 target: "Period".to_owned(),
4552 })?;
4553 return Ok(Self::new(ordinal, PeriodFreq::Quarterly));
4554 }
4555
4556 if let Some((year, month, day)) = parse_ymd_period(trimmed) {
4557 let ordinal = Timestamp::days_from_ymd(year, i64::from(month), i64::from(day));
4558 return Ok(Self::new(ordinal, PeriodFreq::Daily));
4559 }
4560
4561 if let Some((year, month)) = parse_year_month_period(trimmed) {
4562 let ordinal = year
4563 .checked_sub(1970)
4564 .and_then(|offset| offset.checked_mul(12))
4565 .and_then(|base| base.checked_add(i64::from(month) - 1))
4566 .ok_or_else(|| TypeError::ValueNotParseable {
4567 value: s.to_owned(),
4568 target: "Period".to_owned(),
4569 })?;
4570 return Ok(Self::new(ordinal, PeriodFreq::Monthly));
4571 }
4572
4573 if let Some(year) = parse_annual_period(trimmed) {
4574 let ordinal = year
4575 .checked_sub(1970)
4576 .ok_or_else(|| TypeError::ValueNotParseable {
4577 value: s.to_owned(),
4578 target: "Period".to_owned(),
4579 })?;
4580 return Ok(Self::new(ordinal, PeriodFreq::Annual));
4581 }
4582
4583 Err(TypeError::ValueNotParseable {
4584 value: s.to_owned(),
4585 target: "Period".to_owned(),
4586 })
4587 }
4588
4589 #[must_use]
4601 pub fn calendar_string(&self) -> String {
4602 if self.ordinal == i64::MIN {
4603 return "NaT".to_owned();
4604 }
4605 let ord = self.ordinal;
4606 match self.freq {
4607 PeriodFreq::Annual => {
4608 let year = 1970 + ord;
4609 format!("{year}")
4610 }
4611 PeriodFreq::Quarterly => {
4612 let year = 1970 + ord.div_euclid(4);
4613 let quarter = ord.rem_euclid(4) + 1;
4614 format!("{year}Q{quarter}")
4615 }
4616 PeriodFreq::Monthly => {
4617 let year = 1970 + ord.div_euclid(12);
4618 let month = ord.rem_euclid(12) + 1;
4619 format!("{year:04}-{month:02}")
4620 }
4621 PeriodFreq::Daily | PeriodFreq::Business | PeriodFreq::Weekly => {
4622 let (y, m, d) = civil_from_days(ord);
4623 format!("{y:04}-{m:02}-{d:02}")
4624 }
4625 PeriodFreq::Hourly => {
4626 let (y, m, d) = civil_from_days(ord.div_euclid(24));
4627 let hour = ord.rem_euclid(24);
4628 format!("{y:04}-{m:02}-{d:02} {hour:02}:00")
4629 }
4630 PeriodFreq::Minutely => {
4631 let day = ord.div_euclid(1440);
4632 let mins = ord.rem_euclid(1440);
4633 let (y, m, d) = civil_from_days(day);
4634 format!("{y:04}-{m:02}-{d:02} {:02}:{:02}", mins / 60, mins % 60)
4635 }
4636 PeriodFreq::Secondly => {
4637 let day = ord.div_euclid(86_400);
4638 let secs = ord.rem_euclid(86_400);
4639 let (y, m, d) = civil_from_days(day);
4640 format!(
4641 "{y:04}-{m:02}-{d:02} {:02}:{:02}:{:02}",
4642 secs / 3600,
4643 (secs % 3600) / 60,
4644 secs % 60
4645 )
4646 }
4647 }
4648 }
4649}
4650
4651fn civil_from_days(days_since_epoch: i64) -> (i64, u32, u32) {
4655 let days = days_since_epoch + 719_468;
4656 let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
4657 let doe = days - era * 146_097;
4658 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
4659 let y = yoe + era * 400;
4660 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
4661 let mp = (5 * doy + 2) / 153;
4662 let d = doy - (153 * mp + 2) / 5 + 1;
4663 let m = if mp < 10 { mp + 3 } else { mp - 9 };
4664 let year = if m <= 2 { y + 1 } else { y };
4665 (year, m as u32, d as u32)
4666}
4667
4668fn parse_annual_period(value: &str) -> Option<i64> {
4669 (value.len() == 4 && value.chars().all(|ch| ch.is_ascii_digit()))
4670 .then(|| value.parse::<i64>().ok())
4671 .flatten()
4672}
4673
4674fn parse_year_month_period(value: &str) -> Option<(i64, u32)> {
4675 let (year, month) = value.split_once('-')?;
4676 if year.len() != 4 || month.len() != 2 {
4677 return None;
4678 }
4679 let year = year.parse::<i64>().ok()?;
4680 let month = month.parse::<u32>().ok()?;
4681 (1..=12).contains(&month).then_some((year, month))
4682}
4683
4684fn parse_ymd_period(value: &str) -> Option<(i64, u32, u32)> {
4685 let mut parts = value.split('-');
4686 let year = parts.next()?;
4687 let month = parts.next()?;
4688 let day = parts.next()?;
4689 if parts.next().is_some() || year.len() != 4 || month.len() != 2 || day.len() != 2 {
4690 return None;
4691 }
4692 let year = year.parse::<i64>().ok()?;
4693 let month = month.parse::<u32>().ok()?;
4694 let day = day.parse::<u32>().ok()?;
4695 (1..=days_in_month(year, month)?)
4696 .contains(&day)
4697 .then_some((year, month, day))
4698}
4699
4700fn parse_quarter_period(value: &str) -> Option<(i64, u32)> {
4701 let (year, quarter) = value.split_once('Q').or_else(|| value.split_once('q'))?;
4702 if year.len() != 4 || quarter.len() != 1 {
4703 return None;
4704 }
4705 let year = year.parse::<i64>().ok()?;
4706 let quarter = quarter.parse::<u32>().ok()?;
4707 (1..=4).contains(&quarter).then_some((year, quarter))
4708}
4709
4710impl std::fmt::Display for Period {
4711 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4714 f.write_str(&self.calendar_string())
4715 }
4716}
4717
4718#[must_use]
4737pub fn period_range(start: Period, periods: usize) -> Vec<Period> {
4738 (0..periods).map(|i| start.shift(i as i64)).collect()
4739}
4740
4741#[cfg(test)]
4742mod tests {
4743 use super::{
4744 DType, Interval, IntervalClosed, NullKind, Period, PeriodFreq, Scalar, SparseDType,
4745 cast_scalar, common_dtype, infer_dtype,
4746 };
4747
4748 #[test]
4758 fn semantic_cmp_cross_numeric_int_float_cdpai() {
4759 use std::cmp::Ordering;
4762 let mut st: u64 = 0xc205_a1b2_c3d4_e5f6;
4763 let mut next = || {
4764 st = st
4765 .wrapping_mul(6_364_136_223_846_793_005)
4766 .wrapping_add(1_442_695_040_888_963_407);
4767 (st >> 33) as u32
4768 };
4769 for _ in 0..3000u32 {
4770 let i = (next() % 21) as i64 - 10;
4771 let f = (next() % 400) as f64 / 20.0 - 10.0; let exp = (i as f64).partial_cmp(&f).unwrap();
4773 assert_eq!(Scalar::Int64(i).semantic_cmp(&Scalar::Float64(f)), exp);
4774 assert_eq!(
4775 Scalar::Float64(f).semantic_cmp(&Scalar::Int64(i)),
4776 exp.reverse()
4777 );
4778 }
4779 assert_eq!(
4781 Scalar::Int64(5).semantic_cmp(&Scalar::Float64(5.0)),
4782 Ordering::Equal
4783 );
4784 assert_eq!(
4785 Scalar::Float64(5.0).semantic_cmp(&Scalar::Int64(5)),
4786 Ordering::Equal
4787 );
4788 assert_eq!(
4789 Scalar::Int64(3).semantic_cmp(&Scalar::Float64(3.5)),
4790 Ordering::Less
4791 );
4792 assert_eq!(
4793 Scalar::Int64(4).semantic_cmp(&Scalar::Float64(3.5)),
4794 Ordering::Greater
4795 );
4796 }
4797
4798 #[test]
4799 fn semantic_cmp_nat_degeneracy_temporal_767ak() {
4800 use std::cmp::Ordering;
4801 const NAT: i64 = i64::MIN;
4802
4803 let td_nat = Scalar::Timedelta64(NAT);
4805 for v in [-3i64, 0, 5, 99] {
4806 let td = Scalar::Timedelta64(v);
4807 assert_eq!(td_nat.semantic_cmp(&td), Ordering::Equal, "td NAT vs {v}");
4808 assert_eq!(td.semantic_cmp(&td_nat), Ordering::Equal, "td {v} vs NAT");
4809 }
4810 assert_eq!(td_nat.semantic_cmp(&td_nat), Ordering::Equal);
4811
4812 let dt_nat = Scalar::Datetime64(NAT);
4814 for v in [-3i64, 0, 5, 99] {
4815 let dt = Scalar::Datetime64(v);
4816 assert_eq!(dt_nat.semantic_cmp(&dt), Ordering::Equal, "dt NAT vs {v}");
4817 assert_eq!(dt.semantic_cmp(&dt_nat), Ordering::Equal, "dt {v} vs NAT");
4818 }
4819 assert_eq!(dt_nat.semantic_cmp(&dt_nat), Ordering::Equal);
4820
4821 for (a, b) in [(1i64, 2i64), (5, 5), (9, -1)] {
4823 let (ta, tb) = (Scalar::Timedelta64(a), Scalar::Timedelta64(b));
4824 assert_eq!(ta.semantic_cmp(&ta), Ordering::Equal);
4825 assert_eq!(ta.semantic_cmp(&tb), b.cmp(&a).reverse()); assert_eq!(ta.semantic_cmp(&tb), a.cmp(&b));
4827 let (da, db) = (Scalar::Datetime64(a), Scalar::Datetime64(b));
4828 assert_eq!(da.semantic_cmp(&db), a.cmp(&b));
4829 }
4830 }
4831
4832 #[test]
4833 fn semantic_cmp_total_order_axioms_ay8o9() {
4834 use std::cmp::Ordering;
4835
4836 let mut state: u64 = 0xc0ff_eeba_df00_d123;
4837 let mut next = || {
4838 state = state
4839 .wrapping_mul(6_364_136_223_846_793_005)
4840 .wrapping_add(1_442_695_040_888_963_407);
4841 (state >> 33) as u32
4842 };
4843
4844 for iter in 0..6000u32 {
4845 let dt = next() % 4;
4846 let mk = |r: u32| match dt {
4848 0 => Scalar::Int64((r % 11) as i64 - 5),
4849 1 => Scalar::Float64(f64::from((r % 21) as i32 - 10) / 2.0),
4851 2 => Scalar::Utf8(format!("s{}", r % 5)),
4852 _ => Scalar::Bool(r.is_multiple_of(2)),
4853 };
4854 let a = mk(next());
4855 let b = mk(next());
4856 let c = mk(next());
4857 let ctx = format!("iter={iter} a={a:?} b={b:?} c={c:?}");
4858
4859 assert_eq!(a.semantic_cmp(&a), Ordering::Equal, "reflexive {ctx}");
4861 assert_eq!(
4863 a.semantic_cmp(&b),
4864 b.semantic_cmp(&a).reverse(),
4865 "antisymmetric {ctx}"
4866 );
4867 let ab = a.semantic_cmp(&b);
4869 assert_eq!(
4870 a.semantic_le(&b),
4871 ab != Ordering::Greater,
4872 "le-consistent {ctx}"
4873 );
4874 assert_eq!(
4875 a.semantic_ge(&b),
4876 ab != Ordering::Less,
4877 "ge-consistent {ctx}"
4878 );
4879 assert_eq!(
4880 a.semantic_le(&b) && a.semantic_ge(&b),
4881 ab == Ordering::Equal,
4882 "le&ge<=>eq {ctx}"
4883 );
4884 if a.semantic_cmp(&b) != Ordering::Greater && b.semantic_cmp(&c) != Ordering::Greater {
4886 assert_ne!(a.semantic_cmp(&c), Ordering::Greater, "transitivity {ctx}");
4887 }
4888 }
4889
4890 let nan = Scalar::Float64(f64::NAN);
4894 for v in [
4895 Scalar::Float64(-3.5),
4896 Scalar::Float64(0.0),
4897 Scalar::Float64(7.25),
4898 ] {
4899 assert_eq!(nan.semantic_cmp(&v), Ordering::Equal, "NaN cmp finite");
4900 assert_eq!(v.semantic_cmp(&nan), Ordering::Equal, "finite cmp NaN");
4901 }
4902 assert_eq!(nan.semantic_cmp(&nan), Ordering::Equal, "NaN cmp NaN");
4903 }
4904
4905 #[test]
4911 fn common_dtype_lattice_axioms_be314() {
4912 const ALL: [DType; 13] = [
4913 DType::Null,
4914 DType::Bool,
4915 DType::BoolNullable,
4916 DType::Int64,
4917 DType::Int64Nullable,
4918 DType::Float64,
4919 DType::Utf8,
4920 DType::Categorical,
4921 DType::Timedelta64,
4922 DType::Datetime64,
4923 DType::Period,
4924 DType::Interval,
4925 DType::Sparse,
4926 ];
4927
4928 for &a in &ALL {
4929 assert_eq!(common_dtype(a, a), Ok(a), "idempotent {a:?}");
4931 assert_eq!(
4933 common_dtype(DType::Null, a),
4934 Ok(a),
4935 "null-left identity {a:?}"
4936 );
4937 assert_eq!(
4938 common_dtype(a, DType::Null),
4939 Ok(a),
4940 "null-right identity {a:?}"
4941 );
4942
4943 for &b in &ALL {
4944 assert_eq!(
4947 common_dtype(a, b).ok(),
4948 common_dtype(b, a).ok(),
4949 "commutative value {a:?},{b:?}"
4950 );
4951 assert_eq!(
4952 common_dtype(a, b).is_ok(),
4953 common_dtype(b, a).is_ok(),
4954 "commutative ok-ness {a:?},{b:?}"
4955 );
4956 }
4957 }
4958
4959 for &a in &ALL {
4962 for &b in &ALL {
4963 for &c in &ALL {
4964 if let (Ok(ab), Ok(bc)) = (common_dtype(a, b), common_dtype(b, c))
4965 && let (Ok(left), Ok(right)) = (common_dtype(ab, c), common_dtype(a, bc))
4966 {
4967 assert_eq!(left, right, "associative {a:?},{b:?},{c:?}");
4968 }
4969 }
4970 }
4971 }
4972 }
4973
4974 #[test]
4977 fn infer_dtype_coercion_rules_e3sfq() {
4978 use DType::{Bool, Float64, Int64, Null, Utf8};
4979
4980 assert_eq!(infer_dtype(&[]), Ok(Null));
4982 assert_eq!(
4983 infer_dtype(&[Scalar::Null(NullKind::Null), Scalar::Null(NullKind::NaN)]),
4984 Ok(Null)
4985 );
4986
4987 let mut s: u64 = 0x132d_a7e0_0e3f_c0de;
4989 let mut next = || {
4990 s = s
4991 .wrapping_mul(6_364_136_223_846_793_005)
4992 .wrapping_add(1_442_695_040_888_963_407);
4993 (s >> 33) as u32
4994 };
4995 for _ in 0..600u32 {
4996 let n = (next() % 6) as usize + 1;
4997 let ints: Vec<Scalar> = (0..n)
4998 .map(|_| Scalar::Int64((next() % 9) as i64 - 4))
4999 .collect();
5000 assert_eq!(infer_dtype(&ints), Ok(Int64));
5001 let floats: Vec<Scalar> = (0..n)
5002 .map(|_| Scalar::Float64(f64::from((next() % 7) as i32)))
5003 .collect();
5004 assert_eq!(infer_dtype(&floats), Ok(Float64));
5005 let bools: Vec<Scalar> = (0..n).map(|_| Scalar::Bool(next() % 2 == 0)).collect();
5006 assert_eq!(infer_dtype(&bools), Ok(Bool));
5007 let strs: Vec<Scalar> = (0..n)
5008 .map(|_| Scalar::Utf8(format!("s{}", next() % 4)))
5009 .collect();
5010 assert_eq!(infer_dtype(&strs), Ok(Utf8));
5011 }
5012
5013 assert_eq!(
5015 infer_dtype(&[
5016 Scalar::Int64(1),
5017 Scalar::Null(NullKind::Null),
5018 Scalar::Int64(2)
5019 ]),
5020 Ok(Int64),
5021 "Int64 + nulls -> Int64"
5022 );
5023 assert_eq!(
5024 infer_dtype(&[Scalar::Int64(1), Scalar::Float64(2.5)]),
5025 Ok(Float64),
5026 "Int64 + Float64 -> Float64"
5027 );
5028 assert_eq!(
5029 infer_dtype(&[Scalar::Bool(true), Scalar::Int64(3)]),
5030 Ok(Int64),
5031 "Bool + Int64 -> Int64"
5032 );
5033 assert_eq!(
5034 infer_dtype(&[Scalar::Utf8("a".into()), Scalar::Int64(3)]),
5035 Ok(Utf8),
5036 "Utf8 + Int64 -> Utf8 (object fallback)"
5037 );
5038 }
5039
5040 #[test]
5045 fn missing_for_dtype_always_missing_1ews0() {
5046 const ALL: [DType; 13] = [
5047 DType::Null,
5048 DType::Bool,
5049 DType::BoolNullable,
5050 DType::Int64,
5051 DType::Int64Nullable,
5052 DType::Float64,
5053 DType::Utf8,
5054 DType::Categorical,
5055 DType::Timedelta64,
5056 DType::Datetime64,
5057 DType::Period,
5058 DType::Interval,
5059 DType::Sparse,
5060 ];
5061 for &dt in &ALL {
5062 let m = Scalar::missing_for_dtype(dt);
5063 assert!(m.is_missing(), "missing_for_dtype({dt:?}) must be missing");
5064 for &target in &ALL {
5065 let cast = cast_scalar(&m, target).expect("cast of missing");
5066 if target == DType::Utf8 {
5067 assert!(
5070 matches!(cast, Scalar::Utf8(_)),
5071 "cast(missing {dt:?} -> Utf8) yields a string, got {cast:?}"
5072 );
5073 } else {
5074 assert!(
5077 cast.is_missing(),
5078 "cast(missing {dt:?} -> {target:?}) must stay missing, got {cast:?}"
5079 );
5080 }
5081 }
5082 }
5083 }
5084
5085 #[test]
5086 fn cast_scalar_bool_int_roundtrip_6w07b() {
5087 use super::cast_scalar;
5088 assert_eq!(
5090 cast_scalar(&Scalar::Bool(true), DType::Int64).unwrap(),
5091 Scalar::Int64(1)
5092 );
5093 assert_eq!(
5094 cast_scalar(&Scalar::Bool(false), DType::Int64).unwrap(),
5095 Scalar::Int64(0)
5096 );
5097 assert_eq!(
5098 cast_scalar(&Scalar::Int64(0), DType::Bool).unwrap(),
5099 Scalar::Bool(false)
5100 );
5101 let mut st: u64 = 0x4b07_0b1c_2d3e_4f50;
5102 let mut next = || {
5103 st = st
5104 .wrapping_mul(6_364_136_223_846_793_005)
5105 .wrapping_add(1_442_695_040_888_963_407);
5106 (st >> 33) as u32
5107 };
5108 for _ in 0..2000u32 {
5109 let v = (next() % 21) as i64 - 10;
5110 assert_eq!(
5111 cast_scalar(&Scalar::Int64(v), DType::Bool).unwrap(),
5112 Scalar::Bool(v != 0),
5113 "int->bool v={v}"
5114 );
5115 }
5116 }
5117
5118 #[test]
5120 fn cast_scalar_float_to_int_truncates_toward_zero_u9lec() {
5121 use super::cast_scalar;
5122 let mut st: u64 = 0x4ca5_0b1c_2d3e_4f50;
5125 let mut next = || {
5126 st = st
5127 .wrapping_mul(6_364_136_223_846_793_005)
5128 .wrapping_add(1_442_695_040_888_963_407);
5129 (st >> 33) as u32
5130 };
5131 for (f, exp) in [
5133 (-3.7, -3i64),
5134 (3.7, 3),
5135 (-3.2, -3),
5136 (3.2, 3),
5137 (-0.9, 0),
5138 (0.9, 0),
5139 (-5.0, -5),
5140 (5.0, 5),
5141 ] {
5142 assert_eq!(
5143 cast_scalar(&Scalar::Float64(f), DType::Int64).unwrap(),
5144 Scalar::Int64(exp),
5145 "cast {f}"
5146 );
5147 }
5148 for _ in 0..3000u32 {
5150 let v = (next() % 2_000_001) as f64 / 1000.0 - 1000.0; let got = cast_scalar(&Scalar::Float64(v), DType::Int64).unwrap();
5152 assert_eq!(
5153 got,
5154 Scalar::Int64(v.trunc() as i64),
5155 "trunc-toward-zero v={v}"
5156 );
5157 }
5158 }
5159
5160 #[test]
5161 fn nancount_nunique_prod_any_all_mx60x() {
5162 use super::{nanall, nanany, nancount, nannunique, nanprod};
5163 let mut s: u64 = 0x4e2a_0b1c_2d3e_4f50;
5166 let mut next = || {
5167 s = s
5168 .wrapping_mul(6_364_136_223_846_793_005)
5169 .wrapping_add(1_442_695_040_888_963_407);
5170 (s >> 33) as u32
5171 };
5172 let asf = |sc: Scalar| -> f64 { sc.to_f64().unwrap_or(f64::NAN) };
5173 let asb = |sc: Scalar| -> bool { matches!(sc, Scalar::Bool(true)) };
5174 for iter in 0..1000u32 {
5175 let n = (next() % 10) as usize + 1;
5176 let raw: Vec<f64> = (0..n)
5177 .map(|_| {
5178 if next() % 4 == 0 {
5179 f64::NAN
5180 } else {
5181 (next() % 5) as f64
5182 }
5183 })
5184 .collect();
5185 let finite: Vec<f64> = raw.iter().copied().filter(|x| !x.is_nan()).collect();
5186 if finite.is_empty() {
5187 continue;
5188 }
5189 let scalars: Vec<Scalar> = raw.iter().map(|&x| Scalar::Float64(x)).collect();
5190 let distinct: std::collections::HashSet<u64> =
5191 finite.iter().map(|x| x.to_bits()).collect();
5192 let prod: f64 = finite.iter().product();
5193 assert!(
5194 (asf(nancount(&scalars)) - finite.len() as f64).abs() < 1e-9,
5195 "nancount iter={iter}"
5196 );
5197 assert!(
5198 (asf(nannunique(&scalars)) - distinct.len() as f64).abs() < 1e-9,
5199 "nannunique iter={iter}"
5200 );
5201 assert!(
5202 (asf(nanprod(&scalars)) - prod).abs() < 1e-7,
5203 "nanprod iter={iter}"
5204 );
5205 assert_eq!(
5206 asb(nanany(&scalars)),
5207 finite.iter().any(|&x| x != 0.0),
5208 "nanany iter={iter}"
5209 );
5210 assert_eq!(
5211 asb(nanall(&scalars)),
5212 finite.iter().all(|&x| x != 0.0),
5213 "nanall iter={iter}"
5214 );
5215 }
5216 }
5217
5218 #[test]
5219 fn nan_reduction_kernels_skip_correctness_1uagc() {
5220 use super::{nanmax, nanmedian, nanmin, nansum};
5221 let mut s: u64 = 0x4e1a_0b2c_2d3e_4f50;
5224 let mut next = || {
5225 s = s
5226 .wrapping_mul(6_364_136_223_846_793_005)
5227 .wrapping_add(1_442_695_040_888_963_407);
5228 (s >> 33) as u32
5229 };
5230 let val = |sc: Scalar| -> f64 { sc.to_f64().unwrap_or(f64::NAN) };
5231 for iter in 0..1000u32 {
5232 let n = (next() % 12) as usize + 1;
5233 let raw: Vec<f64> = (0..n)
5234 .map(|_| {
5235 if next() % 4 == 0 {
5236 f64::NAN
5237 } else {
5238 (next() % 200) as f64 - 100.0
5239 }
5240 })
5241 .collect();
5242 let mut finite: Vec<f64> = raw.iter().copied().filter(|x| !x.is_nan()).collect();
5243 if finite.is_empty() {
5244 continue;
5245 }
5246 let scalars: Vec<Scalar> = raw.iter().map(|&x| Scalar::Float64(x)).collect();
5247 let sum: f64 = finite.iter().sum();
5248 let mn = finite.iter().copied().fold(f64::INFINITY, f64::min);
5249 let mx = finite.iter().copied().fold(f64::NEG_INFINITY, f64::max);
5250 finite.sort_by(|a, b| a.partial_cmp(b).unwrap());
5251 let m = finite.len();
5252 let med = if m % 2 == 1 {
5253 finite[m / 2]
5254 } else {
5255 (finite[m / 2 - 1] + finite[m / 2]) / 2.0
5256 };
5257 assert!(
5258 (val(nansum(&scalars)) - sum).abs() < 1e-7,
5259 "nansum iter={iter}"
5260 );
5261 assert!(
5262 (val(nanmin(&scalars)) - mn).abs() < 1e-9,
5263 "nanmin iter={iter}"
5264 );
5265 assert!(
5266 (val(nanmax(&scalars)) - mx).abs() < 1e-9,
5267 "nanmax iter={iter}"
5268 );
5269 assert!(
5270 (val(nanmedian(&scalars)) - med).abs() < 1e-9,
5271 "nanmedian iter={iter}"
5272 );
5273 }
5274 }
5275
5276 #[test]
5277 fn nanvar_ddof_nanstd_nan_skip_p00ag() {
5278 use super::{nanmean, nanstd, nanvar};
5279 let mut s: u64 = 0x4e0a_0b1c_2d3e_4f50;
5282 let mut next = || {
5283 s = s
5284 .wrapping_mul(6_364_136_223_846_793_005)
5285 .wrapping_add(1_442_695_040_888_963_407);
5286 (s >> 33) as u32
5287 };
5288 let val = |sc: Scalar| -> f64 { sc.to_f64().unwrap_or(f64::NAN) };
5289 for iter in 0..1000u32 {
5290 let n = (next() % 10) as usize + 2;
5291 let raw: Vec<f64> = (0..n)
5292 .map(|_| {
5293 if next() % 4 == 0 {
5294 f64::NAN
5295 } else {
5296 (next() % 200) as f64 / 7.0
5297 }
5298 })
5299 .collect();
5300 let finite: Vec<f64> = raw.iter().copied().filter(|x| !x.is_nan()).collect();
5301 if finite.len() < 2 {
5302 continue;
5303 }
5304 let scalars: Vec<Scalar> = raw.iter().map(|&x| Scalar::Float64(x)).collect();
5305 let nf = finite.len() as f64;
5306 let mean = finite.iter().sum::<f64>() / nf;
5307 let ss = finite.iter().map(|x| (x - mean).powi(2)).sum::<f64>();
5308 assert!(
5309 (val(nanmean(&scalars)) - mean).abs() < 1e-7,
5310 "nanmean iter={iter}"
5311 );
5312 assert!(
5313 (val(nanvar(&scalars, 0)) - ss / nf).abs() < 1e-7,
5314 "nanvar ddof0 iter={iter}"
5315 );
5316 assert!(
5317 (val(nanvar(&scalars, 1)) - ss / (nf - 1.0)).abs() < 1e-7,
5318 "nanvar ddof1 iter={iter}"
5319 );
5320 assert!(
5321 (val(nanstd(&scalars, 1)) - (ss / (nf - 1.0)).sqrt()).abs() < 1e-7,
5322 "nanstd ddof1 iter={iter}"
5323 );
5324 }
5325 }
5326
5327 #[test]
5328 fn nanskew_nankurt_min_sample_and_known_xybnq() {
5329 use super::{nankurt, nanskew};
5332 let f = |xs: &[f64]| -> Vec<Scalar> { xs.iter().map(|&x| Scalar::Float64(x)).collect() };
5333 let val = |s: Scalar| -> Option<f64> {
5334 if s.is_missing() {
5335 None
5336 } else {
5337 s.to_f64().ok()
5338 }
5339 };
5340
5341 assert_eq!(val(nanskew(&f(&[1.0, 2.0]))), None, "skew n=2 -> NaN");
5343 let sym = val(nanskew(&f(&[1.0, 2.0, 3.0]))).expect("skew n=3");
5344 assert!(sym.abs() < 1e-9, "symmetric skew ~0, got {sym}");
5345 let right = val(nanskew(&f(&[1.0, 1.0, 1.0, 5.0]))).expect("skew n=4");
5346 assert!(right > 0.0, "right-skewed -> positive skew, got {right}");
5347
5348 assert_eq!(val(nankurt(&f(&[1.0, 2.0, 3.0]))), None, "kurt n=3 -> NaN");
5350 let k = val(nankurt(&f(&[1.0, 2.0, 3.0, 4.0, 5.0]))).expect("kurt n=5");
5351 assert!(
5352 (k - (-1.2)).abs() < 1e-6,
5353 "pandas kurt([1..5]) == -1.2, got {k}"
5354 );
5355 }
5356
5357 #[test]
5360 fn cast_scalar_to_utf8_formatting_yes7i() {
5361 let mut st: u64 = 0x4e57_0b1c_2d3e_4f50;
5364 let mut next = || {
5365 st = st
5366 .wrapping_mul(6_364_136_223_846_793_005)
5367 .wrapping_add(1_442_695_040_888_963_407);
5368 (st >> 33) as u32
5369 };
5370 for _ in 0..3000u32 {
5371 let n = (next() % 4_000_001) as i64 - 2_000_000;
5372 assert_eq!(
5373 cast_scalar(&Scalar::Int64(n), DType::Utf8).unwrap(),
5374 Scalar::Utf8(n.to_string())
5375 );
5376 }
5377 assert_eq!(
5378 cast_scalar(&Scalar::Bool(true), DType::Utf8).unwrap(),
5379 Scalar::Utf8("True".to_string())
5380 );
5381 assert_eq!(
5382 cast_scalar(&Scalar::Bool(false), DType::Utf8).unwrap(),
5383 Scalar::Utf8("False".to_string())
5384 );
5385 }
5386
5387 #[test]
5388 fn cast_scalar_coercion_rules_6a83t() {
5389 let mut state: u64 = 0x5a17_c0de_1234_abcd;
5390 let mut next = || {
5391 state = state
5392 .wrapping_mul(6_364_136_223_846_793_005)
5393 .wrapping_add(1_442_695_040_888_963_407);
5394 (state >> 33) as u32
5395 };
5396
5397 for _ in 0..4000u32 {
5398 let n = (next() % 21) as i64 - 10; let b = next() % 2 == 0;
5400 let f = f64::from((next() % 41) as i32 - 20) / 4.0; let i = Scalar::Int64(n);
5403 let bo = Scalar::Bool(b);
5404 let fl = Scalar::Float64(f);
5405
5406 assert_eq!(cast_scalar(&i, DType::Int64), Ok(i.clone()));
5408 assert_eq!(cast_scalar(&bo, DType::Bool), Ok(bo.clone()));
5409 assert_eq!(cast_scalar(&fl, DType::Float64), Ok(fl.clone()));
5410
5411 assert_eq!(cast_scalar(&i, DType::Int64Nullable), Ok(i.clone()));
5413 assert_eq!(cast_scalar(&bo, DType::BoolNullable), Ok(bo.clone()));
5414
5415 assert_eq!(cast_scalar(&i, DType::Bool), Ok(Scalar::Bool(n != 0)));
5417 assert_eq!(
5418 cast_scalar(&i, DType::Float64),
5419 Ok(Scalar::Float64(n as f64))
5420 );
5421
5422 assert_eq!(
5424 cast_scalar(&bo, DType::Int64),
5425 Ok(Scalar::Int64(i64::from(b)))
5426 );
5427 assert_eq!(
5428 cast_scalar(&bo, DType::Float64),
5429 Ok(Scalar::Float64(if b { 1.0 } else { 0.0 }))
5430 );
5431
5432 assert_eq!(cast_scalar(&fl, DType::Int64), Ok(Scalar::Int64(f as i64)));
5434 }
5435 }
5436
5437 #[test]
5439 fn scalar_from_primitive_types() {
5440 assert_eq!(Scalar::from(true), Scalar::Bool(true));
5442 assert_eq!(Scalar::from(42i64), Scalar::Int64(42));
5443 assert_eq!(Scalar::from(1.5f64), Scalar::Float64(1.5));
5444 assert_eq!(Scalar::from("hi"), Scalar::Utf8("hi".to_owned()));
5445 assert_eq!(
5446 Scalar::from(String::from("world")),
5447 Scalar::Utf8("world".to_owned())
5448 );
5449
5450 let mixed: Vec<Scalar> = vec![1i64.into(), 2.0f64.into(), "three".into()];
5454 assert_eq!(mixed.len(), 3);
5455 assert_eq!(mixed[0], Scalar::Int64(1));
5456 assert_eq!(mixed[1], Scalar::Float64(2.0));
5457 assert_eq!(mixed[2], Scalar::Utf8("three".to_owned()));
5458 }
5459
5460 #[test]
5461 fn dtype_inference_coerces_numeric_values() {
5462 let values = vec![Scalar::Bool(true), Scalar::Int64(7), Scalar::Float64(3.5)];
5463 assert_eq!(
5464 infer_dtype(&values).expect("dtype should infer"),
5465 DType::Float64
5466 );
5467 }
5468
5469 #[test]
5470 fn interval_scalar_has_dtype_storage_and_unique_semantics_5g5uj() {
5471 let left = Scalar::Interval(Interval::new(0.0, 1.0, IntervalClosed::Right));
5472 let right = Scalar::Interval(Interval::new(1.0, 2.0, IntervalClosed::Right));
5473 assert_eq!(left.dtype(), DType::Interval);
5474 assert!(!left.is_missing());
5475 assert_eq!(
5476 infer_dtype(&[left.clone(), right.clone()]).expect("interval dtype"),
5477 DType::Interval
5478 );
5479 assert_eq!(
5480 common_dtype(DType::Interval, DType::Interval).expect("same interval dtype"),
5481 DType::Interval
5482 );
5483 assert_eq!(
5484 cast_scalar(&Scalar::Null(NullKind::Null), DType::Interval).expect("missing casts"),
5485 Scalar::Null(NullKind::Null)
5486 );
5487 assert_eq!(
5488 cast_scalar(&left, DType::Utf8).expect("interval string cast"),
5489 Scalar::Utf8("(0.0, 1.0]".to_owned())
5490 );
5491 assert_eq!(
5492 super::nannunique(&[left.clone(), right, left, Scalar::Null(NullKind::Null)]),
5493 Scalar::Int64(2)
5494 );
5495 }
5496
5497 #[test]
5498 fn cast_scalar_parses_temporal_extension_strings_avm08() {
5499 let expected_nanos = super::Timestamp::parse("2024-01-15T10:30:45")
5500 .expect("timestamp parse")
5501 .nanos;
5502 assert_eq!(
5503 cast_scalar(
5504 &Scalar::Utf8("2024-01-15T10:30:45".to_owned()),
5505 DType::Datetime64
5506 )
5507 .expect("datetime cast"),
5508 Scalar::Datetime64(expected_nanos)
5509 );
5510 assert_eq!(
5511 cast_scalar(&Scalar::Utf8("2024Q1".to_owned()), DType::Period).expect("period cast"),
5512 Scalar::Period(Period::new(216, PeriodFreq::Quarterly))
5513 );
5514 assert_eq!(
5515 cast_scalar(&Scalar::Utf8("(0, 1]".to_owned()), DType::Interval)
5516 .expect("interval cast"),
5517 Scalar::Interval(Interval::new(0.0, 1.0, IntervalClosed::Right))
5518 );
5519 }
5520
5521 #[test]
5522 fn missing_values_get_target_missing_marker() {
5523 let missing = Scalar::Null(NullKind::Null);
5524 let cast = cast_scalar(&missing, DType::Float64).expect("missing casts");
5525 assert_eq!(cast, Scalar::Null(NullKind::NaN));
5526 }
5527
5528 #[test]
5529 fn cast_scalar_to_utf8_uses_pandas_string_spellings() {
5530 let cases = [
5531 (Scalar::Bool(true), "True"),
5532 (Scalar::Bool(false), "False"),
5533 (Scalar::Int64(-7), "-7"),
5534 (Scalar::Float64(1.0), "1.0"),
5535 (Scalar::Float64(1.5), "1.5"),
5536 (Scalar::Float64(f64::NAN), "nan"),
5537 (Scalar::Null(NullKind::Null), "None"),
5538 (Scalar::Null(NullKind::NaN), "nan"),
5539 (Scalar::Null(NullKind::NaT), "NaT"),
5540 ];
5541
5542 for (value, expected) in cases {
5543 assert_eq!(
5544 cast_scalar(&value, DType::Utf8).expect("cast"),
5545 Scalar::Utf8(expected.to_owned())
5546 );
5547 }
5548 }
5549
5550 #[test]
5551 fn semantic_eq_treats_nan_as_equal() {
5552 let left = Scalar::Float64(f64::NAN);
5553 let right = Scalar::Null(NullKind::NaN);
5554 assert!(left.semantic_eq(&right));
5555 }
5556
5557 #[test]
5558 fn semantic_eq_treats_nan_as_missing_null() {
5559 let left = Scalar::Float64(f64::NAN);
5560 let right = Scalar::Null(NullKind::Null);
5561 assert!(left.semantic_eq(&right));
5562 }
5563
5564 #[test]
5565 fn common_dtype_rejects_string_numeric_mix() {
5566 let err = common_dtype(DType::Utf8, DType::Int64).expect_err("must fail");
5567 assert_eq!(
5568 err.to_string(),
5569 "dtype coercion from Utf8 to Int64 has no compatible common type"
5570 );
5571 let err = common_dtype(DType::Float64, DType::Utf8).expect_err("must fail");
5572 assert_eq!(
5573 err.to_string(),
5574 "dtype coercion from Float64 to Utf8 has no compatible common type"
5575 );
5576 }
5577
5578 #[test]
5579 fn sparse_dtype_normalizes_fill_value_to_value_dtype() {
5580 let dtype = SparseDType::new(DType::Float64, Scalar::Int64(0)).expect("fill should cast");
5581
5582 assert_eq!(dtype.value_dtype, DType::Float64);
5583 assert_eq!(dtype.fill_value, Scalar::Float64(0.0));
5584 }
5585
5586 #[test]
5587 fn sparse_dtype_rejects_sparse_value_dtype() {
5588 let err = SparseDType::new(DType::Sparse, Scalar::Int64(0)).expect_err("must reject");
5589
5590 assert_eq!(err.to_string(), "sparse value dtype cannot be Sparse");
5591 }
5592
5593 #[test]
5594 fn common_dtype_rejects_sparse_dense_mix() {
5595 let err = common_dtype(DType::Sparse, DType::Int64).expect_err("must fail");
5596
5597 assert_eq!(
5598 err.to_string(),
5599 "dtype coercion from Sparse to Int64 has no compatible common type"
5600 );
5601 }
5602
5603 #[test]
5606 fn nullable_int64_promotion_matrix() {
5607 assert_eq!(
5609 common_dtype(DType::Int64, DType::Int64Nullable).unwrap(),
5610 DType::Int64Nullable
5611 );
5612 assert_eq!(
5613 common_dtype(DType::Int64Nullable, DType::Int64).unwrap(),
5614 DType::Int64Nullable
5615 );
5616
5617 assert_eq!(
5619 common_dtype(DType::Int64Nullable, DType::Float64).unwrap(),
5620 DType::Float64
5621 );
5622 assert_eq!(
5623 common_dtype(DType::Float64, DType::Int64Nullable).unwrap(),
5624 DType::Float64
5625 );
5626
5627 assert_eq!(
5629 common_dtype(DType::Int64Nullable, DType::Int64Nullable).unwrap(),
5630 DType::Int64Nullable
5631 );
5632
5633 assert_eq!(
5635 common_dtype(DType::Bool, DType::Int64Nullable).unwrap(),
5636 DType::Int64Nullable
5637 );
5638
5639 assert_eq!(
5641 common_dtype(DType::BoolNullable, DType::Int64).unwrap(),
5642 DType::Int64Nullable
5643 );
5644 }
5645
5646 #[test]
5647 fn nullable_bool_promotion_matrix() {
5648 assert_eq!(
5650 common_dtype(DType::Bool, DType::BoolNullable).unwrap(),
5651 DType::BoolNullable
5652 );
5653 assert_eq!(
5654 common_dtype(DType::BoolNullable, DType::Bool).unwrap(),
5655 DType::BoolNullable
5656 );
5657
5658 assert_eq!(
5660 common_dtype(DType::BoolNullable, DType::Float64).unwrap(),
5661 DType::Float64
5662 );
5663 }
5664
5665 #[test]
5666 fn dtype_is_nullable_helper() {
5667 assert!(DType::Int64Nullable.is_nullable());
5668 assert!(DType::BoolNullable.is_nullable());
5669 assert!(!DType::Int64.is_nullable());
5670 assert!(!DType::Bool.is_nullable());
5671 assert!(!DType::Float64.is_nullable());
5672 assert!(!DType::Utf8.is_nullable());
5673 }
5674
5675 #[test]
5676 fn dtype_to_nullable_conversions() {
5677 assert_eq!(DType::Int64.to_nullable(), DType::Int64Nullable);
5678 assert_eq!(DType::Bool.to_nullable(), DType::BoolNullable);
5679 assert_eq!(DType::Float64.to_nullable(), DType::Float64); assert_eq!(DType::Int64Nullable.to_nullable(), DType::Int64Nullable);
5681 }
5682
5683 #[test]
5684 fn dtype_to_non_nullable_conversions() {
5685 assert_eq!(DType::Int64Nullable.to_non_nullable(), DType::Int64);
5686 assert_eq!(DType::BoolNullable.to_non_nullable(), DType::Bool);
5687 assert_eq!(DType::Int64.to_non_nullable(), DType::Int64); assert_eq!(DType::Float64.to_non_nullable(), DType::Float64);
5689 }
5690
5691 #[test]
5692 fn nullable_dtype_name_reports_pandas_style() {
5693 assert_eq!(DType::Int64.name(), "int64");
5694 assert_eq!(DType::Int64Nullable.name(), "Int64"); assert_eq!(DType::Bool.name(), "bool");
5696 assert_eq!(DType::BoolNullable.name(), "boolean");
5697 }
5698
5699 #[test]
5700 fn cast_scalar_int64_nullable_identity() {
5701 let val = Scalar::Int64(42);
5702 let result = cast_scalar(&val, DType::Int64Nullable).unwrap();
5704 assert_eq!(result, Scalar::Int64(42));
5705
5706 let result2 = cast_scalar(&val, DType::Int64).unwrap();
5708 assert_eq!(result2, Scalar::Int64(42));
5709 }
5710
5711 #[test]
5712 fn cast_float_to_utf8_uses_pandas_str_float_with_scientific() {
5713 let cases: &[(f64, &str)] = &[
5718 (1.0, "1.0"),
5719 (2.5, "2.5"),
5720 (100.0, "100.0"),
5721 (0.1, "0.1"),
5722 (0.0001, "0.0001"),
5723 (1e16, "1e+16"),
5724 (1e20, "1e+20"),
5725 (1e-5, "1e-05"),
5726 (1e-7, "1e-07"),
5727 (f64::INFINITY, "inf"),
5728 (f64::NEG_INFINITY, "-inf"),
5729 ];
5730 for (v, expected) in cases {
5731 assert_eq!(
5732 cast_scalar(&Scalar::Float64(*v), DType::Utf8).unwrap(),
5733 Scalar::Utf8((*expected).to_owned()),
5734 "float {v} -> str"
5735 );
5736 }
5737 }
5738
5739 #[test]
5740 fn cast_float_to_utf8_threshold_boundaries_match_python() {
5741 let cases: &[(f64, &str)] = &[
5746 (1e15, "1000000000000000.0"),
5747 (9_999_999_999_999_998.0, "9999999999999998.0"),
5748 (1_234_567_890_123_456.0, "1234567890123456.0"),
5749 (123_456_789_012_345.0, "123456789012345.0"),
5750 (12_345_678_901_234_567.0, "1.2345678901234568e+16"),
5751 (1e16, "1e+16"),
5752 (1.5e16, "1.5e+16"),
5753 (1e17, "1e+17"),
5754 (1e-4, "0.0001"),
5755 (5e-5, "5e-05"),
5756 (-1e15, "-1000000000000000.0"),
5757 (-1e16, "-1e+16"),
5758 (-1e-5, "-1e-05"),
5759 ];
5760 for (v, expected) in cases {
5761 assert_eq!(
5762 cast_scalar(&Scalar::Float64(*v), DType::Utf8).unwrap(),
5763 Scalar::Utf8((*expected).to_owned()),
5764 "float {v} -> str"
5765 );
5766 }
5767 }
5768
5769 #[test]
5770 fn cast_to_bool_uses_pandas_nonzero_truthiness() {
5771 let cases_int: &[(i64, bool)] = &[(0, false), (1, true), (-3, true), (2, true)];
5775 for (v, expected) in cases_int {
5776 assert_eq!(
5777 cast_scalar(&Scalar::Int64(*v), DType::Bool).unwrap(),
5778 Scalar::Bool(*expected),
5779 "int {v} -> bool"
5780 );
5781 }
5782 let cases_float: &[(f64, bool)] = &[
5783 (0.0, false),
5784 (-0.0, false),
5785 (0.1, true),
5786 (2.5, true),
5787 (1.0, true),
5788 (f64::NAN, true),
5790 ];
5791 for (v, expected) in cases_float {
5792 assert_eq!(
5793 cast_scalar(&Scalar::Float64(*v), DType::Bool).unwrap(),
5794 Scalar::Bool(*expected),
5795 "float {v} -> bool"
5796 );
5797 }
5798 }
5799
5800 #[test]
5801 fn nullable_dtype_is_extension() {
5802 assert!(DType::Int64Nullable.is_extension());
5803 assert!(DType::BoolNullable.is_extension());
5804 assert!(!DType::Int64.is_extension());
5805 assert!(!DType::Bool.is_extension());
5806 }
5807
5808 #[test]
5809 fn infer_dtype_preserves_string_numeric_mix_as_utf8_bucket() {
5810 let values = vec![Scalar::Utf8("x".into()), Scalar::Int64(7)];
5811 assert_eq!(
5812 infer_dtype(&values).expect("dtype should infer"),
5813 DType::Utf8
5814 );
5815 }
5816
5817 #[test]
5820 fn is_null_detects_explicit_nulls() {
5821 assert!(Scalar::Null(NullKind::Null).is_null());
5822 assert!(Scalar::Null(NullKind::NaN).is_null());
5823 assert!(!Scalar::Int64(42).is_null());
5824 assert!(!Scalar::Float64(f64::NAN).is_null());
5825 }
5826
5827 #[test]
5828 fn is_na_matches_is_missing() {
5829 let vals = vec![
5830 Scalar::Null(NullKind::Null),
5831 Scalar::Float64(f64::NAN),
5832 Scalar::Int64(0),
5833 Scalar::Bool(false),
5834 ];
5835 for v in &vals {
5836 assert_eq!(v.is_na(), v.is_missing());
5837 }
5838 }
5839
5840 #[test]
5841 fn coalesce_picks_first_non_missing() {
5842 let null = Scalar::Null(NullKind::Null);
5843 let fill = Scalar::Int64(99);
5844 assert_eq!(null.coalesce(&fill), fill);
5845 assert_eq!(fill.coalesce(&null), fill);
5846 }
5847
5848 #[test]
5851 fn isna_notna_complement() {
5852 let vals = vec![
5853 Scalar::Int64(1),
5854 Scalar::Null(NullKind::Null),
5855 Scalar::Float64(f64::NAN),
5856 Scalar::Float64(3.0),
5857 ];
5858 let na = super::isna(&vals);
5859 let not = super::notna(&vals);
5860 assert_eq!(na, vec![false, true, true, false]);
5861 for (a, b) in na.iter().zip(not.iter()) {
5862 assert_ne!(a, b);
5863 }
5864 }
5865
5866 #[test]
5867 fn count_na_counts_missing() {
5868 let vals = vec![
5869 Scalar::Int64(1),
5870 Scalar::Null(NullKind::Null),
5871 Scalar::Float64(f64::NAN),
5872 ];
5873 assert_eq!(super::count_na(&vals), 2);
5874 }
5875
5876 #[test]
5877 fn fill_na_replaces_missing() {
5878 let vals = vec![
5879 Scalar::Int64(1),
5880 Scalar::Null(NullKind::Null),
5881 Scalar::Float64(f64::NAN),
5882 Scalar::Int64(4),
5883 ];
5884 let filled = super::fill_na(&vals, &Scalar::Int64(0));
5885 assert_eq!(filled[0], Scalar::Int64(1));
5886 assert_eq!(filled[1], Scalar::Int64(0));
5887 assert_eq!(filled[2], Scalar::Int64(0));
5888 assert_eq!(filled[3], Scalar::Int64(4));
5889 }
5890
5891 #[test]
5892 fn dropna_removes_missing() {
5893 let vals = vec![
5894 Scalar::Int64(1),
5895 Scalar::Null(NullKind::Null),
5896 Scalar::Int64(3),
5897 Scalar::Float64(f64::NAN),
5898 ];
5899 let kept = super::dropna(&vals);
5900 assert_eq!(kept.len(), 2);
5901 assert_eq!(kept[0], Scalar::Int64(1));
5902 assert_eq!(kept[1], Scalar::Int64(3));
5903 }
5904
5905 #[test]
5906 fn null_helpers_match_scalar_oracle_imt0c() {
5907 fn next(seed: &mut u64) -> u64 {
5910 *seed = seed
5911 .wrapping_mul(3202034522624059733)
5912 .wrapping_add(4354685564936845319);
5913 *seed
5914 }
5915
5916 fn assert_null_helpers(case: usize, values: &[Scalar], fill: &Scalar) {
5917 let expected_missing = values.iter().filter(|value| value.is_missing()).count();
5918 let expected_dropped = values
5919 .iter()
5920 .filter(|value| !value.is_missing())
5921 .cloned()
5922 .collect::<Vec<_>>();
5923 let expected_filled = values
5924 .iter()
5925 .map(|value| {
5926 if value.is_missing() {
5927 fill.clone()
5928 } else {
5929 value.clone()
5930 }
5931 })
5932 .collect::<Vec<_>>();
5933
5934 assert_eq!(
5935 super::count_na(values),
5936 expected_missing,
5937 "case={case}: count_na mismatch for {values:?}"
5938 );
5939
5940 let dropped = super::dropna(values);
5941 assert_eq!(
5942 dropped.len(),
5943 expected_dropped.len(),
5944 "case={case}: dropna length mismatch for {values:?}"
5945 );
5946 for (pos, (actual, expected)) in dropped.iter().zip(expected_dropped.iter()).enumerate()
5947 {
5948 assert!(
5949 actual.semantic_eq(expected),
5950 "case={case} pos={pos}: dropna expected {expected:?}, got {actual:?}"
5951 );
5952 }
5953
5954 let filled = super::fill_na(values, fill);
5955 assert_eq!(
5956 filled.len(),
5957 expected_filled.len(),
5958 "case={case}: fill_na length mismatch for {values:?}"
5959 );
5960 for (pos, (actual, expected)) in filled.iter().zip(expected_filled.iter()).enumerate() {
5961 assert!(
5962 actual.semantic_eq(expected),
5963 "case={case} pos={pos}: fill_na expected {expected:?}, got {actual:?}"
5964 );
5965 }
5966 }
5967
5968 let all_missing = [
5969 Scalar::Null(NullKind::Null),
5970 Scalar::Null(NullKind::NaN),
5971 Scalar::Float64(f64::NAN),
5972 Scalar::Timedelta64(i64::MIN),
5973 ];
5974 assert_null_helpers(usize::MAX, &all_missing, &Scalar::Utf8("filled".into()));
5975
5976 let mut seed = 0xc011_a7ed_0b5e_1a55_u64;
5977 for case in 0..260 {
5978 let len = (next(&mut seed) % 83 + 1) as usize;
5979 let mut values = Vec::with_capacity(len);
5980 for pos in 0..len {
5981 let raw = (next(&mut seed) % 20_001) as i64 - 10_000;
5982 values.push(match next(&mut seed) % 11 {
5983 0 => Scalar::Null(NullKind::Null),
5984 1 => Scalar::Null(NullKind::NaN),
5985 2 => Scalar::Float64(f64::NAN),
5986 3 => Scalar::Timedelta64(i64::MIN),
5987 4 => Scalar::Bool(raw & 1 == 0),
5988 5 => Scalar::Int64(raw),
5989 6 => Scalar::Float64(raw as f64 / 37.0),
5990 7 => Scalar::Float64(if raw & 1 == 0 { 0.0 } else { -0.0 }),
5991 8 => Scalar::Utf8(format!("null_helper_{case}_{pos}")),
5992 9 => Scalar::Utf8(String::new()),
5993 _ => Scalar::Timedelta64(raw),
5994 });
5995 }
5996
5997 let fill = match case % 5 {
5998 0 => Scalar::Bool(true),
5999 1 => Scalar::Int64(-777),
6000 2 => Scalar::Float64(12.5),
6001 3 => Scalar::Utf8("filled".into()),
6002 _ => Scalar::Timedelta64(123_456),
6003 };
6004 assert_null_helpers(case, &values, &fill);
6005 }
6006 }
6007
6008 #[test]
6011 fn nansum_skips_nulls() {
6012 let vals = vec![
6013 Scalar::Float64(1.0),
6014 Scalar::Null(NullKind::Null),
6015 Scalar::Float64(2.0),
6016 Scalar::Float64(f64::NAN),
6017 Scalar::Int64(7),
6018 ];
6019 assert_eq!(super::nansum(&vals), Scalar::Float64(10.0));
6020 }
6021
6022 #[test]
6023 fn nansum_empty_returns_zero() {
6024 assert_eq!(super::nansum(&[]), Scalar::Float64(0.0));
6025 }
6026
6027 #[test]
6028 fn nansum_nanmean_match_numeric_and_timedelta_oracle_1xmi7() {
6029 fn next(seed: &mut u64) -> u64 {
6032 *seed = seed
6033 .wrapping_mul(6364136223846793005)
6034 .wrapping_add(1442695040888963407);
6035 *seed
6036 }
6037
6038 fn expected_numeric(values: &[Scalar]) -> (Scalar, Scalar) {
6039 let mut sum = 0.0;
6040 let mut count = 0usize;
6041 for value in values {
6042 if value.is_missing() {
6043 continue;
6044 }
6045 if let Ok(value) = value.to_f64() {
6046 sum += value;
6047 count += 1;
6048 }
6049 }
6050 let mean = if count == 0 {
6051 Scalar::Null(NullKind::NaN)
6052 } else {
6053 Scalar::Float64(sum / count as f64)
6054 };
6055 (Scalar::Float64(sum), mean)
6056 }
6057
6058 fn expected_timedelta(values: &[Scalar]) -> (Scalar, Scalar) {
6059 let mut sum = 0_i128;
6060 let mut count = 0_i128;
6061 for value in values {
6062 if let Scalar::Timedelta64(ns) = value
6063 && !value.is_missing()
6064 {
6065 sum += i128::from(*ns);
6066 count += 1;
6067 }
6068 }
6069 if count == 0 {
6070 return (Scalar::Float64(0.0), Scalar::Null(NullKind::NaN));
6071 }
6072 let sum = sum.clamp(i128::from(i64::MIN), i128::from(i64::MAX));
6073 let mean = (sum / count).clamp(i128::from(i64::MIN), i128::from(i64::MAX));
6074 (
6075 Scalar::Timedelta64(sum as i64),
6076 Scalar::Timedelta64(mean as i64),
6077 )
6078 }
6079
6080 fn assert_sum_mean(
6081 case: usize,
6082 family: &str,
6083 values: &[Scalar],
6084 expected_sum: Scalar,
6085 expected_mean: Scalar,
6086 ) {
6087 let actual_sum = super::nansum(values);
6088 let actual_mean = super::nanmean(values);
6089 assert!(
6090 actual_sum.semantic_eq(&expected_sum),
6091 "case={case} family={family}: expected sum {expected_sum:?}, got {actual_sum:?} for {values:?}"
6092 );
6093 assert!(
6094 actual_mean.semantic_eq(&expected_mean),
6095 "case={case} family={family}: expected mean {expected_mean:?}, got {actual_mean:?} for {values:?}"
6096 );
6097 }
6098
6099 let all_missing = [Scalar::Null(NullKind::Null), Scalar::Float64(f64::NAN)];
6100 let (sum, mean) = expected_numeric(&all_missing);
6101 assert_sum_mean(usize::MAX, "numeric_all_missing", &all_missing, sum, mean);
6102
6103 let td_all_missing = [Scalar::Timedelta64(i64::MIN), Scalar::Null(NullKind::NaN)];
6104 let (sum, mean) = expected_timedelta(&td_all_missing);
6105 assert_sum_mean(
6106 usize::MAX - 1,
6107 "timedelta_all_missing",
6108 &td_all_missing,
6109 sum,
6110 mean,
6111 );
6112
6113 let mut seed = 0x511d_ed5a_7a11_1a55_u64;
6114 for case in 0..260 {
6115 let len = (next(&mut seed) % 89 + 1) as usize;
6116
6117 let mut numeric = Vec::with_capacity(len);
6118 numeric.push(Scalar::Int64(case as i64 - 130));
6119 for _ in 1..len {
6120 let raw = (next(&mut seed) % 20_001) as i64 - 10_000;
6121 numeric.push(match next(&mut seed) % 8 {
6122 0 => Scalar::Null(NullKind::Null),
6123 1 => Scalar::Null(NullKind::NaN),
6124 2 => Scalar::Float64(f64::NAN),
6125 3 => Scalar::Bool(raw & 1 == 0),
6126 4 => Scalar::Int64(raw % 257),
6127 5 => Scalar::Float64(raw as f64 / 67.0),
6128 6 => Scalar::Float64(0.0),
6129 _ => Scalar::Float64(-0.0),
6130 });
6131 }
6132 let (sum, mean) = expected_numeric(&numeric);
6133 assert_sum_mean(case, "numeric", &numeric, sum, mean);
6134
6135 let mut timedeltas = Vec::with_capacity(len);
6136 timedeltas.push(Scalar::Timedelta64(case as i64 - 130));
6137 for _ in 1..len {
6138 let raw = (next(&mut seed) % 20_001) as i64 - 10_000;
6139 timedeltas.push(match next(&mut seed) % 7 {
6140 0 => Scalar::Null(NullKind::Null),
6141 1 => Scalar::Timedelta64(i64::MIN),
6142 _ => Scalar::Timedelta64(raw),
6143 });
6144 }
6145 let (sum, mean) = expected_timedelta(&timedeltas);
6146 assert_sum_mean(case, "timedelta", &timedeltas, sum, mean);
6147 }
6148 }
6149
6150 #[test]
6151 fn nannunique_merges_negative_zero_and_zero() {
6152 let vals = vec![
6153 Scalar::Float64(-0.0),
6154 Scalar::Float64(0.0),
6155 Scalar::Float64(1.0),
6156 ];
6157 assert_eq!(super::nannunique(&vals), Scalar::Int64(2));
6158 }
6159
6160 #[test]
6161 fn nannunique_matches_scalar_bucket_oracle_elvbg() {
6162 fn next(seed: &mut u64) -> u64 {
6165 *seed = seed
6166 .wrapping_mul(2862933555777941757)
6167 .wrapping_add(3037000493);
6168 *seed
6169 }
6170
6171 fn same_bucket(left: &Scalar, right: &Scalar) -> bool {
6172 match (left, right) {
6173 (Scalar::Float64(left), Scalar::Float64(right)) => {
6174 let left = if *left == 0.0 { 0.0 } else { *left };
6175 let right = if *right == 0.0 { 0.0 } else { *right };
6176 left.to_bits() == right.to_bits()
6177 }
6178 _ => left == right,
6179 }
6180 }
6181
6182 fn expected_nunique(values: &[Scalar]) -> i64 {
6183 let mut seen = Vec::<Scalar>::new();
6184 for value in values {
6185 if value.is_missing() {
6186 continue;
6187 }
6188 if !seen.iter().any(|existing| same_bucket(existing, value)) {
6189 seen.push(value.clone());
6190 }
6191 }
6192 seen.len() as i64
6193 }
6194
6195 fn assert_nannunique(case: usize, values: &[Scalar]) {
6196 assert_eq!(
6197 super::nannunique(values),
6198 Scalar::Int64(expected_nunique(values)),
6199 "case={case}: nannunique mismatch for {values:?}"
6200 );
6201 }
6202
6203 assert_nannunique(
6204 usize::MAX,
6205 &[
6206 Scalar::Float64(-0.0),
6207 Scalar::Float64(0.0),
6208 Scalar::Float64(f64::NAN),
6209 Scalar::Null(NullKind::Null),
6210 Scalar::Timedelta64(i64::MIN),
6211 ],
6212 );
6213
6214 let mut seed = 0x0e1b_60d0_b5e7_u64;
6215 for case in 0..320 {
6216 let len = (next(&mut seed) % 97 + 1) as usize;
6217 let mut values = Vec::with_capacity(len);
6218 for pos in 0..len {
6219 let raw = (next(&mut seed) % 1_001) as i64 - 500;
6220 values.push(match next(&mut seed) % 11 {
6221 0 => Scalar::Null(NullKind::Null),
6222 1 => Scalar::Null(NullKind::NaN),
6223 2 => Scalar::Float64(f64::NAN),
6224 3 => Scalar::Bool(raw & 1 == 0),
6225 4 => Scalar::Int64(raw % 37),
6226 5 => Scalar::Float64(raw as f64 / 19.0),
6227 6 => Scalar::Float64(0.0),
6228 7 => Scalar::Float64(-0.0),
6229 8 => Scalar::Utf8(format!("uniq_{}", pos % 13)),
6230 9 => Scalar::Utf8(String::new()),
6231 _ => Scalar::Timedelta64(raw % 41),
6232 });
6233 }
6234 assert_nannunique(case, &values);
6235 }
6236 }
6237
6238 #[test]
6239 fn nanmean_basic() {
6240 let vals = vec![
6241 Scalar::Float64(2.0),
6242 Scalar::Null(NullKind::Null),
6243 Scalar::Float64(4.0),
6244 ];
6245 assert_eq!(super::nanmean(&vals), Scalar::Float64(3.0));
6246 }
6247
6248 #[test]
6249 fn nanmean_all_null_returns_nan() {
6250 let vals = vec![Scalar::Null(NullKind::Null), Scalar::Float64(f64::NAN)];
6251 assert!(super::nanmean(&vals).is_missing());
6252 }
6253
6254 #[test]
6255 fn nansum_nanmean_timedelta64_preserves_dtype_620mj() {
6256 let one_hour = 3_600 * 1_000_000_000_i64;
6260 let vals = vec![
6261 Scalar::Timedelta64(one_hour),
6262 Scalar::Timedelta64(2 * one_hour),
6263 Scalar::Timedelta64(3 * one_hour),
6264 ];
6265 assert_eq!(super::nansum(&vals), Scalar::Timedelta64(6 * one_hour));
6266 assert_eq!(super::nanmean(&vals), Scalar::Timedelta64(2 * one_hour));
6267 }
6268
6269 #[test]
6270 fn nansum_nanmean_timedelta64_skips_nat_620mj() {
6271 let one_hour = 3_600 * 1_000_000_000_i64;
6272 let vals = vec![
6273 Scalar::Timedelta64(Timedelta::NAT),
6274 Scalar::Timedelta64(one_hour),
6275 Scalar::Timedelta64(3 * one_hour),
6276 Scalar::Timedelta64(Timedelta::NAT),
6277 ];
6278 assert_eq!(super::nansum(&vals), Scalar::Timedelta64(4 * one_hour));
6280 assert_eq!(super::nanmean(&vals), Scalar::Timedelta64(2 * one_hour));
6281 }
6282
6283 #[test]
6284 fn nansum_nanmean_mixed_timedelta_other_falls_back_620mj() {
6285 let vals = vec![Scalar::Timedelta64(3600 * 1_000_000_000), Scalar::Int64(5)];
6289 assert_eq!(super::nansum(&vals), Scalar::Float64(5.0));
6291 }
6292
6293 #[test]
6294 fn nancount_counts_non_missing() {
6295 let vals = vec![
6296 Scalar::Int64(1),
6297 Scalar::Null(NullKind::Null),
6298 Scalar::Float64(3.0),
6299 ];
6300 assert_eq!(super::nancount(&vals), Scalar::Int64(2));
6301 }
6302
6303 #[test]
6304 fn nanany_nanall_nancount_match_scalar_oracle_zr2qg() {
6305 fn next(seed: &mut u64) -> u64 {
6308 *seed = seed
6309 .wrapping_mul(6364136223846793005)
6310 .wrapping_add(1442695040888963407);
6311 *seed
6312 }
6313
6314 fn truthy(value: &Scalar) -> Option<bool> {
6315 if value.is_missing() {
6316 return None;
6317 }
6318 match value {
6319 Scalar::Bool(value) => Some(*value),
6320 Scalar::Int64(value) => Some(*value != 0),
6321 Scalar::Float64(value) => Some(*value != 0.0),
6322 Scalar::Utf8(value) => Some(!value.is_empty()),
6323 Scalar::Timedelta64(value) => Some(*value != 0),
6324 _ => None,
6325 }
6326 }
6327
6328 fn assert_nanops(case: usize, values: &[Scalar]) {
6329 let truth_values = values.iter().filter_map(truthy).collect::<Vec<_>>();
6330 let expected_any = truth_values.iter().any(|value| *value);
6331 let expected_all = !truth_values.iter().any(|value| !*value);
6332 let expected_count = values.iter().filter(|value| !value.is_missing()).count() as i64;
6333
6334 assert_eq!(
6335 super::nanany(values),
6336 Scalar::Bool(expected_any),
6337 "case={case}: nanany mismatch for {values:?}"
6338 );
6339 assert_eq!(
6340 super::nanall(values),
6341 Scalar::Bool(expected_all),
6342 "case={case}: nanall mismatch for {values:?}"
6343 );
6344 assert_eq!(
6345 super::nancount(values),
6346 Scalar::Int64(expected_count),
6347 "case={case}: nancount mismatch for {values:?}"
6348 );
6349 }
6350
6351 assert_nanops(
6352 usize::MAX,
6353 &[
6354 Scalar::Null(NullKind::Null),
6355 Scalar::Null(NullKind::NaN),
6356 Scalar::Float64(f64::NAN),
6357 Scalar::Timedelta64(i64::MIN),
6358 ],
6359 );
6360
6361 let mut seed = 0x7a20_2f7e_5ca1_ab1e_u64;
6362 for case in 0..320 {
6363 let len = (next(&mut seed) % 89 + 1) as usize;
6364 let mut values = Vec::with_capacity(len);
6365 for pos in 0..len {
6366 let raw = (next(&mut seed) % 10_001) as i64 - 5_000;
6367 let value = match next(&mut seed) % 12 {
6368 0 => Scalar::Null(NullKind::Null),
6369 1 => Scalar::Null(NullKind::NaN),
6370 2 => Scalar::Float64(f64::NAN),
6371 3 => Scalar::Bool(raw & 1 == 0),
6372 4 => Scalar::Bool(false),
6373 5 => Scalar::Int64(raw % 17),
6374 6 => Scalar::Int64(0),
6375 7 => Scalar::Float64(raw as f64 / 23.0),
6376 8 => Scalar::Float64(0.0),
6377 9 => Scalar::Utf8(if raw & 1 == 0 {
6378 String::new()
6379 } else {
6380 format!("nanops_{case}_{pos}")
6381 }),
6382 10 => Scalar::Timedelta64(raw),
6383 _ => Scalar::Timedelta64(0),
6384 };
6385 values.push(value);
6386 }
6387 assert_nanops(case, &values);
6388 }
6389 }
6390
6391 #[test]
6392 fn nanmin_basic() {
6393 let vals = vec![
6394 Scalar::Float64(5.0),
6395 Scalar::Null(NullKind::Null),
6396 Scalar::Float64(2.0),
6397 Scalar::Float64(8.0),
6398 ];
6399 assert_eq!(super::nanmin(&vals), Scalar::Float64(2.0));
6400 }
6401
6402 #[test]
6403 fn nanmax_basic() {
6404 let vals = vec![
6405 Scalar::Float64(5.0),
6406 Scalar::Float64(f64::NAN),
6407 Scalar::Float64(8.0),
6408 ];
6409 assert_eq!(super::nanmax(&vals), Scalar::Float64(8.0));
6410 }
6411
6412 #[test]
6413 fn nanmin_nanmax_empty_returns_nan() {
6414 assert!(super::nanmin(&[]).is_missing());
6415 assert!(super::nanmax(&[]).is_missing());
6416 }
6417
6418 #[test]
6419 fn nanmin_nanmax_match_same_family_oracle_vj7ds() {
6420 fn next(seed: &mut u64) -> u64 {
6423 *seed = seed
6424 .wrapping_mul(6364136223846793005)
6425 .wrapping_add(1442695040888963407);
6426 *seed
6427 }
6428
6429 fn family_cmp(left: &Scalar, right: &Scalar) -> std::cmp::Ordering {
6430 match (left, right) {
6431 (Scalar::Bool(left), Scalar::Bool(right)) => left.cmp(right),
6432 (Scalar::Int64(left), Scalar::Int64(right)) => left.cmp(right),
6433 (Scalar::Float64(left), Scalar::Float64(right)) => {
6434 left.partial_cmp(right).expect("finite floats")
6435 }
6436 (Scalar::Utf8(left), Scalar::Utf8(right)) => left.cmp(right),
6437 (Scalar::Timedelta64(left), Scalar::Timedelta64(right)) => left.cmp(right),
6438 _ => panic!("mixed family in nanmin/nanmax oracle"),
6439 }
6440 }
6441
6442 fn assert_minmax(case: usize, family: &str, values: &[Scalar]) {
6443 let present = values
6444 .iter()
6445 .filter(|value| !value.is_missing())
6446 .cloned()
6447 .collect::<Vec<_>>();
6448 let actual_min = super::nanmin(values);
6449 let actual_max = super::nanmax(values);
6450 if present.is_empty() {
6451 assert!(
6452 actual_min.is_missing(),
6453 "case={case} family={family}: expected missing min for {values:?}, got {actual_min:?}"
6454 );
6455 assert!(
6456 actual_max.is_missing(),
6457 "case={case} family={family}: expected missing max for {values:?}, got {actual_max:?}"
6458 );
6459 return;
6460 }
6461
6462 let expected_min = present.iter().min_by(|left, right| family_cmp(left, right));
6463 let expected_max = present.iter().max_by(|left, right| family_cmp(left, right));
6464 assert!(
6465 actual_min.semantic_eq(expected_min.expect("min")),
6466 "case={case} family={family}: expected min {:?}, got {actual_min:?} for {values:?}",
6467 expected_min.expect("min")
6468 );
6469 assert!(
6470 actual_max.semantic_eq(expected_max.expect("max")),
6471 "case={case} family={family}: expected max {:?}, got {actual_max:?} for {values:?}",
6472 expected_max.expect("max")
6473 );
6474 }
6475
6476 let all_missing = [
6477 Scalar::Null(NullKind::Null),
6478 Scalar::Null(NullKind::NaN),
6479 Scalar::Float64(f64::NAN),
6480 Scalar::Timedelta64(i64::MIN),
6481 ];
6482 assert_minmax(usize::MAX, "all_missing", &all_missing);
6483
6484 let mut seed = 0xa11c_0aba_2e7d_f00d_u64;
6485 for case in 0..240 {
6486 let len = (next(&mut seed) % 73 + 1) as usize;
6487
6488 let mut ints = Vec::with_capacity(len);
6489 ints.push(Scalar::Int64(case as i64 - 120));
6490 for _ in 1..len {
6491 let raw = (next(&mut seed) % 1_001) as i64 - 500;
6492 ints.push(match next(&mut seed) % 6 {
6493 0 => Scalar::Null(NullKind::Null),
6494 1 => Scalar::Null(NullKind::NaN),
6495 _ => Scalar::Int64(raw),
6496 });
6497 }
6498 assert_minmax(case, "int", &ints);
6499
6500 let mut floats = Vec::with_capacity(len);
6501 floats.push(Scalar::Float64(case as f64 / 11.0));
6502 for _ in 1..len {
6503 let raw = (next(&mut seed) % 20_001) as i64 - 10_000;
6504 floats.push(match next(&mut seed) % 8 {
6505 0 | 1 => Scalar::Float64(f64::NAN),
6506 2 => Scalar::Float64(f64::INFINITY),
6507 3 => Scalar::Float64(f64::NEG_INFINITY),
6508 4 => Scalar::Float64(0.0),
6509 5 => Scalar::Float64(-0.0),
6510 _ => Scalar::Float64(raw as f64 / 41.0),
6511 });
6512 }
6513 assert_minmax(case, "float", &floats);
6514
6515 let mut bools = Vec::with_capacity(len);
6516 bools.push(Scalar::Bool(case & 1 == 0));
6517 for _ in 1..len {
6518 bools.push(match next(&mut seed) % 5 {
6519 0 => Scalar::Null(NullKind::Null),
6520 1 => Scalar::Null(NullKind::NaN),
6521 raw => Scalar::Bool(raw & 1 == 0),
6522 });
6523 }
6524 assert_minmax(case, "bool", &bools);
6525
6526 let mut utf8 = Vec::with_capacity(len);
6527 utf8.push(Scalar::Utf8(format!("minmax_{}", case % 17)));
6528 for pos in 1..len {
6529 utf8.push(match next(&mut seed) % 7 {
6530 0 => Scalar::Null(NullKind::Null),
6531 1 => Scalar::Null(NullKind::NaN),
6532 raw => Scalar::Utf8(format!("minmax_{}_{}", raw, pos % 11)),
6533 });
6534 }
6535 assert_minmax(case, "utf8", &utf8);
6536
6537 let mut timedeltas = Vec::with_capacity(len);
6538 timedeltas.push(Scalar::Timedelta64(case as i64 - 120));
6539 for _ in 1..len {
6540 let raw = (next(&mut seed) % 1_003) as i64 - 501;
6541 timedeltas.push(match next(&mut seed) % 7 {
6542 0 => Scalar::Null(NullKind::Null),
6543 1 => Scalar::Timedelta64(i64::MIN),
6544 _ => Scalar::Timedelta64(raw),
6545 });
6546 }
6547 assert_minmax(case, "timedelta", &timedeltas);
6548 }
6549 }
6550
6551 #[test]
6552 fn nanmin_nanmax_mixed_incompatible_types_returns_nan() {
6553 let vals = vec![Scalar::Int64(5), Scalar::Utf8("hello".into())];
6554 assert!(super::nanmin(&vals).is_missing());
6555 assert!(super::nanmax(&vals).is_missing());
6556
6557 let vals2 = vec![Scalar::Utf8("a".into()), Scalar::Float64(3.0)];
6558 assert!(super::nanmin(&vals2).is_missing());
6559 assert!(super::nanmax(&vals2).is_missing());
6560 }
6561
6562 #[test]
6563 fn nanmin_nanmax_compatible_numeric_types_ok() {
6564 let vals = vec![Scalar::Int64(5), Scalar::Float64(3.0), Scalar::Bool(true)];
6565 assert_eq!(super::nanmin(&vals), Scalar::Bool(true));
6566 assert_eq!(super::nanmax(&vals), Scalar::Int64(5));
6567 }
6568
6569 #[test]
6570 fn nanmin_nanmax_timedelta64_returns_timedelta_yic5m() {
6571 let one_hour = 3_600 * 1_000_000_000_i64;
6575 let vals = vec![
6576 Scalar::Timedelta64(3 * one_hour),
6577 Scalar::Timedelta64(one_hour),
6578 Scalar::Timedelta64(2 * one_hour),
6579 ];
6580 assert_eq!(super::nanmin(&vals), Scalar::Timedelta64(one_hour));
6581 assert_eq!(super::nanmax(&vals), Scalar::Timedelta64(3 * one_hour));
6582 }
6583
6584 #[test]
6585 fn nanmin_nanmax_timedelta64_skips_nat_yic5m() {
6586 let one_hour = 3_600 * 1_000_000_000_i64;
6587 let vals = vec![
6588 Scalar::Timedelta64(Timedelta::NAT),
6589 Scalar::Timedelta64(one_hour),
6590 Scalar::Timedelta64(2 * one_hour),
6591 Scalar::Timedelta64(Timedelta::NAT),
6592 ];
6593 assert_eq!(super::nanmin(&vals), Scalar::Timedelta64(one_hour));
6594 assert_eq!(super::nanmax(&vals), Scalar::Timedelta64(2 * one_hour));
6595 }
6596
6597 #[test]
6598 fn nanmedian_odd_count() {
6599 let vals = vec![
6600 Scalar::Float64(3.0),
6601 Scalar::Null(NullKind::Null),
6602 Scalar::Float64(1.0),
6603 Scalar::Float64(2.0),
6604 ];
6605 assert_eq!(super::nanmedian(&vals), Scalar::Float64(2.0));
6606 }
6607
6608 #[test]
6609 fn nanmedian_even_count() {
6610 let vals = vec![
6611 Scalar::Float64(1.0),
6612 Scalar::Float64(3.0),
6613 Scalar::Float64(2.0),
6614 Scalar::Float64(4.0),
6615 ];
6616 assert_eq!(super::nanmedian(&vals), Scalar::Float64(2.5));
6617 }
6618
6619 #[test]
6620 fn nanmedian_matches_numeric_and_timedelta_oracle_oabhi() {
6621 fn next(seed: &mut u64) -> u64 {
6624 *seed = seed
6625 .wrapping_mul(6364136223846793005)
6626 .wrapping_add(1442695040888963407);
6627 *seed
6628 }
6629
6630 fn expected_numeric(values: &[Scalar]) -> Scalar {
6631 let mut finite = values
6632 .iter()
6633 .filter(|value| !value.is_missing())
6634 .filter_map(|value| value.to_f64().ok())
6635 .filter(|value| !value.is_nan())
6636 .collect::<Vec<_>>();
6637 if finite.is_empty() {
6638 return Scalar::Null(NullKind::NaN);
6639 }
6640 finite.sort_by(|left, right| left.partial_cmp(right).expect("finite values"));
6641 let mid = finite.len() / 2;
6642 if finite.len().is_multiple_of(2) {
6643 Scalar::Float64((finite[mid - 1] + finite[mid]) / 2.0)
6644 } else {
6645 Scalar::Float64(finite[mid])
6646 }
6647 }
6648
6649 fn expected_timedelta(values: &[Scalar]) -> Scalar {
6650 let mut finite = values
6651 .iter()
6652 .filter_map(|value| match value {
6653 Scalar::Timedelta64(ns) if !value.is_missing() => Some(*ns as f64),
6654 _ => None,
6655 })
6656 .collect::<Vec<_>>();
6657 if finite.is_empty() {
6658 return Scalar::Null(NullKind::NaN);
6659 }
6660 finite.sort_by(|left, right| left.partial_cmp(right).expect("finite values"));
6661 let mid = finite.len() / 2;
6662 let median = if finite.len().is_multiple_of(2) {
6663 (finite[mid - 1] + finite[mid]) / 2.0
6664 } else {
6665 finite[mid]
6666 };
6667 Scalar::Timedelta64(median as i64)
6668 }
6669
6670 fn assert_median(case: usize, family: &str, values: &[Scalar], expected: Scalar) {
6671 let actual = super::nanmedian(values);
6672 assert!(
6673 actual.semantic_eq(&expected),
6674 "case={case} family={family}: expected {expected:?}, got {actual:?} for {values:?}"
6675 );
6676 }
6677
6678 assert_median(
6679 usize::MAX,
6680 "numeric_all_missing",
6681 &[Scalar::Null(NullKind::Null), Scalar::Float64(f64::NAN)],
6682 Scalar::Null(NullKind::NaN),
6683 );
6684 assert_median(
6685 usize::MAX - 1,
6686 "timedelta_all_missing",
6687 &[Scalar::Timedelta64(i64::MIN), Scalar::Null(NullKind::NaN)],
6688 Scalar::Null(NullKind::NaN),
6689 );
6690
6691 let mut seed = 0x0ab1_1eda_57a7_15e5_u64;
6692 for case in 0..220 {
6693 let len = (next(&mut seed) % 79 + 1) as usize;
6694
6695 let mut numeric = Vec::with_capacity(len);
6696 numeric.push(Scalar::Int64(case as i64 - 110));
6697 for _ in 1..len {
6698 let raw = (next(&mut seed) % 20_001) as i64 - 10_000;
6699 numeric.push(match next(&mut seed) % 8 {
6700 0 => Scalar::Null(NullKind::Null),
6701 1 => Scalar::Null(NullKind::NaN),
6702 2 => Scalar::Float64(f64::NAN),
6703 3 => Scalar::Bool(raw & 1 == 0),
6704 4 => Scalar::Int64(raw % 251),
6705 5 => Scalar::Float64(raw as f64 / 61.0),
6706 6 => Scalar::Float64(0.0),
6707 _ => Scalar::Float64(-0.0),
6708 });
6709 }
6710 assert_median(case, "numeric", &numeric, expected_numeric(&numeric));
6711
6712 let mut timedeltas = Vec::with_capacity(len);
6713 timedeltas.push(Scalar::Timedelta64(case as i64 - 110));
6714 for _ in 1..len {
6715 let raw = (next(&mut seed) % 20_001) as i64 - 10_000;
6716 timedeltas.push(match next(&mut seed) % 7 {
6717 0 => Scalar::Null(NullKind::Null),
6718 1 => Scalar::Timedelta64(i64::MIN),
6719 _ => Scalar::Timedelta64(raw),
6720 });
6721 }
6722 assert_median(
6723 case,
6724 "timedelta",
6725 &timedeltas,
6726 expected_timedelta(&timedeltas),
6727 );
6728 }
6729 }
6730
6731 #[test]
6732 fn nanvar_nanstd_nansem_match_numeric_and_timedelta_oracle_k7apg() {
6733 fn next(seed: &mut u64) -> u64 {
6736 *seed = seed
6737 .wrapping_mul(2862933555777941757)
6738 .wrapping_add(3037000493);
6739 *seed
6740 }
6741
6742 fn numeric_samples(values: &[Scalar]) -> Vec<f64> {
6743 values
6744 .iter()
6745 .filter(|value| !value.is_missing())
6746 .filter_map(|value| value.to_f64().ok())
6747 .collect()
6748 }
6749
6750 fn timedelta_samples(values: &[Scalar]) -> Vec<f64> {
6751 values
6752 .iter()
6753 .filter_map(|value| match value {
6754 Scalar::Timedelta64(ns) if !value.is_missing() => Some(*ns as f64),
6755 _ => None,
6756 })
6757 .collect()
6758 }
6759
6760 fn reductions_from_samples(samples: &[f64], ddof: usize) -> Option<(f64, f64, f64)> {
6761 if samples.len() <= ddof {
6762 return None;
6763 }
6764 let mean = samples.iter().sum::<f64>() / samples.len() as f64;
6765 let sum_sq = samples
6766 .iter()
6767 .map(|value| (value - mean).powi(2))
6768 .sum::<f64>();
6769 let var = sum_sq / (samples.len() - ddof) as f64;
6770 let std = var.sqrt();
6771 let sem = std / (samples.len() as f64).sqrt();
6772 Some((var, std, sem))
6773 }
6774
6775 fn expected_numeric(values: &[Scalar], ddof: usize) -> (Scalar, Scalar, Scalar) {
6776 let samples = numeric_samples(values);
6777 let Some((var, std, sem)) = reductions_from_samples(&samples, ddof) else {
6778 let missing = Scalar::Null(NullKind::NaN);
6779 return (missing.clone(), missing.clone(), missing);
6780 };
6781 (
6782 Scalar::Float64(var),
6783 Scalar::Float64(std),
6784 Scalar::Float64(sem),
6785 )
6786 }
6787
6788 fn expected_timedelta(values: &[Scalar], ddof: usize) -> (Scalar, Scalar, Scalar) {
6789 let samples = timedelta_samples(values);
6790 if samples.is_empty() {
6791 let missing = Scalar::Null(NullKind::NaN);
6792 return (missing.clone(), missing.clone(), missing);
6793 }
6794 let Some((var, std, sem)) = reductions_from_samples(&samples, ddof) else {
6795 let missing = Scalar::Timedelta64(i64::MIN);
6796 return (missing.clone(), missing.clone(), missing);
6797 };
6798 (
6799 Scalar::Timedelta64(var as i64),
6800 Scalar::Timedelta64(std as i64),
6801 Scalar::Timedelta64(sem as i64),
6802 )
6803 }
6804
6805 fn assert_reductions(
6806 case: usize,
6807 family: &str,
6808 values: &[Scalar],
6809 ddof: usize,
6810 expected: (Scalar, Scalar, Scalar),
6811 ) {
6812 let (expected_var, expected_std, expected_sem) = expected;
6813 let actual_var = super::nanvar(values, ddof);
6814 let actual_std = super::nanstd(values, ddof);
6815 let actual_sem = super::nansem(values, ddof);
6816 assert!(
6817 actual_var.semantic_eq(&expected_var),
6818 "case={case} family={family} ddof={ddof}: expected var {expected_var:?}, got {actual_var:?} for {values:?}"
6819 );
6820 assert!(
6821 actual_std.semantic_eq(&expected_std),
6822 "case={case} family={family} ddof={ddof}: expected std {expected_std:?}, got {actual_std:?} for {values:?}"
6823 );
6824 assert!(
6825 actual_sem.semantic_eq(&expected_sem),
6826 "case={case} family={family} ddof={ddof}: expected sem {expected_sem:?}, got {actual_sem:?} for {values:?}"
6827 );
6828 }
6829
6830 let numeric_all_missing = [Scalar::Null(NullKind::Null), Scalar::Float64(f64::NAN)];
6831 assert_reductions(
6832 usize::MAX,
6833 "numeric_all_missing",
6834 &numeric_all_missing,
6835 0,
6836 expected_numeric(&numeric_all_missing, 0),
6837 );
6838
6839 let td_all_missing = [Scalar::Timedelta64(i64::MIN), Scalar::Null(NullKind::NaN)];
6840 assert_reductions(
6841 usize::MAX - 1,
6842 "timedelta_all_missing",
6843 &td_all_missing,
6844 0,
6845 expected_timedelta(&td_all_missing, 0),
6846 );
6847
6848 let mut seed = 0x7a11_c0de_5eed_0421_u64;
6849 for case in 0..240 {
6850 let len = (next(&mut seed) % 83 + 1) as usize;
6851 let ddof = (next(&mut seed) % 4) as usize;
6852
6853 let mut numeric = Vec::with_capacity(len);
6854 numeric.push(Scalar::Float64(case as f64 / 13.0));
6855 for _ in 1..len {
6856 let raw = (next(&mut seed) % 20_001) as i64 - 10_000;
6857 numeric.push(match next(&mut seed) % 8 {
6858 0 => Scalar::Null(NullKind::Null),
6859 1 => Scalar::Null(NullKind::NaN),
6860 2 => Scalar::Float64(f64::NAN),
6861 3 => Scalar::Bool(raw & 1 == 0),
6862 4 => Scalar::Int64(raw % 251),
6863 5 => Scalar::Float64(raw as f64 / 73.0),
6864 6 => Scalar::Float64(0.0),
6865 _ => Scalar::Float64(-0.0),
6866 });
6867 }
6868 assert_reductions(
6869 case,
6870 "numeric",
6871 &numeric,
6872 ddof,
6873 expected_numeric(&numeric, ddof),
6874 );
6875
6876 let mut timedeltas = Vec::with_capacity(len);
6877 timedeltas.push(Scalar::Timedelta64(case as i64 - 120));
6878 for _ in 1..len {
6879 let raw = (next(&mut seed) % 20_001) as i64 - 10_000;
6880 timedeltas.push(match next(&mut seed) % 7 {
6881 0 => Scalar::Null(NullKind::Null),
6882 1 => Scalar::Timedelta64(i64::MIN),
6883 _ => Scalar::Timedelta64(raw),
6884 });
6885 }
6886 assert_reductions(
6887 case,
6888 "timedelta",
6889 &timedeltas,
6890 ddof,
6891 expected_timedelta(&timedeltas, ddof),
6892 );
6893 }
6894 }
6895
6896 #[test]
6897 fn nanvar_population() {
6898 let vals = vec![
6899 Scalar::Float64(2.0),
6900 Scalar::Float64(4.0),
6901 Scalar::Float64(4.0),
6902 Scalar::Float64(4.0),
6903 Scalar::Float64(5.0),
6904 Scalar::Float64(5.0),
6905 Scalar::Float64(7.0),
6906 Scalar::Float64(9.0),
6907 ];
6908 let var = super::nanvar(&vals, 0);
6909 assert!(matches!(var, Scalar::Float64(_)), "expected Float64");
6910 if let Scalar::Float64(v) = var {
6911 assert!((v - 4.0).abs() < 1e-10);
6912 }
6913 }
6914
6915 #[test]
6916 fn nanvar_sample_ddof1() {
6917 let vals = vec![
6918 Scalar::Float64(2.0),
6919 Scalar::Float64(4.0),
6920 Scalar::Float64(4.0),
6921 Scalar::Float64(4.0),
6922 Scalar::Float64(5.0),
6923 Scalar::Float64(5.0),
6924 Scalar::Float64(7.0),
6925 Scalar::Float64(9.0),
6926 ];
6927 let var = super::nanvar(&vals, 1);
6928 assert!(matches!(var, Scalar::Float64(_)), "expected Float64");
6929 if let Scalar::Float64(v) = var {
6930 assert!((v - 32.0 / 7.0).abs() < 1e-10);
6931 }
6932 }
6933
6934 #[test]
6935 fn nanvar_insufficient_values_returns_nan() {
6936 let vals = vec![Scalar::Float64(5.0)];
6937 assert!(super::nanvar(&vals, 1).is_missing());
6938 }
6939
6940 #[test]
6941 fn nanstd_is_sqrt_of_var() {
6942 let vals = vec![
6943 Scalar::Float64(2.0),
6944 Scalar::Float64(4.0),
6945 Scalar::Float64(4.0),
6946 Scalar::Float64(4.0),
6947 Scalar::Float64(5.0),
6948 Scalar::Float64(5.0),
6949 Scalar::Float64(7.0),
6950 Scalar::Float64(9.0),
6951 ];
6952 let std = super::nanstd(&vals, 0);
6953 assert!(matches!(std, Scalar::Float64(_)), "expected Float64");
6954 if let Scalar::Float64(v) = std {
6955 assert!((v - 2.0).abs() < 1e-10);
6956 }
6957 }
6958
6959 #[test]
6960 fn nanmedian_timedelta64_preserves_dtype_j8ntk() {
6961 let one_hour = 3_600 * 1_000_000_000_i64;
6964 let vals = vec![
6965 Scalar::Timedelta64(one_hour),
6966 Scalar::Timedelta64(2 * one_hour),
6967 Scalar::Timedelta64(3 * one_hour),
6968 ];
6969 assert_eq!(super::nanmedian(&vals), Scalar::Timedelta64(2 * one_hour));
6970 }
6971
6972 #[test]
6973 fn nanstd_timedelta64_preserves_dtype_j8ntk() {
6974 let one_hour: i64 = 3_600 * 1_000_000_000;
6978 let vals = vec![
6979 Scalar::Timedelta64(one_hour),
6980 Scalar::Timedelta64(2 * one_hour),
6981 Scalar::Timedelta64(3 * one_hour),
6982 ];
6983 let std = super::nanstd(&vals, 0);
6984 match std {
6985 Scalar::Timedelta64(ns) => {
6986 let expected = (2.0_f64 / 3.0).sqrt() * one_hour as f64;
6987 assert!(
6988 (ns as f64 - expected).abs() < 1e6,
6989 "expected ~{expected} ns, got {ns}"
6990 );
6991 }
6992 other => panic!("expected Timedelta64, got {other:?}"),
6993 }
6994 }
6995
6996 #[test]
6997 fn nanstd_nansem_timedelta64_insufficient_returns_nat_j8ntk() {
6998 let one_hour = 3_600 * 1_000_000_000_i64;
6999 let vals = vec![Scalar::Timedelta64(one_hour)];
7000 match super::nanstd(&vals, 1) {
7002 Scalar::Timedelta64(v) => assert_eq!(v, Timedelta::NAT),
7003 other => panic!("expected Timedelta64 NAT, got {other:?}"),
7004 }
7005 match super::nansem(&vals, 1) {
7006 Scalar::Timedelta64(v) => assert_eq!(v, Timedelta::NAT),
7007 other => panic!("expected Timedelta64 NAT, got {other:?}"),
7008 }
7009 }
7010
7011 #[test]
7012 fn nanops_with_mixed_types() {
7013 let vals = vec![
7014 Scalar::Bool(true),
7015 Scalar::Int64(3),
7016 Scalar::Float64(6.0),
7017 Scalar::Null(NullKind::Null),
7018 ];
7019 assert_eq!(super::nansum(&vals), Scalar::Float64(10.0));
7020 assert_eq!(super::nancount(&vals), Scalar::Int64(3));
7021 }
7022
7023 #[test]
7024 fn nanops_all_missing_returns_identity() {
7025 let vals = vec![Scalar::Null(NullKind::Null), Scalar::Float64(f64::NAN)];
7026 assert_eq!(super::nansum(&vals), Scalar::Float64(0.0));
7027 assert!(super::nanmean(&vals).is_missing());
7028 assert!(super::nanmedian(&vals).is_missing());
7029 assert!(super::nanvar(&vals, 0).is_missing());
7030 assert!(super::nanstd(&vals, 0).is_missing());
7031 }
7032
7033 #[test]
7036 fn timedelta_parse_simple_units() {
7037 use super::Timedelta;
7038 assert_eq!(Timedelta::parse("1d").unwrap(), Timedelta::NANOS_PER_DAY);
7039 assert_eq!(
7040 Timedelta::parse("2h").unwrap(),
7041 2 * Timedelta::NANOS_PER_HOUR
7042 );
7043 assert_eq!(
7044 Timedelta::parse("30m").unwrap(),
7045 30 * Timedelta::NANOS_PER_MIN
7046 );
7047 assert_eq!(
7048 Timedelta::parse("45s").unwrap(),
7049 45 * Timedelta::NANOS_PER_SEC
7050 );
7051 assert_eq!(
7052 Timedelta::parse("100ms").unwrap(),
7053 100 * Timedelta::NANOS_PER_MILLI
7054 );
7055 assert_eq!(
7056 Timedelta::parse("500us").unwrap(),
7057 500 * Timedelta::NANOS_PER_MICRO
7058 );
7059 assert_eq!(Timedelta::parse("1000ns").unwrap(), 1000);
7060 }
7061
7062 #[test]
7063 fn timedelta_parse_compound() {
7064 use super::Timedelta;
7065 let expected = Timedelta::NANOS_PER_DAY
7066 + 2 * Timedelta::NANOS_PER_HOUR
7067 + 30 * Timedelta::NANOS_PER_MIN;
7068 assert_eq!(Timedelta::parse("1d 2h 30m").unwrap(), expected);
7069 assert_eq!(Timedelta::parse("1d2h30m").unwrap(), expected);
7070 }
7071
7072 #[test]
7073 fn timedelta_parse_iso8601_matches_pandas_tdiso() {
7074 use super::Timedelta;
7075 assert_eq!(Timedelta::parse("P1DT2H3M4S").unwrap(), 93_784_000_000_000);
7077 assert_eq!(Timedelta::parse("PT1H").unwrap(), 3_600_000_000_000);
7078 assert_eq!(Timedelta::parse("PT1H30M").unwrap(), 5_400_000_000_000);
7079 assert_eq!(Timedelta::parse("P1D").unwrap(), 86_400_000_000_000);
7080 assert_eq!(Timedelta::parse("P2W").unwrap(), 1_209_600_000_000_000);
7081 assert_eq!(Timedelta::parse("PT0.5S").unwrap(), 500_000_000);
7082 assert_eq!(Timedelta::parse("P1M").unwrap(), 60_000_000_000);
7084 assert_eq!(Timedelta::parse("P1H").unwrap(), 3_600_000_000_000);
7085 assert_eq!(Timedelta::parse("PT1D").unwrap(), 86_400_000_000_000);
7086 assert_eq!(Timedelta::parse("P1D1H").unwrap(), 90_000_000_000_000);
7087 assert_eq!(Timedelta::parse("-P1DT2H").unwrap(), -93_600_000_000_000);
7088 assert!(Timedelta::parse("P1Y").is_err());
7090 assert!(Timedelta::parse("p1d").is_err());
7091 assert!(Timedelta::parse("P").is_err());
7092 assert!(Timedelta::parse("PT").is_err());
7093 }
7094
7095 #[test]
7096 fn timedelta_parse_time_format() {
7097 use super::Timedelta;
7098 let expected = Timedelta::NANOS_PER_HOUR
7099 + 30 * Timedelta::NANOS_PER_MIN
7100 + 45 * Timedelta::NANOS_PER_SEC;
7101 assert_eq!(Timedelta::parse("01:30:45").unwrap(), expected);
7102 }
7103
7104 #[test]
7105 fn timedelta_parse_time_fraction_rejects_unicode_without_panic() {
7106 use super::{Timedelta, TimedeltaError};
7107 let err = Timedelta::parse("00:00:00.\u{00e9}\u{00e9}\u{00e9}\u{00e9}\u{00e9}")
7108 .expect_err("non-ASCII fractional seconds must reject");
7109 assert!(matches!(err, TimedeltaError::InvalidFormat(_)));
7110 }
7111
7112 #[test]
7113 fn timedelta_parse_time_format_rejects_overflow_without_panic() {
7114 use super::{Timedelta, TimedeltaError};
7115 let err = Timedelta::parse("9223372036854775807:00")
7116 .expect_err("oversized hour component must reject");
7117 assert!(matches!(err, TimedeltaError::InvalidFormat(_)));
7118 }
7119
7120 #[test]
7121 fn timedelta_parse_rejects_huge_value_overflow_zw3mg() {
7122 use super::{Timedelta, TimedeltaError};
7129 let huge = format!("{} days", "9".repeat(18));
7130 assert!(matches!(
7131 Timedelta::parse(&huge).expect_err("9...(18 9s) days must overflow"),
7132 TimedeltaError::Overflow
7133 ));
7134 }
7135
7136 #[test]
7137 fn timedelta_parse_nat() {
7138 use super::Timedelta;
7139 assert_eq!(Timedelta::parse("NaT").unwrap(), Timedelta::NAT);
7140 assert_eq!(Timedelta::parse("nat").unwrap(), Timedelta::NAT);
7141 }
7142
7143 #[test]
7144 fn timedelta_parse_negative() {
7145 use super::Timedelta;
7146 assert_eq!(Timedelta::parse("-1d").unwrap(), -Timedelta::NANOS_PER_DAY);
7147 }
7148
7149 #[test]
7150 fn timedelta_components() {
7151 use super::Timedelta;
7152 let nanos = Timedelta::NANOS_PER_DAY
7153 + Timedelta::NANOS_PER_HOUR
7154 + Timedelta::NANOS_PER_MIN
7155 + Timedelta::NANOS_PER_SEC
7156 + Timedelta::NANOS_PER_MILLI
7157 + 2 * Timedelta::NANOS_PER_MICRO
7158 + 3;
7159 let comp = Timedelta::components(nanos);
7160 assert_eq!(comp.days, 1);
7161 assert_eq!(comp.hours, 1);
7162 assert_eq!(comp.minutes, 1);
7163 assert_eq!(comp.seconds, 1);
7164 assert_eq!(comp.milliseconds, 1);
7165 assert_eq!(comp.microseconds, 2);
7166 assert_eq!(comp.nanoseconds, 3);
7167 }
7168
7169 #[test]
7170 fn timedelta_negative_components_floor_div() {
7171 use super::Timedelta;
7172 let neg_1s = -Timedelta::NANOS_PER_SEC;
7175 assert_eq!(Timedelta::days(neg_1s), -1);
7176 assert_eq!(Timedelta::seconds(neg_1s), 86399);
7177 assert_eq!(Timedelta::microseconds(neg_1s), 0);
7178 assert_eq!(Timedelta::nanoseconds(neg_1s), 0);
7179 let comp = Timedelta::components(neg_1s);
7180 assert_eq!(
7181 (
7182 comp.days,
7183 comp.hours,
7184 comp.minutes,
7185 comp.seconds,
7186 comp.milliseconds,
7187 comp.microseconds,
7188 comp.nanoseconds
7189 ),
7190 (-1, 23, 59, 59, 0, 0, 0)
7191 );
7192
7193 let neg = -86_401 * Timedelta::NANOS_PER_SEC;
7195 assert_eq!(Timedelta::days(neg), -2);
7196 assert_eq!(Timedelta::seconds(neg), 86399);
7197 }
7198
7199 #[test]
7200 fn timedelta_total_seconds() {
7201 use super::Timedelta;
7202 let nanos = 90_000_000_000i64; assert!((Timedelta::total_seconds(nanos) - 90.0).abs() < 1e-9);
7204 assert!(Timedelta::total_seconds(Timedelta::NAT).is_nan());
7205 }
7206
7207 #[test]
7208 fn timedelta_format_basic() {
7209 use super::Timedelta;
7210 assert_eq!(Timedelta::format(Timedelta::NAT), "NaT");
7211 assert_eq!(
7212 Timedelta::format(Timedelta::NANOS_PER_DAY),
7213 "1 days 00:00:00"
7214 );
7215 assert_eq!(
7216 Timedelta::format(Timedelta::NANOS_PER_DAY + 2 * Timedelta::NANOS_PER_HOUR),
7217 "1 days 02:00:00"
7218 );
7219 }
7220
7221 #[test]
7222 fn timedelta_format_subsecond_matches_pandas() {
7223 use super::Timedelta;
7224 assert_eq!(
7228 Timedelta::format(1_500_000_000), "0 days 00:00:01.500000"
7230 );
7231 assert_eq!(
7232 Timedelta::format(1_000_000), "0 days 00:00:00.001000"
7234 );
7235 assert_eq!(
7236 Timedelta::format(123_456_000), "0 days 00:00:00.123456"
7238 );
7239 assert_eq!(
7241 Timedelta::format(500), "0 days 00:00:00.000000500"
7243 );
7244 assert_eq!(Timedelta::format(123_456_789), "0 days 00:00:00.123456789");
7245 }
7246
7247 #[test]
7248 fn timedelta_format_negative_uses_python_borrow_form() {
7249 use super::Timedelta;
7250 assert_eq!(Timedelta::format(-1_000_000_000), "-1 days +23:59:59");
7254 assert_eq!(
7255 Timedelta::format(-Timedelta::NANOS_PER_DAY),
7256 "-1 days +00:00:00"
7257 );
7258 assert_eq!(
7259 Timedelta::format(-25 * Timedelta::NANOS_PER_HOUR),
7260 "-2 days +23:00:00"
7261 );
7262 assert_eq!(
7263 Timedelta::format(-1_500_000_000),
7264 "-1 days +23:59:58.500000"
7265 );
7266 assert_eq!(Timedelta::format(-500), "-1 days +23:59:59.999999500");
7267 assert_eq!(Timedelta::format(-1), "-1 days +23:59:59.999999999");
7268 }
7269
7270 #[test]
7271 fn timedelta_isoformat_basic() {
7272 use super::Timedelta;
7273 assert_eq!(Timedelta::isoformat(Timedelta::NAT), "NaT");
7274 assert_eq!(Timedelta::isoformat(0), "P0DT0H0M0S");
7275 assert_eq!(Timedelta::isoformat(Timedelta::NANOS_PER_DAY), "P1DT0H0M0S");
7276 assert_eq!(
7277 Timedelta::isoformat(
7278 Timedelta::NANOS_PER_DAY
7279 + 2 * Timedelta::NANOS_PER_HOUR
7280 + 30 * Timedelta::NANOS_PER_MIN
7281 + 45 * Timedelta::NANOS_PER_SEC
7282 ),
7283 "P1DT2H30M45S"
7284 );
7285 assert_eq!(
7286 Timedelta::isoformat(Timedelta::NANOS_PER_SEC + 500_000_000),
7287 "P0DT0H0M1.5S"
7288 );
7289 assert_eq!(
7290 Timedelta::isoformat(-(Timedelta::NANOS_PER_DAY + Timedelta::NANOS_PER_HOUR)),
7291 "-P1DT1H0M0S"
7292 );
7293 }
7294
7295 #[test]
7296 fn timedelta_floor_ceil_round() {
7297 use super::Timedelta;
7298 let nanos = Timedelta::NANOS_PER_HOUR + 30 * Timedelta::NANOS_PER_MIN;
7299
7300 assert_eq!(Timedelta::floor(nanos, "h"), Timedelta::NANOS_PER_HOUR);
7302 assert_eq!(Timedelta::floor(nanos, "d"), 0);
7303
7304 assert_eq!(Timedelta::ceil(nanos, "h"), 2 * Timedelta::NANOS_PER_HOUR);
7306 assert_eq!(Timedelta::ceil(nanos, "d"), Timedelta::NANOS_PER_DAY);
7307
7308 assert_eq!(Timedelta::round(nanos, "h"), 2 * Timedelta::NANOS_PER_HOUR);
7310
7311 assert_eq!(Timedelta::floor(Timedelta::NAT, "h"), Timedelta::NAT);
7313 assert_eq!(Timedelta::ceil(Timedelta::NAT, "h"), Timedelta::NAT);
7314 assert_eq!(Timedelta::round(Timedelta::NAT, "h"), Timedelta::NAT);
7315
7316 assert_eq!(Timedelta::floor(nanos, "invalid"), Timedelta::NAT);
7318 }
7319
7320 #[test]
7321 fn timedelta_floor_ceil_negative_use_euclidean_rounding_t79yh() {
7322 use super::Timedelta;
7323
7324 assert_eq!(
7325 Timedelta::floor(-1, "s"),
7326 -Timedelta::NANOS_PER_SEC,
7327 "floor(-1ns, 1s)"
7328 );
7329 assert_eq!(Timedelta::ceil(-1, "s"), 0, "ceil(-1ns, 1s)");
7330 assert_eq!(
7331 Timedelta::floor(-1_500_000_000, "s"),
7332 -2 * Timedelta::NANOS_PER_SEC
7333 );
7334 assert_eq!(
7335 Timedelta::ceil(-1_500_000_000, "s"),
7336 -Timedelta::NANOS_PER_SEC
7337 );
7338 assert_eq!(
7339 Timedelta::floor(-Timedelta::NANOS_PER_SEC, "s"),
7340 -Timedelta::NANOS_PER_SEC
7341 );
7342 assert_eq!(
7343 Timedelta::ceil(-Timedelta::NANOS_PER_SEC, "s"),
7344 -Timedelta::NANOS_PER_SEC
7345 );
7346 assert_eq!(
7347 Timedelta::floor(1_500_000_000, "s"),
7348 Timedelta::NANOS_PER_SEC
7349 );
7350 assert_eq!(
7351 Timedelta::ceil(1_500_000_000, "s"),
7352 2 * Timedelta::NANOS_PER_SEC
7353 );
7354 }
7355
7356 #[test]
7357 fn timedelta_scalar_dtype() {
7358 let td = Scalar::Timedelta64(86_400_000_000_000);
7359 assert_eq!(td.dtype(), DType::Timedelta64);
7360 }
7361
7362 #[test]
7363 fn timedelta_scalar_is_missing() {
7364 use super::Timedelta;
7365 let valid = Scalar::Timedelta64(1000);
7366 let nat = Scalar::Timedelta64(Timedelta::NAT);
7367 assert!(!valid.is_missing());
7368 assert!(nat.is_missing());
7369 }
7370
7371 #[test]
7372 fn dtype_utf8_deserializes_legacy_aliases() {
7373 let dtype: DType = serde_json::from_str("\"str\"").unwrap();
7374 assert_eq!(dtype, DType::Utf8);
7375
7376 let dtype: DType = serde_json::from_str("\"string\"").unwrap();
7377 assert_eq!(dtype, DType::Utf8);
7378 }
7379
7380 #[test]
7381 fn scalar_utf8_deserializes_legacy_aliases() {
7382 let scalar: Scalar = serde_json::from_str(r#"{"kind":"str","value":"x"}"#).unwrap();
7383 assert_eq!(scalar, Scalar::Utf8("x".to_owned()));
7384
7385 let scalar: Scalar = serde_json::from_str(r#"{"kind":"string","value":"y"}"#).unwrap();
7386 assert_eq!(scalar, Scalar::Utf8("y".to_owned()));
7387 }
7388
7389 #[test]
7390 fn nancumsum_skips_nulls_and_accumulates() {
7391 let values = vec![
7392 Scalar::Float64(1.0),
7393 Scalar::Null(NullKind::NaN),
7394 Scalar::Float64(2.0),
7395 Scalar::Float64(3.0),
7396 ];
7397 let out = super::nancumsum(&values);
7398 assert!(matches!(out[0], Scalar::Float64(v) if (v - 1.0).abs() < 1e-9));
7399 assert!(out[1].is_missing());
7400 assert!(matches!(out[2], Scalar::Float64(v) if (v - 3.0).abs() < 1e-9));
7401 assert!(matches!(out[3], Scalar::Float64(v) if (v - 6.0).abs() < 1e-9));
7402 }
7403
7404 #[test]
7405 fn nancumprod_skips_nulls_and_multiplies() {
7406 let values = vec![
7407 Scalar::Float64(2.0),
7408 Scalar::Null(NullKind::NaN),
7409 Scalar::Float64(3.0),
7410 Scalar::Float64(4.0),
7411 ];
7412 let out = super::nancumprod(&values);
7413 assert!(matches!(out[0], Scalar::Float64(v) if (v - 2.0).abs() < 1e-9));
7414 assert!(out[1].is_missing());
7415 assert!(matches!(out[2], Scalar::Float64(v) if (v - 6.0).abs() < 1e-9));
7416 assert!(matches!(out[3], Scalar::Float64(v) if (v - 24.0).abs() < 1e-9));
7417 }
7418
7419 #[test]
7420 fn nancummax_tracks_running_max() {
7421 let values = vec![
7422 Scalar::Float64(1.0),
7423 Scalar::Float64(3.0),
7424 Scalar::Null(NullKind::NaN),
7425 Scalar::Float64(2.0),
7426 Scalar::Float64(5.0),
7427 ];
7428 let out = super::nancummax(&values);
7429 assert_eq!(out[0], Scalar::Float64(1.0));
7430 assert_eq!(out[1], Scalar::Float64(3.0));
7431 assert!(out[2].is_missing());
7432 assert_eq!(out[3], Scalar::Float64(3.0));
7433 assert_eq!(out[4], Scalar::Float64(5.0));
7434 }
7435
7436 #[test]
7437 fn nancummin_tracks_running_min() {
7438 let values = vec![
7439 Scalar::Float64(5.0),
7440 Scalar::Float64(3.0),
7441 Scalar::Null(NullKind::NaN),
7442 Scalar::Float64(4.0),
7443 Scalar::Float64(1.0),
7444 ];
7445 let out = super::nancummin(&values);
7446 assert_eq!(out[0], Scalar::Float64(5.0));
7447 assert_eq!(out[1], Scalar::Float64(3.0));
7448 assert!(out[2].is_missing());
7449 assert_eq!(out[3], Scalar::Float64(3.0));
7450 assert_eq!(out[4], Scalar::Float64(1.0));
7451 }
7452
7453 #[test]
7454 fn nancumsum_timedelta64_preserves_dtype_x0x91() {
7455 let one_hour = 3_600 * 1_000_000_000_i64;
7458 let values = vec![
7459 Scalar::Timedelta64(one_hour),
7460 Scalar::Timedelta64(2 * one_hour),
7461 Scalar::Timedelta64(3 * one_hour),
7462 ];
7463 let out = super::nancumsum(&values);
7464 assert_eq!(out[0], Scalar::Timedelta64(one_hour));
7465 assert_eq!(out[1], Scalar::Timedelta64(3 * one_hour));
7466 assert_eq!(out[2], Scalar::Timedelta64(6 * one_hour));
7467 }
7468
7469 #[test]
7470 fn nancummax_nancummin_timedelta64_preserves_dtype_x0x91() {
7471 let one_hour = 3_600 * 1_000_000_000_i64;
7472 let values = vec![
7473 Scalar::Timedelta64(2 * one_hour),
7474 Scalar::Timedelta64(5 * one_hour),
7475 Scalar::Timedelta64(one_hour),
7476 Scalar::Timedelta64(3 * one_hour),
7477 ];
7478 let mx = super::nancummax(&values);
7479 assert_eq!(mx[0], Scalar::Timedelta64(2 * one_hour));
7480 assert_eq!(mx[1], Scalar::Timedelta64(5 * one_hour));
7481 assert_eq!(mx[2], Scalar::Timedelta64(5 * one_hour));
7482 assert_eq!(mx[3], Scalar::Timedelta64(5 * one_hour));
7483
7484 let mn = super::nancummin(&values);
7485 assert_eq!(mn[0], Scalar::Timedelta64(2 * one_hour));
7486 assert_eq!(mn[1], Scalar::Timedelta64(2 * one_hour));
7487 assert_eq!(mn[2], Scalar::Timedelta64(one_hour));
7488 assert_eq!(mn[3], Scalar::Timedelta64(one_hour));
7489 }
7490
7491 #[test]
7492 fn nancumulative_timedelta64_skips_nat_x0x91() {
7493 let one_hour = 3_600 * 1_000_000_000_i64;
7494 let values = vec![
7495 Scalar::Timedelta64(one_hour),
7496 Scalar::Timedelta64(Timedelta::NAT),
7497 Scalar::Timedelta64(2 * one_hour),
7498 ];
7499 let cs = super::nancumsum(&values);
7500 assert_eq!(cs[0], Scalar::Timedelta64(one_hour));
7501 assert!(cs[1].is_missing());
7502 assert_eq!(cs[2], Scalar::Timedelta64(3 * one_hour));
7503 }
7504
7505 #[test]
7506 fn nancumulative_matches_numeric_and_timedelta_oracle_k63oz() {
7507 fn next(seed: &mut u64) -> u64 {
7510 *seed = seed
7511 .wrapping_mul(3202034522624059733)
7512 .wrapping_add(4354685564936845319);
7513 *seed
7514 }
7515
7516 fn assert_vec(case: usize, family: &str, op: &str, actual: &[Scalar], expected: &[Scalar]) {
7517 assert_eq!(
7518 actual.len(),
7519 expected.len(),
7520 "case={case} family={family} op={op}: length mismatch"
7521 );
7522 for (pos, (actual, expected)) in actual.iter().zip(expected.iter()).enumerate() {
7523 assert!(
7524 actual.semantic_eq(expected),
7525 "case={case} family={family} op={op} pos={pos}: expected {expected:?}, got {actual:?}"
7526 );
7527 }
7528 }
7529
7530 fn expected_numeric(
7531 values: &[Scalar],
7532 ) -> (Vec<Scalar>, Vec<Scalar>, Vec<Scalar>, Vec<Scalar>) {
7533 let mut sum = Vec::with_capacity(values.len());
7534 let mut prod = Vec::with_capacity(values.len());
7535 let mut max = Vec::with_capacity(values.len());
7536 let mut min = Vec::with_capacity(values.len());
7537 let mut running_sum = 0.0_f64;
7538 let mut running_prod = 1.0_f64;
7539 let mut running_max: Option<f64> = None;
7540 let mut running_min: Option<f64> = None;
7541
7542 for value in values {
7543 if value.is_missing() {
7544 sum.push(Scalar::Null(NullKind::NaN));
7545 prod.push(Scalar::Null(NullKind::NaN));
7546 max.push(Scalar::Null(NullKind::NaN));
7547 min.push(Scalar::Null(NullKind::NaN));
7548 continue;
7549 }
7550 let Ok(value) = value.to_f64() else {
7551 sum.push(Scalar::Null(NullKind::NaN));
7552 prod.push(Scalar::Null(NullKind::NaN));
7553 max.push(Scalar::Null(NullKind::NaN));
7554 min.push(Scalar::Null(NullKind::NaN));
7555 continue;
7556 };
7557 if value.is_nan() {
7558 sum.push(Scalar::Null(NullKind::NaN));
7559 prod.push(Scalar::Null(NullKind::NaN));
7560 max.push(Scalar::Null(NullKind::NaN));
7561 min.push(Scalar::Null(NullKind::NaN));
7562 continue;
7563 }
7564 running_sum += value;
7565 running_prod *= value;
7566 running_max = Some(running_max.map_or(value, |current| current.max(value)));
7567 running_min = Some(running_min.map_or(value, |current| current.min(value)));
7568 sum.push(Scalar::Float64(running_sum));
7569 prod.push(Scalar::Float64(running_prod));
7570 max.push(Scalar::Float64(running_max.expect("initialized")));
7571 min.push(Scalar::Float64(running_min.expect("initialized")));
7572 }
7573
7574 (sum, prod, max, min)
7575 }
7576
7577 fn expected_timedelta(values: &[Scalar]) -> (Vec<Scalar>, Vec<Scalar>, Vec<Scalar>) {
7578 let mut sum = Vec::with_capacity(values.len());
7579 let mut max = Vec::with_capacity(values.len());
7580 let mut min = Vec::with_capacity(values.len());
7581 let mut running_sum = 0_i128;
7582 let mut running_max: Option<i64> = None;
7583 let mut running_min: Option<i64> = None;
7584
7585 for value in values {
7586 if value.is_missing() {
7587 sum.push(Scalar::Null(NullKind::NaT));
7588 max.push(Scalar::Null(NullKind::NaT));
7589 min.push(Scalar::Null(NullKind::NaT));
7590 continue;
7591 }
7592 let Scalar::Timedelta64(ns) = value else {
7593 sum.push(Scalar::Null(NullKind::NaT));
7594 max.push(Scalar::Null(NullKind::NaT));
7595 min.push(Scalar::Null(NullKind::NaT));
7596 continue;
7597 };
7598 running_sum = running_sum.saturating_add(i128::from(*ns));
7599 let clamped = running_sum.clamp(i128::from(i64::MIN), i128::from(i64::MAX));
7600 running_max = Some(running_max.map_or(*ns, |current| current.max(*ns)));
7601 running_min = Some(running_min.map_or(*ns, |current| current.min(*ns)));
7602 sum.push(Scalar::Timedelta64(clamped as i64));
7603 max.push(Scalar::Timedelta64(running_max.expect("initialized")));
7604 min.push(Scalar::Timedelta64(running_min.expect("initialized")));
7605 }
7606
7607 (sum, max, min)
7608 }
7609
7610 let mut seed = 0xc0de_c63a_5eed_0421_u64;
7611 for case in 0..260 {
7612 let len = (next(&mut seed) % 89 + 1) as usize;
7613
7614 let mut numeric = Vec::with_capacity(len);
7615 numeric.push(Scalar::Int64(case as i64 - 130));
7616 for _ in 1..len {
7617 let raw = (next(&mut seed) % 20_001) as i64 - 10_000;
7618 numeric.push(match next(&mut seed) % 8 {
7619 0 => Scalar::Null(NullKind::Null),
7620 1 => Scalar::Null(NullKind::NaN),
7621 2 => Scalar::Float64(f64::NAN),
7622 3 => Scalar::Bool(raw & 1 == 0),
7623 4 => Scalar::Int64(raw % 251),
7624 5 => Scalar::Float64(raw as f64 / 79.0),
7625 6 => Scalar::Float64(0.0),
7626 _ => Scalar::Float64(-0.0),
7627 });
7628 }
7629 let (sum, prod, max, min) = expected_numeric(&numeric);
7630 assert_vec(case, "numeric", "cumsum", &super::nancumsum(&numeric), &sum);
7631 assert_vec(
7632 case,
7633 "numeric",
7634 "cumprod",
7635 &super::nancumprod(&numeric),
7636 &prod,
7637 );
7638 assert_vec(case, "numeric", "cummax", &super::nancummax(&numeric), &max);
7639 assert_vec(case, "numeric", "cummin", &super::nancummin(&numeric), &min);
7640
7641 let mut timedeltas = Vec::with_capacity(len);
7642 timedeltas.push(Scalar::Timedelta64(case as i64 - 130));
7643 for _ in 1..len {
7644 let raw = (next(&mut seed) % 20_001) as i64 - 10_000;
7645 timedeltas.push(match next(&mut seed) % 7 {
7646 0 => Scalar::Null(NullKind::Null),
7647 1 => Scalar::Timedelta64(i64::MIN),
7648 _ => Scalar::Timedelta64(raw),
7649 });
7650 }
7651 let (td_sum, td_max, td_min) = expected_timedelta(&timedeltas);
7652 assert_vec(
7653 case,
7654 "timedelta",
7655 "cumsum",
7656 &super::nancumsum(&timedeltas),
7657 &td_sum,
7658 );
7659 assert_vec(
7660 case,
7661 "timedelta",
7662 "cummax",
7663 &super::nancummax(&timedeltas),
7664 &td_max,
7665 );
7666 assert_vec(
7667 case,
7668 "timedelta",
7669 "cummin",
7670 &super::nancummin(&timedeltas),
7671 &td_min,
7672 );
7673 }
7674 }
7675
7676 #[test]
7677 fn nanquantile_linear_interpolation_matches_numpy() {
7678 let values = vec![
7679 Scalar::Float64(1.0),
7680 Scalar::Float64(2.0),
7681 Scalar::Float64(3.0),
7682 Scalar::Float64(4.0),
7683 Scalar::Float64(5.0),
7684 ];
7685 let q = super::nanquantile(&values, 0.5);
7687 assert!(matches!(q, Scalar::Float64(v) if (v - 3.0).abs() < 1e-9));
7688 let q25 = super::nanquantile(&values, 0.25);
7690 assert!(matches!(q25, Scalar::Float64(v) if (v - 2.0).abs() < 1e-9));
7691 }
7692
7693 #[test]
7694 fn nanquantile_ignores_nulls() {
7695 let values = vec![
7696 Scalar::Float64(1.0),
7697 Scalar::Null(NullKind::NaN),
7698 Scalar::Float64(3.0),
7699 ];
7700 let q = super::nanquantile(&values, 0.5);
7701 assert!(matches!(q, Scalar::Float64(v) if (v - 2.0).abs() < 1e-9));
7702 }
7703
7704 #[test]
7705 fn nanquantile_empty_and_out_of_range_yield_null() {
7706 assert!(super::nanquantile(&[], 0.5).is_missing());
7707 assert!(super::nanquantile(&[Scalar::Float64(1.0)], 1.5).is_missing());
7708 assert!(super::nanquantile(&[Scalar::Float64(1.0)], -0.1).is_missing());
7709 }
7710
7711 #[test]
7712 fn nanquantile_timedelta64_preserves_dtype_5djk7() {
7713 let one_hour: i64 = 3_600 * 1_000_000_000;
7716 let vals = vec![
7717 Scalar::Timedelta64(one_hour),
7718 Scalar::Timedelta64(2 * one_hour),
7719 Scalar::Timedelta64(3 * one_hour),
7720 Scalar::Timedelta64(4 * one_hour),
7721 Scalar::Timedelta64(5 * one_hour),
7722 ];
7723 assert_eq!(
7724 super::nanquantile(&vals, 0.5),
7725 Scalar::Timedelta64(3 * one_hour)
7726 );
7727 assert_eq!(
7728 super::nanquantile(&vals, 0.0),
7729 Scalar::Timedelta64(one_hour)
7730 );
7731 assert_eq!(
7732 super::nanquantile(&vals, 1.0),
7733 Scalar::Timedelta64(5 * one_hour)
7734 );
7735 }
7736
7737 #[test]
7738 fn nanquantile_timedelta64_linear_interpolation_5djk7() {
7739 let one_hour: i64 = 3_600 * 1_000_000_000;
7740 let vals = vec![
7741 Scalar::Timedelta64(one_hour),
7742 Scalar::Timedelta64(3 * one_hour),
7743 ];
7744 assert_eq!(
7746 super::nanquantile(&vals, 0.5),
7747 Scalar::Timedelta64(2 * one_hour)
7748 );
7749 }
7750
7751 #[test]
7752 fn nanquantile_matches_numeric_and_timedelta_oracle_ecb7r() {
7753 fn next(seed: &mut u64) -> u64 {
7756 *seed = seed
7757 .wrapping_mul(3202034522624059733)
7758 .wrapping_add(4354685564936845319);
7759 *seed
7760 }
7761
7762 fn interpolated(sorted: &[f64], q: f64) -> f64 {
7763 if sorted.len() == 1 {
7764 return sorted[0];
7765 }
7766 let pos = q * (sorted.len() - 1) as f64;
7767 let lo = pos.floor() as usize;
7768 let hi = pos.ceil() as usize;
7769 if lo == hi {
7770 sorted[lo]
7771 } else {
7772 let weight = pos - lo as f64;
7773 sorted[lo] + (sorted[hi] - sorted[lo]) * weight
7774 }
7775 }
7776
7777 fn expected_numeric(values: &[Scalar], q: f64) -> Scalar {
7778 if !(0.0..=1.0).contains(&q) {
7779 return Scalar::Null(NullKind::NaN);
7780 }
7781 let mut samples = values
7782 .iter()
7783 .filter(|value| !value.is_missing())
7784 .filter_map(|value| value.to_f64().ok())
7785 .collect::<Vec<_>>();
7786 if samples.is_empty() {
7787 return Scalar::Null(NullKind::NaN);
7788 }
7789 samples.sort_by(|left, right| left.partial_cmp(right).expect("finite values"));
7790 Scalar::Float64(interpolated(&samples, q))
7791 }
7792
7793 fn expected_timedelta(values: &[Scalar], q: f64) -> Scalar {
7794 if !(0.0..=1.0).contains(&q) {
7795 return Scalar::Null(NullKind::NaN);
7796 }
7797 let mut samples = values
7798 .iter()
7799 .filter_map(|value| match value {
7800 Scalar::Timedelta64(ns) if !value.is_missing() => Some(*ns as f64),
7801 _ => None,
7802 })
7803 .collect::<Vec<_>>();
7804 if samples.is_empty() {
7805 return Scalar::Null(NullKind::NaN);
7806 }
7807 samples.sort_by(|left, right| left.partial_cmp(right).expect("finite values"));
7808 Scalar::Timedelta64(interpolated(&samples, q) as i64)
7809 }
7810
7811 fn assert_quantile(case: usize, family: &str, values: &[Scalar], q: f64, expected: Scalar) {
7812 let actual = super::nanquantile(values, q);
7813 assert!(
7814 actual.semantic_eq(&expected),
7815 "case={case} family={family} q={q}: expected {expected:?}, got {actual:?} for {values:?}"
7816 );
7817 }
7818
7819 let numeric_all_missing = [Scalar::Null(NullKind::Null), Scalar::Float64(f64::NAN)];
7820 assert_quantile(
7821 usize::MAX,
7822 "numeric_all_missing",
7823 &numeric_all_missing,
7824 0.5,
7825 expected_numeric(&numeric_all_missing, 0.5),
7826 );
7827 assert_quantile(
7828 usize::MAX - 1,
7829 "numeric_out_of_range",
7830 &[Scalar::Float64(1.0), Scalar::Float64(2.0)],
7831 1.25,
7832 Scalar::Null(NullKind::NaN),
7833 );
7834
7835 let td_all_missing = [Scalar::Timedelta64(i64::MIN), Scalar::Null(NullKind::NaN)];
7836 assert_quantile(
7837 usize::MAX - 2,
7838 "timedelta_all_missing",
7839 &td_all_missing,
7840 0.5,
7841 expected_timedelta(&td_all_missing, 0.5),
7842 );
7843 assert_quantile(
7844 usize::MAX - 3,
7845 "timedelta_out_of_range",
7846 &[Scalar::Timedelta64(1), Scalar::Timedelta64(2)],
7847 -0.25,
7848 Scalar::Null(NullKind::NaN),
7849 );
7850
7851 let mut seed = 0x4a17_1e5e_0b5e_a11d_u64;
7852 for case in 0..260 {
7853 let len = (next(&mut seed) % 83 + 1) as usize;
7854 let q = match next(&mut seed) % 8 {
7855 0 => 0.0,
7856 1 => 0.25,
7857 2 => 0.5,
7858 3 => 0.75,
7859 4 => 1.0,
7860 _ => (next(&mut seed) % 1_001) as f64 / 1_000.0,
7861 };
7862
7863 let mut numeric = Vec::with_capacity(len);
7864 numeric.push(Scalar::Int64(case as i64 - 130));
7865 for _ in 1..len {
7866 let raw = (next(&mut seed) % 20_001) as i64 - 10_000;
7867 numeric.push(match next(&mut seed) % 8 {
7868 0 => Scalar::Null(NullKind::Null),
7869 1 => Scalar::Null(NullKind::NaN),
7870 2 => Scalar::Float64(f64::NAN),
7871 3 => Scalar::Bool(raw & 1 == 0),
7872 4 => Scalar::Int64(raw % 251),
7873 5 => Scalar::Float64(raw as f64 / 67.0),
7874 6 => Scalar::Float64(0.0),
7875 _ => Scalar::Float64(-0.0),
7876 });
7877 }
7878 assert_quantile(case, "numeric", &numeric, q, expected_numeric(&numeric, q));
7879
7880 let mut timedeltas = Vec::with_capacity(len);
7881 timedeltas.push(Scalar::Timedelta64(case as i64 - 130));
7882 for _ in 1..len {
7883 let raw = (next(&mut seed) % 20_001) as i64 - 10_000;
7884 timedeltas.push(match next(&mut seed) % 7 {
7885 0 => Scalar::Null(NullKind::Null),
7886 1 => Scalar::Timedelta64(i64::MIN),
7887 _ => Scalar::Timedelta64(raw),
7888 });
7889 }
7890 assert_quantile(
7891 case,
7892 "timedelta",
7893 &timedeltas,
7894 q,
7895 expected_timedelta(&timedeltas, q),
7896 );
7897 }
7898 }
7899
7900 #[test]
7901 fn nanargmax_returns_first_position() {
7902 let values = vec![
7903 Scalar::Float64(1.0),
7904 Scalar::Null(NullKind::NaN),
7905 Scalar::Float64(4.0),
7906 Scalar::Float64(4.0),
7907 Scalar::Float64(2.0),
7908 ];
7909 assert_eq!(super::nanargmax(&values), Some(2));
7910 }
7911
7912 #[test]
7913 fn nanargmin_returns_first_position() {
7914 let values = vec![
7915 Scalar::Float64(3.0),
7916 Scalar::Null(NullKind::NaN),
7917 Scalar::Float64(1.0),
7918 Scalar::Float64(1.0),
7919 ];
7920 assert_eq!(super::nanargmin(&values), Some(2));
7921 }
7922
7923 #[test]
7924 fn nanargmax_all_missing_returns_none() {
7925 let values = vec![Scalar::Null(NullKind::NaN), Scalar::Null(NullKind::Null)];
7926 assert_eq!(super::nanargmax(&values), None);
7927 assert_eq!(super::nanargmin(&values), None);
7928 }
7929
7930 #[test]
7931 fn nanargmax_nanargmin_match_numeric_and_timedelta_oracle_unkj6() {
7932 fn next(seed: &mut u64) -> u64 {
7935 *seed = seed
7936 .wrapping_mul(2862933555777941757)
7937 .wrapping_add(3037000493);
7938 *seed
7939 }
7940
7941 fn expected_numeric(values: &[Scalar], find_max: bool) -> Option<usize> {
7942 let mut best: Option<(usize, f64)> = None;
7943 for (idx, value) in values.iter().enumerate() {
7944 if value.is_missing() {
7945 continue;
7946 }
7947 let Ok(value) = value.to_f64() else {
7948 continue;
7949 };
7950 if value.is_nan() {
7951 continue;
7952 }
7953 match best {
7954 None => best = Some((idx, value)),
7955 Some((_, current))
7956 if (find_max && value > current) || (!find_max && value < current) =>
7957 {
7958 best = Some((idx, value));
7959 }
7960 _ => {}
7961 }
7962 }
7963 best.map(|(idx, _)| idx)
7964 }
7965
7966 fn expected_timedelta(values: &[Scalar], find_max: bool) -> Option<usize> {
7967 let mut best: Option<(usize, i64)> = None;
7968 for (idx, value) in values.iter().enumerate() {
7969 if value.is_missing() {
7970 continue;
7971 }
7972 let Scalar::Timedelta64(ns) = value else {
7973 continue;
7974 };
7975 match best {
7976 None => best = Some((idx, *ns)),
7977 Some((_, current))
7978 if (find_max && *ns > current) || (!find_max && *ns < current) =>
7979 {
7980 best = Some((idx, *ns));
7981 }
7982 _ => {}
7983 }
7984 }
7985 best.map(|(idx, _)| idx)
7986 }
7987
7988 fn assert_args(
7989 case: usize,
7990 family: &str,
7991 values: &[Scalar],
7992 expected_min: Option<usize>,
7993 expected_max: Option<usize>,
7994 ) {
7995 assert_eq!(
7996 super::nanargmin(values),
7997 expected_min,
7998 "case={case} family={family}: nanargmin mismatch for {values:?}"
7999 );
8000 assert_eq!(
8001 super::nanargmax(values),
8002 expected_max,
8003 "case={case} family={family}: nanargmax mismatch for {values:?}"
8004 );
8005 }
8006
8007 let all_missing = [Scalar::Null(NullKind::Null), Scalar::Float64(f64::NAN)];
8008 assert_args(usize::MAX, "numeric_all_missing", &all_missing, None, None);
8009 let td_all_missing = [Scalar::Timedelta64(i64::MIN), Scalar::Null(NullKind::NaN)];
8010 assert_args(
8011 usize::MAX - 1,
8012 "timedelta_all_missing",
8013 &td_all_missing,
8014 None,
8015 None,
8016 );
8017
8018 let mut seed = 0xa126_5eed_ed9e_u64;
8019 for case in 0..260 {
8020 let len = (next(&mut seed) % 83 + 1) as usize;
8021
8022 let mut numeric = Vec::with_capacity(len);
8023 numeric.push(Scalar::Int64(case as i64 - 130));
8024 if len > 1 {
8025 numeric.push(Scalar::Int64(case as i64 - 130));
8026 }
8027 for _ in numeric.len()..len {
8028 let raw = (next(&mut seed) % 20_001) as i64 - 10_000;
8029 numeric.push(match next(&mut seed) % 9 {
8030 0 => Scalar::Null(NullKind::Null),
8031 1 => Scalar::Null(NullKind::NaN),
8032 2 => Scalar::Float64(f64::NAN),
8033 3 => Scalar::Bool(raw & 1 == 0),
8034 4 => Scalar::Int64(raw % 211),
8035 5 => Scalar::Float64(raw as f64 / 47.0),
8036 6 => Scalar::Float64(0.0),
8037 7 => Scalar::Float64(-0.0),
8038 _ => Scalar::Float64(raw.signum() as f64 * f64::INFINITY),
8039 });
8040 }
8041 assert_args(
8042 case,
8043 "numeric",
8044 &numeric,
8045 expected_numeric(&numeric, false),
8046 expected_numeric(&numeric, true),
8047 );
8048
8049 let mut timedeltas = Vec::with_capacity(len);
8050 timedeltas.push(Scalar::Timedelta64(case as i64 - 130));
8051 if len > 1 {
8052 timedeltas.push(Scalar::Timedelta64(case as i64 - 130));
8053 }
8054 for _ in timedeltas.len()..len {
8055 let raw = (next(&mut seed) % 20_001) as i64 - 10_000;
8056 timedeltas.push(match next(&mut seed) % 7 {
8057 0 => Scalar::Null(NullKind::Null),
8058 1 => Scalar::Timedelta64(i64::MIN),
8059 _ => Scalar::Timedelta64(raw),
8060 });
8061 }
8062 assert_args(
8063 case,
8064 "timedelta",
8065 &timedeltas,
8066 expected_timedelta(&timedeltas, false),
8067 expected_timedelta(&timedeltas, true),
8068 );
8069 }
8070 }
8071
8072 #[test]
8073 fn nansem_matches_std_over_sqrt_n() {
8074 let values = vec![
8075 Scalar::Float64(2.0),
8076 Scalar::Float64(4.0),
8077 Scalar::Float64(4.0),
8078 Scalar::Float64(4.0),
8079 Scalar::Float64(5.0),
8080 Scalar::Float64(5.0),
8081 Scalar::Float64(7.0),
8082 Scalar::Float64(9.0),
8083 ];
8084 let sem = super::nansem(&values, 1);
8086 assert!(matches!(sem, Scalar::Float64(_)));
8087 let Scalar::Float64(v) = sem else {
8088 return;
8089 };
8090 assert!((v - 0.7559289460184544).abs() < 1e-9);
8091 }
8092
8093 #[test]
8094 fn nansem_empty_returns_null() {
8095 assert!(super::nansem(&[], 1).is_missing());
8096 assert!(super::nansem(&[Scalar::Float64(1.0)], 1).is_missing());
8097 }
8098
8099 #[test]
8100 fn nanptp_returns_max_minus_min() {
8101 let values = vec![
8102 Scalar::Float64(3.0),
8103 Scalar::Null(NullKind::NaN),
8104 Scalar::Float64(7.0),
8105 Scalar::Float64(1.0),
8106 ];
8107 assert_eq!(super::nanptp(&values), Scalar::Float64(6.0));
8108 }
8109
8110 #[test]
8111 fn nanptp_empty_returns_null() {
8112 assert!(super::nanptp(&[]).is_missing());
8113 assert!(super::nanptp(&[Scalar::Null(NullKind::NaN)]).is_missing());
8114 }
8115
8116 #[test]
8117 fn nanptp_timedelta64_preserves_dtype_u2g0r() {
8118 let one_hour: i64 = 3_600 * 1_000_000_000;
8120 let values = vec![
8121 Scalar::Timedelta64(one_hour),
8122 Scalar::Timedelta64(5 * one_hour),
8123 Scalar::Timedelta64(2 * one_hour),
8124 ];
8125 assert_eq!(super::nanptp(&values), Scalar::Timedelta64(4 * one_hour));
8126 }
8127
8128 #[test]
8129 fn nanptp_matches_numeric_and_timedelta_oracle_affjt() {
8130 fn next(seed: &mut u64) -> u64 {
8133 *seed = seed
8134 .wrapping_mul(3202034522624059733)
8135 .wrapping_add(4354685564936845319);
8136 *seed
8137 }
8138
8139 fn expected_numeric(values: &[Scalar]) -> Scalar {
8140 let mut lo = f64::INFINITY;
8141 let mut hi = f64::NEG_INFINITY;
8142 let mut seen = false;
8143 for value in values {
8144 if value.is_missing() {
8145 continue;
8146 }
8147 if let Ok(value) = value.to_f64() {
8148 seen = true;
8149 lo = lo.min(value);
8150 hi = hi.max(value);
8151 }
8152 }
8153 if seen {
8154 Scalar::Float64(hi - lo)
8155 } else {
8156 Scalar::Null(NullKind::NaN)
8157 }
8158 }
8159
8160 fn expected_timedelta(values: &[Scalar]) -> Scalar {
8161 let mut lo = i64::MAX;
8162 let mut hi = i64::MIN;
8163 let mut seen = false;
8164 for value in values {
8165 if let Scalar::Timedelta64(ns) = value
8166 && !value.is_missing()
8167 {
8168 seen = true;
8169 lo = lo.min(*ns);
8170 hi = hi.max(*ns);
8171 }
8172 }
8173 if seen {
8174 Scalar::Timedelta64(hi - lo)
8175 } else {
8176 Scalar::Null(NullKind::NaN)
8177 }
8178 }
8179
8180 fn assert_ptp(case: usize, family: &str, values: &[Scalar], expected: Scalar) {
8181 let actual = super::nanptp(values);
8182 assert!(
8183 actual.semantic_eq(&expected),
8184 "case={case} family={family}: expected {expected:?}, got {actual:?} for {values:?}"
8185 );
8186 }
8187
8188 assert_ptp(
8189 usize::MAX,
8190 "numeric_all_missing",
8191 &[Scalar::Null(NullKind::Null), Scalar::Float64(f64::NAN)],
8192 Scalar::Null(NullKind::NaN),
8193 );
8194 assert_ptp(
8195 usize::MAX - 1,
8196 "timedelta_all_missing",
8197 &[Scalar::Timedelta64(i64::MIN), Scalar::Null(NullKind::NaN)],
8198 Scalar::Null(NullKind::NaN),
8199 );
8200
8201 let mut seed = 0xa22f_17ed_57a7_15e5_u64;
8202 for case in 0..260 {
8203 let len = (next(&mut seed) % 83 + 1) as usize;
8204
8205 let mut numeric = Vec::with_capacity(len);
8206 numeric.push(Scalar::Int64(case as i64 - 130));
8207 for _ in 1..len {
8208 let raw = (next(&mut seed) % 20_001) as i64 - 10_000;
8209 numeric.push(match next(&mut seed) % 9 {
8210 0 => Scalar::Null(NullKind::Null),
8211 1 => Scalar::Null(NullKind::NaN),
8212 2 => Scalar::Float64(f64::NAN),
8213 3 => Scalar::Bool(raw & 1 == 0),
8214 4 => Scalar::Int64(raw),
8215 5 => Scalar::Float64(raw as f64 / 53.0),
8216 6 => Scalar::Float64(0.0),
8217 7 => Scalar::Float64(-0.0),
8218 _ => Scalar::Float64(raw.signum() as f64 * f64::INFINITY),
8219 });
8220 }
8221 assert_ptp(case, "numeric", &numeric, expected_numeric(&numeric));
8222
8223 let mut timedeltas = Vec::with_capacity(len);
8224 timedeltas.push(Scalar::Timedelta64(case as i64 - 130));
8225 for _ in 1..len {
8226 let raw = (next(&mut seed) % 10_001) as i64 - 5_000;
8227 timedeltas.push(match next(&mut seed) % 7 {
8228 0 => Scalar::Null(NullKind::Null),
8229 1 => Scalar::Timedelta64(i64::MIN),
8230 _ => Scalar::Timedelta64(raw),
8231 });
8232 }
8233 assert_ptp(
8234 case,
8235 "timedelta",
8236 &timedeltas,
8237 expected_timedelta(&timedeltas),
8238 );
8239 }
8240 }
8241
8242 #[test]
8243 fn nanargmax_nanargmin_timedelta64_compare_by_ns_ql1t5() {
8244 let one_hour: i64 = 3_600 * 1_000_000_000;
8247 let values = vec![
8248 Scalar::Timedelta64(2 * one_hour),
8249 Scalar::Timedelta64(5 * one_hour),
8250 Scalar::Timedelta64(one_hour),
8251 Scalar::Timedelta64(3 * one_hour),
8252 ];
8253 assert_eq!(super::nanargmax(&values), Some(1));
8254 assert_eq!(super::nanargmin(&values), Some(2));
8255 }
8256
8257 #[test]
8258 fn nanprod_timedelta64_returns_null_szq6a() {
8259 let one_hour: i64 = 3_600 * 1_000_000_000;
8263 let values = vec![
8264 Scalar::Timedelta64(2 * one_hour),
8265 Scalar::Timedelta64(3 * one_hour),
8266 ];
8267 assert!(super::nanprod(&values).is_missing());
8268 }
8269
8270 #[test]
8271 fn nanprod_matches_numeric_and_timedelta_oracle_9938h() {
8272 fn next(seed: &mut u64) -> u64 {
8275 *seed = seed
8276 .wrapping_mul(6364136223846793005)
8277 .wrapping_add(1442695040888963407);
8278 *seed
8279 }
8280
8281 fn expected_numeric(values: &[Scalar]) -> Scalar {
8282 let mut product = 1.0_f64;
8283 for value in values {
8284 if value.is_missing() {
8285 continue;
8286 }
8287 if let Ok(value) = value.to_f64() {
8288 product *= value;
8289 }
8290 }
8291 Scalar::Float64(product)
8292 }
8293
8294 fn expected_timedelta(values: &[Scalar]) -> Scalar {
8295 if values
8296 .iter()
8297 .any(|value| matches!(value, Scalar::Timedelta64(_)) && !value.is_missing())
8298 {
8299 Scalar::Null(NullKind::NaN)
8300 } else {
8301 Scalar::Float64(1.0)
8302 }
8303 }
8304
8305 fn assert_prod(case: usize, family: &str, values: &[Scalar], expected: Scalar) {
8306 let actual = super::nanprod(values);
8307 assert!(
8308 actual.semantic_eq(&expected),
8309 "case={case} family={family}: expected {expected:?}, got {actual:?} for {values:?}"
8310 );
8311 }
8312
8313 let all_missing = [Scalar::Null(NullKind::Null), Scalar::Float64(f64::NAN)];
8314 assert_prod(
8315 usize::MAX,
8316 "numeric_all_missing",
8317 &all_missing,
8318 Scalar::Float64(1.0),
8319 );
8320
8321 let td_all_missing = [Scalar::Timedelta64(i64::MIN), Scalar::Null(NullKind::NaN)];
8322 assert_prod(
8323 usize::MAX - 1,
8324 "timedelta_all_missing",
8325 &td_all_missing,
8326 expected_timedelta(&td_all_missing),
8327 );
8328
8329 let mut seed = 0x6e0d_9938_a11c_0de5_u64;
8330 for case in 0..280 {
8331 let len = (next(&mut seed) % 89 + 1) as usize;
8332
8333 let mut numeric = Vec::with_capacity(len);
8334 numeric.push(Scalar::Int64((case % 17) as i64 - 8));
8335 for _ in 1..len {
8336 let raw = (next(&mut seed) % 2_001) as i64 - 1_000;
8337 numeric.push(match next(&mut seed) % 9 {
8338 0 => Scalar::Null(NullKind::Null),
8339 1 => Scalar::Null(NullKind::NaN),
8340 2 => Scalar::Float64(f64::NAN),
8341 3 => Scalar::Bool(raw & 1 == 0),
8342 4 => Scalar::Int64(raw % 19),
8343 5 => Scalar::Float64(raw as f64 / 97.0),
8344 6 => Scalar::Float64(0.0),
8345 7 => Scalar::Float64(-0.0),
8346 _ => Scalar::Float64(1.0),
8347 });
8348 }
8349 assert_prod(case, "numeric", &numeric, expected_numeric(&numeric));
8350
8351 let mut timedeltas = Vec::with_capacity(len);
8352 timedeltas.push(Scalar::Timedelta64(case as i64 - 140));
8353 for _ in 1..len {
8354 let raw = (next(&mut seed) % 10_001) as i64 - 5_000;
8355 timedeltas.push(match next(&mut seed) % 7 {
8356 0 => Scalar::Null(NullKind::Null),
8357 1 => Scalar::Timedelta64(i64::MIN),
8358 _ => Scalar::Timedelta64(raw),
8359 });
8360 }
8361 assert_prod(
8362 case,
8363 "timedelta",
8364 &timedeltas,
8365 expected_timedelta(&timedeltas),
8366 );
8367 }
8368 }
8369
8370 #[test]
8371 fn nanskew_symmetric_distribution_near_zero() {
8372 let values = vec![
8373 Scalar::Float64(1.0),
8374 Scalar::Float64(2.0),
8375 Scalar::Float64(3.0),
8376 Scalar::Float64(4.0),
8377 Scalar::Float64(5.0),
8378 ];
8379 let skew = super::nanskew(&values);
8381 assert!(matches!(skew, Scalar::Float64(_)));
8382 let Scalar::Float64(v) = skew else {
8383 return;
8384 };
8385 assert!(v.abs() < 1e-9);
8386 }
8387
8388 #[test]
8389 fn nanskew_too_few_values_returns_null() {
8390 assert!(super::nanskew(&[]).is_missing());
8391 assert!(super::nanskew(&[Scalar::Float64(1.0), Scalar::Float64(2.0)]).is_missing());
8392 }
8393
8394 #[test]
8395 fn nankurt_symmetric_uniform_distribution() {
8396 let values = vec![
8397 Scalar::Float64(1.0),
8398 Scalar::Float64(2.0),
8399 Scalar::Float64(3.0),
8400 Scalar::Float64(4.0),
8401 Scalar::Float64(5.0),
8402 ];
8403 let kurt = super::nankurt(&values);
8405 assert!(matches!(kurt, Scalar::Float64(_)));
8406 let Scalar::Float64(v) = kurt else {
8407 return;
8408 };
8409 assert!((v + 1.2).abs() < 1e-9);
8410 }
8411
8412 #[test]
8413 fn nankurt_too_few_values_returns_null() {
8414 let vals: Vec<Scalar> = (0..3).map(|i| Scalar::Float64(i as f64)).collect();
8415 assert!(super::nankurt(&vals).is_missing());
8416 }
8417
8418 #[test]
8419 fn nanskew_constant_series_returns_zero() {
8420 let values = vec![
8421 Scalar::Float64(5.0),
8422 Scalar::Float64(5.0),
8423 Scalar::Float64(5.0),
8424 ];
8425 assert_eq!(super::nanskew(&values), Scalar::Float64(0.0));
8426 assert_eq!(
8427 super::nankurt(&[
8428 Scalar::Float64(5.0),
8429 Scalar::Float64(5.0),
8430 Scalar::Float64(5.0),
8431 Scalar::Float64(5.0),
8432 ]),
8433 Scalar::Float64(0.0)
8434 );
8435 }
8436
8437 #[test]
8438 fn nanskew_nankurt_match_numeric_oracle_jr7zk() {
8439 fn next(seed: &mut u64) -> u64 {
8442 *seed = seed
8443 .wrapping_mul(2862933555777941757)
8444 .wrapping_add(3037000493);
8445 *seed
8446 }
8447
8448 fn samples(values: &[Scalar]) -> Vec<f64> {
8449 values
8450 .iter()
8451 .filter(|value| !value.is_missing())
8452 .filter_map(|value| value.to_f64().ok())
8453 .collect()
8454 }
8455
8456 fn expected_skew(values: &[Scalar]) -> Scalar {
8457 let samples = samples(values);
8458 let n = samples.len() as f64;
8459 if n < 3.0 {
8460 return Scalar::Null(NullKind::NaN);
8461 }
8462 let mean = samples.iter().sum::<f64>() / n;
8463 let m2 = samples
8464 .iter()
8465 .map(|value| (value - mean).powi(2))
8466 .sum::<f64>();
8467 let m3 = samples
8468 .iter()
8469 .map(|value| (value - mean).powi(3))
8470 .sum::<f64>();
8471 let s2 = m2 / (n - 1.0);
8472 if s2 == 0.0 {
8473 return Scalar::Float64(0.0);
8474 }
8475 Scalar::Float64((n / ((n - 1.0) * (n - 2.0))) * (m3 / s2.powf(1.5)))
8476 }
8477
8478 fn expected_kurt(values: &[Scalar]) -> Scalar {
8479 let samples = samples(values);
8480 let n = samples.len() as f64;
8481 if n < 4.0 {
8482 return Scalar::Null(NullKind::NaN);
8483 }
8484 let mean = samples.iter().sum::<f64>() / n;
8485 let m2 = samples
8486 .iter()
8487 .map(|value| (value - mean).powi(2))
8488 .sum::<f64>();
8489 let m4 = samples
8490 .iter()
8491 .map(|value| (value - mean).powi(4))
8492 .sum::<f64>();
8493 let s2 = m2 / (n - 1.0);
8494 if s2 == 0.0 {
8495 return Scalar::Float64(0.0);
8496 }
8497 let adj = (n * (n + 1.0)) / ((n - 1.0) * (n - 2.0) * (n - 3.0));
8498 let sub = (3.0 * (n - 1.0).powi(2)) / ((n - 2.0) * (n - 3.0));
8499 Scalar::Float64(adj * (m4 / (s2 * s2)) - sub)
8500 }
8501
8502 fn assert_moments(case: usize, values: &[Scalar]) {
8503 let expected_skew = expected_skew(values);
8504 let expected_kurt = expected_kurt(values);
8505 let actual_skew = super::nanskew(values);
8506 let actual_kurt = super::nankurt(values);
8507 assert!(
8508 actual_skew.semantic_eq(&expected_skew),
8509 "case={case}: expected skew {expected_skew:?}, got {actual_skew:?} for {values:?}"
8510 );
8511 assert!(
8512 actual_kurt.semantic_eq(&expected_kurt),
8513 "case={case}: expected kurt {expected_kurt:?}, got {actual_kurt:?} for {values:?}"
8514 );
8515 }
8516
8517 assert_moments(
8518 usize::MAX,
8519 &[Scalar::Null(NullKind::Null), Scalar::Float64(f64::NAN)],
8520 );
8521 assert_moments(
8522 usize::MAX - 1,
8523 &[
8524 Scalar::Float64(7.0),
8525 Scalar::Float64(7.0),
8526 Scalar::Float64(7.0),
8527 Scalar::Float64(7.0),
8528 ],
8529 );
8530
8531 let mut seed = 0x5ce7_9a55_a11c_0de5_u64;
8532 for case in 0..260 {
8533 let len = (next(&mut seed) % 89 + 1) as usize;
8534 let mut values = Vec::with_capacity(len);
8535 if case % 11 == 0 {
8536 values.extend((0..len).map(|_| Scalar::Float64(3.0)));
8537 } else {
8538 values.push(Scalar::Float64(case as f64 / 19.0));
8539 for _ in 1..len {
8540 let raw = (next(&mut seed) % 20_001) as i64 - 10_000;
8541 values.push(match next(&mut seed) % 8 {
8542 0 => Scalar::Null(NullKind::Null),
8543 1 => Scalar::Null(NullKind::NaN),
8544 2 => Scalar::Float64(f64::NAN),
8545 3 => Scalar::Bool(raw & 1 == 0),
8546 4 => Scalar::Int64(raw % 251),
8547 5 => Scalar::Float64(raw as f64 / 83.0),
8548 6 => Scalar::Float64(0.0),
8549 _ => Scalar::Float64(-0.0),
8550 });
8551 }
8552 }
8553 assert_moments(case, &values);
8554 }
8555 }
8556
8557 #[test]
8560 fn interval_default_closed_is_right() {
8561 assert_eq!(IntervalClosed::default(), IntervalClosed::Right);
8562 }
8563
8564 #[test]
8565 fn interval_left_and_right_closed_helpers() {
8566 assert!(IntervalClosed::Left.left_closed());
8567 assert!(!IntervalClosed::Left.right_closed());
8568 assert!(!IntervalClosed::Right.left_closed());
8569 assert!(IntervalClosed::Right.right_closed());
8570 assert!(IntervalClosed::Both.left_closed());
8571 assert!(IntervalClosed::Both.right_closed());
8572 assert!(!IntervalClosed::Neither.left_closed());
8573 assert!(!IntervalClosed::Neither.right_closed());
8574 }
8575
8576 #[test]
8577 fn interval_display_matches_pandas_notation() {
8578 assert_eq!(
8579 Interval::new(0.0, 5.0, IntervalClosed::Right).to_string(),
8580 "(0.0, 5.0]"
8581 );
8582 assert_eq!(
8583 Interval::new(0.0, 5.0, IntervalClosed::Left).to_string(),
8584 "[0.0, 5.0)"
8585 );
8586 assert_eq!(
8587 Interval::new(0.0, 5.0, IntervalClosed::Both).to_string(),
8588 "[0.0, 5.0]"
8589 );
8590 assert_eq!(
8591 Interval::new(0.0, 5.0, IntervalClosed::Neither).to_string(),
8592 "(0.0, 5.0)"
8593 );
8594 assert_eq!(
8595 Interval::new(2.5, 3.5, IntervalClosed::Right).to_string(),
8596 "(2.5, 3.5]"
8597 );
8598 assert_eq!(
8599 Interval::new(-1.0, 0.0, IntervalClosed::Right).to_string(),
8600 "(-1.0, 0.0]"
8601 );
8602 assert_eq!(
8603 Interval::new(1e20, 2e20, IntervalClosed::Right).to_string(),
8604 "(1e+20, 2e+20]"
8605 );
8606 }
8607
8608 #[test]
8609 fn interval_length_and_mid() {
8610 let i = Interval::new(2.0, 10.0, IntervalClosed::Right);
8611 assert_eq!(i.length(), 8.0);
8612 assert_eq!(i.mid(), 6.0);
8613 }
8614
8615 #[test]
8616 fn interval_contains_matches_closed_policy() {
8617 let right = Interval::new(0.0, 5.0, IntervalClosed::Right);
8618 assert!(!right.contains(0.0));
8619 assert!(right.contains(2.5));
8620 assert!(right.contains(5.0));
8621
8622 let left = Interval::new(0.0, 5.0, IntervalClosed::Left);
8623 assert!(left.contains(0.0));
8624 assert!(left.contains(2.5));
8625 assert!(!left.contains(5.0));
8626
8627 let both = Interval::new(0.0, 5.0, IntervalClosed::Both);
8628 assert!(both.contains(0.0));
8629 assert!(both.contains(5.0));
8630
8631 let neither = Interval::new(0.0, 5.0, IntervalClosed::Neither);
8632 assert!(!neither.contains(0.0));
8633 assert!(!neither.contains(5.0));
8634 assert!(neither.contains(2.5));
8635 }
8636
8637 #[test]
8638 fn interval_contains_nan_returns_false() {
8639 let i = Interval::new(0.0, 10.0, IntervalClosed::Both);
8640 assert!(!i.contains(f64::NAN));
8641 }
8642
8643 #[test]
8644 fn interval_is_empty_matches_pandas() {
8645 assert!(Interval::new(3.0, 3.0, IntervalClosed::Right).is_empty());
8647 assert!(Interval::new(3.0, 3.0, IntervalClosed::Left).is_empty());
8648 assert!(Interval::new(3.0, 3.0, IntervalClosed::Neither).is_empty());
8649 assert!(!Interval::new(3.0, 3.0, IntervalClosed::Both).is_empty());
8651 assert!(!Interval::new(0.0, 5.0, IntervalClosed::Right).is_empty());
8653 }
8654
8655 #[test]
8656 fn interval_overlaps_disjoint_returns_false() {
8657 let a = Interval::new(0.0, 1.0, IntervalClosed::Right);
8658 let b = Interval::new(2.0, 3.0, IntervalClosed::Right);
8659 assert!(!a.overlaps(&b));
8660 assert!(!b.overlaps(&a));
8661 }
8662
8663 #[test]
8664 fn interval_overlaps_nested_returns_true() {
8665 let outer = Interval::new(0.0, 10.0, IntervalClosed::Right);
8666 let inner = Interval::new(3.0, 7.0, IntervalClosed::Right);
8667 assert!(outer.overlaps(&inner));
8668 assert!(inner.overlaps(&outer));
8669 }
8670
8671 #[test]
8672 fn interval_overlaps_touching_respects_closed_policy() {
8673 let right_right = (
8675 Interval::new(0.0, 1.0, IntervalClosed::Right),
8676 Interval::new(1.0, 2.0, IntervalClosed::Right),
8677 );
8678 assert!(!right_right.0.overlaps(&right_right.1));
8680
8681 let both_both = (
8683 Interval::new(0.0, 1.0, IntervalClosed::Both),
8684 Interval::new(1.0, 2.0, IntervalClosed::Both),
8685 );
8686 assert!(both_both.0.overlaps(&both_both.1));
8687 }
8688
8689 #[test]
8690 fn interval_roundtrips_through_serde_json() {
8691 let i = Interval::new(1.5, 3.25, IntervalClosed::Both);
8692 let json = serde_json::to_string(&i).expect("serialize");
8693 let back: Interval = serde_json::from_str(&json).expect("deserialize");
8694 assert_eq!(i, back);
8695 }
8696
8697 #[test]
8698 fn interval_serde_default_closed_is_right_when_missing() {
8699 let back: Interval =
8701 serde_json::from_str(r#"{"left":0.0,"right":5.0}"#).expect("deserialize");
8702 assert_eq!(back.closed, IntervalClosed::Right);
8703 }
8704
8705 #[test]
8708 fn period_freq_parses_canonical_aliases() {
8709 assert_eq!(PeriodFreq::parse("A"), Some(PeriodFreq::Annual));
8710 assert_eq!(PeriodFreq::parse("Y"), Some(PeriodFreq::Annual));
8711 assert_eq!(PeriodFreq::parse("Q"), Some(PeriodFreq::Quarterly));
8712 assert_eq!(PeriodFreq::parse("M"), Some(PeriodFreq::Monthly));
8713 assert_eq!(PeriodFreq::parse("W"), Some(PeriodFreq::Weekly));
8714 assert_eq!(PeriodFreq::parse("D"), Some(PeriodFreq::Daily));
8715 assert_eq!(PeriodFreq::parse("B"), Some(PeriodFreq::Business));
8716 assert_eq!(PeriodFreq::parse("H"), Some(PeriodFreq::Hourly));
8717 assert_eq!(PeriodFreq::parse("T"), Some(PeriodFreq::Minutely));
8718 assert_eq!(PeriodFreq::parse("min"), Some(PeriodFreq::Minutely));
8719 assert_eq!(PeriodFreq::parse("S"), Some(PeriodFreq::Secondly));
8720 }
8721
8722 #[test]
8723 fn period_freq_parse_is_case_insensitive() {
8724 assert_eq!(PeriodFreq::parse("quarterly"), Some(PeriodFreq::Quarterly));
8725 assert_eq!(PeriodFreq::parse("MONTHLY"), Some(PeriodFreq::Monthly));
8726 }
8727
8728 #[test]
8729 fn period_freq_rejects_unknown_aliases() {
8730 assert_eq!(PeriodFreq::parse("nanosec"), None);
8731 assert_eq!(PeriodFreq::parse(""), None);
8732 assert_eq!(PeriodFreq::parse("xyz"), None);
8733 }
8734
8735 #[test]
8736 fn period_freq_alias_roundtrip() {
8737 for freq in [
8738 PeriodFreq::Annual,
8739 PeriodFreq::Quarterly,
8740 PeriodFreq::Monthly,
8741 PeriodFreq::Weekly,
8742 PeriodFreq::Daily,
8743 PeriodFreq::Business,
8744 PeriodFreq::Hourly,
8745 PeriodFreq::Minutely,
8746 PeriodFreq::Secondly,
8747 ] {
8748 assert_eq!(PeriodFreq::parse(freq.alias()), Some(freq));
8749 }
8750 }
8751
8752 #[test]
8753 fn period_freq_anchored_aliases_are_pandas_canonical_h2wiv() {
8754 assert_eq!(PeriodFreq::Annual.alias(), "Y-DEC");
8755 assert_eq!(PeriodFreq::Quarterly.alias(), "Q-DEC");
8756 assert_eq!(PeriodFreq::Weekly.alias(), "W-SUN");
8757
8758 assert_eq!(PeriodFreq::parse("A"), Some(PeriodFreq::Annual));
8759 assert_eq!(PeriodFreq::parse("Y"), Some(PeriodFreq::Annual));
8760 assert_eq!(PeriodFreq::parse("Y-DEC"), Some(PeriodFreq::Annual));
8761 assert_eq!(PeriodFreq::parse("Q"), Some(PeriodFreq::Quarterly));
8762 assert_eq!(PeriodFreq::parse("Q-DEC"), Some(PeriodFreq::Quarterly));
8763 assert_eq!(PeriodFreq::parse("W"), Some(PeriodFreq::Weekly));
8764 assert_eq!(PeriodFreq::parse("W-SUN"), Some(PeriodFreq::Weekly));
8765 }
8766
8767 #[test]
8768 fn period_freq_intraday_aliases_are_pandas_canonical_8kfdo() {
8769 assert_eq!(PeriodFreq::Hourly.alias(), "h");
8770 assert_eq!(PeriodFreq::Minutely.alias(), "min");
8771 assert_eq!(PeriodFreq::Secondly.alias(), "s");
8772
8773 assert_eq!(PeriodFreq::parse("H"), Some(PeriodFreq::Hourly));
8774 assert_eq!(PeriodFreq::parse("T"), Some(PeriodFreq::Minutely));
8775 assert_eq!(PeriodFreq::parse("S"), Some(PeriodFreq::Secondly));
8776 }
8777
8778 #[test]
8779 fn period_scalar_accessors_match_pandas_star8() {
8780 let period = Period::new(600, PeriodFreq::Monthly);
8781
8782 assert_eq!(period.ordinal(), 600);
8783 assert_eq!(period.freq(), PeriodFreq::Monthly);
8784 assert_eq!(period.freqstr(), "M");
8785 }
8786
8787 #[test]
8788 fn period_parse_common_pandas_ordinals_avm08() {
8789 assert_eq!(
8790 Period::parse("2024").unwrap(),
8791 Period::new(54, PeriodFreq::Annual)
8792 );
8793 assert_eq!(
8794 Period::parse("2024Q1").unwrap(),
8795 Period::new(216, PeriodFreq::Quarterly)
8796 );
8797 assert_eq!(
8798 Period::parse("2024-01").unwrap(),
8799 Period::new(648, PeriodFreq::Monthly)
8800 );
8801 assert_eq!(
8802 Period::parse("2024-01-15").unwrap(),
8803 Period::new(19_737, PeriodFreq::Daily)
8804 );
8805 assert!(Period::parse("216").is_err());
8806 }
8807
8808 #[test]
8809 fn period_shift_advances_ordinal() {
8810 let q1 = Period::new(216, PeriodFreq::Quarterly);
8811 let q2 = q1.shift(1);
8812 assert_eq!(q2.ordinal, 217);
8813 assert_eq!(q2.freq, PeriodFreq::Quarterly);
8814 let q0 = q1.shift(-1);
8815 assert_eq!(q0.ordinal, 215);
8816 }
8817
8818 #[test]
8819 fn period_shift_saturates_on_overflow() {
8820 let p = Period::new(i64::MAX - 2, PeriodFreq::Daily);
8821 assert_eq!(p.shift(100).ordinal, i64::MAX);
8822 let p = Period::new(i64::MIN + 2, PeriodFreq::Daily);
8823 assert_eq!(p.shift(-100).ordinal, i64::MIN);
8824 }
8825
8826 #[test]
8827 fn period_diff_returns_period_count() {
8828 let a = Period::new(216, PeriodFreq::Quarterly);
8829 let b = Period::new(220, PeriodFreq::Quarterly);
8830 assert_eq!(b.diff(&a), Some(4));
8831 assert_eq!(a.diff(&b), Some(-4));
8832 }
8833
8834 #[test]
8835 fn period_diff_rejects_mismatched_freq() {
8836 let monthly = Period::new(100, PeriodFreq::Monthly);
8837 let quarterly = Period::new(100, PeriodFreq::Quarterly);
8838 assert_eq!(monthly.diff(&quarterly), None);
8839 assert_eq!(quarterly.diff(&monthly), None);
8840 }
8841
8842 #[test]
8843 fn period_cmp_same_freq_respects_ordinal_order() {
8844 use std::cmp::Ordering;
8845 let a = Period::new(10, PeriodFreq::Monthly);
8846 let b = Period::new(20, PeriodFreq::Monthly);
8847 assert_eq!(a.cmp_same_freq(&b), Some(Ordering::Less));
8848 assert_eq!(b.cmp_same_freq(&a), Some(Ordering::Greater));
8849 assert_eq!(a.cmp_same_freq(&a), Some(Ordering::Equal));
8850 }
8851
8852 #[test]
8853 fn period_cmp_cross_freq_returns_none() {
8854 let m = Period::new(1, PeriodFreq::Monthly);
8855 let q = Period::new(1, PeriodFreq::Quarterly);
8856 assert_eq!(m.cmp_same_freq(&q), None);
8857 }
8858
8859 #[test]
8860 fn period_arithmetic_matches_seeded_oracles_bac28() {
8861 use std::cmp::Ordering;
8862
8863 fn next(seed: &mut u64) -> u64 {
8864 *seed = seed
8865 .wrapping_mul(2862933555777941757)
8866 .wrapping_add(3037000493);
8867 *seed
8868 }
8869
8870 fn freq_for(raw: u64) -> PeriodFreq {
8871 match raw % 9 {
8872 0 => PeriodFreq::Annual,
8873 1 => PeriodFreq::Quarterly,
8874 2 => PeriodFreq::Monthly,
8875 3 => PeriodFreq::Weekly,
8876 4 => PeriodFreq::Daily,
8877 5 => PeriodFreq::Business,
8878 6 => PeriodFreq::Hourly,
8879 7 => PeriodFreq::Minutely,
8880 _ => PeriodFreq::Secondly,
8881 }
8882 }
8883
8884 fn different_freq(freq: PeriodFreq) -> PeriodFreq {
8885 match freq {
8886 PeriodFreq::Annual => PeriodFreq::Quarterly,
8887 PeriodFreq::Quarterly => PeriodFreq::Monthly,
8888 PeriodFreq::Monthly => PeriodFreq::Weekly,
8889 PeriodFreq::Weekly => PeriodFreq::Daily,
8890 PeriodFreq::Daily => PeriodFreq::Business,
8891 PeriodFreq::Business => PeriodFreq::Hourly,
8892 PeriodFreq::Hourly => PeriodFreq::Minutely,
8893 PeriodFreq::Minutely => PeriodFreq::Secondly,
8894 PeriodFreq::Secondly => PeriodFreq::Annual,
8895 }
8896 }
8897
8898 fn assert_period_case(
8899 case: usize,
8900 freq: PeriodFreq,
8901 ordinal: i64,
8902 shift_by: i64,
8903 other_ordinal: i64,
8904 ) {
8905 let period = Period::new(ordinal, freq);
8906 let shifted = period.shift(shift_by);
8907 assert_eq!(
8908 shifted.ordinal,
8909 ordinal.saturating_add(shift_by),
8910 "case {case}: shift ordinal"
8911 );
8912 assert_eq!(shifted.freq, freq, "case {case}: shift freq");
8913
8914 let same_freq_other = Period::new(other_ordinal, freq);
8915 assert_eq!(
8916 period.diff(&same_freq_other),
8917 Some(ordinal.saturating_sub(other_ordinal)),
8918 "case {case}: same-freq diff"
8919 );
8920 assert_eq!(
8921 period.cmp_same_freq(&same_freq_other),
8922 Some(ordinal.cmp(&other_ordinal)),
8923 "case {case}: same-freq cmp"
8924 );
8925
8926 let cross_freq_other = Period::new(other_ordinal, different_freq(freq));
8927 assert_eq!(
8928 period.diff(&cross_freq_other),
8929 None,
8930 "case {case}: cross-freq diff"
8931 );
8932 assert_eq!(
8933 period.cmp_same_freq(&cross_freq_other),
8934 None,
8935 "case {case}: cross-freq cmp"
8936 );
8937 }
8938
8939 assert_period_case(usize::MAX, PeriodFreq::Daily, i64::MAX - 2, 10, i64::MIN);
8940 assert_period_case(
8941 usize::MAX - 1,
8942 PeriodFreq::Daily,
8943 i64::MIN + 2,
8944 -10,
8945 i64::MAX,
8946 );
8947 assert_eq!(
8948 Period::new(10, PeriodFreq::Monthly)
8949 .cmp_same_freq(&Period::new(10, PeriodFreq::Monthly)),
8950 Some(Ordering::Equal)
8951 );
8952
8953 let mut seed = 0xbac2_8d1f_0d1c_5eed_u64;
8954 for case in 0..260 {
8955 let freq = freq_for(next(&mut seed));
8956 let ordinal = match case % 53 {
8957 0 => i64::MAX - (next(&mut seed) % 8) as i64,
8958 1 => i64::MIN + (next(&mut seed) % 8) as i64,
8959 _ => (next(&mut seed) % 200_001) as i64 - 100_000,
8960 };
8961 let shift_by = match case % 47 {
8962 0 => 512,
8963 1 => -512,
8964 _ => (next(&mut seed) % 4097) as i64 - 2048,
8965 };
8966 let other_ordinal = match case % 41 {
8967 0 => i64::MAX,
8968 1 => i64::MIN,
8969 _ => (next(&mut seed) % 200_001) as i64 - 100_000,
8970 };
8971 assert_period_case(case, freq, ordinal, shift_by, other_ordinal);
8972 }
8973 }
8974
8975 #[test]
8976 fn period_display_is_pandas_calendar_string() {
8977 assert_eq!(
8979 Period::new(216, PeriodFreq::Quarterly).to_string(),
8980 "2024Q1"
8981 );
8982 assert_eq!(Period::new(54, PeriodFreq::Annual).to_string(), "2024");
8984 assert_eq!(Period::new(650, PeriodFreq::Monthly).to_string(), "2024-03");
8986 assert_eq!(
8988 Period::new(fp_days("2024-01-15"), PeriodFreq::Daily).to_string(),
8989 "2024-01-15"
8990 );
8991 assert_eq!(
8992 Scalar::Period(Period::new(i64::MIN, PeriodFreq::Daily)).to_string(),
8993 "NaT"
8994 );
8995 }
8996
8997 #[cfg(test)]
8998 fn fp_days(ymd: &str) -> i64 {
8999 Period::parse(ymd).expect("daily period").ordinal
9000 }
9001
9002 #[test]
9003 fn period_roundtrips_through_serde_json() {
9004 let p = Period::new(42, PeriodFreq::Weekly);
9005 let json = serde_json::to_string(&p).expect("serialize");
9006 let back: Period = serde_json::from_str(&json).expect("deserialize");
9007 assert_eq!(p, back);
9008 }
9009
9010 use super::period_range;
9013
9014 #[test]
9015 fn period_range_zero_periods_is_empty() {
9016 let start = Period::new(216, PeriodFreq::Quarterly);
9017 assert!(period_range(start, 0).is_empty());
9018 }
9019
9020 #[test]
9021 fn period_range_single_period_returns_start_only() {
9022 let start = Period::new(216, PeriodFreq::Quarterly);
9023 let r = period_range(start, 1);
9024 assert_eq!(r.len(), 1);
9025 assert_eq!(r[0], start);
9026 }
9027
9028 #[test]
9029 fn period_range_increments_ordinal_by_one_per_step() {
9030 let start = Period::new(216, PeriodFreq::Quarterly);
9031 let r = period_range(start, 4);
9032 assert_eq!(r.len(), 4);
9033 assert_eq!(r[0].ordinal, 216);
9034 assert_eq!(r[1].ordinal, 217);
9035 assert_eq!(r[2].ordinal, 218);
9036 assert_eq!(r[3].ordinal, 219);
9037 }
9038
9039 #[test]
9040 fn period_range_preserves_frequency() {
9041 let start = Period::new(0, PeriodFreq::Monthly);
9042 let r = period_range(start, 12);
9043 assert!(r.iter().all(|p| p.freq == PeriodFreq::Monthly));
9044 }
9045
9046 #[test]
9047 fn period_range_negative_starting_ordinal_works() {
9048 let start = Period::new(-3, PeriodFreq::Annual);
9050 let r = period_range(start, 5);
9051 assert_eq!(
9052 r.iter().map(|p| p.ordinal).collect::<Vec<_>>(),
9053 vec![-3, -2, -1, 0, 1]
9054 );
9055 }
9056
9057 #[test]
9058 fn period_range_large_n_does_not_panic() {
9059 let start = Period::new(0, PeriodFreq::Monthly);
9061 let r = period_range(start, 1024);
9062 assert_eq!(r.len(), 1024);
9063 assert_eq!(r[1023].ordinal, 1023);
9064 }
9065
9066 #[test]
9067 fn period_range_matches_seeded_ordinal_oracle_z3zh2() {
9068 fn next(seed: &mut u64) -> u64 {
9069 *seed = seed
9070 .wrapping_mul(6364136223846793005)
9071 .wrapping_add(1442695040888963407);
9072 *seed
9073 }
9074
9075 fn freq_for(raw: u64) -> PeriodFreq {
9076 match raw % 9 {
9077 0 => PeriodFreq::Annual,
9078 1 => PeriodFreq::Quarterly,
9079 2 => PeriodFreq::Monthly,
9080 3 => PeriodFreq::Weekly,
9081 4 => PeriodFreq::Daily,
9082 5 => PeriodFreq::Business,
9083 6 => PeriodFreq::Hourly,
9084 7 => PeriodFreq::Minutely,
9085 _ => PeriodFreq::Secondly,
9086 }
9087 }
9088
9089 fn assert_oracle_case(case: usize, start: Period, periods: usize) {
9090 let got = period_range(start, periods);
9091 assert_eq!(got.len(), periods, "case {case}: length");
9092
9093 for (position, period) in got.iter().enumerate() {
9094 let expected_ordinal = start.ordinal.saturating_add(position as i64);
9095 assert_eq!(
9096 period.ordinal, expected_ordinal,
9097 "case {case}: ordinal at {position}"
9098 );
9099 assert_eq!(period.freq, start.freq, "case {case}: freq at {position}");
9100 }
9101 }
9102
9103 assert_oracle_case(usize::MAX, Period::new(42, PeriodFreq::Monthly), 0);
9104 assert_oracle_case(
9105 usize::MAX - 1,
9106 Period::new(i64::MAX - 3, PeriodFreq::Daily),
9107 8,
9108 );
9109
9110 let mut seed = 0x9e21_0d1c_5eed_0421_u64;
9111 for case in 0..260 {
9112 let freq = freq_for(next(&mut seed));
9113 let periods = (next(&mut seed) % 80) as usize;
9114 let start_ordinal = if case % 37 == 0 {
9115 i64::MAX - 7
9116 } else {
9117 (next(&mut seed) % 20_001) as i64 - 10_000
9118 };
9119 assert_oracle_case(case, Period::new(start_ordinal, freq), periods);
9120 }
9121 }
9122
9123 use super::{TypeError, interval_range_by_periods, interval_range_by_step};
9126
9127 #[test]
9128 fn interval_range_by_periods_matches_pandas_default_case() {
9129 let bins = interval_range_by_periods(0.0, 10.0, 5, IntervalClosed::Right);
9131 assert_eq!(bins.len(), 5);
9132 for (i, bin) in bins.iter().enumerate() {
9133 assert_eq!(bin.left, (i as f64) * 2.0);
9134 assert_eq!(bin.right, ((i + 1) as f64) * 2.0);
9135 assert_eq!(bin.closed, IntervalClosed::Right);
9136 }
9137 }
9138
9139 #[test]
9140 fn interval_range_by_periods_final_edge_is_exact_end() {
9141 let bins = interval_range_by_periods(0.0, 1.0, 7, IntervalClosed::Right);
9143 assert_eq!(bins.last().unwrap().right, 1.0);
9144 }
9145
9146 #[test]
9147 fn interval_range_by_periods_zero_periods_is_empty() {
9148 assert!(interval_range_by_periods(0.0, 10.0, 0, IntervalClosed::Right).is_empty());
9149 }
9150
9151 #[test]
9152 fn interval_range_by_periods_reversed_range_is_empty() {
9153 assert!(interval_range_by_periods(10.0, 0.0, 5, IntervalClosed::Right).is_empty());
9155 }
9156
9157 #[test]
9158 fn interval_range_by_periods_preserves_closed_policy() {
9159 for closed in [
9160 IntervalClosed::Left,
9161 IntervalClosed::Right,
9162 IntervalClosed::Both,
9163 IntervalClosed::Neither,
9164 ] {
9165 let bins = interval_range_by_periods(0.0, 4.0, 2, closed);
9166 assert!(bins.iter().all(|b| b.closed == closed));
9167 }
9168 }
9169
9170 #[test]
9171 fn interval_range_by_step_matches_pandas_default_case() {
9172 let bins = interval_range_by_step(0.0, 10.0, 2.0, IntervalClosed::Right).expect("ok");
9174 assert_eq!(bins.len(), 5);
9175 assert_eq!(bins[0].left, 0.0);
9176 assert_eq!(bins[4].right, 10.0);
9177 }
9178
9179 #[test]
9180 fn interval_range_by_step_rejects_non_positive_step() {
9181 assert!(matches!(
9182 interval_range_by_step(0.0, 10.0, 0.0, IntervalClosed::Right),
9183 Err(TypeError::InvalidIntervalStep { .. })
9184 ));
9185 assert!(matches!(
9186 interval_range_by_step(0.0, 10.0, -2.0, IntervalClosed::Right),
9187 Err(TypeError::InvalidIntervalStep { .. })
9188 ));
9189 assert!(matches!(
9190 interval_range_by_step(0.0, 10.0, f64::NAN, IntervalClosed::Right),
9191 Err(TypeError::InvalidIntervalStep { .. })
9192 ));
9193 assert!(matches!(
9194 interval_range_by_step(0.0, 10.0, f64::INFINITY, IntervalClosed::Right),
9195 Err(TypeError::InvalidIntervalStep { .. })
9196 ));
9197 }
9198
9199 #[test]
9200 fn interval_range_by_step_rejects_non_dividing_step() {
9201 assert!(matches!(
9204 interval_range_by_step(0.0, 10.0, 3.0, IntervalClosed::Right),
9205 Err(TypeError::IntervalStepDoesNotDivide { .. })
9206 ));
9207 }
9208
9209 #[test]
9210 fn interval_range_by_step_reversed_range_is_empty() {
9211 let bins = interval_range_by_step(10.0, 0.0, 2.0, IntervalClosed::Right).expect("ok");
9212 assert!(bins.is_empty());
9213 }
9214
9215 #[test]
9216 fn interval_range_by_step_degenerate_zero_span_is_empty() {
9217 let bins = interval_range_by_step(5.0, 5.0, 1.0, IntervalClosed::Right).expect("ok");
9218 assert!(bins.is_empty());
9219 }
9220
9221 #[test]
9222 fn interval_range_by_step_accepts_float_step_within_tolerance() {
9223 let bins = interval_range_by_step(0.0, 1.0, 0.1, IntervalClosed::Right).expect("ok");
9225 assert_eq!(bins.len(), 10);
9226 assert_eq!(bins.last().unwrap().right, 1.0);
9227 }
9228
9229 #[test]
9230 fn interval_range_matches_seeded_arithmetic_oracle_t9ozf() {
9231 fn next(seed: &mut u64) -> u64 {
9234 *seed = seed
9235 .wrapping_mul(2862933555777941757)
9236 .wrapping_add(3037000493);
9237 *seed
9238 }
9239
9240 fn closed_for(raw: u64) -> IntervalClosed {
9241 match raw % 4 {
9242 0 => IntervalClosed::Left,
9243 1 => IntervalClosed::Right,
9244 2 => IntervalClosed::Both,
9245 _ => IntervalClosed::Neither,
9246 }
9247 }
9248
9249 fn assert_interval(
9250 case: usize,
9251 kind: &str,
9252 pos: usize,
9253 actual: &Interval,
9254 expected: &Interval,
9255 ) {
9256 assert!(
9257 (actual.left - expected.left).abs() < 1e-12,
9258 "case={case} kind={kind} pos={pos}: expected left {}, got {}",
9259 expected.left,
9260 actual.left
9261 );
9262 assert!(
9263 (actual.right - expected.right).abs() < 1e-12,
9264 "case={case} kind={kind} pos={pos}: expected right {}, got {}",
9265 expected.right,
9266 actual.right
9267 );
9268 assert_eq!(
9269 actual.closed, expected.closed,
9270 "case={case} kind={kind} pos={pos}: closed mismatch"
9271 );
9272 }
9273
9274 fn expected_by_periods(
9275 start: f64,
9276 end: f64,
9277 periods: usize,
9278 closed: IntervalClosed,
9279 ) -> Vec<Interval> {
9280 if periods == 0 || !start.is_finite() || !end.is_finite() || start >= end {
9281 return Vec::new();
9282 }
9283 let step = (end - start) / periods as f64;
9284 (0..periods)
9285 .map(|pos| {
9286 let left = start + step * pos as f64;
9287 let right = if pos + 1 == periods {
9288 end
9289 } else {
9290 start + step * (pos + 1) as f64
9291 };
9292 Interval::new(left, right, closed)
9293 })
9294 .collect()
9295 }
9296
9297 fn expected_by_step(
9298 start: f64,
9299 end: f64,
9300 step: f64,
9301 closed: IntervalClosed,
9302 ) -> Vec<Interval> {
9303 if start >= end {
9304 return Vec::new();
9305 }
9306 let count = ((end - start) / step).round() as usize;
9307 (0..count)
9308 .map(|pos| {
9309 let left = start + step * pos as f64;
9310 let right = if pos + 1 == count {
9311 end
9312 } else {
9313 start + step * (pos + 1) as f64
9314 };
9315 Interval::new(left, right, closed)
9316 })
9317 .collect()
9318 }
9319
9320 assert!(interval_range_by_periods(5.0, 5.0, 4, IntervalClosed::Right).is_empty());
9321 assert!(interval_range_by_periods(5.0, 4.0, 4, IntervalClosed::Right).is_empty());
9322 assert!(
9323 interval_range_by_step(5.0, 5.0, 1.0, IntervalClosed::Right)
9324 .expect("zero span")
9325 .is_empty()
9326 );
9327
9328 let mut seed = 0x171e_7a11_c0de_5eed_u64;
9329 for case in 0..220 {
9330 let start = (next(&mut seed) % 2_001) as f64 / 10.0 - 100.0;
9331 let periods = (next(&mut seed) % 24 + 1) as usize;
9332 let width = (next(&mut seed) % 1_000 + 1) as f64 / 4.0;
9333 let end = start + width;
9334 let closed = closed_for(next(&mut seed));
9335
9336 let actual = interval_range_by_periods(start, end, periods, closed);
9337 let expected = expected_by_periods(start, end, periods, closed);
9338 assert_eq!(
9339 actual.len(),
9340 expected.len(),
9341 "case={case} periods: length mismatch"
9342 );
9343 for (pos, (actual, expected)) in actual.iter().zip(expected.iter()).enumerate() {
9344 assert_interval(case, "periods", pos, actual, expected);
9345 }
9346
9347 let step_count = (next(&mut seed) % 20 + 1) as usize;
9348 let step = (next(&mut seed) % 25 + 1) as f64;
9349 let step_end = start + step * step_count as f64;
9350 let actual = interval_range_by_step(start, step_end, step, closed).expect("divides");
9351 let expected = expected_by_step(start, step_end, step, closed);
9352 assert_eq!(
9353 actual.len(),
9354 expected.len(),
9355 "case={case} step: length mismatch"
9356 );
9357 for (pos, (actual, expected)) in actual.iter().zip(expected.iter()).enumerate() {
9358 assert_interval(case, "step", pos, actual, expected);
9359 }
9360 }
9361 }
9362
9363 use super::Timedelta;
9366
9367 #[test]
9368 fn timedelta_add_sums_non_nat() {
9369 let one_hour = Timedelta::NANOS_PER_HOUR;
9370 let one_day = Timedelta::NANOS_PER_DAY;
9371 assert_eq!(Timedelta::add(one_hour, one_day), one_hour + one_day);
9372 }
9373
9374 #[test]
9375 fn timedelta_add_propagates_nat() {
9376 assert_eq!(Timedelta::add(Timedelta::NAT, 100), Timedelta::NAT);
9377 assert_eq!(Timedelta::add(100, Timedelta::NAT), Timedelta::NAT);
9378 assert_eq!(
9379 Timedelta::add(Timedelta::NAT, Timedelta::NAT),
9380 Timedelta::NAT
9381 );
9382 }
9383
9384 #[test]
9385 fn timedelta_add_saturates_on_overflow() {
9386 assert_eq!(Timedelta::add(i64::MAX - 10, 100), i64::MAX);
9387 assert_eq!(Timedelta::add(i64::MIN + 10, -100), i64::MIN);
9389 }
9390
9391 #[test]
9392 fn timedelta_sub_subtracts_non_nat() {
9393 let one_hour = Timedelta::NANOS_PER_HOUR;
9394 assert_eq!(
9395 Timedelta::sub(one_hour, Timedelta::NANOS_PER_MIN),
9396 one_hour - Timedelta::NANOS_PER_MIN
9397 );
9398 }
9399
9400 #[test]
9401 fn timedelta_sub_propagates_nat() {
9402 assert_eq!(Timedelta::sub(Timedelta::NAT, 100), Timedelta::NAT);
9403 assert_eq!(Timedelta::sub(100, Timedelta::NAT), Timedelta::NAT);
9404 }
9405
9406 #[test]
9407 fn timedelta_neg_flips_sign_non_nat() {
9408 assert_eq!(Timedelta::neg(5), -5);
9409 assert_eq!(Timedelta::neg(-5), 5);
9410 assert_eq!(Timedelta::neg(0), 0);
9411 }
9412
9413 #[test]
9414 fn timedelta_neg_preserves_nat() {
9415 assert_eq!(Timedelta::neg(Timedelta::NAT), Timedelta::NAT);
9416 }
9417
9418 #[test]
9419 fn timedelta_abs_returns_magnitude() {
9420 assert_eq!(Timedelta::abs(-5), 5);
9421 assert_eq!(Timedelta::abs(5), 5);
9422 assert_eq!(Timedelta::abs(0), 0);
9423 assert_eq!(Timedelta::abs(Timedelta::NAT), Timedelta::NAT);
9424 }
9425
9426 #[test]
9427 fn timedelta_mul_scalar_scales() {
9428 let three_hours = Timedelta::NANOS_PER_HOUR * 3;
9429 assert_eq!(
9430 Timedelta::mul_scalar(Timedelta::NANOS_PER_HOUR, 3),
9431 three_hours
9432 );
9433 assert_eq!(Timedelta::mul_scalar(100, 0), 0);
9434 assert_eq!(Timedelta::mul_scalar(100, -2), -200);
9435 }
9436
9437 #[test]
9438 fn timedelta_mul_scalar_saturates() {
9439 assert_eq!(Timedelta::mul_scalar(i64::MAX, 2), i64::MAX);
9440 assert_eq!(Timedelta::mul_scalar(i64::MIN + 1, 2), i64::MIN);
9442 }
9443
9444 #[test]
9445 fn timedelta_mul_scalar_propagates_nat() {
9446 assert_eq!(Timedelta::mul_scalar(Timedelta::NAT, 5), Timedelta::NAT);
9447 }
9448
9449 #[test]
9450 fn timedelta_div_scalar_floor_divides() {
9451 assert_eq!(Timedelta::div_scalar(100, 3), 33);
9453 assert_eq!(Timedelta::div_scalar(-100, 3), -34);
9454 assert_eq!(Timedelta::div_scalar(100, -3), -34);
9455 assert_eq!(Timedelta::div_scalar(-100, -3), 33);
9456 }
9457
9458 #[test]
9459 fn timedelta_div_scalar_zero_divisor_returns_nat() {
9460 assert_eq!(Timedelta::div_scalar(100, 0), Timedelta::NAT);
9461 }
9462
9463 #[test]
9464 fn timedelta_div_scalar_min_neg_one_propagates_nat() {
9465 assert_eq!(Timedelta::div_scalar(i64::MIN, -1), Timedelta::NAT);
9468 assert_eq!(Timedelta::div_scalar(i64::MIN + 1, -1), i64::MAX);
9470 }
9471
9472 #[test]
9473 fn timedelta_div_scalar_propagates_nat() {
9474 assert_eq!(Timedelta::div_scalar(Timedelta::NAT, 10), Timedelta::NAT);
9475 }
9476
9477 #[test]
9478 fn timedelta_div_timedelta_returns_float_ratio() {
9479 let two_hours = Timedelta::NANOS_PER_HOUR * 2;
9480 let one_hour = Timedelta::NANOS_PER_HOUR;
9481 assert!((Timedelta::div_timedelta(two_hours, one_hour) - 2.0).abs() < 1e-12);
9482 assert!((Timedelta::div_timedelta(one_hour, two_hours) - 0.5).abs() < 1e-12);
9483 }
9484
9485 #[test]
9486 fn timedelta_div_timedelta_nat_returns_nan() {
9487 assert!(Timedelta::div_timedelta(Timedelta::NAT, 100).is_nan());
9488 assert!(Timedelta::div_timedelta(100, Timedelta::NAT).is_nan());
9489 }
9490
9491 use super::Timestamp;
9494
9495 #[test]
9496 fn timestamp_from_nanos_is_naive_utc() {
9497 let ts = Timestamp::from_nanos(1_700_000_000_000_000_000);
9498 assert_eq!(ts.nanos, 1_700_000_000_000_000_000);
9499 assert_eq!(ts.tz, None);
9500 assert!(!ts.is_nat());
9501 }
9502
9503 #[test]
9504 fn timestamp_from_nanos_tz_carries_tz_name() {
9505 let ts = Timestamp::from_nanos_tz(1_700_000_000_000_000_000, "US/Eastern");
9506 assert_eq!(ts.tz.as_deref(), Some("US/Eastern"));
9507 }
9508
9509 #[test]
9510 fn timestamp_now_returns_current_time() {
9511 let before = std::time::SystemTime::now()
9512 .duration_since(std::time::UNIX_EPOCH)
9513 .unwrap()
9514 .as_nanos() as i64;
9515 let ts = Timestamp::now();
9516 let after = std::time::SystemTime::now()
9517 .duration_since(std::time::UNIX_EPOCH)
9518 .unwrap()
9519 .as_nanos() as i64;
9520 assert!(ts.nanos >= before);
9521 assert!(ts.nanos <= after);
9522 assert!(!ts.is_nat());
9523 }
9524
9525 #[test]
9526 fn timestamp_today_returns_midnight() {
9527 let ts = Timestamp::today();
9528 assert!(!ts.is_nat());
9529 assert_eq!(ts.hour(), Some(0));
9531 assert_eq!(ts.minute(), Some(0));
9532 assert_eq!(ts.second(), Some(0));
9533 }
9534
9535 #[test]
9536 fn timestamp_add_timedelta_shifts_nanos_and_preserves_tz() {
9537 let ts = Timestamp::from_nanos_tz(0, "US/Eastern");
9538 let one_day = Timedelta::NANOS_PER_DAY;
9539 let shifted = ts.add_timedelta(one_day);
9540 assert_eq!(shifted.nanos, one_day);
9541 assert_eq!(shifted.tz.as_deref(), Some("US/Eastern"));
9542 }
9543
9544 #[test]
9545 fn timestamp_add_timedelta_saturates_on_overflow() {
9546 let ts = Timestamp::from_nanos(i64::MAX - 10);
9547 let shifted = ts.add_timedelta(100);
9548 assert_eq!(shifted.nanos, i64::MAX);
9549 }
9550
9551 #[test]
9552 fn timestamp_add_timedelta_propagates_nat() {
9553 assert!(Timestamp::nat().add_timedelta(100).is_nat());
9555 assert!(
9557 Timestamp::from_nanos(0)
9558 .add_timedelta(Timedelta::NAT)
9559 .is_nat()
9560 );
9561 }
9562
9563 #[test]
9564 fn timestamp_sub_timedelta_shifts_backward() {
9565 let ts = Timestamp::from_nanos(1_000);
9566 let shifted = ts.sub_timedelta(Timedelta::NANOS_PER_MICRO);
9567 assert_eq!(shifted.nanos, 0);
9568 }
9569
9570 #[test]
9571 fn timestamp_sub_timestamp_returns_timedelta_nanos() {
9572 let t0 = Timestamp::from_nanos(0);
9573 let t1 = Timestamp::from_nanos(Timedelta::NANOS_PER_HOUR);
9574 assert_eq!(t1.sub_timestamp(&t0), Timedelta::NANOS_PER_HOUR);
9575 assert_eq!(t0.sub_timestamp(&t1), -Timedelta::NANOS_PER_HOUR);
9576 }
9577
9578 #[test]
9579 fn timestamp_sub_timestamp_nat_propagates() {
9580 let ts = Timestamp::from_nanos(1_000);
9581 assert_eq!(Timestamp::nat().sub_timestamp(&ts), Timedelta::NAT);
9582 assert_eq!(ts.sub_timestamp(&Timestamp::nat()), Timedelta::NAT);
9583 }
9584
9585 #[test]
9586 fn timestamp_semantic_eq_treats_two_nat_as_equal() {
9587 assert!(Timestamp::nat().semantic_eq(&Timestamp::nat()));
9588 assert!(!Timestamp::nat().semantic_eq(&Timestamp::from_nanos(0)));
9589 assert!(!Timestamp::from_nanos(0).semantic_eq(&Timestamp::nat()));
9590 }
9591
9592 #[test]
9593 fn timestamp_partial_cmp_orders_by_nanos_nat_is_incomparable() {
9594 use std::cmp::Ordering;
9595 let a = Timestamp::from_nanos(0);
9596 let b = Timestamp::from_nanos(100);
9597 assert_eq!(a.partial_cmp(&b), Some(Ordering::Less));
9598 assert_eq!(b.partial_cmp(&a), Some(Ordering::Greater));
9599 assert_eq!(a.partial_cmp(&a), Some(Ordering::Equal));
9600 assert_eq!(a.partial_cmp(&Timestamp::nat()), None);
9601 assert_eq!(Timestamp::nat().partial_cmp(&Timestamp::nat()), None);
9602 }
9603
9604 #[test]
9605 fn timestamp_display_matches_phase2_debug_format() {
9606 assert_eq!(Timestamp::from_nanos(42).to_string(), "Timestamp[42, UTC]");
9607 assert_eq!(
9608 Timestamp::from_nanos_tz(42, "US/Eastern").to_string(),
9609 "Timestamp[42, US/Eastern]"
9610 );
9611 assert_eq!(Timestamp::nat().to_string(), "NaT");
9612 }
9613
9614 #[test]
9615 fn timestamp_value_and_unit_match_pandas_l0edr() {
9616 let ts = Timestamp::from_nanos(1_000_000_123);
9617 assert_eq!(ts.value(), 1_000_000_123);
9618 assert_eq!(ts.unit(), Some("ns"));
9619
9620 let nat = Timestamp::nat();
9621 assert_eq!(nat.value(), Timestamp::NAT);
9622 assert_eq!(nat.unit(), None);
9623 }
9624
9625 #[test]
9626 fn timestamp_numpy_datetime64_materializers_match_value_twksi() {
9627 let ts = Timestamp::from_nanos(1_000_000_123);
9628 assert_eq!(ts.asm8(), ts.value());
9629 assert_eq!(ts.to_datetime64(), ts.value());
9630 assert_eq!(ts.to_numpy(), ts.value());
9631
9632 let nat = Timestamp::nat();
9633 assert_eq!(nat.asm8(), Timestamp::NAT);
9634 assert_eq!(nat.to_datetime64(), Timestamp::NAT);
9635 assert_eq!(nat.to_numpy(), Timestamp::NAT);
9636 }
9637
9638 #[test]
9639 fn timestamp_timestamp_accessor_matches_pandas_microsecond_rounding_py0h3() {
9640 assert_eq!(Timestamp::from_nanos(0).timestamp(), Ok(0.0));
9641 assert_eq!(Timestamp::from_nanos(1_500_000_000).timestamp(), Ok(1.5));
9642 assert_eq!(Timestamp::from_nanos(500).timestamp(), Ok(0.0));
9643 assert_eq!(Timestamp::from_nanos(501).timestamp(), Ok(0.000001));
9644 assert_eq!(Timestamp::from_nanos(2_500).timestamp(), Ok(0.000003));
9645
9646 assert!(matches!(
9647 Timestamp::from_nanos(-500).timestamp(),
9648 Ok(value) if value == -0.0 && value.is_sign_negative()
9649 ));
9650 assert_eq!(Timestamp::from_nanos(-2_500).timestamp(), Ok(-0.000003));
9651 assert_eq!(
9652 Timestamp::nat().timestamp(),
9653 Err(TypeError::ValueIsMissing {
9654 kind: NullKind::NaT,
9655 })
9656 );
9657 }
9658
9659 #[test]
9660 fn timestamp_roundtrips_through_serde_json() {
9661 let naive = Timestamp::from_nanos(1_700_000_000_000_000_000);
9662 let json = serde_json::to_string(&naive).expect("serialize");
9663 let back: Timestamp = serde_json::from_str(&json).expect("deserialize");
9664 assert_eq!(naive, back);
9665
9666 let tz_aware = Timestamp::from_nanos_tz(1_700_000_000_000_000_000, "US/Eastern");
9667 let json = serde_json::to_string(&tz_aware).expect("serialize");
9668 let back: Timestamp = serde_json::from_str(&json).expect("deserialize");
9669 assert_eq!(tz_aware, back);
9670 }
9671
9672 #[test]
9673 fn timestamp_is_send_and_sync() {
9674 fn assert_send_sync<T: Send + Sync>() {}
9675 assert_send_sync::<Timestamp>();
9676 }
9677
9678 #[test]
9681 fn timestamp_pre_epoch_accessors_floor_not_truncate() {
9682 let ts = Timestamp::from_nanos(-1);
9686 assert_eq!(ts.year(), Some(1969));
9687 assert_eq!(ts.month(), Some(12));
9688 assert_eq!(ts.day(), Some(31));
9689 assert_eq!(ts.hour(), Some(23));
9690 assert_eq!(ts.minute(), Some(59));
9691 assert_eq!(ts.second(), Some(59));
9692 assert_eq!(ts.microsecond(), Some(999_999));
9693 assert_eq!(ts.nanosecond(), Some(999));
9694 assert_eq!(ts.dayofweek(), Some(2));
9696
9697 let noon = Timestamp::from_nanos(-43200 * Timedelta::NANOS_PER_SEC);
9699 assert_eq!(noon.year(), Some(1969));
9700 assert_eq!(noon.month(), Some(12));
9701 assert_eq!(noon.day(), Some(31));
9702 assert_eq!(noon.hour(), Some(12));
9703
9704 let midnight = Timestamp::from_nanos(-Timedelta::NANOS_PER_DAY);
9706 assert_eq!(midnight.year(), Some(1969));
9707 assert_eq!(midnight.day(), Some(31));
9708 assert_eq!(midnight.hour(), Some(0));
9709 }
9710
9711 #[test]
9712 fn timestamp_floor_to_rounds_down() {
9713 let h = Timedelta::NANOS_PER_HOUR;
9715 let twelve_h = h * 12;
9716 let twelve_thirty_four =
9717 twelve_h + Timedelta::NANOS_PER_MIN * 34 + Timedelta::NANOS_PER_SEC * 56;
9718 let ts = Timestamp::from_nanos(twelve_thirty_four);
9719 let floored = ts.floor_to(h);
9720 assert_eq!(floored.nanos, twelve_h);
9721 }
9722
9723 #[test]
9724 fn timestamp_floor_to_handles_already_aligned() {
9725 let h = Timedelta::NANOS_PER_HOUR;
9727 let twelve_h = h * 12;
9728 let ts = Timestamp::from_nanos(twelve_h);
9729 assert_eq!(ts.floor_to(h).nanos, twelve_h);
9730 }
9731
9732 #[test]
9733 fn timestamp_floor_to_handles_negative_nanos() {
9734 let ts = Timestamp::from_nanos(-100);
9738 assert_eq!(ts.floor_to(60).nanos, -120);
9739 }
9740
9741 #[test]
9742 fn timestamp_floor_to_returns_nat_on_axis_underflow_30hdi() {
9743 let ts = Timestamp::from_nanos(i64::MIN + 1);
9744 assert!(ts.floor_to(10).is_nat());
9745
9746 let safe = Timestamp::from_nanos(i64::MIN + 10);
9747 assert_eq!(safe.floor_to(10).nanos, i64::MIN + 8);
9748
9749 let tz = Timestamp::from_nanos_tz(-100, "UTC").floor_to(60);
9750 assert_eq!(tz.nanos, -120);
9751 assert_eq!(tz.tz.as_deref(), Some("UTC"));
9752 }
9753
9754 #[test]
9755 fn timestamp_ceil_to_rounds_up() {
9756 let h = Timedelta::NANOS_PER_HOUR;
9758 let twelve_h = h * 12;
9759 let thirteen_h = h * 13;
9760 let twelve_thirty_four =
9761 twelve_h + Timedelta::NANOS_PER_MIN * 34 + Timedelta::NANOS_PER_SEC * 56;
9762 let ts = Timestamp::from_nanos(twelve_thirty_four);
9763 assert_eq!(ts.ceil_to(h).nanos, thirteen_h);
9764 }
9765
9766 #[test]
9767 fn timestamp_ceil_to_no_op_on_aligned() {
9768 let h = Timedelta::NANOS_PER_HOUR;
9769 let twelve_h = h * 12;
9770 let ts = Timestamp::from_nanos(twelve_h);
9771 assert_eq!(ts.ceil_to(h).nanos, twelve_h);
9772 }
9773
9774 #[test]
9775 fn timestamp_round_to_rounds_to_nearest() {
9776 let h = Timedelta::NANOS_PER_HOUR;
9778 let twelve_h = h * 12;
9779 let twelve_thirty_one_sec =
9780 twelve_h + Timedelta::NANOS_PER_MIN * 30 + Timedelta::NANOS_PER_SEC;
9781 let ts = Timestamp::from_nanos(twelve_thirty_one_sec);
9782 assert_eq!(ts.round_to(h).nanos, h * 13);
9783
9784 let twelve_twenty_nine_sec =
9786 twelve_h + Timedelta::NANOS_PER_MIN * 29 + Timedelta::NANOS_PER_SEC * 59;
9787 let ts = Timestamp::from_nanos(twelve_twenty_nine_sec);
9788 assert_eq!(ts.round_to(h).nanos, twelve_h);
9789 }
9790
9791 #[test]
9792 fn timestamp_round_to_bankers_tie_to_even() {
9793 assert_eq!(Timestamp::from_nanos(5).round_to(10).nanos, 0);
9799 assert_eq!(Timestamp::from_nanos(15).round_to(10).nanos, 20);
9800 assert_eq!(Timestamp::from_nanos(25).round_to(10).nanos, 20);
9801 assert_eq!(Timestamp::from_nanos(35).round_to(10).nanos, 40);
9802 }
9803
9804 #[test]
9805 fn timestamp_round_to_zero_unit_returns_nat() {
9806 let ts = Timestamp::from_nanos(100);
9807 assert!(ts.round_to(0).is_nat());
9808 assert!(ts.floor_to(0).is_nat());
9809 assert!(ts.ceil_to(0).is_nat());
9810 }
9811
9812 #[test]
9813 fn timestamp_round_to_negative_unit_returns_nat() {
9814 let ts = Timestamp::from_nanos(100);
9815 assert!(ts.round_to(-10).is_nat());
9816 assert!(ts.floor_to(-10).is_nat());
9817 assert!(ts.ceil_to(-10).is_nat());
9818 }
9819
9820 #[test]
9821 fn timestamp_rounding_propagates_nat() {
9822 let nat = Timestamp::nat();
9823 assert!(nat.floor_to(60).is_nat());
9824 assert!(nat.ceil_to(60).is_nat());
9825 assert!(nat.round_to(60).is_nat());
9826 }
9827
9828 #[test]
9829 fn timestamp_rounding_preserves_tz() {
9830 let ts = Timestamp::from_nanos_tz(100, "US/Eastern");
9831 assert_eq!(ts.floor_to(60).tz.as_deref(), Some("US/Eastern"));
9832 assert_eq!(ts.ceil_to(60).tz.as_deref(), Some("US/Eastern"));
9833 assert_eq!(ts.round_to(60).tz.as_deref(), Some("US/Eastern"));
9834 }
9835
9836 #[test]
9839 fn timestamp_floor_to_unit_h_rounds_to_hour() {
9840 let h = Timedelta::NANOS_PER_HOUR;
9841 let twelve_h = h * 12;
9842 let twelve_thirty_four =
9843 twelve_h + Timedelta::NANOS_PER_MIN * 34 + Timedelta::NANOS_PER_SEC * 56;
9844 let ts = Timestamp::from_nanos(twelve_thirty_four);
9845 assert_eq!(ts.floor_to_unit("H").nanos, twelve_h);
9846 assert_eq!(ts.floor_to_unit("h").nanos, twelve_h);
9847 assert_eq!(ts.floor_to_unit("hour").nanos, twelve_h);
9848 assert_eq!(ts.floor_to_unit("hours").nanos, twelve_h);
9849 assert_eq!(ts.floor_to_unit("hr").nanos, twelve_h);
9850 }
9851
9852 #[test]
9853 fn timestamp_ceil_to_unit_d_rounds_to_day() {
9854 let h = Timedelta::NANOS_PER_HOUR;
9856 let d = Timedelta::NANOS_PER_DAY;
9857 let twelve_thirty_four = h * 12 + Timedelta::NANOS_PER_MIN * 34;
9858 let ts = Timestamp::from_nanos(twelve_thirty_four);
9859 assert_eq!(ts.ceil_to_unit("D").nanos, d);
9860 assert_eq!(ts.ceil_to_unit("day").nanos, d);
9861 assert_eq!(ts.ceil_to_unit("days").nanos, d);
9862 }
9863
9864 #[test]
9865 fn timestamp_round_to_unit_min_rounds_to_minute() {
9866 let m = Timedelta::NANOS_PER_MIN;
9868 let twelve_thirty_four_thirty_one =
9869 Timedelta::NANOS_PER_HOUR * 12 + m * 34 + Timedelta::NANOS_PER_SEC * 31;
9870 let ts = Timestamp::from_nanos(twelve_thirty_four_thirty_one);
9871 let expected = Timedelta::NANOS_PER_HOUR * 12 + m * 35;
9872 assert_eq!(ts.round_to_unit("min").nanos, expected);
9873 assert_eq!(ts.round_to_unit("T").nanos, expected); assert_eq!(ts.round_to_unit("minute").nanos, expected);
9875 }
9876
9877 #[test]
9878 fn timestamp_floor_ceil_round_aliases_match_unit_methods_li897() {
9879 let ts = Timestamp::from_nanos(
9880 Timedelta::NANOS_PER_HOUR * 12
9881 + Timedelta::NANOS_PER_MIN * 34
9882 + Timedelta::NANOS_PER_SEC * 31,
9883 );
9884
9885 assert_eq!(ts.floor("H"), ts.floor_to_unit("H"));
9886 assert_eq!(ts.ceil("D"), ts.ceil_to_unit("D"));
9887 assert_eq!(ts.round("min"), ts.round_to_unit("min"));
9888 }
9889
9890 #[test]
9891 fn timestamp_normalize_floors_to_day_and_preserves_tz_455op() {
9892 let ts = Timestamp::from_nanos_tz(
9893 Timedelta::NANOS_PER_DAY * 3
9894 + Timedelta::NANOS_PER_HOUR * 12
9895 + Timedelta::NANOS_PER_MIN * 34,
9896 "US/Eastern",
9897 );
9898 let normalized = ts.normalize();
9899
9900 assert_eq!(normalized.nanos, Timedelta::NANOS_PER_DAY * 3);
9901 assert_eq!(normalized.tz.as_deref(), Some("US/Eastern"));
9902 assert!(Timestamp::nat().normalize().is_nat());
9903 }
9904
9905 #[test]
9906 fn timestamp_unit_rounding_unknown_unit_returns_nat() {
9907 let ts = Timestamp::from_nanos(100);
9908 assert!(ts.floor_to_unit("fortnight").is_nat());
9909 assert!(ts.ceil_to_unit("century").is_nat());
9910 assert!(ts.round_to_unit("xyz").is_nat());
9911 }
9912
9913 #[test]
9914 fn timestamp_unit_rounding_propagates_nat() {
9915 let nat = Timestamp::nat();
9916 assert!(nat.floor_to_unit("H").is_nat());
9917 assert!(nat.ceil_to_unit("H").is_nat());
9918 assert!(nat.round_to_unit("H").is_nat());
9919 }
9920
9921 #[test]
9922 fn timestamp_unit_rounding_preserves_tz() {
9923 let ts = Timestamp::from_nanos_tz(Timedelta::NANOS_PER_HOUR * 12 + 100, "US/Eastern");
9924 assert_eq!(ts.floor_to_unit("H").tz.as_deref(), Some("US/Eastern"));
9925 assert_eq!(ts.ceil_to_unit("H").tz.as_deref(), Some("US/Eastern"));
9926 assert_eq!(ts.round_to_unit("H").tz.as_deref(), Some("US/Eastern"));
9927 }
9928
9929 #[test]
9930 fn timedelta_unit_to_nanos_is_now_public_and_matches_pandas_aliases() {
9931 assert_eq!(
9933 Timedelta::unit_to_nanos("W"),
9934 Some(Timedelta::NANOS_PER_WEEK)
9935 );
9936 assert_eq!(
9937 Timedelta::unit_to_nanos("D"),
9938 Some(Timedelta::NANOS_PER_DAY)
9939 );
9940 assert_eq!(
9941 Timedelta::unit_to_nanos("H"),
9942 Some(Timedelta::NANOS_PER_HOUR)
9943 );
9944 assert_eq!(
9945 Timedelta::unit_to_nanos("min"),
9946 Some(Timedelta::NANOS_PER_MIN)
9947 );
9948 assert_eq!(
9949 Timedelta::unit_to_nanos("s"),
9950 Some(Timedelta::NANOS_PER_SEC)
9951 );
9952 assert_eq!(
9953 Timedelta::unit_to_nanos("ms"),
9954 Some(Timedelta::NANOS_PER_MILLI)
9955 );
9956 assert_eq!(
9957 Timedelta::unit_to_nanos("us"),
9958 Some(Timedelta::NANOS_PER_MICRO)
9959 );
9960 assert_eq!(Timedelta::unit_to_nanos("ns"), Some(1));
9961 assert_eq!(Timedelta::unit_to_nanos(""), Some(Timedelta::NANOS_PER_DAY));
9963 assert_eq!(Timedelta::unit_to_nanos("century"), None);
9965 }
9966
9967 #[test]
9968 fn timestamp_isoformat_basic() {
9969 let ts = Timestamp::from_nanos(0);
9970 assert_eq!(ts.isoformat(), "1970-01-01T00:00:00");
9971
9972 let ts_utc = Timestamp::from_nanos_tz(0, "UTC");
9973 assert_eq!(ts_utc.isoformat(), "1970-01-01T00:00:00+00:00");
9974
9975 let ts_tz = Timestamp::from_nanos_tz(
9976 Timedelta::NANOS_PER_DAY
9977 + Timedelta::NANOS_PER_HOUR * 14
9978 + Timedelta::NANOS_PER_MIN * 30,
9979 "America/New_York",
9980 );
9981 assert!(ts_tz.isoformat().contains("1970-01-02T14:30:00"));
9982 assert!(ts_tz.isoformat().contains("[America/New_York]"));
9983
9984 assert_eq!(Timestamp::nat().isoformat(), "NaT");
9985 }
9986
9987 #[test]
9988 fn timestamp_isoformat_pre_epoch_subsecond_uses_floor_day_263m5() {
9989 assert_eq!(
9990 Timestamp::from_nanos(-1).isoformat(),
9991 "1969-12-31T23:59:59.999999999"
9992 );
9993 assert_eq!(
9994 Timestamp::from_nanos(-Timedelta::NANOS_PER_SEC).isoformat(),
9995 "1969-12-31T23:59:59"
9996 );
9997 assert_eq!(
9998 Timestamp::from_nanos(-Timedelta::NANOS_PER_DAY).isoformat(),
9999 "1969-12-31T00:00:00"
10000 );
10001 assert_eq!(
10002 Timestamp::from_nanos_tz(-1, "UTC").isoformat(),
10003 "1969-12-31T23:59:59.999999999+00:00"
10004 );
10005 }
10006
10007 #[test]
10008 fn timestamp_isoformat_preserves_nanosecond_fraction_4r99y() {
10009 assert_eq!(
10010 Timestamp::from_nanos(123_456_789).isoformat(),
10011 "1970-01-01T00:00:00.123456789"
10012 );
10013 assert_eq!(
10014 Timestamp::from_nanos(123_456_000).isoformat(),
10015 "1970-01-01T00:00:00.123456"
10016 );
10017 assert_eq!(
10018 Timestamp::from_nanos(123_000_000).isoformat(),
10019 "1970-01-01T00:00:00.123000"
10020 );
10021 assert_eq!(
10022 Timestamp::from_nanos_tz(1, "UTC").isoformat(),
10023 "1970-01-01T00:00:00.000000001+00:00"
10024 );
10025 }
10026
10027 #[test]
10028 fn timestamp_strftime_basic() {
10029 let ts = Timestamp::from_nanos(
10030 Timedelta::NANOS_PER_DAY * 365
10031 + Timedelta::NANOS_PER_HOUR * 9
10032 + Timedelta::NANOS_PER_MIN * 15,
10033 );
10034 assert_eq!(ts.strftime("%Y-%m-%d"), "1971-01-01");
10035 assert_eq!(ts.strftime("%H:%M:%S"), "09:15:00");
10036 assert_eq!(ts.strftime("%Y/%m/%d %H:%M"), "1971/01/01 09:15");
10037 assert_eq!(Timestamp::nat().strftime("%Y-%m-%d"), "NaT");
10038 }
10039
10040 #[test]
10041 fn timestamp_day_name_and_month_name() {
10042 let ts = Timestamp::from_nanos(0);
10043 assert_eq!(ts.day_name(), "Thursday");
10044 assert_eq!(ts.month_name(), "January");
10045
10046 let ts2 = Timestamp::from_nanos(Timedelta::NANOS_PER_DAY * 365);
10047 assert_eq!(ts2.day_name(), "Friday");
10048 assert_eq!(ts2.month_name(), "January");
10049
10050 assert_eq!(Timestamp::nat().day_name(), "NaT");
10051 assert_eq!(Timestamp::nat().month_name(), "NaT");
10052 }
10053
10054 #[test]
10055 fn timestamp_replace_validates_components_zw0y2() {
10056 let ts = Timestamp::parse("2024-01-15T10:30:45.123456789").unwrap();
10057 let replaced = ts.replace(
10058 Some(2024),
10059 Some(2),
10060 Some(29),
10061 Some(23),
10062 Some(59),
10063 Some(58),
10064 Some(987_654),
10065 Some(321),
10066 );
10067 assert_eq!(replaced.year(), Some(2024));
10068 assert_eq!(replaced.month(), Some(2));
10069 assert_eq!(replaced.day(), Some(29));
10070 assert_eq!(replaced.hour(), Some(23));
10071 assert_eq!(replaced.minute(), Some(59));
10072 assert_eq!(replaced.second(), Some(58));
10073 assert_eq!(replaced.microsecond(), Some(987_654));
10074 assert_eq!(replaced.nanosecond(), Some(321));
10075
10076 let tz = Timestamp::from_nanos_tz(ts.nanos, "UTC");
10077 assert_eq!(
10078 tz.replace(None, Some(2), Some(29), None, None, None, None, None)
10079 .tz
10080 .as_deref(),
10081 Some("UTC")
10082 );
10083
10084 assert!(
10085 ts.replace(None, Some(13), None, None, None, None, None, None)
10086 .is_nat()
10087 );
10088 assert!(
10089 ts.replace(Some(2023), Some(2), Some(29), None, None, None, None, None)
10090 .is_nat()
10091 );
10092 assert!(
10093 ts.replace(None, None, None, Some(24), None, None, None, None)
10094 .is_nat()
10095 );
10096 assert!(
10097 ts.replace(None, None, None, None, Some(60), None, None, None)
10098 .is_nat()
10099 );
10100 assert!(
10101 ts.replace(None, None, None, None, None, Some(60), None, None)
10102 .is_nat()
10103 );
10104 assert!(
10105 ts.replace(None, None, None, None, None, None, Some(1_000_000), None)
10106 .is_nat()
10107 );
10108 assert!(
10109 ts.replace(None, None, None, None, None, None, None, Some(1_000))
10110 .is_nat()
10111 );
10112 assert!(
10113 Timestamp::nat()
10114 .replace(Some(2024), Some(1), Some(1), None, None, None, None, None)
10115 .is_nat()
10116 );
10117 }
10118
10119 #[test]
10120 fn timestamp_component_accessors() {
10121 let ts = Timestamp::from_nanos(0);
10122 assert_eq!(ts.year(), Some(1970));
10123 assert_eq!(ts.month(), Some(1));
10124 assert_eq!(ts.day(), Some(1));
10125 assert_eq!(ts.hour(), Some(0));
10126 assert_eq!(ts.minute(), Some(0));
10127 assert_eq!(ts.second(), Some(0));
10128 assert_eq!(ts.microsecond(), Some(0));
10129 assert_eq!(ts.nanosecond(), Some(0));
10130
10131 let ts2 = Timestamp::from_nanos(
10132 Timedelta::NANOS_PER_DAY * 365
10133 + Timedelta::NANOS_PER_HOUR * 14
10134 + Timedelta::NANOS_PER_MIN * 30
10135 + Timedelta::NANOS_PER_SEC * 45
10136 + 123_456_789,
10137 );
10138 assert_eq!(ts2.year(), Some(1971));
10139 assert_eq!(ts2.month(), Some(1));
10140 assert_eq!(ts2.day(), Some(1));
10141 assert_eq!(ts2.hour(), Some(14));
10142 assert_eq!(ts2.minute(), Some(30));
10143 assert_eq!(ts2.second(), Some(45));
10144 assert_eq!(ts2.microsecond(), Some(123456));
10145 assert_eq!(ts2.nanosecond(), Some(789));
10146
10147 assert_eq!(Timestamp::nat().year(), None);
10148 assert_eq!(Timestamp::nat().month(), None);
10149 assert_eq!(Timestamp::nat().day(), None);
10150 }
10151
10152 #[test]
10153 fn timestamp_dayofweek_dayofyear_quarter() {
10154 let ts = Timestamp::from_nanos(0);
10155 assert_eq!(ts.dayofweek(), Some(3));
10156 assert_eq!(ts.weekday(), Some(3));
10157 assert_eq!(ts.dayofyear(), Some(1));
10158 assert_eq!(ts.quarter(), Some(1));
10159
10160 let ts2 = Timestamp::from_nanos(Timedelta::NANOS_PER_DAY * 90);
10161 assert_eq!(ts2.quarter(), Some(2));
10162
10163 let ts3 = Timestamp::from_nanos(Timedelta::NANOS_PER_DAY * 365);
10164 assert_eq!(ts3.dayofyear(), Some(1));
10165 assert_eq!(ts3.dayofweek(), Some(4));
10166
10167 assert_eq!(Timestamp::nat().dayofweek(), None);
10168 assert_eq!(Timestamp::nat().dayofyear(), None);
10169 assert_eq!(Timestamp::nat().quarter(), None);
10170 }
10171
10172 #[test]
10173 fn timestamp_is_boundary_methods() {
10174 let jan1 = Timestamp::from_nanos(0);
10175 assert_eq!(jan1.is_leap_year(), Some(false));
10176 assert_eq!(jan1.is_month_start(), Some(true));
10177 assert_eq!(jan1.is_month_end(), Some(false));
10178 assert_eq!(jan1.is_quarter_start(), Some(true));
10179 assert_eq!(jan1.is_quarter_end(), Some(false));
10180 assert_eq!(jan1.is_year_start(), Some(true));
10181 assert_eq!(jan1.is_year_end(), Some(false));
10182
10183 let dec31 = Timestamp::from_nanos(Timedelta::NANOS_PER_DAY * 364);
10184 assert_eq!(dec31.is_month_start(), Some(false));
10185 assert_eq!(dec31.is_month_end(), Some(true));
10186 assert_eq!(dec31.is_quarter_end(), Some(true));
10187 assert_eq!(dec31.is_year_end(), Some(true));
10188
10189 assert_eq!(Timestamp::nat().is_leap_year(), None);
10190 assert_eq!(Timestamp::nat().is_month_start(), None);
10191 }
10192
10193 #[test]
10194 fn timestamp_days_in_month() {
10195 let jan = Timestamp::from_nanos(0);
10196 assert_eq!(jan.days_in_month(), Some(31));
10197 assert_eq!(jan.daysinmonth(), Some(31));
10198
10199 let feb_non_leap = Timestamp::from_nanos(Timedelta::NANOS_PER_DAY * 31);
10200 assert_eq!(feb_non_leap.days_in_month(), Some(28));
10201
10202 assert_eq!(Timestamp::nat().days_in_month(), None);
10203 }
10204
10205 #[test]
10206 fn timestamp_weekofyear() {
10207 let jan1 = Timestamp::from_nanos(0);
10208 assert_eq!(jan1.weekofyear(), Some(1));
10209 assert_eq!(jan1.week(), Some(1));
10210
10211 let jan8 = Timestamp::from_nanos(Timedelta::NANOS_PER_DAY * 7);
10212 assert_eq!(jan8.weekofyear(), Some(2));
10213
10214 assert_eq!(Timestamp::nat().weekofyear(), None);
10215 assert_eq!(Timestamp::nat().week(), None);
10216 }
10217
10218 #[test]
10219 fn timestamp_weekofyear_iso_53_week_boundaries() {
10220 fn week_of(date_days: i64) -> Option<i64> {
10223 Timestamp::from_nanos(date_days * Timedelta::NANOS_PER_DAY).weekofyear()
10224 }
10225 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)); }
10239
10240 #[test]
10241 fn iso_weeks_in_year_53_week_years() {
10242 use super::iso_weeks_in_year;
10243 for y in [2004, 2009, 2015, 2020, 2026] {
10245 assert_eq!(iso_weeks_in_year(y), 53, "{y} should have 53 ISO weeks");
10246 }
10247 for y in [2018, 2019, 2021, 2022, 2023, 2024] {
10248 assert_eq!(iso_weeks_in_year(y), 52, "{y} should have 52 ISO weeks");
10249 }
10250 }
10251
10252 #[test]
10253 fn timestamp_to_unit() {
10254 let ts = Timestamp::from_nanos(1_000_000_000);
10255 assert_eq!(ts.to_unit("ns"), Some(1_000_000_000));
10256 assert_eq!(ts.to_unit("us"), Some(1_000_000));
10257 assert_eq!(ts.to_unit("ms"), Some(1_000));
10258 assert_eq!(ts.to_unit("s"), Some(1));
10259 assert_eq!(ts.to_unit("invalid"), None);
10260
10261 assert_eq!(Timestamp::nat().to_unit("ns"), None);
10262 }
10263
10264 #[test]
10265 fn timestamp_toordinal() {
10266 let nanos_2026_01_01 = 19723_i64 * 24 * 60 * 60 * 1_000_000_000;
10269 let ts = Timestamp::from_nanos(nanos_2026_01_01);
10270 assert_eq!(ts.toordinal(), Some(738886));
10271
10272 assert_eq!(Timestamp::nat().toordinal(), None);
10274 }
10275
10276 #[test]
10277 fn timestamp_fromordinal() {
10278 let nanos_2026_01_01 = 19723_i64 * 24 * 60 * 60 * 1_000_000_000;
10281 let ts_orig = Timestamp::from_nanos(nanos_2026_01_01);
10282 let ordinal = ts_orig.toordinal().unwrap();
10283
10284 let ts = Timestamp::fromordinal(ordinal);
10286 assert_eq!(ts.year(), ts_orig.year());
10287 assert_eq!(ts.month(), ts_orig.month());
10288 assert_eq!(ts.day(), ts_orig.day());
10289
10290 let nat = Timestamp::fromordinal(0);
10292 assert!(nat.is_nat());
10293 }
10294
10295 #[test]
10296 fn timestamp_fromordinal_guards_nanosecond_overflow_ycvrd() {
10297 const EPOCH_ORDINAL: i64 = 719_163;
10298 let max_day_offset = i64::MAX / Timedelta::NANOS_PER_DAY;
10299 let max_valid_ordinal = EPOCH_ORDINAL + max_day_offset;
10300
10301 let max_valid = Timestamp::fromordinal(max_valid_ordinal);
10302 assert!(!max_valid.is_nat());
10303 assert_eq!(max_valid.nanos, max_day_offset * Timedelta::NANOS_PER_DAY);
10304
10305 assert!(Timestamp::fromordinal(max_valid_ordinal + 1).is_nat());
10306 assert!(Timestamp::fromordinal(i64::MAX).is_nat());
10307 }
10308
10309 #[test]
10310 fn timestamp_ordinal_matches_seeded_epoch_oracle_l2f0p() {
10311 const EPOCH_ORDINAL: i64 = 719_163;
10312 const DAY: i64 = Timedelta::NANOS_PER_DAY;
10313
10314 fn next(seed: &mut u64) -> u64 {
10315 *seed = seed
10316 .wrapping_mul(3202034522624059733)
10317 .wrapping_add(4354685564936845319);
10318 *seed
10319 }
10320
10321 fn assert_ordinal_case(case: usize, day_offset: i64, subday_nanos: i64) {
10322 let nanos = day_offset.saturating_mul(DAY).saturating_add(subday_nanos);
10323 let ts = Timestamp::from_nanos(nanos);
10324 let expected_day_offset = nanos.div_euclid(DAY);
10325 let expected_ordinal = EPOCH_ORDINAL + expected_day_offset;
10326
10327 assert_eq!(
10328 ts.toordinal(),
10329 Some(expected_ordinal),
10330 "case {case}: toordinal"
10331 );
10332
10333 let midnight = Timestamp::fromordinal(expected_ordinal);
10334 assert_eq!(
10335 midnight.nanos,
10336 expected_day_offset * DAY,
10337 "case {case}: fromordinal nanos"
10338 );
10339 assert_eq!(
10340 midnight.toordinal(),
10341 Some(expected_ordinal),
10342 "case {case}: fromordinal roundtrip"
10343 );
10344 }
10345
10346 assert_eq!(Timestamp::nat().toordinal(), None);
10347 assert!(Timestamp::fromordinal(0).is_nat());
10348 assert!(Timestamp::fromordinal(-1).is_nat());
10349
10350 assert_ordinal_case(usize::MAX, -1, DAY - 1);
10351 assert_ordinal_case(usize::MAX - 1, 0, -1);
10352 assert_ordinal_case(usize::MAX - 2, 19_723, 0);
10353
10354 let mut seed = 0x1f20_f0d1_0a11_0d1e_u64;
10355 for case in 0..260 {
10356 let day_offset = (next(&mut seed) % 40_001) as i64 - 10_000;
10357 let subday_nanos = match case % 7 {
10358 0 => 0,
10359 1 => DAY - 1,
10360 2 => -1,
10361 _ => (next(&mut seed) % (2 * DAY as u64 - 1)) as i64 - (DAY - 1),
10362 };
10363 assert_ordinal_case(case, day_offset, subday_nanos);
10364 }
10365 }
10366
10367 #[test]
10368 fn timestamp_parse_iso8601_date_only() {
10369 let ts = Timestamp::parse("2024-01-15").unwrap();
10370 assert_eq!(ts.year(), Some(2024));
10371 assert_eq!(ts.month(), Some(1));
10372 assert_eq!(ts.day(), Some(15));
10373 assert_eq!(ts.hour(), Some(0));
10374 assert_eq!(ts.minute(), Some(0));
10375 assert_eq!(ts.second(), Some(0));
10376 }
10377
10378 #[test]
10379 fn timestamp_parse_iso8601_datetime() {
10380 let ts = Timestamp::parse("2024-01-15T10:30:45").unwrap();
10381 assert_eq!(ts.year(), Some(2024));
10382 assert_eq!(ts.month(), Some(1));
10383 assert_eq!(ts.day(), Some(15));
10384 assert_eq!(ts.hour(), Some(10));
10385 assert_eq!(ts.minute(), Some(30));
10386 assert_eq!(ts.second(), Some(45));
10387 }
10388
10389 #[test]
10390 fn timestamp_parse_space_separator() {
10391 let ts = Timestamp::parse("2024-01-15 10:30:45").unwrap();
10392 assert_eq!(ts.year(), Some(2024));
10393 assert_eq!(ts.hour(), Some(10));
10394 }
10395
10396 #[test]
10397 fn timestamp_parse_with_fractional_seconds() {
10398 let ts = Timestamp::parse("2024-01-15T10:30:45.123456789").unwrap();
10399 assert_eq!(ts.second(), Some(45));
10400 assert_eq!(ts.microsecond(), Some(123456));
10401 assert_eq!(ts.nanosecond(), Some(789));
10402 }
10403
10404 #[test]
10405 fn timestamp_parse_utc_timezone() {
10406 let ts = Timestamp::parse("2024-01-15T10:30:45Z").unwrap();
10407 assert_eq!(ts.tz, Some("UTC".to_string()));
10408 }
10409
10410 #[test]
10411 fn timestamp_parse_offset_timezone() {
10412 let ts = Timestamp::parse("2024-01-15T10:30:45+05:30").unwrap();
10413 assert_eq!(ts.tz, Some("+05:30".to_string()));
10414
10415 let ts = Timestamp::parse("2024-01-15T10:30:45-05:30").unwrap();
10416 assert_eq!(ts.tz, Some("-05:30".to_string()));
10417 }
10418
10419 #[test]
10420 fn timestamp_parse_rejects_invalid_timezone_offsets_0v676() {
10421 assert!(Timestamp::parse("2024-01-15T10:30:45+bad").is_err());
10422 assert!(Timestamp::parse("2024-01-15T10:30:45+0500").is_err());
10423 assert!(Timestamp::parse("2024-01-15T10:30:45+24:00").is_err());
10424 assert!(Timestamp::parse("2024-01-15T10:30:45+05:60").is_err());
10425 assert!(Timestamp::parse("2024-01-15T10:30:45-25:00").is_err());
10426 }
10427
10428 #[test]
10429 fn timestamp_parse_nat() {
10430 let ts = Timestamp::parse("NaT").unwrap();
10431 assert!(ts.is_nat());
10432 let ts2 = Timestamp::parse("nat").unwrap();
10433 assert!(ts2.is_nat());
10434 }
10435
10436 #[test]
10437 fn timestamp_parse_invalid() {
10438 assert!(Timestamp::parse("not a date").is_err());
10439 assert!(Timestamp::parse("2024-13-01").is_err()); assert!(Timestamp::parse("2024-01-32").is_err()); }
10442
10443 #[test]
10444 fn timestamp_parse_rejects_invalid_fractional_seconds_87se2() {
10445 assert!(Timestamp::parse("2024-01-15T10:30:45.").is_err());
10446 assert!(Timestamp::parse("2024-01-15T10:30:45.abc").is_err());
10447 assert!(Timestamp::parse("2024-01-15T10:30:45.-1").is_err());
10448 assert!(Timestamp::parse("2024-01-15T10:30:45.123").is_err());
10449
10450 let ts = Timestamp::parse("2024-01-15T10:30:45.123456789987").unwrap();
10451 assert_eq!(ts.second(), Some(45));
10452 assert_eq!(ts.microsecond(), Some(123456));
10453 assert_eq!(ts.nanosecond(), Some(789));
10454 }
10455
10456 #[test]
10457 fn timestamp_parse_matches_seeded_iso_component_oracle_1u7a0() {
10458 fn next(seed: &mut u64) -> u64 {
10459 *seed = seed
10460 .wrapping_mul(3935559000370003845)
10461 .wrapping_add(2691343689449507681);
10462 *seed
10463 }
10464
10465 fn leap(year: i64) -> bool {
10466 (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
10467 }
10468
10469 fn month_len(year: i64, month: u32) -> u32 {
10470 match month {
10471 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
10472 4 | 6 | 9 | 11 => 30,
10473 2 if leap(year) => 29,
10474 2 => 28,
10475 _ => 0,
10476 }
10477 }
10478
10479 struct Components {
10480 year: i64,
10481 month: u32,
10482 day: u32,
10483 hour: u32,
10484 minute: u32,
10485 second: u32,
10486 nanos: u64,
10487 }
10488
10489 fn assert_components(case: usize, text: &str, expected: Components) {
10490 let ts = Timestamp::parse(text).expect("seeded valid timestamp");
10491 let Components {
10492 year,
10493 month,
10494 day,
10495 hour,
10496 minute,
10497 second,
10498 nanos,
10499 } = expected;
10500 assert_eq!(ts.year(), Some(year), "case {case}: year");
10501 assert_eq!(ts.month(), Some(month as i64), "case {case}: month");
10502 assert_eq!(ts.day(), Some(day as i64), "case {case}: day");
10503 assert_eq!(ts.hour(), Some(hour as i64), "case {case}: hour");
10504 assert_eq!(ts.minute(), Some(minute as i64), "case {case}: minute");
10505 assert_eq!(ts.second(), Some(second as i64), "case {case}: second");
10506 assert_eq!(
10507 ts.microsecond(),
10508 Some((nanos / 1000) as i64),
10509 "case {case}: microsecond"
10510 );
10511 assert_eq!(
10512 ts.nanosecond(),
10513 Some((nanos % 1000) as i64),
10514 "case {case}: nanosecond"
10515 );
10516 }
10517
10518 assert!(Timestamp::parse("NaT").expect("NaT parses").is_nat());
10519 assert!(
10520 Timestamp::parse("nAt")
10521 .expect("mixed-case NaT parses")
10522 .is_nat()
10523 );
10524 assert!(Timestamp::parse("1900-02-29").is_err());
10525 assert!(Timestamp::parse("2001-04-31").is_err());
10526 assert!(Timestamp::parse("2024-00-15").is_err());
10527
10528 assert_components(
10529 usize::MAX,
10530 "2000-02-29",
10531 Components {
10532 year: 2000,
10533 month: 2,
10534 day: 29,
10535 hour: 0,
10536 minute: 0,
10537 second: 0,
10538 nanos: 0,
10539 },
10540 );
10541 assert_components(
10542 usize::MAX - 1,
10543 "2024-02-29T23:59:59.000000001",
10544 Components {
10545 year: 2024,
10546 month: 2,
10547 day: 29,
10548 hour: 23,
10549 minute: 59,
10550 second: 59,
10551 nanos: 1,
10552 },
10553 );
10554
10555 let mut seed = 0x15e0_1d50_1f0a_cade_u64;
10556 for case in 0..260 {
10557 let year = 1900 + (next(&mut seed) % 201) as i64;
10558 let month = 1 + (next(&mut seed) % 12) as u32;
10559 let day = 1 + (next(&mut seed) % month_len(year, month) as u64) as u32;
10560 let hour = (next(&mut seed) % 24) as u32;
10561 let minute = (next(&mut seed) % 60) as u32;
10562 let second = (next(&mut seed) % 60) as u32;
10563 let nanos = next(&mut seed) % 1_000_000_000;
10564
10565 match case % 4 {
10566 0 => {
10567 let text = format!("{year:04}-{month:02}-{day:02}");
10568 assert_components(
10569 case,
10570 &text,
10571 Components {
10572 year,
10573 month,
10574 day,
10575 hour: 0,
10576 minute: 0,
10577 second: 0,
10578 nanos: 0,
10579 },
10580 );
10581 }
10582 1 => {
10583 let text =
10584 format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}");
10585 assert_components(
10586 case,
10587 &text,
10588 Components {
10589 year,
10590 month,
10591 day,
10592 hour,
10593 minute,
10594 second,
10595 nanos: 0,
10596 },
10597 );
10598 }
10599 2 => {
10600 let text =
10601 format!("{year:04}-{month:02}-{day:02} {hour:02}:{minute:02}:{second:02}");
10602 assert_components(
10603 case,
10604 &text,
10605 Components {
10606 year,
10607 month,
10608 day,
10609 hour,
10610 minute,
10611 second,
10612 nanos: 0,
10613 },
10614 );
10615 }
10616 _ => {
10617 let text = format!(
10618 "{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}.{nanos:09}"
10619 );
10620 assert_components(
10621 case,
10622 &text,
10623 Components {
10624 year,
10625 month,
10626 day,
10627 hour,
10628 minute,
10629 second,
10630 nanos,
10631 },
10632 );
10633 }
10634 }
10635 }
10636 }
10637
10638 #[test]
10639 fn period_parse_annual() {
10640 let p = Period::parse("2024").unwrap();
10641 assert_eq!(p.freq(), PeriodFreq::Annual);
10642 assert_eq!(p.ordinal(), 2024 - 1970);
10643 }
10644
10645 #[test]
10646 fn period_parse_quarterly() {
10647 let p = Period::parse("2024Q1").unwrap();
10648 assert_eq!(p.freq(), PeriodFreq::Quarterly);
10649 assert_eq!(p.ordinal(), (2024 - 1970) * 4);
10650
10651 let p2 = Period::parse("2024q3").unwrap();
10652 assert_eq!(p2.freq(), PeriodFreq::Quarterly);
10653 assert_eq!(p2.ordinal(), (2024 - 1970) * 4 + 2);
10654 }
10655
10656 #[test]
10657 fn period_parse_monthly() {
10658 let p = Period::parse("2024-01").unwrap();
10659 assert_eq!(p.freq(), PeriodFreq::Monthly);
10660 assert_eq!(p.ordinal(), (2024 - 1970) * 12);
10661
10662 let p2 = Period::parse("2024-12").unwrap();
10663 assert_eq!(p2.freq(), PeriodFreq::Monthly);
10664 assert_eq!(p2.ordinal(), (2024 - 1970) * 12 + 11);
10665 }
10666
10667 #[test]
10668 fn period_parse_nat() {
10669 let p = Period::parse("NaT").unwrap();
10670 assert_eq!(p.ordinal(), i64::MIN);
10671 }
10672
10673 #[test]
10674 fn period_parse_invalid() {
10675 assert!(Period::parse("not a period").is_err());
10676 assert!(Period::parse("2024Q5").is_err()); assert!(Period::parse("2024-13").is_err()); }
10679
10680 #[test]
10681 fn interval_parse_basic() {
10682 let i = Interval::parse("[0, 1]").unwrap();
10683 assert_eq!(i.left, 0.0);
10684 assert_eq!(i.right, 1.0);
10685 assert_eq!(i.closed, IntervalClosed::Both);
10686
10687 let i2 = Interval::parse("(0, 1)").unwrap();
10688 assert_eq!(i2.left, 0.0);
10689 assert_eq!(i2.right, 1.0);
10690 assert_eq!(i2.closed, IntervalClosed::Neither);
10691
10692 let i3 = Interval::parse("[0, 1)").unwrap();
10693 assert_eq!(i3.closed, IntervalClosed::Left);
10694
10695 let i4 = Interval::parse("(0, 1]").unwrap();
10696 assert_eq!(i4.closed, IntervalClosed::Right);
10697 }
10698
10699 #[test]
10700 fn interval_parse_floats() {
10701 let i = Interval::parse("[-1.5, 2.5)").unwrap();
10702 assert_eq!(i.left, -1.5);
10703 assert_eq!(i.right, 2.5);
10704 assert_eq!(i.closed, IntervalClosed::Left);
10705 }
10706
10707 #[test]
10708 fn interval_parse_invalid() {
10709 assert!(Interval::parse("invalid").is_err());
10710 assert!(Interval::parse("[0]").is_err());
10711 assert!(Interval::parse("0, 1").is_err()); }
10713}