Possible improvements:
- localise functions of the
math
library, and sometimes, self.r
and self.i
. It will reduce the number of table lookups, improving the performance, and will make the expressions simpler,
- define
__call
metamethod for Complex
. It will allow you to replace Complex:new(x, y)
with Complex(x, y)
,
- the 'constructor' should correctly handle cases when it is called with one parameter (
Complex(5)
= 5 + 0i). It can be achieved with a 'nullsafe operator' emulated as local a = user_input or value_if_null
,
- in can also be mage more concise, using the fact that
setmetatable
returns the table,
__newindex
metamethod is not needed here, unless you want to set the absolute value or phase with code like c.abs = 1
or code.arg = pi
. In addition, the arguments to __newindex
are the table, the absent key and value set to it,
- you can emulate the ternary operator in Lua with
and
and or
, which allows to write more concise code: local a = condition and on_success or on_failure
,
- your arithmetic metamethods should handle the cases when the first argument (
self
) is a number,
- you can define
conjugate
method and __div
metamethod for division,
- to square a table element, I'd rather recommend using
^
rather than multiplying by self. It will save table lookups,
- you can define methods for polar coordinates and
__pow
metamethod for Euler's and de Moivre's formulae,
- the
__lt
metamethod can be simplified using boolean operations,
Complex:tostring()
can be simplified using 'ternary' operators, also handling of negative imaginary parts can be improved,
- whether to put the i before or after the imaginary part and whether to surround a complex number with parentheses can be customisable,
- the expression tests can be automated somewhat with
load()
function.
The improved code:
local math = require 'math'
local pi, sin, cos, atan2 = math.pi, math.sin, math.cos, math.atan2
local sqrt, exp, ln = math.sqrt, math.exp, math.log
local floor, abs = math.floor, math.abs
Complex = {
IMAGINARY_CHAR = 'i'
}
function Complex:new(r, i)
self.__index = self
return setmetatable({r = r, i = i or 0}, self)
end
setmetatable(Complex, {
__call = function (tbl, r, i)
return Complex:new(r, i)
end}
)
function Complex:__unm()
return Complex(-self.r, -self.i)
end
function Complex:__add(other)
return type(self) == 'number' and Complex(self + other.r, other.i)
or type(other) == 'number' and Complex(self.r + other, self.i)
or Complex(self.r + other.r, self.i + other.i)
end
function Complex:__sub(other)
return self + (-other);
end
function Complex:__mul(other)
return type(self) == 'number' and Complex(self * other.r, self * other.i)
or type(other) == 'number' and Complex(self.r * other, self.i * other)
or Complex(self.r * other.r - self.i * other.i, self.i * other.r + self.r * other.i)
end
function Complex:conjugate()
return Complex(self.r, -self.i)
end
function Complex:__div(denominator)
-- https://www.mesacc.edu/~scotz47781/mat120/notes/complex/dividing/dividing_complex.html
local conjugate = type(denominator) == 'number' and Complex(denominator) or denominator:conjugate()
local new_numerator, new_denominator = self * conjugate, denominator * conjugate
-- new_denominator is real.
return Complex(new_numerator.r / new_denominator.r, new_numerator.i / new_denominator.r)
end
function Complex:abs2()
return self.r ^ 2 + self.i ^ 2
end
function Complex:abs()
return sqrt(self:abs2())
end
function Complex:polar(abs, arg)
return Complex(abs * cos(arg), abs * sin(arg))
end
function Complex:arg()
return atan2(self.i, self.r)
end
function Complex:exp()
local abs = exp(self.r)
return Complex(abs * cos(self.i), abs * sin(self.i))
end
function Complex:__pow(power)
-- Euler:
local x = type(self) == 'number' and self or self.i == 0 and self.r > 0 and self.r or nil
if x then
return (power * ln(x)):exp()
else
-- de Moivre:
local n = type(power) == 'number' and power or power.i == 0 and power.r or nil
if n and floor(n) == n then
local abs, arg = self:abs(), self:arg()
return Complex:polar(abs ^ n, n * arg)
end
end
end
function Complex:__eq(other)
return
self.r == other.r and
self.i == other.i
end
function Complex:__lt(other)
-- incorrect but for sorting:
return self.r < other.r or self.r == other.r and self.i < other.i
end
function Complex:tostring(i, prefix, parentheses)
local im = type(i) == 'string' and i or 'i'
local r, i = self.r, self.i
local str = ((r ~= 0 or i == 0) and r or '')
.. (r ~= 0 and i > 0 and ' + ' or '')
.. (i < 0 and ' - ' or '')
.. (i ~= 0 and (prefix and im .. abs(i) or abs(i) .. im) or '')
if parentheses then
str = '(' .. str .. ')'
end
return str
end
function Complex:__tostring(i)
return self:tostring(self.IMAGINARY_CHAR, self.PREFIX, self.PARENTHESES)
end
-- ===================================================================
Complex.IMAGINARY_CHAR = 'j' -- electrical notation.
Complex.PREFIX = true
Complex.PARENTHESES = true
local context = {
Complex = Complex,
x = Complex(0, 10),
y = Complex(-5, 5),
z = Complex(5),
pi = pi
}
local cases = {
'x', 'y', 'z', 'x:abs()',
'x + y', 'x - y', 'x + 12', 'x - 12', '12 + x', '12 - x',
'x * y', 'y * 12', '2 * x',
'y:conjugate()', 'x / y', 'y / y', 'x / 5', '5 / x', '5 / x * x',
'y:abs()', 'y:arg() / pi * 180', 'Complex:polar(2, pi / 4)', 'y:exp()', 'x ^ 2', '2 ^ x',
'x == y', 'x ~= y', 'x < y', 'x > y', 'x <= y', 'x >= y'
}
for _, expr in ipairs(cases) do
print(expr, assert(load('return ' .. expr, nil, 't', context))())
end
```