sql_cli/sql/functions/
format_number.rs

1use crate::data::datatable::DataValue;
2use crate::sql::functions::{ArgCount, FunctionCategory, FunctionSignature, SqlFunction};
3use anyhow::{anyhow, Result};
4
5/// RENDER_NUMBER function - Format numbers with various separators and abbreviations
6pub struct RenderNumberFunction;
7
8impl SqlFunction for RenderNumberFunction {
9    fn signature(&self) -> FunctionSignature {
10        FunctionSignature {
11            name: "RENDER_NUMBER",
12            category: FunctionCategory::String,
13            arg_count: ArgCount::Range(1, 3),
14            description: "Format numbers with separators, abbreviations, or regional formats",
15            returns: "STRING",
16            examples: vec![
17                "SELECT RENDER_NUMBER(1234567.89)",             // "1,234,567.89"
18                "SELECT RENDER_NUMBER(1234567.89, 'compact')",  // "1.2M"
19                "SELECT RENDER_NUMBER(1234.56, 'eu')",          // "1.234,56"
20                "SELECT RENDER_NUMBER(-1234.56, 'accounting')", // "(1,234.56)"
21                "SELECT RENDER_NUMBER(1500000, 'compact', 1)",  // "1.5M"
22            ],
23        }
24    }
25
26    fn evaluate(&self, args: &[DataValue]) -> Result<DataValue> {
27        self.validate_args(args)?;
28
29        let value = match &args[0] {
30            DataValue::Integer(n) => *n as f64,
31            DataValue::Float(f) => *f,
32            DataValue::Null => return Ok(DataValue::Null),
33            _ => return Err(anyhow!("RENDER_NUMBER requires a numeric value")),
34        };
35
36        let format = if args.len() >= 2 {
37            match &args[1] {
38                DataValue::String(s) => s.to_lowercase(),
39                DataValue::Null => "standard".to_string(),
40                _ => "standard".to_string(),
41            }
42        } else {
43            "standard".to_string()
44        };
45
46        let decimals = if args.len() >= 3 {
47            match &args[2] {
48                DataValue::Integer(n) => *n as usize,
49                DataValue::Float(f) => *f as usize,
50                _ => 2,
51            }
52        } else {
53            2
54        };
55
56        let formatted = match format.as_str() {
57            "compact" => format_compact(value, decimals),
58            "eu" | "european" => format_european(value, decimals),
59            "ch" | "swiss" => format_swiss(value, decimals),
60            "in" | "indian" => format_indian(value, decimals),
61            "accounting" => format_accounting(value, decimals),
62            _ => format_standard(value, decimals), // Default US/UK style
63        };
64
65        Ok(DataValue::String(formatted))
66    }
67}
68
69/// FORMAT_CURRENCY function - Format numbers as currency with flexible options
70pub struct FormatCurrencyFunction;
71
72impl SqlFunction for FormatCurrencyFunction {
73    fn signature(&self) -> FunctionSignature {
74        FunctionSignature {
75            name: "FORMAT_CURRENCY",
76            category: FunctionCategory::String,
77            arg_count: ArgCount::Range(2, 4),
78            description: "Format numbers as currency with symbols, codes, or names. Currency can be from a column.",
79            returns: "STRING",
80            examples: vec![
81                "SELECT FORMAT_CURRENCY(1234.56, 'USD')",              // "$1,234.56"
82                "SELECT FORMAT_CURRENCY(1234.56, 'GBP', 'symbol')",    // "£1,234.56"
83                "SELECT FORMAT_CURRENCY(amount, currency_code)",       // Uses currency from column
84                "SELECT FORMAT_CURRENCY(3000, 'GBP', 'compact_code')", // "3k GBP"
85                "SELECT FORMAT_CURRENCY(1234.56, 'EUR', 'eu')",        // "1.234,56 €"
86            ],
87        }
88    }
89
90    fn evaluate(&self, args: &[DataValue]) -> Result<DataValue> {
91        self.validate_args(args)?;
92
93        let value = match &args[0] {
94            DataValue::Integer(n) => *n as f64,
95            DataValue::Float(f) => *f,
96            DataValue::Null => return Ok(DataValue::Null),
97            _ => return Err(anyhow!("FORMAT_CURRENCY requires a numeric value")),
98        };
99
100        let currency_code = match &args[1] {
101            DataValue::String(s) => s.to_uppercase(),
102            DataValue::Null => return Ok(DataValue::Null),
103            _ => {
104                return Err(anyhow!(
105                    "FORMAT_CURRENCY requires a currency code (e.g., 'USD', 'EUR', 'GBP')"
106                ))
107            }
108        };
109
110        let format = if args.len() >= 3 {
111            match &args[2] {
112                DataValue::String(s) => s.to_lowercase(),
113                DataValue::Null => "symbol".to_string(),
114                _ => "symbol".to_string(),
115            }
116        } else {
117            "symbol".to_string()
118        };
119
120        let decimals_override = if args.len() >= 4 {
121            match &args[3] {
122                DataValue::Integer(n) => Some(*n as usize),
123                DataValue::Float(f) => Some(*f as usize),
124                _ => None,
125            }
126        } else {
127            None
128        };
129
130        let currency_info = get_currency_info(&currency_code);
131        let decimals = decimals_override.unwrap_or(currency_info.decimals as usize);
132
133        let formatted = format_currency(value, &currency_code, &format, decimals, &currency_info);
134
135        Ok(DataValue::String(formatted))
136    }
137}
138
139// Currency information structure
140struct CurrencyInfo {
141    symbol: &'static str,
142    name: &'static str,
143    decimals: u8,
144    symbol_before: bool,
145}
146
147// Get currency information
148fn get_currency_info(code: &str) -> CurrencyInfo {
149    match code {
150        "USD" => CurrencyInfo {
151            symbol: "$",
152            name: "US dollar",
153            decimals: 2,
154            symbol_before: true,
155        },
156        "EUR" => CurrencyInfo {
157            symbol: "€",
158            name: "Euro",
159            decimals: 2,
160            symbol_before: true,
161        },
162        "GBP" => CurrencyInfo {
163            symbol: "£",
164            name: "British pound",
165            decimals: 2,
166            symbol_before: true,
167        },
168        "JPY" | "YEN" => CurrencyInfo {
169            symbol: "¥",
170            name: "Japanese yen",
171            decimals: 0,
172            symbol_before: true,
173        },
174        "CHF" => CurrencyInfo {
175            symbol: "CHF",
176            name: "Swiss franc",
177            decimals: 2,
178            symbol_before: true,
179        },
180        "CNY" | "RMB" => CurrencyInfo {
181            symbol: "¥",
182            name: "Chinese yuan",
183            decimals: 2,
184            symbol_before: true,
185        },
186        "INR" => CurrencyInfo {
187            symbol: "₹",
188            name: "Indian rupee",
189            decimals: 2,
190            symbol_before: true,
191        },
192        "AUD" => CurrencyInfo {
193            symbol: "A$",
194            name: "Australian dollar",
195            decimals: 2,
196            symbol_before: true,
197        },
198        "CAD" => CurrencyInfo {
199            symbol: "C$",
200            name: "Canadian dollar",
201            decimals: 2,
202            symbol_before: true,
203        },
204        "SEK" => CurrencyInfo {
205            symbol: "kr",
206            name: "Swedish krona",
207            decimals: 2,
208            symbol_before: false,
209        },
210        "NOK" => CurrencyInfo {
211            symbol: "kr",
212            name: "Norwegian krone",
213            decimals: 2,
214            symbol_before: false,
215        },
216        "DKK" => CurrencyInfo {
217            symbol: "kr",
218            name: "Danish krone",
219            decimals: 2,
220            symbol_before: false,
221        },
222        _ => CurrencyInfo {
223            symbol: "¤", // Generic currency symbol
224            name: "Unknown currency",
225            decimals: 2,
226            symbol_before: false,
227        },
228    }
229}
230
231// Format currency based on options
232fn format_currency(
233    value: f64,
234    code: &str,
235    format: &str,
236    decimals: usize,
237    info: &CurrencyInfo,
238) -> String {
239    let is_negative = value < 0.0;
240    let abs_value = value.abs();
241
242    match format {
243        "accounting" => {
244            // Accounting format: negative numbers in parentheses
245            let formatted = format_standard(abs_value, decimals);
246            if info.symbol_before {
247                if is_negative {
248                    format!("({}{})", info.symbol, formatted)
249                } else {
250                    format!("{}{}", info.symbol, formatted)
251                }
252            } else {
253                if is_negative {
254                    format!("({}{})", formatted, info.symbol)
255                } else {
256                    format!("{}{}", formatted, info.symbol)
257                }
258            }
259        }
260        "accounting_code" => {
261            // Accounting format with currency code
262            let formatted = format_standard(abs_value, decimals);
263            if is_negative {
264                format!("({} {})", formatted, code)
265            } else {
266                format!("{} {}", formatted, code)
267            }
268        }
269        "compact" => {
270            let compact = format_compact(abs_value, decimals.min(1));
271            if info.symbol_before {
272                format!(
273                    "{}{}{}",
274                    if is_negative { "-" } else { "" },
275                    info.symbol,
276                    compact
277                )
278            } else {
279                format!(
280                    "{}{}{}",
281                    if is_negative { "-" } else { "" },
282                    compact,
283                    info.symbol
284                )
285            }
286        }
287        "compact_code" => {
288            let compact = format_compact(abs_value, decimals.min(1));
289            format!("{}{} {}", if is_negative { "-" } else { "" }, compact, code)
290        }
291        "code" => {
292            let formatted = format_standard(abs_value, decimals);
293            format!(
294                "{}{} {}",
295                if is_negative { "-" } else { "" },
296                formatted,
297                code
298            )
299        }
300        "name" => {
301            let formatted = format_standard(abs_value, decimals);
302            let plural = if abs_value != 1.0 { "s" } else { "" };
303            format!(
304                "{}{} {}{}",
305                if is_negative { "-" } else { "" },
306                formatted,
307                info.name,
308                plural
309            )
310        }
311        "eu" | "european" => {
312            let formatted = format_european(abs_value, decimals);
313            if code == "EUR" {
314                format!("{}{} €", if is_negative { "-" } else { "" }, formatted)
315            } else {
316                format!(
317                    "{}{} {}",
318                    if is_negative { "-" } else { "" },
319                    formatted,
320                    code
321                )
322            }
323        }
324        "ch" | "swiss" => {
325            let formatted = format_swiss(abs_value, decimals);
326            format!(
327                "{}{} {}",
328                if is_negative { "-" } else { "" },
329                code,
330                formatted
331            )
332        }
333        _ => {
334            // Default "symbol" format
335            let formatted = format_standard(abs_value, decimals);
336            if info.symbol_before {
337                format!(
338                    "{}{}{}",
339                    if is_negative { "-" } else { "" },
340                    info.symbol,
341                    formatted
342                )
343            } else {
344                format!(
345                    "{}{}{}",
346                    if is_negative { "-" } else { "" },
347                    formatted,
348                    info.symbol
349                )
350            }
351        }
352    }
353}
354
355// Format with US/UK style (comma thousands, period decimal)
356fn format_standard(value: f64, decimals: usize) -> String {
357    let formatted = format!("{:.prec$}", value, prec = decimals);
358    add_separators(&formatted, ',', '.')
359}
360
361// Format with European style (period thousands, comma decimal)
362fn format_european(value: f64, decimals: usize) -> String {
363    let formatted = format!("{:.prec$}", value, prec = decimals);
364    add_separators(&formatted, '.', ',')
365}
366
367// Format with Swiss style (apostrophe thousands, period decimal)
368fn format_swiss(value: f64, decimals: usize) -> String {
369    let formatted = format!("{:.prec$}", value, prec = decimals);
370    add_separators(&formatted, '\'', '.')
371}
372
373// Format with Indian style (lakhs and crores)
374fn format_indian(value: f64, decimals: usize) -> String {
375    let formatted = format!("{:.prec$}", value, prec = decimals);
376    add_indian_separators(&formatted)
377}
378
379// Format with accounting style (negatives in parentheses)
380fn format_accounting(value: f64, decimals: usize) -> String {
381    let is_negative = value < 0.0;
382    let abs_value = value.abs();
383    let formatted = format!("{:.prec$}", abs_value, prec = decimals);
384    let with_separators = add_separators(&formatted, ',', '.');
385
386    if is_negative {
387        format!("({})", with_separators)
388    } else {
389        with_separators
390    }
391}
392
393// Format in compact notation (k, M, B, T)
394fn format_compact(value: f64, decimals: usize) -> String {
395    let (num, suffix) = if value >= 1_000_000_000_000.0 {
396        (value / 1_000_000_000_000.0, "T")
397    } else if value >= 1_000_000_000.0 {
398        (value / 1_000_000_000.0, "B")
399    } else if value >= 1_000_000.0 {
400        (value / 1_000_000.0, "M")
401    } else if value >= 1_000.0 {
402        (value / 1_000.0, "k")
403    } else {
404        (value, "")
405    };
406
407    if suffix.is_empty() {
408        format!("{:.prec$}", num, prec = decimals)
409    } else {
410        // Remove trailing zeros and decimal point if not needed
411        let formatted = format!("{:.prec$}", num, prec = decimals);
412        let trimmed = formatted.trim_end_matches('0').trim_end_matches('.');
413        format!("{}{}", trimmed, suffix)
414    }
415}
416
417// Add thousand separators with specified characters
418fn add_separators(formatted: &str, thousand_sep: char, decimal_sep: char) -> String {
419    let parts: Vec<&str> = formatted.split('.').collect();
420    let integer_part = parts[0];
421    let decimal_part = parts.get(1);
422
423    let mut result = String::new();
424    let mut count = 0;
425
426    for ch in integer_part.chars().rev() {
427        if count == 3 {
428            result.push(thousand_sep);
429            count = 0;
430        }
431        result.push(ch);
432        count += 1;
433    }
434
435    let mut final_result: String = result.chars().rev().collect();
436
437    if let Some(dec) = decimal_part {
438        final_result.push(decimal_sep);
439        final_result.push_str(dec);
440    }
441
442    final_result
443}
444
445// Add Indian-style separators (lakhs and crores)
446fn add_indian_separators(formatted: &str) -> String {
447    let parts: Vec<&str> = formatted.split('.').collect();
448    let integer_part = parts[0];
449    let decimal_part = parts.get(1);
450
451    let mut result = String::new();
452    let chars: Vec<char> = integer_part.chars().rev().collect();
453
454    for (i, ch) in chars.iter().enumerate() {
455        if i == 3 || (i > 3 && i % 2 == 1) {
456            result.push(',');
457        }
458        result.push(*ch);
459    }
460
461    let mut final_result: String = result.chars().rev().collect();
462
463    if let Some(dec) = decimal_part {
464        final_result.push('.');
465        final_result.push_str(dec);
466    }
467
468    final_result
469}
470
471#[cfg(test)]
472mod tests {
473    use super::*;
474
475    #[test]
476    fn test_render_number_standard() {
477        let func = RenderNumberFunction;
478
479        let result = func.evaluate(&[DataValue::Float(1234567.89)]).unwrap();
480        assert_eq!(result, DataValue::String("1,234,567.89".to_string()));
481
482        let result = func.evaluate(&[DataValue::Integer(1000)]).unwrap();
483        assert_eq!(result, DataValue::String("1,000.00".to_string()));
484    }
485
486    #[test]
487    fn test_render_number_compact() {
488        let func = RenderNumberFunction;
489
490        let result = func
491            .evaluate(&[
492                DataValue::Float(1500000.0),
493                DataValue::String("compact".to_string()),
494            ])
495            .unwrap();
496        assert_eq!(result, DataValue::String("1.5M".to_string()));
497
498        let result = func
499            .evaluate(&[
500                DataValue::Integer(3000),
501                DataValue::String("compact".to_string()),
502            ])
503            .unwrap();
504        assert_eq!(result, DataValue::String("3k".to_string()));
505    }
506
507    #[test]
508    fn test_format_currency() {
509        let func = FormatCurrencyFunction;
510
511        let result = func
512            .evaluate(&[
513                DataValue::Float(1234.56),
514                DataValue::String("USD".to_string()),
515            ])
516            .unwrap();
517        assert_eq!(result, DataValue::String("$1,234.56".to_string()));
518
519        let result = func
520            .evaluate(&[
521                DataValue::Float(1234.56),
522                DataValue::String("GBP".to_string()),
523            ])
524            .unwrap();
525        assert_eq!(result, DataValue::String("£1,234.56".to_string()));
526
527        let result = func
528            .evaluate(&[
529                DataValue::Integer(3000),
530                DataValue::String("GBP".to_string()),
531                DataValue::String("compact_code".to_string()),
532            ])
533            .unwrap();
534        assert_eq!(result, DataValue::String("3k GBP".to_string()));
535    }
536}