Modul:FormatNum
--[[
Main table - used by invoke.
Exported functions: numberToString, formatNumber ]] local formatNum = {};
--[[ Constant for round method "round half to even" (IEEE 754). ]] local ROUND_TO_EVEN = 0;
--[[ Constant for round method "round half away from zero" (German: "kaufmaennisches Runden"), also filters "-0" and converts it to "0". ]] local ROUND_AWAY_FROM_ZERO = 1;
--[[ Table storing the format options. ]] local FORMAT_TABLE = {};
--[[ Format table for "de". ]] FORMAT_TABLE.de = {}; FORMAT_TABLE.de.decimalMark = ","; FORMAT_TABLE.de.groupMark = " "; FORMAT_TABLE.de.groupMinLength = 5; FORMAT_TABLE.de.groupOnlyIntegerPart = false;
--[[ Format table for "de_currency". ]] FORMAT_TABLE.de_currency = {}; FORMAT_TABLE.de_currency.decimalMark = ","; FORMAT_TABLE.de_currency.groupMark = "."; FORMAT_TABLE.de_currency.groupMinLength = 5; FORMAT_TABLE.de_currency.groupOnlyIntegerPart = true;
--[[ Format table for "ch". ]] FORMAT_TABLE.ch = {}; FORMAT_TABLE.ch.decimalMark = ","; FORMAT_TABLE.ch.groupMark = "'"; FORMAT_TABLE.ch.groupMinLength = 5; FORMAT_TABLE.ch.groupOnlyIntegerPart = true;
--[[ Format table for "en". ]] FORMAT_TABLE.en = {}; FORMAT_TABLE.en.decimalMark = "."; FORMAT_TABLE.en.groupMark = ","; FORMAT_TABLE.en.groupMinLength = 4; FORMAT_TABLE.en.groupOnlyIntegerPart = true;
--[[ Format table for "iso31_0" (ISO 31-0 using comma as decimal mark). ]] FORMAT_TABLE.iso31_0 = {}; FORMAT_TABLE.iso31_0.decimalMark = ","; FORMAT_TABLE.iso31_0.groupMark = " "; FORMAT_TABLE.iso31_0.groupMinLength = 4; FORMAT_TABLE.iso31_0.groupOnlyIntegerPart = false;
--[[ Format table for "iso31_0_point" (ISO 31-0 using point as decimal mark). ]] FORMAT_TABLE.iso31_0_point = {}; FORMAT_TABLE.iso31_0_point.decimalMark = "."; FORMAT_TABLE.iso31_0_point.groupMark = " "; FORMAT_TABLE.iso31_0_point.groupMinLength = 4; FORMAT_TABLE.iso31_0_point.groupOnlyIntegerPart = false;
--[[ Format table for "pc" (simply nil to prevent formatting). ]] FORMAT_TABLE.pc = nil;
--[[
Format table for "comma" (no grouping - groupMark "").
]] FORMAT_TABLE.comma = {}; FORMAT_TABLE.comma.decimalMark = ","; FORMAT_TABLE.comma.groupMark = ""; FORMAT_TABLE.comma.groupMinLength = 1000; -- (for performance, but also small values wouldn't matter) FORMAT_TABLE.comma.groupOnlyIntegerPart = true;
--[[ Format table for "at" (only for convenience, same as "iso31_0"). ]] FORMAT_TABLE.at = FORMAT_TABLE.iso31_0;
--[[ Format table for "ch_currency" (only for convenience, same as "de_currency"). ]] FORMAT_TABLE.ch_currency = FORMAT_TABLE.de_currency;
--[[
Format table for "dewiki" (only for convenience, same as "de_currency").
]] FORMAT_TABLE.dewiki = FORMAT_TABLE.de_currency;
--[[ Constant defining digit group lenghts when digit grouping is used. ]] local DIGIT_GROUPING_SIZE = 3;
--[[ Internal used function for rounding.
@param a_number : Number to be rounded. @param a_precision : Number of significant digits of the fractional part. If it is negative, the according number of digits of the integer part are also rounded. @param a_roundMethod : Numeric constant defining the round method to use. Supported are ROUND_TO_EVEN and ROUND_AWAY_FROM_ZERO.
@return String of the rounded number like returned by Lua function string.format(). ]] local function internalNumberToString(a_number, a_precision, a_roundMethod) if (a_precision < 0) then a_precision = -a_precision; if (a_roundMethod == ROUND_TO_EVEN) then local integerPart = math.floor(math.abs(a_number) / (10 ^ a_precision)); if (integerPart % 2 == 0) then -- next even number smaller than a_number / 10^precision a_number = a_number - 5 * (10 ^ (a_precision - 1)); a_number = math.ceil(a_number / (10 ^ a_precision)) * (10 ^ a_precision); else -- next even number bigger than a_number / 10^precision a_number = a_number + 5 * (10 ^ (a_precision - 1)); a_number = math.floor(a_number / (10 ^ a_precision)) * (10 ^ a_precision); end elseif (a_roundMethod == ROUND_AWAY_FROM_ZERO) then if (a_number >= 0) then a_number = a_number + 5 * (10 ^ (a_precision - 1)); a_number = math.floor(a_number / (10 ^ a_precision)) * (10 ^ a_precision); else a_number = a_number - 5 * (10 ^ (a_precision - 1)); a_number = math.ceil(a_number / (10 ^ a_precision)) * (10 ^ a_precision); end end -- handle it as normal integer a_precision = 0; end if (a_roundMethod == ROUND_AWAY_FROM_ZERO) then if ((a_number * (10 ^ a_precision)) - math.floor(a_number * (10 ^ a_precision)) == 0.5) then -- because string.format() uses round to even, we have to add (numbers >0) or -- subtract (numbers <0) a little bit to point into the "correct" rounding -- direction if a_number is exactly in the middle between two rounded numbers if (a_number >= 0) then a_number = a_number + (10 ^ -(a_precision + 1)); else a_number = a_number - (10 ^ -(a_precision + 1)); end else if (math.abs(a_number * (10 ^ a_precision)) < 0.5) then -- filter "-0" and convert it to 0 a_number = math.abs(a_number); end end end return string.format("%." .. tostring(a_precision) .. "f", a_number); end
--[[ Internal used function for formatting.
@param a_number : String of a non-negative number to be formatted. @param a_decimalMark : String of the decimal mark to use. @param a_groupMark : String of the mark used for digit grouping. @param a_groupMinLength : Number defining the minimum length of integer part to use digit grouping (normally 4 or 5). However if fractional part is longer than DIGIT_GROUPING_SIZE (3 as default) and digit grouping of fractional part is not disabled via 'a_groupOnlyIntegerPart', then this value is ignored and set to DIGIT_GROUPING_SIZE + 1. @param a_groupOnlyIntegerPart : Boolean defining whether activating digit grouping only for integer part (true) or for integer and fractional part (false).
@return String of the formatted number according to the parameters. ]] local function internalFormatNumber(a_number, a_decimalMark, a_groupMark, a_groupMinLength, a_groupOnlyIntegerPart) -- find the decimal point local decimalPosition = mw.ustring.find(a_number, ".", 1, true); local needsGrouping = false; if (not decimalPosition) then -- no decimal point - integer number decimalPosition = mw.ustring.len(a_number) + 1; if (decimalPosition > a_groupMinLength) then needsGrouping = true; end else -- decimal point present if ((decimalPosition > a_groupMinLength) or (((mw.ustring.len(a_number) - decimalPosition) > DIGIT_GROUPING_SIZE) and (not a_groupOnlyIntegerPart))) then needsGrouping = true; end -- replace the decimal point a_number = mw.ustring.sub(a_number, 1, decimalPosition - 1) .. a_decimalMark .. mw.ustring.sub(a_number, decimalPosition + 1); end if (needsGrouping and (decimalPosition > DIGIT_GROUPING_SIZE + 1)) then -- grouping of integer part necessary local i = decimalPosition - DIGIT_GROUPING_SIZE; while (i > 1) do -- group the integer part a_number = mw.ustring.sub(a_number, 1, i - 1) .. a_groupMark .. mw.ustring.sub(a_number, i); decimalPosition = decimalPosition + mw.ustring.len(a_groupMark); i = i - DIGIT_GROUPING_SIZE; end end -- skip to the end of the new decimal mark (in case it is more than one char) decimalPosition = decimalPosition + mw.ustring.len(a_decimalMark) - 1; if (a_groupOnlyIntegerPart) then needsGrouping = false; end if (needsGrouping and ((mw.ustring.len(a_number) - decimalPosition) > DIGIT_GROUPING_SIZE)) then -- grouping of fractional part necessary -- using negative numbers (index from the end of the string) local i = decimalPosition - mw.ustring.len(a_number) + DIGIT_GROUPING_SIZE; while (i <= -1) do -- group the fractional part a_number = mw.ustring.sub(a_number, 1, i - 1) .. a_groupMark .. mw.ustring.sub(a_number, i); i = i + DIGIT_GROUPING_SIZE; end end return a_number; end
--[[ Exported function for rounding numbers.
@param a_frame : Frame holding the arguments from the invoke call.
@argument number : String representation of an unformatted floating point or integer number. @argument precision : String representation of the number of significant fractional part digits. If it is negative, the integer part is rounded as well. @argument method : String representation of the number defining the rounding method to use. Currently only "0" for 'IEEE 754' rounding and "1" for 'round half away from zero' are supported. If another number is supplied, the result is undefined.
@return String of the rounded number like returned by Lua function string.format(). If one of the arguments is not a number, the argument 'number' is returned unmodified. ]] function formatNum.numberToString(a_frame) local number = tonumber(a_frame.args["number"]); local precision = tonumber(a_frame.args["precision"]); local method = tonumber(a_frame.args["method"]); if (number and precision and method) then return internalNumberToString(number, math.floor(precision), math.floor(method)); end return a_frame.args["number"]; end
--[[ Exported function for formatting numbers.
@param a_frame : Frame holding the arguments from the invoke call.
@argument number : String representation of an unformatted (but maybe rounded) floating point or integer number. @argument format : String defining the formatting options. Currently there are "at", "comma", "de", "dewiki", "de_currency", "ch", "ch_currency", "en", "iso31_0", "iso31_0_point" and "pc" supported. See the FORMAT_TABLE for details.
@return String of the formatted number. If the argument 'format' is invalid or 'number' is not a valid string representation of a number, the argument 'number' is returned unmodified. ]] function formatNum.formatNumber(a_frame) local number = a_frame.args["number"]; local format = a_frame.args["format"]; if (number and format) then format = FORMAT_TABLE[format]; if (tonumber(number) and format) then -- lua can parse the number (first check passed) and format entry found local sign = mw.ustring.sub(number, 1, 1); if ((sign == "+") or (sign == "-")) then -- remove sign from number, add it later again number = mw.ustring.sub(number, 2); else -- was not a sign sign = ""; end if (mw.ustring.sub(number, 1, 1) == ".") then -- number begins with "." -> add a 0 to the beginning number = "0" .. number; else if (mw.ustring.sub(number, -1) == ".") then -- number ends with "." -> remove it number = mw.ustring.sub(number, 1, -2); end end if ((number == mw.ustring.match(number, "^%d+$")) or (number == mw.ustring.match(number, "^%d+%.%d+$"))) then -- number has valid format (only digits or digits.digits) -> format it and add sign (if any) again number = sign .. internalFormatNumber(number, format.decimalMark, format.groupMark, format.groupMinLength, format.groupOnlyIntegerPart); else -- number has no valid format -> undo all modifications number = a_frame.args["number"]; end end end return number; end
return formatNum;