Difference between revisions of "Module:Math"

From MINR.ORG WIKI
m (fmt)
(adds expression checking, more error messages, and better handling of some precision format cases, and precision flooding check)
Line 1: Line 1:
 
local z = {}
 
local z = {}
 +
require( "mw.language" );
 +
 +
-- Clean numeric value
 +
function z._cleanNumber( frame, number_string )
 +
    -- Attempt basic conversion
 +
    local number = tonumber( number_string )
 +
   
 +
    -- If failed, attempt to evaluate input as an expression
 +
    if number == nil then
 +
        local attempt = frame:preprocess( '{{#expr: ' .. number_string .. '}}' );
 +
        attempt = tonumber( attempt );
 +
        if attempt ~= nil then
 +
            number = attempt;
 +
            number_string = tostring( number );
 +
        else
 +
            number = nil;
 +
            number_string = nil;
 +
        end
 +
    else
 +
    -- String is valid but may contain padding, clean it.
 +
        number_string = number_string:match( "^%s*(.-)%s*$" );
 +
    end
 +
   
 +
    return number, number_string;
 +
end
  
 
-- Generate random number
 
-- Generate random number
Line 17: Line 42:
 
-- Determine order of magnitude
 
-- Determine order of magnitude
 
function z.order(frame)
 
function z.order(frame)
     return z._order(tonumber(frame.args[1] or frame.args.x or 0))
+
     local input_string = (frame.args[1] or frame.args.x or '0');
 +
    local input_number;
 +
   
 +
    input_number = z._cleanNumber( frame, input_string );
 +
    if input_number == nil then
 +
        return '<strong class="error">Formatting error: Order of magnitude input appears non-numeric</strong>'
 +
    else
 +
        return z._order( input_number )
 +
    end   
 
end
 
end
 
function z._order(x)
 
function z._order(x)
Line 26: Line 59:
 
-- Determines precision of a number using the string representation
 
-- Determines precision of a number using the string representation
 
function z.precision( frame )
 
function z.precision( frame )
     return z._precision( frame.args[1] or frame.args.x or '0' )
+
     local input_string = (frame.args[1] or frame.args.x or '0');
 +
    local input_number;
 +
   
 +
    input_number, input_string = z._cleanNumber( frame, input_string );
 +
    if input_string == nil then
 +
        return '<strong class="error">Formatting error: Precision input appears non-numeric</strong>'
 +
    else
 +
        return z._precision( input_string )
 +
    end   
 
end
 
end
function z._precision( x )
+
function z._precision( x )  
 
     x = string.upper( x )
 
     x = string.upper( x )
 
    -- Remove leading / trailing whitespace
 
    x = x:match "^%s*(.-)%s*$";
 
  
 
     local decimal = string.find( x, '.', 1, true )
 
     local decimal = string.find( x, '.', 1, true )
Line 66: Line 104:
 
         return ''
 
         return ''
 
     end
 
     end
     local max_value = tonumber( frame.args[1] )
+
     local max_value = nil;
 
      
 
      
     local i = 2;
+
     local i = 1;
 
     while frame.args[i] ~= nil do
 
     while frame.args[i] ~= nil do
         local val = tonumber( frame.args[i] );
+
         local val = z._cleanNumber( frame, frame.args[i] );
 
         if val ~= nil then
 
         if val ~= nil then
             if val > max_value then
+
             if max_value == nil or val > max_value then
 
                 max_value = val;
 
                 max_value = val;
 
             end
 
             end
Line 87: Line 125:
 
         return ''
 
         return ''
 
     end
 
     end
     local min_value = tonumber( frame.args[1] )
+
     local min_value = nil;
 
      
 
      
     local i = 2;
+
     local i = 1;
 
     while frame.args[i] ~= nil do
 
     while frame.args[i] ~= nil do
         local val = tonumber( frame.args[i] );
+
         local val = z._cleanNumber( frame, frame.args[i] );
 
         if val ~= nil then
 
         if val ~= nil then
             if val < min_value then
+
             if min_value == nil or val < min_value then
 
                 min_value = val;
 
                 min_value = val;
 
             end
 
             end
Line 105: Line 143:
 
-- Rounds a number to specified precision
 
-- Rounds a number to specified precision
 
function z.round(frame)
 
function z.round(frame)
     local value = tonumber(frame.args[1] or frame.args.value or 0);
+
     local value, precision;
     local precision = tonumber(frame.args[2] or frame.args.precision or 0);
+
   
     return z._round( value, precision );
+
    value = z._cleanNumber( frame, frame.args[1] or frame.args.value or 0 );
 +
     precision = z._cleanNumber( frame, frame.args[2] or frame.args.precision or 0 );
 +
      
 +
    if value == nil or precision == nil then
 +
        return '<strong class="error">Formatting error: Round input appears non-numeric</strong>'
 +
    else
 +
        return z._round( value, precision );
 +
    end   
 
