Skip to main content

ironcalc_base/expressions/utils/
mod.rs

1use super::types::*;
2use crate::constants::{LAST_COLUMN, LAST_ROW};
3
4#[cfg(test)]
5mod test;
6
7/// Converts column letter identifier to number.
8pub fn column_to_number(column: &str) -> Result<i32, String> {
9    if column.is_empty() {
10        return Err("Column identifier cannot be empty.".to_string());
11    }
12
13    if !column.is_ascii() {
14        return Err("Column identifier must be ASCII.".to_string());
15    }
16
17    let mut column_number = 0;
18    for character in column.chars() {
19        if !character.is_ascii_uppercase() {
20            return Err("Column identifier can use only A-Z characters".to_string());
21        }
22        column_number = column_number * 26 + ((character as i32) - 64);
23    }
24
25    match is_valid_column_number(column_number) {
26        true => Ok(column_number),
27        false => Err("Column is not valid.".to_string()),
28    }
29}
30
31/// If input number is outside valid range `None` is returned.
32pub fn number_to_column(mut i: i32) -> Option<String> {
33    if !is_valid_column_number(i) {
34        return None;
35    }
36    let mut column = "".to_string();
37    while i > 0 {
38        let r = ((i - 1) % 26) as u8;
39        column.insert(0, (65 + r) as char);
40        i = (i - 1) / 26;
41    }
42    Some(column)
43}
44
45/// Checks if column number is in valid range.
46pub fn is_valid_column_number(column: i32) -> bool {
47    (1..=LAST_COLUMN).contains(&column)
48}
49
50pub fn is_valid_column(column: &str) -> bool {
51    // last column XFD
52    if column.len() > 3 {
53        return false;
54    }
55
56    let column_number = column_to_number(column);
57
58    match column_number {
59        Ok(column_number) => is_valid_column_number(column_number),
60        Err(_) => false,
61    }
62}
63
64pub fn is_valid_row(row: i32) -> bool {
65    (1..=LAST_ROW).contains(&row)
66}
67
68fn is_valid_row_str(row: &str) -> bool {
69    match row.parse::<i32>() {
70        Ok(r) => is_valid_row(r),
71        Err(_r) => false,
72    }
73}
74
75pub fn parse_reference_r1c1(r: &str) -> Option<ParsedReference> {
76    let chars = r.as_bytes();
77    let len = chars.len();
78    let absolute_column;
79    let absolute_row;
80    let mut row = "".to_string();
81    let mut column = "".to_string();
82    if len < 4 {
83        return None;
84    }
85    if chars[0] != b'R' {
86        return None;
87    }
88    let mut i = 1;
89    if chars[i] == b'[' {
90        i += 1;
91        absolute_row = false;
92        if chars[i] == b'-' {
93            i += 1;
94            row.push('-');
95        }
96    } else {
97        absolute_row = true;
98    }
99    while i < len {
100        let ch = chars[i];
101        if ch.is_ascii_digit() {
102            row.push(ch as char);
103        } else {
104            break;
105        }
106        i += 1;
107    }
108    if !absolute_row {
109        if i >= len || chars[i] != b']' {
110            return None;
111        };
112        i += 1;
113    }
114    if i >= len || chars[i] != b'C' {
115        return None;
116    };
117    i += 1;
118    if i < len && chars[i] == b'[' {
119        absolute_column = false;
120        i += 1;
121        if i < len && chars[i] == b'-' {
122            i += 1;
123            column.push('-');
124        }
125    } else {
126        absolute_column = true;
127    }
128    while i < len {
129        let ch = chars[i];
130        if ch.is_ascii_digit() {
131            column.push(ch as char);
132        } else {
133            break;
134        }
135        i += 1;
136    }
137    if !absolute_column {
138        if i >= len || chars[i] != b']' {
139            return None;
140        };
141        i += 1;
142    }
143    if i != len {
144        return None;
145    }
146    Some(ParsedReference {
147        row: row.parse::<i32>().unwrap_or(0),
148        column: column.parse::<i32>().unwrap_or(0),
149        absolute_column,
150        absolute_row,
151    })
152}
153
154pub fn parse_reference_a1(r: &str) -> Option<ParsedReference> {
155    let chars = r.chars();
156    let mut absolute_column = false;
157    let mut absolute_row = false;
158    let mut row = "".to_string();
159    let mut column = "".to_string();
160    let mut state = 1; // 1(colum), 2(row)
161
162    for ch in chars {
163        match ch {
164            'A'..='Z' if state == 1 => {
165                column.push(ch);
166            }
167            '0'..='9' => {
168                if state == 1 {
169                    state = 2
170                }
171                row.push(ch);
172            }
173            '$' => {
174                if column == *"" {
175                    absolute_column = true;
176                } else if state == 1 {
177                    absolute_row = true;
178                    state = 2;
179                } else {
180                    return None;
181                }
182            }
183            _ => {
184                return None;
185            }
186        }
187    }
188    if !is_valid_column(&column) {
189        return None;
190    }
191    if !is_valid_row_str(&row) {
192        return None;
193    }
194    let row = match row.parse::<i32>() {
195        Ok(r) => r,
196        Err(_) => return None,
197    };
198
199    Some(ParsedReference {
200        row,
201        column: column_to_number(&column).ok()?,
202        absolute_column,
203        absolute_row,
204    })
205}
206
207/// Returns true if `name` is a valid identifier in an A1-mode formula expression.
208/// This is a superset of `is_valid_identifier`: it permits the single-character
209/// names "R" and "C" that are valid as LAMBDA parameters or LET variables but
210/// not as worksheet-level defined names.
211pub fn is_valid_a1_identifier(name: &str) -> bool {
212    // https://support.microsoft.com/en-us/office/names-in-formulas-fc2935f9-115d-4bef-a370-3aa8bb4c91f1
213    // https://github.com/MartinTrummer/excel-names/
214    let upper = name.to_uppercase();
215    let len = upper.chars().count();
216
217    let mut chars = upper.chars();
218
219    if len > 255 || len == 0 {
220        return false;
221    }
222    let first = match chars.next() {
223        Some(ch) => ch,
224        None => return false,
225    };
226
227    // The first character of a name must be a letter, an underscore character (_), or a backslash (\).
228    if !(first.is_ascii_alphabetic() || first == '_' || first == '\\') {
229        return false;
230    }
231    if upper == *"TRUE" || upper == *"FALSE" {
232        return false;
233    }
234    if parse_reference_a1(name).is_some() {
235        return false;
236    }
237    if parse_reference_r1c1(name).is_some() {
238        return false;
239    }
240    for ch in chars {
241        if !(ch.is_alphanumeric() || ch == '_' || ch == '.') {
242            return false;
243        }
244    }
245
246    true
247}
248
249pub fn is_valid_identifier(name: &str) -> bool {
250    // You cannot use the uppercase and lowercase characters "C", "c", "R", or "r" as a defined name
251    let upper = name.to_uppercase();
252    if upper == "R" || upper == "C" {
253        return false;
254    }
255    is_valid_a1_identifier(name)
256}
257
258fn name_needs_quoting(name: &str) -> bool {
259    let chars = name.chars();
260    // it contains any of these characters: ()'$,;-+{} or space
261    for (i, char) in chars.enumerate() {
262        if [' ', '(', ')', '\'', '$', ',', ';', '-', '+', '{', '}'].contains(&char) {
263            return true;
264        }
265        // if it starts with a number
266        if i == 0 && char.is_ascii_digit() {
267            return true;
268        }
269    }
270    if parse_reference_a1(name).is_some() {
271        // cell reference in A1 notation, e.g. B1048576 is quoted, B1048577 is not
272        return true;
273    }
274    if parse_reference_r1c1(name).is_some() {
275        // cell reference in R1C1 notation, e.g. RC, RC2, R5C, R-4C, RC-8, R, C
276        return true;
277    }
278    false
279}
280
281/// Quotes a string sheet name if it needs to
282/// NOTE: Invalid characters in a sheet name \, /, *, \[, \], :, ?
283pub fn quote_name(name: &str) -> String {
284    if name_needs_quoting(name) {
285        return format!("'{}'", name.replace('\'', "''"));
286    };
287    name.to_string()
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn test_quote_name() {
296        assert_eq!(quote_name("Sheet1"), "Sheet1");
297        assert_eq!(quote_name("Sheet 1"), "'Sheet 1'");
298        // escape and quote
299        assert_eq!(quote_name("Sheet1'"), "'Sheet1'''");
300        assert_eq!(quote_name("Data(2024)"), "'Data(2024)'");
301        assert_eq!(quote_name("Data$2024"), "'Data$2024'");
302        assert_eq!(quote_name("Data-2024"), "'Data-2024'");
303        assert_eq!(quote_name("Data+2024"), "'Data+2024'");
304        assert_eq!(quote_name("Data,2024"), "'Data,2024'");
305        assert_eq!(quote_name("Data;2024"), "'Data;2024'");
306        assert_eq!(quote_name("Data{2024}"), "'Data{2024}'");
307
308        assert_eq!(quote_name("2024"), "'2024'");
309        assert_eq!(quote_name("1Data"), "'1Data'");
310        assert_eq!(quote_name("A1"), "'A1'");
311        assert_eq!(quote_name("R1C1"), "'R1C1'");
312        assert_eq!(quote_name("MySheet"), "MySheet");
313
314        assert_eq!(quote_name("B1048576"), "'B1048576'");
315        assert_eq!(quote_name("B1048577"), "B1048577");
316    }
317}