1use 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 #[default]
22 Strict,
23 NonStrict,
25 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 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 _ => out,
119 };
120
121 Ok(out)
122}
123
124fn cast_impl(
125 name: PlSmallStr,
126 chunks: &[ArrayRef],
127 dtype: &DataType,
128 options: CastOptions,
129) -> PolarsResult<Series> {
130 cast_impl_inner(name, chunks, dtype, options)
131}
132
133#[cfg(feature = "dtype-struct")]
134fn cast_single_to_struct(
135 name: PlSmallStr,
136 chunks: &[ArrayRef],
137 fields: &[Field],
138 options: CastOptions,
139) -> PolarsResult<Series> {
140 polars_ensure!(fields.len() == 1, InvalidOperation: "must specify one field in the struct");
141 let mut new_fields = Vec::with_capacity(fields.len());
142 let mut fields = fields.iter();
144 let fld = fields.next().unwrap();
145 let s = cast_impl_inner(fld.name.clone(), chunks, &fld.dtype, options)?;
146 let length = s.len();
147 new_fields.push(s);
148
149 for fld in fields {
150 new_fields.push(Series::full_null(fld.name.clone(), length, &fld.dtype));
151 }
152
153 StructChunked::from_series(name, length, new_fields.iter()).map(|ca| ca.into_series())
154}
155
156impl<T> ChunkedArray<T>
157where
158 T: PolarsNumericType,
159{
160 fn cast_impl(&self, dtype: &DataType, options: CastOptions) -> PolarsResult<Series> {
161 if self.dtype() == dtype {
162 let mut out = unsafe {
164 Series::from_chunks_and_dtype_unchecked(
165 self.name().clone(),
166 self.chunks.clone(),
167 dtype,
168 )
169 };
170 out.set_sorted_flag(self.is_sorted_flag());
171 return Ok(out);
172 }
173 match dtype {
174 #[cfg(feature = "dtype-categorical")]
177 DataType::Categorical(cats, _mapping) => {
178 let s = self.cast_with_options(&cats.physical().dtype(), options)?;
179 with_match_categorical_physical_type!(cats.physical(), |$C| {
180 type PhysCa = ChunkedArray<<$C as PolarsCategoricalType>::PolarsPhysical>;
182 let ca: &PhysCa = s.as_ref().as_ref();
183 Ok(CategoricalChunked::<$C>::from_cats_and_dtype(ca.clone(), dtype.clone())
184 .into_series())
185 })
186 },
187
188 #[cfg(feature = "dtype-categorical")]
191 DataType::Enum(fcats, _mapping) => {
192 let s = self.cast_with_options(&fcats.physical().dtype(), options)?;
193 with_match_categorical_physical_type!(fcats.physical(), |$C| {
194 type PhysCa = ChunkedArray<<$C as PolarsCategoricalType>::PolarsPhysical>;
196 let ca: &PhysCa = s.as_ref().as_ref();
197 Ok(CategoricalChunked::<$C>::from_cats_and_dtype(ca.clone(), dtype.clone()).into_series())
198 })
199 },
200
201 #[cfg(feature = "dtype-struct")]
202 DataType::Struct(fields) => {
203 cast_single_to_struct(self.name().clone(), &self.chunks, fields, options)
204 },
205 _ => cast_impl_inner(self.name().clone(), &self.chunks, dtype, options).map(|mut s| {
206 let to_signed = dtype.is_signed_integer();
211 let unsigned2unsigned =
212 self.dtype().is_unsigned_integer() && dtype.is_unsigned_integer();
213 let allowed = to_signed || unsigned2unsigned;
214
215 if (allowed)
216 && (s.null_count() == self.null_count())
217 || (self.dtype().to_physical() == dtype.to_physical())
219 {
220 let is_sorted = self.is_sorted_flag();
221 s.set_sorted_flag(is_sorted)
222 }
223 s
224 }),
225 }
226 }
227}
228
229impl<T> ChunkCast for ChunkedArray<T>
230where
231 T: PolarsNumericType,
232{
233 fn cast_with_options(&self, dtype: &DataType, options: CastOptions) -> PolarsResult<Series> {
234 self.cast_impl(dtype, options)
235 }
236
237 unsafe fn cast_unchecked(&self, dtype: &DataType) -> PolarsResult<Series> {
238 match dtype {
239 #[cfg(feature = "dtype-categorical")]
242 DataType::Categorical(cats, _mapping) => {
243 polars_ensure!(self.dtype() == &cats.physical().dtype(), ComputeError: "cannot cast numeric types to 'Categorical'");
244 with_match_categorical_physical_type!(cats.physical(), |$C| {
245 type PhysCa = ChunkedArray<<$C as PolarsCategoricalType>::PolarsPhysical>;
247 let ca = unsafe { &*(self as *const ChunkedArray<T> as *const PhysCa) };
248 Ok(CategoricalChunked::<$C>::from_cats_and_dtype_unchecked(ca.clone(), dtype.clone())
249 .into_series())
250 })
251 },
252
253 #[cfg(feature = "dtype-categorical")]
256 DataType::Enum(fcats, _mapping) => {
257 polars_ensure!(self.dtype() == &fcats.physical().dtype(), ComputeError: "cannot cast numeric types to 'Enum'");
258 with_match_categorical_physical_type!(fcats.physical(), |$C| {
259 type PhysCa = ChunkedArray<<$C as PolarsCategoricalType>::PolarsPhysical>;
261 let ca = unsafe { &*(self as *const ChunkedArray<T> as *const PhysCa) };
262 Ok(CategoricalChunked::<$C>::from_cats_and_dtype_unchecked(ca.clone(), dtype.clone()).into_series())
263 })
264 },
265
266 _ => self.cast_impl(dtype, CastOptions::Overflowing),
267 }
268 }
269}
270
271impl ChunkCast for StringChunked {
272 fn cast_with_options(&self, dtype: &DataType, options: CastOptions) -> PolarsResult<Series> {
273 match dtype {
274 #[cfg(feature = "dtype-categorical")]
275 DataType::Categorical(cats, _mapping) => {
276 with_match_categorical_physical_type!(cats.physical(), |$C| {
277 Ok(CategoricalChunked::<$C>::from_str_iter(self.name().clone(), dtype.clone(), self.iter())?
278 .into_series())
279 })
280 },
281 #[cfg(feature = "dtype-categorical")]
282 DataType::Enum(fcats, _mapping) => {
283 let ret = with_match_categorical_physical_type!(fcats.physical(), |$C| {
284 CategoricalChunked::<$C>::from_str_iter(self.name().clone(), dtype.clone(), self.iter())?
285 .into_series()
286 });
287
288 if options.is_strict() && self.null_count() != ret.null_count() {
289 handle_casting_failures(&self.clone().into_series(), &ret)?;
290 }
291
292 Ok(ret)
293 },
294 #[cfg(feature = "dtype-struct")]
295 DataType::Struct(fields) => {
296 cast_single_to_struct(self.name().clone(), &self.chunks, fields, options)
297 },
298 #[cfg(feature = "dtype-decimal")]
299 DataType::Decimal(precision, scale) => {
300 let chunks = self.downcast_iter().map(|arr| {
301 polars_compute::cast::binview_to_decimal(&arr.to_binview(), *precision, *scale)
302 .to(ArrowDataType::Int128)
303 });
304 let ca = Int128Chunked::from_chunk_iter(self.name().clone(), chunks);
305 Ok(ca.into_decimal_unchecked(*precision, *scale).into_series())
306 },
307 #[cfg(feature = "dtype-date")]
308 DataType::Date => {
309 let result = cast_chunks(&self.chunks, dtype, options)?;
310 let out = Series::try_from((self.name().clone(), result))?;
311 Ok(out)
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 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 {
432 fn cast_with_options(&self, dtype: &DataType, options: CastOptions) -> PolarsResult<Series> {
433 let ca = self
434 .trim_lists_to_normalized_offsets()
435 .map_or(Cow::Borrowed(self), Cow::Owned);
436 let ca = ca.propagate_nulls().map_or(ca, Cow::Owned);
437
438 use DataType::*;
439 match dtype {
440 List(child_type) => {
441 match (ca.inner_dtype(), &**child_type) {
442 (old, new) if old == new => Ok(ca.into_owned().into_series()),
443 #[cfg(feature = "dtype-categorical")]
445 (dt, Categorical(_, _) | Enum(_, _))
446 if !matches!(dt, Categorical(_, _) | Enum(_, _) | String | Null) =>
447 {
448 polars_bail!(InvalidOperation: "cannot cast List inner type: '{:?}' to Categorical", dt)
449 },
450 _ => {
451 let (arr, child_type) = cast_list(ca.as_ref(), child_type, options)?;
453 unsafe {
456 Ok(Series::from_chunks_and_dtype_unchecked(
457 ca.name().clone(),
458 vec![arr],
459 &List(Box::new(child_type)),
460 ))
461 }
462 },
463 }
464 },
465 #[cfg(feature = "dtype-array")]
466 Array(child_type, width) => {
467 let physical_type = dtype.to_physical();
468
469 let chunks = cast_chunks(ca.chunks(), &physical_type, options)?;
471 unsafe {
474 Ok(Series::from_chunks_and_dtype_unchecked(
475 ca.name().clone(),
476 chunks,
477 &Array(child_type.clone(), *width),
478 ))
479 }
480 },
481 #[cfg(feature = "dtype-u8")]
482 Binary => {
483 polars_ensure!(
484 matches!(self.inner_dtype(), UInt8),
485 InvalidOperation: "cannot cast List type (inner: '{:?}', to: '{:?}')",
486 self.inner_dtype(),
487 dtype,
488 );
489 let chunks = cast_chunks(self.chunks(), &DataType::Binary, options)?;
490
491 unsafe {
493 Ok(Series::from_chunks_and_dtype_unchecked(
494 self.name().clone(),
495 chunks,
496 &DataType::Binary,
497 ))
498 }
499 },
500 _ => {
501 polars_bail!(
502 InvalidOperation: "cannot cast List type (inner: '{:?}', to: '{:?}')",
503 ca.inner_dtype(),
504 dtype,
505 )
506 },
507 }
508 }
509
510 unsafe fn cast_unchecked(&self, dtype: &DataType) -> PolarsResult<Series> {
511 use DataType::*;
512 match dtype {
513 List(child_type) => cast_list_unchecked(self, child_type),
514 _ => self.cast_with_options(dtype, CastOptions::Overflowing),
515 }
516 }
517}
518
519#[cfg(feature = "dtype-array")]
522impl ChunkCast for ArrayChunked {
523 fn cast_with_options(&self, dtype: &DataType, options: CastOptions) -> PolarsResult<Series> {
524 let ca = self
525 .trim_lists_to_normalized_offsets()
526 .map_or(Cow::Borrowed(self), Cow::Owned);
527 let ca = ca.propagate_nulls().map_or(ca, Cow::Owned);
528
529 use DataType::*;
530 match dtype {
531 Array(child_type, width) => {
532 polars_ensure!(
533 *width == ca.width(),
534 InvalidOperation: "cannot cast Array to a different width"
535 );
536
537 match (ca.inner_dtype(), &**child_type) {
538 (old, new) if old == new => Ok(ca.into_owned().into_series()),
539 #[cfg(feature = "dtype-categorical")]
541 (dt, Categorical(_, _) | Enum(_, _)) if !matches!(dt, String) => {
542 polars_bail!(InvalidOperation: "cannot cast Array inner type: '{:?}' to dtype: {:?}", dt, child_type)
543 },
544 _ => {
545 let (arr, child_type) =
547 cast_fixed_size_list(ca.as_ref(), child_type, options)?;
548 unsafe {
551 Ok(Series::from_chunks_and_dtype_unchecked(
552 ca.name().clone(),
553 vec![arr],
554 &Array(Box::new(child_type), *width),
555 ))
556 }
557 },
558 }
559 },
560 List(child_type) => {
561 let physical_type = dtype.to_physical();
562 let chunks = cast_chunks(ca.chunks(), &physical_type, options)?;
564 unsafe {
567 Ok(Series::from_chunks_and_dtype_unchecked(
568 ca.name().clone(),
569 chunks,
570 &List(child_type.clone()),
571 ))
572 }
573 },
574 _ => {
575 polars_bail!(
576 InvalidOperation: "cannot cast Array type (inner: '{:?}', to: '{:?}')",
577 ca.inner_dtype(),
578 dtype,
579 )
580 },
581 }
582 }
583
584 unsafe fn cast_unchecked(&self, dtype: &DataType) -> PolarsResult<Series> {
585 self.cast_with_options(dtype, CastOptions::Overflowing)
586 }
587}
588
589fn cast_list(
592 ca: &ListChunked,
593 child_type: &DataType,
594 options: CastOptions,
595) -> PolarsResult<(ArrayRef, DataType)> {
596 let ca = ca.rechunk();
599 let arr = ca.downcast_as_array();
600 let s = unsafe {
602 Series::from_chunks_and_dtype_unchecked(
603 PlSmallStr::EMPTY,
604 vec![arr.values().clone()],
605 ca.inner_dtype(),
606 )
607 };
608 let new_inner = s.cast_with_options(child_type, options)?;
609
610 let inner_dtype = new_inner.dtype().clone();
611 debug_assert_eq!(&inner_dtype, child_type);
612
613 let new_values = new_inner.array_ref(0).clone();
614
615 let dtype = ListArray::<i64>::default_datatype(new_values.dtype().clone());
616 let new_arr = ListArray::<i64>::new(
617 dtype,
618 arr.offsets().clone(),
619 new_values,
620 arr.validity().cloned(),
621 );
622 Ok((new_arr.boxed(), inner_dtype))
623}
624
625unsafe fn cast_list_unchecked(ca: &ListChunked, child_type: &DataType) -> PolarsResult<Series> {
626 let ca = ca.rechunk();
628 let arr = ca.downcast_as_array();
629 let s = unsafe {
631 Series::from_chunks_and_dtype_unchecked(
632 PlSmallStr::EMPTY,
633 vec![arr.values().clone()],
634 ca.inner_dtype(),
635 )
636 };
637 let new_inner = s.cast_unchecked(child_type)?;
638 let new_values = new_inner.array_ref(0).clone();
639
640 let dtype = ListArray::<i64>::default_datatype(new_values.dtype().clone());
641 let new_arr = ListArray::<i64>::new(
642 dtype,
643 arr.offsets().clone(),
644 new_values,
645 arr.validity().cloned(),
646 );
647 Ok(ListChunked::from_chunks_and_dtype_unchecked(
648 ca.name().clone(),
649 vec![Box::new(new_arr)],
650 DataType::List(Box::new(child_type.clone())),
651 )
652 .into_series())
653}
654
655#[cfg(feature = "dtype-array")]
658fn cast_fixed_size_list(
659 ca: &ArrayChunked,
660 child_type: &DataType,
661 options: CastOptions,
662) -> PolarsResult<(ArrayRef, DataType)> {
663 let ca = ca.rechunk();
664 let arr = ca.downcast_as_array();
665 let s = unsafe {
667 Series::from_chunks_and_dtype_unchecked(
668 PlSmallStr::EMPTY,
669 vec![arr.values().clone()],
670 ca.inner_dtype(),
671 )
672 };
673 let new_inner = s.cast_with_options(child_type, options)?;
674
675 let inner_dtype = new_inner.dtype().clone();
676 debug_assert_eq!(&inner_dtype, child_type);
677
678 let new_values = new_inner.array_ref(0).clone();
679
680 let dtype = FixedSizeListArray::default_datatype(new_values.dtype().clone(), ca.width());
681 let new_arr = FixedSizeListArray::new(dtype, ca.len(), new_values, arr.validity().cloned());
682 Ok((Box::new(new_arr), inner_dtype))
683}
684
685#[cfg(test)]
686mod test {
687 use crate::chunked_array::cast::CastOptions;
688 use crate::prelude::*;
689
690 #[test]
691 fn test_cast_list() -> PolarsResult<()> {
692 let mut builder = ListPrimitiveChunkedBuilder::<Int32Type>::new(
693 PlSmallStr::from_static("a"),
694 10,
695 10,
696 DataType::Int32,
697 );
698 builder.append_opt_slice(Some(&[1i32, 2, 3]));
699 builder.append_opt_slice(Some(&[1i32, 2, 3]));
700 let ca = builder.finish();
701
702 let new = ca.cast_with_options(
703 &DataType::List(DataType::Float64.into()),
704 CastOptions::Strict,
705 )?;
706
707 assert_eq!(new.dtype(), &DataType::List(DataType::Float64.into()));
708 Ok(())
709 }
710
711 #[test]
712 #[cfg(feature = "dtype-categorical")]
713 fn test_cast_noop() {
714 let ca = StringChunked::new(PlSmallStr::from_static("foo"), &["bar", "ham"]);
716 let cats = Categories::global();
717 let out = ca
718 .cast_with_options(
719 &DataType::from_categories(cats.clone()),
720 CastOptions::Strict,
721 )
722 .unwrap();
723 let out = out.cast(&DataType::from_categories(cats)).unwrap();
724 assert!(matches!(out.dtype(), &DataType::Categorical(_, _)))
725 }
726}