mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-26 11:46:49 -06:00
Merge main into fix/lambda-recursion-stack
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
version: 2
|
||||
updates:
|
||||
# Maintain Rust / Cargo dependencies
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
time: "06:00"
|
||||
timezone: "America/New_York"
|
||||
# Consolidate updates into a single PR rather than one PR per crate
|
||||
groups:
|
||||
cargo-dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
commit-message:
|
||||
prefix: "chore(deps)"
|
||||
|
||||
# Maintain GitHub Actions workflows
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
time: "06:00"
|
||||
timezone: "America/New_York"
|
||||
groups:
|
||||
actions:
|
||||
patterns:
|
||||
- "*"
|
||||
commit-message:
|
||||
prefix: "ci(deps)"
|
||||
Generated
+16
-16
@@ -1496,7 +1496,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gridcraft"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"craft-print",
|
||||
@@ -1518,7 +1518,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gridcraft-calc"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"gridcraft-core",
|
||||
"gridcraft-formula",
|
||||
@@ -1530,7 +1530,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gridcraft-chart"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"gridcraft-calc",
|
||||
"gridcraft-core",
|
||||
@@ -1541,7 +1541,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gridcraft-cli"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"gridcraft-engine",
|
||||
"gridcraft-mcp",
|
||||
@@ -1553,7 +1553,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gridcraft-core"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"serde",
|
||||
@@ -1563,7 +1563,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gridcraft-engine"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"gridcraft-calc",
|
||||
"gridcraft-chart",
|
||||
@@ -1584,7 +1584,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gridcraft-formula"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"gridcraft-core",
|
||||
"serde",
|
||||
@@ -1593,7 +1593,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gridcraft-functions"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"gridcraft-core",
|
||||
"regex",
|
||||
@@ -1601,7 +1601,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gridcraft-mcp"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"gridcraft-engine",
|
||||
"log",
|
||||
@@ -1610,7 +1610,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gridcraft-model"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"gridcraft-core",
|
||||
"gridcraft-formula",
|
||||
@@ -1622,18 +1622,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gridcraft-numfmt"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"gridcraft-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gridcraft-pdf"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
|
||||
[[package]]
|
||||
name = "gridcraft-ui-egui"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"craft-ui",
|
||||
"egui",
|
||||
@@ -1655,7 +1655,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gridcraft-web"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
@@ -1672,7 +1672,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gridcraft-xlsx"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"gridcraft-core",
|
||||
@@ -4526,7 +4526,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"gridcraft-engine",
|
||||
"serde_json",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
||||
default-members = ["crates/*", "apps/gridcraft", "apps/gridcraft-cli", "xtask"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.90"
|
||||
|
||||
@@ -1298,3 +1298,31 @@ fn offset_extreme_arguments_return_ref() {
|
||||
assert_eq!(crate::evaluate(&wb, 0, c("D1"), formula), Value::Number(expected), "{formula}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn median_large_values_recalculate_dependents() {
|
||||
let mut t = T::new();
|
||||
t.set("A1", "1e308");
|
||||
t.set("A2", "1.6e308");
|
||||
t.set("B1", "=MEDIAN(A1:A2)");
|
||||
t.set("B2", "=AGGREGATE(12,0,A1:A2)");
|
||||
t.set("C1", "=B1/1e308");
|
||||
t.set("C2", "=B2/1e308");
|
||||
for cell in ["B1", "B2"] {
|
||||
assert!((t.num(cell) / 1e308 - 1.3).abs() < 1e-14);
|
||||
}
|
||||
for cell in ["C1", "C2"] {
|
||||
assert!((t.num(cell) - 1.3).abs() < 1e-14);
|
||||
}
|
||||
t.set("A2", "-1e308");
|
||||
for cell in ["B1", "B2", "C1", "C2"] {
|
||||
assert_eq!(t.num(cell), 0.0);
|
||||
}
|
||||
t.set("A1", "-1.6e308");
|
||||
for cell in ["B1", "B2"] {
|
||||
assert!((t.num(cell) / 1e308 + 1.3).abs() < 1e-14);
|
||||
}
|
||||
for cell in ["C1", "C2"] {
|
||||
assert!((t.num(cell) + 1.3).abs() < 1e-14);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -675,7 +675,13 @@ fn insert_pivot(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
Json::String(n) => add_to_area("insert.pivotTable", pt, &src, n, area, None, None, None)?,
|
||||
Json::Object(_) => {
|
||||
let n = str_param(f, "field").ok_or_else(|| bad("insert.pivotTable", "a field object needs `field`"))?;
|
||||
add_to_area("insert.pivotTable", pt, &src, n, area, func_param(str_param(f, "func")), str_param(f, "name"), None)?;
|
||||
let func = match str_param(f, "func") {
|
||||
Some(fname) => {
|
||||
Some(func_param(Some(fname)).ok_or_else(|| bad("insert.pivotTable", format!("unknown function `{fname}`")))?)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
add_to_area("insert.pivotTable", pt, &src, n, area, func, str_param(f, "name"), None)?;
|
||||
let by = group_param(str_param(f, "group"));
|
||||
if let Some(by) = by.filter(|b| *b != PivotDateGroup::None)
|
||||
&& matches!(area, "rows" | "columns")
|
||||
|
||||
@@ -650,6 +650,10 @@ fn cell_look(wb: &Workbook, si: usize, sh: &Sheet, c: CellRef, cf: &mut Option<c
|
||||
if st.font.underline != Underline::None {
|
||||
style.font.underline = st.font.underline;
|
||||
}
|
||||
// In a differential style General means "not set" (#1163).
|
||||
if st.num_fmt.as_str() != "General" {
|
||||
style.num_fmt = st.num_fmt.clone();
|
||||
}
|
||||
for (dst, src) in [
|
||||
(&mut style.borders.left, st.borders.left),
|
||||
(&mut style.borders.right, st.borders.right),
|
||||
@@ -1076,7 +1080,8 @@ fn draw_text(pg: &mut Page, b: &Block, c: CellRef, l: &Look, rect: (f32, f32, f3
|
||||
let max_chars = (avail / digit).floor().max(1.0) as usize;
|
||||
gridcraft_numfmt::format_general_fit(n, max_chars.min(11)).unwrap_or_else(|| "#".repeat(max_chars.min(255)))
|
||||
} else {
|
||||
crate::display::cell_text(wb, sh, c)
|
||||
// The look's code, which a conditional format can override (#1163).
|
||||
crate::display::value_text_as(wb, &v, code)
|
||||
};
|
||||
if !wrap {
|
||||
text = text.replace(['\n', '\r'], " ");
|
||||
@@ -1932,6 +1937,21 @@ mod tests {
|
||||
assert_eq!(expand_header_footer("&", &cx)[1].text, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conditional_number_format_prints() {
|
||||
// #1163: a rule's number format replaces the cell's own, whether that is General or not;
|
||||
// cells the rule doesn't match keep theirs.
|
||||
let mut s = s();
|
||||
s.execute("range.setValues", json!({"range": "A1", "values": [[1], [3], [4]]})).unwrap();
|
||||
s.execute("home.numberFormat", json!({"range": "A3", "code": "0.0"})).unwrap();
|
||||
let rule = json!({"type": "cellIs", "operator": "greater", "value": "2", "style": {"num_fmt": "0.00"}});
|
||||
s.execute("home.conditionalFormat", json!({"range": "A1:A3", "rule": rule})).unwrap();
|
||||
let (pdf, _) = pdf_of(&mut s, json!({}));
|
||||
assert!(contains(&pdf, "(1) Tj"), "an unmatched cell keeps General");
|
||||
assert!(contains(&pdf, "(3.00) Tj") && !contains(&pdf, "(3) Tj"), "General cell under the rule");
|
||||
assert!(contains(&pdf, "(4.00) Tj") && !contains(&pdf, "(4.0) Tj"), "formatted cell under the rule");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_writes_file() {
|
||||
let mut s = s();
|
||||
|
||||
@@ -95,7 +95,13 @@ pub fn cell_text(wb: &Workbook, sheet: &Sheet, c: CellRef) -> String {
|
||||
/// and no General narrowing (Save As CSV/Text writes this). Formulas give their result.
|
||||
pub fn value_text(wb: &Workbook, sheet: &Sheet, c: CellRef) -> String {
|
||||
let style = wb.styles.get(sheet.style_id(c));
|
||||
let f = format(&sheet.value(c), style.num_fmt.as_str(), wb);
|
||||
value_text_as(wb, &sheet.value(c), style.num_fmt.as_str())
|
||||
}
|
||||
|
||||
/// [`value_text`] for a value and a format code given apart from a cell (print takes the code
|
||||
/// from the cell's effective look, which a conditional format can override).
|
||||
pub fn value_text_as(wb: &Workbook, v: &Value, code: &str) -> String {
|
||||
let f = format(v, code, wb);
|
||||
match f.fill {
|
||||
Some((_, pos)) => {
|
||||
let mut t = f.text;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
use gridcraft_engine::Session;
|
||||
use serde_json::json;
|
||||
|
||||
fn session() -> Session {
|
||||
let mut s = Session::new();
|
||||
s.new_workbook();
|
||||
s.execute("range.setValues", json!({"range":"A1", "values":[["Region", "Sales"], ["East", "10"], ["East", "2"], ["West", "4"]]})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_pivot_table_rejects_unknown_func() {
|
||||
for func in ["stdev", "sumx"] {
|
||||
let mut s = session();
|
||||
let r = s.execute(
|
||||
"insert.pivotTable",
|
||||
json!({"source":"Sheet1!A1:B4", "destination":"Sheet1!F1", "rows":["Region"], "values":[{"field":"Sales", "func":func}]}),
|
||||
);
|
||||
assert!(r.is_err(), "unknown func `{func}` accepted silently");
|
||||
assert!(s.doc().unwrap().wb.active().unwrap().pivots.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_pivot_table_accepts_known_func() {
|
||||
let mut s = session();
|
||||
s.execute(
|
||||
"insert.pivotTable",
|
||||
json!({"source":"Sheet1!A1:B4", "destination":"Sheet1!F1", "rows":["Region"], "values":[{"field":"Sales", "func":"stddev"}]}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(s.doc().unwrap().wb.active().unwrap().pivots.len(), 1);
|
||||
}
|
||||
@@ -54,7 +54,7 @@ fn median(v: Vec<f64>) -> R<f64> {
|
||||
let s = sorted(v);
|
||||
let n = s.len();
|
||||
let hi = s.get(n / 2).copied().unwrap_or(0.0);
|
||||
if n % 2 == 1 { Ok(hi) } else { Ok((s.get(n / 2 - 1).copied().unwrap_or(0.0) + hi) / 2.0) }
|
||||
if n % 2 == 1 { Ok(hi) } else { Ok(s.get(n / 2 - 1).copied().unwrap_or(0.0).midpoint(hi)) }
|
||||
}
|
||||
|
||||
/// Values occurring at least twice, ordered by first occurrence, with their (equal, maximal) count.
|
||||
@@ -2255,6 +2255,46 @@ mod tests {
|
||||
av(col(xs))
|
||||
}
|
||||
|
||||
// The two middle values are finite and their midpoint is representable even when
|
||||
// adding them first would overflow. Exercise both callers of the shared helper.
|
||||
#[test]
|
||||
fn median_finite_midpoint_does_not_overflow() {
|
||||
for (values, expected) in [
|
||||
(vec![1e308, 1e308], 1e308),
|
||||
(vec![-1e308, -1e308], -1e308),
|
||||
(vec![1.6e308, 1e308], 1.3e308),
|
||||
(vec![-1e308, -1.6e308], -1.3e308),
|
||||
(vec![f64::MAX, f64::MAX], f64::MAX),
|
||||
(vec![-f64::MAX, -f64::MAX], -f64::MAX),
|
||||
(vec![-f64::MAX, f64::MAX], 0.0),
|
||||
(vec![1.7e308, -1.7e308, 1e308, 1.6e308], 1.3e308),
|
||||
] {
|
||||
close(ev("MEDIAN", values.iter().copied().map(n).collect()), expected);
|
||||
close(ev("MEDIAN", vec![rf(col(&values))]), expected);
|
||||
close(aggregate_values(12, &values.iter().copied().map(nv).collect::<Vec<_>>()), expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn median_midpoint_keeps_small_values_and_argument_semantics() {
|
||||
// Halving each operand unconditionally can lose the smallest numbers. Use exact
|
||||
// equality here: the usual aggregate-test tolerance would accept zero.
|
||||
for value in [f64::MIN_POSITIVE, -f64::MIN_POSITIVE, f64::from_bits(1), -f64::from_bits(1)] {
|
||||
assert_eq!(ev("MEDIAN", vec![n(value), n(value)]), nv(value));
|
||||
assert_eq!(aggregate_values(12, &[nv(value), nv(value)]), nv(value));
|
||||
}
|
||||
close(ev("MEDIAN", vec![c(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0])]), 3.5);
|
||||
close(ev("MEDIAN", vec![c(&[1e308, -1e308, 3.0])]), 3.0);
|
||||
close(ev("MEDIAN", vec![n(1e308)]), 1e308);
|
||||
close(ev("MEDIAN", vec![t("4"), b(true)]), 2.5);
|
||||
close(ev("MEDIAN", vec![rf(arr(vec![vec![nv(1.0), tv("ignored"), Value::Empty, Value::Bool(true), nv(6.0)]]))]), 3.5);
|
||||
is_err(ev("MEDIAN", vec![t("invalid")]), CellError::Value);
|
||||
is_err(ev("MEDIAN", vec![e(CellError::NA), n(1e308)]), CellError::NA);
|
||||
is_err(ev("MEDIAN", vec![rf(arr(vec![vec![tv("ignored"), Value::Empty]]))]), CellError::Num);
|
||||
is_err(aggregate_values(12, &[]), CellError::Num);
|
||||
is_err(aggregate_values(12, &[nv(1e308), Value::Error(CellError::Div0)]), CellError::Div0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn averages_and_counts() {
|
||||
close(ev("AVERAGE", vec![n(1.0), n(2.0), n(6.0)]), 3.0);
|
||||
|
||||
@@ -482,6 +482,10 @@ fn look(wb: &Workbook, si: usize, sh: &Sheet, c: CellRef, cf: &mut CfCache) -> (
|
||||
if st.font.strike {
|
||||
style.font.strike = true;
|
||||
}
|
||||
// In a differential style General means "not set" (#1163).
|
||||
if st.num_fmt.as_str() != "General" {
|
||||
style.num_fmt = st.num_fmt.clone();
|
||||
}
|
||||
for (dst, src) in [
|
||||
(&mut style.borders.left, st.borders.left),
|
||||
(&mut style.borders.right, st.borders.right),
|
||||
@@ -3406,6 +3410,28 @@ mod tests {
|
||||
assert_eq!(center_across_end(0, 1, |_| true), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conditional_number_format_reaches_the_cell_text() {
|
||||
// #1163: a rule's number format replaces the cell's own; unmatched cells keep theirs.
|
||||
let mut session = gridcraft_engine::Session::new();
|
||||
session.new_workbook();
|
||||
session.execute("range.setValues", json!({"range": "A1", "values": [[1], [3], [4]]})).unwrap();
|
||||
session.execute("home.numberFormat", json!({"range": "A3", "code": "0.0"})).unwrap();
|
||||
let rule = json!({"type": "cellIs", "operator": "greater", "value": "2", "style": {"num_fmt": "0.00"}});
|
||||
session.execute("home.conditionalFormat", json!({"range": "A1:A3", "rule": rule})).unwrap();
|
||||
let wb = session.active().unwrap().wb.clone();
|
||||
let sh = wb.sheet(0).unwrap();
|
||||
let mut cf = CfCache::new();
|
||||
let text = |row: u32, cf: &mut CfCache| {
|
||||
let c = CellRef::new(row, 0);
|
||||
let (lk, _) = look(&wb, 0, sh, c, cf);
|
||||
display_text(&wb, sh, c, &sh.value(c), &lk.style, 200.0, 7.0).0
|
||||
};
|
||||
assert_eq!(text(0, &mut cf), "1");
|
||||
assert_eq!(text(1, &mut cf), "3.00");
|
||||
assert_eq!(text(2, &mut cf), "4.00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_menu_uses_the_table_whose_header_was_clicked() {
|
||||
use gridcraft_engine::model::Table;
|
||||
|
||||
@@ -252,7 +252,8 @@ fn candidates(role: &str) -> Vec<(String, u32)> {
|
||||
let mac_sys = "/System/Library/Fonts";
|
||||
let mac_sup = "/System/Library/Fonts/Supplemental";
|
||||
let win = std::env::var("WINDIR").map(|w| format!("{w}\\Fonts")).unwrap_or_else(|_| "C:\\Windows\\Fonts".into());
|
||||
let lin = ["/usr/share/fonts/truetype", "/usr/share/fonts", "/usr/local/share/fonts", "/usr/share/fonts/TTF"];
|
||||
// ChromeOS's Linux container (Crostini) mounts the host's fonts under `chromeos/` (#1762).
|
||||
let lin = ["/usr/share/fonts/truetype", "/usr/share/fonts", "/usr/local/share/fonts", "/usr/share/fonts/TTF", "/usr/share/fonts/chromeos"];
|
||||
let mut v: Vec<(String, u32)> = Vec::new();
|
||||
let mut add = |s: String, i: u32| v.push((s, i));
|
||||
match role {
|
||||
@@ -498,10 +499,10 @@ fn fallback_faces(han: HanOrder) -> Vec<(&'static str, Vec<(String, u32)>)> {
|
||||
}
|
||||
|
||||
/// Linux directories that hold CJK fonts across distributions: Debian/Ubuntu (`opentype/noto`,
|
||||
/// `truetype/wqy`), Fedora (`google-noto-cjk`), Arch (`noto-cjk`, `wenquanyi`), plus local and
|
||||
/// flat layouts.
|
||||
/// `truetype/wqy`), Fedora (`google-noto-cjk`), Arch (`noto-cjk`, `wenquanyi`), ChromeOS's Linux
|
||||
/// container (`chromeos/notocjk`, #1762), plus local and flat layouts.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
const LINUX_CJK_DIRS: [&str; 10] = [
|
||||
const LINUX_CJK_DIRS: [&str; 11] = [
|
||||
"/usr/share/fonts/opentype/noto",
|
||||
"/usr/share/fonts/google-noto-cjk",
|
||||
"/usr/share/fonts/noto-cjk",
|
||||
@@ -512,6 +513,7 @@ const LINUX_CJK_DIRS: [&str; 10] = [
|
||||
"/usr/share/fonts",
|
||||
"/usr/local/share/fonts",
|
||||
"/usr/local/share/fonts/noto-cjk",
|
||||
"/usr/share/fonts/chromeos/notocjk",
|
||||
];
|
||||
|
||||
/// Japanese Han + Kana faces (Japanese kanji glyph shapes).
|
||||
@@ -581,7 +583,13 @@ fn thai() -> Vec<(String, u32)> {
|
||||
let mac_sys = "/System/Library/Fonts";
|
||||
let mac_sup = "/System/Library/Fonts/Supplemental";
|
||||
let win = std::env::var("WINDIR").map(|w| format!("{w}\\Fonts")).unwrap_or_else(|_| "C:\\Windows\\Fonts".into());
|
||||
let lin = ["/usr/share/fonts/truetype/noto", "/usr/share/fonts/opentype/noto", "/usr/share/fonts/truetype", "/usr/share/fonts"];
|
||||
let lin = [
|
||||
"/usr/share/fonts/truetype/noto",
|
||||
"/usr/share/fonts/opentype/noto",
|
||||
"/usr/share/fonts/truetype",
|
||||
"/usr/share/fonts",
|
||||
"/usr/share/fonts/chromeos/noto", // ChromeOS's Linux container (#1762)
|
||||
];
|
||||
let mut v: Vec<(String, u32)> = Vec::new();
|
||||
v.push((format!("{win}\\LeelawUI.ttf"), 0)); // Leelawadee UI
|
||||
v.push((format!("{win}\\tahoma.ttf"), 0)); // Tahoma
|
||||
@@ -603,7 +611,13 @@ fn hebrew() -> Vec<(String, u32)> {
|
||||
let mac_sys = "/System/Library/Fonts";
|
||||
let mac_sup = "/System/Library/Fonts/Supplemental";
|
||||
let win = std::env::var("WINDIR").map(|w| format!("{w}\\Fonts")).unwrap_or_else(|_| "C:\\Windows\\Fonts".into());
|
||||
let lin = ["/usr/share/fonts/truetype/noto", "/usr/share/fonts/opentype/noto", "/usr/share/fonts/truetype", "/usr/share/fonts"];
|
||||
let lin = [
|
||||
"/usr/share/fonts/truetype/noto",
|
||||
"/usr/share/fonts/opentype/noto",
|
||||
"/usr/share/fonts/truetype",
|
||||
"/usr/share/fonts",
|
||||
"/usr/share/fonts/chromeos/noto", // ChromeOS's Linux container (#1762)
|
||||
];
|
||||
let mut v: Vec<(String, u32)> = Vec::new();
|
||||
v.push((format!("{win}\\david.ttf"), 0)); // David
|
||||
v.push((format!("{win}\\frank.ttf"), 0)); // FrankRuehl
|
||||
@@ -657,6 +671,7 @@ fn symbols_more() -> Vec<(String, u32)> {
|
||||
"/usr/share/fonts/google-noto-sans-symbols-2-fonts",
|
||||
"/usr/share/fonts/google-noto",
|
||||
"/usr/local/share/fonts/noto",
|
||||
"/usr/share/fonts/chromeos/noto",
|
||||
] {
|
||||
v.push((format!("{d}/NotoSansSymbols2-Regular.ttf"), 0));
|
||||
}
|
||||
@@ -888,6 +903,22 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chromeos_font_layout_is_probed() {
|
||||
// #1762: Crostini mounts the host's fonts under /usr/share/fonts/chromeos, one folder per
|
||||
// family set; a script whose list misses it renders as tofu with the font installed.
|
||||
let probes = |cands: Vec<(String, u32)>, path: &str| cands.iter().any(|(p, _)| p == path);
|
||||
let cros = "/usr/share/fonts/chromeos";
|
||||
assert!(probes(thai(), &format!("{cros}/noto/NotoSansThai-Regular.ttf")));
|
||||
assert!(probes(hebrew(), &format!("{cros}/noto/NotoSansHebrew-Regular.ttf")));
|
||||
assert!(probes(cjk_han_japanese(), &format!("{cros}/notocjk/NotoSansCJK-Regular.ttc")));
|
||||
assert!(probes(cjk_han_simplified(), &format!("{cros}/notocjk/NotoSansCJK-Regular.ttc")));
|
||||
assert!(probes(cjk_hangul(), &format!("{cros}/notocjk/NotoSansCJK-Regular.ttc")));
|
||||
assert!(probes(symbols_more(), &format!("{cros}/noto/NotoSansSymbols2-Regular.ttf")));
|
||||
assert!(probes(candidates(UI), &format!("{cros}/noto/NotoSans-Regular.ttf")));
|
||||
assert!(probes(candidates(CELL), &format!("{cros}/crosextra/Carlito-Regular.ttf")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unparseable_font_files_are_rejected_not_fatal() {
|
||||
// #226: epaint panics in `set_fonts` on a face it can't parse ("An offset was out of
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Function parity: worksheet functions and formulas
|
||||
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-13 · **Change:** minor (clearing large sparse ranges recalculates every changed cell's dependents, #1032) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (MEDIAN and AGGREGATE median avoid intermediate overflow) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
|
||||
Excel's worksheet functions, category by category, against GridCraft's function library. Part of
|
||||
[target-app-parity.md](target-app-parity.md); gaps feed [gaps.md](gaps.md).
|
||||
@@ -101,6 +101,11 @@ fraction sections with literal text (`#\:#=/=#`, `#_#/#`), zero fractions, zero-
|
||||
denominators, `?` placeholders padding the thousands separator, and booleans through the text
|
||||
section (`TEXT(TRUE,";;;\@@\@")` = `@TRUE@`).
|
||||
|
||||
`MEDIAN` and `AGGREGATE` function number 12 use an overflow-safe midpoint for even-sized
|
||||
inputs, so large finite middle values do not become `#NUM!` merely because their sum overflows (#1579).
|
||||
Sorting, argument coercion and error propagation are unchanged; this is a numerical correction
|
||||
under the documented median rule, not an extreme-value comparison against desktop Excel.
|
||||
|
||||
Work to close (16–26 h in total): the in-scope missing functions (10–16 h with the table above),
|
||||
an oracle corpus of Excel-computed results per function (owner runs Excel on generated
|
||||
workbooks; results checked in as data, not Excel files) and fixing what it finds (6–10 h).
|
||||
@@ -109,6 +114,7 @@ workbooks; results checked in as data, not Excel files) and fixing what it finds
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-15 | minor | MEDIAN and AGGREGATE median use an overflow-safe midpoint (#1579); regressions cover finite extremes, tiny values and dependent recalculation |
|
||||
| 2026-10-13 | minor | Clear reports changed stored cells sparsely, including those beyond the first million coordinates; dependent formulas and removed spills update, while clearing empty merges still unblocks spills (#1032) |
|
||||
| 2026-10-12 | minor | USDOLLAR added as a Compatibility function (not on Microsoft's list, so the counts don't change); MIRR with no positive flow is -1, COUNTIF's bare `"<="` / `">="` skip empty strings, INDIRECT with `a1` = FALSE gives `#REF!` for A1-style text (#738) |
|
||||
| 2026-10-11 | minor | TEXT / number formats fixed against Excel-cached results: fraction literals and zero fractions, `0` denominators zero padded, `?` pads the thousands separator, booleans use the text section (226 mismatches → 0 in 352 TEXT cells) |
|
||||
|
||||
Reference in New Issue
Block a user