Skip to main content

truecalc_core/eval/functions/engineering/complex/
mod.rs

1use crate::eval::coercion::{to_number, to_string_val};
2use crate::eval::functions::check_arity;
3use crate::types::{ErrorKind, Value};
4
5// ── Internal complex type ─────────────────────────────────────────────────────
6
7#[derive(Clone, Copy, Debug, PartialEq)]
8pub(super) struct Complex {
9    pub re: f64,
10    pub im: f64,
11}
12
13impl Complex {
14    fn new(re: f64, im: f64) -> Self {
15        Self { re, im }
16    }
17
18    fn abs(self) -> f64 {
19        libm::sqrt(self.re * self.re + self.im * self.im)
20    }
21
22    fn arg(self) -> f64 {
23        libm::atan2(self.im, self.re)
24    }
25
26    fn mul(self, rhs: Self) -> Self {
27        Self {
28            re: self.re * rhs.re - self.im * rhs.im,
29            im: self.re * rhs.im + self.im * rhs.re,
30        }
31    }
32
33    fn pow(self, n: Self) -> Option<Self> {
34        // c^n = exp(n * ln(c))
35        let r = self.abs();
36        if r == 0.0 {
37            // 0^0 = 1 by convention; 0^n = 0 for n != 0
38            if n.re == 0.0 && n.im == 0.0 {
39                return Some(Complex::new(1.0, 0.0));
40            }
41            if n.re > 0.0 {
42                return Some(Complex::new(0.0, 0.0));
43            }
44            return None; // 0^negative
45        }
46        let theta = self.arg();
47        let ln_r = libm::log(r);
48        // ln(c) = ln_r + i*theta
49        // n * ln(c) = (n.re*ln_r - n.im*theta) + i*(n.im*ln_r + n.re*theta)
50        let exp_re = n.re * ln_r - n.im * theta;
51        let exp_im = n.im * ln_r + n.re * theta;
52        let scale = libm::exp(exp_re);
53        Some(Complex::new(scale * libm::cos(exp_im), scale * libm::sin(exp_im)))
54    }
55
56    fn sqrt(self) -> Self {
57        // Principal square root.
58        //
59        // For pure negative reals (im==0, re<0) GS uses the polar/trig form which
60        // yields a tiny non-zero real part (cos(pi/2) ≈ 6.12e-17). Conformance
61        // fixtures expect that floating-point residual, so we match it here.
62        //
63        // For all other inputs the algebraic formula avoids trig round-off --
64        // most importantly IMSQRT("4j"): both components come out as sqrt(2)
65        // exactly rather than differing by one ULP.
66        let r = self.abs();
67        if r == 0.0 {
68            return Complex::new(0.0, 0.0);
69        }
70        if self.im == 0.0 && self.re < 0.0 {
71            // Polar/trig form preserves the cos(pi/2) residual GS shows.
72            let theta = self.arg(); // = pi
73            let sqrt_r = libm::sqrt(r);
74            return Complex::new(sqrt_r * libm::cos(theta / 2.0), sqrt_r * libm::sin(theta / 2.0));
75        }
76        // Algebraic form: sqrt((|z|+re)/2) + i*sign(im)*sqrt((|z|-re)/2)
77        let re_out = libm::sqrt((r + self.re) / 2.0);
78        let im_out = libm::sqrt((r - self.re) / 2.0);
79        let im_out = if self.im < 0.0 { -im_out } else { im_out };
80        Complex::new(re_out, im_out)
81    }
82
83    fn ln(self) -> Option<Self> {
84        let r = self.abs();
85        if r == 0.0 {
86            return None;
87        }
88        Some(Complex::new(libm::log(r), self.arg()))
89    }
90}
91
92// ── Complex string parsing / formatting ──────────────────────────────────────
93
94/// Parse a complex number string like "3+4i", "3-4i", "i", "-i", "5", "2j", etc.
95/// Returns None on parse failure.
96pub(super) fn parse_complex(s: &str) -> Option<Complex> {
97    let s = s.trim();
98    if s.is_empty() {
99        return None;
100    }
101
102    // Detect suffix ('i' or 'j')
103    let suffix = if s.ends_with('i') || s.ends_with('j') {
104        Some(s.chars().last().unwrap())
105    } else {
106        None
107    };
108
109    if suffix.is_none() {
110        // Pure real number
111        let re = s.parse::<f64>().ok()?;
112        return Some(Complex::new(re, 0.0));
113    }
114
115    // Strip suffix
116    let s = &s[..s.len() - 1];
117
118    // Pure imaginary: "i", "-i", "+i"
119    if s.is_empty() || s == "+" {
120        return Some(Complex::new(0.0, 1.0));
121    }
122    if s == "-" {
123        return Some(Complex::new(0.0, -1.0));
124    }
125
126    // Try parsing the whole thing as real (shouldn't happen but cover edge case)
127    if !s.contains('+') && !s.contains('-') || s.starts_with('-') && s[1..].find(['+', '-']).is_none() {
128        // E.g. "4i" or "-4i"
129        let im = s.parse::<f64>().ok()?;
130        return Some(Complex::new(0.0, im));
131    }
132
133    // Find the split point between real and imaginary parts.
134    // We look for the last '+' or '-' that isn't at position 0 (sign of real).
135    let bytes = s.as_bytes();
136    let mut split = None;
137    let start = if bytes[0] == b'-' || bytes[0] == b'+' { 1 } else { 0 };
138    for i in (start + 1..bytes.len()).rev() {
139        if bytes[i] == b'+' || bytes[i] == b'-' {
140            split = Some(i);
141            break;
142        }
143    }
144
145    if let Some(idx) = split {
146        let mut re_str = &s[..idx];
147        let im_str = &s[idx..];
148
149        // "3++4i": re_str="3+", im_str="+4". Strip a trailing operator from re_str.
150        if !re_str.is_empty()
151            && (re_str.ends_with('+') || re_str.ends_with('-'))
152            && (re_str[..re_str.len()-1].parse::<f64>().is_ok() || re_str.len() == 1)
153        {
154            re_str = &re_str[..re_str.len()-1];
155        }
156
157        let re = if re_str.is_empty() { 0.0 } else { re_str.parse::<f64>().ok()? };
158        let im = if im_str == "+" || im_str.is_empty() {
159            1.0
160        } else if im_str == "-" {
161            -1.0
162        } else {
163            im_str.parse::<f64>().ok()?
164        };
165        Some(Complex::new(re, im))
166    } else {
167        // No split found; it's pure imaginary like "4i"
168        let im = s.parse::<f64>().ok()?;
169        Some(Complex::new(0.0, im))
170    }
171}
172
173/// Format a complex number back to a string using the given suffix ('i' or 'j').
174/// Always returns `Value::Text` — even for purely real results — to match
175/// the Google Sheets contract that all IM* functions return a text string.
176pub(super) fn format_complex(c: Complex, suffix: char) -> Value {
177    let re = c.re;
178    let im = c.im;
179
180    if im == 0.0 {
181        return Value::Text(format_num(re));
182    }
183
184    let re_str = if re == 0.0 {
185        String::new()
186    } else {
187        format_num(re)
188    };
189
190    let im_str = if im == 1.0 {
191        suffix.to_string()
192    } else if im == -1.0 {
193        format!("-{}", suffix)
194    } else {
195        format!("{}{}", format_num(im), suffix)
196    };
197
198    let result = if re == 0.0 {
199        im_str
200    } else if im > 0.0 {
201        format!("{}+{}", re_str, im_str)
202    } else {
203        format!("{}{}", re_str, im_str)
204    };
205
206    Value::Text(result)
207}
208
209/// Format a float component using Google Sheets' 15-significant-figure rules:
210/// - Whole numbers are printed without a decimal point.
211/// - Very small (|x| < 1e-9) or very large (|x| >= 1e15) values use uppercase
212///   scientific notation with up to 15 sig figs (e.g. `6.12323399573677E-17`).
213/// - All other values use decimal notation with up to 15 sig figs, trailing
214///   zeros stripped.
215fn format_num(n: f64) -> String {
216    if n == 0.0 {
217        return "0".to_string();
218    }
219    if n.fract() == 0.0 && n.abs() < 1e15 {
220        return format!("{}", n as i64);
221    }
222
223    let abs = n.abs();
224
225    if !(1e-9..1e15).contains(&abs) {
226        // Scientific notation: 14 decimal places = 15 significant figures.
227        let s = format!("{:.14e}", n);
228        let (mantissa, exp_part) = s.split_once('e').unwrap();
229        let exp_num: i32 = exp_part.parse().unwrap();
230        let mantissa = mantissa.trim_end_matches('0').trim_end_matches('.');
231        // GS uses uppercase E with a sign and at least 2 digits in the exponent.
232        format!("{}E{:+03}", mantissa, exp_num)
233    } else {
234        // Convert via 15-sig-fig scientific notation, then render as decimal.
235        let s = format!("{:.14e}", n);
236        let (mantissa, exp_part) = s.split_once('e').unwrap();
237        let exp_num: i32 = exp_part.parse().unwrap();
238        let mantissa_stripped = mantissa.trim_end_matches('0').trim_end_matches('.');
239        let sign = if n < 0.0 { "-" } else { "" };
240        let mantissa_abs = mantissa_stripped.trim_start_matches('-');
241        let digits: String = mantissa_abs.chars().filter(|c| *c != '.').collect();
242
243        if exp_num < 0 {
244            let leading_zeros = (-exp_num - 1) as usize;
245            format!("{}0.{}{}", sign, "0".repeat(leading_zeros), digits)
246        } else {
247            let int_part_len = (exp_num + 1) as usize;
248            if int_part_len >= digits.len() {
249                format!(
250                    "{}{}{}",
251                    sign,
252                    digits,
253                    "0".repeat(int_part_len - digits.len())
254                )
255            } else {
256                let (int_part, frac_part) = digits.split_at(int_part_len);
257                format!("{}{}.{}", sign, int_part, frac_part)
258            }
259        }
260    }
261}
262
263/// Parse a Value as a complex number. Accepts Text or Number.
264fn value_to_complex(v: Value) -> Result<Complex, Value> {
265    match v {
266        Value::Number(n) | Value::Date(n) => Ok(Complex::new(n, 0.0)),
267        Value::Text(s) => {
268            parse_complex(&s).ok_or(Value::Error(ErrorKind::Num))
269        }
270        Value::Error(_) => Err(v),
271        // GS: booleans are NOT valid complex arguments -> #NUM!
272        Value::Bool(_) => Err(Value::Error(ErrorKind::Num)),
273        _ => {
274            match to_number(v) {
275                Ok(n) => Ok(Complex::new(n, 0.0)),
276                Err(e) => Err(e),
277            }
278        }
279    }
280}
281
282/// Extract the imaginary suffix ('i' or 'j') from a Value, defaulting to 'i'.
283fn get_suffix(v: &Value) -> char {
284    if let Value::Text(s) = v {
285        if s.ends_with('j') { return 'j'; }
286    }
287    'i'
288}
289
290/// Determine the common suffix for a list of complex arguments.
291/// Returns Ok('i') or Ok('j') if all text args use the same suffix (or there are none).
292/// Returns Err(#NUM!) if any two text args have conflicting suffixes.
293/// Non-text args (numbers/booleans) don't contribute a suffix.
294fn determine_suffix(args: &[Value]) -> Result<char, Value> {
295    let mut found: Option<char> = None;
296    for arg in args {
297        let s = match arg {
298            Value::Text(s) => s,
299            _ => continue,
300        };
301        // Only args that parse as complex with a suffix count
302        let suffix = if s.ends_with('i') {
303            'i'
304        } else if s.ends_with('j') {
305            'j'
306        } else {
307            continue; // pure real string — no suffix
308        };
309        match found {
310            None => found = Some(suffix),
311            Some(existing) if existing != suffix => {
312                return Err(Value::Error(ErrorKind::Num));
313            }
314            _ => {}
315        }
316    }
317    Ok(found.unwrap_or('i'))
318}
319
320// ── COMPLEX ───────────────────────────────────────────────────────────────────
321
322/// `COMPLEX(real, imaginary, [suffix])` — create a complex number string.
323pub fn complex_fn(args: &[Value]) -> Value {
324    if let Some(err) = check_arity(args, 2, 3) {
325        return err;
326    }
327    let re = match to_number(args[0].clone()) {
328        Err(e) => return e,
329        Ok(v) => v,
330    };
331    let im = match to_number(args[1].clone()) {
332        Err(e) => return e,
333        Ok(v) => v,
334    };
335    let suffix = if args.len() == 3 {
336        match to_string_val(args[2].clone()) {
337            Err(e) => return e,
338            Ok(s) => {
339                if s == "i" || s == "j" {
340                    s.chars().next().unwrap()
341                } else {
342                    return Value::Error(ErrorKind::Value);
343                }
344            }
345        }
346    } else {
347        'i'
348    };
349    format_complex(Complex::new(re, im), suffix)
350}
351
352// ── IMREAL / IMAGINARY ────────────────────────────────────────────────────────
353
354/// `IMREAL(complex)` — return real part of complex number.
355pub fn imreal_fn(args: &[Value]) -> Value {
356    if let Some(err) = check_arity(args, 1, 1) {
357        return err;
358    }
359    match value_to_complex(args[0].clone()) {
360        Err(e) => e,
361        Ok(c) => Value::Number(c.re),
362    }
363}
364
365/// `IMAGINARY(complex)` — return imaginary part of complex number.
366pub fn imaginary_fn(args: &[Value]) -> Value {
367    if let Some(err) = check_arity(args, 1, 1) {
368        return err;
369    }
370    match value_to_complex(args[0].clone()) {
371        Err(e) => e,
372        Ok(c) => Value::Number(c.im),
373    }
374}
375
376// ── IMABS ─────────────────────────────────────────────────────────────────────
377
378/// `IMABS(complex)` — return absolute value (modulus) of complex number.
379pub fn imabs_fn(args: &[Value]) -> Value {
380    if let Some(err) = check_arity(args, 1, 1) {
381        return err;
382    }
383    match value_to_complex(args[0].clone()) {
384        Err(e) => e,
385        Ok(c) => Value::Number(c.abs()),
386    }
387}
388
389// ── IMPRODUCT ─────────────────────────────────────────────────────────────────
390
391/// `IMPRODUCT(complex1, ...)` — product of complex numbers.
392pub fn improduct_fn(args: &[Value]) -> Value {
393    if let Some(err) = check_arity(args, 1, usize::MAX) {
394        return err;
395    }
396    // Determine suffix and check for mismatches.
397    let suffix = match determine_suffix(args) {
398        Err(e) => return e,
399        Ok(s) => s,
400    };
401    let mut result = Complex::new(1.0, 0.0);
402    for arg in args {
403        match value_to_complex(arg.clone()) {
404            Err(e) => return e,
405            Ok(c) => result = result.mul(c),
406        }
407    }
408    format_complex(result, suffix)
409}
410
411// ── IMSUB ────────────────────────────────────────────────────────────────────
412
413/// `IMSUB(complex1, complex2)` — subtract complex numbers.
414pub fn imsub_fn(args: &[Value]) -> Value {
415    if let Some(err) = check_arity(args, 2, 2) {
416        return err;
417    }
418    // Determine suffix and check for mismatches.
419    let suffix = match determine_suffix(args) {
420        Err(e) => return e,
421        Ok(s) => s,
422    };
423    let a = match value_to_complex(args[0].clone()) {
424        Err(e) => return e,
425        Ok(c) => c,
426    };
427    let b = match value_to_complex(args[1].clone()) {
428        Err(e) => return e,
429        Ok(c) => c,
430    };
431    format_complex(Complex::new(a.re - b.re, a.im - b.im), suffix)
432}
433
434// ── IMSUM ────────────────────────────────────────────────────────────────────
435
436/// `IMSUM(complex1, ...)` — sum of complex numbers.
437pub fn imsum_fn(args: &[Value]) -> Value {
438    if let Some(err) = check_arity(args, 1, usize::MAX) {
439        return err;
440    }
441    // Determine suffix and check for mismatches.
442    let suffix = match determine_suffix(args) {
443        Err(e) => return e,
444        Ok(s) => s,
445    };
446    let mut re = 0.0f64;
447    let mut im = 0.0f64;
448    for arg in args {
449        match value_to_complex(arg.clone()) {
450            Err(e) => return e,
451            Ok(c) => {
452                re += c.re;
453                im += c.im;
454            }
455        }
456    }
457    format_complex(Complex::new(re, im), suffix)
458}
459
460// ── IMDIV ────────────────────────────────────────────────────────────────────
461
462/// `IMDIV(complex1, complex2)` — divide complex numbers.
463pub fn imdiv_fn(args: &[Value]) -> Value {
464    if let Some(err) = check_arity(args, 2, 2) {
465        return err;
466    }
467    // Determine suffix and check for mismatches.
468    let suffix = match determine_suffix(args) {
469        Err(e) => return e,
470        Ok(s) => s,
471    };
472    let a = match value_to_complex(args[0].clone()) {
473        Err(e) => return e,
474        Ok(c) => c,
475    };
476    let b = match value_to_complex(args[1].clone()) {
477        Err(e) => return e,
478        Ok(c) => c,
479    };
480    let denom = b.re * b.re + b.im * b.im;
481    if denom == 0.0 {
482        return Value::Error(ErrorKind::DivByZero);
483    }
484    let re = (a.re * b.re + a.im * b.im) / denom;
485    let im = (a.im * b.re - a.re * b.im) / denom;
486    format_complex(Complex::new(re, im), suffix)
487}
488
489// ── IMCONJUGATE ───────────────────────────────────────────────────────────────
490
491/// `IMCONJUGATE(complex)` — complex conjugate.
492pub fn imconjugate_fn(args: &[Value]) -> Value {
493    if let Some(err) = check_arity(args, 1, 1) {
494        return err;
495    }
496    let suffix = get_suffix(&args[0]);
497    match value_to_complex(args[0].clone()) {
498        Err(e) => e,
499        Ok(c) => format_complex(Complex::new(c.re, -c.im), suffix),
500    }
501}
502
503// ── IMARGUMENT ────────────────────────────────────────────────────────────────
504
505/// `IMARGUMENT(complex)` — argument (angle) of complex number.
506pub fn imargument_fn(args: &[Value]) -> Value {
507    if let Some(err) = check_arity(args, 1, 1) {
508        return err;
509    }
510    match value_to_complex(args[0].clone()) {
511        Err(e) => e,
512        Ok(c) => {
513            if c.re == 0.0 && c.im == 0.0 {
514                Value::Error(ErrorKind::DivByZero)
515            } else {
516                Value::Number(c.arg())
517            }
518        }
519    }
520}
521
522// ── IMLN ─────────────────────────────────────────────────────────────────────
523
524/// `IMLN(complex)` — natural log of complex number.
525/// GS always returns results with 'i' suffix, regardless of input notation.
526pub fn imln_fn(args: &[Value]) -> Value {
527    if let Some(err) = check_arity(args, 1, 1) {
528        return err;
529    }
530    match value_to_complex(args[0].clone()) {
531        Err(e) => e,
532        Ok(c) => match c.ln() {
533            None => Value::Error(ErrorKind::DivByZero),
534            Some(result) => format_complex(result, 'i'),
535        },
536    }
537}
538
539// ── IMLOG10 / IMLOG2 / IMLOG ─────────────────────────────────────────────────
540
541/// `IMLOG10(complex)` — base-10 log of complex number.
542pub fn imlog10_fn(args: &[Value]) -> Value {
543    if let Some(err) = check_arity(args, 1, 1) {
544        return err;
545    }
546    match value_to_complex(args[0].clone()) {
547        Err(e) => e,
548        Ok(c) => match c.ln() {
549            None => Value::Error(ErrorKind::DivByZero),
550            Some(result) => {
551                let ln10 = libm::log(10.0f64);
552                format_complex(Complex::new(result.re / ln10, result.im / ln10), 'i')
553            }
554        },
555    }
556}
557
558/// `IMLOG2(complex)` — base-2 log of complex number.
559pub fn imlog2_fn(args: &[Value]) -> Value {
560    if let Some(err) = check_arity(args, 1, 1) {
561        return err;
562    }
563    match value_to_complex(args[0].clone()) {
564        Err(e) => e,
565        Ok(c) => match c.ln() {
566            None => Value::Error(ErrorKind::DivByZero),
567            Some(result) => {
568                let ln2 = libm::log(2.0f64);
569                format_complex(Complex::new(result.re / ln2, result.im / ln2), 'i')
570            }
571        },
572    }
573}
574
575/// `IMLOG(complex, base)` — general log of complex number.
576pub fn imlog_fn(args: &[Value]) -> Value {
577    if let Some(err) = check_arity(args, 2, 2) {
578        return err;
579    }
580    let c = match value_to_complex(args[0].clone()) {
581        Err(e) => return e,
582        Ok(v) => v,
583    };
584    let base = match to_number(args[1].clone()) {
585        Err(e) => return e,
586        Ok(v) => v,
587    };
588    if base <= 0.0 || base == 1.0 {
589        return Value::Error(ErrorKind::Num);
590    }
591    match c.ln() {
592        None => Value::Error(ErrorKind::DivByZero),
593        Some(result) => {
594            let ln_base = libm::log(base);
595            format_complex(Complex::new(result.re / ln_base, result.im / ln_base), 'i')
596        }
597    }
598}
599
600// ── IMEXP ────────────────────────────────────────────────────────────────────
601
602/// `IMEXP(complex)` — e raised to a complex power.
603pub fn imexp_fn(args: &[Value]) -> Value {
604    if let Some(err) = check_arity(args, 1, 1) {
605        return err;
606    }
607    let suffix = get_suffix(&args[0]);
608    match value_to_complex(args[0].clone()) {
609        Err(e) => e,
610        Ok(c) => {
611            let scale = libm::exp(c.re);
612            format_complex(Complex::new(scale * libm::cos(c.im), scale * libm::sin(c.im)), suffix)
613        }
614    }
615}
616
617// ── IMPOWER ──────────────────────────────────────────────────────────────────
618
619/// `IMPOWER(complex, number)` — complex number raised to a power.
620pub fn impower_fn(args: &[Value]) -> Value {
621    if let Some(err) = check_arity(args, 2, 2) {
622        return err;
623    }
624    let base = match value_to_complex(args[0].clone()) {
625        Err(e) => return e,
626        Ok(c) => c,
627    };
628    // The exponent is always real. Use to_number so that:
629    //   - TRUE/FALSE coerce to 1/0 (GS rows 1042/1043)
630    //   - non-numeric text like "abc" returns #VALUE! not #NUM! (GS row 1047)
631    let exp_n = match to_number(args[1].clone()) {
632        Err(e) => return e,
633        Ok(v) => v,
634    };
635    let exp = Complex::new(exp_n, 0.0);
636    let suffix = get_suffix(&args[0]);
637    match base.pow(exp) {
638        None => Value::Error(ErrorKind::Num),
639        Some(mut result) => {
640            // Snap near-integer components to exact integers (FP rounding).
641            // e.g. IMPOWER("5+2i",3) real part: 64.999...999 -> 65.
642            // Only snap when the rounded value is non-zero: tiny residuals that
643            // round to zero (e.g. 16·sin(2π) ≈ -3.9e-15) must be preserved —
644            // Google Sheets includes them (rows 1039/1040/1053).
645            if result.re.round() != 0.0
646                && (result.re.round() - result.re).abs() < 1e-9 * result.re.abs().max(1.0)
647            {
648                result.re = result.re.round();
649            }
650            if result.im.round() != 0.0
651                && (result.im.round() - result.im).abs() < 1e-9 * result.im.abs().max(1.0)
652            {
653                result.im = result.im.round();
654            }
655            format_complex(result, suffix)
656        }
657    }
658}
659
660// ── IMSQRT ───────────────────────────────────────────────────────────────────
661
662/// `IMSQRT(complex)` — principal square root of complex number.
663pub fn imsqrt_fn(args: &[Value]) -> Value {
664    if let Some(err) = check_arity(args, 1, 1) {
665        return err;
666    }
667    let suffix = get_suffix(&args[0]);
668    match value_to_complex(args[0].clone()) {
669        Err(e) => e,
670        Ok(c) => format_complex(c.sqrt(), suffix),
671    }
672}
673
674// ── Trig functions ────────────────────────────────────────────────────────────
675
676/// `IMSIN(complex)` — sine of complex number.
677pub fn imsin_fn(args: &[Value]) -> Value {
678    if let Some(err) = check_arity(args, 1, 1) {
679        return err;
680    }
681    let suffix = get_suffix(&args[0]);
682    match value_to_complex(args[0].clone()) {
683        Err(e) => e,
684        Ok(c) => {
685            let re = libm::sin(c.re) * libm::cosh(c.im);
686            let im = libm::cos(c.re) * libm::sinh(c.im);
687            format_complex(Complex::new(re, im), suffix)
688        }
689    }
690}
691
692/// `IMCOS(complex)` — cosine of complex number.
693pub fn imcos_fn(args: &[Value]) -> Value {
694    if let Some(err) = check_arity(args, 1, 1) {
695        return err;
696    }
697    let suffix = get_suffix(&args[0]);
698    match value_to_complex(args[0].clone()) {
699        Err(e) => e,
700        Ok(c) => {
701            let re = libm::cos(c.re) * libm::cosh(c.im);
702            let im = -(libm::sin(c.re) * libm::sinh(c.im));
703            format_complex(Complex::new(re, im), suffix)
704        }
705    }
706}
707
708/// `IMTAN(complex)` — tangent of complex number.
709/// Uses double-angle formula for precision matching Google Sheets:
710///   tan(a+bi) = sin(2a)/(cos(2a)+cosh(2b)) + i*sinh(2b)/(cos(2a)+cosh(2b))
711pub fn imtan_fn(args: &[Value]) -> Value {
712    if let Some(err) = check_arity(args, 1, 1) {
713        return err;
714    }
715    let suffix = get_suffix(&args[0]);
716    match value_to_complex(args[0].clone()) {
717        Err(e) => e,
718        Ok(c) => {
719            let a2 = 2.0 * c.re;
720            let b2 = 2.0 * c.im;
721            let denom = libm::cos(a2) + libm::cosh(b2);
722            if denom == 0.0 {
723                return Value::Error(ErrorKind::DivByZero);
724            }
725            let re = libm::sin(a2) / denom;
726            let im = libm::sinh(b2) / denom;
727            format_complex(Complex::new(re, im), suffix)
728        }
729    }
730}
731
732/// `IMCOT(complex)` — cotangent of complex number.
733/// Uses double-angle formula for precision matching Google Sheets:
734///   cot(a+bi) = sin(2a)/(cosh(2b)-cos(2a)) - i*sinh(2b)/(cosh(2b)-cos(2a))
735pub fn imcot_fn(args: &[Value]) -> Value {
736    if let Some(err) = check_arity(args, 1, 1) {
737        return err;
738    }
739    let suffix = get_suffix(&args[0]);
740    match value_to_complex(args[0].clone()) {
741        Err(e) => e,
742        Ok(c) => {
743            let a2 = 2.0 * c.re;
744            let b2 = 2.0 * c.im;
745            let denom = libm::cosh(b2) - libm::cos(a2);
746            if denom == 0.0 {
747                return Value::Error(ErrorKind::DivByZero);
748            }
749            let re = libm::sin(a2) / denom;
750            let im = -(libm::sinh(b2) / denom);
751            format_complex(Complex::new(re, im), suffix)
752        }
753    }
754}
755
756/// `IMCSC(complex)` — cosecant of complex number.
757pub fn imcsc_fn(args: &[Value]) -> Value {
758    if let Some(err) = check_arity(args, 1, 1) {
759        return err;
760    }
761    let suffix = get_suffix(&args[0]);
762    match value_to_complex(args[0].clone()) {
763        Err(e) => e,
764        Ok(c) => {
765            let sin_re = libm::sin(c.re) * libm::cosh(c.im);
766            let sin_im = libm::cos(c.re) * libm::sinh(c.im);
767            let denom = sin_re * sin_re + sin_im * sin_im;
768            if denom == 0.0 {
769                return Value::Error(ErrorKind::Num);
770            }
771            let re = sin_re / denom;
772            let im = -sin_im / denom;
773            format_complex(Complex::new(re, im), suffix)
774        }
775    }
776}
777
778/// `IMSEC(complex)` — secant of complex number.
779pub fn imsec_fn(args: &[Value]) -> Value {
780    if let Some(err) = check_arity(args, 1, 1) {
781        return err;
782    }
783    let suffix = get_suffix(&args[0]);
784    match value_to_complex(args[0].clone()) {
785        Err(e) => e,
786        Ok(c) => {
787            let cos_re = libm::cos(c.re) * libm::cosh(c.im);
788            let cos_im = -(libm::sin(c.re) * libm::sinh(c.im));
789            let denom = cos_re * cos_re + cos_im * cos_im;
790            if denom == 0.0 {
791                return Value::Error(ErrorKind::DivByZero);
792            }
793            let re = cos_re / denom;
794            let im = -cos_im / denom;
795            format_complex(Complex::new(re, im), suffix)
796        }
797    }
798}
799
800// ── Hyperbolic trig ────────────────────────────────────────────────────────────
801
802/// `IMSINH(complex)` — hyperbolic sine of complex number.
803pub fn imsinh_fn(args: &[Value]) -> Value {
804    if let Some(err) = check_arity(args, 1, 1) {
805        return err;
806    }
807    let suffix = get_suffix(&args[0]);
808    match value_to_complex(args[0].clone()) {
809        Err(e) => e,
810        Ok(c) => {
811            let re = libm::sinh(c.re) * libm::cos(c.im);
812            let im = libm::cosh(c.re) * libm::sin(c.im);
813            format_complex(Complex::new(re, im), suffix)
814        }
815    }
816}
817
818/// `IMCOSH(complex)` — hyperbolic cosine of complex number.
819pub fn imcosh_fn(args: &[Value]) -> Value {
820    if let Some(err) = check_arity(args, 1, 1) {
821        return err;
822    }
823    let suffix = get_suffix(&args[0]);
824    match value_to_complex(args[0].clone()) {
825        Err(e) => e,
826        Ok(c) => {
827            let re = libm::cosh(c.re) * libm::cos(c.im);
828            let im = libm::sinh(c.re) * libm::sin(c.im);
829            format_complex(Complex::new(re, im), suffix)
830        }
831    }
832}
833
834/// `IMTANH(complex)` — hyperbolic tangent of complex number.
835/// Uses double-angle formula for precision matching Google Sheets:
836///   tanh(a+bi) = sinh(2a)/(cosh(2a)+cos(2b)) + i*sin(2b)/(cosh(2a)+cos(2b))
837pub fn imtanh_fn(args: &[Value]) -> Value {
838    if let Some(err) = check_arity(args, 1, 1) {
839        return err;
840    }
841    let suffix = get_suffix(&args[0]);
842    match value_to_complex(args[0].clone()) {
843        Err(e) => e,
844        Ok(c) => {
845            let a2 = 2.0 * c.re;
846            let b2 = 2.0 * c.im;
847            let denom = libm::cosh(a2) + libm::cos(b2);
848            if denom == 0.0 {
849                return Value::Error(ErrorKind::DivByZero);
850            }
851            let re = libm::sinh(a2) / denom;
852            let im = libm::sin(b2) / denom;
853            if im == 0.0 {
854                return Value::Text(format_num(re));
855            }
856            format_complex(Complex::new(re, im), suffix)
857        }
858    }
859}
860
861/// `IMCOTH(complex)` — hyperbolic cotangent of complex number.
862/// Uses double-angle formula for precision matching Google Sheets:
863///   coth(a+bi) = sinh(2a)/(cosh(2a)-cos(2b)) - i*sin(2b)/(cosh(2a)-cos(2b))
864pub fn imcoth_fn(args: &[Value]) -> Value {
865    if let Some(err) = check_arity(args, 1, 1) {
866        return err;
867    }
868    let suffix = get_suffix(&args[0]);
869    match value_to_complex(args[0].clone()) {
870        Err(e) => e,
871        Ok(c) => {
872            let a2 = 2.0 * c.re;
873            let b2 = 2.0 * c.im;
874            let denom = libm::cosh(a2) - libm::cos(b2);
875            if denom == 0.0 {
876                return Value::Error(ErrorKind::DivByZero);
877            }
878            let re = libm::sinh(a2) / denom;
879            let im = -(libm::sin(b2) / denom);
880            if im == 0.0 {
881                return Value::Text(format_num(re));
882            }
883            format_complex(Complex::new(re, im), suffix)
884        }
885    }
886}
887
888/// `IMCSCH(complex)` — hyperbolic cosecant of complex number.
889pub fn imcsch_fn(args: &[Value]) -> Value {
890    if let Some(err) = check_arity(args, 1, 1) {
891        return err;
892    }
893    let suffix = get_suffix(&args[0]);
894    match value_to_complex(args[0].clone()) {
895        Err(e) => e,
896        Ok(c) => {
897            let sinh_re = libm::sinh(c.re) * libm::cos(c.im);
898            let sinh_im = libm::cosh(c.re) * libm::sin(c.im);
899            let denom = sinh_re * sinh_re + sinh_im * sinh_im;
900            if denom == 0.0 {
901                return Value::Error(ErrorKind::DivByZero);
902            }
903            let re = sinh_re / denom;
904            let im = -sinh_im / denom;
905            format_complex(Complex::new(re, im), suffix)
906        }
907    }
908}
909
910/// `IMSECH(complex)` — hyperbolic secant of complex number.
911pub fn imsech_fn(args: &[Value]) -> Value {
912    if let Some(err) = check_arity(args, 1, 1) {
913        return err;
914    }
915    let suffix = get_suffix(&args[0]);
916    match value_to_complex(args[0].clone()) {
917        Err(e) => e,
918        Ok(c) => {
919            let cosh_re = libm::cosh(c.re) * libm::cos(c.im);
920            let cosh_im = libm::sinh(c.re) * libm::sin(c.im);
921            let denom = cosh_re * cosh_re + cosh_im * cosh_im;
922            if denom == 0.0 {
923                return Value::Error(ErrorKind::DivByZero);
924            }
925            let re = cosh_re / denom;
926            let im = -cosh_im / denom;
927            format_complex(Complex::new(re, im), suffix)
928        }
929    }
930}
931
932#[cfg(test)]
933mod tests;