Module:FormatNumber

From The Perfect Tower II
Revision as of 18:28, 19 March 2021 by Lmnt0 (talk | contribs) (Created page with "--[[---------------------------------------------------------------------------- Module:Num For displaying large numbers in a consistent format Usage:...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Documentation for this module may be created at Module:FormatNumber/doc

--[[----------------------------------------------------------------------------
    Module:Num
        For displaying large numbers in a consistent format

    Usage:
        {{#invoke:FormatNumber|main|<number>|format=<format>}}

        number - The number to format, in long or scientific format
               - e.g. 1e34
        format - (Optional) force a specific format, scientific or named
----------------------------------------------------------------------------]]--
local p = {}

local THRESHOLD = 1e6 -- Anything below this number is displayed in long format

local LANG = mw.language.getContentLanguage()

local suffixes = {
    't', 'M', 'B', 'T', 'Qa', 'Qi', 'Sx', 'Sp', 'Oc', 'No', 'De',
    'UDe', 'DDe', 'TDe', 'QaD', 'QiD', 'SxD', 'SpD', 'OcD', 'NoD',
    'Vi', 'UVi', 'DVi', 'TVi', 'QaV', 'QiV', 'SxV', 'SpV', 'OcV', 'NoV',
    'Tr', 'UTr', 'DTr', 'TTr', 'QaT', 'QiT', 'SxT', 'SpT', 'OcT', 'NoT',
    'Qua', 'UQu', 'DQu', 'TQu', 'QQu', 'QiQ', 'SxQ', 'SpQ', 'OcQ', 'NoQ',
    'Qui', 'UQi', 'DQi', 'TQi', 'QQi', 'QiQi', 'SxQi', 'SpQi', 'OcQi', 'NoQi',
    'Sg', 'USg', 'DSg', 'TSg', 'QSg', 'QiS', 'SxS', 'SpS', 'OcS', 'NoS',
    'Spt', 'USp', 'DSp', 'TSp', 'QSp', 'QiSp', 'SxSp', 'SpSp', 'OcSp', 'NoSp',
    'Og', 'UOg', 'DOg', 'TOg', 'QOg', 'QiO', 'SxO', 'SpO', 'OcO', 'NoO',
    'Ng', 'UNg', 'DNg', 'TNg', 'QNg', 'QiNg', 'SxN', 'SpN', 'OcN', 'NoN',
    'Ce', 'UCe', 'DCe'
}

-- Round a value to X decimal places
local function round(value, decimals)
    local p = 10^(decimals or 3)
    local n = value * p
    return (value >= 0 and math.floor(n + 0.5) or math.ceil(n - 0.5)) / p
end

local function belowThreshold(value)
    return (value > -THRESHOLD and value < THRESHOLD)
end

-- Ensure a number is a valid number and return it
local function getNumber(value)
    local num
    if value == '-inf' or value == '-Infinity' then
        num = -math.huge
    elseif value == 'inf' or value == 'Infinity' then
        num = math.huge
    else
        -- use this instead of tonumber() so we can parse thousand seperators
        num = LANG:parseFormattedNumber(value)
    end
    if not num or num == nil then
        error(string.format('"%s" is not a valid number', value))
    end
    return num
end

-- Get the number for use in data-sort-value attributes
local function getSortValue(value)
    if belowThreshold(value) then return string.format('%d', value) end
    return string.format('%e', value)
end

-- Get a number in scientific notation, ingame style (e.g. 12e7)
local function getScientific(value)
    -- Do this manually rather than using string.format "%e" representation,
    -- so that we can match output to ingame notation
    local exponent = math.floor(math.log10(math.abs(value)))
    local mantissa = round(value / (10^exponent), 3)
    return string.format('%se%s', mantissa, exponent)
end

-- Get a number with its suffix (e.g. 120 M)
local function getNamed(value)
    local exponent = math.floor(math.log10(math.abs(value)))
    local remainder = exponent % 3
    return string.format(
        '%s %s',
        round(value / (10 ^ (exponent - remainder)), 3), -- number
        suffixes[math.floor(exponent / 3)] -- suffix
    )
end

-- Choose a number representation depending on the size of the number
-- Second parameter forces format: 'named' or 'scientific'
-- For modules only - do not call this from template, use 'main' instead.
-- No type checking is performed on 'value' - this is the responsibility
-- of the calling function
function p.format(value, format)
    if value == -math.huge then
        return '-Infinity'
    elseif value == math.huge then
        return 'Infinity'
    elseif belowThreshold(value) then
        return LANG:formatNum(value)
    elseif format == 'named' then
        return string.format(
            '<abbr title="%s">%s</abbr>', getScientific(value), getNamed(value)
        )
    elseif format == 'scientific' then
        return string.format(
            '<abbr title="%s">%s</abbr>', getNamed(value), getScientific(value)
        )
    else
        return string.format('%s (%s)', getNamed(value), getScientific(value))
    end
end

-- Use this function when calling from template
-- See usage above
function p.main(frame)
    if not frame then error('No frame found') end
    local rawNum = frame.args[1] or frame:getParent().args[1]
    local format = frame.args.format or frame:getParent().args.format
    if not rawNum or rawNum == '' then error('Number is required') end
    local num = getNumber(rawNum)

    return mw.html.create('span')
        :wikitext(p.format(num, format))
        :attr('data-sort-value', getSortValue(num))
end

return p