Module:Rational

Revision as of 17:20, 1 October 2022 by Plumtree (talk | contribs)
Module documentation[view] [edit] [history] [purge]
Todo: add documentation

local u = require('Module:Utils')
local p = {}

-- construct a rational number n/m
function p.new(n, m)
	m = m or 1
	if n == 0 then
		if m == 0 then
			return { nan = true }
		else
			return { zero = true, sign = u.signum(m) }
		end
	else
		if m == 0 then
			return { inf = true, sign = u.signum(n) }
		end
	end
	local sign = u.signum(n) * u.signum(m)
	n = n * u.signum(n)
	m = m * u.signum(m)
	local n_factors = u.prime_factorization_raw(n)
	local m_factors = u.prime_factorization_raw(m)
	local factors = n_factors
	factors.sign = sign
	for factor, power in pairs(m_factors) do
		factors[factor] = factors[factor] or 0
		factors[factor] = factors[factor] - power
		if factors[factor] == 0 then
			factors[factor] = nil
		end
	end
	return factors
end

function p.copy(a)
	b = {}
	for factor, power in pairs(a) do
		b[factor] = power
	end
	return b
end

-- multiply two rational numbers; integers are also allowed
function p.mul(a, b)
	if type(a) == 'number' then
		a = p.new(a)
	end
	if type(b) == 'number' then
		b = p.new(b)
	end
	-- special case: NaN
	if a.nan or b.nan then
		return { nan = true }
	end
	-- special case: infinities
	if (a.inf and not b.zero) or (b.inf and not a.zero) then
		return { inf = true, sign = a.sign * b.sign }
	end
	-- special case: infinity * zero
	if (a.inf and b.zero) or (b.inf and a.zero) then
		return { nan = true }
	end
	-- special case: zeros
	if a.zero or b.zero then
		return { zero = true, sign = a.sign * b.sign }
	end
	-- the regular case: both not NaN, not infinities, not zeros
	local c = p.copy(a)
	for factor, power in pairs(b) do
		if type(factor) == 'number' then
			c[factor] = c[factor] or 0
			c[factor] = c[factor] + power
			if c[factor] == 0 then
				c[factor] = nil
			end
		end
	end
	c.sign = a.sign * b.sign
	return c
end

-- compute 1/a for a rational number a; integers are also allowed
function p.inv(a)
	if type(a) == 'number' then
		a = p.new(a)
	end
	-- special case: NaN
	if a.nan then
		return { nan = true }
	end
	-- special case: infinity
	if a.inf then
		return { zero = true, sign = a.sign }
	end
	-- special case: zero
	if a.zero then
		return { inf = true, sign = a.sign }
	end
	-- the regular case: not NaN, not infinity, not zero
	b = {}
	for factor, power in pairs(a) do
		if type(factor) == 'number' then
			b[factor] = -power
		end
	end
	b.sign = a.sign
	return b
end

-- divide a rational number a by b; integers are also allowed
function p.div(a, b)
	return p.mul(a, p.inv(b))
end

-- add two rational numbers; integers are also allowed
function p.add(a, b)
	if type(a) == 'number' then
		a = p.new(a)
	end
	if type(b) == 'number' then
		b = p.new(b)
	end
	
	-- special case: NaN
	if a.nan or b.nan then
		return { nan = true }
	end
	-- special case: infinities
	if a.inf and b.inf then
		if a.sign == b.sign then
			return { inf = true, sign = a.sign }
		else
			return { nan = true }
		end
	end
	if a.inf then
		return { inf = true, sign = a.sign }
	end
	if b.inf then
		return { inf = true, sign = b.sign }
	end
	-- regular case: both not NaN, not infinities
	
	n_a, m_a = p.as_pair(a)
	n_b, m_b = p.as_pair(b)
	
	n = n_a * m_b + n_b * m_a
	m = m_a * m_b
	
	return p.new(n, m)
end

-- substract a rational number from another; integers are also allowed
function p.sub(a, b)
	return p.add(a, p.mul(b, -1))
end

-- determine whether a rational number is less than another; integers are also allowed
function p.lt(a, b)
	local c = p.sub(a, b)
	if c.zero then
		return false
	else
		return c.sign == -1
	end
end

-- determine whether a rational number is less or equal to the other; integers are also allowed
function p.leq(a, b)
	local c = p.sub(a, b)
	if c.zero then
		return true
	else
		return c.sign == -1
	end
