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 #[cfg(feature = "dtype-extension")]
119 Extension(typ, _) => out.into_extension(typ.clone()),
120 _ => out,
121 };
122
123 Ok(out)
124}
125
126fn cast_impl(
127 name: PlSmallStr,
128 chunks: &[ArrayRef],
129 dtype: &DataType,
130 options: CastOptions,
131) -> PolarsResult<Series> {
132 cast_impl_inner(name, chunks, dtype, options)
133}
134
135#[cfg(feature = "dtype-struct")]
136fn cast_single_to_struct(
137 name: PlSmallStr,
138 chunks: &[ArrayRef],
139 fields: &[Field],
140 options: CastOptions,
141) -> PolarsResult<Series> {
142 polars_ensure!(fields.len() == 1, InvalidOperation: "must specify one field in the struct");
143 let mut new_fields = Vec::with_capacity(fields.len());
144 let mut fields = fields.iter();
146 let fld = fields.next().unwrap();
147 let s = cast_impl_inner(fld.name.clone(), chunks, &fld.dtype, options)?;
148 let length = s.len();
149 new_fields.push(s);
150
151 for fld in fields {
152 new_fields.push(Series::full_null(fld.name.clone(), length, &fld.dtype));
153 }
154
155 StructChunked::from_series(name, length, new_fields.iter()).map(|ca| ca.into_series())
156}
157
158impl<T> ChunkedArray<T>
159where
160 T: PolarsNumericType,
161{
162 fn cast_impl(&self, dtype: &DataType, options: CastOptions) -> PolarsResult<Series> {
163 if self.dtype() == dtype {
164 let mut out = unsafe {
166 Series::from_chunks_and_dtype_unchecked(
167 self.name().clone(),
168 self.chunks.clone(),
169 dtype,
170 )
171 };
172 out.set_sorted_flag(self.is_sorted_flag());
173 return Ok(out);
174 }
175 match dtype {
176 #[cfg(feature = "dtype-categorical")]
177 DataType::Categorical(..) | DataType::Enum(..) => {
178 polars_bail!(
179 ComputeError:
180 "casting from {} to {dtype} is not supported.\n\
181 Instead of `.cast({dtype:?}`, use `.cat.to({dtype:?})`.",
182 T::get_static_dtype()
183 );
184 },
185
186 #[cfg(feature = "dtype-struct")]
187 DataType::Struct(fields) => {
188 cast_single_to_struct(self.name().clone(), &self.chunks, fields, options)
189 },
190 _ => cast_impl_inner(self.name().clone(), &self.chunks, dtype, options).map(|mut s| {
191 let to_signed = dtype.is_signed_integer();
196 let unsigned2unsigned =
197 self.dtype().is_unsigned_integer() && dtype.is_unsigned_integer();
198 let allowed = to_signed || unsigned2unsigned;
199
200 if (allowed)
201 && (s.null_count() == self.null_count())
202 || (self.dtype().to_physical() == dtype.to_physical())
204 {
205 let is_sorted = self.is_sorted_flag();
206 s.set_sorted_flag(is_sorted)
207 }
208 s
209 }),
210 }
211 }
212}
213
214impl<T> ChunkCast for ChunkedArray<T>
215where
216 T: PolarsNumericType,
217{
218 fn cast_with_options(&self, dtype: &DataType, options: CastOptions) -> PolarsResult<Series> {
219 self.cast_impl(dtype, options)
220 }
221
222 unsafe fn cast_unchecked(&self, dtype: &DataType) -> PolarsResult<Series> {
223 match dtype {
224 #[cfg(feature = "dtype-categorical")]
227 DataType::Categorical(cats, _mapping) => {
228 polars_ensure!(self.dtype() == &cats.physical().dtype(), ComputeError: "cannot cast numeric types to 'Categorical'");
229 with_match_categorical_physical_type!(cats.physical(), |$C| {
230 type PhysCa = ChunkedArray<<$C as PolarsCategoricalType>::PolarsPhysical>;
232 let ca = unsafe { &*(self as *const ChunkedArray<T> as *const PhysCa) };
233 Ok(CategoricalChunked::<$C>::from_cats_and_dtype_unchecked(ca.clone(), dtype.clone())
234 .into_series())
235 })
236 },
237
238 #[cfg(feature = "dtype-categorical")]
241 DataType::Enum(fcats, _mapping) => {
242 polars_ensure!(self.dtype() == &fcats.physical().dtype(), ComputeError: "cannot cast numeric types to 'Enum'");
243 with_match_categorical_physical_type!(fcats.physical(), |$C| {
244 type PhysCa = ChunkedArray<<$C as PolarsCategoricalType>::PolarsPhysical>;
246 let ca = unsafe { &*(self as *const ChunkedArray<T> as *const PhysCa) };
247 Ok(CategoricalChunked::<$C>::from_cats_and_dtype_unchecked(ca.clone(), dtype.clone()).into_series())
248 })
249 },
250
251 _ => self.cast_impl(dtype, CastOptions::Overflowing),
252 }
253 }
254}
255
256impl ChunkCast for StringChunked {
257 fn cast_with_options(&self, dtype: &DataType, options: CastOptions) -> PolarsResult<Series> {
258 match dtype {
259 #[cfg(feature = "dtype-categorical")]
260 DataType::Categorical(cats, _mapping) => {
261 with_match_categorical_physical_type!(cats.physical(), |$C| {
262 Ok(CategoricalChunked::<$C>::from_str_iter(self.name().clone(), dtype.clone(), self.iter())?
263 .into_series())
264 })
265 },
266 #[cfg(feature = "dtype-categorical")]
267 DataType::Enum(fcats, _mapping) => {
268 let ret = with_match_categorical_physical_type!(fcats.physical(), |$C| {
269 CategoricalChunked::<$C>::from_str_iter(self.name().clone(), dtype.clone(), self.iter())?
270 .into_series()
271 });
272
273 if options.is_strict() && self.null_count() != ret.null_count() {
274 handle_casting_failures(&self.clone().into_series(), &ret)?;
275 }
276
277 Ok(ret)
278 },
279 #[cfg(feature = "dtype-struct")]
280 DataType::Struct(fields) => {
281 cast_single_to_struct(self.name().clone(), &self.chunks, fields, options)
282 },
283 #[cfg(feature = "dtype-decimal")]
284 DataType::Decimal(precision, scale) => {
285 let chunks = self.downcast_iter().map(|arr| {
286 polars_compute::cast::binview_to_decimal(&arr.to_binview(), *precision, *scale)
287 .to(ArrowDataType::Int128)
288 });
289 let ca = Int128Chunked::from_chunk_iter(self.name().clone(), chunks);
290 Ok(ca.into_decimal_unchecked(*precision, *scale).into_series())
291 },
292 #[cfg(feature = "dtype-date")]
293 DataType::Date => {
294 let result = cast_chunks(&self.chunks, dtype, options)?;
295 let out = Series::try_from((self.name().clone(), result))?;
296 Ok(out)
297 },
298 #[cfg(feature = "dtype-time")]
299 DataType::Time => {
300 let result = cast_chunks(&self.chunks, dtype, options)?;
301 Series::try_from((self.name().clone(), result))
302 },
303 #[cfg(feature = "dtype-datetime")]
304 DataType::Datetime(time_unit, time_zone) => match time_zone {
305 #[cfg(feature = "timezones")]
306 Some(time_zone) => {
307 TimeZone::validate_time_zone(time_zone)?;
308 let result = cast_chunks(
309 &self.chunks,
310 &Datetime(time_unit.to_owned(), Some(time_zone.clone())),
311 options,
312 )?;
313 Series::try_from((self.name().clone(), result))
314 },
315 _ => {
316 let result =
317 cast_chunks(&self.chunks, &Datetime(time_unit.to_owned(), None), options)?;
318 Series::try_from((self.name().clone(), result))
319 },
320 },
321 _ => cast_impl(self.name().clone(), &self.chunks, dtype, options),
322 }
323 }
324
325 unsafe fn cast_unchecked(&self, dtype: &DataType) -> PolarsResult<Series> {
326 self.cast_with_options(dtype, CastOptions::Overflowing)
327 }
328}
329
330impl BinaryChunked {
331 pub unsafe fn to_string_unchecked(&self) -> StringChunked {
334 let chunks = self
335 .downcast_iter()
336 .map(|arr| unsafe { arr.to_utf8view_unchecked() }.boxed())
337 .collect();
338 let field = Arc::new(Field::new(self.name().clone(), DataType::String));
339
340 let mut ca = StringChunked::new_with_compute_len(field, chunks);
341
342 use StatisticsFlags as F;
343 ca.retain_flags_from(self, F::IS_SORTED_ANY | F::CAN_FAST_EXPLODE_LIST);
344 ca
345 }
346}
347
348impl StringChunked {
349 pub fn as_binary(&self) -> BinaryChunked {
350 let chunks = self
351 .downcast_iter()
352 .map(|arr| arr.to_binview().boxed())
353 .collect();
354 let field = Arc::new(Field::new(self.name().clone(), DataType::Binary));
355
356 let mut ca = BinaryChunked::new_with_compute_len(field, chunks);
357
358 use StatisticsFlags as F;
359 ca.retain_flags_from(self, F::IS_SORTED_ANY | F::CAN_FAST_EXPLODE_LIST);
360 ca
361 }
362}
363
364impl ChunkCast for BinaryChunked {
365 fn cast_with_options(&self, dtype: &DataType, options: CastOptions) -> PolarsResult<Series> {
366 match dtype {
367 #[cfg(feature = "dtype-struct")]
368 DataType::Struct(fields) => {
369 cast_single_to_struct(self.name().clone(), &self.chunks, fields, options)
370 },
371 _ => cast_impl(self.name().clone(), &self.chunks, dtype, options),
372 }
373 }
374
375 unsafe fn cast_unchecked(&self, dtype: &DataType) -> PolarsResult<Series> {
376 match dtype {
377 DataType::String => unsafe { Ok(self.to_string_unchecked().into_series()) },
378 _ => self.cast_with_options(dtype, CastOptions::Overflowing),
379 }
380 }
381}
382
383impl ChunkCast for BinaryOffsetChunked {
384 fn cast_with_options(&self, dtype: &DataType, options: CastOptions) -> PolarsResult<Series> {
385 match dtype {
386 #[cfg(feature = "dtype-struct")]
387 DataType::Struct(fields) => {
388 cast_single_to_struct(self.name().clone(), &self.chunks, fields, options)
389 },
390 _ => cast_impl(self.name().clone(), &self.chunks, dtype, options),
391 }
392 }
393
394 unsafe fn cast_unchecked(&self, dtype: &DataType) -> PolarsResult<Series> {
395 self.cast_with_options(dtype, CastOptions::Overflowing)
396 }
397}
398
399impl ChunkCast for BooleanChunked {
400 fn cast_with_options(&self, dtype: &DataType, options: CastOptions) -> PolarsResult<Series> {
401 match dtype {
402 #[cfg(feature = "dtype-struct")]
403 DataType::Struct(fields) => {
404 cast_single_to_struct(self.name().clone(), &self.chunks, fields, options)
405 },
406 #[cfg(feature = "dtype-categorical")]
407 DataType::Categorical(_, _) | DataType::Enum(_, _) => {
408 polars_bail!(InvalidOperation: "cannot cast Boolean to Categorical");
409 },
410 _ => cast_impl(self.name().clone(), &self.chunks, dtype, options),
411 }
412 }
413
414 unsafe fn cast_unchecked(&self, dtype: &DataType) -> PolarsResult<Series> {
415 self.cast_with_options(dtype, CastOptions::Overflowing)
416 }
417}
418
419impl ChunkCast for ListChunked {
420 fn cast_with_options(&self, dtype: &DataType, options: CastOptions) -> PolarsResult<Series> {
421 let ca = self
422 .trim_lists_to_normalized_offsets()
423 .map_or(Cow::Borrowed(self), Cow::Owned);
424 let ca = ca.propagate_nulls().map_or(ca, Cow::Owned);
425
426 use DataType::*;
427 match dtype {
428 List(child_type) => {
429 match (ca.inner_dtype(), &**child_type) {
430 (old, new) if old == new => Ok(ca.into_owned().into_series()),
431 #[cfg(feature = "dtype-categorical")]
433 (dt, Categorical(_, _) | Enum(_, _))
434 if !matches!(dt, Categorical(_, _) | Enum(_, _) | String | Null) =>
435 {
436 polars_bail!(InvalidOperation: "cannot cast List inner type: '{:?}' to Categorical", dt)
437 },
438 _ => {
439 let (arr, child_type) = cast_list(ca.as_ref(), child_type, options)?;
441 unsafe {
444 Ok(Series::from_chunks_and_dtype_unchecked(
445 ca.name().clone(),
446 vec![arr],
447 &List(Box::new(child_type)),
448 ))
449 }
450 },
451 }
452 },
453 #[cfg(feature = "dtype-array")]
454 Array(child_type, width) => {
455 let physical_type = dtype.to_physical();
456
457 let chunks = cast_chunks(ca.chunks(), &physical_type, options)?;
459 unsafe {
462 Ok(Series::from_chunks_and_dtype_unchecked(
463 ca.name().clone(),
464 chunks,
465 &Array(child_type.clone(), *width),
466 ))
467 }
468 },
469 #[cfg(feature = "dtype-u8")]
470 Binary => {
471 polars_ensure!(
472 matches!(self.inner_dtype(), UInt8),
473 InvalidOperation: "cannot cast List type (inner: '{:?}', to: '{:?}')",
474 self.inner_dtype(),
475 dtype,
476 );
477 let chunks = cast_chunks(self.chunks(), &DataType::Binary, options)?;
478
479 unsafe {
481 Ok(Series::from_chunks_and_dtype_unchecked(
482 self.name().clone(),
483 chunks,
484 &DataType::Binary,
485 ))
486 }
487 },
488 #[cfg(feature = "dtype-map")]
489 Map(to_key, to_value) => {
490 let storage = if ca.inner_dtype().is_nested_null() {
491 ca.cast_with_options(&dtype.map_storage_dtype().unwrap(), options)?
493 } else {
494 try_apply_map_entries(ca.as_ref(), |key, value| {
495 Ok((
496 key.cast_with_options(to_key, options)?,
497 value.cast_with_options(to_value, options)?,
498 ))
499 })?
500 .into_series()
501 };
502
503 Ok(MapChunked::try_from_storage(dtype.clone(), storage)?.into_series())
504 },
505 _ => {
506 polars_bail!(
507 InvalidOperation: "cannot cast List type (inner: '{:?}', to: '{:?}')",
508 ca.inner_dtype(),
509 dtype,
510 )
511 },
512 }
513 }
514
515 unsafe fn cast_unchecked(&self, dtype: &DataType) -> PolarsResult<Series> {
516 use DataType::*;
517 match dtype {
518 List(child_type) => cast_list_unchecked(self, child_type),
519 _ => self.cast_with_options(dtype, CastOptions::Overflowing),
520 }
521 }
522}
523
524#[cfg(feature = "dtype-array")]
527impl ChunkCast for ArrayChunked {
528 fn cast_with_options(&self, dtype: &DataType, options: CastOptions) -> PolarsResult<Series> {
529 let ca = self
530 .trim_lists_to_normalized_offsets()
531 .map_or(Cow::Borrowed(self), Cow::Owned);
532 let ca = ca.propagate_nulls().map_or(ca, Cow::Owned);
533
534 use DataType::*;
535 match dtype {
536 Array(child_type, width) => {
537 polars_ensure!(
538 *width == ca.width(),
539 InvalidOperation: "cannot cast Array to a different width"
540 );
541
542 match (ca.inner_dtype(), &**child_type) {
543 (old, new) if old == new => Ok(ca.into_owned().into_series()),
544 #[cfg(feature = "dtype-categorical")]
546 (dt, Categorical(_, _) | Enum(_, _)) if !matches!(dt, String | Null) => {
547 polars_bail!(InvalidOperation: "cannot cast Array inner type: '{:?}' to dtype: {:?}", dt, child_type)
548 },
549 _ => {
550 let (arr, child_type) =
552 cast_fixed_size_list(ca.as_ref(), child_type, options)?;
553 unsafe {
556 Ok(Series::from_chunks_and_dtype_unchecked(
557 ca.name().clone(),
558 vec![arr],
559 &Array(Box::new(child_type), *width),
560 ))
561 }
562 },
563 }
564 },
565 List(child_type) => {
566 let physical_type = dtype.to_physical();
567 let chunks = cast_chunks(ca.chunks(), &physical_type, options)?;
569 unsafe {
572 Ok(Series::from_chunks_and_dtype_unchecked(
573 ca.name().clone(),
574 chunks,
575 &List(child_type.clone()),
576 ))
577 }
578 },
579 _ => {
580 polars_bail!(
581 InvalidOperation: "cannot cast Array type (inner: '{:?}', to: '{:?}')",
582 ca.inner_dtype(),
583 dtype,
584 )
585 },
586 }
587 }
588
589 unsafe fn cast_unchecked(&self, dtype: &DataType) -> PolarsResult<Series> {
590 self.cast_with_options(dtype, CastOptions::Overflowing)
591 }
592}
593
594fn cast_list(
597 ca: &ListChunked,
598 child_type: &DataType,
599 options: CastOptions,
600) -> PolarsResult<(ArrayRef, DataType)> {
601 let ca = ca.rechunk();
604 let arr = ca.downcast_as_array();
605 let s = unsafe {
607 Series::from_chunks_and_dtype_unchecked(
608 PlSmallStr::EMPTY,
609 vec![arr.values().clone()],
610 ca.inner_dtype(),
611 )
612 };
613 let new_inner = s.cast_with_options(child_type, options)?;
614
615 let inner_dtype = new_inner.dtype().clone();
616 debug_assert_eq!(&inner_dtype, child_type);
617
618 let new_values = new_inner.array_ref(0).clone();
619
620 let dtype = ListArray::<i64>::default_datatype(new_values.dtype().clone());
621 let new_arr = ListArray::<i64>::new(
622 dtype,
623 arr.offsets().clone(),
624 new_values,
625 arr.validity().cloned(),
626 );
627 Ok((new_arr.boxed(), inner_dtype))
628}
629
630unsafe fn cast_list_unchecked(ca: &ListChunked, child_type: &DataType) -> PolarsResult<Series> {
631 let ca = ca.rechunk();
633 let arr = ca.downcast_as_array();
634 let s = unsafe {
636 Series::from_chunks_and_dtype_unchecked(
637 PlSmallStr::EMPTY,
638 vec![arr.values().clone()],
639 ca.inner_dtype(),
640 )
641 };
642 let new_inner = s.cast_unchecked(child_type)?;
643 let new_values = new_inner.array_ref(0).clone();
644
645 let dtype = ListArray::<i64>::default_datatype(new_values.dtype().clone());
646 let new_arr = ListArray::<i64>::new(
647 dtype,
648 arr.offsets().clone(),
649 new_values,
650 arr.validity().cloned(),
651 );
652 Ok(ListChunked::from_chunks_and_dtype_unchecked(
653 ca.name().clone(),
654 vec![Box::new(new_arr)],
655 DataType::List(Box::new(child_type.clone())),
656 )
657 .into_series())
658}
659
660#[cfg(feature = "dtype-array")]
663fn cast_fixed_size_list(
664 ca: &ArrayChunked,
665 child_type: &DataType,
666 options: CastOptions,
667) -> PolarsResult<(ArrayRef, DataType)> {
668 let ca = ca.rechunk();
669 let arr = ca.downcast_as_array();
670 let s = unsafe {
672 Series::from_chunks_and_dtype_unchecked(
673 PlSmallStr::EMPTY,
674 vec![arr.values().clone()],
675 ca.inner_dtype(),
676 )
677 };
678 let new_inner = s.cast_with_options(child_type, options)?;
679
680 let inner_dtype = new_inner.dtype().clone();
681 debug_assert_eq!(&inner_dtype, child_type);
682
683 let new_values = new_inner.array_ref(0).clone();
684
685 let dtype = FixedSizeListArray::default_datatype(new_values.dtype().clone(), ca.width());
686 let new_arr = FixedSizeListArray::new(dtype, ca.len(), new_values, arr.validity().cloned());
687 Ok((Box::new(new_arr), inner_dtype))
688}
689
690#[cfg(test)]
691mod test {
692 use crate::chunked_array::cast::CastOptions;
693 use crate::prelude::*;
694
695 #[test]
696 fn test_cast_list() -> PolarsResult<()> {
697 let mut builder = ListPrimitiveChunkedBuilder::<Int32Type>::new(
698 PlSmallStr::from_static("a"),
699 10,
700 10,
701 DataType::Int32,
702 );
703 builder.append_opt_slice(Some(&[1i32, 2, 3]));
704 builder.append_opt_slice(Some(&[1i32, 2, 3]));
705 let ca = builder.finish();
706
707 let new = ca.cast_with_options(
708 &DataType::List(DataType::Float64.into()),
709 CastOptions::Strict,
710 )?;
711
712 assert_eq!(new.dtype(), &DataType::List(DataType::Float64.into()));
713 Ok(())
714 }
715
716 #[test]
717 #[cfg(feature = "dtype-categorical")]
718 fn test_cast_noop() {
719 let ca = StringChunked::new(PlSmallStr::from_static("foo"), &["bar", "ham"]);
721 let cats = Categories::global();
722 let out = ca
723 .cast_with_options(
724 &DataType::from_categories(cats.clone()),
725 CastOptions::Strict,
726 )
727 .unwrap();
728 let out = out.cast(&DataType::from_categories(cats)).unwrap();
729 assert!(matches!(out.dtype(), &DataType::Categorical(_, _)))
730 }
731}