Jump to content

Module:Formatnum/sandbox: Difference between revisions

From Wikipedia, the free encyclopedia
Content deleted Content added
Simplify
Wordify moved to Module:Wordify
 
Line 5: Line 5:
Helper functions used to avoid redundant code.
Helper functions used to avoid redundant code.
]]
]]

local function err(msg)
-- Generates wikitext error messages.
return mw.ustring.format('<strong class="error">Formatting error: %s</strong>', msg)
end


local function getArgs(frame)
local function getArgs(frame)
Line 21: Line 16:
return args
return args
end
end

local function _round(value, precision)
local rescale = math.pow(10, precision or 0);
return math.floor(value * rescale + 0.5) / rescale;
end

--[[
------------------------------------------------------------------------------------
-- isPositiveInteger
--
-- This function returns true if the given value is a positive integer, and false
-- if not. Although it doesn't operate on tables, it is included here as it is
-- useful for determining whether a given table key is in the array part or the
-- hash part of a table.
------------------------------------------------------------------------------------
--]]
function isPositiveInteger(v)
return type(v) == 'number' and v >= 1 and math.floor(v) == v and v < math.huge
end


--[[
------------------------------------------------------------------------------------
-- reverseNumKeys
--
-- This takes a table and returns an array containing the numbers of any numerical
-- keys that have non-nil values, sorted in reverse numerical order.
------------------------------------------------------------------------------------
--]]
local function reverseNumKeys(t)
local nums = {}
for k, v in pairs(t) do
if isPositiveInteger(k) then
nums[#nums + 1] = k
end
end
table.sort(nums, function(a, b) return a > b end)
return nums
end

--[[
------------------------------------------------------------------------------------
-- reverseSparseIpairs
--
-- This is a reverse iterator for sparse arrays. It can be used like a resersed ipairs, but can
-- handle nil values.
------------------------------------------------------------------------------------
--]]
local function reverseSparseIpairs(t)
local nums = reverseNumKeys(t)
local i = 0
local lim = #nums
return function ()
i = i + 1
if i <= lim then
local key = nums[i]
return key, t[key]
else
return nil, nil
end
end
end



function p.main(frame)
function p.main(frame)
Line 207: Line 139:


return number
return number
end

--[[
wordify

Usage:
{{#invoke:Formatnum | wordify | x | y | round= | precision= }}

--]]

local usa = {
[6] = 'million',
[9] = 'billion',
[12] = 'trillion',
[15] = 'quadrillion',
[18] = 'quintillion',
[21] = 'sextillion',
[24] = 'septillion',
[27] = 'octillion',
[30] = 'nonillion',
[33] = 'decillion',
[36] = 'undecillion',
[39] = 'duodecillion',
[42] = 'tredecillion',
[45] = 'quattuordecillion',
[48] = 'quindecillion',
[51] = 'sexdecillion',
[54] = 'septendecillion',
[57] = 'octodecillion',
[60] = 'novemdecillion',
[63] = 'vigintillion',
[100] = 'googol',
[303] = 'centillion'
}

local ind = {
[5] = 'lakh',
[7] = 'crore',
[12] = 'lakh crore',
[14] = 'crore crore'
}

local buildOne

local unit

local declension = {
['ca'] = { '', function (u)
local n = mw.ustring.len(u)
if mw.ustring.sub(u, n, n) == "d" then return u .. "s" else return mw.ustring.sub(u, 1, -2) .. "ons" end
end },
['da'] = { '', 'er' },
['de'] = { '', function (u)
local n = mw.ustring.len(u)
if mw.ustring.sub(u, n, n) == "e" then return u .. "n" else return u .. "en" end
end },
['eo'] = { '', 'j' },
['es'] = { function (arr, i, lang)
if i < 15 then
return buildOne(arr, i, lang)
end
local j = math.floor(i / 6)
if (j*6)+3 == i then
return "mil " .. unit(arr, i-3, lang, 1000), true
else
return buildOne(arr, i, lang)
end
end,
function (u)
if mw.ustring.sub(u, 1, 4) == "mil " then
return u
else
local n = mw.ustring.len(u)
if mw.ustring.sub(u, n, n) == "o" then return u .. "s" else return mw.ustring.sub(u, 1, -3) .. "ones" end
end
end },
['fr'] = { '', 's' },
['it'] = { '', function (u)
return mw.ustring.sub(u, 1, -2) .. "i"
end },
['la'] = { '', function (u)
local n = mw.ustring.len(u)
if mw.ustring.sub(u, n, n) == "o" then return u .. "nibus" else return mw.ustring.sub(u, 1, -3) .. "a" end
end },
['no'] = { '', 'er' },
['pl'] = { '', 'y', [5] = 'ów', ["fraction"] = 'a' },
['pt'] = { function (arr, i, lang)
if i < 9 then
return buildOne(arr, i, lang)
end
local j = math.floor(i / 6)
if (j*6)+3 == i then
return "mil " .. unit(arr, i-3, lang, 1000), true
else
return buildOne(arr, i, lang)
end
end,
function (u)
if mw.ustring.sub(u, 1, 4) == "mil " then
return u
else
return mw.ustring.sub(u, 1, -3) .. "ões"
end
end },
['sk'] = { '', function (u)
local n = mw.ustring.len(u)
if mw.ustring.sub(u, n, n) == "n" then return u .. 'y' else return mw.ustring.sub(u, 1, -2) .. "y" end
end,
[5] = function (u)
local n = mw.ustring.len(u)
if mw.ustring.sub(u, n, n) == "n" then return u .. 'ov' else return mw.ustring.sub(u, 1, -5) .. "árd" end
end,
["fraction"] = function (u)
local n = mw.ustring.len(u)
if mw.ustring.sub(u, n, n) == "n" then return u .. 'a' else return mw.ustring.sub(u, 1, -5) .. "árd" end
end
},
['sl'] = { '', function (u)
local n = mw.ustring.len(u)
if mw.ustring.sub(u, n, n) == "a" then return mw.ustring.sub(u, 1, -2) .. 'i' else return u .. "a" end
end,
function (u)
local n = mw.ustring.len(u)
if mw.ustring.sub(u, n, n) == "a" then return mw.ustring.sub(u, 1, -2) .. 'e' else return u .. "e" end
end,
[5] = function (u)
local n = mw.ustring.len(u)
if mw.ustring.sub(u, n, n) == "a" then return mw.ustring.sub(u, 1, -2) else return u .. "ov" end
end,
["fraction"] = function (u)
local n = mw.ustring.len(u)
if mw.ustring.sub(u, n, n) == "a" then return mw.ustring.sub(u, 1, -2) .. 'e' else return u .. "a" end
end
},
['sv'] = { '', 'er' }
}

local latin = {
['language_top'] = {
[''] = 306,
['ca'] = 60,
['da'] = 60,
['de'] = 60,
['en'] = 306,
['eo'] = 117,
['es'] = 120,
['fr'] = 123,
['it'] = 60,
['la'] = 306,
['nl'] = 60,
['no'] = 60,
['pl'] = 60,
['pt'] = 60,
['sk'] = 60,
['sl'] = 60,
['sv'] = 60
},
['exception'] = {
[6] = {
['pt'] = { 'milhão' }
},
[36] = {
['it'] = 'none'
},
[39] = {
['it'] = 'none'
},
[42] = {
['it'] = 'none'
},
[45] = {
['it'] = 'none'
},
[48] = {
['it'] = 'none'
},
[51] = {
['it'] = 'none',
},
[54] = {
['it'] = 'none'
},
[57] = {
['it'] = 'none'
}
},

['base'] = {
[''] = { 'lli' },
['ca'] = { 'li' },
['da'] = { 'lli' },
['de'] = { 'lli' },
['en'] = { 'lli' },
['eo'] = { 'li' },
['es'] = { 'll' },
['fr'] = { 'lli' },
['it'] = { 'li' },
['la'] = { 'lli' },
['nl'] = { 'lj' },
['no'] = { 'lli' },
['pl'] = { 'li' },
['pt'] = { 'li' },
['sk'] = { 'li' },
['sl'] = { 'lij' },
['sv'] = { 'lj' }
},
['suffix'] = {
[''] = { 'on', 'ard' },
['ca'] = { 'ó', 'ard' },
['da'] = { 'on', 'ard' },
['de'] = { 'on', 'arde' },
['en'] = { 'on', 'ard' },
['eo'] = { 'ono', 'ardo' },
['es'] = { 'ón', 'ardo' },
['fr'] = { 'on', 'ard' },
['it'] = { 'one', 'ardo' },
['la'] = { 'o', 'ardum' },
['nl'] = { 'oen', 'ard' },
['no'] = { 'on', 'ard' },
['pl'] = { 'on', 'ard' },
['pt'] = { 'ão' },
['sk'] = { 'ón', 'arda' },
['sl'] = { 'on', 'arda' },
['sv'] = { 'on', 'ard' }
},

['capitalize'] = {
['de'] = true
},
['prefix'] = { 'mi', 'bi', 'tri', 'quadri', 'quinti', 'sexti', 'septi', 'octi', 'noni', 'deci',
[16] = 'sedeci',
[20] = 'viginti',
[30] = 'triginti',
[40] = 'quadraginti',
[50] = 'quinquaginti',
[60] = 'sexaginti',
[70] = 'septuaginti',
[80] = 'octoginti',
[90] = 'nonaginti',
[100] = 'centi'
},
['unit_prefix'] = { 'un', 'duo', 'tre', 'quattuor', 'quin', 'sex', 'septen', 'octo', 'novem' },
['prefix_exception'] = {
[1] = {},
[2] = {
['eo'] = { 'dui' }
},
[3] = {
['pl'] = { 'try' }
},
[4] = {
['da'] = { 'kvadri' },
['eo'] = { 'kvari' },
['es'] = { 'cuatri' },
['no'] = { 'kvadri' },
['pl'] = { 'kwadry' },
['sk'] = { 'kvadri' },
['sl'] = { 'kvadri' },
['sv'] = { 'kvadri' }
},
[5] = {
['da'] = { 'kvinti' },
['eo'] = { 'kvini' },
['no'] = { 'kvinti' },
['pl'] = { 'kwinty' },
['sk'] = { 'kvinti' },
['sl'] = { 'kvinti' },
['sv'] = { 'kvinti' }
},
[6] = {
['da'] = { 'seksti' },
['eo'] = { 'sesi' },
['no'] = { 'seksti' },
['pl'] = { 'seksty' },
['sl'] = { 'seksti' }
},
[7] = {
['eo'] = { 'sepi' },
['pl'] = { 'septy' },
},
[8] = {
['da'] = { 'okti' },
['de'] = { 'Okti' },
['eo'] = { 'oki' },
['no'] = { 'okti' },
['pl'] = { 'okty' },
['sk'] = { 'okti' },
['sl'] = { 'okti' },
['sv'] = { 'okti' }
},
[9] = {
['eo'] = { 'naŭi' }
},
[10] = {
['de'] = { 'Dezi' },
['eo'] = { 'deki' },
['fr'] = { 'déci' },
['no'] = { 'desi' },
['pl'] = { 'decy' }
},
[11] = {
},
[12] = {
},
[13] = {
},
[14] = {
},
[15] = {
},
[16] = {
},
[17] = {
},
[18] = {
},
[19] = {
},
[20] = {
}
},
['unit_prefix_exception'] = {
[1] = {},
[2] = {
},
[3] = {
['fr'] = { 'tré' }
},
[4] = {
['eo'] = { 'kvatuor' },
['es'] = { 'cuatro' }
},
[5] = {
['eo'] = { 'kvin' }
},
[6] = {
['eo'] = { 'seks' }
},
[7] = {
},
[8] = {
['eo'] = { 'okto' }
},
[9] = {
['es'] = { 'noven' },
['fr'] = { 'noni' }
}
}
}

local function makeprefix(arr, j, lang)
local entry = arr['prefix_exception'][j][(lang or "")]
local p
if entry then
p = entry[1]
else
p = arr['prefix'][j]
if not p then
if j < 11 then
return nil
elseif p < 100 then
local k = math.floor(j / 10)
local d = k * 10
local l = j - d
local unitentry = arr['unit_prefix_exception'][l][(lang or "")]
local u
if unitentry then
u = unitentry[1]
else
u = arr['unit_prefix'][l]
end
local decentry = arr['prefix_exception'][d][(lang or "")]
local x
if decentry then
x = deceentry[1]
else
x = arr['prefix'][d]
end
p = u .. x
else
return nil
end
end
end
if arr['capitalize'][(lang or "")] then
return mw.ustring.upper(mw.ustring.sub(p, 1, 1)) .. mw.ustring.sub(p, 2)
else
return p
end
end

buildOne = function(arr, i, lang)
local j = math.floor(i / 6)
if j < 1 then
return nil
else
local pref = makeprefix(arr, j, lang)
local suf = (j * 6) == i and arr['suffix'][(lang or "")][1] or arr['suffix'][(lang or "")][2]
return pref .. arr['base'][(lang or "")][1] .. suf
end
end

local function makeOne(arr, i, lang)
local top = arr['exception'][i]
local entry = top and top[(lang or "")]
local one = nil
local simplify = nil
if entry then
one, simplify = entry[1], entry['simplify']
end
if one then
return one, simplify
else
if declension[lang] and declension[lang][1] then
local d = declension[lang][1]
if type(d) == 'function' then
return d(arr, i, lang)
end
end
return buildOne(arr, i, lang)
end
end

unit = function(arr, i, lang, r)
local one, simplify = makeOne(arr, i, lang)
local top = arr['exception'][i]
local entry = top and top[(lang or "")]
if r == 1 then
return one, simplify
elseif r == 2 then
if entry and entry[2] then
return entry and entry[2]
elseif declension[lang] and declension[lang][2] then
local d = declension[lang][2]
if type(d) == 'function' then
return d(one)
else
return one .. d
end
else
return one
end
elseif r == 3 or r == 4 then
if entry and entry[3] then
return entry and entry[3]
elseif declension[lang] and declension[lang][3] then
local d = declension[lang][3]
if type(d) == 'function' then
return d(one)
else
return one .. d
end
elseif entry and entry[2] then
return entry and entry[2]
elseif declension[lang] and declension[lang][2] then
local d = declension[lang][2]
if type(d) == 'function' then
return d(one)
else
return one .. d
end
else
return one
end
elseif r == _round(r, 0) then
if entry and entry[5] then
return entry and entry[5]
elseif declension[lang] and declension[lang][5] then
local d = declension[lang][5]
if type(d) == 'function' then
return d(one)
else
return one .. d
end
elseif entry and entry[3] then
return entry and entry[3]
elseif declension[lang] and declension[lang][3] then
local d = declension[lang][3]
if type(d) == 'function' then
return d(one)
else
return one .. d
end
elseif entry and entry[2] then
return entry and entry[2]
elseif declension[lang] and declension[lang][2] then
local d = declension[lang][2]
if type(d) == 'function' then
return d(one)
else
return one .. d
end
else
return one
end
else
if entry and entry["fraction"] then
return entry and entry["fraction"]
elseif declension[lang] and declension[lang]["fraction"] then
local d = declension[lang]["fraction"]
if type(d) == 'function' then
return d(one)
else
return one .. d
end
elseif entry and entry[5] then
return entry and entry[5]
elseif declension[lang] and declension[lang][5] then
local d = declension[lang][5]
if type(d) == 'function' then
return d(one)
else
return one .. d
end
elseif entry and entry[3] then
return entry and entry[3]
elseif declension[lang] and declension[lang][3] then
local d = declension[lang][3]
if type(d) == 'function' then
return d(one)
else
return one .. d
end
elseif entry and entry[2] then
return (r < 2 and one or entry and entry[2])
elseif declension[lang] and declension[lang][2] then
local d = declension[lang][2]
if type(d) == 'function' then
return (r < 2 and one or d(one))
else
return (r < 2 and one or (one .. d))
end
else
return one
end
end
end

local function linkend(u)
return "|" .. u .. "]]"
end

local function unitlink(lk, i, u, lang)
if lk then
if lang == "ca" then
return "[[ca:" .. "Escales curta i llarga" .. linkend(u)
elseif lang == "da" then
return "[[da:" .. "Store tal" .. linkend(u)
elseif lang == "de" then
return "[[de:" .. "Zahlennamen" .. linkend(u)
elseif lang == "eo" then
return "[[eo:" .. "Vortoj por grandegaj nombroj" .. linkend(u)
elseif lang == "fr" then
return "[[fr:" .. (i > 36 and "Noms des grands nombres" or ("Ordres de grandeur de nombres#10" .. i)) .. linkend(u)
elseif lang == "it" then
return "[[it:" .. ("Ordini di grandezza (numeri)#10" .. i) .. linkend(u)
elseif lang == "la" then
return "[[la:" .. "Nomina permagnorum numerorum" .. linkend(u)
elseif lang == "nl" then
return "[[nl:" .. "Lijst van machten van tien" .. linkend(u)
elseif lang == "no" then
return "[[no:" .. "Navn på store tall" .. linkend(u)
elseif lang == "pl" then
return "[[pl:" .. "Liczebniki główne potęg tysiąca" .. linkend(u)
elseif lang == "sk" then
return "[[sk:" .. "Veľké čísla" .. linkend(u)
elseif lang == "sl" then
return "[[sl:" .. "Imena velikih števil" .. linkend(u)
elseif lang == "sv" then
return "[[sv:" .. "Namn på stora tal" .. linkend(u)
else
return "[[en:" .. (i > 39 and "Names of large numbers" or ("Orders of magnitude (numbers)#10" .. i)) .. linkend(u)
end
else
return u
end
end

function p.wordify(frame)
local args = getArgs(frame)
local x = args[1]
local numsys = args.numsys
local prec = args.prec
local lk = args.lk
local lang = args.lang
local simplify = args.simplify
return p._wordify(x, numsys, prec, (lk == "on" and true), lang, (simplify == "yes" and true))
end

function p._wordify(x, numsys, prec, lk, lang, simplify)
if tonumber(x) then
if numsys == nil or numsys == "" or numsys:lower() == "usa" then
for i, v in reverseSparseIpairs(usa) do
local y = x / math.pow(10,i)
local r = _round(y, prec)
if r >= 1 then
if r == 1 and simplify then
return unitlink(lk, i, v, (lang or "en"))
else
return p.formatNum(r, (lang or "en"), prec) .. " " .. unitlink(lk, i, v, (lang or "en"))
end
end
end
return p.formatNum(_round(x, prec), (lang or "en"), prec)
elseif numsys:lower() == "fra" then
if latin['language_top'][(lang or "")] then
for i = latin['language_top'][(lang or "")], 6, -3 do
local entry = latin['exception'][i]
if not entry or entry[(lang or "")] ~= "none" then
local y = x / math.pow(10,i)
local r = _round(y, prec)
if r >= 1 then
local u, simp = unit(latin, i, lang, r)
if r == 1 and (simplify or simp) then
return unitlink(lk, i, u, (lang or ""))
else
return p.formatNum(r, (lang or "en"), prec) .. " " .. unitlink(lk, i, u, (lang or "en"))
end
end
end
end
end
return p.formatNum(_round(x, prec), (lang or "en"), prec)
elseif numsys:lower() == "ind" then
local y = x / 1E14
local r = _round(y, prec)
if r >= 1 then
if r == 1 and simplify then
return (lk and "[[crore]] crore" or ind[14])
else
return p.formatNum(r, (lang or "en"), prec) .. " " .. (lk and "[[crore]] crore" or ind[14])
end
else
local y = x / 1E12
local r = _round(y, prec)
if r >= 1 then
if r == 1 and simplify then
return (lk and "[[lakh]] [[crore]]" or ind[12])
else
return p.formatNum(r, (lang or "en"), prec) .. " " .. (lk and "[[lakh]] [[crore]]" or ind[12])
end
else
local y = x / 1E7
local r = _round(y, prec)
if r >= 1 then
local v = ind[7]
if r == 1 and simplify then
return (lk and "[[" .. v .. "]]" or v)
else
return p.formatNum(r, (lang or "en"), prec) .. " " .. (lk and "[[" .. v .. "]]" or v)
end
else
local y = x / 1E5
local r = _round(y, prec)
if r >= 1 then
local v = ind[5]
if r == 1 and simplify then
return (lk and "[[" .. v .. "]]" or v)
else
return p.formatNum(r, (lang or "en"), prec) .. " " .. (lk and "[[" .. v .. "]]" or v)
end
else
return p.formatNum(_round(x, prec), (lang or "en"), prec)
end
end
end
end
else
return err("number system not supported")
end
else
return err("Not a number: " .. x)
end
end

--[[
Helper function that interprets the input numerically. If the
input does not appear to be a number, attempts evaluating it as
a parser functions expression.
]]

function p._cleanNumber(number_string)
if type(number_string) == 'number' then
-- We were passed a number, so we don't need to do any processing.
return number_string, tostring(number_string)
elseif type(number_string) ~= 'string' or not number_string:find('%S') then
-- We were passed a non-string or a blank string, so exit.
return nil, nil;
end

-- Attempt basic conversion
local number = tonumber(number_string)

-- If failed, attempt to evaluate input as an expression
if number == nil then
local success, result = pcall(mw.ext.ParserFunctions.expr, number_string)
if success then
number = tonumber(result)
number_string = tostring(number)
else
number = nil
number_string = nil
end
else
number_string = number_string:match("^%s*(.-)%s*$") -- String is valid but may contain padding, clean it.
number_string = number_string:match("^%+(.*)$") or number_string -- Trim any leading + signs.
if number_string:find('^%-?0[xX]') then
-- Number is using 0xnnn notation to indicate base 16; use the number that Lua detected instead.
number_string = tostring(number)
end
end

return number, number_string
end
end



Latest revision as of 13:54, 1 March 2021

-- This module is intended to replace the functionality of Template:Formatnum and related templates. 
local p = {} -- Holds functions to be returned from #invoke, and functions to make available to other Lua modules.

--[[
Helper functions used to avoid redundant code.
]]

local function getArgs(frame)
    local args = {}
    for key, value in pairs(frame:getParent().args) do
        args[key] = value
    end
    for key, value in pairs(frame.args) do
        args[key] = value
    end
    return args
end

function p.main(frame)
    local args = getArgs(frame)
    local prec    = args.prec or ''
    local sep     = args.sep or ''
    local number  = args[1] or args.number or ''
    local lang    = args[2] or args.lang or ''
    -- validate the language parameter within MediaWiki's caller frame
    if lang:lower() == "none" then
        -- no language, so do nothing
    elseif lang == "arabic-indic" then -- only for back-compatibility ("arabic-indic" is not a SupportedLanguage)
        lang = "fa" -- better support than "ks"
    elseif lang == '' or not mw.language.isSupportedLanguage(lang) then
        -- Note that 'SupportedLanguages' are not necessarily 'BuiltinValidCodes', and so they are not necessarily
        -- 'KnownLanguages' (with a language name defined at least in the default localisation of the local wiki).
        -- But they all are ValidLanguageCodes (suitable as Wiki subpages or identifiers: no slash, colon, HTML tags, or entities)
        -- In addition, they do not contain any capital letter in order to be unique in page titles (restriction inexistant in BCP47),
        -- but they may violate the standard format of BCP47 language tags for specific needs in MediaWiki.
        -- Empty/unspecified and unsupported languages are treated here in Commons using the user's language,
        -- instead of the local 'ContentLanguage' of the Wiki.
        lang = frame:callParserFunction( "int", "lang" ) -- get user's chosen language
        if not mw.language.isSupportedLanguage(lang) then
            lang = mw.language.getContentLanguage().code
        end
    end
    return p.formatNum(number, lang, prec, sep ~= '')
end

local digit = { -- substitution of decimal digits for languages not supported by mw.language:formatNum() in core Lua libraries for MediaWiki
    ["ml-old"] = { '൦', '൧', '൨', '൩', '൪', '൫', '൬', '൭', '൮', '൯' },
    ["mn"]     = { '᠐', '᠑', '᠒', '᠓', '᠔', '᠕', '᠖', '᠗', '᠘', '᠙'},
    ["ta"]     = { '௦', '௧', '௨', '௩', '௪', '௫', '௬', '௭', '௮', '௯'},
    ["te"]     = { '౦', '౧', '౨', '౩', '౪', '౫', '౬', '౭', '౮', '౯'},
    ["th"]     = { '๐', '๑', '๒', '๓', '๔', '๕', '๖', '๗', '๘', '๙'}
}

function p.formatNum(number, lang, prec, compact)
    -- Do not alter the specified value when it is not a valid number, return it as is
    local value = tonumber(number)
    if value == nil then
        return number
    end
    -- Basic ASCII-only formatting (without paddings)
    number = tostring(value)

    -- Check the presence of an exponent (incorrectly managed in mw.language:FormatNum() and even forgotten due to an internal bug, e.g. in Hindi)
    local exponent
    local pos = string.find(number, '[Ee]')
    if pos ~= nil then
        exponent = string.sub(number, pos + 1, string.len(number))
        number = string.sub(number, 1, pos - 1)
    else
        exponent = ''
    end

    -- Check the minimum precision requested
    prec = tonumber(prec) -- nil if not specified as a true number
    if prec ~= nil then
        prec = math.floor(prec)
        if prec < 0 then
            prec = nil -- discard an incorrect precision (not a positive integer)
        elseif prec > 14 then
            prec = 14 -- maximum precision supported by tostring(number)
        end
    end

    -- Preprocess the minimum precision in the ASCII string
    local dot
    if (prec or 0) > 0 then
        pos = string.find(number, '.', 1, true) -- plain search, no regexp
        if pos ~= nil then
            prec = pos + prec - string.len(number) -- effective number of trailing decimals to add or remove
            dot = '' -- already present
        else
            dot = '.' -- must be added
        end
    else
        dot = '' -- don't add dot
        prec = 0 -- don't alter the precision
    end
    
    if lang ~= nil and mw.language.isKnownLanguageTag(lang) == true then
        -- Convert number to localized digits, decimal separator, and group separators
        local language = mw.getLanguage(lang)
        if compact then
            number = language:formatNum(tonumber(number), { noCommafy = 'y' }) -- caveat: can load localized resources for up to 20 languages
        else
            number = language:formatNum(tonumber(number)) -- caveat: can load localized resources for up to 20 languages
        end
        -- Postprocessing the precision
        if prec > 0 then
            local zero = language:formatNum(0)
            number = number .. dot .. mw.ustring.rep(zero, prec)
        elseif prec < 0 then
            -- TODO: rounding of last decimal; here only truncate decimals in excess
            number = mw.ustring.sub(number, 1, mw.ustring.len(number) + prec)
        end
        -- Append the localized base-10 exponent without grouping separators (there's no reliable way to detect a localized leading symbol 'E')
        if exponent ~= '' then
            number = number .. 'E' .. language:formatNum(tonumber(exponent),{noCommafy=true})
        end
    else -- not localized, ASCII only
        -- Postprocessing the precision
        if prec > 0 then
            number = number .. dot .. mw.string.rep('0', prec)
        elseif prec < 0 then
            -- TODO: rounding of last decimal; here only truncate decimals in excess
            number = mw.string.sub(number, 1, mw.string.len(number) + prec)
        end
        -- Append the base-10 exponent
        if exponent ~= '' then
            number = number .. 'E' .. exponent
        end
    end

    -- Special cases for substitution of ASCII digits (missing support in Lua core libraries for some languages)
    if digit[lang] then
        for i, v in ipairs(digit[lang]) do
            number = mw.ustring.gsub(number, tostring(i - 1), v)
        end
    end

    return number
end

return p