end
 
end
 
function z._round( value, precision )
 
function z._round( value, precision )
Line 120: Line 165:
 
     local lang = mw.getContentLanguage();
 
     local lang = mw.getContentLanguage();
 
      
 
      
     local value = tonumber( frame.args[1] or 0 );
+
     local value_string, value, precision;
     local precision = tonumber( frame.args[2] or 0 );
+
    value, value_string = z._cleanNumber( frame, frame.args[1] or 0 );
 +
     precision = z._cleanNumber( frame, frame.args[2] or 0 );
 
      
 
      
 
     -- Check for non-numeric input
 
     -- Check for non-numeric input
Line 135: Line 181:
 
     -- some circumstances because the terminal digits will be inaccurately reported.
 
     -- some circumstances because the terminal digits will be inaccurately reported.
 
     if order + precision >= 14 then
 
     if order + precision >= 14 then
         precision = 13 - order;         
+
         orig_precision = z._precision( value_string );
 +
        if order + orig_precision >= 14 then
 +
            precision = 13 - order;      
 +
        end        
 
     end
 
     end
  
Line 165: Line 214:
 
     formatted_num = sign .. formatted_num;
 
     formatted_num = sign .. formatted_num;
 
      
 
      
     -- Pad with zeros, if needed
+
     -- Pad with zeros, if needed  
 
     if current_precision < precision then
 
     if current_precision < precision then
 +
        local padding;
 
         if current_precision <= 0 then
 
         if current_precision <= 0 then
 
             if precision > 0 then
 
             if precision > 0 then
 
                 local zero_sep = lang:formatNum( 1.1 );
 
                 local zero_sep = lang:formatNum( 1.1 );
 
                 formatted_num = formatted_num .. zero_sep:sub(2,2);
 
                 formatted_num = formatted_num .. zero_sep:sub(2,2);
                 formatted_num = formatted_num .. string.rep( '0', precision );
+
 
 +
                padding = precision;
 +
                if padding > 20 then
 +
                    padding = 20;
 +
                end
 +
               
 +
                 formatted_num = formatted_num .. string.rep( '0', padding );
 
             end             
 
             end             
         else      
+
         else                  
             formatted_num = formatted_num .. string.rep( '0', precision - current_precision );
+
            padding = precision - current_precision
 +
            if padding > 20 then
 +
                padding = 20;
 +
            end
 +
             formatted_num = formatted_num .. string.rep( '0', padding );
 
         end
 
         end
 
     end
 
     end

Revision as of 00:51, 22 February 2013

Documentation for this module may be created at Module:Math/doc

local z = {}
require( "mw.language" );

-- Clean numeric value
function z._cleanNumber( frame, number_string )
    -- Attempt basic conversion
    local number = tonumber( number_string )
    
    -- If failed, attempt to evaluate input as an expression
    if number == nil then
        local attempt = frame:preprocess( '{{#expr: ' .. number_string .. '}}' );
        attempt = tonumber( attempt );
        if attempt ~= nil then
            number = attempt;
            number_string = tostring( number );
        else
            number = nil;
            number_string = nil;
        end
    else
    -- String is valid but may contain padding, clean it.
        number_string = number_string:match( "^%s*(.-)%s*$" );
    end
    
    return number, number_string;
end

-- Generate random number
function z.random( frame )
    first = tonumber(frame.args[1]) -- if it doesn't exist it's NaN, if not a number it's nil
    second = tonumber(frame.args[2])

    if first then -- if NaN or nil, will skip down to final return
        if first <= second then -- could match if both nil, but already checked that first is a number in last line
            return math.random(first, second)
        end
        return math.random(first)
    end   
    return math.random()
end

-- Determine order of magnitude
function z.order(frame)
    local input_string = (frame.args[1] or frame.args.x or '0');
    local input_number;
    
    input_number = z._cleanNumber( frame, input_string );
    if input_number == nil then
        return '<strong class="error">Formatting error: Order of magnitude input appears non-numeric</strong>'
    else
        return z._order( input_number )
    end    
end
function z._order(x)
    if x == 0 then return 0 end
    return math.floor(math.log10(math.abs(x)))
end

-- Determines precision of a number using the string representation
function z.precision( frame )
    local input_string = (frame.args[1] or frame.args.x or '0');
    local input_number;
    
    input_number, input_string = z._cleanNumber( frame, input_string );
    if input_string == nil then
        return '<strong class="error">Formatting error: Precision input appears non-numeric</strong>'
    else
        return z._precision( input_string )
    end    
