Files
gridcraft/crates/engine/src/display.rs
T

137 lines
4.1 KiB
Rust

//! How cells look as text: number formats applied, for the grid, clipboard, CSV and agents.
use std::collections::HashMap;
use std::sync::Mutex;
use gridcraft_core::{CellRef, RangeRef, Value};
use gridcraft_model::{Sheet, Workbook};
use gridcraft_numfmt::{Formatted, NumberFormat, format_value};
/// Parsed number formats are cached by code.
fn parsed(code: &str) -> NumberFormat {
static CACHE: std::sync::LazyLock<Mutex<HashMap<String, NumberFormat>>> = std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));
let mut m = CACHE.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(f) = m.get(code) {
return f.clone();
}
let f = NumberFormat::parse(code);
if m.len() > 4096 {
m.clear();
}
m.insert(code.to_string(), f.clone());
f
}
pub fn number_format(code: &str) -> NumberFormat {
parsed(code)
}
/// Formats a value with a format code.
pub fn format(v: &Value, code: &str, wb: &Workbook) -> Formatted {
format_value(v, &parsed(code), wb.date_system)
}
/// The text a cell shows (full precision General; the grid narrows General to fit the column).
pub fn cell_text(wb: &Workbook, sheet: &Sheet, c: CellRef) -> String {
let v = sheet.value(c);
if sheet.show_formulas
&& let Some(f) = sheet.cell(c).and_then(|x| x.formula.as_ref())
{
return format!("={}", f.text);
}
let style = wb.styles.get(sheet.style_id(c));
let f = format(&v, style.num_fmt.as_str(), wb);
match f.fill {
Some((_, pos)) => {
let mut t = f.text;
if pos <= t.len() && t.is_char_boundary(pos) {
t.insert(pos, ' ');
}
t
}
None => f.text,
}
}
/// Tab-separated text of a range, as the system clipboard gets it.
pub fn range_text(wb: &Workbook, sheet: usize, r: RangeRef) -> String {
let Some(sh) = wb.sheet(sheet) else { return String::new() };
let mut out = String::new();
let r = if r.count() > 2_000_000 { RangeRef::new(r.start, r.start.offset_clamped(1999, 999)) } else { r };
for row in r.start.row..=r.end.row {
if sh.is_row_hidden(row) {
continue;
}
let mut first = true;
for col in r.start.col..=r.end.col {
if !first {
out.push('\t');
}
first = false;
let t = cell_text(wb, sh, CellRef::new(row, col));
if t.contains(['\t', '\n', '"']) {
out.push('"');
out.push_str(&t.replace('"', "\"\""));
out.push('"');
} else {
out.push_str(&t);
}
}
out.push('\n');
}
out
}
/// Status-bar statistics for a selection: average, count (non-empty), numerical count, min,
/// max, sum.
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize)]
pub struct Stats {
pub count: u64,
pub numeric: u64,
pub sum: f64,
pub min: f64,
pub max: f64,
pub average: Option<f64>,
}
pub fn stats(sheet: &Sheet, ranges: &[RangeRef]) -> Stats {
let mut s = Stats { min: f64::INFINITY, max: f64::NEG_INFINITY, ..Default::default() };
let mut seen = std::collections::HashSet::new();
for r in ranges {
let r = match sheet.used_range().and_then(|u| r.intersection(&u)) {
Some(x) => x,
None => continue,
};
let mut visit = |c: CellRef, v: &Value| {
if !seen.insert(c) || sheet.is_row_hidden(c.row) {
return;
}
if v.is_empty() {
return;
}
s.count += 1;
if let Value::Number(n) = v {
s.numeric += 1;
s.sum += n;
s.min = s.min.min(*n);
s.max = s.max.max(*n);
}
};
for (c, cell) in sheet.cells.iter_range(r) {
visit(c, &cell.value);
}
for (c, v) in sheet.spill.range(r.start..=r.end) {
if r.contains(*c) {
visit(*c, v);
}
}
}
if s.numeric > 0 {
s.average = Some(s.sum / s.numeric as f64);
} else {
s.min = 0.0;
s.max = 0.0;
}
s
}