end

-- determine whether a rational number is greater than another; integers are also allowed
function p.gt(a, b)
	local c = p.sub(a, b)
	if c.zero then
		return false
	else
		return c.sign == 1
	end
end

-- determine whether a rational number is greater or equal to the other; integers are also allowed
function p.geq(a, b)
	local c = p.sub(a, b)
	if c.zero then
		return true
	else
		return c.sign == 1
	end
end

-- determine whether a rational number is equal to another; integers are also allowed
function p.eq(a, b)
	local c = p.sub(a, b)
	return c.zero
end

-- determine whether a rational number is integer
function p.is_int(a)
	if type(a) == 'number' then
		return true
	end
	if a.nan then
		return false
	end
	if a.inf then
		return false
	end
	for factor, power in pairs(a) do
		if type(factor) == 'number' then
			if power < 0 then
				return false
			end
		end
	end
	return true
end

-- return the (n, m) pair as a Lua tuple
function p.as_pair(a)
	if type(a) == 'number' then
		a = p.new(a)
	end
	-- special case: NaN
	if a.nan then
		return 0, 0
	end
	-- special case: infinity
	if a.inf then
		return a.sign, 0
	end
	-- special case: zero
	if a.zero then
		return 0, a.sign
	end
	-- regular case: not NaN, not infinity, not zero
	local n = 1
	local m = 1
	for factor, power in pairs(a) do
		if type(factor) == 'number' then
			if power > 0 then
				n = n * (factor ^ power)
			else
				m = m * (factor ^ (-power))
			end
		end
	end
	n = n * a.sign
	return n, m
end

function p.as_ratio(a, separator)
	separator = separator or '/'
	local n, m = p.as_pair(a)
	return n .. separator .. m
end

-- return the {n, m} pair as a Lua table
function p.as_table(a)
	return {p.as_pair(a)}
end

-- return n / m as a float approximation
function p.as_float(a)
	local n, m = p.as_pair(a)
	return n / m
end

-- return a rational number in ket notation
-- NaN, infinity, zero values use special representations
function p.as_ket(a, frame)
	if type(a) == 'number' then
		a = p.new(a)
	end
	-- special case: NaN
	if a.nan then
		return 'NaN'
	end
	-- special case: infinity
	if a.inf then
		local sign = '+'
		if a.sign < 0 then
			sign = '-'
		end
		return sign .. '∞'
	end
	-- special case: zero
	if a.zero then
		return '0'
	end
	-- regular case: not NaN, not infinity, not zero
	
	local s = ''
	if a.sign < 0 then
		s = s .. '-'
	end
	-- preparing the argument
	local largest_prime = -1
	for factor, power in pairs(a) do
		if type(factor) == 'number' then
			if factor > largest_prime then
				largest_prime = factor
			end
		end
	end
	local template_arg = ''
	for i = 2, largest_prime do
		if u.is_prime(i) then
			if i > 2 then template_arg = template_arg .. ' ' end
			template_arg = template_arg .. (a[i] or 0)
		end
	end
	s = s .. frame:expandTemplate{
		title = 'Monzo',
		args = {template_arg}
	}
	return s
end

-- parse a rational number
-- returns nil on failure
function p.parse(unparsed)
	if type(unparsed) ~= 'string' then
		return nil
	end
	-- rational form
	local sign, n, _m, m = unparsed:match('^%s*(%-?)%s*(%d+)%s*(/%s*(%d+))%s*$')
	if n == nil then
		-- integer form
		sign, n = unparsed:match('^%s*(%-?)%s*(%d+)%s*$')
		if n == nil then
			-- parsing failure
			return nil
		else
			m = 1
			n = tonumber(n)
			if #sign > 0 then
				n = -n
			end
		end
	else
		n = tonumber(n)
		m = tonumber(m)
		if #sign > 0 then
			n = -n
		end
	end
	return p.new(n, m)
end

-- a version of as_ket() that can be {{#invoke:}}d
function p.ket(frame)
	local unparsed = frame.args[1] or '1'
	local a = p.parse(unparsed)
	if a == nil then
		return '<span style="color:red;">Invalid rational number: ' .. unparsed .. '.</span>'
	end
	return p.as_ket(a, frame)
end

return p