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