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