use crate::{ expressions::{parser::Node, token::OpProduct, types::CellReferenceIndex}, formatter::parser::{ParsePart, Parser}, functions::Function, model::Model, }; pub enum Units { Number { #[allow(dead_code)] group_separator: bool, precision: i32, num_fmt: String, }, Currency { #[allow(dead_code)] group_separator: bool, precision: i32, num_fmt: String, currency: String, }, Percentage { #[allow(dead_code)] group_separator: bool, precision: i32, num_fmt: String, }, Date(String), } impl Units { pub fn get_num_fmt(&self) -> String { match self { Units::Number { num_fmt, .. } => num_fmt.to_string(), Units::Currency { num_fmt, .. } => num_fmt.to_string(), Units::Percentage { num_fmt, .. } => num_fmt.to_string(), Units::Date(num_fmt) => num_fmt.to_string(), } } pub fn get_precision(&self) -> i32 { match self { Units::Number { precision, .. } => *precision, Units::Currency { precision, .. } => *precision, Units::Percentage { precision, .. } => *precision, Units::Date(_) => 0, } } } fn get_units_from_format_string(num_fmt: &str) -> Option { let mut parser = Parser::new(num_fmt); parser.parse(); // We only care about the first part (positive number) match &parser.parts[0] { ParsePart::Number(part) => { if part.percent > 0 { Some(Units::Percentage { num_fmt: num_fmt.to_string(), group_separator: part.use_thousands, precision: part.precision, }) } else if num_fmt.contains('$') { Some(Units::Currency { num_fmt: num_fmt.to_string(), group_separator: part.use_thousands, precision: part.precision, currency: "$".to_string(), }) } else if num_fmt.contains('€') { Some(Units::Currency { num_fmt: num_fmt.to_string(), group_separator: part.use_thousands, precision: part.precision, currency: "€".to_string(), }) } else { Some(Units::Number { num_fmt: num_fmt.to_string(), group_separator: part.use_thousands, precision: part.precision, }) } } ParsePart::Date(_) => Some(Units::Date(num_fmt.to_string())), ParsePart::Error(_) => None, ParsePart::General(_) => None, } } impl Model { fn compute_cell_units(&self, cell_reference: &CellReferenceIndex) -> Option { let cell_style_res = &self.get_style_for_cell( cell_reference.sheet, cell_reference.row, cell_reference.column, ); match cell_style_res { Ok(style) => get_units_from_format_string(&style.num_fmt), Err(_) => None, } } pub(crate) fn compute_node_units( &self, node: &Node, cell: &CellReferenceIndex, ) -> Option { match node { Node::ReferenceKind { sheet_name: _, sheet_index, absolute_row, absolute_column, row, column, } => { let mut row1 = *row; let mut column1 = *column; if !absolute_row { row1 += cell.row; } if !absolute_column { column1 += cell.column; } self.compute_cell_units(&CellReferenceIndex { sheet: *sheet_index, row: row1, column: column1, }) } Node::RangeKind { sheet_name: _, sheet_index, absolute_row1, absolute_column1, row1, column1, absolute_row2: _, absolute_column2: _, row2: _, column2: _, } => { // We return the unit of the first element let mut row1 = *row1; let mut column1 = *column1; if !absolute_row1 { row1 += cell.row; } if !absolute_column1 { column1 += cell.column; } self.compute_cell_units(&CellReferenceIndex { sheet: *sheet_index, row: row1, column: column1, }) } Node::OpSumKind { kind: _, left, right, } => { let left_units = self.compute_node_units(left, cell); let right_units = self.compute_node_units(right, cell); match (&left_units, &right_units) { (Some(_), None) => left_units, (None, Some(_)) => right_units, (Some(l), Some(r)) => { if l.get_precision() < r.get_precision() { right_units } else { left_units } } (None, None) => None, } } Node::OpProductKind { kind, left, right } => { let left_units = self.compute_node_units(left, cell); let right_units = self.compute_node_units(right, cell); match (&left_units, &right_units) { ( Some(Units::Percentage { precision: l, .. }), Some(Units::Percentage { precision: r, .. }), ) => { if l > r { left_units } else { if *r > 1 { return right_units; } // When multiplying percentage we want at least two decimal places Some(Units::Percentage { group_separator: false, precision: 2, num_fmt: "0.00%".to_string(), }) } } ( Some(Units::Currency { currency, precision, .. }), Some(Units::Percentage { .. }), ) => { match kind { OpProduct::Divide => None, OpProduct::Times => { if *precision > 1 { return left_units; } // This is tricky, we need at least 2 digit precision // but I do not want to mess with the num_fmt string Some(Units::Currency { currency: currency.to_string(), group_separator: true, precision: 2, num_fmt: format!("{currency}#,##0.00"), }) } } } ( Some(Units::Percentage { .. }), Some(Units::Currency { precision, currency, .. }), ) => { match kind { OpProduct::Divide => None, OpProduct::Times => { if *precision > 1 { return right_units; } // This is tricky, we need at least 2 digit precision // but I do not want to mess with the num_fmt string Some(Units::Currency { currency: currency.to_string(), group_separator: true, precision: 2, num_fmt: format!("{currency}#,##0.00"), }) } } } (Some(Units::Percentage { .. }), _) => right_units, (_, Some(Units::Percentage { .. })) => match kind { OpProduct::Divide => None, OpProduct::Times => left_units, }, (None, _) => match kind { OpProduct::Divide => None, OpProduct::Times => right_units, }, (_, None) => left_units, ( Some(Units::Number { precision: l, .. }), Some(Units::Number { precision: r, .. }), ) => { if l > r { left_units } else { right_units } } (Some(Units::Number { .. }), _) => match kind { OpProduct::Divide => None, OpProduct::Times => right_units, }, (_, Some(Units::Number { .. })) => left_units, _ => None, } } Node::FunctionKind { kind, args } => self.compute_function_units(kind, args, cell), Node::UnaryKind { kind: _, right } => { // What happens if kind => OpUnary::Percentage? self.compute_node_units(right, cell) } // The rest of the nodes return None Node::BooleanKind(_) => None, Node::NumberKind(_) => None, Node::StringKind(_) => None, Node::WrongReferenceKind { .. } => None, Node::WrongRangeKind { .. } => None, Node::OpRangeKind { .. } => None, Node::OpConcatenateKind { .. } => None, Node::ErrorKind(_) => None, Node::ParseErrorKind { .. } => None, Node::EmptyArgKind => None, Node::InvalidFunctionKind { .. } => None, Node::ArrayKind(_) => None, Node::VariableKind(_) => None, Node::CompareKind { .. } => None, Node::OpPowerKind { .. } => None, } } fn compute_function_units( &self, kind: &Function, args: &[Node], cell: &CellReferenceIndex, ) -> Option { match kind { Function::Sum => self.units_fn_sum_like(args, cell), Function::Average => self.units_fn_sum_like(args, cell), Function::Pmt => self.units_fn_currency(args, cell), Function::Nper => self.units_fn_currency(args, cell), Function::Npv => self.units_fn_currency(args, cell), Function::Irr => self.units_fn_percentage(args, cell), Function::Mirr => self.units_fn_percentage(args, cell), Function::Sln => self.units_fn_currency(args, cell), Function::Syd => self.units_fn_currency(args, cell), Function::Db => self.units_fn_currency(args, cell), Function::Ddb => self.units_fn_currency(args, cell), Function::Cumipmt => self.units_fn_currency(args, cell), Function::Cumprinc => self.units_fn_currency(args, cell), Function::Tbilleq => self.units_fn_percentage_2(args, cell), Function::Tbillprice => self.units_fn_currency(args, cell), Function::Tbillyield => self.units_fn_percentage_2(args, cell), Function::Date => self.units_fn_dates(args, cell), Function::Today => self.units_fn_dates(args, cell), _ => None, } } fn units_fn_sum_like(&self, args: &[Node], cell: &CellReferenceIndex) -> Option { // We return the unit of the first argument if !args.is_empty() { return self.compute_node_units(&args[0], cell); } None } fn units_fn_currency(&self, _args: &[Node], _cell: &CellReferenceIndex) -> Option { let currency_symbol = &self.locale.currency.symbol; let standard_format = &self.locale.numbers.currency_formats.standard; let num_fmt = standard_format.replace('¤', currency_symbol); // The "space" in the cldr is a weird space. let num_fmt = num_fmt.replace(' ', " "); Some(Units::Currency { num_fmt, group_separator: true, precision: 2, currency: currency_symbol.to_string(), }) } fn units_fn_percentage(&self, _args: &[Node], _cell: &CellReferenceIndex) -> Option { Some(Units::Percentage { group_separator: false, precision: 0, num_fmt: "0%".to_string(), }) } fn units_fn_percentage_2(&self, _args: &[Node], _cell: &CellReferenceIndex) -> Option { Some(Units::Percentage { group_separator: false, precision: 2, num_fmt: "0.00%".to_string(), }) } fn units_fn_dates(&self, _args: &[Node], _cell: &CellReferenceIndex) -> Option { // TODO: update locale and use it here Some(Units::Date("dd/mm/yyyy".to_string())) } }