end
function z._precision( x )    
    x = string.upper( x )

    local decimal = string.find( x, '.', 1, true )
    local exponent_pos = string.find( x, 'E', 1, true )
    local result = 0;
    
    if exponent_pos ~= nil then
        local exponent = string.sub( x, exponent_pos + 1 )
        x = string.sub( x, 1, exponent_pos - 1 )
        result = result - tonumber( exponent )
    end    
    
    if decimal ~= nil then
        result = result + string.len( x ) - decimal
        return result
    end
        
    local pos = string.len( x );
    while x:byte(pos) == string.byte('0') do
        pos = pos - 1
        result = result - 1
        if pos <= 0 then
            return 0
        end
    end
    
    return result
end

-- Finds maximum argument
function z.max( frame )
    if frame.args[1] == nil then
        return ''
    end
    local max_value = nil;
    
    local i = 1;
    while frame.args[i] ~= nil do
        local val = z._cleanNumber( frame, frame.args[i] );
        if val ~= nil then
            if max_value == nil or val > max_value then
                max_value = val;
            end
        end        
        i = i + 1;
    end
  
    return max_value
end

-- Finds minimum argument
function z.min( frame )
    if frame.args[1] == nil then
        return ''
    end
    local min_value = nil;
    
    local i = 1;
    while frame.args[i] ~= nil do
        local val = z._cleanNumber( frame, frame.args[i] );
        if val ~= nil then
            if min_value == nil or val < min_value then
                min_value = val;
            end
        end        
        i = i + 1;
    end
  
    return min_value
end

-- Rounds a number to specified precision
function z.round(frame)
    local value, precision;
    
    value = z._cleanNumber( frame, frame.args[1] or frame.args.value or 0 );
    precision = z._cleanNumber( frame, frame.args[2] or frame.args.precision or 0 );
    
    if value == nil or precision == nil then
        return '<strong class="error">Formatting error: Round input appears non-numeric</strong>'
    else
        return z._round( value, precision );
    end    
end
function z._round( value, precision )
    local rescale = math.pow( 10, precision );
    return math.floor( value * rescale + 0.5 ) / rescale;
end

-- Rounds a number to the specified precision and formats according to rules 
-- originally used for {{template:Rnd}}.  Output is a string.
function z.precision_format( frame )
    -- For access to Mediawiki built-in formatter.
    local lang = mw.getContentLanguage();
    
    local value_string, value, precision;
    value, value_string = z._cleanNumber( frame, frame.args[1] or 0 );
    precision = z._cleanNumber( frame, frame.args[2] or 0 );
    
    -- Check for non-numeric input
    if value == nil or precision == nil then
        return '<strong class="error">Formatting error: invalid input when rounding</strong>'
    end
    
    local current_precision = z._precision( value );

    local order = z._order( value );
    
    -- Due to round-off effects it is neccesary to limit the returned precision under
    -- some circumstances because the terminal digits will be inaccurately reported.
    if order + precision >= 14 then
        orig_precision = z._precision( value_string );
        if order + orig_precision >= 14 then
            precision = 13 - order;        
        end        
    end

    -- If rounding off, truncate extra digits
    if precision < current_precision then
        value = z._round( value, precision );
        current_precision = z._precision( value );
    end    
    
    local formatted_num = lang:formatNum( math.abs(value) );
    local sign;
    
    -- Use proper unary minus sign rather than ASCII default
    if value < 0 then
        sign = '−';
    else
        sign = '';
    end    
        
    -- Handle cases requiring scientific notation
    if string.find( formatted_num, 'E', 1, true ) ~= nil or math.abs(order) >= 9 then
        value = value * math.pow( 10, -order );
        current_precision = current_precision + order;
        precision = precision + order;
        formatted_num = lang:formatNum( math.abs(value) );
    else
        order = 0;        
    end
    formatted_num = sign .. formatted_num;
    
    -- Pad with zeros, if needed    
    if current_precision < precision then
        local padding;
        if current_precision <= 0 then
            if precision > 0 then
                local zero_sep = lang:formatNum( 1.1 );
                formatted_num = formatted_num .. zero_sep:sub(2,2);

                padding = precision;
                if padding > 20 then
                    padding = 20;
                end
                
                formatted_num = formatted_num .. string.rep( '0', padding );
            end            
        else                   
            padding = precision - current_precision
            if padding > 20 then
                padding = 20;
            end
            formatted_num = formatted_num .. string.rep( '0', padding );
        end
    end

    -- Add exponential notation, if necessary.
    if order ~= 0 then
        -- Use proper unary minus sign rather than ASCII default
        if order < 0 then
            order = '−' .. lang:formatNum( math.abs(order) );
        else
            order = lang:formatNum( order );
        end    
        
        formatted_num = formatted_num .. '<span style="margin:0 .15em 0 .25em">×</span>10<sup>' .. order .. '</sup>'
    end
    
    return formatted_num;
end

return z