Skip to main content

ironcalc_base/expressions/parser/
move_formula.rs

1use super::{
2    stringify::{stringify_reference, DisplaceData},
3    ArrayNode, Node, Reference,
4};
5use crate::{
6    constants::{LAST_COLUMN, LAST_ROW},
7    expressions::token::OpUnary,
8    language::Language,
9    locale::Locale,
10};
11use crate::{
12    expressions::types::{Area, CellReferenceRC},
13    number_format::to_excel_precision_str,
14};
15
16pub(crate) fn ref_is_in_area(sheet: u32, row: i32, column: i32, area: &Area) -> bool {
17    if area.sheet != sheet {
18        return false;
19    }
20    if row < area.row || row > area.row + area.height - 1 {
21        return false;
22    }
23    if column < area.column || column > area.column + area.width - 1 {
24        return false;
25    }
26    true
27}
28
29pub(crate) struct MoveContext<'a> {
30    pub source_sheet_name: &'a str,
31    pub row: i32,
32    pub column: i32,
33    pub area: &'a Area,
34    pub target_sheet_name: &'a str,
35    pub row_delta: i32,
36    pub column_delta: i32,
37}
38
39/// This implements Excel's cut && paste
40/// We are moving a formula in (row, column) to (row+row_delta, column + column_delta).
41/// All references that do not point to a cell in area will be left untouched.
42/// All references that point to a cell in area will be displaced
43pub(crate) fn move_formula(
44    node: &Node,
45    move_context: &MoveContext,
46    locale: &Locale,
47    language: &Language,
48) -> String {
49    to_string_moved(node, move_context, locale, language)
50}
51
52fn move_function(
53    name: &str,
54    args: &Vec<Node>,
55    move_context: &MoveContext,
56    locale: &Locale,
57    language: &Language,
58) -> String {
59    let mut first = true;
60    let mut arguments = "".to_string();
61    for el in args {
62        if !first {
63            arguments = format!(
64                "{},{}",
65                arguments,
66                to_string_moved(el, move_context, locale, language)
67            );
68        } else {
69            first = false;
70            arguments = to_string_moved(el, move_context, locale, language);
71        }
72    }
73    format!("{name}({arguments})")
74}
75
76fn format_number_locale(number: f64, locale: &Locale) -> String {
77    let s = to_excel_precision_str(number);
78    let decimal = &locale.numbers.symbols.decimal;
79    if decimal == "." {
80        s
81    } else {
82        s.replace('.', decimal)
83    }
84}
85
86pub(crate) fn to_string_array_node(
87    node: &ArrayNode,
88    locale: &Locale,
89    language: &Language,
90) -> String {
91    match node {
92        ArrayNode::Boolean(value) => {
93            if *value {
94                language.booleans.r#true.to_uppercase()
95            } else {
96                language.booleans.r#false.to_uppercase()
97            }
98        }
99        ArrayNode::Number(number) => format_number_locale(*number, locale),
100        ArrayNode::String(value) => format!("\"{value}\""),
101        ArrayNode::Error(kind) => format!("{kind}"),
102        ArrayNode::Empty => "0".to_string(),
103    }
104}
105
106fn to_string_moved(
107    node: &Node,
108    move_context: &MoveContext,
109    locale: &Locale,
110    language: &Language,
111) -> String {
112    use self::Node::*;
113    match node {
114        BooleanKind(value) => format!("{value}").to_uppercase(),
115        NumberKind(number) => format_number_locale(*number, locale),
116        StringKind(value) => format!("\"{value}\""),
117        ReferenceKind {
118            sheet_name,
119            sheet_index,
120            absolute_row,
121            absolute_column,
122            row,
123            column,
124        } => {
125            let reference_row = if *absolute_row {
126                *row
127            } else {
128                row + move_context.row
129            };
130            let reference_column = if *absolute_column {
131                *column
132            } else {
133                column + move_context.column
134            };
135
136            let new_row;
137            let new_column;
138            let mut ref_sheet_name = sheet_name;
139            let source_sheet_name = &Some(move_context.source_sheet_name.to_string());
140
141            if ref_is_in_area(
142                *sheet_index,
143                reference_row,
144                reference_column,
145                move_context.area,
146            ) {
147                // if the reference is in the area we are moving we want to displace the reference
148                new_row = row + move_context.row_delta;
149                new_column = column + move_context.column_delta;
150            } else {
151                // If the reference is not in the area we are moving the reference remains unchanged
152                new_row = *row;
153                new_column = *column;
154                if move_context.target_sheet_name != move_context.source_sheet_name
155                    && sheet_name.is_none()
156                {
157                    ref_sheet_name = source_sheet_name;
158                }
159            };
160            let context = CellReferenceRC {
161                sheet: move_context.source_sheet_name.to_string(),
162                column: move_context.column,
163                row: move_context.row,
164            };
165            stringify_reference(
166                Some(&context),
167                &DisplaceData::None,
168                &Reference {
169                    sheet_name: ref_sheet_name,
170                    sheet_index: *sheet_index,
171                    absolute_row: *absolute_row,
172                    absolute_column: *absolute_column,
173                    row: new_row,
174                    column: new_column,
175                },
176                false,
177                false,
178            )
179        }
180        RangeKind {
181            sheet_name,
182            sheet_index,
183            absolute_row1,
184            absolute_column1,
185            row1,
186            column1,
187            absolute_row2,
188            absolute_column2,
189            row2,
190            column2,
191        } => {
192            let full_row = *absolute_row1 && *absolute_row2 && (*row1 == 1) && (*row2 == LAST_ROW);
193            let full_column = *absolute_column1
194                && *absolute_column2
195                && (*column1 == 1)
196                && (*column2 == LAST_COLUMN);
197
198            let reference_row1 = if *absolute_row1 {
199                *row1
200            } else {
201                row1 + move_context.row
202            };
203            let reference_column1 = if *absolute_column1 {
204                *column1
205            } else {
206                column1 + move_context.column
207            };
208
209            let reference_row2 = if *absolute_row2 {
210                *row2
211            } else {
212                row2 + move_context.row
213            };
214            let reference_column2 = if *absolute_column2 {
215                *column2
216            } else {
217                column2 + move_context.column
218            };
219
220            let new_row1;
221            let new_column1;
222            let new_row2;
223            let new_column2;
224            let mut ref_sheet_name = sheet_name;
225            let source_sheet_name = &Some(move_context.source_sheet_name.to_string());
226            if ref_is_in_area(
227                *sheet_index,
228                reference_row1,
229                reference_column1,
230                move_context.area,
231            ) && ref_is_in_area(
232                *sheet_index,
233                reference_row2,
234                reference_column2,
235                move_context.area,
236            ) {
237                // if the whole range is inside the area we are moving we want to displace the context
238                new_row1 = row1 + move_context.row_delta;
239                new_column1 = column1 + move_context.column_delta;
240                new_row2 = row2 + move_context.row_delta;
241                new_column2 = column2 + move_context.column_delta;
242            } else {
243                // If the reference is not in the area we are moving the context remains unchanged
244                new_row1 = *row1;
245                new_column1 = *column1;
246                new_row2 = *row2;
247                new_column2 = *column2;
248                if move_context.target_sheet_name != move_context.source_sheet_name
249                    && sheet_name.is_none()
250                {
251                    ref_sheet_name = source_sheet_name;
252                }
253            };
254            let context = CellReferenceRC {
255                sheet: move_context.source_sheet_name.to_string(),
256                column: move_context.column,
257                row: move_context.row,
258            };
259            let s1 = stringify_reference(
260                Some(&context),
261                &DisplaceData::None,
262                &Reference {
263                    sheet_name: ref_sheet_name,
264                    sheet_index: *sheet_index,
265                    absolute_row: *absolute_row1,
266                    absolute_column: *absolute_column1,
267                    row: new_row1,
268                    column: new_column1,
269                },
270                full_row,
271                full_column,
272            );
273            let s2 = stringify_reference(
274                Some(&context),
275                &DisplaceData::None,
276                &Reference {
277                    sheet_name: &None,
278                    sheet_index: *sheet_index,
279                    absolute_row: *absolute_row2,
280                    absolute_column: *absolute_column2,
281                    row: new_row2,
282                    column: new_column2,
283                },
284                full_row,
285                full_column,
286            );
287            format!("{s1}:{s2}")
288        }
289        WrongReferenceKind {
290            sheet_name,
291            absolute_row,
292            absolute_column,
293            row,
294            column,
295        } => {
296            // NB: Excel does not displace wrong references but Google Docs does. We follow Excel
297            let context = CellReferenceRC {
298                sheet: move_context.source_sheet_name.to_string(),
299                column: move_context.column,
300                row: move_context.row,
301            };
302            // It's a wrong reference, so there is no valid `sheet_index`.
303            // We don't need it, since the `sheet_index` is only used if `displace_data` is not `None`.
304            // I should fix it, maybe putting the `sheet_index` inside the `displace_data`
305            stringify_reference(
306                Some(&context),
307                &DisplaceData::None,
308                &Reference {
309                    sheet_name,
310                    sheet_index: 0, // HACK
311                    row: *row,
312                    column: *column,
313                    absolute_row: *absolute_row,
314                    absolute_column: *absolute_column,
315                },
316                false,
317                false,
318            )
319        }
320        WrongRangeKind {
321            sheet_name,
322            absolute_row1,
323            absolute_column1,
324            row1,
325            column1,
326            absolute_row2,
327            absolute_column2,
328            row2,
329            column2,
330        } => {
331            let full_row = *absolute_row1 && *absolute_row2 && (*row1 == 1) && (*row2 == LAST_ROW);
332            let full_column = *absolute_column1
333                && *absolute_column2
334                && (*column1 == 1)
335                && (*column2 == LAST_COLUMN);
336
337            // NB: Excel does not displace wrong references but Google Docs does. We follow Excel
338            let context = CellReferenceRC {
339                sheet: move_context.source_sheet_name.to_string(),
340                column: move_context.column,
341                row: move_context.row,
342            };
343            let s1 = stringify_reference(
344                Some(&context),
345                &DisplaceData::None,
346                &Reference {
347                    sheet_name,
348                    sheet_index: 0, // HACK
349                    row: *row1,
350                    column: *column1,
351                    absolute_row: *absolute_row1,
352                    absolute_column: *absolute_column1,
353                },
354                full_row,
355                full_column,
356            );
357            let s2 = stringify_reference(
358                Some(&context),
359                &DisplaceData::None,
360                &Reference {
361                    sheet_name: &None,
362                    sheet_index: 0, // HACK
363                    row: *row2,
364                    column: *column2,
365                    absolute_row: *absolute_row2,
366                    absolute_column: *absolute_column2,
367                },
368                full_row,
369                full_column,
370            );
371            format!("{s1}:{s2}")
372        }
373        OpRangeKind { left, right } => format!(
374            "{}:{}",
375            to_string_moved(left, move_context, locale, language),
376            to_string_moved(right, move_context, locale, language),
377        ),
378        OpConcatenateKind { left, right } => format!(
379            "{}&{}",
380            to_string_moved(left, move_context, locale, language),
381            to_string_moved(right, move_context, locale, language),
382        ),
383        OpSumKind { kind, left, right } => format!(
384            "{}{}{}",
385            to_string_moved(left, move_context, locale, language),
386            kind,
387            to_string_moved(right, move_context, locale, language),
388        ),
389        OpProductKind { kind, left, right } => {
390            let x = match **left {
391                OpSumKind { .. } => format!(
392                    "({})",
393                    to_string_moved(left, move_context, locale, language)
394                ),
395                CompareKind { .. } => format!(
396                    "({})",
397                    to_string_moved(left, move_context, locale, language)
398                ),
399                _ => to_string_moved(left, move_context, locale, language),
400            };
401            let y = match **right {
402                OpSumKind { .. } => format!(
403                    "({})",
404                    to_string_moved(right, move_context, locale, language)
405                ),
406                CompareKind { .. } => format!(
407                    "({})",
408                    to_string_moved(right, move_context, locale, language)
409                ),
410                OpProductKind { .. } => format!(
411                    "({})",
412                    to_string_moved(right, move_context, locale, language)
413                ),
414                UnaryKind { .. } => {
415                    format!(
416                        "({})",
417                        to_string_moved(right, move_context, locale, language)
418                    )
419                }
420                _ => to_string_moved(right, move_context, locale, language),
421            };
422            format!("{x}{kind}{y}")
423        }
424        OpPowerKind { left, right } => format!(
425            "{}^{}",
426            to_string_moved(left, move_context, locale, language),
427            to_string_moved(right, move_context, locale, language),
428        ),
429        NamedFunctionKind { name, args, id: _ } => {
430            move_function(name, args, move_context, locale, language)
431        }
432        FunctionKind { kind, args } => {
433            let name = &kind.to_localized_name(language);
434            move_function(name, args, move_context, locale, language)
435        }
436        ArrayKind(args) => {
437            let mut first_row = true;
438            let mut matrix_string = String::new();
439
440            // Each element in `args` is assumed to be one "row" (itself a `Vec<T>`).
441            let row_separator = if locale.numbers.symbols.decimal == "." {
442                ';'
443            } else {
444                '/'
445            };
446            let col_separator = if row_separator == ';' { ',' } else { ';' };
447            for row in args {
448                if !first_row {
449                    matrix_string.push(col_separator);
450                } else {
451                    first_row = false;
452                }
453
454                // Build the string for the current row
455                let mut first_col = true;
456                let mut row_string = String::new();
457                for el in row {
458                    if !first_col {
459                        row_string.push(row_separator);
460                    } else {
461                        first_col = false;
462                    }
463
464                    // Reuse your existing element-stringification function
465                    row_string.push_str(&to_string_array_node(el, locale, language));
466                }
467
468                // Enclose the row in braces
469                matrix_string.push('{');
470                matrix_string.push_str(&row_string);
471                matrix_string.push('}');
472            }
473
474            // Enclose the whole matrix in braces
475            format!("{{{matrix_string}}}")
476        }
477        DefinedNameKind((name, ..)) => name.to_string(),
478        TableNameKind(name) => name.to_string(),
479        NamedVariableKind { name, id: _ } => name.to_string(),
480        CompareKind { kind, left, right } => format!(
481            "{}{}{}",
482            to_string_moved(left, move_context, locale, language),
483            kind,
484            to_string_moved(right, move_context, locale, language),
485        ),
486        UnaryKind { kind, right } => match kind {
487            OpUnary::Minus => format!(
488                "-{}",
489                to_string_moved(right, move_context, locale, language)
490            ),
491            OpUnary::Percentage => format!(
492                "{}%",
493                to_string_moved(right, move_context, locale, language)
494            ),
495        },
496        ErrorKind(kind) => format!("{kind}"),
497        ParseErrorKind { formula, .. } => formula.to_string(),
498        EmptyArgKind => "".to_string(),
499        ImplicitIntersection {
500            automatic: _,
501            child,
502        } => {
503            format!(
504                "@{}",
505                to_string_moved(child, move_context, locale, language)
506            )
507        }
508        SpillRangeOperator { child } => {
509            format!(
510                "{}#",
511                to_string_moved(child, move_context, locale, language)
512            )
513        }
514        LambdaDefKind { parameters, body } => {
515            let mut parts: Vec<String> = parameters.iter().map(|p| p.name.clone()).collect();
516            parts.push(to_string_moved(body, move_context, locale, language));
517            format!("LAMBDA({})", parts.join(","))
518        }
519        LambdaCallKind { lambda, args } => {
520            let lambda_str = to_string_moved(lambda, move_context, locale, language);
521            let call_args: Vec<String> = args
522                .iter()
523                .map(|a| to_string_moved(a, move_context, locale, language))
524                .collect();
525            format!("{}({})", lambda_str, call_args.join(","))
526        }
527    }
528}