sql_cli/sql/functions/
roman.rs1use crate::data::datatable::DataValue;
2use crate::sql::functions::{ArgCount, FunctionCategory, FunctionSignature, SqlFunction};
3use anyhow::Result;
4
5pub 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
44pub 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
81fn 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
111fn 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, };
127
128 if value < prev_value {
129 result -= value;
130 } else {
131 result += value;
132 }
133
134 prev_value = value;
135 }
136
137 if int_to_roman(result).to_uppercase() == s.to_uppercase() {
139 Some(result)
140 } else {
141 None }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn test_to_roman() {
151 let func = ToRoman;
152
153 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}