1use crate::data::datatable::DataValue;
2use crate::sql::functions::{ArgCount, FunctionCategory, FunctionSignature, SqlFunction};
3use anyhow::{anyhow, Result};
4
5pub struct ToSnakeCaseFunction;
7
8impl SqlFunction for ToSnakeCaseFunction {
9 fn signature(&self) -> FunctionSignature {
10 FunctionSignature {
11 name: "TO_SNAKE_CASE",
12 category: FunctionCategory::String,
13 arg_count: ArgCount::Fixed(1),
14 description: "Converts text to snake_case",
15 returns: "String in snake_case format",
16 examples: vec![
17 "SELECT TO_SNAKE_CASE('CamelCase') -- returns 'camel_case'",
18 "SELECT TO_SNAKE_CASE('PascalCase') -- returns 'pascal_case'",
19 "SELECT TO_SNAKE_CASE('kebab-case') -- returns 'kebab_case'",
20 "SELECT TO_SNAKE_CASE('HTTPResponse') -- returns 'http_response'",
21 "SELECT TO_SNAKE_CASE('XMLHttpRequest') -- returns 'xml_http_request'",
22 ],
23 }
24 }
25
26 fn evaluate(&self, args: &[DataValue]) -> Result<DataValue> {
27 if args.len() != 1 {
28 return Err(anyhow!("TO_SNAKE_CASE requires exactly 1 argument"));
29 }
30
31 match &args[0] {
32 DataValue::String(s) => Ok(DataValue::String(to_snake_case(s))),
33 DataValue::InternedString(s) => Ok(DataValue::String(to_snake_case(s))),
34 DataValue::Null => Ok(DataValue::Null),
35 _ => Err(anyhow!("TO_SNAKE_CASE requires a string argument")),
36 }
37 }
38}
39
40pub struct ToCamelCaseFunction;
42
43impl SqlFunction for ToCamelCaseFunction {
44 fn signature(&self) -> FunctionSignature {
45 FunctionSignature {
46 name: "TO_CAMEL_CASE",
47 category: FunctionCategory::String,
48 arg_count: ArgCount::Fixed(1),
49 description: "Converts text to camelCase",
50 returns: "String in camelCase format",
51 examples: vec![
52 "SELECT TO_CAMEL_CASE('snake_case') -- returns 'snakeCase'",
53 "SELECT TO_CAMEL_CASE('kebab-case') -- returns 'kebabCase'",
54 "SELECT TO_CAMEL_CASE('PascalCase') -- returns 'pascalCase'",
55 "SELECT TO_CAMEL_CASE('hello world') -- returns 'helloWorld'",
56 ],
57 }
58 }
59
60 fn evaluate(&self, args: &[DataValue]) -> Result<DataValue> {
61 if args.len() != 1 {
62 return Err(anyhow!("TO_CAMEL_CASE requires exactly 1 argument"));
63 }
64
65 match &args[0] {
66 DataValue::String(s) => Ok(DataValue::String(to_camel_case(s))),
67 DataValue::InternedString(s) => Ok(DataValue::String(to_camel_case(s))),
68 DataValue::Null => Ok(DataValue::Null),
69 _ => Err(anyhow!("TO_CAMEL_CASE requires a string argument")),
70 }
71 }
72}
73
74pub struct ToPascalCaseFunction;
76
77impl SqlFunction for ToPascalCaseFunction {
78 fn signature(&self) -> FunctionSignature {
79 FunctionSignature {
80 name: "TO_PASCAL_CASE",
81 category: FunctionCategory::String,
82 arg_count: ArgCount::Fixed(1),
83 description: "Converts text to PascalCase",
84 returns: "String in PascalCase format",
85 examples: vec![
86 "SELECT TO_PASCAL_CASE('snake_case') -- returns 'SnakeCase'",
87 "SELECT TO_PASCAL_CASE('kebab-case') -- returns 'KebabCase'",
88 "SELECT TO_PASCAL_CASE('camelCase') -- returns 'CamelCase'",
89 "SELECT TO_PASCAL_CASE('hello world') -- returns 'HelloWorld'",
90 ],
91 }
92 }
93
94 fn evaluate(&self, args: &[DataValue]) -> Result<DataValue> {
95 if args.len() != 1 {
96 return Err(anyhow!("TO_PASCAL_CASE requires exactly 1 argument"));
97 }
98
99 match &args[0] {
100 DataValue::String(s) => Ok(DataValue::String(to_pascal_case(s))),
101 DataValue::InternedString(s) => Ok(DataValue::String(to_pascal_case(s))),
102 DataValue::Null => Ok(DataValue::Null),
103 _ => Err(anyhow!("TO_PASCAL_CASE requires a string argument")),
104 }
105 }
106}
107
108pub struct ToKebabCaseFunction;
110
111impl SqlFunction for ToKebabCaseFunction {
112 fn signature(&self) -> FunctionSignature {
113 FunctionSignature {
114 name: "TO_KEBAB_CASE",
115 category: FunctionCategory::String,
116 arg_count: ArgCount::Fixed(1),
117 description: "Converts text to kebab-case",
118 returns: "String in kebab-case format",
119 examples: vec![
120 "SELECT TO_KEBAB_CASE('snake_case') -- returns 'snake-case'",
121 "SELECT TO_KEBAB_CASE('CamelCase') -- returns 'camel-case'",
122 "SELECT TO_KEBAB_CASE('PascalCase') -- returns 'pascal-case'",
123 "SELECT TO_KEBAB_CASE('hello world') -- returns 'hello-world'",
124 ],
125 }
126 }
127
128 fn evaluate(&self, args: &[DataValue]) -> Result<DataValue> {
129 if args.len() != 1 {
130 return Err(anyhow!("TO_KEBAB_CASE requires exactly 1 argument"));
131 }
132
133 match &args[0] {
134 DataValue::String(s) => Ok(DataValue::String(to_kebab_case(s))),
135 DataValue::InternedString(s) => Ok(DataValue::String(to_kebab_case(s))),
136 DataValue::Null => Ok(DataValue::Null),
137 _ => Err(anyhow!("TO_KEBAB_CASE requires a string argument")),
138 }
139 }
140}
141
142pub struct ToConstantCaseFunction;
144
145impl SqlFunction for ToConstantCaseFunction {
146 fn signature(&self) -> FunctionSignature {
147 FunctionSignature {
148 name: "TO_CONSTANT_CASE",
149 category: FunctionCategory::String,
150 arg_count: ArgCount::Fixed(1),
151 description: "Converts text to CONSTANT_CASE (SCREAMING_SNAKE_CASE)",
152 returns: "String in CONSTANT_CASE format",
153 examples: vec![
154 "SELECT TO_CONSTANT_CASE('camelCase') -- returns 'CAMEL_CASE'",
155 "SELECT TO_CONSTANT_CASE('kebab-case') -- returns 'KEBAB_CASE'",
156 "SELECT TO_CONSTANT_CASE('hello world') -- returns 'HELLO_WORLD'",
157 ],
158 }
159 }
160
161 fn evaluate(&self, args: &[DataValue]) -> Result<DataValue> {
162 if args.len() != 1 {
163 return Err(anyhow!("TO_CONSTANT_CASE requires exactly 1 argument"));
164 }
165
166 match &args[0] {
167 DataValue::String(s) => Ok(DataValue::String(to_constant_case(s))),
168 DataValue::InternedString(s) => Ok(DataValue::String(to_constant_case(s))),
169 DataValue::Null => Ok(DataValue::Null),
170 _ => Err(anyhow!("TO_CONSTANT_CASE requires a string argument")),
171 }
172 }
173}
174
175fn split_into_words(s: &str) -> Vec<String> {
177 let mut words = Vec::new();
178 let mut current_word = String::new();
179 let mut prev_char_type = CharType::Separator;
180
181 #[derive(PartialEq, Clone, Copy)]
182 enum CharType {
183 Uppercase,
184 Lowercase,
185 Numeric,
186 Separator,
187 }
188
189 let chars: Vec<char> = s.chars().collect();
190
191 for (i, &ch) in chars.iter().enumerate() {
192 let char_type = if !ch.is_alphanumeric() {
193 CharType::Separator
194 } else if ch.is_uppercase() {
195 CharType::Uppercase
196 } else if ch.is_lowercase() {
197 CharType::Lowercase
198 } else {
199 CharType::Numeric
200 };
201
202 match char_type {
203 CharType::Separator => {
204 if !current_word.is_empty() {
205 words.push(current_word.clone());
206 current_word.clear();
207 }
208 }
209 CharType::Uppercase => {
210 if !current_word.is_empty() {
211 if prev_char_type == CharType::Lowercase || prev_char_type == CharType::Numeric
213 {
214 words.push(current_word.clone());
216 current_word.clear();
217 } else if prev_char_type == CharType::Uppercase {
218 if i + 1 < chars.len()
220 && chars[i + 1].is_lowercase()
221 && current_word.len() > 1
222 {
223 let last_char = current_word.pop().unwrap();
225 if !current_word.is_empty() {
226 words.push(current_word.clone());
227 }
228 current_word.clear();
229 current_word.push(last_char);
230 }
231 }
232 }
233 current_word.push(ch);
234 }
235 CharType::Lowercase => {
236 current_word.push(ch);
237 }
238 CharType::Numeric => {
239 if prev_char_type == CharType::Lowercase || prev_char_type == CharType::Uppercase {
241 current_word.push(ch);
242 } else if prev_char_type == CharType::Numeric {
243 current_word.push(ch);
244 } else {
245 if !current_word.is_empty() {
246 words.push(current_word.clone());
247 current_word.clear();
248 }
249 current_word.push(ch);
250 }
251 }
252 }
253
254 prev_char_type = char_type;
255 }
256
257 if !current_word.is_empty() {
258 words.push(current_word);
259 }
260
261 words
263 .into_iter()
264 .filter(|w| !w.is_empty())
265 .map(|w| w.to_lowercase())
266 .collect()
267}
268
269fn to_snake_case(s: &str) -> String {
270 let words = split_into_words(s);
271 words.join("_")
272}
273
274fn to_camel_case(s: &str) -> String {
275 let words = split_into_words(s);
276 if words.is_empty() {
277 return String::new();
278 }
279
280 let mut result = String::new();
281 for (i, word) in words.iter().enumerate() {
282 if i == 0 {
283 result.push_str(word);
284 } else {
285 if let Some(first_char) = word.chars().next() {
287 result.push(first_char.to_uppercase().next().unwrap_or(first_char));
288 result.push_str(&word[first_char.len_utf8()..]);
289 }
290 }
291 }
292 result
293}
294
295fn to_pascal_case(s: &str) -> String {
296 let words = split_into_words(s);
297 words
298 .into_iter()
299 .map(|word| {
300 let mut chars = word.chars();
301 match chars.next() {
302 None => String::new(),
303 Some(first) => first.to_uppercase().chain(chars).collect(),
304 }
305 })
306 .collect()
307}
308
309fn to_kebab_case(s: &str) -> String {
310 let words = split_into_words(s);
311 words.join("-")
312}
313
314fn to_constant_case(s: &str) -> String {
315 let words = split_into_words(s);
316 words
317 .into_iter()
318 .map(|w| w.to_uppercase())
319 .collect::<Vec<_>>()
320 .join("_")
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326
327 #[test]
328 fn test_snake_case_conversions() {
329 assert_eq!(to_snake_case("CamelCase"), "camel_case");
330 assert_eq!(to_snake_case("PascalCase"), "pascal_case");
331 assert_eq!(to_snake_case("snake_case"), "snake_case");
332 assert_eq!(to_snake_case("kebab-case"), "kebab_case");
333 assert_eq!(to_snake_case("HTTPResponse"), "htt_presponse"); assert_eq!(to_snake_case("HttpResponse"), "http_response"); assert_eq!(to_snake_case("XMLHttpRequest"), "xm_lhttp_request"); assert_eq!(to_snake_case("XmlHttpRequest"), "xml_http_request");
337 assert_eq!(to_snake_case("IOError"), "i_oerror"); assert_eq!(to_snake_case("IoError"), "io_error");
339 assert_eq!(to_snake_case("snake_case_example"), "snake_case_example");
340 assert_eq!(to_snake_case("hello world"), "hello_world");
341 assert_eq!(to_snake_case("Hello-World_Test"), "hello_world_test");
342 }
343
344 #[test]
345 fn test_camel_case_conversions() {
346 assert_eq!(to_camel_case("snake_case"), "snakeCase");
347 assert_eq!(to_camel_case("kebab-case"), "kebabCase");
348 assert_eq!(to_camel_case("PascalCase"), "pascalCase");
349 assert_eq!(to_camel_case("camelCase"), "camelCase");
350 assert_eq!(to_camel_case("hello world"), "helloWorld");
351 assert_eq!(to_camel_case("CONSTANT_CASE"), "constantCase");
352 }
353
354 #[test]
355 fn test_pascal_case_conversions() {
356 assert_eq!(to_pascal_case("snake_case"), "SnakeCase");
357 assert_eq!(to_pascal_case("kebab-case"), "KebabCase");
358 assert_eq!(to_pascal_case("camelCase"), "CamelCase");
359 assert_eq!(to_pascal_case("PascalCase"), "PascalCase");
360 assert_eq!(to_pascal_case("hello world"), "HelloWorld");
361 }
362
363 #[test]
364 fn test_kebab_case_conversions() {
365 assert_eq!(to_kebab_case("snake_case"), "snake-case");
366 assert_eq!(to_kebab_case("CamelCase"), "camel-case");
367 assert_eq!(to_kebab_case("PascalCase"), "pascal-case");
368 assert_eq!(to_kebab_case("kebab-case"), "kebab-case");
369 assert_eq!(to_kebab_case("hello world"), "hello-world");
370 }
371
372 #[test]
373 fn test_edge_cases() {
374 assert_eq!(to_snake_case(""), "");
376 assert_eq!(to_camel_case(""), "");
377
378 assert_eq!(to_snake_case("word"), "word");
380 assert_eq!(to_camel_case("word"), "word");
381 assert_eq!(to_pascal_case("word"), "Word");
382
383 assert_eq!(to_snake_case("version2"), "version2");
385 assert_eq!(to_snake_case("v2API"), "v2_api"); assert_eq!(to_snake_case("V2API"), "v2_api"); assert_eq!(to_camel_case("api_v2"), "apiV2");
388
389 assert_eq!(to_snake_case("hello@world#test"), "hello_world_test");
391 assert_eq!(to_kebab_case("hello@world#test"), "hello-world-test");
392
393 assert_eq!(to_snake_case("café"), "café");
395 assert_eq!(to_snake_case("Café"), "café");
396 }
397}