moonnum.
ThreeComplex numbers

Two doubles, and all the ways they surprise you

Complex is a pair of doubles. The arithmetic is the C99 behaviour, including the parts nobody writes down: signed zeros, infinities that would otherwise turn into NaN, and a repr that drops the real part when it is a positive zero.

Reading

Every form you would write by hand is accepted, plus the i suffix for people coming from mathematics rather than engineering.

moonbit
for text in ["1+2j", "(1+2j)", "2j", "j", "-j", "1e3j", "3+5i", "+2j"] {
  println(@moonnum.parse_complex(text))
}
output
(1+2j)
(1+2j)
2j
1j
-1j
1000j
(3+5j)
2j

Spaces are not accepted inside, exactly as in the language this library follows: "1 - 2j" is refused, "1-2j" is not. Accepting both would make it impossible to say what a trailing space means.

Arithmetic that keeps its infinities

Multiplying a complex number with an infinite part by another with a zero part yields nan under the naive formula. The implementation follows the recovery step of C11's Annex G, so the answer is the infinity the arithmetic asks for.

moonbit
let z = @moonnum.parse_complex("3+4j")
z + @moonnum.Complex::new(1.0, 0.0)
z * z
z.divide(z)
z.abs()
z.conjugate()
output
(4+4j)
(-7+24j)
(1+0j)
5
(3-4j)

Integer powers are exact

A small integer exponent is applied by repeated squaring, not by exp(iθ · n). That is why (1+1j)² is exactly 2j and not 1.2246e-16+2j. Non-integer exponents go through the polar form, as they must.

moonbit
let z = @moonnum.parse_complex("1+1j")
z.pow(@moonnum.parse_complex("2"))
z.pow(@moonnum.parse_complex("100"))
z.pow(@moonnum.parse_complex("0.5"))
output
2j
(-1125899906842624+0j)
(1.0986841134678098+0.45508986056222733j)

Division by zero says so

Complex::divide is a function and not an operator, because MoonBit's division trait cannot raise. A zero divisor is ZeroDivision, not a NaN that propagates.

moonbit
let z = @moonnum.parse_complex("3+4j")
let zero = @moonnum.parse_complex("0j")
let answer : String = z.divide(zero).to_string() catch {
  @moonnum.ZeroDivision => "no answer"
  _ => "refused"
}
output
no answer

Formatting

One specifier, both halves. The imaginary part always carries a sign; the real part follows your sign flag. Zero padding and = alignment are refused, as they are in the language this follows, because padding inside a pair has no sensible meaning.

moonbit
let z = @moonnum.parse_complex("3+4j")
z.format("")
z.format(".2f")
z.format("+9.2f")
z.format("<14.1e")
output
(3+4j)
3.00+4.00j
+3.00+4.00j
3.0e+00+4.0e+00j