UPDATE: Dump of initial files
This commit is contained in:
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