Modul:FormatNum
w:de:Wikipedia:Lua/Modul/FormatNum
Detdiar sidj skal di (iarst ans) widjer feer. (huuchsjiisk)
--[[ 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;