Difference between revisions of "Module:FormatNumber"

From The Perfect Tower II
Jump to navigation Jump to search
(Fix sorting for infinity values, add sortkey parameter)
(iconpos casing)
 
(2 intermediate revisions by the same user not shown)
Line 10: 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 36: 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 45: 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
Line 87: Line 85:
 
         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 97: 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 109: 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 115: 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 121: 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 133: 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
 
     local sortkey = frame.args.sortkey or frame:getParent().args.sortkey
 
     local sortkey = frame.args.sortkey or frame:getParent().args.sortkey
     if not rawNum or rawNum == '' then error('Number is required') end
+
     local icon = frame.args.icon or frame:getParent().args.icon
     local num = getNumber(rawNum)
+
    local iconPos = frame.args.iconpos or frame:getParent().args.iconpos
 
+
     local num = parseNumber(input)
    return mw.html.create('span')
+
    if num then
        :wikitext(p.format(num, prefix, suffix, format))
+
        return mw.html.create('span')
        :attr('data-sort-value', sortkey or getSortValue(num))
+
            :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 03: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