1use std::cmp::Ordering;
9use std::collections::{HashMap, HashSet};
10
11use crate::base::date::datetime_from_serial;
12pub use crate::base::quote_sheet;
13use crate::base::{CellError, CellRef, MAX_COLS, MAX_ROWS, RangeRef, Value, split_sheet};
14use crate::model::{
15 BorderLine, BorderStyle, Cell, Chart, Color, Fill, HAlign, NumFmt, PivotDateGroup, PivotField, PivotFilter, PivotFunc, PivotLayout, PivotShowAs,
16 PivotSort, PivotTable, PivotValue, Series, Style, StyleId, Workbook,
17};
18
19use crate::engine::pivot_calc::CalcSet;
20
21mod charts;
22mod compute;
23mod items;
24mod output;
25
26use charts::rebind_charts;
27pub use charts::{chart_link, chart_series, clear_output, links_to, parse_chart_link};
28use compute::compute_from;
29pub use compute::{NOT_A_VALUE_CELL, detail_cells, detail_records};
30pub use items::{distinct_labels, func_caption, item_label};
31#[cfg(test)]
32use output::data_cols_exceed_sheet;
33pub use output::{CellKind, OutCell, Output, RowKind};
34use output::{cell_style, look};
35
36pub const MAX_SOURCE_CELLS: u64 = 10_000_000;
38pub const MAX_OUTPUT_CELLS: u64 = 2_000_000;
40pub const MAX_AXIS_FIELDS: usize = 32;
42pub const MAX_VALUE_FIELDS: usize = 256;
44
45const MONTHS: [&str; 12] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
46
47#[derive(Clone, Debug)]
51pub struct Source {
52 pub sheet: usize,
53 pub range: RangeRef,
54 pub headers: Vec<String>,
55 pub formats: Vec<String>,
56 pub numeric: Vec<bool>,
58 pub dates: Vec<bool>,
60 pub rows: Vec<Vec<Value>>,
61}
62
63impl Source {
64 pub fn col(&self, name: &str) -> Option<usize> {
65 self.headers.iter().position(|h| h.eq_ignore_ascii_case(name))
66 }
67}
68
69pub fn range_text(sheet_name: &str, r: RangeRef) -> String {
71 let abs = |c: CellRef| format!("${}${}", crate::base::col_to_letters(c.col), c.row as u64 + 1);
72 format!("{}!{}:{}", quote_sheet(sheet_name), abs(r.start), abs(r.end))
73}
74
75pub fn resolve_source(wb: &Workbook, text: &str, default_sheet: usize) -> Result<(usize, RangeRef), String> {
77 resolve_inner(wb, text, default_sheet, 0)
78}
79
80fn resolve_inner(wb: &Workbook, text: &str, default_sheet: usize, depth: u32) -> Result<(usize, RangeRef), String> {
81 const INVALID: &str = "The reference isn't valid.";
82 let t = text.trim().trim_start_matches('=').trim();
83 if t.is_empty() || depth > 4 {
84 return Err(INVALID.into());
85 }
86 if let Some((si, ti)) = wb.table(t)
87 && let Some(tb) = wb.sheet(si).and_then(|s| s.tables.get(ti))
88 {
89 if !tb.header_row {
90 return Err("The table needs a header row to be used as a PivotTable source.".into());
91 }
92 let end_row = tb.range.end.row.saturating_sub(tb.totals_row as u32).max(tb.range.start.row);
93 return Ok((si, RangeRef::new(tb.range.start, CellRef::new(end_row, tb.range.end.col))));
94 }
95 let (sheet, body) = split_sheet(t);
96 if let Some(r) = RangeRef::parse(body) {
97 let si = match sheet {
98 Some(n) => wb.sheet_index(&n).ok_or_else(|| INVALID.to_string())?,
99 None => default_sheet,
100 };
101 let sh = wb.sheet(si).ok_or_else(|| INVALID.to_string())?;
102 let r = if r.is_full_cols() || r.is_full_rows() {
103 let used = sh.used_range().ok_or_else(|| INVALID.to_string())?;
104 r.intersection(&used).ok_or_else(|| INVALID.to_string())?
105 } else {
106 r
107 };
108 return Ok((si, r));
109 }
110 if sheet.is_none()
111 && let Some(n) = wb.name(t, default_sheet)
112 {
113 let f = n.formula.clone();
114 return resolve_inner(wb, &f, default_sheet, depth + 1);
115 }
116 Err(INVALID.into())
117}
118
119const FIELD_NAME_ERR: &str =
120 "The PivotTable field name is not valid. To create a PivotTable, you must use data that is organized as a list with labeled columns.";
121
122pub fn read_source(wb: &Workbook, text: &str, default_sheet: usize) -> Result<Source, String> {
124 let (si, r) = resolve_source(wb, text, default_sheet)?;
125 let sh = wb.sheet(si).ok_or("The reference isn't valid.")?;
126 if r.count() > MAX_SOURCE_CELLS {
127 return Err("The source range is too large for a PivotTable.".into());
128 }
129 let mut headers: Vec<String> = Vec::new();
130 let mut taken: HashSet<String> = HashSet::new();
133 let mut next_suffix: HashMap<String, usize> = HashMap::new();
134 let mut formats = Vec::new();
135 for c in r.start.col..=r.end.col {
136 let h = crate::engine::display::cell_text(wb, sh, CellRef::new(r.start.row, c)).trim().to_string();
137 if h.is_empty() {
138 return Err(FIELD_NAME_ERR.into());
139 }
140 let key = h.to_ascii_lowercase();
141 let mut n = h.clone();
142 let mut k = next_suffix.get(&key).copied().unwrap_or(2);
143 while taken.contains(&n.to_ascii_lowercase()) {
144 n = format!("{h}{k}");
145 k += 1;
146 }
147 next_suffix.insert(key, k);
148 taken.insert(n.to_ascii_lowercase());
149 headers.push(n);
150 let fmt = if r.end.row > r.start.row {
151 wb.styles.get(sh.style_id(CellRef::new(r.start.row + 1, c))).num_fmt.as_str().to_string()
152 } else {
153 "General".into()
154 };
155 formats.push(fmt);
156 }
157 let w = headers.len();
158 let mut rows = Vec::new();
159 let mut numeric = vec![true; w];
160 let mut any = vec![false; w];
161 if r.end.row > r.start.row {
162 for row in r.start.row + 1..=r.end.row {
163 let rec: Vec<Value> = (r.start.col..=r.end.col).map(|c| sh.value(CellRef::new(row, c)).scalar()).collect();
164 if rec.iter().all(Value::is_empty) {
165 continue;
166 }
167 for (i, v) in rec.iter().enumerate() {
168 if !v.is_empty() {
169 if let Some(a) = any.get_mut(i) {
170 *a = true;
171 }
172 if !v.is_number()
173 && let Some(n) = numeric.get_mut(i)
174 {
175 *n = false;
176 }
177 }
178 }
179 rows.push(rec);
180 }
181 }
182 for (n, a) in numeric.iter_mut().zip(any.iter()) {
183 *n = *n && *a;
184 }
185 let dates = numeric.iter().zip(formats.iter()).map(|(n, f)| *n && crate::engine::display::number_format(f).is_date()).collect();
186 Ok(Source { sheet: si, range: r, headers, formats, numeric, dates, rows })
187}
188
189pub fn compute(wb: &Workbook, pt: &PivotTable, pivot_sheet: usize) -> Result<Output, String> {
191 let src = read_source(wb, &pt.source, pivot_sheet)?;
192 compute_from(wb, pt, &src, None)
193}
194
195pub fn refresh(wb: &mut Workbook, sheet: usize, idx: usize, replace: bool) -> Result<RangeRef, String> {
198 let pt = wb.sheet(sheet).and_then(|s| s.pivots.get(idx)).cloned().ok_or("There's no such PivotTable.")?;
199 let out = compute(wb, &pt, sheet)?;
200 let h = out.height().max(1);
201 let w = out.width.max(1);
202 let k = out.body_offset;
203 let origin_row = if pt.anchor.row >= k { pt.anchor.row - k } else { pt.anchor.row };
204 let origin = CellRef::new(origin_row, pt.anchor.col);
205 let end_row = origin.row as u64 + h as u64 - 1;
206 let end_col = origin.col as u64 + w as u64 - 1;
207 if end_row >= MAX_ROWS as u64 || end_col >= MAX_COLS as u64 {
208 return Err("The PivotTable report will not fit on the sheet. Choose a different location.".into());
209 }
210 let area = RangeRef::new(origin, CellRef::new(end_row as u32, end_col as u32));
211 let sh = wb.sheet(sheet).ok_or("There's no such sheet.")?;
212 for (j, other) in sh.pivots.iter().enumerate() {
213 if j != idx && other.last_range.is_some_and(|r| r.intersects(&area)) {
214 return Err("A PivotTable report cannot overlap another PivotTable report.".into());
215 }
216 }
217 if sh.is_protected() {
218 return Err("The cell or chart you're trying to change is on a protected sheet.".into());
219 }
220 if let Ok((ssi, sr)) = resolve_source(wb, &pt.source, sheet)
221 && ssi == sheet
222 && sr.intersects(&area)
223 {
224 return Err("The PivotTable report can't overlap its source data. Choose a different location.".into());
225 }
226 if sh.tables.iter().any(|t| t.range.intersects(&area)) {
227 return Err("A PivotTable report cannot overlap a table.".into());
228 }
229 if !replace {
230 for (c, cell) in sh.cells.iter_range(area) {
231 if cell.has_content() && !pt.last_range.is_some_and(|r| r.contains(c)) {
232 return Err(format!(
233 "There's already data in {}!{}. Do you want to replace it? (Run again with replace: true to overwrite.)",
234 quote_sheet(&sh.name),
235 c.a1()
236 ));
237 }
238 }
239 if let Some(c) = sh.spill.keys().find(|c| area.contains(**c) && !pt.last_range.is_some_and(|r| r.contains(**c))) {
240 return Err(format!(
241 "There's already data in {}!{}. Do you want to replace it? (Run again with replace: true to overwrite.)",
242 quote_sheet(&sh.name),
243 c.a1()
244 ));
245 }
246 }
247 let lk = look(&pt.style);
249 let header_last = out.row_kinds.iter().rposition(|k| *k == RowKind::Header);
250 let mut cache: HashMap<Style, StyleId> = HashMap::new();
251 let mut cells: Vec<(CellRef, Cell)> = Vec::new();
252 for (ri, row) in out.grid.iter().enumerate() {
253 let rk = out.row_kinds.get(ri).copied().unwrap_or(RowKind::Blank);
254 if rk == RowKind::Blank {
255 continue;
256 }
257 for (ci, oc) in row.iter().enumerate() {
258 if rk == RowKind::Filter && oc.value.is_empty() {
259 continue;
260 }
261 if rk == RowKind::Placeholder && oc.value.is_empty() {
262 continue;
263 }
264 let flag = match rk {
265 RowKind::Header => Some(ri) == header_last,
266 RowKind::Filter => ci == 0,
267 _ => false,
268 };
269 let st = cell_style(&lk, rk, oc, flag);
270 let id = match cache.get(&st) {
271 Some(id) => *id,
272 None => {
273 let id = wb.styles.intern(st.clone());
274 cache.insert(st, id);
275 id
276 }
277 };
278 if oc.value.is_empty() && id == StyleId::default() {
279 continue;
280 }
281 cells.push((
282 CellRef::new(origin.row + ri as u32, origin.col + ci as u32),
283 Cell { value: oc.value.clone(), formula: None, style: id, py: None },
284 ));
285 }
286 }
287 let mut widths: HashMap<u32, f32> = HashMap::new();
289 for (c, cell) in &cells {
290 if cell.value.is_empty() {
291 continue;
292 }
293 let st = wb.styles.get(cell.style);
294 let text = crate::engine::display::format(&cell.value, st.num_fmt.as_str(), wb).text;
295 let wpt = crate::engine::cmd::format::approx_text_width(&text, st.font.size, st.font.bold) + st.align.indent as f32 * 9.0 + 14.0;
296 let e = widths.entry(c.col).or_insert(0.0);
297 *e = e.max(wpt.min(320.0));
298 }
299 let shm = wb.sheet_mut(sheet).ok_or("There's no such sheet.")?;
300 if let Some(old) = pt.last_range {
301 shm.take_cells(old);
302 }
303 shm.take_cells(area);
304 for (c, cell) in cells {
305 shm.set_cell(c, cell);
306 }
307 for (col, wpt) in widths {
308 if wpt > shm.col_width(col) && !shm.is_col_hidden(col) {
309 shm.cols.entry(col).or_default().size = Some(wpt);
310 }
311 }
312 if let Some(p) = shm.pivots.get_mut(idx) {
313 p.last_range = Some(area);
314 }
315 let sheet_name = shm.name.clone();
316 rebind_charts(wb, &sheet_name, &pt.name, origin, &out);
317 Ok(area)
318}
319
320#[cfg(test)]
321mod fit_tests {
322 use super::data_cols_exceed_sheet;
323
324 #[test]
325 fn column_budget_counts_value_fields_and_grand_total() {
326 assert!(!data_cols_exceed_sheet(64, true, 128));
327 assert!(data_cols_exceed_sheet(64, true, 256));
328 assert!(!data_cols_exceed_sheet(64, false, 256));
329 assert!(!data_cols_exceed_sheet(16_384, false, 0));
330 assert!(data_cols_exceed_sheet(16_385, false, 0));
331 }
332}