Skip to main content

formualizer_eval/builtins/
engineering.rs

1//! Engineering functions
2//! Bitwise: BITAND, BITOR, BITXOR, BITLSHIFT, BITRSHIFT
3
4use super::utils::{ARG_ANY_TWO, ARG_NUM_LENIENT_TWO, coerce_num};
5use crate::args::ArgSchema;
6use crate::function::Function;
7use crate::traits::{ArgumentHandle, FunctionContext};
8use formualizer_common::{ExcelError, LiteralValue};
9use formualizer_macros::func_caps;
10
11mod transcendental;
12
13/// Helper to convert to integer for bitwise operations
14/// Excel's bitwise functions only work with non-negative integers up to 2^48
15fn to_bitwise_int(v: &LiteralValue) -> Result<i64, ExcelError> {
16    let n = coerce_num(v)?;
17    if n < 0.0 || n != n.trunc() || n >= 281474976710656.0 {
18        // 2^48
19        return Err(ExcelError::new_num());
20    }
21    Ok(n as i64)
22}
23
24/* ─────────────────────────── BITAND ──────────────────────────── */
25
26/// Returns the bitwise AND of two non-negative integers.
27///
28/// Combines matching bits from both inputs and keeps only bits set in both numbers.
29///
30/// # Remarks
31/// - Arguments are coerced to numbers and must be whole numbers in the range `[0, 2^48)`.
32/// - Returns `#NUM!` for negative values, fractional values, or values outside the supported range.
33/// - Propagates input errors.
34///
35/// # Examples
36/// ```yaml,sandbox
37/// title: "Mask selected bits"
38/// formula: "=BITAND(13,10)"
39/// expected: 8
40/// ```
41///
42/// ```yaml,sandbox
43/// title: "Check least-significant bit"
44/// formula: "=BITAND(7,1)"
45/// expected: 1
46/// ```
47/// ```yaml,docs
48/// related:
49///   - BITOR
50///   - BITXOR
51///   - BITLSHIFT
52/// faq:
53///   - q: "When does `BITAND` return `#NUM!`?"
54///     a: "Inputs must be whole numbers in `[0, 2^48)`; negatives, fractions, and out-of-range values return `#NUM!`."
55/// ```
56#[derive(Debug)]
57pub struct BitAndFn;
58/// [formualizer-docgen:schema:start]
59/// Name: BITAND
60/// Type: BitAndFn
61/// Min args: 2
62/// Max args: 2
63/// Variadic: false
64/// Signature: BITAND(arg1: number@scalar, arg2: number@scalar)
65/// 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}
66/// Caps: PURE
67/// [formualizer-docgen:schema:end]
68impl Function for BitAndFn {
69    func_caps!(PURE);
70    fn name(&self) -> &'static str {
71        "BITAND"
72    }
73    fn min_args(&self) -> usize {
74        2
75    }
76    fn arg_schema(&self) -> &'static [ArgSchema] {
77        &ARG_NUM_LENIENT_TWO[..]
78    }
79    fn eval<'a, 'b, 'c>(
80        &self,
81        args: &'c [ArgumentHandle<'a, 'b>],
82        _ctx: &dyn FunctionContext<'b>,
83    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
84        let a = match args[0].value()?.into_literal() {
85            LiteralValue::Error(e) => {
86                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
87            }
88            other => match to_bitwise_int(&other) {
89                Ok(n) => n,
90                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
91            },
92        };
93        let b = match args[1].value()?.into_literal() {
94            LiteralValue::Error(e) => {
95                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
96            }
97            other => match to_bitwise_int(&other) {
98                Ok(n) => n,
99                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
100            },
101        };
102        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
103            (a & b) as f64,
104        )))
105    }
106}
107
108/* ─────────────────────────── BITOR ──────────────────────────── */
109
110/// Returns the bitwise OR of two non-negative integers.
111///
112/// Combines matching bits from both inputs and keeps bits set in either number.
113///
114/// # Remarks
115/// - Arguments are coerced to numbers and must be whole numbers in the range `[0, 2^48)`.
116/// - Returns `#NUM!` for negative values, fractional values, or out-of-range values.
117/// - Propagates input errors.
118///
119/// # Examples
120/// ```yaml,sandbox
121/// title: "Merge bit flags"
122/// formula: "=BITOR(13,10)"
123/// expected: 15
124/// ```
125///
126/// ```yaml,sandbox
127/// title: "Set an additional bit"
128/// formula: "=BITOR(8,1)"
129/// expected: 9
130/// ```
131/// ```yaml,docs
132/// related:
133///   - BITAND
134///   - BITXOR
135///   - BITRSHIFT
136/// faq:
137///   - q: "Does `BITOR` accept decimal-looking values like `3.0`?"
138///     a: "Yes if they coerce to whole integers; non-integer values still return `#NUM!`."
139/// ```
140#[derive(Debug)]
141pub struct BitOrFn;
142/// [formualizer-docgen:schema:start]
143/// Name: BITOR
144/// Type: BitOrFn
145/// Min args: 2
146/// Max args: 2
147/// Variadic: false
148/// Signature: BITOR(arg1: number@scalar, arg2: number@scalar)
149/// 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}
150/// Caps: PURE
151/// [formualizer-docgen:schema:end]
152impl Function for BitOrFn {
153    func_caps!(PURE);
154    fn name(&self) -> &'static str {
155        "BITOR"
156    }
157    fn min_args(&self) -> usize {
158        2
159    }
160    fn arg_schema(&self) -> &'static [ArgSchema] {
161        &ARG_NUM_LENIENT_TWO[..]
162    }
163    fn eval<'a, 'b, 'c>(
164        &self,
165        args: &'c [ArgumentHandle<'a, 'b>],
166        _ctx: &dyn FunctionContext<'b>,
167    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
168        let a = match args[0].value()?.into_literal() {
169            LiteralValue::Error(e) => {
170                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
171            }
172            other => match to_bitwise_int(&other) {
173                Ok(n) => n,
174                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
175            },
176        };
177        let b = match args[1].value()?.into_literal() {
178            LiteralValue::Error(e) => {
179                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
180            }
181            other => match to_bitwise_int(&other) {
182                Ok(n) => n,
183                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
184            },
185        };
186        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
187            (a | b) as f64,
188        )))
189    }
190}
191
192/* ─────────────────────────── BITXOR ──────────────────────────── */
193
194/// Returns the bitwise exclusive OR of two non-negative integers.
195///
196/// Keeps bits that differ between the two inputs.
197///
198/// # Remarks
199/// - Arguments are coerced to numbers and must be whole numbers in the range `[0, 2^48)`.
200/// - Returns `#NUM!` for negative values, fractional values, or out-of-range values.
201/// - Propagates input errors.
202///
203/// # Examples
204/// ```yaml,sandbox
205/// title: "Highlight differing bits"
206/// formula: "=BITXOR(13,10)"
207/// expected: 7
208/// ```
209///
210/// ```yaml,sandbox
211/// title: "XOR identical values"
212/// formula: "=BITXOR(5,5)"
213/// expected: 0
214/// ```
215/// ```yaml,docs
216/// related:
217///   - BITAND
218///   - BITOR
219///   - BITLSHIFT
220/// faq:
221///   - q: "Why does `BITXOR(x, x)` return `0`?"
222///     a: "XOR keeps only differing bits; identical operands cancel every bit position."
223/// ```
224#[derive(Debug)]
225pub struct BitXorFn;
226/// [formualizer-docgen:schema:start]
227/// Name: BITXOR
228/// Type: BitXorFn
229/// Min args: 2
230/// Max args: 2
231/// Variadic: false
232/// Signature: BITXOR(arg1: number@scalar, arg2: number@scalar)
233/// 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}
234/// Caps: PURE
235/// [formualizer-docgen:schema:end]
236impl Function for BitXorFn {
237    func_caps!(PURE);
238    fn name(&self) -> &'static str {
239        "BITXOR"
240    }
241    fn min_args(&self) -> usize {
242        2
243    }
244    fn arg_schema(&self) -> &'static [ArgSchema] {
245        &ARG_NUM_LENIENT_TWO[..]
246    }
247    fn eval<'a, 'b, 'c>(
248        &self,
249        args: &'c [ArgumentHandle<'a, 'b>],
250        _ctx: &dyn FunctionContext<'b>,
251    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
252        let a = match args[0].value()?.into_literal() {
253            LiteralValue::Error(e) => {
254                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
255            }
256            other => match to_bitwise_int(&other) {
257                Ok(n) => n,
258                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
259            },
260        };
261        let b = match args[1].value()?.into_literal() {
262            LiteralValue::Error(e) => {
263                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
264            }
265            other => match to_bitwise_int(&other) {
266                Ok(n) => n,
267                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
268            },
269        };
270        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
271            (a ^ b) as f64,
272        )))
273    }
274}
275
276/* ─────────────────────────── BITLSHIFT ──────────────────────────── */
277
278/// Shifts a non-negative integer left or right by a given bit count.
279///
280/// Positive `shift_amount` shifts left; negative `shift_amount` shifts right.
281///
282/// # Remarks
283/// - `number` must be a whole number in `[0, 2^48)`.
284/// - Shift values are numerically coerced; large positive shifts can return `#NUM!`.
285/// - Left-shift results must remain below `2^48`, or the function returns `#NUM!`.
286///
287/// # Examples
288/// ```yaml,sandbox
289/// title: "Shift left by two bits"
290/// formula: "=BITLSHIFT(6,2)"
291/// expected: 24
292/// ```
293///
294/// ```yaml,sandbox
295/// title: "Use negative shift to move right"
296/// formula: "=BITLSHIFT(32,-3)"
297/// expected: 4
298/// ```
299/// ```yaml,docs
300/// related:
301///   - BITRSHIFT
302///   - BITAND
303///   - BITOR
304/// faq:
305///   - q: "What does a negative `shift_amount` do in `BITLSHIFT`?"
306///     a: "Negative shifts are interpreted as right shifts, while positive shifts move bits left."
307/// ```
308#[derive(Debug)]
309pub struct BitLShiftFn;
310/// [formualizer-docgen:schema:start]
311/// Name: BITLSHIFT
312/// Type: BitLShiftFn
313/// Min args: 2
314/// Max args: 2
315/// Variadic: false
316/// Signature: BITLSHIFT(arg1: number@scalar, arg2: number@scalar)
317/// 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}
318/// Caps: PURE
319/// [formualizer-docgen:schema:end]
320impl Function for BitLShiftFn {
321    func_caps!(PURE);
322    fn name(&self) -> &'static str {
323        "BITLSHIFT"
324    }
325    fn min_args(&self) -> usize {
326        2
327    }
328    fn arg_schema(&self) -> &'static [ArgSchema] {
329        &ARG_NUM_LENIENT_TWO[..]
330    }
331    fn eval<'a, 'b, 'c>(
332        &self,
333        args: &'c [ArgumentHandle<'a, 'b>],
334        _ctx: &dyn FunctionContext<'b>,
335    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
336        let n = match args[0].value()?.into_literal() {
337            LiteralValue::Error(e) => {
338                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
339            }
340            other => match to_bitwise_int(&other) {
341                Ok(n) => n,
342                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
343            },
344        };
345        let shift = match args[1].value()?.into_literal() {
346            LiteralValue::Error(e) => {
347                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
348            }
349            other => coerce_num(&other)? as i32,
350        };
351
352        // Negative shift means right shift
353        let result = if shift >= 0 {
354            if shift >= 48 {
355                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
356                    ExcelError::new_num(),
357                )));
358            }
359            let shifted = n << shift;
360            // Check if result exceeds 48-bit limit
361            if shifted >= 281474976710656 {
362                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
363                    ExcelError::new_num(),
364                )));
365            }
366            shifted
367        } else {
368            let rshift = (-shift) as u32;
369            if rshift >= 48 { 0 } else { n >> rshift }
370        };
371
372        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
373            result as f64,
374        )))
375    }
376}
377
378/* ─────────────────────────── BITRSHIFT ──────────────────────────── */
379
380/// Shifts a non-negative integer right or left by a given bit count.
381///
382/// Positive `shift_amount` shifts right; negative `shift_amount` shifts left.
383///
384/// # Remarks
385/// - `number` must be a whole number in `[0, 2^48)`.
386/// - Shift values are numerically coerced; large right shifts return `0`.
387/// - Negative shifts that overflow the 48-bit limit return `#NUM!`.
388///
389/// # Examples
390/// ```yaml,sandbox
391/// title: "Shift right by three bits"
392/// formula: "=BITRSHIFT(32,3)"
393/// expected: 4
394/// ```
395///
396/// ```yaml,sandbox
397/// title: "Use negative shift to move left"
398/// formula: "=BITRSHIFT(5,-1)"
399/// expected: 10
400/// ```
401/// ```yaml,docs
402/// related:
403///   - BITLSHIFT
404///   - BITAND
405///   - BITXOR
406/// faq:
407///   - q: "Why can negative shifts in `BITRSHIFT` return `#NUM!`?"
408///     a: "A negative shift means left-shift; if that left result exceeds the 48-bit limit, `#NUM!` is returned."
409/// ```
410#[derive(Debug)]
411pub struct BitRShiftFn;
412/// [formualizer-docgen:schema:start]
413/// Name: BITRSHIFT
414/// Type: BitRShiftFn
415/// Min args: 2
416/// Max args: 2
417/// Variadic: false
418/// Signature: BITRSHIFT(arg1: number@scalar, arg2: number@scalar)
419/// 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}
420/// Caps: PURE
421/// [formualizer-docgen:schema:end]
422impl Function for BitRShiftFn {
423    func_caps!(PURE);
424    fn name(&self) -> &'static str {
425        "BITRSHIFT"
426    }
427    fn min_args(&self) -> usize {
428        2
429    }
430    fn arg_schema(&self) -> &'static [ArgSchema] {
431        &ARG_NUM_LENIENT_TWO[..]
432    }
433    fn eval<'a, 'b, 'c>(
434        &self,
435        args: &'c [ArgumentHandle<'a, 'b>],
436        _ctx: &dyn FunctionContext<'b>,
437    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
438        let n = match args[0].value()?.into_literal() {
439            LiteralValue::Error(e) => {
440                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
441            }
442            other => match to_bitwise_int(&other) {
443                Ok(n) => n,
444                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
445            },
446        };
447        let shift = match args[1].value()?.into_literal() {
448            LiteralValue::Error(e) => {
449                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
450            }
451            other => coerce_num(&other)? as i32,
452        };
453
454        // Negative shift means left shift
455        let result = if shift >= 0 {
456            if shift >= 48 { 0 } else { n >> shift }
457        } else {
458            let lshift = (-shift) as u32;
459            if lshift >= 48 {
460                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
461                    ExcelError::new_num(),
462                )));
463            }
464            let shifted = n << lshift;
465            // Check if result exceeds 48-bit limit
466            if shifted >= 281474976710656 {
467                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
468                    ExcelError::new_num(),
469                )));
470            }
471            shifted
472        };
473
474        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
475            result as f64,
476        )))
477    }
478}
479
480/* ─────────────────────────── Base Conversion Functions ──────────────────────────── */
481
482use super::utils::ARG_ANY_ONE;
483
484/// Helper to coerce value to text for base conversion
485fn coerce_base_text(v: &LiteralValue) -> Result<String, ExcelError> {
486    match v {
487        LiteralValue::Text(s) => Ok(s.clone()),
488        LiteralValue::Int(i) => Ok(i.to_string()),
489        LiteralValue::Number(n) => Ok((*n as i64).to_string()),
490        LiteralValue::Error(e) => Err(e.clone()),
491        _ => Err(ExcelError::new_value()),
492    }
493}
494
495/// Converts a binary text value to decimal.
496///
497/// Supports up to 10 binary digits, including two's-complement negative values.
498///
499/// # Remarks
500/// - Input is coerced to text and must contain only `0` and `1`.
501/// - 10-digit values starting with `1` are interpreted as signed two's-complement numbers.
502/// - Returns `#NUM!` for invalid characters or inputs longer than 10 digits.
503///
504/// # Examples
505/// ```yaml,sandbox
506/// title: "Convert an unsigned binary value"
507/// formula: "=BIN2DEC(\"101010\")"
508/// expected: 42
509/// ```
510///
511/// ```yaml,sandbox
512/// title: "Interpret signed 10-bit binary"
513/// formula: "=BIN2DEC(\"1111111111\")"
514/// expected: -1
515/// ```
516/// ```yaml,docs
517/// related:
518///   - DEC2BIN
519///   - BIN2HEX
520///   - BIN2OCT
521/// faq:
522///   - q: "How does `BIN2DEC` handle 10-bit values starting with `1`?"
523///     a: "They are interpreted as signed two's-complement values, so `1111111111` becomes `-1`."
524/// ```
525#[derive(Debug)]
526pub struct Bin2DecFn;
527/// [formualizer-docgen:schema:start]
528/// Name: BIN2DEC
529/// Type: Bin2DecFn
530/// Min args: 1
531/// Max args: 1
532/// Variadic: false
533/// Signature: BIN2DEC(arg1: any@scalar)
534/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
535/// Caps: PURE
536/// [formualizer-docgen:schema:end]
537impl Function for Bin2DecFn {
538    func_caps!(PURE);
539    fn name(&self) -> &'static str {
540        "BIN2DEC"
541    }
542    fn min_args(&self) -> usize {
543        1
544    }
545    fn arg_schema(&self) -> &'static [ArgSchema] {
546        &ARG_ANY_ONE[..]
547    }
548    fn eval<'a, 'b, 'c>(
549        &self,
550        args: &'c [ArgumentHandle<'a, 'b>],
551        _ctx: &dyn FunctionContext<'b>,
552    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
553        let text = match args[0].value()?.into_literal() {
554            LiteralValue::Error(e) => {
555                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
556            }
557            other => match coerce_base_text(&other) {
558                Ok(s) => s,
559                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
560            },
561        };
562
563        // Excel accepts 10-character binary (with sign bit)
564        if text.len() > 10 || !text.chars().all(|c| c == '0' || c == '1') {
565            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
566                ExcelError::new_num(),
567            )));
568        }
569
570        // Handle two's complement for negative numbers (10 bits, first bit is sign)
571        let result = if text.len() == 10 && text.starts_with('1') {
572            // Negative number in two's complement
573            let val = i64::from_str_radix(&text, 2).unwrap_or(0);
574            val - 1024 // 2^10
575        } else {
576            i64::from_str_radix(&text, 2).unwrap_or(0)
577        };
578
579        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
580            result as f64,
581        )))
582    }
583}
584
585/// Converts a decimal integer to binary text.
586///
587/// Optionally pads the result with leading zeros using `places`.
588///
589/// # Remarks
590/// - `number` is coerced to an integer and must be in `[-512, 511]`.
591/// - Negative values are returned as 10-bit two's-complement binary strings.
592/// - `places` must be at least the output width and at most `10`, or `#NUM!` is returned.
593///
594/// # Examples
595/// ```yaml,sandbox
596/// title: "Convert a positive integer"
597/// formula: "=DEC2BIN(42)"
598/// expected: "101010"
599/// ```
600///
601/// ```yaml,sandbox
602/// title: "Pad binary output"
603/// formula: "=DEC2BIN(5,8)"
604/// expected: "00000101"
605/// ```
606/// ```yaml,docs
607/// related:
608///   - BIN2DEC
609///   - DEC2HEX
610///   - DEC2OCT
611/// faq:
612///   - q: "What limits apply to `DEC2BIN`?"
613///     a: "`number` must be in `[-512, 511]`, and optional `places` must be between output width and `10`, else `#NUM!`."
614/// ```
615#[derive(Debug)]
616pub struct Dec2BinFn;
617/// [formualizer-docgen:schema:start]
618/// Name: DEC2BIN
619/// Type: Dec2BinFn
620/// Min args: 1
621/// Max args: variadic
622/// Variadic: true
623/// Signature: DEC2BIN(arg1: number@scalar, arg2...: number@scalar)
624/// 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}
625/// Caps: PURE
626/// [formualizer-docgen:schema:end]
627impl Function for Dec2BinFn {
628    func_caps!(PURE);
629    fn name(&self) -> &'static str {
630        "DEC2BIN"
631    }
632    fn min_args(&self) -> usize {
633        1
634    }
635    fn variadic(&self) -> bool {
636        true
637    }
638    fn arg_schema(&self) -> &'static [ArgSchema] {
639        &ARG_NUM_LENIENT_TWO[..]
640    }
641    fn eval<'a, 'b, 'c>(
642        &self,
643        args: &'c [ArgumentHandle<'a, 'b>],
644        _ctx: &dyn FunctionContext<'b>,
645    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
646        let n = match args[0].value()?.into_literal() {
647            LiteralValue::Error(e) => {
648                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
649            }
650            other => coerce_num(&other)? as i64,
651        };
652
653        // Excel limits: -512 to 511
654        if !(-512..=511).contains(&n) {
655            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
656                ExcelError::new_num(),
657            )));
658        }
659
660        let places = if args.len() > 1 {
661            match args[1].value()?.into_literal() {
662                LiteralValue::Error(e) => {
663                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
664                }
665                other => Some(coerce_num(&other)? as usize),
666            }
667        } else {
668            None
669        };
670
671        let binary = if n >= 0 {
672            format!("{:b}", n)
673        } else {
674            // Two's complement with 10 bits
675            format!("{:010b}", (n + 1024) as u64)
676        };
677
678        let result = if let Some(p) = places {
679            if p < binary.len() || p > 10 {
680                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
681                    ExcelError::new_num(),
682                )));
683            }
684            format!("{:0>width$}", binary, width = p)
685        } else {
686            binary
687        };
688
689        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(result)))
690    }
691}
692
693/// Converts a hexadecimal text value to decimal.
694///
695/// Supports up to 10 hex digits, including signed two's-complement values.
696///
697/// # Remarks
698/// - Input is coerced to text and must contain only hexadecimal characters.
699/// - 10-digit values beginning with `8`-`F` are interpreted as signed 40-bit numbers.
700/// - Returns `#NUM!` for invalid characters or inputs longer than 10 digits.
701///
702/// # Examples
703/// ```yaml,sandbox
704/// title: "Convert a positive hex value"
705/// formula: "=HEX2DEC(\"FF\")"
706/// expected: 255
707/// ```
708///
709/// ```yaml,sandbox
710/// title: "Interpret signed 40-bit hex"
711/// formula: "=HEX2DEC(\"FFFFFFFFFF\")"
712/// expected: -1
713/// ```
714/// ```yaml,docs
715/// related:
716///   - DEC2HEX
717///   - HEX2BIN
718///   - HEX2OCT
719/// faq:
720///   - q: "When is a 10-digit hex input treated as negative in `HEX2DEC`?"
721///     a: "If the first digit is `8` through `F`, it is decoded as signed 40-bit two's-complement."
722/// ```
723#[derive(Debug)]
724pub struct Hex2DecFn;
725/// [formualizer-docgen:schema:start]
726/// Name: HEX2DEC
727/// Type: Hex2DecFn
728/// Min args: 1
729/// Max args: 1
730/// Variadic: false
731/// Signature: HEX2DEC(arg1: any@scalar)
732/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
733/// Caps: PURE
734/// [formualizer-docgen:schema:end]
735impl Function for Hex2DecFn {
736    func_caps!(PURE);
737    fn name(&self) -> &'static str {
738        "HEX2DEC"
739    }
740    fn min_args(&self) -> usize {
741        1
742    }
743    fn arg_schema(&self) -> &'static [ArgSchema] {
744        &ARG_ANY_ONE[..]
745    }
746    fn eval<'a, 'b, 'c>(
747        &self,
748        args: &'c [ArgumentHandle<'a, 'b>],
749        _ctx: &dyn FunctionContext<'b>,
750    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
751        let text = match args[0].value()?.into_literal() {
752            LiteralValue::Error(e) => {
753                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
754            }
755            other => match coerce_base_text(&other) {
756                Ok(s) => s.to_uppercase(),
757                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
758            },
759        };
760
761        // Excel accepts 10-character hex
762        if text.len() > 10 || !text.chars().all(|c| c.is_ascii_hexdigit()) {
763            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
764                ExcelError::new_num(),
765            )));
766        }
767
768        let result = if text.len() == 10 && text.starts_with(|c| c >= '8') {
769            // Negative number in two's complement (40 bits)
770            let val = i64::from_str_radix(&text, 16).unwrap_or(0);
771            val - (1i64 << 40)
772        } else {
773            i64::from_str_radix(&text, 16).unwrap_or(0)
774        };
775
776        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
777            result as f64,
778        )))
779    }
780}
781
782/// Converts a decimal integer to hexadecimal text.
783///
784/// Optionally pads the result with leading zeros using `places`.
785///
786/// # Remarks
787/// - `number` is coerced to an integer and must be in `[-2^39, 2^39 - 1]`.
788/// - Negative values are returned as 10-digit two's-complement hexadecimal strings.
789/// - `places` must be at least the output width and at most `10`, or `#NUM!` is returned.
790///
791/// # Examples
792/// ```yaml,sandbox
793/// title: "Convert decimal to hex"
794/// formula: "=DEC2HEX(255)"
795/// expected: "FF"
796/// ```
797///
798/// ```yaml,sandbox
799/// title: "Pad hexadecimal output"
800/// formula: "=DEC2HEX(31,4)"
801/// expected: "001F"
802/// ```
803/// ```yaml,docs
804/// related:
805///   - HEX2DEC
806///   - DEC2BIN
807///   - DEC2OCT
808/// faq:
809///   - q: "How are negative values formatted by `DEC2HEX`?"
810///     a: "Negative outputs use 10-digit two's-complement hexadecimal representation."
811/// ```
812#[derive(Debug)]
813pub struct Dec2HexFn;
814/// [formualizer-docgen:schema:start]
815/// Name: DEC2HEX
816/// Type: Dec2HexFn
817/// Min args: 1
818/// Max args: variadic
819/// Variadic: true
820/// Signature: DEC2HEX(arg1: number@scalar, arg2...: number@scalar)
821/// 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}
822/// Caps: PURE
823/// [formualizer-docgen:schema:end]
824impl Function for Dec2HexFn {
825    func_caps!(PURE);
826    fn name(&self) -> &'static str {
827        "DEC2HEX"
828    }
829    fn min_args(&self) -> usize {
830        1
831    }
832    fn variadic(&self) -> bool {
833        true
834    }
835    fn arg_schema(&self) -> &'static [ArgSchema] {
836        &ARG_NUM_LENIENT_TWO[..]
837    }
838    fn eval<'a, 'b, 'c>(
839        &self,
840        args: &'c [ArgumentHandle<'a, 'b>],
841        _ctx: &dyn FunctionContext<'b>,
842    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
843        let n = match args[0].value()?.into_literal() {
844            LiteralValue::Error(e) => {
845                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
846            }
847            other => coerce_num(&other)? as i64,
848        };
849
850        // Excel limits
851        if !(-(1i64 << 39)..=(1i64 << 39) - 1).contains(&n) {
852            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
853                ExcelError::new_num(),
854            )));
855        }
856
857        let places = if args.len() > 1 {
858            match args[1].value()?.into_literal() {
859                LiteralValue::Error(e) => {
860                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
861                }
862                other => Some(coerce_num(&other)? as usize),
863            }
864        } else {
865            None
866        };
867
868        let hex = if n >= 0 {
869            format!("{:X}", n)
870        } else {
871            // Two's complement with 10 hex digits (40 bits)
872            format!("{:010X}", (n + (1i64 << 40)) as u64)
873        };
874
875        let result = if let Some(p) = places {
876            if p < hex.len() || p > 10 {
877                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
878                    ExcelError::new_num(),
879                )));
880            }
881            format!("{:0>width$}", hex, width = p)
882        } else {
883            hex
884        };
885
886        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(result)))
887    }
888}
889
890/// Converts an octal text value to decimal.
891///
892/// Supports up to 10 octal digits, including signed two's-complement values.
893///
894/// # Remarks
895/// - Input is coerced to text and must contain only digits `0` through `7`.
896/// - 10-digit values beginning with `4`-`7` are interpreted as signed 30-bit numbers.
897/// - Returns `#NUM!` for invalid characters or inputs longer than 10 digits.
898///
899/// # Examples
900/// ```yaml,sandbox
901/// title: "Convert positive octal"
902/// formula: "=OCT2DEC(\"17\")"
903/// expected: 15
904/// ```
905///
906/// ```yaml,sandbox
907/// title: "Interpret signed 30-bit octal"
908/// formula: "=OCT2DEC(\"7777777777\")"
909/// expected: -1
910/// ```
911/// ```yaml,docs
912/// related:
913///   - DEC2OCT
914///   - OCT2BIN
915///   - OCT2HEX
916/// faq:
917///   - q: "How does `OCT2DEC` interpret 10-digit values starting with `4`-`7`?"
918///     a: "Those are treated as signed 30-bit two's-complement octal values."
919/// ```
920#[derive(Debug)]
921pub struct Oct2DecFn;
922/// [formualizer-docgen:schema:start]
923/// Name: OCT2DEC
924/// Type: Oct2DecFn
925/// Min args: 1
926/// Max args: 1
927/// Variadic: false
928/// Signature: OCT2DEC(arg1: any@scalar)
929/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
930/// Caps: PURE
931/// [formualizer-docgen:schema:end]
932impl Function for Oct2DecFn {
933    func_caps!(PURE);
934    fn name(&self) -> &'static str {
935        "OCT2DEC"
936    }
937    fn min_args(&self) -> usize {
938        1
939    }
940    fn arg_schema(&self) -> &'static [ArgSchema] {
941        &ARG_ANY_ONE[..]
942    }
943    fn eval<'a, 'b, 'c>(
944        &self,
945        args: &'c [ArgumentHandle<'a, 'b>],
946        _ctx: &dyn FunctionContext<'b>,
947    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
948        let text = match args[0].value()?.into_literal() {
949            LiteralValue::Error(e) => {
950                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
951            }
952            other => match coerce_base_text(&other) {
953                Ok(s) => s,
954                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
955            },
956        };
957
958        // Excel accepts 10-character octal (30 bits)
959        if text.len() > 10 || !text.chars().all(|c| ('0'..='7').contains(&c)) {
960            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
961                ExcelError::new_num(),
962            )));
963        }
964
965        let result = if text.len() == 10 && text.starts_with(|c| c >= '4') {
966            // Negative number in two's complement (30 bits)
967            let val = i64::from_str_radix(&text, 8).unwrap_or(0);
968            val - (1i64 << 30)
969        } else {
970            i64::from_str_radix(&text, 8).unwrap_or(0)
971        };
972
973        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
974            result as f64,
975        )))
976    }
977}
978
979/// Converts a decimal integer to octal text.
980///
981/// Optionally pads the result with leading zeros using `places`.
982///
983/// # Remarks
984/// - `number` is coerced to an integer and must be in `[-2^29, 2^29 - 1]`.
985/// - Negative values are returned as 10-digit two's-complement octal strings.
986/// - `places` must be at least the output width and at most `10`, or `#NUM!` is returned.
987///
988/// # Examples
989/// ```yaml,sandbox
990/// title: "Convert decimal to octal"
991/// formula: "=DEC2OCT(64)"
992/// expected: "100"
993/// ```
994///
995/// ```yaml,sandbox
996/// title: "Two's-complement negative output"
997/// formula: "=DEC2OCT(-1)"
998/// expected: "7777777777"
999/// ```
1000/// ```yaml,docs
1001/// related:
1002///   - OCT2DEC
1003///   - DEC2BIN
1004///   - DEC2HEX
1005/// faq:
1006///   - q: "What range does `DEC2OCT` support?"
1007///     a: "`number` must be in `[-2^29, 2^29 - 1]`; outside that range returns `#NUM!`."
1008/// ```
1009#[derive(Debug)]
1010pub struct Dec2OctFn;
1011/// [formualizer-docgen:schema:start]
1012/// Name: DEC2OCT
1013/// Type: Dec2OctFn
1014/// Min args: 1
1015/// Max args: variadic
1016/// Variadic: true
1017/// Signature: DEC2OCT(arg1: number@scalar, arg2...: number@scalar)
1018/// 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}
1019/// Caps: PURE
1020/// [formualizer-docgen:schema:end]
1021impl Function for Dec2OctFn {
1022    func_caps!(PURE);
1023    fn name(&self) -> &'static str {
1024        "DEC2OCT"
1025    }
1026    fn min_args(&self) -> usize {
1027        1
1028    }
1029    fn variadic(&self) -> bool {
1030        true
1031    }
1032    fn arg_schema(&self) -> &'static [ArgSchema] {
1033        &ARG_NUM_LENIENT_TWO[..]
1034    }
1035    fn eval<'a, 'b, 'c>(
1036        &self,
1037        args: &'c [ArgumentHandle<'a, 'b>],
1038        _ctx: &dyn FunctionContext<'b>,
1039    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1040        let n = match args[0].value()?.into_literal() {
1041            LiteralValue::Error(e) => {
1042                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1043            }
1044            other => coerce_num(&other)? as i64,
1045        };
1046
1047        // Excel limits: -536870912 to 536870911
1048        if !(-(1i64 << 29)..=(1i64 << 29) - 1).contains(&n) {
1049            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1050                ExcelError::new_num(),
1051            )));
1052        }
1053
1054        let places = if args.len() > 1 {
1055            match args[1].value()?.into_literal() {
1056                LiteralValue::Error(e) => {
1057                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1058                }
1059                other => Some(coerce_num(&other)? as usize),
1060            }
1061        } else {
1062            None
1063        };
1064
1065        let octal = if n >= 0 {
1066            format!("{:o}", n)
1067        } else {
1068            // Two's complement with 10 octal digits (30 bits)
1069            format!("{:010o}", (n + (1i64 << 30)) as u64)
1070        };
1071
1072        let result = if let Some(p) = places {
1073            if p < octal.len() || p > 10 {
1074                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1075                    ExcelError::new_num(),
1076                )));
1077            }
1078            format!("{:0>width$}", octal, width = p)
1079        } else {
1080            octal
1081        };
1082
1083        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(result)))
1084    }
1085}
1086
1087/* ─────────────────────────── Cross-Base Conversions ──────────────────────────── */
1088
1089/// Converts a binary text value to hexadecimal text.
1090///
1091/// Optionally pads the output with leading zeros using `places`.
1092///
1093/// # Remarks
1094/// - Input must be a binary string up to 10 digits; 10-digit values may be signed.
1095/// - Signed binary values are converted using two's-complement semantics.
1096/// - `places` must be at least the output width and at most `10`, or `#NUM!` is returned.
1097///
1098/// # Examples
1099/// ```yaml,sandbox
1100/// title: "Convert binary to hex"
1101/// formula: "=BIN2HEX(\"1010\")"
1102/// expected: "A"
1103/// ```
1104///
1105/// ```yaml,sandbox
1106/// title: "Pad hexadecimal output"
1107/// formula: "=BIN2HEX(\"1010\",4)"
1108/// expected: "000A"
1109/// ```
1110/// ```yaml,docs
1111/// related:
1112///   - HEX2BIN
1113///   - BIN2DEC
1114///   - DEC2HEX
1115/// faq:
1116///   - q: "Does `BIN2HEX` preserve signed binary meaning?"
1117///     a: "Yes. A 10-bit binary with leading `1` is interpreted as signed and converted using two's-complement semantics."
1118/// ```
1119#[derive(Debug)]
1120pub struct Bin2HexFn;
1121/// [formualizer-docgen:schema:start]
1122/// Name: BIN2HEX
1123/// Type: Bin2HexFn
1124/// Min args: 1
1125/// Max args: variadic
1126/// Variadic: true
1127/// Signature: BIN2HEX(arg1: number@scalar, arg2...: number@scalar)
1128/// 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}
1129/// Caps: PURE
1130/// [formualizer-docgen:schema:end]
1131impl Function for Bin2HexFn {
1132    func_caps!(PURE);
1133    fn name(&self) -> &'static str {
1134        "BIN2HEX"
1135    }
1136    fn min_args(&self) -> usize {
1137        1
1138    }
1139    fn variadic(&self) -> bool {
1140        true
1141    }
1142    fn arg_schema(&self) -> &'static [ArgSchema] {
1143        &ARG_NUM_LENIENT_TWO[..]
1144    }
1145    fn eval<'a, 'b, 'c>(
1146        &self,
1147        args: &'c [ArgumentHandle<'a, 'b>],
1148        _ctx: &dyn FunctionContext<'b>,
1149    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1150        let text = match args[0].value()?.into_literal() {
1151            LiteralValue::Error(e) => {
1152                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1153            }
1154            other => match coerce_base_text(&other) {
1155                Ok(s) => s,
1156                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1157            },
1158        };
1159
1160        if text.len() > 10 || !text.chars().all(|c| c == '0' || c == '1') {
1161            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1162                ExcelError::new_num(),
1163            )));
1164        }
1165
1166        // Convert binary to decimal first
1167        let dec = if text.len() == 10 && text.starts_with('1') {
1168            let val = i64::from_str_radix(&text, 2).unwrap_or(0);
1169            val - 1024
1170        } else {
1171            i64::from_str_radix(&text, 2).unwrap_or(0)
1172        };
1173
1174        let places = if args.len() > 1 {
1175            match args[1].value()?.into_literal() {
1176                LiteralValue::Error(e) => {
1177                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1178                }
1179                other => Some(coerce_num(&other)? as usize),
1180            }
1181        } else {
1182            None
1183        };
1184
1185        let hex = if dec >= 0 {
1186            format!("{:X}", dec)
1187        } else {
1188            format!("{:010X}", (dec + (1i64 << 40)) as u64)
1189        };
1190
1191        let result = if let Some(p) = places {
1192            if p < hex.len() || p > 10 {
1193                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1194                    ExcelError::new_num(),
1195                )));
1196            }
1197            format!("{:0>width$}", hex, width = p)
1198        } else {
1199            hex
1200        };
1201
1202        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(result)))
1203    }
1204}
1205
1206/// Converts a hexadecimal text value to binary text.
1207///
1208/// Supports optional left-padding through the `places` argument.
1209///
1210/// # Remarks
1211/// - Input must be hexadecimal text up to 10 characters and may be signed two's-complement.
1212/// - The converted decimal value must be in `[-512, 511]`, or the function returns `#NUM!`.
1213/// - `places` must be at least the output width and at most `10`, or `#NUM!` is returned.
1214///
1215/// # Examples
1216/// ```yaml,sandbox
1217/// title: "Convert positive hex to binary"
1218/// formula: "=HEX2BIN(\"1F\")"
1219/// expected: "11111"
1220/// ```
1221///
1222/// ```yaml,sandbox
1223/// title: "Convert signed hex"
1224/// formula: "=HEX2BIN(\"FFFFFFFFFF\")"
1225/// expected: "1111111111"
1226/// ```
1227/// ```yaml,docs
1228/// related:
1229///   - BIN2HEX
1230///   - HEX2DEC
1231///   - DEC2BIN
1232/// faq:
1233///   - q: "Why can valid hex text still produce `#NUM!` in `HEX2BIN`?"
1234///     a: "After conversion, the decimal value must fit `[-512, 511]`; otherwise binary output is rejected."
1235/// ```
1236#[derive(Debug)]
1237pub struct Hex2BinFn;
1238/// [formualizer-docgen:schema:start]
1239/// Name: HEX2BIN
1240/// Type: Hex2BinFn
1241/// Min args: 1
1242/// Max args: variadic
1243/// Variadic: true
1244/// Signature: HEX2BIN(arg1: any@scalar, arg2...: any@scalar)
1245/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}; arg2{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
1246/// Caps: PURE
1247/// [formualizer-docgen:schema:end]
1248impl Function for Hex2BinFn {
1249    func_caps!(PURE);
1250    fn name(&self) -> &'static str {
1251        "HEX2BIN"
1252    }
1253    fn min_args(&self) -> usize {
1254        1
1255    }
1256    fn variadic(&self) -> bool {
1257        true
1258    }
1259    fn arg_schema(&self) -> &'static [ArgSchema] {
1260        &ARG_ANY_TWO[..]
1261    }
1262    fn eval<'a, 'b, 'c>(
1263        &self,
1264        args: &'c [ArgumentHandle<'a, 'b>],
1265        _ctx: &dyn FunctionContext<'b>,
1266    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1267        let text = match args[0].value()?.into_literal() {
1268            LiteralValue::Error(e) => {
1269                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1270            }
1271            other => match coerce_base_text(&other) {
1272                Ok(s) => s.to_uppercase(),
1273                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1274            },
1275        };
1276
1277        if text.len() > 10 || !text.chars().all(|c| c.is_ascii_hexdigit()) {
1278            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1279                ExcelError::new_num(),
1280            )));
1281        }
1282
1283        // Convert hex to decimal first
1284        let dec = if text.len() == 10 && text.starts_with(|c| c >= '8') {
1285            let val = i64::from_str_radix(&text, 16).unwrap_or(0);
1286            val - (1i64 << 40)
1287        } else {
1288            i64::from_str_radix(&text, 16).unwrap_or(0)
1289        };
1290
1291        // Check range for binary output (-512 to 511)
1292        if !(-512..=511).contains(&dec) {
1293            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1294                ExcelError::new_num(),
1295            )));
1296        }
1297
1298        let places = if args.len() > 1 {
1299            match args[1].value()?.into_literal() {
1300                LiteralValue::Error(e) => {
1301                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1302                }
1303                other => Some(coerce_num(&other)? as usize),
1304            }
1305        } else {
1306            None
1307        };
1308
1309        let binary = if dec >= 0 {
1310            format!("{:b}", dec)
1311        } else {
1312            format!("{:010b}", (dec + 1024) as u64)
1313        };
1314
1315        let result = if let Some(p) = places {
1316            if p < binary.len() || p > 10 {
1317                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1318                    ExcelError::new_num(),
1319                )));
1320            }
1321            format!("{:0>width$}", binary, width = p)
1322        } else {
1323            binary
1324        };
1325
1326        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(result)))
1327    }
1328}
1329
1330/// Converts a binary text value to octal text.
1331///
1332/// Supports optional left-padding through the `places` argument.
1333///
1334/// # Remarks
1335/// - Input must be binary text up to 10 digits and may be signed two's-complement.
1336/// - Signed values are preserved through conversion to octal.
1337/// - `places` must be at least the output width and at most `10`, or `#NUM!` is returned.
1338///
1339/// # Examples
1340/// ```yaml,sandbox
1341/// title: "Convert binary to octal"
1342/// formula: "=BIN2OCT(\"111111\")"
1343/// expected: "77"
1344/// ```
1345///
1346/// ```yaml,sandbox
1347/// title: "Pad octal output"
1348/// formula: "=BIN2OCT(\"111111\",4)"
1349/// expected: "0077"
1350/// ```
1351/// ```yaml,docs
1352/// related:
1353///   - OCT2BIN
1354///   - BIN2DEC
1355///   - DEC2OCT
1356/// faq:
1357///   - q: "How are signed 10-bit binaries handled by `BIN2OCT`?"
1358///     a: "They are first decoded as signed decimal and then re-encoded to octal with two's-complement output for negatives."
1359/// ```
1360#[derive(Debug)]
1361pub struct Bin2OctFn;
1362/// [formualizer-docgen:schema:start]
1363/// Name: BIN2OCT
1364/// Type: Bin2OctFn
1365/// Min args: 1
1366/// Max args: variadic
1367/// Variadic: true
1368/// Signature: BIN2OCT(arg1: number@scalar, arg2...: number@scalar)
1369/// 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}
1370/// Caps: PURE
1371/// [formualizer-docgen:schema:end]
1372impl Function for Bin2OctFn {
1373    func_caps!(PURE);
1374    fn name(&self) -> &'static str {
1375        "BIN2OCT"
1376    }
1377    fn min_args(&self) -> usize {
1378        1
1379    }
1380    fn variadic(&self) -> bool {
1381        true
1382    }
1383    fn arg_schema(&self) -> &'static [ArgSchema] {
1384        &ARG_NUM_LENIENT_TWO[..]
1385    }
1386    fn eval<'a, 'b, 'c>(
1387        &self,
1388        args: &'c [ArgumentHandle<'a, 'b>],
1389        _ctx: &dyn FunctionContext<'b>,
1390    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1391        let text = match args[0].value()?.into_literal() {
1392            LiteralValue::Error(e) => {
1393                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1394            }
1395            other => match coerce_base_text(&other) {
1396                Ok(s) => s,
1397                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1398            },
1399        };
1400
1401        if text.len() > 10 || !text.chars().all(|c| c == '0' || c == '1') {
1402            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1403                ExcelError::new_num(),
1404            )));
1405        }
1406
1407        let dec = if text.len() == 10 && text.starts_with('1') {
1408            let val = i64::from_str_radix(&text, 2).unwrap_or(0);
1409            val - 1024
1410        } else {
1411            i64::from_str_radix(&text, 2).unwrap_or(0)
1412        };
1413
1414        let places = if args.len() > 1 {
1415            match args[1].value()?.into_literal() {
1416                LiteralValue::Error(e) => {
1417                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1418                }
1419                other => Some(coerce_num(&other)? as usize),
1420            }
1421        } else {
1422            None
1423        };
1424
1425        let octal = if dec >= 0 {
1426            format!("{:o}", dec)
1427        } else {
1428            format!("{:010o}", (dec + (1i64 << 30)) as u64)
1429        };
1430
1431        let result = if let Some(p) = places {
1432            if p < octal.len() || p > 10 {
1433                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1434                    ExcelError::new_num(),
1435                )));
1436            }
1437            format!("{:0>width$}", octal, width = p)
1438        } else {
1439            octal
1440        };
1441
1442        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(result)))
1443    }
1444}
1445
1446/// Converts an octal text value to binary text.
1447///
1448/// Supports optional left-padding through the `places` argument.
1449///
1450/// # Remarks
1451/// - Input must be octal text up to 10 digits and may be signed two's-complement.
1452/// - Converted values must fall in `[-512, 511]`, or the function returns `#NUM!`.
1453/// - `places` must be at least the output width and at most `10`, or `#NUM!` is returned.
1454///
1455/// # Examples
1456/// ```yaml,sandbox
1457/// title: "Convert octal to binary"
1458/// formula: "=OCT2BIN(\"77\")"
1459/// expected: "111111"
1460/// ```
1461///
1462/// ```yaml,sandbox
1463/// title: "Convert signed octal"
1464/// formula: "=OCT2BIN(\"7777777777\")"
1465/// expected: "1111111111"
1466/// ```
1467/// ```yaml,docs
1468/// related:
1469///   - BIN2OCT
1470///   - OCT2DEC
1471///   - DEC2BIN
1472/// faq:
1473///   - q: "Why does `OCT2BIN` return `#NUM!` for some octal inputs?"
1474///     a: "After decoding, the value must be within `[-512, 511]` to be representable in Excel-style binary output."
1475/// ```
1476#[derive(Debug)]
1477pub struct Oct2BinFn;
1478/// [formualizer-docgen:schema:start]
1479/// Name: OCT2BIN
1480/// Type: Oct2BinFn
1481/// Min args: 1
1482/// Max args: variadic
1483/// Variadic: true
1484/// Signature: OCT2BIN(arg1: number@scalar, arg2...: number@scalar)
1485/// 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}
1486/// Caps: PURE
1487/// [formualizer-docgen:schema:end]
1488impl Function for Oct2BinFn {
1489    func_caps!(PURE);
1490    fn name(&self) -> &'static str {
1491        "OCT2BIN"
1492    }
1493    fn min_args(&self) -> usize {
1494        1
1495    }
1496    fn variadic(&self) -> bool {
1497        true
1498    }
1499    fn arg_schema(&self) -> &'static [ArgSchema] {
1500        &ARG_NUM_LENIENT_TWO[..]
1501    }
1502    fn eval<'a, 'b, 'c>(
1503        &self,
1504        args: &'c [ArgumentHandle<'a, 'b>],
1505        _ctx: &dyn FunctionContext<'b>,
1506    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1507        let text = match args[0].value()?.into_literal() {
1508            LiteralValue::Error(e) => {
1509                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1510            }
1511            other => match coerce_base_text(&other) {
1512                Ok(s) => s,
1513                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1514            },
1515        };
1516
1517        if text.len() > 10 || !text.chars().all(|c| ('0'..='7').contains(&c)) {
1518            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1519                ExcelError::new_num(),
1520            )));
1521        }
1522
1523        let dec = if text.len() == 10 && text.starts_with(|c| c >= '4') {
1524            let val = i64::from_str_radix(&text, 8).unwrap_or(0);
1525            val - (1i64 << 30)
1526        } else {
1527            i64::from_str_radix(&text, 8).unwrap_or(0)
1528        };
1529
1530        // Check range for binary output (-512 to 511)
1531        if !(-512..=511).contains(&dec) {
1532            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1533                ExcelError::new_num(),
1534            )));
1535        }
1536
1537        let places = if args.len() > 1 {
1538            match args[1].value()?.into_literal() {
1539                LiteralValue::Error(e) => {
1540                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1541                }
1542                other => Some(coerce_num(&other)? as usize),
1543            }
1544        } else {
1545            None
1546        };
1547
1548        let binary = if dec >= 0 {
1549            format!("{:b}", dec)
1550        } else {
1551            format!("{:010b}", (dec + 1024) as u64)
1552        };
1553
1554        let result = if let Some(p) = places {
1555            if p < binary.len() || p > 10 {
1556                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1557                    ExcelError::new_num(),
1558                )));
1559            }
1560            format!("{:0>width$}", binary, width = p)
1561        } else {
1562            binary
1563        };
1564
1565        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(result)))
1566    }
1567}
1568
1569/// Converts a hexadecimal text value to octal text.
1570///
1571/// Supports optional left-padding through the `places` argument.
1572///
1573/// # Remarks
1574/// - Input must be hexadecimal text up to 10 characters and may be signed two's-complement.
1575/// - Converted values must fit the octal range `[-2^29, 2^29 - 1]`, or `#NUM!` is returned.
1576/// - `places` must be at least the output width and at most `10`, or `#NUM!` is returned.
1577///
1578/// # Examples
1579/// ```yaml,sandbox
1580/// title: "Convert hex to octal"
1581/// formula: "=HEX2OCT(\"1F\")"
1582/// expected: "37"
1583/// ```
1584///
1585/// ```yaml,sandbox
1586/// title: "Convert signed hex"
1587/// formula: "=HEX2OCT(\"FFFFFFFFFF\")"
1588/// expected: "7777777777"
1589/// ```
1590/// ```yaml,docs
1591/// related:
1592///   - OCT2HEX
1593///   - HEX2DEC
1594///   - DEC2OCT
1595/// faq:
1596///   - q: "What causes `HEX2OCT` to return `#NUM!`?"
1597///     a: "The decoded value must fit octal output range `[-2^29, 2^29 - 1]`, and optional `places` must be valid."
1598/// ```
1599#[derive(Debug)]
1600pub struct Hex2OctFn;
1601/// [formualizer-docgen:schema:start]
1602/// Name: HEX2OCT
1603/// Type: Hex2OctFn
1604/// Min args: 1
1605/// Max args: variadic
1606/// Variadic: true
1607/// Signature: HEX2OCT(arg1: any@scalar, arg2...: any@scalar)
1608/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}; arg2{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
1609/// Caps: PURE
1610/// [formualizer-docgen:schema:end]
1611impl Function for Hex2OctFn {
1612    func_caps!(PURE);
1613    fn name(&self) -> &'static str {
1614        "HEX2OCT"
1615    }
1616    fn min_args(&self) -> usize {
1617        1
1618    }
1619    fn variadic(&self) -> bool {
1620        true
1621    }
1622    fn arg_schema(&self) -> &'static [ArgSchema] {
1623        &ARG_ANY_TWO[..]
1624    }
1625    fn eval<'a, 'b, 'c>(
1626        &self,
1627        args: &'c [ArgumentHandle<'a, 'b>],
1628        _ctx: &dyn FunctionContext<'b>,
1629    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1630        let text = match args[0].value()?.into_literal() {
1631            LiteralValue::Error(e) => {
1632                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1633            }
1634            other => match coerce_base_text(&other) {
1635                Ok(s) => s.to_uppercase(),
1636                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1637            },
1638        };
1639
1640        if text.len() > 10 || !text.chars().all(|c| c.is_ascii_hexdigit()) {
1641            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1642                ExcelError::new_num(),
1643            )));
1644        }
1645
1646        let dec = if text.len() == 10 && text.starts_with(|c| c >= '8') {
1647            let val = i64::from_str_radix(&text, 16).unwrap_or(0);
1648            val - (1i64 << 40)
1649        } else {
1650            i64::from_str_radix(&text, 16).unwrap_or(0)
1651        };
1652
1653        // Check range for octal output
1654        if !(-(1i64 << 29)..=(1i64 << 29) - 1).contains(&dec) {
1655            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1656                ExcelError::new_num(),
1657            )));
1658        }
1659
1660        let places = if args.len() > 1 {
1661            match args[1].value()?.into_literal() {
1662                LiteralValue::Error(e) => {
1663                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1664                }
1665                other => Some(coerce_num(&other)? as usize),
1666            }
1667        } else {
1668            None
1669        };
1670
1671        let octal = if dec >= 0 {
1672            format!("{:o}", dec)
1673        } else {
1674            format!("{:010o}", (dec + (1i64 << 30)) as u64)
1675        };
1676
1677        let result = if let Some(p) = places {
1678            if p < octal.len() || p > 10 {
1679                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1680                    ExcelError::new_num(),
1681                )));
1682            }
1683            format!("{:0>width$}", octal, width = p)
1684        } else {
1685            octal
1686        };
1687
1688        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(result)))
1689    }
1690}
1691
1692/// Converts an octal text value to hexadecimal text.
1693///
1694/// Supports optional left-padding through the `places` argument.
1695///
1696/// # Remarks
1697/// - Input must be octal text up to 10 digits and may be signed two's-complement.
1698/// - Signed values are converted through their decimal representation.
1699/// - `places` must be at least the output width and at most `10`, or `#NUM!` is returned.
1700///
1701/// # Examples
1702/// ```yaml,sandbox
1703/// title: "Convert octal to hex"
1704/// formula: "=OCT2HEX(\"77\")"
1705/// expected: "3F"
1706/// ```
1707///
1708/// ```yaml,sandbox
1709/// title: "Convert signed octal"
1710/// formula: "=OCT2HEX(\"7777777777\")"
1711/// expected: "FFFFFFFFFF"
1712/// ```
1713/// ```yaml,docs
1714/// related:
1715///   - HEX2OCT
1716///   - OCT2DEC
1717///   - DEC2HEX
1718/// faq:
1719///   - q: "How does `OCT2HEX` treat signed octal input?"
1720///     a: "Signed 10-digit octal is decoded via two's-complement and then emitted as hex, preserving signed meaning."
1721/// ```
1722#[derive(Debug)]
1723pub struct Oct2HexFn;
1724/// [formualizer-docgen:schema:start]
1725/// Name: OCT2HEX
1726/// Type: Oct2HexFn
1727/// Min args: 1
1728/// Max args: variadic
1729/// Variadic: true
1730/// Signature: OCT2HEX(arg1: number@scalar, arg2...: number@scalar)
1731/// 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}
1732/// Caps: PURE
1733/// [formualizer-docgen:schema:end]
1734impl Function for Oct2HexFn {
1735    func_caps!(PURE);
1736    fn name(&self) -> &'static str {
1737        "OCT2HEX"
1738    }
1739    fn min_args(&self) -> usize {
1740        1
1741    }
1742    fn variadic(&self) -> bool {
1743        true
1744    }
1745    fn arg_schema(&self) -> &'static [ArgSchema] {
1746        &ARG_NUM_LENIENT_TWO[..]
1747    }
1748    fn eval<'a, 'b, 'c>(
1749        &self,
1750        args: &'c [ArgumentHandle<'a, 'b>],
1751        _ctx: &dyn FunctionContext<'b>,
1752    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1753        let text = match args[0].value()?.into_literal() {
1754            LiteralValue::Error(e) => {
1755                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1756            }
1757            other => match coerce_base_text(&other) {
1758                Ok(s) => s,
1759                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1760            },
1761        };
1762
1763        if text.len() > 10 || !text.chars().all(|c| ('0'..='7').contains(&c)) {
1764            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1765                ExcelError::new_num(),
1766            )));
1767        }
1768
1769        let dec = if text.len() == 10 && text.starts_with(|c| c >= '4') {
1770            let val = i64::from_str_radix(&text, 8).unwrap_or(0);
1771            val - (1i64 << 30)
1772        } else {
1773            i64::from_str_radix(&text, 8).unwrap_or(0)
1774        };
1775
1776        let places = if args.len() > 1 {
1777            match args[1].value()?.into_literal() {
1778                LiteralValue::Error(e) => {
1779                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1780                }
1781                other => Some(coerce_num(&other)? as usize),
1782            }
1783        } else {
1784            None
1785        };
1786
1787        let hex = if dec >= 0 {
1788            format!("{:X}", dec)
1789        } else {
1790            format!("{:010X}", (dec + (1i64 << 40)) as u64)
1791        };
1792
1793        let result = if let Some(p) = places {
1794            if p < hex.len() || p > 10 {
1795                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1796                    ExcelError::new_num(),
1797                )));
1798            }
1799            format!("{:0>width$}", hex, width = p)
1800        } else {
1801            hex
1802        };
1803
1804        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(result)))
1805    }
1806}
1807
1808/* ─────────────────────────── Engineering Comparison Functions ──────────────────────────── */
1809
1810/// Tests whether two numbers are equal.
1811///
1812/// Returns `1` when values match and `0` otherwise.
1813///
1814/// # Remarks
1815/// - If `number2` is omitted, it defaults to `0`.
1816/// - Inputs are numerically coerced.
1817/// - Uses a small numeric tolerance for floating-point comparison.
1818///
1819/// # Examples
1820/// ```yaml,sandbox
1821/// title: "Equal values"
1822/// formula: "=DELTA(5,5)"
1823/// expected: 1
1824/// ```
1825///
1826/// ```yaml,sandbox
1827/// title: "Default second argument"
1828/// formula: "=DELTA(2.5)"
1829/// expected: 0
1830/// ```
1831/// ```yaml,docs
1832/// related:
1833///   - GESTEP
1834/// faq:
1835///   - q: "Does `DELTA` require exact floating-point equality?"
1836///     a: "It uses a small tolerance (`1e-12`), so values that differ only by tiny floating noise compare as equal."
1837/// ```
1838#[derive(Debug)]
1839pub struct DeltaFn;
1840/// [formualizer-docgen:schema:start]
1841/// Name: DELTA
1842/// Type: DeltaFn
1843/// Min args: 1
1844/// Max args: variadic
1845/// Variadic: true
1846/// Signature: DELTA(arg1: number@scalar, arg2...: number@scalar)
1847/// 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}
1848/// Caps: PURE
1849/// [formualizer-docgen:schema:end]
1850impl Function for DeltaFn {
1851    func_caps!(PURE);
1852    fn name(&self) -> &'static str {
1853        "DELTA"
1854    }
1855    fn min_args(&self) -> usize {
1856        1
1857    }
1858    fn variadic(&self) -> bool {
1859        true
1860    }
1861    fn arg_schema(&self) -> &'static [ArgSchema] {
1862        &ARG_NUM_LENIENT_TWO[..]
1863    }
1864    fn eval<'a, 'b, 'c>(
1865        &self,
1866        args: &'c [ArgumentHandle<'a, 'b>],
1867        _ctx: &dyn FunctionContext<'b>,
1868    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1869        let n1 = match args[0].value()?.into_literal() {
1870            LiteralValue::Error(e) => {
1871                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1872            }
1873            other => coerce_num(&other)?,
1874        };
1875        let n2 = if args.len() > 1 {
1876            match args[1].value()?.into_literal() {
1877                LiteralValue::Error(e) => {
1878                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1879                }
1880                other => coerce_num(&other)?,
1881            }
1882        } else {
1883            0.0
1884        };
1885
1886        let result = if (n1 - n2).abs() < 1e-12 { 1.0 } else { 0.0 };
1887        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1888            result,
1889        )))
1890    }
1891}
1892
1893/// Returns `1` when a number is greater than or equal to a step value.
1894///
1895/// Returns `0` when the number is below the step.
1896///
1897/// # Remarks
1898/// - If `step` is omitted, it defaults to `0`.
1899/// - Inputs are numerically coerced.
1900/// - Propagates input errors.
1901///
1902/// # Examples
1903/// ```yaml,sandbox
1904/// title: "Value meets threshold"
1905/// formula: "=GESTEP(5,3)"
1906/// expected: 1
1907/// ```
1908///
1909/// ```yaml,sandbox
1910/// title: "Default threshold of zero"
1911/// formula: "=GESTEP(-2)"
1912/// expected: 0
1913/// ```
1914/// ```yaml,docs
1915/// related:
1916///   - DELTA
1917/// faq:
1918///   - q: "What default threshold does `GESTEP` use?"
1919///     a: "If omitted, `step` defaults to `0`, so the function returns `1` for non-negative inputs."
1920/// ```
1921#[derive(Debug)]
1922pub struct GestepFn;
1923/// [formualizer-docgen:schema:start]
1924/// Name: GESTEP
1925/// Type: GestepFn
1926/// Min args: 1
1927/// Max args: variadic
1928/// Variadic: true
1929/// Signature: GESTEP(arg1: number@scalar, arg2...: number@scalar)
1930/// 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}
1931/// Caps: PURE
1932/// [formualizer-docgen:schema:end]
1933impl Function for GestepFn {
1934    func_caps!(PURE);
1935    fn name(&self) -> &'static str {
1936        "GESTEP"
1937    }
1938    fn min_args(&self) -> usize {
1939        1
1940    }
1941    fn variadic(&self) -> bool {
1942        true
1943    }
1944    fn arg_schema(&self) -> &'static [ArgSchema] {
1945        &ARG_NUM_LENIENT_TWO[..]
1946    }
1947    fn eval<'a, 'b, 'c>(
1948        &self,
1949        args: &'c [ArgumentHandle<'a, 'b>],
1950        _ctx: &dyn FunctionContext<'b>,
1951    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1952        let n = match args[0].value()?.into_literal() {
1953            LiteralValue::Error(e) => {
1954                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1955            }
1956            other => coerce_num(&other)?,
1957        };
1958        let step = if args.len() > 1 {
1959            match args[1].value()?.into_literal() {
1960                LiteralValue::Error(e) => {
1961                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
1962                }
1963                other => coerce_num(&other)?,
1964            }
1965        } else {
1966            0.0
1967        };
1968
1969        let result = if n >= step { 1.0 } else { 0.0 };
1970        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
1971            result,
1972        )))
1973    }
1974}
1975
1976/* ─────────────────────────── Error Function ──────────────────────────── */
1977
1978/// Approximation of the error function erf(x)
1979/// Uses the approximation: erf(x) = 1 - (a1*t + a2*t^2 + a3*t^3 + a4*t^4 + a5*t^5) * exp(-x^2)
1980/// High-precision error function using Cody's rational approximation
1981/// Achieves precision of about 1e-15 (double precision)
1982#[allow(clippy::excessive_precision)]
1983fn erf_approx(x: f64) -> f64 {
1984    let ax = x.abs();
1985
1986    // For small x, use series expansion
1987    if ax < 0.5 {
1988        // Coefficients for erf(x) = x * P(x^2) / Q(x^2)
1989        const P: [f64; 5] = [
1990            3.20937758913846947e+03,
1991            3.77485237685302021e+02,
1992            1.13864154151050156e+02,
1993            3.16112374387056560e+00,
1994            1.85777706184603153e-01,
1995        ];
1996        const Q: [f64; 5] = [
1997            2.84423748127893300e+03,
1998            1.28261652607737228e+03,
1999            2.44024637934444173e+02,
2000            2.36012909523441209e+01,
2001            1.00000000000000000e+00,
2002        ];
2003
2004        let x2 = x * x;
2005        let p_val = P[4];
2006        let p_val = p_val * x2 + P[3];
2007        let p_val = p_val * x2 + P[2];
2008        let p_val = p_val * x2 + P[1];
2009        let p_val = p_val * x2 + P[0];
2010
2011        let q_val = Q[4];
2012        let q_val = q_val * x2 + Q[3];
2013        let q_val = q_val * x2 + Q[2];
2014        let q_val = q_val * x2 + Q[1];
2015        let q_val = q_val * x2 + Q[0];
2016
2017        return x * p_val / q_val;
2018    }
2019
2020    // For x in [0.5, 4], use erfc approximation and compute erf = 1 - erfc
2021    if ax < 4.0 {
2022        let erfc_val = erfc_mid(ax);
2023        return if x > 0.0 {
2024            1.0 - erfc_val
2025        } else {
2026            erfc_val - 1.0
2027        };
2028    }
2029
2030    // For large x, erf(x) ≈ ±1
2031    let erfc_val = erfc_large(ax);
2032    if x > 0.0 {
2033        1.0 - erfc_val
2034    } else {
2035        erfc_val - 1.0
2036    }
2037}
2038
2039/// erfc for x in [0.5, 4]
2040#[allow(clippy::excessive_precision)]
2041fn erfc_mid(x: f64) -> f64 {
2042    const P: [f64; 9] = [
2043        1.23033935479799725e+03,
2044        2.05107837782607147e+03,
2045        1.71204761263407058e+03,
2046        8.81952221241769090e+02,
2047        2.98635138197400131e+02,
2048        6.61191906371416295e+01,
2049        8.88314979438837594e+00,
2050        5.64188496988670089e-01,
2051        2.15311535474403846e-08,
2052    ];
2053    const Q: [f64; 9] = [
2054        1.23033935480374942e+03,
2055        3.43936767414372164e+03,
2056        4.36261909014324716e+03,
2057        3.29079923573345963e+03,
2058        1.62138957456669019e+03,
2059        5.37181101862009858e+02,
2060        1.17693950891312499e+02,
2061        1.57449261107098347e+01,
2062        1.00000000000000000e+00,
2063    ];
2064
2065    let p_val = P[8];
2066    let p_val = p_val * x + P[7];
2067    let p_val = p_val * x + P[6];
2068    let p_val = p_val * x + P[5];
2069    let p_val = p_val * x + P[4];
2070    let p_val = p_val * x + P[3];
2071    let p_val = p_val * x + P[2];
2072    let p_val = p_val * x + P[1];
2073    let p_val = p_val * x + P[0];
2074
2075    let q_val = Q[8];
2076    let q_val = q_val * x + Q[7];
2077    let q_val = q_val * x + Q[6];
2078    let q_val = q_val * x + Q[5];
2079    let q_val = q_val * x + Q[4];
2080    let q_val = q_val * x + Q[3];
2081    let q_val = q_val * x + Q[2];
2082    let q_val = q_val * x + Q[1];
2083    let q_val = q_val * x + Q[0];
2084
2085    (-x * x).exp() * p_val / q_val
2086}
2087
2088/// erfc for x >= 4
2089#[allow(clippy::excessive_precision)]
2090fn erfc_large(x: f64) -> f64 {
2091    const P: [f64; 6] = [
2092        6.58749161529837803e-04,
2093        1.60837851487422766e-02,
2094        1.25781726111229246e-01,
2095        3.60344899949804439e-01,
2096        3.05326634961232344e-01,
2097        1.63153871373020978e-02,
2098    ];
2099    const Q: [f64; 6] = [
2100        2.33520497626869185e-03,
2101        6.05183413124413191e-02,
2102        5.27905102951428412e-01,
2103        1.87295284992346047e+00,
2104        2.56852019228982242e+00,
2105        1.00000000000000000e+00,
2106    ];
2107
2108    let x2 = x * x;
2109    let inv_x2 = 1.0 / x2;
2110
2111    let p_val = P[5];
2112    let p_val = p_val * inv_x2 + P[4];
2113    let p_val = p_val * inv_x2 + P[3];
2114    let p_val = p_val * inv_x2 + P[2];
2115    let p_val = p_val * inv_x2 + P[1];
2116    let p_val = p_val * inv_x2 + P[0];
2117
2118    let q_val = Q[5];
2119    let q_val = q_val * inv_x2 + Q[4];
2120    let q_val = q_val * inv_x2 + Q[3];
2121    let q_val = q_val * inv_x2 + Q[2];
2122    let q_val = q_val * inv_x2 + Q[1];
2123    let q_val = q_val * inv_x2 + Q[0];
2124
2125    // 1/sqrt(pi) = 0.5641895835477563
2126    const FRAC_1_SQRT_PI: f64 = 0.5641895835477563;
2127    (-x2).exp() / x * (FRAC_1_SQRT_PI + inv_x2 * p_val / q_val)
2128}
2129
2130/// Direct erfc computation for ERFC function
2131fn erfc_direct(x: f64) -> f64 {
2132    if x < 0.0 {
2133        return 2.0 - erfc_direct(-x);
2134    }
2135    if x < 0.5 {
2136        return 1.0 - erf_approx(x);
2137    }
2138    if x < 4.0 {
2139        return erfc_mid(x);
2140    }
2141    erfc_large(x)
2142}
2143
2144/// Returns the Gaussian error function over one bound or between two bounds.
2145///
2146/// With one argument it returns `erf(x)`; with two it returns `erf(upper) - erf(lower)`.
2147///
2148/// # Remarks
2149/// - Inputs are numerically coerced.
2150/// - A second argument switches the function to interval mode.
2151/// - Results are approximate floating-point values.
2152///
2153/// # Examples
2154/// ```yaml,sandbox
2155/// title: "Single-bound ERF"
2156/// formula: "=ERF(1)"
2157/// expected: 0.8427007929497149
2158/// ```
2159///
2160/// ```yaml,sandbox
2161/// title: "Interval ERF"
2162/// formula: "=ERF(0,1)"
2163/// expected: 0.8427007929497149
2164/// ```
2165/// ```yaml,docs
2166/// related:
2167///   - ERFC
2168///   - ERF.PRECISE
2169/// faq:
2170///   - q: "How does two-argument `ERF` work?"
2171///     a: "`ERF(lower, upper)` returns `erf(upper) - erf(lower)`, i.e., an interval difference rather than a single-bound value."
2172/// ```
2173#[derive(Debug)]
2174pub struct ErfFn;
2175/// [formualizer-docgen:schema:start]
2176/// Name: ERF
2177/// Type: ErfFn
2178/// Min args: 1
2179/// Max args: variadic
2180/// Variadic: true
2181/// Signature: ERF(arg1: number@scalar, arg2...: number@scalar)
2182/// 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}
2183/// Caps: PURE
2184/// [formualizer-docgen:schema:end]
2185impl Function for ErfFn {
2186    func_caps!(PURE);
2187    fn name(&self) -> &'static str {
2188        "ERF"
2189    }
2190    fn min_args(&self) -> usize {
2191        1
2192    }
2193    fn variadic(&self) -> bool {
2194        true
2195    }
2196    fn arg_schema(&self) -> &'static [ArgSchema] {
2197        &ARG_NUM_LENIENT_TWO[..]
2198    }
2199    fn eval<'a, 'b, 'c>(
2200        &self,
2201        args: &'c [ArgumentHandle<'a, 'b>],
2202        _ctx: &dyn FunctionContext<'b>,
2203    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2204        let lower = match args[0].value()?.into_literal() {
2205            LiteralValue::Error(e) => {
2206                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2207            }
2208            other => coerce_num(&other)?,
2209        };
2210
2211        let result = if args.len() > 1 {
2212            // ERF(lower, upper) = erf(upper) - erf(lower)
2213            let upper = match args[1].value()?.into_literal() {
2214                LiteralValue::Error(e) => {
2215                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2216                }
2217                other => coerce_num(&other)?,
2218            };
2219            erf_approx(upper) - erf_approx(lower)
2220        } else {
2221            // ERF(x) = erf(x)
2222            erf_approx(lower)
2223        };
2224
2225        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2226            result,
2227        )))
2228    }
2229}
2230
2231/// Returns the complementary error function of a number.
2232///
2233/// `ERFC(x)` is equivalent to `1 - ERF(x)`.
2234///
2235/// # Remarks
2236/// - Input is numerically coerced.
2237/// - Results are approximate floating-point values.
2238/// - Propagates input errors.
2239///
2240/// # Examples
2241/// ```yaml,sandbox
2242/// title: "Complement at one"
2243/// formula: "=ERFC(1)"
2244/// expected: 0.1572992070502851
2245/// ```
2246///
2247/// ```yaml,sandbox
2248/// title: "Complement at zero"
2249/// formula: "=ERFC(0)"
2250/// expected: 1
2251/// ```
2252/// ```yaml,docs
2253/// related:
2254///   - ERF
2255///   - ERF.PRECISE
2256/// faq:
2257///   - q: "Is `ERFC(x)` equivalent to `1-ERF(x)` here?"
2258///     a: "Yes. It computes the complementary error function and matches `1 - erf(x)` behavior."
2259/// ```
2260#[derive(Debug)]
2261pub struct ErfcFn;
2262/// [formualizer-docgen:schema:start]
2263/// Name: ERFC
2264/// Type: ErfcFn
2265/// Min args: 1
2266/// Max args: 1
2267/// Variadic: false
2268/// Signature: ERFC(arg1: any@scalar)
2269/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
2270/// Caps: PURE
2271/// [formualizer-docgen:schema:end]
2272impl Function for ErfcFn {
2273    func_caps!(PURE);
2274    fn name(&self) -> &'static str {
2275        "ERFC"
2276    }
2277    fn min_args(&self) -> usize {
2278        1
2279    }
2280    fn arg_schema(&self) -> &'static [ArgSchema] {
2281        &ARG_ANY_ONE[..]
2282    }
2283    fn eval<'a, 'b, 'c>(
2284        &self,
2285        args: &'c [ArgumentHandle<'a, 'b>],
2286        _ctx: &dyn FunctionContext<'b>,
2287    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2288        let x = match args[0].value()?.into_literal() {
2289            LiteralValue::Error(e) => {
2290                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2291            }
2292            other => coerce_num(&other)?,
2293        };
2294
2295        let result = erfc_direct(x);
2296        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2297            result,
2298        )))
2299    }
2300}
2301
2302/// Returns the complementary error function of a number.
2303///
2304/// Computes `1 - ERF(x)` using the same one-argument precise behavior as Excel
2305/// `ERFC.PRECISE`.
2306///
2307/// # Remarks
2308/// - Accepts one numeric argument.
2309/// - Numeric text is coerced using standard function coercion.
2310/// - Results are approximate floating-point values.
2311///
2312/// ```yaml,sandbox
2313/// title: "Complement at one"
2314/// formula: "=ERFC.PRECISE(1)"
2315/// expected: 0.1572992070502851
2316/// ```
2317///
2318/// ```yaml,sandbox
2319/// title: "Complement at zero"
2320/// formula: "=ERFC.PRECISE(0)"
2321/// expected: 1
2322/// ```
2323///
2324/// ```yaml,docs
2325/// related:
2326///   - ERFC
2327///   - ERF.PRECISE
2328///   - ERF
2329/// faq:
2330///   - q: "How is ERFC.PRECISE different from ERFC?"
2331///     a: "It exposes the precise one-argument complement form; numerically it matches ERFC(x) in this implementation."
2332/// ```
2333#[derive(Debug)]
2334pub struct ErfcPreciseFn;
2335/// [formualizer-docgen:schema:start]
2336/// Name: ERFC.PRECISE
2337/// Type: ErfcPreciseFn
2338/// Min args: 1
2339/// Max args: 1
2340/// Variadic: false
2341/// Signature: ERFC.PRECISE(arg1: any@scalar)
2342/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
2343/// Caps: PURE
2344/// [formualizer-docgen:schema:end]
2345impl Function for ErfcPreciseFn {
2346    func_caps!(PURE);
2347    fn name(&self) -> &'static str {
2348        "ERFC.PRECISE"
2349    }
2350    fn min_args(&self) -> usize {
2351        1
2352    }
2353    fn arg_schema(&self) -> &'static [ArgSchema] {
2354        &ARG_ANY_ONE[..]
2355    }
2356    fn eval<'a, 'b, 'c>(
2357        &self,
2358        args: &'c [ArgumentHandle<'a, 'b>],
2359        _ctx: &dyn FunctionContext<'b>,
2360    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2361        if args.len() != 1 {
2362            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2363                ExcelError::new_value(),
2364            )));
2365        }
2366        let x = match args[0].value()?.into_literal() {
2367            LiteralValue::Error(e) => {
2368                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2369            }
2370            other => coerce_num(&other)?,
2371        };
2372        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2373            erfc_direct(x),
2374        )))
2375    }
2376}
2377
2378fn eval_bessel<'b, F>(
2379    args: &[ArgumentHandle<'_, 'b>],
2380    f: F,
2381) -> Result<crate::traits::CalcValue<'b>, ExcelError>
2382where
2383    F: FnOnce(i32, f64) -> f64,
2384{
2385    if args.len() != 2 {
2386        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2387            ExcelError::new_value(),
2388        )));
2389    }
2390    let x = match args[0].value()?.into_literal() {
2391        LiteralValue::Error(e) => {
2392            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2393        }
2394        other => coerce_num(&other)?,
2395    };
2396    let n = match args[1].value()?.into_literal() {
2397        LiteralValue::Error(e) => {
2398            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2399        }
2400        other => coerce_num(&other)?,
2401    };
2402
2403    if !x.is_finite() || !n.is_finite() {
2404        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2405            ExcelError::new_num(),
2406        )));
2407    }
2408
2409    let n_trunc = n.trunc();
2410    if n_trunc < 0.0 || n_trunc > i32::MAX as f64 {
2411        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2412            ExcelError::new_num(),
2413        )));
2414    }
2415
2416    let result = f(n_trunc as i32, x);
2417    if !result.is_finite() {
2418        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2419            ExcelError::new_num(),
2420        )));
2421    }
2422
2423    Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2424        result,
2425    )))
2426}
2427
2428/// Returns the modified Bessel function In(x).
2429///
2430/// Computes the modified Bessel function of the first kind for a real value `x`
2431/// and a non-negative integer order `n`. The order is truncated toward zero,
2432/// matching spreadsheet behavior.
2433///
2434/// # Remarks
2435/// - Arguments are supplied as `BESSELI(x, n)`.
2436/// - `n` must truncate to a non-negative integer order.
2437/// - Invalid domains or non-finite results return `#NUM!`.
2438///
2439/// ```yaml,sandbox
2440/// title: "First-order modified Bessel I"
2441/// formula: "=BESSELI(0.5,1)"
2442/// expected: 0.2578943053908963
2443/// ```
2444///
2445/// ```yaml,sandbox
2446/// title: "Order is truncated"
2447/// formula: "=BESSELI(0.5,1.9)"
2448/// expected: 0.2578943053908963
2449/// ```
2450///
2451/// ```yaml,docs
2452/// related:
2453///   - BESSELJ
2454///   - BESSELK
2455///   - BESSELY
2456/// faq:
2457///   - q: "What happens to fractional order values?"
2458///     a: "The order argument is truncated toward zero before evaluation."
2459/// ```
2460#[derive(Debug)]
2461pub struct BesselIFn;
2462/// [formualizer-docgen:schema:start]
2463/// Name: BESSELI
2464/// Type: BesselIFn
2465/// Min args: 2
2466/// Max args: 2
2467/// Variadic: false
2468/// Signature: BESSELI(arg1: any@scalar, arg2: any@scalar)
2469/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}; arg2{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
2470/// Caps: PURE
2471/// [formualizer-docgen:schema:end]
2472impl Function for BesselIFn {
2473    func_caps!(PURE);
2474    fn name(&self) -> &'static str {
2475        "BESSELI"
2476    }
2477    fn min_args(&self) -> usize {
2478        2
2479    }
2480    fn arg_schema(&self) -> &'static [ArgSchema] {
2481        &ARG_ANY_TWO[..]
2482    }
2483    fn eval<'a, 'b, 'c>(
2484        &self,
2485        args: &'c [ArgumentHandle<'a, 'b>],
2486        _ctx: &dyn FunctionContext<'b>,
2487    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2488        eval_bessel(args, transcendental::bessel_i)
2489    }
2490}
2491
2492/// Returns the Bessel function Jn(x).
2493///
2494/// Computes the Bessel function of the first kind for a real value `x` and a
2495/// non-negative integer order `n`. The order is truncated toward zero.
2496///
2497/// # Remarks
2498/// - Arguments are supplied as `BESSELJ(x, n)`.
2499/// - Negative orders return `#NUM!` in the public spreadsheet function.
2500/// - Results are approximate floating-point values.
2501///
2502/// ```yaml,sandbox
2503/// title: "Third-order Bessel J"
2504/// formula: "=BESSELJ(0.5,3)"
2505/// expected: 0.002563729994587244
2506/// ```
2507///
2508/// ```yaml,sandbox
2509/// title: "Zero input for positive order"
2510/// formula: "=BESSELJ(0,7)"
2511/// expected: 0
2512/// ```
2513///
2514/// ```yaml,docs
2515/// related:
2516///   - BESSELI
2517///   - BESSELK
2518///   - BESSELY
2519/// faq:
2520///   - q: "Are negative public orders supported?"
2521///     a: "No. Negative order arguments return #NUM! for Excel compatibility."
2522/// ```
2523#[derive(Debug)]
2524pub struct BesselJFn;
2525/// [formualizer-docgen:schema:start]
2526/// Name: BESSELJ
2527/// Type: BesselJFn
2528/// Min args: 2
2529/// Max args: 2
2530/// Variadic: false
2531/// Signature: BESSELJ(arg1: any@scalar, arg2: any@scalar)
2532/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}; arg2{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
2533/// Caps: PURE
2534/// [formualizer-docgen:schema:end]
2535impl Function for BesselJFn {
2536    func_caps!(PURE);
2537    fn name(&self) -> &'static str {
2538        "BESSELJ"
2539    }
2540    fn min_args(&self) -> usize {
2541        2
2542    }
2543    fn arg_schema(&self) -> &'static [ArgSchema] {
2544        &ARG_ANY_TWO[..]
2545    }
2546    fn eval<'a, 'b, 'c>(
2547        &self,
2548        args: &'c [ArgumentHandle<'a, 'b>],
2549        _ctx: &dyn FunctionContext<'b>,
2550    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2551        eval_bessel(args, transcendental::bessel_j)
2552    }
2553}
2554
2555/// Returns the modified Bessel function Kn(x).
2556///
2557/// Computes the modified Bessel function of the second kind for a positive real
2558/// value `x` and a non-negative integer order `n`.
2559///
2560/// # Remarks
2561/// - Arguments are supplied as `BESSELK(x, n)`.
2562/// - `x` must be positive and `n` must truncate to a non-negative integer.
2563/// - Invalid domains or non-finite results return `#NUM!`.
2564///
2565/// ```yaml,sandbox
2566/// title: "First-order modified Bessel K"
2567/// formula: "=BESSELK(0.5,1)"
2568/// expected: 1.656441120003301
2569/// ```
2570///
2571/// ```yaml,sandbox
2572/// title: "Zero-order modified Bessel K"
2573/// formula: "=BESSELK(0.5,0)"
2574/// expected: 0.9244190712276659
2575/// ```
2576///
2577/// ```yaml,docs
2578/// related:
2579///   - BESSELI
2580///   - BESSELJ
2581///   - BESSELY
2582/// faq:
2583///   - q: "Why can BESSELK return #NUM!?"
2584///     a: "BESSELK is undefined for non-positive x values and negative orders."
2585/// ```
2586#[derive(Debug)]
2587pub struct BesselKFn;
2588/// [formualizer-docgen:schema:start]
2589/// Name: BESSELK
2590/// Type: BesselKFn
2591/// Min args: 2
2592/// Max args: 2
2593/// Variadic: false
2594/// Signature: BESSELK(arg1: any@scalar, arg2: any@scalar)
2595/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}; arg2{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
2596/// Caps: PURE
2597/// [formualizer-docgen:schema:end]
2598impl Function for BesselKFn {
2599    func_caps!(PURE);
2600    fn name(&self) -> &'static str {
2601        "BESSELK"
2602    }
2603    fn min_args(&self) -> usize {
2604        2
2605    }
2606    fn arg_schema(&self) -> &'static [ArgSchema] {
2607        &ARG_ANY_TWO[..]
2608    }
2609    fn eval<'a, 'b, 'c>(
2610        &self,
2611        args: &'c [ArgumentHandle<'a, 'b>],
2612        _ctx: &dyn FunctionContext<'b>,
2613    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2614        eval_bessel(args, transcendental::bessel_k)
2615    }
2616}
2617
2618/// Returns the Bessel function Yn(x).
2619///
2620/// Computes the Bessel function of the second kind for a positive real value `x`
2621/// and a non-negative integer order `n`.
2622///
2623/// # Remarks
2624/// - Arguments are supplied as `BESSELY(x, n)`.
2625/// - Negative orders and invalid domains return `#NUM!`.
2626/// - Results are approximate floating-point values.
2627///
2628/// ```yaml,sandbox
2629/// title: "Third-order Bessel Y"
2630/// formula: "=BESSELY(0.5,3)"
2631/// expected: -42.059494304723883
2632/// ```
2633///
2634/// ```yaml,sandbox
2635/// title: "Large-input Bessel Y"
2636/// formula: "=BESSELY(35,3)"
2637/// expected: -0.13191405300596323
2638/// ```
2639///
2640/// ```yaml,docs
2641/// related:
2642///   - BESSELI
2643///   - BESSELJ
2644///   - BESSELK
2645/// faq:
2646///   - q: "Is BESSELY defined at zero?"
2647///     a: "No. Singular or otherwise invalid inputs return #NUM!."
2648/// ```
2649#[derive(Debug)]
2650pub struct BesselYFn;
2651/// [formualizer-docgen:schema:start]
2652/// Name: BESSELY
2653/// Type: BesselYFn
2654/// Min args: 2
2655/// Max args: 2
2656/// Variadic: false
2657/// Signature: BESSELY(arg1: any@scalar, arg2: any@scalar)
2658/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}; arg2{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
2659/// Caps: PURE
2660/// [formualizer-docgen:schema:end]
2661impl Function for BesselYFn {
2662    func_caps!(PURE);
2663    fn name(&self) -> &'static str {
2664        "BESSELY"
2665    }
2666    fn min_args(&self) -> usize {
2667        2
2668    }
2669    fn arg_schema(&self) -> &'static [ArgSchema] {
2670        &ARG_ANY_TWO[..]
2671    }
2672    fn eval<'a, 'b, 'c>(
2673        &self,
2674        args: &'c [ArgumentHandle<'a, 'b>],
2675        _ctx: &dyn FunctionContext<'b>,
2676    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2677        eval_bessel(args, transcendental::bessel_y)
2678    }
2679}
2680
2681/// Returns the error function of a number.
2682///
2683/// This is the one-argument precise variant of `ERF`.
2684///
2685/// # Remarks
2686/// - Input is numerically coerced.
2687/// - Equivalent to `ERF(x)` in single-argument mode.
2688/// - Results are approximate floating-point values.
2689///
2690/// # Examples
2691/// ```yaml,sandbox
2692/// title: "Positive input"
2693/// formula: "=ERF.PRECISE(1)"
2694/// expected: 0.8427007929497149
2695/// ```
2696///
2697/// ```yaml,sandbox
2698/// title: "Negative input"
2699/// formula: "=ERF.PRECISE(-1)"
2700/// expected: -0.8427007929497149
2701/// ```
2702/// ```yaml,docs
2703/// related:
2704///   - ERF
2705///   - ERFC
2706/// faq:
2707///   - q: "How is `ERF.PRECISE` different from `ERF`?"
2708///     a: "`ERF.PRECISE` is the one-argument form only; numerically it matches `ERF(x)` for single input mode."
2709/// ```
2710#[derive(Debug)]
2711pub struct ErfPreciseFn;
2712/// [formualizer-docgen:schema:start]
2713/// Name: ERF.PRECISE
2714/// Type: ErfPreciseFn
2715/// Min args: 1
2716/// Max args: 1
2717/// Variadic: false
2718/// Signature: ERF.PRECISE(arg1: any@scalar)
2719/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
2720/// Caps: PURE
2721/// [formualizer-docgen:schema:end]
2722impl Function for ErfPreciseFn {
2723    func_caps!(PURE);
2724    fn name(&self) -> &'static str {
2725        "ERF.PRECISE"
2726    }
2727    fn min_args(&self) -> usize {
2728        1
2729    }
2730    fn arg_schema(&self) -> &'static [ArgSchema] {
2731        &ARG_ANY_ONE[..]
2732    }
2733    fn eval<'a, 'b, 'c>(
2734        &self,
2735        args: &'c [ArgumentHandle<'a, 'b>],
2736        _ctx: &dyn FunctionContext<'b>,
2737    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2738        let x = match args[0].value()?.into_literal() {
2739            LiteralValue::Error(e) => {
2740                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2741            }
2742            other => coerce_num(&other)?,
2743        };
2744
2745        let result = erf_approx(x);
2746        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
2747            result,
2748        )))
2749    }
2750}
2751
2752/* ─────────────────────────── Complex Number Functions ──────────────────────────── */
2753
2754/// Parse a complex number string like "3+4i", "3-4i", "5i", "3", "-2j", etc.
2755/// Returns (real, imaginary, suffix) where suffix is 'i' or 'j'
2756fn parse_complex(s: &str) -> Result<(f64, f64, char), ExcelError> {
2757    let s = s.trim();
2758    if s.is_empty() {
2759        return Err(ExcelError::new_num());
2760    }
2761
2762    // Determine the suffix (i or j)
2763    let suffix = if s.ends_with('i') || s.ends_with('I') {
2764        'i'
2765    } else if s.ends_with('j') || s.ends_with('J') {
2766        'j'
2767    } else {
2768        // No imaginary suffix - must be purely real
2769        let real: f64 = s.parse().map_err(|_| ExcelError::new_num())?;
2770        return Ok((real, 0.0, 'i'));
2771    };
2772
2773    // Remove the suffix for parsing
2774    let s = &s[..s.len() - 1];
2775
2776    // Handle pure imaginary cases like "i", "-i", "4i"
2777    if s.is_empty() || s == "+" {
2778        return Ok((0.0, 1.0, suffix));
2779    }
2780    if s == "-" {
2781        return Ok((0.0, -1.0, suffix));
2782    }
2783
2784    // Find the last + or - that separates real and imaginary parts
2785    // We need to skip the first character (could be a sign) and find operators
2786    let mut split_pos = None;
2787    let bytes = s.as_bytes();
2788
2789    for i in (1..bytes.len()).rev() {
2790        let c = bytes[i] as char;
2791        if c == '+' || c == '-' {
2792            // Make sure this isn't part of an exponent (e.g., "1e-5")
2793            if i > 0 {
2794                let prev = bytes[i - 1] as char;
2795                if prev == 'e' || prev == 'E' {
2796                    continue;
2797                }
2798            }
2799            split_pos = Some(i);
2800            break;
2801        }
2802    }
2803
2804    match split_pos {
2805        Some(pos) => {
2806            // We have both real and imaginary parts
2807            let real_str = &s[..pos];
2808            let imag_str = &s[pos..];
2809
2810            let real: f64 = if real_str.is_empty() {
2811                0.0
2812            } else {
2813                real_str.parse().map_err(|_| ExcelError::new_num())?
2814            };
2815
2816            // Handle imaginary part (could be "+", "-", "+5", "-5", etc.)
2817            let imag: f64 = if imag_str == "+" {
2818                1.0
2819            } else if imag_str == "-" {
2820                -1.0
2821            } else {
2822                imag_str.parse().map_err(|_| ExcelError::new_num())?
2823            };
2824
2825            Ok((real, imag, suffix))
2826        }
2827        None => {
2828            // Pure imaginary number (no real part), e.g., "5" (before suffix was removed)
2829            let imag: f64 = s.parse().map_err(|_| ExcelError::new_num())?;
2830            Ok((0.0, imag, suffix))
2831        }
2832    }
2833}
2834
2835/// Clean up floating point noise by rounding values very close to integers
2836fn clean_float(val: f64) -> f64 {
2837    let rounded = val.round();
2838    if (val - rounded).abs() < 1e-10 {
2839        rounded
2840    } else {
2841        val
2842    }
2843}
2844
2845/// Format a complex number as a string
2846fn format_complex(real: f64, imag: f64, suffix: char) -> String {
2847    // Clean up floating point noise
2848    let real = clean_float(real);
2849    let imag = clean_float(imag);
2850
2851    // Handle special cases for cleaner output
2852    let real_is_zero = real.abs() < 1e-15;
2853    let imag_is_zero = imag.abs() < 1e-15;
2854
2855    if real_is_zero && imag_is_zero {
2856        return "0".to_string();
2857    }
2858
2859    if imag_is_zero {
2860        // Purely real
2861        if real == real.trunc() && real.abs() < 1e15 {
2862            return format!("{}", real as i64);
2863        }
2864        return format!("{}", real);
2865    }
2866
2867    if real_is_zero {
2868        // Purely imaginary
2869        if (imag - 1.0).abs() < 1e-15 {
2870            return format!("{}", suffix);
2871        }
2872        if (imag + 1.0).abs() < 1e-15 {
2873            return format!("-{}", suffix);
2874        }
2875        if imag == imag.trunc() && imag.abs() < 1e15 {
2876            return format!("{}{}", imag as i64, suffix);
2877        }
2878        return format!("{}{}", imag, suffix);
2879    }
2880
2881    // Both parts are non-zero
2882    let real_str = if real == real.trunc() && real.abs() < 1e15 {
2883        format!("{}", real as i64)
2884    } else {
2885        format!("{}", real)
2886    };
2887
2888    let imag_str = if (imag - 1.0).abs() < 1e-15 {
2889        format!("+{}", suffix)
2890    } else if (imag + 1.0).abs() < 1e-15 {
2891        format!("-{}", suffix)
2892    } else if imag > 0.0 {
2893        if imag == imag.trunc() && imag.abs() < 1e15 {
2894            format!("+{}{}", imag as i64, suffix)
2895        } else {
2896            format!("+{}{}", imag, suffix)
2897        }
2898    } else if imag == imag.trunc() && imag.abs() < 1e15 {
2899        format!("{}{}", imag as i64, suffix)
2900    } else {
2901        format!("{}{}", imag, suffix)
2902    };
2903
2904    format!("{}{}", real_str, imag_str)
2905}
2906
2907/// Coerce a LiteralValue to a complex number string
2908fn coerce_complex_str(v: &LiteralValue) -> Result<String, ExcelError> {
2909    match v {
2910        LiteralValue::Text(s) => Ok(s.clone()),
2911        LiteralValue::Int(i) => Ok(i.to_string()),
2912        LiteralValue::Number(n) => Ok(n.to_string()),
2913        LiteralValue::Error(e) => Err(e.clone()),
2914        _ => Err(ExcelError::new_value()),
2915    }
2916}
2917
2918/// Three-argument schema for COMPLEX function
2919static ARG_COMPLEX_THREE: std::sync::LazyLock<Vec<ArgSchema>> =
2920    std::sync::LazyLock::new(|| vec![ArgSchema::any(), ArgSchema::any(), ArgSchema::any()]);
2921
2922/// Builds a complex number text value from real and imaginary coefficients.
2923///
2924/// Returns canonical text such as `3+4i` or `-2j`.
2925///
2926/// # Remarks
2927/// - `real_num` and `i_num` are numerically coerced.
2928/// - `suffix` may be `"i"`, `"j"`, empty text, or omitted; empty/omitted defaults to `i`.
2929/// - Any other suffix returns `#VALUE!`.
2930///
2931/// # Examples
2932/// ```yaml,sandbox
2933/// title: "Build with default suffix"
2934/// formula: "=COMPLEX(3,4)"
2935/// expected: "3+4i"
2936/// ```
2937///
2938/// ```yaml,sandbox
2939/// title: "Build with j suffix"
2940/// formula: "=COMPLEX(0,-1,\"j\")"
2941/// expected: "-j"
2942/// ```
2943/// ```yaml,docs
2944/// related:
2945///   - IMREAL
2946///   - IMAGINARY
2947///   - IMSUM
2948/// faq:
2949///   - q: "Which suffix values are valid in `COMPLEX`?"
2950///     a: "Only suffixes i or j are accepted (empty or omitted defaults to i); other suffix strings return `#VALUE!`."
2951/// ```
2952#[derive(Debug)]
2953pub struct ComplexFn;
2954/// [formualizer-docgen:schema:start]
2955/// Name: COMPLEX
2956/// Type: ComplexFn
2957/// Min args: 2
2958/// Max args: variadic
2959/// Variadic: true
2960/// Signature: COMPLEX(arg1: any@scalar, arg2: any@scalar, arg3...: any@scalar)
2961/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}; arg2{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}; arg3{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
2962/// Caps: PURE
2963/// [formualizer-docgen:schema:end]
2964impl Function for ComplexFn {
2965    func_caps!(PURE);
2966    fn name(&self) -> &'static str {
2967        "COMPLEX"
2968    }
2969    fn min_args(&self) -> usize {
2970        2
2971    }
2972    fn variadic(&self) -> bool {
2973        true
2974    }
2975    fn arg_schema(&self) -> &'static [ArgSchema] {
2976        &ARG_COMPLEX_THREE[..]
2977    }
2978    fn eval<'a, 'b, 'c>(
2979        &self,
2980        args: &'c [ArgumentHandle<'a, 'b>],
2981        _ctx: &dyn FunctionContext<'b>,
2982    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2983        let real = match args[0].value()?.into_literal() {
2984            LiteralValue::Error(e) => {
2985                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2986            }
2987            other => coerce_num(&other)?,
2988        };
2989
2990        let imag = match args[1].value()?.into_literal() {
2991            LiteralValue::Error(e) => {
2992                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
2993            }
2994            other => coerce_num(&other)?,
2995        };
2996
2997        let suffix = if args.len() > 2 {
2998            match args[2].value()?.into_literal() {
2999                LiteralValue::Error(e) => {
3000                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
3001                }
3002                LiteralValue::Text(s) => {
3003                    let s = s.to_lowercase();
3004                    if s == "i" {
3005                        'i'
3006                    } else if s == "j" {
3007                        'j'
3008                    } else if s.is_empty() {
3009                        'i' // Default to 'i' for empty string
3010                    } else {
3011                        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3012                            ExcelError::new_value(),
3013                        )));
3014                    }
3015                }
3016                LiteralValue::Empty => 'i',
3017                _ => {
3018                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3019                        ExcelError::new_value(),
3020                    )));
3021                }
3022            }
3023        } else {
3024            'i'
3025        };
3026
3027        let result = format_complex(real, imag, suffix);
3028        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(result)))
3029    }
3030}
3031
3032/// Returns the real coefficient of a complex number.
3033///
3034/// Accepts complex text (for example `a+bi`) or numeric values.
3035///
3036/// # Remarks
3037/// - Inputs are coerced to complex-number text before parsing.
3038/// - Purely imaginary values return `0`.
3039/// - Invalid complex text returns `#NUM!`.
3040///
3041/// # Examples
3042/// ```yaml,sandbox
3043/// title: "Real part from a+bi"
3044/// formula: "=IMREAL(\"3+4i\")"
3045/// expected: 3
3046/// ```
3047///
3048/// ```yaml,sandbox
3049/// title: "Real part of pure imaginary"
3050/// formula: "=IMREAL(\"5j\")"
3051/// expected: 0
3052/// ```
3053/// ```yaml,docs
3054/// related:
3055///   - IMAGINARY
3056///   - COMPLEX
3057///   - IMABS
3058/// faq:
3059///   - q: "What does `IMREAL` return for a purely imaginary input?"
3060///     a: "It returns `0` because the real coefficient is zero."
3061/// ```
3062#[derive(Debug)]
3063pub struct ImRealFn;
3064/// [formualizer-docgen:schema:start]
3065/// Name: IMREAL
3066/// Type: ImRealFn
3067/// Min args: 1
3068/// Max args: 1
3069/// Variadic: false
3070/// Signature: IMREAL(arg1: any@scalar)
3071/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
3072/// Caps: PURE
3073/// [formualizer-docgen:schema:end]
3074impl Function for ImRealFn {
3075    func_caps!(PURE);
3076    fn name(&self) -> &'static str {
3077        "IMREAL"
3078    }
3079    fn min_args(&self) -> usize {
3080        1
3081    }
3082    fn arg_schema(&self) -> &'static [ArgSchema] {
3083        &ARG_ANY_ONE[..]
3084    }
3085    fn eval<'a, 'b, 'c>(
3086        &self,
3087        args: &'c [ArgumentHandle<'a, 'b>],
3088        _ctx: &dyn FunctionContext<'b>,
3089    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3090        let inumber = match args[0].value()?.into_literal() {
3091            LiteralValue::Error(e) => {
3092                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
3093            }
3094            other => match coerce_complex_str(&other) {
3095                Ok(s) => s,
3096                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
3097            },
3098        };
3099
3100        let (real, _, _) = match parse_complex(&inumber) {
3101            Ok(c) => c,
3102            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
3103        };
3104
3105        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(real)))
3106    }
3107}
3108
3109/// Returns the imaginary coefficient of a complex number.
3110///
3111/// Accepts complex text (for example `a+bi`) or numeric values.
3112///
3113/// # Remarks
3114/// - Inputs are coerced to complex-number text before parsing.
3115/// - Purely real values return `0`.
3116/// - Invalid complex text returns `#NUM!`.
3117///
3118/// # Examples
3119/// ```yaml,sandbox
3120/// title: "Imaginary part from a+bi"
3121/// formula: "=IMAGINARY(\"3+4i\")"
3122/// expected: 4
3123/// ```
3124///
3125/// ```yaml,sandbox
3126/// title: "Imaginary part with j suffix"
3127/// formula: "=IMAGINARY(\"-2j\")"
3128/// expected: -2
3129/// ```
3130/// ```yaml,docs
3131/// related:
3132///   - IMREAL
3133///   - COMPLEX
3134///   - IMABS
3135/// faq:
3136///   - q: "What does `IMAGINARY` return for a real-only input?"
3137///     a: "It returns `0` because there is no imaginary component."
3138/// ```
3139#[derive(Debug)]
3140pub struct ImaginaryFn;
3141/// [formualizer-docgen:schema:start]
3142/// Name: IMAGINARY
3143/// Type: ImaginaryFn
3144/// Min args: 1
3145/// Max args: 1
3146/// Variadic: false
3147/// Signature: IMAGINARY(arg1: any@scalar)
3148/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
3149/// Caps: PURE
3150/// [formualizer-docgen:schema:end]
3151impl Function for ImaginaryFn {
3152    func_caps!(PURE);
3153    fn name(&self) -> &'static str {
3154        "IMAGINARY"
3155    }
3156    fn min_args(&self) -> usize {
3157        1
3158    }
3159    fn arg_schema(&self) -> &'static [ArgSchema] {
3160        &ARG_ANY_ONE[..]
3161    }
3162    fn eval<'a, 'b, 'c>(
3163        &self,
3164        args: &'c [ArgumentHandle<'a, 'b>],
3165        _ctx: &dyn FunctionContext<'b>,
3166    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3167        let inumber = match args[0].value()?.into_literal() {
3168            LiteralValue::Error(e) => {
3169                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
3170            }
3171            other => match coerce_complex_str(&other) {
3172                Ok(s) => s,
3173                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
3174            },
3175        };
3176
3177        let (_, imag, _) = match parse_complex(&inumber) {
3178            Ok(c) => c,
3179            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
3180        };
3181
3182        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(imag)))
3183    }
3184}
3185
3186/// Returns the modulus (absolute value) of a complex number.
3187///
3188/// Computes `sqrt(real^2 + imaginary^2)`.
3189///
3190/// # Remarks
3191/// - Inputs are coerced to complex-number text before parsing.
3192/// - Returns a non-negative real number.
3193/// - Invalid complex text returns `#NUM!`.
3194///
3195/// # Examples
3196/// ```yaml,sandbox
3197/// title: "3-4-5 triangle modulus"
3198/// formula: "=IMABS(\"3+4i\")"
3199/// expected: 5
3200/// ```
3201///
3202/// ```yaml,sandbox
3203/// title: "Purely real input"
3204/// formula: "=IMABS(\"5\")"
3205/// expected: 5
3206/// ```
3207/// ```yaml,docs
3208/// related:
3209///   - IMREAL
3210///   - IMAGINARY
3211///   - IMARGUMENT
3212/// faq:
3213///   - q: "Can `IMABS` return a negative result?"
3214///     a: "No. It computes the modulus `sqrt(a^2+b^2)`, which is always non-negative."
3215/// ```
3216#[derive(Debug)]
3217pub struct ImAbsFn;
3218/// [formualizer-docgen:schema:start]
3219/// Name: IMABS
3220/// Type: ImAbsFn
3221/// Min args: 1
3222/// Max args: 1
3223/// Variadic: false
3224/// Signature: IMABS(arg1: any@scalar)
3225/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
3226/// Caps: PURE
3227/// [formualizer-docgen:schema:end]
3228impl Function for ImAbsFn {
3229    func_caps!(PURE);
3230    fn name(&self) -> &'static str {
3231        "IMABS"
3232    }
3233    fn min_args(&self) -> usize {
3234        1
3235    }
3236    fn arg_schema(&self) -> &'static [ArgSchema] {
3237        &ARG_ANY_ONE[..]
3238    }
3239    fn eval<'a, 'b, 'c>(
3240        &self,
3241        args: &'c [ArgumentHandle<'a, 'b>],
3242        _ctx: &dyn FunctionContext<'b>,
3243    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3244        let inumber = match args[0].value()?.into_literal() {
3245            LiteralValue::Error(e) => {
3246                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
3247            }
3248            other => match coerce_complex_str(&other) {
3249                Ok(s) => s,
3250                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
3251            },
3252        };
3253
3254        let (real, imag, _) = match parse_complex(&inumber) {
3255            Ok(c) => c,
3256            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
3257        };
3258
3259        let abs = (real * real + imag * imag).sqrt();
3260        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(abs)))
3261    }
3262}
3263
3264/// Returns the argument (angle in radians) of a complex number.
3265///
3266/// The angle is measured from the positive real axis.
3267///
3268/// # Remarks
3269/// - Inputs are coerced to complex-number text before parsing.
3270/// - Returns `#DIV/0!` for `0+0i`, where the angle is undefined.
3271/// - Invalid complex text returns `#NUM!`.
3272///
3273/// # Examples
3274/// ```yaml,sandbox
3275/// title: "First-quadrant angle"
3276/// formula: "=IMARGUMENT(\"1+i\")"
3277/// expected: 0.7853981633974483
3278/// ```
3279///
3280/// ```yaml,sandbox
3281/// title: "Negative real axis"
3282/// formula: "=IMARGUMENT(\"-1\")"
3283/// expected: 3.141592653589793
3284/// ```
3285/// ```yaml,docs
3286/// related:
3287///   - IMABS
3288///   - IMLN
3289///   - IMSQRT
3290/// faq:
3291///   - q: "Why does `IMARGUMENT(0)` return `#DIV/0!`?"
3292///     a: "The argument (angle) of `0+0i` is undefined, so the function returns `#DIV/0!`."
3293/// ```
3294#[derive(Debug)]
3295pub struct ImArgumentFn;
3296/// [formualizer-docgen:schema:start]
3297/// Name: IMARGUMENT
3298/// Type: ImArgumentFn
3299/// Min args: 1
3300/// Max args: 1
3301/// Variadic: false
3302/// Signature: IMARGUMENT(arg1: any@scalar)
3303/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
3304/// Caps: PURE
3305/// [formualizer-docgen:schema:end]
3306impl Function for ImArgumentFn {
3307    func_caps!(PURE);
3308    fn name(&self) -> &'static str {
3309        "IMARGUMENT"
3310    }
3311    fn min_args(&self) -> usize {
3312        1
3313    }
3314    fn arg_schema(&self) -> &'static [ArgSchema] {
3315        &ARG_ANY_ONE[..]
3316    }
3317    fn eval<'a, 'b, 'c>(
3318        &self,
3319        args: &'c [ArgumentHandle<'a, 'b>],
3320        _ctx: &dyn FunctionContext<'b>,
3321    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3322        let inumber = match args[0].value()?.into_literal() {
3323            LiteralValue::Error(e) => {
3324                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
3325            }
3326            other => match coerce_complex_str(&other) {
3327                Ok(s) => s,
3328                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
3329            },
3330        };
3331
3332        let (real, imag, _) = match parse_complex(&inumber) {
3333            Ok(c) => c,
3334            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
3335        };
3336
3337        // Excel returns #DIV/0! for IMARGUMENT(0)
3338        if real.abs() < 1e-15 && imag.abs() < 1e-15 {
3339            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3340                ExcelError::new_div(),
3341            )));
3342        }
3343
3344        let arg = imag.atan2(real);
3345        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(arg)))
3346    }
3347}
3348
3349/// Returns the complex conjugate of a complex number.
3350///
3351/// Negates the imaginary coefficient and keeps the real coefficient unchanged.
3352///
3353/// # Remarks
3354/// - Inputs are coerced to complex-number text before parsing.
3355/// - Preserves the original suffix style (`i` or `j`) when possible.
3356/// - Invalid complex text returns `#NUM!`.
3357///
3358/// # Examples
3359/// ```yaml,sandbox
3360/// title: "Conjugate with i suffix"
3361/// formula: "=IMCONJUGATE(\"3+4i\")"
3362/// expected: "3-4i"
3363/// ```
3364///
3365/// ```yaml,sandbox
3366/// title: "Conjugate with j suffix"
3367/// formula: "=IMCONJUGATE(\"-2j\")"
3368/// expected: "2j"
3369/// ```
3370/// ```yaml,docs
3371/// related:
3372///   - IMSUB
3373///   - IMPRODUCT
3374///   - IMDIV
3375/// faq:
3376///   - q: "Does `IMCONJUGATE` keep the `i`/`j` suffix style?"
3377///     a: "Yes. It negates only the imaginary coefficient and preserves the parsed suffix form."
3378/// ```
3379#[derive(Debug)]
3380pub struct ImConjugateFn;
3381/// [formualizer-docgen:schema:start]
3382/// Name: IMCONJUGATE
3383/// Type: ImConjugateFn
3384/// Min args: 1
3385/// Max args: 1
3386/// Variadic: false
3387/// Signature: IMCONJUGATE(arg1: any@scalar)
3388/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
3389/// Caps: PURE
3390/// [formualizer-docgen:schema:end]
3391impl Function for ImConjugateFn {
3392    func_caps!(PURE);
3393    fn name(&self) -> &'static str {
3394        "IMCONJUGATE"
3395    }
3396    fn min_args(&self) -> usize {
3397        1
3398    }
3399    fn arg_schema(&self) -> &'static [ArgSchema] {
3400        &ARG_ANY_ONE[..]
3401    }
3402    fn eval<'a, 'b, 'c>(
3403        &self,
3404        args: &'c [ArgumentHandle<'a, 'b>],
3405        _ctx: &dyn FunctionContext<'b>,
3406    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3407        let inumber = match args[0].value()?.into_literal() {
3408            LiteralValue::Error(e) => {
3409                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
3410            }
3411            other => match coerce_complex_str(&other) {
3412                Ok(s) => s,
3413                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
3414            },
3415        };
3416
3417        let (real, imag, suffix) = match parse_complex(&inumber) {
3418            Ok(c) => c,
3419            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
3420        };
3421
3422        let result = format_complex(real, -imag, suffix);
3423        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(result)))
3424    }
3425}
3426
3427/// Helper to check if two complex numbers have compatible suffixes
3428fn check_suffix_compatibility(s1: char, s2: char) -> Result<char, ExcelError> {
3429    // If both have the same suffix, use it
3430    // If one is from a purely real number (default 'i'), use the other's suffix
3431    // Excel doesn't allow mixing 'i' and 'j' when both are explicit
3432    if s1 == s2 {
3433        Ok(s1)
3434    } else {
3435        // For simplicity, treat 'i' as the default and allow mixed
3436        // In strict Excel mode, this would error
3437        Ok(s1)
3438    }
3439}
3440
3441/// Returns the sum of one or more complex numbers.
3442///
3443/// Adds real parts together and imaginary parts together.
3444///
3445/// # Remarks
3446/// - Each argument is coerced to complex-number text before parsing.
3447/// - Accepts any number of arguments from one upward.
3448/// - Invalid complex text returns `#NUM!`.
3449///
3450/// # Examples
3451/// ```yaml,sandbox
3452/// title: "Add multiple complex values"
3453/// formula: "=IMSUM(\"3+4i\",\"1-2i\",\"5\")"
3454/// expected: "9+2i"
3455/// ```
3456///
3457/// ```yaml,sandbox
3458/// title: "Add j-suffix values"
3459/// formula: "=IMSUM(\"2j\",\"-j\")"
3460/// expected: "j"
3461/// ```
3462/// ```yaml,docs
3463/// related:
3464///   - IMSUB
3465///   - IMPRODUCT
3466///   - COMPLEX
3467/// faq:
3468///   - q: "Can `IMSUM` take more than two arguments?"
3469///     a: "Yes. It is variadic and sums all provided complex arguments in sequence."
3470/// ```
3471#[derive(Debug)]
3472pub struct ImSumFn;
3473/// [formualizer-docgen:schema:start]
3474/// Name: IMSUM
3475/// Type: ImSumFn
3476/// Min args: 1
3477/// Max args: variadic
3478/// Variadic: true
3479/// Signature: IMSUM(arg1...: any@scalar)
3480/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
3481/// Caps: PURE
3482/// [formualizer-docgen:schema:end]
3483impl Function for ImSumFn {
3484    func_caps!(PURE);
3485    fn name(&self) -> &'static str {
3486        "IMSUM"
3487    }
3488    fn min_args(&self) -> usize {
3489        1
3490    }
3491    fn variadic(&self) -> bool {
3492        true
3493    }
3494    fn arg_schema(&self) -> &'static [ArgSchema] {
3495        &ARG_ANY_ONE[..]
3496    }
3497    fn eval<'a, 'b, 'c>(
3498        &self,
3499        args: &'c [ArgumentHandle<'a, 'b>],
3500        _ctx: &dyn FunctionContext<'b>,
3501    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3502        let mut sum_real = 0.0;
3503        let mut sum_imag = 0.0;
3504        let mut result_suffix = 'i';
3505        let mut first = true;
3506
3507        for arg in args {
3508            let inumber = match arg.value()?.into_literal() {
3509                LiteralValue::Error(e) => {
3510                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
3511                }
3512                other => match coerce_complex_str(&other) {
3513                    Ok(s) => s,
3514                    Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
3515                },
3516            };
3517
3518            let (real, imag, suffix) = match parse_complex(&inumber) {
3519                Ok(c) => c,
3520                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
3521            };
3522
3523            if first {
3524                result_suffix = suffix;
3525                first = false;
3526            } else {
3527                result_suffix = check_suffix_compatibility(result_suffix, suffix)?;
3528            }
3529
3530            sum_real += real;
3531            sum_imag += imag;
3532        }
3533
3534        let result = format_complex(sum_real, sum_imag, result_suffix);
3535        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(result)))
3536    }
3537}
3538
3539/// Returns the difference between two complex numbers.
3540///
3541/// Subtracts the second complex value from the first.
3542///
3543/// # Remarks
3544/// - Inputs are coerced to complex-number text before parsing.
3545/// - Output keeps the suffix style from the parsed inputs.
3546/// - Invalid complex text returns `#NUM!`.
3547///
3548/// # Examples
3549/// ```yaml,sandbox
3550/// title: "Subtract a+bi values"
3551/// formula: "=IMSUB(\"5+3i\",\"2+i\")"
3552/// expected: "3+2i"
3553/// ```
3554///
3555/// ```yaml,sandbox
3556/// title: "Subtract pure imaginary from real"
3557/// formula: "=IMSUB(\"4\",\"7j\")"
3558/// expected: "4-7j"
3559/// ```
3560/// ```yaml,docs
3561/// related:
3562///   - IMSUM
3563///   - IMDIV
3564///   - COMPLEX
3565/// faq:
3566///   - q: "How is subtraction ordered in `IMSUB`?"
3567///     a: "It always computes `inumber1 - inumber2`; swapping arguments changes the sign of the result."
3568/// ```
3569#[derive(Debug)]
3570pub struct ImSubFn;
3571/// [formualizer-docgen:schema:start]
3572/// Name: IMSUB
3573/// Type: ImSubFn
3574/// Min args: 2
3575/// Max args: 2
3576/// Variadic: false
3577/// Signature: IMSUB(arg1: any@scalar, arg2: any@scalar)
3578/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}; arg2{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
3579/// Caps: PURE
3580/// [formualizer-docgen:schema:end]
3581impl Function for ImSubFn {
3582    func_caps!(PURE);
3583    fn name(&self) -> &'static str {
3584        "IMSUB"
3585    }
3586    fn min_args(&self) -> usize {
3587        2
3588    }
3589    fn arg_schema(&self) -> &'static [ArgSchema] {
3590        &ARG_ANY_TWO[..]
3591    }
3592    fn eval<'a, 'b, 'c>(
3593        &self,
3594        args: &'c [ArgumentHandle<'a, 'b>],
3595        _ctx: &dyn FunctionContext<'b>,
3596    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3597        let inumber1 = match args[0].value()?.into_literal() {
3598            LiteralValue::Error(e) => {
3599                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
3600            }
3601            other => match coerce_complex_str(&other) {
3602                Ok(s) => s,
3603                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
3604            },
3605        };
3606
3607        let inumber2 = match args[1].value()?.into_literal() {
3608            LiteralValue::Error(e) => {
3609                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
3610            }
3611            other => match coerce_complex_str(&other) {
3612                Ok(s) => s,
3613                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
3614            },
3615        };
3616
3617        let (real1, imag1, suffix1) = match parse_complex(&inumber1) {
3618            Ok(c) => c,
3619            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
3620        };
3621
3622        let (real2, imag2, suffix2) = match parse_complex(&inumber2) {
3623            Ok(c) => c,
3624            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
3625        };
3626
3627        let result_suffix = check_suffix_compatibility(suffix1, suffix2)?;
3628        let result = format_complex(real1 - real2, imag1 - imag2, result_suffix);
3629        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(result)))
3630    }
3631}
3632
3633/// Returns the product of one or more complex numbers.
3634///
3635/// Multiplies values sequentially using complex multiplication rules.
3636///
3637/// # Remarks
3638/// - Each argument is coerced to complex-number text before parsing.
3639/// - Accepts any number of arguments from one upward.
3640/// - Invalid complex text returns `#NUM!`.
3641///
3642/// # Examples
3643/// ```yaml,sandbox
3644/// title: "Multiply conjugates"
3645/// formula: "=IMPRODUCT(\"1+i\",\"1-i\")"
3646/// expected: "2"
3647/// ```
3648///
3649/// ```yaml,sandbox
3650/// title: "Scale an imaginary value"
3651/// formula: "=IMPRODUCT(\"2i\",\"3\")"
3652/// expected: "6i"
3653/// ```
3654/// ```yaml,docs
3655/// related:
3656///   - IMDIV
3657///   - IMSUM
3658///   - IMPOWER
3659/// faq:
3660///   - q: "Can `IMPRODUCT` multiply a single argument?"
3661///     a: "Yes. With one argument it returns that parsed complex value in canonical formatted form."
3662/// ```
3663#[derive(Debug)]
3664pub struct ImProductFn;
3665/// [formualizer-docgen:schema:start]
3666/// Name: IMPRODUCT
3667/// Type: ImProductFn
3668/// Min args: 1
3669/// Max args: variadic
3670/// Variadic: true
3671/// Signature: IMPRODUCT(arg1...: any@scalar)
3672/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
3673/// Caps: PURE
3674/// [formualizer-docgen:schema:end]
3675impl Function for ImProductFn {
3676    func_caps!(PURE);
3677    fn name(&self) -> &'static str {
3678        "IMPRODUCT"
3679    }
3680    fn min_args(&self) -> usize {
3681        1
3682    }
3683    fn variadic(&self) -> bool {
3684        true
3685    }
3686    fn arg_schema(&self) -> &'static [ArgSchema] {
3687        &ARG_ANY_ONE[..]
3688    }
3689    fn eval<'a, 'b, 'c>(
3690        &self,
3691        args: &'c [ArgumentHandle<'a, 'b>],
3692        _ctx: &dyn FunctionContext<'b>,
3693    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3694        let mut prod_real = 1.0;
3695        let mut prod_imag = 0.0;
3696        let mut result_suffix = 'i';
3697        let mut first = true;
3698
3699        for arg in args {
3700            let inumber = match arg.value()?.into_literal() {
3701                LiteralValue::Error(e) => {
3702                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
3703                }
3704                other => match coerce_complex_str(&other) {
3705                    Ok(s) => s,
3706                    Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
3707                },
3708            };
3709
3710            let (real, imag, suffix) = match parse_complex(&inumber) {
3711                Ok(c) => c,
3712                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
3713            };
3714
3715            if first {
3716                result_suffix = suffix;
3717                prod_real = real;
3718                prod_imag = imag;
3719                first = false;
3720            } else {
3721                result_suffix = check_suffix_compatibility(result_suffix, suffix)?;
3722                // (a + bi) * (c + di) = (ac - bd) + (ad + bc)i
3723                let new_real = prod_real * real - prod_imag * imag;
3724                let new_imag = prod_real * imag + prod_imag * real;
3725                prod_real = new_real;
3726                prod_imag = new_imag;
3727            }
3728        }
3729
3730        let result = format_complex(prod_real, prod_imag, result_suffix);
3731        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(result)))
3732    }
3733}
3734
3735/// Returns the quotient of two complex numbers.
3736///
3737/// Divides the first complex value by the second.
3738///
3739/// # Remarks
3740/// - Inputs are coerced to complex-number text before parsing.
3741/// - Returns `#DIV/0!` when the divisor is `0+0i`.
3742/// - Invalid complex text returns `#NUM!`.
3743///
3744/// # Examples
3745/// ```yaml,sandbox
3746/// title: "Divide complex numbers"
3747/// formula: "=IMDIV(\"3+4i\",\"1-i\")"
3748/// expected: "-0.5+3.5i"
3749/// ```
3750///
3751/// ```yaml,sandbox
3752/// title: "Division by zero complex"
3753/// formula: "=IMDIV(\"2+i\",\"0\")"
3754/// expected: "#DIV/0!"
3755/// ```
3756/// ```yaml,docs
3757/// related:
3758///   - IMPRODUCT
3759///   - IMSUB
3760///   - IMCONJUGATE
3761/// faq:
3762///   - q: "When does `IMDIV` return `#DIV/0!`?"
3763///     a: "If the divisor is `0+0i` (denominator magnitude near zero), division is undefined and returns `#DIV/0!`."
3764/// ```
3765#[derive(Debug)]
3766pub struct ImDivFn;
3767/// [formualizer-docgen:schema:start]
3768/// Name: IMDIV
3769/// Type: ImDivFn
3770/// Min args: 2
3771/// Max args: 2
3772/// Variadic: false
3773/// Signature: IMDIV(arg1: any@scalar, arg2: any@scalar)
3774/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}; arg2{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
3775/// Caps: PURE
3776/// [formualizer-docgen:schema:end]
3777impl Function for ImDivFn {
3778    func_caps!(PURE);
3779    fn name(&self) -> &'static str {
3780        "IMDIV"
3781    }
3782    fn min_args(&self) -> usize {
3783        2
3784    }
3785    fn arg_schema(&self) -> &'static [ArgSchema] {
3786        &ARG_ANY_TWO[..]
3787    }
3788    fn eval<'a, 'b, 'c>(
3789        &self,
3790        args: &'c [ArgumentHandle<'a, 'b>],
3791        _ctx: &dyn FunctionContext<'b>,
3792    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3793        let inumber1 = match args[0].value()?.into_literal() {
3794            LiteralValue::Error(e) => {
3795                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
3796            }
3797            other => match coerce_complex_str(&other) {
3798                Ok(s) => s,
3799                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
3800            },
3801        };
3802
3803        let inumber2 = match args[1].value()?.into_literal() {
3804            LiteralValue::Error(e) => {
3805                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
3806            }
3807            other => match coerce_complex_str(&other) {
3808                Ok(s) => s,
3809                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
3810            },
3811        };
3812
3813        let (a, b, suffix1) = match parse_complex(&inumber1) {
3814            Ok(c) => c,
3815            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
3816        };
3817
3818        let (c, d, suffix2) = match parse_complex(&inumber2) {
3819            Ok(c) => c,
3820            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
3821        };
3822
3823        // Division by zero check - returns #DIV/0! for Excel compatibility
3824        let denom = c * c + d * d;
3825        if denom.abs() < 1e-15 {
3826            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3827                ExcelError::new_div(),
3828            )));
3829        }
3830
3831        let result_suffix = check_suffix_compatibility(suffix1, suffix2)?;
3832
3833        // (a + bi) / (c + di) = ((ac + bd) + (bc - ad)i) / (c^2 + d^2)
3834        let real = (a * c + b * d) / denom;
3835        let imag = (b * c - a * d) / denom;
3836
3837        let result = format_complex(real, imag, result_suffix);
3838        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(result)))
3839    }
3840}
3841
3842/// Returns the complex exponential of a complex number.
3843///
3844/// Computes `e^(a+bi)` and returns the result as complex text.
3845///
3846/// # Remarks
3847/// - Input is coerced to complex-number text before parsing.
3848/// - Uses Euler's identity for the imaginary component.
3849/// - Invalid complex text returns `#NUM!`.
3850///
3851/// # Examples
3852/// ```yaml,sandbox
3853/// title: "Exponential of zero"
3854/// formula: "=IMEXP(\"0\")"
3855/// expected: "1"
3856/// ```
3857///
3858/// ```yaml,sandbox
3859/// title: "Exponential of a real value"
3860/// formula: "=IMEXP(\"1\")"
3861/// expected: "2.718281828459045"
3862/// ```
3863/// ```yaml,docs
3864/// related:
3865///   - IMLN
3866///   - IMPOWER
3867///   - IMSIN
3868///   - IMCOS
3869/// faq:
3870///   - q: "Does `IMEXP` return text or a numeric complex type?"
3871///     a: "It returns a canonical complex text string, consistent with other `IM*` functions."
3872/// ```
3873#[derive(Debug)]
3874pub struct ImExpFn;
3875/// [formualizer-docgen:schema:start]
3876/// Name: IMEXP
3877/// Type: ImExpFn
3878/// Min args: 1
3879/// Max args: 1
3880/// Variadic: false
3881/// Signature: IMEXP(arg1: any@scalar)
3882/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
3883/// Caps: PURE
3884/// [formualizer-docgen:schema:end]
3885impl Function for ImExpFn {
3886    func_caps!(PURE);
3887    fn name(&self) -> &'static str {
3888        "IMEXP"
3889    }
3890    fn min_args(&self) -> usize {
3891        1
3892    }
3893    fn arg_schema(&self) -> &'static [ArgSchema] {
3894        &ARG_ANY_ONE[..]
3895    }
3896    fn eval<'a, 'b, 'c>(
3897        &self,
3898        args: &'c [ArgumentHandle<'a, 'b>],
3899        _ctx: &dyn FunctionContext<'b>,
3900    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3901        let inumber = match args[0].value()?.into_literal() {
3902            LiteralValue::Error(e) => {
3903                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
3904            }
3905            other => match coerce_complex_str(&other) {
3906                Ok(s) => s,
3907                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
3908            },
3909        };
3910
3911        let (a, b, suffix) = match parse_complex(&inumber) {
3912            Ok(c) => c,
3913            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
3914        };
3915
3916        // e^(a+bi) = e^a * (cos(b) + i*sin(b))
3917        let exp_a = a.exp();
3918        let real = exp_a * b.cos();
3919        let imag = exp_a * b.sin();
3920
3921        let result = format_complex(real, imag, suffix);
3922        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(result)))
3923    }
3924}
3925
3926/// Returns the natural logarithm of a complex number.
3927///
3928/// Produces the principal complex logarithm as text.
3929///
3930/// # Remarks
3931/// - Input is coerced to complex-number text before parsing.
3932/// - Returns `#NUM!` for zero input because `ln(0)` is undefined.
3933/// - Invalid complex text returns `#NUM!`.
3934///
3935/// # Examples
3936/// ```yaml,sandbox
3937/// title: "Natural log of 1"
3938/// formula: "=IMLN(\"1\")"
3939/// expected: "0"
3940/// ```
3941///
3942/// ```yaml,sandbox
3943/// title: "Natural log on imaginary axis"
3944/// formula: "=IMLN(\"i\")"
3945/// expected: "1.5707963267948966i"
3946/// ```
3947/// ```yaml,docs
3948/// related:
3949///   - IMEXP
3950///   - IMLOG10
3951///   - IMLOG2
3952/// faq:
3953///   - q: "Why does `IMLN(0)` return `#NUM!`?"
3954///     a: "The complex logarithm at zero is undefined, so this implementation returns `#NUM!`."
3955/// ```
3956#[derive(Debug)]
3957pub struct ImLnFn;
3958/// [formualizer-docgen:schema:start]
3959/// Name: IMLN
3960/// Type: ImLnFn
3961/// Min args: 1
3962/// Max args: 1
3963/// Variadic: false
3964/// Signature: IMLN(arg1: any@scalar)
3965/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
3966/// Caps: PURE
3967/// [formualizer-docgen:schema:end]
3968impl Function for ImLnFn {
3969    func_caps!(PURE);
3970    fn name(&self) -> &'static str {
3971        "IMLN"
3972    }
3973    fn min_args(&self) -> usize {
3974        1
3975    }
3976    fn arg_schema(&self) -> &'static [ArgSchema] {
3977        &ARG_ANY_ONE[..]
3978    }
3979    fn eval<'a, 'b, 'c>(
3980        &self,
3981        args: &'c [ArgumentHandle<'a, 'b>],
3982        _ctx: &dyn FunctionContext<'b>,
3983    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3984        let inumber = match args[0].value()?.into_literal() {
3985            LiteralValue::Error(e) => {
3986                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
3987            }
3988            other => match coerce_complex_str(&other) {
3989                Ok(s) => s,
3990                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
3991            },
3992        };
3993
3994        let (a, b, suffix) = match parse_complex(&inumber) {
3995            Ok(c) => c,
3996            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
3997        };
3998
3999        // ln(0) is undefined
4000        let modulus = (a * a + b * b).sqrt();
4001        if modulus < 1e-15 {
4002            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4003                ExcelError::new_num(),
4004            )));
4005        }
4006
4007        // ln(z) = ln(|z|) + i*arg(z)
4008        let real = modulus.ln();
4009        let imag = b.atan2(a);
4010
4011        let result = format_complex(real, imag, suffix);
4012        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(result)))
4013    }
4014}
4015
4016/// Returns the base-10 logarithm of a complex number.
4017///
4018/// Produces the principal complex logarithm in base 10.
4019///
4020/// # Remarks
4021/// - Input is coerced to complex-number text before parsing.
4022/// - Returns `#NUM!` for zero input.
4023/// - Invalid complex text returns `#NUM!`.
4024///
4025/// # Examples
4026/// ```yaml,sandbox
4027/// title: "Base-10 log of a real value"
4028/// formula: "=IMLOG10(\"10\")"
4029/// expected: "1"
4030/// ```
4031///
4032/// ```yaml,sandbox
4033/// title: "Base-10 log on imaginary axis"
4034/// formula: "=IMLOG10(\"i\")"
4035/// expected: "0.6821881769209206i"
4036/// ```
4037/// ```yaml,docs
4038/// related:
4039///   - IMLN
4040///   - IMLOG2
4041///   - IMEXP
4042/// faq:
4043///   - q: "What branch of the logarithm does `IMLOG10` use?"
4044///     a: "It returns the principal complex logarithm (base 10), derived from principal argument `atan2(imag, real)`."
4045/// ```
4046#[derive(Debug)]
4047pub struct ImLog10Fn;
4048/// [formualizer-docgen:schema:start]
4049/// Name: IMLOG10
4050/// Type: ImLog10Fn
4051/// Min args: 1
4052/// Max args: 1
4053/// Variadic: false
4054/// Signature: IMLOG10(arg1: any@scalar)
4055/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
4056/// Caps: PURE
4057/// [formualizer-docgen:schema:end]
4058impl Function for ImLog10Fn {
4059    func_caps!(PURE);
4060    fn name(&self) -> &'static str {
4061        "IMLOG10"
4062    }
4063    fn min_args(&self) -> usize {
4064        1
4065    }
4066    fn arg_schema(&self) -> &'static [ArgSchema] {
4067        &ARG_ANY_ONE[..]
4068    }
4069    fn eval<'a, 'b, 'c>(
4070        &self,
4071        args: &'c [ArgumentHandle<'a, 'b>],
4072        _ctx: &dyn FunctionContext<'b>,
4073    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4074        let inumber = match args[0].value()?.into_literal() {
4075            LiteralValue::Error(e) => {
4076                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
4077            }
4078            other => match coerce_complex_str(&other) {
4079                Ok(s) => s,
4080                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
4081            },
4082        };
4083
4084        let (a, b, suffix) = match parse_complex(&inumber) {
4085            Ok(c) => c,
4086            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
4087        };
4088
4089        // log10(0) is undefined
4090        let modulus = (a * a + b * b).sqrt();
4091        if modulus < 1e-15 {
4092            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4093                ExcelError::new_num(),
4094            )));
4095        }
4096
4097        // log10(z) = ln(z) / ln(10) = (ln(|z|) + i*arg(z)) / ln(10)
4098        let ln10 = 10.0_f64.ln();
4099        let real = modulus.ln() / ln10;
4100        let imag = b.atan2(a) / ln10;
4101
4102        let result = format_complex(real, imag, suffix);
4103        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(result)))
4104    }
4105}
4106
4107/// Returns the base-2 logarithm of a complex number.
4108///
4109/// Produces the principal complex logarithm in base 2.
4110///
4111/// # Remarks
4112/// - Input is coerced to complex-number text before parsing.
4113/// - Returns `#NUM!` for zero input.
4114/// - Invalid complex text returns `#NUM!`.
4115///
4116/// # Examples
4117/// ```yaml,sandbox
4118/// title: "Base-2 log of a real value"
4119/// formula: "=IMLOG2(\"8\")"
4120/// expected: "3"
4121/// ```
4122///
4123/// ```yaml,sandbox
4124/// title: "Base-2 log on imaginary axis"
4125/// formula: "=IMLOG2(\"i\")"
4126/// expected: "2.266180070913597i"
4127/// ```
4128/// ```yaml,docs
4129/// related:
4130///   - IMLN
4131///   - IMLOG10
4132///   - IMEXP
4133/// faq:
4134///   - q: "When does `IMLOG2` return `#NUM!`?"
4135///     a: "It returns `#NUM!` for invalid complex text or zero input, where logarithm is undefined."
4136/// ```
4137#[derive(Debug)]
4138pub struct ImLog2Fn;
4139/// [formualizer-docgen:schema:start]
4140/// Name: IMLOG2
4141/// Type: ImLog2Fn
4142/// Min args: 1
4143/// Max args: 1
4144/// Variadic: false
4145/// Signature: IMLOG2(arg1: any@scalar)
4146/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
4147/// Caps: PURE
4148/// [formualizer-docgen:schema:end]
4149impl Function for ImLog2Fn {
4150    func_caps!(PURE);
4151    fn name(&self) -> &'static str {
4152        "IMLOG2"
4153    }
4154    fn min_args(&self) -> usize {
4155        1
4156    }
4157    fn arg_schema(&self) -> &'static [ArgSchema] {
4158        &ARG_ANY_ONE[..]
4159    }
4160    fn eval<'a, 'b, 'c>(
4161        &self,
4162        args: &'c [ArgumentHandle<'a, 'b>],
4163        _ctx: &dyn FunctionContext<'b>,
4164    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4165        let inumber = match args[0].value()?.into_literal() {
4166            LiteralValue::Error(e) => {
4167                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
4168            }
4169            other => match coerce_complex_str(&other) {
4170                Ok(s) => s,
4171                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
4172            },
4173        };
4174
4175        let (a, b, suffix) = match parse_complex(&inumber) {
4176            Ok(c) => c,
4177            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
4178        };
4179
4180        // log2(0) is undefined
4181        let modulus = (a * a + b * b).sqrt();
4182        if modulus < 1e-15 {
4183            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4184                ExcelError::new_num(),
4185            )));
4186        }
4187
4188        // log2(z) = ln(z) / ln(2) = (ln(|z|) + i*arg(z)) / ln(2)
4189        let ln2 = 2.0_f64.ln();
4190        let real = modulus.ln() / ln2;
4191        let imag = b.atan2(a) / ln2;
4192
4193        let result = format_complex(real, imag, suffix);
4194        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(result)))
4195    }
4196}
4197
4198/// Raises a complex number to a real power.
4199///
4200/// Uses polar form and returns the principal-value result as complex text.
4201///
4202/// # Remarks
4203/// - `inumber` is coerced to complex-number text; `n` is numerically coerced.
4204/// - Returns `#NUM!` for undefined zero-power cases such as `0^0` or `0^-1`.
4205/// - Invalid complex text returns `#NUM!`.
4206///
4207/// # Examples
4208/// ```yaml,sandbox
4209/// title: "Square a complex value"
4210/// formula: "=IMPOWER(\"1+i\",2)"
4211/// expected: "2i"
4212/// ```
4213///
4214/// ```yaml,sandbox
4215/// title: "Negative real exponent"
4216/// formula: "=IMPOWER(\"2\",-1)"
4217/// expected: "0.5"
4218/// ```
4219/// ```yaml,docs
4220/// related:
4221///   - IMSQRT
4222///   - IMEXP
4223///   - IMLN
4224/// faq:
4225///   - q: "How does `IMPOWER` handle zero base with non-positive exponent?"
4226///     a: "`0^0` and `0` raised to a negative exponent are treated as undefined and return `#NUM!`."
4227/// ```
4228#[derive(Debug)]
4229pub struct ImPowerFn;
4230/// [formualizer-docgen:schema:start]
4231/// Name: IMPOWER
4232/// Type: ImPowerFn
4233/// Min args: 2
4234/// Max args: 2
4235/// Variadic: false
4236/// Signature: IMPOWER(arg1: any@scalar, arg2: any@scalar)
4237/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}; arg2{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
4238/// Caps: PURE
4239/// [formualizer-docgen:schema:end]
4240impl Function for ImPowerFn {
4241    func_caps!(PURE);
4242    fn name(&self) -> &'static str {
4243        "IMPOWER"
4244    }
4245    fn min_args(&self) -> usize {
4246        2
4247    }
4248    fn arg_schema(&self) -> &'static [ArgSchema] {
4249        &ARG_ANY_TWO[..]
4250    }
4251    fn eval<'a, 'b, 'c>(
4252        &self,
4253        args: &'c [ArgumentHandle<'a, 'b>],
4254        _ctx: &dyn FunctionContext<'b>,
4255    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4256        let inumber = match args[0].value()?.into_literal() {
4257            LiteralValue::Error(e) => {
4258                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
4259            }
4260            other => match coerce_complex_str(&other) {
4261                Ok(s) => s,
4262                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
4263            },
4264        };
4265
4266        let n = match args[1].value()?.into_literal() {
4267            LiteralValue::Error(e) => {
4268                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
4269            }
4270            other => coerce_num(&other)?,
4271        };
4272
4273        let (a, b, suffix) = match parse_complex(&inumber) {
4274            Ok(c) => c,
4275            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
4276        };
4277
4278        let modulus = (a * a + b * b).sqrt();
4279        let theta = b.atan2(a);
4280
4281        // Handle 0^n cases
4282        if modulus < 1e-15 {
4283            if n > 0.0 {
4284                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(
4285                    "0".to_string(),
4286                )));
4287            } else {
4288                // 0^0 or 0^negative is undefined
4289                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4290                    ExcelError::new_num(),
4291                )));
4292            }
4293        }
4294
4295        // z^n = |z|^n * (cos(n*theta) + i*sin(n*theta))
4296        let r_n = modulus.powf(n);
4297        let n_theta = n * theta;
4298        let real = r_n * n_theta.cos();
4299        let imag = r_n * n_theta.sin();
4300
4301        let result = format_complex(real, imag, suffix);
4302        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(result)))
4303    }
4304}
4305
4306/// Returns the principal square root of a complex number.
4307///
4308/// Computes the root in polar form and returns complex text.
4309///
4310/// # Remarks
4311/// - Input is coerced to complex-number text before parsing.
4312/// - Returns the principal branch of the square root.
4313/// - Invalid complex text returns `#NUM!`.
4314///
4315/// # Examples
4316/// ```yaml,sandbox
4317/// title: "Square root of a negative real"
4318/// formula: "=IMSQRT(\"-4\")"
4319/// expected: "2i"
4320/// ```
4321///
4322/// ```yaml,sandbox
4323/// title: "Square root of a+bi"
4324/// formula: "=IMSQRT(\"3+4i\")"
4325/// expected: "2+i"
4326/// ```
4327/// ```yaml,docs
4328/// related:
4329///   - IMPOWER
4330///   - IMABS
4331///   - IMARGUMENT
4332/// faq:
4333///   - q: "Which square root does `IMSQRT` return for complex inputs?"
4334///     a: "It returns the principal branch (half-angle polar form), matching spreadsheet-style principal-value behavior."
4335/// ```
4336#[derive(Debug)]
4337pub struct ImSqrtFn;
4338/// [formualizer-docgen:schema:start]
4339/// Name: IMSQRT
4340/// Type: ImSqrtFn
4341/// Min args: 1
4342/// Max args: 1
4343/// Variadic: false
4344/// Signature: IMSQRT(arg1: any@scalar)
4345/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
4346/// Caps: PURE
4347/// [formualizer-docgen:schema:end]
4348impl Function for ImSqrtFn {
4349    func_caps!(PURE);
4350    fn name(&self) -> &'static str {
4351        "IMSQRT"
4352    }
4353    fn min_args(&self) -> usize {
4354        1
4355    }
4356    fn arg_schema(&self) -> &'static [ArgSchema] {
4357        &ARG_ANY_ONE[..]
4358    }
4359    fn eval<'a, 'b, 'c>(
4360        &self,
4361        args: &'c [ArgumentHandle<'a, 'b>],
4362        _ctx: &dyn FunctionContext<'b>,
4363    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4364        let inumber = match args[0].value()?.into_literal() {
4365            LiteralValue::Error(e) => {
4366                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
4367            }
4368            other => match coerce_complex_str(&other) {
4369                Ok(s) => s,
4370                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
4371            },
4372        };
4373
4374        let (a, b, suffix) = match parse_complex(&inumber) {
4375            Ok(c) => c,
4376            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
4377        };
4378
4379        let modulus = (a * a + b * b).sqrt();
4380        let theta = b.atan2(a);
4381
4382        // sqrt(z) = sqrt(|z|) * (cos(theta/2) + i*sin(theta/2))
4383        let sqrt_r = modulus.sqrt();
4384        let half_theta = theta / 2.0;
4385        let real = sqrt_r * half_theta.cos();
4386        let imag = sqrt_r * half_theta.sin();
4387
4388        let result = format_complex(real, imag, suffix);
4389        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(result)))
4390    }
4391}
4392
4393/// Returns the sine of a complex number.
4394///
4395/// Evaluates complex sine and returns the result as complex text.
4396///
4397/// # Remarks
4398/// - Input is coerced to complex-number text before parsing.
4399/// - Uses hyperbolic components for the imaginary part.
4400/// - Invalid complex text returns `#NUM!`.
4401///
4402/// # Examples
4403/// ```yaml,sandbox
4404/// title: "Sine of zero"
4405/// formula: "=IMSIN(\"0\")"
4406/// expected: "0"
4407/// ```
4408///
4409/// ```yaml,sandbox
4410/// title: "Sine on imaginary axis"
4411/// formula: "=IMSIN(\"i\")"
4412/// expected: "1.1752011936438014i"
4413/// ```
4414/// ```yaml,docs
4415/// related:
4416///   - IMCOS
4417///   - IMEXP
4418/// faq:
4419///   - q: "Why can `IMSIN` return non-zero imaginary output for real-looking formulas?"
4420///     a: "For complex inputs `a+bi`, sine uses hyperbolic terms (`cosh`, `sinh`), so imaginary components are expected."
4421/// ```
4422#[derive(Debug)]
4423pub struct ImSinFn;
4424/// [formualizer-docgen:schema:start]
4425/// Name: IMSIN
4426/// Type: ImSinFn
4427/// Min args: 1
4428/// Max args: 1
4429/// Variadic: false
4430/// Signature: IMSIN(arg1: any@scalar)
4431/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
4432/// Caps: PURE
4433/// [formualizer-docgen:schema:end]
4434impl Function for ImSinFn {
4435    func_caps!(PURE);
4436    fn name(&self) -> &'static str {
4437        "IMSIN"
4438    }
4439    fn min_args(&self) -> usize {
4440        1
4441    }
4442    fn arg_schema(&self) -> &'static [ArgSchema] {
4443        &ARG_ANY_ONE[..]
4444    }
4445    fn eval<'a, 'b, 'c>(
4446        &self,
4447        args: &'c [ArgumentHandle<'a, 'b>],
4448        _ctx: &dyn FunctionContext<'b>,
4449    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4450        let inumber = match args[0].value()?.into_literal() {
4451            LiteralValue::Error(e) => {
4452                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
4453            }
4454            other => match coerce_complex_str(&other) {
4455                Ok(s) => s,
4456                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
4457            },
4458        };
4459
4460        let (a, b, suffix) = match parse_complex(&inumber) {
4461            Ok(c) => c,
4462            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
4463        };
4464
4465        // sin(a+bi) = sin(a)*cosh(b) + i*cos(a)*sinh(b)
4466        let real = a.sin() * b.cosh();
4467        let imag = a.cos() * b.sinh();
4468
4469        let result = format_complex(real, imag, suffix);
4470        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(result)))
4471    }
4472}
4473
4474/// Returns the cosine of a complex number.
4475///
4476/// Evaluates complex cosine and returns the result as complex text.
4477///
4478/// # Remarks
4479/// - Input is coerced to complex-number text before parsing.
4480/// - Uses hyperbolic components for the imaginary part.
4481/// - Invalid complex text returns `#NUM!`.
4482///
4483/// # Examples
4484/// ```yaml,sandbox
4485/// title: "Cosine of zero"
4486/// formula: "=IMCOS(\"0\")"
4487/// expected: "1"
4488/// ```
4489///
4490/// ```yaml,sandbox
4491/// title: "Cosine on imaginary axis"
4492/// formula: "=IMCOS(\"i\")"
4493/// expected: "1.5430806348152437"
4494/// ```
4495/// ```yaml,docs
4496/// related:
4497///   - IMSIN
4498///   - IMEXP
4499/// faq:
4500///   - q: "Why is the imaginary part negated in `IMCOS`?"
4501///     a: "Complex cosine uses `cos(a+bi)=cos(a)cosh(b)-i sin(a)sinh(b)`, so the imaginary term carries a minus sign."
4502/// ```
4503#[derive(Debug)]
4504pub struct ImCosFn;
4505/// [formualizer-docgen:schema:start]
4506/// Name: IMCOS
4507/// Type: ImCosFn
4508/// Min args: 1
4509/// Max args: 1
4510/// Variadic: false
4511/// Signature: IMCOS(arg1: any@scalar)
4512/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
4513/// Caps: PURE
4514/// [formualizer-docgen:schema:end]
4515impl Function for ImCosFn {
4516    func_caps!(PURE);
4517    fn name(&self) -> &'static str {
4518        "IMCOS"
4519    }
4520    fn min_args(&self) -> usize {
4521        1
4522    }
4523    fn arg_schema(&self) -> &'static [ArgSchema] {
4524        &ARG_ANY_ONE[..]
4525    }
4526    fn eval<'a, 'b, 'c>(
4527        &self,
4528        args: &'c [ArgumentHandle<'a, 'b>],
4529        _ctx: &dyn FunctionContext<'b>,
4530    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4531        let inumber = match args[0].value()?.into_literal() {
4532            LiteralValue::Error(e) => {
4533                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
4534            }
4535            other => match coerce_complex_str(&other) {
4536                Ok(s) => s,
4537                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
4538            },
4539        };
4540
4541        let (a, b, suffix) = match parse_complex(&inumber) {
4542            Ok(c) => c,
4543            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
4544        };
4545
4546        // cos(a+bi) = cos(a)*cosh(b) - i*sin(a)*sinh(b)
4547        let real = a.cos() * b.cosh();
4548        let imag = -a.sin() * b.sinh();
4549
4550        let result = format_complex(real, imag, suffix);
4551        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(result)))
4552    }
4553}
4554
4555fn eval_complex_unary<'a, 'b, 'c, F>(
4556    args: &'c [ArgumentHandle<'a, 'b>],
4557    f: F,
4558) -> Result<crate::traits::CalcValue<'b>, ExcelError>
4559where
4560    F: FnOnce(f64, f64, char) -> Result<(f64, f64, char), ExcelError>,
4561{
4562    if args.len() != 1 {
4563        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4564            ExcelError::new_value(),
4565        )));
4566    }
4567    let inumber = match args[0].value()?.into_literal() {
4568        LiteralValue::Error(e) => {
4569            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
4570        }
4571        other => match coerce_complex_str(&other) {
4572            Ok(s) => s,
4573            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
4574        },
4575    };
4576    let (a, b, suffix) = match parse_complex(&inumber) {
4577        Ok(c) => c,
4578        Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
4579    };
4580    let (real, imag, suffix) = f(a, b, suffix)?;
4581    if real.is_nan() || imag.is_nan() || real.is_infinite() || imag.is_infinite() {
4582        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
4583            ExcelError::new_num(),
4584        )));
4585    }
4586    Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(
4587        format_complex(real, imag, suffix),
4588    )))
4589}
4590
4591/// Returns the hyperbolic cosine of a complex number.
4592///
4593/// Accepts an Excel-style complex number string and returns the complex
4594/// hyperbolic cosine as text.
4595///
4596/// # Remarks
4597/// - The input may use either `i` or `j` as the imaginary suffix.
4598/// - Invalid complex text returns `#NUM!`.
4599///
4600/// ```yaml,sandbox
4601/// title: "Hyperbolic cosine of zero"
4602/// formula: '=IMCOSH("0")'
4603/// expected: "1"
4604/// ```
4605///
4606/// ```yaml,docs
4607/// related:
4608///   - IMSINH
4609///   - IMSECH
4610///   - IMCOS
4611/// faq:
4612///   - q: "Does IMCOSH preserve the imaginary suffix?"
4613///     a: "Results preserve the input's i/j suffix when an imaginary part is present."
4614/// ```
4615#[derive(Debug)]
4616pub struct ImCoshFn;
4617/// [formualizer-docgen:schema:start]
4618/// Name: IMCOSH
4619/// Type: ImCoshFn
4620/// Min args: 1
4621/// Max args: 1
4622/// Variadic: false
4623/// Signature: IMCOSH(arg1: any@scalar)
4624/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
4625/// Caps: PURE
4626/// [formualizer-docgen:schema:end]
4627impl Function for ImCoshFn {
4628    func_caps!(PURE);
4629    fn name(&self) -> &'static str {
4630        "IMCOSH"
4631    }
4632    fn min_args(&self) -> usize {
4633        1
4634    }
4635    fn arg_schema(&self) -> &'static [ArgSchema] {
4636        &ARG_ANY_ONE[..]
4637    }
4638    fn eval<'a, 'b, 'c>(
4639        &self,
4640        args: &'c [ArgumentHandle<'a, 'b>],
4641        _ctx: &dyn FunctionContext<'b>,
4642    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4643        eval_complex_unary(args, |a, b, suffix| {
4644            Ok((a.cosh() * b.cos(), a.sinh() * b.sin(), suffix))
4645        })
4646    }
4647}
4648
4649/// Returns the hyperbolic sine of a complex number.
4650///
4651/// Accepts an Excel-style complex number string and returns the complex
4652/// hyperbolic sine as text.
4653///
4654/// ```yaml,sandbox
4655/// title: "Hyperbolic sine of zero"
4656/// formula: '=IMSINH("0")'
4657/// expected: "0"
4658/// ```
4659///
4660/// ```yaml,docs
4661/// related:
4662///   - IMCOSH
4663///   - IMTAN
4664///   - IMSIN
4665/// faq:
4666///   - q: "What input format is accepted?"
4667///     a: 'Use standard spreadsheet complex-number text such as "1+2i" or "1+2j".'
4668/// ```
4669#[derive(Debug)]
4670pub struct ImSinhFn;
4671/// [formualizer-docgen:schema:start]
4672/// Name: IMSINH
4673/// Type: ImSinhFn
4674/// Min args: 1
4675/// Max args: 1
4676/// Variadic: false
4677/// Signature: IMSINH(arg1: any@scalar)
4678/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
4679/// Caps: PURE
4680/// [formualizer-docgen:schema:end]
4681impl Function for ImSinhFn {
4682    func_caps!(PURE);
4683    fn name(&self) -> &'static str {
4684        "IMSINH"
4685    }
4686    fn min_args(&self) -> usize {
4687        1
4688    }
4689    fn arg_schema(&self) -> &'static [ArgSchema] {
4690        &ARG_ANY_ONE[..]
4691    }
4692    fn eval<'a, 'b, 'c>(
4693        &self,
4694        args: &'c [ArgumentHandle<'a, 'b>],
4695        _ctx: &dyn FunctionContext<'b>,
4696    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4697        eval_complex_unary(args, |a, b, suffix| {
4698            Ok((a.sinh() * b.cos(), a.cosh() * b.sin(), suffix))
4699        })
4700    }
4701}
4702
4703/// Returns the secant of a complex number.
4704///
4705/// Computes `1 / IMCOS(inumber)` for an Excel-style complex number string.
4706///
4707/// ```yaml,sandbox
4708/// title: "Secant of zero"
4709/// formula: '=IMSEC("0")'
4710/// expected: "1"
4711/// ```
4712///
4713/// ```yaml,docs
4714/// related:
4715///   - IMCOS
4716///   - IMSECH
4717///   - IMCSC
4718/// faq:
4719///   - q: "How are poles represented?"
4720///     a: "Inputs that lead to non-finite results return #NUM!."
4721/// ```
4722#[derive(Debug)]
4723pub struct ImSecFn;
4724/// [formualizer-docgen:schema:start]
4725/// Name: IMSEC
4726/// Type: ImSecFn
4727/// Min args: 1
4728/// Max args: 1
4729/// Variadic: false
4730/// Signature: IMSEC(arg1: any@scalar)
4731/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
4732/// Caps: PURE
4733/// [formualizer-docgen:schema:end]
4734impl Function for ImSecFn {
4735    func_caps!(PURE);
4736    fn name(&self) -> &'static str {
4737        "IMSEC"
4738    }
4739    fn min_args(&self) -> usize {
4740        1
4741    }
4742    fn arg_schema(&self) -> &'static [ArgSchema] {
4743        &ARG_ANY_ONE[..]
4744    }
4745    fn eval<'a, 'b, 'c>(
4746        &self,
4747        args: &'c [ArgumentHandle<'a, 'b>],
4748        _ctx: &dyn FunctionContext<'b>,
4749    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4750        eval_complex_unary(args, |a, b, suffix| {
4751            let cos_a = a.cos();
4752            let sin_a = a.sin();
4753            let cosh_b = b.cosh();
4754            let sinh_b = b.sinh();
4755            let denom = cos_a * cos_a * cosh_b * cosh_b + sin_a * sin_a * sinh_b * sinh_b;
4756            Ok((cos_a * cosh_b / denom, sin_a * sinh_b / denom, suffix))
4757        })
4758    }
4759}
4760
4761/// Returns the hyperbolic secant of a complex number.
4762///
4763/// Computes `1 / IMCOSH(inumber)` for an Excel-style complex number string.
4764///
4765/// ```yaml,sandbox
4766/// title: "Hyperbolic secant of zero"
4767/// formula: '=IMSECH("0")'
4768/// expected: "1"
4769/// ```
4770///
4771/// ```yaml,docs
4772/// related:
4773///   - IMCOSH
4774///   - IMSEC
4775///   - IMCSCH
4776/// faq:
4777///   - q: "What does IMSECH return?"
4778///     a: "It returns a complex number encoded as spreadsheet text."
4779/// ```
4780#[derive(Debug)]
4781pub struct ImSechFn;
4782/// [formualizer-docgen:schema:start]
4783/// Name: IMSECH
4784/// Type: ImSechFn
4785/// Min args: 1
4786/// Max args: 1
4787/// Variadic: false
4788/// Signature: IMSECH(arg1: any@scalar)
4789/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
4790/// Caps: PURE
4791/// [formualizer-docgen:schema:end]
4792impl Function for ImSechFn {
4793    func_caps!(PURE);
4794    fn name(&self) -> &'static str {
4795        "IMSECH"
4796    }
4797    fn min_args(&self) -> usize {
4798        1
4799    }
4800    fn arg_schema(&self) -> &'static [ArgSchema] {
4801        &ARG_ANY_ONE[..]
4802    }
4803    fn eval<'a, 'b, 'c>(
4804        &self,
4805        args: &'c [ArgumentHandle<'a, 'b>],
4806        _ctx: &dyn FunctionContext<'b>,
4807    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4808        eval_complex_unary(args, |a, b, suffix| {
4809            let cosh_a = a.cosh();
4810            let sinh_a = a.sinh();
4811            let cos_b = b.cos();
4812            let sin_b = b.sin();
4813            let denom = cosh_a * cosh_a * cos_b * cos_b + sinh_a * sinh_a * sin_b * sin_b;
4814            Ok((cosh_a * cos_b / denom, -sinh_a * sin_b / denom, suffix))
4815        })
4816    }
4817}
4818
4819/// Returns the cosecant of a complex number.
4820///
4821/// Computes `1 / IMSIN(inumber)` for an Excel-style complex number string.
4822///
4823/// ```yaml,sandbox
4824/// title: "Cosecant of pi over two"
4825/// formula: '=IMCSC("1.5707963267948966")'
4826/// expected: "1"
4827/// ```
4828///
4829/// ```yaml,docs
4830/// related:
4831///   - IMSIN
4832///   - IMCSCH
4833///   - IMSEC
4834/// faq:
4835///   - q: "What happens at a pole?"
4836///     a: "Inputs such as zero that make the reciprocal undefined return #NUM!."
4837/// ```
4838#[derive(Debug)]
4839pub struct ImCscFn;
4840/// [formualizer-docgen:schema:start]
4841/// Name: IMCSC
4842/// Type: ImCscFn
4843/// Min args: 1
4844/// Max args: 1
4845/// Variadic: false
4846/// Signature: IMCSC(arg1: any@scalar)
4847/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
4848/// Caps: PURE
4849/// [formualizer-docgen:schema:end]
4850impl Function for ImCscFn {
4851    func_caps!(PURE);
4852    fn name(&self) -> &'static str {
4853        "IMCSC"
4854    }
4855    fn min_args(&self) -> usize {
4856        1
4857    }
4858    fn arg_schema(&self) -> &'static [ArgSchema] {
4859        &ARG_ANY_ONE[..]
4860    }
4861    fn eval<'a, 'b, 'c>(
4862        &self,
4863        args: &'c [ArgumentHandle<'a, 'b>],
4864        _ctx: &dyn FunctionContext<'b>,
4865    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4866        eval_complex_unary(args, |a, b, suffix| {
4867            let cos_a = a.cos();
4868            let sin_a = a.sin();
4869            let cosh_b = b.cosh();
4870            let sinh_b = b.sinh();
4871            let denom = sin_a * sin_a * cosh_b * cosh_b + cos_a * cos_a * sinh_b * sinh_b;
4872            Ok((sin_a * cosh_b / denom, -cos_a * sinh_b / denom, suffix))
4873        })
4874    }
4875}
4876
4877/// Returns the hyperbolic cosecant of a complex number.
4878///
4879/// Computes `1 / IMSINH(inumber)` for an Excel-style complex number string.
4880///
4881/// ```yaml,sandbox
4882/// title: "Hyperbolic cosecant of one"
4883/// formula: '=IMCSCH("1")'
4884/// expected: "0.8509181282393216"
4885/// ```
4886///
4887/// ```yaml,docs
4888/// related:
4889///   - IMSINH
4890///   - IMCSC
4891///   - IMSECH
4892/// faq:
4893///   - q: "Can IMCSCH return #NUM!?"
4894///     a: "Yes. Undefined reciprocal results return #NUM!."
4895/// ```
4896#[derive(Debug)]
4897pub struct ImCschFn;
4898/// [formualizer-docgen:schema:start]
4899/// Name: IMCSCH
4900/// Type: ImCschFn
4901/// Min args: 1
4902/// Max args: 1
4903/// Variadic: false
4904/// Signature: IMCSCH(arg1: any@scalar)
4905/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
4906/// Caps: PURE
4907/// [formualizer-docgen:schema:end]
4908impl Function for ImCschFn {
4909    func_caps!(PURE);
4910    fn name(&self) -> &'static str {
4911        "IMCSCH"
4912    }
4913    fn min_args(&self) -> usize {
4914        1
4915    }
4916    fn arg_schema(&self) -> &'static [ArgSchema] {
4917        &ARG_ANY_ONE[..]
4918    }
4919    fn eval<'a, 'b, 'c>(
4920        &self,
4921        args: &'c [ArgumentHandle<'a, 'b>],
4922        _ctx: &dyn FunctionContext<'b>,
4923    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4924        eval_complex_unary(args, |a, b, suffix| {
4925            let cosh_a = a.cosh();
4926            let sinh_a = a.sinh();
4927            let cos_b = b.cos();
4928            let sin_b = b.sin();
4929            let denom = sinh_a * sinh_a * cos_b * cos_b + cosh_a * cosh_a * sin_b * sin_b;
4930            Ok((sinh_a * cos_b / denom, -cosh_a * sin_b / denom, suffix))
4931        })
4932    }
4933}
4934
4935/// Returns the tangent of a complex number.
4936///
4937/// Accepts an Excel-style complex number string and returns the complex tangent
4938/// as text.
4939///
4940/// ```yaml,sandbox
4941/// title: "Tangent of zero"
4942/// formula: '=IMTAN("0")'
4943/// expected: "0"
4944/// ```
4945///
4946/// ```yaml,docs
4947/// related:
4948///   - IMSIN
4949///   - IMCOS
4950///   - IMCOT
4951/// faq:
4952///   - q: "How is IMTAN related to sine and cosine?"
4953///     a: "It computes the complex tangent, equivalent to IMSIN divided by IMCOS."
4954/// ```
4955#[derive(Debug)]
4956pub struct ImTanFn;
4957/// [formualizer-docgen:schema:start]
4958/// Name: IMTAN
4959/// Type: ImTanFn
4960/// Min args: 1
4961/// Max args: 1
4962/// Variadic: false
4963/// Signature: IMTAN(arg1: any@scalar)
4964/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
4965/// Caps: PURE
4966/// [formualizer-docgen:schema:end]
4967impl Function for ImTanFn {
4968    func_caps!(PURE);
4969    fn name(&self) -> &'static str {
4970        "IMTAN"
4971    }
4972    fn min_args(&self) -> usize {
4973        1
4974    }
4975    fn arg_schema(&self) -> &'static [ArgSchema] {
4976        &ARG_ANY_ONE[..]
4977    }
4978    fn eval<'a, 'b, 'c>(
4979        &self,
4980        args: &'c [ArgumentHandle<'a, 'b>],
4981        _ctx: &dyn FunctionContext<'b>,
4982    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
4983        eval_complex_unary(args, |a, b, suffix| {
4984            let tan_a = a.tan();
4985            let tanh_b = b.tanh();
4986            let denom = 1.0 + tan_a * tan_a * tanh_b * tanh_b;
4987            Ok((
4988                (tan_a - tan_a * tanh_b * tanh_b) / denom,
4989                (tanh_b + tan_a * tan_a * tanh_b) / denom,
4990                suffix,
4991            ))
4992        })
4993    }
4994}
4995
4996/// Returns the cotangent of a complex number.
4997///
4998/// Computes the reciprocal of the complex tangent for an Excel-style complex
4999/// number string.
5000///
5001/// ```yaml,sandbox
5002/// title: "Cotangent of one"
5003/// formula: '=IMCOT("1")'
5004/// expected: "0.6420926159343306"
5005/// ```
5006///
5007/// ```yaml,docs
5008/// related:
5009///   - IMTAN
5010///   - IMCOS
5011///   - IMSIN
5012/// faq:
5013///   - q: "What happens at undefined inputs?"
5014///     a: "Inputs that produce non-finite reciprocal results return #NUM!."
5015/// ```
5016#[derive(Debug)]
5017pub struct ImCotFn;
5018/// [formualizer-docgen:schema:start]
5019/// Name: IMCOT
5020/// Type: ImCotFn
5021/// Min args: 1
5022/// Max args: 1
5023/// Variadic: false
5024/// Signature: IMCOT(arg1: any@scalar)
5025/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
5026/// Caps: PURE
5027/// [formualizer-docgen:schema:end]
5028impl Function for ImCotFn {
5029    func_caps!(PURE);
5030    fn name(&self) -> &'static str {
5031        "IMCOT"
5032    }
5033    fn min_args(&self) -> usize {
5034        1
5035    }
5036    fn arg_schema(&self) -> &'static [ArgSchema] {
5037        &ARG_ANY_ONE[..]
5038    }
5039    fn eval<'a, 'b, 'c>(
5040        &self,
5041        args: &'c [ArgumentHandle<'a, 'b>],
5042        _ctx: &dyn FunctionContext<'b>,
5043    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5044        eval_complex_unary(args, |a, b, suffix| {
5045            if a == 0.0 && b != 0.0 {
5046                return Ok((0.0, -1.0 / b.tanh(), suffix));
5047            }
5048            if b == 0.0 {
5049                return Ok((1.0 / a.tan(), 0.0, suffix));
5050            }
5051            let cot_a = 1.0 / a.tan();
5052            let coth_b = 1.0 / b.tanh();
5053            let denom = cot_a * cot_a + coth_b * coth_b;
5054            Ok((
5055                (cot_a * coth_b * coth_b - cot_a) / denom,
5056                (-cot_a * cot_a * coth_b - coth_b) / denom,
5057                suffix,
5058            ))
5059        })
5060    }
5061}
5062
5063/* ─────────────────────────── Unit Conversion (CONVERT) ──────────────────────────── */
5064
5065/// Unit categories for CONVERT function
5066#[derive(Clone, Copy, PartialEq, Eq, Debug)]
5067enum UnitCategory {
5068    Length,
5069    Mass,
5070    Temperature,
5071}
5072
5073/// Information about a unit
5074struct UnitInfo {
5075    category: UnitCategory,
5076    /// Conversion factor to base unit (meters for length, grams for mass)
5077    /// For temperature, this is special-cased
5078    to_base: f64,
5079}
5080
5081/// Get unit info for a given unit string
5082fn get_unit_info(unit: &str) -> Option<UnitInfo> {
5083    // Length units (base: meter)
5084    match unit {
5085        // Metric length
5086        "m" => Some(UnitInfo {
5087            category: UnitCategory::Length,
5088            to_base: 1.0,
5089        }),
5090        "km" => Some(UnitInfo {
5091            category: UnitCategory::Length,
5092            to_base: 1000.0,
5093        }),
5094        "cm" => Some(UnitInfo {
5095            category: UnitCategory::Length,
5096            to_base: 0.01,
5097        }),
5098        "mm" => Some(UnitInfo {
5099            category: UnitCategory::Length,
5100            to_base: 0.001,
5101        }),
5102        // Imperial length
5103        "mi" => Some(UnitInfo {
5104            category: UnitCategory::Length,
5105            to_base: 1609.344,
5106        }),
5107        "ft" => Some(UnitInfo {
5108            category: UnitCategory::Length,
5109            to_base: 0.3048,
5110        }),
5111        "in" => Some(UnitInfo {
5112            category: UnitCategory::Length,
5113            to_base: 0.0254,
5114        }),
5115        "yd" => Some(UnitInfo {
5116            category: UnitCategory::Length,
5117            to_base: 0.9144,
5118        }),
5119        "Nmi" => Some(UnitInfo {
5120            category: UnitCategory::Length,
5121            to_base: 1852.0,
5122        }),
5123
5124        // Mass units (base: gram)
5125        "g" => Some(UnitInfo {
5126            category: UnitCategory::Mass,
5127            to_base: 1.0,
5128        }),
5129        "kg" => Some(UnitInfo {
5130            category: UnitCategory::Mass,
5131            to_base: 1000.0,
5132        }),
5133        "mg" => Some(UnitInfo {
5134            category: UnitCategory::Mass,
5135            to_base: 0.001,
5136        }),
5137        "lbm" => Some(UnitInfo {
5138            category: UnitCategory::Mass,
5139            to_base: 453.59237,
5140        }),
5141        "oz" => Some(UnitInfo {
5142            category: UnitCategory::Mass,
5143            to_base: 28.349523125,
5144        }),
5145        "ozm" => Some(UnitInfo {
5146            category: UnitCategory::Mass,
5147            to_base: 28.349523125,
5148        }),
5149        "ton" => Some(UnitInfo {
5150            category: UnitCategory::Mass,
5151            to_base: 907184.74,
5152        }),
5153
5154        // Temperature units (special handling)
5155        "C" | "cel" => Some(UnitInfo {
5156            category: UnitCategory::Temperature,
5157            to_base: 0.0, // Special-cased
5158        }),
5159        "F" | "fah" => Some(UnitInfo {
5160            category: UnitCategory::Temperature,
5161            to_base: 0.0, // Special-cased
5162        }),
5163        "K" | "kel" => Some(UnitInfo {
5164            category: UnitCategory::Temperature,
5165            to_base: 0.0, // Special-cased
5166        }),
5167
5168        _ => None,
5169    }
5170}
5171
5172/// Normalize temperature unit name
5173fn normalize_temp_unit(unit: &str) -> &str {
5174    match unit {
5175        "C" | "cel" => "C",
5176        "F" | "fah" => "F",
5177        "K" | "kel" => "K",
5178        _ => unit,
5179    }
5180}
5181
5182/// Convert temperature between units
5183fn convert_temperature(value: f64, from: &str, to: &str) -> f64 {
5184    let from = normalize_temp_unit(from);
5185    let to = normalize_temp_unit(to);
5186
5187    if from == to {
5188        return value;
5189    }
5190
5191    // First convert to Celsius
5192    let celsius = match from {
5193        "C" => value,
5194        "F" => (value - 32.0) * 5.0 / 9.0,
5195        "K" => value - 273.15,
5196        _ => value,
5197    };
5198
5199    // Then convert from Celsius to target
5200    match to {
5201        "C" => celsius,
5202        "F" => celsius * 9.0 / 5.0 + 32.0,
5203        "K" => celsius + 273.15,
5204        _ => celsius,
5205    }
5206}
5207
5208/// Convert a value between units
5209fn convert_units(value: f64, from: &str, to: &str) -> Result<f64, ExcelError> {
5210    let from_info = get_unit_info(from).ok_or_else(ExcelError::new_na)?;
5211    let to_info = get_unit_info(to).ok_or_else(ExcelError::new_na)?;
5212
5213    // Check category compatibility
5214    if from_info.category != to_info.category {
5215        return Err(ExcelError::new_na());
5216    }
5217
5218    // Handle temperature specially
5219    if from_info.category == UnitCategory::Temperature {
5220        return Ok(convert_temperature(value, from, to));
5221    }
5222
5223    // For other units: convert to base, then to target
5224    let base_value = value * from_info.to_base;
5225    Ok(base_value / to_info.to_base)
5226}
5227
5228/// Converts a numeric value from one supported unit to another.
5229///
5230/// Supports a focused set of length, mass, and temperature units.
5231///
5232/// # Remarks
5233/// - `number` is numerically coerced; unit arguments must be text.
5234/// - Returns `#N/A` for unknown units or incompatible unit categories.
5235/// - Temperature conversions support `C/cel`, `F/fah`, and `K/kel`.
5236///
5237/// # Examples
5238/// ```yaml,sandbox
5239/// title: "Length conversion"
5240/// formula: "=CONVERT(1,\"km\",\"m\")"
5241/// expected: 1000
5242/// ```
5243///
5244/// ```yaml,sandbox
5245/// title: "Temperature conversion"
5246/// formula: "=CONVERT(32,\"F\",\"C\")"
5247/// expected: 0
5248/// ```
5249/// ```yaml,docs
5250/// related:
5251///   - DEC2BIN
5252///   - DEC2HEX
5253///   - DEC2OCT
5254/// faq:
5255///   - q: "When does `CONVERT` return `#N/A`?"
5256///     a: "Unknown unit tokens, non-text unit arguments, or mixing incompatible categories (for example length to mass) return `#N/A`."
5257/// ```
5258#[derive(Debug)]
5259pub struct ConvertFn;
5260/// [formualizer-docgen:schema:start]
5261/// Name: CONVERT
5262/// Type: ConvertFn
5263/// Min args: 3
5264/// Max args: 3
5265/// Variadic: false
5266/// Signature: CONVERT(arg1: any@scalar, arg2: any@scalar, arg3: any@scalar)
5267/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}; arg2{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}; arg3{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
5268/// Caps: PURE
5269/// [formualizer-docgen:schema:end]
5270impl Function for ConvertFn {
5271    func_caps!(PURE);
5272    fn name(&self) -> &'static str {
5273        "CONVERT"
5274    }
5275    fn min_args(&self) -> usize {
5276        3
5277    }
5278    fn arg_schema(&self) -> &'static [ArgSchema] {
5279        &ARG_COMPLEX_THREE[..]
5280    }
5281    fn eval<'a, 'b, 'c>(
5282        &self,
5283        args: &'c [ArgumentHandle<'a, 'b>],
5284        _ctx: &dyn FunctionContext<'b>,
5285    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
5286        // Get the number value
5287        let value = match args[0].value()?.into_literal() {
5288            LiteralValue::Error(e) => {
5289                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
5290            }
5291            other => coerce_num(&other)?,
5292        };
5293
5294        // Get from_unit
5295        let from_unit = match args[1].value()?.into_literal() {
5296            LiteralValue::Error(e) => {
5297                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
5298            }
5299            LiteralValue::Text(s) => s,
5300            _ => {
5301                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5302                    ExcelError::new_na(),
5303                )));
5304            }
5305        };
5306
5307        // Get to_unit
5308        let to_unit = match args[2].value()?.into_literal() {
5309            LiteralValue::Error(e) => {
5310                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
5311            }
5312            LiteralValue::Text(s) => s,
5313            _ => {
5314                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
5315                    ExcelError::new_na(),
5316                )));
5317            }
5318        };
5319
5320        match convert_units(value, &from_unit, &to_unit) {
5321            Ok(result) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
5322                result,
5323            ))),
5324            Err(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
5325        }
5326    }
5327}
5328
5329pub fn register_builtins() {
5330    use std::sync::Arc;
5331    crate::function_registry::register_function(Arc::new(BitAndFn));
5332    crate::function_registry::register_function(Arc::new(BitOrFn));
5333    crate::function_registry::register_function(Arc::new(BitXorFn));
5334    crate::function_registry::register_function(Arc::new(BitLShiftFn));
5335    crate::function_registry::register_function(Arc::new(BitRShiftFn));
5336    crate::function_registry::register_function(Arc::new(Bin2DecFn));
5337    crate::function_registry::register_function(Arc::new(Dec2BinFn));
5338    crate::function_registry::register_function(Arc::new(Hex2DecFn));
5339    crate::function_registry::register_function(Arc::new(Dec2HexFn));
5340    crate::function_registry::register_function(Arc::new(Oct2DecFn));
5341    crate::function_registry::register_function(Arc::new(Dec2OctFn));
5342    crate::function_registry::register_function(Arc::new(Bin2HexFn));
5343    crate::function_registry::register_function(Arc::new(Hex2BinFn));
5344    crate::function_registry::register_function(Arc::new(Bin2OctFn));
5345    crate::function_registry::register_function(Arc::new(Oct2BinFn));
5346    crate::function_registry::register_function(Arc::new(Hex2OctFn));
5347    crate::function_registry::register_function(Arc::new(Oct2HexFn));
5348    crate::function_registry::register_function(Arc::new(DeltaFn));
5349    crate::function_registry::register_function(Arc::new(GestepFn));
5350    crate::function_registry::register_function(Arc::new(ErfFn));
5351    crate::function_registry::register_function(Arc::new(ErfcFn));
5352    crate::function_registry::register_function(Arc::new(ErfPreciseFn));
5353    crate::function_registry::register_function(Arc::new(ErfcPreciseFn));
5354    crate::function_registry::register_function(Arc::new(BesselIFn));
5355    crate::function_registry::register_function(Arc::new(BesselJFn));
5356    crate::function_registry::register_function(Arc::new(BesselKFn));
5357    crate::function_registry::register_function(Arc::new(BesselYFn));
5358    // Complex number functions
5359    crate::function_registry::register_function(Arc::new(ComplexFn));
5360    crate::function_registry::register_function(Arc::new(ImRealFn));
5361    crate::function_registry::register_function(Arc::new(ImaginaryFn));
5362    crate::function_registry::register_function(Arc::new(ImAbsFn));
5363    crate::function_registry::register_function(Arc::new(ImArgumentFn));
5364    crate::function_registry::register_function(Arc::new(ImConjugateFn));
5365    crate::function_registry::register_function(Arc::new(ImSumFn));
5366    crate::function_registry::register_function(Arc::new(ImSubFn));
5367    crate::function_registry::register_function(Arc::new(ImProductFn));
5368    crate::function_registry::register_function(Arc::new(ImDivFn));
5369    // Complex number math functions
5370    crate::function_registry::register_function(Arc::new(ImExpFn));
5371    crate::function_registry::register_function(Arc::new(ImLnFn));
5372    crate::function_registry::register_function(Arc::new(ImLog10Fn));
5373    crate::function_registry::register_function(Arc::new(ImLog2Fn));
5374    crate::function_registry::register_function(Arc::new(ImPowerFn));
5375    crate::function_registry::register_function(Arc::new(ImSqrtFn));
5376    crate::function_registry::register_function(Arc::new(ImSinFn));
5377    crate::function_registry::register_function(Arc::new(ImCosFn));
5378    crate::function_registry::register_function(Arc::new(ImCoshFn));
5379    crate::function_registry::register_function(Arc::new(ImCotFn));
5380    crate::function_registry::register_function(Arc::new(ImCscFn));
5381    crate::function_registry::register_function(Arc::new(ImCschFn));
5382    crate::function_registry::register_function(Arc::new(ImSecFn));
5383    crate::function_registry::register_function(Arc::new(ImSechFn));
5384    crate::function_registry::register_function(Arc::new(ImSinhFn));
5385    crate::function_registry::register_function(Arc::new(ImTanFn));
5386    // Unit conversion
5387    crate::function_registry::register_function(Arc::new(ConvertFn));
5388}
5389
5390#[cfg(test)]
5391mod tests {
5392    use super::*;
5393    use crate::builtins::text::ValueFn;
5394    use crate::test_workbook::TestWorkbook;
5395    use formualizer_common::{ExcelErrorKind, LiteralValue};
5396    use formualizer_parse::parser::parse;
5397    use std::sync::Arc;
5398
5399    fn eval(formula: &str) -> LiteralValue {
5400        let wb = TestWorkbook::new()
5401            .with_function(Arc::new(ErfcFn))
5402            .with_function(Arc::new(ErfcPreciseFn))
5403            .with_function(Arc::new(BesselIFn))
5404            .with_function(Arc::new(BesselJFn))
5405            .with_function(Arc::new(BesselKFn))
5406            .with_function(Arc::new(BesselYFn))
5407            .with_function(Arc::new(ImCoshFn))
5408            .with_function(Arc::new(ImCotFn))
5409            .with_function(Arc::new(ImCscFn))
5410            .with_function(Arc::new(ImCschFn))
5411            .with_function(Arc::new(ImSecFn))
5412            .with_function(Arc::new(ImSechFn))
5413            .with_function(Arc::new(ImSinhFn))
5414            .with_function(Arc::new(ImTanFn))
5415            .with_function(Arc::new(ValueFn));
5416        let interp = wb.interpreter();
5417        let ast = parse(formula).expect("parse");
5418        interp.evaluate_ast(&ast).expect("eval").into_literal()
5419    }
5420
5421    fn assert_number_close(value: LiteralValue, expected: f64) {
5422        match value {
5423            LiteralValue::Number(n) => assert!((n - expected).abs() < 1e-12, "{n} != {expected}"),
5424            other => panic!("expected number, got {other:?}"),
5425        }
5426    }
5427
5428    fn assert_number_rel_close(value: LiteralValue, expected: f64, tol: f64) {
5429        match value {
5430            LiteralValue::Number(n) => {
5431                let denom = (n * n + expected * expected).sqrt().max(1.0);
5432                assert!((n - expected).abs() / denom < tol, "{n} != {expected}");
5433            }
5434            other => panic!("expected number, got {other:?}"),
5435        }
5436    }
5437
5438    #[test]
5439    fn erfc_precise_matches_erfc() {
5440        assert_number_close(eval("=ERFC.PRECISE(1)"), erfc_direct(1.0));
5441        assert_eq!(eval("=ERFC.PRECISE(0)"), LiteralValue::Number(1.0));
5442        // Numeric text is accepted through standard function coercion.
5443        assert_number_close(eval("=ERFC.PRECISE(\"1\")"), erfc_direct(1.0));
5444    }
5445
5446    #[test]
5447    fn bessel_functions_match_known_values_and_excel_order_truncation() {
5448        assert_number_rel_close(eval("=BESSELI(0.5,1)"), 0.2578943053908963, 1e-6);
5449        assert_number_rel_close(eval("=BESSELI(0.5,1.9)"), 0.2578943053908963, 1e-6);
5450        assert_number_rel_close(eval("=BESSELJ(0.5,3)"), 0.002563729994587244, 1e-13);
5451        assert_number_rel_close(eval("=BESSELK(0.5,1)"), 1.656441120003301, 1e-6);
5452        assert_number_rel_close(eval("=BESSELY(0.5,3)"), -42.059494304723883, 1e-13);
5453    }
5454
5455    #[test]
5456    fn bessel_functions_map_invalid_domains_to_num() {
5457        assert!(
5458            matches!(eval("=BESSELJ(1,-1)"), LiteralValue::Error(e) if e.kind == ExcelErrorKind::Num)
5459        );
5460        assert!(
5461            matches!(eval("=BESSELY(1,-1)"), LiteralValue::Error(e) if e.kind == ExcelErrorKind::Num)
5462        );
5463        assert!(
5464            matches!(eval("=BESSELK(0,1)"), LiteralValue::Error(e) if e.kind == ExcelErrorKind::Num)
5465        );
5466        assert!(
5467            matches!(eval("=BESSELY(-1,1)"), LiteralValue::Error(e) if e.kind == ExcelErrorKind::Num)
5468        );
5469    }
5470
5471    #[test]
5472    fn complex_hyperbolic_functions() {
5473        assert_eq!(eval("=IMCOSH(\"0\")"), LiteralValue::Text("1".into()));
5474        assert_eq!(eval("=IMSINH(\"0\")"), LiteralValue::Text("0".into()));
5475        assert_eq!(eval("=IMSECH(\"0\")"), LiteralValue::Text("1".into()));
5476        assert_eq!(eval("=IMTAN(\"0\")"), LiteralValue::Text("0".into()));
5477    }
5478
5479    #[test]
5480    fn complex_reciprocal_trig_functions() {
5481        assert_eq!(eval("=IMSEC(\"0\")"), LiteralValue::Text("1".into()));
5482        assert_eq!(
5483            eval("=IMCOT(\"1\")"),
5484            LiteralValue::Text(format_complex(1.0 / 1.0f64.tan(), 0.0, 'i'))
5485        );
5486        assert_eq!(
5487            eval("=IMCSC(\"1.5707963267948966\")"),
5488            LiteralValue::Text("1".into())
5489        );
5490    }
5491
5492    #[test]
5493    fn complex_functions_preserve_j_suffix_and_error_on_poles() {
5494        match eval("=IMSINH(\"j\")") {
5495            LiteralValue::Text(s) => assert!(s.ends_with('j'), "expected j suffix, got {s}"),
5496            other => panic!("expected text, got {other:?}"),
5497        }
5498        let pole = eval("=IMCSC(\"0\")");
5499        assert!(matches!(pole, LiteralValue::Error(e) if e.kind == ExcelErrorKind::Num));
5500    }
5501}