Strilanc/Quirk

View on GitHub
src/math/Complex.js

Summary

Maintainability
F
3 days
Test Coverage
/**
 * Copyright 2017 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {DetailedError} from "../base/DetailedError.js"
import {Format, UNICODE_FRACTIONS} from "../base/Format.js"
import {Util} from "../base/Util.js"
import {parseFormula} from "./FormulaParser.js"

const PARSE_COMPLEX_TOKEN_MAP_ALL = new Map();
const PARSE_COMPLEX_TOKEN_MAP_RAD = new Map();
const PARSE_COMPLEX_TOKEN_MAP_DEG = new Map();

/**
 * Represents a complex number like `a + b i`, where `a` and `b` are real values and `i` is the square root of -1.
 */
class Complex {
    /**
     * @param {!number} real The real part of the Complex number. The 'a' in a + bi.
     * @param {!number} imag The imaginary part of the Complex number. The 'b' in a + bi.
     */
    constructor(real, imag) {
        /**
         * The real part of the Complex number. The 'a' in a + bi.
         * @type {!number}
         */
        this.real = real;
        /**
         * The imaginary part of the Complex number. The 'b' in a + bi.
         * @type {!number}
         */
        this.imag = imag;
    }

    /**
     * Determines if the receiving complex value is equal to the given complex, integer, or float value.
     * This method returns false, instead of throwing, when given badly typed arguments.
     * @param {!number|!Complex|*} other
     * @returns {!boolean}
     */
    isEqualTo(other) {
        if (other instanceof Complex) {
            return this.real === other.real && this.imag === other.imag;
        }
        if (typeof other === "number") {
            return this.real === other && this.imag === 0;
        }
        return false;
    }

    /**
     * Determines if the receiving complex value is near the given complex, integer, or float value.
     * This method returns false, instead of throwing, when given badly typed arguments.
     * @param {!number|!Complex|*} other
     * @param {!number} epsilon
     * @returns {!boolean}
     */
    isApproximatelyEqualTo(other, epsilon) {
        if (other instanceof Complex || typeof other === "number") {
            let d = this.minus(Complex.from(other));
            return Math.abs(d.real) <= epsilon &&
                Math.abs(d.imag) <= epsilon &&
                d.abs() <= epsilon;
        }
        return false;
    }

    /**
     * Wraps the given number into a Complex value (unless it's already a Complex value).
     * @param {!number|!Complex} v
     * @returns {!Complex}
     */
    static from(v) {
        if (v instanceof Complex) {
            return v;
        }
        if (typeof v === "number") {
            return new Complex(v, 0);
        }
        throw new DetailedError("Unrecognized value type.", {v});
    }

    /**
     * Returns a complex number with the given magnitude and phase.
     * @param {!number} magnitude Distance from origin.
     * @param {!number} phase Phase in radians.
     * @returns {!Complex}
     */
    static polar(magnitude, phase) {
        let [cos, sin] = Util.snappedCosSin(phase);
        return new Complex(magnitude * cos, magnitude * sin);
    }

    /**
     * Returns the real component of a Complex, integer, or float value.
     * @param {!number|!Complex} v
     * @returns {!number}
     */
    static realPartOf(v) {
        if (v instanceof Complex) {
            return v.real;
        }
        if (typeof v === "number") {
            return v;
        }
        throw new DetailedError("Unrecognized value type.", {v});
    }

    /**
     * Returns the imaginary component of a Complex value, or else 0 for integer and float values.
     * @param {!number|!Complex} v
     * @returns {!number}
     */
    static imagPartOf(v) {
        if (v instanceof Complex) {
            return v.imag;
        }
        if (typeof v === "number") {
            return 0;
        }
        throw new DetailedError("Unrecognized value type.", {v});
    }

    /**
     * Returns a compact text representation of the receiving complex value.
     * @param {=Format} format
     * @returns {!string}
     */
    toString(format) {
        format = format || Format.EXACT;

        return format.allowAbbreviation ?
            this._toString_allowSingleValue(format) :
            this._toString_bothValues(format);
    }

