Skip to main content

formualizer_eval/builtins/math/
trig.rs

1use super::super::utils::{
2    ARG_ANY_ONE, ARG_NUM_LENIENT_ONE, ARG_NUM_LENIENT_TWO, EPSILON_NEAR_ZERO,
3    binary_numeric_elementwise, unary_numeric_arg, unary_numeric_elementwise,
4};
5use crate::args::ArgSchema;
6use crate::function::Function;
7use crate::traits::{ArgumentHandle, FunctionContext};
8use formualizer_common::{ExcelError, LiteralValue};
9use formualizer_macros::func_caps;
10use std::f64::consts::PI;
11
12/* ─────────────────────────── TRIG: circular ────────────────────────── */
13
14#[derive(Debug)]
15pub struct SinFn;
16/// Returns the sine of an angle in radians.
17///
18/// # Remarks
19/// - Input is interpreted as radians, not degrees.
20/// - Supports scalar and array-style elementwise evaluation.
21///
22/// # Examples
23/// ```yaml,sandbox
24/// title: "Sine of PI/2"
25/// formula: "=SIN(PI()/2)"
26/// expected: 1
27/// ```
28///
29/// ```yaml,sandbox
30/// title: "Sine from a cell value"
31/// grid:
32///   A1: 0
33/// formula: "=SIN(A1)"
34/// expected: 0
35/// ```
36///
37/// ```yaml,docs
38/// related:
39///   - COS
40///   - TAN
41///   - RADIANS
42/// faq:
43///   - q: "Does SIN expect degrees or radians?"
44///     a: "SIN expects radians; convert degree inputs first with RADIANS."
45/// ```
46/// [formualizer-docgen:schema:start]
47/// Name: SIN
48/// Type: SinFn
49/// Min args: 1
50/// Max args: 1
51/// Variadic: false
52/// Signature: SIN(arg1: number@scalar)
53/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
54/// Caps: PURE, ELEMENTWISE, NUMERIC_ONLY
55/// [formualizer-docgen:schema:end]
56impl Function for SinFn {
57    func_caps!(PURE, ELEMENTWISE, NUMERIC_ONLY);
58    fn name(&self) -> &'static str {
59        "SIN"
60    }
61    fn min_args(&self) -> usize {
62        1
63    }
64    fn arg_schema(&self) -> &'static [ArgSchema] {
65        &ARG_NUM_LENIENT_ONE[..]
66    }
67    fn eval<'a, 'b, 'c>(
68        &self,
69        args: &'c [ArgumentHandle<'a, 'b>],
70        ctx: &dyn FunctionContext<'b>,
71    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
72        unary_numeric_elementwise(args, ctx, |x| Ok(LiteralValue::Number(x.sin())))
73    }
74}
75
76#[cfg(test)]
77mod tests_sin {
78    use super::*;
79    use crate::test_workbook::TestWorkbook;
80    use crate::traits::ArgumentHandle;
81    use formualizer_parse::LiteralValue;
82
83    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
84        wb.interpreter()
85    }
86    fn make_num_ast(n: f64) -> formualizer_parse::parser::ASTNode {
87        formualizer_parse::parser::ASTNode::new(
88            formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Number(n)),
89            None,
90        )
91    }
92    fn assert_close(a: f64, b: f64) {
93        assert!((a - b).abs() < 1e-9, "{a} !~= {b}");
94    }
95
96    #[test]
97    fn test_sin_basic() {
98        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(SinFn));
99        let ctx = interp(&wb);
100        let sin = ctx.context.get_function("", "SIN").unwrap();
101        let a0 = make_num_ast(PI / 2.0);
102        let args = vec![ArgumentHandle::new(&a0, &ctx)];
103        match sin
104            .dispatch(&args, &ctx.function_context(None))
105            .unwrap()
106            .into_literal()
107        {
108            LiteralValue::Number(n) => assert_close(n, 1.0),
109            v => panic!("unexpected {v:?}"),
110        }
111    }
112
113    #[test]
114    fn test_sin_array_literal() {
115        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(SinFn));
116        let ctx = interp(&wb);
117        let sin = ctx.context.get_function("", "SIN").unwrap();
118        let arr = formualizer_parse::parser::ASTNode::new(
119            formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Array(vec![vec![
120                LiteralValue::Number(0.0),
121                LiteralValue::Number(PI / 2.0),
122            ]])),
123            None,
124        );
125        let args = vec![ArgumentHandle::new(&arr, &ctx)];
126
127        match sin
128            .dispatch(&args, &ctx.function_context(None))
129            .unwrap()
130            .into_literal()
131        {
132            formualizer_common::LiteralValue::Array(rows) => {
133                assert_eq!(rows.len(), 1);
134                assert_eq!(rows[0].len(), 2);
135                match (&rows[0][0], &rows[0][1]) {
136                    (
137                        formualizer_common::LiteralValue::Number(a),
138                        formualizer_common::LiteralValue::Number(b),
139                    ) => {
140                        assert_close(*a, 0.0);
141                        assert_close(*b, 1.0);
142                    }
143                    other => panic!("unexpected {other:?}"),
144                }
145            }
146            other => panic!("expected array, got {other:?}"),
147        }
148    }
149}
150
151#[derive(Debug)]
152pub struct CosFn;
153/// Returns the cosine of an angle in radians.
154///
155/// # Remarks
156/// - Input must be in radians.
157/// - Supports elementwise evaluation for array inputs.
158///
159/// # Examples
160/// ```yaml,sandbox
161/// title: "Cosine at zero"
162/// formula: "=COS(0)"
163/// expected: 1
164/// ```
165///
166/// ```yaml,sandbox
167/// title: "Cosine at PI"
168/// formula: "=COS(PI())"
169/// expected: -1
170/// ```
171///
172/// ```yaml,docs
173/// related:
174///   - SIN
175///   - TAN
176///   - PI
177/// faq:
178///   - q: "Why can COS look wrong for degree values like 60?"
179///     a: "COS interprets 60 as radians, not degrees; use COS(RADIANS(60))."
180/// ```
181/// [formualizer-docgen:schema:start]
182/// Name: COS
183/// Type: CosFn
184/// Min args: 1
185/// Max args: 1
186/// Variadic: false
187/// Signature: COS(arg1: number@scalar)
188/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
189/// Caps: PURE, ELEMENTWISE, NUMERIC_ONLY
190/// [formualizer-docgen:schema:end]
191impl Function for CosFn {
192    func_caps!(PURE, ELEMENTWISE, NUMERIC_ONLY);
193    fn name(&self) -> &'static str {
194        "COS"
195    }
196    fn min_args(&self) -> usize {
197        1
198    }
199    fn arg_schema(&self) -> &'static [ArgSchema] {
200        &ARG_NUM_LENIENT_ONE[..]
201    }
202    fn eval<'a, 'b, 'c>(
203        &self,
204        args: &'c [ArgumentHandle<'a, 'b>],
205        ctx: &dyn FunctionContext<'b>,
206    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
207        unary_numeric_elementwise(args, ctx, |x| Ok(LiteralValue::Number(x.cos())))
208    }
209}
210
211#[cfg(test)]
212mod tests_cos {
213    use super::*;
214    use crate::test_workbook::TestWorkbook;
215    use crate::traits::ArgumentHandle;
216    use formualizer_parse::LiteralValue;
217    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
218        wb.interpreter()
219    }
220    fn make_num_ast(n: f64) -> formualizer_parse::parser::ASTNode {
221        formualizer_parse::parser::ASTNode::new(
222            formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Number(n)),
223            None,
224        )
225    }
226    fn assert_close(a: f64, b: f64) {
227        assert!((a - b).abs() < 1e-9);
228    }
229    #[test]
230    fn test_cos_basic() {
231        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(CosFn));
232        let ctx = interp(&wb);
233        let cos = ctx.context.get_function("", "COS").unwrap();
234        let a0 = make_num_ast(0.0);
235        let args = vec![ArgumentHandle::new(&a0, &ctx)];
236        match cos
237            .dispatch(&args, &ctx.function_context(None))
238            .unwrap()
239            .into_literal()
240        {
241            LiteralValue::Number(n) => assert_close(n, 1.0),
242            v => panic!("unexpected {v:?}"),
243        }
244    }
245}
246
247#[derive(Debug)]
248pub struct TanFn;
249/// Returns the tangent of an angle in radians.
250///
251/// # Remarks
252/// - Input is interpreted as radians.
253/// - Near odd multiples of `PI()/2`, results can become very large.
254///
255/// # Examples
256/// ```yaml,sandbox
257/// title: "Tangent at PI/4"
258/// formula: "=TAN(PI()/4)"
259/// expected: 1
260/// ```
261///
262/// ```yaml,sandbox
263/// title: "Tangent at zero"
264/// formula: "=TAN(0)"
265/// expected: 0
266/// ```
267///
268/// ```yaml,docs
269/// related:
270///   - SIN
271///   - COS
272///   - ATAN
273/// faq:
274///   - q: "Why does TAN explode near PI()/2?"
275///     a: "Because COS approaches zero there, TAN can become extremely large in magnitude."
276/// ```
277/// [formualizer-docgen:schema:start]
278/// Name: TAN
279/// Type: TanFn
280/// Min args: 1
281/// Max args: 1
282/// Variadic: false
283/// Signature: TAN(arg1: number@scalar)
284/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
285/// Caps: PURE, ELEMENTWISE, NUMERIC_ONLY
286/// [formualizer-docgen:schema:end]
287impl Function for TanFn {
288    func_caps!(PURE, ELEMENTWISE, NUMERIC_ONLY);
289    fn name(&self) -> &'static str {
290        "TAN"
291    }
292    fn min_args(&self) -> usize {
293        1
294    }
295    fn arg_schema(&self) -> &'static [ArgSchema] {
296        &ARG_NUM_LENIENT_ONE[..]
297    }
298    fn eval<'a, 'b, 'c>(
299        &self,
300        args: &'c [ArgumentHandle<'a, 'b>],
301        ctx: &dyn FunctionContext<'b>,
302    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
303        unary_numeric_elementwise(args, ctx, |x| Ok(LiteralValue::Number(x.tan())))
304    }
305}
306
307#[cfg(test)]
308mod tests_tan {
309    use super::*;
310    use crate::test_workbook::TestWorkbook;
311    use crate::traits::ArgumentHandle;
312    use formualizer_parse::LiteralValue;
313    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
314        wb.interpreter()
315    }
316    fn make_num_ast(n: f64) -> formualizer_parse::parser::ASTNode {
317        formualizer_parse::parser::ASTNode::new(
318            formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Number(n)),
319            None,
320        )
321    }
322    fn assert_close(a: f64, b: f64) {
323        assert!((a - b).abs() < 1e-9);
324    }
325    #[test]
326    fn test_tan_basic() {
327        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(TanFn));
328        let ctx = interp(&wb);
329        let tan = ctx.context.get_function("", "TAN").unwrap();
330        let a0 = make_num_ast(PI / 4.0);
331        let args = vec![ArgumentHandle::new(&a0, &ctx)];
332        match tan
333            .dispatch(&args, &ctx.function_context(None))
334            .unwrap()
335            .into_literal()
336        {
337            LiteralValue::Number(n) => assert_close(n, 1.0),
338            v => panic!("unexpected {v:?}"),
339        }
340    }
341}
342
343#[derive(Debug)]
344pub struct AsinFn;
345/// Returns the angle in radians whose sine is the input value.
346///
347/// Use `ASIN` to recover an angle from a normalized ratio.
348///
349/// # Remarks
350/// - Domain: input must be between `-1` and `1`, inclusive.
351/// - Radians: output is in the range `[-PI()/2, PI()/2]`.
352/// - Errors: returns `#NUM!` when the input is outside the valid domain.
353///
354/// # Examples
355/// ```yaml,sandbox
356/// title: "Arcsine of one half"
357/// formula: "=ASIN(0.5)"
358/// expected: 0.5235987755982989
359/// ```
360///
361/// ```yaml,sandbox
362/// title: "Lower boundary"
363/// formula: "=ASIN(-1)"
364/// expected: -1.5707963267948966
365/// ```
366///
367/// ```yaml,docs
368/// related:
369///   - SIN
370///   - ACOS
371///   - ATAN
372/// faq:
373///   - q: "When does ASIN return #NUM!?"
374///     a: "ASIN returns #NUM! when the input is outside the closed interval [-1, 1]."
375/// ```
376/// [formualizer-docgen:schema:start]
377/// Name: ASIN
378/// Type: AsinFn
379/// Min args: 1
380/// Max args: 1
381/// Variadic: false
382/// Signature: ASIN(arg1: number@scalar)
383/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
384/// Caps: PURE, ELEMENTWISE, NUMERIC_ONLY
385/// [formualizer-docgen:schema:end]
386impl Function for AsinFn {
387    func_caps!(PURE, ELEMENTWISE, NUMERIC_ONLY);
388    fn name(&self) -> &'static str {
389        "ASIN"
390    }
391    fn min_args(&self) -> usize {
392        1
393    }
394    fn arg_schema(&self) -> &'static [ArgSchema] {
395        &ARG_NUM_LENIENT_ONE[..]
396    }
397    fn eval<'a, 'b, 'c>(
398        &self,
399        args: &'c [ArgumentHandle<'a, 'b>],
400        _ctx: &dyn FunctionContext<'b>,
401    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
402        let x = unary_numeric_arg(args)?;
403        if !(-1.0..=1.0).contains(&x) {
404            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
405                ExcelError::new_num(),
406            )));
407        }
408        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
409            x.asin(),
410        )))
411    }
412}
413
414#[cfg(test)]
415mod tests_asin {
416    use super::*;
417    use crate::test_workbook::TestWorkbook;
418    use crate::traits::ArgumentHandle;
419    use formualizer_parse::LiteralValue;
420    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
421        wb.interpreter()
422    }
423    fn make_num_ast(n: f64) -> formualizer_parse::parser::ASTNode {
424        formualizer_parse::parser::ASTNode::new(
425            formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Number(n)),
426            None,
427        )
428    }
429    fn assert_close(a: f64, b: f64) {
430        assert!((a - b).abs() < 1e-9);
431    }
432    #[test]
433    fn test_asin_basic_and_domain() {
434        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(AsinFn));
435        let ctx = interp(&wb);
436        let asin = ctx.context.get_function("", "ASIN").unwrap();
437        // valid
438        let a0 = make_num_ast(0.5);
439        let args = vec![ArgumentHandle::new(&a0, &ctx)];
440        match asin
441            .dispatch(&args, &ctx.function_context(None))
442            .unwrap()
443            .into_literal()
444        {
445            LiteralValue::Number(n) => assert_close(n, (0.5f64).asin()),
446            v => panic!("unexpected {v:?}"),
447        }
448        // invalid domain
449        let a1 = make_num_ast(2.0);
450        let args2 = vec![ArgumentHandle::new(&a1, &ctx)];
451        match asin
452            .dispatch(&args2, &ctx.function_context(None))
453            .unwrap()
454            .into_literal()
455        {
456            LiteralValue::Error(e) => assert_eq!(e, "#NUM!"),
457            v => panic!("expected error, got {v:?}"),
458        }
459    }
460}
461
462#[derive(Debug)]
463pub struct AcosFn;
464/// Returns the angle in radians whose cosine is the input value.
465///
466/// Use `ACOS` when you need an angle from a normalized adjacent/hypotenuse ratio.
467///
468/// # Remarks
469/// - Domain: input must be between `-1` and `1`, inclusive.
470/// - Radians: output is in the range `[0, PI()]`.
471/// - Errors: returns `#NUM!` when the input is outside the valid domain.
472///
473/// # Examples
474/// ```yaml,sandbox
475/// title: "Arccosine of one half"
476/// formula: "=ACOS(0.5)"
477/// expected: 1.0471975511965979
478/// ```
479///
480/// ```yaml,sandbox
481/// title: "Upper-angle boundary"
482/// formula: "=ACOS(-1)"
483/// expected: 3.141592653589793
484/// ```
485///
486/// ```yaml,docs
487/// related:
488///   - COS
489///   - ASIN
490///   - ATAN2
491/// faq:
492///   - q: "Why does ACOS reject values like 1.0001?"
493///     a: "ACOS is only defined on [-1, 1], so out-of-range inputs return #NUM!."
494/// ```
495/// [formualizer-docgen:schema:start]
496/// Name: ACOS
497/// Type: AcosFn
498/// Min args: 1
499/// Max args: 1
500/// Variadic: false
501/// Signature: ACOS(arg1: number@scalar)
502/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
503/// Caps: PURE, ELEMENTWISE, NUMERIC_ONLY
504/// [formualizer-docgen:schema:end]
505impl Function for AcosFn {
506    func_caps!(PURE, ELEMENTWISE, NUMERIC_ONLY);
507    fn name(&self) -> &'static str {
508        "ACOS"
509    }
510    fn min_args(&self) -> usize {
511        1
512    }
513    fn arg_schema(&self) -> &'static [ArgSchema] {
514        &ARG_NUM_LENIENT_ONE[..]
515    }
516    fn eval<'a, 'b, 'c>(
517        &self,
518        args: &'c [ArgumentHandle<'a, 'b>],
519        _ctx: &dyn FunctionContext<'b>,
520    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
521        let x = unary_numeric_arg(args)?;
522        if !(-1.0..=1.0).contains(&x) {
523            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
524                ExcelError::new_num(),
525            )));
526        }
527        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
528            x.acos(),
529        )))
530    }
531}
532
533#[cfg(test)]
534mod tests_acos {
535    use super::*;
536    use crate::test_workbook::TestWorkbook;
537    use crate::traits::ArgumentHandle;
538    use formualizer_parse::LiteralValue;
539    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
540        wb.interpreter()
541    }
542    fn make_num_ast(n: f64) -> formualizer_parse::parser::ASTNode {
543        formualizer_parse::parser::ASTNode::new(
544            formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Number(n)),
545            None,
546        )
547    }
548    fn assert_close(a: f64, b: f64) {
549        assert!((a - b).abs() < 1e-9);
550    }
551    #[test]
552    fn test_acos_basic_and_domain() {
553        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(AcosFn));
554        let ctx = interp(&wb);
555        let acos = ctx.context.get_function("", "ACOS").unwrap();
556        let a0 = make_num_ast(0.5);
557        let args = vec![ArgumentHandle::new(&a0, &ctx)];
558        match acos
559            .dispatch(&args, &ctx.function_context(None))
560            .unwrap()
561            .into_literal()
562        {
563            LiteralValue::Number(n) => assert_close(n, (0.5f64).acos()),
564            v => panic!("unexpected {v:?}"),
565        }
566        let a1 = make_num_ast(-2.0);
567        let args2 = vec![ArgumentHandle::new(&a1, &ctx)];
568        match acos
569            .dispatch(&args2, &ctx.function_context(None))
570            .unwrap()
571            .into_literal()
572        {
573            LiteralValue::Error(e) => assert_eq!(e, "#NUM!"),
574            v => panic!("expected error, got {v:?}"),
575        }
576    }
577}
578
579#[derive(Debug)]
580pub struct AtanFn;
581/// Returns the angle in radians whose tangent is the input value.
582///
583/// `ATAN` is useful for recovering a slope angle from a ratio.
584///
585/// # Remarks
586/// - Domain: accepts any real number.
587/// - Radians: output is in the range `(-PI()/2, PI()/2)`.
588/// - Errors: no function-specific domain errors are produced.
589///
590/// # Examples
591/// ```yaml,sandbox
592/// title: "Arctangent of one"
593/// formula: "=ATAN(1)"
594/// expected: 0.7853981633974483
595/// ```
596///
597/// ```yaml,sandbox
598/// title: "Negative slope angle"
599/// formula: "=ATAN(-1)"
600/// expected: -0.7853981633974483
601/// ```
602///
603/// ```yaml,docs
604/// related:
605///   - TAN
606///   - ATAN2
607///   - ACOT
608/// faq:
609///   - q: "Does ATAN ever return #NUM! for large values?"
610///     a: "No. ATAN accepts any real input and asymptotically approaches +/-PI()/2."
611/// ```
612/// [formualizer-docgen:schema:start]
613/// Name: ATAN
614/// Type: AtanFn
615/// Min args: 1
616/// Max args: 1
617/// Variadic: false
618/// Signature: ATAN(arg1: number@scalar)
619/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
620/// Caps: PURE, ELEMENTWISE, NUMERIC_ONLY
621/// [formualizer-docgen:schema:end]
622impl Function for AtanFn {
623    func_caps!(PURE, ELEMENTWISE, NUMERIC_ONLY);
624    fn name(&self) -> &'static str {
625        "ATAN"
626    }
627    fn min_args(&self) -> usize {
628        1
629    }
630    fn arg_schema(&self) -> &'static [ArgSchema] {
631        &ARG_NUM_LENIENT_ONE[..]
632    }
633    fn eval<'a, 'b, 'c>(
634        &self,
635        args: &'c [ArgumentHandle<'a, 'b>],
636        _ctx: &dyn FunctionContext<'b>,
637    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
638        let x = unary_numeric_arg(args)?;
639        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
640            x.atan(),
641        )))
642    }
643}
644
645#[cfg(test)]
646mod tests_atan {
647    use super::*;
648    use crate::test_workbook::TestWorkbook;
649    use crate::traits::ArgumentHandle;
650    use formualizer_parse::LiteralValue;
651    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
652        wb.interpreter()
653    }
654    fn make_num_ast(n: f64) -> formualizer_parse::parser::ASTNode {
655        formualizer_parse::parser::ASTNode::new(
656            formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Number(n)),
657            None,
658        )
659    }
660    fn assert_close(a: f64, b: f64) {
661        assert!((a - b).abs() < 1e-9);
662    }
663    #[test]
664    fn test_atan_basic() {
665        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(AtanFn));
666        let ctx = interp(&wb);
667        let atan = ctx.context.get_function("", "ATAN").unwrap();
668        let a0 = make_num_ast(1.0);
669        let args = vec![ArgumentHandle::new(&a0, &ctx)];
670        match atan
671            .dispatch(&args, &ctx.function_context(None))
672            .unwrap()
673            .into_literal()
674        {
675            LiteralValue::Number(n) => assert_close(n, (1.0f64).atan()),
676            v => panic!("unexpected {v:?}"),
677        }
678    }
679}
680
681#[derive(Debug)]
682pub struct Atan2Fn;
683/// Returns the arctangent of `y/x`, preserving quadrant information.
684///
685/// # Remarks
686/// - Formualizer uses Excel-style argument order: `ATAN2(x_num, y_num)`.
687/// - Returns `#DIV/0!` when both arguments are zero.
688///
689/// # Examples
690/// ```yaml,sandbox
691/// title: "First quadrant angle"
692/// formula: "=ATAN2(1,1)"
693/// expected: 0.7853981633974483
694/// ```
695///
696/// ```yaml,sandbox
697/// title: "Undefined angle at origin"
698/// formula: "=ATAN2(0,0)"
699/// expected: "#DIV/0!"
700/// ```
701///
702/// ```yaml,docs
703/// related:
704///   - ATAN
705///   - ACOT
706///   - RADIANS
707/// faq:
708///   - q: "Why does ATAN2 return #DIV/0! at (0,0)?"
709///     a: "The origin has no defined direction angle, so ATAN2 reports #DIV/0!."
710/// ```
711/// [formualizer-docgen:schema:start]
712/// Name: ATAN2
713/// Type: Atan2Fn
714/// Min args: 2
715/// Max args: 2
716/// Variadic: false
717/// Signature: ATAN2(arg1: number@scalar, arg2: number@scalar)
718/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
719/// Caps: PURE, ELEMENTWISE, NUMERIC_ONLY
720/// [formualizer-docgen:schema:end]
721impl Function for Atan2Fn {
722    func_caps!(PURE, ELEMENTWISE, NUMERIC_ONLY);
723    fn name(&self) -> &'static str {
724        "ATAN2"
725    }
726    fn min_args(&self) -> usize {
727        2
728    }
729    fn arg_schema(&self) -> &'static [ArgSchema] {
730        &ARG_NUM_LENIENT_TWO[..]
731    }
732    fn eval<'a, 'b, 'c>(
733        &self,
734        args: &'c [ArgumentHandle<'a, 'b>],
735        ctx: &dyn FunctionContext<'b>,
736    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
737        // Excel: ATAN2(x_num, y_num)
738        binary_numeric_elementwise(args, ctx, |x, y| {
739            if x == 0.0 && y == 0.0 {
740                Ok(LiteralValue::Error(ExcelError::from_error_string(
741                    "#DIV/0!",
742                )))
743            } else {
744                Ok(LiteralValue::Number(y.atan2(x)))
745            }
746        })
747    }
748}
749
750#[cfg(test)]
751mod tests_atan2 {
752    use super::*;
753    use crate::test_workbook::TestWorkbook;
754    use crate::traits::ArgumentHandle;
755    use formualizer_parse::LiteralValue;
756    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
757        wb.interpreter()
758    }
759    fn make_num_ast(n: f64) -> formualizer_parse::parser::ASTNode {
760        formualizer_parse::parser::ASTNode::new(
761            formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Number(n)),
762            None,
763        )
764    }
765    fn assert_close(a: f64, b: f64) {
766        assert!((a - b).abs() < 1e-9);
767    }
768    #[test]
769    fn test_atan2_basic_and_zero_zero() {
770        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(Atan2Fn));
771        let ctx = interp(&wb);
772        let atan2 = ctx.context.get_function("", "ATAN2").unwrap();
773        // ATAN2(1,1) = pi/4
774        let a0 = make_num_ast(1.0);
775        let a1 = make_num_ast(1.0);
776        let args = vec![
777            ArgumentHandle::new(&a0, &ctx),
778            ArgumentHandle::new(&a1, &ctx),
779        ];
780        match atan2
781            .dispatch(&args, &ctx.function_context(None))
782            .unwrap()
783            .into_literal()
784        {
785            LiteralValue::Number(n) => assert_close(n, PI / 4.0),
786            v => panic!("unexpected {v:?}"),
787        }
788        // ATAN2(0,0) => #DIV/0!
789        let b0 = make_num_ast(0.0);
790        let b1 = make_num_ast(0.0);
791        let args2 = vec![
792            ArgumentHandle::new(&b0, &ctx),
793            ArgumentHandle::new(&b1, &ctx),
794        ];
795        match atan2
796            .dispatch(&args2, &ctx.function_context(None))
797            .unwrap()
798            .into_literal()
799        {
800            LiteralValue::Error(e) => assert_eq!(e, "#DIV/0!"),
801            v => panic!("expected error, got {v:?}"),
802        }
803    }
804
805    #[test]
806    fn test_atan2_broadcast_scalar_over_array() {
807        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(Atan2Fn));
808        let ctx = interp(&wb);
809        let atan2 = ctx.context.get_function("", "ATAN2").unwrap();
810
811        // ATAN2(x_num=1, y_num={0,1}) => {0, pi/4}
812        let x = make_num_ast(1.0);
813        let y = formualizer_parse::parser::ASTNode::new(
814            formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Array(vec![vec![
815                LiteralValue::Number(0.0),
816                LiteralValue::Number(1.0),
817            ]])),
818            None,
819        );
820        let args = vec![ArgumentHandle::new(&x, &ctx), ArgumentHandle::new(&y, &ctx)];
821
822        match atan2
823            .dispatch(&args, &ctx.function_context(None))
824            .unwrap()
825            .into_literal()
826        {
827            formualizer_common::LiteralValue::Array(rows) => {
828                assert_eq!(rows.len(), 1);
829                assert_eq!(rows[0].len(), 2);
830                match (&rows[0][0], &rows[0][1]) {
831                    (
832                        formualizer_common::LiteralValue::Number(a),
833                        formualizer_common::LiteralValue::Number(b),
834                    ) => {
835                        assert_close(*a, 0.0);
836                        assert_close(*b, PI / 4.0);
837                    }
838                    other => panic!("unexpected {other:?}"),
839                }
840            }
841            other => panic!("expected array, got {other:?}"),
842        }
843    }
844}
845
846#[derive(Debug)]
847pub struct SecFn;
848/// Returns the secant of an angle, defined as `1 / COS(angle)`.
849///
850/// Use `SEC` for reciprocal-cosine calculations in radian-based formulas.
851///
852/// # Remarks
853/// - Domain: valid for all real angles except where `COS(angle) = 0`.
854/// - Radians: input is interpreted in radians.
855/// - Errors: returns `#DIV/0!` near odd multiples of `PI()/2`.
856///
857/// # Examples
858/// ```yaml,sandbox
859/// title: "Secant at zero"
860/// formula: "=SEC(0)"
861/// expected: 1
862/// ```
863///
864/// ```yaml,sandbox
865/// title: "Singularity at PI over 2"
866/// formula: "=SEC(PI()/2)"
867/// expected: "#DIV/0!"
868/// ```
869///
870/// ```yaml,docs
871/// related:
872///   - COS
873///   - COT
874///   - CSC
875/// faq:
876///   - q: "When does SEC return #DIV/0!?"
877///     a: "SEC returns #DIV/0! when COS(angle) is effectively zero."
878/// ```
879/// [formualizer-docgen:schema:start]
880/// Name: SEC
881/// Type: SecFn
882/// Min args: 1
883/// Max args: 1
884/// Variadic: false
885/// Signature: SEC(arg1: number@scalar)
886/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
887/// Caps: PURE, ELEMENTWISE, NUMERIC_ONLY
888/// [formualizer-docgen:schema:end]
889impl Function for SecFn {
890    func_caps!(PURE, ELEMENTWISE, NUMERIC_ONLY);
891    fn name(&self) -> &'static str {
892        "SEC"
893    }
894    fn min_args(&self) -> usize {
895        1
896    }
897    fn arg_schema(&self) -> &'static [ArgSchema] {
898        &ARG_NUM_LENIENT_ONE[..]
899    }
900    fn eval<'a, 'b, 'c>(
901        &self,
902        args: &'c [ArgumentHandle<'a, 'b>],
903        _ctx: &dyn FunctionContext<'b>,
904    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
905        let x = unary_numeric_arg(args)?;
906        let c = x.cos();
907        if c.abs() < EPSILON_NEAR_ZERO {
908            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
909                ExcelError::from_error_string("#DIV/0!"),
910            )));
911        }
912        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
913            1.0 / c,
914        )))
915    }
916}
917
918#[cfg(test)]
919mod tests_sec {
920    use super::*;
921    use crate::test_workbook::TestWorkbook;
922    use crate::traits::ArgumentHandle;
923    use formualizer_parse::LiteralValue;
924    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
925        wb.interpreter()
926    }
927    fn make_num_ast(n: f64) -> formualizer_parse::parser::ASTNode {
928        formualizer_parse::parser::ASTNode::new(
929            formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Number(n)),
930            None,
931        )
932    }
933    fn assert_close(a: f64, b: f64) {
934        assert!((a - b).abs() < 1e-9);
935    }
936    #[test]
937    fn test_sec_basic_and_div0() {
938        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(SecFn));
939        let ctx = interp(&wb);
940        let sec = ctx.context.get_function("", "SEC").unwrap();
941        let a0 = make_num_ast(0.0);
942        let args = vec![ArgumentHandle::new(&a0, &ctx)];
943        match sec
944            .dispatch(&args, &ctx.function_context(None))
945            .unwrap()
946            .into_literal()
947        {
948            LiteralValue::Number(n) => assert_close(n, 1.0),
949            v => panic!("unexpected {v:?}"),
950        }
951        let a1 = make_num_ast(PI / 2.0);
952        let args2 = vec![ArgumentHandle::new(&a1, &ctx)];
953        match sec
954            .dispatch(&args2, &ctx.function_context(None))
955            .unwrap()
956            .into_literal()
957        {
958            LiteralValue::Error(e) => assert_eq!(e, "#DIV/0!"),
959            LiteralValue::Number(n) => assert!(n.abs() > 1e12), // near singularity
960            v => panic!("unexpected {v:?}"),
961        }
962    }
963}
964
965#[derive(Debug)]
966pub struct CscFn;
967/// Returns the cosecant of an angle, defined as `1 / SIN(angle)`.
968///
969/// Use `CSC` for reciprocal-sine calculations in radian-based formulas.
970///
971/// # Remarks
972/// - Domain: valid for all real angles except where `SIN(angle) = 0`.
973/// - Radians: input is interpreted in radians.
974/// - Errors: returns `#DIV/0!` at or near integer multiples of `PI()`.
975///
976/// # Examples
977/// ```yaml,sandbox
978/// title: "Cosecant at PI over 2"
979/// formula: "=CSC(PI()/2)"
980/// expected: 1
981/// ```
982///
983/// ```yaml,sandbox
984/// title: "Zero sine denominator"
985/// formula: "=CSC(0)"
986/// expected: "#DIV/0!"
987/// ```
988///
989/// ```yaml,docs
990/// related:
991///   - SIN
992///   - COT
993///   - SEC
994/// faq:
995///   - q: "When does CSC return #DIV/0!?"
996///     a: "CSC returns #DIV/0! when SIN(angle) is effectively zero."
997/// ```
998/// [formualizer-docgen:schema:start]
999/// Name: CSC
1000/// Type: CscFn
1001/// Min args: 1
1002/// Max args: 1
1003/// Variadic: false
1004/// Signature: CSC(arg1: number@scalar)
1005/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1006/// Caps: PURE, ELEMENTWISE, NUMERIC_ONLY
1007/// [formualizer-docgen:schema:end]
1008impl Function for CscFn {
1009    func_caps!(PURE, ELEMENTWISE, NUMERIC_ONLY);
1010    fn name(&self) -> &'static str {
1011        "CSC"
1012    }
1013    fn min_args(&self) -> usize {
1014        1
1015    }
1016    fn arg_schema(&self) -> &'static [ArgSchema] {
1017        &ARG_NUM_LENIENT_ONE[..]
1018    }
1019    fn eval<'a, 'b, 'c>(
1020        &self,
1021        args: &'c [ArgumentHandle<'a, 'b>],
1022        _ctx: &dyn FunctionContext<'b>,
1023    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1024        let x = unary_numeric_arg(args)?;
1025        let s = x.sin();
1026        if s.abs() < EPSILON_NEAR_ZERO {
1027            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1028                ExcelError::from_error_string("#DIV/0!"),
1029            )));
1030        }
1031        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1032            1.0 / s,
1033        )))
1034    }
1035}
1036
1037#[cfg(test)]
1038mod tests_csc {
1039    use super::*;
1040    use crate::test_workbook::TestWorkbook;
1041    use crate::traits::ArgumentHandle;
1042    use formualizer_parse::LiteralValue;
1043    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
1044        wb.interpreter()
1045    }
1046    fn make_num_ast(n: f64) -> formualizer_parse::parser::ASTNode {
1047        formualizer_parse::parser::ASTNode::new(
1048            formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Number(n)),
1049            None,
1050        )
1051    }
1052    fn assert_close(a: f64, b: f64) {
1053        assert!((a - b).abs() < 1e-9);
1054    }
1055    #[test]
1056    fn test_csc_basic_and_div0() {
1057        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(CscFn));
1058        let ctx = interp(&wb);
1059        let csc = ctx.context.get_function("", "CSC").unwrap();
1060        let a0 = make_num_ast(PI / 2.0);
1061        let args = vec![ArgumentHandle::new(&a0, &ctx)];
1062        match csc
1063            .dispatch(&args, &ctx.function_context(None))
1064            .unwrap()
1065            .into_literal()
1066        {
1067            LiteralValue::Number(n) => assert_close(n, 1.0),
1068            v => panic!("unexpected {v:?}"),
1069        }
1070        let a1 = make_num_ast(0.0);
1071        let args2 = vec![ArgumentHandle::new(&a1, &ctx)];
1072        match csc
1073            .dispatch(&args2, &ctx.function_context(None))
1074            .unwrap()
1075            .into_literal()
1076        {
1077            LiteralValue::Error(e) => assert_eq!(e, "#DIV/0!"),
1078            v => panic!("expected error, got {v:?}"),
1079        }
1080    }
1081}
1082
1083#[derive(Debug)]
1084pub struct CotFn;
1085/// Returns the cotangent of an angle, defined as `1 / TAN(angle)`.
1086///
1087/// `COT` is useful when working with reciprocal tangent relationships.
1088///
1089/// # Remarks
1090/// - Domain: valid for all real angles except where `TAN(angle) = 0`.
1091/// - Radians: input is interpreted in radians.
1092/// - Errors: returns `#DIV/0!` at or near integer multiples of `PI()`.
1093///
1094/// # Examples
1095/// ```yaml,sandbox
1096/// title: "Cotangent at PI over 4"
1097/// formula: "=COT(PI()/4)"
1098/// expected: 1
1099/// ```
1100///
1101/// ```yaml,sandbox
1102/// title: "Undefined cotangent at zero"
1103/// formula: "=COT(0)"
1104/// expected: "#DIV/0!"
1105/// ```
1106///
1107/// ```yaml,docs
1108/// related:
1109///   - TAN
1110///   - SEC
1111///   - CSC
1112/// faq:
1113///   - q: "Why is COT undefined at integer multiples of PI()?"
1114///     a: "At those points TAN is zero, so 1/TAN triggers a #DIV/0! condition."
1115/// ```
1116/// [formualizer-docgen:schema:start]
1117/// Name: COT
1118/// Type: CotFn
1119/// Min args: 1
1120/// Max args: 1
1121/// Variadic: false
1122/// Signature: COT(arg1: number@scalar)
1123/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1124/// Caps: PURE, ELEMENTWISE, NUMERIC_ONLY
1125/// [formualizer-docgen:schema:end]
1126impl Function for CotFn {
1127    func_caps!(PURE, ELEMENTWISE, NUMERIC_ONLY);
1128    fn name(&self) -> &'static str {
1129        "COT"
1130    }
1131    fn min_args(&self) -> usize {
1132        1
1133    }
1134    fn arg_schema(&self) -> &'static [ArgSchema] {
1135        &ARG_NUM_LENIENT_ONE[..]
1136    }
1137    fn eval<'a, 'b, 'c>(
1138        &self,
1139        args: &'c [ArgumentHandle<'a, 'b>],
1140        _ctx: &dyn FunctionContext<'b>,
1141    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1142        let x = unary_numeric_arg(args)?;
1143        let t = x.tan();
1144        if t.abs() < EPSILON_NEAR_ZERO {
1145            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1146                ExcelError::from_error_string("#DIV/0!"),
1147            )));
1148        }
1149        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1150            1.0 / t,
1151        )))
1152    }
1153}
1154
1155#[cfg(test)]
1156mod tests_cot {
1157    use super::*;
1158    use crate::test_workbook::TestWorkbook;
1159    use crate::traits::ArgumentHandle;
1160    use formualizer_parse::LiteralValue;
1161    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
1162        wb.interpreter()
1163    }
1164    fn make_num_ast(n: f64) -> formualizer_parse::parser::ASTNode {
1165        formualizer_parse::parser::ASTNode::new(
1166            formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Number(n)),
1167            None,
1168        )
1169    }
1170    fn assert_close(a: f64, b: f64) {
1171        assert!((a - b).abs() < 1e-9);
1172    }
1173    #[test]
1174    fn test_cot_basic_and_div0() {
1175        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(CotFn));
1176        let ctx = interp(&wb);
1177        let cot = ctx.context.get_function("", "COT").unwrap();
1178        let a0 = make_num_ast(PI / 4.0);
1179        let args = vec![ArgumentHandle::new(&a0, &ctx)];
1180        match cot
1181            .dispatch(&args, &ctx.function_context(None))
1182            .unwrap()
1183            .into_literal()
1184        {
1185            LiteralValue::Number(n) => assert_close(n, 1.0),
1186            v => panic!("unexpected {v:?}"),
1187        }
1188        let a1 = make_num_ast(0.0);
1189        let args2 = vec![ArgumentHandle::new(&a1, &ctx)];
1190        match cot
1191            .dispatch(&args2, &ctx.function_context(None))
1192            .unwrap()
1193            .into_literal()
1194        {
1195            LiteralValue::Error(e) => assert_eq!(e, "#DIV/0!"),
1196            v => panic!("expected error, got {v:?}"),
1197        }
1198    }
1199}
1200
1201#[derive(Debug)]
1202pub struct AcotFn;
1203/// Returns the angle in radians whose cotangent is the input value.
1204///
1205/// `ACOT` maps real inputs to principal angles in `(0, PI())`.
1206///
1207/// # Remarks
1208/// - Domain: accepts any real number, including `0`.
1209/// - Radians: output is in `(0, PI())`, with `ACOT(0) = PI()/2`.
1210/// - Errors: no function-specific domain errors are produced.
1211///
1212/// # Examples
1213/// ```yaml,sandbox
1214/// title: "Arccotangent of one"
1215/// formula: "=ACOT(1)"
1216/// expected: 0.7853981633974483
1217/// ```
1218///
1219/// ```yaml,sandbox
1220/// title: "Arccotangent of negative one"
1221/// formula: "=ACOT(-1)"
1222/// expected: 2.356194490192345
1223/// ```
1224///
1225/// ```yaml,docs
1226/// related:
1227///   - ATAN
1228///   - ATAN2
1229///   - COT
1230/// faq:
1231///   - q: "What is ACOT(0)?"
1232///     a: "ACOT(0) is PI()/2 in the principal-value branch used here."
1233/// ```
1234/// [formualizer-docgen:schema:start]
1235/// Name: ACOT
1236/// Type: AcotFn
1237/// Min args: 1
1238/// Max args: 1
1239/// Variadic: false
1240/// Signature: ACOT(arg1: number@scalar)
1241/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1242/// Caps: PURE, ELEMENTWISE, NUMERIC_ONLY
1243/// [formualizer-docgen:schema:end]
1244impl Function for AcotFn {
1245    func_caps!(PURE, ELEMENTWISE, NUMERIC_ONLY);
1246    fn name(&self) -> &'static str {
1247        "ACOT"
1248    }
1249    fn min_args(&self) -> usize {
1250        1
1251    }
1252    fn arg_schema(&self) -> &'static [ArgSchema] {
1253        &ARG_NUM_LENIENT_ONE[..]
1254    }
1255    fn eval<'a, 'b, 'c>(
1256        &self,
1257        args: &'c [ArgumentHandle<'a, 'b>],
1258        _ctx: &dyn FunctionContext<'b>,
1259    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1260        let x = unary_numeric_arg(args)?;
1261        let result = if x == 0.0 {
1262            PI / 2.0
1263        } else if x > 0.0 {
1264            (1.0 / x).atan()
1265        } else {
1266            (1.0 / x).atan() + PI
1267        };
1268        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1269            result,
1270        )))
1271    }
1272}
1273
1274#[cfg(test)]
1275mod tests_acot {
1276    use super::*;
1277    use crate::test_workbook::TestWorkbook;
1278    use crate::traits::ArgumentHandle;
1279    use formualizer_parse::LiteralValue;
1280    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
1281        wb.interpreter()
1282    }
1283    fn make_num_ast(n: f64) -> formualizer_parse::parser::ASTNode {
1284        formualizer_parse::parser::ASTNode::new(
1285            formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Number(n)),
1286            None,
1287        )
1288    }
1289    fn assert_close(a: f64, b: f64) {
1290        assert!((a - b).abs() < 1e-9);
1291    }
1292    #[test]
1293    fn test_acot_basic() {
1294        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(AcotFn));
1295        let ctx = interp(&wb);
1296        let acot = ctx.context.get_function("", "ACOT").unwrap();
1297        let a0 = make_num_ast(2.0);
1298        let args = vec![ArgumentHandle::new(&a0, &ctx)];
1299        match acot
1300            .dispatch(&args, &ctx.function_context(None))
1301            .unwrap()
1302            .into_literal()
1303        {
1304            LiteralValue::Number(n) => assert_close(n, 0.4636476090008061),
1305            v => panic!("unexpected {v:?}"),
1306        }
1307    }
1308}
1309
1310/* ─────────────────────────── TRIG: hyperbolic ──────────────────────── */
1311
1312#[derive(Debug)]
1313pub struct SinhFn;
1314/// Returns the hyperbolic sine of a number.
1315///
1316/// `SINH` computes `(e^x - e^-x) / 2`.
1317///
1318/// # Remarks
1319/// - Domain: accepts any real number.
1320/// - Radians: this function is hyperbolic, so the input is not treated as an angle in radians.
1321/// - Errors: no function-specific domain errors are produced.
1322///
1323/// # Examples
1324/// ```yaml,sandbox
1325/// title: "Hyperbolic sine at zero"
1326/// formula: "=SINH(0)"
1327/// expected: 0
1328/// ```
1329///
1330/// ```yaml,sandbox
1331/// title: "Hyperbolic sine at one"
1332/// formula: "=SINH(1)"
1333/// expected: 1.1752011936438014
1334/// ```
1335///
1336/// ```yaml,docs
1337/// related:
1338///   - ASINH
1339///   - COSH
1340///   - TANH
1341/// faq:
1342///   - q: "Is SINH expecting radians like SIN?"
1343///     a: "No. SINH is hyperbolic and treats input as a pure numeric value, not an angle unit."
1344/// ```
1345/// [formualizer-docgen:schema:start]
1346/// Name: SINH
1347/// Type: SinhFn
1348/// Min args: 1
1349/// Max args: 1
1350/// Variadic: false
1351/// Signature: SINH(arg1: number@scalar)
1352/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1353/// Caps: PURE, ELEMENTWISE, NUMERIC_ONLY
1354/// [formualizer-docgen:schema:end]
1355impl Function for SinhFn {
1356    func_caps!(PURE, ELEMENTWISE, NUMERIC_ONLY);
1357    fn name(&self) -> &'static str {
1358        "SINH"
1359    }
1360    fn min_args(&self) -> usize {
1361        1
1362    }
1363    fn arg_schema(&self) -> &'static [ArgSchema] {
1364        &ARG_NUM_LENIENT_ONE[..]
1365    }
1366    fn eval<'a, 'b, 'c>(
1367        &self,
1368        args: &'c [ArgumentHandle<'a, 'b>],
1369        _ctx: &dyn FunctionContext<'b>,
1370    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1371        let x = unary_numeric_arg(args)?;
1372        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1373            x.sinh(),
1374        )))
1375    }
1376}
1377
1378#[cfg(test)]
1379mod tests_sinh {
1380    use super::*;
1381    use crate::test_workbook::TestWorkbook;
1382    use crate::traits::ArgumentHandle;
1383    use formualizer_parse::LiteralValue;
1384    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
1385        wb.interpreter()
1386    }
1387    fn make_num_ast(n: f64) -> formualizer_parse::parser::ASTNode {
1388        formualizer_parse::parser::ASTNode::new(
1389            formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Number(n)),
1390            None,
1391        )
1392    }
1393    fn assert_close(a: f64, b: f64) {
1394        assert!((a - b).abs() < 1e-9);
1395    }
1396    #[test]
1397    fn test_sinh_basic() {
1398        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(SinhFn));
1399        let ctx = interp(&wb);
1400        let f = ctx.context.get_function("", "SINH").unwrap();
1401        let a0 = make_num_ast(1.0);
1402        let args = vec![ArgumentHandle::new(&a0, &ctx)];
1403        let fctx = ctx.function_context(None);
1404        match f.dispatch(&args, &fctx).unwrap().into_literal() {
1405            LiteralValue::Number(n) => assert_close(n, (1.0f64).sinh()),
1406            v => panic!("unexpected {v:?}"),
1407        }
1408    }
1409}
1410
1411#[derive(Debug)]
1412pub struct CoshFn;
1413/// Returns the hyperbolic cosine of a number.
1414///
1415/// `COSH` computes `(e^x + e^-x) / 2`.
1416///
1417/// # Remarks
1418/// - Domain: accepts any real number.
1419/// - Radians: this function is hyperbolic, so the input is not treated as an angle in radians.
1420/// - Errors: no function-specific domain errors are produced.
1421///
1422/// # Examples
1423/// ```yaml,sandbox
1424/// title: "Hyperbolic cosine at zero"
1425/// formula: "=COSH(0)"
1426/// expected: 1
1427/// ```
1428///
1429/// ```yaml,sandbox
1430/// title: "Hyperbolic cosine at one"
1431/// formula: "=COSH(1)"
1432/// expected: 1.5430806348152437
1433/// ```
1434///
1435/// ```yaml,docs
1436/// related:
1437///   - ACOSH
1438///   - SINH
1439///   - TANH
1440/// faq:
1441///   - q: "Can COSH produce #NUM! domain errors?"
1442///     a: "No function-specific domain errors are enforced for real inputs."
1443/// ```
1444/// [formualizer-docgen:schema:start]
1445/// Name: COSH
1446/// Type: CoshFn
1447/// Min args: 1
1448/// Max args: 1
1449/// Variadic: false
1450/// Signature: COSH(arg1: number@scalar)
1451/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1452/// Caps: PURE, ELEMENTWISE, NUMERIC_ONLY
1453/// [formualizer-docgen:schema:end]
1454impl Function for CoshFn {
1455    func_caps!(PURE, ELEMENTWISE, NUMERIC_ONLY);
1456    fn name(&self) -> &'static str {
1457        "COSH"
1458    }
1459    fn min_args(&self) -> usize {
1460        1
1461    }
1462    fn arg_schema(&self) -> &'static [ArgSchema] {
1463        &ARG_NUM_LENIENT_ONE[..]
1464    }
1465    fn eval<'a, 'b, 'c>(
1466        &self,
1467        args: &'c [ArgumentHandle<'a, 'b>],
1468        _ctx: &dyn FunctionContext<'b>,
1469    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1470        let x = unary_numeric_arg(args)?;
1471        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1472            x.cosh(),
1473        )))
1474    }
1475}
1476
1477#[cfg(test)]
1478mod tests_cosh {
1479    use super::*;
1480    use crate::test_workbook::TestWorkbook;
1481    use crate::traits::ArgumentHandle;
1482    use formualizer_parse::LiteralValue;
1483    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
1484        wb.interpreter()
1485    }
1486    fn make_num_ast(n: f64) -> formualizer_parse::parser::ASTNode {
1487        formualizer_parse::parser::ASTNode::new(
1488            formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Number(n)),
1489            None,
1490        )
1491    }
1492    fn assert_close(a: f64, b: f64) {
1493        assert!((a - b).abs() < 1e-9);
1494    }
1495    #[test]
1496    fn test_cosh_basic() {
1497        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(CoshFn));
1498        let ctx = interp(&wb);
1499        let f = ctx.context.get_function("", "COSH").unwrap();
1500        let a0 = make_num_ast(1.0);
1501        let args = vec![ArgumentHandle::new(&a0, &ctx)];
1502        match f
1503            .dispatch(&args, &ctx.function_context(None))
1504            .unwrap()
1505            .into_literal()
1506        {
1507            LiteralValue::Number(n) => assert_close(n, (1.0f64).cosh()),
1508            v => panic!("unexpected {v:?}"),
1509        }
1510    }
1511}
1512
1513#[derive(Debug)]
1514pub struct TanhFn;
1515/// Returns the hyperbolic tangent of a number.
1516///
1517/// `TANH` computes `SINH(x) / COSH(x)`.
1518///
1519/// # Remarks
1520/// - Domain: accepts any real number.
1521/// - Radians: this function is hyperbolic, so the input is not treated as an angle in radians.
1522/// - Errors: no function-specific domain errors are produced.
1523///
1524/// # Examples
1525/// ```yaml,sandbox
1526/// title: "Hyperbolic tangent at zero"
1527/// formula: "=TANH(0)"
1528/// expected: 0
1529/// ```
1530///
1531/// ```yaml,sandbox
1532/// title: "Hyperbolic tangent at two"
1533/// formula: "=TANH(2)"
1534/// expected: 0.9640275800758169
1535/// ```
1536///
1537/// ```yaml,docs
1538/// related:
1539///   - ATANH
1540///   - SINH
1541///   - COSH
1542/// faq:
1543///   - q: "What output range should TANH produce?"
1544///     a: "For real inputs, TANH stays strictly between -1 and 1."
1545/// ```
1546/// [formualizer-docgen:schema:start]
1547/// Name: TANH
1548/// Type: TanhFn
1549/// Min args: 1
1550/// Max args: 1
1551/// Variadic: false
1552/// Signature: TANH(arg1: number@scalar)
1553/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1554/// Caps: PURE, ELEMENTWISE, NUMERIC_ONLY
1555/// [formualizer-docgen:schema:end]
1556impl Function for TanhFn {
1557    func_caps!(PURE, ELEMENTWISE, NUMERIC_ONLY);
1558    fn name(&self) -> &'static str {
1559        "TANH"
1560    }
1561    fn min_args(&self) -> usize {
1562        1
1563    }
1564    fn arg_schema(&self) -> &'static [ArgSchema] {
1565        &ARG_NUM_LENIENT_ONE[..]
1566    }
1567    fn eval<'a, 'b, 'c>(
1568        &self,
1569        args: &'c [ArgumentHandle<'a, 'b>],
1570        _ctx: &dyn FunctionContext<'b>,
1571    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1572        let x = unary_numeric_arg(args)?;
1573        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1574            x.tanh(),
1575        )))
1576    }
1577}
1578
1579#[cfg(test)]
1580mod tests_tanh {
1581    use super::*;
1582    use crate::test_workbook::TestWorkbook;
1583    use crate::traits::ArgumentHandle;
1584    use formualizer_parse::LiteralValue;
1585    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
1586        wb.interpreter()
1587    }
1588    fn make_num_ast(n: f64) -> formualizer_parse::parser::ASTNode {
1589        formualizer_parse::parser::ASTNode::new(
1590            formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Number(n)),
1591            None,
1592        )
1593    }
1594    fn assert_close(a: f64, b: f64) {
1595        assert!((a - b).abs() < 1e-9);
1596    }
1597    #[test]
1598    fn test_tanh_basic() {
1599        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(TanhFn));
1600        let ctx = interp(&wb);
1601        let f = ctx.context.get_function("", "TANH").unwrap();
1602        let a0 = make_num_ast(0.5);
1603        let args = vec![ArgumentHandle::new(&a0, &ctx)];
1604        match f
1605            .dispatch(&args, &ctx.function_context(None))
1606            .unwrap()
1607            .into_literal()
1608        {
1609            LiteralValue::Number(n) => assert_close(n, (0.5f64).tanh()),
1610            v => panic!("unexpected {v:?}"),
1611        }
1612    }
1613}
1614
1615#[derive(Debug)]
1616pub struct AsinhFn;
1617/// Returns the inverse hyperbolic sine of a number.
1618///
1619/// `ASINH` is the inverse of `SINH` over all real inputs.
1620///
1621/// # Remarks
1622/// - Domain: accepts any real number.
1623/// - Radians: this function is hyperbolic, so the output is not an angle in radians.
1624/// - Errors: no function-specific domain errors are produced.
1625///
1626/// # Examples
1627/// ```yaml,sandbox
1628/// title: "Inverse hyperbolic sine of one"
1629/// formula: "=ASINH(1)"
1630/// expected: 0.881373587019543
1631/// ```
1632///
1633/// ```yaml,sandbox
1634/// title: "Inverse hyperbolic sine of negative two"
1635/// formula: "=ASINH(-2)"
1636/// expected: -1.4436354751788103
1637/// ```
1638///
1639/// ```yaml,docs
1640/// related:
1641///   - SINH
1642///   - ACOSH
1643///   - ATANH
1644/// faq:
1645///   - q: "Does ASINH have restricted input domain?"
1646///     a: "No. ASINH accepts any real-valued input."
1647/// ```
1648/// [formualizer-docgen:schema:start]
1649/// Name: ASINH
1650/// Type: AsinhFn
1651/// Min args: 1
1652/// Max args: 1
1653/// Variadic: false
1654/// Signature: ASINH(arg1: number@scalar)
1655/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1656/// Caps: PURE, ELEMENTWISE, NUMERIC_ONLY
1657/// [formualizer-docgen:schema:end]
1658impl Function for AsinhFn {
1659    func_caps!(PURE, ELEMENTWISE, NUMERIC_ONLY);
1660    fn name(&self) -> &'static str {
1661        "ASINH"
1662    }
1663    fn min_args(&self) -> usize {
1664        1
1665    }
1666    fn arg_schema(&self) -> &'static [ArgSchema] {
1667        &ARG_NUM_LENIENT_ONE[..]
1668    }
1669    fn eval<'a, 'b, 'c>(
1670        &self,
1671        args: &'c [ArgumentHandle<'a, 'b>],
1672        _ctx: &dyn FunctionContext<'b>,
1673    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1674        let x = unary_numeric_arg(args)?;
1675        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1676            x.asinh(),
1677        )))
1678    }
1679}
1680
1681#[cfg(test)]
1682mod tests_asinh {
1683    use super::*;
1684    use crate::test_workbook::TestWorkbook;
1685    use crate::traits::ArgumentHandle;
1686    use formualizer_parse::LiteralValue;
1687    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
1688        wb.interpreter()
1689    }
1690    fn make_num_ast(n: f64) -> formualizer_parse::parser::ASTNode {
1691        formualizer_parse::parser::ASTNode::new(
1692            formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Number(n)),
1693            None,
1694        )
1695    }
1696    fn assert_close(a: f64, b: f64) {
1697        assert!((a - b).abs() < 1e-9);
1698    }
1699    #[test]
1700    fn test_asinh_basic() {
1701        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(AsinhFn));
1702        let ctx = interp(&wb);
1703        let f = ctx.context.get_function("", "ASINH").unwrap();
1704        let a0 = make_num_ast(1.5);
1705        let args = vec![ArgumentHandle::new(&a0, &ctx)];
1706        match f
1707            .dispatch(&args, &ctx.function_context(None))
1708            .unwrap()
1709            .into_literal()
1710        {
1711            LiteralValue::Number(n) => assert_close(n, (1.5f64).asinh()),
1712            v => panic!("unexpected {v:?}"),
1713        }
1714    }
1715}
1716
1717#[derive(Debug)]
1718pub struct AcoshFn;
1719/// Returns the inverse hyperbolic cosine of a number.
1720///
1721/// `ACOSH` is the inverse of `COSH` for inputs at or above `1`.
1722///
1723/// # Remarks
1724/// - Domain: input must be greater than or equal to `1`.
1725/// - Radians: this function is hyperbolic, so the output is not an angle in radians.
1726/// - Errors: returns `#NUM!` when the input is less than `1`.
1727///
1728/// # Examples
1729/// ```yaml,sandbox
1730/// title: "Boundary value"
1731/// formula: "=ACOSH(1)"
1732/// expected: 0
1733/// ```
1734///
1735/// ```yaml,sandbox
1736/// title: "Inverse hyperbolic cosine of ten"
1737/// formula: "=ACOSH(10)"
1738/// expected: 2.993222846126381
1739/// ```
1740///
1741/// ```yaml,docs
1742/// related:
1743///   - COSH
1744///   - ASINH
1745///   - ATANH
1746/// faq:
1747///   - q: "When does ACOSH return #NUM!?"
1748///     a: "ACOSH requires input >= 1; values below 1 return #NUM!."
1749/// ```
1750/// [formualizer-docgen:schema:start]
1751/// Name: ACOSH
1752/// Type: AcoshFn
1753/// Min args: 1
1754/// Max args: 1
1755/// Variadic: false
1756/// Signature: ACOSH(arg1: any@scalar)
1757/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
1758/// Caps: PURE, ELEMENTWISE, NUMERIC_ONLY
1759/// [formualizer-docgen:schema:end]
1760impl Function for AcoshFn {
1761    func_caps!(PURE, ELEMENTWISE, NUMERIC_ONLY);
1762    fn name(&self) -> &'static str {
1763        "ACOSH"
1764    }
1765    fn min_args(&self) -> usize {
1766        1
1767    }
1768    fn arg_schema(&self) -> &'static [ArgSchema] {
1769        &ARG_ANY_ONE[..]
1770    }
1771    fn eval<'a, 'b, 'c>(
1772        &self,
1773        args: &'c [ArgumentHandle<'a, 'b>],
1774        _ctx: &dyn FunctionContext<'b>,
1775    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1776        let x = unary_numeric_arg(args)?;
1777        if x < 1.0 {
1778            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1779                ExcelError::new_num(),
1780            )));
1781        }
1782        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1783            x.acosh(),
1784        )))
1785    }
1786}
1787
1788#[cfg(test)]
1789mod tests_acosh {
1790    use super::*;
1791    use crate::test_workbook::TestWorkbook;
1792    use crate::traits::ArgumentHandle;
1793    use formualizer_parse::LiteralValue;
1794    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
1795        wb.interpreter()
1796    }
1797    fn make_num_ast(n: f64) -> formualizer_parse::parser::ASTNode {
1798        formualizer_parse::parser::ASTNode::new(
1799            formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Number(n)),
1800            None,
1801        )
1802    }
1803    #[test]
1804    fn test_acosh_basic_and_domain() {
1805        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(AcoshFn));
1806        let ctx = interp(&wb);
1807        let f = ctx.context.get_function("", "ACOSH").unwrap();
1808        let a0 = make_num_ast(1.0);
1809        let args = vec![ArgumentHandle::new(&a0, &ctx)];
1810        assert_eq!(
1811            f.dispatch(&args, &ctx.function_context(None))
1812                .unwrap()
1813                .into_literal(),
1814            LiteralValue::Number(0.0)
1815        );
1816        let a1 = make_num_ast(0.5);
1817        let args2 = vec![ArgumentHandle::new(&a1, &ctx)];
1818        match f
1819            .dispatch(&args2, &ctx.function_context(None))
1820            .unwrap()
1821            .into_literal()
1822        {
1823            LiteralValue::Error(e) => assert_eq!(e, "#NUM!"),
1824            v => panic!("expected error, got {v:?}"),
1825        }
1826    }
1827}
1828
1829#[derive(Debug)]
1830pub struct AtanhFn;
1831/// Returns the inverse hyperbolic tangent of a number.
1832///
1833/// `ATANH` is the inverse of `TANH` on the open interval `(-1, 1)`.
1834///
1835/// # Remarks
1836/// - Domain: input must be strictly between `-1` and `1`.
1837/// - Radians: this function is hyperbolic, so the output is not an angle in radians.
1838/// - Errors: returns `#NUM!` when the input is `<= -1` or `>= 1`.
1839///
1840/// # Examples
1841/// ```yaml,sandbox
1842/// title: "Inverse hyperbolic tangent of one half"
1843/// formula: "=ATANH(0.5)"
1844/// expected: 0.5493061443340548
1845/// ```
1846///
1847/// ```yaml,sandbox
1848/// title: "Domain boundary error"
1849/// formula: "=ATANH(1)"
1850/// expected: "#NUM!"
1851/// ```
1852///
1853/// ```yaml,docs
1854/// related:
1855///   - TANH
1856///   - ATAN
1857///   - ASINH
1858/// faq:
1859///   - q: "Which inputs are invalid for ATANH?"
1860///     a: "ATANH is only defined on (-1, 1); endpoints and outside values return #NUM!."
1861/// ```
1862/// [formualizer-docgen:schema:start]
1863/// Name: ATANH
1864/// Type: AtanhFn
1865/// Min args: 1
1866/// Max args: 1
1867/// Variadic: false
1868/// Signature: ATANH(arg1: any@scalar)
1869/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
1870/// Caps: PURE, ELEMENTWISE, NUMERIC_ONLY
1871/// [formualizer-docgen:schema:end]
1872impl Function for AtanhFn {
1873    func_caps!(PURE, ELEMENTWISE, NUMERIC_ONLY);
1874    fn name(&self) -> &'static str {
1875        "ATANH"
1876    }
1877    fn min_args(&self) -> usize {
1878        1
1879    }
1880    fn arg_schema(&self) -> &'static [ArgSchema] {
1881        &ARG_ANY_ONE[..]
1882    }
1883    fn eval<'a, 'b, 'c>(
1884        &self,
1885        args: &'c [ArgumentHandle<'a, 'b>],
1886        _ctx: &dyn FunctionContext<'b>,
1887    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1888        let x = unary_numeric_arg(args)?;
1889        if x <= -1.0 || x >= 1.0 {
1890            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1891                ExcelError::new_num(),
1892            )));
1893        }
1894        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1895            x.atanh(),
1896        )))
1897    }
1898}
1899
1900#[cfg(test)]
1901mod tests_atanh {
1902    use super::*;
1903    use crate::test_workbook::TestWorkbook;
1904    use crate::traits::ArgumentHandle;
1905    use formualizer_parse::LiteralValue;
1906    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
1907        wb.interpreter()
1908    }
1909    fn make_num_ast(n: f64) -> formualizer_parse::parser::ASTNode {
1910        formualizer_parse::parser::ASTNode::new(
1911            formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Number(n)),
1912            None,
1913        )
1914    }
1915    fn assert_close(a: f64, b: f64) {
1916        assert!((a - b).abs() < 1e-9);
1917    }
1918    #[test]
1919    fn test_atanh_basic_and_domain() {
1920        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(AtanhFn));
1921        let ctx = interp(&wb);
1922        let f = ctx.context.get_function("", "ATANH").unwrap();
1923        let a0 = make_num_ast(0.5);
1924        let args = vec![ArgumentHandle::new(&a0, &ctx)];
1925        match f
1926            .dispatch(&args, &ctx.function_context(None))
1927            .unwrap()
1928            .into_literal()
1929        {
1930            LiteralValue::Number(n) => assert_close(n, (0.5f64).atanh()),
1931            v => panic!("unexpected {v:?}"),
1932        }
1933        let a1 = make_num_ast(1.0);
1934        let args2 = vec![ArgumentHandle::new(&a1, &ctx)];
1935        match f
1936            .dispatch(&args2, &ctx.function_context(None))
1937            .unwrap()
1938            .into_literal()
1939        {
1940            LiteralValue::Error(e) => assert_eq!(e, "#NUM!"),
1941            v => panic!("expected error, got {v:?}"),
1942        }
1943    }
1944}
1945
1946#[derive(Debug)]
1947pub struct SechFn;
1948/// Returns the hyperbolic secant of a number, defined as `1 / COSH(x)`.
1949///
1950/// `SECH` produces values in `(0, 1]` for real inputs.
1951///
1952/// # Remarks
1953/// - Domain: accepts any real number.
1954/// - Radians: this function is hyperbolic, so the input is not treated as an angle in radians.
1955/// - Errors: no function-specific domain errors are produced.
1956///
1957/// # Examples
1958/// ```yaml,sandbox
1959/// title: "Hyperbolic secant at zero"
1960/// formula: "=SECH(0)"
1961/// expected: 1
1962/// ```
1963///
1964/// ```yaml,sandbox
1965/// title: "Hyperbolic secant at two"
1966/// formula: "=SECH(2)"
1967/// expected: 0.2658022288340797
1968/// ```
1969///
1970/// ```yaml,docs
1971/// related:
1972///   - COSH
1973///   - CSCH
1974///   - COTH
1975/// faq:
1976///   - q: "Can SECH be negative for real inputs?"
1977///     a: "No. For real numbers, SECH = 1/COSH is always positive."
1978/// ```
1979/// [formualizer-docgen:schema:start]
1980/// Name: SECH
1981/// Type: SechFn
1982/// Min args: 1
1983/// Max args: 1
1984/// Variadic: false
1985/// Signature: SECH(arg1: any@scalar)
1986/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
1987/// Caps: PURE, ELEMENTWISE, NUMERIC_ONLY
1988/// [formualizer-docgen:schema:end]
1989impl Function for SechFn {
1990    func_caps!(PURE, ELEMENTWISE, NUMERIC_ONLY);
1991    fn name(&self) -> &'static str {
1992        "SECH"
1993    }
1994    fn min_args(&self) -> usize {
1995        1
1996    }
1997    fn arg_schema(&self) -> &'static [ArgSchema] {
1998        &ARG_ANY_ONE[..]
1999    }
2000    fn eval<'a, 'b, 'c>(
2001        &self,
2002        args: &'c [ArgumentHandle<'a, 'b>],
2003        _ctx: &dyn FunctionContext<'b>,
2004    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2005        let x = unary_numeric_arg(args)?;
2006        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2007            1.0 / x.cosh(),
2008        )))
2009    }
2010}
2011
2012#[cfg(test)]
2013mod tests_sech {
2014    use super::*;
2015    use crate::test_workbook::TestWorkbook;
2016    use crate::traits::ArgumentHandle;
2017    use formualizer_parse::LiteralValue;
2018    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
2019        wb.interpreter()
2020    }
2021    fn make_num_ast(n: f64) -> formualizer_parse::parser::ASTNode {
2022        formualizer_parse::parser::ASTNode::new(
2023            formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Number(n)),
2024            None,
2025        )
2026    }
2027    fn assert_close(a: f64, b: f64) {
2028        assert!((a - b).abs() < 1e-9);
2029    }
2030    #[test]
2031    fn test_sech_basic() {
2032        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(SechFn));
2033        let ctx = interp(&wb);
2034        let f = ctx.context.get_function("", "SECH").unwrap();
2035        let a0 = make_num_ast(0.0);
2036        let args = vec![ArgumentHandle::new(&a0, &ctx)];
2037        match f
2038            .dispatch(&args, &ctx.function_context(None))
2039            .unwrap()
2040            .into_literal()
2041        {
2042            LiteralValue::Number(n) => assert_close(n, 1.0),
2043            v => panic!("unexpected {v:?}"),
2044        }
2045    }
2046}
2047
2048#[derive(Debug)]
2049pub struct CschFn;
2050/// Returns the hyperbolic cosecant of a number, defined as `1 / SINH(x)`.
2051///
2052/// `CSCH` is the reciprocal of `SINH` where defined.
2053///
2054/// # Remarks
2055/// - Domain: valid for all real numbers except `0`.
2056/// - Radians: this function is hyperbolic, so the input is not treated as an angle in radians.
2057/// - Errors: returns `#DIV/0!` when `SINH(x)` is zero (at `x = 0`).
2058///
2059/// # Examples
2060/// ```yaml,sandbox
2061/// title: "Hyperbolic cosecant at one"
2062/// formula: "=CSCH(1)"
2063/// expected: 0.8509181282393216
2064/// ```
2065///
2066/// ```yaml,sandbox
2067/// title: "Division by zero at origin"
2068/// formula: "=CSCH(0)"
2069/// expected: "#DIV/0!"
2070/// ```
2071///
2072/// ```yaml,docs
2073/// related:
2074///   - SINH
2075///   - COTH
2076///   - SECH
2077/// faq:
2078///   - q: "When does CSCH return #DIV/0!?"
2079///     a: "CSCH returns #DIV/0! when SINH(x) is zero, which occurs at x = 0."
2080/// ```
2081/// [formualizer-docgen:schema:start]
2082/// Name: CSCH
2083/// Type: CschFn
2084/// Min args: 1
2085/// Max args: 1
2086/// Variadic: false
2087/// Signature: CSCH(arg1: number@scalar)
2088/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2089/// Caps: PURE, ELEMENTWISE, NUMERIC_ONLY
2090/// [formualizer-docgen:schema:end]
2091impl Function for CschFn {
2092    func_caps!(PURE, ELEMENTWISE, NUMERIC_ONLY);
2093    fn name(&self) -> &'static str {
2094        "CSCH"
2095    }
2096    fn min_args(&self) -> usize {
2097        1
2098    }
2099    fn arg_schema(&self) -> &'static [ArgSchema] {
2100        &ARG_NUM_LENIENT_ONE[..]
2101    }
2102    fn eval<'a, 'b, 'c>(
2103        &self,
2104        args: &'c [ArgumentHandle<'a, 'b>],
2105        _ctx: &dyn FunctionContext<'b>,
2106    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2107        let x = unary_numeric_arg(args)?;
2108        let s = x.sinh(); // CSCH = 1/sinh(x), not 1/sin(x)
2109        if s.abs() < EPSILON_NEAR_ZERO {
2110            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2111                ExcelError::from_error_string("#DIV/0!"),
2112            )));
2113        }
2114        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2115            1.0 / s,
2116        )))
2117    }
2118}
2119
2120#[cfg(test)]
2121mod tests_csch {
2122    use super::*;
2123    use crate::test_workbook::TestWorkbook;
2124    use crate::traits::ArgumentHandle;
2125    use formualizer_parse::LiteralValue;
2126    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
2127        wb.interpreter()
2128    }
2129    fn make_num_ast(n: f64) -> formualizer_parse::parser::ASTNode {
2130        formualizer_parse::parser::ASTNode::new(
2131            formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Number(n)),
2132            None,
2133        )
2134    }
2135    fn assert_close(a: f64, b: f64) {
2136        assert!((a - b).abs() < 1e-9);
2137    }
2138    #[test]
2139    fn test_csch_basic_and_div0() {
2140        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(CschFn));
2141        let ctx = interp(&wb);
2142        let csch = ctx.context.get_function("", "CSCH").unwrap();
2143        // CSCH(1) = 1/sinh(1) ~= 0.8509
2144        let a0 = make_num_ast(1.0);
2145        let args = vec![ArgumentHandle::new(&a0, &ctx)];
2146        match csch
2147            .dispatch(&args, &ctx.function_context(None))
2148            .unwrap()
2149            .into_literal()
2150        {
2151            LiteralValue::Number(n) => assert_close(n, 0.8509181282393216),
2152            v => panic!("unexpected {v:?}"),
2153        }
2154        // CSCH(0) should be #DIV/0! since sinh(0) = 0
2155        let a1 = make_num_ast(0.0);
2156        let args2 = vec![ArgumentHandle::new(&a1, &ctx)];
2157        match csch
2158            .dispatch(&args2, &ctx.function_context(None))
2159            .unwrap()
2160            .into_literal()
2161        {
2162            LiteralValue::Error(e) => assert_eq!(e, "#DIV/0!"),
2163            v => panic!("expected error, got {v:?}"),
2164        }
2165    }
2166}
2167
2168#[derive(Debug)]
2169pub struct CothFn;
2170/// Returns the hyperbolic cotangent of a number, defined as `COSH(x) / SINH(x)`.
2171///
2172/// `COTH` is the reciprocal of `TANH` where defined.
2173///
2174/// # Remarks
2175/// - Domain: valid for all real numbers except `0`.
2176/// - Radians: this function is hyperbolic, so the input is not treated as an angle in radians.
2177/// - Errors: returns `#DIV/0!` when `SINH(x)` is zero (at `x = 0`).
2178///
2179/// # Examples
2180/// ```yaml,sandbox
2181/// title: "Hyperbolic cotangent at one"
2182/// formula: "=COTH(1)"
2183/// expected: 1.3130352854993312
2184/// ```
2185///
2186/// ```yaml,sandbox
2187/// title: "Division by zero at origin"
2188/// formula: "=COTH(0)"
2189/// expected: "#DIV/0!"
2190/// ```
2191///
2192/// ```yaml,docs
2193/// related:
2194///   - TANH
2195///   - CSCH
2196///   - SECH
2197/// faq:
2198///   - q: "Why is COTH undefined at zero?"
2199///     a: "COTH divides by SINH(x), and SINH(0) is zero, so #DIV/0! is returned."
2200/// ```
2201/// [formualizer-docgen:schema:start]
2202/// Name: COTH
2203/// Type: CothFn
2204/// Min args: 1
2205/// Max args: 1
2206/// Variadic: false
2207/// Signature: COTH(arg1: number@scalar)
2208/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2209/// Caps: PURE, ELEMENTWISE, NUMERIC_ONLY
2210/// [formualizer-docgen:schema:end]
2211impl Function for CothFn {
2212    func_caps!(PURE, ELEMENTWISE, NUMERIC_ONLY);
2213    fn name(&self) -> &'static str {
2214        "COTH"
2215    }
2216    fn min_args(&self) -> usize {
2217        1
2218    }
2219    fn arg_schema(&self) -> &'static [ArgSchema] {
2220        &ARG_NUM_LENIENT_ONE[..]
2221    }
2222    fn eval<'a, 'b, 'c>(
2223        &self,
2224        args: &'c [ArgumentHandle<'a, 'b>],
2225        _ctx: &dyn FunctionContext<'b>,
2226    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2227        let x = unary_numeric_arg(args)?;
2228        let s = x.sinh();
2229        if s.abs() < EPSILON_NEAR_ZERO {
2230            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2231                ExcelError::from_error_string("#DIV/0!"),
2232            )));
2233        }
2234        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2235            x.cosh() / s,
2236        )))
2237    }
2238}
2239
2240#[cfg(test)]
2241mod tests_coth {
2242    use super::*;
2243    use crate::test_workbook::TestWorkbook;
2244    use crate::traits::ArgumentHandle;
2245    use formualizer_parse::LiteralValue;
2246    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
2247        wb.interpreter()
2248    }
2249    fn make_num_ast(n: f64) -> formualizer_parse::parser::ASTNode {
2250        formualizer_parse::parser::ASTNode::new(
2251            formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Number(n)),
2252            None,
2253        )
2254    }
2255    #[test]
2256    fn test_coth_div0() {
2257        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(CothFn));
2258        let ctx = interp(&wb);
2259        let f = ctx.context.get_function("", "COTH").unwrap();
2260        let a0 = make_num_ast(0.0);
2261        let args = vec![ArgumentHandle::new(&a0, &ctx)];
2262        match f
2263            .dispatch(&args, &ctx.function_context(None))
2264            .unwrap()
2265            .into_literal()
2266        {
2267            LiteralValue::Error(e) => assert_eq!(e, "#DIV/0!"),
2268            v => panic!("expected error, got {v:?}"),
2269        }
2270    }
2271}
2272
2273/* ───────────────────── Angle conversion & constant ─────────────────── */
2274
2275#[derive(Debug)]
2276pub struct RadiansFn;
2277/// Converts an angle from degrees to radians.
2278///
2279/// # Remarks
2280/// - Use this before trigonometric functions when your source angle is in degrees.
2281/// - Output is `degrees * PI() / 180`.
2282///
2283/// # Examples
2284/// ```yaml,sandbox
2285/// title: "Convert 180°"
2286/// formula: "=RADIANS(180)"
2287/// expected: 3.141592653589793
2288/// ```
2289///
2290/// ```yaml,sandbox
2291/// title: "Convert 45°"
2292/// formula: "=RADIANS(45)"
2293/// expected: 0.7853981633974483
2294/// ```
2295///
2296/// ```yaml,docs
2297/// related:
2298///   - DEGREES
2299///   - SIN
2300///   - COS
2301/// faq:
2302///   - q: "When should I wrap angles with RADIANS?"
2303///     a: "Use it whenever your source angle is in degrees and the downstream trig function expects radians."
2304/// ```
2305/// [formualizer-docgen:schema:start]
2306/// Name: RADIANS
2307/// Type: RadiansFn
2308/// Min args: 1
2309/// Max args: 1
2310/// Variadic: false
2311/// Signature: RADIANS(arg1: any@scalar)
2312/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
2313/// Caps: PURE, ELEMENTWISE, NUMERIC_ONLY
2314/// [formualizer-docgen:schema:end]
2315impl Function for RadiansFn {
2316    func_caps!(PURE, ELEMENTWISE, NUMERIC_ONLY);
2317    fn name(&self) -> &'static str {
2318        "RADIANS"
2319    }
2320    fn min_args(&self) -> usize {
2321        1
2322    }
2323    fn arg_schema(&self) -> &'static [ArgSchema] {
2324        &ARG_ANY_ONE[..]
2325    }
2326    fn eval<'a, 'b, 'c>(
2327        &self,
2328        args: &'c [ArgumentHandle<'a, 'b>],
2329        _ctx: &dyn FunctionContext<'b>,
2330    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2331        let deg = unary_numeric_arg(args)?;
2332        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2333            deg * PI / 180.0,
2334        )))
2335    }
2336}
2337
2338#[cfg(test)]
2339mod tests_radians {
2340    use super::*;
2341    use crate::test_workbook::TestWorkbook;
2342    use crate::traits::ArgumentHandle;
2343    use formualizer_parse::LiteralValue;
2344    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
2345        wb.interpreter()
2346    }
2347    fn make_num_ast(n: f64) -> formualizer_parse::parser::ASTNode {
2348        formualizer_parse::parser::ASTNode::new(
2349            formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Number(n)),
2350            None,
2351        )
2352    }
2353    fn assert_close(a: f64, b: f64) {
2354        assert!((a - b).abs() < 1e-9);
2355    }
2356    #[test]
2357    fn test_radians_basic() {
2358        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(RadiansFn));
2359        let ctx = interp(&wb);
2360        let f = ctx.context.get_function("", "RADIANS").unwrap();
2361        let a0 = make_num_ast(180.0);
2362        let args = vec![ArgumentHandle::new(&a0, &ctx)];
2363        match f
2364            .dispatch(&args, &ctx.function_context(None))
2365            .unwrap()
2366            .into_literal()
2367        {
2368            LiteralValue::Number(n) => assert_close(n, PI),
2369            v => panic!("unexpected {v:?}"),
2370        }
2371    }
2372}
2373
2374#[derive(Debug)]
2375pub struct DegreesFn;
2376/// Converts an angle from radians to degrees.
2377///
2378/// # Remarks
2379/// - Useful when converting the output of inverse trig functions.
2380/// - Output is `radians * 180 / PI()`.
2381///
2382/// # Examples
2383/// ```yaml,sandbox
2384/// title: "Convert PI radians"
2385/// formula: "=DEGREES(PI())"
2386/// expected: 180
2387/// ```
2388///
2389/// ```yaml,sandbox
2390/// title: "Convert PI/2 radians"
2391/// formula: "=DEGREES(PI()/2)"
2392/// expected: 90
2393/// ```
2394///
2395/// ```yaml,docs
2396/// related:
2397///   - RADIANS
2398///   - ATAN
2399///   - ACOS
2400/// faq:
2401///   - q: "Does DEGREES change the angle value or just units?"
2402///     a: "It converts units only, multiplying radians by 180/PI()."
2403/// ```
2404/// [formualizer-docgen:schema:start]
2405/// Name: DEGREES
2406/// Type: DegreesFn
2407/// Min args: 1
2408/// Max args: 1
2409/// Variadic: false
2410/// Signature: DEGREES(arg1: any@scalar)
2411/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
2412/// Caps: PURE, ELEMENTWISE, NUMERIC_ONLY
2413/// [formualizer-docgen:schema:end]
2414impl Function for DegreesFn {
2415    func_caps!(PURE, ELEMENTWISE, NUMERIC_ONLY);
2416    fn name(&self) -> &'static str {
2417        "DEGREES"
2418    }
2419    fn min_args(&self) -> usize {
2420        1
2421    }
2422    fn arg_schema(&self) -> &'static [ArgSchema] {
2423        &ARG_ANY_ONE[..]
2424    }
2425    fn eval<'a, 'b, 'c>(
2426        &self,
2427        args: &'c [ArgumentHandle<'a, 'b>],
2428        _ctx: &dyn FunctionContext<'b>,
2429    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2430        let rad = unary_numeric_arg(args)?;
2431        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2432            rad * 180.0 / PI,
2433        )))
2434    }
2435}
2436
2437#[cfg(test)]
2438mod tests_degrees {
2439    use super::*;
2440    use crate::test_workbook::TestWorkbook;
2441    use crate::traits::ArgumentHandle;
2442    use formualizer_parse::LiteralValue;
2443    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
2444        wb.interpreter()
2445    }
2446    fn make_num_ast(n: f64) -> formualizer_parse::parser::ASTNode {
2447        formualizer_parse::parser::ASTNode::new(
2448            formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Number(n)),
2449            None,
2450        )
2451    }
2452    fn assert_close(a: f64, b: f64) {
2453        assert!((a - b).abs() < 1e-9);
2454    }
2455    #[test]
2456    fn test_degrees_basic() {
2457        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(DegreesFn));
2458        let ctx = interp(&wb);
2459        let f = ctx.context.get_function("", "DEGREES").unwrap();
2460        let a0 = make_num_ast(PI);
2461        let args = vec![ArgumentHandle::new(&a0, &ctx)];
2462        match f
2463            .dispatch(&args, &ctx.function_context(None))
2464            .unwrap()
2465            .into_literal()
2466        {
2467            LiteralValue::Number(n) => assert_close(n, 180.0),
2468            v => panic!("unexpected {v:?}"),
2469        }
2470    }
2471}
2472
2473#[derive(Debug)]
2474pub struct PiFn;
2475/// Returns the mathematical constant π.
2476///
2477/// # Remarks
2478/// - `PI()` takes no arguments.
2479/// - Commonly used with trig and geometry formulas.
2480///
2481/// # Examples
2482/// ```yaml,sandbox
2483/// title: "Pi constant"
2484/// formula: "=PI()"
2485/// expected: 3.141592653589793
2486/// ```
2487///
2488/// ```yaml,sandbox
2489/// title: "Circle circumference with radius 2"
2490/// formula: "=2*PI()*2"
2491/// expected: 12.566370614359172
2492/// ```
2493///
2494/// ```yaml,docs
2495/// related:
2496///   - RADIANS
2497///   - DEGREES
2498///   - SIN
2499/// faq:
2500///   - q: "Can PI take arguments?"
2501///     a: "No. PI() has arity zero and always returns the same constant."
2502/// ```
2503/// [formualizer-docgen:schema:start]
2504/// Name: PI
2505/// Type: PiFn
2506/// Min args: 0
2507/// Max args: 0
2508/// Variadic: false
2509/// Signature: PI()
2510/// Arg schema: []
2511/// Caps: PURE
2512/// [formualizer-docgen:schema:end]
2513impl Function for PiFn {
2514    func_caps!(PURE);
2515    fn name(&self) -> &'static str {
2516        "PI"
2517    }
2518    fn min_args(&self) -> usize {
2519        0
2520    }
2521    fn eval<'a, 'b, 'c>(
2522        &self,
2523        _args: &'c [ArgumentHandle<'a, 'b>],
2524        _ctx: &dyn FunctionContext<'b>,
2525    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2526        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(PI)))
2527    }
2528}
2529
2530#[cfg(test)]
2531mod tests_pi {
2532    use super::*;
2533    use crate::test_workbook::TestWorkbook;
2534    use formualizer_parse::LiteralValue;
2535    #[test]
2536    fn test_pi_basic() {
2537        let wb = TestWorkbook::new().with_function(std::sync::Arc::new(PiFn));
2538        let ctx = wb.interpreter();
2539        let f = ctx.context.get_function("", "PI").unwrap();
2540        assert_eq!(
2541            f.eval(&[], &ctx.function_context(None))
2542                .unwrap()
2543                .into_literal(),
2544            LiteralValue::Number(PI)
2545        );
2546    }
2547}
2548
2549pub fn register_builtins() {
2550    // --- Trigonometry: circular ---
2551    crate::function_registry::register_function(std::sync::Arc::new(SinFn));
2552    crate::function_registry::register_function(std::sync::Arc::new(CosFn));
2553    crate::function_registry::register_function(std::sync::Arc::new(TanFn));
2554    // A few elementwise numeric funcs are wired for map path; extend as needed
2555    crate::function_registry::register_function(std::sync::Arc::new(AsinFn));
2556    crate::function_registry::register_function(std::sync::Arc::new(AcosFn));
2557    crate::function_registry::register_function(std::sync::Arc::new(AtanFn));
2558    crate::function_registry::register_function(std::sync::Arc::new(Atan2Fn));
2559    crate::function_registry::register_function(std::sync::Arc::new(SecFn));
2560    crate::function_registry::register_function(std::sync::Arc::new(CscFn));
2561    crate::function_registry::register_function(std::sync::Arc::new(CotFn));
2562    crate::function_registry::register_function(std::sync::Arc::new(AcotFn));
2563
2564    // --- Trigonometry: hyperbolic ---
2565    crate::function_registry::register_function(std::sync::Arc::new(SinhFn));
2566    crate::function_registry::register_function(std::sync::Arc::new(CoshFn));
2567    crate::function_registry::register_function(std::sync::Arc::new(TanhFn));
2568    crate::function_registry::register_function(std::sync::Arc::new(AsinhFn));
2569    crate::function_registry::register_function(std::sync::Arc::new(AcoshFn));
2570    crate::function_registry::register_function(std::sync::Arc::new(AtanhFn));
2571    crate::function_registry::register_function(std::sync::Arc::new(SechFn));
2572    crate::function_registry::register_function(std::sync::Arc::new(CschFn));
2573    crate::function_registry::register_function(std::sync::Arc::new(CothFn));
2574
2575    // --- Angle conversion and constants ---
2576    crate::function_registry::register_function(std::sync::Arc::new(RadiansFn));
2577    crate::function_registry::register_function(std::sync::Arc::new(DegreesFn));
2578    crate::function_registry::register_function(std::sync::Arc::new(PiFn));
2579}