1use crate::formula::FuncId;
14use crate::value::ValueType;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum Kind {
19 Scalar,
22 Aggregate,
27 PerItem,
30 Array,
33 Lazy,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum Arg {
41 Number,
42 Text,
43 Boolean,
44 Date,
46 DateLike,
49 Any,
51 Category,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum Ret {
58 Fixed(ValueType),
59 Unify,
62 Arg(usize),
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
68pub enum Category {
69 Aggregation,
70 Logical,
71 Information,
72 Math,
73 Rounding,
74 Text,
75 DateTime,
76 Statistical,
77 Financial,
78 Dimensional,
79 Engineering,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum Over {
85 At(&'static [usize]),
87 Every(usize, usize),
90 AtEvery(&'static [usize], usize, usize),
93 Leading(usize),
98}
99
100impl Over {
101 pub fn contains(&self, i: usize) -> bool {
103 match self {
104 Over::At(v) => v.contains(&i),
105 Over::Every(start, step) => i >= *start && (i - start).is_multiple_of(*step),
106 Over::AtEvery(v, start, step) => {
107 v.contains(&i) || (i >= *start && (i - start).is_multiple_of(*step))
108 }
109 Over::Leading(min) => i < *min,
110 }
111 }
112}
113
114#[derive(Debug, Clone, Copy)]
116pub struct FnDef {
117 pub id: FuncId,
118 pub name: &'static str,
120 pub aliases: &'static [&'static str],
122 pub kind: Kind,
123 pub category: Category,
124 pub params: &'static [Arg],
127 pub required: usize,
128 pub variadic: bool,
129 pub ret: Ret,
130 pub doc: &'static str,
132 pub over: Over,
134}
135
136impl FnDef {
137 pub fn accepts(&self, n: usize) -> bool {
139 n >= self.required && (self.variadic || n <= self.params.len())
140 }
141
142 pub fn param(&self, i: usize) -> Arg {
144 self.params
145 .get(i)
146 .or(self.params.last())
147 .copied()
148 .unwrap_or(Arg::Any)
149 }
150
151 pub fn reduced(&self, i: usize, arg: &crate::formula::Expr) -> bool {
155 match self.over {
156 Over::Leading(_) => {
157 matches!(arg, crate::formula::Expr::Ref(_, s) if !s.over.is_empty())
158 }
159 o => o.contains(i),
160 }
161 }
162
163 pub fn param_names(&self) -> Vec<String> {
169 self.named_params().unwrap_or_else(|| self.type_names())
170 }
171
172 fn named_params(&self) -> Option<Vec<String>> {
174 let mut rows = params::PARAM_NAMES.iter().filter(|(n, _)| *n == self.name);
177 rows.clone()
178 .find(|(_, p)| p.contains(" OVER ") == self.kind.reduces())
179 .or_else(|| rows.next())
180 .map(|(_, p)| split_params(p))
181 .or_else(|| spelled_params(self.name, self.doc))
182 }
183
184 fn type_names(&self) -> Vec<String> {
186 let mut sig = self.signature();
187 sig.truncate(sig.len() - 1);
188 let inner = &sig[self.name.len() + 1..];
189 if inner.is_empty() {
190 return Vec::new();
191 }
192 inner.split(", ").map(str::to_string).collect()
193 }
194
195 pub fn hint(&self) -> String {
197 format!("{}({})", self.name, self.param_names().join(", "))
198 }
199
200 pub fn signature(&self) -> String {
202 let names: Vec<String> = self
203 .params
204 .iter()
205 .enumerate()
206 .map(|(i, a)| {
207 let t = match a {
208 Arg::Number => "number",
209 Arg::Text => "text",
210 Arg::Boolean => "boolean",
211 Arg::Date => "date",
212 Arg::DateLike => "date",
213 Arg::Any => "value",
214 Arg::Category => "category",
215 };
216 let t = if self.kind.reduces() && self.over.contains(i) {
217 format!("{t} OVER category")
218 } else {
219 t.to_string()
220 };
221 if i >= self.required {
222 format!("[{t}]")
223 } else {
224 t
225 }
226 })
227 .collect();
228 let mut s = names.join(", ");
229 if self.variadic {
230 s.push_str(", …");
231 }
232 format!("{}({s})", self.name)
233 }
234}
235
236use Arg::{Any as A, Boolean as B, Number as N, Text as T};
237use Category as C;
238use ValueType as V;
239
240#[allow(
241 clippy::too_many_arguments,
242 reason = "a table row: one argument per FnDef field"
243)]
244#[cfg_attr(coverage_nightly, coverage(off))]
247pub(crate) const fn f(
248 id: u32,
249 name: &'static str,
250 kind: Kind,
251 category: Category,
252 params: &'static [Arg],
253 required: usize,
254 variadic: bool,
255 ret: Ret,
256 doc: &'static str,
257) -> FnDef {
258 FnDef {
259 id: FuncId(id),
260 name,
261 aliases: &[],
262 kind,
263 category,
264 params,
265 required,
266 variadic,
267 ret,
268 doc,
269 over: Over::At(&[0]),
270 }
271}
272
273#[cfg_attr(coverage_nightly, coverage(off))]
275pub const fn over(mut d: FnDef, over: Over) -> FnDef {
276 d.over = over;
277 d
278}
279
280#[cfg_attr(coverage_nightly, coverage(off))]
281pub(crate) const fn alias(mut d: FnDef, aliases: &'static [&'static str]) -> FnDef {
282 d.aliases = aliases;
283 d
284}
285
286#[cfg_attr(coverage_nightly, coverage(off))]
288pub(crate) const fn s(
289 id: u32,
290 name: &'static str,
291 cat: Category,
292 params: &'static [Arg],
293 ret: ValueType,
294 doc: &'static str,
295) -> FnDef {
296 f(
297 id,
298 name,
299 Kind::Scalar,
300 cat,
301 params,
302 params.len(),
303 false,
304 Ret::Fixed(ret),
305 doc,
306 )
307}
308
309#[cfg_attr(coverage_nightly, coverage(off))]
311pub(crate) const fn so(
312 id: u32,
313 name: &'static str,
314 cat: Category,
315 params: &'static [Arg],
316 required: usize,
317 ret: ValueType,
318 doc: &'static str,
319) -> FnDef {
320 f(
321 id,
322 name,
323 Kind::Scalar,
324 cat,
325 params,
326 required,
327 false,
328 Ret::Fixed(ret),
329 doc,
330 )
331}
332
333#[cfg_attr(coverage_nightly, coverage(off))]
335pub(crate) const fn sv(
336 id: u32,
337 name: &'static str,
338 cat: Category,
339 arg: &'static [Arg],
340 ret: ValueType,
341 doc: &'static str,
342) -> FnDef {
343 f(
344 id,
345 name,
346 Kind::Scalar,
347 cat,
348 arg,
349 1,
350 true,
351 Ret::Fixed(ret),
352 doc,
353 )
354}
355
356#[cfg_attr(coverage_nightly, coverage(off))]
357pub(crate) const fn agg(
358 id: u32,
359 name: &'static str,
360 arg: &'static [Arg],
361 ret: ValueType,
362 doc: &'static str,
363) -> FnDef {
364 f(
365 id,
366 name,
367 Kind::Aggregate,
368 C::Aggregation,
369 arg,
370 1,
371 false,
372 Ret::Fixed(ret),
373 doc,
374 )
375}
376
377#[rustfmt::skip]
380pub static CORE: &[FnDef] = &[
381 alias(agg(1, "SUM", &[N], V::Number, "the sum over a category"), &["DSUM"]),
383 alias(agg(2, "AVERAGE", &[N], V::Number, "the mean over a category"), &["AVG", "DAVERAGE"]),
384 alias(agg(3, "MIN", &[N], V::Number, "the smallest value over a category"), &["DMIN"]),
385 alias(agg(4, "MAX", &[N], V::Number, "the largest value over a category"), &["DMAX"]),
386 alias(agg(5, "PRODUCT", &[N], V::Number, "the product over a category"), &["DPRODUCT"]),
387 agg(6, "SUMSQ", &[N], V::Number, "the sum of squares over a category"),
388 agg(7, "AND", &[B], V::Boolean, "whether every value over a category is true"),
389 agg(8, "OR", &[B], V::Boolean, "whether any value over a category is true"),
390 agg(9, "XOR", &[B], V::Boolean, "whether an odd number of values over a category are true"),
391 sv(22, "SUM", C::Math, &[N], V::Number, "the sum of the arguments"),
393 sv(23, "AVERAGE", C::Math, &[N], V::Number, "the mean of the arguments"),
394 alias(sv(20, "MIN", C::Math, &[N], V::Number, "the smallest argument"), &["MIN2"]),
395 alias(sv(21, "MAX", C::Math, &[N], V::Number, "the largest argument"), &["MAX2"]),
396 sv(24, "PRODUCT", C::Math, &[N], V::Number, "the product of the arguments"),
397 sv(25, "SUMSQ", C::Math, &[N], V::Number, "the sum of the squares of the arguments"),
398 sv(26, "AND", C::Logical, &[B], V::Boolean, "whether every argument is true"),
399 sv(27, "OR", C::Logical, &[B], V::Boolean, "whether any argument is true"),
400 sv(28, "XOR", C::Logical, &[B], V::Boolean, "whether an odd number of arguments are true"),
401 f(30, "IF", Kind::Lazy, C::Logical, &[B, A, A], 2, false, Ret::Unify, "then where the test is true, else (or no cell) where it is false"),
403 f(31, "IFS", Kind::Lazy, C::Logical, &[B, A], 2, true, Ret::Unify, "the value paired with the first true test"),
404 f(32, "SWITCH", Kind::Lazy, C::Logical, &[A, A, A], 3, true, Ret::Unify, "the result paired with the first value equal to the expression, else the default"),
405 f(33, "CHOOSE", Kind::Lazy, C::Logical, &[N, A], 2, true, Ret::Unify, "the index_num-th value (1-based)"),
406 f(34, "IFERROR", Kind::Lazy, C::Logical, &[A, A], 2, false, Ret::Unify, "the value, or the alternative where it is an error"),
407 f(35, "IFNA", Kind::Lazy, C::Logical, &[A, A], 2, false, Ret::Unify, "the value, or the alternative where it is #N/A"),
408 f(36, "CLEARERROR", Kind::Lazy, C::Logical, &[A, A], 1, false, Ret::Unify, "the value, or the alternative (or no cell) where it is an error"),
409 f(37, "CLEARZERO", Kind::Lazy, C::Logical, &[N, N], 1, false, Ret::Fixed(V::Number), "the value, or the alternative (or no cell) where it is zero"),
410 f(41, "COALESCE", Kind::Lazy, C::Logical, &[A, A], 2, true, Ret::Unify, "a cell wherever any argument that reads a measure has one: the first argument with a cell, else the last (the default)"),
411 so(38, "BETWEEN", C::Logical, &[N, N, N, B], 3, V::Boolean, "whether low ≤ x ≤ high (strict when inclusive is FALSE)"),
412 s(39, "TRUE", C::Logical, &[], V::Boolean, "TRUE"),
413 s(40, "FALSE", C::Logical, &[], V::Boolean, "FALSE"),
414 s(50, "ISERROR", C::Information, &[A], V::Boolean, "whether the value is any error"),
416 s(51, "ISERR", C::Information, &[A], V::Boolean, "whether the value is an error other than #N/A"),
417 s(52, "ISNA", C::Information, &[A], V::Boolean, "whether the value is #N/A"),
418 s(53, "NA", C::Information, &[], V::Number, "the #N/A error"),
419 s(54, "ERROR", C::Information, &[T], V::Number, "the error a code names (\"#DIV/0!\")"),
420 s(55, "ERROR.TYPE", C::Information, &[A], V::Number, "1 #NULL!, 2 #DIV/0!, 3 #VALUE!, 4 #REF!, 5 #NAME?, 6 #NUM!, 7 #N/A; #N/A for a non-error"),
421 s(10, "ABS", C::Math, &[N], V::Number, "absolute value"),
423 s(15, "NEG", C::Math, &[N], V::Number, "negation"),
424 s(100, "SIGN", C::Math, &[N], V::Number, "−1, 0 or 1"),
425 s(14, "SQRT", C::Math, &[N], V::Number, "square root (#NUM! below zero)"),
426 s(101, "SQRTPI", C::Math, &[N], V::Number, "√(x·π)"),
427 s(102, "EXP", C::Math, &[N], V::Number, "e to the power x"),
428 s(103, "EXPM1", C::Math, &[N], V::Number, "eˣ − 1, accurate near 0"),
429 s(104, "LN", C::Math, &[N], V::Number, "natural logarithm"),
430 s(105, "LN1P", C::Math, &[N], V::Number, "ln(1 + x), accurate near 0"),
431 so(106, "LOG", C::Math, &[N, N], 1, V::Number, "logarithm to a base (default 10)"),
432 s(107, "LOG10", C::Math, &[N], V::Number, "base-10 logarithm"),
433 s(108, "LOG2", C::Math, &[N], V::Number, "base-2 logarithm"),
434 s(109, "POWER", C::Math, &[N, N], V::Number, "number raised to power (same as ^)"),
435 s(110, "MOD", C::Math, &[N, N], V::Number, "remainder, with the divisor's sign"),
436 s(111, "QUOTIENT", C::Math, &[N, N], V::Number, "integer part of a quotient"),
437 s(112, "PI", C::Math, &[], V::Number, "π"),
438 s(113, "DEGREES", C::Math, &[N], V::Number, "radians to degrees"),
439 s(114, "RADIANS", C::Math, &[N], V::Number, "degrees to radians"),
440 s(115, "SIN", C::Math, &[N], V::Number, "sine (radians)"),
441 s(116, "COS", C::Math, &[N], V::Number, "cosine"),
442 s(117, "TAN", C::Math, &[N], V::Number, "tangent"),
443 s(118, "ASIN", C::Math, &[N], V::Number, "arcsine"),
444 s(119, "ACOS", C::Math, &[N], V::Number, "arccosine"),
445 s(120, "ATAN", C::Math, &[N], V::Number, "arctangent"),
446 s(121, "ATAN2", C::Math, &[N, N], V::Number, "angle of the point (x, y)"),
447 s(122, "SINH", C::Math, &[N], V::Number, "hyperbolic sine"),
448 s(123, "COSH", C::Math, &[N], V::Number, "hyperbolic cosine"),
449 s(124, "TANH", C::Math, &[N], V::Number, "hyperbolic tangent"),
450 s(125, "ASINH", C::Math, &[N], V::Number, "inverse hyperbolic sine"),
451 s(126, "ACOSH", C::Math, &[N], V::Number, "inverse hyperbolic cosine"),
452 s(127, "ATANH", C::Math, &[N], V::Number, "inverse hyperbolic tangent"),
453 s(128, "SEC", C::Math, &[N], V::Number, "secant"),
454 s(129, "CSC", C::Math, &[N], V::Number, "cosecant"),
455 s(130, "COT", C::Math, &[N], V::Number, "cotangent"),
456 s(131, "SECH", C::Math, &[N], V::Number, "hyperbolic secant"),
457 s(132, "CSCH", C::Math, &[N], V::Number, "hyperbolic cosecant"),
458 s(133, "COTH", C::Math, &[N], V::Number, "hyperbolic cotangent"),
459 s(134, "ACOT", C::Math, &[N], V::Number, "inverse cotangent"),
460 s(135, "ACOTH", C::Math, &[N], V::Number, "inverse hyperbolic cotangent"),
461 s(136, "SINPI", C::Math, &[N], V::Number, "sin(πx), exact at integers"),
462 s(137, "COSPI", C::Math, &[N], V::Number, "cos(πx), exact at integers"),
463 s(138, "TANPI", C::Math, &[N], V::Number, "tan(πx), exact at integers"),
464 s(139, "COTPI", C::Math, &[N], V::Number, "cot(πx), exact at integers"),
465 sv(140, "HYPOT", C::Math, &[N], V::Number, "√(Σ xᵢ²) without overflow"),
466 s(141, "FACT", C::Math, &[N], V::Number, "factorial"),
467 s(142, "FACTDOUBLE", C::Math, &[N], V::Number, "double factorial n!!"),
468 s(143, "COMBIN", C::Math, &[N, N], V::Number, "combinations of number items taken number_chosen at a time"),
469 s(144, "COMBINA", C::Math, &[N, N], V::Number, "combinations with repetition"),
470 s(145, "PERMUT", C::Math, &[N, N], V::Number, "permutations of number items taken number_chosen at a time"),
471 s(146, "PERMUTATIONA", C::Math, &[N, N], V::Number, "permutations with repetition"),
472 sv(147, "GCD", C::Math, &[N], V::Number, "greatest common divisor"),
473 sv(148, "LCM", C::Math, &[N], V::Number, "least common multiple"),
474 sv(149, "MULTINOMIAL", C::Math, &[N], V::Number, "(Σ n)! / Π n!"),
475 sv(150, "RAWSUBTRACT", C::Math, &[N], V::Number, "the first argument minus the rest"),
476 sv(151, "INVSUMINV", C::Math, &[N], V::Number, "1 / Σ(1/xᵢ)"),
477 so(11, "ROUND", C::Rounding, &[N, N], 1, V::Number, "to num_digits decimal places, halves away from zero"),
479 so(200, "ROUNDUP", C::Rounding, &[N, N], 1, V::Number, "away from zero to num_digits places"),
480 so(201, "ROUNDDOWN", C::Rounding, &[N, N], 1, V::Number, "toward zero to num_digits places"),
481 s(202, "ROUNDSIG", C::Rounding, &[N, N], V::Number, "to digits significant digits"),
482 so(203, "TRUNC", C::Rounding, &[N, N], 1, V::Number, "truncate to num_digits places"),
483 s(204, "INT", C::Rounding, &[N], V::Number, "down to an integer"),
484 so(12, "FLOOR", C::Rounding, &[N, N], 1, V::Number, "down to a multiple of significance (default 1)"),
485 s(13, "CEIL", C::Rounding, &[N], V::Number, "up to an integer"),
486 so(205, "CEILING", C::Rounding, &[N, N], 1, V::Number, "up to a multiple of significance (default 1)"),
487 so(206, "FLOOR.MATH", C::Rounding, &[N, N, N], 1, V::Number, "down to a multiple; mode ≠ 0 rounds negatives toward zero"),
488 so(207, "CEILING.MATH", C::Rounding, &[N, N, N], 1, V::Number, "up to a multiple; mode ≠ 0 rounds negatives away from zero"),
489 so(208, "FLOOR.PRECISE", C::Rounding, &[N, N], 1, V::Number, "down to a multiple, ignoring the multiple's sign"),
490 so(209, "CEILING.PRECISE", C::Rounding, &[N, N], 1, V::Number, "up to a multiple, ignoring the multiple's sign"),
491 so(210, "ISO.CEILING", C::Rounding, &[N, N], 1, V::Number, "same as CEILING.PRECISE"),
492 s(211, "MROUND", C::Rounding, &[N, N], V::Number, "to the nearest multiple"),
493 s(212, "EVEN", C::Rounding, &[N], V::Number, "away from zero to an even integer"),
494 s(213, "ODD", C::Rounding, &[N], V::Number, "away from zero to an odd integer"),
495 s(300, "CHAR", C::Text, &[N], V::Text, "the character with code n (1–255)"),
497 s(301, "UNICHAR", C::Text, &[N], V::Text, "the Unicode character n"),
498 s(302, "CODE", C::Text, &[T], V::Number, "the code of the first character"),
499 s(303, "UNICODE", C::Text, &[T], V::Number, "the code point of the first character"),
500 s(304, "CLEAN", C::Text, &[T], V::Text, "remove non-printing characters"),
501 sv(305, "CONCAT", C::Text, &[A], V::Text, "join values as text (same as &)"),
502 sv(306, "CONCATENATE", C::Text, &[A], V::Text, "join values as text"),
503 s(307, "EXACT", C::Text, &[T, T], V::Boolean, "case-sensitive equality"),
504 so(308, "FIND", C::Text, &[T, T, N], 2, V::Number, "1-based position of find_text in within_text, case-sensitive"),
505 so(309, "SEARCH", C::Text, &[T, T, N], 2, V::Number, "1-based position, case-insensitive, with * and ? wildcards"),
506 so(310, "LEFT", C::Text, &[T, N], 1, V::Text, "the first num_chars characters (default 1)"),
507 so(311, "RIGHT", C::Text, &[T, N], 1, V::Text, "the last num_chars characters (default 1)"),
508 s(312, "MID", C::Text, &[T, N, N], V::Text, "num_chars characters from a 1-based start_num"),
509 s(313, "LEN", C::Text, &[T], V::Number, "the number of characters"),
510 s(314, "LOWER", C::Text, &[T], V::Text, "lowercase"),
511 s(315, "UPPER", C::Text, &[T], V::Text, "uppercase"),
512 s(316, "PROPER", C::Text, &[T], V::Text, "capitalize each word"),
513 s(317, "REPLACE", C::Text, &[T, N, N, T], V::Text, "replace num_chars characters from a 1-based start_num"),
514 s(318, "REPT", C::Text, &[T, N], V::Text, "repeat text number_times times"),
515 so(319, "SUBSTITUTE", C::Text, &[T, T, T, N], 3, V::Text, "replace every (or the instance_num-th) occurrence of old_text"),
516 s(320, "TRIM", C::Text, &[T], V::Text, "remove leading, trailing and repeated spaces"),
517 s(321, "VALUE", C::Text, &[T], V::Number, "text as a number"),
518 so(322, "TEXTBEFORE", C::Text, &[T, T, N, N, N, A], 2, V::Text, "text before the instance_num-th delimiter"),
519 so(323, "TEXTAFTER", C::Text, &[T, T, N, N, N, A], 2, V::Text, "text after the instance_num-th delimiter"),
520 s(324, "TEXTBETWEEN", C::Text, &[T, T, T], V::Text, "text between the first start delimiter and the next end delimiter"),
521 s(325, "COUNTMATCHES", C::Text, &[T, T], V::Number, "how many times search occurs in text"),
522];
523
524pub const WHERE: FuncId = FuncId(990);
528pub const WINDOW: FuncId = FuncId(991);
533pub const INTO: FuncId = FuncId(992);
536
537pub const NAMED_LAMBDA_BASE: u32 = 1_000_000;
540
541pub fn named_lambda(id: u32) -> FuncId {
543 FuncId(NAMED_LAMBDA_BASE.saturating_add(id))
544}
545
546pub fn named_lambda_of(f: FuncId) -> Option<u32> {
548 f.0.checked_sub(NAMED_LAMBDA_BASE)
549}
550
551pub fn lambda_param(i: usize) -> crate::MeasureId {
553 crate::MeasureId(LAMBDA_PARAMS - i as u32)
554}
555
556const LAMBDA_PARAMS: u32 = LAMBDA_SLOTS - 2048;
558
559pub fn lambda_param_of(id: crate::MeasureId) -> Option<usize> {
561 let d = LAMBDA_PARAMS.checked_sub(id.0)?;
562 (d < 256).then_some(d as usize)
563}
564
565pub fn lambda_category(i: usize) -> crate::CategoryId {
568 crate::CategoryId(LAMBDA_PARAMS - i as u32)
569}
570
571pub fn lambda_category_of(c: crate::CategoryId) -> Option<usize> {
573 lambda_param_of(crate::MeasureId(c.0))
574}
575
576pub const POSITION: FuncId = FuncId(900);
578
579pub fn lambda_slot(k: u32, i: u32) -> crate::MeasureId {
583 crate::MeasureId(LAMBDA_SLOTS - 2 * k - i)
584}
585
586pub const LAMBDA_SLOTS: u32 = u32::MAX - 4096;
588
589pub fn lambda_slot_of(id: crate::MeasureId) -> Option<(u32, u32)> {
591 let d = LAMBDA_SLOTS.checked_sub(id.0)?;
592 (d < 2048).then_some((d / 2, d % 2))
593}
594
595mod analysis;
596mod array;
597mod date;
598mod dimensional;
599mod dist_extra;
600mod engineering;
601mod financial;
602mod financial_reduce;
603mod lookup;
604mod math_extra;
605mod options;
606mod params;
607mod statistical;
608mod statistical_reduce;
609
610static FAMILIES: &[&[FnDef]] = &[
612 CORE,
613 date::DATE,
614 dimensional::DIMENSIONAL,
615 array::ARRAY,
616 statistical::STATISTICAL,
617 statistical_reduce::STATISTICAL_REDUCE,
618 financial::FINANCIAL,
619 financial_reduce::FINANCIAL_REDUCE,
620 lookup::LOOKUP,
621 engineering::ENGINEERING,
622 math_extra::MATH_EXTRA,
623 dist_extra::DIST_EXTRA,
624 options::OPTIONS,
625 analysis::ANALYSIS,
626];
627
628pub fn all() -> impl Iterator<Item = &'static FnDef> {
630 FAMILIES.iter().flat_map(|f| f.iter())
631}
632
633pub fn by_id(id: FuncId) -> Option<&'static FnDef> {
635 all().find(|d| d.id == id)
636}
637
638pub fn by_name(name: &str) -> impl Iterator<Item = &'static FnDef> + '_ {
642 all().filter(move |d| {
643 d.name.eq_ignore_ascii_case(name) || d.aliases.iter().any(|a| a.eq_ignore_ascii_case(name))
644 })
645}
646
647impl Kind {
648 pub fn reduces(self) -> bool {
650 matches!(self, Kind::Aggregate | Kind::PerItem | Kind::Array)
651 }
652}
653
654fn split_params(list: &str) -> Vec<String> {
657 let mut out: Vec<String> = Vec::new();
658 for p in list.split(", ") {
659 match out.last_mut() {
660 Some(last)
661 if (last.contains(" OVER ") || last.contains(" INTO "))
662 && p.starts_with(|c: char| c.is_ascii_uppercase()) =>
663 {
664 last.push_str(", ");
665 last.push_str(p);
666 }
667 _ => out.push(p.to_string()),
668 }
669 }
670 out
671}
672
673fn spelled_params(name: &str, doc: &str) -> Option<Vec<String>> {
676 let at = doc.find(&format!("{name}("))? + name.len() + 1;
677 let mut depth = 0usize;
678 let mut end = None;
679 for (i, c) in doc[at..].char_indices() {
680 match c {
681 '(' => depth += 1,
682 ')' if depth == 0 => {
683 end = Some(at + i);
684 break;
685 }
686 ')' => depth -= 1,
687 _ => {}
688 }
689 }
690 let inner = &doc[at..end?];
691 let parts: Vec<String> = split_params(inner)
692 .iter()
693 .map(|p| p.trim().to_string())
694 .collect();
695 let word = |p: &str| {
696 let p = p.split(" OVER ").next().unwrap_or(p);
698 let p = p.split(" INTO ").next().unwrap_or(p);
699 let p = p.trim_start_matches('[').trim_end_matches(']');
700 !p.is_empty()
701 && p.chars()
702 .all(|c| c.is_alphanumeric() || "_ …=.-".contains(c))
703 && !p.starts_with(|c: char| c.is_ascii_digit() || c == '-')
704 };
705 (!inner.is_empty() && parts.iter().all(|p| word(p))).then_some(parts)
706}
707
708pub fn search(query: &str) -> Vec<&'static FnDef> {
713 let q = query.trim().to_ascii_uppercase();
714 let mut hits: Vec<(u8, &'static FnDef)> = all()
715 .filter_map(|d| {
716 let names = std::iter::once(d.name).chain(d.aliases.iter().copied());
717 let rank = if q.is_empty() {
718 Some(3)
719 } else if names.clone().any(|n| n == q) {
720 Some(0)
721 } else if names.clone().any(|n| n.starts_with(&q)) {
722 Some(1)
723 } else if names.clone().any(|n| n.contains(&q)) {
724 Some(2)
725 } else if d.doc.to_ascii_uppercase().contains(&q) {
726 Some(4)
727 } else {
728 None
729 };
730 rank.map(|r| (r, d))
731 })
732 .collect();
733 hits.sort_by(|a, b| {
734 a.0.cmp(&b.0)
735 .then(a.1.name.cmp(b.1.name))
736 .then(a.1.id.cmp(&b.1.id))
737 });
738 let mut seen = std::collections::HashSet::new();
740 hits.into_iter()
741 .map(|(_, d)| d)
742 .filter(|d| seen.insert(d.name))
743 .collect()
744}
745
746pub fn closest(word: &str, n: usize) -> Vec<&'static str> {
750 let w = word.to_ascii_uppercase();
751 let limit = (w.chars().count() / 3).max(2);
752 let mut scored: Vec<(usize, &'static str)> = all()
753 .flat_map(|d| std::iter::once(d.name).chain(d.aliases.iter().copied()))
754 .map(|name| (edit_distance(&w, name), name))
755 .filter(|(dist, _)| *dist <= limit)
756 .collect();
757 scored.sort();
758 scored.dedup_by(|a, b| a.1 == b.1);
759 scored.into_iter().take(n).map(|(_, name)| name).collect()
760}
761
762pub fn edit_distance(a: &str, b: &str) -> usize {
764 let b: Vec<char> = b.chars().collect();
765 let mut row: Vec<usize> = (0..=b.len()).collect();
766 for (i, ca) in a.chars().enumerate() {
767 let mut prev = row[0];
768 row[0] = i + 1;
769 for (j, cb) in b.iter().enumerate() {
770 let cur = row[j + 1];
771 row[j + 1] = (prev + usize::from(ca != *cb)).min(row[j] + 1).min(cur + 1);
772 prev = cur;
773 }
774 }
775 row[b.len()]
776}
777
778pub fn complete(prefix: &str) -> Vec<(&'static str, &'static FnDef)> {
781 let p = prefix.to_ascii_uppercase();
782 if p.is_empty() {
783 return Vec::new();
784 }
785 let mut out: Vec<(&'static str, &'static FnDef)> = Vec::new();
786 for d in all() {
787 for n in std::iter::once(d.name).chain(d.aliases.iter().copied()) {
788 if n.starts_with(&p) && !out.iter().any(|(m, _)| *m == n) {
789 out.push((n, d));
790 }
791 }
792 }
793 out.sort_by(|a, b| a.0.len().cmp(&b.0.len()).then(a.0.cmp(b.0)));
794 out
795}
796
797pub fn call_at(text: &str, caret: usize) -> Option<(&'static FnDef, usize)> {
801 let before = text.get(..caret)?;
802 let mut stack: Vec<(usize, usize)> = Vec::new();
804 let mut in_str = false;
805 for (i, c) in before.char_indices() {
806 match c {
807 '"' => in_str = !in_str,
808 _ if in_str => {}
809 '(' => stack.push((i, 0)),
810 ')' => {
811 stack.pop();
812 }
813 ',' | ';' => {
814 if let Some(top) = stack.last_mut() {
815 top.1 += 1;
816 }
817 }
818 _ => {}
819 }
820 }
821 let (open, arg) = *stack.last()?;
822 let head = before[..open].trim_end();
823 let start = head
824 .rfind(|c: char| !(c.is_alphanumeric() || c == '_' || c == '.' || c == '@'))
825 .map_or(0, |i| {
826 i + head[i..].chars().next().map_or(1, char::len_utf8)
827 });
828 let name = &head[start..];
829 let def = by_name(name).next()?;
830 Some((def, arg))
831}
832
833pub fn aggregation(name: &str) -> Option<&'static FnDef> {
835 by_name(name).find(|d| d.kind.reduces())
836}
837
838pub fn callable(name: &str) -> Option<&'static FnDef> {
840 by_name(name).find(|d| !d.kind.reduces())
841}
842
843pub fn is_aggregation(id: FuncId) -> bool {
845 by_id(id).is_some_and(|d| d.kind.reduces())
846}
847
848#[cfg(test)]
849#[cfg_attr(coverage_nightly, coverage(off))]
850mod tests {
851 #[test]
855 fn search_complete_and_call_at() {
856 let names = |q: &str| search(q).iter().map(|d| d.name).take(3).collect::<Vec<_>>();
857 assert_eq!(names("pmt")[0], "PMT");
858 assert_eq!(names("ROUNDU"), ["ROUNDUP"]);
859 assert!(
860 search("payment").iter().any(|d| d.name == "PMT"),
861 "found by description"
862 );
863 assert_eq!(
864 search("").len(),
865 search("")
866 .iter()
867 .map(|d| d.name)
868 .collect::<HashSet<_>>()
869 .len()
870 );
871 assert!(search("zzzz-not-a-function").is_empty());
872 assert!(search("DSUM").iter().any(|d| d.name == "SUM"), "by alias");
873 let c: Vec<&str> = complete("roun").iter().map(|(n, _)| *n).collect();
874 assert_eq!(c[0], "ROUND");
875 assert!(c.contains(&"ROUNDUP"));
876 assert!(complete("").is_empty());
877 assert!(complete("DSU")
878 .iter()
879 .any(|(n, d)| *n == "DSUM" && d.name == "SUM"));
880 let at = |t: &str| call_at(t, t.len()).map(|(d, i)| (d.name, i));
881 assert_eq!(at("PMT("), Some(("PMT", 0)));
882 assert_eq!(at("PMT(0.05, 12, "), Some(("PMT", 2)));
883 assert_eq!(at("ROUND(PMT(0.05, 12, 100), "), Some(("ROUND", 1)));
884 assert_eq!(
885 at("ROUND(LEFT(\"a,b\", "),
886 Some(("LEFT", 1)),
887 "commas in strings skipped"
888 );
889 assert_eq!(
890 at("NORM.S.DIST(1; "),
891 Some(("NORM.S.DIST", 1)),
892 "dotted name, ; separator"
893 );
894 assert_eq!(at("PMT(1)"), None, "closed");
895 assert_eq!(at("(1 + "), None, "not a call");
896 assert_eq!(at("Nope("), None);
897 assert!(call_at("PMT(", 99).is_none());
898 let hint = |n: &str| by_name(n).next().unwrap().hint();
900 assert_eq!(hint("PMT"), "PMT(rate, nper, pv, [fv], [type])");
901 assert_eq!(
902 hint("IF"),
903 "IF(logical_test, value_if_true, [value_if_false])"
904 );
905 assert_eq!(callable("SUM").unwrap().hint(), "SUM(number1, …)");
906 assert_eq!(aggregation("SUM").unwrap().hint(), "SUM(values OVER T)");
907 assert_eq!(
908 hint("MINVERSE"),
909 "MINVERSE(A OVER R, C INTO R2, C2)",
910 "categories stay with their argument"
911 );
912 assert_eq!(
913 spelled_params("X", "X(a, b OVER T, [c] INTO K)").map(|v| v.len()),
914 Some(3)
915 );
916 assert_eq!(hint("PI"), "PI()");
917 assert_eq!(closest("PMTT", 3)[0], "PMT");
918 assert_eq!(closest("xlookp", 1), ["XLOOKUP"]);
919 assert!(closest("qqqqqqqqqqqq", 3).is_empty());
920 assert_eq!(edit_distance("kitten", "sitting"), 3);
921 assert_eq!(edit_distance("", "ab"), 2);
922 assert!(
923 spelled_params("X", "X(1, 2)").is_none(),
924 "an example call is not names"
925 );
926 assert!(spelled_params("X", "no call here").is_none());
927 assert!(spelled_params("X", "X(a, (b)").is_none(), "unclosed");
928 }
929
930 #[test]
933 #[ignore]
934 fn dump_hints() {
935 for d in super::all() {
936 let generic = !d.params.is_empty() && d.named_params().is_none();
937 println!("{} {}", if generic { "T" } else { "N" }, d.hint());
938 }
939 }
940
941 #[test]
945 fn every_function_names_its_parameters() {
946 for d in super::all() {
947 let names = d.param_names();
948 assert!(
949 d.params.is_empty() || d.named_params().is_some(),
950 "{} falls back to its types: add it to functions/params.rs",
951 d.hint()
952 );
953 let tail = names.iter().filter(|p| p.starts_with('…')).count();
954 assert_eq!(
955 tail,
956 usize::from(d.variadic),
957 "{}: one … iff variadic",
958 d.hint()
959 );
960 let extra = usize::from(d.name == "SWITCH");
962 assert_eq!(
963 names.len() - tail,
964 d.params.len() + extra,
965 "{}: one name per parameter",
966 d.hint()
967 );
968 for (i, p) in names.iter().filter(|p| !p.starts_with('…')).enumerate() {
969 assert_eq!(p.starts_with('['), i >= d.required, "{}: {p}", d.hint());
970 }
971 if d.kind.reduces() {
972 assert!(
973 names.iter().any(|p| p.contains(" OVER ")),
974 "{}: which argument is reduced",
975 d.hint()
976 );
977 }
978 assert_eq!(
979 names.last().is_some_and(|p| p.contains(" INTO ")),
980 d.kind == Kind::Array,
981 "{}: a list result ends in INTO",
982 d.hint()
983 );
984 }
985 }
986
987 use super::*;
988 use std::collections::{HashMap, HashSet};
989
990 #[test]
991 fn ids_are_unique() {
992 let mut seen = HashSet::new();
993 for d in all() {
994 assert!(seen.insert(d.id), "{} reuses id {:?}", d.name, d.id);
995 }
996 }
997
998 #[test]
1000 fn names_are_unambiguous() {
1001 let mut seen: HashMap<(String, bool), &str> = HashMap::new();
1002 for d in all() {
1003 for n in std::iter::once(&d.name).chain(d.aliases) {
1004 let k = (n.to_ascii_uppercase(), d.kind.reduces());
1005 if let Some(prev) = seen.insert(k, d.name) {
1006 panic!("{n} names both {prev} and {}", d.name);
1007 }
1008 }
1009 }
1010 }
1011
1012 #[test]
1014 fn frozen_ids() {
1015 for (id, name) in [
1016 (1, "SUM"),
1017 (2, "AVERAGE"),
1018 (3, "MIN"),
1019 (4, "MAX"),
1020 (10, "ABS"),
1021 (11, "ROUND"),
1022 (12, "FLOOR"),
1023 (13, "CEIL"),
1024 (14, "SQRT"),
1025 (15, "NEG"),
1026 (20, "MIN"),
1027 (21, "MAX"),
1028 ] {
1029 assert_eq!(by_id(FuncId(id)).map(|d| d.name), Some(name), "{id}");
1030 }
1031 }
1032
1033 #[test]
1034 fn lookups() {
1035 assert_eq!(aggregation("avg").map(|d| d.id), Some(FuncId(2)));
1036 assert_eq!(callable("sum").map(|d| d.id), Some(FuncId(22)));
1037 assert_eq!(callable("min2").map(|d| d.id), Some(FuncId(20)));
1038 assert_eq!(callable("Floor.Math").map(|d| d.id), Some(FuncId(206)));
1039 assert!(callable("nope").is_none());
1040 assert!(is_aggregation(FuncId(7)) && !is_aggregation(FuncId(26)));
1041 let round = by_id(FuncId(11)).unwrap();
1042 assert!(round.accepts(1) && round.accepts(2) && !round.accepts(3) && !round.accepts(0));
1043 let sum = callable("SUM").unwrap();
1044 assert!(sum.accepts(5) && !sum.accepts(0));
1045 assert_eq!(sum.param(4), Arg::Number);
1046 assert_eq!(round.signature(), "ROUND(number, [number])");
1047 assert_eq!(sum.signature(), "SUM(number, …)");
1048 assert_eq!(by_id(FuncId(39)).unwrap().param(0), Arg::Any);
1049 }
1050}
1051
1052pub fn reference_markdown() -> String {
1057 let mut out = String::from(
1058 "# Function Reference\n\n\
1059 <!-- Generated from crates/core_model/src/functions.rs by\n \
1060 `UPDATE_DOCS=1 cargo test -p improv_core_model`. Do not edit. -->\n\n\
1061 Every built-in function, by category. Names are case-insensitive;\n\
1062 `[x]` is optional (`[x=1]`: 1 if omitted), `…` repeats the\n\
1063 arguments before it, `x OVER T` is reduced over a category and\n\
1064 `INTO K` lays a list result along one. Positions are\n\
1065 1-based. An error argument (`#DIV/0!`, `#N/A`, …) makes the result\n\
1066 that error, except in the functions that test for errors. See\n\
1067 [The Formula Language](./formulas.md) for operators, types and\n\
1068 dimensions.\n",
1069 );
1070 let cats = [
1071 (Category::Aggregation, "Aggregations", "Written `F(Measure OVER Category)`: collapse one category. Each also has a scalar form over its arguments, listed under its category."),
1072 (Category::Logical, "Logical", "`IF` and the functions built on it evaluate only the branch they choose, so an error or missing cell in the other branch does not matter."),
1073 (Category::Information, "Information and errors", ""),
1074 (Category::Math, "Math and trigonometry", "Angles are in radians."),
1075 (Category::Rounding, "Rounding", "Rounding is decimal: `ROUND(2.675, 2)` is 2.68."),
1076 (Category::Text, "Text", "Lengths and positions count characters, not bytes."),
1077 (Category::DateTime, "Date and time", "Dates are date-time values (UTC, millisecond precision). A number given where a date is wanted is a spreadsheet serial date (1 = 1900-01-01)."),
1078 (Category::Dimensional, "Dimensional", "Functions of a cell's position in a category, and the ordered (previous/next, running) forms."),
1079 (Category::Statistical, "Statistical", ""),
1080 (Category::Financial, "Financial", "Rates are per period; cash flows are in item order along their `OVER` category."),
1081 (Category::Engineering, "Engineering", "Complex numbers are text, `\"3+4i\"` (suffix `i` or `j`); base conversions use 10-digit two's complement for negatives."),
1082 ];
1083 for (cat, title, intro) in cats {
1084 if all().all(|d| d.category != cat) {
1085 continue;
1086 }
1087 out.push_str(&format!("\n## {title}\n\n"));
1088 if !intro.is_empty() {
1089 out.push_str(intro);
1090 out.push_str("\n\n");
1091 }
1092 out.push_str("| Function | Returns | Description |\n|---|---|---|\n");
1093 for d in all().filter(|d| d.category == cat) {
1094 let sig = d.hint();
1095 let ret = match d.ret {
1096 Ret::Fixed(ValueType::Number) => "number",
1097 Ret::Fixed(ValueType::Boolean) => "boolean",
1098 Ret::Fixed(ValueType::Text) => "text",
1099 Ret::Fixed(ValueType::DateTime) => "date",
1100 Ret::Fixed(_) => "value",
1101 Ret::Unify => "the branches' type",
1102 Ret::Arg(_) => "the values' type",
1103 };
1104 let mut doc = d.doc.replace('[', "\\[").replace(']', "\\]");
1107 if !d.aliases.is_empty() {
1108 doc.push_str(&format!(" (also `{}`)", d.aliases.join("`, `")));
1109 }
1110 out.push_str(&format!("| `{sig}` | {ret} | {doc} |\n"));
1111 }
1112 }
1113 out
1114}
1115
1116#[cfg(test)]
1117#[cfg_attr(coverage_nightly, coverage(off))]
1118mod docs_test {
1119 #[test]
1120 fn reference_page_is_current() {
1121 let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../docs/src/functions.md");
1122 let want = super::reference_markdown();
1123 if std::env::var_os("UPDATE_DOCS").is_some() {
1124 std::fs::write(path, &want).unwrap();
1125 }
1126 let have = std::fs::read_to_string(path)
1128 .unwrap_or_default()
1129 .replace("\r\n", "\n");
1130 assert!(
1131 have == want,
1132 "docs/src/functions.md is stale: run UPDATE_DOCS=1 cargo test -p improv_core_model"
1133 );
1134 }
1135}