    /**
     * @param {!Format} format
     * @returns {!string}
     * @private
     */
    _toString_allowSingleValue(format) {
        if (Math.abs(this.imag) <= format.maxAbbreviationError) {
            return format.formatFloat(this.real);
        }
        if (Math.abs(this.real) <= format.maxAbbreviationError) {
            if (Math.abs(this.imag - 1) <= format.maxAbbreviationError) {
                return "i";
            }
            if (Math.abs(this.imag + 1) <= format.maxAbbreviationError) {
                return "-i";
            }
            return format.formatFloat(this.imag) + "i";
        }

        return this._toString_bothValues(format);
    }

    /**
     * @param {!Format} format
     * @returns {!string}
     * @private
     */
    _toString_bothValues(format) {
        let separator = this.imag >= 0 ? "+" : "-";
        let imagFactor = format.allowAbbreviation && Math.abs(Math.abs(this.imag) - 1) <= format.maxAbbreviationError ?
            "" :
            format.formatFloat(Math.abs(this.imag));
        let prefix = format.allowAbbreviation || format.fixedDigits === undefined || this.real < 0 ? "" : "+";
        return prefix + format.formatFloat(this.real) + separator + imagFactor + "i";
    }

    /**
     * Parses a complex number from an infix arithmetic expression.
     * @param {!string} text
     * @returns {!Complex}
     */
    static parse(text) {
        return Complex.from(parseFormula(text, PARSE_COMPLEX_TOKEN_MAP_DEG));
    }

    /**
     * Returns the squared euclidean length of the receiving complex value.
     * @returns {!number}
     */
    norm2() {
        return this.real * this.real + this.imag * this.imag;
    }

    /**
     * Returns the euclidean length of the receiving complex value.
     * @returns {!number}
     */
    abs() {
        return Math.sqrt(this.norm2());
    }

    /**
     * Returns the complex conjugate of the receiving complex value, with the same real part but a negated imaginary part.
     * @returns {!Complex}
     */
    conjugate() {
        return new Complex(this.real, -this.imag);
    }

    /**
     * Returns the negation of this complex value.
     * @returns {!Complex}
     */
    neg() {
        return new Complex(-this.real, -this.imag);
    }

    /**
     * Returns the angle, in radians, of the receiving complex value with 0 being +real-ward and τ/4 being +imag-ward.
     * Zero defaults to having a phase of zero.
     * @returns {!number}
     */
    phase() {
        return Math.atan2(this.imag, this.real);
    }

    /**
     * Returns a unit complex value parallel to the receiving complex value.
     * Zero defaults to having the unit vector 1+0i.
     * @returns {!Complex}
     */
    unit() {
        let m = this.norm2();
        if (m < 0.00001) {
            return Complex.polar(1, this.phase());
        }
        return this.dividedBy(Math.sqrt(m));
    }

    /**
     * Returns the sum of the receiving complex value plus the given value.
     * @param {!number|!Complex} v
     * @returns {!Complex}
     */
    plus(v) {
        let c = Complex.from(v);
        return new Complex(this.real + c.real, this.imag + c.imag);
    }

    /**
     * Returns the difference from the receiving complex value to the given value.
     * @param {!number|!Complex} v
     * @returns {!Complex}
     */
    minus(v) {
        let c = Complex.from(v);
        return new Complex(this.real - c.real, this.imag - c.imag);
    }

    /**
     * Returns the product of the receiving complex value times the given value.
     * @param {!number|!Complex} v
     * @returns {!Complex}
     */
    times(v) {
        let c = Complex.from(v);
        return new Complex(
            this.real * c.real - this.imag * c.imag,
            this.real * c.imag + this.imag * c.real);
    }

    /**
     * Returns the ratio of the receiving complex value to the given value.
     * @param {!number|!Complex} v
     * @returns {!Complex}
     */
    dividedBy(v) {
        let c = Complex.from(v);
        let d = c.norm2();
        if (d === 0) {
            throw new Error("Division by Zero");
        }

        let n = this.times(c.conjugate());
        return new Complex(n.real / d, n.imag / d);
    }

