Difference between revisions of "Module:FormatNumber"

From The Perfect Tower II
Jump to navigation Jump to search
(Add prefix, suffix to Infinity and long formats)
(iconpos casing)
 
(5 intermediate revisions by the same user not shown)
Line 2: Line 2:
 
     Module:Num
 
     Module:Num
 
         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 string.format('%d', 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
  
Line 83: Line 82:
 
     local remainder = exponent % 3
 
     local remainder = exponent % 3
 
     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
 
         round(value / (10 ^ (exponent - remainder)), 3), -- number
         suffixes[math.floor(exponent / 3)], -- number suffix
+
         SUFFIXES[math.floor(exponent / 3)], -- number suffix
 
         suffix or ''
 
         suffix or ''
 
     )
 
     )
Line 96: Line 95:
 
-- 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 108:
 
         )
 
         )
 
     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 114:
 
         )
 
         )
 
     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),
Line 120: Line 120:
 
         )
 
         )
 
     else
 
     else
         return string.format(
+
         formatted = string.format(
             '%s (%s)',
+
             '%s \'\'(%s)\'\'',
 
             getNamed(value, prefix, suffix),
 
             getNamed(value, prefix, suffix),
 
             getScientific(value, prefix, suffix)
 
             getScientific(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 140:
 
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 02:32, 27 March 2021

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

--[[----------------------------------------------------------------------------
    Module:Num
        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 string.format('%d', 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. 12e7)
local function getScientific(value, prefix, suffix)
    -- 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('%s%se%s%s',
        prefix or '',
        mantissa,
        exponent,
        suffix or ''
    )
end

-- Get a number with its number suffix (e.g. 120 M)
local function getNamed(value, prefix, suffix)
    local exponent = math.floor(math.log10(math.abs(value)))
    local remainder = exponent % 3
    return string.format(
        '%s%s&#8239;%s%s',
        prefix or '',
        round(value / (10 ^ (exponent - remainder)), 3), -- number
        SUFFIXES[math.floor(exponent / 3)], -- number suffix
        suffix or ''
    )
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, 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)
        )
    else
        formatted = string.format(
            '%s \'\'(%s)\'\'',
            getNamed(value, prefix, suffix),
            getScientific(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