diff --git a/crates/engine/src/cmd/data.rs b/crates/engine/src/cmd/data.rs index fd50afbf2..94733071c 100644 --- a/crates/engine/src/cmd/data.rs +++ b/crates/engine/src/cmd/data.rs @@ -248,7 +248,7 @@ fn do_sort(s: &mut Session, r: RangeRef, header: bool, keys: Vec, by_co let pos = |v: &Value| v.as_text().and_then(|t| list.iter().position(|x| x.eq_ignore_ascii_case(t))).unwrap_or(usize::MAX); pos(va).cmp(&pos(vb)) } else if case && va.is_text() && vb.is_text() { - va.as_text().unwrap_or("").cmp(vb.as_text().unwrap_or("")) + sort_compare(va, vb).then_with(|| case_order(va.as_text().unwrap_or(""), vb.as_text().unwrap_or(""))) } else { sort_compare(va, vb) }; @@ -653,6 +653,12 @@ pub fn filter_values(wb: &Workbook, sheet: usize, col: u32) -> Vec<(String, bool seen.into_iter().map(|((_, t), on)| (t, on)).collect() } +/// Case-sensitive tie-break for text that is equal ignoring case: as in Excel, lowercase sorts +/// before uppercase at the first letter that differs in case. +fn case_order(a: &str, b: &str) -> Ordering { + a.chars().zip(b.chars()).find(|(x, y)| x != y).map_or(Ordering::Equal, |(x, y)| y.is_lowercase().cmp(&x.is_lowercase())) +} + fn sort_key(v: &Value) -> (u8, i64, String) { match v { Value::Number(n) => (0, (n * 1e6) as i64, String::new()), diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 6bd03d55e..92ca38665 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -606,6 +606,15 @@ fn hidden_formulas_stay_hidden_on_protected_sheets() { assert_eq!(r["results"][0]["value"], "7"); } +#[test] +fn case_sensitive_sort_puts_lowercase_first() { + let mut s = s(); + s.execute("range.setValues", json!({"range": "A1", "values": [["b"], ["B"], ["Ab"], ["a"], ["aB"], ["A"]]})).unwrap(); + s.execute("data.sort", json!({"range": "A1:A6", "header": false, "matchCase": true, "keys": [{"column": "A"}]})).unwrap(); + let col: Vec = (1..=6).map(|r| v(&s, &format!("A{r}"))).collect(); + assert_eq!(col, ["a", "A", "aB", "Ab", "b", "B"].map(Value::from)); +} + #[test] fn formatting_and_styles() { let mut s = s();