Skip to main content

ironcalc_base/expressions/lexer/
util.rs

1use serde::{Deserialize, Serialize};
2
3use crate::expressions::token;
4use crate::language::{get_language, Language};
5use crate::locale::{get_locale, Locale};
6
7use super::{Lexer, LexerMode};
8
9/// A MarkedToken is a token together with its position on a formula
10#[derive(Debug, PartialEq, Serialize, Deserialize)]
11pub struct MarkedToken {
12    pub token: token::TokenType,
13    pub start: i32,
14    pub end: i32,
15}
16
17/// Returns a list of marked tokens for a formula
18///
19/// # Examples
20/// ```
21/// use ironcalc_base::expressions::{
22///      lexer::util::{get_tokens, MarkedToken},
23///      token::{OpSum, TokenType},
24/// };
25///
26/// let marked_tokens = get_tokens("A1+1");
27/// let first_t = MarkedToken {
28///     token: TokenType::Reference {
29///         sheet: None,
30///         row: 1,
31///         column: 1,
32///         absolute_column: false,
33///         absolute_row: false,
34///     },
35///     start: 0,
36///     end: 2,
37/// };
38/// let second_t = MarkedToken {
39///     token: TokenType::Addition(OpSum::Add),
40///     start:2,
41///     end: 3
42/// };
43/// let third_t = MarkedToken {
44///     token: TokenType::Number(1.0),
45///     start:3,
46///     end: 4
47/// };
48/// assert_eq!(marked_tokens, vec![first_t, second_t, third_t]);
49/// ```
50pub fn get_tokens(formula: &str) -> Vec<MarkedToken> {
51    get_tokens_with_locale(
52        formula,
53        #[allow(clippy::expect_used)]
54        get_locale("en").expect(""),
55        #[allow(clippy::expect_used)]
56        get_language("en").expect(""),
57    )
58}
59
60/// Returns a list of marked tokens for a formula using the given locale and language
61pub fn get_tokens_with_locale(
62    formula: &str,
63    locale: &Locale,
64    language: &Language,
65) -> Vec<MarkedToken> {
66    let mut tokens = Vec::new();
67    let mut lexer = Lexer::new(formula, LexerMode::A1, locale, language);
68    let mut start = lexer.get_position();
69    let mut next_token = lexer.next_token();
70    let mut end = lexer.get_position();
71    loop {
72        match next_token {
73            token::TokenType::EOF => {
74                break;
75            }
76            _ => {
77                tokens.push(MarkedToken {
78                    start,
79                    end,
80                    token: next_token,
81                });
82                start = lexer.get_position();
83                next_token = lexer.next_token();
84                end = lexer.get_position();
85            }
86        }
87    }
88    tokens
89}
90
91// Excel's F4 cycle for the absolute/relative state of a reference:
92// A1 -> $A$1 -> A$1 -> $A1 -> A1
93fn next_state(absolute_column: bool, absolute_row: bool) -> (bool, bool) {
94    match (absolute_column, absolute_row) {
95        (false, false) => (true, true),
96        (true, true) => (false, true),
97        (false, true) => (true, false),
98        (true, false) => (false, false),
99    }
100}
101
102// Cycles one endpoint of a range like "$C3" -> "C3".
103// An endpoint is [$]column[$]row where either the column or the row
104// (but not both) may be missing. Anything else is left untouched.
105fn cycle_endpoint(part: &[char]) -> Vec<char> {
106    let n = part.len();
107    let mut i = 0;
108    let mut absolute_column = false;
109    if part.first() == Some(&'$') {
110        absolute_column = true;
111        i += 1;
112    }
113    let column_start = i;
114    while i < n && part[i].is_ascii_alphabetic() {
115        i += 1;
116    }
117    let column = &part[column_start..i];
118    let mut absolute_row = false;
119    if i < n && part[i] == '$' {
120        absolute_row = true;
121        i += 1;
122    }
123    let row_start = i;
124    while i < n && part[i].is_ascii_digit() {
125        i += 1;
126    }
127    let row = &part[row_start..i];
128    if i != n || (column.is_empty() && row.is_empty()) {
129        return part.to_vec();
130    }
131    let (new_column, new_row) = if column.is_empty() {
132        // a row-only endpoint like "5" in "5:5"; a leading '$' belongs to the row
133        (false, !(absolute_column || absolute_row))
134    } else if row.is_empty() {
135        // a column-only endpoint like "D" in "D:D"
136        (!absolute_column, false)
137    } else {
138        next_state(absolute_column, absolute_row)
139    };
140    let mut result = Vec::with_capacity(n + 2);
141    if new_column {
142        result.push('$');
143    }
144    // cycling also normalizes the reference to uppercase, "a1" -> "$A$1"
145    result.extend(column.iter().map(|c| c.to_ascii_uppercase()));
146    if new_row {
147        result.push('$');
148    }
149    result.extend_from_slice(row);
150    result
151}
152
153// Cycles a whole reference or range token like "Sheet1!C3:D5" or "'My Sheet'!A1".
154// The sheet prefix, if any, is left untouched.
155fn cycle_token_text(text: &[char]) -> Vec<char> {
156    let n = text.len();
157    let mut result = Vec::with_capacity(n + 4);
158    let mut i = 0;
159    // the token span can include leading whitespace
160    while i < n && text[i].is_whitespace() {
161        result.push(text[i]);
162        i += 1;
163    }
164    if i < n && text[i] == '\'' {
165        // quoted sheet name; quotes inside are escaped by doubling them
166        result.push('\'');
167        i += 1;
168        while i < n {
169            let c = text[i];
170            result.push(c);
171            i += 1;
172            if c == '\'' {
173                if i < n && text[i] == '\'' {
174                    result.push('\'');
175                    i += 1;
176                } else {
177                    break;
178                }
179            }
180        }
181        if i < n && text[i] == '!' {
182            result.push('!');
183            i += 1;
184        }
185    } else if let Some(bang) = text.iter().skip(i).position(|&c| c == '!') {
186        let prefix_end = i + bang + 1;
187        result.extend_from_slice(&text[i..prefix_end]);
188        i = prefix_end;
189    }
190    // endpoints separated by ':'
191    loop {
192        let part_start = i;
193        while i < n && text[i] != ':' {
194            i += 1;
195        }
196        result.extend(cycle_endpoint(&text[part_start..i]));
197        if i < n {
198            result.push(':');
199            i += 1;
200        } else {
201            break;
202        }
203    }
204    result
205}
206
207/// Cycles the absolute/relative state of the references touched by the cursor,
208/// Excel F4 style: A1 -> $A$1 -> A$1 -> $A1 -> A1.
209///
210/// `start` and `end` are cursor positions in characters; a cursor grazing the
211/// edge of a reference counts as touching it. Returns the new text together
212/// with the new cursor positions:
213/// * If nothing is cycled the text and cursor are returned unchanged.
214/// * A collapsed cursor lands collapsed at the end of the cycled reference.
215/// * A selection spans all the cycled references in the new text.
216pub fn cycle_reference(
217    value: &str,
218    start: usize,
219    end: usize,
220    locale: &Locale,
221    language: &Language,
222) -> Result<(String, i32, i32), String> {
223    let chars: Vec<char> = value.chars().collect();
224    if start > chars.len() || end > chars.len() {
225        return Err("Cursor index out of bounds".to_string());
226    }
227    let (selection_start, selection_end) = if start <= end {
228        (start, end)
229    } else {
230        (end, start)
231    };
232    if chars.first() != Some(&'=') {
233        return Ok((value.to_string(), start as i32, end as i32));
234    }
235    let body = &chars[1..];
236    let body_str: String = body.iter().collect();
237
238    let mut result: Vec<char> = vec!['='];
239    // characters of the body already copied to the result
240    let mut copied = 0;
241    let mut first_cycled_start = None;
242    let mut last_cycled_end = 0;
243    for marked in get_tokens_with_locale(&body_str, locale, language) {
244        if !matches!(
245            marked.token,
246            token::TokenType::Reference { .. } | token::TokenType::Range { .. }
247        ) {
248            continue;
249        }
250        // token positions are relative to the body: shift by the leading '='
251        let token_start = marked.start.max(0) as usize + 1;
252        let token_end = marked.end.max(0) as usize + 1;
253        if token_start > selection_end || selection_start > token_end {
254            continue;
255        }
256        result.extend_from_slice(&body[copied..token_start - 1]);
257        let token_text = &body[token_start - 1..token_end - 1];
258        if first_cycled_start.is_none() {
259            let whitespace = token_text.iter().take_while(|c| c.is_whitespace()).count();
260            first_cycled_start = Some(result.len() + whitespace);
261        }
262        result.extend(cycle_token_text(token_text));
263        last_cycled_end = result.len();
264        copied = token_end - 1;
265    }
266    let Some(first_start) = first_cycled_start else {
267        // the cursor is not touching any reference
268        return Ok((value.to_string(), start as i32, end as i32));
269    };
270    result.extend_from_slice(&body[copied..]);
271    let new_value: String = result.iter().collect();
272    if start == end {
273        return Ok((new_value, last_cycled_end as i32, last_cycled_end as i32));
274    }
275    Ok((new_value, first_start as i32, last_cycled_end as i32))
276}