mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-31 06:06:48 -06:00
Compare commits
84
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55850841d3 | ||
|
|
1f73f62c4f | ||
|
|
fb1e59ff61 | ||
|
|
8f59487cbe | ||
|
|
71d4e2311d | ||
|
|
213fe1150e | ||
|
|
0db7d97f2e | ||
|
|
4d1233d0a2 | ||
|
|
07c8201786 | ||
|
|
31d5cc5b41 | ||
|
|
c0fcc7de16 | ||
|
|
5390e230d8 | ||
|
|
c9eb797d8b | ||
|
|
5441691ca3 | ||
|
|
d3f54debd9 | ||
|
|
64ea0cb69c | ||
|
|
f887ecf1cf | ||
|
|
ae598a74fe | ||
|
|
bc0fffbe97 | ||
|
|
1ebef5a694 | ||
|
|
f812c53814 | ||
|
|
b775b3ce32 | ||
|
|
76aed738e3 | ||
|
|
a44d3d5ee5 | ||
|
|
2bf5dbac84 | ||
|
|
c269ff483d | ||
|
|
15f6297001 | ||
|
|
006b7962bd | ||
|
|
7ea3a4c2c3 | ||
|
|
cae0c9abdb | ||
|
|
ed81d7e34e | ||
|
|
8f7dff0d04 | ||
|
|
7b7e734524 | ||
|
|
6d5ab57f84 | ||
|
|
a04629576d | ||
|
|
3174020a70 | ||
|
|
781149a593 | ||
|
|
322298b202 | ||
|
|
11d6ff1b79 | ||
|
|
eefa7c4a54 | ||
|
|
45939a8a41 | ||
|
|
70da8bb3ae | ||
|
|
a0da53f576 | ||
|
|
ca44a5d68f | ||
|
|
3b61d660e9 | ||
|
|
cca8a58bfa | ||
|
|
d3cd393dba | ||
|
|
8985685833 | ||
|
|
962a5df841 | ||
|
|
fea4c03ca2 | ||
|
|
1d9096e5ec | ||
|
|
8e740d97e1 | ||
|
|
a9bacdca4c | ||
|
|
53d6de3c16 | ||
|
|
6aa2cb8223 | ||
|
|
83e6408b71 | ||
|
|
1b1224c67a | ||
|
|
213b76af10 | ||
|
|
4e5c86d842 | ||
|
|
ad69d4cbdd | ||
|
|
32dbf6a224 | ||
|
|
11b54ebe40 | ||
|
|
9e710199b1 | ||
|
|
5ee65a8005 | ||
|
|
48c6b57060 | ||
|
|
ec2efe3246 | ||
|
|
5ed6a1b9c7 | ||
|
|
5d89b9efdf | ||
|
|
f1ff38504a | ||
|
|
0435ff50f9 | ||
|
|
550085f8b8 | ||
|
|
3a27743a38 | ||
|
|
dc691d52db | ||
|
|
bd8cf4e14a | ||
|
|
dc75555ee9 | ||
|
|
4d725e6bc1 | ||
|
|
9fccec28af | ||
|
|
52d317c8ff | ||
|
|
00359ee93a | ||
|
|
c895acf1f7 | ||
|
|
1f4c67ef0a | ||
|
|
8cf8425745 | ||
|
|
59a8b14d4d | ||
|
|
8f93ba2e9c |
Generated
+16
-16
@@ -1936,7 +1936,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridcraft"
|
name = "gridcraft"
|
||||||
version = "0.13.0"
|
version = "0.14.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arboard",
|
"arboard",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -1963,7 +1963,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridcraft-calc"
|
name = "gridcraft-calc"
|
||||||
version = "0.13.0"
|
version = "0.14.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gridcraft-core",
|
"gridcraft-core",
|
||||||
"gridcraft-formula",
|
"gridcraft-formula",
|
||||||
@@ -1975,7 +1975,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridcraft-chart"
|
name = "gridcraft-chart"
|
||||||
version = "0.13.0"
|
version = "0.14.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gridcraft-calc",
|
"gridcraft-calc",
|
||||||
"gridcraft-core",
|
"gridcraft-core",
|
||||||
@@ -1986,7 +1986,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridcraft-cli"
|
name = "gridcraft-cli"
|
||||||
version = "0.13.0"
|
version = "0.14.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gridcraft-engine",
|
"gridcraft-engine",
|
||||||
"gridcraft-mcp",
|
"gridcraft-mcp",
|
||||||
@@ -1998,7 +1998,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridcraft-core"
|
name = "gridcraft-core"
|
||||||
version = "0.13.0"
|
version = "0.14.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
@@ -2009,7 +2009,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridcraft-engine"
|
name = "gridcraft-engine"
|
||||||
version = "0.13.0"
|
version = "0.14.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gridcraft-calc",
|
"gridcraft-calc",
|
||||||
"gridcraft-chart",
|
"gridcraft-chart",
|
||||||
@@ -2031,7 +2031,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridcraft-formula"
|
name = "gridcraft-formula"
|
||||||
version = "0.13.0"
|
version = "0.14.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gridcraft-core",
|
"gridcraft-core",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -2040,7 +2040,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridcraft-functions"
|
name = "gridcraft-functions"
|
||||||
version = "0.13.0"
|
version = "0.14.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gridcraft-core",
|
"gridcraft-core",
|
||||||
"regex",
|
"regex",
|
||||||
@@ -2048,7 +2048,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridcraft-mcp"
|
name = "gridcraft-mcp"
|
||||||
version = "0.13.0"
|
version = "0.14.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gridcraft-engine",
|
"gridcraft-engine",
|
||||||
"log",
|
"log",
|
||||||
@@ -2057,7 +2057,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridcraft-model"
|
name = "gridcraft-model"
|
||||||
version = "0.13.0"
|
version = "0.14.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gridcraft-core",
|
"gridcraft-core",
|
||||||
"gridcraft-formula",
|
"gridcraft-formula",
|
||||||
@@ -2069,18 +2069,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridcraft-numfmt"
|
name = "gridcraft-numfmt"
|
||||||
version = "0.13.0"
|
version = "0.14.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gridcraft-core",
|
"gridcraft-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridcraft-pdf"
|
name = "gridcraft-pdf"
|
||||||
version = "0.13.0"
|
version = "0.14.2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridcraft-ui-egui"
|
name = "gridcraft-ui-egui"
|
||||||
version = "0.13.0"
|
version = "0.14.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"craft-ui",
|
"craft-ui",
|
||||||
"craft-update",
|
"craft-update",
|
||||||
@@ -2105,7 +2105,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridcraft-web"
|
name = "gridcraft-web"
|
||||||
version = "0.13.0"
|
version = "0.14.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"eframe",
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
@@ -2122,7 +2122,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridcraft-xlsx"
|
name = "gridcraft-xlsx"
|
||||||
version = "0.13.0"
|
version = "0.14.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"gridcraft-core",
|
"gridcraft-core",
|
||||||
@@ -6403,7 +6403,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xtask"
|
name = "xtask"
|
||||||
version = "0.13.0"
|
version = "0.14.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gridcraft-engine",
|
"gridcraft-engine",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
|||||||
default-members = ["crates/*", "apps/gridcraft", "apps/gridcraft-cli", "xtask"]
|
default-members = ["crates/*", "apps/gridcraft", "apps/gridcraft-cli", "xtask"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.13.0"
|
version = "0.14.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
rust-version = "1.95"
|
rust-version = "1.95"
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ use super::layout::{DENSE_BLOCK, SheetLayout};
|
|||||||
use super::objects::draw_cell_picture;
|
use super::objects::draw_cell_picture;
|
||||||
use super::text::{TextPlacement, draw_text};
|
use super::text::{TextPlacement, draw_text};
|
||||||
use super::{
|
use super::{
|
||||||
BorderLine, BorderStyle, Borders, CellRef, Color, Font, Page, PatternType, Point, RangeRef, Rgb, Sheet, Style, StyleId, Underline, Workbook,
|
BorderLine, BorderStyle, Borders, CellRef, Color, Font, Page, PatternType, Point, RangeRef, Rgb, Sheet, Style, StyleId, Underline, Value,
|
||||||
split_sheet,
|
Workbook, split_sheet,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------------------- drawing
|
// ---------------------------------------------------------------- drawing
|
||||||
@@ -280,6 +280,7 @@ pub(super) fn draw_block(pg: &mut Page, b: &Block, gridlines: bool, cf: &mut Opt
|
|||||||
|| merges.iter().any(|m| m.start == *c)
|
|| merges.iter().any(|m| m.start == *c)
|
||||||
|| !sh.value(*c).is_empty()
|
|| !sh.value(*c).is_empty()
|
||||||
|| l.style.borders != Borders::default()
|
|| l.style.borders != Borders::default()
|
||||||
|
|| l.style.num_fmt.as_str() == "checkbox"
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let rect_of = |c: CellRef| -> (f32, f32, f32, f32, bool) {
|
let rect_of = |c: CellRef| -> (f32, f32, f32, f32, bool) {
|
||||||
@@ -343,6 +344,8 @@ pub(super) fn draw_block(pg: &mut Page, b: &Block, gridlines: bool, cf: &mut Opt
|
|||||||
}
|
}
|
||||||
if let Some(picture) = sh.cell_pictures.get(c) {
|
if let Some(picture) = sh.cell_pictures.get(c) {
|
||||||
draw_cell_picture(pg, &picture.data, (x, y, w, h), b.k);
|
draw_cell_picture(pg, &picture.data, (x, y, w, h), b.k);
|
||||||
|
} else if let Some(on) = checkbox_state(l, &sh.value(*c)) {
|
||||||
|
draw_checkbox(pg, (x, y, w, h), b.k, on);
|
||||||
} else {
|
} else {
|
||||||
draw_text(pg, b, TextPlacement { cell: *c, look: l, rect: (x, y, w, h), merged, merges: &merges, limits: (bx, bx + bw) });
|
draw_text(pg, b, TextPlacement { cell: *c, look: l, rect: (x, y, w, h), merged, merges: &merges, limits: (bx, bx + bw) });
|
||||||
}
|
}
|
||||||
@@ -350,6 +353,37 @@ pub(super) fn draw_block(pg: &mut Page, b: &Block, gridlines: bool, cf: &mut Opt
|
|||||||
pg.restore();
|
pg.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A checkbox cell prints TRUE/FALSE (and empty) as a box, as on screen (#2972); other values
|
||||||
|
/// print as text.
|
||||||
|
fn checkbox_state(l: &Look, v: &Value) -> Option<bool> {
|
||||||
|
if l.style.num_fmt.as_str() != "checkbox" || l.cf.as_ref().is_some_and(|cf| cf.hide_value) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
match v {
|
||||||
|
Value::Bool(on) => Some(*on),
|
||||||
|
Value::Empty => Some(false),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A checkbox centred in the cell: white with a grey outline, or green with a white tick.
|
||||||
|
fn draw_checkbox(pg: &mut Page, (x, y, w, h): (f32, f32, f32, f32), k: f32, on: bool) {
|
||||||
|
let s = (13.2 * k).min(w - 2.0 * k).min(h - 2.0 * k);
|
||||||
|
if s <= 0.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (bx, by) = (x + (w - s) / 2.0, y + (h - s) / 2.0);
|
||||||
|
let corners = [[bx, by], [bx + s, by], [bx + s, by + s], [bx, by + s]];
|
||||||
|
if on {
|
||||||
|
let green = [0x10, 0x7C, 0x41];
|
||||||
|
pg.polygon(&corners, Some(green), Some((green, 0.75 * k)));
|
||||||
|
let tick = [[bx + 0.22 * s, by + 0.52 * s], [bx + 0.42 * s, by + 0.72 * s], [bx + 0.78 * s, by + 0.30 * s]];
|
||||||
|
pg.polyline(&tick, 0.14 * s, [0xFF, 0xFF, 0xFF], &[]);
|
||||||
|
} else {
|
||||||
|
pg.polygon(&corners, Some([0xFF, 0xFF, 0xFF]), Some(([110, 110, 110], 0.75 * k)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) struct PendingPdfLink {
|
pub(super) struct PendingPdfLink {
|
||||||
pub(super) page: usize,
|
pub(super) page: usize,
|
||||||
pub(super) sheet: usize,
|
pub(super) sheet: usize,
|
||||||
|
|||||||
@@ -565,3 +565,19 @@ fn rotated_wrapped_text_prints_rotated() {
|
|||||||
assert_eq!(up + down, blocks.len(), "a header line printed flat: {blocks:?}");
|
assert_eq!(up + down, blocks.len(), "a header line printed flat: {blocks:?}");
|
||||||
assert!((1..=3).contains(&up) && (1..=3).contains(&down), "up {up}, down {down}: {blocks:?}");
|
assert!((1..=3).contains(&up) && (1..=3).contains(&down), "up {up}, down {down}: {blocks:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn checkbox_cells_print_as_boxes_not_true_false() {
|
||||||
|
// #2972: a checkbox cell printed its value as TRUE or FALSE.
|
||||||
|
let mut s = s();
|
||||||
|
s.execute("insert.checkbox", json!({"range": "A1:A2"})).unwrap();
|
||||||
|
s.execute("cell.toggleCheckbox", json!({"cell": "A2"})).unwrap();
|
||||||
|
s.execute("cell.set", json!({"cell": "C1", "input": "TRUE"})).unwrap();
|
||||||
|
let (pdf, _) = pdf_of(&mut s, json!({}));
|
||||||
|
assert!(!contains(&pdf, "(FALSE) Tj"), "the unchecked box printed as FALSE");
|
||||||
|
// Only the plain TRUE in C1 prints as text; the checked box is a filled box.
|
||||||
|
let text = String::from_utf8_lossy(&pdf);
|
||||||
|
assert_eq!(text.matches("(TRUE) Tj").count(), 1, "the checked box printed as TRUE");
|
||||||
|
assert!(contains(&pdf, "0.063 0.486 0.255 rg"), "the checked box is not filled");
|
||||||
|
assert!(contains(&pdf, "0.431 0.431 0.431 RG"), "the unchecked box is not outlined");
|
||||||
|
}
|
||||||
|
|||||||
@@ -175,8 +175,8 @@ fn write_struct_ref(s: &StructRef, at_shorthand: bool) -> String {
|
|||||||
let _ = write!(out, "[{}]", s.specifiers.first().map(item).unwrap_or(""));
|
let _ = write!(out, "[{}]", s.specifiers.first().map(item).unwrap_or(""));
|
||||||
}
|
}
|
||||||
(Some(c), None) if s.specifiers.is_empty() => {
|
(Some(c), None) if s.specifiers.is_empty() => {
|
||||||
// Excel writes a name with spaces at its edges in the double-bracket form.
|
// Names with spaces at their edges, or starting with `@` (this-row shorthand), use the double-bracket form.
|
||||||
if c.trim() == c {
|
if c.trim() == c && !c.starts_with('@') {
|
||||||
let _ = write!(out, "[{}]", esc(c));
|
let _ = write!(out, "[{}]", esc(c));
|
||||||
} else {
|
} else {
|
||||||
let _ = write!(out, "[[{}]]", esc(c));
|
let _ = write!(out, "[[{}]]", esc(c));
|
||||||
|
|||||||
@@ -558,3 +558,12 @@ fn reference_shapes_print_unchanged() -> Result<(), Box<dyn std::error::Error>>
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn column_starting_with_at_round_trips() {
|
||||||
|
for name in ["@a", "a@b"] {
|
||||||
|
let s = StructRef { table: "T".into(), specifiers: vec![], col_start: Some(name.into()), col_end: None };
|
||||||
|
let text = format!("={}", printer::struct_ref(&s));
|
||||||
|
assert_eq!(parse(&text).ok(), Some(Expr::Struct(s)), "printed as {text}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ fn base_source(a: &[Arg]) -> R<String> {
|
|||||||
Ok(number_to_text(n))
|
Ok(number_to_text(n))
|
||||||
}
|
}
|
||||||
Value::Empty => Ok(String::new()),
|
Value::Empty => Ok(String::new()),
|
||||||
v => Ok(v.to_text()?.trim().to_string()),
|
v => Ok(v.to_text()?.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +62,15 @@ fn format_base(v: i64, radix: u32, places: Option<f64>) -> R<Value> {
|
|||||||
if v < min || v > max {
|
if v < min || v > max {
|
||||||
return Err(CellError::Num);
|
return Err(CellError::Num);
|
||||||
}
|
}
|
||||||
|
let places = places
|
||||||
|
.map(|p| {
|
||||||
|
let p = p.trunc();
|
||||||
|
if !(1.0..=10.0).contains(&p) {
|
||||||
|
return Err(CellError::Num);
|
||||||
|
}
|
||||||
|
Ok(p as usize)
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
if v < 0 {
|
if v < 0 {
|
||||||
let u = v + (1i64 << bits);
|
let u = v + (1i64 << bits);
|
||||||
return text_val(to_radix(u, radix));
|
return text_val(to_radix(u, radix));
|
||||||
@@ -70,11 +79,6 @@ fn format_base(v: i64, radix: u32, places: Option<f64>) -> R<Value> {
|
|||||||
match places {
|
match places {
|
||||||
None => text_val(s),
|
None => text_val(s),
|
||||||
Some(p) => {
|
Some(p) => {
|
||||||
let p = p.trunc();
|
|
||||||
if !(1.0..=10.0).contains(&p) {
|
|
||||||
return Err(CellError::Num);
|
|
||||||
}
|
|
||||||
let p = p as usize;
|
|
||||||
if s.len() > p {
|
if s.len() > p {
|
||||||
return Err(CellError::Num);
|
return Err(CellError::Num);
|
||||||
}
|
}
|
||||||
@@ -97,7 +101,13 @@ fn to_radix(mut v: i64, radix: u32) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn places_arg(a: &[Arg]) -> R<Option<f64>> {
|
fn places_arg(a: &[Arg]) -> R<Option<f64>> {
|
||||||
if has(a, 1) { Ok(Some(num(a, 1)?)) } else { Ok(None) }
|
if !has(a, 1) {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
if matches!(a.get(1).map(scalar), Some(Value::Bool(_))) {
|
||||||
|
return Err(CellError::Value);
|
||||||
|
}
|
||||||
|
Ok(Some(num(a, 1)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_base(a: &[Arg], from: u32, to: u32) -> R<Value> {
|
fn convert_base(a: &[Arg], from: u32, to: u32) -> R<Value> {
|
||||||
@@ -175,6 +185,9 @@ fn bitxor(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
|||||||
fn shift(a: &[Arg], left: bool) -> R<Value> {
|
fn shift(a: &[Arg], left: bool) -> R<Value> {
|
||||||
let n = bit_operand(a, 0)?;
|
let n = bit_operand(a, 0)?;
|
||||||
let s = num(a, 1)?.trunc();
|
let s = num(a, 1)?.trunc();
|
||||||
|
if n == 0 {
|
||||||
|
return Ok(Value::Number(0.0));
|
||||||
|
}
|
||||||
if s.abs() > 53.0 {
|
if s.abs() > 53.0 {
|
||||||
return Err(CellError::Num);
|
return Err(CellError::Num);
|
||||||
}
|
}
|
||||||
@@ -402,6 +415,12 @@ mod tests {
|
|||||||
is_text(ev("BIN2OCT", vec![n(1111111111.0)]), "7777777777");
|
is_text(ev("BIN2OCT", vec![n(1111111111.0)]), "7777777777");
|
||||||
is_text(ev("DEC2BIN", vec![n(9.0), n(4.0)]), "1001");
|
is_text(ev("DEC2BIN", vec![n(9.0), n(4.0)]), "1001");
|
||||||
is_text(ev("DEC2BIN", vec![n(-100.0)]), "1110011100");
|
is_text(ev("DEC2BIN", vec![n(-100.0)]), "1110011100");
|
||||||
|
is_err(ev("DEC2BIN", vec![n(-1.0), n(0.0)]), CellError::Num);
|
||||||
|
is_err(ev("DEC2HEX", vec![n(-1.0), n(11.0)]), CellError::Num);
|
||||||
|
is_err(ev("DEC2OCT", vec![n(-1.0), n(-1.0)]), CellError::Num);
|
||||||
|
is_err(ev("DEC2BIN", vec![n(1.0), crate::Arg::val(Value::Bool(true))]), CellError::Value);
|
||||||
|
is_text(ev("DEC2BIN", vec![n(-1.0), n(1.0)]), "1111111111");
|
||||||
|
is_text(ev("DEC2HEX", vec![n(-1.0), n(10.0)]), "FFFFFFFFFF");
|
||||||
is_err(ev("DEC2BIN", vec![n(512.0)]), CellError::Num);
|
is_err(ev("DEC2BIN", vec![n(512.0)]), CellError::Num);
|
||||||
is_text(ev("DEC2HEX", vec![n(100.0), n(4.0)]), "0064");
|
is_text(ev("DEC2HEX", vec![n(100.0), n(4.0)]), "0064");
|
||||||
is_text(ev("DEC2HEX", vec![n(-54.0)]), "FFFFFFFFCA");
|
is_text(ev("DEC2HEX", vec![n(-54.0)]), "FFFFFFFFCA");
|
||||||
@@ -426,6 +445,21 @@ mod tests {
|
|||||||
is_text(ev("OCT2HEX", vec![n(100.0), n(4.0)]), "0040");
|
is_text(ev("OCT2HEX", vec![n(100.0), n(4.0)]), "0040");
|
||||||
is_text(ev("OCT2HEX", vec![n(7777777533.0)]), "FFFFFFFF5B");
|
is_text(ev("OCT2HEX", vec![n(7777777533.0)]), "FFFFFFFF5B");
|
||||||
is_err(ev("BIN2DEC", vec![n(102.0)]), CellError::Num);
|
is_err(ev("BIN2DEC", vec![n(102.0)]), CellError::Num);
|
||||||
|
for (f, input) in [
|
||||||
|
("BIN2DEC", " 10"),
|
||||||
|
("BIN2DEC", "10 "),
|
||||||
|
("BIN2DEC", "\t10"),
|
||||||
|
("BIN2HEX", " 10"),
|
||||||
|
("BIN2OCT", "10 "),
|
||||||
|
("HEX2DEC", " 10"),
|
||||||
|
("HEX2BIN", "10 "),
|
||||||
|
("HEX2OCT", "\t10"),
|
||||||
|
("OCT2DEC", " 10"),
|
||||||
|
("OCT2BIN", "10 "),
|
||||||
|
("OCT2HEX", "\t10"),
|
||||||
|
] {
|
||||||
|
is_err(ev(f, vec![t(input)]), CellError::Num);
|
||||||
|
}
|
||||||
is_err(ev("HEX2DEC", vec![t("GG")]), CellError::Num);
|
is_err(ev("HEX2DEC", vec![t("GG")]), CellError::Num);
|
||||||
is_err(ev("BIN2DEC", vec![t("11111111111")]), CellError::Num);
|
is_err(ev("BIN2DEC", vec![t("11111111111")]), CellError::Num);
|
||||||
is_err(ev("DEC2BIN", vec![t("abc")]), CellError::Value);
|
is_err(ev("DEC2BIN", vec![t("abc")]), CellError::Value);
|
||||||
@@ -446,6 +480,21 @@ mod tests {
|
|||||||
is_err(ev("BITAND", vec![n(1.5), n(5.0)]), CellError::Num);
|
is_err(ev("BITAND", vec![n(1.5), n(5.0)]), CellError::Num);
|
||||||
is_err(ev("BITLSHIFT", vec![n(1.0), n(54.0)]), CellError::Num);
|
is_err(ev("BITLSHIFT", vec![n(1.0), n(54.0)]), CellError::Num);
|
||||||
is_err(ev("BITLSHIFT", vec![n(2f64.powi(47)), n(1.0)]), CellError::Num);
|
is_err(ev("BITLSHIFT", vec![n(2f64.powi(47)), n(1.0)]), CellError::Num);
|
||||||
|
for f in ["BITLSHIFT", "BITRSHIFT"] {
|
||||||
|
for shift in [-100.0, -54.0, -53.0, 53.0, 54.0, 100.0] {
|
||||||
|
close(ev(f, vec![n(0.0), n(shift)]), 0.0);
|
||||||
|
}
|
||||||
|
for shift in [-100.0, -54.0, 54.0, 100.0] {
|
||||||
|
is_err(ev(f, vec![n(1.0), n(shift)]), CellError::Num);
|
||||||
|
}
|
||||||
|
if f == "BITLSHIFT" {
|
||||||
|
is_err(ev(f, vec![n(1.0), n(53.0)]), CellError::Num);
|
||||||
|
} else {
|
||||||
|
close(ev(f, vec![n(1.0), n(53.0)]), 0.0);
|
||||||
|
}
|
||||||
|
is_err(ev(f, vec![n(0.0), t("bad")]), CellError::Value);
|
||||||
|
}
|
||||||
|
is_err(ev("BITRSHIFT", vec![n(-1.0), n(100.0)]), CellError::Num);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -420,7 +420,10 @@ pub(super) fn geomean(v: &[f64]) -> R<f64> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn harmean(v: &[f64]) -> R<f64> {
|
pub(super) fn harmean(v: &[f64]) -> R<f64> {
|
||||||
if v.is_empty() || v.iter().any(|&x| x <= 0.0) {
|
if v.is_empty() {
|
||||||
|
return Err(CellError::NA);
|
||||||
|
}
|
||||||
|
if v.iter().any(|&x| x <= 0.0) {
|
||||||
return Err(CellError::Num);
|
return Err(CellError::Num);
|
||||||
}
|
}
|
||||||
Ok(v.len() as f64 / v.iter().map(|x| 1.0 / x).sum::<f64>())
|
Ok(v.len() as f64 / v.iter().map(|x| 1.0 / x).sum::<f64>())
|
||||||
|
|||||||
@@ -22,7 +22,14 @@ fn t_cdf(t: f64, v: f64) -> f64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn t_pdf(t: f64, v: f64) -> f64 {
|
fn t_pdf(t: f64, v: f64) -> f64 {
|
||||||
(ln_gamma((v + 1.0) / 2.0) - ln_gamma(v / 2.0) - 0.5 * (v * std::f64::consts::PI).ln() - (v + 1.0) / 2.0 * (1.0 + t * t / v).ln()).exp()
|
let z = v / 2.0;
|
||||||
|
let log_gamma_ratio = if z >= 16.0 {
|
||||||
|
let inv_z = 1.0 / z;
|
||||||
|
0.5 * z.ln() - inv_z / 8.0 + inv_z.powi(3) / 192.0 - inv_z.powi(5) / 640.0 + 17.0 * inv_z.powi(7) / 14_336.0
|
||||||
|
} else {
|
||||||
|
ln_gamma(z + 0.5) - ln_gamma(z)
|
||||||
|
};
|
||||||
|
(log_gamma_ratio - 0.5 * (v.ln() + std::f64::consts::PI.ln()) - (v + 1.0) / 2.0 * (t * t / v).ln_1p()).exp()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// t ≥ 0 with P(T > t) = q, q in (0, 0.5].
|
/// t ≥ 0 with P(T > t) = q, q in (0, 0.5].
|
||||||
@@ -51,6 +58,12 @@ fn f_cdf(x: f64, d1: f64, d2: f64) -> f64 {
|
|||||||
if x <= 0.0 {
|
if x <= 0.0 {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
if x == 1.0 && d1 == d2 {
|
||||||
|
return 0.5;
|
||||||
|
}
|
||||||
|
if d1 >= 1e8 && d2 / d1 <= 1e-8 {
|
||||||
|
return reg_gamma_q(d2 / 2.0, d2 / (2.0 * (x + d2 / d1)));
|
||||||
|
}
|
||||||
reg_inc_beta(d1 * x / (d1 * x + d2), d1 / 2.0, d2 / 2.0)
|
reg_inc_beta(d1 * x / (d1 * x + d2), d1 / 2.0, d2 / 2.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +71,10 @@ fn f_upper(x: f64, d1: f64, d2: f64) -> f64 {
|
|||||||
if x <= 0.0 {
|
if x <= 0.0 {
|
||||||
return 1.0;
|
return 1.0;
|
||||||
}
|
}
|
||||||
|
if d1 >= 1e8 && d2 / d1 <= 1e-8 {
|
||||||
|
// With a much larger numerator degree, the scaled beta tail converges to a gamma tail.
|
||||||
|
return reg_gamma_p(d2 / 2.0, d2 / (2.0 * (x + d2 / d1)));
|
||||||
|
}
|
||||||
reg_inc_beta(d2 / (d2 + d1 * x), d2 / 2.0, d1 / 2.0)
|
reg_inc_beta(d2 / (d2 + d1 * x), d2 / 2.0, d1 / 2.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +91,8 @@ fn f_pdf(x: f64, d1: f64, d2: f64) -> f64 {
|
|||||||
((d1 / 2.0) * (d1 / d2).ln() + (d1 / 2.0 - 1.0) * x.ln() - ((d1 + d2) / 2.0) * (1.0 + d1 * x / d2).ln() - ln_beta(d1 / 2.0, d2 / 2.0)).exp()
|
((d1 / 2.0) * (d1 / d2).ln() + (d1 / 2.0 - 1.0) * x.ln() - ((d1 + d2) / 2.0) * (1.0 + d1 * x / d2).ln() - ln_beta(d1 / 2.0, d2 / 2.0)).exp()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_BINOM_TRIALS: f64 = 2_147_483_646.0;
|
||||||
|
|
||||||
fn binom_pmf(k: f64, n: f64, p: f64) -> f64 {
|
fn binom_pmf(k: f64, n: f64, p: f64) -> f64 {
|
||||||
if k < 0.0 || k > n {
|
if k < 0.0 || k > n {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
@@ -176,8 +195,14 @@ fn df_arg(a: &[Arg], i: usize) -> R<f64> {
|
|||||||
|
|
||||||
pub(super) fn t_dist(a: &[Arg], _: C) -> R<Value> {
|
pub(super) fn t_dist(a: &[Arg], _: C) -> R<Value> {
|
||||||
let x = num(a, 0)?;
|
let x = num(a, 0)?;
|
||||||
let v = df_arg(a, 1)?;
|
let v = num(a, 1)?.trunc();
|
||||||
let cum = boolean(a, 2)?;
|
let cum = boolean(a, 2)?;
|
||||||
|
if v == 0.0 {
|
||||||
|
return Err(if cum { CellError::Num } else { CellError::Div0 });
|
||||||
|
}
|
||||||
|
if v < 0.0 {
|
||||||
|
return Err(CellError::Num);
|
||||||
|
}
|
||||||
num_val(if cum { t_cdf(x, v) } else { t_pdf(x, v) })
|
num_val(if cum { t_cdf(x, v) } else { t_pdf(x, v) })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,6 +234,9 @@ pub(super) fn tdist_legacy(a: &[Arg], _: C) -> R<Value> {
|
|||||||
pub(super) fn t_inv(a: &[Arg], _: C) -> R<Value> {
|
pub(super) fn t_inv(a: &[Arg], _: C) -> R<Value> {
|
||||||
let p = num(a, 0)?;
|
let p = num(a, 0)?;
|
||||||
let v = df_arg(a, 1)?;
|
let v = df_arg(a, 1)?;
|
||||||
|
if p == 0.0 {
|
||||||
|
return Err(CellError::Div0);
|
||||||
|
}
|
||||||
if !(p > 0.0 && p < 1.0) {
|
if !(p > 0.0 && p < 1.0) {
|
||||||
return Err(CellError::Num);
|
return Err(CellError::Num);
|
||||||
}
|
}
|
||||||
@@ -347,7 +375,7 @@ fn f_args(a: &[Arg]) -> R<(f64, f64, f64)> {
|
|||||||
let x = num(a, 0)?;
|
let x = num(a, 0)?;
|
||||||
let d1 = num(a, 1)?.trunc();
|
let d1 = num(a, 1)?.trunc();
|
||||||
let d2 = num(a, 2)?.trunc();
|
let d2 = num(a, 2)?.trunc();
|
||||||
if d1 < 1.0 || d2 < 1.0 || d1 >= 1e10 || d2 >= 1e10 {
|
if d1 < 1.0 || d2 < 1.0 || d1 > 1e10 || d2 > 1e10 {
|
||||||
return Err(CellError::Num);
|
return Err(CellError::Num);
|
||||||
}
|
}
|
||||||
Ok((x, d1, d2))
|
Ok((x, d1, d2))
|
||||||
@@ -378,7 +406,8 @@ pub(super) fn f_inv(a: &[Arg], _: C) -> R<Value> {
|
|||||||
if p == 0.0 {
|
if p == 0.0 {
|
||||||
return num_val(0.0);
|
return num_val(0.0);
|
||||||
}
|
}
|
||||||
num_val(invert(|x| f_cdf(x, d1, d2), p, 0.0, 1.0, 1e60))
|
let value = if p < 0.5 { invert(|x| f_cdf(x, d1, d2), p, 0.0, 1.0, 1e60) } else { invert(|x| f_upper(x, d1, d2), 1.0 - p, 0.0, 1.0, 1e60) };
|
||||||
|
num_val(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn f_inv_rt(a: &[Arg], _: C) -> R<Value> {
|
pub(super) fn f_inv_rt(a: &[Arg], _: C) -> R<Value> {
|
||||||
@@ -410,7 +439,7 @@ pub(super) fn binom_dist(a: &[Arg], _: C) -> R<Value> {
|
|||||||
let n = num(a, 1)?.trunc();
|
let n = num(a, 1)?.trunc();
|
||||||
let p = num(a, 2)?;
|
let p = num(a, 2)?;
|
||||||
let cum = boolean(a, 3)?;
|
let cum = boolean(a, 3)?;
|
||||||
if k < 0.0 || k > n || !(0.0..=1.0).contains(&p) {
|
if k < 0.0 || n > MAX_BINOM_TRIALS || k > n || !(0.0..=1.0).contains(&p) {
|
||||||
return Err(CellError::Num);
|
return Err(CellError::Num);
|
||||||
}
|
}
|
||||||
num_val(if cum { binom_cdf(k, n, p) } else { binom_pmf(k, n, p) })
|
num_val(if cum { binom_cdf(k, n, p) } else { binom_pmf(k, n, p) })
|
||||||
@@ -421,7 +450,7 @@ pub(super) fn binom_dist_range(a: &[Arg], _: C) -> R<Value> {
|
|||||||
let p = num(a, 1)?;
|
let p = num(a, 1)?;
|
||||||
let s1 = num(a, 2)?.trunc();
|
let s1 = num(a, 2)?.trunc();
|
||||||
let s2 = if has(a, 3) { num(a, 3)?.trunc() } else { s1 };
|
let s2 = if has(a, 3) { num(a, 3)?.trunc() } else { s1 };
|
||||||
if !(0.0..=1e15).contains(&n) || !(0.0..=1.0).contains(&p) || s1 < 0.0 || s1 > n || s2 < s1 || s2 > n {
|
if !(0.0..=MAX_BINOM_TRIALS).contains(&n) || !(0.0..=1.0).contains(&p) || s1 < 0.0 || s1 > n || s2 < s1 || s2 > n {
|
||||||
return Err(CellError::Num);
|
return Err(CellError::Num);
|
||||||
}
|
}
|
||||||
if s2 - s1 > 10_000.0 {
|
if s2 - s1 > 10_000.0 {
|
||||||
@@ -499,13 +528,7 @@ pub(super) fn gamma_dist(a: &[Arg], _: C) -> R<Value> {
|
|||||||
return num_val(reg_gamma_p(al, x / be));
|
return num_val(reg_gamma_p(al, x / be));
|
||||||
}
|
}
|
||||||
if x == 0.0 {
|
if x == 0.0 {
|
||||||
return if al < 1.0 {
|
return if al <= 1.0 { Err(CellError::Num) } else { num_val(0.0) };
|
||||||
Err(CellError::Num)
|
|
||||||
} else if al == 1.0 {
|
|
||||||
num_val(1.0 / be)
|
|
||||||
} else {
|
|
||||||
num_val(0.0)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
num_val(((al - 1.0) * x.ln() - x / be - al * be.ln() - ln_gamma(al)).exp())
|
num_val(((al - 1.0) * x.ln() - x / be - al * be.ln() - ln_gamma(al)).exp())
|
||||||
}
|
}
|
||||||
@@ -548,7 +571,7 @@ pub(super) fn beta_dist(a: &[Arg], _: C) -> R<Value> {
|
|||||||
if cum {
|
if cum {
|
||||||
return num_val(reg_inc_beta(z, al, be));
|
return num_val(reg_inc_beta(z, al, be));
|
||||||
}
|
}
|
||||||
if (z == 0.0 && al < 1.0) || (z == 1.0 && be < 1.0) {
|
if (z == 0.0 && al <= 1.0) || (z == 1.0 && be <= 1.0) {
|
||||||
return Err(CellError::Num);
|
return Err(CellError::Num);
|
||||||
}
|
}
|
||||||
let lp = if z == 0.0 || z == 1.0 {
|
let lp = if z == 0.0 || z == 1.0 {
|
||||||
@@ -608,6 +631,9 @@ pub(super) fn weibull_dist(a: &[Arg], _: C) -> R<Value> {
|
|||||||
if x < 0.0 || al <= 0.0 || be <= 0.0 {
|
if x < 0.0 || al <= 0.0 || be <= 0.0 {
|
||||||
return Err(CellError::Num);
|
return Err(CellError::Num);
|
||||||
}
|
}
|
||||||
|
if x == 0.0 {
|
||||||
|
return num_val(0.0);
|
||||||
|
}
|
||||||
let t = (x / be).powf(al);
|
let t = (x / be).powf(al);
|
||||||
num_val(if cum { -(-t).exp_m1() } else { al / be.powf(al) * x.powf(al - 1.0) * (-t).exp() })
|
num_val(if cum { -(-t).exp_m1() } else { al / be.powf(al) * x.powf(al - 1.0) * (-t).exp() })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,6 +151,19 @@ fn spread() {
|
|||||||
let g = [4.0, 5.0, 8.0, 7.0, 11.0, 4.0, 3.0];
|
let g = [4.0, 5.0, 8.0, 7.0, 11.0, 4.0, 3.0];
|
||||||
close_tol(ev("GEOMEAN", vec![c(&g)]), 5.476986969, 1e-9);
|
close_tol(ev("GEOMEAN", vec![c(&g)]), 5.476986969, 1e-9);
|
||||||
close_tol(ev("HARMEAN", vec![c(&g)]), 5.028375962, 1e-9);
|
close_tol(ev("HARMEAN", vec![c(&g)]), 5.028375962, 1e-9);
|
||||||
|
let text_values = arr(vec![vec![tv("1"), tv("2")]]);
|
||||||
|
let bool_values = arr(vec![vec![Value::Bool(true), Value::Bool(false)]]);
|
||||||
|
let mixed_values = arr(vec![vec![nv(2.0), tv("ignored"), Value::Bool(true)]]);
|
||||||
|
is_err(ev("HARMEAN", vec![av(text_values.clone())]), CellError::NA);
|
||||||
|
is_err(ev("HARMEAN", vec![av(bool_values.clone())]), CellError::NA);
|
||||||
|
is_err(ev("HARMEAN", vec![rf(text_values)]), CellError::NA);
|
||||||
|
is_err(ev("HARMEAN", vec![rf(bool_values)]), CellError::NA);
|
||||||
|
close(ev("HARMEAN", vec![av(mixed_values.clone())]), 2.0);
|
||||||
|
close(ev("HARMEAN", vec![rf(mixed_values)]), 2.0);
|
||||||
|
close(ev("HARMEAN", vec![b(true)]), 1.0);
|
||||||
|
close(ev("HARMEAN", vec![n(2.0), n(4.0)]), 8.0 / 3.0);
|
||||||
|
is_err(ev("HARMEAN", vec![av(arr(vec![vec![nv(0.0)]]))]), CellError::Num);
|
||||||
|
is_err(ev("HARMEAN", vec![av(arr(vec![vec![nv(-1.0)]]))]), CellError::Num);
|
||||||
is_err(ev("GEOMEAN", vec![c(&[1.0, 0.0])]), CellError::Num);
|
is_err(ev("GEOMEAN", vec![c(&[1.0, 0.0])]), CellError::Num);
|
||||||
close_tol(ev("TRIMMEAN", vec![c(&[4.0, 5.0, 6.0, 7.0, 2.0, 3.0, 4.0, 5.0, 1.0, 2.0, 3.0]), n(0.2)]), 3.777777778, 1e-9);
|
close_tol(ev("TRIMMEAN", vec![c(&[4.0, 5.0, 6.0, 7.0, 2.0, 3.0, 4.0, 5.0, 1.0, 2.0, 3.0]), n(0.2)]), 3.777777778, 1e-9);
|
||||||
is_err(ev("TRIMMEAN", vec![c(&[1.0]), n(1.0)]), CellError::Num);
|
is_err(ev("TRIMMEAN", vec![c(&[1.0]), n(1.0)]), CellError::Num);
|
||||||
@@ -360,6 +373,7 @@ fn normal() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn t_chi_f() {
|
fn t_chi_f() {
|
||||||
|
close_tol(ev("T.DIST", vec![n(0.0), n(6e307), b(false)]), 0.3989422804, 1e-10);
|
||||||
close_tol(ev("T.DIST", vec![n(60.0), n(1.0), b(true)]), 0.99469533, D7);
|
close_tol(ev("T.DIST", vec![n(60.0), n(1.0), b(true)]), 0.99469533, D7);
|
||||||
close_tol(ev("T.DIST", vec![n(8.0), n(3.0), b(false)]), 0.00073691, 1e-5);
|
close_tol(ev("T.DIST", vec![n(8.0), n(3.0), b(false)]), 0.00073691, 1e-5);
|
||||||
close_tol(ev("T.DIST.2T", vec![n(1.959999998), n(60.0)]), 0.054644930, D7);
|
close_tol(ev("T.DIST.2T", vec![n(1.959999998), n(60.0)]), 0.054644930, D7);
|
||||||
@@ -397,6 +411,7 @@ fn t_chi_f() {
|
|||||||
close_tol(ev("F.DIST.RT", vec![n(15.2069), n(6.0), n(4.0)]), 0.01, 1e-6);
|
close_tol(ev("F.DIST.RT", vec![n(15.2069), n(6.0), n(4.0)]), 0.01, 1e-6);
|
||||||
close_tol(ev("FDIST", vec![n(15.2069), n(6.0), n(4.0)]), 0.01, 1e-6);
|
close_tol(ev("FDIST", vec![n(15.2069), n(6.0), n(4.0)]), 0.01, 1e-6);
|
||||||
close_tol(ev("F.INV", vec![n(0.01), n(6.0), n(4.0)]), 0.10930991, D7);
|
close_tol(ev("F.INV", vec![n(0.01), n(6.0), n(4.0)]), 0.10930991, D7);
|
||||||
|
close_tol(ev("F.INV", vec![n(0.99), n(6.0), n(4.0)]), 15.20686, D7);
|
||||||
close_tol(ev("F.INV.RT", vec![n(0.01), n(6.0), n(4.0)]), 15.20686, D7);
|
close_tol(ev("F.INV.RT", vec![n(0.01), n(6.0), n(4.0)]), 15.20686, D7);
|
||||||
close_tol(ev("FINV", vec![n(0.01), n(6.0), n(4.0)]), 15.20686, D7);
|
close_tol(ev("FINV", vec![n(0.01), n(6.0), n(4.0)]), 15.20686, D7);
|
||||||
close_tol(ev("F.TEST", vec![c(&[6.0, 7.0, 9.0, 15.0, 21.0]), c(&[20.0, 28.0, 31.0, 38.0, 40.0])]), 0.64831785, D7);
|
close_tol(ev("F.TEST", vec![c(&[6.0, 7.0, 9.0, 15.0, 21.0]), c(&[20.0, 28.0, 31.0, 38.0, 40.0])]), 0.64831785, D7);
|
||||||
@@ -404,6 +419,48 @@ fn t_chi_f() {
|
|||||||
is_err(ev("F.DIST", vec![n(1.0), n(0.0), n(4.0), b(true)]), CellError::Num);
|
is_err(ev("F.DIST", vec![n(1.0), n(0.0), n(4.0), b(true)]), CellError::Num);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn f_distributions_accept_ten_billion_degrees() {
|
||||||
|
close_tol(ev("F.DIST", vec![n(0.0), n(10_000_000_000.0), n(1.0), b(false)]), 0.0, 0.0);
|
||||||
|
close_tol(ev("F.DIST.RT", vec![n(0.0), n(1.0), n(10_000_000_000.0)]), 1.0, 0.0);
|
||||||
|
close_tol(ev("F.DIST", vec![n(0.01), n(1_000_000_000.0), n(1.0), b(true)]), 1.52397e-23, 1e-28);
|
||||||
|
close_tol(ev("F.DIST", vec![n(1.0), n(10_000_000_000.0), n(10_000_000_000.0), b(true)]), 0.5, 0.0);
|
||||||
|
close_tol(ev("F.DIST", vec![n(1_000_000.0), n(10_000_000_000.0), n(1.0), b(true)]), 0.999202115, 1e-8);
|
||||||
|
close_tol(ev("F.INV", vec![n(0.5), n(10_000_000_000.0), n(1.0)]), 2.198110953542527, 2e-6);
|
||||||
|
close_tol(ev("F.DIST.RT", vec![n(0.0), n(9_999_999_999.0), n(1.0)]), 1.0, 0.0);
|
||||||
|
is_err(ev("F.DIST.RT", vec![n(0.0), n(10_000_000_001.0), n(1.0)]), CellError::Num);
|
||||||
|
is_err(ev("F.DIST.RT", vec![n(0.0), n(1.0), n(10_000_000_001.0)]), CellError::Num);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn t_dist_zero_degrees_error_depends_on_mode() {
|
||||||
|
for df in [0.0, 0.9, -0.1] {
|
||||||
|
is_err(ev("T.DIST", vec![n(0.0), n(df), b(false)]), CellError::Div0);
|
||||||
|
is_err(ev("T.DIST", vec![n(0.0), n(df), b(true)]), CellError::Num);
|
||||||
|
}
|
||||||
|
is_err(ev("T.DIST", vec![n(0.0), n(-1.0), b(false)]), CellError::Num);
|
||||||
|
close_tol(ev("T.DIST", vec![n(0.0), n(1.99), b(false)]), 1.0 / std::f64::consts::PI, 1e-15);
|
||||||
|
close_tol(ev("T.DIST", vec![n(0.0), n(1.0), b(false)]), 1.0 / std::f64::consts::PI, 1e-15);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn t_density_is_stable_for_large_degrees() {
|
||||||
|
close_tol(ev("T.DIST", vec![n(0.0), n(100_000_000.0), b(false)]), 0.398942279404077, 1e-12);
|
||||||
|
close_tol(ev("T.DIST", vec![n(0.0), n(10_000_000_000.0), b(false)]), 0.3989422803914591, 1e-12);
|
||||||
|
close_tol(ev("T.DIST", vec![n(1.0), n(10_000_000_000_000_000.0), b(false)]), 0.24197072451914337, 1e-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn t_inv_zero_probability_returns_div0() {
|
||||||
|
is_err(ev("T.INV", vec![n(-0.1), n(1.0)]), CellError::Num);
|
||||||
|
is_err(ev("T.INV", vec![n(0.0), n(1.0)]), CellError::Div0);
|
||||||
|
close_tol(ev("T.INV", vec![n(0.75), n(2.0)]), 0.8164966, D7);
|
||||||
|
is_err(ev("T.INV", vec![n(1.0), n(1.0)]), CellError::Num);
|
||||||
|
is_err(ev("T.INV", vec![n(1.1), n(1.0)]), CellError::Num);
|
||||||
|
is_err(ev("T.INV", vec![n(0.0), n(0.0)]), CellError::Num);
|
||||||
|
is_err(ev("T.INV.2T", vec![n(0.0), n(1.0)]), CellError::Num);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn binom_inv_huge_trials_returns() {
|
fn binom_inv_huge_trials_returns() {
|
||||||
// Above 2^53 neighbouring f64 values are more than 1 apart; the bisection must still end.
|
// Above 2^53 neighbouring f64 values are more than 1 apart; the bisection must still end.
|
||||||
@@ -427,7 +484,15 @@ fn discrete() {
|
|||||||
close_tol(ev("BINOM.DIST", vec![n(6.0), n(10.0), n(0.5), b(false)]), 0.205078125, 1e-12);
|
close_tol(ev("BINOM.DIST", vec![n(6.0), n(10.0), n(0.5), b(false)]), 0.205078125, 1e-12);
|
||||||
close_tol(ev("BINOM.DIST", vec![n(6.0), n(10.0), n(0.5), b(true)]), 0.828125, 1e-12);
|
close_tol(ev("BINOM.DIST", vec![n(6.0), n(10.0), n(0.5), b(true)]), 0.828125, 1e-12);
|
||||||
close_tol(ev("BINOMDIST", vec![n(6.0), n(10.0), n(0.5), b(false)]), 0.205078125, 1e-12);
|
close_tol(ev("BINOMDIST", vec![n(6.0), n(10.0), n(0.5), b(false)]), 0.205078125, 1e-12);
|
||||||
|
close(ev("BINOM.DIST", vec![n(1.0), n(2_147_483_646.0), n(0.0), b(false)]), 0.0);
|
||||||
|
close(ev("BINOM.DIST", vec![n(1.0), n(2_147_483_646.9), n(0.0), b(false)]), 0.0);
|
||||||
|
is_err(ev("BINOM.DIST", vec![n(1.0), n(2_147_483_647.0), n(0.0), b(false)]), CellError::Num);
|
||||||
|
is_err(ev("BINOM.DIST", vec![n(1.0), n(2_147_483_647.9), n(0.0), b(false)]), CellError::Num);
|
||||||
is_err(ev("BINOM.DIST", vec![n(11.0), n(10.0), n(0.5), b(false)]), CellError::Num);
|
is_err(ev("BINOM.DIST", vec![n(11.0), n(10.0), n(0.5), b(false)]), CellError::Num);
|
||||||
|
close(ev("BINOM.DIST.RANGE", vec![n(2_147_483_646.0), n(0.0), n(1.0)]), 0.0);
|
||||||
|
close(ev("BINOM.DIST.RANGE", vec![n(2_147_483_646.9), n(0.0), n(1.0)]), 0.0);
|
||||||
|
is_err(ev("BINOM.DIST.RANGE", vec![n(2_147_483_647.0), n(0.0), n(1.0)]), CellError::Num);
|
||||||
|
is_err(ev("BINOM.DIST.RANGE", vec![n(2_147_483_647.9), n(0.0), n(1.0)]), CellError::Num);
|
||||||
close_tol(ev("BINOM.DIST.RANGE", vec![n(60.0), n(0.75), n(48.0)]), 0.083974967, 1e-6);
|
close_tol(ev("BINOM.DIST.RANGE", vec![n(60.0), n(0.75), n(48.0)]), 0.083974967, 1e-6);
|
||||||
close_tol(ev("BINOM.DIST.RANGE", vec![n(60.0), n(0.75), n(45.0), n(50.0)]), 0.523629793, 1e-6);
|
close_tol(ev("BINOM.DIST.RANGE", vec![n(60.0), n(0.75), n(45.0), n(50.0)]), 0.523629793, 1e-6);
|
||||||
close(ev("BINOM.INV", vec![n(6.0), n(0.5), n(0.75)]), 4.0);
|
close(ev("BINOM.INV", vec![n(6.0), n(0.5), n(0.75)]), 4.0);
|
||||||
@@ -466,6 +531,19 @@ fn continuous() {
|
|||||||
is_err(ev("GAMMA", vec![n(0.0)]), CellError::Num);
|
is_err(ev("GAMMA", vec![n(0.0)]), CellError::Num);
|
||||||
close_tol(ev("BETA.DIST", vec![n(2.0), n(8.0), n(10.0), b(true), n(1.0), n(3.0)]), 0.6854706, D7);
|
close_tol(ev("BETA.DIST", vec![n(2.0), n(8.0), n(10.0), b(true), n(1.0), n(3.0)]), 0.6854706, D7);
|
||||||
close_tol(ev("BETA.DIST", vec![n(2.0), n(8.0), n(10.0), b(false), n(1.0), n(3.0)]), 1.4837646, D7);
|
close_tol(ev("BETA.DIST", vec![n(2.0), n(8.0), n(10.0), b(false), n(1.0), n(3.0)]), 1.4837646, D7);
|
||||||
|
is_err(ev("BETA.DIST", vec![n(0.0), n(1.0), n(1.0), b(false)]), CellError::Num);
|
||||||
|
is_err(ev("BETA.DIST", vec![n(1.0), n(1.0), n(1.0), b(false)]), CellError::Num);
|
||||||
|
is_err(ev("BETA.DIST", vec![n(0.0), n(0.5), n(2.0), b(false)]), CellError::Num);
|
||||||
|
is_err(ev("BETA.DIST", vec![n(1.0), n(2.0), n(0.5), b(false)]), CellError::Num);
|
||||||
|
close(ev("BETA.DIST", vec![n(0.0), n(2.0), n(1.0), b(false)]), 0.0);
|
||||||
|
close(ev("BETA.DIST", vec![n(1.0), n(1.0), n(2.0), b(false)]), 0.0);
|
||||||
|
close(ev("BETA.DIST", vec![n(0.0), n(1.0), n(1.0), b(true)]), 0.0);
|
||||||
|
close(ev("BETA.DIST", vec![n(1.0), n(1.0), n(1.0), b(true)]), 1.0);
|
||||||
|
is_err(ev("BETA.DIST", vec![n(2.0), n(1.0), n(2.0), b(false), n(2.0), n(6.0)]), CellError::Num);
|
||||||
|
is_err(ev("BETA.DIST", vec![n(6.0), n(2.0), n(1.0), b(false), n(2.0), n(6.0)]), CellError::Num);
|
||||||
|
close(ev("BETA.DIST", vec![n(4.0), n(1.0), n(1.0), b(false), n(2.0), n(6.0)]), 0.25);
|
||||||
|
close(ev("BETA.DIST", vec![n(2.0), n(1.0), n(1.0), b(true), n(2.0), n(6.0)]), 0.0);
|
||||||
|
close(ev("BETA.DIST", vec![n(6.0), n(1.0), n(1.0), b(true), n(2.0), n(6.0)]), 1.0);
|
||||||
close_tol(ev("BETADIST", vec![n(2.0), n(8.0), n(10.0), n(1.0), n(3.0)]), 0.6854706, D7);
|
close_tol(ev("BETADIST", vec![n(2.0), n(8.0), n(10.0), n(1.0), n(3.0)]), 0.6854706, D7);
|
||||||
close_tol(ev("BETA.INV", vec![n(0.685470581), n(8.0), n(10.0), n(1.0), n(3.0)]), 2.0, D7);
|
close_tol(ev("BETA.INV", vec![n(0.685470581), n(8.0), n(10.0), n(1.0), n(3.0)]), 2.0, D7);
|
||||||
close_tol(ev("BETAINV", vec![n(0.685470581), n(8.0), n(10.0), n(1.0), n(3.0)]), 2.0, D7);
|
close_tol(ev("BETAINV", vec![n(0.685470581), n(8.0), n(10.0), n(1.0), n(3.0)]), 2.0, D7);
|
||||||
@@ -479,6 +557,30 @@ fn continuous() {
|
|||||||
close_tol(r[0][1].clone(), 0.841344746, 1e-8);
|
close_tol(r[0][1].clone(), 0.841344746, 1e-8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gamma_density_at_zero_shape_one_is_num() {
|
||||||
|
for scale in [0.1, 0.9, 1.0, 1.9, 2.0, 10_000_000_000.0] {
|
||||||
|
is_err(ev("GAMMA.DIST", vec![n(0.0), n(1.0), n(scale), b(false)]), CellError::Num);
|
||||||
|
}
|
||||||
|
is_err(ev("GAMMA.DIST", vec![n(0.0), n(0.5), n(1.0), b(false)]), CellError::Num);
|
||||||
|
close_tol(ev("GAMMA.DIST", vec![n(0.0), n(2.0), n(1.0), b(false)]), 0.0, 0.0);
|
||||||
|
close_tol(ev("GAMMA.DIST", vec![n(0.0), n(1.0), n(2.0), b(true)]), 0.0, 0.0);
|
||||||
|
close_tol(ev("GAMMA.DIST", vec![n(2.0), n(1.0), n(2.0), b(false)]), 0.5 * (-1.0f64).exp(), 1e-15);
|
||||||
|
close_tol(ev("GAMMA.DIST", vec![n(2.0), n(1.0), n(2.0), b(true)]), 1.0 - (-1.0f64).exp(), 1e-15);
|
||||||
|
is_err(ev("GAMMA.DIST", vec![n(-0.1), n(1.0), n(2.0), b(false)]), CellError::Num);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn weibull_density_at_zero_is_zero() {
|
||||||
|
for shape in [0.5, 1.0, 2.0] {
|
||||||
|
close_tol(ev("WEIBULL.DIST", vec![n(0.0), n(shape), n(1.0), b(false)]), 0.0, 0.0);
|
||||||
|
close_tol(ev("WEIBULL.DIST", vec![n(0.0), n(shape), n(1.0), b(true)]), 0.0, 0.0);
|
||||||
|
}
|
||||||
|
close_tol(ev("WEIBULL.DIST", vec![n(1.0), n(1.0), n(1.0), b(false)]), (-1.0f64).exp(), 1e-15);
|
||||||
|
close_tol(ev("WEIBULL.DIST", vec![n(1.0), n(1.0), n(1.0), b(true)]), 1.0 - (-1.0f64).exp(), 1e-15);
|
||||||
|
is_err(ev("WEIBULL.DIST", vec![n(-0.1), n(1.0), n(1.0), b(false)]), CellError::Num);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn aggregate_helper() {
|
fn aggregate_helper() {
|
||||||
let vals = vec![nv(1.0), nv(2.0), tv("x"), Value::Empty, Value::Bool(true), nv(6.0)];
|
let vals = vec![nv(1.0), nv(2.0), tv("x"), Value::Empty, Value::Bool(true), nv(6.0)];
|
||||||
|
|||||||
+113
-12
@@ -111,12 +111,31 @@ fn len(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
|||||||
num_val(text(a, 0)?.chars().count() as f64)
|
num_val(text(a, 0)?.chars().count() as f64)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn lower_char(ch: char) -> char {
|
||||||
|
match ch.to_lowercase().next() {
|
||||||
|
Some(lower) => lower,
|
||||||
|
None => ch,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn upper_char(ch: char) -> char {
|
||||||
|
let mut uppercase = ch.to_uppercase();
|
||||||
|
match uppercase.next() {
|
||||||
|
Some(upper) if uppercase.next().is_none() => upper,
|
||||||
|
_ => ch,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn proper_upper_char(ch: char) -> char {
|
||||||
|
if ch == '\u{130}' { 'I' } else { upper_char(ch) }
|
||||||
|
}
|
||||||
|
|
||||||
fn lower(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
fn lower(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
||||||
text_val(text(a, 0)?.to_lowercase())
|
text_val(text(a, 0)?.chars().map(lower_char).collect::<String>())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn upper(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
fn upper(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
||||||
text_val(text(a, 0)?.to_uppercase())
|
text_val(text(a, 0)?.chars().map(upper_char).collect::<String>())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn proper(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
fn proper(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
||||||
@@ -126,9 +145,9 @@ fn proper(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
|||||||
for ch in s.chars() {
|
for ch in s.chars() {
|
||||||
if ch.is_alphabetic() {
|
if ch.is_alphabetic() {
|
||||||
if prev_letter {
|
if prev_letter {
|
||||||
out.extend(ch.to_lowercase());
|
out.push(lower_char(ch));
|
||||||
} else {
|
} else {
|
||||||
out.extend(ch.to_uppercase());
|
out.push(proper_upper_char(ch));
|
||||||
}
|
}
|
||||||
prev_letter = true;
|
prev_letter = true;
|
||||||
} else {
|
} else {
|
||||||
@@ -281,7 +300,11 @@ fn unichar(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
|||||||
if !(1.0..=1_114_111.0).contains(&n) {
|
if !(1.0..=1_114_111.0).contains(&n) {
|
||||||
return Err(CellError::Value);
|
return Err(CellError::Value);
|
||||||
}
|
}
|
||||||
match char::from_u32(n as u32) {
|
let cp = n as u32;
|
||||||
|
if (0xFDD0..=0xFDEF).contains(&cp) || cp & 0xFFFE == 0xFFFE {
|
||||||
|
return Err(CellError::NA);
|
||||||
|
}
|
||||||
|
match char::from_u32(cp) {
|
||||||
Some(c) => text_val(c.to_string()),
|
Some(c) => text_val(c.to_string()),
|
||||||
None => Err(CellError::NA),
|
None => Err(CellError::NA),
|
||||||
}
|
}
|
||||||
@@ -350,10 +373,10 @@ fn numbervalue(a: &[Arg], c: &mut dyn Ctx) -> R<Value> {
|
|||||||
/// Formats `x` (already rounded) with `decimals` places and optional thousands separators.
|
/// Formats `x` (already rounded) with `decimals` places and optional thousands separators.
|
||||||
fn format_fixed(x: f64, decimals: i64, commas: bool) -> String {
|
fn format_fixed(x: f64, decimals: i64, commas: bool) -> String {
|
||||||
let d = decimals.max(0) as usize;
|
let d = decimals.max(0) as usize;
|
||||||
let s = format!("{:.*}", d, x.abs());
|
let s = x.abs().to_string();
|
||||||
let (int_part, frac) = match s.split_once('.') {
|
let (int_part, frac) = match s.split_once('.') {
|
||||||
Some((i, f)) => (i.to_string(), Some(f.to_string())),
|
Some((i, f)) => (i, f),
|
||||||
None => (s.clone(), None),
|
None => (s.as_str(), ""),
|
||||||
};
|
};
|
||||||
let int_part = if commas {
|
let int_part = if commas {
|
||||||
let b = int_part.as_bytes();
|
let b = int_part.as_bytes();
|
||||||
@@ -366,12 +389,14 @@ fn format_fixed(x: f64, decimals: i64, commas: bool) -> String {
|
|||||||
}
|
}
|
||||||
o
|
o
|
||||||
} else {
|
} else {
|
||||||
int_part
|
int_part.to_string()
|
||||||
};
|
};
|
||||||
let mut out = int_part;
|
let mut out = int_part;
|
||||||
if let Some(f) = frac {
|
if d > 0 {
|
||||||
|
let frac_len = frac.len().min(d);
|
||||||
out.push('.');
|
out.push('.');
|
||||||
out.push_str(&f);
|
out.push_str(&frac[..frac_len]);
|
||||||
|
out.extend(std::iter::repeat_n('0', d - frac_len));
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
@@ -394,13 +419,21 @@ fn fixed(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
|||||||
let (r, d) = rounded(a, 2.0)?;
|
let (r, d) = rounded(a, 2.0)?;
|
||||||
let no_commas = opt_bool(a, 2, false)?;
|
let no_commas = opt_bool(a, 2, false)?;
|
||||||
let body = format_fixed(r, d, !no_commas);
|
let body = format_fixed(r, d, !no_commas);
|
||||||
|
if body.len() + if r < 0.0 { 1 } else { 0 } > 255 {
|
||||||
|
return Err(CellError::Value);
|
||||||
|
}
|
||||||
text_val(if r < 0.0 { format!("-{body}") } else { body })
|
text_val(if r < 0.0 { format!("-{body}") } else { body })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dollar(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
fn dollar(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
||||||
let (r, d) = rounded(a, 2.0)?;
|
let (r, d) = rounded(a, 2.0)?;
|
||||||
|
let negative = r < 0.0 || (r == 0.0 && d >= 0 && num(a, 0)? < 0.0);
|
||||||
let body = format_fixed(r, d, true);
|
let body = format_fixed(r, d, true);
|
||||||
text_val(if r < 0.0 { format!("(${body})") } else { format!("${body}") })
|
let out = if negative { format!("(${body})") } else { format!("${body}") };
|
||||||
|
if out.len() > 254 {
|
||||||
|
return Err(CellError::Value);
|
||||||
|
}
|
||||||
|
text_val(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------
|
||||||
@@ -1123,6 +1156,20 @@ mod tests {
|
|||||||
fn case_and_cleanup() {
|
fn case_and_cleanup() {
|
||||||
is_text(ev("UPPER", vec![t("abc")]), "ABC");
|
is_text(ev("UPPER", vec![t("abc")]), "ABC");
|
||||||
is_text(ev("LOWER", vec![t("ABC")]), "abc");
|
is_text(ev("LOWER", vec![t("ABC")]), "abc");
|
||||||
|
is_text(ev("UPPER", vec![t("Straße")]), "STRAßE");
|
||||||
|
is_text(ev("LOWER", vec![t("İ")]), "i");
|
||||||
|
is_text(ev("PROPER", vec![t("İSTANBUL")]), "Istanbul");
|
||||||
|
is_text(ev("PROPER", vec![t("ß")]), "ß");
|
||||||
|
is_text(ev("PROPER", vec![t("ÉCOLE")]), "École");
|
||||||
|
let proper = ev("PROPER", vec![t("İSTANBUL Straße")]);
|
||||||
|
is_text(proper.clone(), "Istanbul Straße");
|
||||||
|
assert_eq!(
|
||||||
|
match proper {
|
||||||
|
Value::Text(s) => s.chars().count(),
|
||||||
|
_ => 0,
|
||||||
|
},
|
||||||
|
"Istanbul Straße".chars().count()
|
||||||
|
);
|
||||||
is_text(ev("PROPER", vec![t("this is a TITLE")]), "This Is A Title");
|
is_text(ev("PROPER", vec![t("this is a TITLE")]), "This Is A Title");
|
||||||
is_text(ev("PROPER", vec![t("2-way street's")]), "2-Way Street'S");
|
is_text(ev("PROPER", vec![t("2-way street's")]), "2-Way Street'S");
|
||||||
is_text(ev("TRIM", vec![t(" a b c ")]), "a b c");
|
is_text(ev("TRIM", vec![t(" a b c ")]), "a b c");
|
||||||
@@ -1181,8 +1228,15 @@ mod tests {
|
|||||||
close(ev("CODE", vec![t("€")]), 128.0);
|
close(ev("CODE", vec![t("€")]), 128.0);
|
||||||
is_err(ev("CODE", vec![t("")]), CellError::Value);
|
is_err(ev("CODE", vec![t("")]), CellError::Value);
|
||||||
is_text(ev("UNICHAR", vec![n(9731.0)]), "☃");
|
is_text(ev("UNICHAR", vec![n(9731.0)]), "☃");
|
||||||
|
for cp in [0xFDD0, 0xFDEF, 0xFFFE, 0xFFFF, 0x1FFFE, 0x1FFFF, 0x10FFFE, 0x10FFFF] {
|
||||||
|
is_err(ev("UNICHAR", vec![n(f64::from(cp))]), CellError::NA);
|
||||||
|
}
|
||||||
|
for (cp, expected) in [(0xFDCF, "\u{FDCF}"), (0xFDF0, "\u{FDF0}"), (0xFFFD, "\u{FFFD}"), (0x1FFFD, "\u{1FFFD}")] {
|
||||||
|
is_text(ev("UNICHAR", vec![n(f64::from(cp))]), expected);
|
||||||
|
}
|
||||||
close(ev("UNICODE", vec![t("☃")]), 9731.0);
|
close(ev("UNICODE", vec![t("☃")]), 9731.0);
|
||||||
is_err(ev("UNICHAR", vec![n(0.0)]), CellError::Value);
|
is_err(ev("UNICHAR", vec![n(0.0)]), CellError::Value);
|
||||||
|
is_err(ev("UNICHAR", vec![n(1_114_112.0)]), CellError::Value);
|
||||||
is_err(ev("UNICHAR", vec![n(55296.0)]), CellError::NA);
|
is_err(ev("UNICHAR", vec![n(55296.0)]), CellError::NA);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1237,10 +1291,57 @@ mod tests {
|
|||||||
is_text(ev("DOLLAR", vec![n(0.5), n(0.0)]), "$1");
|
is_text(ev("DOLLAR", vec![n(0.5), n(0.0)]), "$1");
|
||||||
is_text(ev("USDOLLAR", vec![n(1.0)]), "$1.00");
|
is_text(ev("USDOLLAR", vec![n(1.0)]), "$1.00");
|
||||||
is_text(ev("USDOLLAR", vec![n(-1234.567), n(1.0)]), "($1,234.6)");
|
is_text(ev("USDOLLAR", vec![n(-1234.567), n(1.0)]), "($1,234.6)");
|
||||||
|
let fixed_16 = format!("1.1{}", "0".repeat(15));
|
||||||
|
let dollar_16 = format!("$1.1{}", "0".repeat(15));
|
||||||
|
let fixed_20 = format!("1.005{}", "0".repeat(17));
|
||||||
|
let dollar_20 = format!("$1.005{}", "0".repeat(17));
|
||||||
|
let fixed_127 = format!("1.1{}", "0".repeat(126));
|
||||||
|
let dollar_127 = format!("$1.1{}", "0".repeat(126));
|
||||||
|
is_text(ev("FIXED", vec![n(1.1), n(16.0)]), &fixed_16);
|
||||||
|
is_text(ev("DOLLAR", vec![n(1.1), n(16.0)]), &dollar_16);
|
||||||
|
is_text(ev("FIXED", vec![n(1.005), n(20.0)]), &fixed_20);
|
||||||
|
is_text(ev("DOLLAR", vec![n(1.005), n(20.0)]), &dollar_20);
|
||||||
|
is_text(ev("FIXED", vec![n(1.1), n(127.0)]), &fixed_127);
|
||||||
|
is_text(ev("DOLLAR", vec![n(1.1), n(127.0)]), &dollar_127);
|
||||||
is_text(ev("T", vec![t("x")]), "x");
|
is_text(ev("T", vec![t("x")]), "x");
|
||||||
is_text(ev("T", vec![n(1.0)]), "");
|
is_text(ev("T", vec![n(1.0)]), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fixed_and_dollar_output_limits() {
|
||||||
|
assert!(matches!(ev("FIXED", vec![n(1e191), n(0.0)]), Value::Text(ref s) if s.len() == 255));
|
||||||
|
is_err(ev("FIXED", vec![n(1e192), n(0.0)]), CellError::Value);
|
||||||
|
assert!(matches!(ev("FIXED", vec![n(-1e253), n(0.0), b(true)]), Value::Text(ref s) if s.starts_with('-')));
|
||||||
|
is_err(ev("FIXED", vec![n(-1e191), n(0.0)]), CellError::Value);
|
||||||
|
assert!(matches!(ev("FIXED", vec![n(1e189), n(1.0)]), Value::Text(ref s) if s.len() == 255));
|
||||||
|
is_err(ev("FIXED", vec![n(1e190), n(1.0)]), CellError::Value);
|
||||||
|
|
||||||
|
assert!(matches!(ev("DOLLAR", vec![n(1e189), n(0.0)]), Value::Text(ref s) if s.len() == 254));
|
||||||
|
is_err(ev("DOLLAR", vec![n(1e190), n(0.0)]), CellError::Value);
|
||||||
|
assert!(matches!(ev("DOLLAR", vec![n(-1e188), n(0.0)]), Value::Text(ref s) if s.len() == 254));
|
||||||
|
is_err(ev("DOLLAR", vec![n(-1e189), n(0.0)]), CellError::Value);
|
||||||
|
assert!(matches!(ev("DOLLAR", vec![n(1.23456789e187), n(2.0)]), Value::Text(ref s) if s.len() == 254));
|
||||||
|
is_err(ev("DOLLAR", vec![n(1.23456789e188), n(2.0)]), CellError::Value);
|
||||||
|
is_err(ev("DOLLAR", vec![n(-1.23456789e186), n(2.0)]), CellError::Value);
|
||||||
|
assert!(matches!(ev("DOLLAR", vec![n(1e189), n(-1.0)]), Value::Text(ref s) if s.len() == 254));
|
||||||
|
is_err(ev("DOLLAR", vec![n(1e190), n(-1.0)]), CellError::Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dollar_keeps_negative_sign_when_nonnegative_precision_rounds_to_zero() {
|
||||||
|
is_text(ev("DOLLAR", vec![n(-0.0001), n(0.0)]), "($0)");
|
||||||
|
is_text(ev("DOLLAR", vec![n(-0.0001), n(2.0)]), "($0.00)");
|
||||||
|
is_text(ev("DOLLAR", vec![n(0.0), n(0.0)]), "$0");
|
||||||
|
is_text(ev("DOLLAR", vec![n(-0.0), n(0.0)]), "$0");
|
||||||
|
is_text(ev("DOLLAR", vec![n(0.0001), n(0.0)]), "$0");
|
||||||
|
is_text(ev("DOLLAR", vec![n(-0.0001), n(-1.0)]), "$0");
|
||||||
|
is_text(ev("DOLLAR", vec![n(-4.0), n(-1.0)]), "$0");
|
||||||
|
is_text(ev("DOLLAR", vec![n(-5.0), n(-1.0)]), "($10)");
|
||||||
|
is_text(ev("DOLLAR", vec![n(-0.4), n(0.0)]), "($0)");
|
||||||
|
is_text(ev("DOLLAR", vec![n(-0.5), n(0.0)]), "($1)");
|
||||||
|
is_text(ev("FIXED", vec![n(-0.0001), n(0.0)]), "0");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn before_after() {
|
fn before_after() {
|
||||||
is_text(ev("TEXTBEFORE", vec![t("Red riding hood"), t(" ")]), "Red");
|
is_text(ev("TEXTBEFORE", vec![t("Red riding hood"), t(" ")]), "Red");
|
||||||
|
|||||||
+24
-1
@@ -1,6 +1,6 @@
|
|||||||
# Function parity: worksheet functions and formulas
|
# Function parity: worksheet functions and formulas
|
||||||
|
|
||||||
> **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-27 · **Change:** minor (ECMA.CEILING listed with USDOLLAR as not on Microsoft's list; earlier: IMSQRT reproduces the stray real part Excel gives for negative reals, and the other IM* differences from Excel are listed; earlier: the LAMBDA row lists the recursion limit and its `#NUM!`, and the error a defined name holding an error gives when called; earlier: number text with accounting parentheses coerces like Excel 365; INDEX with one index on a 2-D reference is #REF! as in Excel; earlier: SUBTOTAL and AGGREGATE cap the cells they visit, #2175) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
> **Last reviewed:** 2026-10-31 · **Last updated:** 2026-10-31 · **Change:** minor (edge cases matched to Excel: base conversions, BITLSHIFT/BITRSHIFT, UNICHAR noncharacters, UPPER/LOWER/PROPER casing, FIXED/DOLLAR output, F/T/GAMMA/WEIBULL/BETA distributions, BINOM trial cap, HARMEAN; earlier: ECMA.CEILING listed with USDOLLAR as not on Microsoft's list; earlier: IMSQRT reproduces the stray real part Excel gives for negative reals) · **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
|
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).
|
[target-app-parity.md](target-app-parity.md); gaps feed [gaps.md](gaps.md).
|
||||||
@@ -98,6 +98,12 @@ This improves series preparation; it does not change the model-fitting or season
|
|||||||
|
|
||||||
## Depth (estimated)
|
## Depth (estimated)
|
||||||
|
|
||||||
|
| Function | Excel parity note |
|
||||||
|
|---|---|
|
||||||
|
| BINOM.DIST / BINOM.DIST.RANGE | Trial count is truncated, then must be at most 2,147,483,646; larger trials return `#NUM!`. |
|
||||||
|
| BETA.DIST | Density mode returns `#NUM!` at the lower or upper interval endpoint when the corresponding shape parameter is less than or equal to 1; cumulative endpoint results remain 0 and 1. |
|
||||||
|
| HARMEAN | Returns `#N/A` when arrays and references contain no numeric observations; zero and negative numeric observations still return `#NUM!`. |
|
||||||
|
|
||||||
**~82% ready.** The library is broad and has 96 test functions in `crates/functions` with many
|
**~82% ready.** The library is broad and has 96 test functions in `crates/functions` with many
|
||||||
assertions each, but there is no conformance suite against Excel-computed results, and user
|
assertions each, but there is no conformance suite against Excel-computed results, and user
|
||||||
reports keep finding edge cases: MIN() with no arguments (#138), COUNTBLANK past the used range
|
reports keep finding edge cases: MIN() with no arguments (#138), COUNTBLANK past the used range
|
||||||
@@ -124,6 +130,23 @@ workbooks; results checked in as data, not Excel files) and fixing what it finds
|
|||||||
|
|
||||||
| Date | Change | Summary |
|
| Date | Change | Summary |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| 2026-10-31 | minor | BIN2*, HEX2* and OCT2* reject surrounding spaces and tabs in source strings with `#NUM!`, matching Excel; plain unpadded values remain valid |
|
||||||
|
| 2026-10-31 | minor | DEC2BIN, DEC2HEX and DEC2OCT validate `places` before returning negative two's-complement output, and Boolean places return `#VALUE!`, matching Excel |
|
||||||
|
| 2026-10-31 | minor | BITLSHIFT and BITRSHIFT return `0` for zero operands shifted beyond 53 bits in either direction; nonzero operands retain the `#NUM!` limit, matching Excel |
|
||||||
|
| 2026-10-31 | minor | UNICHAR returns `#N/A` for U+FDD0–U+FDEF and U+FFFE/U+FFFF on every plane; adjacent valid characters and existing invalid-range/surrogate errors remain unchanged, matching Excel |
|
||||||
|
| 2026-10-31 | minor | UPPER, LOWER and PROPER use single-character casing: `UPPER("Straße")` keeps ß, `LOWER("İ")` returns i, and `PROPER("İSTANBUL")` returns Istanbul, matching Windows Excel 16.0 build 20527 in LCID 1033 |
|
||||||
|
| 2026-10-31 | minor | FIXED and DOLLAR format the rounded number's shortest decimal representation before padding requested fractional zeroes, so 1.1 at 16 places and 1.005 at 20 places do not expose binary noise; measured in Windows Excel 16.0 build 20527, LCID 1033 |
|
||||||
|
| 2026-10-31 | minor | FIXED accepts output through 255 characters; DOLLAR accepts output through 254. Windows Excel 16.0 build 20527 (LCID 1033) confirmed the distinct cutoffs with grouped and ungrouped values, signs, currency layout and decimal places |
|
||||||
|
| 2026-10-31 | minor | DOLLAR uses the input sign for negative amounts that round to zero at nonnegative decimal places (`DOLLAR(-0.0001,0)` is `($0)`); when negative decimal places round to zero, Excel returns `$0`. Exact zero and negative zero stay unsigned; FIXED still returns `0` for `FIXED(-0.0001,0)` |
|
||||||
|
| 2026-10-31 | minor | F.DIST, F.DIST.RT, F.INV and F.INV.RT accept degrees of freedom at the 10-billion boundary; F.INV uses the complementary tail, with a gamma-tail limit when numerator degrees dominate, to preserve inverse accuracy; values above the limit remain invalid |
|
||||||
|
| 2026-10-31 | minor | T.DIST returns `#DIV/0!` in density mode and `#NUM!` in cumulative mode when degrees truncate to zero; negative integer degrees remain `#NUM!` |
|
||||||
|
| 2026-10-31 | minor | Student-t density uses an asymptotic log-gamma ratio for large degrees of freedom, avoiding cancellation while preserving bounded work; checked against Excel at 100 million and 10 billion degrees |
|
||||||
|
| 2026-10-31 | minor | T.INV probability zero returns `#DIV/0!`, while other out-of-range probabilities and invalid degrees remain `#NUM!`; T.INV.2T keeps its separate boundary behavior |
|
||||||
|
| 2026-10-31 | minor | GAMMA.DIST density at x=0 and shape 1 returns `#NUM!` for all positive scales; the cumulative branch and other shape boundaries are unchanged |
|
||||||
|
| 2026-10-31 | minor | WEIBULL.DIST returns zero at x=0 in both density and cumulative modes, including shape below one; negative x remains `#NUM!` |
|
||||||
|
| 2026-10-31 | minor | BINOM.DIST and BINOM.DIST.RANGE reject trial counts above 2,147,483,646 after truncation |
|
||||||
|
| 2026-10-31 | minor | BETA.DIST density returns `#NUM!` at the lower endpoint with alpha less than or equal to 1 and upper endpoint with beta less than or equal to 1; cumulative behavior is unchanged |
|
||||||
|
| 2026-10-31 | minor | HARMEAN returns `#N/A` when array or reference inputs contain no numbers; nonpositive numeric observations remain `#NUM!` |
|
||||||
| 2026-10-27 | minor | ECMA.CEILING added to the functions that are in our library but not on Microsoft's list (with USDOLLAR), so the counts don't change |
|
| 2026-10-27 | minor | ECMA.CEILING added to the functions that are in our library but not on Microsoft's list (with USDOLLAR), so the counts don't change |
|
||||||
| 2026-10-26 | minor | IMSQRT uses Excel's polar route with its cosine reduction, so `IMSQRT("-4")` is `1.22514845490862E-16+2i` as in Excel (exact `2i` before); the rows list the IM* differences still open |
|
| 2026-10-26 | minor | IMSQRT uses Excel's polar route with its cosine reduction, so `IMSQRT("-4")` is `1.22514845490862E-16+2i` as in Excel (exact `2i` before); the rows list the IM* differences still open |
|
||||||
| 2026-10-25 | minor | LAMBDA row: a LAMBDA that calls itself more than 140 times, and a formula nested past the depth limit, give `#NUM!` (#1809; nesting gave `#CALC!` before). `=Foo(1)` where the defined name Foo holds `#N/A` gives `#N/A`, not `#NAME?` |
|
| 2026-10-25 | minor | LAMBDA row: a LAMBDA that calls itself more than 140 times, and a formula nested past the depth limit, give `#NUM!` (#1809; nesting gave `#CALC!` before). `=Foo(1)` where the defined name Foo holds `#N/A` gives `#N/A`, not `#NAME?` |
|
||||||
|
|||||||
+19
-19
@@ -19,25 +19,24 @@ This is GridCraft's implementation of the shared [release playbook](release-play
|
|||||||
| Web app dir `apps/gridcraft-web` | `packaging/web/package.sh` |
|
| Web app dir `apps/gridcraft-web` | `packaging/web/package.sh` |
|
||||||
| App colour `#1f9d55` (ink `#147a40`), **proposed** | `assets/app-icon/README.md`, metainfo `<branding>` |
|
| App colour `#1f9d55` (ink `#147a40`), **proposed** | `assets/app-icon/README.md`, metainfo `<branding>` |
|
||||||
|
|
||||||
## Prerequisites still owned by the app crates
|
## Application release prerequisites
|
||||||
|
|
||||||
The pipeline references crates that don't exist yet; the first release run fails until they do:
|
The `apps/gridcraft` and `apps/gridcraft-web` crates are workspace members, and
|
||||||
|
`apps/gridcraft-cli` supports `--version`. The `cargo xtask` alias is defined in
|
||||||
|
`.cargo/config.toml`. The current app-specific release setup includes:
|
||||||
|
|
||||||
- **`apps/gridcraft`** (the GUI binary `gridcraft`) and **`apps/gridcraft-web`** (trunk
|
- **Windows resources:** `apps/gridcraft/build.rs` embeds the icon and version information with
|
||||||
site) must be workspace members. `apps/gridcraft-cli` exists and supports `--version`.
|
`winresource` (instructions at the top of the file). `package.ps1` sets
|
||||||
- **Windows resources:** move `packaging/windows/app-build.rs.in` to `apps/gridcraft/build.rs`
|
`GRIDCRAFT_REQUIRE_WINRES=1`, so a release build without the icon fails, and it checks that
|
||||||
and add `winresource = "0.1"` under `[build-dependencies]` (instructions at the top of the
|
`gridcraft.exe` is a GUI-subsystem binary. `apps/gridcraft/src/main.rs` sets
|
||||||
file). `package.ps1` sets `GRIDCRAFT_REQUIRE_WINRES=1`, so a release build without the icon
|
`windows_subsystem = "windows"` for release builds.
|
||||||
fails, and it checks that `gridcraft.exe` is a GUI-subsystem binary, so `main.rs` needs
|
- **Version information:** `gridcraft --version`, `gridcraft-cli --version` and the About dialog
|
||||||
`#![cfg_attr(all(target_os = "windows", not(debug_assertions)), windows_subsystem = "windows")]`.
|
use `env!("CARGO_PKG_VERSION")` from `[workspace.package]`. `packaging/env.sh` also exports
|
||||||
- **Build info:** CI exports `GRIDCRAFT_BUILD_SHA` and `GRIDCRAFT_BUILD_DATE`; read them
|
`GRIDCRAFT_BUILD_SHA` and `GRIDCRAFT_BUILD_DATE` for artifact metadata.
|
||||||
with `option_env!` (fallback: "dev build") for `--version` and *Help › About GridCraft*.
|
- **Runtime icon:** `apps/gridcraft/src/main.rs` embeds
|
||||||
- **Runtime icon:** embed `assets/app-icon/gridcraft-macos-512.png` (macOS Dock) and
|
`assets/app-icon/gridcraft-macos-512.png` (macOS Dock) and
|
||||||
`hicolor/256x256/apps/ai.storyteller.gridcraft.png` (Windows/Linux) as the window icon, and
|
`hicolor/256x256/apps/ai.storyteller.gridcraft.png` (Windows/Linux) as the window icon, and
|
||||||
use `ai.storyteller.gridcraft` as the Wayland app id / `StartupWMClass`.
|
uses `ai.storyteller.gridcraft` as the Wayland app id / `StartupWMClass`.
|
||||||
- **`cargo xtask` alias:** the docs use `cargo xtask …`, which needs
|
|
||||||
`.cargo/config.toml` with `[alias] xtask = "run --package xtask --"`. Until it exists, use
|
|
||||||
`cargo run -p xtask -- …`. The workflows already call `cargo run -p xtask` directly.
|
|
||||||
|
|
||||||
## Cutting a release
|
## Cutting a release
|
||||||
|
|
||||||
@@ -289,7 +288,8 @@ row in `ATTRIBUTION.md` (`cargo xtask assets`).
|
|||||||
installs, runs and uninstalls the MSI on a Windows 11 ARM64 runner.
|
installs, runs and uninstalls the MSI on a Windows 11 ARM64 runner.
|
||||||
- `.github/workflows/freebsd.yml`: FreeBSD build + tests (manual run from `release`).
|
- `.github/workflows/freebsd.yml`: FreeBSD build + tests (manual run from `release`).
|
||||||
|
|
||||||
**Only the `release` branch builds.** No build workflow runs on `main` or pull requests; every
|
**CI and release builds.** `.github/workflows/ci.yml` runs the shared `cargo xtask ci` gate on
|
||||||
build job checks `github.ref == 'refs/heads/release'`, so a manual run from another branch is
|
every push to `main` and every pull request. Release artifacts are built on `release` pushes or
|
||||||
skipped. Only `packaging-lint.yml` (no build, no secrets) runs on `main` and PRs.
|
manually; manual runs from other branches are dry runs as described above. `packaging-lint.yml`
|
||||||
|
also runs on `main` and PRs.
|
||||||
- Locally: `cargo xtask ci` (fmt, clippy, tests, assets, layers, wasm, parity check).
|
- Locally: `cargo xtask ci` (fmt, clippy, tests, assets, layers, wasm, parity check).
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
# UI parity
|
# UI parity
|
||||||
|
|
||||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-29 · **Change:** minor (Ctrl+arrow and Ctrl+Shift+arrow while pointing a formula reference; earlier: editing a hyperlink loads its existing fields; earlier: AutoSum over a selected row puts its total to the right; ribbon catalog count 276 / 292) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (checkbox cells print as boxes; earlier: Ctrl+arrow and Ctrl+Shift+arrow while pointing a formula reference; earlier: editing a hyperlink loads its existing fields; earlier: AutoSum over a selected row puts its total to the right; ribbon catalog count 276 / 292) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||||
|
|
||||||
Interaction, controls and feel, against Excel. Part of [target-app-parity.md](target-app-parity.md).
|
Interaction, controls and feel, against Excel. Part of [target-app-parity.md](target-app-parity.md).
|
||||||
**UI/UX fidelity: ~55% ready, 50–80 h** (estimated). Ribbon command coverage is measured
|
**UI/UX fidelity: ~55% ready, 50–80 h** (estimated). Ribbon command coverage is measured
|
||||||
@@ -194,6 +194,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12.
|
|||||||
|
|
||||||
| Date | Change | Summary |
|
| Date | Change | Summary |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| 2026-10-30 | minor | Checkbox cells print and export to PDF as boxes, as on screen: unchecked white with a grey outline, checked green with a white tick (#2972). They printed their value as TRUE or FALSE; other values in a checkbox cell still print as text |
|
||||||
| 2026-10-29 | minor | Formula pointing from the keyboard: Ctrl+arrow (⌘arrow) moves the pointed cell to the edge of its data and Ctrl+Shift+arrow (⌘⇧arrow) extends the pointed range there, as the same keys move and extend the selection, so typing `=COUNTIF(`, clicking the top of a column and pressing ⌘⇧↓ points at the whole column of data; the keys used to move or extend by one cell. A range reaching the sheet edge is written as whole columns or rows (`A:B`). The editor reads the modifiers of the arrow's own key event, so a keystroke pressed and released within one frame (a control client's `ui.key`, an automation tool) keeps its Shift and Ctrl |
|
| 2026-10-29 | minor | Formula pointing from the keyboard: Ctrl+arrow (⌘arrow) moves the pointed cell to the edge of its data and Ctrl+Shift+arrow (⌘⇧arrow) extends the pointed range there, as the same keys move and extend the selection, so typing `=COUNTIF(`, clicking the top of a column and pressing ⌘⇧↓ points at the whole column of data; the keys used to move or extend by one cell. A range reaching the sheet edge is written as whole columns or rows (`A:B`). The editor reads the modifiers of the arrow's own key event, so a keystroke pressed and released within one frame (a control client's `ui.key`, an automation tool) keeps its Shift and Ctrl |
|
||||||
| 2026-10-29 | minor | Insert Hyperlink opens with an existing link's address, display text and ScreenTip (#2876). An address-only edit preserves the cell's content and formula; Cancel leaves the link unchanged, and Undo restores the edit |
|
| 2026-10-29 | minor | Insert Hyperlink opens with an existing link's address, display text and ScreenTip (#2876). An address-only edit preserves the cell's content and formula; Cancel leaves the link unchanged, and Undo restores the edit |
|
||||||
| 2026-10-27 | minor | Ctrl with the numpad's + inserts rows, columns or cells as Ctrl+Shift+= does, as in Excel; it did nothing because the numpad key arrives without Shift |
|
| 2026-10-27 | minor | Ctrl with the numpad's + inserts rows, columns or cells as Ctrl+Shift+= does, as in Excel; it did nothing because the numpad key arrives without Shift |
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ finish-args:
|
|||||||
- --filesystem=xdg-documents
|
- --filesystem=xdg-documents
|
||||||
- --filesystem=xdg-download
|
- --filesystem=xdg-download
|
||||||
# Network: update checks reach GitHub. Home/host filesystem access is deliberately not granted.
|
# Network: update checks reach GitHub. Home/host filesystem access is deliberately not granted.
|
||||||
|
# The release runner's flatpak-builder has no eu-strip (elfutils), which its debuginfo pass runs
|
||||||
|
# on cups-client's unstripped tools, in whichever module comes first after they're installed. The
|
||||||
|
# app's own binaries come from the release tarball, already stripped.
|
||||||
|
build-options:
|
||||||
|
no-debuginfo: true
|
||||||
modules:
|
modules:
|
||||||
# CUPS client tools for craft-print: the runtime has libcups but not lpstat/lp/lpoptions.
|
# CUPS client tools for craft-print: the runtime has libcups but not lpstat/lp/lpoptions.
|
||||||
# Linked statically against this release's libcups, so a runtime update can't break them;
|
# Linked statically against this release's libcups, so a runtime update can't break them;
|
||||||
|
|||||||
Reference in New Issue
Block a user