    sqrts() {
        let [r, i] = [this.real, this.imag];
        let m = Math.sqrt(Math.sqrt(r*r + i*i));
        if (m === 0) {
            return [Complex.ZERO];
        }
        if (i === 0 && r < 0) {
            return [new Complex(0, m), new Complex(0, -m)]
        }

        let a = this.phase() / 2;
        let c = Complex.polar(m, a);
        return [c, c.times(-1)];
    }

    /**
     * Returns the result of raising Euler's constant to the receiving complex value.
     * @returns {!Complex}
     */
    exp() {
        return Complex.polar(Math.exp(this.real), this.imag);
    }

    /**
     * @returns {!Complex}
     */
    cos() {
        let z = this.times(Complex.I);
        return z.exp().plus(z.neg().exp()).times(0.5);
    }

    /**
     * @returns {!Complex}
     */
    sin() {
        let z = this.times(Complex.I);
        return z.exp().minus(z.neg().exp()).dividedBy(new Complex(0, 2));
    }

    /**
     * @returns {!Complex}
     */
    tan() {
        return this.sin().dividedBy(this.cos());
    }

    /**
     * Returns the natural logarithm of the receiving complex value.
     * @returns {!Complex}
     */
    ln() {
        return new Complex(Math.log(this.abs()), this.phase());
    }

    /**
     * Returns the result of raising the receiving complex value to the given complex exponent.
     * @param {!number|!Complex} exponent
     * @returns {!Complex}
     */
    raisedTo(exponent) {
        if (exponent === 0.5 && this.imag === 0 && this.real >= 0) {
            return new Complex(Math.sqrt(this.real), 0);
        }
        if (Complex.ZERO.isEqualTo(exponent)) {
            return Complex.ONE;
        }
        if (this.isEqualTo(Complex.ZERO)) {
            return Complex.ZERO;
        }
        return this.ln().times(Complex.from(exponent)).exp();
    }

    /**
     * Returns the distinct roots of the quadratic, or linear, equation.
     */
    static rootsOfQuadratic(a, b, c) {
        a = Complex.from(a);
        b = Complex.from(b);
        c = Complex.from(c);

        if (a.isEqualTo(0)) {
            if (!b.isEqualTo(0)) {
                return [c.times(-1).dividedBy(b)];
            }
            if (!c.isEqualTo(0)) {
                return [];
            }
            throw Error("Degenerate");
        }

        let difs = b.times(b).minus(a.times(c).times(4)).sqrts();
        let mid = b.times(-1);
        let denom = a.times(2);
        return difs.map(d => mid.minus(d).dividedBy(denom));
    }
}

/**
 * The complex number equal to zero.
 * @type {!Complex}
 */
Complex.ZERO = new Complex(0, 0);

/**
 * The complex number equal to one.
 * @type {!Complex}
 */
Complex.ONE = new Complex(1, 0);

/**
 * The square root of negative 1.
 * @type {!Complex}
 */
Complex.I = new Complex(0, 1);

PARSE_COMPLEX_TOKEN_MAP_ALL.set("i", Complex.I);
PARSE_COMPLEX_TOKEN_MAP_ALL.set("e", Complex.from(Math.E));
PARSE_COMPLEX_TOKEN_MAP_ALL.set("pi", Complex.from(Math.PI));
PARSE_COMPLEX_TOKEN_MAP_ALL.set("(", "(");
PARSE_COMPLEX_TOKEN_MAP_ALL.set(")", ")");
for (let {character, value} of UNICODE_FRACTIONS) {
    //noinspection JSUnusedAssignment
    PARSE_COMPLEX_TOKEN_MAP_ALL.set(character, value);
}
PARSE_COMPLEX_TOKEN_MAP_ALL.set("sqrt", {
    unary_action: e => Complex.from(e).raisedTo(0.5),
    priority: 4});
PARSE_COMPLEX_TOKEN_MAP_ALL.set("exp", {
    unary_action: e => Complex.from(e).exp(),
    priority: 4});
PARSE_COMPLEX_TOKEN_MAP_ALL.set("ln", {
    unary_action: e => Complex.from(e).ln(),
    priority: 4});
PARSE_COMPLEX_TOKEN_MAP_ALL.set("^", {
    binary_action: (a, b) => Complex.from(a).raisedTo(b),
    priority: 3});
