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