Skip to main content

formualizer_eval/builtins/text/
char_code_rept.rs

1//! CHAR, CODE, REPT text functions
2
3use super::super::utils::{ARG_ANY_ONE, ARG_ANY_TWO, coerce_num};
4use crate::args::ArgSchema;
5use crate::function::Function;
6use crate::traits::{ArgumentHandle, CalcValue, FunctionContext};
7use formualizer_common::{ExcelError, LiteralValue};
8use formualizer_macros::func_caps;
9
10fn scalar_like_value(arg: &ArgumentHandle<'_, '_>) -> Result<LiteralValue, ExcelError> {
11    Ok(match arg.value()? {
12        CalcValue::Scalar(v) => v,
13        CalcValue::Range(rv) => rv.get_cell(0, 0),
14    })
15}
16
17/// CHAR(number) - Returns the character specified by a number
18/// Excel uses Windows-1252 encoding for codes 1-255
19#[derive(Debug)]
20pub struct CharFn;
21impl Function for CharFn {
22    func_caps!(PURE);
23    fn name(&self) -> &'static str {
24        "CHAR"
25    }
26    fn min_args(&self) -> usize {
27        1
28    }
29    fn arg_schema(&self) -> &'static [ArgSchema] {
30        &ARG_ANY_ONE[..]
31    }
32    fn eval<'a, 'b, 'c>(
33        &self,
34        args: &'c [ArgumentHandle<'a, 'b>],
35        _: &dyn FunctionContext<'b>,
36    ) -> Result<CalcValue<'b>, ExcelError> {
37        let v = scalar_like_value(&args[0])?;
38        let n = match v {
39            LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
40            other => coerce_num(&other)?,
41        };
42
43        let code = n.trunc() as i32;
44
45        // Excel CHAR accepts 1-255
46        if !(1..=255).contains(&code) {
47            return Ok(CalcValue::Scalar(LiteralValue::Error(
48                ExcelError::new_value(),
49            )));
50        }
51
52        // Windows-1252 to Unicode mapping for codes 128-159
53        let unicode_char = match code as u8 {
54            0x80 => '\u{20AC}', // Euro sign
55            0x82 => '\u{201A}', // Single low-9 quotation mark
56            0x83 => '\u{0192}', // Latin small letter f with hook
57            0x84 => '\u{201E}', // Double low-9 quotation mark
58            0x85 => '\u{2026}', // Horizontal ellipsis
59            0x86 => '\u{2020}', // Dagger
60            0x87 => '\u{2021}', // Double dagger
61            0x88 => '\u{02C6}', // Modifier letter circumflex accent
62            0x89 => '\u{2030}', // Per mille sign
63            0x8A => '\u{0160}', // Latin capital letter S with caron
64            0x8B => '\u{2039}', // Single left-pointing angle quotation mark
65            0x8C => '\u{0152}', // Latin capital ligature OE
66            0x8E => '\u{017D}', // Latin capital letter Z with caron
67            0x91 => '\u{2018}', // Left single quotation mark
68            0x92 => '\u{2019}', // Right single quotation mark
69            0x93 => '\u{201C}', // Left double quotation mark
70            0x94 => '\u{201D}', // Right double quotation mark
71            0x95 => '\u{2022}', // Bullet
72            0x96 => '\u{2013}', // En dash
73            0x97 => '\u{2014}', // Em dash
74            0x98 => '\u{02DC}', // Small tilde
75            0x99 => '\u{2122}', // Trade mark sign
76            0x9A => '\u{0161}', // Latin small letter s with caron
77            0x9B => '\u{203A}', // Single right-pointing angle quotation mark
78            0x9C => '\u{0153}', // Latin small ligature oe
79            0x9E => '\u{017E}', // Latin small letter z with caron
80            0x9F => '\u{0178}', // Latin capital letter Y with diaeresis
81            0x81 | 0x8D | 0x8F | 0x90 | 0x9D => {
82                // Undefined in Windows-1252, return placeholder
83                '\u{FFFD}'
84            }
85            c => char::from(c),
86        };
87
88        Ok(CalcValue::Scalar(LiteralValue::Text(
89            unicode_char.to_string(),
90        )))
91    }
92}
93
94/// CODE(text) - Returns a numeric code for the first character in a text string
95#[derive(Debug)]
96pub struct CodeFn;
97impl Function for CodeFn {
98    func_caps!(PURE);
99    fn name(&self) -> &'static str {
100        "CODE"
101    }
102    fn min_args(&self) -> usize {
103        1
104    }
105    fn arg_schema(&self) -> &'static [ArgSchema] {
106        &ARG_ANY_ONE[..]
107    }
108    fn eval<'a, 'b, 'c>(
109        &self,
110        args: &'c [ArgumentHandle<'a, 'b>],
111        _: &dyn FunctionContext<'b>,
112    ) -> Result<CalcValue<'b>, ExcelError> {
113        let v = scalar_like_value(&args[0])?;
114        let s = match v {
115            LiteralValue::Text(t) => t,
116            LiteralValue::Empty => {
117                return Ok(CalcValue::Scalar(LiteralValue::Error(
118                    ExcelError::new_value(),
119                )));
120            }
121            LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
122            other => other.to_string(),
123        };
124
125        if s.is_empty() {
126            return Ok(CalcValue::Scalar(LiteralValue::Error(
127                ExcelError::new_value(),
128            )));
129        }
130
131        let first_char = s.chars().next().unwrap();
132
133        // Map Unicode back to Windows-1252 for Excel compatibility
134        let code = match first_char {
135            '\u{20AC}' => 0x80, // Euro sign
136            '\u{201A}' => 0x82, // Single low-9 quotation mark
137            '\u{0192}' => 0x83, // Latin small letter f with hook
138            '\u{201E}' => 0x84, // Double low-9 quotation mark
139            '\u{2026}' => 0x85, // Horizontal ellipsis
140            '\u{2020}' => 0x86, // Dagger
141            '\u{2021}' => 0x87, // Double dagger
142            '\u{02C6}' => 0x88, // Modifier letter circumflex accent
143            '\u{2030}' => 0x89, // Per mille sign
144            '\u{0160}' => 0x8A, // Latin capital letter S with caron
145            '\u{2039}' => 0x8B, // Single left-pointing angle quotation mark
146            '\u{0152}' => 0x8C, // Latin capital ligature OE
147            '\u{017D}' => 0x8E, // Latin capital letter Z with caron
148            '\u{2018}' => 0x91, // Left single quotation mark
149            '\u{2019}' => 0x92, // Right single quotation mark
150            '\u{201C}' => 0x93, // Left double quotation mark
151            '\u{201D}' => 0x94, // Right double quotation mark
152            '\u{2022}' => 0x95, // Bullet
153            '\u{2013}' => 0x96, // En dash
154            '\u{2014}' => 0x97, // Em dash
155            '\u{02DC}' => 0x98, // Small tilde
156            '\u{2122}' => 0x99, // Trade mark sign
157            '\u{0161}' => 0x9A, // Latin small letter s with caron
158            '\u{203A}' => 0x9B, // Single right-pointing angle quotation mark
159            '\u{0153}' => 0x9C, // Latin small ligature oe
160            '\u{017E}' => 0x9E, // Latin small letter z with caron
161            '\u{0178}' => 0x9F, // Latin capital letter Y with diaeresis
162            c if (c as u32) < 256 => c as i64,
163            c => c as i64, // For characters outside Windows-1252, return Unicode code point
164        };
165
166        Ok(CalcValue::Scalar(LiteralValue::Int(code)))
167    }
168}
169
170/// REPT(text, number_times) - Repeats text a given number of times
171#[derive(Debug)]
172pub struct ReptFn;
173impl Function for ReptFn {
174    func_caps!(PURE);
175    fn name(&self) -> &'static str {
176        "REPT"
177    }
178    fn min_args(&self) -> usize {
179        2
180    }
181    fn arg_schema(&self) -> &'static [ArgSchema] {
182        &ARG_ANY_TWO[..]
183    }
184    fn eval<'a, 'b, 'c>(
185        &self,
186        args: &'c [ArgumentHandle<'a, 'b>],
187        _: &dyn FunctionContext<'b>,
188    ) -> Result<CalcValue<'b>, ExcelError> {
189        let text_val = scalar_like_value(&args[0])?;
190        let count_val = scalar_like_value(&args[1])?;
191
192        let text = match text_val {
193            LiteralValue::Text(t) => t,
194            LiteralValue::Empty => String::new(),
195            LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
196            other => other.to_string(),
197        };
198
199        let count = match count_val {
200            LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
201            other => coerce_num(&other)?,
202        };
203
204        let count = count.trunc() as i64;
205
206        if count < 0 {
207            return Ok(CalcValue::Scalar(LiteralValue::Error(
208                ExcelError::new_value(),
209            )));
210        }
211
212        // Excel limits result to 32767 characters
213        let max_result_len = 32767;
214        let result_len = text.len() * (count as usize);
215        if result_len > max_result_len {
216            return Ok(CalcValue::Scalar(LiteralValue::Error(
217                ExcelError::new_value(),
218            )));
219        }
220
221        let result = text.repeat(count as usize);
222        Ok(CalcValue::Scalar(LiteralValue::Text(result)))
223    }
224}
225
226pub fn register_builtins() {
227    use std::sync::Arc;
228    crate::function_registry::register_function(Arc::new(CharFn));
229    crate::function_registry::register_function(Arc::new(CodeFn));
230    crate::function_registry::register_function(Arc::new(ReptFn));
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236    use crate::test_workbook::TestWorkbook;
237    use crate::traits::ArgumentHandle;
238    use formualizer_parse::parser::{ASTNode, ASTNodeType};
239
240    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
241        wb.interpreter()
242    }
243    fn lit(v: LiteralValue) -> ASTNode {
244        ASTNode::new(ASTNodeType::Literal(v), None)
245    }
246
247    #[test]
248    fn char_basic() {
249        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(CharFn));
250        let ctx = interp(&wb);
251        let n = lit(LiteralValue::Number(65.0));
252        let f = ctx.context.get_function("", "CHAR").unwrap();
253        assert_eq!(
254            f.dispatch(
255                &[ArgumentHandle::new(&n, &ctx)],
256                &ctx.function_context(None)
257            )
258            .unwrap()
259            .into_literal(),
260            LiteralValue::Text("A".to_string())
261        );
262    }
263
264    #[test]
265    fn code_basic() {
266        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(CodeFn));
267        let ctx = interp(&wb);
268        let s = lit(LiteralValue::Text("A".to_string()));
269        let f = ctx.context.get_function("", "CODE").unwrap();
270        assert_eq!(
271            f.dispatch(
272                &[ArgumentHandle::new(&s, &ctx)],
273                &ctx.function_context(None)
274            )
275            .unwrap()
276            .into_literal(),
277            LiteralValue::Int(65)
278        );
279    }
280
281    #[test]
282    fn rept_basic() {
283        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(ReptFn));
284        let ctx = interp(&wb);
285        let s = lit(LiteralValue::Text("ab".to_string()));
286        let n = lit(LiteralValue::Number(3.0));
287        let f = ctx.context.get_function("", "REPT").unwrap();
288        assert_eq!(
289            f.dispatch(
290                &[ArgumentHandle::new(&s, &ctx), ArgumentHandle::new(&n, &ctx)],
291                &ctx.function_context(None)
292            )
293            .unwrap()
294            .into_literal(),
295            LiteralValue::Text("ababab".to_string())
296        );
297    }
298}