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