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}