UPDATE: Dump of initial files
This commit is contained in:
762
base/src/expressions/lexer/mod.rs
Normal file
762
base/src/expressions/lexer/mod.rs
Normal file
@@ -0,0 +1,762 @@
|
||||
//! A tokenizer for spreadsheet formulas.
|
||||
//!
|
||||
//! This is meant to feed a formula parser.
|
||||
//!
|
||||
//! You will need to instantiate it with a language and a locale.
|
||||
//!
|
||||
//! It supports two working modes:
|
||||
//!
|
||||
//! 1. A1 or display mode
|
||||
//! This is for user formulas. References are like `D4`, `D$4` or `F5:T10`
|
||||
//! 2. R1C1, internal or runtime mode
|
||||
//! A reference like R1C1 refers to $A$1 and R3C4 to $D$4
|
||||
//! R[2]C[5] refers to a cell two rows below and five columns to the right
|
||||
//! It uses the 'en' locale and language.
|
||||
//! This is used internally at runtime.
|
||||
//!
|
||||
//! Formulas look different in different locales:
|
||||
//!
|
||||
//! =IF(A1, B1, NA()) versus =IF(A1; B1; NA())
|
||||
//!
|
||||
//! Also numbers are different:
|
||||
//!
|
||||
//! 1,123.45 versus 1.123,45
|
||||
//!
|
||||
//! The names of the errors and functions are different in different languages,
|
||||
//! but they stay the same in different locales.
|
||||
//!
|
||||
//! Note that in IronCalc if you are using a locale different from 'en' or a language different from 'en'
|
||||
//! you will still need the 'en' locale and language because formulas are stored in that language and locale
|
||||
//!
|
||||
//! # Examples:
|
||||
//! ```
|
||||
//! use ironcalc_base::expressions::lexer::{Lexer, LexerMode};
|
||||
//! use ironcalc_base::expressions::token::{TokenType, OpCompare};
|
||||
//! use ironcalc_base::locale::get_locale;
|
||||
//! use ironcalc_base::language::get_language;
|
||||
//!
|
||||
//! let locale = get_locale("en").unwrap();
|
||||
//! let language = get_language("en").unwrap();
|
||||
//! let mut lexer = Lexer::new("=A1*SUM(Sheet2!C3:D5)", LexerMode::A1, &locale, &language);
|
||||
//! assert_eq!(lexer.next_token(), TokenType::Compare(OpCompare::Equal));
|
||||
//! assert!(matches!(lexer.next_token(), TokenType::Reference { .. }));
|
||||
//! ```
|
||||
|
||||
use crate::expressions::token::{OpCompare, OpProduct, OpSum};
|
||||
|
||||
use crate::language::Language;
|
||||
use crate::locale::Locale;
|
||||
|
||||
use super::token::{index, Error, TokenType};
|
||||
use super::types::*;
|
||||
use super::utils;
|
||||
|
||||
pub mod util;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
mod ranges;
|
||||
mod structured_references;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct LexerError {
|
||||
pub position: usize,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
pub(super) type Result<T> = std::result::Result<T, LexerError>;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub enum LexerMode {
|
||||
A1,
|
||||
R1C1,
|
||||
}
|
||||
|
||||
/// Tokenize an input
|
||||
#[derive(Clone)]
|
||||
pub struct Lexer {
|
||||
position: usize,
|
||||
next_token_position: Option<usize>,
|
||||
len: usize,
|
||||
chars: Vec<char>,
|
||||
mode: LexerMode,
|
||||
locale: Locale,
|
||||
language: Language,
|
||||
}
|
||||
|
||||
impl Lexer {
|
||||
/// Creates a new `Lexer` that returns the tokens of a formula.
|
||||
pub fn new(formula: &str, mode: LexerMode, locale: &Locale, language: &Language) -> Lexer {
|
||||
let chars: Vec<char> = formula.chars().collect();
|
||||
let len = chars.len();
|
||||
Lexer {
|
||||
chars,
|
||||
position: 0,
|
||||
next_token_position: None,
|
||||
len,
|
||||
mode,
|
||||
locale: locale.clone(),
|
||||
language: language.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes the lexer mode
|
||||
pub fn set_lexer_mode(&mut self, mode: LexerMode) {
|
||||
self.mode = mode;
|
||||
}
|
||||
|
||||
// FIXME: I don't think we should have `is_a1_mode` and `get_formula`.
|
||||
// The caller already knows those two
|
||||
|
||||
/// Returns true if mode is A1
|
||||
pub fn is_a1_mode(&self) -> bool {
|
||||
self.mode == LexerMode::A1
|
||||
}
|
||||
|
||||
/// Returns the formula
|
||||
pub fn get_formula(&self) -> String {
|
||||
self.chars.iter().collect()
|
||||
}
|
||||
|
||||
// FIXME: This is used to get the "marked tokens"
|
||||
// I think a better API would be to return the marked tokens
|
||||
/// Returns the position of the lexer
|
||||
pub fn get_position(&self) -> i32 {
|
||||
self.position as i32
|
||||
}
|
||||
|
||||
/// Resets the formula
|
||||
pub fn set_formula(&mut self, content: &str) {
|
||||
self.chars = content.chars().collect();
|
||||
self.len = self.chars.len();
|
||||
self.position = 0;
|
||||
self.next_token_position = None;
|
||||
}
|
||||
|
||||
/// Returns an error if the token is not the expected one.
|
||||
pub fn expect(&mut self, tk: TokenType) -> Result<()> {
|
||||
let nt = self.next_token();
|
||||
if index(&nt) != index(&tk) {
|
||||
return Err(self.set_error(&format!("Error, expected {}", tk), self.position));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks the next token without advancing position
|
||||
/// See also [advance_token](Self::advance_token)
|
||||
pub fn peek_token(&mut self) -> TokenType {
|
||||
let position = self.position;
|
||||
let tk = self.next_token();
|
||||
self.next_token_position = Some(self.position);
|
||||
self.position = position;
|
||||
tk
|
||||
}
|
||||
|
||||
/// Advances position. This is used in conjunction with [`peek_token`](Self::peek_token)
|
||||
/// It is a noop if the has not been a previous peek_token
|
||||
pub fn advance_token(&mut self) {
|
||||
if let Some(position) = self.next_token_position {
|
||||
self.position = position;
|
||||
self.next_token_position = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the next token
|
||||
pub fn next_token(&mut self) -> TokenType {
|
||||
self.next_token_position = None;
|
||||
self.consume_whitespace();
|
||||
|
||||
match self.read_next_char() {
|
||||
Some(char) => {
|
||||
match char {
|
||||
'+' => TokenType::Addition(OpSum::Add),
|
||||
'-' => TokenType::Addition(OpSum::Minus),
|
||||
'*' => TokenType::Product(OpProduct::Times),
|
||||
'/' => TokenType::Product(OpProduct::Divide),
|
||||
'(' => TokenType::LeftParenthesis,
|
||||
')' => TokenType::RightParenthesis,
|
||||
'=' => TokenType::Compare(OpCompare::Equal),
|
||||
'{' => TokenType::LeftBrace,
|
||||
'}' => TokenType::RightBrace,
|
||||
'[' => TokenType::LeftBracket,
|
||||
']' => TokenType::RightBracket,
|
||||
':' => TokenType::Colon,
|
||||
';' => TokenType::Semicolon,
|
||||
',' => {
|
||||
if self.locale.numbers.symbols.decimal == "," {
|
||||
match self.consume_number(',') {
|
||||
Ok(number) => TokenType::Number(number),
|
||||
Err(error) => TokenType::Illegal(error),
|
||||
}
|
||||
} else {
|
||||
TokenType::Comma
|
||||
}
|
||||
}
|
||||
'.' => {
|
||||
if self.locale.numbers.symbols.decimal == "." {
|
||||
match self.consume_number('.') {
|
||||
Ok(number) => TokenType::Number(number),
|
||||
Err(error) => TokenType::Illegal(error),
|
||||
}
|
||||
} else {
|
||||
// There is no TokenType::PERIOD
|
||||
TokenType::Illegal(self.set_error("Expecting a number", self.position))
|
||||
}
|
||||
}
|
||||
'!' => TokenType::Bang,
|
||||
'^' => TokenType::Power,
|
||||
'%' => TokenType::Percent,
|
||||
'&' => TokenType::And,
|
||||
'$' => self.consume_absolute_reference(),
|
||||
'<' => {
|
||||
let next_token = self.peek_char();
|
||||
if next_token == Some('=') {
|
||||
self.position += 1;
|
||||
TokenType::Compare(OpCompare::LessOrEqualThan)
|
||||
} else if next_token == Some('>') {
|
||||
self.position += 1;
|
||||
TokenType::Compare(OpCompare::NonEqual)
|
||||
} else {
|
||||
TokenType::Compare(OpCompare::LessThan)
|
||||
}
|
||||
}
|
||||
'>' => {
|
||||
if self.peek_char() == Some('=') {
|
||||
self.position += 1;
|
||||
TokenType::Compare(OpCompare::GreaterOrEqualThan)
|
||||
} else {
|
||||
TokenType::Compare(OpCompare::GreaterThan)
|
||||
}
|
||||
}
|
||||
'#' => self.consume_error(),
|
||||
'"' => TokenType::String(self.consume_string()),
|
||||
'\'' => self.consume_quoted_sheet_reference(),
|
||||
'0'..='9' => {
|
||||
let position = self.position - 1;
|
||||
match self.consume_number(char) {
|
||||
Ok(number) => {
|
||||
if self.peek_token() == TokenType::Colon
|
||||
&& self.mode == LexerMode::A1
|
||||
{
|
||||
// Its a row range 3:5
|
||||
// FIXME: There are faster ways of parsing this
|
||||
// Like checking that 'number' is integer and that the next token is integer
|
||||
self.position = position;
|
||||
match self.consume_range_a1() {
|
||||
Ok(ParsedRange { left, right }) => {
|
||||
if let Some(right) = right {
|
||||
TokenType::Range {
|
||||
sheet: None,
|
||||
left,
|
||||
right,
|
||||
}
|
||||
} else {
|
||||
TokenType::Illegal(
|
||||
self.set_error("Expecting row range", position),
|
||||
)
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
// Examples:
|
||||
// * 'Sheet 1'!3.4:5
|
||||
// * 'Sheet 1'!3:A2
|
||||
// * 'Sheet 1'!3:
|
||||
TokenType::Illegal(error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
TokenType::Number(number)
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
// tried to read a number but failed
|
||||
self.position = self.len;
|
||||
TokenType::Illegal(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if char.is_alphabetic() || char == '_' {
|
||||
// At this point is one of the following:
|
||||
// 1. A range with sheet: Sheet3!A3:D7
|
||||
// 2. A boolean: TRUE or FALSE (dependent on the language)
|
||||
// 3. A reference like WS34 or R3C5
|
||||
// 4. A range without sheet ER4:ER7
|
||||
// 5. A column range E:E
|
||||
// 6. An identifier like a function name or a defined name
|
||||
// 7. A range operator A1:OFFSET(...)
|
||||
// 8. An Invalid token
|
||||
let position = self.position;
|
||||
self.position -= 1;
|
||||
let name = self.consume_identifier();
|
||||
let position_indent = self.position;
|
||||
|
||||
let peek_char = self.peek_char();
|
||||
let next_char_is_colon = self.peek_char() == Some(':');
|
||||
|
||||
if peek_char == Some('!') {
|
||||
// reference
|
||||
self.position += 1;
|
||||
return self.consume_range(Some(name));
|
||||
} else if peek_char == Some('$') {
|
||||
self.position = position - 1;
|
||||
return self.consume_range(None);
|
||||
}
|
||||
let name_upper = name.to_ascii_uppercase();
|
||||
if name_upper == self.language.booleans.true_value {
|
||||
return TokenType::Boolean(true);
|
||||
} else if name_upper == self.language.booleans.false_value {
|
||||
return TokenType::Boolean(false);
|
||||
}
|
||||
if self.mode == LexerMode::A1 {
|
||||
let parsed_reference = utils::parse_reference_a1(&name_upper);
|
||||
if parsed_reference.is_some()
|
||||
|| (utils::is_valid_column(name_upper.trim_start_matches('$'))
|
||||
&& next_char_is_colon)
|
||||
{
|
||||
self.position = position - 1;
|
||||
match self.consume_range_a1() {
|
||||
Ok(ParsedRange { left, right }) => {
|
||||
if let Some(right) = right {
|
||||
return TokenType::Range {
|
||||
sheet: None,
|
||||
left,
|
||||
right,
|
||||
};
|
||||
} else {
|
||||
return TokenType::Reference {
|
||||
sheet: None,
|
||||
column: left.column,
|
||||
row: left.row,
|
||||
absolute_row: left.absolute_row,
|
||||
absolute_column: left.absolute_column,
|
||||
};
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
// This could be the range operator: ":"
|
||||
if let Some(r) = parsed_reference {
|
||||
if next_char_is_colon {
|
||||
self.position = position_indent;
|
||||
return TokenType::Reference {
|
||||
sheet: None,
|
||||
row: r.row,
|
||||
column: r.column,
|
||||
absolute_column: r.absolute_column,
|
||||
absolute_row: r.absolute_row,
|
||||
};
|
||||
}
|
||||
}
|
||||
self.position = self.len;
|
||||
return TokenType::Illegal(error);
|
||||
}
|
||||
}
|
||||
} else if utils::is_valid_identifier(&name) {
|
||||
if peek_char == Some('[') {
|
||||
if let Ok(r) = self.consume_structured_reference(&name) {
|
||||
return r;
|
||||
}
|
||||
return TokenType::Illegal(self.set_error(
|
||||
"Invalid structured reference",
|
||||
self.position,
|
||||
));
|
||||
}
|
||||
return TokenType::Ident(name);
|
||||
} else {
|
||||
return TokenType::Illegal(
|
||||
self.set_error("Invalid identifier (A1)", self.position),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let pos = self.position;
|
||||
self.position = position - 1;
|
||||
match self.consume_range_r1c1() {
|
||||
// it's a valid R1C1 range
|
||||
// We need to check it's not something like R1C1P
|
||||
Ok(ParsedRange { left, right }) => {
|
||||
if pos > self.position {
|
||||
self.position = pos;
|
||||
if utils::is_valid_identifier(&name) {
|
||||
return TokenType::Ident(name);
|
||||
} else {
|
||||
self.position = self.len;
|
||||
return TokenType::Illegal(
|
||||
self.set_error(
|
||||
"Invalid identifier (R1C1)",
|
||||
pos,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(right) = right {
|
||||
return TokenType::Range {
|
||||
sheet: None,
|
||||
left,
|
||||
right,
|
||||
};
|
||||
} else {
|
||||
return TokenType::Reference {
|
||||
sheet: None,
|
||||
column: left.column,
|
||||
row: left.row,
|
||||
absolute_row: left.absolute_row,
|
||||
absolute_column: left.absolute_column,
|
||||
};
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
self.position = position - 1;
|
||||
if let Ok(r) = self.consume_reference_r1c1() {
|
||||
if self.peek_char() == Some(':') {
|
||||
return TokenType::Reference {
|
||||
sheet: None,
|
||||
row: r.row,
|
||||
column: r.column,
|
||||
absolute_column: r.absolute_column,
|
||||
absolute_row: r.absolute_row,
|
||||
};
|
||||
}
|
||||
}
|
||||
self.position = pos;
|
||||
|
||||
if utils::is_valid_identifier(&name) {
|
||||
return TokenType::Ident(name);
|
||||
} else {
|
||||
return TokenType::Illegal(self.set_error(
|
||||
&format!("Invalid identifier (R1C1): {name}"),
|
||||
error.position,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
TokenType::Illegal(self.set_error("Unknown error", self.position))
|
||||
}
|
||||
}
|
||||
}
|
||||
None => TokenType::EOF,
|
||||
}
|
||||
}
|
||||
|
||||
// Private methods
|
||||
|
||||
fn set_error(&mut self, message: &str, position: usize) -> LexerError {
|
||||
self.position = self.len;
|
||||
LexerError {
|
||||
position,
|
||||
message: message.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn peek_char(&mut self) -> Option<char> {
|
||||
let position = self.position;
|
||||
if position < self.len {
|
||||
Some(self.chars[position])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_char(&mut self, ch_expected: char) -> Result<()> {
|
||||
let position = self.position;
|
||||
if position >= self.len {
|
||||
return Err(self.set_error(
|
||||
&format!("Error, expected {} found EOF", &ch_expected),
|
||||
self.position,
|
||||
));
|
||||
} else {
|
||||
let ch = self.chars[position];
|
||||
if ch_expected != ch {
|
||||
return Err(self.set_error(
|
||||
&format!("Error, expected {} found {}", &ch_expected, &ch),
|
||||
self.position,
|
||||
));
|
||||
}
|
||||
self.position += 1;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_next_char(&mut self) -> Option<char> {
|
||||
let position = self.position;
|
||||
if position < self.len {
|
||||
self.position = position + 1;
|
||||
Some(self.chars[position])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// Consumes an integer from the input stream
|
||||
fn consume_integer(&mut self, first: char) -> Result<i32> {
|
||||
let mut position = self.position;
|
||||
let len = self.len;
|
||||
let mut chars = first.to_string();
|
||||
while position < len {
|
||||
let next_char = self.chars[position];
|
||||
if next_char.is_ascii_digit() {
|
||||
chars.push(next_char);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
position += 1;
|
||||
}
|
||||
self.position = position;
|
||||
chars.parse::<i32>().map_err(|_| LexerError {
|
||||
position,
|
||||
message: format!("Failed to parse to int: {}", chars),
|
||||
})
|
||||
}
|
||||
|
||||
// Consumes a number in the current locale.
|
||||
// It only takes into account the decimal separator
|
||||
// Note that we do not parse the thousands separator
|
||||
// Let's say ',' is the thousands separator. Then 1,234 would be an error.
|
||||
// This is ok for most cases:
|
||||
// =IF(A1=1,234, TRUE, FALSE) will not work
|
||||
// If a user introduces a single number in the cell 1,234 we should be able to parse
|
||||
// and format the cell appropriately
|
||||
fn consume_number(&mut self, first: char) -> Result<f64> {
|
||||
let mut position = self.position;
|
||||
let len = self.len;
|
||||
let mut chars = first.to_string();
|
||||
// numbers before the decimal point
|
||||
while position < len {
|
||||
let x = self.chars[position];
|
||||
if x.is_ascii_digit() {
|
||||
chars.push(x);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
position += 1;
|
||||
}
|
||||
if position < len && self.chars[position].to_string() == self.locale.numbers.symbols.decimal
|
||||
{
|
||||
// numbers after the decimal point
|
||||
chars.push('.');
|
||||
position += 1;
|
||||
while position < len {
|
||||
let x = self.chars[position];
|
||||
if x.is_ascii_digit() {
|
||||
chars.push(x);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
position += 1;
|
||||
}
|
||||
}
|
||||
if position + 1 < len && (self.chars[position] == 'e' || self.chars[position] == 'E') {
|
||||
// exponential side
|
||||
let x = self.chars[position + 1];
|
||||
if x == '-' || x == '+' || x.is_ascii_digit() {
|
||||
chars.push('e');
|
||||
chars.push(x);
|
||||
position += 2;
|
||||
while position < len {
|
||||
let x = self.chars[position];
|
||||
if x.is_ascii_digit() {
|
||||
chars.push(x);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
position += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.position = position;
|
||||
match chars.parse::<f64>() {
|
||||
Err(_) => {
|
||||
Err(self.set_error(&format!("Failed to parse to double: {}", chars), position))
|
||||
}
|
||||
Ok(v) => Ok(v),
|
||||
}
|
||||
}
|
||||
|
||||
// Consumes an identifier from the input stream
|
||||
fn consume_identifier(&mut self) -> String {
|
||||
let mut position = self.position;
|
||||
while position < self.len {
|
||||
let next_char = self.chars[position];
|
||||
if next_char.is_alphanumeric() || next_char == '_' || next_char == '.' {
|
||||
position += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let chars = self.chars[self.position..position].iter().collect();
|
||||
self.position = position;
|
||||
chars
|
||||
}
|
||||
|
||||
fn consume_string(&mut self) -> String {
|
||||
let mut position = self.position;
|
||||
let len = self.len;
|
||||
let mut chars = "".to_string();
|
||||
while position < len {
|
||||
let x = self.chars[position];
|
||||
position += 1;
|
||||
if x != '"' {
|
||||
chars.push(x);
|
||||
} else if position < len && self.chars[position] == '"' {
|
||||
chars.push(x);
|
||||
chars.push(self.chars[position]);
|
||||
position += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.position = position;
|
||||
chars
|
||||
}
|
||||
|
||||
// Consumes a quoted string from input
|
||||
// 'This is a quoted string'
|
||||
// ' Also is a ''quoted'' string'
|
||||
// Returns an error if it does not find a closing quote
|
||||
fn consume_single_quote_string(&mut self) -> Result<String> {
|
||||
let mut position = self.position;
|
||||
let len = self.len;
|
||||
let mut success = false;
|
||||
let mut needs_escape = false;
|
||||
while position < len {
|
||||
let next_char = self.chars[position];
|
||||
position += 1;
|
||||
if next_char == '\'' {
|
||||
if position == len {
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
if self.chars[position] != '\'' {
|
||||
success = true;
|
||||
break;
|
||||
} else {
|
||||
// In Excel we escape "'" with "''"
|
||||
needs_escape = true;
|
||||
position += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !success {
|
||||
// We reached the end without the closing quote
|
||||
return Err(self.set_error("Expected closing \"'\" but found end of input", position));
|
||||
}
|
||||
let chars: String = self.chars[self.position..position - 1].iter().collect();
|
||||
self.position = position;
|
||||
if needs_escape {
|
||||
// In most cases we will not needs escaping so this would be an overkill
|
||||
return Ok(chars.replace("''", "'"));
|
||||
}
|
||||
|
||||
Ok(chars)
|
||||
}
|
||||
|
||||
// Reads an error from the input stream
|
||||
fn consume_error(&mut self) -> TokenType {
|
||||
let errors = &self.language.errors;
|
||||
let rest_of_formula: String = self.chars[self.position - 1..self.len].iter().collect();
|
||||
if rest_of_formula.starts_with(&errors.ref_value) {
|
||||
self.position += errors.ref_value.chars().count() - 1;
|
||||
return TokenType::Error(Error::REF);
|
||||
} else if rest_of_formula.starts_with(&errors.name) {
|
||||
self.position += errors.name.chars().count() - 1;
|
||||
return TokenType::Error(Error::NAME);
|
||||
} else if rest_of_formula.starts_with(&errors.value) {
|
||||
self.position += errors.value.chars().count() - 1;
|
||||
return TokenType::Error(Error::VALUE);
|
||||
} else if rest_of_formula.starts_with(&errors.div) {
|
||||
self.position += errors.div.chars().count() - 1;
|
||||
return TokenType::Error(Error::DIV);
|
||||
} else if rest_of_formula.starts_with(&errors.na) {
|
||||
self.position += errors.na.chars().count() - 1;
|
||||
return TokenType::Error(Error::NA);
|
||||
} else if rest_of_formula.starts_with(&errors.num) {
|
||||
self.position += errors.num.chars().count() - 1;
|
||||
return TokenType::Error(Error::NUM);
|
||||
} else if rest_of_formula.starts_with(&errors.error) {
|
||||
self.position += errors.error.chars().count() - 1;
|
||||
return TokenType::Error(Error::ERROR);
|
||||
} else if rest_of_formula.starts_with(&errors.nimpl) {
|
||||
self.position += errors.nimpl.chars().count() - 1;
|
||||
return TokenType::Error(Error::NIMPL);
|
||||
} else if rest_of_formula.starts_with(&errors.spill) {
|
||||
self.position += errors.spill.chars().count() - 1;
|
||||
return TokenType::Error(Error::SPILL);
|
||||
} else if rest_of_formula.starts_with(&errors.calc) {
|
||||
self.position += errors.calc.chars().count() - 1;
|
||||
return TokenType::Error(Error::CALC);
|
||||
} else if rest_of_formula.starts_with(&errors.null) {
|
||||
self.position += errors.null.chars().count() - 1;
|
||||
return TokenType::Error(Error::NULL);
|
||||
} else if rest_of_formula.starts_with(&errors.circ) {
|
||||
self.position += errors.circ.chars().count() - 1;
|
||||
return TokenType::Error(Error::CIRC);
|
||||
}
|
||||
TokenType::Illegal(self.set_error("Invalid error.", self.position))
|
||||
}
|
||||
|
||||
fn consume_whitespace(&mut self) {
|
||||
let mut position = self.position;
|
||||
let len = self.len;
|
||||
while position < len {
|
||||
let x = self.chars[position];
|
||||
if !x.is_whitespace() {
|
||||
break;
|
||||
}
|
||||
position += 1;
|
||||
}
|
||||
self.position = position;
|
||||
}
|
||||
|
||||
fn consume_absolute_reference(&mut self) -> TokenType {
|
||||
// This is an absolute reference.
|
||||
// $A$4
|
||||
if self.mode == LexerMode::R1C1 {
|
||||
return TokenType::Illegal(
|
||||
self.set_error("Cannot parse A1 reference in R1C1 mode", self.position),
|
||||
);
|
||||
}
|
||||
self.position -= 1;
|
||||
self.consume_range(None)
|
||||
}
|
||||
|
||||
fn consume_quoted_sheet_reference(&mut self) -> TokenType {
|
||||
// This is a reference:
|
||||
// 'First Sheet'!A34
|
||||
let sheet_name = match self.consume_single_quote_string() {
|
||||
Ok(v) => v,
|
||||
Err(error) => {
|
||||
return TokenType::Illegal(error);
|
||||
}
|
||||
};
|
||||
if self.next_token() != TokenType::Bang {
|
||||
return TokenType::Illegal(self.set_error("Expected '!'", self.position));
|
||||
}
|
||||
self.consume_range(Some(sheet_name))
|
||||
}
|
||||
|
||||
fn consume_range(&mut self, sheet: Option<String>) -> TokenType {
|
||||
let m = if self.mode == LexerMode::A1 {
|
||||
self.consume_range_a1()
|
||||
} else {
|
||||
self.consume_range_r1c1()
|
||||
};
|
||||
match m {
|
||||
Ok(ParsedRange { left, right }) => {
|
||||
if let Some(right) = right {
|
||||
TokenType::Range { sheet, left, right }
|
||||
} else {
|
||||
TokenType::Reference {
|
||||
sheet,
|
||||
column: left.column,
|
||||
row: left.row,
|
||||
absolute_row: left.absolute_row,
|
||||
absolute_column: left.absolute_column,
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => TokenType::Illegal(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
319
base/src/expressions/lexer/ranges.rs
Normal file
319
base/src/expressions/lexer/ranges.rs
Normal file
@@ -0,0 +1,319 @@
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::{token::TokenType, utils::column_to_number};
|
||||
|
||||
use super::Lexer;
|
||||
use super::{ParsedRange, ParsedReference, Result};
|
||||
|
||||
impl Lexer {
|
||||
// Consumes a reference in A1 style like:
|
||||
// AS23, $AS23, AS$23, $AS$23, R12
|
||||
// Or returns an error
|
||||
fn consume_reference_a1(&mut self) -> Result<ParsedReference> {
|
||||
let mut absolute_column = false;
|
||||
let mut absolute_row = false;
|
||||
let mut position = self.position;
|
||||
let len = self.len;
|
||||
if position < len && self.chars[position] == '$' {
|
||||
absolute_column = true;
|
||||
position += 1;
|
||||
}
|
||||
let mut column = "".to_string();
|
||||
while position < len {
|
||||
let x = self.chars[position].to_ascii_uppercase();
|
||||
match x {
|
||||
'A'..='Z' => column.push(x),
|
||||
_ => break,
|
||||
}
|
||||
position += 1;
|
||||
}
|
||||
if column.is_empty() {
|
||||
return Err(self.set_error("Failed to parse reference", position));
|
||||
}
|
||||
if position < len && self.chars[position] == '$' {
|
||||
absolute_row = true;
|
||||
position += 1;
|
||||
}
|
||||
let mut row = "".to_string();
|
||||
while position < len {
|
||||
let x = self.chars[position];
|
||||
match x {
|
||||
'0'..='9' => row.push(x),
|
||||
_ => break,
|
||||
}
|
||||
position += 1;
|
||||
}
|
||||
// Note that row numbers could start with 0
|
||||
self.position = position;
|
||||
let column = column_to_number(&column).map_err(|error| self.set_error(&error, position))?;
|
||||
|
||||
match row.parse::<i32>() {
|
||||
Ok(row) => {
|
||||
if row > LAST_ROW {
|
||||
return Err(self.set_error("Row too large in reference", position));
|
||||
}
|
||||
Ok(ParsedReference {
|
||||
column,
|
||||
row,
|
||||
absolute_column,
|
||||
absolute_row,
|
||||
})
|
||||
}
|
||||
Err(..) => Err(self.set_error("Failed to parse integer", position)),
|
||||
}
|
||||
}
|
||||
|
||||
// Parsing a range is a parser on it's own right. Here is the grammar:
|
||||
//
|
||||
// range -> cell | cell ':' cell | row ':' row | column ':' column
|
||||
// cell -> column row
|
||||
// column -> '$' column_name | column_name
|
||||
// row -> '$' row_name | row_name
|
||||
// column_name -> 'A'..'XFD'
|
||||
// row_name -> 1..1_048_576
|
||||
pub(super) fn consume_range_a1(&mut self) -> Result<ParsedRange> {
|
||||
// first let's try to parse a cell
|
||||
let mut position = self.position;
|
||||
match self.consume_reference_a1() {
|
||||
Ok(cell) => {
|
||||
if self.peek_char() == Some(':') {
|
||||
// It's a range
|
||||
self.position += 1;
|
||||
if let Ok(cell2) = self.consume_reference_a1() {
|
||||
Ok(ParsedRange {
|
||||
left: cell,
|
||||
right: Some(cell2),
|
||||
})
|
||||
} else {
|
||||
Err(self.set_error("Expecting reference in range", self.position))
|
||||
}
|
||||
} else {
|
||||
// just a reference
|
||||
Ok(ParsedRange {
|
||||
left: cell,
|
||||
right: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
self.position = position;
|
||||
// It's either a row range or a column range (or not a range at all)
|
||||
let len = self.len;
|
||||
let mut absolute_left = false;
|
||||
if position < len && self.chars[position] == '$' {
|
||||
absolute_left = true;
|
||||
position += 1;
|
||||
}
|
||||
let mut column_left = "".to_string();
|
||||
let mut row_left = "".to_string();
|
||||
while position < len {
|
||||
let x = self.chars[position].to_ascii_uppercase();
|
||||
match x {
|
||||
'A'..='Z' => column_left.push(x),
|
||||
'0'..='9' => row_left.push(x),
|
||||
_ => break,
|
||||
}
|
||||
position += 1;
|
||||
}
|
||||
if position >= len || self.chars[position] != ':' {
|
||||
return Err(self.set_error("Expecting reference in range", self.position));
|
||||
}
|
||||
position += 1;
|
||||
let mut absolute_right = false;
|
||||
if position < len && self.chars[position] == '$' {
|
||||
absolute_right = true;
|
||||
position += 1;
|
||||
}
|
||||
let mut column_right = "".to_string();
|
||||
let mut row_right = "".to_string();
|
||||
while position < len {
|
||||
let x = self.chars[position].to_ascii_uppercase();
|
||||
match x {
|
||||
'A'..='Z' => column_right.push(x),
|
||||
'0'..='9' => row_right.push(x),
|
||||
_ => break,
|
||||
}
|
||||
position += 1;
|
||||
}
|
||||
self.position = position;
|
||||
// At this point either the columns are the empty string or the rows are the empty string
|
||||
if !row_left.is_empty() {
|
||||
// It is a row range 23:56
|
||||
if row_right.is_empty() || !column_left.is_empty() || !column_right.is_empty() {
|
||||
return Err(self.set_error("Error parsing Range", position));
|
||||
}
|
||||
// Note that row numbers can start with 0
|
||||
let row_left = match row_left.parse::<i32>() {
|
||||
Ok(n) => n,
|
||||
Err(_) => {
|
||||
return Err(self
|
||||
.set_error(&format!("Failed parsing row {}", row_left), position))
|
||||
}
|
||||
};
|
||||
let row_right = match row_right.parse::<i32>() {
|
||||
Ok(n) => n,
|
||||
Err(_) => {
|
||||
return Err(self
|
||||
.set_error(&format!("Failed parsing row {}", row_right), position))
|
||||
}
|
||||
};
|
||||
if row_left > LAST_ROW {
|
||||
return Err(self.set_error("Row too large in reference", position));
|
||||
}
|
||||
if row_right > LAST_ROW {
|
||||
return Err(self.set_error("Row too large in reference", position));
|
||||
}
|
||||
return Ok(ParsedRange {
|
||||
left: ParsedReference {
|
||||
row: row_left,
|
||||
absolute_row: absolute_left,
|
||||
column: 1,
|
||||
absolute_column: true,
|
||||
},
|
||||
right: Some(ParsedReference {
|
||||
row: row_right,
|
||||
absolute_row: absolute_right,
|
||||
column: LAST_COLUMN,
|
||||
absolute_column: true,
|
||||
}),
|
||||
});
|
||||
}
|
||||
// It is a column range
|
||||
if column_right.is_empty() || !row_right.is_empty() {
|
||||
return Err(self.set_error("Error parsing Range", position));
|
||||
}
|
||||
let column_left = column_to_number(&column_left)
|
||||
.map_err(|error| self.set_error(&error, position))?;
|
||||
let column_right = column_to_number(&column_right)
|
||||
.map_err(|error| self.set_error(&error, position))?;
|
||||
Ok(ParsedRange {
|
||||
left: ParsedReference {
|
||||
row: 1,
|
||||
absolute_row: true,
|
||||
column: column_left,
|
||||
absolute_column: absolute_left,
|
||||
},
|
||||
right: Some(ParsedReference {
|
||||
row: LAST_ROW,
|
||||
absolute_row: true,
|
||||
column: column_right,
|
||||
absolute_column: absolute_right,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn consume_range_r1c1(&mut self) -> Result<ParsedRange> {
|
||||
// first let's try to parse a cell
|
||||
match self.consume_reference_r1c1() {
|
||||
Ok(cell) => {
|
||||
if self.peek_char() == Some(':') {
|
||||
// It's a range
|
||||
self.position += 1;
|
||||
if let Ok(cell2) = self.consume_reference_r1c1() {
|
||||
Ok(ParsedRange {
|
||||
left: cell,
|
||||
right: Some(cell2),
|
||||
})
|
||||
} else {
|
||||
Err(self.set_error("Expecting reference in range", self.position))
|
||||
}
|
||||
} else {
|
||||
// just a reference
|
||||
Ok(ParsedRange {
|
||||
left: cell,
|
||||
right: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
Err(s) => Err(s),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn consume_reference_r1c1(&mut self) -> Result<ParsedReference> {
|
||||
// R12C3, R[2]C[-2], R3C[6], R[-3]C4, RC1, R[-2]C
|
||||
let absolute_column;
|
||||
let absolute_row;
|
||||
let position = self.position;
|
||||
let row;
|
||||
let column;
|
||||
self.expect_char('R')?;
|
||||
match self.peek_char() {
|
||||
Some('[') => {
|
||||
absolute_row = false;
|
||||
self.expect_char('[')?;
|
||||
let c = match self.read_next_char() {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
return Err(self.set_error("Expected column number", position));
|
||||
}
|
||||
};
|
||||
match self.consume_integer(c) {
|
||||
Ok(v) => row = v,
|
||||
Err(_) => {
|
||||
return Err(self.set_error("Expected row number", position));
|
||||
}
|
||||
}
|
||||
self.expect(TokenType::RightBracket)?;
|
||||
}
|
||||
Some(c) => {
|
||||
absolute_row = true;
|
||||
self.expect_char(c)?;
|
||||
match self.consume_integer(c) {
|
||||
Ok(v) => row = v,
|
||||
Err(_) => {
|
||||
return Err(self.set_error("Expected row number", position));
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
return Err(self.set_error("Expected row number or '['", position));
|
||||
}
|
||||
}
|
||||
self.expect_char('C')?;
|
||||
match self.peek_char() {
|
||||
Some('[') => {
|
||||
self.expect_char('[')?;
|
||||
absolute_column = false;
|
||||
let c = match self.read_next_char() {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
return Err(self.set_error("Expected column number", position));
|
||||
}
|
||||
};
|
||||
match self.consume_integer(c) {
|
||||
Ok(v) => column = v,
|
||||
Err(_) => {
|
||||
return Err(self.set_error("Expected column number", position));
|
||||
}
|
||||
}
|
||||
self.expect(TokenType::RightBracket)?;
|
||||
}
|
||||
Some(c) => {
|
||||
absolute_column = true;
|
||||
self.expect_char(c)?;
|
||||
match self.consume_integer(c) {
|
||||
Ok(v) => column = v,
|
||||
Err(_) => {
|
||||
return Err(self.set_error("Expected column number", position));
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
return Err(self.set_error("Expected column number or '['", position));
|
||||
}
|
||||
}
|
||||
if let Some(c) = self.peek_char() {
|
||||
if c.is_alphanumeric() {
|
||||
return Err(self.set_error("Expected end of reference", position));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ParsedReference {
|
||||
column,
|
||||
row,
|
||||
absolute_column,
|
||||
absolute_row,
|
||||
})
|
||||
}
|
||||
}
|
||||
188
base/src/expressions/lexer/structured_references.rs
Normal file
188
base/src/expressions/lexer/structured_references.rs
Normal file
@@ -0,0 +1,188 @@
|
||||
// Grammar:
|
||||
// structured references -> table_name "[" arguments "]"
|
||||
// arguments -> table_reference | "["specifier"]" "," table_reference
|
||||
// specifier > "#All" |
|
||||
// "#This Row" |
|
||||
// "#Data" |
|
||||
// "#Headers" |
|
||||
// "#Totals"
|
||||
// table_reference -> column_reference | range_reference
|
||||
// column reference -> column_name | "["column_name"]"
|
||||
// range_reference -> column_reference":"column_reference
|
||||
|
||||
use crate::expressions::token::TokenType;
|
||||
use crate::expressions::token::{TableReference, TableSpecifier};
|
||||
|
||||
use super::Result;
|
||||
use super::{Lexer, LexerError};
|
||||
|
||||
impl Lexer {
|
||||
fn consume_table_specifier(&mut self) -> Result<Option<TableSpecifier>> {
|
||||
if self.peek_char() == Some('#') {
|
||||
// It's a specifier
|
||||
// TODO(TD): There are better ways of doing this :)
|
||||
let rest_of_formula: String = self.chars[self.position..self.len].iter().collect();
|
||||
let specifier = if rest_of_formula.starts_with("#This Row]") {
|
||||
self.position += "#This Row]".bytes().len();
|
||||
TableSpecifier::ThisRow
|
||||
} else if rest_of_formula.starts_with("#All]") {
|
||||
self.position += "#All]".bytes().len();
|
||||
TableSpecifier::All
|
||||
} else if rest_of_formula.starts_with("#Data]") {
|
||||
self.position += "#Data]".bytes().len();
|
||||
TableSpecifier::Data
|
||||
} else if rest_of_formula.starts_with("#Headers]") {
|
||||
self.position += "#Headers]".bytes().len();
|
||||
TableSpecifier::Headers
|
||||
} else if rest_of_formula.starts_with("#Totals]") {
|
||||
self.position += "#Totals]".bytes().len();
|
||||
TableSpecifier::Totals
|
||||
} else {
|
||||
return Err(LexerError {
|
||||
position: self.position,
|
||||
message: "Invalid structured reference".to_string(),
|
||||
});
|
||||
};
|
||||
Ok(Some(specifier))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn consume_column_reference(&mut self) -> Result<String> {
|
||||
self.consume_whitespace();
|
||||
let end_char = if self.peek_char() == Some('[') {
|
||||
self.position += 1;
|
||||
']'
|
||||
} else {
|
||||
')'
|
||||
};
|
||||
|
||||
let mut position = self.position;
|
||||
while position < self.len {
|
||||
let next_char = self.chars[position];
|
||||
if next_char != end_char {
|
||||
position += 1;
|
||||
if next_char == '\'' {
|
||||
if position == self.len {
|
||||
return Err(LexerError {
|
||||
position: self.position,
|
||||
message: "Invalid column name".to_string(),
|
||||
});
|
||||
}
|
||||
// skip next char
|
||||
position += 1
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let chars: String = self.chars[self.position..position].iter().collect();
|
||||
if end_char == ']' {
|
||||
position += 1;
|
||||
}
|
||||
self.position = position;
|
||||
Ok(chars
|
||||
.replace("'[", "[")
|
||||
.replace("']", "]")
|
||||
.replace("'#", "#")
|
||||
.replace("'@", "@")
|
||||
.replace("''", "'"))
|
||||
}
|
||||
|
||||
// Possibilities:
|
||||
// 1. MyTable[#Totals] or MyTable[#This Row]
|
||||
// 2. MyTable[MyColumn]
|
||||
// 3. MyTable[[My Column]]
|
||||
// 4. MyTable[[#This Row], [My Column]]
|
||||
// 5. MyTable[[#Totals], [MyColumn]]
|
||||
// 6. MyTable[[#This Row], [Jan]:[Dec]]
|
||||
// 7. MyTable[]
|
||||
//
|
||||
// Multiple specifiers are not supported yet:
|
||||
// 1. MyTable[[#Data], [#Totals], [MyColumn]]
|
||||
//
|
||||
// In particular note that names of columns are escaped only when they are in the first argument
|
||||
// We use '[' and ']'
|
||||
// When there is only a specifier but not a reference the specifier is not in brackets
|
||||
//
|
||||
// Invalid:
|
||||
// * MyTable[#Totals, [Jan]:[March]] => MyTable[[#Totals], [Jan]:[March]]
|
||||
//
|
||||
// NOTES:
|
||||
// * MyTable[[#Totals]] is translated into MyTable[#Totals]
|
||||
// * Excel shows '@' instead of '#This Row':
|
||||
// MyTable[[#This Row], [Jan]:[Dec]] => MyTable[@[Jan]:[Dec]]
|
||||
// But this is only a UI thing that we will ignore for now.
|
||||
pub(crate) fn consume_structured_reference(&mut self, table_name: &str) -> Result<TokenType> {
|
||||
self.expect(TokenType::LeftBracket)?;
|
||||
let peek_char = self.peek_char();
|
||||
if peek_char == Some(']') {
|
||||
// This is just a reference to the full table
|
||||
self.expect(TokenType::RightBracket)?;
|
||||
return Ok(TokenType::Ident(table_name.to_string()));
|
||||
}
|
||||
if peek_char == Some('#') {
|
||||
// Expecting MyTable[#Totals]
|
||||
if let Some(specifier) = self.consume_table_specifier()? {
|
||||
return Ok(TokenType::StructuredReference {
|
||||
table_name: table_name.to_string(),
|
||||
specifier: Some(specifier),
|
||||
table_reference: None,
|
||||
});
|
||||
} else {
|
||||
return Err(LexerError {
|
||||
position: self.position,
|
||||
message: "Invalid structured reference".to_string(),
|
||||
});
|
||||
}
|
||||
} else if peek_char != Some('[') {
|
||||
// Expecting MyTable[MyColumn]
|
||||
self.position -= 1;
|
||||
let column_name = self.consume_column_reference()?;
|
||||
return Ok(TokenType::StructuredReference {
|
||||
table_name: table_name.to_string(),
|
||||
specifier: None,
|
||||
table_reference: Some(TableReference::ColumnReference(column_name)),
|
||||
});
|
||||
}
|
||||
self.expect(TokenType::LeftBracket)?;
|
||||
let specifier = self.consume_table_specifier()?;
|
||||
if specifier.is_some() {
|
||||
let peek_token = self.peek_token();
|
||||
if peek_token == TokenType::Comma {
|
||||
self.advance_token();
|
||||
self.expect(TokenType::LeftBracket)?;
|
||||
} else if peek_token == TokenType::RightBracket {
|
||||
return Ok(TokenType::StructuredReference {
|
||||
table_name: table_name.to_string(),
|
||||
specifier,
|
||||
table_reference: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Now it's either:
|
||||
// [Column Name]
|
||||
// [Column Name]:[Column Name]
|
||||
self.position -= 1;
|
||||
let column_reference = self.consume_column_reference()?;
|
||||
let table_reference = if self.peek_char() == Some(':') {
|
||||
self.position += 1;
|
||||
let column_reference_right = self.consume_column_reference()?;
|
||||
self.expect(TokenType::RightBracket)?;
|
||||
Some(TableReference::RangeReference((
|
||||
column_reference,
|
||||
column_reference_right,
|
||||
)))
|
||||
} else {
|
||||
self.expect(TokenType::RightBracket)?;
|
||||
Some(TableReference::ColumnReference(column_reference))
|
||||
};
|
||||
Ok(TokenType::StructuredReference {
|
||||
table_name: table_name.to_string(),
|
||||
specifier,
|
||||
table_reference,
|
||||
})
|
||||
}
|
||||
}
|
||||
6
base/src/expressions/lexer/test/mod.rs
Normal file
6
base/src/expressions/lexer/test/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
mod test_common;
|
||||
mod test_language;
|
||||
mod test_locale;
|
||||
mod test_ranges;
|
||||
mod test_tables;
|
||||
mod test_util;
|
||||
508
base/src/expressions/lexer/test/test_common.rs
Normal file
508
base/src/expressions/lexer/test/test_common.rs
Normal file
@@ -0,0 +1,508 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::language::get_language;
|
||||
use crate::locale::get_locale;
|
||||
|
||||
use crate::expressions::{
|
||||
lexer::{Lexer, LexerMode},
|
||||
token::TokenType::*,
|
||||
token::{Error, OpSum},
|
||||
};
|
||||
|
||||
fn new_lexer(formula: &str, a1_mode: bool) -> Lexer {
|
||||
let locale = get_locale("en").unwrap();
|
||||
let language = get_language("en").unwrap();
|
||||
let mode = if a1_mode {
|
||||
LexerMode::A1
|
||||
} else {
|
||||
LexerMode::R1C1
|
||||
};
|
||||
Lexer::new(formula, mode, locale, language)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_number_zero() {
|
||||
let mut lx = new_lexer("0", true);
|
||||
assert_eq!(lx.next_token(), Number(0.0));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
#[test]
|
||||
fn test_number_integer() {
|
||||
let mut lx = new_lexer("42", true);
|
||||
assert_eq!(lx.next_token(), Number(42.0));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
#[test]
|
||||
fn test_number_pi() {
|
||||
let mut lx = new_lexer("3.415", true);
|
||||
assert_eq!(lx.next_token(), Number(3.415));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
#[test]
|
||||
fn test_number_less_than_one() {
|
||||
let mut lx = new_lexer(".1415", true);
|
||||
assert_eq!(lx.next_token(), Number(0.1415));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
#[test]
|
||||
fn test_number_less_than_one_bis() {
|
||||
let mut lx = new_lexer("0.1415", true);
|
||||
assert_eq!(lx.next_token(), Number(0.1415));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
#[test]
|
||||
fn test_number_scientific() {
|
||||
let mut lx = new_lexer("1.1415e12", true);
|
||||
assert_eq!(lx.next_token(), Number(1.1415e12));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
#[test]
|
||||
fn test_number_scientific_1() {
|
||||
let mut lx = new_lexer("2.4e-12", true);
|
||||
assert_eq!(lx.next_token(), Number(2.4e-12));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_number_scientific_1b() {
|
||||
let mut lx = new_lexer("2.4E-12", true);
|
||||
assert_eq!(lx.next_token(), Number(2.4e-12));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_not_a_number() {
|
||||
let mut lx = new_lexer("..", true);
|
||||
assert!(matches!(lx.next_token(), Illegal(_)));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string() {
|
||||
let mut lx = new_lexer("\"Hello World!\"", true);
|
||||
assert_eq!(lx.next_token(), String("Hello World!".to_string()));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_unicode() {
|
||||
let mut lx = new_lexer("\"你好,世界!\"", true);
|
||||
assert_eq!(lx.next_token(), String("你好,世界!".to_string()));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_boolean() {
|
||||
let mut lx = new_lexer("FALSE", true);
|
||||
assert_eq!(lx.next_token(), Boolean(false));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_boolean_true() {
|
||||
let mut lx = new_lexer("True", true);
|
||||
assert_eq!(lx.next_token(), Boolean(true));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reference() {
|
||||
let mut lx = new_lexer("A1", true);
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Reference {
|
||||
sheet: None,
|
||||
column: 1,
|
||||
row: 1,
|
||||
absolute_column: false,
|
||||
absolute_row: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reference_absolute() {
|
||||
let mut lx = new_lexer("$A$1", true);
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Reference {
|
||||
sheet: None,
|
||||
column: 1,
|
||||
row: 1,
|
||||
absolute_column: true,
|
||||
absolute_row: true,
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reference_absolute_1() {
|
||||
let mut lx = new_lexer("AB$12", true);
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Reference {
|
||||
sheet: None,
|
||||
column: 28,
|
||||
row: 12,
|
||||
absolute_column: false,
|
||||
absolute_row: true,
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reference_absolute_2() {
|
||||
let mut lx = new_lexer("$CC234", true);
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Reference {
|
||||
sheet: None,
|
||||
column: 81,
|
||||
row: 234,
|
||||
absolute_column: true,
|
||||
absolute_row: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reference_sheet() {
|
||||
let mut lx = new_lexer("Sheet1!C34", true);
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Reference {
|
||||
sheet: Some("Sheet1".to_string()),
|
||||
column: 3,
|
||||
row: 34,
|
||||
absolute_column: false,
|
||||
absolute_row: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reference_sheet_unicode() {
|
||||
// Not that also tests the '!'
|
||||
let mut lx = new_lexer("'A € world!'!C34", true);
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Reference {
|
||||
sheet: Some("A € world!".to_string()),
|
||||
column: 3,
|
||||
row: 34,
|
||||
absolute_column: false,
|
||||
absolute_row: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reference_sheet_unicode_absolute() {
|
||||
let mut lx = new_lexer("'A €'!$C$34", true);
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Reference {
|
||||
sheet: Some("A €".to_string()),
|
||||
column: 3,
|
||||
row: 34,
|
||||
absolute_column: true,
|
||||
absolute_row: true,
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unmatched_quote() {
|
||||
let mut lx = new_lexer("'A €!$C$34", true);
|
||||
assert!(matches!(lx.next_token(), Illegal(_)));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sum() {
|
||||
let mut lx = new_lexer("2.4+3.415", true);
|
||||
assert_eq!(lx.next_token(), Number(2.4));
|
||||
assert_eq!(lx.next_token(), Addition(OpSum::Add));
|
||||
assert_eq!(lx.next_token(), Number(3.415));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sum_1() {
|
||||
let mut lx = new_lexer("A2 + 'First Sheet'!$B$3", true);
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Reference {
|
||||
sheet: None,
|
||||
column: 1,
|
||||
row: 2,
|
||||
absolute_column: false,
|
||||
absolute_row: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), Addition(OpSum::Add));
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Reference {
|
||||
sheet: Some("First Sheet".to_string()),
|
||||
column: 2,
|
||||
row: 3,
|
||||
absolute_column: true,
|
||||
absolute_row: true,
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_value() {
|
||||
let mut lx = new_lexer("#VALUE!", true);
|
||||
assert_eq!(lx.next_token(), Error(Error::VALUE));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_error() {
|
||||
let mut lx = new_lexer("#ERROR!", true);
|
||||
assert_eq!(lx.next_token(), Error(Error::ERROR));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_div() {
|
||||
let mut lx = new_lexer("#DIV/0!", true);
|
||||
assert_eq!(lx.next_token(), Error(Error::DIV));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_na() {
|
||||
let mut lx = new_lexer("#N/A", true);
|
||||
assert_eq!(lx.next_token(), Error(Error::NA));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_name() {
|
||||
let mut lx = new_lexer("#NAME?", true);
|
||||
assert_eq!(lx.next_token(), Error(Error::NAME));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_num() {
|
||||
let mut lx = new_lexer("#NUM!", true);
|
||||
assert_eq!(lx.next_token(), Error(Error::NUM));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_calc() {
|
||||
let mut lx = new_lexer("#CALC!", true);
|
||||
assert_eq!(lx.next_token(), Error(Error::CALC));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_null() {
|
||||
let mut lx = new_lexer("#NULL!", true);
|
||||
assert_eq!(lx.next_token(), Error(Error::NULL));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_spill() {
|
||||
let mut lx = new_lexer("#SPILL!", true);
|
||||
assert_eq!(lx.next_token(), Error(Error::SPILL));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_circ() {
|
||||
let mut lx = new_lexer("#CIRC!", true);
|
||||
assert_eq!(lx.next_token(), Error(Error::CIRC));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_invalid() {
|
||||
let mut lx = new_lexer("#VALU!", true);
|
||||
assert!(matches!(lx.next_token(), Illegal(_)));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_errors() {
|
||||
let mut lx = new_lexer("#DIV/0!+#NUM!", true);
|
||||
assert_eq!(lx.next_token(), Error(Error::DIV));
|
||||
assert_eq!(lx.next_token(), Addition(OpSum::Add));
|
||||
assert_eq!(lx.next_token(), Error(Error::NUM));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_variable_name() {
|
||||
let mut lx = new_lexer("MyVar", true);
|
||||
assert_eq!(lx.next_token(), Ident("MyVar".to_string()));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_last_reference() {
|
||||
let mut lx = new_lexer("XFD1048576", true);
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Reference {
|
||||
sheet: None,
|
||||
column: 16384,
|
||||
row: 1048576,
|
||||
absolute_column: false,
|
||||
absolute_row: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_not_a_reference() {
|
||||
let mut lx = new_lexer("XFE10", true);
|
||||
assert_eq!(lx.next_token(), Ident("XFE10".to_string()));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reference_r1c1() {
|
||||
let mut lx = new_lexer("R1C1", false);
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Reference {
|
||||
sheet: None,
|
||||
column: 1,
|
||||
row: 1,
|
||||
absolute_column: true,
|
||||
absolute_row: true,
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reference_r1c1_true() {
|
||||
let mut lx = new_lexer("R1C1", true);
|
||||
// NOTE: This is what google docs does.
|
||||
// Excel will not let you enter this formula.
|
||||
// Online Excel will let you and will mark the cell as in Error
|
||||
assert!(matches!(lx.next_token(), Illegal(_)));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_name_r1c1p() {
|
||||
let mut lx = new_lexer("R1C1P", false);
|
||||
assert_eq!(lx.next_token(), Ident("R1C1P".to_string()));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_name_wrong_ref() {
|
||||
let mut lx = new_lexer("Sheet1!2", false);
|
||||
assert!(matches!(lx.next_token(), Illegal(_)));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reference_1() {
|
||||
let mut lx = new_lexer("Sheet1!R[1]C[2]", false);
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Reference {
|
||||
sheet: Some("Sheet1".to_string()),
|
||||
column: 2,
|
||||
row: 1,
|
||||
absolute_column: false,
|
||||
absolute_row: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reference_quotes() {
|
||||
let mut lx = new_lexer("'Sheet 1'!R[1]C[2]", false);
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Reference {
|
||||
sheet: Some("Sheet 1".to_string()),
|
||||
column: 2,
|
||||
row: 1,
|
||||
absolute_column: false,
|
||||
absolute_row: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reference_escape_quotes() {
|
||||
let mut lx = new_lexer("'Sheet ''one'' 1'!R[1]C[2]", false);
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Reference {
|
||||
sheet: Some("Sheet 'one' 1".to_string()),
|
||||
column: 2,
|
||||
row: 1,
|
||||
absolute_column: false,
|
||||
absolute_row: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reference_unfinished_quotes() {
|
||||
let mut lx = new_lexer("'Sheet 1!R[1]C[2]", false);
|
||||
assert!(matches!(lx.next_token(), Illegal(_)));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_function() {
|
||||
let mut lx = new_lexer("ROUND", false);
|
||||
assert_eq!(lx.next_token(), Ident("ROUND".to_string()));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ident_with_underscore() {
|
||||
let mut lx = new_lexer("_IDENT", false);
|
||||
assert_eq!(lx.next_token(), Ident("_IDENT".to_string()));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ident_with_period() {
|
||||
let mut lx = new_lexer("IDENT.IFIER", false);
|
||||
assert_eq!(lx.next_token(), Ident("IDENT.IFIER".to_string()));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ident_cannot_start_with_period() {
|
||||
let mut lx = new_lexer(".IFIER", false);
|
||||
assert!(matches!(lx.next_token(), Illegal(_)));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_xlfn() {
|
||||
let mut lx = new_lexer("_xlfn.MyVar", true);
|
||||
assert_eq!(lx.next_token(), Ident("_xlfn.MyVar".to_string()));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
101
base/src/expressions/lexer/test/test_language.rs
Normal file
101
base/src/expressions/lexer/test/test_language.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{
|
||||
expressions::{
|
||||
lexer::{Lexer, LexerMode},
|
||||
token::{Error, TokenType},
|
||||
},
|
||||
language::get_language,
|
||||
locale::get_locale,
|
||||
};
|
||||
|
||||
fn new_language_lexer(formula: &str, language: &str) -> Lexer {
|
||||
let locale = get_locale("en").unwrap();
|
||||
let language = get_language(language).unwrap();
|
||||
Lexer::new(formula, LexerMode::A1, locale, language)
|
||||
}
|
||||
|
||||
// Spanish
|
||||
|
||||
#[test]
|
||||
fn test_verdadero_falso() {
|
||||
let mut lx = new_language_lexer("IF(A1, VERDADERO, FALSO)", "es");
|
||||
assert_eq!(lx.next_token(), TokenType::Ident("IF".to_string()));
|
||||
assert_eq!(lx.next_token(), TokenType::LeftParenthesis);
|
||||
assert!(matches!(lx.next_token(), TokenType::Reference { .. }));
|
||||
assert_eq!(lx.next_token(), TokenType::Comma);
|
||||
assert_eq!(lx.next_token(), TokenType::Boolean(true));
|
||||
assert_eq!(lx.next_token(), TokenType::Comma);
|
||||
assert_eq!(lx.next_token(), TokenType::Boolean(false));
|
||||
assert_eq!(lx.next_token(), TokenType::RightParenthesis);
|
||||
assert_eq!(lx.next_token(), TokenType::EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_spanish_errors_ref() {
|
||||
let mut lx = new_language_lexer("#¡REF!", "es");
|
||||
assert_eq!(lx.next_token(), TokenType::Error(Error::REF));
|
||||
assert_eq!(lx.next_token(), TokenType::EOF);
|
||||
}
|
||||
|
||||
// German
|
||||
|
||||
#[test]
|
||||
fn test_wahr_falsch() {
|
||||
let mut lx = new_language_lexer("IF(A1, WAHR, FALSCH)", "de");
|
||||
assert_eq!(lx.next_token(), TokenType::Ident("IF".to_string()));
|
||||
assert_eq!(lx.next_token(), TokenType::LeftParenthesis);
|
||||
assert!(matches!(lx.next_token(), TokenType::Reference { .. }));
|
||||
assert_eq!(lx.next_token(), TokenType::Comma);
|
||||
assert_eq!(lx.next_token(), TokenType::Boolean(true));
|
||||
assert_eq!(lx.next_token(), TokenType::Comma);
|
||||
assert_eq!(lx.next_token(), TokenType::Boolean(false));
|
||||
assert_eq!(lx.next_token(), TokenType::RightParenthesis);
|
||||
assert_eq!(lx.next_token(), TokenType::EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_german_errors_ref() {
|
||||
let mut lx = new_language_lexer("#BEZUG!", "de");
|
||||
assert_eq!(lx.next_token(), TokenType::Error(Error::REF));
|
||||
assert_eq!(lx.next_token(), TokenType::EOF);
|
||||
}
|
||||
|
||||
// French
|
||||
|
||||
#[test]
|
||||
fn test_vrai_faux() {
|
||||
let mut lx = new_language_lexer("IF(A1, VRAI, FAUX)", "fr");
|
||||
assert_eq!(lx.next_token(), TokenType::Ident("IF".to_string()));
|
||||
assert_eq!(lx.next_token(), TokenType::LeftParenthesis);
|
||||
assert!(matches!(lx.next_token(), TokenType::Reference { .. }));
|
||||
assert_eq!(lx.next_token(), TokenType::Comma);
|
||||
assert_eq!(lx.next_token(), TokenType::Boolean(true));
|
||||
assert_eq!(lx.next_token(), TokenType::Comma);
|
||||
assert_eq!(lx.next_token(), TokenType::Boolean(false));
|
||||
assert_eq!(lx.next_token(), TokenType::RightParenthesis);
|
||||
assert_eq!(lx.next_token(), TokenType::EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_french_errors_ref() {
|
||||
let mut lx = new_language_lexer("#REF!", "fr");
|
||||
assert_eq!(lx.next_token(), TokenType::Error(Error::REF));
|
||||
assert_eq!(lx.next_token(), TokenType::EOF);
|
||||
}
|
||||
|
||||
// English with errors
|
||||
|
||||
#[test]
|
||||
fn test_english_with_spanish_words() {
|
||||
let mut lx = new_language_lexer("IF(A1, VERDADERO, FALSO)", "en");
|
||||
assert_eq!(lx.next_token(), TokenType::Ident("IF".to_string()));
|
||||
assert_eq!(lx.next_token(), TokenType::LeftParenthesis);
|
||||
assert!(matches!(lx.next_token(), TokenType::Reference { .. }));
|
||||
assert_eq!(lx.next_token(), TokenType::Comma);
|
||||
assert_eq!(lx.next_token(), TokenType::Ident("VERDADERO".to_string()));
|
||||
assert_eq!(lx.next_token(), TokenType::Comma);
|
||||
assert_eq!(lx.next_token(), TokenType::Ident("FALSO".to_string()));
|
||||
assert_eq!(lx.next_token(), TokenType::RightParenthesis);
|
||||
assert_eq!(lx.next_token(), TokenType::EOF);
|
||||
}
|
||||
48
base/src/expressions/lexer/test/test_locale.rs
Normal file
48
base/src/expressions/lexer/test/test_locale.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{
|
||||
expressions::{
|
||||
lexer::{Lexer, LexerMode},
|
||||
token::TokenType,
|
||||
},
|
||||
language::get_language,
|
||||
locale::get_locale_fix,
|
||||
};
|
||||
|
||||
fn new_language_lexer(formula: &str, locale: &str, language: &str) -> Lexer {
|
||||
let locale = get_locale_fix(locale).unwrap();
|
||||
let language = get_language(language).unwrap();
|
||||
Lexer::new(formula, LexerMode::A1, locale, language)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_german_locale() {
|
||||
let mut lx = new_language_lexer("2,34e-3", "de", "en");
|
||||
assert_eq!(lx.next_token(), TokenType::Number(2.34e-3));
|
||||
assert_eq!(lx.next_token(), TokenType::EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_german_locale_does_not_parse() {
|
||||
let mut lx = new_language_lexer("2.34e-3", "de", "en");
|
||||
assert_eq!(lx.next_token(), TokenType::Number(2.0));
|
||||
assert!(matches!(lx.next_token(), TokenType::Illegal { .. }));
|
||||
assert_eq!(lx.next_token(), TokenType::EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_english_locale() {
|
||||
let mut lx = new_language_lexer("2.34e-3", "en", "en");
|
||||
assert_eq!(lx.next_token(), TokenType::Number(2.34e-3));
|
||||
assert_eq!(lx.next_token(), TokenType::EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_english_locale_does_not_parse() {
|
||||
// a comma is a separator
|
||||
let mut lx = new_language_lexer("2,34e-3", "en", "en");
|
||||
assert_eq!(lx.next_token(), TokenType::Number(2.0));
|
||||
assert_eq!(lx.next_token(), TokenType::Comma);
|
||||
assert_eq!(lx.next_token(), TokenType::Number(34e-3));
|
||||
assert_eq!(lx.next_token(), TokenType::EOF);
|
||||
}
|
||||
487
base/src/expressions/lexer/test/test_ranges.rs
Normal file
487
base/src/expressions/lexer/test/test_ranges.rs
Normal file
@@ -0,0 +1,487 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::lexer::LexerError;
|
||||
use crate::expressions::{
|
||||
lexer::{Lexer, LexerMode},
|
||||
token::TokenType::*,
|
||||
types::ParsedReference,
|
||||
};
|
||||
use crate::language::get_language;
|
||||
use crate::locale::get_locale;
|
||||
|
||||
fn new_lexer(formula: &str) -> Lexer {
|
||||
let locale = get_locale("en").unwrap();
|
||||
let language = get_language("en").unwrap();
|
||||
Lexer::new(formula, LexerMode::A1, locale, language)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range() {
|
||||
let mut lx = new_lexer("C4:D4");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Range {
|
||||
sheet: None,
|
||||
left: ParsedReference {
|
||||
column: 3,
|
||||
row: 4,
|
||||
absolute_column: false,
|
||||
absolute_row: false,
|
||||
},
|
||||
right: ParsedReference {
|
||||
column: 4,
|
||||
row: 4,
|
||||
absolute_column: false,
|
||||
absolute_row: false,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_absolute_column() {
|
||||
let mut lx = new_lexer("$A1:B$4");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Range {
|
||||
sheet: None,
|
||||
left: ParsedReference {
|
||||
column: 1,
|
||||
row: 1,
|
||||
absolute_column: true,
|
||||
absolute_row: false,
|
||||
},
|
||||
right: ParsedReference {
|
||||
column: 2,
|
||||
row: 4,
|
||||
absolute_column: false,
|
||||
absolute_row: true,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_with_sheet() {
|
||||
let mut lx = new_lexer("Sheet1!A1:B4");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Range {
|
||||
sheet: Some("Sheet1".to_string()),
|
||||
left: ParsedReference {
|
||||
column: 1,
|
||||
row: 1,
|
||||
absolute_column: false,
|
||||
absolute_row: false,
|
||||
},
|
||||
right: ParsedReference {
|
||||
column: 2,
|
||||
row: 4,
|
||||
absolute_column: false,
|
||||
absolute_row: false,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_with_sheet_with_space() {
|
||||
let mut lx = new_lexer("'New sheet'!$A$1:B44");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Range {
|
||||
sheet: Some("New sheet".to_string()),
|
||||
left: ParsedReference {
|
||||
column: 1,
|
||||
row: 1,
|
||||
absolute_column: true,
|
||||
absolute_row: true,
|
||||
},
|
||||
right: ParsedReference {
|
||||
column: 2,
|
||||
row: 44,
|
||||
absolute_column: false,
|
||||
absolute_row: false,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_column() {
|
||||
let mut lx = new_lexer("C:D");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Range {
|
||||
sheet: None,
|
||||
left: ParsedReference {
|
||||
column: 3,
|
||||
row: 1,
|
||||
absolute_column: false,
|
||||
absolute_row: true,
|
||||
},
|
||||
right: ParsedReference {
|
||||
column: 4,
|
||||
row: LAST_ROW,
|
||||
absolute_column: false,
|
||||
absolute_row: true,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_column_out_of_range() {
|
||||
let mut lx = new_lexer("C:XFE");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Illegal(LexerError {
|
||||
position: 5,
|
||||
message: "Column is not valid.".to_string(),
|
||||
})
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_column_absolute1() {
|
||||
let mut lx = new_lexer("$C:D");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Range {
|
||||
sheet: None,
|
||||
left: ParsedReference {
|
||||
column: 3,
|
||||
row: 1,
|
||||
absolute_column: true,
|
||||
absolute_row: true,
|
||||
},
|
||||
right: ParsedReference {
|
||||
column: 4,
|
||||
row: LAST_ROW,
|
||||
absolute_column: false,
|
||||
absolute_row: true,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_column_absolute2() {
|
||||
let mut lx = new_lexer("$C:$AA");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Range {
|
||||
sheet: None,
|
||||
left: ParsedReference {
|
||||
column: 3,
|
||||
row: 1,
|
||||
absolute_column: true,
|
||||
absolute_row: true,
|
||||
},
|
||||
right: ParsedReference {
|
||||
column: 27,
|
||||
row: LAST_ROW,
|
||||
absolute_column: true,
|
||||
absolute_row: true,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_rows() {
|
||||
let mut lx = new_lexer("3:5");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Range {
|
||||
sheet: None,
|
||||
left: ParsedReference {
|
||||
column: 1,
|
||||
row: 3,
|
||||
absolute_column: true,
|
||||
absolute_row: false,
|
||||
},
|
||||
right: ParsedReference {
|
||||
column: LAST_COLUMN,
|
||||
row: 5,
|
||||
absolute_column: true,
|
||||
absolute_row: false,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_rows_absolute1() {
|
||||
let mut lx = new_lexer("$3:5");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Range {
|
||||
sheet: None,
|
||||
left: ParsedReference {
|
||||
column: 1,
|
||||
row: 3,
|
||||
absolute_column: true,
|
||||
absolute_row: true,
|
||||
},
|
||||
right: ParsedReference {
|
||||
column: LAST_COLUMN,
|
||||
row: 5,
|
||||
absolute_column: true,
|
||||
absolute_row: false,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_rows_absolute2() {
|
||||
let mut lx = new_lexer("$3:$55");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Range {
|
||||
sheet: None,
|
||||
left: ParsedReference {
|
||||
column: 1,
|
||||
row: 3,
|
||||
absolute_column: true,
|
||||
absolute_row: true,
|
||||
},
|
||||
right: ParsedReference {
|
||||
column: LAST_COLUMN,
|
||||
row: 55,
|
||||
absolute_column: true,
|
||||
absolute_row: true,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_column_sheet() {
|
||||
let mut lx = new_lexer("Sheet1!C:D");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Range {
|
||||
sheet: Some("Sheet1".to_string()),
|
||||
left: ParsedReference {
|
||||
column: 3,
|
||||
row: 1,
|
||||
absolute_column: false,
|
||||
absolute_row: true,
|
||||
},
|
||||
right: ParsedReference {
|
||||
column: 4,
|
||||
row: LAST_ROW,
|
||||
absolute_column: false,
|
||||
absolute_row: true,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_column_sheet_absolute() {
|
||||
let mut lx = new_lexer("Sheet1!$C:$D");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Range {
|
||||
sheet: Some("Sheet1".to_string()),
|
||||
left: ParsedReference {
|
||||
column: 3,
|
||||
row: 1,
|
||||
absolute_column: true,
|
||||
absolute_row: true,
|
||||
},
|
||||
right: ParsedReference {
|
||||
column: 4,
|
||||
row: LAST_ROW,
|
||||
absolute_column: true,
|
||||
absolute_row: true,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
|
||||
let mut lx = new_lexer("'Woops ans'!$C:$D");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Range {
|
||||
sheet: Some("Woops ans".to_string()),
|
||||
left: ParsedReference {
|
||||
column: 3,
|
||||
row: 1,
|
||||
absolute_column: true,
|
||||
absolute_row: true,
|
||||
},
|
||||
right: ParsedReference {
|
||||
column: 4,
|
||||
row: LAST_ROW,
|
||||
absolute_column: true,
|
||||
absolute_row: true,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_rows_sheet() {
|
||||
let mut lx = new_lexer("'A new sheet'!3:5");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Range {
|
||||
sheet: Some("A new sheet".to_string()),
|
||||
left: ParsedReference {
|
||||
column: 1,
|
||||
row: 3,
|
||||
absolute_column: true,
|
||||
absolute_row: false,
|
||||
},
|
||||
right: ParsedReference {
|
||||
column: LAST_COLUMN,
|
||||
row: 5,
|
||||
absolute_column: true,
|
||||
absolute_row: false,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
let mut lx = new_lexer("Sheet12!3:5");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Range {
|
||||
sheet: Some("Sheet12".to_string()),
|
||||
left: ParsedReference {
|
||||
column: 1,
|
||||
row: 3,
|
||||
absolute_column: true,
|
||||
absolute_row: false,
|
||||
},
|
||||
right: ParsedReference {
|
||||
column: LAST_COLUMN,
|
||||
row: 5,
|
||||
absolute_column: true,
|
||||
absolute_row: false,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
// Non ranges
|
||||
|
||||
#[test]
|
||||
fn test_non_range_variable_name() {
|
||||
let mut lx = new_lexer("AB");
|
||||
assert_eq!(lx.next_token(), Ident("AB".to_string()));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_non_range_invalid_variable_name() {
|
||||
let mut lx = new_lexer("$AB");
|
||||
assert!(matches!(lx.next_token(), Illegal(_)));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_non_range_invalid_variable_name_a03() {
|
||||
let mut lx = new_lexer("A03");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Reference {
|
||||
sheet: None,
|
||||
row: 3,
|
||||
column: 1,
|
||||
absolute_column: false,
|
||||
absolute_row: false
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_non_range_invalid_variable_name_sheet1_a03() {
|
||||
let mut lx = new_lexer("Sheet1!A03");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Reference {
|
||||
sheet: Some("Sheet1".to_string()),
|
||||
row: 3,
|
||||
column: 1,
|
||||
absolute_column: false,
|
||||
absolute_row: false
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_rows_with_0() {
|
||||
let mut lx = new_lexer("03:05");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Range {
|
||||
sheet: None,
|
||||
left: ParsedReference {
|
||||
column: 1,
|
||||
row: 3,
|
||||
absolute_column: true,
|
||||
absolute_row: false,
|
||||
},
|
||||
right: ParsedReference {
|
||||
column: LAST_COLUMN,
|
||||
row: 5,
|
||||
absolute_column: true,
|
||||
absolute_row: false,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_incomplete_row() {
|
||||
let mut lx = new_lexer("R[");
|
||||
lx.set_lexer_mode(LexerMode::R1C1);
|
||||
assert!(matches!(lx.next_token(), Illegal(_)));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_incomplete_column() {
|
||||
let mut lx = new_lexer("R[3][");
|
||||
lx.set_lexer_mode(LexerMode::R1C1);
|
||||
assert!(matches!(lx.next_token(), Illegal(_)));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_operator() {
|
||||
let mut lx = new_lexer("A1:OFFSET(B1,1,2)");
|
||||
lx.set_lexer_mode(LexerMode::A1);
|
||||
assert!(matches!(lx.next_token(), Reference { .. }));
|
||||
assert!(matches!(lx.next_token(), Colon));
|
||||
assert!(matches!(lx.next_token(), Ident(_)));
|
||||
assert!(matches!(lx.next_token(), LeftParenthesis));
|
||||
assert!(matches!(lx.next_token(), Reference { .. }));
|
||||
assert_eq!(lx.next_token(), Comma);
|
||||
assert!(matches!(lx.next_token(), Number(_)));
|
||||
assert_eq!(lx.next_token(), Comma);
|
||||
assert!(matches!(lx.next_token(), Number(_)));
|
||||
assert!(matches!(lx.next_token(), RightParenthesis));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
73
base/src/expressions/lexer/test/test_tables.rs
Normal file
73
base/src/expressions/lexer/test/test_tables.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::expressions::{
|
||||
lexer::{Lexer, LexerMode},
|
||||
token::{TableReference, TableSpecifier, TokenType::*},
|
||||
};
|
||||
use crate::language::get_language;
|
||||
use crate::locale::get_locale;
|
||||
|
||||
fn new_lexer(formula: &str) -> Lexer {
|
||||
let locale = get_locale("en").unwrap();
|
||||
let language = get_language("en").unwrap();
|
||||
Lexer::new(formula, LexerMode::A1, locale, language)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_this_row() {
|
||||
let mut lx = new_lexer("tbInfo[[#This Row], [Jan]:[Dec]]");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
StructuredReference {
|
||||
table_name: "tbInfo".to_string(),
|
||||
specifier: Some(TableSpecifier::ThisRow),
|
||||
table_reference: Some(TableReference::RangeReference((
|
||||
"Jan".to_string(),
|
||||
"Dec".to_string()
|
||||
)))
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_no_specifier() {
|
||||
let mut lx = new_lexer("tbInfo[December]");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
StructuredReference {
|
||||
table_name: "tbInfo".to_string(),
|
||||
specifier: None,
|
||||
table_reference: Some(TableReference::ColumnReference("December".to_string()))
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_no_specifier_white_spaces() {
|
||||
let mut lx = new_lexer("tbInfo[[First Month]]");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
StructuredReference {
|
||||
table_name: "tbInfo".to_string(),
|
||||
specifier: None,
|
||||
table_reference: Some(TableReference::ColumnReference("First Month".to_string()))
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_totals_no_reference() {
|
||||
let mut lx = new_lexer("tbInfo[#Totals]");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
StructuredReference {
|
||||
table_name: "tbInfo".to_string(),
|
||||
specifier: Some(TableSpecifier::Totals),
|
||||
table_reference: None
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
146
base/src/expressions/lexer/test/test_util.rs
Normal file
146
base/src/expressions/lexer/test/test_util.rs
Normal file
@@ -0,0 +1,146 @@
|
||||
use crate::expressions::{
|
||||
lexer::util::get_tokens,
|
||||
token::{OpCompare, OpSum, TokenType},
|
||||
};
|
||||
|
||||
fn get_tokens_types(formula: &str) -> Vec<TokenType> {
|
||||
let marked_tokens = get_tokens(formula);
|
||||
marked_tokens.iter().map(|s| s.token.clone()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_tokens() {
|
||||
let formula = "1+1";
|
||||
let t = get_tokens(formula);
|
||||
assert_eq!(t.len(), 3);
|
||||
|
||||
let formula = "1 + AA23 +";
|
||||
let t = get_tokens(formula);
|
||||
assert_eq!(t.len(), 4);
|
||||
let l = t.get(2).expect("expected token");
|
||||
assert_eq!(l.start, 3);
|
||||
assert_eq!(l.end, 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_tokens() {
|
||||
assert_eq!(
|
||||
get_tokens_types("()"),
|
||||
vec![TokenType::LeftParenthesis, TokenType::RightParenthesis]
|
||||
);
|
||||
assert_eq!(
|
||||
get_tokens_types("{}"),
|
||||
vec![TokenType::LeftBrace, TokenType::RightBrace]
|
||||
);
|
||||
assert_eq!(
|
||||
get_tokens_types("[]"),
|
||||
vec![TokenType::LeftBracket, TokenType::RightBracket]
|
||||
);
|
||||
assert_eq!(get_tokens_types("&"), vec![TokenType::And]);
|
||||
assert_eq!(
|
||||
get_tokens_types("<"),
|
||||
vec![TokenType::Compare(OpCompare::LessThan)]
|
||||
);
|
||||
assert_eq!(
|
||||
get_tokens_types(">"),
|
||||
vec![TokenType::Compare(OpCompare::GreaterThan)]
|
||||
);
|
||||
assert_eq!(
|
||||
get_tokens_types("<="),
|
||||
vec![TokenType::Compare(OpCompare::LessOrEqualThan)]
|
||||
);
|
||||
assert_eq!(
|
||||
get_tokens_types(">="),
|
||||
vec![TokenType::Compare(OpCompare::GreaterOrEqualThan)]
|
||||
);
|
||||
assert_eq!(
|
||||
get_tokens_types("IF"),
|
||||
vec![TokenType::Ident("IF".to_owned())]
|
||||
);
|
||||
assert_eq!(get_tokens_types("45"), vec![TokenType::Number(45.0)]);
|
||||
// The lexer parses this as two tokens
|
||||
assert_eq!(
|
||||
get_tokens_types("-45"),
|
||||
vec![TokenType::Addition(OpSum::Minus), TokenType::Number(45.0)]
|
||||
);
|
||||
assert_eq!(
|
||||
get_tokens_types("23.45e-2"),
|
||||
vec![TokenType::Number(23.45e-2)]
|
||||
);
|
||||
assert_eq!(
|
||||
get_tokens_types("4-3"),
|
||||
vec![
|
||||
TokenType::Number(4.0),
|
||||
TokenType::Addition(OpSum::Minus),
|
||||
TokenType::Number(3.0)
|
||||
]
|
||||
);
|
||||
assert_eq!(get_tokens_types("True"), vec![TokenType::Boolean(true)]);
|
||||
assert_eq!(get_tokens_types("FALSE"), vec![TokenType::Boolean(false)]);
|
||||
assert_eq!(
|
||||
get_tokens_types("2,3.5"),
|
||||
vec![
|
||||
TokenType::Number(2.0),
|
||||
TokenType::Comma,
|
||||
TokenType::Number(3.5)
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
get_tokens_types("2.4;3.5"),
|
||||
vec![
|
||||
TokenType::Number(2.4),
|
||||
TokenType::Semicolon,
|
||||
TokenType::Number(3.5)
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
get_tokens_types("AB34"),
|
||||
vec![TokenType::Reference {
|
||||
sheet: None,
|
||||
row: 34,
|
||||
column: 28,
|
||||
absolute_column: false,
|
||||
absolute_row: false
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
get_tokens_types("$A3"),
|
||||
vec![TokenType::Reference {
|
||||
sheet: None,
|
||||
row: 3,
|
||||
column: 1,
|
||||
absolute_column: true,
|
||||
absolute_row: false
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
get_tokens_types("AB$34"),
|
||||
vec![TokenType::Reference {
|
||||
sheet: None,
|
||||
row: 34,
|
||||
column: 28,
|
||||
absolute_column: false,
|
||||
absolute_row: true
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
get_tokens_types("$AB$34"),
|
||||
vec![TokenType::Reference {
|
||||
sheet: None,
|
||||
row: 34,
|
||||
column: 28,
|
||||
absolute_column: true,
|
||||
absolute_row: true
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
get_tokens_types("'My House'!AB34"),
|
||||
vec![TokenType::Reference {
|
||||
sheet: Some("My House".to_string()),
|
||||
row: 34,
|
||||
column: 28,
|
||||
absolute_column: false,
|
||||
absolute_row: false
|
||||
}]
|
||||
);
|
||||
}
|
||||
85
base/src/expressions/lexer/util.rs
Normal file
85
base/src/expressions/lexer/util.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use std::fmt;
|
||||
|
||||
use crate::expressions::token;
|
||||
use crate::language::get_language;
|
||||
use crate::locale::get_locale;
|
||||
|
||||
use super::{Lexer, LexerMode};
|
||||
|
||||
/// A MarkedToken is a token together with its position on a formula
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct MarkedToken {
|
||||
pub token: token::TokenType,
|
||||
pub start: i32,
|
||||
pub end: i32,
|
||||
}
|
||||
|
||||
/// Returns a list of marked tokens for a formula
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use ironcalc_base::expressions::{
|
||||
/// lexer::util::{get_tokens, MarkedToken},
|
||||
/// token::{OpSum, TokenType},
|
||||
/// };
|
||||
///
|
||||
/// let marked_tokens = get_tokens("A1+1");
|
||||
/// let first_t = MarkedToken {
|
||||
/// token: TokenType::Reference {
|
||||
/// sheet: None,
|
||||
/// row: 1,
|
||||
/// column: 1,
|
||||
/// absolute_column: false,
|
||||
/// absolute_row: false,
|
||||
/// },
|
||||
/// start: 0,
|
||||
/// end: 2,
|
||||
/// };
|
||||
/// let second_t = MarkedToken {
|
||||
/// token: TokenType::Addition(OpSum::Add),
|
||||
/// start:2,
|
||||
/// end: 3
|
||||
/// };
|
||||
/// let third_t = MarkedToken {
|
||||
/// token: TokenType::Number(1.0),
|
||||
/// start:3,
|
||||
/// end: 4
|
||||
/// };
|
||||
/// assert_eq!(marked_tokens, vec![first_t, second_t, third_t]);
|
||||
/// ```
|
||||
pub fn get_tokens(formula: &str) -> Vec<MarkedToken> {
|
||||
let mut tokens = Vec::new();
|
||||
let mut lexer = Lexer::new(
|
||||
formula,
|
||||
LexerMode::A1,
|
||||
get_locale("en").expect(""),
|
||||
get_language("en").expect(""),
|
||||
);
|
||||
let mut start = lexer.get_position();
|
||||
let mut next_token = lexer.next_token();
|
||||
let mut end = lexer.get_position();
|
||||
loop {
|
||||
match next_token {
|
||||
token::TokenType::EOF => {
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
tokens.push(MarkedToken {
|
||||
start,
|
||||
end,
|
||||
token: next_token,
|
||||
});
|
||||
start = lexer.get_position();
|
||||
next_token = lexer.next_token();
|
||||
end = lexer.get_position();
|
||||
}
|
||||
}
|
||||
}
|
||||
tokens
|
||||
}
|
||||
|
||||
impl fmt::Display for MarkedToken {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(fmt, "{}", self.token)
|
||||
}
|
||||
}
|
||||
6
base/src/expressions/mod.rs
Normal file
6
base/src/expressions/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
// public modules
|
||||
pub mod lexer;
|
||||
pub mod parser;
|
||||
pub mod token;
|
||||
pub mod types;
|
||||
pub mod utils;
|
||||
877
base/src/expressions/parser/mod.rs
Normal file
877
base/src/expressions/parser/mod.rs
Normal file
@@ -0,0 +1,877 @@
|
||||
/*!
|
||||
# GRAMAR
|
||||
|
||||
<pre class="rust">
|
||||
opComp => '=' | '<' | '>' | '<=' } '>=' | '<>'
|
||||
opFactor => '*' | '/'
|
||||
unaryOp => '-' | '+'
|
||||
|
||||
expr => concat (opComp concat)*
|
||||
concat => term ('&' term)*
|
||||
term => factor (opFactor factor)*
|
||||
factor => prod (opProd prod)*
|
||||
prod => power ('^' power)*
|
||||
power => (unaryOp)* range '%'*
|
||||
range => primary (':' primary)?
|
||||
primary => '(' expr ')'
|
||||
=> number
|
||||
=> function '(' f_args ')'
|
||||
=> name
|
||||
=> string
|
||||
=> '{' a_args '}'
|
||||
=> bool
|
||||
=> bool()
|
||||
=> error
|
||||
|
||||
f_args => e (',' e)*
|
||||
</pre>
|
||||
*/
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::functions::Function;
|
||||
use crate::language::get_language;
|
||||
use crate::locale::get_locale;
|
||||
use crate::types::Table;
|
||||
|
||||
use super::lexer;
|
||||
use super::token;
|
||||
use super::token::OpUnary;
|
||||
use super::token::TableReference;
|
||||
use super::token::TokenType;
|
||||
use super::types::*;
|
||||
use super::utils::number_to_column;
|
||||
|
||||
use token::OpCompare;
|
||||
|
||||
pub mod move_formula;
|
||||
pub mod stringify;
|
||||
pub mod walk;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_ranges;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_move_formula;
|
||||
#[cfg(test)]
|
||||
mod test_tables;
|
||||
|
||||
pub(crate) fn parse_range(formula: &str) -> Result<(i32, i32, i32, i32), String> {
|
||||
let mut lexer = lexer::Lexer::new(
|
||||
formula,
|
||||
lexer::LexerMode::A1,
|
||||
get_locale("en").expect(""),
|
||||
get_language("en").expect(""),
|
||||
);
|
||||
if let TokenType::Range {
|
||||
left,
|
||||
right,
|
||||
sheet: _,
|
||||
} = lexer.next_token()
|
||||
{
|
||||
Ok((left.column, left.row, right.column, right.row))
|
||||
} else {
|
||||
Err("Not a range".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn get_table_column_by_name(table_column_name: &str, table: &Table) -> Option<i32> {
|
||||
for (index, table_column) in table.columns.iter().enumerate() {
|
||||
if table_column.name == table_column_name {
|
||||
return Some(index as i32);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) struct Reference<'a> {
|
||||
sheet_name: &'a Option<String>,
|
||||
sheet_index: u32,
|
||||
absolute_row: bool,
|
||||
absolute_column: bool,
|
||||
row: i32,
|
||||
column: i32,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
pub enum Node {
|
||||
BooleanKind(bool),
|
||||
NumberKind(f64),
|
||||
StringKind(String),
|
||||
ReferenceKind {
|
||||
sheet_name: Option<String>,
|
||||
sheet_index: u32,
|
||||
absolute_row: bool,
|
||||
absolute_column: bool,
|
||||
row: i32,
|
||||
column: i32,
|
||||
},
|
||||
RangeKind {
|
||||
sheet_name: Option<String>,
|
||||
sheet_index: u32,
|
||||
absolute_row1: bool,
|
||||
absolute_column1: bool,
|
||||
row1: i32,
|
||||
column1: i32,
|
||||
absolute_row2: bool,
|
||||
absolute_column2: bool,
|
||||
row2: i32,
|
||||
column2: i32,
|
||||
},
|
||||
WrongReferenceKind {
|
||||
sheet_name: Option<String>,
|
||||
absolute_row: bool,
|
||||
absolute_column: bool,
|
||||
row: i32,
|
||||
column: i32,
|
||||
},
|
||||
WrongRangeKind {
|
||||
sheet_name: Option<String>,
|
||||
absolute_row1: bool,
|
||||
absolute_column1: bool,
|
||||
row1: i32,
|
||||
column1: i32,
|
||||
absolute_row2: bool,
|
||||
absolute_column2: bool,
|
||||
row2: i32,
|
||||
column2: i32,
|
||||
},
|
||||
OpRangeKind {
|
||||
left: Box<Node>,
|
||||
right: Box<Node>,
|
||||
},
|
||||
OpConcatenateKind {
|
||||
left: Box<Node>,
|
||||
right: Box<Node>,
|
||||
},
|
||||
OpSumKind {
|
||||
kind: token::OpSum,
|
||||
left: Box<Node>,
|
||||
right: Box<Node>,
|
||||
},
|
||||
OpProductKind {
|
||||
kind: token::OpProduct,
|
||||
left: Box<Node>,
|
||||
right: Box<Node>,
|
||||
},
|
||||
OpPowerKind {
|
||||
left: Box<Node>,
|
||||
right: Box<Node>,
|
||||
},
|
||||
FunctionKind {
|
||||
kind: Function,
|
||||
args: Vec<Node>,
|
||||
},
|
||||
InvalidFunctionKind {
|
||||
name: String,
|
||||
args: Vec<Node>,
|
||||
},
|
||||
ArrayKind(Vec<Node>),
|
||||
VariableKind(String),
|
||||
CompareKind {
|
||||
kind: OpCompare,
|
||||
left: Box<Node>,
|
||||
right: Box<Node>,
|
||||
},
|
||||
UnaryKind {
|
||||
kind: OpUnary,
|
||||
right: Box<Node>,
|
||||
},
|
||||
ErrorKind(token::Error),
|
||||
ParseErrorKind {
|
||||
formula: String,
|
||||
message: String,
|
||||
position: usize,
|
||||
},
|
||||
EmptyArgKind,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Parser {
|
||||
lexer: lexer::Lexer,
|
||||
worksheets: Vec<String>,
|
||||
context: Option<CellReferenceRC>,
|
||||
tables: HashMap<String, Table>,
|
||||
}
|
||||
|
||||
impl Parser {
|
||||
pub fn new(worksheets: Vec<String>, tables: HashMap<String, Table>) -> Parser {
|
||||
let lexer = lexer::Lexer::new(
|
||||
"",
|
||||
lexer::LexerMode::A1,
|
||||
get_locale("en").expect(""),
|
||||
get_language("en").expect(""),
|
||||
);
|
||||
Parser {
|
||||
lexer,
|
||||
worksheets,
|
||||
context: None,
|
||||
tables,
|
||||
}
|
||||
}
|
||||
pub fn set_lexer_mode(&mut self, mode: lexer::LexerMode) {
|
||||
self.lexer.set_lexer_mode(mode)
|
||||
}
|
||||
|
||||
pub fn set_worksheets(&mut self, worksheets: Vec<String>) {
|
||||
self.worksheets = worksheets;
|
||||
}
|
||||
|
||||
pub fn parse(&mut self, formula: &str, context: &Option<CellReferenceRC>) -> Node {
|
||||
self.lexer.set_formula(formula);
|
||||
self.context = context.clone();
|
||||
self.parse_expr()
|
||||
}
|
||||
|
||||
fn get_sheet_index_by_name(&self, name: &str) -> Option<u32> {
|
||||
let worksheets = &self.worksheets;
|
||||
for (i, sheet) in worksheets.iter().enumerate() {
|
||||
if sheet == name {
|
||||
return Some(i as u32);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn parse_expr(&mut self) -> Node {
|
||||
let mut t = self.parse_concat();
|
||||
if let Node::ParseErrorKind { .. } = t {
|
||||
return t;
|
||||
}
|
||||
let mut next_token = self.lexer.peek_token();
|
||||
while let TokenType::Compare(op) = next_token {
|
||||
self.lexer.advance_token();
|
||||
let p = self.parse_concat();
|
||||
if let Node::ParseErrorKind { .. } = p {
|
||||
return p;
|
||||
}
|
||||
t = Node::CompareKind {
|
||||
kind: op,
|
||||
left: Box::new(t),
|
||||
right: Box::new(p),
|
||||
};
|
||||
next_token = self.lexer.peek_token();
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
fn parse_concat(&mut self) -> Node {
|
||||
let mut t = self.parse_term();
|
||||
if let Node::ParseErrorKind { .. } = t {
|
||||
return t;
|
||||
}
|
||||
let mut next_token = self.lexer.peek_token();
|
||||
while next_token == TokenType::And {
|
||||
self.lexer.advance_token();
|
||||
let p = self.parse_term();
|
||||
if let Node::ParseErrorKind { .. } = p {
|
||||
return p;
|
||||
}
|
||||
t = Node::OpConcatenateKind {
|
||||
left: Box::new(t),
|
||||
right: Box::new(p),
|
||||
};
|
||||
next_token = self.lexer.peek_token();
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
fn parse_term(&mut self) -> Node {
|
||||
let mut t = self.parse_factor();
|
||||
if let Node::ParseErrorKind { .. } = t {
|
||||
return t;
|
||||
}
|
||||
let mut next_token = self.lexer.peek_token();
|
||||
while let TokenType::Addition(op) = next_token {
|
||||
self.lexer.advance_token();
|
||||
let p = self.parse_factor();
|
||||
if let Node::ParseErrorKind { .. } = p {
|
||||
return p;
|
||||
}
|
||||
t = Node::OpSumKind {
|
||||
kind: op,
|
||||
left: Box::new(t),
|
||||
right: Box::new(p),
|
||||
};
|
||||
|
||||
next_token = self.lexer.peek_token();
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
fn parse_factor(&mut self) -> Node {
|
||||
let mut t = self.parse_prod();
|
||||
if let Node::ParseErrorKind { .. } = t {
|
||||
return t;
|
||||
}
|
||||
let mut next_token = self.lexer.peek_token();
|
||||
while let TokenType::Product(op) = next_token {
|
||||
self.lexer.advance_token();
|
||||
let p = self.parse_prod();
|
||||
if let Node::ParseErrorKind { .. } = p {
|
||||
return p;
|
||||
}
|
||||
t = Node::OpProductKind {
|
||||
kind: op,
|
||||
left: Box::new(t),
|
||||
right: Box::new(p),
|
||||
};
|
||||
next_token = self.lexer.peek_token();
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
fn parse_prod(&mut self) -> Node {
|
||||
let mut t = self.parse_power();
|
||||
if let Node::ParseErrorKind { .. } = t {
|
||||
return t;
|
||||
}
|
||||
let mut next_token = self.lexer.peek_token();
|
||||
while next_token == TokenType::Power {
|
||||
self.lexer.advance_token();
|
||||
let p = self.parse_power();
|
||||
if let Node::ParseErrorKind { .. } = p {
|
||||
return p;
|
||||
}
|
||||
t = Node::OpPowerKind {
|
||||
left: Box::new(t),
|
||||
right: Box::new(p),
|
||||
};
|
||||
next_token = self.lexer.peek_token();
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
fn parse_power(&mut self) -> Node {
|
||||
let mut next_token = self.lexer.peek_token();
|
||||
let mut sign = 1;
|
||||
while let TokenType::Addition(op) = next_token {
|
||||
self.lexer.advance_token();
|
||||
if op == token::OpSum::Minus {
|
||||
sign = -sign;
|
||||
}
|
||||
next_token = self.lexer.peek_token();
|
||||
}
|
||||
|
||||
let mut t = self.parse_range();
|
||||
if let Node::ParseErrorKind { .. } = t {
|
||||
return t;
|
||||
}
|
||||
if sign == -1 {
|
||||
t = Node::UnaryKind {
|
||||
kind: token::OpUnary::Minus,
|
||||
right: Box::new(t),
|
||||
}
|
||||
}
|
||||
next_token = self.lexer.peek_token();
|
||||
while next_token == TokenType::Percent {
|
||||
self.lexer.advance_token();
|
||||
t = Node::UnaryKind {
|
||||
kind: token::OpUnary::Percentage,
|
||||
right: Box::new(t),
|
||||
};
|
||||
next_token = self.lexer.peek_token();
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
fn parse_range(&mut self) -> Node {
|
||||
let t = self.parse_primary();
|
||||
if let Node::ParseErrorKind { .. } = t {
|
||||
return t;
|
||||
}
|
||||
let next_token = self.lexer.peek_token();
|
||||
if next_token == TokenType::Colon {
|
||||
self.lexer.advance_token();
|
||||
let p = self.parse_primary();
|
||||
if let Node::ParseErrorKind { .. } = p {
|
||||
return p;
|
||||
}
|
||||
return Node::OpRangeKind {
|
||||
left: Box::new(t),
|
||||
right: Box::new(p),
|
||||
};
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
fn parse_primary(&mut self) -> Node {
|
||||
let next_token = self.lexer.next_token();
|
||||
match next_token {
|
||||
TokenType::LeftParenthesis => {
|
||||
let t = self.parse_expr();
|
||||
if let Node::ParseErrorKind { .. } = t {
|
||||
return t;
|
||||
}
|
||||
|
||||
if let Err(err) = self.lexer.expect(TokenType::RightParenthesis) {
|
||||
return Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: err.position,
|
||||
message: err.message,
|
||||
};
|
||||
}
|
||||
t
|
||||
}
|
||||
TokenType::Number(s) => Node::NumberKind(s),
|
||||
TokenType::String(s) => Node::StringKind(s),
|
||||
TokenType::LeftBrace => {
|
||||
let t = self.parse_expr();
|
||||
if let Node::ParseErrorKind { .. } = t {
|
||||
return t;
|
||||
}
|
||||
let mut next_token = self.lexer.peek_token();
|
||||
let mut args: Vec<Node> = vec![t];
|
||||
while next_token == TokenType::Semicolon {
|
||||
self.lexer.advance_token();
|
||||
let p = self.parse_expr();
|
||||
if let Node::ParseErrorKind { .. } = p {
|
||||
return p;
|
||||
}
|
||||
next_token = self.lexer.peek_token();
|
||||
args.push(p);
|
||||
}
|
||||
if let Err(err) = self.lexer.expect(TokenType::RightBrace) {
|
||||
return Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: err.position,
|
||||
message: err.message,
|
||||
};
|
||||
}
|
||||
Node::ArrayKind(args)
|
||||
}
|
||||
TokenType::Reference {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
absolute_column,
|
||||
absolute_row,
|
||||
} => {
|
||||
let context = match &self.context {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
return Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: self.lexer.get_position() as usize,
|
||||
message: "Expected context for the reference".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
let sheet_index = match &sheet {
|
||||
Some(name) => self.get_sheet_index_by_name(name),
|
||||
None => self.get_sheet_index_by_name(&context.sheet),
|
||||
};
|
||||
let a1_mode = self.lexer.is_a1_mode();
|
||||
let row = if absolute_row || !a1_mode {
|
||||
row
|
||||
} else {
|
||||
row - context.row
|
||||
};
|
||||
let column = if absolute_column || !a1_mode {
|
||||
column
|
||||
} else {
|
||||
column - context.column
|
||||
};
|
||||
match sheet_index {
|
||||
Some(index) => Node::ReferenceKind {
|
||||
sheet_name: sheet,
|
||||
sheet_index: index,
|
||||
row,
|
||||
column,
|
||||
absolute_row,
|
||||
absolute_column,
|
||||
},
|
||||
None => Node::WrongReferenceKind {
|
||||
sheet_name: sheet,
|
||||
row,
|
||||
column,
|
||||
absolute_row,
|
||||
absolute_column,
|
||||
},
|
||||
}
|
||||
}
|
||||
TokenType::Range { sheet, left, right } => {
|
||||
let context = match &self.context {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
return Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: self.lexer.get_position() as usize,
|
||||
message: "Expected context for the reference".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
let sheet_index = match &sheet {
|
||||
Some(name) => self.get_sheet_index_by_name(name),
|
||||
None => self.get_sheet_index_by_name(&context.sheet),
|
||||
};
|
||||
let mut row1 = left.row;
|
||||
let mut column1 = left.column;
|
||||
let mut row2 = right.row;
|
||||
let mut column2 = right.column;
|
||||
|
||||
let mut absolute_column1 = left.absolute_column;
|
||||
let mut absolute_column2 = right.absolute_column;
|
||||
let mut absolute_row1 = left.absolute_row;
|
||||
let mut absolute_row2 = right.absolute_row;
|
||||
|
||||
if self.lexer.is_a1_mode() {
|
||||
if !left.absolute_row {
|
||||
row1 -= context.row
|
||||
};
|
||||
if !left.absolute_column {
|
||||
column1 -= context.column
|
||||
};
|
||||
if !right.absolute_row {
|
||||
row2 -= context.row
|
||||
};
|
||||
if !right.absolute_column {
|
||||
column2 -= context.column
|
||||
};
|
||||
}
|
||||
if row1 > row2 {
|
||||
(row2, row1) = (row1, row2);
|
||||
(absolute_row2, absolute_row1) = (absolute_row1, absolute_row2);
|
||||
}
|
||||
if column1 > column2 {
|
||||
(column2, column1) = (column1, column2);
|
||||
(absolute_column2, absolute_column1) = (absolute_column1, absolute_column2);
|
||||
}
|
||||
match sheet_index {
|
||||
Some(index) => Node::RangeKind {
|
||||
sheet_name: sheet,
|
||||
sheet_index: index,
|
||||
row1,
|
||||
column1,
|
||||
row2,
|
||||
column2,
|
||||
absolute_column1,
|
||||
absolute_column2,
|
||||
absolute_row1,
|
||||
absolute_row2,
|
||||
},
|
||||
None => Node::WrongRangeKind {
|
||||
sheet_name: sheet,
|
||||
row1,
|
||||
column1,
|
||||
row2,
|
||||
column2,
|
||||
absolute_column1,
|
||||
absolute_column2,
|
||||
absolute_row1,
|
||||
absolute_row2,
|
||||
},
|
||||
}
|
||||
}
|
||||
TokenType::Ident(name) => {
|
||||
let next_token = self.lexer.peek_token();
|
||||
if next_token == TokenType::LeftParenthesis {
|
||||
// It's a function call "SUM(.."
|
||||
self.lexer.advance_token();
|
||||
let args = match self.parse_function_args() {
|
||||
Ok(s) => s,
|
||||
Err(e) => return e,
|
||||
};
|
||||
if let Err(err) = self.lexer.expect(TokenType::RightParenthesis) {
|
||||
return Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: err.position,
|
||||
message: err.message,
|
||||
};
|
||||
}
|
||||
if let Some(function_kind) = Function::get_function(&name) {
|
||||
return Node::FunctionKind {
|
||||
kind: function_kind,
|
||||
args,
|
||||
};
|
||||
} else {
|
||||
return Node::InvalidFunctionKind { name, args };
|
||||
}
|
||||
}
|
||||
Node::VariableKind(name)
|
||||
}
|
||||
TokenType::Error(kind) => Node::ErrorKind(kind),
|
||||
TokenType::Illegal(error) => Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: error.position,
|
||||
message: error.message,
|
||||
},
|
||||
TokenType::EOF => Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: 0,
|
||||
message: "Unexpected end of input.".to_string(),
|
||||
},
|
||||
TokenType::Boolean(value) => Node::BooleanKind(value),
|
||||
TokenType::Compare(_) => {
|
||||
// A primary Node cannot start with an operator
|
||||
Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: 0,
|
||||
message: "Unexpected token: 'COMPARE'".to_string(),
|
||||
}
|
||||
}
|
||||
TokenType::Addition(_) => {
|
||||
// A primary Node cannot start with an operator
|
||||
Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: 0,
|
||||
message: "Unexpected token: 'SUM'".to_string(),
|
||||
}
|
||||
}
|
||||
TokenType::Product(_) => {
|
||||
// A primary Node cannot start with an operator
|
||||
Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: 0,
|
||||
message: "Unexpected token: 'PRODUCT'".to_string(),
|
||||
}
|
||||
}
|
||||
TokenType::Power => {
|
||||
// A primary Node cannot start with an operator
|
||||
Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: 0,
|
||||
message: "Unexpected token: 'POWER'".to_string(),
|
||||
}
|
||||
}
|
||||
TokenType::RightParenthesis
|
||||
| TokenType::RightBracket
|
||||
| TokenType::Colon
|
||||
| TokenType::Semicolon
|
||||
| TokenType::RightBrace
|
||||
| TokenType::Comma
|
||||
| TokenType::Bang
|
||||
| TokenType::And
|
||||
| TokenType::Percent => Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: 0,
|
||||
message: format!("Unexpected token: '{}'", next_token),
|
||||
},
|
||||
TokenType::LeftBracket => Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: 0,
|
||||
message: "Unexpected token: '['".to_string(),
|
||||
},
|
||||
TokenType::StructuredReference {
|
||||
table_name,
|
||||
specifier,
|
||||
table_reference,
|
||||
} => {
|
||||
// We will try to convert to a normal reference
|
||||
// table_name[column_name] => cell1:cell2
|
||||
// table_name[[#This Row], [column_name]:[column_name]] => cell1:cell2
|
||||
if let Some(context) = &self.context {
|
||||
let context_sheet_index = match self.get_sheet_index_by_name(&context.sheet) {
|
||||
Some(i) => i,
|
||||
None => {
|
||||
return Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: 0,
|
||||
message: "sheet not found".to_string(),
|
||||
};
|
||||
}
|
||||
};
|
||||
// table-name => table
|
||||
let table = self.tables.get(&table_name).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"Table not found: '{table_name}' at '{}!{}{}'",
|
||||
context.sheet,
|
||||
number_to_column(context.column).expect(""),
|
||||
context.row
|
||||
)
|
||||
});
|
||||
let table_sheet_index = match self.get_sheet_index_by_name(&table.sheet_name) {
|
||||
Some(i) => i,
|
||||
None => {
|
||||
return Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: 0,
|
||||
message: "sheet not found".to_string(),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let sheet_name = if table_sheet_index == context_sheet_index {
|
||||
None
|
||||
} else {
|
||||
Some(table.sheet_name.clone())
|
||||
};
|
||||
|
||||
// context must be with tables.reference
|
||||
let (column_start, mut row_start, column_end, mut row_end) =
|
||||
parse_range(&table.reference).expect("Failed parsing range");
|
||||
|
||||
let totals_row_count = table.totals_row_count as i32;
|
||||
let header_row_count = table.header_row_count as i32;
|
||||
row_end -= totals_row_count;
|
||||
|
||||
match specifier {
|
||||
Some(token::TableSpecifier::ThisRow) => {
|
||||
row_start = context.row;
|
||||
row_end = context.row;
|
||||
}
|
||||
Some(token::TableSpecifier::Totals) => {
|
||||
if totals_row_count != 0 {
|
||||
row_start = row_end + 1;
|
||||
row_end = row_start;
|
||||
} else {
|
||||
// Table1[#Totals] is #REF! if Table1 does not have totals
|
||||
return Node::ErrorKind(token::Error::REF);
|
||||
}
|
||||
}
|
||||
Some(token::TableSpecifier::Headers) => {
|
||||
row_end = row_start;
|
||||
}
|
||||
Some(token::TableSpecifier::Data) => {
|
||||
row_start += header_row_count;
|
||||
}
|
||||
Some(token::TableSpecifier::All) => {
|
||||
if totals_row_count != 0 {
|
||||
row_end += 1;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// skip the headers
|
||||
row_start += header_row_count;
|
||||
}
|
||||
}
|
||||
match table_reference {
|
||||
None => {
|
||||
return Node::RangeKind {
|
||||
sheet_name,
|
||||
sheet_index: table_sheet_index,
|
||||
absolute_row1: true,
|
||||
absolute_column1: true,
|
||||
row1: row_start,
|
||||
column1: column_start,
|
||||
absolute_row2: true,
|
||||
absolute_column2: true,
|
||||
row2: row_end,
|
||||
column2: column_end,
|
||||
};
|
||||
}
|
||||
Some(TableReference::ColumnReference(s)) => {
|
||||
let column_index = match get_table_column_by_name(&s, table) {
|
||||
Some(s) => s + column_start,
|
||||
None => {
|
||||
return Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: self.lexer.get_position() as usize,
|
||||
message: format!(
|
||||
"Expecting column: {s} in table {table_name}"
|
||||
),
|
||||
};
|
||||
}
|
||||
};
|
||||
if row_start == row_end {
|
||||
return Node::ReferenceKind {
|
||||
sheet_name,
|
||||
sheet_index: table_sheet_index,
|
||||
absolute_row: true,
|
||||
absolute_column: true,
|
||||
row: row_start,
|
||||
column: column_index,
|
||||
};
|
||||
}
|
||||
return Node::RangeKind {
|
||||
sheet_name,
|
||||
sheet_index: table_sheet_index,
|
||||
absolute_row1: true,
|
||||
absolute_column1: true,
|
||||
row1: row_start,
|
||||
column1: column_index,
|
||||
absolute_row2: true,
|
||||
absolute_column2: true,
|
||||
row2: row_end,
|
||||
column2: column_index,
|
||||
};
|
||||
}
|
||||
Some(TableReference::RangeReference((left, right))) => {
|
||||
let left_column_index = match get_table_column_by_name(&left, table) {
|
||||
Some(f) => f + column_start,
|
||||
None => {
|
||||
return Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: self.lexer.get_position() as usize,
|
||||
message: format!(
|
||||
"Expecting column: {left} in table {table_name}"
|
||||
),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let right_column_index = match get_table_column_by_name(&right, table) {
|
||||
Some(f) => f + column_start,
|
||||
None => {
|
||||
return Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: self.lexer.get_position() as usize,
|
||||
message: format!(
|
||||
"Expecting column: {right} in table {table_name}"
|
||||
),
|
||||
};
|
||||
}
|
||||
};
|
||||
return Node::RangeKind {
|
||||
sheet_name,
|
||||
sheet_index: table_sheet_index,
|
||||
absolute_row1: true,
|
||||
absolute_column1: true,
|
||||
row1: row_start,
|
||||
column1: left_column_index,
|
||||
absolute_row2: true,
|
||||
absolute_column2: true,
|
||||
row2: row_end,
|
||||
column2: right_column_index,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: 0,
|
||||
message: "Structured references not supported in R1C1 mode".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_function_args(&mut self) -> Result<Vec<Node>, Node> {
|
||||
let mut args: Vec<Node> = Vec::new();
|
||||
let mut next_token = self.lexer.peek_token();
|
||||
if next_token == TokenType::RightParenthesis {
|
||||
return Ok(args);
|
||||
}
|
||||
if self.lexer.peek_token() == TokenType::Comma {
|
||||
args.push(Node::EmptyArgKind);
|
||||
} else {
|
||||
let t = self.parse_expr();
|
||||
if let Node::ParseErrorKind { .. } = t {
|
||||
return Err(t);
|
||||
}
|
||||
args.push(t);
|
||||
}
|
||||
next_token = self.lexer.peek_token();
|
||||
while next_token == TokenType::Comma {
|
||||
self.lexer.advance_token();
|
||||
if self.lexer.peek_token() == TokenType::Comma {
|
||||
args.push(Node::EmptyArgKind);
|
||||
next_token = TokenType::Comma;
|
||||
} else if self.lexer.peek_token() == TokenType::RightParenthesis {
|
||||
args.push(Node::EmptyArgKind);
|
||||
return Ok(args);
|
||||
} else {
|
||||
let p = self.parse_expr();
|
||||
if let Node::ParseErrorKind { .. } = p {
|
||||
return Err(p);
|
||||
}
|
||||
next_token = self.lexer.peek_token();
|
||||
args.push(p);
|
||||
}
|
||||
}
|
||||
Ok(args)
|
||||
}
|
||||
}
|
||||
397
base/src/expressions/parser/move_formula.rs
Normal file
397
base/src/expressions/parser/move_formula.rs
Normal file
@@ -0,0 +1,397 @@
|
||||
use super::{
|
||||
stringify::{stringify_reference, DisplaceData},
|
||||
Node, Reference,
|
||||
};
|
||||
use crate::{
|
||||
constants::{LAST_COLUMN, LAST_ROW},
|
||||
expressions::token::OpUnary,
|
||||
};
|
||||
use crate::{
|
||||
expressions::types::{Area, CellReferenceRC},
|
||||
number_format::to_excel_precision_str,
|
||||
};
|
||||
|
||||
pub(crate) fn ref_is_in_area(sheet: u32, row: i32, column: i32, area: &Area) -> bool {
|
||||
if area.sheet != sheet {
|
||||
return false;
|
||||
}
|
||||
if row < area.row || row > area.row + area.height - 1 {
|
||||
return false;
|
||||
}
|
||||
if column < area.column || column > area.column + area.width - 1 {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) struct MoveContext<'a> {
|
||||
pub source_sheet_name: &'a str,
|
||||
pub row: i32,
|
||||
pub column: i32,
|
||||
pub area: &'a Area,
|
||||
pub target_sheet_name: &'a str,
|
||||
pub row_delta: i32,
|
||||
pub column_delta: i32,
|
||||
}
|
||||
|
||||
/// This implements Excel's cut && paste
|
||||
/// We are moving a formula in (row, column) to (row+row_delta, column + column_delta).
|
||||
/// All references that do not point to a cell in area will be left untouched.
|
||||
/// All references that point to a cell in area will be displaced
|
||||
pub(crate) fn move_formula(node: &Node, move_context: &MoveContext) -> String {
|
||||
to_string_moved(node, move_context)
|
||||
}
|
||||
|
||||
fn move_function(name: &str, args: &Vec<Node>, move_context: &MoveContext) -> String {
|
||||
let mut first = true;
|
||||
let mut arguments = "".to_string();
|
||||
for el in args {
|
||||
if !first {
|
||||
arguments = format!("{},{}", arguments, to_string_moved(el, move_context));
|
||||
} else {
|
||||
first = false;
|
||||
arguments = to_string_moved(el, move_context);
|
||||
}
|
||||
}
|
||||
format!("{}({})", name, arguments)
|
||||
}
|
||||
|
||||
fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
|
||||
use self::Node::*;
|
||||
match node {
|
||||
BooleanKind(value) => format!("{}", value).to_ascii_uppercase(),
|
||||
NumberKind(number) => to_excel_precision_str(*number),
|
||||
StringKind(value) => format!("\"{}\"", value),
|
||||
ReferenceKind {
|
||||
sheet_name,
|
||||
sheet_index,
|
||||
absolute_row,
|
||||
absolute_column,
|
||||
row,
|
||||
column,
|
||||
} => {
|
||||
let reference_row = if *absolute_row {
|
||||
*row
|
||||
} else {
|
||||
row + move_context.row
|
||||
};
|
||||
let reference_column = if *absolute_column {
|
||||
*column
|
||||
} else {
|
||||
column + move_context.column
|
||||
};
|
||||
|
||||
let new_row;
|
||||
let new_column;
|
||||
let mut ref_sheet_name = sheet_name;
|
||||
let source_sheet_name = &Some(move_context.source_sheet_name.to_string());
|
||||
|
||||
if ref_is_in_area(
|
||||
*sheet_index,
|
||||
reference_row,
|
||||
reference_column,
|
||||
move_context.area,
|
||||
) {
|
||||
// if the reference is in the area we are moving we want to displace the reference
|
||||
new_row = row + move_context.row_delta;
|
||||
new_column = column + move_context.column_delta;
|
||||
} else {
|
||||
// If the reference is not in the area we are moving the reference remains unchanged
|
||||
new_row = *row;
|
||||
new_column = *column;
|
||||
if move_context.target_sheet_name != move_context.source_sheet_name
|
||||
&& sheet_name.is_none()
|
||||
{
|
||||
ref_sheet_name = source_sheet_name;
|
||||
}
|
||||
};
|
||||
let context = CellReferenceRC {
|
||||
sheet: move_context.source_sheet_name.to_string(),
|
||||
column: move_context.column,
|
||||
row: move_context.row,
|
||||
};
|
||||
stringify_reference(
|
||||
Some(&context),
|
||||
&DisplaceData::None,
|
||||
&Reference {
|
||||
sheet_name: ref_sheet_name,
|
||||
sheet_index: *sheet_index,
|
||||
absolute_row: *absolute_row,
|
||||
absolute_column: *absolute_column,
|
||||
row: new_row,
|
||||
column: new_column,
|
||||
},
|
||||
false,
|
||||
false,
|
||||
)
|
||||
}
|
||||
RangeKind {
|
||||
sheet_name,
|
||||
sheet_index,
|
||||
absolute_row1,
|
||||
absolute_column1,
|
||||
row1,
|
||||
column1,
|
||||
absolute_row2,
|
||||
absolute_column2,
|
||||
row2,
|
||||
column2,
|
||||
} => {
|
||||
let full_row = *absolute_row1 && *absolute_row2 && (*row1 == 1) && (*row2 == LAST_ROW);
|
||||
let full_column = *absolute_column1
|
||||
&& *absolute_column2
|
||||
&& (*column1 == 1)
|
||||
&& (*column2 == LAST_COLUMN);
|
||||
|
||||
let reference_row1 = if *absolute_row1 {
|
||||
*row1
|
||||
} else {
|
||||
row1 + move_context.row
|
||||
};
|
||||
let reference_column1 = if *absolute_column1 {
|
||||
*column1
|
||||
} else {
|
||||
column1 + move_context.column
|
||||
};
|
||||
|
||||
let reference_row2 = if *absolute_row2 {
|
||||
*row2
|
||||
} else {
|
||||
row2 + move_context.row
|
||||
};
|
||||
let reference_column2 = if *absolute_column2 {
|
||||
*column2
|
||||
} else {
|
||||
column2 + move_context.column
|
||||
};
|
||||
|
||||
let new_row1;
|
||||
let new_column1;
|
||||
let new_row2;
|
||||
let new_column2;
|
||||
let mut ref_sheet_name = sheet_name;
|
||||
let source_sheet_name = &Some(move_context.source_sheet_name.to_string());
|
||||
if ref_is_in_area(
|
||||
*sheet_index,
|
||||
reference_row1,
|
||||
reference_column1,
|
||||
move_context.area,
|
||||
) && ref_is_in_area(
|
||||
*sheet_index,
|
||||
reference_row2,
|
||||
reference_column2,
|
||||
move_context.area,
|
||||
) {
|
||||
// if the whole range is inside the area we are moving we want to displace the context
|
||||
new_row1 = row1 + move_context.row_delta;
|
||||
new_column1 = column1 + move_context.column_delta;
|
||||
new_row2 = row2 + move_context.row_delta;
|
||||
new_column2 = column2 + move_context.column_delta;
|
||||
} else {
|
||||
// If the reference is not in the area we are moving the context remains unchanged
|
||||
new_row1 = *row1;
|
||||
new_column1 = *column1;
|
||||
new_row2 = *row2;
|
||||
new_column2 = *column2;
|
||||
if move_context.target_sheet_name != move_context.source_sheet_name
|
||||
&& sheet_name.is_none()
|
||||
{
|
||||
ref_sheet_name = source_sheet_name;
|
||||
}
|
||||
};
|
||||
let context = CellReferenceRC {
|
||||
sheet: move_context.source_sheet_name.to_string(),
|
||||
column: move_context.column,
|
||||
row: move_context.row,
|
||||
};
|
||||
let s1 = stringify_reference(
|
||||
Some(&context),
|
||||
&DisplaceData::None,
|
||||
&Reference {
|
||||
sheet_name: ref_sheet_name,
|
||||
sheet_index: *sheet_index,
|
||||
absolute_row: *absolute_row1,
|
||||
absolute_column: *absolute_column1,
|
||||
row: new_row1,
|
||||
column: new_column1,
|
||||
},
|
||||
full_row,
|
||||
full_column,
|
||||
);
|
||||
let s2 = stringify_reference(
|
||||
Some(&context),
|
||||
&DisplaceData::None,
|
||||
&Reference {
|
||||
sheet_name: &None,
|
||||
sheet_index: *sheet_index,
|
||||
absolute_row: *absolute_row2,
|
||||
absolute_column: *absolute_column2,
|
||||
row: new_row2,
|
||||
column: new_column2,
|
||||
},
|
||||
full_row,
|
||||
full_column,
|
||||
);
|
||||
format!("{}:{}", s1, s2)
|
||||
}
|
||||
WrongReferenceKind {
|
||||
sheet_name,
|
||||
absolute_row,
|
||||
absolute_column,
|
||||
row,
|
||||
column,
|
||||
} => {
|
||||
// NB: Excel does not displace wrong references but Google Docs does. We follow Excel
|
||||
let context = CellReferenceRC {
|
||||
sheet: move_context.source_sheet_name.to_string(),
|
||||
column: move_context.column,
|
||||
row: move_context.row,
|
||||
};
|
||||
// It's a wrong reference, so there is no valid `sheet_index`.
|
||||
// We don't need it, since the `sheet_index` is only used if `displace_data` is not `None`.
|
||||
// I should fix it, maybe putting the `sheet_index` inside the `displace_data`
|
||||
stringify_reference(
|
||||
Some(&context),
|
||||
&DisplaceData::None,
|
||||
&Reference {
|
||||
sheet_name,
|
||||
sheet_index: 0, // HACK
|
||||
row: *row,
|
||||
column: *column,
|
||||
absolute_row: *absolute_row,
|
||||
absolute_column: *absolute_column,
|
||||
},
|
||||
false,
|
||||
false,
|
||||
)
|
||||
}
|
||||
WrongRangeKind {
|
||||
sheet_name,
|
||||
absolute_row1,
|
||||
absolute_column1,
|
||||
row1,
|
||||
column1,
|
||||
absolute_row2,
|
||||
absolute_column2,
|
||||
row2,
|
||||
column2,
|
||||
} => {
|
||||
let full_row = *absolute_row1 && *absolute_row2 && (*row1 == 1) && (*row2 == LAST_ROW);
|
||||
let full_column = *absolute_column1
|
||||
&& *absolute_column2
|
||||
&& (*column1 == 1)
|
||||
&& (*column2 == LAST_COLUMN);
|
||||
|
||||
// NB: Excel does not displace wrong references but Google Docs does. We follow Excel
|
||||
let context = CellReferenceRC {
|
||||
sheet: move_context.source_sheet_name.to_string(),
|
||||
column: move_context.column,
|
||||
row: move_context.row,
|
||||
};
|
||||
let s1 = stringify_reference(
|
||||
Some(&context),
|
||||
&DisplaceData::None,
|
||||
&Reference {
|
||||
sheet_name,
|
||||
sheet_index: 0, // HACK
|
||||
row: *row1,
|
||||
column: *column1,
|
||||
absolute_row: *absolute_row1,
|
||||
absolute_column: *absolute_column1,
|
||||
},
|
||||
full_row,
|
||||
full_column,
|
||||
);
|
||||
let s2 = stringify_reference(
|
||||
Some(&context),
|
||||
&DisplaceData::None,
|
||||
&Reference {
|
||||
sheet_name: &None,
|
||||
sheet_index: 0, // HACK
|
||||
row: *row2,
|
||||
column: *column2,
|
||||
absolute_row: *absolute_row2,
|
||||
absolute_column: *absolute_column2,
|
||||
},
|
||||
full_row,
|
||||
full_column,
|
||||
);
|
||||
format!("{}:{}", s1, s2)
|
||||
}
|
||||
OpRangeKind { left, right } => format!(
|
||||
"{}:{}",
|
||||
to_string_moved(left, move_context),
|
||||
to_string_moved(right, move_context),
|
||||
),
|
||||
OpConcatenateKind { left, right } => format!(
|
||||
"{}&{}",
|
||||
to_string_moved(left, move_context),
|
||||
to_string_moved(right, move_context),
|
||||
),
|
||||
OpSumKind { kind, left, right } => format!(
|
||||
"{}{}{}",
|
||||
to_string_moved(left, move_context),
|
||||
kind,
|
||||
to_string_moved(right, move_context),
|
||||
),
|
||||
OpProductKind { kind, left, right } => {
|
||||
let x = match **left {
|
||||
OpSumKind { .. } => format!("({})", to_string_moved(left, move_context)),
|
||||
CompareKind { .. } => format!("({})", to_string_moved(left, move_context)),
|
||||
_ => to_string_moved(left, move_context),
|
||||
};
|
||||
let y = match **right {
|
||||
OpSumKind { .. } => format!("({})", to_string_moved(right, move_context)),
|
||||
CompareKind { .. } => format!("({})", to_string_moved(right, move_context)),
|
||||
OpProductKind { .. } => format!("({})", to_string_moved(right, move_context)),
|
||||
UnaryKind { .. } => {
|
||||
format!("({})", to_string_moved(right, move_context))
|
||||
}
|
||||
_ => to_string_moved(right, move_context),
|
||||
};
|
||||
format!("{}{}{}", x, kind, y)
|
||||
}
|
||||
OpPowerKind { left, right } => format!(
|
||||
"{}^{}",
|
||||
to_string_moved(left, move_context),
|
||||
to_string_moved(right, move_context),
|
||||
),
|
||||
InvalidFunctionKind { name, args } => move_function(name, args, move_context),
|
||||
FunctionKind { kind, args } => {
|
||||
let name = &kind.to_string();
|
||||
move_function(name, args, move_context)
|
||||
}
|
||||
ArrayKind(args) => {
|
||||
// This code is a placeholder. Arrays are not yet implemented
|
||||
let mut first = true;
|
||||
let mut arguments = "".to_string();
|
||||
for el in args {
|
||||
if !first {
|
||||
arguments = format!("{},{}", arguments, to_string_moved(el, move_context));
|
||||
} else {
|
||||
first = false;
|
||||
arguments = to_string_moved(el, move_context);
|
||||
}
|
||||
}
|
||||
format!("{{{}}}", arguments)
|
||||
}
|
||||
VariableKind(value) => value.to_string(),
|
||||
CompareKind { kind, left, right } => format!(
|
||||
"{}{}{}",
|
||||
to_string_moved(left, move_context),
|
||||
kind,
|
||||
to_string_moved(right, move_context),
|
||||
),
|
||||
UnaryKind { kind, right } => match kind {
|
||||
OpUnary::Minus => format!("-{}", to_string_moved(right, move_context)),
|
||||
OpUnary::Percentage => format!("{}%", to_string_moved(right, move_context)),
|
||||
},
|
||||
ErrorKind(kind) => format!("{}", kind),
|
||||
ParseErrorKind {
|
||||
formula,
|
||||
message: _,
|
||||
position: _,
|
||||
} => formula.to_string(),
|
||||
EmptyArgKind => "".to_string(),
|
||||
}
|
||||
}
|
||||
612
base/src/expressions/parser/stringify.rs
Normal file
612
base/src/expressions/parser/stringify.rs
Normal file
@@ -0,0 +1,612 @@
|
||||
use super::{super::utils::quote_name, Node, Reference};
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::token::OpUnary;
|
||||
use crate::{expressions::types::CellReferenceRC, number_format::to_excel_precision_str};
|
||||
|
||||
pub enum DisplaceData {
|
||||
Column {
|
||||
sheet: u32,
|
||||
column: i32,
|
||||
delta: i32,
|
||||
},
|
||||
Row {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
delta: i32,
|
||||
},
|
||||
CellHorizontal {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
delta: i32,
|
||||
},
|
||||
CellVertical {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
delta: i32,
|
||||
},
|
||||
ColumnMove {
|
||||
sheet: u32,
|
||||
column: i32,
|
||||
delta: i32,
|
||||
},
|
||||
None,
|
||||
}
|
||||
|
||||
pub fn to_rc_format(node: &Node) -> String {
|
||||
stringify(node, None, &DisplaceData::None, false)
|
||||
}
|
||||
|
||||
pub fn to_string_displaced(
|
||||
node: &Node,
|
||||
context: &CellReferenceRC,
|
||||
displace_data: &DisplaceData,
|
||||
) -> String {
|
||||
stringify(node, Some(context), displace_data, false)
|
||||
}
|
||||
|
||||
pub fn to_string(node: &Node, context: &CellReferenceRC) -> String {
|
||||
stringify(node, Some(context), &DisplaceData::None, false)
|
||||
}
|
||||
|
||||
pub fn to_excel_string(node: &Node, context: &CellReferenceRC) -> String {
|
||||
stringify(node, Some(context), &DisplaceData::None, true)
|
||||
}
|
||||
|
||||
/// Converts a local reference to a string applying some displacement if needed.
|
||||
/// It uses A1 style if context is not None. If context is None it uses R1C1 style
|
||||
/// If full_row is true then the row details will be omitted in the A1 case
|
||||
/// If full_colum is true then column details will be omitted.
|
||||
pub(crate) fn stringify_reference(
|
||||
context: Option<&CellReferenceRC>,
|
||||
displace_data: &DisplaceData,
|
||||
reference: &Reference,
|
||||
full_row: bool,
|
||||
full_column: bool,
|
||||
) -> String {
|
||||
let sheet_name = reference.sheet_name;
|
||||
let sheet_index = reference.sheet_index;
|
||||
let absolute_row = reference.absolute_row;
|
||||
let absolute_column = reference.absolute_column;
|
||||
let row = reference.row;
|
||||
let column = reference.column;
|
||||
match context {
|
||||
Some(context) => {
|
||||
let mut row = if absolute_row { row } else { row + context.row };
|
||||
let mut column = if absolute_column {
|
||||
column
|
||||
} else {
|
||||
column + context.column
|
||||
};
|
||||
match displace_data {
|
||||
DisplaceData::Row {
|
||||
sheet,
|
||||
row: displace_row,
|
||||
delta,
|
||||
} => {
|
||||
if sheet_index == *sheet && !full_row {
|
||||
if *delta < 0 {
|
||||
if &row >= displace_row {
|
||||
if row < displace_row - *delta {
|
||||
return "#REF!".to_string();
|
||||
}
|
||||
row += *delta;
|
||||
}
|
||||
} else if &row >= displace_row {
|
||||
row += *delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
DisplaceData::Column {
|
||||
sheet,
|
||||
column: displace_column,
|
||||
delta,
|
||||
} => {
|
||||
if sheet_index == *sheet && !full_column {
|
||||
if *delta < 0 {
|
||||
if &column >= displace_column {
|
||||
if column < displace_column - *delta {
|
||||
return "#REF!".to_string();
|
||||
}
|
||||
column += *delta;
|
||||
}
|
||||
} else if &column >= displace_column {
|
||||
column += *delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
DisplaceData::CellHorizontal {
|
||||
sheet,
|
||||
row: displace_row,
|
||||
column: displace_column,
|
||||
delta,
|
||||
} => {
|
||||
if sheet_index == *sheet && displace_row == &row {
|
||||
if *delta < 0 {
|
||||
if &column >= displace_column {
|
||||
if column < displace_column - *delta {
|
||||
return "#REF!".to_string();
|
||||
}
|
||||
column += *delta;
|
||||
}
|
||||
} else if &column >= displace_column {
|
||||
column += *delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
DisplaceData::CellVertical {
|
||||
sheet,
|
||||
row: displace_row,
|
||||
column: displace_column,
|
||||
delta,
|
||||
} => {
|
||||
if sheet_index == *sheet && displace_column == &column {
|
||||
if *delta < 0 {
|
||||
if &row >= displace_row {
|
||||
if row < displace_row - *delta {
|
||||
return "#REF!".to_string();
|
||||
}
|
||||
row += *delta;
|
||||
}
|
||||
} else if &row >= displace_row {
|
||||
row += *delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
DisplaceData::ColumnMove {
|
||||
sheet,
|
||||
column: move_column,
|
||||
delta,
|
||||
} => {
|
||||
if sheet_index == *sheet {
|
||||
if column == *move_column {
|
||||
column += *delta;
|
||||
} else if (*delta > 0
|
||||
&& column > *move_column
|
||||
&& column <= *move_column + *delta)
|
||||
|| (*delta < 0
|
||||
&& column < *move_column
|
||||
&& column >= *move_column + *delta)
|
||||
{
|
||||
column -= *delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
DisplaceData::None => {}
|
||||
}
|
||||
if row < 1 {
|
||||
return "#REF!".to_string();
|
||||
}
|
||||
let mut row_abs = if absolute_row {
|
||||
format!("${}", row)
|
||||
} else {
|
||||
format!("{}", row)
|
||||
};
|
||||
let column = match crate::expressions::utils::number_to_column(column) {
|
||||
Some(s) => s,
|
||||
None => return "#REF!".to_string(),
|
||||
};
|
||||
let mut col_abs = if absolute_column {
|
||||
format!("${}", column)
|
||||
} else {
|
||||
column
|
||||
};
|
||||
if full_row {
|
||||
row_abs = "".to_string()
|
||||
}
|
||||
if full_column {
|
||||
col_abs = "".to_string()
|
||||
}
|
||||
match &sheet_name {
|
||||
Some(name) => {
|
||||
format!("{}!{}{}", quote_name(name), col_abs, row_abs)
|
||||
}
|
||||
None => {
|
||||
format!("{}{}", col_abs, row_abs)
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let row_abs = if absolute_row {
|
||||
format!("R{}", row)
|
||||
} else {
|
||||
format!("R[{}]", row)
|
||||
};
|
||||
let col_abs = if absolute_column {
|
||||
format!("C{}", column)
|
||||
} else {
|
||||
format!("C[{}]", column)
|
||||
};
|
||||
match &sheet_name {
|
||||
Some(name) => {
|
||||
format!("{}!{}{}", quote_name(name), row_abs, col_abs)
|
||||
}
|
||||
None => {
|
||||
format!("{}{}", row_abs, col_abs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_function(
|
||||
name: &str,
|
||||
args: &Vec<Node>,
|
||||
context: Option<&CellReferenceRC>,
|
||||
displace_data: &DisplaceData,
|
||||
use_original_name: bool,
|
||||
) -> String {
|
||||
let mut first = true;
|
||||
let mut arguments = "".to_string();
|
||||
for el in args {
|
||||
if !first {
|
||||
arguments = format!(
|
||||
"{},{}",
|
||||
arguments,
|
||||
stringify(el, context, displace_data, use_original_name)
|
||||
);
|
||||
} else {
|
||||
first = false;
|
||||
arguments = stringify(el, context, displace_data, use_original_name);
|
||||
}
|
||||
}
|
||||
format!("{}({})", name, arguments)
|
||||
}
|
||||
|
||||
fn stringify(
|
||||
node: &Node,
|
||||
context: Option<&CellReferenceRC>,
|
||||
displace_data: &DisplaceData,
|
||||
use_original_name: bool,
|
||||
) -> String {
|
||||
use self::Node::*;
|
||||
match node {
|
||||
BooleanKind(value) => format!("{}", value).to_ascii_uppercase(),
|
||||
NumberKind(number) => to_excel_precision_str(*number),
|
||||
StringKind(value) => format!("\"{}\"", value),
|
||||
WrongReferenceKind {
|
||||
sheet_name,
|
||||
column,
|
||||
row,
|
||||
absolute_row,
|
||||
absolute_column,
|
||||
} => stringify_reference(
|
||||
context,
|
||||
&DisplaceData::None,
|
||||
&Reference {
|
||||
sheet_name,
|
||||
sheet_index: 0,
|
||||
row: *row,
|
||||
column: *column,
|
||||
absolute_row: *absolute_row,
|
||||
absolute_column: *absolute_column,
|
||||
},
|
||||
false,
|
||||
false,
|
||||
),
|
||||
ReferenceKind {
|
||||
sheet_name,
|
||||
sheet_index,
|
||||
column,
|
||||
row,
|
||||
absolute_row,
|
||||
absolute_column,
|
||||
} => stringify_reference(
|
||||
context,
|
||||
displace_data,
|
||||
&Reference {
|
||||
sheet_name,
|
||||
sheet_index: *sheet_index,
|
||||
row: *row,
|
||||
column: *column,
|
||||
absolute_row: *absolute_row,
|
||||
absolute_column: *absolute_column,
|
||||
},
|
||||
false,
|
||||
false,
|
||||
),
|
||||
RangeKind {
|
||||
sheet_name,
|
||||
sheet_index,
|
||||
absolute_row1,
|
||||
absolute_column1,
|
||||
row1,
|
||||
column1,
|
||||
absolute_row2,
|
||||
absolute_column2,
|
||||
row2,
|
||||
column2,
|
||||
} => {
|
||||
// Note that open ranges SUM(A:A) or SUM(1:1) will be treated as normal ranges in the R1C1 (internal) representation
|
||||
// A:A will be R1C[0]:R1048576C[0]
|
||||
// So when we are forming the A1 range we need to strip the irrelevant information
|
||||
let full_row = *absolute_row1 && *absolute_row2 && (*row1 == 1) && (*row2 == LAST_ROW);
|
||||
let full_column = *absolute_column1
|
||||
&& *absolute_column2
|
||||
&& (*column1 == 1)
|
||||
&& (*column2 == LAST_COLUMN);
|
||||
let s1 = stringify_reference(
|
||||
context,
|
||||
displace_data,
|
||||
&Reference {
|
||||
sheet_name,
|
||||
sheet_index: *sheet_index,
|
||||
row: *row1,
|
||||
column: *column1,
|
||||
absolute_row: *absolute_row1,
|
||||
absolute_column: *absolute_column1,
|
||||
},
|
||||
full_row,
|
||||
full_column,
|
||||
);
|
||||
let s2 = stringify_reference(
|
||||
context,
|
||||
displace_data,
|
||||
&Reference {
|
||||
sheet_name: &None,
|
||||
sheet_index: *sheet_index,
|
||||
row: *row2,
|
||||
column: *column2,
|
||||
absolute_row: *absolute_row2,
|
||||
absolute_column: *absolute_column2,
|
||||
},
|
||||
full_row,
|
||||
full_column,
|
||||
);
|
||||
format!("{}:{}", s1, s2)
|
||||
}
|
||||
WrongRangeKind {
|
||||
sheet_name,
|
||||
absolute_row1,
|
||||
absolute_column1,
|
||||
row1,
|
||||
column1,
|
||||
absolute_row2,
|
||||
absolute_column2,
|
||||
row2,
|
||||
column2,
|
||||
} => {
|
||||
// Note that open ranges SUM(A:A) or SUM(1:1) will be treated as normal ranges in the R1C1 (internal) representation
|
||||
// A:A will be R1C[0]:R1048576C[0]
|
||||
// So when we are forming the A1 range we need to strip the irrelevant information
|
||||
let full_row = *absolute_row1 && *absolute_row2 && (*row1 == 1) && (*row2 == LAST_ROW);
|
||||
let full_column = *absolute_column1
|
||||
&& *absolute_column2
|
||||
&& (*column1 == 1)
|
||||
&& (*column2 == LAST_COLUMN);
|
||||
let s1 = stringify_reference(
|
||||
context,
|
||||
&DisplaceData::None,
|
||||
&Reference {
|
||||
sheet_name,
|
||||
sheet_index: 0, // HACK
|
||||
row: *row1,
|
||||
column: *column1,
|
||||
absolute_row: *absolute_row1,
|
||||
absolute_column: *absolute_column1,
|
||||
},
|
||||
full_row,
|
||||
full_column,
|
||||
);
|
||||
let s2 = stringify_reference(
|
||||
context,
|
||||
&DisplaceData::None,
|
||||
&Reference {
|
||||
sheet_name: &None,
|
||||
sheet_index: 0, // HACK
|
||||
row: *row2,
|
||||
column: *column2,
|
||||
absolute_row: *absolute_row2,
|
||||
absolute_column: *absolute_column2,
|
||||
},
|
||||
full_row,
|
||||
full_column,
|
||||
);
|
||||
format!("{}:{}", s1, s2)
|
||||
}
|
||||
OpRangeKind { left, right } => format!(
|
||||
"{}:{}",
|
||||
stringify(left, context, displace_data, use_original_name),
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
),
|
||||
OpConcatenateKind { left, right } => format!(
|
||||
"{}&{}",
|
||||
stringify(left, context, displace_data, use_original_name),
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
),
|
||||
CompareKind { kind, left, right } => format!(
|
||||
"{}{}{}",
|
||||
stringify(left, context, displace_data, use_original_name),
|
||||
kind,
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
),
|
||||
OpSumKind { kind, left, right } => format!(
|
||||
"{}{}{}",
|
||||
stringify(left, context, displace_data, use_original_name),
|
||||
kind,
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
),
|
||||
OpProductKind { kind, left, right } => {
|
||||
let x = match **left {
|
||||
OpSumKind { .. } => format!(
|
||||
"({})",
|
||||
stringify(left, context, displace_data, use_original_name)
|
||||
),
|
||||
CompareKind { .. } => format!(
|
||||
"({})",
|
||||
stringify(left, context, displace_data, use_original_name)
|
||||
),
|
||||
_ => stringify(left, context, displace_data, use_original_name),
|
||||
};
|
||||
let y = match **right {
|
||||
OpSumKind { .. } => format!(
|
||||
"({})",
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
),
|
||||
CompareKind { .. } => format!(
|
||||
"({})",
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
),
|
||||
OpProductKind { .. } => format!(
|
||||
"({})",
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
),
|
||||
_ => stringify(right, context, displace_data, use_original_name),
|
||||
};
|
||||
format!("{}{}{}", x, kind, y)
|
||||
}
|
||||
OpPowerKind { left, right } => format!(
|
||||
"{}^{}",
|
||||
stringify(left, context, displace_data, use_original_name),
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
),
|
||||
InvalidFunctionKind { name, args } => {
|
||||
format_function(name, args, context, displace_data, use_original_name)
|
||||
}
|
||||
FunctionKind { kind, args } => {
|
||||
let name = if use_original_name {
|
||||
kind.to_xlsx_string()
|
||||
} else {
|
||||
kind.to_string()
|
||||
};
|
||||
format_function(&name, args, context, displace_data, use_original_name)
|
||||
}
|
||||
ArrayKind(args) => {
|
||||
let mut first = true;
|
||||
let mut arguments = "".to_string();
|
||||
for el in args {
|
||||
if !first {
|
||||
arguments = format!(
|
||||
"{},{}",
|
||||
arguments,
|
||||
stringify(el, context, displace_data, use_original_name)
|
||||
);
|
||||
} else {
|
||||
first = false;
|
||||
arguments = stringify(el, context, displace_data, use_original_name);
|
||||
}
|
||||
}
|
||||
format!("{{{}}}", arguments)
|
||||
}
|
||||
VariableKind(value) => value.to_string(),
|
||||
UnaryKind { kind, right } => match kind {
|
||||
OpUnary::Minus => {
|
||||
format!(
|
||||
"-{}",
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
)
|
||||
}
|
||||
OpUnary::Percentage => {
|
||||
format!(
|
||||
"{}%",
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
)
|
||||
}
|
||||
},
|
||||
ErrorKind(kind) => format!("{}", kind),
|
||||
ParseErrorKind {
|
||||
formula,
|
||||
position: _,
|
||||
message: _,
|
||||
} => formula.to_string(),
|
||||
EmptyArgKind => "".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn rename_sheet_in_node(node: &mut Node, sheet_index: u32, new_name: &str) {
|
||||
match node {
|
||||
// Rename
|
||||
Node::ReferenceKind {
|
||||
sheet_name,
|
||||
sheet_index: index,
|
||||
..
|
||||
} => {
|
||||
if *index == sheet_index && sheet_name.is_some() {
|
||||
*sheet_name = Some(new_name.to_owned());
|
||||
}
|
||||
}
|
||||
Node::RangeKind {
|
||||
sheet_name,
|
||||
sheet_index: index,
|
||||
..
|
||||
} => {
|
||||
if *index == sheet_index && sheet_name.is_some() {
|
||||
*sheet_name = Some(new_name.to_owned());
|
||||
}
|
||||
}
|
||||
Node::WrongReferenceKind { sheet_name, .. } => {
|
||||
if let Some(name) = sheet_name {
|
||||
if name.to_uppercase() == new_name.to_uppercase() {
|
||||
*sheet_name = Some(name.to_owned())
|
||||
}
|
||||
}
|
||||
}
|
||||
Node::WrongRangeKind { sheet_name, .. } => {
|
||||
if sheet_name.is_some() {
|
||||
*sheet_name = Some(new_name.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
// Go next level
|
||||
Node::OpRangeKind { left, right } => {
|
||||
rename_sheet_in_node(left, sheet_index, new_name);
|
||||
rename_sheet_in_node(right, sheet_index, new_name);
|
||||
}
|
||||
Node::OpConcatenateKind { left, right } => {
|
||||
rename_sheet_in_node(left, sheet_index, new_name);
|
||||
rename_sheet_in_node(right, sheet_index, new_name);
|
||||
}
|
||||
Node::OpSumKind {
|
||||
kind: _,
|
||||
left,
|
||||
right,
|
||||
} => {
|
||||
rename_sheet_in_node(left, sheet_index, new_name);
|
||||
rename_sheet_in_node(right, sheet_index, new_name);
|
||||
}
|
||||
Node::OpProductKind {
|
||||
kind: _,
|
||||
left,
|
||||
right,
|
||||
} => {
|
||||
rename_sheet_in_node(left, sheet_index, new_name);
|
||||
rename_sheet_in_node(right, sheet_index, new_name);
|
||||
}
|
||||
Node::OpPowerKind { left, right } => {
|
||||
rename_sheet_in_node(left, sheet_index, new_name);
|
||||
rename_sheet_in_node(right, sheet_index, new_name);
|
||||
}
|
||||
Node::FunctionKind { kind: _, args } => {
|
||||
for arg in args {
|
||||
rename_sheet_in_node(arg, sheet_index, new_name);
|
||||
}
|
||||
}
|
||||
Node::InvalidFunctionKind { name: _, args } => {
|
||||
for arg in args {
|
||||
rename_sheet_in_node(arg, sheet_index, new_name);
|
||||
}
|
||||
}
|
||||
Node::CompareKind {
|
||||
kind: _,
|
||||
left,
|
||||
right,
|
||||
} => {
|
||||
rename_sheet_in_node(left, sheet_index, new_name);
|
||||
rename_sheet_in_node(right, sheet_index, new_name);
|
||||
}
|
||||
Node::UnaryKind { kind: _, right } => {
|
||||
rename_sheet_in_node(right, sheet_index, new_name);
|
||||
}
|
||||
|
||||
// Do nothing
|
||||
Node::BooleanKind(_) => {}
|
||||
Node::NumberKind(_) => {}
|
||||
Node::StringKind(_) => {}
|
||||
Node::ErrorKind(_) => {}
|
||||
Node::ParseErrorKind { .. } => {}
|
||||
Node::ArrayKind(_) => {}
|
||||
Node::VariableKind(_) => {}
|
||||
Node::EmptyArgKind => {}
|
||||
}
|
||||
}
|
||||
497
base/src/expressions/parser/test.rs
Normal file
497
base/src/expressions/parser/test.rs
Normal file
@@ -0,0 +1,497 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::expressions::lexer::LexerMode;
|
||||
use crate::expressions::parser::stringify::DisplaceData;
|
||||
|
||||
use super::super::types::CellReferenceRC;
|
||||
use super::Parser;
|
||||
use super::{
|
||||
super::parser::{
|
||||
stringify::{to_rc_format, to_string},
|
||||
Node,
|
||||
},
|
||||
stringify::to_string_displaced,
|
||||
};
|
||||
|
||||
struct Formula<'a> {
|
||||
initial: &'a str,
|
||||
expected: &'a str,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_reference() {
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
let t = parser.parse("A2", &Some(cell_reference));
|
||||
assert_eq!(to_rc_format(&t), "R[1]C[0]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_absolute_column() {
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
let t = parser.parse("$A1", &Some(cell_reference));
|
||||
assert_eq!(to_rc_format(&t), "R[0]C1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_absolute_row_col() {
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
let t = parser.parse("$C$5", &Some(cell_reference));
|
||||
assert_eq!(to_rc_format(&t), "R5C3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_absolute_row_col_1() {
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
let t = parser.parse("$A$1", &Some(cell_reference));
|
||||
assert_eq!(to_rc_format(&t), "R1C1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_simple_formula() {
|
||||
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
|
||||
let t = parser.parse("C3+Sheet2!D4", &Some(cell_reference));
|
||||
assert_eq!(to_rc_format(&t), "R[2]C[2]+Sheet2!R[3]C[3]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_boolean() {
|
||||
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
|
||||
let t = parser.parse("true", &Some(cell_reference));
|
||||
assert_eq!(to_rc_format(&t), "TRUE");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_bad_formula() {
|
||||
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
let t = parser.parse("#Value", &Some(cell_reference));
|
||||
match &t {
|
||||
Node::ParseErrorKind {
|
||||
formula,
|
||||
message,
|
||||
position,
|
||||
} => {
|
||||
assert_eq!(formula, "#Value");
|
||||
assert_eq!(message, "Invalid error.");
|
||||
assert_eq!(*position, 1);
|
||||
}
|
||||
_ => {
|
||||
panic!("Expected error in formula");
|
||||
}
|
||||
}
|
||||
assert_eq!(to_rc_format(&t), "#Value");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_bad_formula_1() {
|
||||
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
let t = parser.parse("<5", &Some(cell_reference));
|
||||
match &t {
|
||||
Node::ParseErrorKind {
|
||||
formula,
|
||||
message,
|
||||
position,
|
||||
} => {
|
||||
assert_eq!(formula, "<5");
|
||||
assert_eq!(message, "Unexpected token: 'COMPARE'");
|
||||
assert_eq!(*position, 0);
|
||||
}
|
||||
_ => {
|
||||
panic!("Expected error in formula");
|
||||
}
|
||||
}
|
||||
assert_eq!(to_rc_format(&t), "<5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_bad_formula_2() {
|
||||
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
let t = parser.parse("*5", &Some(cell_reference));
|
||||
match &t {
|
||||
Node::ParseErrorKind {
|
||||
formula,
|
||||
message,
|
||||
position,
|
||||
} => {
|
||||
assert_eq!(formula, "*5");
|
||||
assert_eq!(message, "Unexpected token: 'PRODUCT'");
|
||||
assert_eq!(*position, 0);
|
||||
}
|
||||
_ => {
|
||||
panic!("Expected error in formula");
|
||||
}
|
||||
}
|
||||
assert_eq!(to_rc_format(&t), "*5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_bad_formula_3() {
|
||||
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
let t = parser.parse("SUM(#VALVE!)", &Some(cell_reference));
|
||||
match &t {
|
||||
Node::ParseErrorKind {
|
||||
formula,
|
||||
message,
|
||||
position,
|
||||
} => {
|
||||
assert_eq!(formula, "SUM(#VALVE!)");
|
||||
assert_eq!(message, "Invalid error.");
|
||||
assert_eq!(*position, 5);
|
||||
}
|
||||
_ => {
|
||||
panic!("Expected error in formula");
|
||||
}
|
||||
}
|
||||
assert_eq!(to_rc_format(&t), "SUM(#VALVE!)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_formulas() {
|
||||
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
let formulas = vec![
|
||||
Formula {
|
||||
initial: "IF(C3:D4>2,B5,SUM(D1:D7))",
|
||||
expected: "IF(R[2]C[2]:R[3]C[3]>2,R[4]C[1],SUM(R[0]C[3]:R[6]C[3]))",
|
||||
},
|
||||
Formula {
|
||||
initial: "-A1",
|
||||
expected: "-R[0]C[0]",
|
||||
},
|
||||
Formula {
|
||||
initial: "#VALUE!",
|
||||
expected: "#VALUE!",
|
||||
},
|
||||
Formula {
|
||||
initial: "SUM(C3:D4)",
|
||||
expected: "SUM(R[2]C[2]:R[3]C[3])",
|
||||
},
|
||||
Formula {
|
||||
initial: "A1/(B1-C1)",
|
||||
expected: "R[0]C[0]/(R[0]C[1]-R[0]C[2])",
|
||||
},
|
||||
];
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
for formula in formulas {
|
||||
let t = parser.parse(
|
||||
formula.initial,
|
||||
&Some(CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
}),
|
||||
);
|
||||
assert_eq!(to_rc_format(&t), formula.expected);
|
||||
assert_eq!(to_string(&t, &cell_reference), formula.initial);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_r1c1_formulas() {
|
||||
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
parser.set_lexer_mode(LexerMode::R1C1);
|
||||
|
||||
let formulas = vec![
|
||||
Formula {
|
||||
initial: "IF(R[2]C[2]:R[3]C[3]>2,R[4]C[1],SUM(R[0]C[3]:R[6]C[3]))",
|
||||
expected: "IF(E5:F6>2,D7,SUM(F3:F9))",
|
||||
},
|
||||
Formula {
|
||||
initial: "-R[0]C[0]",
|
||||
expected: "-C3",
|
||||
},
|
||||
Formula {
|
||||
initial: "R[1]C[-1]+1",
|
||||
expected: "B4+1",
|
||||
},
|
||||
Formula {
|
||||
initial: "#VALUE!",
|
||||
expected: "#VALUE!",
|
||||
},
|
||||
Formula {
|
||||
initial: "SUM(R[2]C[2]:R[3]C[3])",
|
||||
expected: "SUM(E5:F6)",
|
||||
},
|
||||
Formula {
|
||||
initial: "R[-3]C[0]",
|
||||
expected: "#REF!",
|
||||
},
|
||||
Formula {
|
||||
initial: "R[0]C[-3]",
|
||||
expected: "#REF!",
|
||||
},
|
||||
Formula {
|
||||
initial: "R[-2]C[-2]",
|
||||
expected: "A1",
|
||||
},
|
||||
Formula {
|
||||
initial: "SIN(R[-3]C[-3])",
|
||||
expected: "SIN(#REF!)",
|
||||
},
|
||||
];
|
||||
|
||||
// Reference cell is Sheet1!C3
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 3,
|
||||
column: 3,
|
||||
};
|
||||
for formula in formulas {
|
||||
let t = parser.parse(
|
||||
formula.initial,
|
||||
&Some(CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
}),
|
||||
);
|
||||
assert_eq!(to_string(&t, &cell_reference), formula.expected);
|
||||
assert_eq!(to_rc_format(&t), formula.initial);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_quotes() {
|
||||
let worksheets = vec!["Sheet1".to_string(), "Second Sheet".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
|
||||
let t = parser.parse("C3+'Second Sheet'!D4", &Some(cell_reference));
|
||||
assert_eq!(to_rc_format(&t), "R[2]C[2]+'Second Sheet'!R[3]C[3]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_escape_quotes() {
|
||||
let worksheets = vec!["Sheet1".to_string(), "Second '2' Sheet".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
|
||||
let t = parser.parse("C3+'Second ''2'' Sheet'!D4", &Some(cell_reference));
|
||||
assert_eq!(to_rc_format(&t), "R[2]C[2]+'Second ''2'' Sheet'!R[3]C[3]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_parenthesis() {
|
||||
let worksheets = vec!["Sheet1".to_string(), "Second2".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
|
||||
let t = parser.parse("(C3=\"Yes\")*5", &Some(cell_reference));
|
||||
assert_eq!(to_rc_format(&t), "(R[2]C[2]=\"Yes\")*5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_excel_xlfn() {
|
||||
let worksheets = vec!["Sheet1".to_string(), "Second2".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
|
||||
let t = parser.parse("_xlfn.CONCAT(C3)", &Some(cell_reference));
|
||||
assert_eq!(to_rc_format(&t), "CONCAT(R[2]C[2])");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_string_displaced() {
|
||||
let context = &CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
let node = parser.parse("C3", &Some(context.clone()));
|
||||
let displace_data = DisplaceData::Column {
|
||||
sheet: 0,
|
||||
column: 1,
|
||||
delta: 4,
|
||||
};
|
||||
let t = to_string_displaced(&node, context, &displace_data);
|
||||
assert_eq!(t, "G3".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_string_displaced_full_ranges() {
|
||||
let context = &CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
let node = parser.parse("SUM(3:3)", &Some(context.clone()));
|
||||
let displace_data = DisplaceData::Column {
|
||||
sheet: 0,
|
||||
column: 1,
|
||||
delta: 4,
|
||||
};
|
||||
assert_eq!(
|
||||
to_string_displaced(&node, context, &displace_data),
|
||||
"SUM(3:3)".to_string()
|
||||
);
|
||||
|
||||
let node = parser.parse("SUM(D:D)", &Some(context.clone()));
|
||||
let displace_data = DisplaceData::Row {
|
||||
sheet: 0,
|
||||
row: 3,
|
||||
delta: 4,
|
||||
};
|
||||
assert_eq!(
|
||||
to_string_displaced(&node, context, &displace_data),
|
||||
"SUM(D:D)".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_string_displaced_too_low() {
|
||||
let context = &CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
let node = parser.parse("C3", &Some(context.clone()));
|
||||
let displace_data = DisplaceData::Column {
|
||||
sheet: 0,
|
||||
column: 1,
|
||||
delta: -40,
|
||||
};
|
||||
let t = to_string_displaced(&node, context, &displace_data);
|
||||
assert_eq!(t, "#REF!".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_string_displaced_too_high() {
|
||||
let context = &CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
let node = parser.parse("C3", &Some(context.clone()));
|
||||
let displace_data = DisplaceData::Column {
|
||||
sheet: 0,
|
||||
column: 1,
|
||||
delta: 4000000,
|
||||
};
|
||||
let t = to_string_displaced(&node, context, &displace_data);
|
||||
assert_eq!(t, "#REF!".to_string());
|
||||
}
|
||||
482
base/src/expressions/parser/test_move_formula.rs
Normal file
482
base/src/expressions/parser/test_move_formula.rs
Normal file
@@ -0,0 +1,482 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::expressions::parser::move_formula::{move_formula, MoveContext};
|
||||
use crate::expressions::types::Area;
|
||||
|
||||
use super::super::types::CellReferenceRC;
|
||||
use super::Parser;
|
||||
|
||||
#[test]
|
||||
fn test_move_formula() {
|
||||
// top left corner C2
|
||||
let row = 2;
|
||||
let column = 3;
|
||||
let context = &CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row,
|
||||
column,
|
||||
};
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Area is C2:F6
|
||||
let area = &Area {
|
||||
sheet: 0,
|
||||
row,
|
||||
column,
|
||||
width: 4,
|
||||
height: 5,
|
||||
};
|
||||
|
||||
// formula AB31 will not change
|
||||
let node = parser.parse("AB31", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "AB31");
|
||||
|
||||
// formula $AB$31 will not change
|
||||
let node = parser.parse("AB31", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "AB31");
|
||||
|
||||
// but formula D5 will change to N15 (N = D + 10)
|
||||
let node = parser.parse("D5", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "N15");
|
||||
|
||||
// Also formula $D$5 will change to N15 (N = D + 10)
|
||||
let node = parser.parse("$D$5", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "$N$15");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_formula_context_offset() {
|
||||
// context is E4
|
||||
let row = 4;
|
||||
let column = 5;
|
||||
let context = &CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row,
|
||||
column,
|
||||
};
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Area is C2:F6
|
||||
let area = &Area {
|
||||
sheet: 0,
|
||||
row: 2,
|
||||
column: 3,
|
||||
width: 4,
|
||||
height: 5,
|
||||
};
|
||||
|
||||
let node = parser.parse("-X9+C2%", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "-X9+M12%");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_formula_area_limits() {
|
||||
// context is E4
|
||||
let row = 4;
|
||||
let column = 5;
|
||||
let context = &CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row,
|
||||
column,
|
||||
};
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Area is C2:F6
|
||||
let area = &Area {
|
||||
sheet: 0,
|
||||
row: 2,
|
||||
column: 3,
|
||||
width: 4,
|
||||
height: 5,
|
||||
};
|
||||
|
||||
// Outside of the area. Not moved
|
||||
let node = parser.parse("B2+B3+C1+G6+H5", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "B2+B3+C1+G6+H5");
|
||||
|
||||
// In the area. Moved
|
||||
let node = parser.parse("C2+F4+F5+F6", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "M12+P14+P15+P16");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_formula_ranges() {
|
||||
// top left corner C2
|
||||
let row = 2;
|
||||
let column = 3;
|
||||
let context = &CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row,
|
||||
column,
|
||||
};
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
let area = &Area {
|
||||
sheet: 0,
|
||||
row,
|
||||
column,
|
||||
width: 4,
|
||||
height: 5,
|
||||
};
|
||||
// Ranges inside the area are fully displaced (absolute or not)
|
||||
let node = parser.parse("SUM(C2:F5)", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "SUM(M12:P15)");
|
||||
|
||||
let node = parser.parse("SUM($C$2:$F$5)", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "SUM($M$12:$P$15)");
|
||||
|
||||
// Ranges completely outside of the area are not touched
|
||||
let node = parser.parse("SUM(A1:B3)", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "SUM(A1:B3)");
|
||||
|
||||
let node = parser.parse("SUM($A$1:$B$3)", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "SUM($A$1:$B$3)");
|
||||
|
||||
// Ranges that overlap with the area are also NOT displaced
|
||||
let node = parser.parse("SUM(A1:F5)", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "SUM(A1:F5)");
|
||||
|
||||
// Ranges that contain the area are also NOT displaced
|
||||
let node = parser.parse("SUM(A1:X50)", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "SUM(A1:X50)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_formula_wrong_reference() {
|
||||
// context is E4
|
||||
let row = 4;
|
||||
let column = 5;
|
||||
let context = &CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row,
|
||||
column,
|
||||
};
|
||||
// Area is C2:G5
|
||||
let area = &Area {
|
||||
sheet: 0,
|
||||
row: 2,
|
||||
column: 3,
|
||||
width: 4,
|
||||
height: 5,
|
||||
};
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Wrong formulas will NOT be displaced
|
||||
let node = parser.parse("Sheet3!AB31", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "Sheet3!AB31");
|
||||
let node = parser.parse("Sheet3!$X$9", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "Sheet3!$X$9");
|
||||
|
||||
let node = parser.parse("SUM(Sheet3!D2:D3)", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "SUM(Sheet3!D2:D3)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_formula_misc() {
|
||||
// context is E4
|
||||
let row = 4;
|
||||
let column = 5;
|
||||
let context = &CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row,
|
||||
column,
|
||||
};
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Area is C2:F6
|
||||
let area = &Area {
|
||||
sheet: 0,
|
||||
row: 2,
|
||||
column: 3,
|
||||
width: 4,
|
||||
height: 5,
|
||||
};
|
||||
let node = parser.parse("X9^C2-F4*H2", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "X9^M12-P14*H2");
|
||||
|
||||
let node = parser.parse("F5*(-D5)*SUM(A1, X9, $D$5)", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "P15*(-N15)*SUM(A1,X9,$N$15)");
|
||||
|
||||
let node = parser.parse("IF(F5 < -D5, X9 & F5, FALSE)", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "IF(P15<-N15,X9&P15,FALSE)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_formula_another_sheet() {
|
||||
// top left corner C2
|
||||
let row = 2;
|
||||
let column = 3;
|
||||
let context = &CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row,
|
||||
column,
|
||||
};
|
||||
// we add two sheets and we cut/paste from Sheet1 to Sheet2
|
||||
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Area is C2:F6
|
||||
let area = &Area {
|
||||
sheet: 0,
|
||||
row,
|
||||
column,
|
||||
width: 4,
|
||||
height: 5,
|
||||
};
|
||||
|
||||
// Formula AB31 and JJ3:JJ4 refers to original Sheet1!AB31 and Sheet1!JJ3:JJ4
|
||||
let node = parser.parse(
|
||||
"AB31*SUM(JJ3:JJ4)+SUM(Sheet2!C2:F6)*SUM(C2:F6)",
|
||||
&Some(context.clone()),
|
||||
);
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet2",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
t,
|
||||
"Sheet1!AB31*SUM(Sheet1!JJ3:JJ4)+SUM(Sheet2!C2:F6)*SUM(M12:P16)"
|
||||
);
|
||||
}
|
||||
102
base/src/expressions/parser/test_ranges.rs
Normal file
102
base/src/expressions/parser/test_ranges.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::expressions::lexer::LexerMode;
|
||||
|
||||
use super::super::parser::stringify::{to_rc_format, to_string};
|
||||
use super::super::types::CellReferenceRC;
|
||||
use super::Parser;
|
||||
|
||||
struct Formula<'a> {
|
||||
formula_a1: &'a str,
|
||||
formula_r1c1: &'a str,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_formulas_with_full_ranges() {
|
||||
let worksheets = vec!["Sheet1".to_string(), "Second Sheet".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
let formulas = vec![
|
||||
Formula {
|
||||
formula_a1: "IF(C:D>2,B5,SUM(D:D))",
|
||||
formula_r1c1: "IF(R1C[2]:R1048576C[3]>2,R[4]C[1],SUM(R1C[3]:R1048576C[3]))",
|
||||
},
|
||||
Formula {
|
||||
formula_a1: "A:A",
|
||||
formula_r1c1: "R1C[0]:R1048576C[0]",
|
||||
},
|
||||
Formula {
|
||||
formula_a1: "SUM(3:3)",
|
||||
formula_r1c1: "SUM(R[2]C1:R[2]C16384)",
|
||||
},
|
||||
Formula {
|
||||
formula_a1: "SUM($3:$3)",
|
||||
formula_r1c1: "SUM(R3C1:R3C16384)",
|
||||
},
|
||||
Formula {
|
||||
formula_a1: "SUM(Sheet1!3:$3)",
|
||||
formula_r1c1: "SUM(Sheet1!R[2]C1:R3C16384)",
|
||||
},
|
||||
Formula {
|
||||
formula_a1: "SUM('Second Sheet'!C:D)",
|
||||
formula_r1c1: "SUM('Second Sheet'!R1C[2]:R1048576C[3])",
|
||||
},
|
||||
];
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
for formula in &formulas {
|
||||
let t = parser.parse(
|
||||
formula.formula_a1,
|
||||
&Some(CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
}),
|
||||
);
|
||||
assert_eq!(to_rc_format(&t), formula.formula_r1c1);
|
||||
assert_eq!(to_string(&t, &cell_reference), formula.formula_a1);
|
||||
}
|
||||
|
||||
// Now the inverse
|
||||
parser.set_lexer_mode(LexerMode::R1C1);
|
||||
for formula in &formulas {
|
||||
let t = parser.parse(
|
||||
formula.formula_r1c1,
|
||||
&Some(CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
}),
|
||||
);
|
||||
assert_eq!(to_rc_format(&t), formula.formula_r1c1);
|
||||
assert_eq!(to_string(&t, &cell_reference), formula.formula_a1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_inverse_order() {
|
||||
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
|
||||
// D4:C2 => C2:D4
|
||||
let t = parser.parse(
|
||||
"SUM(D4:C2)*SUM(Sheet2!D4:C20)*SUM($C$20:D4)",
|
||||
&Some(cell_reference.clone()),
|
||||
);
|
||||
assert_eq!(
|
||||
to_string(&t, &cell_reference),
|
||||
"SUM(C2:D4)*SUM(Sheet2!C4:D20)*SUM($C4:D$20)".to_string()
|
||||
);
|
||||
}
|
||||
100
base/src/expressions/parser/test_tables.rs
Normal file
100
base/src/expressions/parser/test_tables.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::expressions::parser::stringify::to_string;
|
||||
use crate::expressions::utils::{number_to_column, parse_reference_a1};
|
||||
use crate::types::{Table, TableColumn, TableStyleInfo};
|
||||
|
||||
use super::super::types::CellReferenceRC;
|
||||
use super::Parser;
|
||||
|
||||
fn create_test_table(
|
||||
table_name: &str,
|
||||
column_names: &[&str],
|
||||
cell_ref: &str,
|
||||
row_count: i32,
|
||||
) -> HashMap<String, Table> {
|
||||
let mut table = HashMap::new();
|
||||
let mut columns = Vec::new();
|
||||
for (id, name) in column_names.iter().enumerate() {
|
||||
columns.push(TableColumn {
|
||||
id: id as u32,
|
||||
name: name.to_string(),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
let init_cell = parse_reference_a1(cell_ref).unwrap();
|
||||
let start_row = init_cell.row;
|
||||
let start_column = number_to_column(init_cell.column).unwrap();
|
||||
let end_column = number_to_column(init_cell.column + column_names.len() as i32).unwrap();
|
||||
let end_row = start_row + row_count - 1;
|
||||
|
||||
let area_ref = format!("{start_column}{start_row}:{end_column}{end_row}");
|
||||
|
||||
table.insert(
|
||||
table_name.to_string(),
|
||||
Table {
|
||||
name: table_name.to_string(),
|
||||
display_name: table_name.to_string(),
|
||||
sheet_name: "Sheet One".to_string(),
|
||||
reference: area_ref,
|
||||
totals_row_count: 0,
|
||||
header_row_count: 1,
|
||||
header_row_dxf_id: None,
|
||||
data_dxf_id: None,
|
||||
columns,
|
||||
style_info: TableStyleInfo {
|
||||
..Default::default()
|
||||
},
|
||||
totals_row_dxf_id: None,
|
||||
has_filters: false,
|
||||
},
|
||||
);
|
||||
table
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_table() {
|
||||
let worksheets = vec!["Sheet One".to_string(), "Second Sheet".to_string()];
|
||||
|
||||
// This is a table A1:F3, the column F has a formula
|
||||
let column_names = ["Jan", "Feb", "Mar", "Apr", "Dec", "Year"];
|
||||
let row_count = 3;
|
||||
let tables = create_test_table("tblIncome", &column_names, "A1", row_count);
|
||||
|
||||
let mut parser = Parser::new(worksheets, tables);
|
||||
// Reference cell is 'Sheet One'!F2
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet One".to_string(),
|
||||
row: 2,
|
||||
column: 6,
|
||||
};
|
||||
|
||||
let formula = "SUM(tblIncome[[#This Row],[Jan]:[Dec]])";
|
||||
let t = parser.parse(formula, &Some(cell_reference.clone()));
|
||||
assert_eq!(to_string(&t, &cell_reference), "SUM($A$2:$E$2)");
|
||||
|
||||
// Cell A3
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet One".to_string(),
|
||||
row: 4,
|
||||
column: 1,
|
||||
};
|
||||
let formula = "SUBTOTAL(109, tblIncome[Jan])";
|
||||
let t = parser.parse(formula, &Some(cell_reference.clone()));
|
||||
assert_eq!(to_string(&t, &cell_reference), "SUBTOTAL(109,$A$2:$A$3)");
|
||||
|
||||
// Cell A3 in 'Second Sheet'
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Second Sheet".to_string(),
|
||||
row: 4,
|
||||
column: 1,
|
||||
};
|
||||
let formula = "SUBTOTAL(109, tblIncome[Jan])";
|
||||
let t = parser.parse(formula, &Some(cell_reference.clone()));
|
||||
assert_eq!(
|
||||
to_string(&t, &cell_reference),
|
||||
"SUBTOTAL(109,'Sheet One'!$A$2:$A$3)"
|
||||
);
|
||||
}
|
||||
276
base/src/expressions/parser/walk.rs
Normal file
276
base/src/expressions/parser/walk.rs
Normal file
@@ -0,0 +1,276 @@
|
||||
use super::{move_formula::ref_is_in_area, Node};
|
||||
|
||||
use crate::expressions::types::{Area, CellReferenceIndex};
|
||||
|
||||
pub(crate) fn forward_references(
|
||||
node: &mut Node,
|
||||
context: &CellReferenceIndex,
|
||||
source_area: &Area,
|
||||
target_sheet: u32,
|
||||
target_sheet_name: &str,
|
||||
target_row: i32,
|
||||
target_column: i32,
|
||||
) {
|
||||
match node {
|
||||
Node::ReferenceKind {
|
||||
sheet_name,
|
||||
sheet_index: reference_sheet,
|
||||
absolute_row,
|
||||
absolute_column,
|
||||
row: reference_row,
|
||||
column: reference_column,
|
||||
} => {
|
||||
let reference_row_absolute = if *absolute_row {
|
||||
*reference_row
|
||||
} else {
|
||||
*reference_row + context.row
|
||||
};
|
||||
let reference_column_absolute = if *absolute_column {
|
||||
*reference_column
|
||||
} else {
|
||||
*reference_column + context.column
|
||||
};
|
||||
if ref_is_in_area(
|
||||
*reference_sheet,
|
||||
reference_row_absolute,
|
||||
reference_column_absolute,
|
||||
source_area,
|
||||
) {
|
||||
if *reference_sheet != target_sheet {
|
||||
*sheet_name = Some(target_sheet_name.to_string());
|
||||
*reference_sheet = target_sheet;
|
||||
}
|
||||
*reference_row = target_row + *reference_row - source_area.row;
|
||||
*reference_column = target_column + *reference_column - source_area.column;
|
||||
}
|
||||
}
|
||||
Node::RangeKind {
|
||||
sheet_name,
|
||||
sheet_index,
|
||||
absolute_row1,
|
||||
absolute_column1,
|
||||
row1,
|
||||
column1,
|
||||
absolute_row2,
|
||||
absolute_column2,
|
||||
row2,
|
||||
column2,
|
||||
} => {
|
||||
let reference_row1 = if *absolute_row1 {
|
||||
*row1
|
||||
} else {
|
||||
*row1 + context.row
|
||||
};
|
||||
let reference_column1 = if *absolute_column1 {
|
||||
*column1
|
||||
} else {
|
||||
*column1 + context.column
|
||||
};
|
||||
|
||||
let reference_row2 = if *absolute_row2 {
|
||||
*row2
|
||||
} else {
|
||||
*row2 + context.row
|
||||
};
|
||||
let reference_column2 = if *absolute_column2 {
|
||||
*column2
|
||||
} else {
|
||||
*column2 + context.column
|
||||
};
|
||||
if ref_is_in_area(*sheet_index, reference_row1, reference_column1, source_area)
|
||||
&& ref_is_in_area(*sheet_index, reference_row2, reference_column2, source_area)
|
||||
{
|
||||
if *sheet_index != target_sheet {
|
||||
*sheet_index = target_sheet;
|
||||
*sheet_name = Some(target_sheet_name.to_string());
|
||||
}
|
||||
*row1 = target_row + *row1 - source_area.row;
|
||||
*column1 = target_column + *column1 - source_area.column;
|
||||
*row2 = target_row + *row2 - source_area.row;
|
||||
*column2 = target_column + *column2 - source_area.column;
|
||||
}
|
||||
}
|
||||
// Recurse
|
||||
Node::OpRangeKind { left, right } => {
|
||||
forward_references(
|
||||
left,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
forward_references(
|
||||
right,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
Node::OpConcatenateKind { left, right } => {
|
||||
forward_references(
|
||||
left,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
forward_references(
|
||||
right,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
Node::OpSumKind {
|
||||
kind: _,
|
||||
left,
|
||||
right,
|
||||
} => {
|
||||
forward_references(
|
||||
left,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
forward_references(
|
||||
right,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
Node::OpProductKind {
|
||||
kind: _,
|
||||
left,
|
||||
right,
|
||||
} => {
|
||||
forward_references(
|
||||
left,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
forward_references(
|
||||
right,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
Node::OpPowerKind { left, right } => {
|
||||
forward_references(
|
||||
left,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
forward_references(
|
||||
right,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
Node::FunctionKind { kind: _, args } => {
|
||||
for arg in args {
|
||||
forward_references(
|
||||
arg,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
}
|
||||
Node::InvalidFunctionKind { name: _, args } => {
|
||||
for arg in args {
|
||||
forward_references(
|
||||
arg,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
}
|
||||
Node::CompareKind {
|
||||
kind: _,
|
||||
left,
|
||||
right,
|
||||
} => {
|
||||
forward_references(
|
||||
left,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
forward_references(
|
||||
right,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
Node::UnaryKind { kind: _, right } => {
|
||||
forward_references(
|
||||
right,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
// TODO: Not implemented
|
||||
Node::ArrayKind(_) => {}
|
||||
// Do nothing. Note: we could do a blanket _ => {}
|
||||
Node::VariableKind(_) => {}
|
||||
Node::ErrorKind(_) => {}
|
||||
Node::ParseErrorKind { .. } => {}
|
||||
Node::EmptyArgKind => {}
|
||||
Node::BooleanKind(_) => {}
|
||||
Node::NumberKind(_) => {}
|
||||
Node::StringKind(_) => {}
|
||||
Node::WrongReferenceKind { .. } => {}
|
||||
Node::WrongRangeKind { .. } => {}
|
||||
}
|
||||
}
|
||||
21
base/src/expressions/test.rs
Normal file
21
base/src/expressions/test.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_error_codes() {
|
||||
let errors = vec![
|
||||
Error::REF,
|
||||
Error::NAME,
|
||||
Error::VALUE,
|
||||
Error::DIV,
|
||||
Error::NA,
|
||||
Error::NUM,
|
||||
Error::ERROR,
|
||||
];
|
||||
for (i, error) in errors.iter().enumerate() {
|
||||
let s = format!("{}", error);
|
||||
let index = error_index(s.clone()).unwrap();
|
||||
assert_eq!(i as i32, index);
|
||||
let s2 = error_string(i as usize).unwrap();
|
||||
assert_eq!(s, s2);
|
||||
}
|
||||
}
|
||||
388
base/src/expressions/token.rs
Normal file
388
base/src/expressions/token.rs
Normal file
@@ -0,0 +1,388 @@
|
||||
use std::fmt;
|
||||
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
|
||||
use crate::language::Language;
|
||||
|
||||
use super::{lexer::LexerError, types::ParsedReference};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum OpCompare {
|
||||
LessThan,
|
||||
GreaterThan,
|
||||
Equal,
|
||||
LessOrEqualThan,
|
||||
GreaterOrEqualThan,
|
||||
NonEqual,
|
||||
}
|
||||
|
||||
impl fmt::Display for OpCompare {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
OpCompare::LessThan => write!(fmt, "<"),
|
||||
OpCompare::GreaterThan => write!(fmt, ">"),
|
||||
OpCompare::Equal => write!(fmt, "="),
|
||||
OpCompare::LessOrEqualThan => write!(fmt, "<="),
|
||||
OpCompare::GreaterOrEqualThan => write!(fmt, ">="),
|
||||
OpCompare::NonEqual => write!(fmt, "<>"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum OpUnary {
|
||||
Minus,
|
||||
Percentage,
|
||||
}
|
||||
|
||||
impl fmt::Display for OpUnary {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
OpUnary::Minus => write!(fmt, "-"),
|
||||
OpUnary::Percentage => write!(fmt, "%"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum OpSum {
|
||||
Add,
|
||||
Minus,
|
||||
}
|
||||
|
||||
impl fmt::Display for OpSum {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
OpSum::Add => write!(fmt, "+"),
|
||||
OpSum::Minus => write!(fmt, "-"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum OpProduct {
|
||||
Times,
|
||||
Divide,
|
||||
}
|
||||
|
||||
impl fmt::Display for OpProduct {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
OpProduct::Times => write!(fmt, "*"),
|
||||
OpProduct::Divide => write!(fmt, "/"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// List of `errors`
|
||||
/// Note that "#ERROR!" and "#N/IMPL!" are not part of the xlsx standard
|
||||
/// * "#ERROR!" means there was an error processing the formula (for instance "=A1+")
|
||||
/// * "#N/IMPL!" means the formula or feature in Excel but has not been implemented in IronCalc
|
||||
/// Note that they are serialized/deserialized by index
|
||||
#[derive(Serialize_repr, Deserialize_repr, Debug, PartialEq, Eq, Clone)]
|
||||
#[repr(u8)]
|
||||
pub enum Error {
|
||||
REF,
|
||||
NAME,
|
||||
VALUE,
|
||||
DIV,
|
||||
NA,
|
||||
NUM,
|
||||
ERROR,
|
||||
NIMPL,
|
||||
SPILL,
|
||||
CALC,
|
||||
CIRC,
|
||||
NULL,
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Error::NULL => write!(fmt, "#NULL!"),
|
||||
Error::REF => write!(fmt, "#REF!"),
|
||||
Error::NAME => write!(fmt, "#NAME?"),
|
||||
Error::VALUE => write!(fmt, "#VALUE!"),
|
||||
Error::DIV => write!(fmt, "#DIV/0!"),
|
||||
Error::NA => write!(fmt, "#N/A"),
|
||||
Error::NUM => write!(fmt, "#NUM!"),
|
||||
Error::ERROR => write!(fmt, "#ERROR!"),
|
||||
Error::NIMPL => write!(fmt, "#N/IMPL"),
|
||||
Error::SPILL => write!(fmt, "#SPILL!"),
|
||||
Error::CALC => write!(fmt, "#CALC!"),
|
||||
Error::CIRC => write!(fmt, "#CIRC!"),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Error {
|
||||
pub fn to_localized_error_string(&self, language: &Language) -> String {
|
||||
match self {
|
||||
Error::NULL => language.errors.null.to_string(),
|
||||
Error::REF => language.errors.ref_value.to_string(),
|
||||
Error::NAME => language.errors.name.to_string(),
|
||||
Error::VALUE => language.errors.value.to_string(),
|
||||
Error::DIV => language.errors.div.to_string(),
|
||||
Error::NA => language.errors.na.to_string(),
|
||||
Error::NUM => language.errors.num.to_string(),
|
||||
Error::ERROR => language.errors.error.to_string(),
|
||||
Error::NIMPL => language.errors.nimpl.to_string(),
|
||||
Error::SPILL => language.errors.spill.to_string(),
|
||||
Error::CALC => language.errors.calc.to_string(),
|
||||
Error::CIRC => language.errors.circ.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_error_by_name(name: &str, language: &Language) -> Option<Error> {
|
||||
let errors = &language.errors;
|
||||
if name == errors.ref_value {
|
||||
return Some(Error::REF);
|
||||
} else if name == errors.name {
|
||||
return Some(Error::NAME);
|
||||
} else if name == errors.value {
|
||||
return Some(Error::VALUE);
|
||||
} else if name == errors.div {
|
||||
return Some(Error::DIV);
|
||||
} else if name == errors.na {
|
||||
return Some(Error::NA);
|
||||
} else if name == errors.num {
|
||||
return Some(Error::NUM);
|
||||
} else if name == errors.error {
|
||||
return Some(Error::ERROR);
|
||||
} else if name == errors.nimpl {
|
||||
return Some(Error::NIMPL);
|
||||
} else if name == errors.spill {
|
||||
return Some(Error::SPILL);
|
||||
} else if name == errors.calc {
|
||||
return Some(Error::CALC);
|
||||
} else if name == errors.circ {
|
||||
return Some(Error::CIRC);
|
||||
} else if name == errors.null {
|
||||
return Some(Error::NULL);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_error_by_english_name(name: &str) -> Option<Error> {
|
||||
if name == "#REF!" {
|
||||
return Some(Error::REF);
|
||||
} else if name == "#NAME?" {
|
||||
return Some(Error::NAME);
|
||||
} else if name == "#VALUE!" {
|
||||
return Some(Error::VALUE);
|
||||
} else if name == "#DIV/0!" {
|
||||
return Some(Error::DIV);
|
||||
} else if name == "#N/A" {
|
||||
return Some(Error::NA);
|
||||
} else if name == "#NUM!" {
|
||||
return Some(Error::NUM);
|
||||
} else if name == "#ERROR!" {
|
||||
return Some(Error::ERROR);
|
||||
} else if name == "#N/IMPL!" {
|
||||
return Some(Error::NIMPL);
|
||||
} else if name == "#SPILL!" {
|
||||
return Some(Error::SPILL);
|
||||
} else if name == "#CALC!" {
|
||||
return Some(Error::CALC);
|
||||
} else if name == "#CIRC!" {
|
||||
return Some(Error::CIRC);
|
||||
} else if name == "#NULL!" {
|
||||
return Some(Error::NULL);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn is_english_error_string(name: &str) -> bool {
|
||||
let names = [
|
||||
"#REF!", "#NAME?", "#VALUE!", "#DIV/0!", "#N/A", "#NUM!", "#ERROR!", "#N/IMPL!", "#SPILL!",
|
||||
"#CALC!", "#CIRC!", "#NULL!",
|
||||
];
|
||||
names.iter().any(|e| *e == name)
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum TableSpecifier {
|
||||
All,
|
||||
Data,
|
||||
Headers,
|
||||
ThisRow,
|
||||
Totals,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum TableReference {
|
||||
ColumnReference(String),
|
||||
RangeReference((String, String)),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum TokenType {
|
||||
Illegal(LexerError),
|
||||
EOF,
|
||||
Ident(String), // abc123
|
||||
String(String), // "A season"
|
||||
Number(f64), // 123.4
|
||||
Boolean(bool), // TRUE | FALSE
|
||||
Error(Error), // #VALUE!
|
||||
Compare(OpCompare), // <,>, ...
|
||||
Addition(OpSum), // +,-
|
||||
Product(OpProduct), // *,/
|
||||
Power, // ^
|
||||
LeftParenthesis, // (
|
||||
RightParenthesis, // )
|
||||
Colon, // :
|
||||
Semicolon, // ;
|
||||
LeftBracket, // [
|
||||
RightBracket, // ]
|
||||
LeftBrace, // {
|
||||
RightBrace, // }
|
||||
Comma, // ,
|
||||
Bang, // !
|
||||
Percent, // %
|
||||
And, // &
|
||||
Reference {
|
||||
sheet: Option<String>,
|
||||
row: i32,
|
||||
column: i32,
|
||||
absolute_column: bool,
|
||||
absolute_row: bool,
|
||||
},
|
||||
Range {
|
||||
sheet: Option<String>,
|
||||
left: ParsedReference,
|
||||
right: ParsedReference,
|
||||
},
|
||||
StructuredReference {
|
||||
table_name: String,
|
||||
specifier: Option<TableSpecifier>,
|
||||
table_reference: Option<TableReference>,
|
||||
},
|
||||
}
|
||||
|
||||
impl fmt::Display for TokenType {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
use self::TokenType::*;
|
||||
match self {
|
||||
Illegal(_) => write!(fmt, "Illegal"),
|
||||
EOF => write!(fmt, ""),
|
||||
Ident(value) => write!(fmt, "{}", value),
|
||||
String(value) => write!(fmt, "\"{}\"", value),
|
||||
Number(value) => write!(fmt, "{}", value),
|
||||
Boolean(value) => write!(fmt, "{}", value),
|
||||
Error(value) => write!(fmt, "{}", value),
|
||||
Compare(value) => write!(fmt, "{}", value),
|
||||
Addition(value) => write!(fmt, "{}", value),
|
||||
Product(value) => write!(fmt, "{}", value),
|
||||
Power => write!(fmt, "^"),
|
||||
LeftParenthesis => write!(fmt, "("),
|
||||
RightParenthesis => write!(fmt, ")"),
|
||||
Colon => write!(fmt, ":"),
|
||||
Semicolon => write!(fmt, ";"),
|
||||
LeftBracket => write!(fmt, "["),
|
||||
RightBracket => write!(fmt, "]"),
|
||||
LeftBrace => write!(fmt, "{{"),
|
||||
RightBrace => write!(fmt, "}}"),
|
||||
Comma => write!(fmt, ","),
|
||||
Bang => write!(fmt, "!"),
|
||||
Percent => write!(fmt, "%"),
|
||||
And => write!(fmt, "&"),
|
||||
Reference {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
absolute_column,
|
||||
absolute_row,
|
||||
} => {
|
||||
let row_data = if *absolute_row {
|
||||
format!("{}", row)
|
||||
} else {
|
||||
format!("${}", row)
|
||||
};
|
||||
let column_data = if *absolute_column {
|
||||
format!("{}", column)
|
||||
} else {
|
||||
format!("${}", column)
|
||||
};
|
||||
match sheet {
|
||||
Some(name) => write!(fmt, "{}!{}{}", name, column_data, row_data),
|
||||
None => write!(fmt, "{}{}", column, row),
|
||||
}
|
||||
}
|
||||
Range { sheet, left, right } => {
|
||||
let row_left_data = if left.absolute_row {
|
||||
format!("{}", left.row)
|
||||
} else {
|
||||
format!("${}", left.row)
|
||||
};
|
||||
let column_left_data = if left.absolute_column {
|
||||
format!("{}", left.column)
|
||||
} else {
|
||||
format!("${}", left.column)
|
||||
};
|
||||
|
||||
let row_right_data = if right.absolute_row {
|
||||
format!("{}", right.row)
|
||||
} else {
|
||||
format!("${}", right.row)
|
||||
};
|
||||
let column_right_data = if right.absolute_column {
|
||||
format!("{}", right.column)
|
||||
} else {
|
||||
format!("${}", right.column)
|
||||
};
|
||||
match sheet {
|
||||
Some(name) => write!(
|
||||
fmt,
|
||||
"{}!{}{}:{}{}",
|
||||
name, column_left_data, row_left_data, column_right_data, row_right_data
|
||||
),
|
||||
None => write!(
|
||||
fmt,
|
||||
"{}{}:{}{}",
|
||||
left.column, left.row, right.column, right.row
|
||||
),
|
||||
}
|
||||
}
|
||||
StructuredReference {
|
||||
table_name: _,
|
||||
specifier: _,
|
||||
table_reference: _,
|
||||
} => {
|
||||
// This should never happen
|
||||
write!(fmt, "-----ERROR-----")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn index(token: &TokenType) -> u32 {
|
||||
use self::TokenType::*;
|
||||
match token {
|
||||
Illegal(..) => 1,
|
||||
EOF => 2,
|
||||
Ident(..) => 3,
|
||||
String(..) => 4,
|
||||
Number(..) => 6,
|
||||
Boolean(..) => 7,
|
||||
Error(..) => 8,
|
||||
Addition(..) => 9,
|
||||
Product(..) => 10,
|
||||
Power => 14,
|
||||
LeftParenthesis => 15,
|
||||
RightParenthesis => 16,
|
||||
Colon => 17,
|
||||
Semicolon => 18,
|
||||
LeftBracket => 19,
|
||||
RightBracket => 20,
|
||||
LeftBrace => 21,
|
||||
RightBrace => 22,
|
||||
Comma => 23,
|
||||
Bang => 24,
|
||||
Percent => 30,
|
||||
And => 31,
|
||||
Reference { .. } => 34,
|
||||
Range { .. } => 35,
|
||||
Compare(..) => 37,
|
||||
StructuredReference { .. } => 40,
|
||||
}
|
||||
}
|
||||
51
base/src/expressions/types.rs
Normal file
51
base/src/expressions/types.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// $A$34
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct ParsedReference {
|
||||
pub column: i32,
|
||||
pub row: i32,
|
||||
pub absolute_column: bool,
|
||||
pub absolute_row: bool,
|
||||
}
|
||||
|
||||
/// If right is None it is just a reference
|
||||
/// Column ranges like D:D will have `absolute_row=true` and `left.row=1` and `right.row=LAST_ROW`
|
||||
/// Row ranges like 5:5 will have `absolute_column=true` and `left.column=1` and `right.column=LAST_COLUMN`
|
||||
pub struct ParsedRange {
|
||||
pub left: ParsedReference,
|
||||
pub right: Option<ParsedReference>,
|
||||
}
|
||||
|
||||
// FIXME: It does not make sense to have two different structures.
|
||||
// We should have a single one CellReferenceNamed or something like that.
|
||||
// Sheet1!C3
|
||||
pub struct CellReference {
|
||||
pub sheet: String,
|
||||
pub column: String,
|
||||
pub row: String,
|
||||
}
|
||||
|
||||
// Sheet1!C3 -> CellReferenceRC{Sheet1, 3, 3}
|
||||
#[derive(Clone)]
|
||||
pub struct CellReferenceRC {
|
||||
pub sheet: String,
|
||||
pub column: i32,
|
||||
pub row: i32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CellReferenceIndex {
|
||||
pub sheet: u32,
|
||||
pub column: i32,
|
||||
pub row: i32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Area {
|
||||
pub sheet: u32,
|
||||
pub row: i32,
|
||||
pub column: i32,
|
||||
pub width: i32,
|
||||
pub height: i32,
|
||||
}
|
||||
281
base/src/expressions/utils/mod.rs
Normal file
281
base/src/expressions/utils/mod.rs
Normal file
@@ -0,0 +1,281 @@
|
||||
use super::types::*;
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
/// Converts column letter identifier to number.
|
||||
pub fn column_to_number(column: &str) -> Result<i32, String> {
|
||||
if column.is_empty() {
|
||||
return Err("Column identifier cannot be empty.".to_string());
|
||||
}
|
||||
|
||||
if !column.is_ascii() {
|
||||
return Err("Column identifier must be ASCII.".to_string());
|
||||
}
|
||||
|
||||
let mut column_number = 0;
|
||||
for character in column.chars() {
|
||||
if !character.is_ascii_uppercase() {
|
||||
return Err("Column identifier can use only A-Z characters".to_string());
|
||||
}
|
||||
column_number = column_number * 26 + ((character as i32) - 64);
|
||||
}
|
||||
|
||||
match is_valid_column_number(column_number) {
|
||||
true => Ok(column_number),
|
||||
false => Err("Column is not valid.".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// If input number is outside valid range `None` is returned.
|
||||
pub fn number_to_column(mut i: i32) -> Option<String> {
|
||||
if !is_valid_column_number(i) {
|
||||
return None;
|
||||
}
|
||||
let mut column = "".to_string();
|
||||
while i > 0 {
|
||||
let r = ((i - 1) % 26) as u8;
|
||||
column.insert(0, (65 + r) as char);
|
||||
i = (i - 1) / 26;
|
||||
}
|
||||
Some(column)
|
||||
}
|
||||
|
||||
/// Checks if column number is in valid range.
|
||||
pub fn is_valid_column_number(column: i32) -> bool {
|
||||
(1..=LAST_COLUMN).contains(&column)
|
||||
}
|
||||
|
||||
pub fn is_valid_column(column: &str) -> bool {
|
||||
// last column XFD
|
||||
if column.len() > 3 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let column_number = column_to_number(column);
|
||||
|
||||
match column_number {
|
||||
Ok(column_number) => is_valid_column_number(column_number),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_valid_row(row: i32) -> bool {
|
||||
(1..=LAST_ROW).contains(&row)
|
||||
}
|
||||
|
||||
fn is_valid_row_str(row: &str) -> bool {
|
||||
match row.parse::<i32>() {
|
||||
Ok(r) => is_valid_row(r),
|
||||
Err(_r) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_reference_r1c1(r: &str) -> Option<ParsedReference> {
|
||||
let chars = r.as_bytes();
|
||||
let len = chars.len();
|
||||
let absolute_column;
|
||||
let absolute_row;
|
||||
let mut row = "".to_string();
|
||||
let mut column = "".to_string();
|
||||
if len < 4 {
|
||||
return None;
|
||||
}
|
||||
if chars[0] != b'R' {
|
||||
return None;
|
||||
}
|
||||
let mut i = 1;
|
||||
if chars[i] == b'[' {
|
||||
i += 1;
|
||||
absolute_row = false;
|
||||
if chars[i] == b'-' {
|
||||
i += 1;
|
||||
row.push('-');
|
||||
}
|
||||
} else {
|
||||
absolute_row = true;
|
||||
}
|
||||
while i < len {
|
||||
let ch = chars[i];
|
||||
if ch.is_ascii_digit() {
|
||||
row.push(ch as char);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
if !absolute_row {
|
||||
if i >= len || chars[i] != b']' {
|
||||
return None;
|
||||
};
|
||||
i += 1;
|
||||
}
|
||||
if i >= len || chars[i] != b'C' {
|
||||
return None;
|
||||
};
|
||||
i += 1;
|
||||
if i < len && chars[i] == b'[' {
|
||||
absolute_column = false;
|
||||
i += 1;
|
||||
if i < len && chars[i] == b'-' {
|
||||
i += 1;
|
||||
column.push('-');
|
||||
}
|
||||
} else {
|
||||
absolute_column = true;
|
||||
}
|
||||
while i < len {
|
||||
let ch = chars[i];
|
||||
if ch.is_ascii_digit() {
|
||||
column.push(ch as char);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
if !absolute_column {
|
||||
if i >= len || chars[i] != b']' {
|
||||
return None;
|
||||
};
|
||||
i += 1;
|
||||
}
|
||||
if i != len {
|
||||
return None;
|
||||
}
|
||||
Some(ParsedReference {
|
||||
row: row.parse::<i32>().unwrap_or(0),
|
||||
column: column.parse::<i32>().unwrap_or(0),
|
||||
absolute_column,
|
||||
absolute_row,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_reference_a1(r: &str) -> Option<ParsedReference> {
|
||||
let chars = r.chars();
|
||||
let mut absolute_column = false;
|
||||
let mut absolute_row = false;
|
||||
let mut row = "".to_string();
|
||||
let mut column = "".to_string();
|
||||
let mut state = 1; // 1(colum), 2(row)
|
||||
|
||||
for ch in chars {
|
||||
match ch {
|
||||
'A'..='Z' => {
|
||||
if state == 1 {
|
||||
column.push(ch);
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
'0'..='9' => {
|
||||
if state == 1 {
|
||||
state = 2
|
||||
}
|
||||
row.push(ch);
|
||||
}
|
||||
'$' => {
|
||||
if column == *"" {
|
||||
absolute_column = true;
|
||||
} else if state == 1 {
|
||||
absolute_row = true;
|
||||
state = 2;
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !is_valid_column(&column) {
|
||||
return None;
|
||||
}
|
||||
if !is_valid_row_str(&row) {
|
||||
return None;
|
||||
}
|
||||
let row = match row.parse::<i32>() {
|
||||
Ok(r) => r,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
Some(ParsedReference {
|
||||
row,
|
||||
column: column_to_number(&column).ok()?,
|
||||
absolute_column,
|
||||
absolute_row,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_valid_identifier(name: &str) -> bool {
|
||||
// https://support.microsoft.com/en-us/office/names-in-formulas-fc2935f9-115d-4bef-a370-3aa8bb4c91f1
|
||||
// https://github.com/MartinTrummer/excel-names/
|
||||
// NOTE: We are being much more restrictive than Excel.
|
||||
// In particular we do not support non ascii characters.
|
||||
let upper = name.to_ascii_uppercase();
|
||||
let bytes = upper.as_bytes();
|
||||
let len = bytes.len();
|
||||
if len > 255 || len == 0 {
|
||||
return false;
|
||||
}
|
||||
let first = bytes[0] as char;
|
||||
// The first character of a name must be a letter, an underscore character (_), or a backslash (\).
|
||||
if !(first.is_ascii_alphabetic() || first == '_' || first == '\\') {
|
||||
return false;
|
||||
}
|
||||
// You cannot use the uppercase and lowercase characters "C", "c", "R", or "r" as a defined name
|
||||
if len == 1 && (first == 'R' || first == 'C') {
|
||||
return false;
|
||||
}
|
||||
if upper == *"TRUE" || upper == *"FALSE" {
|
||||
return false;
|
||||
}
|
||||
if parse_reference_a1(name).is_some() {
|
||||
return false;
|
||||
}
|
||||
if parse_reference_r1c1(name).is_some() {
|
||||
return false;
|
||||
}
|
||||
let mut i = 1;
|
||||
while i < len {
|
||||
let ch = bytes[i] as char;
|
||||
match ch {
|
||||
'a'..='z' => {}
|
||||
'A'..='Z' => {}
|
||||
'0'..='9' => {}
|
||||
'_' => {}
|
||||
'.' => {}
|
||||
_ => {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn name_needs_quoting(name: &str) -> bool {
|
||||
let chars = name.chars();
|
||||
// it contains any of these characters: ()'$,;-+{} or space
|
||||
for char in chars {
|
||||
if [' ', '(', ')', '\'', '$', ',', ';', '-', '+', '{', '}'].contains(&char) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// TODO:
|
||||
// cell reference in A1 notation, e.g. B1048576 is quoted, B1048577 is not
|
||||
// cell reference in R1C1 notation, e.g. RC, RC2, R5C, R-4C, RC-8, R, C
|
||||
// integers
|
||||
false
|
||||
}
|
||||
|
||||
/// Quotes a string sheet name if it needs to
|
||||
/// NOTE: Invalid characters in a sheet name \, /, *, \[, \], :, ?
|
||||
pub fn quote_name(name: &str) -> String {
|
||||
if name_needs_quoting(name) {
|
||||
return format!("'{}'", name.replace('\'', "''"));
|
||||
};
|
||||
name.to_string()
|
||||
}
|
||||
214
base/src/expressions/utils/test.rs
Normal file
214
base/src/expressions/utils/test.rs
Normal file
@@ -0,0 +1,214 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_column_to_number() {
|
||||
assert_eq!(column_to_number("A"), Ok(1));
|
||||
assert_eq!(column_to_number("Z"), Ok(26));
|
||||
assert_eq!(column_to_number("AA"), Ok(27));
|
||||
assert_eq!(column_to_number("AB"), Ok(28));
|
||||
assert_eq!(column_to_number("XFD"), Ok(16_384));
|
||||
assert_eq!(column_to_number("XFD"), Ok(LAST_COLUMN));
|
||||
|
||||
assert_eq!(
|
||||
column_to_number("XFE"),
|
||||
Err("Column is not valid.".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
column_to_number(""),
|
||||
Err("Column identifier cannot be empty.".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
column_to_number("💥"),
|
||||
Err("Column identifier must be ASCII.".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
column_to_number("A1"),
|
||||
Err("Column identifier can use only A-Z characters".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
column_to_number("ab"),
|
||||
Err("Column identifier can use only A-Z characters".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_valid_column() {
|
||||
assert!(is_valid_column("A"));
|
||||
assert!(is_valid_column("AA"));
|
||||
assert!(is_valid_column("XFD"));
|
||||
|
||||
assert!(!is_valid_column("a"));
|
||||
assert!(!is_valid_column("aa"));
|
||||
assert!(!is_valid_column("xfd"));
|
||||
|
||||
assert!(!is_valid_column("1"));
|
||||
assert!(!is_valid_column("-1"));
|
||||
assert!(!is_valid_column("XFE"));
|
||||
assert!(!is_valid_column(""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_number_to_column() {
|
||||
assert_eq!(number_to_column(1), Some("A".to_string()));
|
||||
assert_eq!(number_to_column(26), Some("Z".to_string()));
|
||||
assert_eq!(number_to_column(27), Some("AA".to_string()));
|
||||
assert_eq!(number_to_column(28), Some("AB".to_string()));
|
||||
assert_eq!(number_to_column(16_384), Some("XFD".to_string()));
|
||||
|
||||
assert_eq!(number_to_column(0), None);
|
||||
assert_eq!(number_to_column(16_385), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_references() {
|
||||
assert_eq!(
|
||||
parse_reference_a1("A1"),
|
||||
Some(ParsedReference {
|
||||
row: 1,
|
||||
column: 1,
|
||||
absolute_column: false,
|
||||
absolute_row: false
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_references_1() {
|
||||
assert_eq!(
|
||||
parse_reference_a1("AB$23"),
|
||||
Some(ParsedReference {
|
||||
row: 23,
|
||||
column: 28,
|
||||
absolute_column: false,
|
||||
absolute_row: true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_references_2() {
|
||||
assert_eq!(
|
||||
parse_reference_a1("$AB123"),
|
||||
Some(ParsedReference {
|
||||
row: 123,
|
||||
column: 28,
|
||||
absolute_column: true,
|
||||
absolute_row: false
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_references_3() {
|
||||
assert_eq!(
|
||||
parse_reference_a1("$AB$123"),
|
||||
Some(ParsedReference {
|
||||
row: 123,
|
||||
column: 28,
|
||||
absolute_column: true,
|
||||
absolute_row: true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_r1c1_references() {
|
||||
assert_eq!(
|
||||
parse_reference_r1c1("R1C1"),
|
||||
Some(ParsedReference {
|
||||
row: 1,
|
||||
column: 1,
|
||||
absolute_column: true,
|
||||
absolute_row: true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_r1c1_references_1() {
|
||||
assert_eq!(
|
||||
parse_reference_r1c1("R32C[-3]"),
|
||||
Some(ParsedReference {
|
||||
row: 32,
|
||||
column: -3,
|
||||
absolute_column: false,
|
||||
absolute_row: true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_r1c1_references_2() {
|
||||
assert_eq!(
|
||||
parse_reference_r1c1("R32C"),
|
||||
Some(ParsedReference {
|
||||
row: 32,
|
||||
column: 0,
|
||||
absolute_column: true,
|
||||
absolute_row: true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_r1c1_references_3() {
|
||||
assert_eq!(
|
||||
parse_reference_r1c1("R[-2]C[-3]"),
|
||||
Some(ParsedReference {
|
||||
row: -2,
|
||||
column: -3,
|
||||
absolute_column: false,
|
||||
absolute_row: false
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_r1c1_references_4() {
|
||||
assert_eq!(
|
||||
parse_reference_r1c1("RC[-3]"),
|
||||
Some(ParsedReference {
|
||||
row: 0,
|
||||
column: -3,
|
||||
absolute_column: false,
|
||||
absolute_row: true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_names() {
|
||||
assert!(is_valid_identifier("hola1"));
|
||||
assert!(is_valid_identifier("hola_1"));
|
||||
assert!(is_valid_identifier("hola.1"));
|
||||
assert!(is_valid_identifier("sum_total_"));
|
||||
assert!(is_valid_identifier("sum.total"));
|
||||
assert!(is_valid_identifier("_hola"));
|
||||
assert!(is_valid_identifier("t"));
|
||||
assert!(is_valid_identifier("q"));
|
||||
assert!(is_valid_identifier("true_that"));
|
||||
assert!(is_valid_identifier("true1"));
|
||||
|
||||
// weird names apparently valid in Excel
|
||||
assert!(is_valid_identifier("_"));
|
||||
assert!(is_valid_identifier("\\hola1"));
|
||||
assert!(is_valid_identifier("__"));
|
||||
assert!(is_valid_identifier("_."));
|
||||
assert!(is_valid_identifier("_1"));
|
||||
assert!(is_valid_identifier("\\."));
|
||||
|
||||
// invalid
|
||||
assert!(!is_valid_identifier("true"));
|
||||
assert!(!is_valid_identifier("false"));
|
||||
assert!(!is_valid_identifier("SUM THAT"));
|
||||
assert!(!is_valid_identifier("A23"));
|
||||
assert!(!is_valid_identifier("R1C1"));
|
||||
assert!(!is_valid_identifier("R23C"));
|
||||
assert!(!is_valid_identifier("R"));
|
||||
assert!(!is_valid_identifier("c"));
|
||||
assert!(!is_valid_identifier("1true"));
|
||||
|
||||
assert!(!is_valid_identifier("test€"));
|
||||
assert!(!is_valid_identifier("truñe"));
|
||||
assert!(!is_valid_identifier("tr&ue"));
|
||||
}
|
||||
Reference in New Issue
Block a user