1use super::{
2 Cell, CellRef, CfOperator, EngineError, ErrorStyle, Json, RangeRef, Result, Session, Validation, ValidationKind, Value, Workbook, bad,
3 bool_param, cell_param, edit, json, ok, str_param, target_ranges, target_sheet,
4};
5
6fn op_param(s: Option<&str>) -> CfOperator {
9 match s.unwrap_or("between") {
10 "notBetween" => CfOperator::NotBetween,
11 "equal" => CfOperator::Equal,
12 "notEqual" => CfOperator::NotEqual,
13 "greater" | "greaterThan" => CfOperator::Greater,
14 "less" | "lessThan" => CfOperator::Less,
15 "greaterOrEqual" | "greaterThanOrEqual" => CfOperator::GreaterOrEqual,
16 "lessOrEqual" | "lessThanOrEqual" => CfOperator::LessOrEqual,
17 _ => CfOperator::Between,
18 }
19}
20
21fn subtract_ranges(r: RangeRef, cuts: &[RangeRef]) -> Vec<RangeRef> {
24 let mut events = std::collections::BTreeMap::<u64, Vec<((u32, u32), bool)>>::new();
25 for cut in cuts {
26 let Some(i) = r.intersection(cut) else { continue };
27 let columns = (i.start.col, i.end.col);
28 events.entry(u64::from(i.start.row)).or_default().push((columns, true));
29 events.entry(u64::from(i.end.row) + 1).or_default().push((columns, false));
30 }
31 if events.is_empty() {
32 return vec![r];
33 }
34 events.entry(u64::from(r.end.row) + 1).or_default();
35
36 let mut active = std::collections::BTreeMap::<(u32, u32), usize>::new();
37 let mut out: Vec<RangeRef> = Vec::new();
38 let mut last_by_columns = std::collections::BTreeMap::<(u32, u32), usize>::new();
39 let mut previous_row = u64::from(r.start.row);
40 for (row, changes) in events {
41 if previous_row < row {
42 let start_row = previous_row as u32;
43 let end_row = (row - 1).min(u64::from(r.end.row)) as u32;
44 let mut covered = Vec::<(u32, u32)>::new();
45 for &(start, end) in active.keys() {
46 match covered.last_mut() {
47 Some(last) if start <= last.1.saturating_add(1) => last.1 = last.1.max(end),
48 _ => covered.push((start, end)),
49 }
50 }
51 let mut next_col = u64::from(r.start.col);
52 let mut remaining = Vec::new();
53 for (start, end) in covered {
54 if next_col < u64::from(start) {
55 remaining.push((next_col as u32, start - 1));
56 }
57 next_col = u64::from(end) + 1;
58 }
59 if next_col <= u64::from(r.end.col) {
60 remaining.push((next_col as u32, r.end.col));
61 }
62 for (start_col, end_col) in remaining {
63 let columns = (start_col, end_col);
64 if let Some(&index) = last_by_columns.get(&columns)
65 && out[index].end.row.saturating_add(1) == start_row
66 {
67 out[index].end.row = end_row;
68 } else {
69 let index = out.len();
70 out.push(RangeRef::new(CellRef::new(start_row, start_col), CellRef::new(end_row, end_col)));
71 last_by_columns.insert(columns, index);
72 }
73 }
74 }
75 for (columns, add) in changes {
76 if add {
77 *active.entry(columns).or_default() += 1;
78 } else if let Some(count) = active.get_mut(&columns) {
79 *count -= 1;
80 if *count == 0 {
81 active.remove(&columns);
82 }
83 }
84 }
85 previous_row = row;
86 }
87 out
88}
89
90pub(in super::super) fn remove_validation_cells(validations: &mut Vec<Validation>, cuts: &[RangeRef]) {
94 for dv in validations.iter_mut() {
95 let before = RangeRef::top_left(&dv.ranges);
96 dv.ranges = dv.ranges.iter().flat_map(|r| subtract_ranges(*r, cuts)).collect();
97 if let (Some(from), Some(to)) = (before, RangeRef::top_left(&dv.ranges)) {
98 dv.f1 = move_rule_formula(&dv.f1, from, to);
99 dv.f2 = dv.f2.take().map(|f| move_rule_formula(&f, from, to));
100 }
101 }
102 validations.retain(|d| !d.ranges.is_empty());
103}
104
105pub(super) fn validation(s: &mut Session, p: &Json) -> Result<Json> {
106 let ranges = target_ranges(s, p)?;
107 let sheet = target_sheet(s, p)?;
108 if bool_param(p, "clear").unwrap_or(false) {
109 return edit(s, |cx| {
110 let sh = cx.sheet_mut(sheet)?;
111 remove_validation_cells(&mut sh.validations, &ranges);
112 Ok(Json::Null)
113 });
114 }
115 let Some(kind) = str_param(p, "type") else {
116 s.ui_requests.push(crate::engine::UiRequest::Dialog("dataValidation".into(), json!({})));
117 return ok();
118 };
119 let kind = match kind {
120 "whole" => ValidationKind::Whole,
121 "decimal" => ValidationKind::Decimal,
122 "list" => ValidationKind::List,
123 "date" => ValidationKind::Date,
124 "time" => ValidationKind::Time,
125 "textLength" => ValidationKind::TextLength,
126 "custom" => ValidationKind::Custom,
127 "any" => ValidationKind::Any,
128 other => return Err(bad("data.validation", format!("unknown type `{other}` (any, whole, decimal, list, date, time, textLength, custom)"))),
130 };
131 if kind == ValidationKind::List && str_param(p, "formula1").is_none_or(|f| f.trim().trim_start_matches('=').is_empty()) {
132 return Err(bad("data.validation", "a list validation needs `formula1`"));
133 }
134 const OPERATORS: [&str; 12] = [
135 "between",
136 "notBetween",
137 "equal",
138 "notEqual",
139 "greater",
140 "greaterThan",
141 "less",
142 "lessThan",
143 "greaterOrEqual",
144 "greaterThanOrEqual",
145 "lessOrEqual",
146 "lessThanOrEqual",
147 ];
148 if let Some(op) = str_param(p, "operator").filter(|op| !OPERATORS.contains(op)) {
149 return Err(bad("data.validation", format!("unknown operator `{op}` ({})", OPERATORS.join(", "))));
150 }
151 let error_style = match str_param(p, "errorStyle") {
152 None | Some("stop") => ErrorStyle::Stop,
153 Some("warning") => ErrorStyle::Warning,
154 Some("information") => ErrorStyle::Information,
155 Some(other) => return Err(bad("data.validation", format!("unknown errorStyle `{other}` (stop, warning, information)"))),
156 };
157 let f1 = str_param(p, "formula1").unwrap_or("");
158 let f1 = if kind == ValidationKind::List { list_source(f1) } else { f1.trim_start_matches('=').to_string() };
159 let dv = Validation {
160 ranges: ranges.clone(),
161 kind,
162 op: op_param(str_param(p, "operator")),
163 f1,
164 f2: str_param(p, "formula2").map(|f| f.trim_start_matches('=').to_string()),
165 allow_blank: bool_param(p, "ignoreBlank").unwrap_or(true),
166 in_cell_dropdown: bool_param(p, "dropdown").unwrap_or(true),
167 input_title: str_param(p, "inputTitle").unwrap_or("").into(),
168 input_message: str_param(p, "inputMessage").unwrap_or("").into(),
169 show_input: true,
170 error_title: str_param(p, "errorTitle").unwrap_or("").into(),
171 error_message: str_param(p, "errorMessage").unwrap_or("").into(),
172 error_style,
173 show_error: true,
174 };
175 edit(s, |cx| {
176 let sh = cx.sheet_mut(sheet)?;
177 remove_validation_cells(&mut sh.validations, &ranges);
178 sh.validations.push(dv.clone());
179 Ok(Json::Null)
180 })
181}
182
183fn is_literal_list(f: &str) -> bool {
186 f.starts_with('"')
187 || (!f.contains('!')
188 && crate::formula::parse(f).map(|e| !matches!(e, crate::formula::Expr::Ref(_) | crate::formula::Expr::Name(_))).unwrap_or(true))
189}
190
191fn list_source(f: &str) -> String {
196 let t = f.trim();
197 if t.is_empty() || t.starts_with('=') || t.starts_with('"') || !is_literal_list(t) {
198 return f.trim_start_matches('=').to_string();
199 }
200 format!("\"{}\"", t.replace('"', "\"\""))
201}
202
203pub fn list_items(wb: &Workbook, sheet: usize, at: CellRef, dv: &Validation) -> Vec<String> {
207 let f = dv.f1.trim();
208 if is_literal_list(f) {
209 let items = match f.strip_prefix('"').and_then(|x| x.strip_suffix('"')) {
211 Some(body) => body.replace("\"\"", "\""),
212 None => f.trim_matches('"').to_string(),
213 };
214 return items.split(',').map(|x| x.trim().to_string()).filter(|x| !x.is_empty()).collect();
215 }
216 let first = RangeRef::top_left(&dv.ranges).unwrap_or(at);
217 match crate::calc::evaluate(wb, sheet, at, &formula_at(f, first, at)) {
218 Value::Array(a) => a.data.iter().filter(|v| !v.is_empty()).map(Value::display).collect(),
219 v if !v.is_empty() && !v.is_error() => vec![v.display()],
220 _ => vec![],
221 }
222}
223
224pub fn check_validation(wb: &Workbook, sheet: usize, at: CellRef, input: &str) -> Option<(Validation, String)> {
226 let sh = wb.sheet(sheet)?;
227 let dv = sh.validations.iter().find(|d| d.ranges.iter().any(|r| r.contains(at)))?;
228 if dv.kind == ValidationKind::Any || !dv.show_error {
229 return None;
230 }
231 if input.is_empty() && dv.allow_blank {
232 return None;
233 }
234 let v = crate::base::parse::parse_input(input, wb.date_system).value;
235 let first = RangeRef::top_left(&dv.ranges).unwrap_or(at);
238 let original = wb;
239 let entered;
240 let wb = if sh.value(at) == v {
241 wb
242 } else {
243 let mut w = wb.clone();
244 if let Some(sh) = w.sheet_mut(sheet) {
245 let style = sh.style_id(at);
246 sh.cells.set(at, Cell { value: v.clone(), style, ..Default::default() });
247 }
248 entered = w;
249 &entered
250 };
251 let eval = |f: &str| crate::calc::evaluate(wb, sheet, at, &formula_at(f, first, at));
252 let num = |f: &str| eval(f).to_number().ok();
253 let ok = match dv.kind {
254 ValidationKind::List => list_items(original, sheet, at, dv).iter().any(|x| x.eq_ignore_ascii_case(input)),
257 ValidationKind::Custom => eval(&dv.f1).to_bool().unwrap_or(false),
258 ValidationKind::TextLength => {
259 let n = input.chars().count() as f64;
260 compare_op(dv.op, n, num(&dv.f1), dv.f2.as_deref().and_then(num))
261 }
262 _ => match v.as_f64() {
263 Some(n) => {
264 if dv.kind == ValidationKind::Whole && n.fract() != 0.0 {
265 false
266 } else {
267 compare_op(dv.op, n, num(&dv.f1), dv.f2.as_deref().and_then(num))
268 }
269 }
270 None => false,
271 },
272 };
273 if ok {
274 return None;
275 }
276 let msg = if dv.error_message.is_empty() {
277 "This value doesn't match the data validation restrictions defined for this cell.".to_string()
278 } else {
279 dv.error_message.clone()
280 };
281 Some((dv.clone(), msg))
282}
283
284fn formula_at(f: &str, first: CellRef, at: CellRef) -> String {
286 if first == at {
287 return f.to_string();
288 }
289 match crate::formula::parse(f.trim_start_matches('=')) {
290 Ok(e) => {
291 crate::formula::print(&crate::formula::adjust::shift_relative(e, at.row as i64 - first.row as i64, at.col as i64 - first.col as i64))
292 }
293 Err(_) => f.to_string(),
294 }
295}
296
297pub(in super::super) fn move_rule_formula(f: &str, first: CellRef, to: CellRef) -> String {
300 if first == to {
301 return f.to_string();
302 }
303 let moved = formula_at(f, first, to);
304 let has_relative = crate::formula::parse(f.trim_start_matches('=')).is_ok_and(|e| crate::formula::print(&e) != moved);
305 let eq = if f.starts_with('=') { "=" } else { "" };
306 if has_relative { format!("{eq}{moved}") } else { f.to_string() }
307}
308
309fn compare_op(op: CfOperator, n: f64, a: Option<f64>, b: Option<f64>) -> bool {
310 let Some(a) = a else { return false };
311 match op {
312 CfOperator::Between => b.is_some_and(|b| n >= a.min(b) && n <= a.max(b)),
313 CfOperator::NotBetween => b.is_some_and(|b| n < a.min(b) || n > a.max(b)),
314 CfOperator::Equal => n == a,
315 CfOperator::NotEqual => n != a,
316 CfOperator::Greater => n > a,
317 CfOperator::Less => n < a,
318 CfOperator::GreaterOrEqual => n >= a,
319 CfOperator::LessOrEqual => n <= a,
320 }
321}
322
323pub(super) fn validate_cmd(s: &mut Session, p: &Json) -> Result<Json> {
324 let d = s.doc()?;
325 let at = cell_param(s, p, "cell")?.unwrap_or(d.selection.active);
326 let input = str_param(p, "input").unwrap_or("");
327 match check_validation(&d.wb, d.wb.active_sheet(), at, input) {
328 None => Ok(json!({"ok": true})),
329 Some((dv, msg)) => Ok(json!({"ok": false, "message": msg, "title": dv.error_title, "style": format!("{:?}", dv.error_style)})),
330 }
331}
332
333pub(super) fn circle_invalid(s: &mut Session, _: &Json) -> Result<Json> {
334 let d = s.doc()?;
335 let sheet = d.wb.active_sheet();
336 let Some(sh) = d.wb.sheet(sheet) else { return Err(EngineError::NoDocument) };
337 let mut bad_cells = Vec::new();
338 for dv in &sh.validations {
339 for r in &dv.ranges {
340 let r = sh.used_range().and_then(|u| r.intersection(&u));
341 let Some(r) = r else { continue };
342 for c in r.iter().take(100_000) {
343 let text = sh.input_text(c);
344 if !text.is_empty() && check_validation(&d.wb, sheet, c, &text).is_some() {
345 bad_cells.push(c.a1());
346 }
347 }
348 }
349 }
350 Ok(json!({"invalid": bad_cells}))
351}