Введение

В последнее время я играл с множеством Мандельброта, а это означало, что мне пришлось стряхнуть пыль с памяти о том, как выполнять арифметические действия с комплексными числами. Я создал хороший класс JavaScript, помогающий смазать их. Вот теперь это работает (это довольно сухое объяснение, но я надеюсь, что оно будет полезным).

Быстрое обновление

Когда мы умножаем два числа вместе, результат будет нечетным или четным в зависимости от того, являются ли числа, которые мы умножаем:

-1 *  1 = -1
 1 * -1 = -1
 1 *  1 =  1
-1 * -1 =  1

Обратите внимание, что возведение в квадрат дает положительное число! Не существует числа, которое можно возвести в квадрат и получить отрицательное число, и здесь в игру вступает мнимое число. Буква i используется для обозначения воображаемого числа, которое при возведении в квадрат равно -1.

  i  ≔ √(-1)
∴ i² =  -1

Это буквально другая числовая линия, точно так же, как у нас есть -2, -1, 0, 1, 2… у нас есть -2i, -1i, 0i, 1i, 2i, 3i….

Комплексное число — это число, состоящее из действительной части и мнимой части. Они записываются в форме a + bi.

Поскольку 0i = 0 числовые линии пересекаются в 0, мы можем изобразить эти числа на графике, обычно ось x — это прямая с действительными числами, а ось y — воображаемая.

Некоторые примеры

Арифметика

Возможно, вы заметили, что комплексные числа очень похожи на векторы, и с той оговоркой, что они i * i = -1 таковыми и являются. Все операции являются обычными векторными операциями, которые корректируются с учетом этого предостережения.

Код

Конструктор класса Complex имеет два параметра.

  • real
  • imaginary
export class Complex {
  constructor(real, imaginary) {
    this.real = real;
    this.imaginary = imaginary;
  }
}

Нуль

Ноль равен 0 + 0i.

/***
 * Generate a new <code>Complex(0,0)</code>
 * @returns {Complex}
 */
static zero = () => new Complex(0, 0);

нанизывать()

/***
 * Returns a string in the form <code>a ± bi</code>.
 * @returns {string}
 */
toString() {
    const operator = this.imaginary < 0 ? '-' : '+';
    return `${this.real} ${operator} ${Math.abs(this.imaginary)}i`;
}

Равно

/***
 * Both <i>real</i> and <i>imaginary</i> parts are equal.
 * @param other
 * @returns {boolean}
 */
equals(other) {
 return this.real === other.real && this.imaginary === other.imaginary;
}

Добавлять

add(other) {
 return new Complex(
 this.real + other.real,
 this.imaginary + other.imaginary
 );
}

Вычесть

subtract(other) {
 return new Complex(
 this.real — other.real,
 this.imaginary — other.imaginary
 );
}

Умножить

/***
 * Multiple this Complex with another.<br/>
 * <code>(a + bi)(c + di) = (ac - bd) + (ad + bc)i</code>
 * @param other
 * @returns {Complex}
 */
multiply(other) {
    return new Complex(
        this.real * other.real - this.imaginary * other.imaginary,
        this.real * other.imaginary + this.imaginary * other.real
    );
}

Разделение

/***
 * <code>(a + bi) / (c + di) = [(ac + bd) / (c^2 + d^2)] + [(bc - ad) / (c^2 + d^2)]i<code>
 * @param other
 */
divide(other) {
    const otherMagnitudeSquared =
        other.real * other.real + other.imaginary * other.imaginary;
    const r =
        (this.real * other.real + this.imaginary * other.imaginary) /
        otherMagnitudeSquared;
    const i =
        (this.imaginary * other.real - this.real * other.imaginary) /
        otherMagnitudeSquared;

    return new Complex(r, i);
}

Полный код

Класс приложения

   export class Complex {
    constructor(real, imaginary) {
        this.real = real;
        this.imaginary = imaginary;
    }

    /***
     * Generate a new <code>Complex(0,0)</code>
     * @returns {Complex}
     */
    static zero = () => new Complex(0, 0);

    add(other) {
        return new Complex(
            this.real + other.real,
            this.imaginary + other.imaginary
        );
    }

    subtract(other) {
        return new Complex(
            this.real - other.real,
            this.imaginary - other.imaginary
        );
    }

    /***
     * Multiple this Complex with another.<br/>
     * <code>(a + bi)(c + di) = (ac - bd) + (ad + bc)i</code>
     * @param other
     * @returns {Complex}
     */
    multiply(other) {
        return new Complex(
            this.real * other.real - this.imaginary * other.imaginary,
            this.real * other.imaginary + this.imaginary * other.real
        );
    }

    /***
     * <code>(a + bi) / (c + di) = [(ac + bd) / (c^2 + d^2)] + [(bc - ad) / (c^2 + d^2)]i<code>
     * @param other
     */
    divide(other) {
        const otherMagnitudeSquared =
            other.real * other.real + other.imaginary * other.imaginary;
        const r =
            (this.real * other.real + this.imaginary * other.imaginary) /
            otherMagnitudeSquared;
        const i =
            (this.imaginary * other.real - this.real * other.imaginary) /
            otherMagnitudeSquared;

        return new Complex(r, i);
    }

    magnitude() {
        return Math.sqrt(
            this.real * this.real + this.imaginary * this.imaginary
        );
    }

    /***
     * Returns a string in the form <code>a ± bi</code>.
     * @returns {string}
     */
    toString() {
        const operator = this.imaginary < 0 ? '-' : '+';
        return `${this.real} ${operator} ${Math.abs(this.imaginary)}i`;
    }

    /***
     * Both <i>real</i> and <i>imaginary</i> parts are equal.
     * @param other
     * @returns {boolean}
     */
    equals(other) {
        return this.real === other.real && this.imaginary === other.imaginary;
    }
}

