Module:FormatNumber: Difference between revisions
add ability to add an icon (e.g. for currency) |
Speedybrain (talk | contribs) No edit summary |
||
| (4 intermediate revisions by 2 users not shown) | |||
| Line 1: | Line 1: | ||
--[[---------------------------------------------------------------------------- | --[[---------------------------------------------------------------------------- | ||
Module: | Module:FormatNumber | ||
For displaying large numbers in a consistent format | For displaying large numbers in a consistent format | ||
See Template:Number | See Template:Number | ||
| Line 53: | Line 53: | ||
local function getSortValue(value) | local function getSortValue(value) | ||
if belowThreshold(value) then | if belowThreshold(value) then | ||
return | return tostring(value) | ||
elseif value == -math.huge then | elseif value == -math.huge then | ||
return '-2e+308' -- lower than the lowest possible non-inf number | return '-2e+308' -- lower than the lowest possible non-inf number | ||
| Line 63: | Line 63: | ||
end | end | ||
-- Get a number in scientific notation, ingame style (e.g. | -- 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% | -- 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 79: | 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 | local group = math.floor(math.floor(math.log10(math.abs(value))) / 3) | ||
local | 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 %s%s', | ||
prefix or '', | prefix or '', | ||
number, | |||
SUFFIXES[ | SUFFIXES[group], -- number suffix | ||
suffix or '' | suffix or '' | ||
) | ) | ||
| Line 91: | Line 105: | ||
-- Choose a number representation depending on the size of the number | -- 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. | -- 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 | ||
| Line 117: | Line 131: | ||
'<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 | ||
-- 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( | formatted = string.format( | ||
'%s | '<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 | end | ||
| Line 146: | Line 164: | ||
local sortkey = frame.args.sortkey or frame:getParent().args.sortkey | local sortkey = frame.args.sortkey or frame:getParent().args.sortkey | ||
local icon = frame.args.icon or frame:getParent().args.icon | local icon = frame.args.icon or frame:getParent().args.icon | ||
local iconPos = frame.args. | local iconPos = frame.args.iconpos or frame:getParent().args.iconpos | ||
local num = parseNumber(input) | local num = parseNumber(input) | ||
if num then | if num then | ||
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 and1e7notation work, and anything that isn't a number comes back unchanged),format,prefix,suffix,sortkey,iconandiconpos. The result is wrapped in a span withdata-sort-value, for sortable tables.format(value, prefix, suffix, icon, iconPos, format): for other modules.valuemust be a number; it isn't checked.formatset tonamedorscientificforces one form for every reader, with the other in anabbrtooltip.iconfollows the number, or comes before it withiconPos = "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 %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 %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