Skip to main content

simd_kernels/kernels/arithmetic/
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 Arithmetic Module** - *String Operations with Numeric Interactions*
16//!
17//! String-specific arithmetic operations including string multiplication, concatenation, and manipulation.
18//! This unifies strings into a typical numeric-compatible workloads. E.g., "hello" + "there" = "hellothere".
19//! These are opt-in via the "str_arithmetic" feature.
20//!
21//! ## Overview
22//! - **String multiplication**: Repeat strings by numeric factors with configurable limits
23//! - **String-numeric conversions**: Format numbers into string representations  
24//! - **Categorical operations**: Efficient string deduplication and categorical array generation
25//! - **Null-aware processing**: Full Arrow-compatible null propagation
26//!
27//! ## Features
28//! - **Memory efficiency**: Uses string interning and categorical encoding to reduce allocation overhead
29//! - **Safety limits**: Configurable multiplication limits prevent excessive memory usage
30//! - **Optional dependencies**: String-numeric arithmetic gated behind `str_arithmetic` feature
31
32include!(concat!(env!("OUT_DIR"), "/simd_lanes.rs"));
33
34#[cfg(feature = "fast_hash")]
35use ahash::AHashMap;
36#[cfg(feature = "str_arithmetic")]
37use core::ptr::copy_nonoverlapping;
38#[cfg(not(feature = "fast_hash"))]
39use std::collections::HashMap;
40
41use crate::enums::error::{KernelError, log_length_mismatch};
42use crate::kernels::bitmask::merge_bitmasks_to_new;
43use crate::structs::variants::categorical::CategoricalArray;
44#[cfg(feature = "str_arithmetic")]
45use memchr::memmem::Finder;
46
47use crate::structs::variants::string::StringArray;
48use crate::traits::type_unions::Integer;
49use crate::{Bitmask, Vec64};
50#[cfg(feature = "str_arithmetic")]
51use num_traits::ToPrimitive;
52
53use crate::enums::operators::ArithmeticOperator::{self};
54#[cfg(feature = "str_arithmetic")]
55use crate::utils::{
56    confirm_mask_capacity, estimate_categorical_cardinality, estimate_string_cardinality,
57};
58use crate::{CategoricalAVT, StringAVTExt};
59#[cfg(feature = "str_arithmetic")]
60use crate::{MaskedArray, StringAVT};
61
62#[cfg(feature = "str_arithmetic")]
63use ryu::Float;
64#[cfg(feature = "str_arithmetic")]
65use std::mem::MaybeUninit;
66
67/// Maximum allowed repetitions for string multiplication operations.
68///
69/// Prevents excessive memory allocation when repeating strings through multiplication.
70/// Operations exceeding this limit will return an error rather than allocating unbounded memory.
71pub const STRING_MULTIPLICATION_LIMIT: usize = 1_000_000;
72
73/// Threshold for dictionary size checks in categorical array operations.
74///
75/// Controls when the `cmp_dict_in` function in `kernels/logical.rs` switches from dictionary
76/// lookups to direct string comparisons. Arrays with fewer unique values than this threshold
77/// use optimised dictionary-based comparisons.
78pub const MAX_DICT_CHECK: usize = 256;
79
80/// String-numeric arithmetic operation dispatcher.
81/// Supports string multiplication (repeat string N times) with safety limits.
82/// Other operations pass through unchanged. Handles null propagation correctly.
83///
84/// # Type Parameters
85/// - `T`: Offset type for the input array (e.g., `u32`, `u64`)
86/// - `N`: Numeric type convertible to `usize`
87/// - `O`: Offset type for the output array
88pub fn apply_str_num<T, N, O>(
89    lhs: StringAVTExt<T>,
90    rhs: &[N],
91    op: ArithmeticOperator,
92) -> Result<StringArray<O>, KernelError>
93where
94    T: Integer,
95    N: num_traits::ToPrimitive + Copy,
96    O: Integer + num_traits::NumCast,
97{
98    let (array, offset, logical_len, physical_bytes_len) = lhs;
99
100    if logical_len != rhs.len() {
101        return Err(KernelError::LengthMismatch(log_length_mismatch(
102            "apply_str_num".to_string(),
103            logical_len,
104            rhs.len(),
105        )));
106    }
107
108    // Preallocate offsets and estimate capacity
109    let lhs_mask = array.null_mask.as_ref();
110    let mut out_mask = lhs_mask.map(|_| crate::Bitmask::new_set_all(logical_len, true));
111
112    let mut offsets = Vec64::<O>::with_capacity(logical_len + 1);
113    offsets.push(O::zero()); // initial offset 0
114
115    let estimated_bytes = physical_bytes_len.min(STRING_MULTIPLICATION_LIMIT * logical_len);
116    let mut data = Vec64::with_capacity(estimated_bytes);
117
118    for (out_idx, i) in (offset..offset + logical_len).enumerate() {
119        let valid = lhs_mask.map_or(true, |mask| unsafe { mask.get_unchecked(i) });
120
121        if let Some(mask) = &mut out_mask {
122            unsafe { mask.set_unchecked(out_idx, valid) };
123        }
124
125        if valid {
126            let s = unsafe { array.get_str_unchecked(i) };
127            let n = rhs[out_idx].to_usize().unwrap_or(0);
128
129            match op {
130                ArithmeticOperator::Multiply => {
131                    let count = n.min(STRING_MULTIPLICATION_LIMIT);
132                    for _ in 0..count {
133                        data.extend_from_slice(s.as_bytes());
134                    }
135                }
136                _ => {
137                    data.extend_from_slice(s.as_bytes());
138                }
139            }
140        }
141
142        // Push offset regardless of validity to keep offsets aligned
143        // This ensures we can still slice [a..b] intuitively.
144        let new_offset = O::from(data.len()).expect("offset conversion overflow");
145        offsets.push(new_offset);
146    }
147
148    Ok(StringArray {
149        offsets: offsets.into(),
150        data: data.into(),
151        null_mask: out_mask,
152    })
153}
154
155/// Applies an element-wise binary operation between a `StringArray<T>` and a slice of floating-point values,
156/// producing a new `StringArray<T>`. Each operation is performed by interpreting the float as a finite
157/// decimal string representation (`f64` formatted with `ryu`), and applying string transformations accordingly.
158///
159/// Supported operations:
160///
161/// - `Add`: Appends the stringified float to the string from `lhs`.
162/// - `Subtract`: Removes the first occurrence of the stringified float from `lhs`, if present.
163///               If not found, `lhs` is returned unchanged.
164/// - `Multiply`: Repeats the `lhs` string `N` times, where `N = abs(round(rhs)) % (STRING_MULTIPLICATION_LIMIT + 1)`.
165/// - `Divide`: Splits the `lhs` string by the stringified float, and joins the segments using `'|'`.
166///             If the float pattern is not found, the original string is returned unchanged.
167///
168/// Null handling:
169/// - If the `lhs` value is null at an index, the result is null at that index.
170/// - The float operand cannot be null; the caller must guarantee its presence.
171///
172/// Output:
173/// - A new `StringArray<T>` with the same length as `lhs`.
174/// - The underlying byte storage is preallocated based on a prepass analysis of required capacity.
175///
176/// Errors:
177/// - Returns `KernelError::LengthMismatch` if `lhs` and `rhs` lengths differ.
178/// - Returns `KernelError::UnsupportedType` if the operator is not one of Add, Subtract, Multiply, Divide.
179///
180/// # Features
181/// This function is only available when the `str_arithmetic` feature is enabled.
182///
183/// This kernel is optional as it pulls in external dependencies, and fits more niche use cases. It's a
184/// good fit for flex-typing scenarios where users are concatenating strings and numbers, or working
185/// with semi-structured web content, string formatting pipelines etc.
186///
187/// # Safety
188/// - Uses unchecked access and raw pointer copies for performance. Invariants around memory safety must hold.
189/// - Assumes `rhs` contains only finite floating-point values.
190#[cfg(feature = "str_arithmetic")]
191pub fn apply_str_float<T, F>(
192    lhs: StringAVT<T>,
193    rhs: &[F],
194    op: ArithmeticOperator,
195) -> Result<StringArray<T>, KernelError>
196where
197    T: Integer,
198    F: Into<f64> + Copy + ryu::Float,
199{
200    // Destructure the string slice: array, offset, and logical length
201    let (array, offset, logical_len) = lhs;
202
203    // Validate inputs
204
205    use std::mem::MaybeUninit;
206    if rhs.len() != logical_len {
207        return Err(KernelError::LengthMismatch(log_length_mismatch(
208            "apply_str_float".into(),
209            logical_len,
210            rhs.len(),
211        )));
212    }
213    let lhs_mask = &array.null_mask;
214    let _ = confirm_mask_capacity(array.len(), lhs_mask.as_ref())?;
215
216    // 1st pass: size accounting
217    let mut total_bytes = 0usize;
218    let mut fmt_buf: [MaybeUninit<u8>; 24] = unsafe { MaybeUninit::uninit().assume_init() };
219
220    for (out_idx, i) in (offset..offset + logical_len).enumerate() {
221        if !lhs_mask
222            .as_ref()
223            .map_or(true, |m| unsafe { m.get_unchecked(i) })
224        {
225            continue;
226        }
227
228        // src_len = string length at physical index
229        let src_len = {
230            let a = array.offsets[i].to_usize();
231            let b = array.offsets[i + 1].to_usize();
232            b - a
233        };
234        let n_s = format_finite(&mut fmt_buf, rhs[out_idx]);
235        total_bytes += match op {
236            ArithmeticOperator::Add => src_len + n_s.len(),
237            ArithmeticOperator::Subtract => src_len,
238            ArithmeticOperator::Multiply => {
239                let times =
240                    rhs[out_idx].into().round().abs() as usize % (STRING_MULTIPLICATION_LIMIT + 1);
241                src_len * times
242            }
243            ArithmeticOperator::Divide => {
244                let pat_len = n_s.len();
245                let splits = (src_len + pat_len).saturating_sub(1) / pat_len;
246                src_len + splits
247            }
248            _ => {
249                return Err(KernelError::UnsupportedType(format!(
250                    "Unsupported {:?}",
251                    op
252                )));
253            }
254        };
255    }
256
257    // allocate outputs once
258    let mut offsets = Vec64::<T>::with_capacity(logical_len + 1);
259
260    // 2nd pass: copy / build strings
261    let mut data = Vec64::<u8>::with_capacity(total_bytes);
262    unsafe {
263        offsets.set_len(logical_len + 1);
264        data.set_len(total_bytes);
265    }
266
267    let mut out_mask = lhs_mask
268        .as_ref()
269        .map(|_| Bitmask::new_set_all(logical_len, false));
270
271    let mut cursor = 0usize;
272    offsets[0] = T::zero();
273
274    for (out_idx, i) in (offset..offset + logical_len).enumerate() {
275        let valid = lhs_mask
276            .as_ref()
277            .map_or(true, |m| unsafe { m.get_unchecked(i) });
278        if let Some(mask) = &mut out_mask {
279            unsafe { mask.set_unchecked(out_idx, valid) };
280        }
281
282        if !valid {
283            offsets[out_idx + 1] = T::from(cursor).unwrap();
284            continue;
285        }
286
287        let start = array.offsets[i].to_usize();
288        let end = array.offsets[i + 1].to_usize();
289        let src = &array.data[start..end];
290        let n_s = format_finite(&mut fmt_buf, rhs[out_idx]);
291        let pat = n_s.as_bytes();
292
293        let mut write = |bytes: &[u8]| unsafe {
294            copy_nonoverlapping(bytes.as_ptr(), data.as_mut_ptr().add(cursor), bytes.len());
295            cursor += bytes.len();
296        };
297
298        match op {
299            ArithmeticOperator::Add => {
300                write(src);
301                write(pat);
302            }
303            ArithmeticOperator::Subtract => {
304                if let Some(idx) = Finder::new(pat).find(src) {
305                    write(&src[..idx]);
306                    write(&src[(idx + pat.len())..]);
307                } else {
308                    write(src);
309                }
310            }
311            ArithmeticOperator::Multiply => {
312                let times =
313                    rhs[out_idx].into().round().abs() as usize % (STRING_MULTIPLICATION_LIMIT + 1);
314                for _ in 0..times {
315                    write(src);
316                }
317            }
318            ArithmeticOperator::Divide => {
319                let finder = Finder::new(pat);
320                let mut start_pos = 0;
321                let mut first = true;
322                while let Some(idx) = finder.find(&src[start_pos..]) {
323                    if !first {
324                        data[cursor] = b'|';
325                        cursor += 1;
326                    }
327                    let rel_idx = idx;
328                    let segment = &src[start_pos..start_pos + rel_idx];
329                    unsafe {
330                        copy_nonoverlapping(
331                            segment.as_ptr(),
332                            data.as_mut_ptr().add(cursor),
333                            segment.len(),
334                        );
335                        cursor += segment.len();
336                    }
337                    start_pos += rel_idx + pat.len();
338                    first = false;
339                }
340                if !first {
341                    data[cursor] = b'|';
342                    cursor += 1;
343                }
344                let tail = &src[start_pos..];
345                unsafe {
346                    copy_nonoverlapping(tail.as_ptr(), data.as_mut_ptr().add(cursor), tail.len());
347                    cursor += tail.len();
348                }
349            }
350            _ => unreachable!(),
351        }
352        offsets[out_idx + 1] = T::from(cursor).unwrap();
353    }
354
355    // build & return
356
357    Ok(StringArray {
358        offsets: offsets.into(),
359        data: data.into(),
360        null_mask: out_mask,
361    })
362}
363
364/// String interning helper for categorical array generation with fast hashing.
365/// Deduplicates strings and assigns numeric codes for memory efficiency.
366#[cfg(feature = "fast_hash")]
367#[inline(always)]
368fn intern<T: Integer>(s: &str, dict: &mut AHashMap<String, T>, uniq: &mut Vec64<String>) -> T {
369    if let Some(&code) = dict.get(s) {
370        code
371    } else {
372        let idx = T::from_usize(uniq.len());
373        uniq.push(s.to_owned());
374        dict.insert(s.to_owned(), idx);
375        idx
376    }
377}
378
379/// String interning for categorical array generation with standard hashing.
380/// Deduplicates strings and assigns numeric codes for memory efficiency.
381#[cfg(not(feature = "fast_hash"))]
382#[inline(always)]
383fn intern<T: Integer>(s: &str, dict: &mut HashMap<String, T>, uniq: &mut Vec64<String>) -> T {
384    if let Some(&code) = dict.get(s) {
385        code
386    } else {
387        let idx = T::from_usize(uniq.len());
388        uniq.push(s.to_owned());
389        dict.insert(s.to_owned(), idx);
390        idx
391    }
392}
393
394/// Applies an element-wise binary operation between two `CategoricalArray<T>` arrays,
395/// producing a new `CategoricalArray<T>`. The result reuses or extends the unified dictionary
396/// from both input arrays and ensures deterministic interned value codes.
397///
398/// Supported operations:
399///
400/// - `Add`: Concatenates strings from `lhs` and `rhs`. Result is interned into the output dictionary.
401/// - `Subtract`: Removes the first occurrence of `rhs` from `lhs`. If `rhs` is empty or not found,
402///               returns `lhs` unchanged.
403/// - `Multiply`: Returns `lhs` unchanged. No actual repetition occurs - identity operation.
404/// - `Divide`: Splits `lhs` by occurrences of `rhs`, and each resulting segment is interned separately.
405///             If `rhs` is empty, `lhs` is returned unchanged.
406///
407/// Null handling:
408/// - If either side is null at a given index, the result is marked null and the empty string code is emitted.
409/// - Null mask is propagated accordingly.
410///
411/// Output:
412/// - The resulting `CategoricalArray<T>` may have a different length than the input
413///   if `Divide` produces multiple segments per row.
414/// - The dictionary (`unique_values`) is the union of all unique values observed in inputs and results,
415///   with stable interned codes.
416///
417/// Errors:
418/// - Returns `KernelError::LengthMismatch` if `lhs` and `rhs` differ in length.
419/// - Returns `KernelError::UnsupportedType` for any operator other than Add, Subtract, Multiply, Divide.
420///
421/// # Panics
422/// - Panics if internal memory allocation fails or if invariants are violated in unsafe regions.
423/// - Panics if the dictionary grows beyond `T::MAX` entries (relevant for small index types like `u8`).
424fn apply_dict_dict_impl<T: Integer>(
425    lhs: CategoricalAVT<T>,
426    rhs: CategoricalAVT<T>,
427    op: ArithmeticOperator,
428) -> Result<CategoricalArray<T>, KernelError> {
429    // Destructure slice tuples for offset/length-local processing
430    let (lhs_array, lhs_offset, lhs_logical_len) = lhs;
431    let (rhs_array, rhs_offset, rhs_logical_len) = rhs;
432
433    if lhs_logical_len != rhs_logical_len {
434        return Err(KernelError::LengthMismatch(log_length_mismatch(
435            "apply_dict_dict_impl".into(),
436            lhs_logical_len,
437            rhs_logical_len,
438        )));
439    }
440
441    // Input mask: merge only over local window
442    let in_mask = merge_bitmasks_to_new(
443        lhs_array.null_mask.as_ref(),
444        rhs_array.null_mask.as_ref(),
445        lhs_logical_len,
446    );
447
448    // Build unique dictionary for the output, initially union of both inputs
449    let mut uniq: Vec64<String> = Vec64::with_capacity(
450        lhs_array.unique_values.len() + rhs_array.unique_values.len() + lhs_logical_len,
451    );
452
453    #[cfg(feature = "fast_hash")]
454    let mut dict: AHashMap<String, T> = AHashMap::with_capacity(uniq.capacity());
455
456    #[cfg(not(feature = "fast_hash"))]
457    let mut dict: HashMap<String, T> = HashMap::with_capacity(uniq.capacity());
458
459    for v in lhs_array
460        .unique_values
461        .iter()
462        .chain(rhs_array.unique_values.iter())
463    {
464        if !dict.contains_key(v) {
465            let idx = T::from_usize(uniq.len());
466            uniq.push(v.clone());
467            dict.insert(uniq.last().unwrap().clone(), idx);
468        }
469    }
470
471    // Ensure "" is present and get its code
472    let empty_code = *dict.entry("".to_owned()).or_insert_with(|| {
473        let idx = T::from_usize(uniq.len());
474        uniq.push("".to_owned());
475        idx
476    });
477
478    // 1st pass: Count output rows for precise allocation
479    let mut total_out = 0usize;
480    for local_idx in 0..lhs_logical_len {
481        let i = lhs_offset + local_idx;
482        let j = rhs_offset + local_idx;
483        let valid = in_mask
484            .as_ref()
485            .map_or(true, |m| unsafe { m.get_unchecked(local_idx) });
486        if !valid {
487            total_out += 1;
488        } else if let ArithmeticOperator::Divide = op {
489            let l = unsafe { lhs_array.get_str_unchecked(i) };
490            let r = unsafe { rhs_array.get_str_unchecked(j) };
491            if r.is_empty() {
492                total_out += 1;
493            } else {
494                let mut parts = 0;
495                let mut start = 0;
496                while let Some(pos) = l[start..].find(r) {
497                    parts += 1;
498                    start += pos + r.len();
499                }
500                total_out += parts + 1;
501            }
502        } else {
503            total_out += 1;
504        }
505    }
506
507    // Preallocate output buffer for window only
508    let mut out_data = Vec64::with_capacity(total_out);
509    unsafe {
510        out_data.set_len(total_out);
511    }
512    let mut out_mask = Bitmask::new_set_all(total_out, false);
513
514    // 2nd pass: Populate output for this slice only
515    let mut write_ptr = 0;
516    for local_idx in 0..lhs_logical_len {
517        let i = lhs_offset + local_idx;
518        let j = rhs_offset + local_idx;
519        let valid = in_mask
520            .as_ref()
521            .map_or(true, |m| unsafe { m.get_unchecked(local_idx) });
522
523        if !valid {
524            out_data.push(empty_code);
525            unsafe { out_mask.set_unchecked(write_ptr, false) };
526            write_ptr += 1;
527            continue;
528        }
529
530        let l = unsafe { lhs_array.get_str_unchecked(i) };
531        let r = unsafe { rhs_array.get_str_unchecked(j) };
532
533        match op {
534            ArithmeticOperator::Add => {
535                let mut tmp = String::with_capacity(l.len() + r.len());
536                tmp.push_str(l);
537                tmp.push_str(r);
538                let code = intern(&tmp, &mut dict, &mut uniq);
539                unsafe {
540                    *out_data.get_unchecked_mut(write_ptr) = code;
541                }
542                out_mask.set(write_ptr, true);
543                write_ptr += 1;
544            }
545            ArithmeticOperator::Subtract => {
546                let result = if r.is_empty() {
547                    l.to_owned()
548                } else if let Some(pos) = l.find(r) {
549                    let mut tmp = String::with_capacity(l.len() - r.len());
550                    tmp.push_str(&l[..pos]);
551                    tmp.push_str(&l[pos + r.len()..]);
552                    tmp
553                } else {
554                    l.to_owned()
555                };
556                let code = intern(&result, &mut dict, &mut uniq);
557                unsafe {
558                    *out_data.get_unchecked_mut(write_ptr) = code;
559                }
560                out_mask.set(write_ptr, true);
561                write_ptr += 1;
562            }
563            ArithmeticOperator::Multiply => {
564                let code = intern(l, &mut dict, &mut uniq);
565                unsafe {
566                    *out_data.get_unchecked_mut(write_ptr) = code;
567                }
568                out_mask.set(write_ptr, true);
569                write_ptr += 1;
570            }
571            ArithmeticOperator::Divide => {
572                if r.is_empty() {
573                    let code = intern(l, &mut dict, &mut uniq);
574                    unsafe {
575                        *out_data.get_unchecked_mut(write_ptr) = code;
576                    }
577                    out_mask.set(write_ptr, true);
578                    write_ptr += 1;
579                } else {
580                    let mut start = 0;
581                    while let Some(pos) = l[start..].find(r) {
582                        let part = &l[start..start + pos];
583                        let code = intern(part, &mut dict, &mut uniq);
584                        unsafe {
585                            *out_data.get_unchecked_mut(write_ptr) = code;
586                        }
587                        out_mask.set(write_ptr, true);
588                        write_ptr += 1;
589                        start += pos + r.len();
590                    }
591                    let tail = &l[start..];
592                    let code = intern(tail, &mut dict, &mut uniq);
593                    unsafe {
594                        *out_data.get_unchecked_mut(write_ptr) = code;
595                    }
596                    out_mask.set(write_ptr, true);
597                    write_ptr += 1;
598                }
599            }
600            _ => {
601                return Err(KernelError::UnsupportedType(format!(
602                    "Unsupported apply_dict_dict_impl op={:?}",
603                    op
604                )));
605            }
606        }
607    }
608
609    debug_assert_eq!(write_ptr, total_out);
610
611    Ok(CategoricalArray {
612        data: out_data.into(),
613        unique_values: uniq,
614        null_mask: Some(out_mask),
615    })
616}
617
618/// Applies an element-wise binary operation between two `StringArray`s,
619/// producing a new `StringArray`. Requires both arrays to have the same length.
620///
621/// Supported operations:
622///
623/// - `Add`: Concatenates each pair of strings (`a + b`).
624/// - `Subtract`: Removes the first occurrence of `b` from `a`, if present.
625///               If `b` is empty or not found, `a` is returned unchanged.
626/// - `Multiply`: Repeats string `a` N times, where `N = min(b.len(), STRING_MULTIPLICATION_LIMIT)`.
627/// - `Divide`: Splits string `a` by occurrences of `b` and rejoins the segments using a `'|'` separator.
628///             If `b` is empty, returns `a` unchanged.
629///
630/// Null handling:
631/// - If either side is null at an index, the output will be null at that index.
632///
633/// Returns:
634/// - A new `StringArray<T>` containing the result of applying the binary operation to each pair.
635///
636/// Errors:
637/// - Returns `KernelError::LengthMismatch` if `lhs` and `rhs` lengths differ.
638/// - Returns `KernelError::UnsupportedType` if an unsupported binary operator is passed.
639///
640/// # Features
641/// This function is available only when the `str_arithmetic` feature is enabled.
642#[cfg(feature = "str_arithmetic")]
643pub fn apply_str_str<T, U>(
644    lhs: StringAVT<T>,
645    rhs: StringAVT<U>,
646    op: ArithmeticOperator,
647) -> Result<StringArray<T>, KernelError>
648where
649    T: Integer,
650    U: Integer,
651{
652    let (larr, loff, llen) = lhs;
653    let (rarr, roff, rlen) = rhs;
654
655    if llen != rlen {
656        return Err(KernelError::LengthMismatch(log_length_mismatch(
657            "apply_str_str".to_string(),
658            llen,
659            rlen,
660        )));
661    }
662
663    // slice the incoming masks down to [offset .. offset+llen)
664    let lmask_slice = larr.null_mask.as_ref().map(|m| {
665        let mut m2 = Bitmask::new_set_all(llen, true);
666        for i in 0..llen {
667            unsafe {
668                m2.set_unchecked(i, m.get_unchecked(loff + i));
669            }
670        }
671        m2
672    });
673    let rmask_slice = rarr.null_mask.as_ref().map(|m| {
674        let mut m2 = Bitmask::new_set_all(llen, true);
675        for i in 0..llen {
676            unsafe {
677                m2.set_unchecked(i, m.get_unchecked(roff + i));
678            }
679        }
680        m2
681    });
682    let lmask_ref = lmask_slice.as_ref();
683    let rmask_ref = rmask_slice.as_ref();
684
685    // build per-position validity
686    let lmask = lmask_ref;
687    let rmask = rmask_ref;
688    let mut out_mask = Bitmask::new_set_all(llen, false);
689    let _ = confirm_mask_capacity(llen, lmask)?;
690    let _ = confirm_mask_capacity(llen, rmask)?;
691
692    // 1) size pass
693    let mut total_bytes = 0;
694    for idx in 0..llen {
695        let valid = lmask.map_or(true, |m| unsafe { m.get_unchecked(idx) })
696            && rmask.map_or(true, |m| unsafe { m.get_unchecked(idx) });
697        if !valid {
698            continue;
699        }
700        let a = unsafe { larr.get_str_unchecked(loff + idx) };
701        let b = unsafe { rarr.get_str_unchecked(roff + idx) };
702        total_bytes += match op {
703            ArithmeticOperator::Add => a.len() + b.len(),
704            ArithmeticOperator::Subtract => a.len(),
705            ArithmeticOperator::Multiply => a.len() * b.len().min(STRING_MULTIPLICATION_LIMIT),
706            ArithmeticOperator::Divide => {
707                if b.is_empty() {
708                    a.len()
709                } else {
710                    a.len() + a.matches(b).count().saturating_sub(1)
711                }
712            }
713            _ => {
714                return Err(KernelError::UnsupportedType(format!(
715                    "Unsupported {:?}",
716                    op
717                )));
718            }
719        };
720    }
721
722    // 2) allocate
723    let mut offsets = Vec64::<T>::with_capacity(llen + 1);
724    let mut data = Vec64::<u8>::with_capacity(total_bytes);
725    offsets.push(T::zero());
726
727    // 3) build pass
728    for idx in 0..llen {
729        let valid = lmask.map_or(true, |m| unsafe { m.get_unchecked(idx) })
730            && rmask.map_or(true, |m| unsafe { m.get_unchecked(idx) });
731        if valid {
732            let a = unsafe { larr.get_str_unchecked(loff + idx) };
733            let b = unsafe { rarr.get_str_unchecked(roff + idx) };
734            match op {
735                ArithmeticOperator::Add => {
736                    data.extend_from_slice(a.as_bytes());
737                    data.extend_from_slice(b.as_bytes());
738                }
739                ArithmeticOperator::Subtract => {
740                    if b.is_empty() {
741                        data.extend_from_slice(a.as_bytes());
742                    } else if let Some(p) =
743                        memchr::memmem::Finder::new(b.as_bytes()).find(a.as_bytes())
744                    {
745                        data.extend_from_slice(&a.as_bytes()[..p]);
746                        data.extend_from_slice(&a.as_bytes()[p + b.len()..]);
747                    } else {
748                        data.extend_from_slice(a.as_bytes());
749                    }
750                }
751                ArithmeticOperator::Multiply => {
752                    let times = b.len().min(STRING_MULTIPLICATION_LIMIT);
753                    for _ in 0..times {
754                        data.extend_from_slice(a.as_bytes());
755                    }
756                }
757                ArithmeticOperator::Divide => {
758                    if b.is_empty() {
759                        data.extend_from_slice(a.as_bytes());
760                    } else {
761                        let finder = memchr::memmem::Finder::new(b.as_bytes());
762                        let mut start = 0;
763                        let mut first = true;
764                        while let Some(p) = finder.find(&a.as_bytes()[start..]) {
765                            if !first {
766                                data.push(b'|');
767                            }
768                            let abs = start + p;
769                            data.extend_from_slice(&a.as_bytes()[start..abs]);
770                            start = abs + b.len();
771                            first = false;
772                        }
773                        if !first {
774                            data.push(b'|');
775                        }
776                        data.extend_from_slice(&a.as_bytes()[start..]);
777                    }
778                }
779                _ => unreachable!(),
780            }
781            unsafe { out_mask.set_unchecked(idx, true) };
782        }
783        offsets.push(T::from_usize(data.len()));
784    }
785
786    Ok(StringArray {
787        offsets: offsets.into(),
788        data: data.into(),
789        null_mask: Some(out_mask),
790    })
791}
792
793/// Generates a public dict-dict arithmetic function for a specific index type.
794macro_rules! impl_apply_dict_dict {
795    ($fn_name:ident, $idx:ty) => {
796        pub fn $fn_name(
797            lhs: CategoricalAVT<$idx>,
798            rhs: CategoricalAVT<$idx>,
799            op: ArithmeticOperator,
800        ) -> Result<CategoricalArray<$idx>, KernelError> {
801            apply_dict_dict_impl(lhs, rhs, op)
802        }
803    };
804}
805
806#[cfg(any(not(feature = "default_categorical_8"), feature = "extended_categorical"))]
807impl_apply_dict_dict!(apply_dict32_dict32, u32);
808
809#[cfg(feature = "default_categorical_8")]
810impl_apply_dict_dict!(apply_dict8_dict8, u8);
811
812/// Applies element-wise binary arithmetic ops between a `CategoricalArray<u32>` and a `StringArray<T>`.
813#[cfg(feature = "str_arithmetic")]
814pub fn apply_dict32_str<T>(
815    lhs: CategoricalAVT<u32>,
816    rhs: StringAVT<T>,
817    op: ArithmeticOperator,
818) -> Result<CategoricalArray<u32>, KernelError>
819where
820    T: Integer,
821{
822    const SAMPLE_SIZE: usize = 256;
823    const CARDINALITY_THRESHOLD: f64 = 0.75;
824
825    // Destructure slice for local scope
826    let (larr, loff, llen) = lhs;
827    let (rarr, roff, rlen) = rhs;
828
829    if llen != rlen {
830        return Err(KernelError::LengthMismatch(log_length_mismatch(
831            "apply_dict32_str".to_string(),
832            llen,
833            rlen,
834        )));
835    }
836
837    // --- Estimate string diversity, pick path ---
838    let cat_ratio = estimate_categorical_cardinality(larr, SAMPLE_SIZE);
839    let str_ratio = estimate_string_cardinality(rarr, SAMPLE_SIZE);
840    let max_ratio = cat_ratio.max(str_ratio);
841
842    if max_ratio > CARDINALITY_THRESHOLD {
843        // High cardinality: materialise, do flat string ops, then re-categorise.
844        let lhs_str = larr.to_string_array();
845        let str_result = apply_str_str((&lhs_str, loff, llen), (rarr, roff, rlen), op)?;
846        return Ok(str_result.to_categorical_array());
847    }
848
849    // --- Low-cardinality: interned path ---
850    let out_mask = merge_bitmasks_to_new(larr.null_mask.as_ref(), rarr.null_mask.as_ref(), llen);
851
852    // First pass: Pre-count total number of output rows
853    let mut total_out = 0usize;
854    for local_idx in 0..llen {
855        let valid = out_mask
856            .as_ref()
857            .map_or(true, |m| unsafe { m.get_unchecked(local_idx) });
858        if !valid {
859            total_out += 1;
860        } else if let ArithmeticOperator::Divide = op {
861            let i = loff + local_idx;
862            let j = roff + local_idx;
863            let l_val = unsafe { larr.get_str_unchecked(i) };
864            let r_val = unsafe { rarr.get_str_unchecked(j) };
865            if r_val.is_empty() {
866                total_out += 1;
867            } else {
868                let mut start = 0;
869                while let Some(pos) = l_val[start..].find(r_val) {
870                    total_out += 1;
871                    start += pos + r_val.len();
872                }
873                total_out += 1; // final segment
874            }
875        } else {
876            total_out += 1;
877        }
878    }
879
880    // Pre-allocate output buffers
881    let mut out_data = Vec64::<u32>::with_capacity(total_out);
882    unsafe {
883        out_data.set_len(total_out);
884    }
885    let mut out_null = Bitmask::new_set_all(total_out, false);
886
887    // Prepare dictionary and unique values (for this slice)
888    let mut uniq: Vec64<String> = Vec64::with_capacity(larr.unique_values.len() + llen);
889    uniq.extend(larr.unique_values.iter().cloned());
890
891    #[cfg(feature = "fast_hash")]
892    let mut dict: AHashMap<String, u32> = AHashMap::with_capacity(uniq.len());
893
894    #[cfg(not(feature = "fast_hash"))]
895    let mut dict: HashMap<String, u32> = HashMap::with_capacity(uniq.len());
896
897    for (i, s) in uniq.iter().enumerate() {
898        dict.insert(s.clone(), i as u32);
899    }
900    // Ensure "" is interned
901    let empty_code = *dict.entry("".to_string()).or_insert_with(|| {
902        let idx = uniq.len() as u32;
903        uniq.push(String::new());
904        idx
905    });
906
907    // Second pass: Fill output buffers for the slice
908    let mut write_ptr = 0usize;
909    for local_idx in 0..llen {
910        let valid = out_mask
911            .as_ref()
912            .map_or(true, |m| unsafe { m.get_unchecked(local_idx) });
913        if !valid {
914            out_data.push(empty_code);
915            out_null.set(write_ptr, false);
916            write_ptr += 1;
917            continue;
918        }
919        let i = loff + local_idx;
920        let j = roff + local_idx;
921        let l_val = unsafe { larr.get_str_unchecked(i) };
922        let r_val = unsafe { rarr.get_str_unchecked(j) };
923        match op {
924            ArithmeticOperator::Add => {
925                let mut s = String::with_capacity(l_val.len() + r_val.len());
926                s.push_str(l_val);
927                s.push_str(r_val);
928                let code = intern(&s, &mut dict, &mut uniq);
929                *unsafe { out_data.get_unchecked_mut(write_ptr) } = code;
930                out_null.set(write_ptr, true);
931                write_ptr += 1;
932            }
933            ArithmeticOperator::Subtract => {
934                let result = if r_val.is_empty() {
935                    l_val.to_string()
936                } else if let Some(pos) = l_val.find(r_val) {
937                    let mut s = l_val[..pos].to_owned();
938                    s.push_str(&l_val[pos + r_val.len()..]);
939                    s
940                } else {
941                    l_val.to_string()
942                };
943                let code = intern(&result, &mut dict, &mut uniq);
944                *unsafe { out_data.get_unchecked_mut(write_ptr) } = code;
945                out_null.set(write_ptr, true);
946                write_ptr += 1;
947            }
948            ArithmeticOperator::Multiply => {
949                let code = intern(l_val, &mut dict, &mut uniq);
950                *unsafe { out_data.get_unchecked_mut(write_ptr) } = code;
951                out_null.set(write_ptr, true);
952                write_ptr += 1;
953            }
954            ArithmeticOperator::Divide => {
955                if r_val.is_empty() {
956                    let code = intern(l_val, &mut dict, &mut uniq);
957                    *unsafe { out_data.get_unchecked_mut(write_ptr) } = code;
958                    out_null.set(write_ptr, true);
959                    write_ptr += 1;
960                } else {
961                    let mut start = 0;
962                    loop {
963                        match l_val[start..].find(r_val) {
964                            Some(pos) => {
965                                let part = &l_val[start..start + pos];
966                                let code = intern(part, &mut dict, &mut uniq);
967                                *unsafe { out_data.get_unchecked_mut(write_ptr) } = code;
968                                out_null.set(write_ptr, true);
969                                write_ptr += 1;
970                                start += pos + r_val.len();
971                            }
972                            None => {
973                                let tail = &l_val[start..];
974                                let code = intern(tail, &mut dict, &mut uniq);
975                                *unsafe { out_data.get_unchecked_mut(write_ptr) } = code;
976                                out_null.set(write_ptr, true);
977                                write_ptr += 1;
978                                break;
979                            }
980                        }
981                    }
982                }
983            }
984            _ => {
985                return Err(KernelError::UnsupportedType(
986                    "Unsupported Type Error.".to_string(),
987                ));
988            }
989        }
990    }
991
992    debug_assert_eq!(write_ptr, total_out);
993
994    Ok(CategoricalArray {
995        data: out_data.into(),
996        unique_values: uniq,
997        null_mask: Some(out_null),
998    })
999}
1000
1001/// Applies element-wise binary arithmetic ops between `StringArray<T>` and `CategoricalArray<u32>`
1002#[cfg(feature = "str_arithmetic")]
1003pub fn apply_str_dict32<T>(
1004    lhs: StringAVT<T>,
1005    rhs: CategoricalAVT<u32>,
1006    op: ArithmeticOperator,
1007) -> Result<StringArray<T>, KernelError>
1008where
1009    T: Integer,
1010{
1011    // Destructure input slices
1012    let (larr, loff, llen) = lhs;
1013    let (rarr, roff, rlen) = rhs;
1014
1015    if llen != rlen {
1016        return Err(KernelError::LengthMismatch(log_length_mismatch(
1017            "apply_str_dict32".to_string(),
1018            llen,
1019            rlen,
1020        )));
1021    }
1022
1023    let out_mask = merge_bitmasks_to_new(larr.null_mask.as_ref(), rarr.null_mask.as_ref(), llen);
1024
1025    // --- Pre-count output rows and byte size ---
1026    let mut total_rows = 0usize;
1027    let mut total_bytes = 0usize;
1028
1029    for local_idx in 0..llen {
1030        let valid = out_mask
1031            .as_ref()
1032            .map_or(true, |m| unsafe { m.get_unchecked(local_idx) });
1033        if !valid {
1034            total_rows += 1;
1035            continue;
1036        }
1037
1038        let i = loff + local_idx;
1039        let j = roff + local_idx;
1040
1041        let l = unsafe { larr.get_str_unchecked(i) };
1042        let r = unsafe { rarr.get_str_unchecked(j) };
1043
1044        match op {
1045            ArithmeticOperator::Divide => {
1046                total_rows += l.split(r).count();
1047                total_bytes += l.len(); // splitting doesn't remove data
1048            }
1049            ArithmeticOperator::Add => {
1050                total_rows += 1;
1051                total_bytes += l.len() + r.len();
1052            }
1053            ArithmeticOperator::Subtract => {
1054                total_rows += 1;
1055                total_bytes += l.len();
1056            }
1057            ArithmeticOperator::Multiply => {
1058                total_rows += 1;
1059                total_bytes += l.len();
1060            }
1061            _ => {
1062                return Err(KernelError::UnsupportedType(
1063                    "Unsupported Type Error.".to_string(),
1064                ));
1065            }
1066        }
1067    }
1068
1069    // Allocate output buffers for local window
1070    let mut offsets = Vec64::<T>::with_capacity(total_rows + 1);
1071    let mut data = Vec64::<u8>::with_capacity(total_bytes);
1072
1073    unsafe {
1074        offsets.set_len(total_rows + 1);
1075    }
1076    offsets[0] = T::zero();
1077
1078    let mut cursor = 0;
1079    let mut offset_idx = 1;
1080
1081    for local_idx in 0..llen {
1082        let valid = out_mask
1083            .as_ref()
1084            .map_or(true, |m| unsafe { m.get_unchecked(local_idx) });
1085        if !valid {
1086            offsets[offset_idx] = T::from_usize(cursor);
1087            offset_idx += 1;
1088            continue;
1089        }
1090
1091        let i = loff + local_idx;
1092        let j = roff + local_idx;
1093
1094        let l = unsafe { larr.get_str_unchecked(i) };
1095        let r = unsafe { rarr.get_str_unchecked(j) };
1096
1097        match op {
1098            ArithmeticOperator::Divide => {
1099                for part in l.split(r) {
1100                    data.extend_from_slice(part.as_bytes());
1101                    cursor += part.len();
1102                    offsets[offset_idx] = T::from_usize(cursor);
1103                    offset_idx += 1;
1104                }
1105            }
1106            ArithmeticOperator::Add => {
1107                data.extend_from_slice(l.as_bytes());
1108                data.extend_from_slice(r.as_bytes());
1109                cursor += l.len() + r.len();
1110                offsets[offset_idx] = T::from_usize(cursor);
1111                offset_idx += 1;
1112            }
1113            ArithmeticOperator::Subtract => {
1114                if r.is_empty() {
1115                    data.extend_from_slice(l.as_bytes());
1116                    cursor += l.len();
1117                } else if let Some(pos) = l.find(r) {
1118                    data.extend_from_slice(&l.as_bytes()[..pos]);
1119                    data.extend_from_slice(&l.as_bytes()[pos + r.len()..]);
1120                    cursor += l.len() - r.len();
1121                } else {
1122                    data.extend_from_slice(l.as_bytes());
1123                    cursor += l.len();
1124                }
1125                offsets[offset_idx] = T::from_usize(cursor);
1126                offset_idx += 1;
1127            }
1128            ArithmeticOperator::Multiply => {
1129                data.extend_from_slice(l.as_bytes());
1130                cursor += l.len();
1131                offsets[offset_idx] = T::from_usize(cursor);
1132                offset_idx += 1;
1133            }
1134            _ => unreachable!(),
1135        }
1136    }
1137
1138    debug_assert_eq!(offset_idx, total_rows + 1);
1139
1140    Ok(StringArray {
1141        offsets: offsets.into(),
1142        data: data.into(),
1143        null_mask: out_mask,
1144    })
1145}
1146
1147/// Applies element-wise binary arithmetic op between `CategoricalArray<u32>`s
1148/// a numeric slice.
1149#[cfg(feature = "str_arithmetic")]
1150pub fn apply_dict32_num<T>(
1151    lhs: CategoricalAVT<u32>,
1152    rhs: &[T],
1153    op: ArithmeticOperator,
1154) -> Result<CategoricalArray<u32>, KernelError>
1155where
1156    T: ToPrimitive + Copy,
1157{
1158    #[cfg(feature = "fast_hash")]
1159    use ahash::{HashMap, HashMapExt};
1160
1161    #[cfg(not(feature = "fast_hash"))]
1162    use std::collections::HashMap;
1163
1164    let (larr, loff, llen) = lhs;
1165
1166    if llen != rhs.len() {
1167        return Err(KernelError::LengthMismatch(log_length_mismatch(
1168            "apply_dict32_num".to_string(),
1169            llen,
1170            rhs.len(),
1171        )));
1172    }
1173
1174    let has_mask = larr.null_mask.is_some();
1175    let mut out_mask = if has_mask {
1176        Some(Bitmask::new_set_all(llen, true))
1177    } else {
1178        None
1179    };
1180
1181    let mut data = Vec64::<u32>::with_capacity(llen);
1182    unsafe {
1183        data.set_len(llen);
1184    }
1185
1186    let mut unique_values = Vec64::<String>::with_capacity(llen);
1187    let mut seen: HashMap<String, u32> = HashMap::with_capacity(llen);
1188    let mut unique_idx = 0;
1189
1190    for local_idx in 0..llen {
1191        let valid = !has_mask
1192            || unsafe {
1193                larr.null_mask
1194                    .as_ref()
1195                    .unwrap()
1196                    .get_unchecked(loff + local_idx)
1197            };
1198
1199        if valid {
1200            let i = loff + local_idx;
1201            let l_val = unsafe { larr.get_str_unchecked(i) };
1202            let n = rhs[local_idx].to_usize().unwrap_or(0);
1203
1204            let cat = match op {
1205                ArithmeticOperator::Multiply => {
1206                    let count = n.min(1_000_000);
1207                    l_val.repeat(count)
1208                }
1209                _ => l_val.to_owned(),
1210            };
1211
1212            let idx = if let Some(&ix) = seen.get(&cat) {
1213                ix
1214            } else {
1215                let ix = unique_idx as u32;
1216                seen.insert(cat.clone(), ix);
1217                unique_values.push(cat);
1218                unique_idx += 1;
1219                ix
1220            };
1221
1222            unsafe {
1223                *data.get_unchecked_mut(local_idx) = idx;
1224                if let Some(mask) = &mut out_mask {
1225                    mask.set_unchecked(local_idx, true);
1226                }
1227            }
1228        } else {
1229            unsafe {
1230                *data.get_unchecked_mut(local_idx) = 0;
1231                if let Some(mask) = &mut out_mask {
1232                    mask.set_unchecked(local_idx, false);
1233                }
1234            }
1235        }
1236    }
1237
1238    Ok(CategoricalArray {
1239        data: data.into(),
1240        unique_values,
1241        null_mask: out_mask,
1242    })
1243}
1244
1245/// Strips '.0' from concatenated decimal values so 'Hello1.0' becomes 'Hello1'.
1246#[inline]
1247#[cfg(feature = "str_arithmetic")]
1248pub fn format_finite<F: Float>(buf: &mut [MaybeUninit<u8>; 24], f: F) -> &str {
1249    unsafe {
1250        let ptr = buf.as_mut_ptr() as *mut u8;
1251        let n = f.write_to_ryu_buffer(ptr);
1252        debug_assert!(n <= buf.len());
1253
1254        let slice = core::slice::from_raw_parts(ptr, n);
1255        let s = core::str::from_utf8_unchecked(slice);
1256
1257        // Strip trailing ".0" if present
1258        if s.ends_with(".0") {
1259            let trimmed_len = s.len() - 2;
1260            core::str::from_utf8_unchecked(&slice[..trimmed_len])
1261        } else {
1262            s
1263        }
1264    }
1265}
1266
1267#[cfg(test)]
1268mod tests {
1269    use crate::MaskedArray;
1270    use crate::structs::variants::string::StringArray;
1271    #[cfg(feature = "str_arithmetic")]
1272    use crate::{Bitmask, CategoricalArray};
1273
1274    use super::*;
1275    use crate::enums::operators::ArithmeticOperator;
1276    #[cfg(feature = "str_arithmetic")]
1277    use crate::vec64;
1278
1279    // Helpers
1280
1281    /// Assert that a `StringArray<T>` matches the supplied `Vec<&str>` and nullity.
1282    fn assert_str<T>(arr: &StringArray<T>, expect: &[&str], valid: Option<&[bool]>)
1283    where
1284        T: Integer + std::fmt::Debug,
1285    {
1286        assert_eq!(arr.len(), expect.len());
1287        for (i, exp) in expect.iter().enumerate() {
1288            assert_eq!(unsafe { arr.get_str_unchecked(i) }, *exp);
1289        }
1290        match (valid, &arr.null_mask) {
1291            (None, None) => {}
1292            (Some(expected), Some(mask)) => {
1293                for (i, bit) in expected.iter().enumerate() {
1294                    assert_eq!(unsafe { mask.get_unchecked(i) }, *bit);
1295                }
1296            }
1297            (None, Some(mask)) => {
1298                assert!(mask.all_true());
1299            }
1300            (Some(_), None) => panic!("expected mask missing"),
1301        }
1302    }
1303
1304    // String - Numeric Kernels
1305
1306    #[test]
1307    fn str_num_multiply() {
1308        let input = StringArray::<u32>::from_slice(&["hi", "bye", "x"]);
1309        let nums: &[i32] = &[3, 2, 0];
1310        let input_slice = (&input, 0, input.len(), input.data.len());
1311        let out =
1312            super::apply_str_num::<u32, i32, u32>(input_slice, nums, ArithmeticOperator::Multiply)
1313                .unwrap();
1314        assert_str(&out, &["hihihi", "byebye", ""], None);
1315    }
1316
1317    #[test]
1318    fn str_num_multiply_chunk() {
1319        let base = StringArray::<u32>::from_slice(&["pad", "hi", "bye", "x", "pad2"]);
1320        let nums: &[i32] = &[3, 2, 0];
1321        // Window: ["hi", "bye", "x"]
1322        let input_slice = (&base, 1, 3, base.data.len());
1323        let out =
1324            super::apply_str_num::<u32, i32, u32>(input_slice, nums, ArithmeticOperator::Multiply)
1325                .unwrap();
1326        assert_str(&out, &["hihihi", "byebye", ""], None);
1327    }
1328
1329    #[test]
1330    fn str_num_len_mismatch() {
1331        let input = StringArray::<u32>::from_slice(&["a"]);
1332        let nums: &[i32] = &[1, 2];
1333        let input_slice = (&input, 0, input.len(), input.data.len());
1334        let err = super::apply_str_num::<u32, i32, u32>(input_slice, nums, ArithmeticOperator::Add)
1335            .unwrap_err();
1336        match err {
1337            KernelError::LengthMismatch(_) => {}
1338            _ => panic!("wrong error variant"),
1339        }
1340    }
1341
1342    #[test]
1343    fn str_num_len_mismatch_chunk() {
1344        let base = StringArray::<u32>::from_slice(&["pad", "a", "pad2"]);
1345        let nums: &[i32] = &[1, 2];
1346        // Window: ["a"]
1347        let input_slice = (&base, 1, 1, base.data.len());
1348        let err = super::apply_str_num::<u32, i32, u32>(input_slice, nums, ArithmeticOperator::Add)
1349            .unwrap_err();
1350        match err {
1351            KernelError::LengthMismatch(_) => {}
1352            _ => panic!("wrong error variant"),
1353        }
1354    }
1355
1356    #[cfg(feature = "str_arithmetic")]
1357    #[test]
1358    fn str_float_all_ops() {
1359        let input = StringArray::<u32>::from_slice(&["foo", "bar1", "baz"]);
1360        let nums: &[f64] = &[1.0, 1.0, 2.0];
1361        let input_slice = (&input, 0, input.len());
1362        // Add
1363        let add = super::apply_str_float(input_slice, nums, ArithmeticOperator::Add).unwrap();
1364        assert_str(&add, &["foo1", "bar11", "baz2"], None);
1365        // Subtract
1366        let sub = super::apply_str_float(input_slice, nums, ArithmeticOperator::Subtract).unwrap();
1367        assert_str(&sub, &["foo", "bar", "baz"], None);
1368        // Multiply
1369        let mul = super::apply_str_float(input_slice, nums, ArithmeticOperator::Multiply).unwrap();
1370        assert_str(&mul, &["foo", "bar1", "bazbaz"], None);
1371        // Divide
1372        let div = super::apply_str_float(input_slice, nums, ArithmeticOperator::Divide).unwrap();
1373        assert_str(&div, &["foo", "bar|", "baz"], None);
1374    }
1375
1376    #[cfg(feature = "str_arithmetic")]
1377    #[test]
1378    fn str_float_all_ops_chunk() {
1379        let base = StringArray::<u32>::from_slice(&["pad", "foo", "bar1", "baz", "pad2"]);
1380        let nums: &[f64] = &[1.0, 1.0, 2.0];
1381        // Window: ["foo", "bar1", "baz"]
1382        let input_slice = (&base, 1, 3);
1383        // Add
1384        let add = super::apply_str_float(input_slice, nums, ArithmeticOperator::Add).unwrap();
1385        assert_str(&add, &["foo1", "bar11", "baz2"], None);
1386        // Subtract
1387        let sub = super::apply_str_float(input_slice, nums, ArithmeticOperator::Subtract).unwrap();
1388        assert_str(&sub, &["foo", "bar", "baz"], None);
1389        // Multiply
1390        let mul = super::apply_str_float(input_slice, nums, ArithmeticOperator::Multiply).unwrap();
1391        assert_str(&mul, &["foo", "bar1", "bazbaz"], None);
1392        // Divide
1393        let div = super::apply_str_float(input_slice, nums, ArithmeticOperator::Divide).unwrap();
1394        assert_str(&div, &["foo", "bar|", "baz"], None);
1395    }
1396
1397    // Dictionary Kernels
1398
1399    #[cfg(feature = "str_arithmetic")]
1400    fn cat(values: &[&str]) -> CategoricalArray<u32> {
1401        CategoricalArray::<u32>::from_values(values.iter().copied())
1402    }
1403
1404    #[cfg(feature = "str_arithmetic")]
1405    #[test]
1406    fn dict32_dict32_add() {
1407        let lhs = cat(&["A", "B", ""]);
1408        let rhs = cat(&["1", "2", "3"]);
1409        let lhs_slice = (&lhs, 0, lhs.data.len());
1410        let rhs_slice = (&rhs, 0, rhs.data.len());
1411        let out =
1412            super::apply_dict32_dict32(lhs_slice, rhs_slice, ArithmeticOperator::Add).unwrap();
1413        let expected = vec64!["A1", "B2", "3"];
1414        for (i, exp) in expected.iter().enumerate() {
1415            assert_eq!(out.get(i).unwrap_or(""), *exp);
1416        }
1417    }
1418
1419    #[cfg(feature = "str_arithmetic")]
1420    #[test]
1421    fn dict32_dict32_add_chunk() {
1422        let lhs = cat(&["pad", "A", "B", "", "pad2"]);
1423        let rhs = cat(&["padx", "1", "2", "3", "pady"]);
1424        let lhs_slice = (&lhs, 1, 3); // "A", "B", ""
1425        let rhs_slice = (&rhs, 1, 3); // "1", "2", "3"
1426        let out =
1427            super::apply_dict32_dict32(lhs_slice, rhs_slice, ArithmeticOperator::Add).unwrap();
1428        let expected = vec64!["A1", "B2", "3"];
1429        for (i, exp) in expected.iter().enumerate() {
1430            assert_eq!(out.get(i).unwrap_or(""), *exp);
1431        }
1432    }
1433
1434    #[cfg(feature = "str_arithmetic")]
1435    #[test]
1436    fn dict32_str_subtract() {
1437        let lhs = cat(&["hello", "yellow"]);
1438        let rhs = StringArray::<u32>::from_slice(&["l", "el"]);
1439        let lhs_slice = (&lhs, 0, lhs.data.len());
1440        let rhs_slice = (&rhs, 0, rhs.len());
1441        let out =
1442            super::apply_dict32_str(lhs_slice, rhs_slice, ArithmeticOperator::Subtract).unwrap();
1443        assert_eq!(out.get(0).unwrap(), "helo");
1444        assert_eq!(out.get(1).unwrap(), "ylow");
1445    }
1446
1447    #[cfg(feature = "str_arithmetic")]
1448    #[test]
1449    fn dict32_str_subtract_chunk() {
1450        let lhs = cat(&["pad", "hello", "yellow", "pad2"]);
1451        let rhs = StringArray::<u32>::from_slice(&["pad", "l", "el", "pad2"]);
1452        let lhs_slice = (&lhs, 1, 2); // "hello", "yellow"
1453        let rhs_slice = (&rhs, 1, 2); // "l", "el"
1454        let out =
1455            super::apply_dict32_str(lhs_slice, rhs_slice, ArithmeticOperator::Subtract).unwrap();
1456        assert_eq!(out.get(0).unwrap(), "helo");
1457        assert_eq!(out.get(1).unwrap(), "ylow");
1458    }
1459
1460    #[cfg(feature = "str_arithmetic")]
1461    #[test]
1462    fn str_dict32_divide() {
1463        let lhs = StringArray::<u32>::from_slice(&["a:b:c"]);
1464        let rhs = cat(&[":"]);
1465        let lhs_slice = (&lhs, 0, lhs.len());
1466        let rhs_slice = (&rhs, 0, rhs.data.len());
1467        let out =
1468            super::apply_str_dict32(lhs_slice, rhs_slice, ArithmeticOperator::Divide).unwrap();
1469        assert_str(&out, &["a", "b", "c"], None);
1470    }
1471
1472    #[cfg(feature = "str_arithmetic")]
1473    #[test]
1474    fn str_dict32_divide_chunk() {
1475        // Extended arrays for windowing
1476        let lhs = StringArray::<u32>::from_slice(&["pad", "a:b:c", "pad2"]);
1477        let rhs = cat(&["pad", ":", "pad2"]);
1478        let lhs_slice = (&lhs, 1, 1); // only "a:b:c"
1479        let rhs_slice = (&rhs, 1, 1); // only ":"
1480        let out =
1481            super::apply_str_dict32(lhs_slice, rhs_slice, ArithmeticOperator::Divide).unwrap();
1482        assert_str(&out, &["a", "b", "c"], None);
1483    }
1484
1485    #[cfg(feature = "str_arithmetic")]
1486    #[test]
1487    fn dict32_num_multiply() {
1488        let lhs = cat(&["x", "y"]);
1489        let nums: &[u32] = &[3, 1];
1490        let lhs_slice = (&lhs, 0, lhs.data.len());
1491        let nums_window = &nums[0..lhs.data.len()];
1492        let out =
1493            super::apply_dict32_num(lhs_slice, nums_window, ArithmeticOperator::Multiply).unwrap();
1494        assert_eq!(out.get(0).unwrap(), "xxx");
1495        assert_eq!(out.get(1).unwrap(), "y");
1496    }
1497
1498    #[cfg(feature = "str_arithmetic")]
1499    #[test]
1500    fn dict32_num_multiply_chunk() {
1501        let lhs = cat(&["pad", "x", "y", "pad2"]);
1502        let nums: &[u32] = &[0, 3, 1, 0];
1503        let lhs_slice = (&lhs, 1, 2); // only "x", "y"
1504        let nums_window = &nums[1..3];
1505        let out =
1506            super::apply_dict32_num(lhs_slice, nums_window, ArithmeticOperator::Multiply).unwrap();
1507        assert_eq!(out.get(0).unwrap(), "xxx");
1508        assert_eq!(out.get(1).unwrap(), "y");
1509    }
1510
1511    #[cfg(feature = "str_arithmetic")]
1512    fn cat32_str_arr(strings: &[&str]) -> (CategoricalArray<u32>, StringArray<u32>) {
1513        let str_arr = StringArray::from_vec(strings.to_vec(), None);
1514        let cat_arr = str_arr.to_categorical_array();
1515        (cat_arr, str_arr)
1516    }
1517
1518    #[cfg(feature = "str_arithmetic")]
1519    #[test]
1520    fn test_apply_dict32_str_add_and_divide() {
1521        let (lhs_cat, rhs_str) = cat32_str_arr(&["foo", "bar|baz", ""]);
1522        // Add: Use slices
1523        let lhs_cat_slice = (&lhs_cat, 0, lhs_cat.data.len());
1524        let rhs_str_slice = (&rhs_str, 0, rhs_str.len());
1525        let added =
1526            apply_dict32_str(lhs_cat_slice, rhs_str_slice, ArithmeticOperator::Add).unwrap();
1527        let expected_cat = apply_str_str(
1528            (&lhs_cat.to_string_array(), 0, lhs_cat.len()),
1529            rhs_str_slice,
1530            ArithmeticOperator::Add,
1531        )
1532        .unwrap()
1533        .to_categorical_array();
1534        assert_eq!(added.unique_values, expected_cat.unique_values);
1535        assert_eq!(added.data, expected_cat.data);
1536
1537        // Divide: Use slices
1538        let divided =
1539            apply_dict32_str(lhs_cat_slice, rhs_str_slice, ArithmeticOperator::Divide).unwrap();
1540        let expected_div = apply_str_str(
1541            (&lhs_cat.to_string_array(), 0, lhs_cat.len()),
1542            rhs_str_slice,
1543            ArithmeticOperator::Divide,
1544        )
1545        .unwrap()
1546        .to_categorical_array();
1547        assert_eq!(divided.unique_values, expected_div.unique_values);
1548        assert_eq!(divided.data, expected_div.data);
1549    }
1550
1551    #[cfg(feature = "str_arithmetic")]
1552    #[test]
1553    fn test_apply_dict32_str_add_and_divide_chunk() {
1554        let (lhs_cat, rhs_str) = cat32_str_arr(&["pad", "foo", "bar|baz", "", "pad2"]);
1555        let lhs_cat_slice = (&lhs_cat, 1, 3); // only "foo", "bar|baz", ""
1556        let rhs_str_slice = (&rhs_str, 1, 3);
1557
1558        // Add
1559        let added =
1560            apply_dict32_str(lhs_cat_slice, rhs_str_slice, ArithmeticOperator::Add).unwrap();
1561        let expected_cat = apply_str_str(
1562            (&lhs_cat.to_string_array(), 1, 3),
1563            rhs_str_slice,
1564            ArithmeticOperator::Add,
1565        )
1566        .unwrap()
1567        .to_categorical_array();
1568        assert_eq!(added.unique_values, expected_cat.unique_values);
1569        assert_eq!(added.data, expected_cat.data);
1570
1571        // Divide
1572        let divided =
1573            apply_dict32_str(lhs_cat_slice, rhs_str_slice, ArithmeticOperator::Divide).unwrap();
1574        let expected_div = apply_str_str(
1575            (&lhs_cat.to_string_array(), 1, 3),
1576            rhs_str_slice,
1577            ArithmeticOperator::Divide,
1578        )
1579        .unwrap()
1580        .to_categorical_array();
1581        assert_eq!(divided.unique_values, expected_div.unique_values);
1582        assert_eq!(divided.data, expected_div.data);
1583    }
1584
1585    // String arithmetic
1586
1587    #[cfg(feature = "str_arithmetic")]
1588    fn string_array<T: Integer>(data: &[&str], nulls: Option<&[bool]>) -> StringArray<T> {
1589        let array = StringArray::from_vec(data.to_vec(), nulls.map(Bitmask::from_bools));
1590        assert_eq!(array.len(), data.len());
1591        array
1592    }
1593
1594    #[cfg(feature = "str_arithmetic")]
1595    #[test]
1596    fn test_add_str() {
1597        let lhs = string_array::<u32>(&["a", "b", "c"], None);
1598        let rhs = string_array::<u32>(&["x", "y", "z"], None);
1599        let lhs_slice = (&lhs, 0, lhs.len());
1600        let rhs_slice = (&rhs, 0, rhs.len());
1601        let result = apply_str_str(lhs_slice, rhs_slice, ArithmeticOperator::Add).unwrap();
1602
1603        assert_eq!(result.get(0), Some("ax"));
1604        assert_eq!(result.get(1), Some("by"));
1605        assert_eq!(result.get(2), Some("cz"));
1606    }
1607
1608    #[cfg(feature = "str_arithmetic")]
1609    #[test]
1610    fn test_add_str_chunk() {
1611        let lhs = string_array::<u32>(&["pad", "a", "b", "c", "pad2"], None);
1612        let rhs = string_array::<u32>(&["pad", "x", "y", "z", "pad2"], None);
1613        // window: indices 1..4
1614        let lhs_slice = (&lhs, 1, 3);
1615        let rhs_slice = (&rhs, 1, 3);
1616        let result = apply_str_str(lhs_slice, rhs_slice, ArithmeticOperator::Add).unwrap();
1617
1618        assert_eq!(result.get(0), Some("ax"));
1619        assert_eq!(result.get(1), Some("by"));
1620        assert_eq!(result.get(2), Some("cz"));
1621    }
1622
1623    #[cfg(feature = "str_arithmetic")]
1624    #[test]
1625    fn test_subtract_str() {
1626        let lhs = string_array::<u32>(&["hello", "goodbye", "test"], None);
1627        let rhs = string_array::<u32>(&["l", "bye", "xyz"], None);
1628        let lhs_slice = (&lhs, 0, lhs.len());
1629        let rhs_slice = (&rhs, 0, rhs.len());
1630        let result = apply_str_str(lhs_slice, rhs_slice, ArithmeticOperator::Subtract).unwrap();
1631
1632        assert_eq!(result.get(0), Some("helo"));
1633        assert_eq!(result.get(1), Some("good"));
1634        assert_eq!(result.get(2), Some("test")); // no match
1635    }
1636
1637    #[cfg(feature = "str_arithmetic")]
1638    #[test]
1639    fn test_subtract_str_chunk() {
1640        let lhs = string_array::<u32>(&["pad", "hello", "goodbye", "test", "pad2"], None);
1641        let rhs = string_array::<u32>(&["pad", "l", "bye", "xyz", "pad2"], None);
1642        // window: indices 1..4
1643        let lhs_slice = (&lhs, 1, 3);
1644        let rhs_slice = (&rhs, 1, 3);
1645        let result = apply_str_str(lhs_slice, rhs_slice, ArithmeticOperator::Subtract).unwrap();
1646
1647        assert_eq!(result.get(0), Some("helo"));
1648        assert_eq!(result.get(1), Some("good"));
1649        assert_eq!(result.get(2), Some("test")); // no match
1650    }
1651
1652    #[cfg(feature = "str_arithmetic")]
1653    #[test]
1654    fn test_multiply_str() {
1655        let lhs = string_array::<u32>(&["x", "ab", "c"], None);
1656        let rhs = string_array::<u32>(&["123", "12", "long_string"], None);
1657        let lhs_slice = (&lhs, 0, lhs.len());
1658        let rhs_slice = (&rhs, 0, rhs.len());
1659        let result = apply_str_str(lhs_slice, rhs_slice, ArithmeticOperator::Multiply).unwrap();
1660
1661        assert_eq!(result.get(0), Some("xxx"));
1662        assert_eq!(result.get(1), Some("abab"));
1663        assert_eq!(
1664            result.get(2),
1665            Some("c".repeat("long_string".len()).as_str())
1666        );
1667    }
1668
1669    #[cfg(feature = "str_arithmetic")]
1670    #[test]
1671    fn test_multiply_str_chunk() {
1672        let lhs = string_array::<u32>(&["pad", "x", "ab", "c", "pad2"], None);
1673        let rhs = string_array::<u32>(&["pad", "123", "12", "long_string", "pad2"], None);
1674        // window: indices 1..4
1675        let lhs_slice = (&lhs, 1, 3);
1676        let rhs_slice = (&rhs, 1, 3);
1677        let result = apply_str_str(lhs_slice, rhs_slice, ArithmeticOperator::Multiply).unwrap();
1678
1679        assert_eq!(result.get(0), Some("xxx"));
1680        assert_eq!(result.get(1), Some("abab"));
1681        assert_eq!(
1682            result.get(2),
1683            Some("c".repeat("long_string".len()).as_str())
1684        );
1685    }
1686
1687    #[cfg(feature = "str_arithmetic")]
1688    #[test]
1689    fn test_divide_str() {
1690        let lhs = string_array::<u32>(&["a,b,c", "a--b--c", "abc"], None);
1691        let rhs = string_array::<u32>(&[",", "--", ""], None);
1692        let lhs_slice = (&lhs, 0, lhs.len());
1693        let rhs_slice = (&rhs, 0, rhs.len());
1694        let result = apply_str_str(lhs_slice, rhs_slice, ArithmeticOperator::Divide).unwrap();
1695
1696        assert_eq!(result.get(0), Some("a|b|c"));
1697        assert_eq!(result.get(1), Some("a|b|c"));
1698        assert_eq!(result.get(2), Some("abc"));
1699    }
1700
1701    #[cfg(feature = "str_arithmetic")]
1702    #[test]
1703    fn test_divide_str_chunk() {
1704        let lhs = string_array::<u32>(&["xxx", "a,b,c", "a--b--c", "abc", "yyy"], None);
1705        let rhs = string_array::<u32>(&["", ",", "--", "", ""], None);
1706        // operate only on the window: indices 1,2,3
1707        let lhs_slice = (&lhs, 1, 3);
1708        let rhs_slice = (&rhs, 1, 3);
1709        let result = apply_str_str(lhs_slice, rhs_slice, ArithmeticOperator::Divide).unwrap();
1710
1711        assert_eq!(result.get(0), Some("a|b|c"));
1712        assert_eq!(result.get(1), Some("a|b|c"));
1713        assert_eq!(result.get(2), Some("abc"));
1714    }
1715
1716    #[cfg(feature = "str_arithmetic")]
1717    #[test]
1718    fn test_nulls_str() {
1719        let lhs = string_array::<u32>(&["a", "b", "c"], Some(&[true, false, true]));
1720        let rhs = string_array::<u32>(&["x", "y", "z"], Some(&[true, true, false]));
1721        let lhs_slice = (&lhs, 0, lhs.len());
1722        let rhs_slice = (&rhs, 0, rhs.len());
1723        let result = apply_str_str(lhs_slice, rhs_slice, ArithmeticOperator::Add).unwrap();
1724
1725        assert_eq!(result.get(0), Some("ax"));
1726        assert_eq!(result.get(1), None);
1727        assert_eq!(result.get(2), None);
1728    }
1729
1730    #[cfg(feature = "str_arithmetic")]
1731    #[test]
1732    fn test_nulls_str_chunk() {
1733        let lhs = string_array::<u32>(
1734            &["0", "a", "b", "c", "9"],
1735            Some(&[false, true, false, true, false]),
1736        );
1737        let rhs = string_array::<u32>(
1738            &["y", "x", "y", "z", "w"],
1739            Some(&[true, true, true, false, false]),
1740        );
1741        // window covering indices 1..4
1742        let lhs_slice = (&lhs, 1, 3);
1743        let rhs_slice = (&rhs, 1, 3);
1744        let result = apply_str_str(lhs_slice, rhs_slice, ArithmeticOperator::Add).unwrap();
1745
1746        assert_eq!(result.get(0), Some("ax"));
1747        assert_eq!(result.get(1), None);
1748        assert_eq!(result.get(2), None);
1749    }
1750
1751    #[cfg(feature = "str_arithmetic")]
1752    #[test]
1753    fn test_mismatched_length_str() {
1754        let lhs = string_array::<u32>(&["a", "b"], None);
1755        let rhs = string_array::<u32>(&["x"], None);
1756        let lhs_slice = (&lhs, 0, lhs.len());
1757        let rhs_slice = (&rhs, 0, rhs.len());
1758        let result = apply_str_str(lhs_slice, rhs_slice, ArithmeticOperator::Add);
1759        assert!(matches!(result, Err(KernelError::LengthMismatch(_))));
1760    }
1761
1762    #[cfg(feature = "str_arithmetic")]
1763    #[test]
1764    fn test_mismatched_length_str_chunk() {
1765        let lhs = string_array::<u32>(&["a", "b", "c"], None);
1766        let rhs = string_array::<u32>(&["x"], None);
1767        // windowed call: indices 1..3, lhs has 2 elements, rhs has 1, so mismatch
1768        let lhs_slice = (&lhs, 1, 2);
1769        let rhs_slice = (&rhs, 0, 1);
1770        let result = apply_str_str(lhs_slice, rhs_slice, ArithmeticOperator::Add);
1771        assert!(matches!(result, Err(KernelError::LengthMismatch(_))));
1772    }
1773}