Ir para o conteúdo

Módulo:Math

Permanently protected module
Origem: Wikipédia, a enciclopédia livre.

--[[

Este módulo fornece uma série de operações matemáticas básicas.

]]

local yesno, getArgs -- inicialização tardia (lazy load)

local p = {} -- Armazena funções a serem retornadas por #invoke e funções disponíveis para outros módulos Lua.
local wrap = {} -- Armazena funções wrapper que processam argumentos vindos de #invoke.

--[[
Funções auxiliares usadas para evitar código redundante.
]]

local function err(msg)
    -- Gera mensagens de erro em wikitexto.
    return mw.ustring.format('<strong class="error">Erro de formatação: %s</strong>', msg)
end

local function unpackNumberArgs(args)
    -- Retorna uma lista desempacotada de argumentos especificados com chaves numéricas.
    local ret = {}
    for k, v in pairs(args) do
        if type(k) == 'number' then
            table.insert(ret, v)
        end
    end
    return unpack(ret)
end

local function makeArgArray(...)
    -- Cria um array de argumentos a partir de uma lista que pode conter nils.
    local args = {...} -- Tabela de argumentos. Pode conter nils ou valores não numéricos, por isso não usamos ipairs.
    local nums = {} -- Armazena os índices dos argumentos numéricos válidos.
    local ret = {}
    for k, v in pairs(args) do
        v = p._cleanNumber(v)
        if v then
            nums[#nums + 1] = k
            args[k] = v
        end
    end
    table.sort(nums)
    for i, num in ipairs(nums) do
        ret[#ret + 1] = args[num]
    end
    return ret
end

local function fold(func, ...)
    -- Aplica uma função a todos os argumentos fornecidos e retorna o resultado.
    local vals = makeArgArray(...)
    local count = #vals -- O número de argumentos válidos
    if count == 0 then return
        -- Sai se não houver argumentos válidos para evitar erro ao remover o primeiro argumento.
        nil, 0
    end
    local ret = table.remove(vals, 1)
    for _, val in ipairs(vals) do
        ret = func(ret, val)
    end
    return ret, count
end

--[[
Agrupa argumentos escolhendo valores seletivamente.
]]
local function binary_fold(func, ...)
    local value = fold((function(a, b) if func(a, b) then return a else return b end end), ...)
    return value
end

--[[
random

Gera um número aleatório

Uso:
{{#invoke: Math | random }}
{{#invoke: Math | random | valor máximo }}
{{#invoke: Math | random | valor mínimo | valor máximo }}
]]

function wrap.random(args)
    local first = p._cleanNumber(args[1])
    local second = p._cleanNumber(args[2])
    return p._random(first, second)
end

function p._random(first, second)
    math.randomseed(mw.site.stats.edits + mw.site.stats.pages + os.time() + math.floor(os.clock() * 1000000000))
    if first and second then
        if first <= second then
            return math.random(first, second)
        end
    elseif first then
        return math.random(first)
    else
        return math.random()
    end
end

--[[
order

Determina a ordem de grandeza de um número

Uso:
{{#invoke: Math | order | valor }}
]]

function wrap.order(args)
    local input_string = (args[1] or args.x or '0');
    local input_number = p._cleanNumber(input_string);
    if input_number == nil then
        return err('a entrada de ordem de grandeza parece não ser numérica')
    else
        return p._order(input_number)
    end
end

function p._order(x)
    if x == 0 then return 0 end
    return math.floor(math.log10(math.abs(x)))
end

--[[
precision

Determina a precisão de um número usando sua representação em string

Uso:
{{ #invoke: Math | precision | valor }}
]]

function wrap.precision(args)
    local input_string = (args[1] or args.x or '0');
    local trap_fraction = args.check_fraction;

    if not yesno then
        yesno = require('Módulo:Yesno')
    end
    if yesno(trap_fraction, true) then
        local pos = string.find(input_string, '/', 1, true);
        if pos ~= nil then
            if string.find(input_string, '/', pos + 1, true) == nil then
                local denominator = string.sub(input_string, pos+1, -1);
                local denom_value = tonumber(denominator);
                if denom_value ~= nil then
                    return math.log10(denom_value);
                end
            end
        end
    end

    local _, clean_string = p._cleanNumber(input_string);
    if clean_string == nil then
        return err('a entrada de precisão parece não ser numérica')
    else
        return p._precision(clean_string)
    end
end

function p._precision(x)
    if type(x) == 'number' then
        x = tostring(x)
    elseif type(x) == 'string' then
        x = x:gsub(',', '.')
    end

    local result = 0;

    -- Subtrai expoente
    local _, _, coefficient, exponent = x:find("^(.*)[eE](.*)$")
    if coefficient then
        result = result - (tonumber(exponent) or 0)
        x = coefficient
    end

    -- Adiciona casas decimais
    local i, j = x:find('%..*$')
    if i then
        result = result + (j - i)
        return result
    end

    -- Subtrai zeros à direita
    i, j = x:find('[^0]0+$')
    if i then
        result = result - (j - i)
    end

    return result
end

--[[
max

Encontra o valor máximo entre os argumentos

Uso:
{{#invoke:Math| max | valor1 | valor2 | ... }}
]]

function wrap.max(args)
    return p._max(unpackNumberArgs(args))
end

function p._max(...)
    local max_value = binary_fold((function(a, b) return a > b end), ...)
    if max_value then
        return max_value
    end
end

--[[
median

Encontra a mediana de um conjunto de números

Uso:
{{#invoke:Math | median | número1 | número2 | ...}}
]]

function wrap.median(args)
    return p._median(unpackNumberArgs(args))
end

function p._median(...)
    local vals = makeArgArray(...)
    local count = #vals
    table.sort(vals)

    if count == 0 then
        return 0
    end

    if p._mod(count, 2) == 0 then
        return (vals[count/2] + vals[count/2+1])/2
    else
        return vals[math.ceil(count/2)]
    end
end

--[[
min

Encontra o valor mínimo entre os argumentos

Uso:
{{#invoke:Math| min | valor1 | valor2 | ... }}
]]

function wrap.min(args)
    return p._min(unpackNumberArgs(args))
end

function p._min(...)
    local min_value = binary_fold((function(a, b) return a < b end), ...)
    if min_value then
        return min_value
    end
end

--[[
sum

Calcula a soma dos argumentos

Uso:
{{#invoke:Math| sum | valor1 | valor2 | ... }}
]]

function wrap.sum(args)
    return p._sum(unpackNumberArgs(args))
end

function p._sum(...)
    local sums = fold((function(a, b) return a + b end), ...)
    if not sums then
        return 0
    else
        return sums
    end
end

--[[
average

Calcula a média dos argumentos

Uso:
{{#invoke:Math| average | valor1 | valor2 | ... }}
]]

function wrap.average(args)
    return p._average(unpackNumberArgs(args))
end

function p._average(...)
    local sum, count = fold((function(a, b) return a + b end), ...)
    if not sum then
        return 0
    else
        return sum / count
    end
end

--[[
round

Arredonda um número para a precisão especificada

Uso:
{{#invoke:Math | round | valor | precisão }}
]]

function wrap.round(args)
    local value = p._cleanNumber(args[1] or args.value or 0)
    local precision = p._cleanNumber(args[2] or args.precision or 0)
    if value == nil or precision == nil then
        return err('a entrada para arredondar parece não ser numérica')
    else
        return p._round(value, precision)
    end
end

function p._round(value, precision)
    local rescale = math.pow(10, precision or 0);
    return math.floor(value * rescale + 0.5) / rescale;
end

--[[
log10

Retorna o logaritmo (base 10) de um número

Uso:
{{#invoke:Math | log10 | x }}
]]

function wrap.log10(args)
    local x = p._cleanNumber(args[1])
    return x and math.log10(x) or err('entrada inválida para log10')
end

--[[
mod

Implementa o operador resto da divisão (módulo)

Uso:
{{#invoke:Math | mod | x | y }}
]]

function wrap.mod(args)
    local x = p._cleanNumber(args[1])
    local y = p._cleanNumber(args[2])
    if not x then
        return err('o primeiro argumento de mod parece não ser numérico')
    elseif not y then
        return err('o segundo argumento de mod parece não ser numérico')
    else
        return p._mod(x, y)
    end
end

function p._mod(x, y)
    local ret = x % y
    if not (0 <= ret and ret < y) then
        ret = 0
    end
    return ret
end

--[[
gcd

Calcula o máximo divisor comum (MDC) de vários números

Uso:
{{#invoke:Math | gcd | valor 1 | valor 2 | valor 3 | ... }}
]]

function wrap.gcd(args)
    return p._gcd(unpackNumberArgs(args))
end

function p._gcd(...)
    local function findGcd(a, b)
        local r = b
        local oldr = a
        while r ~= 0 do
            local quotient = math.floor(oldr / r)
            oldr, r = r, oldr - quotient * r
        end
        if oldr < 0 then
            oldr = oldr * -1
        end
        return oldr
    end
    local result = fold(findGcd, ...)
    return result
end

--[[
precision_format

Arredonda um número para a precisão especificada e formata o resultado em string.

Uso:
{{#invoke: Math | precision_format | número | precisão }}
]]

function wrap.precision_format(args)
    local value_string = args[1] or 0
    local precision = args[2] or 0
    return p._precision_format(value_string, precision)
end

function p._precision_format(value_string, precision)
    local lang = mw.getContentLanguage();

    local value
    value, value_string = p._cleanNumber(value_string)
    precision = p._cleanNumber(precision)

    if value == nil or precision == nil then
        return err('entrada inválida ao arredondar')
    end

    local abs_value = math.abs(value)
    local current_precision = p._precision(value)
    local order = p._order(value)

    if order + precision >= 14 then
        if order + p._precision(value_string) >= 14 then
            precision = 13 - order;
        end
    end

    if precision < current_precision then
        value = p._round(value, precision)
        abs_value = math.abs(value)
        current_precision = p._precision(value)
    end

    local formatted_num = lang:formatNum(abs_value)

    if math.abs(order) >= 9 or string.find(formatted_num, 'E', 1, true) then
        value = value * (10 ^ (-order))
        abs_value = math.abs(value)
        current_precision = current_precision + order
        precision = precision + order
        formatted_num = lang:formatNum(abs_value)
    else
        order = 0;
    end
    
    formatted_num = ((value < 0) and '−' or '') .. formatted_num

    if current_precision < precision then
        local padding
        if current_precision <= 0 then
            if precision > 0 then
                local zero_sep = lang:formatNum(1.1)
                formatted_num = formatted_num .. zero_sep:sub(2,2)

                padding = precision
                if padding > 20 then
                    padding = 20
                end

                formatted_num = formatted_num .. string.rep('0', padding)
            end
        else
            padding = precision - current_precision
            if padding > 20 then
                padding = 20
            end
            formatted_num = formatted_num .. string.rep('0', padding)
        end
    end

    if order ~= 0 then
        if order < 0 then
            order = '−' .. lang:formatNum(math.abs(order))
        else
            order = lang:formatNum(order)
        end

        formatted_num = formatted_num .. '<span style="margin:0 .15em 0 .25em">×</span>10<sup>' .. order .. '</sup>'
    end

    return formatted_num
end

--[[
divide

Implementa o operador de divisão

Uso:
{{#invoke:Math | divide | x | y | round= | precision= }}
]]

function wrap.divide(args)
    local x = args[1]
    local y = args[2]
    local round = args.round
    local precision = args.precision
    if not yesno then
        yesno = require('Módulo:Yesno')
    end
    return p._divide(x, y, yesno(round), precision)
end

function p._divide(x, y, round, precision)
    local num_x = p._cleanNumber(x)
    local num_y = p._cleanNumber(y)

    if y == nil or y == "" then
        return err("Divisor vazio")
    elseif not num_y then
        if type(y) == 'string' and string.sub(y, 1, 1) == '<' then
            return y
        else
            return err("Não é um número: " .. y)
        end
    elseif x == nil or x == "" then
        return err("Dividendo vazio")
    elseif not num_x then
        if type(x) == 'string' and string.sub(x, 1, 1) == '<' then
            return x
        else
            return err("Não é um número: " .. x)
        end
    else
        local z = num_x / num_y
        if round then
            return p._round(z, 0)
        elseif precision then
            return p._round(z, precision)
        else
            return z    
        end
    end
end

--[[
Função auxiliar que interpreta a entrada numericamente. Trata separadores decimais (vírgula/ponto)
e numeração formatada (ex: 1.234,56 ou 1,234.56). Se a entrada não for diretamente numérica,
tenta avaliá-la como uma expressão via ParserFunctions.
]]

function p._cleanNumber(number_string, unformat)
    if type(number_string) == 'number' then
        return number_string, tostring(number_string)
    elseif type(number_string) ~= 'string' or not number_string:find('%S') then
        return nil, nil
    end

    -- Limpa espaços nas pontas
    number_string = number_string:match("^%s*(.-)%s*$")

    -- 1. Remove reticências (...) do final de constantes físicas (ex: "12,5..." -> "12,5")
    number_string = number_string:gsub("%.%.%.$", ""):match("^%s*(.-)%s*$")

    -- 2. Rejeita separadores malformados intermediários (ex: "1..2" ou "1,,2")
    if number_string:find('%.%.') or number_string:find(',,') then
        return nil, nil
    end

    -- 3. Trata casos com AMBOS os separadores (ex: 1.234,56 ou 1,234.56)
    if number_string:find('%.') and number_string:find(',') then
        local last_dot = number_string:find('%.[^%.]*$')
        local last_comma = number_string:find(',[^,]*$')
        
        if last_comma > last_dot then
            -- Padrão PT/BR: 1.234,56 -> remove ponto de milhar, troca vírgula por ponto
            number_string = number_string:gsub('%.', ''):gsub(',', '.')
        else
            -- Padrão EN: 1,234.56 -> remove vírgula de milhar
            number_string = number_string:gsub(',', '')
        end
    -- 4. Trata casos com APENAS VÍRGULA (ex: 12,34)
    elseif number_string:find(',') then
        number_string = number_string:gsub(',', '.')
    end

    -- Conversão direta para número no Lua
    local number = tonumber(number_string)

    -- Tentativa via ParserFunctions #expr caso contenha expressões (ex: "5 + 2,5")
    if number == nil then
        -- Substitui vírgulas residuais por pontos antes de enviar ao #expr
        local expr_string = number_string:gsub(',', '.')
        local success, result = pcall(mw.ext.ParserFunctions.expr, expr_string)
        if success then
            number = tonumber(result)
            number_string = tostring(number)
        else
            number = nil
            number_string = nil
        end
    else
        number_string = number_string:match("^%+(.*)$") or number_string -- Remove sinal de + inicial se houver.
        if number_string:find('^%-?0[xX]') then
            -- Se usar notação hexadecimal 0xnnn, utiliza o número detectado pelo Lua.
            number_string = tostring(number)
        end
    end

    return number, number_string
end

--[[
Função wrapper que faz o processamento básico de argumentos. Garante que todas as funções vindas de #invoke
possam usar tanto o frame atual quanto o frame pai, removendo espaços e argumentos vazios.
]]

local mt = { __index = function(_, k)
    return function(frame)
        if not getArgs then
            getArgs = require('Módulo:Arguments').getArgs
        end
        return wrap[k](getArgs(frame))
    end
end }

return setmetatable(p, mt)