ironcalc_base 0.7.1

Open source spreadsheet engine
Documentation
use crate::expressions::token::get_error_by_name;
use crate::expressions::types::CellReferenceIndex;
use crate::language::Language;

use crate::{
    expressions::{
        lexer::{Lexer, LexerMode},
        token::TokenType,
    },
    language::get_language,
    locale::Locale,
};

#[derive(Debug, Eq, PartialEq)]
pub enum ParsedReference {
    CellReference(CellReferenceIndex),
    Range(CellReferenceIndex, CellReferenceIndex),
}

impl ParsedReference {
    /// Parses reference in formula format. For example:  `Sheet1!A1`, `Sheet1!$A$1:$B$9`.
    /// Absolute references (`$`) do not affect parsing.
    ///
    /// # Arguments
    ///
    /// * `sheet_index_context` - if available, sheet index can be provided so references
    ///   without explicit sheet name can be recognized
    /// * `reference` - text string to parse as reference
    /// * `locale` - locale that will be used to set-up parser
    /// * `get_sheet_index_by_name` - function that allows to translate sheet name to index
    pub(crate) fn parse_reference_formula<F: Fn(&str) -> Option<u32>>(
        sheet_index_context: Option<u32>,
        reference: &str,
        locale: &Locale,
        get_sheet_index_by_name: F,
    ) -> Result<ParsedReference, String> {
        #[allow(clippy::expect_used)]
        let language = get_language("en").expect("");
        let mut lexer = Lexer::new(reference, LexerMode::A1, locale, language);

        let reference_token = lexer.next_token();
        let eof_token = lexer.next_token();

        if TokenType::EOF != eof_token {
            return Err("Invalid reference. Expected only one token.".to_string());
        }

        match reference_token {
            TokenType::Reference {
                sheet: sheet_name,
                column: column_id,
                row: row_id,
                ..
            } => {
                let sheet_index;
                if let Some(name) = sheet_name {
                    match get_sheet_index_by_name(&name) {
                        Some(i) => sheet_index = i,
                        None => {
                            return Err(format!(
                                "Invalid reference. Sheet \"{}\" could not be found.",
                                name.as_str(),
                            ));
                        }
                    }
                } else if let Some(sheet_index_context) = sheet_index_context {
                    sheet_index = sheet_index_context;
                } else {
                    return Err(
                        "Reference doesn't contain sheet name and relative cell is not known."
                            .to_string(),
                    );
                }

                Ok(ParsedReference::CellReference(CellReferenceIndex {
                    sheet: sheet_index,
                    row: row_id,
                    column: column_id,
                }))
            }
            TokenType::Range {
                sheet: sheet_name,
                left,
                right,
            } => {
                let sheet_index;
                if let Some(name) = sheet_name {
                    match get_sheet_index_by_name(&name) {
                        Some(i) => sheet_index = i,
                        None => {
                            return Err(format!(
                                "Invalid reference. Sheet \"{}\" could not be found.",
                                name.as_str(),
                            ));
                        }
                    }
                } else if let Some(sheet_index_context) = sheet_index_context {
                    sheet_index = sheet_index_context;
                } else {
                    return Err(
                        "Reference doesn't contain sheet name and relative cell is not known."
                            .to_string(),
                    );
                }

                Ok(ParsedReference::Range(
                    CellReferenceIndex {
                        sheet: sheet_index,
                        row: left.row,
                        column: left.column,
                    },
                    CellReferenceIndex {
                        sheet: sheet_index,
                        row: right.row,
                        column: right.column,
                    },
                ))
            }
            _ => Err("Invalid reference. First token is not a reference.".to_string()),
        }
    }
}

/// Returns true if the string value could be interpreted as:
///  * a formula
///  * a number
///  * a boolean
///  * an error (i.e "#VALUE!")
pub(crate) fn value_needs_quoting(value: &str, language: &Language) -> bool {
    value.starts_with('=')
        || value.parse::<f64>().is_ok()
        || value.to_lowercase().parse::<bool>().is_ok()
        || get_error_by_name(&value.to_uppercase(), language).is_some()
}

