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