Zum Inhalt springen

Modul:FormatNum

Vun Wikipedia

Die Dokumentation für dieses Modul kann unter Modul:FormatNum/Doku erstellt werden

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