/// Gets all timezones
pub fn get_all_timezones() -> Vec<String> {
    chrono_tz::TZ_VARIANTS
        .iter()
        .map(|tz| tz.name().to_string())
        .collect()
}

/// Valid hex colors are #FFAABB
/// #fff is not valid
pub(crate) fn is_valid_hex_color(color: &str) -> bool {
    if color.chars().count() != 7 {
        return false;
    }
    if !color.starts_with('#') {
        return false;
    }
    if let Ok(z) = i32::from_str_radix(&color[1..], 16) {
        if (0..=0xffffff).contains(&z) {
            return true;
        }
    }
    false
}

#[cfg(test)]
mod tests {
    #![allow(clippy::expect_used)]

    use super::*;
    use crate::language::get_language;
    use crate::locale::{get_locale, Locale};

    fn get_test_locale() -> &'static Locale {
        #![allow(clippy::unwrap_used)]
        get_locale("en").unwrap()
    }

    fn get_sheet_index_by_name(sheet_names: &[&str], name: &str) -> Option<u32> {
        sheet_names
            .iter()
            .position(|&sheet_name| sheet_name == name)
            .map(|index| index as u32)
    }

    #[test]
    fn test_parse_cell_references() {
        let locale = get_test_locale();
        let sheet_names = vec!["Sheet1", "Sheet2", "Sheet3"];

        assert_eq!(
            ParsedReference::parse_reference_formula(Some(7), "A1", locale, |name| {
                get_sheet_index_by_name(&sheet_names, name)
            },),
            Ok(ParsedReference::CellReference(CellReferenceIndex {
                sheet: 7,
                row: 1,
                column: 1,
            })),
        );

        assert_eq!(
            ParsedReference::parse_reference_formula(None, "Sheet1!A1", locale, |name| {
                get_sheet_index_by_name(&sheet_names, name)
            },),
            Ok(ParsedReference::CellReference(CellReferenceIndex {
                sheet: 0,
                row: 1,
                column: 1,
            })),
        );

        assert_eq!(
            ParsedReference::parse_reference_formula(None, "Sheet1!$A$1", locale, |name| {
                get_sheet_index_by_name(&sheet_names, name)
            },),
            Ok(ParsedReference::CellReference(CellReferenceIndex {
                sheet: 0,
                row: 1,
                column: 1,
            })),
        );

        assert_eq!(
            ParsedReference::parse_reference_formula(None, "Sheet2!$A$1", locale, |name| {
                get_sheet_index_by_name(&sheet_names, name)
            },),
            Ok(ParsedReference::CellReference(CellReferenceIndex {
                sheet: 1,
                row: 1,
                column: 1,
            })),
        );
    }

    #[test]
    fn test_parse_range_references() {
        let locale = get_test_locale();
        let sheet_names = vec!["Sheet1", "Sheet2", "Sheet3"];

        assert_eq!(
            ParsedReference::parse_reference_formula(Some(5), "A1:A2", locale, |name| {
                get_sheet_index_by_name(&sheet_names, name)
            },),
            Ok(ParsedReference::Range(
                CellReferenceIndex {
                    sheet: 5,
                    column: 1,
                    row: 1,
                },
                CellReferenceIndex {
                    sheet: 5,
                    column: 1,
                    row: 2,
                },
            )),
        );

        assert_eq!(
            ParsedReference::parse_reference_formula(None, "Sheet1!$A$1:$B$10", locale, |name| {
                get_sheet_index_by_name(&sheet_names, name)
            },),
            Ok(ParsedReference::Range(
                CellReferenceIndex {
                    sheet: 0,
                    row: 1,
                    column: 1,
                },
                CellReferenceIndex {
                    sheet: 0,
                    row: 10,
                    column: 2,
                },
            )),
        );

        assert_eq!(
            ParsedReference::parse_reference_formula(None, "Sheet2!AA1:E$11", locale, |name| {
                get_sheet_index_by_name(&sheet_names, name)
            },),
            Ok(ParsedReference::Range(
                CellReferenceIndex {
                    sheet: 1,
                    row: 1,
                    column: 27,
                },
                CellReferenceIndex {
                    sheet: 1,
                    row: 11,
                    column: 5,
                },
            )),
        );
    }

    #[test]
    fn test_error_reject_assignments() {
        let locale = get_test_locale();
        let sheet_index = Some(1);
        assert_eq!(
            ParsedReference::parse_reference_formula(sheet_index, "=A1", locale, |_| Some(1)),
            Err("Invalid reference. Expected only one token.".to_string()),
        );
        assert_eq!(
            ParsedReference::parse_reference_formula(sheet_index, "=$A$1", locale, |_| { Some(1) }),
            Err("Invalid reference. Expected only one token.".to_string()),
        );
        assert_eq!(
            ParsedReference::parse_reference_formula(None, "=Sheet1!A1", locale, |_| Some(1)),
            Err("Invalid reference. Expected only one token.".to_string()),
        );
    }

    #[test]
    fn test_error_reject_formulas_without_equal_sign() {
        let locale = get_test_locale();
        assert_eq!(
            ParsedReference::parse_reference_formula(None, "SUM", locale, |_| Some(1)),
            Err("Invalid reference. First token is not a reference.".to_string()),
        );
        assert_eq!(
            ParsedReference::parse_reference_formula(None, "SUM(A1:A2)", locale, |_| Some(1)),
            Err("Invalid reference. Expected only one token.".to_string()),
        );
    }

    #[test]
    fn test_error_reject_without_sheet_and_relative_cell() {
        let locale = get_test_locale();
        assert_eq!(
            ParsedReference::parse_reference_formula(None, "A1", locale, |_| Some(1)),
            Err("Reference doesn't contain sheet name and relative cell is not known.".to_string()),
        );
        assert_eq!(
            ParsedReference::parse_reference_formula(None, "A1:A2", locale, |_| Some(1)),
            Err("Reference doesn't contain sheet name and relative cell is not known.".to_string()),
        );
    }

    #[test]
    fn test_error_unrecognized_sheet_name() {
        let locale = get_test_locale();
        assert_eq!(
            ParsedReference::parse_reference_formula(None, "SheetName!A1", locale, |_| None),
            Err("Invalid reference. Sheet \"SheetName\" could not be found.".to_string()),
        );
        assert_eq!(
            ParsedReference::parse_reference_formula(None, "SheetName2!A1:A4", locale, |_| None),
            Err("Invalid reference. Sheet \"SheetName2\" could not be found.".to_string()),
        );
    }

    #[test]
    fn test_value_needs_quoting() {
        let en_language = get_language("en").expect("en language expected");

        assert!(!value_needs_quoting("", en_language));
        assert!(!value_needs_quoting("hello", en_language));

        assert!(value_needs_quoting("12", en_language));
        assert!(value_needs_quoting("true", en_language));
        assert!(value_needs_quoting("False", en_language));

        assert!(value_needs_quoting("=A1", en_language));

        assert!(value_needs_quoting("#REF!", en_language));
        assert!(value_needs_quoting("#NAME?", en_language));
    }

    #[test]
    fn test_is_valid_hex_color() {
        assert!(is_valid_hex_color("#000000"));
        assert!(is_valid_hex_color("#ffffff"));

        assert!(!is_valid_hex_color("000000"));
        assert!(!is_valid_hex_color("ffffff"));

        assert!(!is_valid_hex_color("#gggggg"));

        // Not obvious cases unrecognized as colors
        assert!(!is_valid_hex_color("#ffffff "));
        assert!(!is_valid_hex_color("#fff")); // CSS shorthand
        assert!(!is_valid_hex_color("#ffffff00")); // with alpha channel
    }
}