1use std::collections::HashSet;
9use std::sync::OnceLock;
10
11use crate::engine::cmd::edit::is_formula_input;
12use crate::formula::lexer::{Tok, tokenize};
13use crate::formula::normalize_function_name;
14use serde::Deserialize;
15use serde_json::Value as Json;
16
17pub const MAX: usize = 10;
19const FORMULA_MAX: usize = 8_192;
21const SCAN_BUDGET: usize = 2_000;
23
24pub fn defaults() -> Vec<String> {
26 ["SUM", "AVERAGE", "IF", "COUNT", "MAX", "MIN", "SUMIF", "XLOOKUP", "PMT", "TODAY"].map(String::from).to_vec()
27}
28
29fn known(name: &str) -> bool {
31 static NAMES: OnceLock<HashSet<String>> = OnceLock::new();
32 NAMES
33 .get_or_init(|| {
34 crate::engine::cmd::formulas::function_list().iter().filter_map(|f| f["name"].as_str().map(str::to_ascii_uppercase)).collect()
35 })
36 .contains(name)
37}
38
39pub fn functions_in_formula(text: &str) -> Vec<String> {
42 if !is_formula_input(text) {
43 return vec![];
44 }
45 let body = text.strip_prefix('=').unwrap_or(text);
46 if body.len() > FORMULA_MAX {
47 return vec![];
48 }
49 let mut out: Vec<String> = Vec::new();
50 for t in tokenize(body).unwrap_or_default() {
51 if let Tok::Func(name) = t.tok {
52 let name = normalize_function_name(&name);
53 if known(&name) && !out.contains(&name) {
54 out.push(name);
55 }
56 }
57 }
58 out
59}
60
61fn formulas_newest_first<'a>(id: &str, params: &'a Json, budget: usize) -> Vec<&'a str> {
63 fn walk<'a>(v: &'a Json, out: &mut Vec<&'a str>, depth: usize, budget: usize) {
64 match v {
65 Json::String(s) if is_formula_input(s) && out.len() < budget => out.push(s),
66 Json::Array(a) if depth < 2 => {
68 for x in a.iter().rev() {
69 if out.len() >= budget {
70 return;
71 }
72 walk(x, out, depth + 1, budget);
73 }
74 }
75 _ => {}
76 }
77 }
78 let mut out = Vec::new();
79 if id.starts_with("cell.") || id.starts_with("range.") {
80 for key in ["formula", "values", "value", "input"] {
81 if let Some(v) = params.get(key) {
82 walk(v, &mut out, 0, budget);
83 }
84 }
85 }
86 out
87}
88
89pub fn push(recent: &mut Vec<String>, names: &[String]) {
91 let mut next: Vec<String> = Vec::with_capacity(MAX);
92 for n in names.iter().chain(recent.iter()) {
93 if next.len() == MAX {
94 break;
95 }
96 if !next.contains(n) {
97 next.push(n.clone());
98 }
99 }
100 *recent = next;
101}
102
103pub(crate) fn note(recent: &mut Vec<String>, id: &str, params: &Json) {
106 let mut names: Vec<String> = Vec::new();
107 for f in formulas_newest_first(id, params, SCAN_BUDGET) {
108 for n in functions_in_formula(f) {
109 if !names.contains(&n) {
110 names.push(n);
111 }
112 }
113 if names.len() >= MAX {
114 break;
115 }
116 }
117 if !names.is_empty() {
118 push(recent, &names);
119 }
120}
121
122pub(crate) fn lenient<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Vec<String>, D::Error> {
126 let raw = Json::deserialize(d)?;
127 let Some(list) = raw.as_array() else { return Ok(defaults()) };
128 let mut out = Vec::new();
129 for name in list.iter().filter_map(Json::as_str) {
130 if out.len() == MAX {
131 break;
132 }
133 let name = name.to_ascii_uppercase();
134 if known(&name) && !out.contains(&name) {
135 out.push(name);
136 }
137 }
138 Ok(out)
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use serde_json::json;
145
146 #[test]
147 fn formulas_name_their_functions_outermost_first() {
148 assert_eq!(functions_in_formula("=IF(SUM(A1:A3)>2,_xlfn.XLOOKUP(1,B:B,C:C),sum(1))"), ["IF", "SUM", "XLOOKUP"]);
149 assert!(functions_in_formula("=MYLAMBDA(1)+A1").is_empty(), "only known functions");
150 assert!(functions_in_formula("SUM(1)").is_empty(), "not a formula");
151 assert!(functions_in_formula("=\"SUM(\"").is_empty(), "inside a string");
152 assert!(functions_in_formula(&format!("=SUM({})", "1+".repeat(9000))).is_empty(), "too long");
153 assert!(functions_in_formula("=((((").is_empty());
154 assert!(functions_in_formula("=").is_empty());
155 assert_eq!(functions_in_formula("+SUM(1)"), ["SUM"]);
157 assert_eq!(functions_in_formula("-ABS(A1)"), ["ABS"]);
158 assert!(functions_in_formula("-5").is_empty() && functions_in_formula("+1e3").is_empty(), "numbers");
159 assert!(functions_in_formula("+").is_empty() && functions_in_formula("- SUM(1)").is_empty(), "not formulas");
160 }
161
162 #[test]
163 fn commands_give_their_formulas_newest_first() {
164 let read = |id: &str, params: Json| formulas_newest_first(id, ¶ms, SCAN_BUDGET).into_iter().map(String::from).collect::<Vec<_>>();
165 assert_eq!(read("range.setValues", json!({"values": [["=SUM(1)", 2], ["x", "=PMT(1,2,3)"]]})), ["=PMT(1,2,3)", "=SUM(1)"]);
166 assert_eq!(read("cell.set", json!({"input": "=NPV(1,2)"})), ["=NPV(1,2)"]);
167 assert_eq!(read("range.setValues", json!({"values": [["+SUM(1)", "-5", "-ABS(2)"]]})), ["-ABS(2)", "+SUM(1)"]);
168 assert!(read("home.bold", json!({"input": "=IRR(1)"})).is_empty(), "only cell.* and range.* enter formulas");
169 assert!(read("range.setValues", json!({"values": [[["=FV(1,2,3)"]]]})).is_empty(), "deeper than rows of cells");
170 let rows: Vec<Json> = (0..5_000).map(|i| json!([format!("=ABS({i})")])).collect();
172 let paste = json!({ "values": rows });
173 let last = formulas_newest_first("range.setValues", &paste, 3);
174 assert_eq!(last, ["=ABS(4999)", "=ABS(4998)", "=ABS(4997)"]);
175 }
176
177 #[test]
178 fn recent_keeps_ten_distinct_newest_first() {
179 let mut r = defaults();
180 assert!(r.iter().all(|n| known(n)), "the default list names known functions");
181 push(&mut r, &["PMT".into(), "NPV".into()]);
182 assert_eq!(r.len(), MAX);
183 assert_eq!(&r[..3], ["PMT", "NPV", "SUM"]);
184 assert_eq!(r.iter().filter(|n| *n == "PMT").count(), 1);
185 let many: Vec<String> =
186 ["ABS", "ACOS", "AND", "CHOOSE", "DATE", "EXP", "FIND", "INDEX", "LEFT", "LN", "MOD", "NOW"].map(String::from).to_vec();
187 push(&mut r, &many);
188 assert_eq!(r, many[..MAX]);
189 }
190
191 #[test]
192 fn a_saved_list_is_read_leniently() {
193 let prefs = |v: Json| serde_json::from_value::<crate::engine::Prefs>(v).unwrap();
194 assert_eq!(prefs(json!({"r1c1": true})).recent_functions, defaults(), "a prefs.json from before the list gets the default");
195 let bad = prefs(json!({"r1c1": true, "recentFunctions": 5}));
197 assert!(bad.r1c1);
198 assert_eq!(bad.recent_functions, defaults());
199 let mut odd: Vec<Json> = ["npv", "NOT_A_FUNCTION", "SUM", "NPV"].iter().map(|n| json!(n)).chain([json!(3), json!(null)]).collect();
200 odd.extend(crate::engine::cmd::formulas::function_list().into_iter().take(40).map(|f| f["name"].clone()));
201 let odd = prefs(json!({ "recentFunctions": odd }));
202 assert_eq!(&odd.recent_functions[..2], ["NPV", "SUM"]);
203 assert_eq!(odd.recent_functions.len(), MAX);
204 let back = prefs(serde_json::to_value(&odd).unwrap());
205 assert_eq!(back.recent_functions, odd.recent_functions, "the list survives a save and reload");
206 }
207}