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_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 polars_ensure!(arr.null_count() == new.null_count(), ComputeError: "strict cast failed");
64 Ok(new)
65 })
66
67 } else {
68 out
69 }
70 })
71 .collect::<PolarsResult<Vec<_>>>()
72}
73
74fn cast_impl_inner(
75 name: PlSmallStr,
76 chunks: &[ArrayRef],
77 dtype: &DataType,
78 options: CastOptions,
79) -> PolarsResult<Series> {
80 let chunks = match dtype {
81 #[cfg(feature = "dtype-decimal")]
82 DataType::Decimal(_, _) => {
83 let mut chunks = cast_chunks(chunks, dtype, options)?;
84 for chunk in chunks.iter_mut() {
86 *chunk = std::mem::take(
87 chunk
88 .as_any_mut()
89 .downcast_mut::<PrimitiveArray<i128>>()
90 .unwrap(),
91 )
92 .to(ArrowDataType::Int128)
93 .to_boxed();
94 }
95 chunks
96 },
97 _ => cast_chunks(chunks, &dtype.to_physical(), options)?,
98 };
99
100 let out = Series::try_from((name, chunks))?;
101 use DataType::*;
102 let out = match dtype {
103 Date => out.into_date(),
104 Datetime(tu, tz) => match tz {
105 #[cfg(feature = "timezones")]
106 Some(tz) => {
107 TimeZone::validate_time_zone(tz)?;
108 out.into_datetime(*tu, Some(tz.clone()))
109 },
110 _ => out.into_datetime(*tu, None),
111 },
112 Duration(tu) => out.into_duration(*tu),
113 #[cfg(feature = "dtype-time")]
114 Time => out.into_time(),
115 #[cfg(feature = "dtype-decimal")]
116 Decimal(precision, scale) => out.into_decimal(*precision, *scale)?,
117 _ => out,
118 };
119
120 Ok(out)
121}
122
123fn cast_impl(
124 name: PlSmallStr,
125 chunks: &[ArrayRef],
126 dtype: &DataType,
127 options: CastOptions,
128) -> PolarsResult<Series> {
129 cast_impl_inner(name, chunks, dtype, options)
130}
131
132#[cfg(feature = "dtype-struct")]
133fn cast_single_to_struct(
134 name: PlSmallStr,
135 chunks: &[ArrayRef],
136 fields: &[Field],
137 options: CastOptions,
138) -> PolarsResult<Series> {
139 polars_ensure!(fields.len() == 1, InvalidOperation: "must specify one field in the struct");
140 let mut new_fields = Vec::with_capacity(fields.len());
141 let mut fields = fields.iter();
143 let fld = fields.next().unwrap();
144 let s = cast_impl_inner(fld.name.clone(), chunks, &fld.dtype, options)?;
145 let length = s.len();
146 new_fields.push(s);
147
148 for fld in fields {
149 new_fields.push(Series::full_null(fld.name.clone(), length, &fld.dtype));
150 }
151
152 StructChunked::from_series(name, length, new_fields.iter()).map(|ca| ca.into_series())
153}
154
155impl<T> ChunkedArray<T>
156where
157 T: PolarsNumericType,
158{
159 fn cast_impl(&self, dtype: &DataType, options: CastOptions) -> PolarsResult<Series> {
160 if self.dtype() == dtype {
161 let mut out = unsafe {
163 Series::from_chunks_and_dtype_unchecked(
164 self.name().clone(),
165 self.chunks.clone(),
166 dtype,
167 )
168 };
169 out.set_sorted_flag(self.is_sorted_flag());
170 return Ok(out);
171 }
172 match dtype {
173 #[cfg(feature = "dtype-categorical")]
176 DataType::Categorical(cats, _mapping) => {
177 let s = self.cast_with_options(&cats.physical().dtype(), options)?;
178 with_match_categorical_physical_type!(cats.physical(), |$C| {
179 type PhysCa = ChunkedArray<<$C as PolarsCategoricalType>::PolarsPhysical>;
181 let ca: &PhysCa = s.as_ref().as_ref();
182 Ok(CategoricalChunked::<$C>::from_cats_and_dtype(ca.clone(), dtype.clone())
183 .into_series())
184 })
185 },
186
187 #[cfg(feature = "dtype-categorical")]
190 DataType::Enum(fcats, _mapping) => {
191 let s = self.cast_with_options(&fcats.physical().dtype(), options)?;
192 with_match_categorical_physical_type!(fcats.physical(), |$C| {
193 type PhysCa = ChunkedArray<<$C as PolarsCategoricalType>::PolarsPhysical>;
195 let ca: &PhysCa = s.as_ref().as_ref();
196 Ok(CategoricalChunked::<$C>::from_cats_and_dtype(ca.clone(), dtype.clone()).into_series())
197 })
198 },
199
200 #[cfg(feature = "dtype-struct")]
201 DataType::Struct(fields) => {
202 cast_single_to_struct(self.name().clone(), &self.chunks, fields, options)
203 },
204 _ => cast_impl_inner(self.name().clone(), &self.chunks, dtype, options).map(|mut s| {
205 let to_signed = dtype.is_signed_integer();
210 let unsigned2unsigned =
211 self.dtype().is_unsigned_integer() && dtype.is_unsigned_integer();
212 let allowed = to_signed || unsigned2unsigned;
213
214 if (allowed)
215 && (s.null_count() == self.null_count())
216 || (self.dtype().to_physical() == dtype.to_physical())
218 {
219 let is_sorted = self.is_sorted_flag();
220 s.set_sorted_flag(is_sorted)
221 }
222 s
223 }),
224 }
225 }
226}
227
228impl<T> ChunkCast for ChunkedArray<T>
229where
230 T: PolarsNumericType,
231{
232 fn cast_with_options(&self, dtype: &DataType, options: CastOptions) -> PolarsResult<Series> {
233 self.cast_impl(dtype, options)
234 }
235
236 unsafe fn cast_unchecked(&self, dtype: &DataType) -> PolarsResult<Series> {
237 match dtype {
238 #[cfg(feature = "dtype-categorical")]
241 DataType::Categorical(cats, _mapping) => {
242 polars_ensure!(self.dtype() == &cats.physical().dtype(), ComputeError: "cannot cast numeric types to 'Categorical'");
243 with_match_categorical_physical_type!(cats.physical(), |$C| {
244 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())
248 .into_series())
249 })
250 },
251
252 #[cfg(feature = "dtype-categorical")]
255 DataType::Enum(fcats, _mapping) => {
256 polars_ensure!(self.dtype() == &fcats.physical().dtype(), ComputeError: "cannot cast numeric types to 'Enum'");
257 with_match_categorical_physical_type!(fcats.physical(), |$C| {
258 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-datetime")]
313 DataType::Datetime(time_unit, time_zone) => match time_zone {
314 #[cfg(feature = "timezones")]
315 Some(time_zone) => {
316 TimeZone::validate_time_zone(time_zone)?;
317 let result = cast_chunks(
318 &self.chunks,
319 &Datetime(time_unit.to_owned(), Some(time_zone.clone())),
320 options,
321 )?;
322 Series::try_from((self.name().clone(), result))
323 },
324 _ => {
325 let result =
326 cast_chunks(&self.chunks, &Datetime(time_unit.to_owned(), None), options)?;
327 Series::try_from((self.name().clone(), result))
328 },
329 },
330 _ => cast_impl(self.name().clone(), &self.chunks, dtype, options),
331 }
332 }
333
334 unsafe fn cast_unchecked(&self, dtype: &DataType) -> PolarsResult<Series> {
335 self.cast_with_options(dtype, CastOptions::Overflowing)
336 }
337}
338
339impl BinaryChunked {
340 pub unsafe fn to_string_unchecked(&self) -> StringChunked {
343 let chunks = self
344 .downcast_iter()
345 .map(|arr| unsafe { arr.to_utf8view_unchecked() }.boxed())
346 .collect();
347 let field = Arc::new(Field::new(self.name().clone(), DataType::String));
348
349 let mut ca = StringChunked::new_with_compute_len(field, chunks);
350
351 use StatisticsFlags as F;
352 ca.retain_flags_from(self, F::IS_SORTED_ANY | F::CAN_FAST_EXPLODE_LIST);
353 ca
354 }
355}
356
357impl StringChunked {
358 pub fn as_binary(&self) -> BinaryChunked {
359 let chunks = self
360 .downcast_iter()
361 .map(|arr| arr.to_binview().boxed())
362 .collect();
363 let field = Arc::new(Field::new(self.name().clone(), DataType::Binary));
364
365 let mut ca = BinaryChunked::new_with_compute_len(field, chunks);
366
367 use StatisticsFlags as F;
368 ca.retain_flags_from(self, F::IS_SORTED_ANY | F::CAN_FAST_EXPLODE_LIST);
369 ca
370 }
371}
372
373impl ChunkCast for BinaryChunked {
374 fn cast_with_options(&self, dtype: &DataType, options: CastOptions) -> PolarsResult<Series> {
375 match dtype {
376 #[cfg(feature = "dtype-struct")]
377 DataType::Struct(fields) => {
378 cast_single_to_struct(self.name().clone(), &self.chunks, fields, options)
379 },
380 _ => cast_impl(self.name().clone(), &self.chunks, dtype, options),
381 }
382 }
383
384 unsafe fn cast_unchecked(&self, dtype: &DataType) -> PolarsResult<Series> {
385 match dtype {
386 DataType::String => unsafe { Ok(self.to_string_unchecked().into_series()) },
387 _ => self.cast_with_options(dtype, CastOptions::Overflowing),
388 }
389 }
390}
391
392impl ChunkCast for BinaryOffsetChunked {
393 fn cast_with_options(&self, dtype: &DataType, options: CastOptions) -> PolarsResult<Series> {
394 match dtype {
395 #[cfg(feature = "dtype-struct")]
396 DataType::Struct(fields) => {
397 cast_single_to_struct(self.name().clone(), &self.chunks, fields, options)
398 },
399 _ => cast_impl(self.name().clone(), &self.chunks, dtype, options),
400 }
401 }
402
403 unsafe fn cast_unchecked(&self, dtype: &DataType) -> PolarsResult<Series> {
404 self.cast_with_options(dtype, CastOptions::Overflowing)
405 }
406}
407
408impl ChunkCast for BooleanChunked {
409 fn cast_with_options(&self, dtype: &DataType, options: CastOptions) -> PolarsResult<Series> {
410 match dtype {
411 #[cfg(feature = "dtype-struct")]
412 DataType::Struct(fields) => {
413 cast_single_to_struct(self.name().clone(), &self.chunks, fields, options)
414 },
415 #[cfg(feature = "dtype-categorical")]
416 DataType::Categorical(_, _) | DataType::Enum(_, _) => {
417 polars_bail!(InvalidOperation: "cannot cast Boolean to Categorical");
418 },
419 _ => cast_impl(self.name().clone(), &self.chunks, dtype, options),
420 }
421 }
422
423 unsafe fn cast_unchecked(&self, dtype: &DataType) -> PolarsResult<Series> {
424 self.cast_with_options(dtype, CastOptions::Overflowing)
425 }
426}
427
428impl ChunkCast for ListChunked {
431 fn cast_with_options(&self, dtype: &DataType, options: CastOptions) -> PolarsResult<Series> {
432 let ca = self
433 .trim_lists_to_normalized_offsets()
434 .map_or(Cow::Borrowed(self), Cow::Owned);
435 let ca = ca.propagate_nulls().map_or(ca, Cow::Owned);
436
437 use DataType::*;
438 match dtype {
439 List(child_type) => {
440 match (ca.inner_dtype(), &**child_type) {
441 (old, new) if old == new => Ok(ca.into_owned().into_series()),
442 #[cfg(feature = "dtype-categorical")]
444 (dt, Categorical(_, _) | Enum(_, _))
445 if !matches!(dt, Categorical(_, _) | Enum(_, _) | String | Null) =>
446 {
447 polars_bail!(InvalidOperation: "cannot cast List inner type: '{:?}' to Categorical", dt)
448 },
449 _ => {
450 let (arr, child_type) = cast_list(ca.as_ref(), child_type, options)?;
452 unsafe {
455 Ok(Series::from_chunks_and_dtype_unchecked(
456 ca.name().clone(),
457 vec![arr],
458 &List(Box::new(child_type)),
459 ))
460 }
461 },
462 }
463 },
464 #[cfg(feature = "dtype-array")]
465 Array(child_type, width) => {
466 let physical_type = dtype.to_physical();
467
468 let chunks = cast_chunks(ca.chunks(), &physical_type, options)?;
470 unsafe {
473 Ok(Series::from_chunks_and_dtype_unchecked(
474 ca.name().clone(),
475 chunks,
476 &Array(child_type.clone(), *width),
477 ))
478 }
479 },
480 #[cfg(feature = "dtype-u8")]
481 Binary => {
482 polars_ensure!(
483 matches!(self.inner_dtype(), UInt8),
484 InvalidOperation: "cannot cast List type (inner: '{:?}', to: '{:?}')",
485 self.inner_dtype(),
486 dtype,
487 );
488 let chunks = cast_chunks(self.chunks(), &DataType::Binary, options)?;
489
490 unsafe {
492 Ok(Series::from_chunks_and_dtype_unchecked(
493 self.name().clone(),
494 chunks,
495 &DataType::Binary,
496 ))
497 }
498 },
499 _ => {
500 polars_bail!(
501 InvalidOperation: "cannot cast List type (inner: '{:?}', to: '{:?}')",
502 ca.inner_dtype(),
503 dtype,
504 )
505 },
506 }
507 }
508
509 unsafe fn cast_unchecked(&self, dtype: &DataType) -> PolarsResult<Series> {
510 use DataType::*;
511 match dtype {
512 List(child_type) => cast_list_unchecked(self, child_type),
513 _ => self.cast_with_options(dtype, CastOptions::Overflowing),
514 }
515 }
516}
517
518#[cfg(feature = "dtype-array")]
521impl ChunkCast for ArrayChunked {
522 fn cast_with_options(&self, dtype: &DataType, options: CastOptions) -> PolarsResult<Series> {
523 let ca = self
524 .trim_lists_to_normalized_offsets()
525 .map_or(Cow::Borrowed(self), Cow::Owned);
526 let ca = ca.propagate_nulls().map_or(ca, Cow::Owned);
527
528 use DataType::*;
529 match dtype {
530 Array(child_type, width) => {
531 polars_ensure!(
532 *width == ca.width(),
533 InvalidOperation: "cannot cast Array to a different width"
534 );
535
536 match (ca.inner_dtype(), &**child_type) {
537 (old, new) if old == new => Ok(ca.into_owned().into_series()),
538 #[cfg(feature = "dtype-categorical")]
540 (dt, Categorical(_, _) | Enum(_, _)) if !matches!(dt, String) => {
541 polars_bail!(InvalidOperation: "cannot cast Array inner type: '{:?}' to dtype: {:?}", dt, child_type)
542 },
543 _ => {
544 let (arr, child_type) =
546 cast_fixed_size_list(ca.as_ref(), child_type, options)?;
547 unsafe {
550 Ok(Series::from_chunks_and_dtype_unchecked(
551 ca.name().clone(),
552 vec![arr],
553 &Array(Box::new(child_type), *width),
554 ))
555 }
556 },
557 }
558 },
559 List(child_type) => {
560 let physical_type = dtype.to_physical();
561 let chunks = cast_chunks(ca.chunks(), &physical_type, options)?;
563 unsafe {
566 Ok(Series::from_chunks_and_dtype_unchecked(
567 ca.name().clone(),
568 chunks,
569 &List(child_type.clone()),
570 ))
571 }
572 },
573 _ => {
574 polars_bail!(
575 InvalidOperation: "cannot cast Array type (inner: '{:?}', to: '{:?}')",
576 ca.inner_dtype(),
577 dtype,
578 )
579 },
580 }
581 }
582
583 unsafe fn cast_unchecked(&self, dtype: &DataType) -> PolarsResult<Series> {
584 self.cast_with_options(dtype, CastOptions::Overflowing)
585 }
586}
587
588fn cast_list(
591 ca: &ListChunked,
592 child_type: &DataType,
593 options: CastOptions,
594) -> PolarsResult<(ArrayRef, DataType)> {
595 let ca = ca.rechunk();
598 let arr = ca.downcast_as_array();
599 let s = unsafe {
601 Series::from_chunks_and_dtype_unchecked(
602 PlSmallStr::EMPTY,
603 vec![arr.values().clone()],
604 ca.inner_dtype(),
605 )
606 };
607 let new_inner = s.cast_with_options(child_type, options)?;
608
609 let inner_dtype = new_inner.dtype().clone();
610 debug_assert_eq!(&inner_dtype, child_type);
611
612 let new_values = new_inner.array_ref(0).clone();
613
614 let dtype = ListArray::<i64>::default_datatype(new_values.dtype().clone());
615 let new_arr = ListArray::<i64>::new(
616 dtype,
617 arr.offsets().clone(),
618 new_values,
619 arr.validity().cloned(),
620 );
621 Ok((new_arr.boxed(), inner_dtype))
622}
623
624unsafe fn cast_list_unchecked(ca: &ListChunked, child_type: &DataType) -> PolarsResult<Series> {
625 let ca = ca.rechunk();
627 let arr = ca.downcast_as_array();
628 let s = unsafe {
630 Series::from_chunks_and_dtype_unchecked(
631 PlSmallStr::EMPTY,
632 vec![arr.values().clone()],
633 ca.inner_dtype(),
634 )
635 };
636 let new_inner = s.cast_unchecked(child_type)?;
637 let new_values = new_inner.array_ref(0).clone();
638
639 let dtype = ListArray::<i64>::default_datatype(new_values.dtype().clone());
640 let new_arr = ListArray::<i64>::new(
641 dtype,
642 arr.offsets().clone(),
643 new_values,
644 arr.validity().cloned(),
645 );
646 Ok(ListChunked::from_chunks_and_dtype_unchecked(
647 ca.name().clone(),
648 vec![Box::new(new_arr)],
649 DataType::List(Box::new(child_type.clone())),
650 )
651 .into_series())
652}
653
654#[cfg(feature = "dtype-array")]
657fn cast_fixed_size_list(
658 ca: &ArrayChunked,
659 child_type: &DataType,
660 options: CastOptions,
661) -> PolarsResult<(ArrayRef, DataType)> {
662 let ca = ca.rechunk();
663 let arr = ca.downcast_as_array();
664 let s = unsafe {
666 Series::from_chunks_and_dtype_unchecked(
667 PlSmallStr::EMPTY,
668 vec![arr.values().clone()],
669 ca.inner_dtype(),
670 )
671 };
672 let new_inner = s.cast_with_options(child_type, options)?;
673
674 let inner_dtype = new_inner.dtype().clone();
675 debug_assert_eq!(&inner_dtype, child_type);
676
677 let new_values = new_inner.array_ref(0).clone();
678
679 let dtype = FixedSizeListArray::default_datatype(new_values.dtype().clone(), ca.width());
680 let new_arr = FixedSizeListArray::new(dtype, ca.len(), new_values, arr.validity().cloned());
681 Ok((Box::new(new_arr), inner_dtype))
682}
683
684#[cfg(test)]
685mod test {
686 use crate::chunked_array::cast::CastOptions;
687 use crate::prelude::*;
688
689 #[test]
690 fn test_cast_list() -> PolarsResult<()> {
691 let mut builder = ListPrimitiveChunkedBuilder::<Int32Type>::new(
692 PlSmallStr::from_static("a"),
693 10,
694 10,
695 DataType::Int32,
696 );
697 builder.append_opt_slice(Some(&[1i32, 2, 3]));
698 builder.append_opt_slice(Some(&[1i32, 2, 3]));
699 let ca = builder.finish();
700
701 let new = ca.cast_with_options(
702 &DataType::List(DataType::Float64.into()),
703 CastOptions::Strict,
704 )?;
705
706 assert_eq!(new.dtype(), &DataType::List(DataType::Float64.into()));
707 Ok(())
708 }
709
710 #[test]
711 #[cfg(feature = "dtype-categorical")]
712 fn test_cast_noop() {
713 let ca = StringChunked::new(PlSmallStr::from_static("foo"), &["bar", "ham"]);
715 let cats = Categories::global();
716 let out = ca
717 .cast_with_options(
718 &DataType::from_categories(cats.clone()),
719 CastOptions::Strict,
720 )
721 .unwrap();
722 let out = out.cast(&DataType::from_categories(cats)).unwrap();
723 assert!(matches!(out.dtype(), &DataType::Categorical(_, _)))
724 }
725}