Тестовые случаи

Здесь используется Витест.

import { describe, it, expect } from 'vitest';
import { Complex } from './Complex.js';

describe('Calling zero()', () => {
    const zero = Complex.zero();
    it('should return 0 real part', () => {
        expect(zero.real).toBe(0);
    });

    it('should return 0 imaginary part', () => {
        expect(zero.imaginary).toBe(0);
    });
});

describe('Creating a new number', () => {
    const expectedReal = Math.random();
    const expectedImaginary = Math.random();

    const actual = new Complex(expectedReal, expectedImaginary);

    it(`should return the expected real part (${expectedReal})`, () => {
        expect(actual.real).toBe(expectedReal);
    });

    it(`should return the expected imaginary part (${expectedImaginary})`, () => {
        expect(actual.imaginary).toBe(expectedImaginary);
    });
});

it('Should correctly calculate the magnitude', () => {
    const dummyReal = Math.random();
    const dummyImaginary = Math.random();
    const expected = Math.sqrt(
        dummyReal * dummyReal + dummyImaginary * dummyImaginary
    );

    const actual = new Complex(dummyReal, dummyImaginary).magnitude();

    console.dir({ dummyReal, dummyImaginary, actual, expected });

    expect(actual).toBe(expected);
});

describe('Equality', () => {
    it('should return true when equal', () => {
        const complex1 = new Complex(Math.random(), Math.random());
        const complex2 = new Complex(complex1.real, complex1.imaginary);

        const actual = complex1.equals(complex2);

        expect(actual).toBe(true);
    });

    it('should return false when real part differs', () => {
        const complex1 = new Complex(Math.random(), Math.random());
        const complex2 = new Complex(complex1.real + 1, complex1.imaginary);

        const actual = complex1.equals(complex2);

        expect(actual).toBe(false);
    });

    it('should return true when equal', () => {
        const complex1 = new Complex(Math.random(), Math.random());
        const complex2 = new Complex(complex1.real, complex1.imaginary + 1);

        const actual = complex1.equals(complex2);

        expect(actual).toBe(false);
    });
});

describe('Arithmetic', () => {
    const complex1 = new Complex(6, 3);
    const complex2 = new Complex(7, -5);

    describe('Add', () => {
        const expectedReal = complex1.real + complex2.real;
        const expectedImaginary = complex1.imaginary + complex2.imaginary;

        const actual = complex1.add(complex2);

        it(`Real part should be ${expectedReal}`, () => {
            expect(actual.real).toBe(expectedReal);
        });

        it(`Imaginary part should be ${expectedImaginary}`, () => {
            expect(actual.imaginary).toBe(expectedImaginary);
        });
    });

    describe('Subtract', () => {
        const expectedReal = complex1.real - complex2.real;
        const expectedImaginary = complex1.imaginary - complex2.imaginary;

        const actual = complex1.subtract(complex2);

        it(`Real part should be ${expectedReal}`, () => {
            expect(actual.real).toBe(expectedReal);
        });

        it(`Imaginary part should be ${expectedImaginary}`, () => {
            expect(actual.imaginary).toBe(expectedImaginary);
        });
    });

    describe('Multiply', () => {
        const expectedReal = complex1.real + complex2.real;
        const expectedImaginary = complex1.imaginary + complex2.imaginary;

        const actual = complex1.add(complex2);

        it(`Real part should be ${expectedReal}`, () => {
            expect(actual.real).toBe(expectedReal);
        });

        it(`Imaginary part should be ${expectedImaginary}`, () => {
            expect(actual.imaginary).toBe(expectedImaginary);
        });
    });

    describe('Divide', () => {
        const expectedReal = 27 / 74;
        const expectedImaginary = 51 / 74;

        const actual = complex1.divide(complex2);
        it(`should have correct real`, () => {
            expect(actual.real).toBe(expectedReal);
        });

        it(`should have correct imaginary`, () => {
            expect(actual.imaginary).toBe(expectedImaginary);
        });
    });
});

describe('toString()', () => {
    it('for positive imaginary part', () => {
        const dummyReal = 1;
        const dummyImaginary = 1;

        const expected = `${dummyReal} + ${dummyImaginary}i`;

        const actual = new Complex(dummyReal, dummyImaginary).toString();

        expect(actual).toBe(expected);
    });

    it('for zero imaginary part', () => {
        const dummyReal = 1;
        const dummyImaginary = 0;

        const expected = `${dummyReal} + ${dummyImaginary}i`;

        const actual = new Complex(dummyReal, dummyImaginary).toString();

        expect(actual).toBe(expected);
    });

    it('for negative imaginary part', () => {
        const dummyReal = 1;
        const dummyImaginary = -1;

        const expected = `${dummyReal} - ${Math.abs(dummyImaginary)}i`;

        const actual = new Complex(dummyReal, dummyImaginary).toString();

        expect(actual).toBe(expected);
    });
});

Заключение

Должен признаться, было довольно весело заново открывать для себя эти выводы, я давно не занимался векторной математикой.

Это была не совсем захватывающая статья, она очень сухая, но функции важны, если вы хотите поиграть с множествами Мандельброта или Жюлиа или с чем-то, что требует комплексных чисел.

Спасибо за прочтение.