sql_cli/sql/functions/
roman.rs

1use crate::data::datatable::DataValue;
2use crate::sql::functions::{ArgCount, FunctionCategory, FunctionSignature, SqlFunction};
3use anyhow::Result;
4
5/// Convert integer to Roman numerals
6pub struct ToRoman;
7
8impl SqlFunction for ToRoman {
9    fn signature(&self) -> FunctionSignature {
10        FunctionSignature {
11            name: "TO_ROMAN",
12            category: FunctionCategory::Conversion,
13            arg_count: ArgCount::Fixed(1),
14            description: "Convert integer to Roman numerals (1-3999)",
15            returns: "String with Roman numeral representation",
16            examples: vec![
17                "SELECT TO_ROMAN(2024)     -- Returns 'MMXXIV'",
18                "SELECT TO_ROMAN(1984)     -- Returns 'MCMLXXXIV'",
19                "SELECT TO_ROMAN(49)       -- Returns 'XLIX'",
20            ],
21        }
22    }
23
24    fn evaluate(&self, args: &[DataValue]) -> Result<DataValue> {
25        if args.len() != 1 {
26            return Ok(DataValue::Null);
27        }
28
29        let num = match &args[0] {
30            DataValue::Float(n) => *n as i32,
31            DataValue::Integer(n) => *n as i32,
32            DataValue::Null => return Ok(DataValue::Null),
33            _ => return Ok(DataValue::Null),
34        };
35
36        if num <= 0 || num > 3999 {
37            return Ok(DataValue::String(format!("OUT_OF_RANGE({})", num)));
38        }
39
40        Ok(DataValue::String(int_to_roman(num)))
41    }
42}
43
44/// Convert Roman numerals to integer
45pub struct FromRoman;
46
47impl SqlFunction for FromRoman {
48    fn signature(&self) -> FunctionSignature {
49        FunctionSignature {
50            name: "FROM_ROMAN",
51            category: FunctionCategory::Conversion,
52            arg_count: ArgCount::Fixed(1),
53            description: "Convert Roman numerals to integer",
54            returns: "Integer value of Roman numeral",
55            examples: vec![
56                "SELECT FROM_ROMAN('MMXXIV')     -- Returns 2024",
57                "SELECT FROM_ROMAN('MCMLXXXIV')  -- Returns 1984",
58                "SELECT FROM_ROMAN('XLIX')       -- Returns 49",
59            ],
60        }
61    }
62
63    fn evaluate(&self, args: &[DataValue]) -> Result<DataValue> {
64        if args.len() != 1 {
65            return Ok(DataValue::Null);
66        }
67
68        let roman = match &args[0] {
69            DataValue::String(s) => s.to_uppercase(),
70            DataValue::Null => return Ok(DataValue::Null),
71            _ => return Ok(DataValue::Null),
72        };
73
74        match roman_to_int(&roman) {
75            Some(n) => Ok(DataValue::Float(n as f64)),
76            None => Ok(DataValue::Null),
77        }
78    }
79}
80
81/// Convert integer to Roman numerals
82fn int_to_roman(mut num: i32) -> String {
83    let values = [
84        (1000, "M"),
85        (900, "CM"),
86        (500, "D"),
87        (400, "CD"),
88        (100, "C"),
89        (90, "XC"),
90        (50, "L"),
91        (40, "XL"),
92        (10, "X"),
93        (9, "IX"),
94        (5, "V"),
95        (4, "IV"),
96        (1, "I"),
97    ];
98
99    let mut result = String::new();
100
101    for (value, numeral) in values.iter() {
102        while num >= *value {
103            result.push_str(numeral);
104            num -= *value;
105        }
106    }
107
108    result
109}
110
111/// Convert Roman numerals to integer
112fn roman_to_int(s: &str) -> Option<i32> {
113    let mut result = 0;
114    let mut prev_value = 0;
115
116    for c in s.chars().rev() {
117        let value = match c {
118            'I' => 1,
119            'V' => 5,
120            'X' => 10,
121            'L' => 50,
122            'C' => 100,
123            'D' => 500,
124            'M' => 1000,
125            _ => return None, // Invalid character
126        };
127
128        if value < prev_value {
129            result -= value;
130        } else {
131            result += value;
132        }
133
134        prev_value = value;
135    }
136
137    // Validate the result by converting back
138    if int_to_roman(result).to_uppercase() == s.to_uppercase() {
139        Some(result)
140    } else {
141        None // Invalid Roman numeral
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn test_to_roman() {
151        let func = ToRoman;
152
153        // Test various numbers
154        assert_eq!(
155            func.evaluate(&[DataValue::Float(1.0)]).unwrap(),
156            DataValue::String("I".to_string())
157        );
158
159        assert_eq!(
160            func.evaluate(&[DataValue::Float(49.0)]).unwrap(),
161            DataValue::String("XLIX".to_string())
162        );
163
164        assert_eq!(
165            func.evaluate(&[DataValue::Float(2024.0)]).unwrap(),
166            DataValue::String("MMXXIV".to_string())
167        );
168
169        assert_eq!(
170            func.evaluate(&[DataValue::Float(1984.0)]).unwrap(),
171            DataValue::String("MCMLXXXIV".to_string())
172        );
173
174        assert_eq!(
175            func.evaluate(&[DataValue::Float(3999.0)]).unwrap(),
176            DataValue::String("MMMCMXCIX".to_string())
177        );
178    }
179
180    #[test]
181    fn test_from_roman() {
182        let func = FromRoman;
183
184        assert_eq!(
185            func.evaluate(&[DataValue::String("I".to_string())])
186                .unwrap(),
187            DataValue::Float(1.0)
188        );
189
190        assert_eq!(
191            func.evaluate(&[DataValue::String("XLIX".to_string())])
192                .unwrap(),
193            DataValue::Float(49.0)
194        );
195
196        assert_eq!(
197            func.evaluate(&[DataValue::String("MMXXIV".to_string())])
198                .unwrap(),
199            DataValue::Float(2024.0)
200        );
201
202        assert_eq!(
203            func.evaluate(&[DataValue::String("mcmlxxxiv".to_string())])
204                .unwrap(),
205            DataValue::Float(1984.0)
206        );
207    }
208
209    #[test]
210    fn test_round_trip() {
211        let to_roman = ToRoman;
212        let from_roman = FromRoman;
213
214        for i in 1..=3999 {
215            let roman = to_roman.evaluate(&[DataValue::Float(i as f64)]).unwrap();
216            let back = from_roman.evaluate(&[roman]).unwrap();
217            assert_eq!(back, DataValue::Float(i as f64));
218        }
219    }
220}