Skip to main content

polars_core/chunked_array/
cast.rs

1//! Implementations of the ChunkCast Trait.
2
3use std::borrow::Cow;
4
5use polars_compute::cast::CastOptionsImpl;
6#[cfg(feature = "serde-lazy")]
7use serde::{Deserialize, Serialize};
8
9use super::flags::StatisticsFlags;
10#[cfg(feature = "dtype-datetime")]
11use crate::prelude::DataType::Datetime;
12use crate::prelude::*;
13use crate::utils::{handle_array_casting_failures, handle_casting_failures};
14
15#[derive(Copy, Clone, Debug, Default, PartialEq, Hash, Eq)]
16#[cfg_attr(feature = "serde-lazy", derive(Serialize, Deserialize))]
17#[cfg_attr(feature = "dsl-schema", derive(schemars::JsonSchema))]
18#[repr(u8)]
19pub enum CastOptions {
20    /// Raises on overflow
21    #[default]
22    Strict,
23    /// Overflow is replaced with null
24    NonStrict,
25    /// Allows wrapping overflow
26    Overflowing,
27}
28
29impl CastOptions {
30    pub fn is_strict(&self) -> bool {
31        matches!(self, CastOptions::Strict)
32    }
33}
34
35impl From<CastOptions> for CastOptionsImpl {
36    fn from(value: CastOptions) -> Self {
37        let wrapped = match value {
38            CastOptions::Strict | CastOptions::NonStrict => false,
39            CastOptions::Overflowing => true,
40        };
41        CastOptionsImpl {
42            wrapped,
43            partial: false,
44        }
45    }
46}
47
48pub(crate) fn cast_chunks(
49    chunks: &[ArrayRef],
50    dtype: &DataType,
51    options: CastOptions,
52) -> PolarsResult<Vec<ArrayRef>> {
53    let check_nulls = matches!(options, CastOptions::Strict);
54    let options = options.into();
55
56    let arrow_dtype = dtype.try_to_arrow(CompatLevel::newest())?;
57    chunks
58        .iter()
59        .map(|arr| {
60            let out = polars_compute::cast::cast(arr.as_ref(), &arrow_dtype, options);
61            if check_nulls {
62                out.and_then(|new| {
63                    if arr.null_count() != new.null_count() {
64                        handle_array_casting_failures(&**arr, &*new)?;
65                    }
66                    Ok(new)
67                })
68            } else {
69                out
70            }
71        })
72        .collect::<PolarsResult<Vec<_>>>()
73}
74
75fn cast_impl_inner(
76    name: PlSmallStr,
77    chunks: &[ArrayRef],
78    dtype: &DataType,
79    options: CastOptions,
80) -> PolarsResult<Series> {
81    let chunks = match dtype {
82        #[cfg(feature = "dtype-decimal")]
83        DataType::Decimal(_, _) => {
84            let mut chunks = cast_chunks(chunks, dtype, options)?;
85            // @NOTE: We cannot cast here as that will lower the scale.
86            for chunk in chunks.iter_mut() {
87                *chunk = std::mem::take(
88                    chunk
89                        .as_any_mut()
90                        .downcast_mut::<PrimitiveArray<i128>>()
91                        .unwrap(),
92                )
93                .to(ArrowDataType::Int128)
94                .to_boxed();
95            }
96            chunks
97        },
98        _ => cast_chunks(chunks, &dtype.to_physical(), options)?,
99    };
100
101    let out = Series::try_from((name, chunks))?;
102    use DataType::*;
103    let out = match dtype {
104        Date => out.into_date(),
105        Datetime(tu, tz) => match tz {
106            #[cfg(feature = "timezones")]
107            Some(tz) => {
108                TimeZone::validate_time_zone(tz)?;
109                out.into_datetime(*tu, Some(tz.clone()))
110            },
111            _ => out.into_datetime(*tu, None),
112        },
113        Duration(tu) => out.into_duration(*tu),
114        #[cfg(feature = "dtype-time")]
115        Time => out.into_time(),
116        #[cfg(feature = "dtype-decimal")]
117        Decimal(precision, scale) => out.into_decimal(*precision, *scale)?,
118        _ => out,
119    };
120
121    Ok(out)
122}
123
124fn cast_impl(
125    name: PlSmallStr,
126    chunks: &[ArrayRef],
127    dtype: &DataType,
128    options: CastOptions,
129) -> PolarsResult<Series> {
130    cast_impl_inner(name, chunks, dtype, options)
131}
132
133#[cfg(feature = "dtype-struct")]
134fn cast_single_to_struct(
135    name: PlSmallStr,
136    chunks: &[ArrayRef],
137    fields: &[Field],
138    options: CastOptions,
139) -> PolarsResult<Series> {
140    polars_ensure!(fields.len() == 1, InvalidOperation: "must specify one field in the struct");
141    let mut new_fields = Vec::with_capacity(fields.len());
142    // cast to first field dtype
143    let mut fields = fields.iter();
144    let fld = fields.next().unwrap();
145    let s = cast_impl_inner(fld.name.clone(), chunks, &fld.dtype, options)?;
146    let length = s.len();
147    new_fields.push(s);
148
149    for fld in fields {
150        new_fields.push(Series::full_null(fld.name.clone(), length, &fld.dtype));
151    }
152
153    StructChunked::from_series(name, length, new_fields.iter()).map(|ca| ca.into_series())
154}
155
156impl<T> ChunkedArray<T>
157where
158    T: PolarsNumericType,
159{
160    fn cast_impl(&self, dtype: &DataType, options: CastOptions) -> PolarsResult<Series> {
161        if self.dtype() == dtype {
162            // SAFETY: chunks are correct dtype
163            let mut out = unsafe {
164                Series::from_chunks_and_dtype_unchecked(
165                    self.name().clone(),
166                    self.chunks.clone(),
167                    dtype,
168                )
169            };
170            out.set_sorted_flag(self.is_sorted_flag());
171            return Ok(out);
172        }
173        match dtype {
174            // LEGACY
175            // TODO @ cat-rework: remove after exposing to/from physical functions.
176            #[cfg(feature = "dtype-categorical")]
177            DataType::Categorical(cats, _mapping) => {
178                polars_warn!(
179                    Deprecation,
180                    "casting from {:?} to {dtype:?} is deprecated.\n\
181                    Instead of `.cast({dtype:?}`, use `.cat.to({dtype:?})`.",
182                    T::get_static_dtype()
183                );
184                let s = self.cast_with_options(&cats.physical().dtype(), options)?;
185                with_match_categorical_physical_type!(cats.physical(), |$C| {
186                    // SAFETY: we are guarded by the type system.
187                    type PhysCa = ChunkedArray<<$C as PolarsCategoricalType>::PolarsPhysical>;
188                    let ca: &PhysCa = s.as_ref().as_ref();
189                    Ok(CategoricalChunked::<$C>::from_cats_and_dtype(ca.clone(), dtype.clone())
190                        .into_series())
191                })
192            },
193
194            // LEGACY
195            // TODO @ cat-rework: remove after exposing to/from physical functions.
196            #[cfg(feature = "dtype-categorical")]
197            DataType::Enum(fcats, _mapping) => {
198                polars_warn!(
199                    Deprecation,
200                    "casting from {:?} to {dtype:?} is deprecated.\n\
201                    Instead of `.cast({dtype:?}`, use `.cat.to({dtype:?})`.",
202                    T::get_static_dtype()
203                );
204                let s = self.cast_with_options(&fcats.physical().dtype(), options)?;
205                with_match_categorical_physical_type!(fcats.physical(), |$C| {
206                    // SAFETY: we are guarded by the type system.
207                    type PhysCa = ChunkedArray<<$C as PolarsCategoricalType>::PolarsPhysical>;
208                    let ca: &PhysCa = s.as_ref().as_ref();
209                    Ok(CategoricalChunked::<$C>::from_cats_and_dtype(ca.clone(), dtype.clone()).into_series())
210                })
211            },
212
213            #[cfg(feature = "dtype-struct")]
214            DataType::Struct(fields) => {
215                cast_single_to_struct(self.name().clone(), &self.chunks, fields, options)
216            },
217            _ => cast_impl_inner(self.name().clone(), &self.chunks, dtype, options).map(|mut s| {
218                // maintain sorted if data types
219                // - remain signed
220                // - unsigned -> signed
221                // this may still fail with overflow?
222                let to_signed = dtype.is_signed_integer();
223                let unsigned2unsigned =
224                    self.dtype().is_unsigned_integer() && dtype.is_unsigned_integer();
225                let allowed = to_signed || unsigned2unsigned;
226
227                if (allowed)
228                    && (s.null_count() == self.null_count())
229                    // physical to logicals
230                    || (self.dtype().to_physical() == dtype.to_physical())
231                {
232                    let is_sorted = self.is_sorted_flag();
233                    s.set_sorted_flag(is_sorted)
234                }
235                s
236            }),
237        }
238    }
239}
240
241impl<T> ChunkCast for ChunkedArray<T>
242where
243    T: PolarsNumericType,
244{
245    fn cast_with_options(&self, dtype: &DataType, options: CastOptions) -> PolarsResult<Series> {
246        self.cast_impl(dtype, options)
247    }
248
249    unsafe fn cast_unchecked(&self, dtype: &DataType) -> PolarsResult<Series> {
250        match dtype {
251            // LEGACY
252            // TODO @ cat-rework: remove after exposing to/from physical functions.
253            #[cfg(feature = "dtype-categorical")]
254            DataType::Categorical(cats, _mapping) => {
255                polars_ensure!(self.dtype() == &cats.physical().dtype(), ComputeError: "cannot cast numeric types to 'Categorical'");
256                with_match_categorical_physical_type!(cats.physical(), |$C| {
257                    // SAFETY: we are guarded by the type system.
258                    type PhysCa = ChunkedArray<<$C as PolarsCategoricalType>::PolarsPhysical>;
259                    let ca = unsafe { &*(self as *const ChunkedArray<T> as *const PhysCa) };
260                    Ok(CategoricalChunked::<$C>::from_cats_and_dtype_unchecked(ca.clone(), dtype.clone())
261                        .into_series())
262                })
263            },
264
265            // LEGACY
266            // TODO @ cat-rework: remove after exposing to/from physical functions.
267            #[cfg(feature = "dtype-categorical")]
268            DataType::Enum(fcats, _mapping) => {
269                polars_ensure!(self.dtype() == &fcats.physical().dtype(), ComputeError: "cannot cast numeric types to 'Enum'");
270                with_match_categorical_physical_type!(fcats.physical(), |$C| {
271                    // SAFETY: we are guarded by the type system.
272                    type PhysCa = ChunkedArray<<$C as PolarsCategoricalType>::PolarsPhysical>;
273                    let ca = unsafe { &*(self as *const ChunkedArray<T> as *const PhysCa) };
274                    Ok(CategoricalChunked::<$C>::from_cats_and_dtype_unchecked(ca.clone(), dtype.clone()).into_series())
275                })
276            },
277
278            _ => self.cast_impl(dtype, CastOptions::Overflowing),
279        }
280    }
281}
282
283impl ChunkCast for StringChunked {
284    fn cast_with_options(&self, dtype: &DataType, options: CastOptions) -> PolarsResult<Series> {
285        match dtype {
286            #[cfg(feature = "dtype-categorical")]
287            DataType::Categorical(cats, _mapping) => {
288                with_match_categorical_physical_type!(cats.physical(), |$C| {
289                    Ok(CategoricalChunked::<$C>::from_str_iter(self.name().clone(), dtype.clone(), self.iter())?
290                        .into_series())
291                })
292            },
293            #[cfg(feature = "dtype-categorical")]
294            DataType::Enum(fcats, _mapping) => {
295                let ret = with_match_categorical_physical_type!(fcats.physical(), |$C| {
296                    CategoricalChunked::<$C>::from_str_iter(self.name().clone(), dtype.clone(), self.iter())?
297                        .into_series()
298                });
299
300                if options.is_strict() && self.null_count() != ret.null_count() {
301                    handle_casting_failures(&self.clone().into_series(), &ret)?;
302                }
303
304                Ok(ret)
305            },
306            #[cfg(feature = "dtype-struct")]
307            DataType::Struct(fields) => {
308                cast_single_to_struct(self.name().clone(), &self.chunks, fields, options)
309            },
310            #[cfg(feature = "dtype-decimal")]
311            DataType::Decimal(precision, scale) => {
312                let chunks = self.downcast_iter().map(|arr| {
313                    polars_compute::cast::binview_to_decimal(&arr.to_binview(), *precision, *scale)
314                        .to(ArrowDataType::Int128)
315                });
316                let ca = Int128Chunked::from_chunk_iter(self.name().clone(), chunks);
317                Ok(ca.into_decimal_unchecked(*precision, *scale).into_series())
318            },
319            #[cfg(feature = "dtype-date")]
320            DataType::Date => {
321                let result = cast_chunks(&self.chunks, dtype, options)?;
322                let out = Series::try_from((self.name().clone(), result))?;
323                Ok(out)
324            },
325            #[cfg(feature = "dtype-datetime")]
326            DataType::Datetime(time_unit, time_zone) => match time_zone {
327                #[cfg(feature = "timezones")]
328                Some(time_zone) => {
329                    TimeZone::validate_time_zone(time_zone)?;
330                    let result = cast_chunks(
331                        &self.chunks,
332                        &Datetime(time_unit.to_owned(), Some(time_zone.clone())),
333                        options,
334                    )?;
335                    Series::try_from((self.name().clone(), result))
336                },
337                _ => {
338                    let result =
339                        cast_chunks(&self.chunks, &Datetime(time_unit.to_owned(), None), options)?;
340                    Series::try_from((self.name().clone(), result))
341                },
342            },
343            _ => cast_impl(self.name().clone(), &self.chunks, dtype, options),
344        }
345    }
346
347    unsafe fn cast_unchecked(&self, dtype: &DataType) -> PolarsResult<Series> {
348        self.cast_with_options(dtype, CastOptions::Overflowing)
349    }
350}
351
352impl BinaryChunked {
353    /// # Safety
354    /// String is not validated
355    pub unsafe fn to_string_unchecked(&self) -> StringChunked {
356        let chunks = self
357            .downcast_iter()
358            .map(|arr| unsafe { arr.to_utf8view_unchecked() }.boxed())
359            .collect();
360        let field = Arc::new(Field::new(self.name().clone(), DataType::String));
361
362        let mut ca = StringChunked::new_with_compute_len(field, chunks);
363
364        use StatisticsFlags as F;
365        ca.retain_flags_from(self, F::IS_SORTED_ANY | F::CAN_FAST_EXPLODE_LIST);
366        ca
367    }
368}
369
370impl StringChunked {
371    pub fn as_binary(&self) -> BinaryChunked {
372        let chunks = self
373            .downcast_iter()
374            .map(|arr| arr.to_binview().boxed())
375            .collect();
376        let field = Arc::new(Field::new(self.name().clone(), DataType::Binary));
377
378        let mut ca = BinaryChunked::new_with_compute_len(field, chunks);
379
380        use StatisticsFlags as F;
381        ca.retain_flags_from(self, F::IS_SORTED_ANY | F::CAN_FAST_EXPLODE_LIST);
382        ca
383    }
384}
385
386impl ChunkCast for BinaryChunked {
387    fn cast_with_options(&self, dtype: &DataType, options: CastOptions) -> PolarsResult<Series> {
388        match dtype {
389            #[cfg(feature = "dtype-struct")]
390            DataType::Struct(fields) => {
391                cast_single_to_struct(self.name().clone(), &self.chunks, fields, options)
392            },
393            _ => cast_impl(self.name().clone(), &self.chunks, dtype, options),
394        }
395    }
396
397    unsafe fn cast_unchecked(&self, dtype: &DataType) -> PolarsResult<Series> {
398        match dtype {
399            DataType::String => unsafe { Ok(self.to_string_unchecked().into_series()) },
400            _ => self.cast_with_options(dtype, CastOptions::Overflowing),
401        }
402    }
403}
404
405impl ChunkCast for BinaryOffsetChunked {
406    fn cast_with_options(&self, dtype: &DataType, options: CastOptions) -> PolarsResult<Series> {
407        match dtype {
408            #[cfg(feature = "dtype-struct")]
409            DataType::Struct(fields) => {
410                cast_single_to_struct(self.name().clone(), &self.chunks, fields, options)
411            },
412            _ => cast_impl(self.name().clone(), &self.chunks, dtype, options),
413        }
414    }
415
416    unsafe fn cast_unchecked(&self, dtype: &DataType) -> PolarsResult<Series> {
417        self.cast_with_options(dtype, CastOptions::Overflowing)
418    }
419}
420
421impl ChunkCast for BooleanChunked {
422    fn cast_with_options(&self, dtype: &DataType, options: CastOptions) -> PolarsResult<Series> {
423        match dtype {
424            #[cfg(feature = "dtype-struct")]
425            DataType::Struct(fields) => {
426                cast_single_to_struct(self.name().clone(), &self.chunks, fields, options)
427            },
428            #[cfg(feature = "dtype-categorical")]
429            DataType::Categorical(_, _) | DataType::Enum(_, _) => {
430                polars_bail!(InvalidOperation: "cannot cast Boolean to Categorical");
431            },
432            _ => cast_impl(self.name().clone(), &self.chunks, dtype, options),
433        }
434    }
435
436    unsafe fn cast_unchecked(&self, dtype: &DataType) -> PolarsResult<Series> {
437        self.cast_with_options(dtype, CastOptions::Overflowing)
438    }
439}
440
441/// We cannot cast anything to or from List/LargeList
442/// So this implementation casts the inner type
443impl ChunkCast for ListChunked {
444    fn cast_with_options(&self, dtype: &DataType, options: CastOptions) -> PolarsResult<Series> {
445        let ca = self
446            .trim_lists_to_normalized_offsets()
447            .map_or(Cow::Borrowed(self), Cow::Owned);
448        let ca = ca.propagate_nulls().map_or(ca, Cow::Owned);
449
450        use DataType::*;
451        match dtype {
452            List(child_type) => {
453                match (ca.inner_dtype(), &**child_type) {
454                    (old, new) if old == new => Ok(ca.into_owned().into_series()),
455                    // TODO @ cat-rework: can we implement this now?
456                    #[cfg(feature = "dtype-categorical")]
457                    (dt, Categorical(_, _) | Enum(_, _))
458                        if !matches!(dt, Categorical(_, _) | Enum(_, _) | String | Null) =>
459                    {
460                        polars_bail!(InvalidOperation: "cannot cast List inner type: '{:?}' to Categorical", dt)
461                    },
462                    _ => {
463                        // ensure the inner logical type bubbles up
464                        let (arr, child_type) = cast_list(ca.as_ref(), child_type, options)?;
465                        // SAFETY: we just cast so the dtype matches.
466                        // we must take this path to correct for physical types.
467                        unsafe {
468                            Ok(Series::from_chunks_and_dtype_unchecked(
469                                ca.name().clone(),
470                                vec![arr],
471                                &List(Box::new(child_type)),
472                            ))
473                        }
474                    },
475                }
476            },
477            #[cfg(feature = "dtype-array")]
478            Array(child_type, width) => {
479                let physical_type = dtype.to_physical();
480
481                // cast to the physical type to avoid logical chunks.
482                let chunks = cast_chunks(ca.chunks(), &physical_type, options)?;
483                // SAFETY: we just cast so the dtype matches.
484                // we must take this path to correct for physical types.
485                unsafe {
486                    Ok(Series::from_chunks_and_dtype_unchecked(
487                        ca.name().clone(),
488                        chunks,
489                        &Array(child_type.clone(), *width),
490                    ))
491                }
492            },
493            #[cfg(feature = "dtype-u8")]
494            Binary => {
495                polars_ensure!(
496                    matches!(self.inner_dtype(), UInt8),
497                    InvalidOperation: "cannot cast List type (inner: '{:?}', to: '{:?}')",
498                    self.inner_dtype(),
499                    dtype,
500                );
501                let chunks = cast_chunks(self.chunks(), &DataType::Binary, options)?;
502
503                // SAFETY: we just cast so the dtype matches.
504                unsafe {
505                    Ok(Series::from_chunks_and_dtype_unchecked(
506                        self.name().clone(),
507                        chunks,
508                        &DataType::Binary,
509                    ))
510                }
511            },
512            _ => {
513                polars_bail!(
514                    InvalidOperation: "cannot cast List type (inner: '{:?}', to: '{:?}')",
515                    ca.inner_dtype(),
516                    dtype,
517                )
518            },
519        }
520    }
521
522    unsafe fn cast_unchecked(&self, dtype: &DataType) -> PolarsResult<Series> {
523        use DataType::*;
524        match dtype {
525            List(child_type) => cast_list_unchecked(self, child_type),
526            _ => self.cast_with_options(dtype, CastOptions::Overflowing),
527        }
528    }
529}
530
531/// We cannot cast anything to or from List/LargeList
532/// So this implementation casts the inner type
533#[cfg(feature = "dtype-array")]
534impl ChunkCast for ArrayChunked {
535    fn cast_with_options(&self, dtype: &DataType, options: CastOptions) -> PolarsResult<Series> {
536        let ca = self
537            .trim_lists_to_normalized_offsets()
538            .map_or(Cow::Borrowed(self), Cow::Owned);
539        let ca = ca.propagate_nulls().map_or(ca, Cow::Owned);
540
541        use DataType::*;
542        match dtype {
543            Array(child_type, width) => {
544                polars_ensure!(
545                    *width == ca.width(),
546                    InvalidOperation: "cannot cast Array to a different width"
547                );
548
549                match (ca.inner_dtype(), &**child_type) {
550                    (old, new) if old == new => Ok(ca.into_owned().into_series()),
551                    // TODO @ cat-rework: can we implement this now?
552                    #[cfg(feature = "dtype-categorical")]
553                    (dt, Categorical(_, _) | Enum(_, _)) if !matches!(dt, String) => {
554                        polars_bail!(InvalidOperation: "cannot cast Array inner type: '{:?}' to dtype: {:?}", dt, child_type)
555                    },
556                    _ => {
557                        // ensure the inner logical type bubbles up
558                        let (arr, child_type) =
559                            cast_fixed_size_list(ca.as_ref(), child_type, options)?;
560                        // SAFETY: we just cast so the dtype matches.
561                        // we must take this path to correct for physical types.
562                        unsafe {
563                            Ok(Series::from_chunks_and_dtype_unchecked(
564                                ca.name().clone(),
565                                vec![arr],
566                                &Array(Box::new(child_type), *width),
567                            ))
568                        }
569                    },
570                }
571            },
572            List(child_type) => {
573                let physical_type = dtype.to_physical();
574                // cast to the physical type to avoid logical chunks.
575                let chunks = cast_chunks(ca.chunks(), &physical_type, options)?;
576                // SAFETY: we just cast so the dtype matches.
577                // we must take this path to correct for physical types.
578                unsafe {
579                    Ok(Series::from_chunks_and_dtype_unchecked(
580                        ca.name().clone(),
581                        chunks,
582                        &List(child_type.clone()),
583                    ))
584                }
585            },
586            _ => {
587                polars_bail!(
588                    InvalidOperation: "cannot cast Array type (inner: '{:?}', to: '{:?}')",
589                    ca.inner_dtype(),
590                    dtype,
591                )
592            },
593        }
594    }
595
596    unsafe fn cast_unchecked(&self, dtype: &DataType) -> PolarsResult<Series> {
597        self.cast_with_options(dtype, CastOptions::Overflowing)
598    }
599}
600
601// Returns inner data type. This is needed because a cast can instantiate the dtype inner
602// values for instance with categoricals
603fn cast_list(
604    ca: &ListChunked,
605    child_type: &DataType,
606    options: CastOptions,
607) -> PolarsResult<(ArrayRef, DataType)> {
608    // We still rechunk because we must bubble up a single data-type
609    // TODO!: consider a version that works on chunks and merges the data-types and arrays.
610    let ca = ca.rechunk();
611    let arr = ca.downcast_as_array();
612    // SAFETY: inner dtype is passed correctly
613    let s = unsafe {
614        Series::from_chunks_and_dtype_unchecked(
615            PlSmallStr::EMPTY,
616            vec![arr.values().clone()],
617            ca.inner_dtype(),
618        )
619    };
620    let new_inner = s.cast_with_options(child_type, options)?;
621
622    let inner_dtype = new_inner.dtype().clone();
623    debug_assert_eq!(&inner_dtype, child_type);
624
625    let new_values = new_inner.array_ref(0).clone();
626
627    let dtype = ListArray::<i64>::default_datatype(new_values.dtype().clone());
628    let new_arr = ListArray::<i64>::new(
629        dtype,
630        arr.offsets().clone(),
631        new_values,
632        arr.validity().cloned(),
633    );
634    Ok((new_arr.boxed(), inner_dtype))
635}
636
637unsafe fn cast_list_unchecked(ca: &ListChunked, child_type: &DataType) -> PolarsResult<Series> {
638    // TODO! add chunked, but this must correct for list offsets.
639    let ca = ca.rechunk();
640    let arr = ca.downcast_as_array();
641    // SAFETY: inner dtype is passed correctly
642    let s = unsafe {
643        Series::from_chunks_and_dtype_unchecked(
644            PlSmallStr::EMPTY,
645            vec![arr.values().clone()],
646            ca.inner_dtype(),
647        )
648    };
649    let new_inner = s.cast_unchecked(child_type)?;
650    let new_values = new_inner.array_ref(0).clone();
651
652    let dtype = ListArray::<i64>::default_datatype(new_values.dtype().clone());
653    let new_arr = ListArray::<i64>::new(
654        dtype,
655        arr.offsets().clone(),
656        new_values,
657        arr.validity().cloned(),
658    );
659    Ok(ListChunked::from_chunks_and_dtype_unchecked(
660        ca.name().clone(),
661        vec![Box::new(new_arr)],
662        DataType::List(Box::new(child_type.clone())),
663    )
664    .into_series())
665}
666
667// Returns inner data type. This is needed because a cast can instantiate the dtype inner
668// values for instance with categoricals
669#[cfg(feature = "dtype-array")]
670fn cast_fixed_size_list(
671    ca: &ArrayChunked,
672    child_type: &DataType,
673    options: CastOptions,
674) -> PolarsResult<(ArrayRef, DataType)> {
675    let ca = ca.rechunk();
676    let arr = ca.downcast_as_array();
677    // SAFETY: inner dtype is passed correctly
678    let s = unsafe {
679        Series::from_chunks_and_dtype_unchecked(
680            PlSmallStr::EMPTY,
681            vec![arr.values().clone()],
682            ca.inner_dtype(),
683        )
684    };
685    let new_inner = s.cast_with_options(child_type, options)?;
686
687    let inner_dtype = new_inner.dtype().clone();
688    debug_assert_eq!(&inner_dtype, child_type);
689
690    let new_values = new_inner.array_ref(0).clone();
691
692    let dtype = FixedSizeListArray::default_datatype(new_values.dtype().clone(), ca.width());
693    let new_arr = FixedSizeListArray::new(dtype, ca.len(), new_values, arr.validity().cloned());
694    Ok((Box::new(new_arr), inner_dtype))
695}
696
697#[cfg(test)]
698mod test {
699    use crate::chunked_array::cast::CastOptions;
700    use crate::prelude::*;
701
702    #[test]
703    fn test_cast_list() -> PolarsResult<()> {
704        let mut builder = ListPrimitiveChunkedBuilder::<Int32Type>::new(
705            PlSmallStr::from_static("a"),
706            10,
707            10,
708            DataType::Int32,
709        );
710        builder.append_opt_slice(Some(&[1i32, 2, 3]));
711        builder.append_opt_slice(Some(&[1i32, 2, 3]));
712        let ca = builder.finish();
713
714        let new = ca.cast_with_options(
715            &DataType::List(DataType::Float64.into()),
716            CastOptions::Strict,
717        )?;
718
719        assert_eq!(new.dtype(), &DataType::List(DataType::Float64.into()));
720        Ok(())
721    }
722
723    #[test]
724    #[cfg(feature = "dtype-categorical")]
725    fn test_cast_noop() {
726        // check if we can cast categorical twice without panic
727        let ca = StringChunked::new(PlSmallStr::from_static("foo"), &["bar", "ham"]);
728        let cats = Categories::global();
729        let out = ca
730            .cast_with_options(
731                &DataType::from_categories(cats.clone()),
732                CastOptions::Strict,
733            )
734            .unwrap();
735        let out = out.cast(&DataType::from_categories(cats)).unwrap();
736        assert!(matches!(out.dtype(), &DataType::Categorical(_, _)))
737    }
738}