Skip to main content

polars_core/series/
mod.rs

1#![allow(unsafe_op_in_unsafe_fn)]
2//! Type agnostic columnar data structure.
3use crate::chunked_array::flags::StatisticsFlags;
4pub use crate::prelude::ChunkCompareEq;
5use crate::prelude::*;
6use crate::{HEAD_DEFAULT_LENGTH, TAIL_DEFAULT_LENGTH};
7
8macro_rules! invalid_operation_panic {
9    ($op:ident, $s:expr) => {
10        panic!(
11            "`{}` operation not supported for dtype `{}`",
12            stringify!($op),
13            $s._dtype()
14        )
15    };
16}
17
18pub mod amortized_iter;
19mod any_value;
20pub mod arithmetic;
21pub mod arrow_export;
22pub mod builder;
23
24mod comparison;
25mod from;
26pub mod implementations;
27pub(crate) mod iterator;
28pub mod ops;
29#[cfg(feature = "proptest")]
30pub mod proptest;
31mod series_trait;
32
33use std::borrow::Cow;
34use std::hash::{Hash, Hasher};
35use std::ops::Deref;
36
37pub use from::*;
38pub use iterator::SeriesIter;
39use num_traits::NumCast;
40use polars_arrow::compute::aggregate::estimated_bytes_size;
41use polars_error::feature_gated;
42use polars_utils::broadcast::BroadcastLength;
43use polars_utils::float::IsFloat;
44pub use series_trait::{IsSorted, *};
45
46use crate::chunked_array::cast::CastOptions;
47use crate::runtime::RAYON;
48#[cfg(feature = "zip_with")]
49use crate::series::arithmetic::coerce_lhs_rhs;
50use crate::utils::{Wrap, handle_casting_failures, materialize_dyn_int};
51
52/// # Series
53/// The columnar data type for a DataFrame.
54///
55/// Most of the available functions are defined in the [SeriesTrait trait](crate::series::SeriesTrait).
56///
57/// The `Series` struct consists
58/// of typed [ChunkedArray]'s. To quickly cast
59/// a `Series` to a `ChunkedArray` you can call the method with the name of the type:
60///
61/// ```
62/// # use polars_core::prelude::*;
63/// let s: Series = [1, 2, 3].iter().collect();
64/// // Quickly obtain the ChunkedArray wrapped by the Series.
65/// let chunked_array = s.i32().unwrap();
66/// ```
67///
68/// ## Arithmetic
69///
70/// You can do standard arithmetic on series.
71/// ```
72/// # use polars_core::prelude::*;
73/// let s = Series::new("a".into(), [1 , 2, 3]);
74/// let out_add = &s + &s;
75/// let out_sub = &s - &s;
76/// let out_div = &s / &s;
77/// let out_mul = &s * &s;
78/// ```
79///
80/// Or with series and numbers.
81///
82/// ```
83/// # use polars_core::prelude::*;
84/// let s: Series = (1..3).collect();
85/// let out_add_one = &s + 1;
86/// let out_multiply = &s * 10;
87///
88/// // Could not overload left hand side operator.
89/// let out_divide = 1.div(&s);
90/// let out_add = 1.add(&s);
91/// let out_subtract = 1.sub(&s);
92/// let out_multiply = 1.mul(&s);
93/// ```
94///
95/// ## Comparison
96/// You can obtain boolean mask by comparing series.
97///
98/// ```
99/// # use polars_core::prelude::*;
100/// let s = Series::new("dollars".into(), &[1, 2, 3]);
101/// let mask = s.equal(1).unwrap();
102/// let valid = [true, false, false].iter();
103/// assert!(mask
104///     .iter()
105///     .map(|opt_bool| opt_bool.unwrap()) // option, because series can be null
106///     .zip(valid)
107///     .all(|(a, b)| a == *b))
108/// ```
109///
110/// See all the comparison operators in the [ChunkCompareEq trait](crate::chunked_array::ops::ChunkCompareEq) and
111/// [ChunkCompareIneq trait](crate::chunked_array::ops::ChunkCompareIneq).
112///
113/// ## Iterators
114/// The Series variants contain differently typed [ChunkedArray]s.
115/// These structs can be turned into iterators, making it possible to use any function/ closure you want
116/// on a Series.
117///
118/// These iterators return an `Option<T>` because the values of a series may be null.
119///
120/// ```
121/// use polars_core::prelude::*;
122/// let pi = 3.14;
123/// let s = Series::new("angle".into(), [2f32 * pi, pi, 1.5 * pi].as_ref());
124/// let s_cos: Series = s.f32()
125///                     .expect("series was not an f32 dtype")
126///                     .iter()
127///                     .map(|opt_angle| opt_angle.map(|angle| angle.cos()))
128///                     .collect();
129/// ```
130///
131/// ## Creation
132/// Series can be create from different data structures. Below we'll show a few ways we can create
133/// a Series object.
134///
135/// ```
136/// # use polars_core::prelude::*;
137/// // Series can be created from Vec's, slices and arrays
138/// Series::new("boolean series".into(), &[true, false, true]);
139/// Series::new("int series".into(), &[1, 2, 3]);
140/// // And can be nullable
141/// Series::new("got nulls".into(), &[Some(1), None, Some(2)]);
142///
143/// // Series can also be collected from iterators
144/// let from_iter: Series = (0..10)
145///     .into_iter()
146///     .collect();
147///
148/// ```
149#[derive(Clone)]
150#[must_use]
151pub struct Series(pub Arc<dyn SeriesTrait>);
152
153impl PartialEq for Wrap<Series> {
154    fn eq(&self, other: &Self) -> bool {
155        self.0.equals_missing(other)
156    }
157}
158
159impl Eq for Wrap<Series> {}
160
161impl Hash for Wrap<Series> {
162    fn hash<H: Hasher>(&self, state: &mut H) {
163        self.dtype().hash(state);
164        self.len().hash(state);
165
166        for av in self.iter() {
167            av.hash(state);
168        }
169    }
170}
171
172impl Series {
173    /// Create a new empty Series.
174    pub fn new_empty(name: PlSmallStr, dtype: &DataType) -> Series {
175        Series::full_null(name, 0, dtype)
176    }
177
178    pub fn clear(&self) -> Series {
179        if self.is_empty() {
180            self.clone()
181        } else {
182            match self.dtype() {
183                #[cfg(feature = "object")]
184                DataType::Object(_) => self
185                    .take(&ChunkedArray::<IdxType>::new_vec(PlSmallStr::EMPTY, vec![]))
186                    .unwrap(),
187                dt => Series::new_empty(self.name().clone(), dt),
188            }
189        }
190    }
191
192    #[doc(hidden)]
193    pub fn _get_inner_mut(&mut self) -> &mut dyn SeriesTrait {
194        if Arc::weak_count(&self.0) + Arc::strong_count(&self.0) != 1 {
195            self.0 = self.0.clone_inner();
196        }
197        Arc::get_mut(&mut self.0).expect("implementation error")
198    }
199
200    /// Take or clone a owned copy of the inner [`ChunkedArray`].
201    pub fn take_inner<T: PolarsPhysicalType>(self) -> ChunkedArray<T> {
202        let arc_any = self.0.as_arc_any();
203        let downcast = arc_any
204            .downcast::<implementations::SeriesWrap<ChunkedArray<T>>>()
205            .unwrap();
206
207        match Arc::try_unwrap(downcast) {
208            Ok(ca) => ca.0,
209            Err(ca) => ca.as_ref().as_ref().clone(),
210        }
211    }
212
213    /// Returns a reference to the Arrow ArrayRef
214    #[inline]
215    pub fn array_ref(&self, chunk_idx: usize) -> &ArrayRef {
216        &self.chunks()[chunk_idx] as &ArrayRef
217    }
218
219    /// # Safety
220    /// The caller must ensure the length and the data types of `ArrayRef` does not change.
221    /// And that the null_count is updated (e.g. with a `compute_len()`)
222    pub unsafe fn chunks_mut(&mut self) -> &mut Vec<ArrayRef> {
223        #[allow(unused_mut)]
224        let mut ca = self._get_inner_mut();
225        ca.chunks_mut()
226    }
227
228    pub fn into_chunks(mut self) -> Vec<ArrayRef> {
229        let ca = self._get_inner_mut();
230        let chunks = std::mem::take(unsafe { ca.chunks_mut() });
231        ca.compute_len();
232        chunks
233    }
234
235    // TODO! this probably can now be removed, now we don't have special case for structs.
236    pub fn select_chunk(&self, i: usize) -> Self {
237        let mut new = self.clear();
238        let mut flags = self.get_flags();
239
240        use StatisticsFlags as F;
241        flags &= F::IS_SORTED_ANY | F::CAN_FAST_EXPLODE_LIST;
242
243        // Assign mut so we go through arc only once.
244        let mut_new = new._get_inner_mut();
245        let chunks = unsafe { mut_new.chunks_mut() };
246        let chunk = self.chunks()[i].clone();
247        chunks.clear();
248        chunks.push(chunk);
249        mut_new.compute_len();
250        mut_new._set_flags(flags);
251        new
252    }
253
254    pub fn is_sorted_flag(&self) -> IsSorted {
255        if self.len() <= 1 {
256            return IsSorted::Ascending;
257        }
258        self.get_flags().is_sorted()
259    }
260
261    pub fn set_sorted_flag(&mut self, sorted: IsSorted) {
262        let mut flags = self.get_flags();
263        flags.set_sorted(sorted);
264        self.set_flags(flags);
265    }
266
267    pub(crate) fn clear_flags(&mut self) {
268        self.set_flags(StatisticsFlags::empty());
269    }
270    pub fn get_flags(&self) -> StatisticsFlags {
271        self.0._get_flags()
272    }
273
274    pub(crate) fn set_flags(&mut self, flags: StatisticsFlags) {
275        self._get_inner_mut()._set_flags(flags)
276    }
277
278    pub fn into_frame(self) -> DataFrame {
279        // SAFETY: A single-column dataframe cannot have length mismatches or duplicate names
280        unsafe { DataFrame::new_unchecked(self.len(), vec![self.into()]) }
281    }
282
283    /// Rename series.
284    pub fn rename(&mut self, name: PlSmallStr) -> &mut Series {
285        self._get_inner_mut().rename(name);
286        self
287    }
288
289    /// Return this Series with a new name.
290    pub fn with_name(mut self, name: PlSmallStr) -> Series {
291        self.rename(name);
292        self
293    }
294
295    pub fn from_arrow_chunks(name: PlSmallStr, arrays: Vec<ArrayRef>) -> PolarsResult<Series> {
296        Self::try_from((name, arrays))
297    }
298
299    pub fn from_arrow(name: PlSmallStr, array: ArrayRef) -> PolarsResult<Series> {
300        Self::try_from((name, array))
301    }
302
303    /// Shrink the capacity of this array to fit its length.
304    pub fn shrink_to_fit(&mut self) {
305        self._get_inner_mut().shrink_to_fit()
306    }
307
308    /// Append in place. This is done by adding the chunks of `other` to this [`Series`].
309    ///
310    /// See [`ChunkedArray::append`] and [`ChunkedArray::extend`].
311    pub fn append(&mut self, other: &Series) -> PolarsResult<&mut Self> {
312        let must_cast = other.dtype().matches_schema_type(self.dtype())?;
313        if must_cast {
314            let other = other.cast(self.dtype())?;
315            self.append_owned(other)?;
316        } else {
317            self._get_inner_mut().append(other)?;
318        }
319        Ok(self)
320    }
321
322    /// Append in place. This is done by adding the chunks of `other` to this [`Series`].
323    ///
324    /// See [`ChunkedArray::append_owned`] and [`ChunkedArray::extend`].
325    pub fn append_owned(&mut self, other: Series) -> PolarsResult<&mut Self> {
326        let must_cast = other.dtype().matches_schema_type(self.dtype())?;
327        if must_cast {
328            let other = other.cast(self.dtype())?;
329            self._get_inner_mut().append_owned(other)?;
330        } else {
331            self._get_inner_mut().append_owned(other)?;
332        }
333        Ok(self)
334    }
335
336    /// Redo a length and null_count compute
337    pub fn compute_len(&mut self) {
338        self._get_inner_mut().compute_len()
339    }
340
341    /// Extend the memory backed by this array with the values from `other`.
342    ///
343    /// See [`ChunkedArray::extend`] and [`ChunkedArray::append`].
344    pub fn extend(&mut self, other: &Series) -> PolarsResult<&mut Self> {
345        let must_cast = other.dtype().matches_schema_type(self.dtype())?;
346        if must_cast {
347            let other = other.cast(self.dtype())?;
348            self._get_inner_mut().extend(&other)?;
349        } else {
350            self._get_inner_mut().extend(other)?;
351        }
352        Ok(self)
353    }
354
355    /// Returns a series with the given length.
356    ///
357    /// Errors if this series' length is not 1 and also not equal to the requested length.
358    pub fn broadcast_to(&self, length: usize) -> PolarsResult<Cow<'_, Self>> {
359        let len = self.len();
360        if len == length {
361            Ok(Cow::Borrowed(self))
362        } else if len == 1 {
363            Ok(Cow::Owned(self.new_from_index(0, length)))
364        } else {
365            polars_bail!(
366                ShapeMismatch: "can't broadcast Series '{}' of length {len} to length {length}",
367                self.name()
368            );
369        }
370    }
371
372    /// See broadcast_to.
373    pub fn broadcast_in_place_to(&mut self, length: usize) -> PolarsResult<()> {
374        if let Cow::Owned(new) = self.broadcast_to(length)? {
375            *self = new;
376        }
377        Ok(())
378    }
379
380    /// See broadcast_to.
381    pub fn broadcast_owned_to(mut self, length: usize) -> PolarsResult<Self> {
382        self.broadcast_in_place_to(length)?;
383        Ok(self)
384    }
385
386    /// Sort the series with specific options.
387    ///
388    /// # Example
389    ///
390    /// ```rust
391    /// # use polars_core::prelude::*;
392    /// # fn main() -> PolarsResult<()> {
393    /// let s = Series::new("foo".into(), [2, 1, 3]);
394    /// let sorted = s.sort(SortOptions::default())?;
395    /// assert_eq!(sorted, Series::new("foo".into(), [1, 2, 3]));
396    /// # Ok(())
397    /// }
398    /// ```
399    ///
400    /// See [`SortOptions`] for more options.
401    pub fn sort(&self, sort_options: SortOptions) -> PolarsResult<Self> {
402        self.sort_with(sort_options)
403    }
404
405    /// Only implemented for numeric types
406    pub fn as_single_ptr(&mut self) -> PolarsResult<usize> {
407        self._get_inner_mut().as_single_ptr()
408    }
409
410    pub fn cast(&self, dtype: &DataType) -> PolarsResult<Self> {
411        self.cast_with_options(dtype, CastOptions::NonStrict)
412    }
413
414    /// Cast [`Series`] to another [`DataType`].
415    pub fn cast_with_options(&self, dtype: &DataType, options: CastOptions) -> PolarsResult<Self> {
416        let slf = self
417            .trim_lists_to_normalized_offsets()
418            .map_or(Cow::Borrowed(self), Cow::Owned);
419        let slf = slf.propagate_nulls().map_or(slf, Cow::Owned);
420
421        use DataType as D;
422        let do_clone = match dtype {
423            D::Unknown(UnknownKind::Any) => true,
424            D::Unknown(UnknownKind::Int(_)) if slf.dtype().is_integer() => true,
425            D::Unknown(UnknownKind::Float) if slf.dtype().is_float() => true,
426            D::Unknown(UnknownKind::Str)
427                if slf.dtype().is_string() | slf.dtype().is_categorical() =>
428            {
429                true
430            },
431            dt if (dt.is_primitive() || dt.is_extension()) && dt == slf.dtype() => true,
432            _ => false,
433        };
434
435        if do_clone {
436            return Ok(slf.into_owned());
437        }
438
439        pub fn cast_dtype(dtype: &DataType) -> Option<DataType> {
440            match dtype {
441                D::Unknown(UnknownKind::Int(v)) => Some(materialize_dyn_int(*v).dtype()),
442                D::Unknown(UnknownKind::Float) => Some(DataType::Float64),
443                D::Unknown(UnknownKind::Str) => Some(DataType::String),
444                // Best leave as is.
445                D::List(inner) => cast_dtype(inner.as_ref()).map(Box::new).map(D::List),
446                #[cfg(feature = "dtype-struct")]
447                D::Struct(fields) => {
448                    // @NOTE: We only allocate if we really need to.
449
450                    let mut field_iter = fields.iter().enumerate();
451                    let mut new_fields = loop {
452                        let (i, field) = field_iter.next()?;
453
454                        if let Some(dtype) = cast_dtype(&field.dtype) {
455                            let mut new_fields = Vec::with_capacity(fields.len());
456                            new_fields.extend(fields.iter().take(i).cloned());
457                            new_fields.push(Field {
458                                name: field.name.clone(),
459                                dtype,
460                            });
461                            break new_fields;
462                        }
463                    };
464
465                    new_fields.extend(fields.iter().skip(new_fields.len()).cloned().map(|field| {
466                        let dtype = cast_dtype(&field.dtype).unwrap_or(field.dtype);
467                        Field {
468                            name: field.name,
469                            dtype,
470                        }
471                    }));
472
473                    Some(D::Struct(new_fields))
474                },
475                _ => None,
476            }
477        }
478
479        let mut casted = cast_dtype(dtype);
480        if dtype.is_list() && dtype.inner_dtype().is_some_and(|dt| dt.is_null()) {
481            if let Some(from_inner_dtype) = slf.dtype().inner_dtype() {
482                casted = Some(DataType::List(Box::new(from_inner_dtype.clone())));
483            }
484        }
485        let dtype = match casted {
486            None => dtype,
487            Some(ref dtype) => dtype,
488        };
489
490        // Always allow casting all nulls to other all nulls.
491        let len = slf.len();
492        if slf.null_count() == len {
493            return Ok(Series::full_null(slf.name().clone(), len, dtype));
494        }
495
496        let new_options = match options {
497            // Strictness is handled on this level to improve error messages, if not nested.
498            // Nested types could hide cast errors, so have to be done internally.
499            CastOptions::Strict if !dtype.is_nested() => CastOptions::NonStrict,
500            opt => opt,
501        };
502
503        let out = slf.0.cast(dtype, new_options)?;
504        if options.is_strict() {
505            handle_casting_failures(slf.as_ref(), &out)?;
506        }
507        Ok(out)
508    }
509
510    /// Cast from physical to logical types without any checks on the validity of the cast.
511    ///
512    /// # Safety
513    ///
514    /// This can lead to invalid memory access in downstream code.
515    pub unsafe fn cast_unchecked(&self, dtype: &DataType) -> PolarsResult<Self> {
516        match self.dtype() {
517            #[cfg(feature = "dtype-struct")]
518            DataType::Struct(_) => self.struct_().unwrap().cast_unchecked(dtype),
519            DataType::List(_) => self.list().unwrap().cast_unchecked(dtype),
520            dt if dt.is_primitive_numeric() => {
521                with_match_physical_numeric_polars_type!(dt, |$T| {
522                    let ca: &ChunkedArray<$T> = self.as_ref().as_ref().as_ref();
523                        ca.cast_unchecked(dtype)
524                })
525            },
526            DataType::Binary => self.binary().unwrap().cast_unchecked(dtype),
527            _ => self.cast_with_options(dtype, CastOptions::Overflowing),
528        }
529    }
530
531    /// Convert a non-logical series back into a logical series without casting.
532    ///
533    /// # Safety
534    ///
535    /// Payloads must be safe to read as `dtype`: categorical codes in range for every
536    /// non-null slot, and Maps satisfying the `MapChunked` storage safety contract. Null
537    /// Map rows may span entries, and those entries may themselves be null; they are left
538    /// alone. Null entries or keys in live rows are errors. Unsafe payloads can cause
539    /// invalid memory access downstream.
540    ///
541    /// # Key uniqueness
542    /// Not required for safety. Whole-row transformations preserve existing uniqueness;
543    /// key-changing transformations must use validated construction.
544    pub unsafe fn from_physical_unchecked(&self, dtype: &DataType) -> PolarsResult<Self> {
545        debug_assert!(!self.dtype().is_logical(), "{:?}", self.dtype());
546
547        if self.dtype() == dtype {
548            return Ok(self.clone());
549        }
550
551        use DataType as D;
552        match (self.dtype(), dtype) {
553            #[cfg(feature = "dtype-decimal")]
554            (D::Int128, D::Decimal(precision, scale)) => {
555                let ca = self.i128().unwrap();
556                Ok(ca
557                    .clone()
558                    .into_decimal_unchecked(*precision, *scale)
559                    .into_series())
560            },
561
562            #[cfg(feature = "dtype-categorical")]
563            (phys, D::Categorical(cats, _)) if &cats.physical().dtype() == phys => {
564                with_match_categorical_physical_type!(cats.physical(), |$C| {
565                    type CA = ChunkedArray<<$C as PolarsCategoricalType>::PolarsPhysical>;
566                    let ca = self.as_ref().as_any().downcast_ref::<CA>().unwrap();
567                    Ok(CategoricalChunked::<$C>::from_cats_and_dtype_unchecked(
568                        ca.clone(),
569                        dtype.clone(),
570                    )
571                    .into_series())
572                })
573            },
574            #[cfg(feature = "dtype-categorical")]
575            (phys, D::Enum(fcats, _)) if &fcats.physical().dtype() == phys => {
576                with_match_categorical_physical_type!(fcats.physical(), |$C| {
577                    type CA = ChunkedArray<<$C as PolarsCategoricalType>::PolarsPhysical>;
578                    let ca = self.as_ref().as_any().downcast_ref::<CA>().unwrap();
579                    Ok(CategoricalChunked::<$C>::from_cats_and_dtype_unchecked(
580                        ca.clone(),
581                        dtype.clone(),
582                    )
583                    .into_series())
584                })
585            },
586
587            // Arrow `LargeBinary` arrays are read into a series as `Binary`.
588            (D::Binary, D::BinaryOffset) => self.cast(&D::BinaryOffset),
589
590            (D::Int32, D::Date) => feature_gated!("dtype-date", Ok(self.clone().into_date())),
591            (D::Int64, D::Datetime(tu, tz)) => feature_gated!(
592                "dtype-datetime",
593                Ok(self.clone().into_datetime(*tu, tz.clone()))
594            ),
595            (D::Int64, D::Duration(tu)) => {
596                feature_gated!("dtype-duration", Ok(self.clone().into_duration(*tu)))
597            },
598            (D::Int64, D::Time) => feature_gated!("dtype-time", Ok(self.clone().into_time())),
599
600            (D::List(_), D::List(to)) => unsafe {
601                self.list()
602                    .unwrap()
603                    .from_physical_unchecked(to.as_ref().clone())
604                    .map(|ca| ca.into_series())
605            },
606            #[cfg(feature = "dtype-array")]
607            (D::Array(_, lw), D::Array(to, rw)) if lw == rw => unsafe {
608                self.array()
609                    .unwrap()
610                    .from_physical_unchecked(to.as_ref().clone())
611                    .map(|ca| ca.into_series())
612            },
613            #[cfg(feature = "dtype-struct")]
614            (D::Struct(_), D::Struct(to)) => unsafe {
615                self.struct_()
616                    .unwrap()
617                    .from_physical_unchecked(to.as_slice())
618                    .map(|ca| ca.into_series())
619            },
620
621            #[cfg(feature = "dtype-map")]
622            (D::List(_), D::Map(_, _)) => {
623                use crate::chunked_array::logical::ensure_live_entries_non_null;
624
625                let storage = self.from_physical_unchecked(&dtype.map_storage_dtype().unwrap())?;
626                // Nulls that propagation left under null rows are legal; only live rows
627                // must own no null entry or key.
628                ensure_live_entries_non_null(storage.list().unwrap())?;
629                Ok(MapChunked::from_storage_unchecked(dtype.clone(), storage).into_series())
630            },
631            #[cfg(feature = "dtype-extension")]
632            (_, D::Extension(typ, storage)) => {
633                let storage_series = self.from_physical_unchecked(storage.as_ref())?;
634                let ext = ExtensionChunked::from_storage(typ.clone(), storage_series);
635                Ok(ext.into_series())
636            },
637
638            _ => panic!("invalid from_physical({dtype:?}) for {:?}", self.dtype()),
639        }
640    }
641
642    #[cfg(feature = "dtype-extension")]
643    pub fn into_extension(self, typ: ExtensionTypeInstance) -> Series {
644        assert!(!self.dtype().is_extension());
645        let ext = ExtensionChunked::from_storage(typ, self);
646        ext.into_series()
647    }
648
649    /// Cast numerical types to f64, and keep floats as is.
650    pub fn to_float(&self) -> PolarsResult<Series> {
651        match self.dtype() {
652            DataType::Float32 | DataType::Float64 => Ok(self.clone()),
653            _ => self.cast_with_options(&DataType::Float64, CastOptions::Overflowing),
654        }
655    }
656
657    /// Get the sum of the Series as a `Scalar`.
658    /// Returns a `Scalar` with a zeroed value if self is an empty numeric series.
659    ///
660    /// If the [`DataType`] is one of `{Int8, UInt8, Int16, UInt16}` the sum is
661    /// computed in an `Int64` accumulator and the result is returned as `Int64`
662    /// to prevent overflow issues.
663    pub fn sum<T>(&self) -> PolarsResult<T>
664    where
665        T: NumCast + IsFloat,
666    {
667        let sum = self.sum_reduce()?;
668        let sum = sum.value().extract().unwrap();
669        Ok(sum)
670    }
671
672    /// Returns the minimum value in the array, according to the natural order.
673    /// Returns an option because the array is nullable.
674    pub fn min<T>(&self) -> PolarsResult<Option<T>>
675    where
676        T: NumCast + IsFloat,
677    {
678        let min = self.min_reduce()?;
679        let min = min.value().extract::<T>();
680        Ok(min)
681    }
682
683    /// Returns the maximum value in the array, according to the natural order.
684    /// Returns an option because the array is nullable.
685    pub fn max<T>(&self) -> PolarsResult<Option<T>>
686    where
687        T: NumCast + IsFloat,
688    {
689        let max = self.max_reduce()?;
690        let max = max.value().extract::<T>();
691        Ok(max)
692    }
693
694    /// Explode a list Series. This expands every item to a new row..
695    pub fn explode(&self, options: ExplodeOptions) -> PolarsResult<Series> {
696        match self.dtype() {
697            DataType::List(_) => self.list().unwrap().explode(options),
698            #[cfg(feature = "dtype-array")]
699            DataType::Array(_, _) => self.array().unwrap().explode(options),
700            _ => Ok(self.clone()),
701        }
702    }
703
704    /// Check if numeric value is NaN (note this is different than missing/ null)
705    pub fn is_nan(&self) -> PolarsResult<BooleanChunked> {
706        match self.dtype() {
707            #[cfg(feature = "dtype-f16")]
708            DataType::Float16 => Ok(self.f16().unwrap().is_nan()),
709            DataType::Float32 => Ok(self.f32().unwrap().is_nan()),
710            DataType::Float64 => Ok(self.f64().unwrap().is_nan()),
711            DataType::Null => Ok(BooleanChunked::full_null(self.name().clone(), self.len())),
712            dt if dt.is_primitive_numeric() => {
713                let arr = BooleanArray::full(self.len(), false, ArrowDataType::Boolean)
714                    .with_validity(self.rechunk_validity());
715                Ok(BooleanChunked::with_chunk(self.name().clone(), arr))
716            },
717            _ => polars_bail!(opq = is_nan, self.dtype()),
718        }
719    }
720
721    /// Check if numeric value is NaN (note this is different than missing/null)
722    pub fn is_not_nan(&self) -> PolarsResult<BooleanChunked> {
723        match self.dtype() {
724            #[cfg(feature = "dtype-f16")]
725            DataType::Float16 => Ok(self.f16().unwrap().is_not_nan()),
726            DataType::Float32 => Ok(self.f32().unwrap().is_not_nan()),
727            DataType::Float64 => Ok(self.f64().unwrap().is_not_nan()),
728            DataType::Null => Ok(BooleanChunked::full_null(self.name().clone(), self.len())),
729            dt if dt.is_primitive_numeric() => {
730                let arr = BooleanArray::full(self.len(), true, ArrowDataType::Boolean)
731                    .with_validity(self.rechunk_validity());
732                Ok(BooleanChunked::with_chunk(self.name().clone(), arr))
733            },
734            _ => polars_bail!(opq = is_not_nan, self.dtype()),
735        }
736    }
737
738    /// Check if numeric value is finite
739    pub fn is_finite(&self) -> PolarsResult<BooleanChunked> {
740        match self.dtype() {
741            #[cfg(feature = "dtype-f16")]
742            DataType::Float16 => Ok(self.f16().unwrap().is_finite()),
743            DataType::Float32 => Ok(self.f32().unwrap().is_finite()),
744            DataType::Float64 => Ok(self.f64().unwrap().is_finite()),
745            DataType::Null => Ok(BooleanChunked::full_null(self.name().clone(), self.len())),
746            dt if dt.is_primitive_numeric() => {
747                let arr = BooleanArray::full(self.len(), true, ArrowDataType::Boolean)
748                    .with_validity(self.rechunk_validity());
749                Ok(BooleanChunked::with_chunk(self.name().clone(), arr))
750            },
751            _ => polars_bail!(opq = is_finite, self.dtype()),
752        }
753    }
754
755    /// Check if numeric value is infinite
756    pub fn is_infinite(&self) -> PolarsResult<BooleanChunked> {
757        match self.dtype() {
758            #[cfg(feature = "dtype-f16")]
759            DataType::Float16 => Ok(self.f16().unwrap().is_infinite()),
760            DataType::Float32 => Ok(self.f32().unwrap().is_infinite()),
761            DataType::Float64 => Ok(self.f64().unwrap().is_infinite()),
762            DataType::Null => Ok(BooleanChunked::full_null(self.name().clone(), self.len())),
763            dt if dt.is_primitive_numeric() => {
764                let arr = BooleanArray::full(self.len(), false, ArrowDataType::Boolean)
765                    .with_validity(self.rechunk_validity());
766                Ok(BooleanChunked::with_chunk(self.name().clone(), arr))
767            },
768            _ => polars_bail!(opq = is_infinite, self.dtype()),
769        }
770    }
771
772    /// Create a new ChunkedArray with values from self where the mask evaluates `true` and values
773    /// from `other` where the mask evaluates `false`. This function automatically broadcasts unit
774    /// length inputs.
775    #[cfg(feature = "zip_with")]
776    pub fn zip_with(&self, mask: &BooleanChunked, other: &Series) -> PolarsResult<Series> {
777        let (lhs, rhs) = coerce_lhs_rhs(self, other)?;
778        lhs.zip_with_same_type(mask, rhs.as_ref())
779    }
780
781    /// Converts a Series to their physical representation, if they have one,
782    /// otherwise the series is left unchanged.
783    ///
784    /// * Date -> Int32
785    /// * Datetime -> Int64
786    /// * Duration -> Int64
787    /// * Decimal -> Int128
788    /// * Time -> Int64
789    /// * Categorical -> U8/U16/U32
790    /// * List(inner) -> List(physical of inner)
791    /// * Array(inner) -> Array(physical of inner)
792    /// * Struct -> Struct with physical repr of each struct column
793    /// * Extension -> physical of storage type
794    pub fn to_physical_repr(&self) -> Cow<'_, Series> {
795        use DataType::*;
796        match self.dtype() {
797            // NOTE: Don't use cast here, as it might rechunk (if all nulls)
798            // which is not allowed in a phys repr.
799            #[cfg(feature = "dtype-date")]
800            Date => Cow::Owned(self.date().unwrap().phys.clone().into_series()),
801            #[cfg(feature = "dtype-datetime")]
802            Datetime(_, _) => Cow::Owned(self.datetime().unwrap().phys.clone().into_series()),
803            #[cfg(feature = "dtype-duration")]
804            Duration(_) => Cow::Owned(self.duration().unwrap().phys.clone().into_series()),
805            #[cfg(feature = "dtype-time")]
806            Time => Cow::Owned(self.time().unwrap().phys.clone().into_series()),
807            #[cfg(feature = "dtype-categorical")]
808            dt @ (Categorical(_, _) | Enum(_, _)) => {
809                with_match_categorical_physical_type!(dt.cat_physical().unwrap(), |$C| {
810                    let ca = self.cat::<$C>().unwrap();
811                    Cow::Owned(ca.physical().clone().into_series())
812                })
813            },
814            #[cfg(feature = "dtype-decimal")]
815            Decimal(_, _) => Cow::Owned(self.decimal().unwrap().phys.clone().into_series()),
816            List(_) => match self.list().unwrap().to_physical_repr() {
817                Cow::Borrowed(_) => Cow::Borrowed(self),
818                Cow::Owned(ca) => Cow::Owned(ca.into_series()),
819            },
820            #[cfg(feature = "dtype-array")]
821            Array(_, _) => match self.array().unwrap().to_physical_repr() {
822                Cow::Borrowed(_) => Cow::Borrowed(self),
823                Cow::Owned(ca) => Cow::Owned(ca.into_series()),
824            },
825            #[cfg(feature = "dtype-struct")]
826            Struct(_) => match self.struct_().unwrap().to_physical_repr() {
827                Cow::Borrowed(_) => Cow::Borrowed(self),
828                Cow::Owned(ca) => Cow::Owned(ca.into_series()),
829            },
830            #[cfg(feature = "dtype-map")]
831            Map(_, _) => match self.map().unwrap().storage().to_physical_repr() {
832                Cow::Borrowed(storage) => Cow::Owned(storage.clone()),
833                Cow::Owned(storage) => Cow::Owned(storage),
834            },
835            #[cfg(feature = "dtype-extension")]
836            Extension(_, _) => self.ext().unwrap().storage().to_physical_repr(),
837            _ => Cow::Borrowed(self),
838        }
839    }
840
841    /// If the Series is an Extension type, return its storage Series.
842    /// Otherwise, return itself.
843    pub fn to_storage(&self) -> &Series {
844        #[cfg(feature = "dtype-extension")]
845        {
846            if let DataType::Extension(_, _) = self.dtype() {
847                return self.ext().unwrap().storage();
848            }
849        }
850        self
851    }
852
853    /// Traverse and collect every nth element in a new array.
854    pub fn gather_every(&self, n: usize, offset: usize) -> PolarsResult<Series> {
855        polars_ensure!(n > 0, ComputeError: "cannot perform gather every for `n=0`");
856        let idx = ((offset as IdxSize)..self.len() as IdxSize)
857            .step_by(n)
858            .collect_ca(PlSmallStr::EMPTY);
859        // SAFETY: we stay in-bounds.
860        Ok(unsafe { self.take_unchecked(&idx) })
861    }
862
863    #[cfg(feature = "dot_product")]
864    pub fn dot(&self, other: &Series) -> PolarsResult<f64> {
865        std::ops::Mul::mul(self, other)?.sum::<f64>()
866    }
867
868    /// Get the sum of the [`ChunkedArray`] as a `Scalar`.
869    /// Returns a `Scalar` with a single zeroed value if self is an empty numeric series.
870    ///
871    /// If the [`DataType`] is one of `{Int8, UInt8, Int16, UInt16}` the sum is
872    /// computed in an `Int64` accumulator and the result is returned as `Int64`
873    /// to prevent overflow issues.
874    pub fn sum_reduce(&self) -> PolarsResult<Scalar> {
875        self.0.sum_reduce()
876    }
877
878    /// Get the mean of the Series as a new Series of length 1.
879    /// Returns a Series with a single null entry if self is an empty numeric series.
880    pub fn mean_reduce(&self) -> PolarsResult<Scalar> {
881        self.0.mean_reduce()
882    }
883
884    /// Get the product of an array.
885    ///
886    /// If the [`DataType`] is one of `{Int8, UInt8, Int16, UInt16}` the `Series` is
887    /// first cast to `Int64` to prevent overflow issues.
888    pub fn product(&self) -> PolarsResult<Scalar> {
889        #[cfg(feature = "product")]
890        {
891            use DataType::*;
892            match self.dtype() {
893                Boolean => self.cast(&DataType::Int64).unwrap().product(),
894                Int8 | UInt8 | Int16 | UInt16 | Int32 | UInt32 => {
895                    let s = self.cast(&Int64).unwrap();
896                    s.product()
897                },
898                Int64 => Ok(self.i64().unwrap().prod_reduce()),
899                UInt64 => Ok(self.u64().unwrap().prod_reduce()),
900                #[cfg(feature = "dtype-i128")]
901                Int128 => Ok(self.i128().unwrap().prod_reduce()),
902                #[cfg(feature = "dtype-u128")]
903                UInt128 => Ok(self.u128().unwrap().prod_reduce()),
904                #[cfg(feature = "dtype-f16")]
905                Float16 => Ok(self.f16().unwrap().prod_reduce()),
906                Float32 => Ok(self.f32().unwrap().prod_reduce()),
907                Float64 => Ok(self.f64().unwrap().prod_reduce()),
908                #[cfg(feature = "dtype-decimal")]
909                Decimal(..) => Ok(self.decimal().unwrap().prod_reduce()),
910                dt => {
911                    polars_bail!(InvalidOperation: "`product` operation not supported for dtype `{dt}`")
912                },
913            }
914        }
915        #[cfg(not(feature = "product"))]
916        {
917            panic!("activate 'product' feature")
918        }
919    }
920
921    /// Cast throws an error if conversion had overflows
922    pub fn strict_cast(&self, dtype: &DataType) -> PolarsResult<Series> {
923        self.cast_with_options(dtype, CastOptions::Strict)
924    }
925
926    /// Cast non-strictly, also returning the rows the cast did not represent exactly.
927    ///
928    /// A row is inexact if it became null or does not survive the cast back unchanged. The mask
929    /// is `None` if every row cast exactly.
930    #[doc(hidden)]
931    pub fn _cast_reporting_inexact(
932        &self,
933        dtype: &DataType,
934    ) -> PolarsResult<(Series, Option<BooleanChunked>)> {
935        let casted = self.cast_with_options(dtype, CastOptions::NonStrict)?;
936        // An integer converts to an integer or decimal exactly or not at all, and the non-strict
937        // cast makes the values out of range null. The null counts tell whether any were.
938        if self.dtype().is_integer() && (dtype.is_integer() || dtype.is_decimal()) {
939            if casted.null_count() == self.null_count() {
940                return Ok((casted, None));
941            }
942            let inexact = &casted.is_null() & &self.is_not_null();
943            return Ok((casted, Some(inexact)));
944        }
945        // Null-safe: a cast back that overflows to null must count as inexact.
946        let roundtrip = casted.cast_with_options(self.dtype(), CastOptions::NonStrict)?;
947        let inexact = roundtrip.not_equal_missing(self)?;
948        debug_assert_eq!(inexact.null_count(), 0);
949        Ok((casted, inexact.any().then_some(inexact)))
950    }
951
952    #[cfg(feature = "dtype-decimal")]
953    pub fn into_decimal(self, precision: usize, scale: usize) -> PolarsResult<Series> {
954        match self.dtype() {
955            DataType::Int128 => Ok(self
956                .i128()
957                .unwrap()
958                .clone()
959                .into_decimal(precision, scale)?
960                .into_series()),
961            DataType::Decimal(cur_prec, cur_scale)
962                if scale == *cur_scale && precision >= *cur_prec =>
963            {
964                Ok(self)
965            },
966            dt => panic!("into_decimal({precision:?}, {scale}) not implemented for {dt:?}"),
967        }
968    }
969
970    #[cfg(feature = "dtype-time")]
971    pub fn into_time(self) -> Series {
972        match self.dtype() {
973            DataType::Int64 => self.i64().unwrap().clone().into_time().into_series(),
974            DataType::Time => self
975                .time()
976                .unwrap()
977                .physical()
978                .clone()
979                .into_time()
980                .into_series(),
981            dt => panic!("date not implemented for {dt:?}"),
982        }
983    }
984
985    pub fn into_date(self) -> Series {
986        #[cfg(not(feature = "dtype-date"))]
987        {
988            panic!("activate feature dtype-date")
989        }
990        #[cfg(feature = "dtype-date")]
991        match self.dtype() {
992            DataType::Int32 => self.i32().unwrap().clone().into_date().into_series(),
993            DataType::Date => self
994                .date()
995                .unwrap()
996                .physical()
997                .clone()
998                .into_date()
999                .into_series(),
1000            dt => panic!("date not implemented for {dt:?}"),
1001        }
1002    }
1003
1004    #[allow(unused_variables)]
1005    pub fn into_datetime(self, timeunit: TimeUnit, tz: Option<TimeZone>) -> Series {
1006        #[cfg(not(feature = "dtype-datetime"))]
1007        {
1008            panic!("activate feature dtype-datetime")
1009        }
1010
1011        #[cfg(feature = "dtype-datetime")]
1012        match self.dtype() {
1013            DataType::Int64 => self
1014                .i64()
1015                .unwrap()
1016                .clone()
1017                .into_datetime(timeunit, tz)
1018                .into_series(),
1019            DataType::Datetime(_, _) => self
1020                .datetime()
1021                .unwrap()
1022                .physical()
1023                .clone()
1024                .into_datetime(timeunit, tz)
1025                .into_series(),
1026            dt => panic!("into_datetime not implemented for {dt:?}"),
1027        }
1028    }
1029
1030    #[allow(unused_variables)]
1031    pub fn into_duration(self, timeunit: TimeUnit) -> Series {
1032        #[cfg(not(feature = "dtype-duration"))]
1033        {
1034            panic!("activate feature dtype-duration")
1035        }
1036        #[cfg(feature = "dtype-duration")]
1037        match self.dtype() {
1038            DataType::Int64 => self
1039                .i64()
1040                .unwrap()
1041                .clone()
1042                .into_duration(timeunit)
1043                .into_series(),
1044            DataType::Duration(_) => self
1045                .duration()
1046                .unwrap()
1047                .physical()
1048                .clone()
1049                .into_duration(timeunit)
1050                .into_series(),
1051            dt => panic!("into_duration not implemented for {dt:?}"),
1052        }
1053    }
1054
1055    // used for formatting
1056    pub fn str_value(&self, index: usize) -> PolarsResult<Cow<'_, str>> {
1057        Ok(self.0.get(index)?.str_value())
1058    }
1059    /// Get the head of the Series.
1060    pub fn head(&self, length: Option<usize>) -> Series {
1061        let len = length.unwrap_or(HEAD_DEFAULT_LENGTH);
1062        self.slice(0, std::cmp::min(len, self.len()))
1063    }
1064
1065    /// Get the tail of the Series.
1066    pub fn tail(&self, length: Option<usize>) -> Series {
1067        let len = length.unwrap_or(TAIL_DEFAULT_LENGTH);
1068        let len = std::cmp::min(len, self.len());
1069        self.slice(-(len as i64), len)
1070    }
1071
1072    /// Compute the unique elements, but maintain order. This requires more work
1073    /// than a naive [`Series::unique`](SeriesTrait::unique).
1074    pub fn unique_stable(&self) -> PolarsResult<Series> {
1075        let idx = self.arg_unique()?;
1076        // SAFETY: Indices are in bounds.
1077        unsafe { Ok(self.take_unchecked(&idx)) }
1078    }
1079
1080    pub fn try_idx(&self) -> Option<&IdxCa> {
1081        #[cfg(feature = "bigidx")]
1082        {
1083            self.try_u64()
1084        }
1085        #[cfg(not(feature = "bigidx"))]
1086        {
1087            self.try_u32()
1088        }
1089    }
1090
1091    pub fn idx(&self) -> PolarsResult<&IdxCa> {
1092        #[cfg(feature = "bigidx")]
1093        {
1094            self.u64()
1095        }
1096        #[cfg(not(feature = "bigidx"))]
1097        {
1098            self.u32()
1099        }
1100    }
1101
1102    /// Returns an estimation of the total (heap) allocated size of the `Series` in bytes.
1103    ///
1104    /// # Implementation
1105    /// This estimation is the sum of the size of its buffers, validity, including nested arrays.
1106    /// Multiple arrays may share buffers and bitmaps. Therefore, the size of 2 arrays is not the
1107    /// sum of the sizes computed from this function. In particular, [`StructArray`]'s size is an upper bound.
1108    ///
1109    /// When an array is sliced, its allocated size remains constant because the buffer unchanged.
1110    /// However, this function will yield a smaller number. This is because this function returns
1111    /// the visible size of the buffer, not its total capacity.
1112    ///
1113    /// FFI buffers are included in this estimation.
1114    pub fn estimated_size(&self) -> usize {
1115        let mut size = 0;
1116        match self.dtype() {
1117            // TODO @ cat-rework: include mapping size here?
1118            #[cfg(feature = "object")]
1119            DataType::Object(_) => {
1120                let ArrowDataType::FixedSizeBinary(size) = self.chunks()[0].dtype() else {
1121                    unreachable!()
1122                };
1123                // This is only the pointer size in python. So will be a huge underestimation.
1124                return self.len() * *size;
1125            },
1126            _ => {},
1127        }
1128
1129        size += self
1130            .chunks()
1131            .iter()
1132            .map(|arr| estimated_bytes_size(&**arr))
1133            .sum::<usize>();
1134
1135        size
1136    }
1137
1138    pub fn row_encode_unordered(&self) -> PolarsResult<BinaryOffsetChunked> {
1139        row_encode::_get_rows_encoded_ca_unordered(
1140            self.name().clone(),
1141            &[self.clone().into_column()],
1142        )
1143    }
1144
1145    pub fn row_encode_ordered(
1146        &self,
1147        descending: bool,
1148        nulls_last: bool,
1149    ) -> PolarsResult<BinaryOffsetChunked> {
1150        row_encode::_get_rows_encoded_ca(
1151            self.name().clone(),
1152            &[self.clone().into_column()],
1153            &[descending],
1154            &[nulls_last],
1155            false,
1156        )
1157    }
1158}
1159
1160impl Default for Series {
1161    fn default() -> Self {
1162        NullChunked::new(PlSmallStr::EMPTY, 0).into_series()
1163    }
1164}
1165
1166impl Deref for Series {
1167    type Target = dyn SeriesTrait;
1168
1169    #[inline(always)]
1170    fn deref(&self) -> &Self::Target {
1171        self.0.as_ref()
1172    }
1173}
1174
1175impl<'a> AsRef<dyn SeriesTrait + 'a> for Series {
1176    fn as_ref(&self) -> &(dyn SeriesTrait + 'a) {
1177        self.0.as_ref()
1178    }
1179}
1180
1181impl<T: PolarsPhysicalType> AsRef<ChunkedArray<T>> for dyn SeriesTrait + '_ {
1182    fn as_ref(&self) -> &ChunkedArray<T> {
1183        // @NOTE: SeriesTrait `as_any` returns a std::any::Any for the underlying ChunkedArray /
1184        // Logical (so not the SeriesWrap).
1185        let Some(ca) = self.as_any().downcast_ref::<ChunkedArray<T>>() else {
1186            panic!(
1187                "implementation error, cannot get ref {:?} from {:?}",
1188                T::get_static_dtype(),
1189                self.dtype()
1190            );
1191        };
1192
1193        ca
1194    }
1195}
1196
1197impl<T: PolarsPhysicalType> AsMut<ChunkedArray<T>> for dyn SeriesTrait + '_ {
1198    fn as_mut(&mut self) -> &mut ChunkedArray<T> {
1199        if !self.as_any_mut().is::<ChunkedArray<T>>() {
1200            panic!(
1201                "implementation error, cannot get ref {:?} from {:?}",
1202                T::get_static_dtype(),
1203                self.dtype()
1204            );
1205        }
1206
1207        // @NOTE: SeriesTrait `as_any` returns a std::any::Any for the underlying ChunkedArray /
1208        // Logical (so not the SeriesWrap).
1209        self.as_any_mut().downcast_mut::<ChunkedArray<T>>().unwrap()
1210    }
1211}
1212
1213impl BroadcastLength for Series {
1214    fn _broadcast_len(&self) -> usize {
1215        self.len()
1216    }
1217
1218    fn _column_name(&self) -> Option<&str> {
1219        Some(self.name())
1220    }
1221}
1222
1223#[cfg(test)]
1224mod test {
1225    use crate::series::*;
1226
1227    #[test]
1228    fn cast() {
1229        let ar = UInt32Chunked::new("a".into(), &[1, 2]);
1230        let s = ar.into_series();
1231        let s2 = s.cast(&DataType::Int64).unwrap();
1232
1233        assert!(s2.i64().is_ok());
1234        let s2 = s.cast(&DataType::Float32).unwrap();
1235        assert!(s2.f32().is_ok());
1236    }
1237
1238    #[test]
1239    fn new_series() {
1240        let _ = Series::new("boolean series".into(), &vec![true, false, true]);
1241        let _ = Series::new("int series".into(), &[1, 2, 3]);
1242        let ca = Int32Chunked::new("a".into(), &[1, 2, 3]);
1243        let _ = ca.into_series();
1244    }
1245
1246    #[test]
1247    #[cfg(feature = "dtype-date")]
1248    fn roundtrip_list_logical_20311() {
1249        let list = ListChunked::from_chunk_iter(
1250            PlSmallStr::from_static("a"),
1251            [ListArray::new(
1252                ArrowDataType::LargeList(Box::new(ArrowField::new(
1253                    LIST_VALUES_NAME,
1254                    ArrowDataType::Int32,
1255                    true,
1256                ))),
1257                unsafe { polars_arrow::offset::Offsets::new_unchecked(vec![0, 1]) }.into(),
1258                PrimitiveArray::new(ArrowDataType::Int32, vec![1i32].into(), None).to_boxed(),
1259                None,
1260            )],
1261        );
1262        let list = unsafe { list.from_physical_unchecked(DataType::Date) }.unwrap();
1263        assert_eq!(list.dtype(), &DataType::List(Box::new(DataType::Date)));
1264    }
1265
1266    #[test]
1267    #[cfg(feature = "dtype-struct")]
1268    fn new_series_from_empty_structs() {
1269        let dtype = DataType::Struct(vec![]);
1270        let empties = vec![AnyValue::StructOwned(Box::new((vec![], vec![]))); 3];
1271        let s = Series::from_any_values_and_dtype("".into(), &empties, &dtype, false).unwrap();
1272        assert_eq!(s.len(), 3);
1273    }
1274    #[test]
1275    fn new_series_from_arrow_primitive_array() {
1276        let array = UInt32Array::from_slice([1, 2, 3, 4, 5]);
1277        let array_ref: ArrayRef = Box::new(array);
1278
1279        let _ = Series::try_new("foo".into(), array_ref).unwrap();
1280    }
1281
1282    #[test]
1283    fn series_append() {
1284        let mut s1 = Series::new("a".into(), &[1, 2]);
1285        let s2 = Series::new("b".into(), &[3]);
1286        s1.append(&s2).unwrap();
1287        assert_eq!(s1.len(), 3);
1288
1289        // add wrong type
1290        let s2 = Series::new("b".into(), &[3.0]);
1291        assert!(s1.append(&s2).is_err())
1292    }
1293
1294    #[test]
1295    #[cfg(feature = "dtype-decimal")]
1296    fn series_append_decimal() {
1297        let s1 = Series::new("a".into(), &[1.1, 2.3])
1298            .cast(&DataType::Decimal(38, 2))
1299            .unwrap();
1300        let s2 = Series::new("b".into(), &[3])
1301            .cast(&DataType::Decimal(38, 0))
1302            .unwrap();
1303
1304        {
1305            let mut s1 = s1.clone();
1306            s1.append(&s2).unwrap();
1307            assert_eq!(s1.len(), 3);
1308            assert_eq!(s1.get(2).unwrap(), AnyValue::Decimal(300, 38, 2));
1309        }
1310
1311        {
1312            let mut s2 = s2;
1313            s2.extend(&s1).unwrap();
1314            assert_eq!(s2.get(2).unwrap(), AnyValue::Decimal(2, 38, 0));
1315        }
1316    }
1317
1318    #[test]
1319    fn series_slice_works() {
1320        let series = Series::new("a".into(), &[1i64, 2, 3, 4, 5]);
1321
1322        let slice_1 = series.slice(-3, 3);
1323        let slice_2 = series.slice(-5, 5);
1324        let slice_3 = series.slice(0, 5);
1325
1326        assert_eq!(slice_1.get(0).unwrap(), AnyValue::Int64(3));
1327        assert_eq!(slice_2.get(0).unwrap(), AnyValue::Int64(1));
1328        assert_eq!(slice_3.get(0).unwrap(), AnyValue::Int64(1));
1329    }
1330
1331    #[test]
1332    fn out_of_range_slice_does_not_panic() {
1333        let series = Series::new("a".into(), &[1i64, 2, 3, 4, 5]);
1334
1335        let _ = series.slice(-3, 4);
1336        let _ = series.slice(-6, 2);
1337        let _ = series.slice(4, 2);
1338    }
1339}