Jump to content

Module:FormatNumber: Difference between revisions

From The Perfect Tower II
Lmnt0 (talk | contribs)
Add prefix, suffix to Infinity and long formats
No edit summary
 
(9 intermediate revisions by 2 users not shown)
Line 1: Line 1:
--[[----------------------------------------------------------------------------
--[[----------------------------------------------------------------------------
     Module:Num
     Module:FormatNumber
         For displaying large numbers in a consistent format
         For displaying large numbers in a consistent format
 
         See Template:Number
    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 p = {}
Line 16: Line 10:
local LANG = mw.language.getContentLanguage()
local LANG = mw.language.getContentLanguage()


local suffixes = {
local SUFFIXES = {
     't', 'M', 'B', 'T', 'Qa', 'Qi', 'Sx', 'Sp', 'Oc', 'No', 'De',
     't', 'M', 'B', 'T', 'Qa', 'Qi', 'Sx', 'Sp', 'Oc', 'No', 'De',
     'UDe', 'DDe', 'TDe', 'QaD', 'QiD', 'SxD', 'SpD', 'OcD', 'NoD',
     'UDe', 'DDe', 'TDe', 'QaD', 'QiD', 'SxD', 'SpD', 'OcD', 'NoD',
Line 42: Line 36:


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


-- Get the number for use in data-sort-value attributes
-- Get the number for use in data-sort-value attributes (for table sorting)
local function getSortValue(value)
local function getSortValue(value)
     if belowThreshold(value) then return string.format('%d', value) end
     if belowThreshold(value) then
     return string.format('%e', value)
        return tostring(value)
    elseif value == -math.huge then
        return '-2e+308' -- lower than the lowest possible non-inf number
    elseif value == math.huge then
        return '2e+308' -- higher than the highest possible non-inf number
     else
        return string.format('%e', value)
    end
end
end


-- Get a number in scientific notation, ingame style (e.g. 12e7)
-- Get a number in scientific notation, ingame style (e.g. 1.2E7)
local function getScientific(value, prefix, suffix)
-- The E is uppercase, as in the game and the brand kit. With html it gets the kit's
-- E-notation style (.tpt-num__e, smaller than the digits); text for attributes such as
-- an abbr title stays plain
local function getScientific(value, prefix, suffix, html)
     -- Do this manually rather than using string.format "%e" representation,
     -- Do this manually rather than using string.format "%e" representation,
     -- so that we can match output to ingame notation
     -- so that we can match output to ingame notation
     local exponent = math.floor(math.log10(math.abs(value)))
     local exponent = math.floor(math.log10(math.abs(value)))
     local mantissa = round(value / (10^exponent), 3)
     local mantissa = round(value / (10^exponent), 3)
     return string.format('%s%se%s%s',
    -- Rounding can carry into the next power of ten (9.9996 becomes 10): 1E9, not 10E8
    if math.abs(mantissa) >= 10 then
        mantissa = round(mantissa / 10, 3)
        exponent = exponent + 1
    end
     return string.format('%s%s%s%s%s',
         prefix or '',
         prefix or '',
         mantissa,
         mantissa,
        html and '<span class="tpt-num__e">E</span>' or 'E',
         exponent,
         exponent,
         suffix or ''
         suffix or ''
Line 80: Line 88:
-- Get a number with its number suffix (e.g. 120 M)
-- Get a number with its number suffix (e.g. 120 M)
local function getNamed(value, prefix, suffix)
local function getNamed(value, prefix, suffix)
     local exponent = math.floor(math.log10(math.abs(value)))
     local group = math.floor(math.floor(math.log10(math.abs(value))) / 3)
     local remainder = exponent % 3
     local number = round(value / (10 ^ (group * 3)), 3)
    -- Rounding can carry into the next suffix (999.9996 M becomes 1000 M): 1 B instead
    if math.abs(number) >= 1000 and SUFFIXES[group + 1] then
        number = round(number / 1000, 3)
        group = group + 1
    end
     return string.format(
     return string.format(
         '%s%s%s%s',
         '%s%s&#8239;%s%s',
         prefix or '',
         prefix or '',
         round(value / (10 ^ (exponent - remainder)), 3), -- number
         number,
         suffixes[math.floor(exponent / 3)], -- number suffix
         SUFFIXES[group], -- number suffix
         suffix or ''
         suffix or ''
     )
     )
Line 92: Line 105:


-- Choose a number representation depending on the size of the number
-- Choose a number representation depending on the size of the number
-- Second parameter forces format: 'named' or 'scientific'
-- The format parameter forces one form for every reader: 'named' or 'scientific'
-- For modules only - do not call this from template, use 'main' instead.
-- For modules only - do not call this from template, use 'main' instead.
-- No type checking is performed on 'value' - this is the responsibility
-- No type checking is performed on 'value' - this is the responsibility
-- of the calling function
-- of the calling function
function p.format(value, prefix, suffix, format)
function p.format(value, prefix, suffix, icon, iconPos, format)
    local formatted = ''
     if value == -math.huge then
     if value == -math.huge then
         return string.format('%s-Infinity%s', prefix or '', suffix or '')
         formatted = string.format('%s-Infinity%s', prefix or '', suffix or '')
     elseif value == math.huge then
     elseif value == math.huge then
         return string.format('%sInfinity%s', prefix or '', suffix or '')
         formatted = string.format('%sInfinity%s', prefix or '', suffix or '')
     elseif belowThreshold(value) then
     elseif belowThreshold(value) then
         return string.format('%s%s%s',
         formatted = string.format('%s%s%s',
             prefix or '',
             prefix or '',
             LANG:formatNum(value),
             LANG:formatNum(value),
Line 108: Line 122:
         )
         )
     elseif format == 'named' then
     elseif format == 'named' then
         return string.format(
         formatted = string.format(
             '<abbr title="%s">%s</abbr>',
             '<abbr title="%s">%s</abbr>',
             getScientific(value, prefix, suffix),
             getScientific(value, prefix, suffix),
Line 114: Line 128:
         )
         )
     elseif format == 'scientific' then
     elseif format == 'scientific' then
         return string.format(
         formatted = string.format(
             '<abbr title="%s">%s</abbr>',
             '<abbr title="%s">%s</abbr>',
             getNamed(value, prefix, suffix),
             getNamed(value, prefix, suffix),
             getScientific(value, prefix, suffix)
             getScientific(value, prefix, suffix, true)
         )
         )
     else
     else
         return string.format(
         -- Both forms; MediaWiki:Common.css shows the one the reader picked under Appearance
             '%s (%s)',
        -- (scientific by default), and each names the other in its tooltip
        formatted = string.format(
             '<span class="wiki-num__sci" title="%s">%s</span><span class="wiki-num__named" title="%s">%s</span>',
             getNamed(value, prefix, suffix),
             getNamed(value, prefix, suffix),
             getScientific(value, prefix, suffix)
             getScientific(value, prefix, suffix, true),
     )
            getScientific(value, prefix, suffix),
            getNamed(value, prefix, suffix)
        )
    end
     if icon then
        return string.format('%s&nbsp;%s',
            iconPos == 'left' and icon or formatted,
            iconPos == 'left' and formatted or icon
        )
    else
        return formatted
     end
     end
end
end
Line 132: Line 158:
function p.main(frame)
function p.main(frame)
     if not frame then error('No frame found') end
     if not frame then error('No frame found') end
     local rawNum = frame.args[1] or frame:getParent().args[1]
     local input = frame.args[1] or frame:getParent().args[1]
     local format = frame.args.format or frame:getParent().args.format
     local format = frame.args.format or frame:getParent().args.format
     local prefix = frame.args.prefix or frame:getParent().args.prefix
     local prefix = frame.args.prefix or frame:getParent().args.prefix
     local suffix = frame.args.suffix or frame:getParent().args.suffix
     local suffix = frame.args.suffix or frame:getParent().args.suffix
     if not rawNum or rawNum == '' then error('Number is required') end
     local sortkey = frame.args.sortkey or frame:getParent().args.sortkey
     local num = getNumber(rawNum)
    local icon = frame.args.icon or frame:getParent().args.icon
 
    local iconPos = frame.args.iconpos or frame:getParent().args.iconpos
    return mw.html.create('span')
     local num = parseNumber(input)
        :wikitext(p.format(num, prefix, suffix, format))
    if num then
        :attr('data-sort-value', getSortValue(num))
        return mw.html.create('span')
            :wikitext(p.format(num, prefix, suffix, icon, iconPos, format))
            :attr('data-sort-value', sortkey or getSortValue(num))
    else
        return input
    end
end
end


return p
return p

Latest revision as of 01:41, 7 October 2026

Formats numbers as the game does. It backs {{Number}} and, through it, {{Cost}}. Module:UI (crafting cost) and Module:ModuleView (full view) call it directly.

Numbers below a million show in full, with thousands separators (120,000). Larger numbers show as scientific notation (1.2E7) or as named numbers (12 M); the reader picks under Appearance > Numbers, and scientific notation is the default. The module writes both forms, in span.wiki-num__sci and span.wiki-num__named, and MediaWiki:Common.css shows the one the reader picked. Each form has the other in its tooltip. The E is uppercase, as in the game, inside the brand kit class .tpt-num__e, which sets it smaller than the digits. Page previews (TextExtracts) leave out the named form. inf and Infinity show as Infinity.

Entry points

  • main(frame): for the template. It reads the arguments of the invoke, or else of the calling template: 1 (the number; thousands separators and 1e7 notation work, and anything that isn't a number comes back unchanged), format, prefix, suffix, sortkey, icon and iconpos. The result is wrapped in a span with data-sort-value, for sortable tables.
  • format(value, prefix, suffix, icon, iconPos, format): for other modules. value must be a number; it isn't checked. format set to named or scientific forces one form for every reader, with the other in an abbr tooltip. icon follows the number, or comes before it with iconPos = "left".
local formatNumber = require('Module:FormatNumber').format
formatNumber(1.8e12, nil, '%') -- 1.8E12% or 1.8 T%, as the reader chose



--[[----------------------------------------------------------------------------
    Module:FormatNumber
        For displaying large numbers in a consistent format
        See Template:Number
----------------------------------------------------------------------------]]--
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
-- If value is not a valid number, return nil
local function parseNumber(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
    return num
end

-- Get the number for use in data-sort-value attributes (for table sorting)
local function getSortValue(value)
    if belowThreshold(value) then
        return tostring(value)
    elseif value == -math.huge then
        return '-2e+308' -- lower than the lowest possible non-inf number
    elseif value == math.huge then
        return '2e+308' -- higher than the highest possible non-inf number
    else
        return string.format('%e', value)
    end
end

-- Get a number in scientific notation, ingame style (e.g. 1.2E7)
-- The E is uppercase, as in the game and the brand kit. With html it gets the kit's
-- E-notation style (.tpt-num__e, smaller than the digits); text for attributes such as
-- an abbr title stays plain
local function getScientific(value, prefix, suffix, html)
    -- 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)
    -- Rounding can carry into the next power of ten (9.9996 becomes 10): 1E9, not 10E8
    if math.abs(mantissa) >= 10 then
        mantissa = round(mantissa / 10, 3)
        exponent = exponent + 1
    end
    return string.format('%s%s%s%s%s',
        prefix or '',
        mantissa,
        html and '<span class="tpt-num__e">E</span>' or 'E',
        exponent,
        suffix or ''
    )
end

-- Get a number with its number suffix (e.g. 120 M)
local function getNamed(value, prefix, suffix)
    local group = math.floor(math.floor(math.log10(math.abs(value))) / 3)
    local number = round(value / (10 ^ (group * 3)), 3)
    -- Rounding can carry into the next suffix (999.9996 M becomes 1000 M): 1 B instead
    if math.abs(number) >= 1000 and SUFFIXES[group + 1] then
        number = round(number / 1000, 3)
        group = group + 1
    end
    return string.format(
        '%s%s&#8239;%s%s',
        prefix or '',
        number,
        SUFFIXES[group], -- number suffix
        suffix or ''
    )
end

-- Choose a number representation depending on the size of the number
-- The format parameter forces one form for every reader: '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, prefix, suffix, icon, iconPos, format)
    local formatted = ''
    if value == -math.huge then
        formatted = string.format('%s-Infinity%s', prefix or '', suffix or '')
    elseif value == math.huge then
        formatted = string.format('%sInfinity%s', prefix or '', suffix or '')
    elseif belowThreshold(value) then
        formatted = string.format('%s%s%s',
            prefix or '',
            LANG:formatNum(value),
            suffix or ''
        )
    elseif format == 'named' then
        formatted = string.format(
            '<abbr title="%s">%s</abbr>',
            getScientific(value, prefix, suffix),
            getNamed(value, prefix, suffix)
        )
    elseif format == 'scientific' then
        formatted = string.format(
            '<abbr title="%s">%s</abbr>',
            getNamed(value, prefix, suffix),
            getScientific(value, prefix, suffix, true)
        )
    else
        -- Both forms; MediaWiki:Common.css shows the one the reader picked under Appearance
        -- (scientific by default), and each names the other in its tooltip
        formatted = string.format(
            '<span class="wiki-num__sci" title="%s">%s</span><span class="wiki-num__named" title="%s">%s</span>',
            getNamed(value, prefix, suffix),
            getScientific(value, prefix, suffix, true),
            getScientific(value, prefix, suffix),
            getNamed(value, prefix, suffix)
        )
    end
    if icon then
        return string.format('%s&nbsp;%s',
            iconPos == 'left' and icon or formatted,
            iconPos == 'left' and formatted or icon
        )
    else
        return formatted
    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 input = frame.args[1] or frame:getParent().args[1]
    local format = frame.args.format or frame:getParent().args.format
    local prefix = frame.args.prefix or frame:getParent().args.prefix
    local suffix = frame.args.suffix or frame:getParent().args.suffix
    local sortkey = frame.args.sortkey or frame:getParent().args.sortkey
    local icon = frame.args.icon or frame:getParent().args.icon
    local iconPos = frame.args.iconpos or frame:getParent().args.iconpos
    local num = parseNumber(input)
    if num then
        return mw.html.create('span')
            :wikitext(p.format(num, prefix, suffix, icon, iconPos, format))
            :attr('data-sort-value', sortkey or getSortValue(num))
    else
        return input
    end
end

return p