PARSE_COMPLEX_TOKEN_MAP_ALL.set("*", {
    binary_action: (a, b) => Complex.from(a).times(b),
    priority: 2});
PARSE_COMPLEX_TOKEN_MAP_ALL.set("/", {
    binary_action: (a, b) => Complex.from(a).dividedBy(b),
    priority: 2});
PARSE_COMPLEX_TOKEN_MAP_ALL.set("-", {
    unary_action: e => Complex.from(e).neg(),
    binary_action: (a, b) => Complex.from(a).minus(b),
    priority: 1});
PARSE_COMPLEX_TOKEN_MAP_ALL.set("+", {
    unary_action: e => e,
    binary_action: (a, b) => Complex.from(a).plus(b),
    priority: 1});
PARSE_COMPLEX_TOKEN_MAP_ALL.set("√", PARSE_COMPLEX_TOKEN_MAP_ALL.get("sqrt"));

PARSE_COMPLEX_TOKEN_MAP_DEG.set("cos", {
    unary_action: e => new Complex(Math.PI/180, 0).times(e).cos(),
    priority: 4});
PARSE_COMPLEX_TOKEN_MAP_DEG.set("sin", {
    unary_action: e => new Complex(Math.PI/180, 0).times(e).sin(),
    priority: 4});
PARSE_COMPLEX_TOKEN_MAP_DEG.set("asin", {
    unary_action: e => {
        if (Complex.imagPartOf(e) !== 0) {
            throw new DetailedError("asin input out of range", {e});
        }
        return Complex.from(Math.asin(Complex.realPartOf(e))*180/Math.PI);
    },
    priority: 4});
PARSE_COMPLEX_TOKEN_MAP_DEG.set("acos", {
    unary_action: e => {
        if (Complex.imagPartOf(e) !== 0) {
            throw new DetailedError("acos input out of range", {e});
        }
        return Complex.from(Math.acos(Complex.realPartOf(e))*180/Math.PI);
    },
    priority: 4});
PARSE_COMPLEX_TOKEN_MAP_DEG.set("arccos", PARSE_COMPLEX_TOKEN_MAP_DEG.get("acos"));
PARSE_COMPLEX_TOKEN_MAP_DEG.set("arcsin", PARSE_COMPLEX_TOKEN_MAP_DEG.get("asin"));

PARSE_COMPLEX_TOKEN_MAP_RAD.set("cos", {
    unary_action: e => Complex.from(e).cos(),
    priority: 4});
PARSE_COMPLEX_TOKEN_MAP_RAD.set("sin", {
    unary_action: e => Complex.from(e).sin(),
    priority: 4});
PARSE_COMPLEX_TOKEN_MAP_RAD.set("tan", {
    unary_action: e => Complex.from(e).tan(),
    priority: 4});
PARSE_COMPLEX_TOKEN_MAP_RAD.set("asin", {
    unary_action: e => {
        if (Complex.imagPartOf(e) !== 0) {
            throw new DetailedError("asin input out of range", {e});
        }
        return Complex.from(Math.asin(Complex.realPartOf(e)));
    },
    priority: 4});
PARSE_COMPLEX_TOKEN_MAP_RAD.set("acos", {
    unary_action: e => {
        if (Complex.imagPartOf(e) !== 0) {
            throw new DetailedError("acos input out of range", {e});
        }
        return Complex.from(Math.acos(Complex.realPartOf(e)));
    },
    priority: 4});
PARSE_COMPLEX_TOKEN_MAP_RAD.set("atan", {
    unary_action: e => {
        if (Complex.imagPartOf(e) !== 0) {
            throw new DetailedError("atan input out of range", {e});
        }
        return Complex.from(Math.atan(Complex.realPartOf(e)));
    },
    priority: 4});

for (let [k, v] of PARSE_COMPLEX_TOKEN_MAP_ALL.entries()) {
    PARSE_COMPLEX_TOKEN_MAP_DEG.set(k, v);
    PARSE_COMPLEX_TOKEN_MAP_RAD.set(k, v);
}

export {Complex, PARSE_COMPLEX_TOKEN_MAP_DEG, PARSE_COMPLEX_TOKEN_MAP_RAD}