-
Notifications
You must be signed in to change notification settings - Fork 0
Implement Formatnum module in Lua #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,5 +6,3 @@ jobs: | |
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
| - name: Luacheck linter | ||
| uses: lunarmodules/luacheck@v1 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| -- 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 | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Uses ASCII dot '.' as decimal separator after language localization, causing incorrect formatting for languages that use different decimal separators (e.g., comma in French/German).
When
dot = '.'(set on line 92 when the original number had no decimal point), this appends an ASCII period even thoughlanguage:formatNum()already localized the number. For example, with French locale:Fix by extracting the localized decimal separator:
Spotted by Graphite Agent

Is this helpful? React 👍 or 👎 to let us know.