logisheets_controller 1.16.1

Core spreadsheet engine for LogiSheets: formula evaluation, dependency tracking, and undo/redo.
Documentation
use super::calc_vertex::Value;
use logisheets_parser::ast;
use std::cmp::Ordering;

/// Compare two strings the way a spreadsheet does: **without regard to case**.
///
/// `="a"="A"` is TRUE in Excel, and the ordering follows the same rule, so "a"
/// sorts before "B" rather than after it the way raw bytes would have it.
/// `EXACT` is the case-sensitive comparison, and the only one.
///
/// This decides more than the `=` operator: every `*IF` criterion, `VLOOKUP`
/// and `MATCH` route through here, and while they were case-sensitive a
/// formula's answer depended on which of them the comparison happened to go
/// through — `MATCH` matched "A" against "a" and `COUNTIF` did not.
///
/// Folded character by character rather than through `to_lowercase()` so a
/// comparison inside a `COUNTIF` over a long range allocates nothing.
pub fn cmp_text(lhs: &str, rhs: &str) -> Ordering {
    lhs.chars()
        .flat_map(char::to_lowercase)
        .cmp(rhs.chars().flat_map(char::to_lowercase))
}

pub fn compare(lhs: &Value, rhs: &Value) -> CompareResult {
    match (lhs, rhs) {
        (Value::Blank, Value::Blank) => CompareResult::Equal,
        // A blank compares as zero, so this is `0` against `num` and nothing
        // else. The 1e-10 that used to be here made every number smaller than
        // that equal to blank — `5e-11 = ""` came back TRUE — and the negative
        // branch's `< 1e-10` was redundant besides, since every negative is.
        (Value::Blank, Value::Number(num)) => {
            if *num > 0_f64 {
                CompareResult::Less
            } else if *num < 0_f64 {
                CompareResult::Greater
            } else {
                CompareResult::Equal
            }
        }
        // A blank cell reads as `""` against text, exactly as it reads as 0
        // against a number two arms up: `=A1=""` on an empty A1 is TRUE. It is
        // still less than any text that has something in it.
        (Value::Blank, Value::Text(t)) => {
            if t.is_empty() {
                CompareResult::Equal
            } else {
                CompareResult::Less
            }
        }
        (Value::Blank, Value::Boolean(_)) => CompareResult::Less,
        (Value::Blank, Value::Error(e)) => CompareResult::Error(e.clone()),
        (Value::Number(num), Value::Blank) => {
            if *num > 0_f64 {
                CompareResult::Greater
            } else if *num < 0_f64 {
                CompareResult::Less
            } else {
                CompareResult::Equal
            }
        }
        (Value::Number(num), Value::Number(rhs_num)) => {
            if *num > *rhs_num {
                CompareResult::Greater
            } else if *num < *rhs_num {
                CompareResult::Less
            } else {
                CompareResult::Equal
            }
        }
        (Value::Number(_), Value::Text(_)) => CompareResult::Less,
        (Value::Number(_), Value::Boolean(_)) => CompareResult::Less,
        (Value::Number(_), Value::Error(e)) => CompareResult::Error(e.clone()),
        (Value::Text(t), Value::Blank) => {
            if t.is_empty() {
                CompareResult::Equal
            } else {
                CompareResult::Greater
            }
        }
        (Value::Text(_), Value::Number(_)) => CompareResult::Greater,
        (Value::Text(l_text), Value::Text(r_text)) => match cmp_text(l_text, r_text) {
            Ordering::Greater => CompareResult::Greater,
            Ordering::Less => CompareResult::Less,
            Ordering::Equal => CompareResult::Equal,
        },
        (Value::Text(_), Value::Boolean(_)) => CompareResult::Less,
        (Value::Text(_), Value::Error(e)) => CompareResult::Error(e.clone()),
        (Value::Boolean(_), Value::Blank) => CompareResult::Greater,
        (Value::Boolean(_), Value::Number(_)) => CompareResult::Greater,
        (Value::Boolean(_), Value::Text(_)) => CompareResult::Greater,
        (Value::Boolean(lhs_bool), Value::Boolean(rhs_bool)) => {
            let l = *lhs_bool;
            let r = *rhs_bool;
            if l == r {
                CompareResult::Equal
            } else if l {
                CompareResult::Greater
            } else {
                CompareResult::Less
            }
        }
        (Value::Boolean(_), Value::Error(e)) => CompareResult::Error(e.clone()),
        (Value::Error(e), _) => CompareResult::Error(e.clone()),
    }
}

pub enum CompareResult {
    Less,
    Greater,
    Equal,
    Error(ast::Error),
}