Skip to main content

simd_kernels/kernels/
string.rs

1// Copyright 2025 Peter Garfield Bower
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! # **String Operations Kernels Module** - *High-Performance String Processing and Text Analysis*
16//!
17//! String processing kernels for text manipulation, pattern matching,
18//! and string analysis operations with UTF-8 awareness and null-safe semantics. Essential infrastructure
19//! for text analytics, data cleansing, and string-heavy analytical workloads.
20//!
21//! ## Core Operations
22//! - **String transformations**: Case conversion, trimming, padding, and substring operations
23//! - **Pattern matching**: Regular expression support with compiled pattern caching  
24//! - **String comparison**: Lexicographic ordering with UTF-8 aware collation
25//! - **Text analysis**: Length calculation, character counting, and encoding detection
26//! - **String aggregation**: Concatenation with configurable delimiters and null handling
27//! - **Search operations**: Contains, starts with, ends with predicates with optimised implementations
28
29#[cfg(feature = "fast_hash")]
30use ahash::{AHashMap, AHashSet};
31#[cfg(not(feature = "fast_hash"))]
32use std::collections::{HashMap, HashSet};
33
34use crate::{
35    Bitmask, BooleanArray, CategoricalArray, Integer, IntegerArray, MaskedArray, StringArray,
36    Vec64,
37    aliases::{CategoricalAVT, StringAVT},
38};
39#[cfg(feature = "views")]
40use crate::TextArrayV;
41#[cfg(feature = "regex")]
42use regex::Regex;
43
44use crate::enums::error::KernelError;
45use crate::utils::confirm_mask_capacity;
46use std::marker::PhantomData;
47
48/// Side for string padding operations.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum PadSide {
51    Left,
52    Right,
53    Both,
54}
55
56
57// Concatenation
58
59/// Concatenates corresponding string pairs from two string arrays element-wise.
60///
61/// Performs element-wise concatenation of strings from two `StringArray` sources,
62/// producing a new string array where each result string is the concatenation of
63/// the corresponding left and right input strings.
64///
65/// # Parameters
66/// - `lhs`: Left-hand string array view tuple `(StringArray, offset, length)`
67/// - `rhs`: Right-hand string array view tuple `(StringArray, offset, length)`
68///
69/// # Returns
70/// A new `StringArray<T>` containing concatenated strings with proper null handling.
71///
72/// # Null Handling
73/// - If either input string is null, the result is null
74/// - Output null mask reflects the union of input null positions
75///
76/// # Performance
77/// - Pre-computes total memory requirements to minimise allocations
78/// - Uses unsafe unchecked access for validated indices
79/// - Optimised for bulk string concatenation operations
80pub fn concat_str_str<T: Integer>(lhs: StringAVT<T>, rhs: StringAVT<T>) -> StringArray<T> {
81    let (larr, loff, llen) = lhs;
82    let (rarr, roff, rlen) = rhs;
83    let len = llen.min(rlen);
84
85    let lmask = larr.null_mask.as_ref();
86    let rmask = rarr.null_mask.as_ref();
87    let mut out_mask = Bitmask::new_set_all(len, false);
88    let _ = confirm_mask_capacity(larr.len(), lmask);
89    let _ = confirm_mask_capacity(rarr.len(), rmask);
90
91    // Compute total byte size required
92    let mut total_bytes = 0;
93    for i in 0..len {
94        let valid = lmask.map_or(true, |m| unsafe { m.get_unchecked(loff + i) })
95            && rmask.map_or(true, |m| unsafe { m.get_unchecked(roff + i) });
96        if valid {
97            let l = unsafe { larr.get_str_unchecked(loff + i) };
98            let r = unsafe { rarr.get_str_unchecked(roff + i) };
99            total_bytes += l.len() + r.len();
100        }
101    }
102
103    // Allocate offsets and data buffers
104    let mut offsets = Vec64::<T>::with_capacity(len + 1);
105    unsafe {
106        offsets.set_len(len + 1);
107    }
108    let mut values = Vec64::<u8>::with_capacity(total_bytes);
109
110    // Fill values and offsets
111    offsets[0] = T::zero();
112    let mut cur = 0;
113
114    for i in 0..len {
115        let valid = lmask.map_or(true, |m| unsafe { m.get_unchecked(loff + i) })
116            && rmask.map_or(true, |m| unsafe { m.get_unchecked(roff + i) });
117
118        if valid {
119            let l = unsafe { larr.get_str_unchecked(loff + i).as_bytes() };
120            let r = unsafe { rarr.get_str_unchecked(roff + i).as_bytes() };
121
122            values.extend_from_slice(l);
123            values.extend_from_slice(r);
124            cur += l.len() + r.len();
125
126            unsafe {
127                out_mask.set_unchecked(i, true);
128            }
129        } else {
130            unsafe {
131                out_mask.set_unchecked(i, false);
132            }
133        }
134
135        offsets[i + 1] = T::from_usize(cur);
136    }
137
138    StringArray {
139        offsets: offsets.into(),
140        data: values.into(),
141        null_mask: Some(out_mask),
142    }
143}
144
145/// Concatenates corresponding string pairs from two categorical arrays element-wise.
146///
147/// Performs element-wise concatenation by looking up dictionary values from both
148/// categorical arrays and concatenating the resolved strings. Creates a new string
149/// array with the concatenated results.
150///
151/// # Parameters
152/// - `lhs`: Left-hand categorical array view tuple `(CategoricalArray, offset, length)`
153/// - `rhs`: Right-hand categorical array view tuple `(CategoricalArray, offset, length)`
154///
155/// # Returns
156/// A new `StringArray<T>` containing concatenated dictionary strings.
157///
158/// # Null Handling
159/// - If either categorical value is null, the result is null
160/// - Output null mask reflects the union of input null positions
161///
162/// # Implementation
163/// - Dictionary lookups resolve categorical codes to actual strings
164/// - Memory allocation optimised based on total concatenated length
165pub fn concat_dict_dict<T: Integer>(
166    lhs: CategoricalAVT<T>,
167    rhs: CategoricalAVT<T>,
168) -> Result<CategoricalArray<T>, KernelError> {
169    let (larr, loff, llen) = lhs;
170    let (rarr, roff, rlen) = rhs;
171    let len = llen.min(rlen);
172
173    let lmask = larr.null_mask.as_ref();
174    let rmask = rarr.null_mask.as_ref();
175    let mut out_mask = Bitmask::new_set_all(len, false);
176    let _ = confirm_mask_capacity(larr.data.len(), lmask)?;
177    let _ = confirm_mask_capacity(rarr.data.len(), rmask)?;
178
179    // Use max possible unique count for preallocation. Worst case is all unique.
180    let mut data = Vec64::<T>::with_capacity(len);
181    unsafe {
182        data.set_len(len);
183    }
184
185    let mut unique_values = Vec64::<String>::with_capacity(len);
186    #[cfg(feature = "fast_hash")]
187    let mut seen: AHashMap<String, T> = AHashMap::with_capacity(len);
188    #[cfg(not(feature = "fast_hash"))]
189    let mut seen: HashMap<String, T> = HashMap::with_capacity(len);
190    let mut unique_idx = 0;
191
192    for i in 0..len {
193        let valid = lmask.map_or(true, |m| unsafe { m.get_unchecked(loff + i) })
194            && rmask.map_or(true, |m| unsafe { m.get_unchecked(roff + i) });
195
196        if valid {
197            let l = unsafe { larr.get_str_unchecked(loff + i) };
198            let r = unsafe { rarr.get_str_unchecked(roff + i) };
199            let cat = format!("{l}{r}");
200
201            let idx = match seen.get(&cat) {
202                Some(ix) => *ix,
203                None => {
204                    let ix = T::from_usize(unique_idx);
205                    unique_values.push(cat.clone());
206                    seen.insert(cat, ix);
207                    unique_idx += 1;
208                    ix
209                }
210            };
211
212            unsafe {
213                *data.get_unchecked_mut(i) = idx;
214                out_mask.set_unchecked(i, true);
215            }
216        } else {
217            unsafe {
218                *data.get_unchecked_mut(i) = T::zero();
219                out_mask.set_unchecked(i, false);
220            }
221        }
222    }
223
224    unsafe {
225        unique_values.set_len(unique_idx);
226    }
227
228    Ok(CategoricalArray {
229        data: data.into(),
230        unique_values,
231        null_mask: Some(out_mask),
232    })
233}
234
235/// Concatenates strings from a string array with dictionary values from a categorical array.
236///
237/// Performs element-wise concatenation where left operands come from a string array
238/// and right operands are resolved from a categorical array's dictionary.
239///
240/// # Parameters
241/// - `lhs`: String array view tuple `(StringArray, offset, length)`
242/// - `rhs`: Categorical array view tuple `(CategoricalArray, offset, length)`
243///
244/// # Type Parameters
245/// - `T`: Integer type for string array offsets
246/// - `U`: Integer type for categorical array indices
247///
248/// # Returns
249/// A new `StringArray<T>` containing concatenated string-dictionary pairs.
250///
251/// # Null Handling
252/// Results are null if either input value is null at the corresponding position.
253pub fn concat_str_dict<T: Integer, U: Integer>(
254    lhs: StringAVT<T>,
255    rhs: CategoricalAVT<U>,
256) -> Result<StringArray<T>, KernelError> {
257    let (larr, loff, llen) = lhs;
258    let (rarr, roff, rlen) = rhs;
259    let len = llen.min(rlen);
260
261    let lmask = larr.null_mask.as_ref();
262    let rmask = rarr.null_mask.as_ref();
263    let mut out_mask = Bitmask::new_set_all(len, false);
264    let _ = confirm_mask_capacity(larr.len(), lmask)?;
265    let _ = confirm_mask_capacity(rarr.data.len(), rmask)?;
266
267    // Compute total byte size required
268    let mut total_bytes = 0;
269    for i in 0..len {
270        let valid = lmask.map_or(true, |m| unsafe { m.get_unchecked(loff + i) })
271            && rmask.map_or(true, |m| unsafe { m.get_unchecked(roff + i) });
272        if valid {
273            let a = unsafe { larr.get_str_unchecked(loff + i) };
274            let b = unsafe { rarr.get_str_unchecked(roff + i) };
275            total_bytes += a.len() + b.len();
276        }
277    }
278
279    // Preallocate offsets and values
280    let mut offsets = Vec64::<T>::with_capacity(len + 1);
281    unsafe {
282        offsets.set_len(len + 1);
283    }
284    let mut values = Vec64::<u8>::with_capacity(total_bytes);
285
286    // Fill values and offsets
287    offsets[0] = T::zero();
288    let mut cur = 0;
289
290    for i in 0..len {
291        let valid = lmask.map_or(true, |m| unsafe { m.get_unchecked(loff + i) })
292            && rmask.map_or(true, |m| unsafe { m.get_unchecked(roff + i) });
293
294        if valid {
295            let a = unsafe { larr.get_str_unchecked(loff + i).as_bytes() };
296            let b = unsafe { rarr.get_str_unchecked(roff + i).as_bytes() };
297
298            values.extend_from_slice(a);
299            values.extend_from_slice(b);
300            cur += a.len() + b.len();
301
302            unsafe {
303                out_mask.set_unchecked(i, true);
304            }
305        } else {
306            unsafe {
307                out_mask.set_unchecked(i, false);
308            }
309        }
310
311        offsets[i + 1] = T::from_usize(cur);
312    }
313
314    Ok(StringArray {
315        offsets: offsets.into(),
316        data: values.into(),
317        null_mask: Some(out_mask),
318    })
319}
320
321/// Concatenates dictionary values from a categorical array with strings from a string array.
322///
323/// Performs element-wise concatenation where left operands are resolved from a
324/// categorical array's dictionary and right operands come from a string array.
325///
326/// # Parameters
327/// - `lhs`: Categorical array view tuple `(CategoricalArray, offset, length)`
328/// - `rhs`: String array view tuple `(StringArray, offset, length)`
329///
330/// # Type Parameters
331/// - `T`: Integer type for string array offsets
332/// - `U`: Integer type for categorical array indices
333///
334/// # Returns
335/// A new `StringArray<T>` containing concatenated dictionary-string pairs.
336///
337/// # Null Handling
338/// Results are null if either input value is null at the corresponding position.
339pub fn concat_dict_str<T: Integer, U: Integer>(
340    lhs: CategoricalAVT<U>,
341    rhs: StringAVT<T>,
342) -> Result<StringArray<T>, KernelError> {
343    concat_str_dict(rhs, lhs)
344}
345
346macro_rules! binary_str_pred_loop {
347    ($len:expr, $lmask:expr, $rmask:expr, $out_mask:expr, $lhs:expr, $rhs:expr, $method:ident) => {{
348        let mut data = Bitmask::new_set_all($len, false);
349        // ensure masks cover offset + len
350        let lhs_off = $lhs.1;
351        let rhs_off = $rhs.1;
352        let _ = confirm_mask_capacity(lhs_off + $len, $lmask)?;
353        let _ = confirm_mask_capacity(rhs_off + $len, $rmask)?;
354        for i in 0..$len {
355            let li = lhs_off + i;
356            let ri = rhs_off + i;
357            let valid = $lmask.map_or(true, |m| unsafe { m.get_unchecked(li) })
358                && $rmask.map_or(true, |m| unsafe { m.get_unchecked(ri) });
359            let result = valid && {
360                let s = unsafe { $lhs.0.get_str_unchecked(li) };
361                let pat = unsafe { $rhs.0.get_str_unchecked(ri) };
362                !pat.is_empty() && s.$method(pat)
363            };
364            unsafe {
365                data.set_unchecked(i, result);
366                $out_mask.set_unchecked(i, valid);
367            }
368        }
369        data
370    }};
371}
372
373// STRING PREDICATES - contains/starts_with/ends_with
374
375/// Generates string predicate functions that compare string arrays.
376macro_rules! str_predicate {
377    ($fn_name:ident, $method:ident) => {
378        /// Performs string predicate operations between two string arrays.
379        ///
380        /// Applies the specified string method (contains, starts_with, ends_with)
381        /// to compare corresponding elements of two string arrays.
382        ///
383        /// # Type Parameters
384        ///
385        /// * `T` - Integer type for left string array offsets
386        /// * `U` - Integer type for right string array offsets
387        ///
388        /// # Arguments
389        ///
390        /// * `lhs` - Left string array (data, offset, length)
391        /// * `rhs` - Right string array (data, offset, length)
392        ///
393        /// # Returns
394        ///
395        /// Boolean array containing comparison results
396        pub fn $fn_name<T: Integer, U: Integer>(
397            lhs: StringAVT<T>,
398            rhs: StringAVT<U>,
399        ) -> BooleanArray<()> {
400            let (larr, loff, llen) = lhs;
401            let (rarr, roff, rlen) = rhs;
402            let len = llen.min(rlen);
403            // Grab raw pointers & slices once
404            let lmask = larr.null_mask.as_ref();
405            let rmask = rarr.null_mask.as_ref();
406            let mut out = Bitmask::new_set_all(len, false);
407
408            for i in 0..len {
409                unsafe {
410                    // Check null‐mask validity without bounds checks
411                    let lv = lmask.map_or(true, |m| m.get_unchecked(loff + i));
412                    let rv = rmask.map_or(true, |m| m.get_unchecked(roff + i));
413                    if !lv || !rv {
414                        // leave out[i]=false
415                        continue;
416                    }
417                    // Slice out the raw bytes
418                    let ls = larr.offsets[loff + i].to_usize();
419                    let le = larr.offsets[loff + i + 1].to_usize();
420                    let rs = rarr.offsets[roff + i].to_usize();
421                    let re = rarr.offsets[roff + i + 1].to_usize();
422                    let s = std::str::from_utf8_unchecked(&larr.data[ls..le]);
423                    let p = std::str::from_utf8_unchecked(&rarr.data[rs..re]);
424                    // Only non-empty pattern can match
425                    if !p.is_empty() && s.$method(p) {
426                        out.set_unchecked(i, true);
427                    }
428                }
429            }
430            // Tight bitmask with no nulls - nulls became 'false'
431            BooleanArray {
432                data: out.into(),
433                null_mask: None,
434                len,
435                _phantom: PhantomData,
436            }
437        }
438    };
439}
440
441/// Generates string-to-categorical predicate functions.
442macro_rules! str_cat_predicate {
443    ($fn_name:ident, $method:ident) => {
444        /// Performs string predicate operations between string and categorical arrays.
445        ///
446        /// Applies the specified string method (contains, starts_with, ends_with)
447        /// to compare string array elements with categorical array elements.
448        ///
449        /// # Type Parameters
450        ///
451        /// * `T` - Integer type for string array offsets
452        /// * `U` - Integer type for categorical array offsets
453        ///
454        /// # Arguments
455        ///
456        /// * `lhs` - String array (data, offset, length)
457        /// * `rhs` - Categorical array (data, offset, length)
458        ///
459        /// # Returns
460        ///
461        /// Result containing boolean array with comparison results, or error
462        pub fn $fn_name<T: Integer, U: Integer>(
463            lhs: StringAVT<T>,
464            rhs: CategoricalAVT<U>,
465        ) -> Result<BooleanArray<()>, KernelError> {
466            let (larr, loff, llen) = lhs;
467            let (rarr, roff, rlen) = rhs;
468            let len = llen.min(rlen);
469
470            let lmask = larr.null_mask.as_ref();
471            let rmask = rarr.null_mask.as_ref();
472            let mut out_mask = Bitmask::new_set_all(len, false);
473
474            let data = binary_str_pred_loop!(
475                len,
476                lmask,
477                rmask,
478                out_mask,
479                (larr, loff),
480                (rarr, roff),
481                $method
482            );
483
484            Ok(BooleanArray {
485                data: data.into(),
486                null_mask: Some(out_mask),
487                len,
488                _phantom: PhantomData,
489            })
490        }
491    };
492}
493
494/// Generates categorical-to-categorical predicate functions.
495macro_rules! cat_cat_predicate {
496    ($fn_name:ident, $method:ident) => {
497        /// Performs string predicate operations between two categorical arrays.
498        ///
499        /// Applies the specified string method (contains, starts_with, ends_with)
500        /// to compare corresponding elements of two categorical arrays.
501        ///
502        /// # Type Parameters
503        ///
504        /// * `T` - Integer type for categorical array offsets
505        ///
506        /// # Arguments
507        ///
508        /// * `lhs` - Left categorical array (data, offset, length)
509        /// * `rhs` - Right categorical array (data, offset, length)
510        ///
511        /// # Returns
512        ///
513        /// Result containing boolean array with comparison results, or error
514        pub fn $fn_name<T: Integer>(
515            lhs: CategoricalAVT<T>,
516            rhs: CategoricalAVT<T>,
517        ) -> Result<BooleanArray<()>, KernelError> {
518            let (larr, loff, llen) = lhs;
519            let (rarr, roff, rlen) = rhs;
520            let len = llen.min(rlen);
521
522            let lmask = larr.null_mask.as_ref();
523            let rmask = rarr.null_mask.as_ref();
524            let mut out_mask = Bitmask::new_set_all(len, false);
525
526            let data = binary_str_pred_loop!(
527                len,
528                lmask,
529                rmask,
530                out_mask,
531                (larr, loff),
532                (rarr, roff),
533                $method
534            );
535
536            Ok(BooleanArray {
537                data: data.into(),
538                null_mask: Some(out_mask),
539                len,
540                _phantom: PhantomData,
541            })
542        }
543    };
544}
545
546/// Generates categorical-to-string predicate functions.
547macro_rules! dict_str_predicate {
548    ($fn_name:ident, $method:ident) => {
549        /// Performs string predicate operations between categorical and string arrays.
550        ///
551        /// Applies the specified string method (contains, starts_with, ends_with)
552        /// to compare categorical array elements with string array elements.
553        ///
554        /// # Type Parameters
555        ///
556        /// * `T` - Integer type for categorical array offsets
557        /// * `U` - Integer type for string array offsets
558        ///
559        /// # Arguments
560        ///
561        /// * `lhs` - Categorical array (data, offset, length)
562        /// * `rhs` - String array (data, offset, length)
563        ///
564        /// # Returns
565        ///
566        /// Result containing boolean array with comparison results, or error
567        pub fn $fn_name<T: Integer, U: Integer>(
568            lhs: CategoricalAVT<T>,
569            rhs: StringAVT<U>,
570        ) -> Result<BooleanArray<()>, KernelError> {
571            let (larr, loff, llen) = lhs;
572            let (rarr, roff, rlen) = rhs;
573            let len = llen.min(rlen);
574
575            let lmask = larr.null_mask.as_ref();
576            let rmask = rarr.null_mask.as_ref();
577            let mut out_mask = Bitmask::new_set_all(len, false);
578            let _ = confirm_mask_capacity(larr.data.len(), lmask)?;
579            let _ = confirm_mask_capacity(rarr.len(), rmask)?;
580
581            let mut data = Bitmask::new_set_all(len, false);
582            for i in 0..len {
583                let valid = lmask.map_or(true, |m| unsafe { m.get_unchecked(loff + i) })
584                    && rmask.map_or(true, |m| unsafe { m.get_unchecked(roff + i) });
585                let match_i = valid && {
586                    let hay = unsafe { larr.get_str_unchecked(loff + i) };
587                    let needle = unsafe { rarr.get_str_unchecked(roff + i) };
588                    !needle.is_empty() && hay.$method(needle)
589                };
590                unsafe { data.set_unchecked(i, match_i) };
591                unsafe { out_mask.set_unchecked(i, valid) };
592            }
593
594            Ok(BooleanArray {
595                data: data.into(),
596                null_mask: Some(out_mask),
597                len,
598                _phantom: PhantomData,
599            })
600        }
601    };
602}
603
604str_predicate!(contains_str_str, contains);
605str_predicate!(starts_with_str_str, starts_with);
606str_predicate!(ends_with_str_str, ends_with);
607str_cat_predicate!(contains_str_dict, contains);
608cat_cat_predicate!(contains_dict_dict, contains);
609str_cat_predicate!(starts_with_str_dict, starts_with);
610cat_cat_predicate!(starts_with_dict_dict, starts_with);
611str_cat_predicate!(ends_with_str_dict, ends_with);
612cat_cat_predicate!(ends_with_dict_dict, ends_with);
613dict_str_predicate!(contains_dict_str, contains);
614dict_str_predicate!(starts_with_dict_str, starts_with);
615dict_str_predicate!(ends_with_dict_str, ends_with);
616
617// Regex match
618
619#[cfg(feature = "regex")]
620macro_rules! regex_match_loop {
621    ($len:expr, $lmask:expr, $rmask:expr, $out_mask:expr, $lhs_arr:expr, $lhs_off:expr, $rhs_arr:expr, $rhs_off:expr) => {{
622        let mut data = Bitmask::new_set_all($len, false);
623        let _ = confirm_mask_capacity($len + $lhs_off, $lmask)?;
624        let _ = confirm_mask_capacity($len + $rhs_off, $rmask)?;
625        for i in 0..$len {
626            let valid = $lmask.map_or(true, |m| unsafe { m.get_unchecked($lhs_off + i) })
627                && $rmask.map_or(true, |m| unsafe { m.get_unchecked($rhs_off + i) });
628            let matched = if valid {
629                let s = unsafe { $lhs_arr.get_str_unchecked($lhs_off + i) };
630                let pat = unsafe { $rhs_arr.get_str_unchecked($rhs_off + i) };
631                if pat.is_empty() {
632                    false
633                } else {
634                    match Regex::new(pat) {
635                        Ok(re) => re.is_match(s),
636                        Err(_) => {
637                            return Err(KernelError::InvalidArguments(
638                                "Invalid regex string".to_string(),
639                            ));
640                        }
641                    }
642                }
643            } else {
644                false
645            };
646            unsafe { data.set_unchecked(i, matched) };
647            unsafe { $out_mask.set_unchecked(i, valid) };
648        }
649        data
650    }};
651}
652
653/// Applies regular expression pattern matching between two string arrays.
654///
655/// Evaluates regex patterns from the right-hand string array against corresponding
656/// strings in the left-hand array, producing a boolean array indicating matches.
657///
658/// # Parameters
659/// - `lhs`: Source string array view tuple `(StringArray, offset, length)`
660/// - `rhs`: Pattern string array view tuple `(StringArray, offset, length)`
661///
662/// # Type Parameters
663/// - `T`: Integer type for left array offsets
664/// - `U`: Integer type for right array offsets
665///
666/// # Returns
667/// `Result<BooleanArray<()>, KernelError>` where true indicates pattern match.
668///
669/// # Errors
670/// Returns `KernelError` for invalid regular expression patterns.
671///
672/// # Feature Gate
673/// Requires the `regex` feature to be enabled.
674///
675/// # Performance
676/// - Regex compilation overhead amortised across bulk operations
677/// - Pattern caching opportunities for repeated patterns
678#[cfg(feature = "regex")]
679pub fn regex_str_str<'a, T: Integer, U: Integer>(
680    lhs: StringAVT<'a, T>,
681    rhs: StringAVT<'a, U>,
682) -> Result<BooleanArray<()>, KernelError> {
683    let (larr, loff, llen) = lhs;
684    let (rarr, roff, rlen) = rhs;
685    let len = llen.min(rlen);
686    let lmask = larr.null_mask.as_ref();
687    let rmask = rarr.null_mask.as_ref();
688    let mut out_mask = Bitmask::new_set_all(len, false);
689
690    let data = regex_match_loop!(len, lmask, rmask, out_mask, larr, loff, rarr, roff);
691    Ok(BooleanArray {
692        data: data.into(),
693        null_mask: Some(out_mask),
694        len,
695        _phantom: PhantomData,
696    })
697}
698
699/// Applies regular expression patterns to categorical array values against string patterns.
700///
701/// Evaluates regex patterns from the string array against dictionary-resolved strings
702/// from the categorical array, producing a boolean array indicating matches.
703///
704/// # Parameters
705/// - `lhs`: Categorical array view tuple `(CategoricalArray, offset, length)`
706/// - `rhs`: Pattern string array view tuple `(StringArray, offset, length)`
707///
708/// # Type Parameters
709/// - `U`: Integer type for categorical array indices
710/// - `T`: Integer type for pattern array offsets
711///
712/// # Returns
713/// `Result<BooleanArray<()>, KernelError>` where true indicates pattern match.
714///
715/// # Errors
716/// Returns `KernelError` for invalid regular expression patterns.
717///
718/// # Feature Gate
719/// Requires the `regex` feature to be enabled.
720#[cfg(feature = "regex")]
721pub fn regex_dict_str<'a, U: Integer, T: Integer>(
722    lhs: CategoricalAVT<'a, U>,
723    rhs: StringAVT<'a, T>,
724) -> Result<BooleanArray<()>, KernelError> {
725    let (larr, loff, llen) = lhs;
726    let (rarr, roff, rlen) = rhs;
727    let len = llen.min(rlen);
728    let lmask = larr.null_mask.as_ref();
729    let rmask = rarr.null_mask.as_ref();
730    let mut out_mask = Bitmask::new_set_all(len, false);
731
732    let data = regex_match_loop!(len, lmask, rmask, out_mask, larr, loff, rarr, roff);
733    Ok(BooleanArray {
734        data: data.into(),
735        null_mask: Some(out_mask),
736        len,
737        _phantom: PhantomData,
738    })
739}
740
741/// Applies regular expression patterns from categorical dictionary against string values.
742///
743/// Evaluates regex patterns resolved from the categorical array's dictionary against
744/// corresponding strings in the string array, producing a boolean array of matches.
745///
746/// # Parameters
747/// - `lhs`: Source string array view tuple `(StringArray, offset, length)`
748/// - `rhs`: Pattern categorical array view tuple `(CategoricalArray, offset, length)`
749///
750/// # Type Parameters
751/// - `T`: Integer type for string array offsets
752/// - `U`: Integer type for categorical array indices
753///
754/// # Returns
755/// `Result<BooleanArray<()>, KernelError>` where true indicates pattern match.
756///
757/// # Errors
758/// Returns `KernelError` for invalid regular expression patterns in dictionary.
759///
760/// # Feature Gate
761/// Requires the `regex` feature to be enabled.
762#[cfg(feature = "regex")]
763pub fn regex_str_dict<'a, T: Integer, U: Integer>(
764    lhs: StringAVT<'a, T>,
765    rhs: CategoricalAVT<'a, U>,
766) -> Result<BooleanArray<()>, KernelError> {
767    let (larr, loff, llen) = lhs;
768    let (rarr, roff, rlen) = rhs;
769    let len = llen.min(rlen);
770    let lmask = larr.null_mask.as_ref();
771    let rmask = rarr.null_mask.as_ref();
772    let mut out_mask = Bitmask::new_set_all(len, false);
773
774    let data = regex_match_loop!(len, lmask, rmask, out_mask, larr, loff, rarr, roff);
775    Ok(BooleanArray {
776        data: data.into(),
777        null_mask: Some(out_mask),
778        len,
779        _phantom: PhantomData,
780    })
781}
782
783/// Applies regular expression patterns between two categorical arrays via dictionary lookup.
784///
785/// Evaluates regex patterns by resolving both pattern and target strings from their
786/// respective categorical dictionaries, producing a boolean array indicating matches.
787///
788/// # Parameters
789/// - `lhs`: Source categorical array view tuple `(CategoricalArray, offset, length)`
790/// - `rhs`: Pattern categorical array view tuple `(CategoricalArray, offset, length)`
791///
792/// # Type Parameters
793/// - `T`: Integer type for categorical array indices
794///
795/// # Returns
796/// `Result<BooleanArray<()>, KernelError>` where true indicates pattern match.
797///
798/// # Errors
799/// Returns `KernelError` for invalid regular expression patterns in dictionaries.
800///
801/// # Feature Gate
802/// Requires the `regex` feature to be enabled.
803///
804/// # Performance
805/// - Dictionary lookups amortised across categorical operations
806/// - Pattern compilation cached for repeated dictionary patterns
807#[cfg(feature = "regex")]
808pub fn regex_dict_dict<'a, T: Integer>(
809    lhs: CategoricalAVT<'a, T>,
810    rhs: CategoricalAVT<'a, T>,
811) -> Result<BooleanArray<()>, KernelError> {
812    let (larr, loff, llen) = lhs;
813    let (rarr, roff, rlen) = rhs;
814    let len = llen.min(rlen);
815    let lmask = larr.null_mask.as_ref();
816    let rmask = rarr.null_mask.as_ref();
817    let mut out_mask = Bitmask::new_set_all(len, false);
818
819    let data = regex_match_loop!(len, lmask, rmask, out_mask, larr, loff, rarr, roff);
820    Ok(BooleanArray {
821        data: data.into(),
822        null_mask: Some(out_mask),
823        len,
824        _phantom: PhantomData,
825    })
826}
827
828/// Computes the character length of each string in a `StringArray<T>` slice,
829/// returning an `IntegerArray<T>` with the same length and null semantics.
830///
831/// This applies to a windowed slice (`offset`, `len`) of the input array.
832/// The output null mask mirrors the sliced portion of the input mask.
833///
834/// # Parameters
835/// - `input`: A `(StringArray<T>, offset, len)` tuple defining the slice to operate on.
836///
837/// # Returns
838/// An `IntegerArray<T>` of the same length, with each element representing the character count
839/// of the corresponding (non-null) string value.
840pub fn len_str<'a, T: Integer + Copy>(
841    input: StringAVT<'a, T>,
842) -> Result<IntegerArray<T>, KernelError> {
843    let (array, offset, len) = input;
844    debug_assert!(offset + len <= array.offsets.len() - 1);
845
846    let mask_opt = array.null_mask.as_ref().map(|orig| {
847        let mut m = Bitmask::new_set_all(len, true);
848        for i in 0..len {
849            unsafe {
850                m.set_unchecked(i, orig.get_unchecked(offset + i));
851            }
852        }
853        m
854    });
855
856    let mut data = Vec64::<T>::with_capacity(len);
857    unsafe { data.set_len(len) };
858    for i in 0..len {
859        let valid = mask_opt
860            .as_ref()
861            .map_or(true, |m| unsafe { m.get_unchecked(i) });
862        if valid {
863            let start = array.offsets[offset + i].to_usize();
864            let end = array.offsets[offset + i + 1].to_usize();
865            let s = unsafe { std::str::from_utf8_unchecked(&array.data[start..end]) };
866            data[i] = T::from(s.chars().count()).unwrap();
867        } else {
868            data[i] = T::zero();
869        }
870    }
871
872    Ok(IntegerArray {
873        data: data.into(),
874        null_mask: mask_opt,
875    })
876}
877
878/// Computes the character length of each string in a `CategoricalArray<T>` slice,
879/// returning an `IntegerArray<T>` with the same length and null semantics.
880///
881/// This applies to a windowed slice (`offset`, `len`) of the input categorical array,
882/// using dictionary lookup to resolve each string.
883///
884/// # Parameters
885/// - `input`: A `(CategoricalArray<T>, offset, len)` tuple defining the slice to operate on.
886///
887/// # Returns
888/// An `IntegerArray<T>` of the same length, with each element representing the character count
889/// of the corresponding (non-null) resolved string.
890pub fn len_dict<'a, T: Integer>(
891    input: CategoricalAVT<'a, T>,
892) -> Result<IntegerArray<T>, KernelError> {
893    let (array, offset, len) = input;
894    debug_assert!(offset + len <= array.data.len());
895
896    let mask_opt = array.null_mask.as_ref().map(|orig| {
897        let mut m = Bitmask::new_set_all(len, true);
898        for i in 0..len {
899            unsafe {
900                m.set_unchecked(i, orig.get_unchecked(offset + i));
901            }
902        }
903        m
904    });
905
906    let mut data = Vec64::<T>::with_capacity(len);
907    unsafe { data.set_len(len) };
908    for i in 0..len {
909        let valid = mask_opt
910            .as_ref()
911            .map_or(true, |m| unsafe { m.get_unchecked(i) });
912        data[i] = if valid {
913            T::from(
914                unsafe { array.get_str_unchecked(offset + i) }
915                    .chars()
916                    .count(),
917            )
918            .unwrap()
919        } else {
920            T::zero()
921        };
922    }
923
924    Ok(IntegerArray {
925        data: data.into(),
926        null_mask: mask_opt,
927    })
928}
929
930/// Finds the lexicographically minimum string in a string array window.
931///
932/// Scans through a windowed portion of a string array to determine the minimum
933/// string value according to lexicographic ordering, ignoring null values.
934///
935/// # Parameters
936/// - `window`: String array view tuple `(StringArray, offset, length)` defining the scan window
937///
938/// # Returns
939/// `Option<String>` containing the minimum string, or `None` if all values are null.
940#[inline]
941pub fn min_string_array<T: Integer>(window: StringAVT<T>) -> Option<String> {
942    let (arr, offset, len) = window;
943    let mut min_str: Option<&str> = None;
944    for i in offset..offset + len {
945        if arr
946            .null_mask
947            .as_ref()
948            .map_or(true, |b| unsafe { b.get_unchecked(i) })
949        {
950            let s = unsafe { arr.get_str_unchecked(i) };
951            if min_str.map_or(true, |min| s < min) {
952                min_str = Some(s);
953            }
954        }
955    }
956    min_str.map(str::to_owned)
957}
958
959/// Finds the lexicographically maximum string in a string array window.
960///
961/// Scans through a windowed portion of a string array to determine the maximum
962/// string value according to lexicographic ordering, ignoring null values.
963///
964/// # Parameters
965/// - `window`: String array view tuple `(StringArray, offset, length)` defining the scan window
966///
967/// # Returns
968/// `Option<String>` containing the maximum string, or `None` if all values are null.
969#[inline]
970pub fn max_string_array<T: Integer>(window: StringAVT<T>) -> Option<String> {
971    let (arr, offset, len) = window;
972    let mut max_str: Option<&str> = None;
973    for i in offset..offset + len {
974        if arr
975            .null_mask
976            .as_ref()
977            .map_or(true, |b| unsafe { b.get_unchecked(i) })
978        {
979            let s = unsafe { arr.get_str_unchecked(i) };
980            if max_str.map_or(true, |max| s > max) {
981                max_str = Some(s);
982            }
983        }
984    }
985    max_str.map(str::to_owned)
986}
987
988/// Finds the lexicographically minimum dictionary string in a categorical array window.
989///
990/// Scans through a windowed portion of a categorical array, resolves dictionary values,
991/// and determines the minimum string according to lexicographic ordering.
992///
993/// # Parameters
994/// - `window`: Categorical array view tuple `(CategoricalArray, offset, length)` defining the scan window
995///
996/// # Returns
997/// `Option<String>` containing the minimum dictionary string, or `None` if all values are null.
998#[inline]
999pub fn min_categorical_array<T: Integer>(window: CategoricalAVT<T>) -> Option<String> {
1000    let (arr, offset, len) = window;
1001    let mut min_code: Option<T> = None;
1002    for i in offset..offset + len {
1003        if arr
1004            .null_mask
1005            .as_ref()
1006            .map_or(true, |b| unsafe { b.get_unchecked(i) })
1007        {
1008            let code = arr.data[i];
1009            if min_code.map_or(true, |min| {
1010                let sc = &arr.unique_values[code.to_usize()];
1011                let sm = &arr.unique_values[min.to_usize()];
1012                sc < sm
1013            }) {
1014                min_code = Some(code);
1015            }
1016        }
1017    }
1018    min_code.map(|code| arr.unique_values[code.to_usize()].clone())
1019}
1020
1021/// Finds the lexicographically maximum dictionary string in a categorical array window.
1022///
1023/// Scans through a windowed portion of a categorical array, resolves dictionary values,
1024/// and determines the maximum string according to lexicographic ordering.
1025///
1026/// # Parameters
1027/// - `window`: Categorical array view tuple `(CategoricalArray, offset, length)` defining the scan window
1028///
1029/// # Returns
1030/// `Option<String>` containing the maximum dictionary string, or `None` if all values are null.
1031#[inline]
1032pub fn max_categorical_array<T: Integer>(window: CategoricalAVT<T>) -> Option<String> {
1033    let (arr, offset, len) = window;
1034    let mut max_code: Option<T> = None;
1035    for i in offset..offset + len {
1036        if arr
1037            .null_mask
1038            .as_ref()
1039            .map_or(true, |b| unsafe { b.get_unchecked(i) })
1040        {
1041            let code = arr.data[i];
1042            if max_code.map_or(true, |max| {
1043                let sc = &arr.unique_values[code.to_usize()];
1044                let sm = &arr.unique_values[max.to_usize()];
1045                sc > sm
1046            }) {
1047                max_code = Some(code);
1048            }
1049        }
1050    }
1051    max_code.map(|code| arr.unique_values[code.to_usize()].clone())
1052}
1053
1054/// Counts the number of distinct string values in a string array window.
1055///
1056/// Computes the cardinality of unique strings within a windowed portion of a
1057/// string array, using efficient hash-based deduplication.
1058///
1059/// # Parameters
1060/// - `window`: String array view tuple `(StringArray, offset, length)` defining the count window
1061///
1062/// # Returns
1063/// `usize` representing the count of distinct non-null string values.
1064///
1065/// # Hash Algorithm
1066/// Uses either AHash (if `fast_hash` feature enabled) or standard HashMap for deduplication.
1067#[inline(always)]
1068pub fn count_distinct_string<T: Integer>(window: StringAVT<T>) -> usize {
1069    let (arr, offset, len) = window;
1070    #[cfg(feature = "fast_hash")]
1071    let mut set = AHashSet::with_capacity(len);
1072    #[cfg(not(feature = "fast_hash"))]
1073    let mut set = HashSet::with_capacity(len);
1074    let null_mask = arr.null_mask.as_ref();
1075
1076    for i in offset..offset + len {
1077        let valid = null_mask.map_or(true, |b| unsafe { b.get_unchecked(i) });
1078        if valid {
1079            let s = unsafe { arr.get_str_unchecked(i) };
1080            set.insert(s);
1081            if set.len() == len {
1082                break;
1083            }
1084        }
1085    }
1086    set.len()
1087}
1088
1089// Unary string transformations
1090
1091/// Generates a unary string transform kernel for StringArray.
1092/// The transform closure receives `&str` and returns `String`.
1093macro_rules! unary_str_transform {
1094    ($fn_name:ident, $doc:expr, $transform:expr) => {
1095        #[doc = $doc]
1096        pub fn $fn_name<T: Integer>(input: StringAVT<T>) -> Result<StringArray<T>, KernelError> {
1097            let (arr, offset, len) = input;
1098
1099            let mask_opt = arr.null_mask.as_ref().map(|orig| {
1100                let mut m = Bitmask::new_set_all(len, true);
1101                for i in 0..len {
1102                    unsafe { m.set_unchecked(i, orig.get_unchecked(offset + i)); }
1103                }
1104                m
1105            });
1106
1107            let input_bytes = arr.offsets[offset + len].to_usize()
1108                - arr.offsets[offset].to_usize();
1109            let mut offsets = Vec64::<T>::with_capacity(len + 1);
1110            unsafe { offsets.set_len(len + 1); }
1111            let mut data = Vec64::<u8>::with_capacity(input_bytes);
1112
1113            offsets[0] = T::zero();
1114            let mut cur = 0usize;
1115
1116            for i in 0..len {
1117                let valid = mask_opt.as_ref()
1118                    .map_or(true, |m| unsafe { m.get_unchecked(i) });
1119
1120                if valid {
1121                    let s = unsafe { arr.get_str_unchecked(offset + i) };
1122                    let t: String = ($transform)(s);
1123                    data.extend_from_slice(t.as_bytes());
1124                    cur += t.len();
1125                }
1126                offsets[i + 1] = T::from_usize(cur);
1127            }
1128
1129            Ok(StringArray {
1130                offsets: offsets.into(),
1131                data: data.into(),
1132                null_mask: mask_opt,
1133            })
1134        }
1135    };
1136}
1137
1138/// Generates a unary string transform kernel for CategoricalArray.
1139/// Transforms dictionary values once and remaps indices.
1140macro_rules! unary_dict_transform {
1141    ($fn_name:ident, $doc:expr, $transform:expr) => {
1142        #[doc = $doc]
1143        pub fn $fn_name<T: Integer>(input: CategoricalAVT<T>) -> Result<CategoricalArray<T>, KernelError> {
1144            let (arr, offset, len) = input;
1145
1146            let mask_opt = arr.null_mask.as_ref().map(|orig| {
1147                let mut m = Bitmask::new_set_all(len, true);
1148                for i in 0..len {
1149                    unsafe { m.set_unchecked(i, orig.get_unchecked(offset + i)); }
1150                }
1151                m
1152            });
1153
1154            // Transform each dictionary value once, build old->new index mapping
1155            #[cfg(feature = "fast_hash")]
1156            let mut seen: AHashMap<String, T> = AHashMap::with_capacity(arr.unique_values.len());
1157            #[cfg(not(feature = "fast_hash"))]
1158            let mut seen: std::collections::HashMap<String, T> =
1159                std::collections::HashMap::with_capacity(arr.unique_values.len());
1160
1161            let mut new_unique = Vec64::<String>::new();
1162            let mut idx_map = Vec64::<T>::with_capacity(arr.unique_values.len());
1163
1164            for old_val in arr.unique_values.iter() {
1165                let t: String = ($transform)(old_val.as_str());
1166                let new_idx = match seen.get(&t) {
1167                    Some(&ix) => ix,
1168                    None => {
1169                        let ix = T::from_usize(new_unique.len());
1170                        new_unique.push(t.clone());
1171                        seen.insert(t, ix);
1172                        ix
1173                    }
1174                };
1175                idx_map.push(new_idx);
1176            }
1177
1178            // Remap indices for the window
1179            let mut data = Vec64::<T>::with_capacity(len);
1180            unsafe { data.set_len(len); }
1181            for i in 0..len {
1182                let valid = mask_opt.as_ref().map_or(true, |m| unsafe { m.get_unchecked(i) });
1183                data[i] = if valid {
1184                    idx_map[arr.data[offset + i].to_usize()]
1185                } else {
1186                    T::zero()
1187                };
1188            }
1189
1190            Ok(CategoricalArray {
1191                data: data.into(),
1192                unique_values: new_unique,
1193                null_mask: mask_opt,
1194            })
1195        }
1196    };
1197}
1198
1199unary_str_transform!(to_uppercase_str,
1200    "Converts each string element to uppercase.",
1201    |s: &str| s.to_uppercase()
1202);
1203unary_dict_transform!(to_uppercase_dict,
1204    "Converts each categorical string element to uppercase.",
1205    |s: &str| s.to_uppercase()
1206);
1207
1208unary_str_transform!(to_lowercase_str,
1209    "Converts each string element to lowercase.",
1210    |s: &str| s.to_lowercase()
1211);
1212unary_dict_transform!(to_lowercase_dict,
1213    "Converts each categorical string element to lowercase.",
1214    |s: &str| s.to_lowercase()
1215);
1216
1217unary_str_transform!(trim_str,
1218    "Trims leading and trailing whitespace from each string element.",
1219    |s: &str| s.trim().to_owned()
1220);
1221unary_dict_transform!(trim_dict,
1222    "Trims leading and trailing whitespace from each categorical string element.",
1223    |s: &str| s.trim().to_owned()
1224);
1225
1226unary_str_transform!(ltrim_str,
1227    "Trims leading whitespace from each string element.",
1228    |s: &str| s.trim_start().to_owned()
1229);
1230unary_dict_transform!(ltrim_dict,
1231    "Trims leading whitespace from each categorical string element.",
1232    |s: &str| s.trim_start().to_owned()
1233);
1234
1235unary_str_transform!(rtrim_str,
1236    "Trims trailing whitespace from each string element.",
1237    |s: &str| s.trim_end().to_owned()
1238);
1239unary_dict_transform!(rtrim_dict,
1240    "Trims trailing whitespace from each categorical string element.",
1241    |s: &str| s.trim_end().to_owned()
1242);
1243
1244unary_str_transform!(reverse_str,
1245    "Reverses each string element by Unicode characters.",
1246    |s: &str| s.chars().rev().collect::<String>()
1247);
1248unary_dict_transform!(reverse_dict,
1249    "Reverses each categorical string element by Unicode characters.",
1250    |s: &str| s.chars().rev().collect::<String>()
1251);
1252
1253// Byte length
1254
1255/// Computes the byte length of each string in a StringArray slice.
1256pub fn byte_length_str<T: Integer + Copy>(
1257    input: StringAVT<T>,
1258) -> Result<IntegerArray<T>, KernelError> {
1259    let (array, offset, len) = input;
1260
1261    let mask_opt = array.null_mask.as_ref().map(|orig| {
1262        let mut m = Bitmask::new_set_all(len, true);
1263        for i in 0..len {
1264            unsafe { m.set_unchecked(i, orig.get_unchecked(offset + i)); }
1265        }
1266        m
1267    });
1268
1269    let mut data = Vec64::<T>::with_capacity(len);
1270    unsafe { data.set_len(len); }
1271    for i in 0..len {
1272        let valid = mask_opt.as_ref().map_or(true, |m| unsafe { m.get_unchecked(i) });
1273        if valid {
1274            let start = array.offsets[offset + i].to_usize();
1275            let end = array.offsets[offset + i + 1].to_usize();
1276            data[i] = T::from_usize(end - start);
1277        } else {
1278            data[i] = T::zero();
1279        }
1280    }
1281
1282    Ok(IntegerArray {
1283        data: data.into(),
1284        null_mask: mask_opt,
1285    })
1286}
1287
1288/// Computes the byte length of each string in a CategoricalArray slice.
1289pub fn byte_length_dict<T: Integer>(
1290    input: CategoricalAVT<T>,
1291) -> Result<IntegerArray<T>, KernelError> {
1292    let (array, offset, len) = input;
1293
1294    let mask_opt = array.null_mask.as_ref().map(|orig| {
1295        let mut m = Bitmask::new_set_all(len, true);
1296        for i in 0..len {
1297            unsafe { m.set_unchecked(i, orig.get_unchecked(offset + i)); }
1298        }
1299        m
1300    });
1301
1302    let mut data = Vec64::<T>::with_capacity(len);
1303    unsafe { data.set_len(len); }
1304    for i in 0..len {
1305        let valid = mask_opt.as_ref().map_or(true, |m| unsafe { m.get_unchecked(i) });
1306        data[i] = if valid {
1307            T::from_usize(unsafe { array.get_str_unchecked(offset + i) }.len())
1308        } else {
1309            T::zero()
1310        };
1311    }
1312
1313    Ok(IntegerArray {
1314        data: data.into(),
1315        null_mask: mask_opt,
1316    })
1317}
1318
1319// Find and count
1320
1321/// Finds the first byte index of `needle` in each string element. Returns -1 if not found.
1322pub fn find_str<T: Integer>(
1323    input: StringAVT<T>,
1324    needle: &str,
1325) -> Result<IntegerArray<i32>, KernelError> {
1326    let (arr, offset, len) = input;
1327
1328    let mask_opt = arr.null_mask.as_ref().map(|orig| {
1329        let mut m = Bitmask::new_set_all(len, true);
1330        for i in 0..len {
1331            unsafe { m.set_unchecked(i, orig.get_unchecked(offset + i)); }
1332        }
1333        m
1334    });
1335
1336    let mut data = Vec64::<i32>::with_capacity(len);
1337    unsafe { data.set_len(len); }
1338    for i in 0..len {
1339        let valid = mask_opt.as_ref().map_or(true, |m| unsafe { m.get_unchecked(i) });
1340        data[i] = if valid {
1341            let s = unsafe { arr.get_str_unchecked(offset + i) };
1342            s.find(needle).map_or(-1, |pos| pos as i32)
1343        } else {
1344            0
1345        };
1346    }
1347
1348    Ok(IntegerArray {
1349        data: data.into(),
1350        null_mask: mask_opt,
1351    })
1352}
1353
1354/// Finds the first byte index of `needle` in each categorical string element. Returns -1 if not found.
1355pub fn find_dict<T: Integer>(
1356    input: CategoricalAVT<T>,
1357    needle: &str,
1358) -> Result<IntegerArray<i32>, KernelError> {
1359    let (arr, offset, len) = input;
1360
1361    let mask_opt = arr.null_mask.as_ref().map(|orig| {
1362        let mut m = Bitmask::new_set_all(len, true);
1363        for i in 0..len {
1364            unsafe { m.set_unchecked(i, orig.get_unchecked(offset + i)); }
1365        }
1366        m
1367    });
1368
1369    let mut data = Vec64::<i32>::with_capacity(len);
1370    unsafe { data.set_len(len); }
1371    for i in 0..len {
1372        let valid = mask_opt.as_ref().map_or(true, |m| unsafe { m.get_unchecked(i) });
1373        data[i] = if valid {
1374            let s = unsafe { arr.get_str_unchecked(offset + i) };
1375            s.find(needle).map_or(-1, |pos| pos as i32)
1376        } else {
1377            0
1378        };
1379    }
1380
1381    Ok(IntegerArray {
1382        data: data.into(),
1383        null_mask: mask_opt,
1384    })
1385}
1386
1387/// Counts non-overlapping occurrences of `needle` in each string element.
1388pub fn count_match_str<T: Integer>(
1389    input: StringAVT<T>,
1390    needle: &str,
1391) -> Result<IntegerArray<i32>, KernelError> {
1392    let (arr, offset, len) = input;
1393
1394    let mask_opt = arr.null_mask.as_ref().map(|orig| {
1395        let mut m = Bitmask::new_set_all(len, true);
1396        for i in 0..len {
1397            unsafe { m.set_unchecked(i, orig.get_unchecked(offset + i)); }
1398        }
1399        m
1400    });
1401
1402    let mut data = Vec64::<i32>::with_capacity(len);
1403    unsafe { data.set_len(len); }
1404    for i in 0..len {
1405        let valid = mask_opt.as_ref().map_or(true, |m| unsafe { m.get_unchecked(i) });
1406        data[i] = if valid {
1407            let s = unsafe { arr.get_str_unchecked(offset + i) };
1408            s.matches(needle).count() as i32
1409        } else {
1410            0
1411        };
1412    }
1413
1414    Ok(IntegerArray {
1415        data: data.into(),
1416        null_mask: mask_opt,
1417    })
1418}
1419
1420/// Counts non-overlapping occurrences of `needle` in each categorical string element.
1421pub fn count_match_dict<T: Integer>(
1422    input: CategoricalAVT<T>,
1423    needle: &str,
1424) -> Result<IntegerArray<i32>, KernelError> {
1425    let (arr, offset, len) = input;
1426
1427    let mask_opt = arr.null_mask.as_ref().map(|orig| {
1428        let mut m = Bitmask::new_set_all(len, true);
1429        for i in 0..len {
1430            unsafe { m.set_unchecked(i, orig.get_unchecked(offset + i)); }
1431        }
1432        m
1433    });
1434
1435    let mut data = Vec64::<i32>::with_capacity(len);
1436    unsafe { data.set_len(len); }
1437    for i in 0..len {
1438        let valid = mask_opt.as_ref().map_or(true, |m| unsafe { m.get_unchecked(i) });
1439        data[i] = if valid {
1440            let s = unsafe { arr.get_str_unchecked(offset + i) };
1441            s.matches(needle).count() as i32
1442        } else {
1443            0
1444        };
1445    }
1446
1447    Ok(IntegerArray {
1448        data: data.into(),
1449        null_mask: mask_opt,
1450    })
1451}
1452
1453// Substring
1454
1455/// Extracts a character-based substring from each string element.
1456/// `start` is the 0-based character offset. `opt_len` limits the number of characters taken.
1457pub fn substring_str<T: Integer>(
1458    input: StringAVT<T>,
1459    start: usize,
1460    opt_len: Option<usize>,
1461) -> Result<StringArray<T>, KernelError> {
1462    let (arr, offset, len) = input;
1463    let mask_opt = arr.null_mask.as_ref().map(|orig| {
1464        let mut m = Bitmask::new_set_all(len, true);
1465        for i in 0..len {
1466            unsafe { m.set_unchecked(i, orig.get_unchecked(offset + i)); }
1467        }
1468        m
1469    });
1470
1471    let input_bytes = arr.offsets[offset + len].to_usize()
1472        - arr.offsets[offset].to_usize();
1473    let mut offsets = Vec64::<T>::with_capacity(len + 1);
1474    unsafe { offsets.set_len(len + 1); }
1475    let mut data = Vec64::<u8>::with_capacity(input_bytes);
1476
1477    offsets[0] = T::zero();
1478    let mut cur = 0usize;
1479
1480    for i in 0..len {
1481        let valid = mask_opt.as_ref().map_or(true, |m| unsafe { m.get_unchecked(i) });
1482        if valid {
1483            let s = unsafe { arr.get_str_unchecked(offset + i) };
1484            let chars = s.chars().skip(start);
1485            let t: String = match opt_len {
1486                Some(n) => chars.take(n).collect(),
1487                None => chars.collect(),
1488            };
1489            data.extend_from_slice(t.as_bytes());
1490            cur += t.len();
1491        }
1492        offsets[i + 1] = T::from_usize(cur);
1493    }
1494
1495    Ok(StringArray {
1496        offsets: offsets.into(),
1497        data: data.into(),
1498        null_mask: mask_opt,
1499    })
1500}
1501
1502/// Extracts a character-based substring from each categorical string element.
1503pub fn substring_dict<T: Integer>(
1504    input: CategoricalAVT<T>,
1505    start: usize,
1506    opt_len: Option<usize>,
1507) -> Result<CategoricalArray<T>, KernelError> {
1508    let (arr, offset, len) = input;
1509    let mask_opt = arr.null_mask.as_ref().map(|orig| {
1510        let mut m = Bitmask::new_set_all(len, true);
1511        for i in 0..len {
1512            unsafe { m.set_unchecked(i, orig.get_unchecked(offset + i)); }
1513        }
1514        m
1515    });
1516
1517    #[cfg(feature = "fast_hash")]
1518    let mut seen: AHashMap<String, T> = AHashMap::with_capacity(arr.unique_values.len());
1519    #[cfg(not(feature = "fast_hash"))]
1520    let mut seen: std::collections::HashMap<String, T> =
1521        std::collections::HashMap::with_capacity(arr.unique_values.len());
1522
1523    let mut new_unique = Vec64::<String>::new();
1524    let mut idx_map = Vec64::<T>::with_capacity(arr.unique_values.len());
1525
1526    for old_val in arr.unique_values.iter() {
1527        let chars = old_val.chars().skip(start);
1528        let t: String = match opt_len {
1529            Some(n) => chars.take(n).collect(),
1530            None => chars.collect(),
1531        };
1532        let new_idx = match seen.get(&t) {
1533            Some(&ix) => ix,
1534            None => {
1535                let ix = T::from_usize(new_unique.len());
1536                new_unique.push(t.clone());
1537                seen.insert(t, ix);
1538                ix
1539            }
1540        };
1541        idx_map.push(new_idx);
1542    }
1543
1544    let mut data = Vec64::<T>::with_capacity(len);
1545    unsafe { data.set_len(len); }
1546    for i in 0..len {
1547        let valid = mask_opt.as_ref().map_or(true, |m| unsafe { m.get_unchecked(i) });
1548        data[i] = if valid {
1549            idx_map[arr.data[offset + i].to_usize()]
1550        } else {
1551            T::zero()
1552        };
1553    }
1554
1555    Ok(CategoricalArray {
1556        data: data.into(),
1557        unique_values: new_unique,
1558        null_mask: mask_opt,
1559    })
1560}
1561
1562// Replace
1563
1564/// Replaces all occurrences of `from` with `to` in each string element.
1565pub fn replace_str<T: Integer>(
1566    input: StringAVT<T>,
1567    from: &str,
1568    to: &str,
1569) -> Result<StringArray<T>, KernelError> {
1570    let (arr, offset, len) = input;
1571    let mask_opt = arr.null_mask.as_ref().map(|orig| {
1572        let mut m = Bitmask::new_set_all(len, true);
1573        for i in 0..len {
1574            unsafe { m.set_unchecked(i, orig.get_unchecked(offset + i)); }
1575        }
1576        m
1577    });
1578
1579    let input_bytes = arr.offsets[offset + len].to_usize()
1580        - arr.offsets[offset].to_usize();
1581    let mut offsets = Vec64::<T>::with_capacity(len + 1);
1582    unsafe { offsets.set_len(len + 1); }
1583    let mut data = Vec64::<u8>::with_capacity(input_bytes);
1584
1585    offsets[0] = T::zero();
1586    let mut cur = 0usize;
1587
1588    for i in 0..len {
1589        let valid = mask_opt.as_ref().map_or(true, |m| unsafe { m.get_unchecked(i) });
1590        if valid {
1591            let s = unsafe { arr.get_str_unchecked(offset + i) };
1592            let t = s.replace(from, to);
1593            data.extend_from_slice(t.as_bytes());
1594            cur += t.len();
1595        }
1596        offsets[i + 1] = T::from_usize(cur);
1597    }
1598
1599    Ok(StringArray {
1600        offsets: offsets.into(),
1601        data: data.into(),
1602        null_mask: mask_opt,
1603    })
1604}
1605
1606/// Replaces all occurrences of `from` with `to` in each categorical string element.
1607pub fn replace_dict<T: Integer>(
1608    input: CategoricalAVT<T>,
1609    from: &str,
1610    to: &str,
1611) -> Result<CategoricalArray<T>, KernelError> {
1612    let (arr, offset, len) = input;
1613    let mask_opt = arr.null_mask.as_ref().map(|orig| {
1614        let mut m = Bitmask::new_set_all(len, true);
1615        for i in 0..len {
1616            unsafe { m.set_unchecked(i, orig.get_unchecked(offset + i)); }
1617        }
1618        m
1619    });
1620
1621    #[cfg(feature = "fast_hash")]
1622    let mut seen: AHashMap<String, T> = AHashMap::with_capacity(arr.unique_values.len());
1623    #[cfg(not(feature = "fast_hash"))]
1624    let mut seen: std::collections::HashMap<String, T> =
1625        std::collections::HashMap::with_capacity(arr.unique_values.len());
1626
1627    let mut new_unique = Vec64::<String>::new();
1628    let mut idx_map = Vec64::<T>::with_capacity(arr.unique_values.len());
1629
1630    for old_val in arr.unique_values.iter() {
1631        let t = old_val.replace(from, to);
1632        let new_idx = match seen.get(&t) {
1633            Some(&ix) => ix,
1634            None => {
1635                let ix = T::from_usize(new_unique.len());
1636                new_unique.push(t.clone());
1637                seen.insert(t, ix);
1638                ix
1639            }
1640        };
1641        idx_map.push(new_idx);
1642    }
1643
1644    let mut data = Vec64::<T>::with_capacity(len);
1645    unsafe { data.set_len(len); }
1646    for i in 0..len {
1647        let valid = mask_opt.as_ref().map_or(true, |m| unsafe { m.get_unchecked(i) });
1648        data[i] = if valid {
1649            idx_map[arr.data[offset + i].to_usize()]
1650        } else {
1651            T::zero()
1652        };
1653    }
1654
1655    Ok(CategoricalArray {
1656        data: data.into(),
1657        unique_values: new_unique,
1658        null_mask: mask_opt,
1659    })
1660}
1661
1662// Repeat
1663
1664/// Repeats each string element `n` times.
1665pub fn repeat_str<T: Integer>(
1666    input: StringAVT<T>,
1667    n: usize,
1668) -> Result<StringArray<T>, KernelError> {
1669    let (arr, offset, len) = input;
1670    let mask_opt = arr.null_mask.as_ref().map(|orig| {
1671        let mut m = Bitmask::new_set_all(len, true);
1672        for i in 0..len {
1673            unsafe { m.set_unchecked(i, orig.get_unchecked(offset + i)); }
1674        }
1675        m
1676    });
1677
1678    let input_bytes = arr.offsets[offset + len].to_usize()
1679        - arr.offsets[offset].to_usize();
1680    let mut offsets = Vec64::<T>::with_capacity(len + 1);
1681    unsafe { offsets.set_len(len + 1); }
1682    let mut data = Vec64::<u8>::with_capacity(input_bytes * n);
1683
1684    offsets[0] = T::zero();
1685    let mut cur = 0usize;
1686
1687    for i in 0..len {
1688        let valid = mask_opt.as_ref().map_or(true, |m| unsafe { m.get_unchecked(i) });
1689        if valid {
1690            let s = unsafe { arr.get_str_unchecked(offset + i) };
1691            let bytes = s.as_bytes();
1692            for _ in 0..n {
1693                data.extend_from_slice(bytes);
1694            }
1695            cur += bytes.len() * n;
1696        }
1697        offsets[i + 1] = T::from_usize(cur);
1698    }
1699
1700    Ok(StringArray {
1701        offsets: offsets.into(),
1702        data: data.into(),
1703        null_mask: mask_opt,
1704    })
1705}
1706
1707/// Repeats each categorical string element `n` times.
1708pub fn repeat_dict<T: Integer>(
1709    input: CategoricalAVT<T>,
1710    n: usize,
1711) -> Result<CategoricalArray<T>, KernelError> {
1712    let (arr, offset, len) = input;
1713    let mask_opt = arr.null_mask.as_ref().map(|orig| {
1714        let mut m = Bitmask::new_set_all(len, true);
1715        for i in 0..len {
1716            unsafe { m.set_unchecked(i, orig.get_unchecked(offset + i)); }
1717        }
1718        m
1719    });
1720
1721    // Repeat is a 1:1 mapping on dictionary values, no merging possible
1722    let mut new_unique = Vec64::<String>::new();
1723    for old_val in arr.unique_values.iter() {
1724        new_unique.push(old_val.repeat(n));
1725    }
1726
1727    let mut data = Vec64::<T>::with_capacity(len);
1728    unsafe { data.set_len(len); }
1729    for i in 0..len {
1730        let valid = mask_opt.as_ref().map_or(true, |m| unsafe { m.get_unchecked(i) });
1731        data[i] = if valid {
1732            arr.data[offset + i]
1733        } else {
1734            T::zero()
1735        };
1736    }
1737
1738    Ok(CategoricalArray {
1739        data: data.into(),
1740        unique_values: new_unique,
1741        null_mask: mask_opt,
1742    })
1743}
1744
1745// Pad
1746
1747/// Pads each string element to `width` characters using `fill_char`.
1748pub fn pad_str<T: Integer>(
1749    input: StringAVT<T>,
1750    width: usize,
1751    fill_char: char,
1752    side: PadSide,
1753) -> Result<StringArray<T>, KernelError> {
1754    let (arr, offset, len) = input;
1755    let mask_opt = arr.null_mask.as_ref().map(|orig| {
1756        let mut m = Bitmask::new_set_all(len, true);
1757        for i in 0..len {
1758            unsafe { m.set_unchecked(i, orig.get_unchecked(offset + i)); }
1759        }
1760        m
1761    });
1762
1763    let max_padded = width * fill_char.len_utf8();
1764    let mut offsets = Vec64::<T>::with_capacity(len + 1);
1765    unsafe { offsets.set_len(len + 1); }
1766    let mut data = Vec64::<u8>::with_capacity(len * max_padded);
1767
1768    offsets[0] = T::zero();
1769    let mut cur = 0usize;
1770
1771    for i in 0..len {
1772        let valid = mask_opt.as_ref().map_or(true, |m| unsafe { m.get_unchecked(i) });
1773        if valid {
1774            let s = unsafe { arr.get_str_unchecked(offset + i) };
1775            let char_len = s.chars().count();
1776            if char_len >= width {
1777                data.extend_from_slice(s.as_bytes());
1778                cur += s.len();
1779            } else {
1780                let padding = width - char_len;
1781                let pad_bytes = fill_char.len_utf8();
1782                let mut pad_buf = [0u8; 4];
1783                let pad_slice = fill_char.encode_utf8(&mut pad_buf);
1784                match side {
1785                    PadSide::Left => {
1786                        for _ in 0..padding { data.extend_from_slice(pad_slice.as_bytes()); }
1787                        data.extend_from_slice(s.as_bytes());
1788                    }
1789                    PadSide::Right => {
1790                        data.extend_from_slice(s.as_bytes());
1791                        for _ in 0..padding { data.extend_from_slice(pad_slice.as_bytes()); }
1792                    }
1793                    PadSide::Both => {
1794                        let left = padding / 2;
1795                        let right = padding - left;
1796                        for _ in 0..left { data.extend_from_slice(pad_slice.as_bytes()); }
1797                        data.extend_from_slice(s.as_bytes());
1798                        for _ in 0..right { data.extend_from_slice(pad_slice.as_bytes()); }
1799                    }
1800                }
1801                cur += s.len() + padding * pad_bytes;
1802            }
1803        }
1804        offsets[i + 1] = T::from_usize(cur);
1805    }
1806
1807    Ok(StringArray {
1808        offsets: offsets.into(),
1809        data: data.into(),
1810        null_mask: mask_opt,
1811    })
1812}
1813
1814/// Pads each categorical string element to `width` characters using `fill_char`.
1815pub fn pad_dict<T: Integer>(
1816    input: CategoricalAVT<T>,
1817    width: usize,
1818    fill_char: char,
1819    side: PadSide,
1820) -> Result<CategoricalArray<T>, KernelError> {
1821    let (arr, offset, len) = input;
1822    let mask_opt = arr.null_mask.as_ref().map(|orig| {
1823        let mut m = Bitmask::new_set_all(len, true);
1824        for i in 0..len {
1825            unsafe { m.set_unchecked(i, orig.get_unchecked(offset + i)); }
1826        }
1827        m
1828    });
1829
1830    let pad_one = |s: &str| -> String {
1831        let char_len = s.chars().count();
1832        if char_len >= width {
1833            return s.to_owned();
1834        }
1835        let padding = width - char_len;
1836        match side {
1837            PadSide::Left => {
1838                let mut out = String::with_capacity(s.len() + padding * fill_char.len_utf8());
1839                for _ in 0..padding { out.push(fill_char); }
1840                out.push_str(s);
1841                out
1842            }
1843            PadSide::Right => {
1844                let mut out = String::with_capacity(s.len() + padding * fill_char.len_utf8());
1845                out.push_str(s);
1846                for _ in 0..padding { out.push(fill_char); }
1847                out
1848            }
1849            PadSide::Both => {
1850                let left = padding / 2;
1851                let right = padding - left;
1852                let mut out = String::with_capacity(s.len() + padding * fill_char.len_utf8());
1853                for _ in 0..left { out.push(fill_char); }
1854                out.push_str(s);
1855                for _ in 0..right { out.push(fill_char); }
1856                out
1857            }
1858        }
1859    };
1860
1861    // Pad is 1:1 on dictionary values since padding a unique string always produces a unique result
1862    let mut new_unique = Vec64::<String>::new();
1863    for old_val in arr.unique_values.iter() {
1864        new_unique.push(pad_one(old_val));
1865    }
1866
1867    let mut data = Vec64::<T>::with_capacity(len);
1868    unsafe { data.set_len(len); }
1869    for i in 0..len {
1870        let valid = mask_opt.as_ref().map_or(true, |m| unsafe { m.get_unchecked(i) });
1871        data[i] = if valid {
1872            arr.data[offset + i]
1873        } else {
1874            T::zero()
1875        };
1876    }
1877
1878    Ok(CategoricalArray {
1879        data: data.into(),
1880        unique_values: new_unique,
1881        null_mask: mask_opt,
1882    })
1883}
1884
1885// Join (aggregation)
1886
1887/// Joins all non-null string elements with `delimiter`, returning a single string.
1888/// Returns `None` if all elements are null.
1889pub fn join_str<T: Integer>(input: StringAVT<T>, delimiter: &str) -> Option<String> {
1890    let (arr, offset, len) = input;
1891    let mut parts: Vec<&str> = Vec::with_capacity(len);
1892    for i in offset..offset + len {
1893        let valid = arr.null_mask.as_ref().map_or(true, |b| unsafe { b.get_unchecked(i) });
1894        if valid {
1895            parts.push(unsafe { arr.get_str_unchecked(i) });
1896        }
1897    }
1898    if parts.is_empty() { None } else { Some(parts.join(delimiter)) }
1899}
1900
1901/// Joins all non-null categorical string elements with `delimiter`, returning a single string.
1902/// Returns `None` if all elements are null.
1903pub fn join_dict<T: Integer>(input: CategoricalAVT<T>, delimiter: &str) -> Option<String> {
1904    let (arr, offset, len) = input;
1905    let mut parts: Vec<&str> = Vec::with_capacity(len);
1906    for i in offset..offset + len {
1907        let valid = arr.null_mask.as_ref().map_or(true, |b| unsafe { b.get_unchecked(i) });
1908        if valid {
1909            parts.push(unsafe { arr.get_str_unchecked(i) });
1910        }
1911    }
1912    if parts.is_empty() { None } else { Some(parts.join(delimiter)) }
1913}
1914
1915// Regex replace
1916
1917/// Replaces all regex matches in each string element with `replacement`.
1918#[cfg(feature = "regex")]
1919pub fn regex_replace_str<T: Integer>(
1920    input: StringAVT<T>,
1921    pattern: &str,
1922    replacement: &str,
1923) -> Result<StringArray<T>, KernelError> {
1924    let re = Regex::new(pattern).map_err(|_| {
1925        KernelError::InvalidArguments("Invalid regex pattern".to_string())
1926    })?;
1927    let (arr, offset, len) = input;
1928    let mask_opt = arr.null_mask.as_ref().map(|orig| {
1929        let mut m = Bitmask::new_set_all(len, true);
1930        for i in 0..len {
1931            unsafe { m.set_unchecked(i, orig.get_unchecked(offset + i)); }
1932        }
1933        m
1934    });
1935
1936    let input_bytes = arr.offsets[offset + len].to_usize()
1937        - arr.offsets[offset].to_usize();
1938    let mut offsets = Vec64::<T>::with_capacity(len + 1);
1939    unsafe { offsets.set_len(len + 1); }
1940    let mut data = Vec64::<u8>::with_capacity(input_bytes);
1941
1942    offsets[0] = T::zero();
1943    let mut cur = 0usize;
1944
1945    for i in 0..len {
1946        let valid = mask_opt.as_ref().map_or(true, |m| unsafe { m.get_unchecked(i) });
1947        if valid {
1948            let s = unsafe { arr.get_str_unchecked(offset + i) };
1949            let t = re.replace_all(s, replacement).into_owned();
1950            data.extend_from_slice(t.as_bytes());
1951            cur += t.len();
1952        }
1953        offsets[i + 1] = T::from_usize(cur);
1954    }
1955
1956    Ok(StringArray {
1957        offsets: offsets.into(),
1958        data: data.into(),
1959        null_mask: mask_opt,
1960    })
1961}
1962
1963/// Replaces all regex matches in each categorical string element with `replacement`.
1964#[cfg(feature = "regex")]
1965pub fn regex_replace_dict<T: Integer>(
1966    input: CategoricalAVT<T>,
1967    pattern: &str,
1968    replacement: &str,
1969) -> Result<CategoricalArray<T>, KernelError> {
1970    let re = Regex::new(pattern).map_err(|_| {
1971        KernelError::InvalidArguments("Invalid regex pattern".to_string())
1972    })?;
1973    let (arr, offset, len) = input;
1974    let mask_opt = arr.null_mask.as_ref().map(|orig| {
1975        let mut m = Bitmask::new_set_all(len, true);
1976        for i in 0..len {
1977            unsafe { m.set_unchecked(i, orig.get_unchecked(offset + i)); }
1978        }
1979        m
1980    });
1981
1982    #[cfg(feature = "fast_hash")]
1983    let mut seen: AHashMap<String, T> = AHashMap::with_capacity(arr.unique_values.len());
1984    #[cfg(not(feature = "fast_hash"))]
1985    let mut seen: std::collections::HashMap<String, T> =
1986        std::collections::HashMap::with_capacity(arr.unique_values.len());
1987
1988    let mut new_unique = Vec64::<String>::new();
1989    let mut idx_map = Vec64::<T>::with_capacity(arr.unique_values.len());
1990
1991    for old_val in arr.unique_values.iter() {
1992        let t = re.replace_all(old_val, replacement).into_owned();
1993        let new_idx = match seen.get(&t) {
1994            Some(&ix) => ix,
1995            None => {
1996                let ix = T::from_usize(new_unique.len());
1997                new_unique.push(t.clone());
1998                seen.insert(t, ix);
1999                ix
2000            }
2001        };
2002        idx_map.push(new_idx);
2003    }
2004
2005    let mut data = Vec64::<T>::with_capacity(len);
2006    unsafe { data.set_len(len); }
2007    for i in 0..len {
2008        let valid = mask_opt.as_ref().map_or(true, |m| unsafe { m.get_unchecked(i) });
2009        data[i] = if valid {
2010            idx_map[arr.data[offset + i].to_usize()]
2011        } else {
2012            T::zero()
2013        };
2014    }
2015
2016    Ok(CategoricalArray {
2017        data: data.into(),
2018        unique_values: new_unique,
2019        null_mask: mask_opt,
2020    })
2021}
2022
2023// Cross-tabulation
2024
2025/// Cross-tabulate two text array views into a contingency table.
2026///
2027/// Iterates both views in lockstep, discovering categories on first appearance and
2028/// incrementing the corresponding cell for each jointly non-null pair. Null positions
2029/// in either array cause the row to be skipped.
2030///
2031/// Returns the count matrix, row labels from `x`, column labels from `y`,
2032/// and total number of valid pairs counted.
2033#[cfg(feature = "views")]
2034pub fn cross_tabulate(
2035    x: &TextArrayV,
2036    y: &TextArrayV,
2037) -> Result<(Vec64<Vec64<u64>>, Vec64<String>, Vec64<String>, usize), KernelError> {
2038    let n = x.len().min(y.len());
2039
2040    if x.len() != y.len() {
2041        return Err(KernelError::LengthMismatch(format!(
2042            "cross_tabulate: x has length {} but y has length {}",
2043            x.len(),
2044            y.len()
2045        )));
2046    }
2047
2048    #[cfg(feature = "fast_hash")]
2049    let mut row_map: AHashMap<String, usize> = AHashMap::with_capacity(n.min(256));
2050    #[cfg(not(feature = "fast_hash"))]
2051    let mut row_map: HashMap<String, usize> = HashMap::with_capacity(n.min(256));
2052
2053    #[cfg(feature = "fast_hash")]
2054    let mut col_map: AHashMap<String, usize> = AHashMap::with_capacity(n.min(256));
2055    #[cfg(not(feature = "fast_hash"))]
2056    let mut col_map: HashMap<String, usize> = HashMap::with_capacity(n.min(256));
2057
2058    let mut row_labels = Vec64::<String>::with_capacity(32);
2059    let mut col_labels = Vec64::<String>::with_capacity(32);
2060
2061    // Single pass: discover categories and count co-occurrences.
2062    // We build a flat count vec keyed by (ri * max_cols + ci) and resize as
2063    // new categories are discovered. For typical cardinalities this stays small.
2064    let mut counts = Vec64::<u64>::new();
2065    let mut n_cols_seen = 0_usize;
2066    let mut total = 0_usize;
2067
2068    for i in 0..n {
2069        let (Some(xv), Some(yv)) = (x.get_str(i), y.get_str(i)) else {
2070            continue;
2071        };
2072
2073        let row_len = row_labels.len();
2074        let ri = *row_map.entry(xv.to_string()).or_insert_with(|| {
2075            row_labels.push(xv.to_string());
2076            row_len
2077        });
2078
2079        let col_len = col_labels.len();
2080        let ci = *col_map.entry(yv.to_string()).or_insert_with(|| {
2081            col_labels.push(yv.to_string());
2082            col_len
2083        });
2084
2085        // If new columns were added, widen every existing row in the flat buffer
2086        if col_labels.len() > n_cols_seen {
2087            let old_cols = n_cols_seen;
2088            n_cols_seen = col_labels.len();
2089            let nrows = row_labels.len();
2090            // Rebuild flat buffer with wider stride
2091            let mut new_counts = Vec64::<u64>::with_capacity(nrows * n_cols_seen);
2092            new_counts.resize(nrows * n_cols_seen, 0);
2093            for r in 0..nrows.min(ri + 1) {
2094                for c in 0..old_cols {
2095                    if r * old_cols + c < counts.len() {
2096                        new_counts[r * n_cols_seen + c] = counts[r * old_cols + c];
2097                    }
2098                }
2099            }
2100            counts = new_counts;
2101        }
2102
2103        // New row may need the buffer extended
2104        let needed = (ri + 1) * n_cols_seen;
2105        if needed > counts.len() {
2106            counts.resize(needed, 0);
2107        }
2108
2109        counts[ri * n_cols_seen + ci] += 1;
2110        total += 1;
2111    }
2112
2113    if total == 0 {
2114        return Err(KernelError::InvalidArguments(
2115            "cross_tabulate: no valid non-null pairs found".to_string(),
2116        ));
2117    }
2118
2119    // Convert flat counts to Vec64<Vec64<u64>> matrix
2120    let nrows = row_labels.len();
2121    let ncols = col_labels.len();
2122    let mut matrix = Vec64::<Vec64<u64>>::with_capacity(nrows);
2123    for r in 0..nrows {
2124        let start = r * ncols;
2125        let end = start + ncols;
2126        let row = Vec64::from_slice(&counts[start..end]);
2127        matrix.push(row);
2128    }
2129
2130    Ok((matrix, row_labels, col_labels, total))
2131}
2132
2133#[cfg(test)]
2134mod tests {
2135    use crate::{CategoricalArray, StringArray, vec64};
2136
2137    use super::*;
2138
2139    // --- Helper constructors
2140
2141    fn str_array<T: Integer>(vals: &[&str]) -> StringArray<T> {
2142        StringArray::<T>::from_slice(vals)
2143    }
2144
2145    fn dict_array<T: Integer>(vals: &[&str]) -> CategoricalArray<T> {
2146        let owned: Vec<&str> = vals.to_vec();
2147        CategoricalArray::<T>::from_values(owned)
2148    }
2149
2150    fn bm(bools: &[bool]) -> Bitmask {
2151        Bitmask::from_bools(bools)
2152    }
2153
2154    // --- Concat
2155
2156    #[test]
2157    fn test_concat_str_str() {
2158        let a = str_array::<u32>(&["foo", "bar", ""]);
2159        let b = str_array::<u32>(&["baz", "qux", "quux"]);
2160        let out = concat_str_str((&a, 0, a.len()), (&b, 0, b.len()));
2161        assert_eq!(out.get(0), Some("foobaz"));
2162        assert_eq!(out.get(1), Some("barqux"));
2163        assert_eq!(out.get(2), Some("quux"));
2164        assert!(out.null_mask.as_ref().unwrap().all_set());
2165    }
2166
2167    #[test]
2168    fn test_concat_str_str_chunk() {
2169        let a = str_array::<u32>(&["XXX", "foo", "bar", ""]);
2170        let b = str_array::<u32>(&["YYY", "baz", "qux", "quux"]);
2171        // Window is [1..4) for both, i.e., ["foo", "bar", ""]
2172        let out = concat_str_str((&a, 1, 3), (&b, 1, 3));
2173        assert_eq!(out.get(0), Some("foobaz"));
2174        assert_eq!(out.get(1), Some("barqux"));
2175        assert_eq!(out.get(2), Some("quux"));
2176        assert!(out.null_mask.as_ref().unwrap().all_set());
2177    }
2178
2179    #[test]
2180    fn test_concat_dict_dict() {
2181        let a = dict_array::<u32>(&["x", "y"]);
2182        let b = dict_array::<u32>(&["1", "2"]);
2183        let out = concat_dict_dict((&a, 0, a.len()), (&b, 0, b.len())).unwrap();
2184        let s0 = out.get(0).unwrap();
2185        let s1 = out.get(1).unwrap();
2186        assert!(["x1", "y2"].contains(&s0));
2187        assert!(["x1", "y2"].contains(&s1));
2188        assert!(out.null_mask.as_ref().unwrap().all_set());
2189    }
2190
2191    #[test]
2192    fn test_concat_dict_dict_chunk() {
2193        let a = dict_array::<u32>(&["foo", "x", "y", "bar"]);
2194        let b = dict_array::<u32>(&["A", "1", "2", "B"]);
2195        let out = concat_dict_dict((&a, 1, 2), (&b, 1, 2)).unwrap();
2196        let s0 = out.get(0).unwrap();
2197        let s1 = out.get(1).unwrap();
2198        assert!(["x1", "y2"].contains(&s0));
2199        assert!(["x1", "y2"].contains(&s1));
2200        assert!(out.null_mask.as_ref().unwrap().all_set());
2201    }
2202
2203    #[test]
2204    fn test_concat_str_dict() {
2205        let a = str_array::<u32>(&["ab", "cd", ""]);
2206        let b = dict_array::<u32>(&["xy", "zq", ""]);
2207        let out = concat_str_dict((&a, 0, a.len()), (&b, 0, b.len())).unwrap();
2208        assert_eq!(out.get(0), Some("abxy"));
2209        assert_eq!(out.get(1), Some("cdzq"));
2210        assert_eq!(out.get(2), Some(""));
2211        assert!(out.null_mask.as_ref().unwrap().all_set());
2212    }
2213
2214    #[test]
2215    fn test_concat_str_dict_chunk() {
2216        let a = str_array::<u32>(&["dummy", "ab", "cd", ""]);
2217        let b = dict_array::<u32>(&["dummy", "xy", "zq", ""]);
2218        let out = concat_str_dict((&a, 1, 3), (&b, 1, 3)).unwrap();
2219        assert_eq!(out.get(0), Some("abxy"));
2220        assert_eq!(out.get(1), Some("cdzq"));
2221        assert_eq!(out.get(2), Some(""));
2222        assert!(out.null_mask.as_ref().unwrap().all_set());
2223    }
2224
2225    #[test]
2226    fn test_concat_dict_str() {
2227        let a = dict_array::<u32>(&["hi", "ho"]);
2228        let b = str_array::<u32>(&["yo", "no"]);
2229        let out = concat_dict_str((&a, 0, a.len()), (&b, 0, b.len())).unwrap();
2230        assert_eq!(out.get(0), Some("yohi"));
2231        assert_eq!(out.get(1), Some("noho"));
2232        assert!(out.null_mask.as_ref().unwrap().all_set());
2233    }
2234
2235    #[test]
2236    fn test_concat_dict_str_chunk() {
2237        let a = dict_array::<u32>(&["dummy", "hi", "ho", "zzz"]);
2238        let b = str_array::<u32>(&["dummy", "yo", "no", "xxx"]);
2239        let out = concat_dict_str((&a, 1, 2), (&b, 1, 2)).unwrap();
2240        assert_eq!(out.get(0), Some("yohi"));
2241        assert_eq!(out.get(1), Some("noho"));
2242        assert!(out.null_mask.as_ref().unwrap().all_set());
2243    }
2244
2245    // --- String predicates
2246
2247    #[test]
2248    fn test_contains_str_str() {
2249        let s = str_array::<u32>(&["abc", "def", "ghijk"]);
2250        let p = str_array::<u32>(&["b", "x", "jk"]);
2251        let out = contains_str_str((&s, 0, s.len()), (&p, 0, p.len()));
2252        assert_eq!(out.get(0), Some(true));
2253        assert_eq!(out.get(1), Some(false));
2254        assert_eq!(out.get(2), Some(true));
2255    }
2256
2257    #[test]
2258    fn test_contains_str_str_chunk() {
2259        let s = str_array::<u32>(&["dummy", "abc", "def", "ghijk"]);
2260        let p = str_array::<u32>(&["dummy", "b", "x", "jk"]);
2261        let out = contains_str_str((&s, 1, 3), (&p, 1, 3));
2262        assert_eq!(out.get(0), Some(true));
2263        assert_eq!(out.get(1), Some(false));
2264        assert_eq!(out.get(2), Some(true));
2265    }
2266
2267    #[test]
2268    fn test_starts_with_str_str() {
2269        let s = str_array::<u32>(&["apricot", "banana", "apple"]);
2270        let p = str_array::<u32>(&["ap", "ba", "a"]);
2271        let out = starts_with_str_str((&s, 0, s.len()), (&p, 0, p.len()));
2272        assert_eq!(out.get(0), Some(true));
2273        assert_eq!(out.get(1), Some(true));
2274        assert_eq!(out.get(2), Some(true));
2275    }
2276
2277    #[test]
2278    fn test_starts_with_str_str_chunk() {
2279        let s = str_array::<u32>(&["dummy", "apricot", "banana", "apple"]);
2280        let p = str_array::<u32>(&["dummy", "ap", "ba", "a"]);
2281        let out = starts_with_str_str((&s, 1, 3), (&p, 1, 3));
2282        assert_eq!(out.get(0), Some(true));
2283        assert_eq!(out.get(1), Some(true));
2284        assert_eq!(out.get(2), Some(true));
2285    }
2286
2287    #[test]
2288    fn test_ends_with_str_str() {
2289        let s = str_array::<u32>(&["robot", "fast", "last"]);
2290        let p = str_array::<u32>(&["ot", "st", "ast"]);
2291        let out = ends_with_str_str((&s, 0, s.len()), (&p, 0, p.len()));
2292        assert_eq!(out.get(0), Some(true));
2293        assert_eq!(out.get(1), Some(true));
2294        assert_eq!(out.get(2), Some(true));
2295    }
2296
2297    #[test]
2298    fn test_ends_with_str_str_chunk() {
2299        let s = str_array::<u32>(&["dummy", "robot", "fast", "last"]);
2300        let p = str_array::<u32>(&["dummy", "ot", "st", "ast"]);
2301        let out = ends_with_str_str((&s, 1, 3), (&p, 1, 3));
2302        assert_eq!(out.get(0), Some(true));
2303        assert_eq!(out.get(1), Some(true));
2304        assert_eq!(out.get(2), Some(true));
2305    }
2306
2307    #[test]
2308    fn test_contains_str_dict() {
2309        let s = str_array::<u32>(&["abcde", "xyz", "qrstuv"]);
2310        let p = dict_array::<u32>(&["c", "z", "tu"]);
2311        let out = contains_str_dict((&s, 0, s.len()), (&p, 0, p.len())).unwrap();
2312        assert_eq!(out.get(0), Some(true));
2313        assert_eq!(out.get(1), Some(true));
2314        assert_eq!(out.get(2), Some(true));
2315    }
2316
2317    #[test]
2318    fn test_contains_str_dict_chunk() {
2319        let s = str_array::<u32>(&["dummy", "abcde", "xyz", "qrstuv"]);
2320        let p = dict_array::<u32>(&["dummy", "c", "z", "tu"]);
2321        let out = contains_str_dict((&s, 1, 3), (&p, 1, 3)).unwrap();
2322        assert_eq!(out.get(0), Some(true));
2323        assert_eq!(out.get(1), Some(true));
2324        assert_eq!(out.get(2), Some(true));
2325    }
2326
2327    #[test]
2328    fn test_contains_dict_dict() {
2329        let s = dict_array::<u32>(&["cdef", "foo", "bar"]);
2330        let p = dict_array::<u32>(&["cd", "oo", "baz"]);
2331        let out = contains_dict_dict((&s, 0, s.len()), (&p, 0, p.len())).unwrap();
2332        assert_eq!(out.get(0), Some(true));
2333        assert_eq!(out.get(1), Some(true));
2334        assert_eq!(out.get(2), Some(false));
2335    }
2336
2337    #[test]
2338    fn test_contains_dict_dict_chunk() {
2339        let s = dict_array::<u32>(&["dummy", "cdef", "foo", "bar"]);
2340        let p = dict_array::<u32>(&["dummy", "cd", "oo", "baz"]);
2341        let out = contains_dict_dict((&s, 1, 3), (&p, 1, 3)).unwrap();
2342        assert_eq!(out.get(0), Some(true));
2343        assert_eq!(out.get(1), Some(true));
2344        assert_eq!(out.get(2), Some(false));
2345    }
2346
2347    #[test]
2348    fn test_contains_dict_str() {
2349        let s = dict_array::<u32>(&["hello", "world"]);
2350        let p = str_array::<u32>(&["he", "o"]);
2351        let out = contains_dict_str((&s, 0, s.len()), (&p, 0, p.len())).unwrap();
2352        assert_eq!(out.get(0), Some(true));
2353        assert_eq!(out.get(1), Some(true));
2354    }
2355
2356    #[test]
2357    fn test_contains_dict_str_chunk() {
2358        let s = dict_array::<u32>(&["dummy", "hello", "world"]);
2359        let p = str_array::<u32>(&["dummy", "he", "o"]);
2360        let out = contains_dict_str((&s, 1, 2), (&p, 1, 2)).unwrap();
2361        assert_eq!(out.get(0), Some(true));
2362        assert_eq!(out.get(1), Some(true));
2363    }
2364
2365    #[test]
2366    fn test_starts_with_str_dict() {
2367        let s = str_array::<u32>(&["abcdef", "foobar", "quux"]);
2368        let p = dict_array::<u32>(&["ab", "foo", "qu"]);
2369        let out = starts_with_str_dict((&s, 0, s.len()), (&p, 0, p.len())).unwrap();
2370        assert_eq!(out.get(0), Some(true));
2371        assert_eq!(out.get(1), Some(true));
2372        assert_eq!(out.get(2), Some(true));
2373    }
2374
2375    #[test]
2376    fn test_starts_with_str_dict_chunk() {
2377        let s = str_array::<u32>(&["dummy", "abcdef", "foobar", "quux"]);
2378        let p = dict_array::<u32>(&["dummy", "ab", "foo", "qu"]);
2379        let out = starts_with_str_dict((&s, 1, 3), (&p, 1, 3)).unwrap();
2380        assert_eq!(out.get(0), Some(true));
2381        assert_eq!(out.get(1), Some(true));
2382        assert_eq!(out.get(2), Some(true));
2383    }
2384
2385    #[test]
2386    fn test_starts_with_dict_dict() {
2387        let s = dict_array::<u32>(&["qwerty", "banana"]);
2388        let p = dict_array::<u32>(&["qw", "ban"]);
2389        let out = starts_with_dict_dict((&s, 0, s.len()), (&p, 0, p.len())).unwrap();
2390        assert_eq!(out.get(0), Some(true));
2391        assert_eq!(out.get(1), Some(true));
2392    }
2393
2394    #[test]
2395    fn test_starts_with_dict_dict_chunk() {
2396        let s = dict_array::<u32>(&["dummy", "qwerty", "banana"]);
2397        let p = dict_array::<u32>(&["dummy", "qw", "ban"]);
2398        let out = starts_with_dict_dict((&s, 1, 2), (&p, 1, 2)).unwrap();
2399        assert_eq!(out.get(0), Some(true));
2400        assert_eq!(out.get(1), Some(true));
2401    }
2402
2403    #[test]
2404    fn test_ends_with_str_dict() {
2405        let s = str_array::<u32>(&["poem", "dome", "gnome"]);
2406        let p = dict_array::<u32>(&["em", "me", "ome"]);
2407        let out = ends_with_str_dict((&s, 0, s.len()), (&p, 0, p.len())).unwrap();
2408        assert_eq!(out.get(0), Some(true));
2409        assert_eq!(out.get(1), Some(true));
2410        assert_eq!(out.get(2), Some(true));
2411    }
2412
2413    #[test]
2414    fn test_ends_with_str_dict_chunk() {
2415        let s = str_array::<u32>(&["dummy", "poem", "dome", "gnome"]);
2416        let p = dict_array::<u32>(&["dummy", "em", "me", "ome"]);
2417        let out = ends_with_str_dict((&s, 1, 3), (&p, 1, 3)).unwrap();
2418        assert_eq!(out.get(0), Some(true));
2419        assert_eq!(out.get(1), Some(true));
2420        assert_eq!(out.get(2), Some(true));
2421    }
2422
2423    #[test]
2424    fn test_ends_with_dict_dict() {
2425        let s = dict_array::<u32>(&["tablet", "let", "bet"]);
2426        let p = dict_array::<u32>(&["let", "et", "xyz"]);
2427        let out = ends_with_dict_dict((&s, 0, s.len()), (&p, 0, p.len())).unwrap();
2428        assert_eq!(out.get(0), Some(true));
2429        assert_eq!(out.get(1), Some(true));
2430        assert_eq!(out.get(2), Some(false));
2431    }
2432
2433    #[test]
2434    fn test_ends_with_dict_dict_chunk() {
2435        let s = dict_array::<u32>(&["dummy", "tablet", "let", "bet"]);
2436        let p = dict_array::<u32>(&["dummy", "let", "et", "xyz"]);
2437        let out = ends_with_dict_dict((&s, 1, 3), (&p, 1, 3)).unwrap();
2438        assert_eq!(out.get(0), Some(true));
2439        assert_eq!(out.get(1), Some(true));
2440        assert_eq!(out.get(2), Some(false));
2441    }
2442
2443    // --- len_str, len_dict
2444
2445    #[test]
2446    fn test_len_str() {
2447        let arr = str_array::<u32>(&["", "a", "abc", "bar"]);
2448        let out = len_str((&arr, 0, arr.len())).unwrap();
2449        assert_eq!(&out.data[..], &[0, 1, 3, 3]);
2450    }
2451
2452    #[test]
2453    fn test_len_str_chunk() {
2454        let arr = str_array::<u32>(&["zzz", "", "a", "abc", "bar"]);
2455        let out = len_str((&arr, 1, 4)).unwrap(); // ["", "a", "abc", "bar"]
2456        assert_eq!(&out.data[..], &[0, 1, 3, 3]);
2457    }
2458
2459    #[test]
2460    fn test_len_dict() {
2461        let arr = dict_array::<u32>(&["", "one", "seven"]);
2462        let out = len_dict((&arr, 0, arr.len())).unwrap();
2463        assert_eq!(&out.data[..], &[0, 3, 5]);
2464    }
2465
2466    #[test]
2467    fn test_len_dict_chunk() {
2468        let arr = dict_array::<u32>(&["q", "", "one", "seven"]);
2469        let out = len_dict((&arr, 1, 3)).unwrap(); // ["", "one", "seven"]
2470        assert_eq!(&out.data[..], &[0, 3, 5]);
2471    }
2472
2473    #[test]
2474    fn test_contains_empty_pattern() {
2475        let s = str_array::<u32>(&["foo", "bar"]);
2476        let p = str_array::<u32>(&["", ""]);
2477        let out = contains_str_str((&s, 0, s.len()), (&p, 0, p.len()));
2478        // always false
2479        assert_eq!(out.get(0), Some(false));
2480        assert_eq!(out.get(1), Some(false));
2481        assert!(out.null_mask.as_ref().is_none());
2482    }
2483
2484    #[test]
2485    fn test_contains_empty_pattern_chunk() {
2486        let s = str_array::<u32>(&["z", "foo", "bar"]);
2487        let p = str_array::<u32>(&["z", "", ""]);
2488        let out = contains_str_str((&s, 1, 2), (&p, 1, 2));
2489        assert_eq!(out.get(0), Some(false));
2490        assert_eq!(out.get(1), Some(false));
2491        assert!(out.null_mask.as_ref().is_none());
2492    }
2493
2494    #[test]
2495    fn test_contains_str_str_nulls_on_pattern() {
2496        let mut s = str_array::<u32>(&["abc", "def"]);
2497        s.null_mask = Some(bm(&[true, true]));
2498        let mut p = str_array::<u32>(&["b", "e"]);
2499        p.null_mask = Some(bm(&[true, false])); // second pattern is null
2500        let out = contains_str_str((&s, 0, s.len()), (&p, 0, p.len()));
2501        assert_eq!(out.get(0), Some(true));
2502        assert_eq!(out.get(1), Some(false));
2503    }
2504
2505    #[test]
2506    fn test_contains_str_str_nulls_on_pattern_chunk() {
2507        let mut s = str_array::<u32>(&["X", "abc", "def"]);
2508        s.null_mask = Some(bm(&[true, true, true]));
2509        let mut p = str_array::<u32>(&["X", "b", "e"]);
2510        p.null_mask = Some(bm(&[true, true, false])); // last pattern is null
2511        let out = contains_str_str((&s, 1, 2), (&p, 1, 2));
2512        assert_eq!(out.get(0), Some(true));
2513        assert_eq!(out.get(1), Some(false));
2514    }
2515
2516    #[cfg(feature = "regex")]
2517    #[test]
2518    fn test_regex_invalid_pattern_returns_err() {
2519        let s = str_array::<u32>(&["abc"]);
2520        let p = str_array::<u32>(&["["]);
2521        let err = regex_str_str((&s, 0, s.len()), (&p, 0, p.len())).unwrap_err();
2522        match err {
2523            KernelError::InvalidArguments(_) => {}
2524            _ => panic!("expected InvalidArguments"),
2525        }
2526    }
2527
2528    #[cfg(feature = "regex")]
2529    #[test]
2530    fn test_regex_invalid_pattern_returns_err_chunk() {
2531        let s = str_array::<u32>(&["foo", "abc"]);
2532        let p = str_array::<u32>(&["bar", "["]);
2533        let err = regex_str_str((&s, 1, 1), (&p, 1, 1)).unwrap_err();
2534        match err {
2535            KernelError::InvalidArguments(_) => {}
2536            _ => panic!("expected InvalidArguments"),
2537        }
2538    }
2539
2540    #[cfg(feature = "regex")]
2541    #[test]
2542    fn test_regex_empty_pattern_always_false() {
2543        let s = str_array::<u32>(&["abc", "def"]);
2544        let p = str_array::<u32>(&["", ""]);
2545        let out = regex_str_str((&s, 0, s.len()), (&p, 0, p.len())).unwrap();
2546        assert_eq!(out.get(0), Some(false));
2547        assert_eq!(out.get(1), Some(false));
2548        assert!(out.null_mask.unwrap().all_set());
2549    }
2550
2551    #[cfg(feature = "regex")]
2552    #[test]
2553    fn test_regex_empty_pattern_always_false_chunk() {
2554        let s = str_array::<u32>(&["z", "abc", "def"]);
2555        let p = str_array::<u32>(&["z", "", ""]);
2556        let out = regex_str_str((&s, 1, 2), (&p, 1, 2)).unwrap();
2557        assert_eq!(out.get(0), Some(false));
2558        assert_eq!(out.get(1), Some(false));
2559        assert!(out.null_mask.unwrap().all_set());
2560    }
2561
2562    #[test]
2563    fn test_len_str_with_nulls() {
2564        let mut arr = str_array::<u32>(&["foo", "", "bar"]);
2565        arr.null_mask = Some(bm(&[true, false, true]));
2566        let len_arr = len_str((&arr, 0, arr.len())).unwrap();
2567        assert_eq!(len_arr.data.as_slice(), &[3, 0, 3]);
2568        assert_eq!(
2569            len_arr.null_mask.unwrap().as_slice(),
2570            bm(&[true, false, true]).as_slice()
2571        );
2572    }
2573
2574    #[test]
2575    fn test_len_str_with_nulls_chunk() {
2576        let mut arr = str_array::<u32>(&["x", "foo", "", "bar"]);
2577        arr.null_mask = Some(bm(&[true, true, false, true]));
2578        let len_arr = len_str((&arr, 1, 3)).unwrap();
2579        assert_eq!(len_arr.data.as_slice(), &[3, 0, 3]);
2580        assert_eq!(
2581            len_arr.null_mask.unwrap().as_slice(),
2582            bm(&[true, false, true]).as_slice()
2583        );
2584    }
2585
2586    #[test]
2587    fn test_len_dict_with_nulls() {
2588        let mut arr = dict_array::<u32>(&["x", "yy", "zzz"]);
2589        arr.null_mask = Some(bm(&[false, true, true]));
2590        let len_arr = len_dict((&arr, 0, arr.len())).unwrap();
2591        assert_eq!(len_arr.data.as_slice(), &[0, 2, 3]);
2592        assert_eq!(
2593            len_arr.null_mask.unwrap().as_slice(),
2594            bm(&[false, true, true]).as_slice()
2595        );
2596    }
2597
2598    #[test]
2599    fn test_len_dict_with_nulls_chunk() {
2600        let mut arr = dict_array::<u32>(&["z", "x", "yy", "zzz"]);
2601        arr.null_mask = Some(bm(&[true, false, true, true]));
2602        let len_arr = len_dict((&arr, 1, 3)).unwrap();
2603        assert_eq!(len_arr.data.as_slice(), &[0, 2, 3]);
2604        assert_eq!(
2605            len_arr.null_mask.unwrap().as_slice(),
2606            bm(&[false, true, true]).as_slice()
2607        );
2608    }
2609
2610    fn bitmask_from_vec(v: &[bool]) -> Bitmask {
2611        let mut bm = Bitmask::with_capacity(v.len());
2612        for (i, &b) in v.iter().enumerate() {
2613            bm.set(i, b);
2614        }
2615        bm
2616    }
2617
2618    #[test]
2619    fn test_min_string_array_all_valid() {
2620        let arr = StringArray::<u32>::from_slice(&["zulu", "alpha", "echo", "bravo"]);
2621        let view = (&arr, 0, arr.len());
2622        let result = min_string_array::<u32>(view);
2623        assert_eq!(result, Some("alpha".to_string()));
2624    }
2625
2626    #[test]
2627    fn test_min_string_array_with_nulls() {
2628        let mut arr = StringArray::<u32>::from_slice(&["zulu", "alpha", "echo", "bravo"]);
2629        arr.null_mask = Some(bitmask_from_vec(&[false, true, true, true]));
2630        let view = (&arr, 0, arr.len());
2631        let result = min_string_array::<u32>(view);
2632        assert_eq!(result, Some("alpha".to_string()));
2633    }
2634
2635    #[test]
2636    fn test_min_string_array_all_null() {
2637        let arr = StringArray::<u32>::from_slice(&["zulu", "alpha", "echo", "bravo"]);
2638        let mut null_mask = Bitmask::with_capacity(arr.len());
2639        for i in 0..arr.len() {
2640            null_mask.set(i, false);
2641        }
2642        let arr = StringArray::<u32> {
2643            null_mask: Some(null_mask),
2644            ..arr
2645        };
2646        let view = (&arr, 0, arr.len());
2647        let result = min_string_array::<u32>(view);
2648        assert_eq!(result, None);
2649    }
2650
2651    #[test]
2652    fn test_max_string_array_all_valid() {
2653        let arr = StringArray::<u32>::from_slice(&["zulu", "alpha", "echo", "bravo"]);
2654        let view = (&arr, 0, arr.len());
2655        let result = max_string_array::<u32>(view);
2656        assert_eq!(result, Some("zulu".to_string()));
2657    }
2658
2659    #[test]
2660    fn test_max_string_array_with_nulls() {
2661        let mut arr = StringArray::<u32>::from_slice(&["zulu", "alpha", "echo", "bravo"]);
2662        arr.null_mask = Some(bitmask_from_vec(&[true, false, true, false]));
2663        let view = (&arr, 0, arr.len());
2664        let result = max_string_array::<u32>(view);
2665        assert_eq!(result, Some("zulu".to_string()));
2666    }
2667
2668    #[test]
2669    fn test_max_string_array_all_null() {
2670        let arr = StringArray::<u32>::from_slice(&["zulu", "alpha", "echo", "bravo"]);
2671        let mut null_mask = Bitmask::with_capacity(arr.len());
2672        for i in 0..arr.len() {
2673            null_mask.set(i, false);
2674        }
2675        let arr = StringArray::<u32> {
2676            null_mask: Some(null_mask),
2677            ..arr
2678        };
2679        let view = (&arr, 0, arr.len());
2680        let result = max_string_array::<u32>(view);
2681        assert_eq!(result, None);
2682    }
2683
2684    #[test]
2685    fn test_min_categorical_array() {
2686        let uniques = vec64![
2687            "dog".to_string(),
2688            "zebra".to_string(),
2689            "ant".to_string(),
2690            "bee".to_string()
2691        ];
2692        let indices = vec64![1u32, 0, 3, 2]; // "zebra", "dog", "bee", "ant"
2693        let cat = CategoricalArray {
2694            data: indices.clone().into(),
2695            unique_values: uniques.clone().into(),
2696            null_mask: None,
2697        };
2698        let result = min_categorical_array((&cat, 0, indices.len()));
2699        assert_eq!(result, Some("ant".to_string()));
2700    }
2701
2702    #[test]
2703    fn test_max_categorical_array() {
2704        let uniques = vec64![
2705            "dog".to_string(),
2706            "zebra".to_string(),
2707            "ant".to_string(),
2708            "bee".to_string()
2709        ];
2710        let indices = vec64![2u32, 0, 1, 3]; // "ant", "dog", "zebra", "bee"
2711        let cat = CategoricalArray {
2712            data: indices.clone().into(),
2713            unique_values: uniques.clone().into(),
2714            null_mask: None,
2715        };
2716        let result = max_categorical_array((&cat, 0, indices.len()));
2717        assert_eq!(result, Some("zebra".to_string()));
2718    }
2719
2720    #[test]
2721    fn test_min_categorical_array_with_nulls() {
2722        let uniques = vec64!["dog".to_string(), "zebra".to_string(), "ant".to_string()];
2723        let indices = vec64![1u32, 2, 0];
2724        let mut null_mask = Bitmask::with_capacity(indices.len());
2725        null_mask.set(0, true);
2726        null_mask.set(1, false);
2727        null_mask.set(2, true);
2728        let cat = CategoricalArray {
2729            data: indices.clone().into(),
2730            unique_values: uniques.clone().into(),
2731            null_mask: Some(null_mask),
2732        };
2733        let result = min_categorical_array((&cat, 0, indices.len()));
2734        assert_eq!(result, Some("dog".to_string())); // Only positions 0 and 2 valid: "zebra", "dog" -> "dog" is smaller
2735    }
2736
2737    #[test]
2738    fn test_max_categorical_array_with_nulls() {
2739        let uniques = vec64!["dog".to_string(), "zebra".to_string(), "ant".to_string()];
2740        let indices = vec64![1u32, 2, 0];
2741        let mut null_mask = Bitmask::with_capacity(indices.len());
2742        null_mask.set(0, true);
2743        null_mask.set(1, false);
2744        null_mask.set(2, true);
2745        let cat = CategoricalArray {
2746            data: indices.clone().into(),
2747            unique_values: uniques.clone().into(),
2748            null_mask: Some(null_mask),
2749        };
2750        let result = max_categorical_array((&cat, 0, indices.len()));
2751        assert_eq!(result, Some("zebra".to_string())); // Only positions 0 and 2 valid: "zebra", "dog" -> "zebra" is larger
2752    }
2753
2754    // --- Unary transforms
2755
2756    #[test]
2757    fn test_to_uppercase_str() {
2758        let a = str_array::<u32>(&["hello", "World", "FOO"]);
2759        let out = to_uppercase_str((&a, 0, a.len())).unwrap();
2760        assert_eq!(out.get(0), Some("HELLO"));
2761        assert_eq!(out.get(1), Some("WORLD"));
2762        assert_eq!(out.get(2), Some("FOO"));
2763    }
2764
2765    #[test]
2766    fn test_to_uppercase_str_chunk() {
2767        let a = str_array::<u32>(&["skip", "hello", "world", "skip"]);
2768        let out = to_uppercase_str((&a, 1, 2)).unwrap();
2769        assert_eq!(out.get(0), Some("HELLO"));
2770        assert_eq!(out.get(1), Some("WORLD"));
2771    }
2772
2773    #[test]
2774    fn test_to_uppercase_dict() {
2775        let a = dict_array::<u32>(&["hello", "world"]);
2776        let out = to_uppercase_dict((&a, 0, a.data.len())).unwrap();
2777        assert_eq!(out.get_str(0), Some("HELLO"));
2778        assert_eq!(out.get_str(1), Some("WORLD"));
2779    }
2780
2781    #[test]
2782    fn test_to_lowercase_str() {
2783        let a = str_array::<u32>(&["HELLO", "World"]);
2784        let out = to_lowercase_str((&a, 0, a.len())).unwrap();
2785        assert_eq!(out.get(0), Some("hello"));
2786        assert_eq!(out.get(1), Some("world"));
2787    }
2788
2789    #[test]
2790    fn test_trim_str() {
2791        let a = str_array::<u32>(&["  hello  ", "world ", " foo"]);
2792        let out = trim_str((&a, 0, a.len())).unwrap();
2793        assert_eq!(out.get(0), Some("hello"));
2794        assert_eq!(out.get(1), Some("world"));
2795        assert_eq!(out.get(2), Some("foo"));
2796    }
2797
2798    #[test]
2799    fn test_ltrim_str() {
2800        let a = str_array::<u32>(&["  hello", "  world "]);
2801        let out = ltrim_str((&a, 0, a.len())).unwrap();
2802        assert_eq!(out.get(0), Some("hello"));
2803        assert_eq!(out.get(1), Some("world "));
2804    }
2805
2806    #[test]
2807    fn test_rtrim_str() {
2808        let a = str_array::<u32>(&["hello  ", " world "]);
2809        let out = rtrim_str((&a, 0, a.len())).unwrap();
2810        assert_eq!(out.get(0), Some("hello"));
2811        assert_eq!(out.get(1), Some(" world"));
2812    }
2813
2814    #[test]
2815    fn test_reverse_str() {
2816        let a = str_array::<u32>(&["abc", "hello"]);
2817        let out = reverse_str((&a, 0, a.len())).unwrap();
2818        assert_eq!(out.get(0), Some("cba"));
2819        assert_eq!(out.get(1), Some("olleh"));
2820    }
2821
2822    #[test]
2823    fn test_reverse_dict() {
2824        let a = dict_array::<u32>(&["abc", "xy"]);
2825        let out = reverse_dict((&a, 0, a.data.len())).unwrap();
2826        assert_eq!(out.get_str(0), Some("cba"));
2827        assert_eq!(out.get_str(1), Some("yx"));
2828    }
2829
2830    // --- Byte length
2831
2832    #[test]
2833    fn test_byte_length_str() {
2834        let a = str_array::<u32>(&["hi", "café"]);
2835        let out = byte_length_str((&a, 0, a.len())).unwrap();
2836        assert_eq!(out.data[0], 2u32); // "hi" = 2 bytes
2837        assert_eq!(out.data[1], 5u32); // "café" = 5 bytes (é is 2 bytes in UTF-8)
2838    }
2839
2840    #[test]
2841    fn test_byte_length_dict() {
2842        let a = dict_array::<u32>(&["hi", "café"]);
2843        let out = byte_length_dict((&a, 0, a.data.len())).unwrap();
2844        assert_eq!(out.data[0], 2u32);
2845        assert_eq!(out.data[1], 5u32);
2846    }
2847
2848    // --- Find and count
2849
2850    #[test]
2851    fn test_find_str() {
2852        let a = str_array::<u32>(&["hello world", "foo bar", "baz"]);
2853        let out = find_str((&a, 0, a.len()), "world").unwrap();
2854        assert_eq!(out.data[0], 6);
2855        assert_eq!(out.data[1], -1);
2856        assert_eq!(out.data[2], -1);
2857    }
2858
2859    #[test]
2860    fn test_find_dict() {
2861        let a = dict_array::<u32>(&["hello", "world"]);
2862        let out = find_dict((&a, 0, a.data.len()), "llo").unwrap();
2863        assert_eq!(out.data[0], 2);
2864        assert_eq!(out.data[1], -1);
2865    }
2866
2867    #[test]
2868    fn test_count_match_str() {
2869        let a = str_array::<u32>(&["abcabc", "abc", "xyz"]);
2870        let out = count_match_str((&a, 0, a.len()), "abc").unwrap();
2871        assert_eq!(out.data[0], 2);
2872        assert_eq!(out.data[1], 1);
2873        assert_eq!(out.data[2], 0);
2874    }
2875
2876    #[test]
2877    fn test_count_match_dict() {
2878        let a = dict_array::<u32>(&["aaa", "a"]);
2879        let out = count_match_dict((&a, 0, a.data.len()), "a").unwrap();
2880        assert_eq!(out.data[0], 3);
2881        assert_eq!(out.data[1], 1);
2882    }
2883
2884    // --- Substring
2885
2886    #[test]
2887    fn test_substring_str() {
2888        let a = str_array::<u32>(&["hello world", "foo"]);
2889        let out = substring_str((&a, 0, a.len()), 6, None).unwrap();
2890        assert_eq!(out.get(0), Some("world"));
2891        assert_eq!(out.get(1), Some(""));
2892    }
2893
2894    #[test]
2895    fn test_substring_str_with_len() {
2896        let a = str_array::<u32>(&["hello world"]);
2897        let out = substring_str((&a, 0, a.len()), 0, Some(5)).unwrap();
2898        assert_eq!(out.get(0), Some("hello"));
2899    }
2900
2901    #[test]
2902    fn test_substring_dict() {
2903        let a = dict_array::<u32>(&["hello", "world"]);
2904        let out = substring_dict((&a, 0, a.data.len()), 1, Some(3)).unwrap();
2905        assert_eq!(out.get_str(0), Some("ell"));
2906        assert_eq!(out.get_str(1), Some("orl"));
2907    }
2908
2909    // --- Replace
2910
2911    #[test]
2912    fn test_replace_str() {
2913        let a = str_array::<u32>(&["hello world", "foo bar foo"]);
2914        let out = replace_str((&a, 0, a.len()), "foo", "baz").unwrap();
2915        assert_eq!(out.get(0), Some("hello world"));
2916        assert_eq!(out.get(1), Some("baz bar baz"));
2917    }
2918
2919    #[test]
2920    fn test_replace_dict() {
2921        let a = dict_array::<u32>(&["aXb", "cXd"]);
2922        let out = replace_dict((&a, 0, a.data.len()), "X", "Y").unwrap();
2923        assert_eq!(out.get_str(0), Some("aYb"));
2924        assert_eq!(out.get_str(1), Some("cYd"));
2925    }
2926
2927    // --- Repeat
2928
2929    #[test]
2930    fn test_repeat_str() {
2931        let a = str_array::<u32>(&["ab", "x"]);
2932        let out = repeat_str((&a, 0, a.len()), 3).unwrap();
2933        assert_eq!(out.get(0), Some("ababab"));
2934        assert_eq!(out.get(1), Some("xxx"));
2935    }
2936
2937    #[test]
2938    fn test_repeat_dict() {
2939        let a = dict_array::<u32>(&["ha"]);
2940        let out = repeat_dict((&a, 0, a.data.len()), 2).unwrap();
2941        assert_eq!(out.get_str(0), Some("haha"));
2942    }
2943
2944    // --- Pad
2945
2946    #[test]
2947    fn test_pad_str_left() {
2948        let a = str_array::<u32>(&["hi", "hello"]);
2949        let out = pad_str((&a, 0, a.len()), 5, ' ', PadSide::Left).unwrap();
2950        assert_eq!(out.get(0), Some("   hi"));
2951        assert_eq!(out.get(1), Some("hello")); // already 5 chars
2952    }
2953
2954    #[test]
2955    fn test_pad_str_right() {
2956        let a = str_array::<u32>(&["hi"]);
2957        let out = pad_str((&a, 0, a.len()), 5, '-', PadSide::Right).unwrap();
2958        assert_eq!(out.get(0), Some("hi---"));
2959    }
2960
2961    #[test]
2962    fn test_pad_str_both() {
2963        let a = str_array::<u32>(&["hi"]);
2964        let out = pad_str((&a, 0, a.len()), 6, '*', PadSide::Both).unwrap();
2965        assert_eq!(out.get(0), Some("**hi**"));
2966    }
2967
2968    #[test]
2969    fn test_pad_dict() {
2970        let a = dict_array::<u32>(&["x"]);
2971        let out = pad_dict((&a, 0, a.data.len()), 3, '0', PadSide::Left).unwrap();
2972        assert_eq!(out.get_str(0), Some("00x"));
2973    }
2974
2975    // --- Join
2976
2977    #[test]
2978    fn test_join_str() {
2979        let a = str_array::<u32>(&["a", "b", "c"]);
2980        assert_eq!(join_str((&a, 0, a.len()), ", "), Some("a, b, c".to_owned()));
2981    }
2982
2983    #[test]
2984    fn test_join_str_chunk() {
2985        let a = str_array::<u32>(&["skip", "a", "b", "skip"]);
2986        assert_eq!(join_str((&a, 1, 2), "-"), Some("a-b".to_owned()));
2987    }
2988
2989    #[test]
2990    fn test_join_dict() {
2991        let a = dict_array::<u32>(&["x", "y"]);
2992        assert_eq!(join_dict((&a, 0, a.data.len()), "+"), Some("x+y".to_owned()));
2993    }
2994
2995    // --- Regex replace (feature-gated)
2996
2997    #[cfg(feature = "regex")]
2998    #[test]
2999    fn test_regex_replace_str() {
3000        let a = str_array::<u32>(&["abc123def456", "no digits"]);
3001        let out = regex_replace_str((&a, 0, a.len()), r"\d+", "N").unwrap();
3002        assert_eq!(out.get(0), Some("abcNdefN"));
3003        assert_eq!(out.get(1), Some("no digits"));
3004    }
3005
3006    #[cfg(feature = "regex")]
3007    #[test]
3008    fn test_regex_replace_dict() {
3009        let a = dict_array::<u32>(&["foo123"]);
3010        let out = regex_replace_dict((&a, 0, a.data.len()), r"\d+", "").unwrap();
3011        assert_eq!(out.get_str(0), Some("foo"));
3012    }
3013
3014    // --- Null handling tests
3015
3016    #[test]
3017    fn test_to_uppercase_str_with_nulls() {
3018        let mut a = str_array::<u32>(&["hello", "skip", "world"]);
3019        a.null_mask = Some(bm(&[true, false, true]));
3020        let out = to_uppercase_str((&a, 0, a.len())).unwrap();
3021        assert_eq!(out.get(0), Some("HELLO"));
3022        assert!(!out.null_mask.as_ref().unwrap().get(1));
3023        assert_eq!(out.get(2), Some("WORLD"));
3024    }
3025
3026    #[test]
3027    fn test_find_str_with_nulls() {
3028        let mut a = str_array::<u32>(&["hello", "skip"]);
3029        a.null_mask = Some(bm(&[true, false]));
3030        let out = find_str((&a, 0, a.len()), "ello").unwrap();
3031        assert_eq!(out.data[0], 1);
3032        assert_eq!(out.data[1], 0);
3033        assert!(!out.null_mask.as_ref().unwrap().get(1));
3034    }
3035
3036    #[test]
3037    fn test_join_str_with_nulls() {
3038        let mut a = str_array::<u32>(&["a", "SKIP", "b"]);
3039        a.null_mask = Some(bm(&[true, false, true]));
3040        assert_eq!(join_str((&a, 0, a.len()), ","), Some("a,b".to_owned()));
3041    }
3042}