--[[ Apply optional formatting and translation to a number. For example, an infobox may accept numbers in international or local digits, but processing may require international digits. The result may be wanted in international or local digits, and may be formatted by grouping digits. ]]  local MINUS = '−'  -- Unicode U+2212 MINUS SIGN (UTF-8: e2 88 92) local ustring = mw.ustring local unknown_error = 'অজানা ত্রুটি'  local mtext_bn = {     -- Module text: input parameter names/values, and output messages.     -- The name on the left of "=" is the name used in this module.     -- The text in single quotes is input used in the template, or     -- is output text displayed by the result.     parm_type = 'প্রকার',      -- parameter to specify wanted type of output     type_int = 'আন্তর্জাতিক',        -- ...output in international digits     type_ben = 'বাংলা',        -- ...output in bengali digits     parm_group = 'বিন্যাস',    -- parameter to specify wanted number grouping in output     grp_int = 'আন্তর্জাতিক',    -- ...three-digit groups     grp_ben = 'বাংলাদেশী',        -- ...three-then-two-digit groups     parm_numdot = 'decimal', -- parameter to specify decimal mark (single byte; default dot)     parm_numsep = 'sep',     -- parameter to specify group separator (single byte; default comma)     -- Output messages ("%s" is replaced with the invalid input text).     invalid_number = 'ত্রুটি: মান "%s" বৈধ সংখ্যা নয়।',     invalid_parm = 'ত্রুটি: প্যারামিটার "%s"-এ অবৈধ মান "%s" রয়েছে',     missing_parm = 'ত্রুটি: দয়া করে নম্বর লিখুন।', }  local mtext_en = {     -- English text for testing.     parm_type = 'type',     type_int = 'int',     type_ben = 'ben',     parm_group = 'format',     grp_int = 'int',     grp_ben = 'ben',     parm_numdot = 'decimal',     parm_numsep = 'sep',     invalid_number = 'Value "%s" is not a valid number',     invalid_parm = 'Parameter "%s" has invalid value "%s',     missing_parm = 'Need value', }  local from_international_table = {     ['0'] = '০',     ['1'] = '১',     ['2'] = '২',     ['3'] = '৩',     ['4'] = '৪',     ['5'] = '৫',     ['6'] = '৬',     ['7'] = '৭',     ['8'] = '৮',     ['9'] = '৯', }  local to_international_table = {     ['০'] = '0',     ['১'] = '1',     ['২'] = '2',     ['৩'] = '3',     ['৪'] = '4',     ['৫'] = '5',     ['৬'] = '6',     ['৭'] = '7',     ['৮'] = '8',     ['৯'] = '9', }  local function collection()     -- Return a table to hold items.     return {         n = 0,         add = function (self, item)             self.n = self.n + 1             self[self.n] = item         end,     } end  local function empty(text)     -- Return true if text is nil or empty (assuming a string).     return text == nil or text == end  local function strip(text)     -- If text is a string, return its content with no leading/trailing     -- whitespace. Otherwise return nil (a nil argument gives a nil result).     if type(text) == 'string' then         return text:match("^%s*(.-)%s*$")     end end  local function strip_to_nil(text)     -- Return stripped text or nil if empty.     if text ~= nil then         text = strip(text)         if text ==  then             text = nil         end     end     return text end  local function from_international(parms, text)     -- Input is a string representing a number in en digits with '.' decimal mark,     -- without digit grouping (which is done just after calling this).     -- Return the string with numdot and, if wanted, after translating     -- each digit to the local language.     if parms.numdot ~= '.' then         text = text:gsub('%.', parms.numdot)     end     if parms.otype ~= 'int' then         text = text:gsub('%d', from_international_table)     end     return text end  local function to_international(parms, text)     -- Input is a string representing a number in the local language with     -- optional numdot decimal mark and numsep digit grouping.     -- The input may also use international digits (which are not changed).     -- Return the translation of the string with '.' mark and en digits,     -- and no separators (they have to be removed here to handle cases like     -- numsep = '.' and numdot = ',' with input "1.234.567,8").     if parms.numsep ~=  then         text = text:gsub('[' .. parms.numsep .. ']', )  -- use '[x]' in case x is '.'     end     if parms.numdot ~= '.' then         text = text:gsub('[' .. parms.numdot .. ']', '.')     end     text = ustring.gsub(text, '%d', to_international_table)     return text end  local function extract_groups(parms, digits)     -- Return digits split into groups and translated, if wanted.     -- Parameter digits is 0..9 only (no sign, no decimal point, no exponent).     -- Each digit must be a byte because am not using mw.ustring functions.     local length = #digits     local places = collection()     local pos, step = 0, 3     while pos < length do         places:add(pos)         pos = pos + step         if parms.grouping == 'ben' then             step = 2         end     end     places:add(length)     local groups = collection()     for i = places.n, 2, -1 do         local p1 = length - places[i] + 1         local p2 = length - places[i - 1]         groups:add(from_international(parms, digits:sub(p1, p2)))     end     return groups end  local function with_separator(parms, text)     -- Return text with group separators inserted, if wanted.     -- Input uses international digits.     -- Output optionally uses digits in local language.     -- The given text is like '123' or '12345.6789' or '1.23e45'.     -- The text has no sign (caller inserts that later, if necessary).     -- Separator is inserted only in the integer part of the significand     -- (not after numdot, and not after 'e' or 'E').     if parms.grouping == nil or parms.numsep ==  then         return from_international(parms, text)     end     local last = text:match('()[.eE]')  -- () returns position     if last == nil then         last = #text     else         last = last - 1  -- index of last character before dot/e/E     end     if last < 4 or (last == 4 and parms.opt_comma5) then         return from_international(parms, text)     end     local groups = extract_groups(parms, text:sub(1, last))     return table.concat(groups, parms.numsep) .. from_international(parms, text:sub(last+1)) end  local function get_parms(args)     -- Return a table of parameter names and values, using terms known in this module.     -- Input numstr is converted to international digits with no formatting.     -- Input padlen is converted to a number (or nil if no padding).     -- Throw an error if input is invalid.     local parms = {}     local mtext = (args.lang == 'en') and mtext_en or mtext_bn     local function die(code, parm1, parm2)         error(string.format(mtext[code] or unknown_error, parm1, parm2), 0)     end     local numstr = strip_to_nil(args[1])     if not numstr then         die('missing_parm')     end     local otype = strip_to_nil(args[mtext.parm_type])     if otype then         if otype == mtext.type_int then             otype = 'int'         elseif otype == mtext.type_ben then             otype = 'ben'         else             die('invalid_parm', mtext.parm_type, otype)         end     else         -- Output type is opposite of input type.         -- Assume finding any type_int digit means input is type_int.         otype = numstr:find('%d') and 'ben' or 'int'     end     parms.otype = otype     local grp = strip_to_nil(args[mtext.parm_group])     if grp then         if grp == mtext.grp_int then             grp = 'int'         elseif grp == mtext.grp_ben then             grp = 'ben'         else             die('invalid_parm', mtext.parm_group, grp)         end     end     parms.grouping = grp  -- nil is no grouping     local dot = strip_to_nil(args[mtext.parm_numdot])     if dot then         if not (dot == '.' or dot == ',') then             die('invalid_parm', mtext.parm_numdot, dot)         end     else         dot = '.'     end     parms.numdot = dot     local sep = strip_to_nil(args[mtext.parm_numsep])     if sep then         if not (sep == '.' or sep == ',') then             die('invalid_parm', mtext.parm_numsep, sep)         end     else         sep = ','     end     parms.numsep = sep     -- Clean input number and remove any sign.     local clean = to_international(parms, numstr)     local sign     for _, prefix in ipairs({ '+', '-', MINUS }) do         local plen = #prefix         if clean:sub(1, plen) == prefix then             if sign then  -- more than one sign                 die('invalid_number', numstr)             end             sign = (prefix == '+') and '+' or MINUS             clean = strip(clean:sub(plen + 1))         end     end     parms.sign = sign or      if tonumber(clean) then         -- Number is valid, but omit any trailing '.' as redundant.         if clean:sub(-1) == '.' then             clean = clean:sub(1, -2)         end         parms.numstr = clean     else         die('invalid_number', numstr)     end     local padlen = strip_to_nil(args[2])     if padlen then         local value = tonumber(to_international(parms, padlen))         if value then             parms.padlen = value         else             die('invalid_number', padlen)         end     end     return parms end  local function do_number(args)     -- Return the processed input number, or throw an error if invalid.     -- Padding applies to the total number of digits, and does not count any     -- group separators or decimal mark. For example, if a number would give     -- "1,234,567.890" without padding, the result with padlen = 15 would     -- have 5 extra zeros, giving "000,001,234,567.890".     local parms = get_parms(args)     local numstr = parms.numstr     local padlen = parms.padlen     if padlen then         local reslen = #numstr         if numstr:find('.', 1, true) then             reslen = reslen - 1  -- do not count decimal mark         end         if padlen > reslen then             if padlen > 100 then  -- silently limit to something reasonable                 padlen = 100             end             numstr = string.rep('0', padlen - reslen) .. numstr         end     end     return parms.sign .. with_separator(parms, numstr) end  local function number(frame)     local success, result = pcall(do_number, frame.args)     if success then         return result     end     return '' .. result .. '' end  return { number = number }