Modul:FormatNum

Aus KyllburgWiki

--[[ 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;