Skip to main content

akar_function/scalar/
string.rs

1use super::get_cached_regex;
2use crate::registry::*;
3use akar_common::types::Value;
4
5// ==================== String ====================
6
7fn soundex_impl(s: &str) -> String {
8    let mut chars = s.chars().filter(|c| c.is_ascii_alphabetic());
9    let first_char = match chars.next() {
10        Some(c) => c.to_ascii_uppercase(),
11        None => return "".to_string(),
12    };
13
14    let mut result = String::with_capacity(4);
15    result.push(first_char);
16
17    let get_code = |c: char| -> char {
18        match c.to_ascii_uppercase() {
19            'B' | 'F' | 'P' | 'V' => '1',
20            'C' | 'G' | 'J' | 'K' | 'Q' | 'S' | 'X' | 'Z' => '2',
21            'D' | 'T' => '3',
22            'L' => '4',
23            'M' | 'N' => '5',
24            'R' => '6',
25            _ => '0',
26        }
27    };
28
29    let mut prev_code = get_code(first_char);
30    for c in chars {
31        let code = get_code(c);
32        if code != '0' && code != prev_code {
33            result.push(code);
34            if result.len() == 4 {
35                break;
36            }
37        }
38        if !c.eq_ignore_ascii_case(&'H') && !c.eq_ignore_ascii_case(&'W') {
39            prev_code = code;
40        }
41    }
42
43    while result.len() < 4 {
44        result.push('0');
45    }
46    result
47}
48
49pub(crate) fn evaluate_string(op: StringOp, args: &[Value]) -> Result<Value, String> {
50    if args.is_empty() {
51        return Err("String function requires arguments".into());
52    }
53
54    match op {
55        StringOp::Concat => {
56            let s: String = args
57                .iter()
58                .map(|v| match v {
59                    Value::String(s) => s.clone(),
60                    Value::Null => "NULL".into(),
61                    other => format!("{:?}", other),
62                })
63                .collect();
64            Ok(Value::String(s))
65        }
66        StringOp::Contains => {
67            let s = get_string(&args[0])?;
68            let pat = get_string(&args[1])?;
69            Ok(Value::Bool(s.contains(&pat)))
70        }
71        StringOp::StartsWith => {
72            let s = get_string(&args[0])?;
73            let pat = get_string(&args[1])?;
74            Ok(Value::Bool(s.starts_with(&pat)))
75        }
76        StringOp::EndsWith => {
77            let s = get_string(&args[0])?;
78            let pat = get_string(&args[1])?;
79            Ok(Value::Bool(s.ends_with(&pat)))
80        }
81        StringOp::Like => {
82            let s = get_string(&args[0])?;
83            let pat = get_string(&args[1])?;
84            // Convert SQL LIKE pattern to regex pattern
85            let mut regex_str = String::with_capacity(pat.len() + 2);
86            regex_str.push('^');
87            for ch in pat.chars() {
88                match ch {
89                    '%' => regex_str.push_str(".*"),
90                    '_' => regex_str.push('.'),
91                    // Escape regex metacharacters
92                    '.' | '\\' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '^' | '$' | '|' => {
93                        regex_str.push('\\');
94                        regex_str.push(ch);
95                    }
96                    other => regex_str.push(other),
97                }
98            }
99            regex_str.push('$');
100            let re = get_cached_regex(&regex_str)?;
101            Ok(Value::Bool(re.is_match(&s)))
102        }
103        StringOp::ToUpper => {
104            let s = get_string(&args[0])?;
105            Ok(Value::String(s.to_uppercase()))
106        }
107        StringOp::ToLower => {
108            let s = get_string(&args[0])?;
109            Ok(Value::String(s.to_lowercase()))
110        }
111        StringOp::Trim => {
112            let s = get_string(&args[0])?;
113            Ok(Value::String(s.trim().to_string()))
114        }
115        StringOp::LTrim => {
116            let s = get_string(&args[0])?;
117            Ok(Value::String(s.trim_start().to_string()))
118        }
119        StringOp::RTrim => {
120            let s = get_string(&args[0])?;
121            Ok(Value::String(s.trim_end().to_string()))
122        }
123        StringOp::Length => {
124            let s = get_string(&args[0])?;
125            Ok(Value::Int64(s.len() as i64))
126        }
127        StringOp::Reverse => {
128            let s = get_string(&args[0])?;
129            Ok(Value::String(s.chars().rev().collect()))
130        }
131        StringOp::Repeat => {
132            let s = get_string(&args[0])?;
133            let n = match &args[1] {
134                Value::Int64(x) => *x as usize,
135                _ => return Err("Repeat count must be integer".into()),
136            };
137            Ok(Value::String(s.repeat(n)))
138        }
139        StringOp::Replace => {
140            let s = get_string(&args[0])?;
141            let from = get_string(&args[1])?;
142            let to = get_string(&args[2])?;
143            Ok(Value::String(s.replace(&from, &to)))
144        }
145        StringOp::Substring => {
146            let s = get_string(&args[0])?;
147            // Cypher uses 1-based indexing
148            let start = match &args[1] {
149                Value::Int64(x) => {
150                    if *x < 1 {
151                        return Err("Substring start must be >= 1".into());
152                    }
153                    (*x - 1) as usize
154                }
155                _ => return Err("Start must be integer".into()),
156            };
157            let len = if args.len() > 2 {
158                match &args[2] {
159                    Value::Int64(x) => Some(*x as usize),
160                    _ => None,
161                }
162            } else {
163                None
164            };
165            let result = match len {
166                Some(l) => s.chars().skip(start).take(l).collect(),
167                None => s.chars().skip(start).collect(),
168            };
169            Ok(Value::String(result))
170        }
171        StringOp::RegexMatches => {
172            let s = get_string(&args[0])?;
173            let pat = get_string(&args[1])?;
174            let re = get_cached_regex(&pat)?;
175            Ok(Value::Bool(re.is_match(&s)))
176        }
177        StringOp::RegexReplace => {
178            let s = get_string(&args[0])?;
179            let pat = get_string(&args[1])?;
180            let repl = get_string(&args[2])?;
181            let re = get_cached_regex(&pat)?;
182            Ok(Value::String(re.replace_all(&s, repl).to_string()))
183        }
184        StringOp::Split => {
185            let s = get_string(&args[0])?;
186            let delim = if args.len() > 1 {
187                get_string(&args[1])?
188            } else {
189                ",".to_string()
190            };
191            let parts: Vec<Value> = s.split(&delim).map(|p| Value::String(p.to_string())).collect();
192            Ok(Value::List(parts))
193        }
194        StringOp::Head => {
195            let s = get_string(&args[0])?;
196            let n = if args.len() > 1 {
197                match &args[1] {
198                    Value::Int64(x) => *x as usize,
199                    _ => 1,
200                }
201            } else {
202                1
203            };
204            Ok(Value::String(s.chars().take(n).collect()))
205        }
206        StringOp::Tail => {
207            let s = get_string(&args[0])?;
208            let n = if args.len() > 1 {
209                match &args[1] {
210                    Value::Int64(x) => *x as usize,
211                    _ => 1,
212                }
213            } else {
214                1
215            };
216            let chars: String = s.chars().collect();
217            let start = chars.len().saturating_sub(n);
218            Ok(Value::String(chars.chars().skip(start).collect()))
219        }
220        StringOp::Left => {
221            let s = get_string(&args[0])?;
222            let n = match &args[1] {
223                Value::Int64(x) => *x as usize,
224                _ => return Err("left requires integer length".into()),
225            };
226            Ok(Value::String(s.chars().take(n).collect()))
227        }
228        StringOp::Right => {
229            let s = get_string(&args[0])?;
230            let n = match &args[1] {
231                Value::Int64(x) => *x as usize,
232                _ => return Err("right requires integer length".into()),
233            };
234            let chars: Vec<char> = s.chars().collect();
235            let start = chars.len().saturating_sub(n);
236            Ok(Value::String(chars[start..].iter().collect()))
237        }
238        StringOp::Lpad => {
239            let s = get_string(&args[0])?;
240            let len = match &args[1] {
241                Value::Int64(x) => *x as usize,
242                _ => return Err("lpad requires integer length".into()),
243            };
244            let pad = if args.len() >= 3 {
245                get_string(&args[2])?
246            } else {
247                " ".into()
248            };
249            if s.len() >= len {
250                return Ok(Value::String(s[..len].to_string()));
251            }
252            let pad_needed = len - s.len();
253            let pad_repeat = pad.repeat((pad_needed / pad.len()) + 1);
254            Ok(Value::String(format!("{}{}", &pad_repeat[..pad_needed], s)))
255        }
256        StringOp::Rpad => {
257            let s = get_string(&args[0])?;
258            let len = match &args[1] {
259                Value::Int64(x) => *x as usize,
260                _ => return Err("rpad requires integer length".into()),
261            };
262            let pad = if args.len() >= 3 {
263                get_string(&args[2])?
264            } else {
265                " ".into()
266            };
267            if s.len() >= len {
268                return Ok(Value::String(s[..len].to_string()));
269            }
270            let pad_needed = len - s.len();
271            let pad_repeat = pad.repeat((pad_needed / pad.len()) + 1);
272            Ok(Value::String(format!("{}{}", s, &pad_repeat[..pad_needed])))
273        }
274        // --- String basic (C++ port) ---
275        StringOp::InitCap => {
276            let s = get_string(&args[0])?;
277            let lower = s.to_lowercase();
278            let mut chars = lower.chars();
279            match chars.next() {
280                None => Ok(Value::String(String::new())),
281                Some(c) => Ok(Value::String(c.to_uppercase().collect::<String>() + chars.as_str())),
282            }
283        }
284        StringOp::ConcatWs => {
285            if args.len() < 2 {
286                return Err("concat_ws requires at least 2 arguments (separator + strings)".into());
287            }
288            let separator = get_string(&args[0])?;
289            let mut result = String::new();
290            let mut first = true;
291            for arg in args.iter().skip(1) {
292                match arg {
293                    Value::Null => {
294                        // Skip NULL elements (no separator before or after)
295                        continue;
296                    }
297                    Value::String(s) => {
298                        if !first {
299                            result.push_str(&separator);
300                        }
301                        result.push_str(s);
302                        first = false;
303                    }
304                    _ => {
305                        if !first {
306                            result.push_str(&separator);
307                        }
308                        result.push_str(&format!("{:?}", arg));
309                        first = false;
310                    }
311                }
312            }
313            Ok(Value::String(result))
314        }
315        StringOp::SplitPart => {
316            if args.len() < 3 {
317                return Err("split_part requires 3 arguments (string, delimiter, index)".into());
318            }
319            let s = get_string(&args[0])?;
320            let delim = get_string(&args[1])?;
321            let idx = match &args[2] {
322                Value::Int64(x) => *x,
323                _ => return Err("split_part index must be integer".into()),
324            };
325            // 1-based index, matching C++ semantics
326            let parts: Vec<&str> = s.split(&delim).collect();
327            if idx <= 0 || (idx as usize) > parts.len() {
328                Ok(Value::String(String::new()))
329            } else {
330                Ok(Value::String(parts[(idx - 1) as usize].to_string()))
331            }
332        }
333        StringOp::ArrayExtract => {
334            if args.len() < 2 {
335                return Err("array_extract requires 2 arguments (string, index)".into());
336            }
337            let s = get_string(&args[0])?;
338            let idx = match &args[1] {
339                Value::Int64(x) => *x,
340                _ => return Err("array_extract index must be integer".into()),
341            };
342            let chars: Vec<char> = s.chars().collect();
343            if idx == 0 || chars.is_empty() {
344                Ok(Value::String(String::new()))
345            } else if idx > 0 {
346                // 1-based: clamp to string length
347                let pos = (idx as usize).saturating_sub(1).min(chars.len() - 1);
348                Ok(Value::String(chars[pos].to_string()))
349            } else {
350                // Negative: from end (-1 = last char)
351                let abs_idx = (-idx) as usize;
352                if abs_idx > chars.len() {
353                    Ok(Value::String(String::new()))
354                } else {
355                    let pos = chars.len() - abs_idx;
356                    Ok(Value::String(chars[pos].to_string()))
357                }
358            }
359        }
360        // --- Regex string functions (C++ port) ---
361        StringOp::RegexpFullMatch => {
362            let s = get_string(&args[0])?;
363            let pat = get_string(&args[1])?;
364            let re = get_cached_regex(&pat)?;
365            Ok(Value::Bool(
366                re.find(&s).is_some_and(|m| m.start() == 0 && m.end() == s.len()),
367            ))
368        }
369        StringOp::RegexpExtract => {
370            let s = get_string(&args[0])?;
371            let pat = get_string(&args[1])?;
372            let group = if args.len() > 2 {
373                match &args[2] {
374                    Value::Int64(x) => *x as usize,
375                    _ => return Err("RegexpExtract group must be integer".into()),
376                }
377            } else {
378                0
379            };
380            let re = get_cached_regex(&pat)?;
381            let result = re
382                .captures(&s)
383                .and_then(|caps| caps.get(group))
384                .map(|m| m.as_str().to_string())
385                .unwrap_or_default();
386            Ok(Value::String(result))
387        }
388        StringOp::RegexpExtractAll => {
389            let s = get_string(&args[0])?;
390            let pat = get_string(&args[1])?;
391            let group = if args.len() > 2 {
392                match &args[2] {
393                    Value::Int64(x) => *x as usize,
394                    _ => return Err("RegexpExtractAll group must be integer".into()),
395                }
396            } else {
397                0
398            };
399            let re = get_cached_regex(&pat)?;
400            let matches: Vec<Value> = re
401                .captures_iter(&s)
402                .filter_map(|caps| caps.get(group))
403                .map(|m| Value::String(m.as_str().to_string()))
404                .collect();
405            Ok(Value::List(matches))
406        }
407        StringOp::RegexpSplitToArray => {
408            let s = get_string(&args[0])?;
409            let pat = get_string(&args[1])?;
410            let re = get_cached_regex(&pat)?;
411            let parts: Vec<Value> = re.split(&s).map(|p| Value::String(p.to_string())).collect();
412            Ok(Value::List(parts))
413        }
414        StringOp::Levenshtein => {
415            let a = get_string(&args[0])?;
416            let b = get_string(&args[1])?;
417            let a_chars: Vec<char> = a.chars().collect();
418            let b_chars: Vec<char> = b.chars().collect();
419            let n = b_chars.len();
420            let mut prev_row: Vec<usize> = (0..=n).collect();
421            let mut curr_row = vec![0usize; n + 1];
422            for (i, ca) in a_chars.iter().enumerate() {
423                curr_row[0] = i + 1;
424                for (j, cb) in b_chars.iter().enumerate() {
425                    let cost = if ca == cb { 0 } else { 1 };
426                    curr_row[j + 1] = (curr_row[j] + 1).min(prev_row[j + 1] + 1).min(prev_row[j] + cost);
427                }
428                std::mem::swap(&mut prev_row, &mut curr_row);
429            }
430            Ok(Value::Int64(prev_row[n] as i64))
431        }
432        StringOp::Soundex => {
433            let s = get_string(&args[0])?;
434            Ok(Value::String(soundex_impl(&s)))
435        }
436    }
437}
438
439pub(crate) fn get_string(v: &Value) -> Result<String, String> {
440    match v {
441        Value::String(s) => Ok(s.clone()),
442        Value::Null => Ok("NULL".into()),
443        _ => Err(format!("Expected string, got {:?}", v.logical_type())),
444    }
445}