simd_kernels/kernels/
unary.rs

1// Copyright Peter Bower 2025. All Rights Reserved.
2// Licensed under Mozilla Public License (MPL) 2.0.
3
4//! # **Unary Operations Kernels Module** - *Single-Array Transformations*
5//!
6//! Unary operation kernels for single-array transformations
7//! with SIMD acceleration and null-aware semantics. Essential building blocks for data
8//! preprocessing, mathematical transformations, and analytical computations.
9//!
10//! ## Core Operations
11//! - **Mathematical functions**: Absolute value, negation, square root, logarithmic, and trigonometric functions
12//! - **Type casting**: Safe and unsafe type conversions with overflow detection
13//! - **Boolean operations**: Logical NOT operations on boolean arrays with bitmask optimisation
14//! - **Null handling**: NULL coalescing and null indicator operations  
15//! - **Statistical transforms**: Standardisation, normalisation, and ranking operations
16//! - **String transformations**: Length calculation, case conversion, and format operations
17
18include!(concat!(env!("OUT_DIR"), "/simd_lanes.rs"));
19
20use std::hash::Hash;
21#[cfg(feature = "simd")]
22use std::simd::num::SimdUint;
23use std::simd::{LaneCount, SupportedLaneCount};
24
25#[cfg(feature = "fast_hash")]
26use ahash::AHashMap;
27use minarrow::{
28    BooleanAVT, BooleanArray, CategoricalAVT, CategoricalArray, FloatAVT, FloatArray, Integer,
29    IntegerAVT, IntegerArray, StringAVT, StringArray, Vec64,
30};
31#[cfg(feature = "datetime")]
32use minarrow::{DatetimeAVT, DatetimeArray};
33use num_traits::{Float, Signed};
34#[cfg(not(feature = "fast_hash"))]
35use std::collections::HashMap;
36
37use crate::errors::KernelError;
38use crate::kernels::logical::not_bool;
39
40// Helper
41
42#[inline(always)]
43fn prealloc_vec<T: Copy>(len: usize) -> Vec64<T> {
44    let mut v = Vec64::<T>::with_capacity(len);
45    // SAFETY: we will write every slot beforee reading.
46    unsafe { v.set_len(len) };
47    v
48}
49
50// SIMD helpers
51
52#[cfg(feature = "simd")]
53mod simd_impl {
54    use core::simd::{LaneCount, Simd, SimdElement, SupportedLaneCount};
55
56    use num_traits::Zero;
57
58    /// `out = -a`  (works even if `Simd` lacks a direct `Neg`)
59    #[inline(always)]
60    pub fn negate_dense<T, const LANES: usize>(a: &[T], out: &mut [T])
61    where
62        T: SimdElement + core::ops::Neg<Output = T> + Copy + Zero,
63        LaneCount<LANES>: SupportedLaneCount,
64        Simd<T, LANES>: core::ops::Sub<Output = Simd<T, LANES>>,
65    {
66        let mut i = 0;
67        while i + LANES <= a.len() {
68            let v = Simd::<T, LANES>::from_slice(&a[i..i + LANES]);
69            let res = Simd::<T, LANES>::splat(T::zero()) - v;
70            res.copy_to_slice(&mut out[i..i + LANES]);
71            i += LANES;
72        }
73        // Tail often caused by `n % LANES != 0`; uses scalar fallback.
74        for j in i..a.len() {
75            out[j] = -a[j];
76        }
77    }
78}
79
80// Integer negate
81
82/// Generates integer negation functions with SIMD optimisation.
83macro_rules! impl_unary_neg_int {
84    ($fn_name:ident, $ty:ty, $lanes:expr) => {
85        /// Negates all elements in an integer array.
86        /// 
87        /// Applies the unary negation operator to each element in the array,
88        /// using SIMD operations when available for optimal performance.
89        /// 
90        /// # Arguments
91        /// 
92        /// * `window` - Integer array view tuple (array, offset, length)
93        /// 
94        /// # Returns
95        /// 
96        /// New integer array containing negated values
97        #[inline(always)]
98        pub fn $fn_name(window: IntegerAVT<$ty>) -> IntegerArray<$ty> {
99            let (arr, offset, len) = window;
100            let src = &arr.data[offset..offset + len];
101            let mut data = prealloc_vec::<$ty>(len);
102
103            #[cfg(feature = "simd")]
104            simd_impl::negate_dense::<$ty, $lanes>(src, &mut data);
105
106            #[cfg(not(feature = "simd"))]
107            for i in 0..len {
108                data[i] = -src[i];
109            }
110
111            IntegerArray {
112                data: data.into(),
113                null_mask: arr.null_mask.as_ref().map(|m| m.slice_clone(offset, len)),
114            }
115        }
116    };
117}
118
119#[cfg(feature = "datetime")]
120/// Generates datetime negation functions with SIMD optimisation.
121macro_rules! impl_unary_neg_datetime {
122    ($fn_name:ident, $ty:ty, $lanes:expr) => {
123        /// Negates all elements in a datetime array.
124        /// 
125        /// Applies the unary negation operator to each element in the datetime array,
126        /// using SIMD operations when available for optimal performance.
127        /// 
128        /// # Arguments
129        /// 
130        /// * `window` - Datetime array view tuple (array, offset, length)
131        /// 
132        /// # Returns
133        /// 
134        /// New datetime array containing negated values
135        #[inline(always)]
136        pub fn $fn_name(window: DatetimeAVT<$ty>) -> DatetimeArray<$ty> {
137            let (arr, offset, len) = window;
138            let src = &arr.data[offset..offset + len];
139            let mut data = prealloc_vec::<$ty>(len);
140
141            #[cfg(feature = "simd")]
142            simd_impl::negate_dense::<$ty, $lanes>(src, &mut data);
143
144            #[cfg(not(feature = "simd"))]
145            for i in 0..len {
146                data[i] = -src[i];
147            }
148
149            DatetimeArray {
150                data: data.into(),
151                null_mask: arr.null_mask.as_ref().map(|m| m.slice_clone(offset, len)),
152                time_unit: arr.time_unit.clone(),
153            }
154        }
155    };
156}
157
158#[cfg(feature = "datetime")]
159/// Generic datetime negation dispatcher.
160/// 
161/// Dispatches to the appropriate type-specific datetime negation function
162/// based on the concrete integer type at runtime.
163/// 
164/// # Type Parameters
165/// 
166/// * `T` - Signed integer type for datetime values
167/// 
168/// # Arguments
169/// 
170/// * `window` - Datetime array view tuple (array, offset, length)
171/// 
172/// # Returns
173/// 
174/// New datetime array containing negated values
175#[inline(always)]
176pub fn unary_negate_int_datetime<T>(window: DatetimeAVT<T>) -> DatetimeArray<T>
177where
178    T: Signed + Copy + 'static,
179{
180    if std::any::TypeId::of::<T>() == std::any::TypeId::of::<i32>() {
181        return unsafe { std::mem::transmute(unary_neg_datetime_i32(std::mem::transmute(window))) };
182    }
183    if std::any::TypeId::of::<T>() == std::any::TypeId::of::<i64>() {
184        return unsafe { std::mem::transmute(unary_neg_datetime_i64(std::mem::transmute(window))) };
185    }
186    unreachable!("unsupported datetime type for negation")
187}
188
189#[cfg(feature = "datetime")]
190impl_unary_neg_datetime!(unary_neg_datetime_i32, i32, W32);
191#[cfg(feature = "datetime")]
192impl_unary_neg_datetime!(unary_neg_datetime_i64, i64, W64);
193#[cfg(feature = "extended_numeric_types")]
194impl_unary_neg_int!(unary_neg_i8, i8, W8);
195#[cfg(feature = "extended_numeric_types")]
196impl_unary_neg_int!(unary_neg_i16, i16, W16);
197impl_unary_neg_int!(unary_neg_i32, i32, W32);
198impl_unary_neg_int!(unary_neg_i64, i64, W64);
199
200// Unified entry point
201
202/// Generic integer negation dispatcher.
203/// 
204/// Dispatches to the appropriate type-specific integer negation function
205/// based on the concrete integer type at runtime.
206/// 
207/// # Type Parameters
208/// 
209/// * `T` - Signed integer type
210/// 
211/// # Arguments
212/// 
213/// * `window` - Integer array view tuple (array, offset, length)
214/// 
215/// # Returns
216/// 
217/// New integer array containing negated values
218#[inline(always)]
219pub fn unary_negate_int<T>(window: IntegerAVT<T>) -> IntegerArray<T>
220where
221    T: Signed + Copy + 'static,
222{
223    macro_rules! dispatch {
224        ($t:ty, $f:ident) => {
225            if std::any::TypeId::of::<T>() == std::any::TypeId::of::<$t>() {
226                return unsafe { std::mem::transmute($f(std::mem::transmute(window))) };
227            }
228        };
229    }
230    #[cfg(feature = "extended_numeric_types")]
231    dispatch!(i8, unary_neg_i8);
232    #[cfg(feature = "extended_numeric_types")]
233    dispatch!(i16, unary_neg_i16);
234    dispatch!(i32, unary_neg_i32);
235    dispatch!(i64, unary_neg_i64);
236
237    unreachable!("unsupported integer type")
238}
239
240/// Negates u32 values and converts them to i32.
241/// 
242/// Applies unary negation to unsigned 32-bit integers and returns
243/// the result as signed 32-bit integers.
244/// 
245/// # Arguments
246/// 
247/// * `window` - u32 integer array view tuple (array, offset, length)
248/// 
249/// # Returns
250/// 
251/// New i32 integer array containing negated values
252pub fn unary_negate_u32_to_i32(window: IntegerAVT<u32>) -> IntegerArray<i32> {
253    let (arr, offset, len) = window;
254    let src = &arr.data[offset..offset + len];
255    let mut data = prealloc_vec::<i32>(len);
256
257    // There is very little benefit of using SIMD here, and the copy will slow things down.
258    // #[cfg(feature = "simd")]
259    // {
260    //     crate::utils::check_simd_alignment::<u32>(src);
261    //     use core::simd::Simd;
262    //     const LANES: usize = W32;
263    //     let mut i = 0;
264    //     while i + LANES <= len {
265    //         let v = Simd::<u32, LANES>::from_slice(&src[i..i + LANES]).cast::<i32>();
266    //         (Simd::<i32, LANES>::splat(0) - v).copy_to_slice(&mut data[i..i + LANES]);
267    //         i += LANES;
268    //     }
269    //     // Tail often caused by `n % LANES != 0`; uses scalar fallback.
270    //     for j in i..len {
271    //         data[j] = -(src[j] as i32);
272    //     }
273    // }
274    // #[cfg(not(feature = "simd"))]
275    for (dst, &v) in data.iter_mut().zip(src) {
276        *dst = -(v as i32);
277    }
278
279    IntegerArray {
280        data: data.into(),
281        null_mask: arr.null_mask.as_ref().map(|m| m.slice_clone(offset, len)),
282    }
283}
284
285/// Negates u64 values and converts them to i64.
286/// 
287/// Applies unary negation to unsigned 64-bit integers and returns
288/// the result as signed 64-bit integers.
289/// 
290/// # Arguments
291/// 
292/// * `window` - u64 integer array view tuple (array, offset, length)
293/// 
294/// # Returns
295/// 
296/// New i64 integer array containing negated values
297pub fn unary_negate_u64_to_i64(window: IntegerAVT<u64>) -> IntegerArray<i64> {
298    let (arr, offset, len) = window;
299    let src = &arr.data[offset..offset + len];
300    let mut data = prealloc_vec::<i64>(len);
301
302    #[cfg(feature = "simd")]
303    {
304        use core::simd::Simd;
305        const LANES: usize = W64;
306        let mut i = 0;
307        while i + LANES <= len {
308            let v = Simd::<u64, LANES>::from_slice(&src[i..i + LANES]).cast::<i64>();
309            (Simd::<i64, LANES>::splat(0) - v).copy_to_slice(&mut data[i..i + LANES]);
310            i += LANES;
311        }
312        for j in i..len {
313            data[j] = -(src[j] as i64);
314        }
315    }
316    #[cfg(not(feature = "simd"))]
317    for (dst, &v) in data.iter_mut().zip(src) {
318        *dst = -(v as i64);
319    }
320
321    IntegerArray {
322        data: data.into(),
323        null_mask: arr.null_mask.as_ref().map(|m| m.slice_clone(offset, len)),
324    }
325}
326
327// Float negate
328
329/// Generates floating-point negation functions with SIMD optimisation.
330macro_rules! impl_unary_neg_float {
331    ($fname:ident, $ty:ty, $lanes:expr) => {
332        /// Negates all elements in a floating-point array.
333        /// 
334        /// Applies the unary negation operator to each element in the array,
335        /// using SIMD operations when available for optimal performance.
336        /// 
337        /// # Arguments
338        /// 
339        /// * `window` - Float array view tuple (array, offset, length)
340        /// 
341        /// # Returns
342        /// 
343        /// New float array containing negated values
344        #[inline(always)]
345        pub fn $fname(window: FloatAVT<$ty>) -> FloatArray<$ty> {
346            let (arr, offset, len) = window;
347            let src = &arr.data[offset..offset + len];
348            let mut data = prealloc_vec::<$ty>(len);
349
350            #[cfg(feature = "simd")]
351            simd_impl::negate_dense::<$ty, $lanes>(src, &mut data);
352
353            #[cfg(not(feature = "simd"))]
354            for i in 0..len {
355                data[i] = -src[i];
356            }
357
358            FloatArray {
359                data: data.into(),
360                null_mask: arr.null_mask.as_ref().map(|m| m.slice_clone(offset, len)),
361            }
362        }
363    };
364}
365
366impl_unary_neg_float!(unary_neg_f32, f32, W32);
367impl_unary_neg_float!(unary_neg_f64, f64, W64);
368
369/// Generic floating-point negation dispatcher.
370/// 
371/// Dispatches to the appropriate type-specific negation function
372/// based on the concrete float type at runtime.
373/// 
374/// # Type Parameters
375/// 
376/// * `T` - Floating-point type implementing Float trait
377/// 
378/// # Arguments
379/// 
380/// * `window` - Float array view tuple (array, offset, length)
381/// 
382/// # Returns
383/// 
384/// New float array containing negated values
385#[inline(always)]
386pub fn unary_negate_float<T>(window: FloatAVT<T>) -> FloatArray<T>
387where
388    T: Float + Copy + 'static,
389{
390    if std::any::TypeId::of::<T>() == std::any::TypeId::of::<f32>() {
391        return unsafe { std::mem::transmute(unary_neg_f32(std::mem::transmute(window))) };
392    }
393    if std::any::TypeId::of::<T>() == std::any::TypeId::of::<f64>() {
394        return unsafe { std::mem::transmute(unary_neg_f64(std::mem::transmute(window))) };
395    }
396    unreachable!("unsupported float type")
397}
398
399/// Applies logical NOT to boolean values element-wise with SIMD acceleration.\n///\n/// Performs bitwise logical negation on boolean arrays using vectorised operations\n/// with configurable lane width for optimal performance.\n///\n/// # Parameters\n/// - `arr_window`: Boolean array view tuple `(BooleanArray, offset, length)`\n///\n/// # Const Generics\n/// - `LANES`: SIMD lane count for vectorised operations (typically W64)\n///\n/// # Returns\n/// `Result<BooleanArray<()>, KernelError>` with logically negated boolean values.\n///\n/// # Errors\n/// May return `KernelError` for invalid array configurations or memory issues.\n///\n/// # Performance\n/// Uses SIMD bitwise NOT operations with lane-parallel processing for bulk negation.
400pub fn unary_not_bool<const LANES: usize>(
401    arr_window: BooleanAVT<()>,
402) -> Result<BooleanArray<()>, KernelError>
403where
404    LaneCount<LANES>: SupportedLaneCount,
405{
406    not_bool::<LANES>(arr_window)
407}
408
409/// Reverses the character order within each string in a string array.
410/// 
411/// Creates a new string array where each string has its characters reversed,
412/// preserving UTF-8 encoding and null mask patterns.
413/// 
414/// # Type Parameters
415/// 
416/// * `T` - Integer type for string array offsets
417/// 
418/// # Arguments
419/// 
420/// * `arr` - String array view tuple (array, offset, length)
421/// 
422/// # Returns
423/// 
424/// New string array with character-reversed strings
425pub fn unary_reverse_str<T: Integer>(arr: StringAVT<T>) -> StringArray<T> {
426    let (array, offset, len) = arr;
427    let offsets = &array.offsets;
428    let data_buf = &array.data;
429    let mask = array.null_mask.as_ref();
430
431    // Estimate output buffer size: sum of windowed input string lengths
432    let total_bytes = if len == 0 {
433        0
434    } else {
435        let start = offsets[offset].to_usize();
436        let end = offsets[offset + len].to_usize();
437        end - start
438    };
439
440    // Prepare output buffers
441    let mut out_offsets = Vec64::<T>::with_capacity(len + 1);
442    let mut out_data = Vec64::<u8>::with_capacity(total_bytes);
443    unsafe {
444        out_offsets.set_len(len + 1);
445    }
446    out_offsets[0] = T::zero();
447
448    for i in 0..len {
449        // Preserve payload (set empty string), do not write reversed string.
450        if mask.map_or(true, |m| m.get(offset + i)) {
451            let start = offsets[offset + i].to_usize();
452            let end = offsets[offset + i + 1].to_usize();
453            let s = unsafe { std::str::from_utf8_unchecked(&data_buf[start..end]) };
454            for ch in s.chars().rev() {
455                let mut buf = [0u8; 4];
456                let encoded = ch.encode_utf8(&mut buf);
457                out_data.extend_from_slice(encoded.as_bytes());
458            }
459        }
460        // For nulls-  do not append any bytes.
461        out_offsets[i + 1] = T::from_usize(out_data.len());
462    }
463
464    let out_null_mask = mask.map(|m| m.slice_clone(offset, len));
465
466    StringArray {
467        offsets: out_offsets.into(),
468        data: out_data.into(),
469        null_mask: out_null_mask,
470    }
471}
472
473/// Reverses the character order within each string in a categorical array dictionary.
474/// 
475/// Creates a new categorical array where the dictionary strings have their 
476/// characters reversed, while preserving the codes and null mask patterns.
477/// 
478/// # Type Parameters
479/// 
480/// * `T` - Integer type implementing Hash for categorical codes
481/// 
482/// # Arguments
483/// 
484/// * `arr` - Categorical array view tuple (array, offset, length)
485/// 
486/// # Returns
487/// 
488/// New categorical array with character-reversed dictionary strings
489pub fn unary_reverse_dict<T: Integer + Hash>(arr: CategoricalAVT<T>) -> CategoricalArray<T> {
490    let (array, offset, len) = arr;
491    let mask = array.null_mask.as_ref();
492
493    // Window the data codes
494    let windowed_codes = array.data[offset..offset + len].to_vec();
495
496    // Build the set of codes actually used in this window, remap to new indices.
497    #[cfg(feature = "fast_hash")]
498    let mut remap: AHashMap<T, T> = AHashMap::new();
499    #[cfg(not(feature = "fast_hash"))]
500    let mut remap: HashMap<T, T> = HashMap::new();
501    let mut new_uniques = Vec64::<String>::new();
502    let mut new_codes = Vec64::<T>::with_capacity(len);
503
504    for &code in &windowed_codes {
505        if !remap.contains_key(&code) {
506            let reversed = array.unique_values[code.to_usize()]
507                .chars()
508                .rev()
509                .collect::<String>();
510            remap.insert(code, T::from_usize(new_uniques.len()));
511            new_uniques.push(reversed);
512        }
513        new_codes.push(remap[&code]);
514    }
515
516    let out_null_mask = mask.map(|m| m.slice_clone(offset, len));
517
518    CategoricalArray {
519        data: new_codes.into(),
520        unique_values: new_uniques,
521        null_mask: out_null_mask,
522    }
523}
524
525#[cfg(test)]
526mod tests {
527    use minarrow::structs::variants::categorical::CategoricalArray;
528    use minarrow::structs::variants::float::FloatArray;
529    use minarrow::structs::variants::integer::IntegerArray;
530    use minarrow::structs::variants::string::StringArray;
531    use minarrow::{Bitmask, BooleanArray, MaskedArray};
532
533    use super::*;
534
535    // Helpers
536
537    fn bm(bits: &[bool]) -> Bitmask {
538        let mut m = Bitmask::new_set_all(bits.len(), false);
539        for (i, &b) in bits.iter().enumerate() {
540            m.set(i, b);
541        }
542        m
543    }
544
545    fn expect_int<T: PartialEq + std::fmt::Debug>(
546        arr: &IntegerArray<T>,
547        values: &[T],
548        valid: &[bool],
549    ) {
550        assert_eq!(arr.data.as_slice(), values);
551        let mask = arr.null_mask.as_ref().expect("mask missing");
552        for (i, &v) in valid.iter().enumerate() {
553            assert_eq!(mask.get(i), v, "mask bit {}", i);
554        }
555    }
556
557    fn expect_float<T: num_traits::Float + std::fmt::Debug>(
558        arr: &FloatArray<T>,
559        values: &[T],
560        valid: &[bool],
561        eps: T,
562    ) {
563        assert_eq!(arr.data.len(), values.len());
564        for (a, b) in arr.data.iter().zip(values.iter()) {
565            assert!((*a - *b).abs() <= eps, "value mismatch {:?} vs {:?}", a, b);
566        }
567        let mask = arr.null_mask.as_ref().expect("mask missing");
568        for (i, &v) in valid.iter().enumerate() {
569            assert_eq!(mask.get(i), v, "mask bit {}", i);
570        }
571    }
572
573    // Integer Negation
574
575    #[cfg(feature = "extended_numeric_types")]
576    #[test]
577    fn neg_i8_dense() {
578        let arr = IntegerArray::<i8>::from_slice(&[1, -2, 127]);
579        let out = unary_neg_i8((&arr, 0, arr.len()));
580        assert_eq!(out.data.as_slice(), &[-1, 2, -127]);
581        assert!(out.null_mask.is_none());
582    }
583
584    #[cfg(feature = "extended_numeric_types")]
585    #[test]
586    fn neg_i16_masked() {
587        let mut arr = IntegerArray::<i16>::from_slice(&[-4, 12, 8, 0]);
588        arr.null_mask = Some(bm(&[true, false, true, true]));
589        let out = unary_neg_i16((&arr, 0, arr.len()));
590        expect_int(&out, &[4, -12, -8, 0], &[true, false, true, true]);
591    }
592
593    #[test]
594    fn neg_i32_empty() {
595        let arr = IntegerArray::<i32>::from_slice(&[]);
596        let out = unary_neg_i32((&arr, 0, arr.len()));
597        assert_eq!(out.data.len(), 0);
598    }
599
600    #[test]
601    fn neg_i64_all_nulls() {
602        let mut arr = IntegerArray::<i64>::from_slice(&[5, 10]);
603        arr.null_mask = Some(bm(&[false, false]));
604        let out = unary_neg_i64((&arr, 0, arr.len()));
605        expect_int(&out, &[-5, -10], &[false, false]);
606    }
607
608    #[cfg(feature = "extended_numeric_types")]
609    #[test]
610    fn neg_dispatch_i16() {
611        let mut arr = IntegerArray::<i16>::from_slice(&[-2, 4]);
612        arr.null_mask = Some(bm(&[true, true]));
613        let out = unary_negate_int((&arr, 0, arr.len()));
614        expect_int(&out, &[2, -4], &[true, true]);
615    }
616
617    // Unsigned to Signed Negation
618
619    #[test]
620    fn neg_u32_to_i32() {
621        let mut arr = IntegerArray::<u32>::from_slice(&[1, 2, 100]);
622        arr.null_mask = Some(bm(&[true, false, true]));
623        let out = unary_negate_u32_to_i32((&arr, 0, arr.len()));
624        expect_int(&out, &[-1, -2, -100], &[true, false, true]);
625    }
626
627    #[test]
628    fn neg_u64_to_i64() {
629        let arr = IntegerArray::<u64>::from_slice(&[3, 4, 0]);
630        let out = unary_negate_u64_to_i64((&arr, 0, arr.len()));
631        assert_eq!(out.data.as_slice(), &[-3, -4, 0]);
632    }
633
634    // Float Negation
635
636    #[test]
637    fn neg_f32_dense() {
638        let arr = FloatArray::<f32>::from_slice(&[0.5, -1.5, 2.0]);
639        let out = unary_neg_f32((&arr, 0, arr.len()));
640        assert_eq!(out.data.as_slice(), &[-0.5, 1.5, -2.0]);
641        assert!(out.null_mask.is_none());
642    }
643
644    #[test]
645    fn neg_f64_masked() {
646        let mut arr = FloatArray::<f64>::from_slice(&[1.1, -2.2, 3.3]);
647        arr.null_mask = Some(bm(&[true, false, true]));
648        let out = unary_neg_f64((&arr, 0, arr.len()));
649        expect_float(&out, &[-1.1, 2.2, -3.3], &[true, false, true], 1e-12);
650    }
651
652    #[test]
653    fn neg_dispatch_f64() {
654        let arr = FloatArray::<f64>::from_slice(&[2.2, -4.4]);
655        let out = unary_negate_float((&arr, 0, arr.len()));
656        assert_eq!(out.data.as_slice(), &[-2.2, 4.4]);
657    }
658
659    // Boolean NOT
660
661    #[test]
662    fn not_bool_basic() {
663        let arr = BooleanArray::from_slice(&[true, false, true, false]);
664        let out = unary_not_bool::<W64>((&arr, 0, arr.len())).unwrap();
665        assert_eq!(out.data.as_slice(), &[0b00001010]);
666        assert!(out.null_mask.is_none());
667    }
668
669    #[test]
670    fn not_bool_masked() {
671        let mut arr = BooleanArray::from_slice(&[false, false, true, true]);
672        arr.null_mask = Some(bm(&[true, false, true, true]));
673        let out = unary_not_bool::<W64>((&arr, 0, arr.len())).unwrap();
674        assert_eq!(out.data.as_slice(), &[0b00001100]);
675        assert_eq!(out.null_mask, arr.null_mask);
676    }
677
678    // String Reverse
679
680    #[test]
681    fn reverse_str_basic() {
682        let arr = StringArray::<u32>::from_slice(&["ab", "xyz", ""]);
683        let out = unary_reverse_str((&arr, 0, arr.len()));
684        assert_eq!(out.get(0), Some("ba"));
685        assert_eq!(out.get(1), Some("zyx"));
686        assert_eq!(out.get(2), Some(""));
687    }
688
689    #[test]
690    fn reverse_str_basic_chunk() {
691        let arr = StringArray::<u32>::from_slice(&["xxx", "ab", "xyz", ""]);
692        let out = unary_reverse_str((&arr, 1, 3)); // "ab", "xyz", ""
693        assert_eq!(out.get(0), Some("ba"));
694        assert_eq!(out.get(1), Some("zyx"));
695        assert_eq!(out.get(2), Some(""));
696    }
697
698    #[test]
699    fn reverse_str_with_nulls() {
700        let mut arr = StringArray::<u32>::from_slice(&["apple", "banana", "carrot"]);
701        arr.null_mask = Some(bm(&[true, false, true]));
702        let out = unary_reverse_str((&arr, 0, arr.len()));
703        assert_eq!(out.get(0), Some("elppa"));
704        assert_eq!(out.get(1), None);
705        assert_eq!(out.get(2), Some("torrac"));
706        assert_eq!(out.null_mask, arr.null_mask);
707    }
708
709    #[test]
710    fn reverse_str_with_nulls_chunk() {
711        let mut arr = StringArray::<u32>::from_slice(&["zero", "apple", "banana", "carrot"]);
712        arr.null_mask = Some(bm(&[true, true, false, true]));
713        let out = unary_reverse_str((&arr, 1, 3)); // "apple", "banana", "carrot"
714        assert_eq!(out.get(0), Some("elppa"));
715        assert_eq!(out.get(1), None);
716        assert_eq!(out.get(2), Some("torrac"));
717        assert_eq!(
718            out.null_mask.as_ref().unwrap().as_slice(),
719            bm(&[true, false, true]).as_slice()
720        );
721    }
722
723    // Categorical Reverse
724
725    #[test]
726    fn reverse_dict_basic() {
727        let arr = CategoricalArray::<u32>::from_values(["cat", "dog", "bird", "cat"]);
728        let out = unary_reverse_dict((&arr, 0, arr.data.len()));
729        let uniq: Vec<String> = out.unique_values.iter().map(|s| s.clone()).collect();
730        assert!(uniq.contains(&"tac".to_string()));
731        assert!(uniq.contains(&"god".to_string()));
732        assert!(uniq.contains(&"drib".to_string()));
733        assert_eq!(out.data, arr.data);
734    }
735
736    #[test]
737    fn reverse_dict_basic_chunk() {
738        let arr = CategoricalArray::<u32>::from_values(["z", "cat", "dog", "bird"]);
739        let out = unary_reverse_dict((&arr, 1, 3));
740        let uniq: Vec<String> = out.unique_values.iter().map(|s| s.clone()).collect();
741        assert!(uniq.contains(&"tac".to_string()));
742        assert!(uniq.contains(&"god".to_string()));
743        assert!(uniq.contains(&"drib".to_string()));
744        assert_eq!(&out.data[..], &[0, 1, 2]);
745    }
746
747    #[test]
748    fn test_unary_reverse_str_empty_and_all_nulls() {
749        // empty array
750        let arr0 = StringArray::<u32>::from_slice(&[]);
751        let out0 = unary_reverse_str((&arr0, 0, arr0.len()));
752        assert_eq!(out0.len(), 0);
753
754        // all-null array
755        let mut arr1 = StringArray::<u32>::from_slice(&["a", "b"]);
756        arr1.null_mask = Some(bm(&[false, false]));
757        let out1 = unary_reverse_str((&arr1, 0, arr1.len()));
758        // get() should return None for each, and mask preserved
759        assert_eq!(out1.get(0), None);
760        assert_eq!(out1.get(1), None);
761        assert_eq!(out1.null_mask, arr1.null_mask);
762    }
763
764    #[test]
765    fn test_unary_reverse_str_empty_and_all_nulls_chunk() {
766        // chunk of all-null
767        let mut arr = StringArray::<u32>::from_slice(&["x", "a", "b", "y"]);
768        arr.null_mask = Some(bm(&[true, false, false, true]));
769        let out = unary_reverse_str((&arr, 1, 2)); // "a", "b"
770        assert_eq!(out.get(0), None);
771        assert_eq!(out.get(1), None);
772        assert_eq!(out.null_mask, Some(bm(&[false, false])));
773    }
774}