Modul:FormatNum
--[[ 2013-06-16 FormatNum
- format
- round
FormatNum() ]] 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 numberToString(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 -- numberToString()
--[[
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 formatNumber(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 -- formatNumber()
--[[
Formatting numbers.
@param source : String representation of an unformatted (but maybe rounded) floating point or integer number. @param spec : Formatting option. 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 'spec' is invalid or 'source' is not a valid string representation of a number, 'source' is returned unmodified.
]] function FormatNum.format(source, spec)
local number; if type(source) == "string" then number = mw.text.trim(source); end if not spec or spec == "" then spec = "dewiki" end if (number and spec) then local format = FORMAT_TABLE[spec]; if (format) then -- 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 .. formatNumber(number, format.decimalMark, format.groupMark, format.groupMinLength, format.groupOnlyIntegerPart); else -- number has no valid format -> undo all modifications number = source; end end end return number;
end -- FormatNum.format()
--[[
Rounding numbers.
@param number : string with unformatted floating point or integer number. @param precision : number of significant fractional part digits. If precision is negative, the integer part is rounded as well. @param method : number defining the rounding method to use. Currently are supported only 0 for 'IEEE 754' rounding and 1 for 'round half away from zero'. If another number is supplied, the result is undefined.
@return String of the rounded number as returned by Lua function string.format(). If one of the arguments is not a number, 'number' is returned unmodified.
]] function FormatNum.round(source, precision, method)
local number = tonumber(source); if (number and precision and method) then return numberToString(number, math.floor(precision), math.floor(method)); end return source;
end -- FormatNum.round()
local p = { };
function p.format(frame)
-- @param 1 : unformatted (but maybe rounded) floating point or integer number. -- @param number : same as 1 (DEPRECATED, backward compatibility). -- @param format : Formatting option. Currently there are -- "at", "comma", "de", "dewiki", "de_currency", "ch", "ch_currency", -- "en", "iso31_0", "iso31_0_point" and "pc" supported. local source = frame.args[1]; if (not source) then source = frame.args.number; -- DEPRECATED pcall( require, "Module:FormatNumDEPRECATED" ) end return FormatNum.format(source, frame.args.format) or "";
end -- .format()
function p.round(frame)
-- @param 1 : string with unformatted floating point or integer number. -- @param number : same as 1 (DEPRECATED, backward compatibility). -- @param precision : number of significant fractional part digits. -- If precision is negative, the integer part is rounded as well. -- @param method : number defining the rounding method to be used. -- Currently are supported only -- 0 for 'IEEE 754' rounding and -- 1 for 'round half away from zero'. -- If another number is supplied, the result is undefined. local precision = tonumber(frame.args.precision); local method = tonumber(frame.args.method); local source = frame.args[1]; if (not source) then source = frame.args.number; -- DEPRECATED pcall( require, "Module:FormatNumDEPRECATED" ) end if (source and precision) then return FormatNum.round(source, precision, method); end return source or "";
end -- .round()
-- Export access for Lua modules function p.FormatNum()
return FormatNum;
end -- .FormatNum()
-- DEPRECATED
function p.formatNumber(frame)
pcall( require, "Module:FormatNumDEPRECATED" ) return p.format(frame);
end function p.numberToString(frame)
pcall( require, "Module:FormatNumDEPRECATED" ) return p.round(frame);
end
return p;