Jump to content

Module:Formatnum

Permanently protected module
From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Trigenibinion (talk | contribs) at 18:47, 19 February 2021 (Typo). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.

-- This module is intended to replace the functionality of Template:Formatnum and related templates. 
local mm = require('Module:Math')

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

function p.main(frame)
    local args = frame.args
    if not args[1] and not args.number then
        args = frame.args
    end
    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

--[[
mod

Implements the division operator

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

--]]

function p.wordify(frame)
	local args = frame.args
    if not args[1] and not args.number then
        args = frame.args
    end
	local x = args[1]
	local numsys = args.numsys
	local prec =  args.prec
	local lk = args.lk
	return p._wordify(x, numsys, prec, (lk == "on" and true or false))
end

function p._wordify(x, numsys, prec, lk)
	if tonumber(x) then
		if numsys == "usa" or numsys == nil or numsys == "" then
			if x / 1E12 >= 1 then
				return p.formatNum(mm._round(x / 1E12, prec), "en") .. " " .. (lk and "[[1,000,000,000,000|trillion]]" or "trillion")
			elseif x / 1E9 >= 1 then
				return p.formatNum(mm._round(x / 1E9, prec), "en") .. " " .. (lk and "[[1,000,000,000|billion]]" or "billion")
			elseif x / 1E6 >= 1 then
				return p.formatNum(mm._round(x / 1E6, prec), "en") .. " million"
			else
				return p.formatNum(mm._round(x, prec), "en") 
			end
		elseif numsys == "ind" then
			if x / 1E12 >= 1 then
				return p.formatNum(mm._round(x / 1E12, prec), "en") .. " " .. (lk and "[[lakh]] [[crore]]" or "lakh crore")
			elseif x / 1E7 >= 1 then
				return p.formatNum(mm._round(x / 1E7, prec), "en") .. " " .. (lk and "[[crore]]" or "crore")
			elseif x / 1E5 >= 1 then
				return p.formatNum(mm._round(x / 1E5, prec), "en") .. " " .. (lk and "[[lakh]]" or "lakh")
			else
				return p.formatNum(mm._round(x, prec), "en") 
			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

return p