Skip to content

Commit

Permalink
chart: replace regexes
Browse files Browse the repository at this point in the history
Feature #108
  • Loading branch information
jmcnamara committed Aug 27, 2024
1 parent 8caaeea commit 2185a21
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 34 deletions.
80 changes: 56 additions & 24 deletions src/chart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -759,8 +759,8 @@ use crate::drawing::{DrawingObject, DrawingType};
use crate::utility::{self, ToXmlBoolean};

use crate::{
static_regex, xmlwriter::XMLWriter, ColNum, Color, IntoExcelDateTime, ObjectMovement, RowNum,
XlsxError, COL_MAX, ROW_MAX,
xmlwriter::XMLWriter, ColNum, Color, IntoExcelDateTime, ObjectMovement, RowNum, XlsxError,
COL_MAX, ROW_MAX,
};

#[derive(Clone)]
Expand Down Expand Up @@ -8347,37 +8347,69 @@ impl ChartRange {
/// ```
///
pub fn new_from_string(range_string: &str) -> ChartRange {
let chart_cell = static_regex!(r"^=?([^!]+)'?!\$?(\w+)\$?(\d+)");
let chart_range = static_regex!(r"^=?([^!]+)'?!\$?(\w+)\$?(\d+):\$?(\w+)\$?(\d+)");

// Default values. If the string parsing fails these values will remain
// the same and it will flag an invalid result.
let mut sheet_name = "";
let mut first_row = 0;
let mut first_col = 0;
let mut last_row = 0;
let mut last_col = 0;

if let Some(caps) = chart_range.captures(range_string) {
sheet_name = caps.get(1).unwrap().as_str();
first_row = caps.get(3).unwrap().as_str().parse::<u32>().unwrap() - 1;
last_row = caps.get(5).unwrap().as_str().parse::<u32>().unwrap() - 1;
first_col = utility::column_name_to_number(caps.get(2).unwrap().as_str());
last_col = utility::column_name_to_number(caps.get(4).unwrap().as_str());
} else if let Some(caps) = chart_cell.captures(range_string) {
sheet_name = caps.get(1).unwrap().as_str();
first_row = caps.get(3).unwrap().as_str().parse::<u32>().unwrap() - 1;
first_col = utility::column_name_to_number(caps.get(2).unwrap().as_str());
last_row = first_row;
last_col = first_col;
}

let sheet_name: String = if sheet_name.starts_with('\'') && sheet_name.ends_with('\'') {
sheet_name[1..sheet_name.len() - 1].to_string()
} else {
sheet_name.to_string()
};
// Parse the chart range string into the worksheet name and range parts.
if let Some(position) = range_string.find('!') {
let range = &range_string[position + 1..].replace('$', "");

if utility::is_valid_range(range) {
sheet_name = &range_string[..position];
match range.find(':') {
// Multi-cell range like A1:A5.
Some(position) => {
let first_cell = &range[..position];
let last_cell = &range[position + 1..];

let (first_col_string, first_row_string) =
utility::split_cell_reference(first_cell);
let (last_col_string, last_row_string) =
utility::split_cell_reference(last_cell);

first_row = first_row_string.parse::<u32>().unwrap_or_default();
first_row = first_row.saturating_sub(1);

last_row = last_row_string.parse::<u32>().unwrap_or_default();
last_row = last_row.saturating_sub(1);

first_col = utility::column_name_to_number(&first_col_string);
last_col = utility::column_name_to_number(&last_col_string);
}
None => {
// Single-cell range like A1.
let (first_col_string, first_row_string) =
utility::split_cell_reference(range);

first_row = first_row_string.parse::<u32>().unwrap_or_default();
first_row = first_row.saturating_sub(1);

first_col = utility::column_name_to_number(&first_col_string);
last_row = first_row;
last_col = first_col;
}
}
}
}

// Clean up the sheet name.
if sheet_name.starts_with('=') {
sheet_name = &sheet_name[1..];
}

// Strip the quotes from quoted sheet names.
if sheet_name.starts_with('\'') && sheet_name.ends_with('\'') {
sheet_name = &sheet_name[1..sheet_name.len() - 1];
}

ChartRange {
sheet_name,
sheet_name: sheet_name.to_string(),
first_row,
first_col,
last_row,
Expand Down
54 changes: 44 additions & 10 deletions src/utility.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,11 @@ pub fn column_number_to_name(col_num: ColNum) -> String {
/// ```
///
pub fn column_name_to_number(column: &str) -> ColNum {
let mut col_num = 0;
if column.is_empty() {
return 0;
}

let mut col_num = 0;
for char in column.chars() {
col_num = (col_num * 26) + (char as u16 - 'A' as u16 + 1);
}
Expand Down Expand Up @@ -441,15 +444,9 @@ pub(crate) fn quote_sheetname(sheetname: &str) -> String {
let col_max = u64::from(COL_MAX);
let row_max = u64::from(ROW_MAX);

// When possible split the sheetname into a leading string and a trailing
// number. We use these to look for A1 and R1C1 style cell references.
let (string_part, number_part) = match sheetname.find(|c: char| c.is_ascii_digit()) {
Some(position) => (
(sheetname[..position]).to_uppercase(),
(sheetname[position..]).to_uppercase(),
),
None => (String::new(), "0".to_string()),
};
// Split sheetnames that look like A1 and R1C1 style cell references into a
// leading string and a trailing number.
let (string_part, number_part) = split_cell_reference(&sheetname);

// The number part of the sheet name can have trailing non-digit characters
// and still be a valid R1C1 match. However, to test the R1C1 row/col part
Expand Down Expand Up @@ -551,6 +548,43 @@ pub(crate) fn unquote_sheetname(sheetname: &str) -> String {
}
}

// Split sheetnames that look like A1 and R1C1 style cell references into a
// leading string and a trailing number.
pub(crate) fn split_cell_reference(sheetname: &str) -> (String, String) {
match sheetname.find(|c: char| c.is_ascii_digit()) {
Some(position) => (
(sheetname[..position]).to_uppercase(),
(sheetname[position..]).to_uppercase(),
),
None => (String::new(), String::new()),
}
}

// Check that a range string like "A1" or "A1:B3" are valid. This function
// assumes that the '$' absolute anchor has already been stripped.
pub(crate) fn is_valid_range(range: &str) -> bool {
if range.is_empty() {
return false;
}

// The range should start with a letter and end in a number.
if !range.starts_with(|c: char| c.is_ascii_uppercase())
|| !range.ends_with(|c: char| c.is_ascii_digit())
{
return false;
}

// The range should only include the characters 'A-Z', '0-9' and ':'
if !range
.chars()
.all(|c: char| c.is_ascii_uppercase() || c.is_ascii_digit() || c == ':')
{
return false;
}

true
}

/// Check that a worksheet name is valid in Excel.
///
/// This function checks if an worksheet name is valid according to the Excel
Expand Down
11 changes: 11 additions & 0 deletions src/utility/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -512,4 +512,15 @@ mod utility_tests {
let result = utility::validate_vba_name(name);
assert!(matches!(result, Err(XlsxError::VbaNameError(_))));
}

#[test]
fn check_is_valid_range() {
assert_eq!(true, utility::is_valid_range("A1"));
assert_eq!(true, utility::is_valid_range("A1:B3"));

assert_eq!(false, utility::is_valid_range(""));
assert_eq!(false, utility::is_valid_range("1A"));
assert_eq!(false, utility::is_valid_range("a1"));
assert_eq!(false, utility::is_valid_range("1:3"));
}
}

0 comments on commit 2185a21

Please sign in to comment.