sql_cli/sql/functions/
string_fun.rs

1use crate::data::datatable::DataValue;
2use crate::sql::functions::{ArgCount, FunctionCategory, FunctionSignature, SqlFunction};
3use anyhow::{anyhow, Result};
4use std::collections::HashMap;
5
6/// REVERSE(string) - Reverses a string
7pub struct ReverseFunction;
8
9impl SqlFunction for ReverseFunction {
10    fn signature(&self) -> FunctionSignature {
11        FunctionSignature {
12            name: "REVERSE",
13            category: FunctionCategory::String,
14            arg_count: ArgCount::Fixed(1),
15            description: "Reverses the characters in a string",
16            returns: "Reversed string",
17            examples: vec![
18                "SELECT REVERSE('hello') -- returns 'olleh'",
19                "SELECT REVERSE('SQL CLI') -- returns 'ILC LQS'",
20            ],
21        }
22    }
23
24    fn evaluate(&self, args: &[DataValue]) -> Result<DataValue> {
25        if args.len() != 1 {
26            return Err(anyhow!("REVERSE requires exactly 1 argument"));
27        }
28
29        match &args[0] {
30            DataValue::String(s) => Ok(DataValue::String(s.chars().rev().collect())),
31            DataValue::InternedString(s) => Ok(DataValue::String(s.chars().rev().collect())),
32            DataValue::Null => Ok(DataValue::Null),
33            _ => Err(anyhow!("REVERSE requires a string argument")),
34        }
35    }
36}
37
38/// INITCAP(string) / PROPER(string) - Capitalizes the first letter of each word
39pub struct InitCapFunction;
40
41impl SqlFunction for InitCapFunction {
42    fn signature(&self) -> FunctionSignature {
43        FunctionSignature {
44            name: "INITCAP",
45            category: FunctionCategory::String,
46            arg_count: ArgCount::Fixed(1),
47            description: "Capitalizes the first letter of each word",
48            returns: "String with each word capitalized",
49            examples: vec![
50                "SELECT INITCAP('hello world') -- returns 'Hello World'",
51                "SELECT INITCAP('sql-cli is great') -- returns 'Sql-Cli Is Great'",
52            ],
53        }
54    }
55
56    fn evaluate(&self, args: &[DataValue]) -> Result<DataValue> {
57        if args.len() != 1 {
58            return Err(anyhow!("INITCAP requires exactly 1 argument"));
59        }
60
61        match &args[0] {
62            DataValue::String(s) => {
63                let mut result = String::new();
64                let mut capitalize_next = true;
65
66                for ch in s.chars() {
67                    if ch.is_alphabetic() {
68                        if capitalize_next {
69                            result.push(ch.to_uppercase().next().unwrap_or(ch));
70                            capitalize_next = false;
71                        } else {
72                            result.push(ch.to_lowercase().next().unwrap_or(ch));
73                        }
74                    } else {
75                        result.push(ch);
76                        capitalize_next = !ch.is_ascii_alphanumeric();
77                    }
78                }
79
80                Ok(DataValue::String(result))
81            }
82            DataValue::InternedString(s) => {
83                let mut result = String::new();
84                let mut capitalize_next = true;
85
86                for ch in s.chars() {
87                    if ch.is_alphabetic() {
88                        if capitalize_next {
89                            result.push(ch.to_uppercase().next().unwrap_or(ch));
90                            capitalize_next = false;
91                        } else {
92                            result.push(ch.to_lowercase().next().unwrap_or(ch));
93                        }
94                    } else {
95                        result.push(ch);
96                        capitalize_next = !ch.is_ascii_alphanumeric();
97                    }
98                }
99
100                Ok(DataValue::String(result))
101            }
102            DataValue::Null => Ok(DataValue::Null),
103            _ => Err(anyhow!("INITCAP requires a string argument")),
104        }
105    }
106}
107
108/// PROPER(string) - Alias for INITCAP
109pub struct ProperFunction;
110
111impl SqlFunction for ProperFunction {
112    fn signature(&self) -> FunctionSignature {
113        FunctionSignature {
114            name: "PROPER",
115            category: FunctionCategory::String,
116            arg_count: ArgCount::Fixed(1),
117            description: "Alias for INITCAP - capitalizes first letter of each word",
118            returns: "String with each word capitalized",
119            examples: vec!["SELECT PROPER('hello world') -- returns 'Hello World'"],
120        }
121    }
122
123    fn evaluate(&self, args: &[DataValue]) -> Result<DataValue> {
124        InitCapFunction.evaluate(args)
125    }
126}
127
128/// ROT13(string) - ROT13 encoding
129pub struct Rot13Function;
130
131impl SqlFunction for Rot13Function {
132    fn signature(&self) -> FunctionSignature {
133        FunctionSignature {
134            name: "ROT13",
135            category: FunctionCategory::String,
136            arg_count: ArgCount::Fixed(1),
137            description: "Applies ROT13 encoding (shifts letters by 13 positions)",
138            returns: "ROT13 encoded string",
139            examples: vec![
140                "SELECT ROT13('hello') -- returns 'uryyb'",
141                "SELECT ROT13('uryyb') -- returns 'hello' (ROT13 is reversible)",
142                "SELECT ROT13('SQL123') -- returns 'FDY123' (numbers unchanged)",
143            ],
144        }
145    }
146
147    fn evaluate(&self, args: &[DataValue]) -> Result<DataValue> {
148        if args.len() != 1 {
149            return Err(anyhow!("ROT13 requires exactly 1 argument"));
150        }
151
152        match &args[0] {
153            DataValue::String(s) => {
154                let result: String = s
155                    .chars()
156                    .map(|ch| match ch {
157                        'A'..='M' | 'a'..='m' => ((ch as u8) + 13) as char,
158                        'N'..='Z' | 'n'..='z' => ((ch as u8) - 13) as char,
159                        _ => ch,
160                    })
161                    .collect();
162                Ok(DataValue::String(result))
163            }
164            DataValue::InternedString(s) => {
165                let result: String = s
166                    .chars()
167                    .map(|ch| match ch {
168                        'A'..='M' | 'a'..='m' => ((ch as u8) + 13) as char,
169                        'N'..='Z' | 'n'..='z' => ((ch as u8) - 13) as char,
170                        _ => ch,
171                    })
172                    .collect();
173                Ok(DataValue::String(result))
174            }
175            DataValue::Null => Ok(DataValue::Null),
176            _ => Err(anyhow!("ROT13 requires a string argument")),
177        }
178    }
179}
180
181/// SOUNDEX(string) - Soundex phonetic encoding
182pub struct SoundexFunction;
183
184impl SqlFunction for SoundexFunction {
185    fn signature(&self) -> FunctionSignature {
186        FunctionSignature {
187            name: "SOUNDEX",
188            category: FunctionCategory::String,
189            arg_count: ArgCount::Fixed(1),
190            description: "Returns the Soundex code for phonetic matching",
191            returns: "4-character Soundex code",
192            examples: vec![
193                "SELECT SOUNDEX('Smith') -- returns 'S530'",
194                "SELECT SOUNDEX('Smythe') -- returns 'S530' (sounds similar)",
195                "SELECT SOUNDEX('Johnson') -- returns 'J525'",
196                "SELECT SOUNDEX('Jonson') -- returns 'J525' (sounds similar)",
197            ],
198        }
199    }
200
201    fn evaluate(&self, args: &[DataValue]) -> Result<DataValue> {
202        if args.len() != 1 {
203            return Err(anyhow!("SOUNDEX requires exactly 1 argument"));
204        }
205
206        match &args[0] {
207            DataValue::String(s) => Ok(DataValue::String(compute_soundex(s))),
208            DataValue::InternedString(s) => Ok(DataValue::String(compute_soundex(s))),
209            DataValue::Null => Ok(DataValue::Null),
210            _ => Err(anyhow!("SOUNDEX requires a string argument")),
211        }
212    }
213}
214
215fn compute_soundex(s: &str) -> String {
216    if s.is_empty() {
217        return String::new();
218    }
219
220    let s_upper = s.to_uppercase();
221    let mut result = String::new();
222
223    // Keep the first letter
224    if let Some(first_char) = s_upper.chars().next() {
225        if first_char.is_alphabetic() {
226            result.push(first_char);
227        } else {
228            return String::new();
229        }
230    }
231
232    // Soundex digit mapping
233    let soundex_map: HashMap<char, char> = [
234        ('B', '1'),
235        ('F', '1'),
236        ('P', '1'),
237        ('V', '1'),
238        ('C', '2'),
239        ('G', '2'),
240        ('J', '2'),
241        ('K', '2'),
242        ('Q', '2'),
243        ('S', '2'),
244        ('X', '2'),
245        ('Z', '2'),
246        ('D', '3'),
247        ('T', '3'),
248        ('L', '4'),
249        ('M', '5'),
250        ('N', '5'),
251        ('R', '6'),
252    ]
253    .iter()
254    .cloned()
255    .collect();
256
257    let mut last_code = ' ';
258    for ch in s_upper.chars().skip(1) {
259        if let Some(&code) = soundex_map.get(&ch) {
260            if code != last_code {
261                result.push(code);
262                last_code = code;
263            }
264        } else if "AEIOUYHW".contains(ch) {
265            last_code = ' ';
266        }
267
268        if result.len() >= 4 {
269            break;
270        }
271    }
272
273    // Pad with zeros to length 4
274    while result.len() < 4 {
275        result.push('0');
276    }
277
278    result.chars().take(4).collect()
279}
280
281/// PIG_LATIN(string) - Convert to Pig Latin
282pub struct PigLatinFunction;
283
284impl SqlFunction for PigLatinFunction {
285    fn signature(&self) -> FunctionSignature {
286        FunctionSignature {
287            name: "PIG_LATIN",
288            category: FunctionCategory::String,
289            arg_count: ArgCount::Fixed(1),
290            description: "Converts text to Pig Latin",
291            returns: "Pig Latin version of the text",
292            examples: vec![
293                "SELECT PIG_LATIN('hello') -- returns 'ellohay'",
294                "SELECT PIG_LATIN('apple') -- returns 'appleway'",
295                "SELECT PIG_LATIN('SQL') -- returns 'SQLay'",
296                "SELECT PIG_LATIN('hello world') -- returns 'ellohay orldway'",
297            ],
298        }
299    }
300
301    fn evaluate(&self, args: &[DataValue]) -> Result<DataValue> {
302        if args.len() != 1 {
303            return Err(anyhow!("PIG_LATIN requires exactly 1 argument"));
304        }
305
306        match &args[0] {
307            DataValue::String(s) => {
308                let result: Vec<String> = s
309                    .split_whitespace()
310                    .map(|word| pig_latin_word(word))
311                    .collect();
312                Ok(DataValue::String(result.join(" ")))
313            }
314            DataValue::InternedString(s) => {
315                let result: Vec<String> = s
316                    .split_whitespace()
317                    .map(|word| pig_latin_word(word))
318                    .collect();
319                Ok(DataValue::String(result.join(" ")))
320            }
321            DataValue::Null => Ok(DataValue::Null),
322            _ => Err(anyhow!("PIG_LATIN requires a string argument")),
323        }
324    }
325}
326
327fn pig_latin_word(word: &str) -> String {
328    if word.is_empty() {
329        return String::new();
330    }
331
332    let vowels = "aeiouAEIOU";
333    let word_lower = word.to_lowercase();
334
335    // Check if word starts with a vowel
336    if let Some(first_char) = word_lower.chars().next() {
337        if vowels.contains(first_char) {
338            return format!("{}way", word);
339        }
340    }
341
342    // Find the first vowel position
343    if let Some(vowel_pos) = word_lower.chars().position(|c| vowels.contains(c)) {
344        if vowel_pos > 0 {
345            let consonant_cluster = &word[..vowel_pos];
346            let rest = &word[vowel_pos..];
347            return format!("{}{}ay", rest, consonant_cluster.to_lowercase());
348        }
349    }
350
351    // No vowels found, just add 'ay'
352    format!("{}ay", word.to_lowercase())
353}
354
355/// MORSE_CODE(string) - Convert to Morse code
356pub struct MorseCodeFunction;
357
358impl SqlFunction for MorseCodeFunction {
359    fn signature(&self) -> FunctionSignature {
360        FunctionSignature {
361            name: "MORSE_CODE",
362            category: FunctionCategory::String,
363            arg_count: ArgCount::Fixed(1),
364            description: "Converts text to Morse code",
365            returns: "Morse code representation",
366            examples: vec![
367                "SELECT MORSE_CODE('SOS') -- returns '... --- ...'",
368                "SELECT MORSE_CODE('HELLO') -- returns '.... . .-.. .-.. ---'",
369                "SELECT MORSE_CODE('SQL 123') -- returns '... --.- .-..   .---- ..--- ...--'",
370            ],
371        }
372    }
373
374    fn evaluate(&self, args: &[DataValue]) -> Result<DataValue> {
375        if args.len() != 1 {
376            return Err(anyhow!("MORSE_CODE requires exactly 1 argument"));
377        }
378
379        match &args[0] {
380            DataValue::String(s) => Ok(DataValue::String(to_morse_code(s))),
381            DataValue::InternedString(s) => Ok(DataValue::String(to_morse_code(s))),
382            DataValue::Null => Ok(DataValue::Null),
383            _ => Err(anyhow!("MORSE_CODE requires a string argument")),
384        }
385    }
386}
387
388fn to_morse_code(text: &str) -> String {
389    let morse_map: HashMap<char, &str> = [
390        ('A', ".-"),
391        ('B', "-..."),
392        ('C', "-.-."),
393        ('D', "-.."),
394        ('E', "."),
395        ('F', "..-."),
396        ('G', "--."),
397        ('H', "...."),
398        ('I', ".."),
399        ('J', ".---"),
400        ('K', "-.-"),
401        ('L', ".-.."),
402        ('M', "--"),
403        ('N', "-."),
404        ('O', "---"),
405        ('P', ".--."),
406        ('Q', "--.-"),
407        ('R', ".-."),
408        ('S', "..."),
409        ('T', "-"),
410        ('U', "..-"),
411        ('V', "...-"),
412        ('W', ".--"),
413        ('X', "-..-"),
414        ('Y', "-.--"),
415        ('Z', "--.."),
416        ('0', "-----"),
417        ('1', ".----"),
418        ('2', "..---"),
419        ('3', "...--"),
420        ('4', "....-"),
421        ('5', "....."),
422        ('6', "-...."),
423        ('7', "--..."),
424        ('8', "---.."),
425        ('9', "----."),
426        (' ', " "),
427    ]
428    .iter()
429    .cloned()
430    .collect();
431
432    text.to_uppercase()
433        .chars()
434        .filter_map(|ch| morse_map.get(&ch).copied())
435        .collect::<Vec<_>>()
436        .join(" ")
437        .replace("   ", "  ") // Collapse multiple spaces between words
438}
439
440/// SCRAMBLE(string) - Scrambles the letters in a string (keeping first and last)
441pub struct ScrambleFunction;
442
443impl SqlFunction for ScrambleFunction {
444    fn signature(&self) -> FunctionSignature {
445        FunctionSignature {
446            name: "SCRAMBLE",
447            category: FunctionCategory::String,
448            arg_count: ArgCount::Fixed(1),
449            description: "Scrambles letters in words (keeps first and last letter)",
450            returns: "Scrambled text that's still somewhat readable",
451            examples: vec![
452                "SELECT SCRAMBLE('hello') -- might return 'hlelo'",
453                "SELECT SCRAMBLE('according') -- might return 'acdorcnig'",
454                "SELECT SCRAMBLE('The quick brown fox') -- scrambles each word",
455            ],
456        }
457    }
458
459    fn evaluate(&self, args: &[DataValue]) -> Result<DataValue> {
460        if args.len() != 1 {
461            return Err(anyhow!("SCRAMBLE requires exactly 1 argument"));
462        }
463
464        match &args[0] {
465            DataValue::String(s) => Ok(DataValue::String(scramble_text(s))),
466            DataValue::InternedString(s) => Ok(DataValue::String(scramble_text(s))),
467            DataValue::Null => Ok(DataValue::Null),
468            _ => Err(anyhow!("SCRAMBLE requires a string argument")),
469        }
470    }
471}
472
473fn scramble_text(text: &str) -> String {
474    use rand::seq::SliceRandom;
475    use rand::thread_rng;
476
477    let words: Vec<String> = text
478        .split_whitespace()
479        .map(|word| {
480            if word.len() <= 3 {
481                word.to_string()
482            } else {
483                let chars: Vec<char> = word.chars().collect();
484                let first = chars[0];
485                let last = chars[chars.len() - 1];
486                let mut middle: Vec<char> = chars[1..chars.len() - 1].to_vec();
487                middle.shuffle(&mut thread_rng());
488
489                let mut result = String::new();
490                result.push(first);
491                result.extend(middle);
492                result.push(last);
493                result
494            }
495        })
496        .collect();
497
498    words.join(" ")
499}