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