1use std::collections::HashMap;
4use std::sync::Mutex;
5
6use crate::base::{CellRef, RangeRef, Value};
7use crate::model::{Sheet, Workbook};
8use crate::numfmt::{Formatted, NumberFormat, format_value};
9
10fn parsed(code: &str) -> NumberFormat {
12 static CACHE: std::sync::LazyLock<Mutex<HashMap<String, NumberFormat>>> = std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));
13 let lock = || CACHE.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
14 if let Some(f) = lock().get(code) {
15 return f.clone();
16 }
17 let f = NumberFormat::parse(code);
19 let mut m = lock();
20 if m.len() > 4096 {
21 m.clear();
22 }
23 m.insert(code.to_string(), f.clone());
24 f
25}
26
27pub fn number_format(code: &str) -> NumberFormat {
28 parsed(code)
29}
30
31pub fn format(v: &Value, code: &str, wb: &Workbook) -> Formatted {
33 format_value(v, &parsed(code), wb.date_system)
34}
35
36fn text_needs_quote_prefix(t: &str, sys: crate::base::date::DateSystem) -> bool {
41 if t.is_empty() {
42 return false;
43 }
44 let mut chars = t.chars();
45 let (first, second) = (chars.next(), chars.next());
46 if matches!(first, Some('=' | '\'')) {
47 return true;
48 }
49 if matches!(first, Some('+' | '-')) && second.is_some_and(|c| c.is_ascii_alphabetic() || c == '(') {
50 return true;
51 }
52 !matches!(crate::base::parse::parse_input(t, sys).value, Value::Text(ref x) if x.as_ref() == t)
53}
54
55pub fn edit_text(wb: &Workbook, sheet: &Sheet, c: CellRef) -> String {
61 let raw = sheet.input_text(c);
62 let Some(cell) = sheet.cell(c) else { return raw };
63 if let Value::Text(t) = &cell.value
64 && cell.formula.is_none()
65 && wb.styles.get(cell.style).num_fmt.as_str() != "@"
66 && text_needs_quote_prefix(t, wb.date_system)
67 {
68 return format!("'{raw}");
69 }
70 let Value::Number(n) = &cell.value else { return raw };
71 let code = wb.styles.get(cell.style).num_fmt.as_str();
72 if cell.formula.is_some() || !parsed(code).is_date() {
73 return raw;
74 }
75 let reads_back = |s: &str| matches!(crate::base::parse::parse_input(s, wb.date_system).value, Value::Number(m) if (m - n).abs() < 1e-9);
76 let shown = format(&cell.value, code, wb).text;
77 if reads_back(&shown) {
78 return shown;
79 }
80 let Some(dt) = crate::base::date::datetime_from_serial(wb.date_system, *n) else { return raw };
81 let time = format!("{:02}:{:02}:{:02}", dt.hour, dt.minute, dt.second);
82 let date = format!("{:04}-{:02}-{:02}", dt.year, dt.month, dt.day);
83 let iso = if *n < 1.0 {
84 time
85 } else if n.fract() == 0.0 {
86 date
87 } else {
88 format!("{date} {time}")
89 };
90 if reads_back(&iso) { iso } else { raw }
91}
92
93pub fn formula_hidden(wb: &Workbook, sheet: &Sheet, c: CellRef) -> bool {
96 if !sheet.is_protected() {
97 return false;
98 }
99 let at = match sheet.cell(c) {
100 Some(x) if x.formula.is_some() || x.py.is_some() => c,
101 _ => match sheet.spill_ranges.iter().find(|(_, r)| r.contains(c)) {
102 Some((anchor, _)) => *anchor,
103 None => return false,
104 },
105 };
106 wb.styles.get(sheet.style_id(at)).protection.hidden
107}
108
109pub fn cell_text(wb: &Workbook, sheet: &Sheet, c: CellRef) -> String {
111 if sheet.show_formulas
112 && let Some(f) = sheet.cell(c).and_then(|x| x.formula.as_ref())
113 && !formula_hidden(wb, sheet, c)
114 {
115 return format!("={}", f.text);
116 }
117 value_text(wb, sheet, c)
118}
119
120pub fn value_text(wb: &Workbook, sheet: &Sheet, c: CellRef) -> String {
123 let style = wb.styles.get(sheet.style_id(c));
124 value_text_as(wb, &sheet.value(c), style.num_fmt.as_str())
125}
126
127pub fn value_text_as(wb: &Workbook, v: &Value, code: &str) -> String {
130 let f = format(v, code, wb);
131 match f.fill {
132 Some((_, pos)) => {
133 let mut t = f.text;
134 if pos <= t.len() && t.is_char_boundary(pos) {
135 t.insert(pos, ' ');
136 }
137 t
138 }
139 None => f.text,
140 }
141}
142
143pub fn range_text(wb: &Workbook, sheet: usize, r: RangeRef) -> String {
145 let Some(sh) = wb.sheet(sheet) else { return String::new() };
146 let mut out = String::new();
147 let r = clipboard_range(r);
148 for row in r.start.row..=r.end.row {
149 if sh.is_row_hidden(row) {
150 continue;
151 }
152 let mut first = true;
153 for col in r.start.col..=r.end.col {
154 if !first {
155 out.push('\t');
156 }
157 first = false;
158 let t = cell_text(wb, sh, CellRef::new(row, col));
159 if t.contains(['\t', '\n', '\r']) {
162 out.push('"');
163 out.push_str(&t.replace('"', "\"\""));
164 out.push('"');
165 } else {
166 out.push_str(&t);
167 }
168 }
169 out.push('\n');
170 }
171 out
172}
173
174pub(crate) fn clipboard_range(r: RangeRef) -> RangeRef {
176 if r.count() > 2_000_000 { RangeRef::new(r.start, r.start.offset_clamped(1999, 999)) } else { r }
177}
178
179#[derive(Clone, Debug, Default, PartialEq, serde::Serialize)]
182pub struct Stats {
183 pub count: u64,
184 pub numeric: u64,
185 pub sum: f64,
186 pub min: f64,
187 pub max: f64,
188 pub average: Option<f64>,
189}
190
191pub fn stats(sheet: &Sheet, ranges: &[RangeRef]) -> Stats {
192 let mut s = Stats { min: f64::INFINITY, max: f64::NEG_INFINITY, ..Default::default() };
193 let mut seen = std::collections::HashSet::new();
194 for r in ranges {
195 let r = match sheet.used_range().and_then(|u| r.intersection(&u)) {
196 Some(x) => x,
197 None => continue,
198 };
199 let mut visit = |c: CellRef, v: &Value| {
200 if !seen.insert(c) || sheet.is_row_hidden(c.row) {
201 return;
202 }
203 if v.is_empty() {
204 return;
205 }
206 s.count += 1;
207 if let Value::Number(n) = v {
208 s.numeric += 1;
209 s.sum += n;
210 s.min = s.min.min(*n);
211 s.max = s.max.max(*n);
212 }
213 };
214 for (c, cell) in sheet.cells.iter_range(r) {
215 visit(c, &cell.value);
216 }
217 for (c, v) in sheet.spill.range(r.start..=r.end) {
218 if r.contains(*c) {
219 visit(*c, v);
220 }
221 }
222 }
223 if s.numeric > 0 {
224 s.average = Some(s.sum / s.numeric as f64);
225 } else {
226 s.min = 0.0;
227 s.max = 0.0;
228 }
229 s
230}