Module:Convert: Difference between revisions

From Zoophilia Wiki
Jump to navigationJump to search
meta>WOSlinker
km2|sqmi
meta>Johnuniq
sorry, but I'm dropping in a new version to see whether others want to work with this
Line 1: Line 1:
--require "mw.text"
--[[
--require "mw.page"
Start of convert that I hope will be driven from tables.
Lots to do (first want to fix the default rounding).
I (enwiki Johnuniq) started this two days ago; it's not ready for release
but I'm dropping it here per BRD to see if others want to work with this.


-- TODO: lots of things still need doing but below are a few of the major bits
Testing makes me think these results will occur with current code:
--      Support for numeric precision
{{convert|3.21|kg|lb|2}}    3.21 kg (7.08 lb)
--       Support for all the different conversion units
{{convert|212|F|C|2}}        212 F (100.00 C)
{{convert|5|to|10|kg|lb|2}}  5 to 10 kg (11.02 to 22.05 lb)
{{convert|5|-|10|kg|lb|2}}  5–10 kg (11.02–22.05 lb)
{{convert|15|ft|m|2}}        15 ft (4.57 m)
{{convert|3.21|kg|m|2}}      Error: Cannot convert mass to length
]]--


local p = {}
--[[-----BEGIN DATA TABLE-----
Plan to write a program to generate the conversion tables below.
The input would be a text file in human-friendly format, and
the output would be the tables following like length/mass/units.
Values from http://en.wikipedia.org/wiki/Conversion_of_units
Check with  http://en.wikipedia.org/wiki/Template:Convert/list_of_units
]]--


function convert(in_val,in_unit,out_unit)
local function scaled(self, value, in_unit, out_unit)
     local out_val;
     -- Return scaled value for a simple convert.
    local units = in_unit .. "|" .. out_unit
     if out_unit == nil then
     if units == "°C|°F" or units == "C|F" then
         out_unit = self[in_unit][2]
         out_val = in_val * 9 / 5 + 32;
    elseif units == "°F|°C" or units == "F|C" then
        out_val = (in_val - 32) * 5 / 9;
    elseif units == "kg|lb" then
        out_val = in_val * 2.20462262;
    elseif units == "lb|kg" then
        out_val = in_val / 2.20462262;
    elseif units == "km|mi" then
        out_val = in_val * 0.621371192;
    elseif units == "mi|km" then
        out_val = in_val / 0.621371192;
    elseif units == "km2|sqmi" then
        out_val = in_val * 0.621371192^2;
    elseif units == "sqmi|2km" then
        out_val = in_val / 0.621371192^2;
    else
        error ("Module:Converting between " .. in_unit .. " and " .. out_unit .. " not supported.");
     end
     end
     return out_val
     return value * (self[in_unit] / self[out_unit])
end
end


local length = {
    convert = scaled,
    ['NAME'] = 'length',
    ['m'] = 1,
    ['km'] = 1000,
    ['mi'] = 1609.344,
    ['ft'] = 0.3048,
}
local mass = {
    convert = scaled,
    ['NAME'] = 'mass',
    ['kg'] = 1,
    ['lb'] = 0.45359237,
}


-- This is the top-level function called by {{convert}}.
local temperature = {
function p.main(frame, config, args)
    convert = function (self, value, in_unit, out_unit)
    local pframe = frame:getParent();
        -- FIXME I have thought of a better way, but not implemented
    local args = pframe.args; -- the arguments passed TO the {{convert}} template, in the wikitext that instantiates the template
        -- (each value will be a table of two functions: convert to/from C).
    local config = frame.args; -- the arguments passed BY the {{convert}} template, in the wikitext of the template itself
        local icode = self[in_unit]
     local output;
        local ocode = self[out_unit]
      
        if icode ~= ocode then
     in_val1 = tonumber(args[1]);
            if icode == 1 then
     in_val2 = tonumber(args[3]);
                if ocode == 2 then
     disp = args["disp"];
                    return value - 273.15
                elseif ocode == 3 then
                    return value*(9/5) - 459.67
                end
            elseif icode == 2 then
                if ocode == 1 then
                    return value + 273.15
                elseif ocode == 3 then
                    return value*(9/5) + 32
                end
            elseif icode == 3 then
                if ocode == 1 then
                    return (value + 459.67)*(5/9)
                elseif ocode == 2 then
                    return (value - 32)*(5/9)
                end
            end
        end
        return value
     end,
     ['NAME'] = 'temperature',
     ['K'] = 1,
     ['C'] = 2,
     ['F'] = 3,
}


     if in_val1 == nil then
local units = {
         error ("Module:Convert value not supplied");
     -- Each value is {converter_table, default_out_unit}.
         return ""
    lookup = function (self, unit)
        -- If unit is known, return its converter_table, default_out_unit.
        local t = self[unit]
        if t == nil then
            local msg = 'Unit %s not known'
            error(msg:format(unit))
        end
         return t[1], t[2]
    end,
    ['kg'] = {mass, 'lb'},
    ['lb'] = {mass, 'kg'},
    ['m'] = {length, 'ft'},
    ['km'] = {length, 'mi'},
    ['mi'] = {length, 'km'},
    ['ft'] = {length, 'm'},
    ['K'] = {temperature, 'C'},
    ['C'] = {temperature, 'F'},
    ['F'] = {temperature, 'C'},
}
-------END DATA TABLE-----
 
local function require_number(value, missing, invalid)
    -- If value is missing or not a number, throw an error.
    -- Return value as a number if valid.
    if value == nil then error(missing) end
    local number = tonumber(value)
    if number == nil then error(invalid:format(value)) end
    return number
end
 
local function require_integer(value, missing, invalid)
    -- If value is missing or not an integer, throw an error.
    -- Return value as a number if valid.
    local number = require_number(value, missing, invalid)
    if number ~= math.floor(number) then
         error(invalid:format(value))
    end
    return number
end
 
local function get_parms(pframe)
    -- Return table with all arguments passed by template converted to
    -- named arguments. The numeric args are used to add named args:
    --  in_text, in_text2 (strings given for value, value2)
    --  value, in_unit, out_unit, value2, range, round_to
    -- (except for range, which is nil or a table, the named args that are
    -- added here could be provided by the user of the template).
    local range_types = {  -- text to separate input, output ranges
        ['and'] = {' and ', ' and '},
        ['by'] = {' by ', ' by '},
        ['to'] = {' to ', ' to '},
        ['-'] = {'–', '–'},
        ['to(-)'] = {' to ', '–'},
        ['x'] = {' by ', ' × '},
        ['+/-'] = {' ± ', ' ± '},
    }
    local args = {}                        -- arguments passed to template
    for k,v in pframe:argumentPairs() do
        args[k] = v
     end
     end
    args.in_text = args[1]
    args.value = require_number(args.in_text, 'Need value', 'Value "%s" must be a number')
    local in_unit = args[2]
    local i = 3
    local range = range_types[in_unit]
    if range ~= nil then
        args.in_text2 = args[3]
        args.value2 = require_number(args.in_text2, 'Need second value', 'Second value "%s" must be a number')
        in_unit = args[4]
        i = 5
    end
    local out_unit = args[i]
    local round_to = args[i+1]
    if in_unit == nil then error('Need input unit') end
    args.in_unit = in_unit
    args.out_unit = out_unit
    args.range = range
    args.round_to = round_to
    return args
end


     if in_val2 == nil then
local function converter(parms)
        -- Single value supplied
    -- If we can convert from given in to out unit, return the table
         in_unit  = args[2];
    -- that handles the two given unit types.
        out_unit = args[3];
    -- If no out_unit, change it to the default for the in_unit.
         out_val1 = convert(in_val1,in_unit,out_unit);
    local t1, outdefault = units:lookup(parms.in_unit)
       
     if parms.out_unit == nil then           -- need to catch empty string also?
        if disp == "or" then
         parms.out_unit = outdefault
            output = in_val1 .. " " .. in_unit .. " or " .. out_val1 .. " " .. out_unit;
    end
         elseif disp == "sqbr" then
    local t2 = units:lookup(parms.out_unit)
             output = in_val1 .. " " .. in_unit .. " <nowiki>[</nowiki>" .. out_val1 .. " " .. out_unit .. "<nowiki>]</nowiki>";
    if t1 ~= t2 then
         local msg = 'Cannot convert %s to %s'
        error(msg:format(t1.NAME, t2.NAME))
    end
    return t1
end
 
local function cvtround(invalue, parms, cvt)
    -- Convert given invalue using parms, cvt (return '' if invalue == nil).
    -- Return rounded, formatted string for result, using rounding
    -- specified in parms.
    -- This code combines convert/round because some rounding requires
    -- knowledge of what we are converting.
    -- TODO Fix!
    local text = ''
    if invalue == nil then return text end
    local outvalue = cvt:convert(invalue, parms.in_unit, parms.out_unit)
    local round_to = parms.round_to
    local sigfig = parms.sigfig
    local disp = parms.disp
    if round_to then
        -- Ignore sigfig, disp.
        round_to = require_integer(round_to, 'Need value', 'round_to "%s" must be an integer')
         if round_to >= 0 then
             local fmt = '%.' .. string.format('%.0f', round_to) .. 'f'
            text = string.format(fmt, outvalue)
         else
         else
             output = in_val1 .. " " .. in_unit .. " (" .. out_val1 .. " " .. out_unit .. ")";
             factor = 10^(-round_to)
            zeroes = string.sub(tonumber(factor), 2)
            text = string.format('%.0f', outvalue/factor) .. zeroes
         end
         end
    elseif sigfig then
        -- Ignore disp.
        sigfig = require_integer(sigfig, 'Need value', 'sigfig "%s" must be an integer')
        if sigfig <= 0 then
            msg = 'sigfig "%s" must be positive'
            error(msg:format(parms.sigfig))
        end
        local fmt = '%.' .. string.format('%.0f', sigfig) .. 'g'
        text = string.format(fmt, outvalue)
    elseif disp then
        text = 'TODO: disp'
     else
     else
         -- Two values supplied
         -- Default rounding.
         range    = args[2];
         text = 'TODO: default rounding'
        in_unit  = args[4];
    end
        out_unit = args[5];
    return text
        out_val1 = convert(in_val1,in_unit,out_unit);
end
        out_val2 = convert(in_val2,in_unit,out_unit);


        output = in_val1 .. " " .. range .. " " .. in_val2 .. " " .. in_unit .. " (" .. out_val1 .. " " .. range .. " " .. out_val2 .. " " .. out_unit .. ")";
local function process(parms)
    local cvt = converter(parms)
    local outext = cvtround(parms.value, parms, cvt)
    local outext2 = cvtround(parms.value2, parms, cvt)
    local intext = parms.in_text
    local intext2 = parms.in_text2
    local range = parms.range
    local wikitext
    if range == nil then
        wikitext = '%s %s (%s %s)'
        wikitext = wikitext:format(intext, parms.in_unit, outext, parms.out_unit)
    else
        wikitext = '%s%s%s %s (%s%s%s %s)'
        wikitext = wikitext:format(intext, range[1], intext2, parms.in_unit, outext, range[2], outext2, parms.out_unit)
     end
     end
      
     return wikitext
    --error ("Module:Convert is not implemented");
end
     return output   
 
-- Used by template {{convert2}}.
-- We will have to keep old {{convert}} for a long time, and run
-- {{convert2}} in parallel with {{convert}} while testing/developing.
local p = {}
 
function p.convert(frame)
    local pframe = frame:getParent()
    local parms = get_parms(pframe)
     return process(parms)
end
end


return p
return p

Revision as of 21:34, 4 September 2012

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

--[[
Start of convert that I hope will be driven from tables.
Lots to do (first want to fix the default rounding).
I (enwiki Johnuniq) started this two days ago; it's not ready for release
but I'm dropping it here per BRD to see if others want to work with this.

Testing makes me think these results will occur with current code:
{{convert|3.21|kg|lb|2}}     3.21 kg (7.08 lb)
{{convert|212|F|C|2}}        212 F (100.00 C)
{{convert|5|to|10|kg|lb|2}}  5 to 10 kg (11.02 to 22.05 lb)
{{convert|5|-|10|kg|lb|2}}   5–10 kg (11.02–22.05 lb)
{{convert|15|ft|m|2}}        15 ft (4.57 m)
{{convert|3.21|kg|m|2}}      Error: Cannot convert mass to length
]]--

--[[-----BEGIN DATA TABLE-----
Plan to write a program to generate the conversion tables below.
The input would be a text file in human-friendly format, and
the output would be the tables following like length/mass/units.
Values from http://en.wikipedia.org/wiki/Conversion_of_units
Check with  http://en.wikipedia.org/wiki/Template:Convert/list_of_units
]]--

local function scaled(self, value, in_unit, out_unit)
    -- Return scaled value for a simple convert.
    if out_unit == nil then
        out_unit = self[in_unit][2]
    end
    return value * (self[in_unit] / self[out_unit])
end

local length = {
    convert = scaled,
    ['NAME'] = 'length',
    ['m'] = 1,
    ['km'] = 1000,
    ['mi'] = 1609.344,
    ['ft'] = 0.3048,
}

local mass = {
    convert = scaled,
    ['NAME'] = 'mass',
    ['kg'] = 1,
    ['lb'] = 0.45359237,
}

local temperature = {
    convert = function (self, value, in_unit, out_unit)
        -- FIXME I have thought of a better way, but not implemented
        -- (each value will be a table of two functions: convert to/from C).
        local icode = self[in_unit]
        local ocode = self[out_unit]
        if icode ~= ocode then
            if icode == 1 then
                if ocode == 2 then
                    return value - 273.15
                elseif ocode == 3 then
                    return value*(9/5) - 459.67
                end
            elseif icode == 2 then
                if ocode == 1 then
                    return value + 273.15
                elseif ocode == 3 then
                    return value*(9/5) + 32
                end
            elseif icode == 3 then
                if ocode == 1 then
                    return (value + 459.67)*(5/9)
                elseif ocode == 2 then
                    return (value - 32)*(5/9)
                end
            end
        end
        return value
    end,
    ['NAME'] = 'temperature',
    ['K'] = 1,
    ['C'] = 2,
    ['F'] = 3,
}

local units = {
    -- Each value is {converter_table, default_out_unit}.
    lookup = function (self, unit)
        -- If unit is known, return its converter_table, default_out_unit.
        local t = self[unit]
        if t == nil then
            local msg = 'Unit %s not known'
            error(msg:format(unit))
        end
        return t[1], t[2]
    end,
    ['kg'] = {mass, 'lb'},
    ['lb'] = {mass, 'kg'},
    ['m'] = {length, 'ft'},
    ['km'] = {length, 'mi'},
    ['mi'] = {length, 'km'},
    ['ft'] = {length, 'm'},
    ['K'] = {temperature, 'C'},
    ['C'] = {temperature, 'F'},
    ['F'] = {temperature, 'C'},
}
-------END DATA TABLE-----

local function require_number(value, missing, invalid)
    -- If value is missing or not a number, throw an error.
    -- Return value as a number if valid.
    if value == nil then error(missing) end
    local number = tonumber(value)
    if number == nil then error(invalid:format(value)) end
    return number
end

local function require_integer(value, missing, invalid)
    -- If value is missing or not an integer, throw an error.
    -- Return value as a number if valid.
    local number = require_number(value, missing, invalid)
    if number ~= math.floor(number) then
        error(invalid:format(value))
    end
    return number
end

local function get_parms(pframe)
    -- Return table with all arguments passed by template converted to
    -- named arguments. The numeric args are used to add named args:
    --   in_text, in_text2 (strings given for value, value2)
    --   value, in_unit, out_unit, value2, range, round_to
    -- (except for range, which is nil or a table, the named args that are
    -- added here could be provided by the user of the template).
    local range_types = {  -- text to separate input, output ranges
        ['and'] = {' and ', ' and '},
        ['by'] = {' by ', ' by '},
        ['to'] = {' to ', ' to '},
        ['-'] = {'–', '–'},
        ['to(-)'] = {' to ', '–'},
        ['x'] = {' by ', ' × '},
        ['+/-'] = {' ± ', ' ± '},
    }
    local args = {}                         -- arguments passed to template
    for k,v in pframe:argumentPairs() do
        args[k] = v
    end
    args.in_text = args[1]
    args.value = require_number(args.in_text, 'Need value', 'Value "%s" must be a number')
    local in_unit = args[2]
    local i = 3
    local range = range_types[in_unit]
    if range ~= nil then
        args.in_text2 = args[3]
        args.value2 = require_number(args.in_text2, 'Need second value', 'Second value "%s" must be a number')
        in_unit = args[4]
        i = 5
    end
    local out_unit = args[i]
    local round_to = args[i+1]
    if in_unit == nil then error('Need input unit') end
    args.in_unit = in_unit
    args.out_unit = out_unit
    args.range = range
    args.round_to = round_to
    return args
end

local function converter(parms)
    -- If we can convert from given in to out unit, return the table
    -- that handles the two given unit types.
    -- If no out_unit, change it to the default for the in_unit.
    local t1, outdefault = units:lookup(parms.in_unit)
    if parms.out_unit == nil then           -- need to catch empty string also?
        parms.out_unit = outdefault
    end
    local t2 = units:lookup(parms.out_unit)
    if t1 ~= t2 then
        local msg = 'Cannot convert %s to %s'
        error(msg:format(t1.NAME, t2.NAME))
    end
    return t1
end

local function cvtround(invalue, parms, cvt)
    -- Convert given invalue using parms, cvt (return '' if invalue == nil).
    -- Return rounded, formatted string for result, using rounding
    -- specified in parms.
    -- This code combines convert/round because some rounding requires
    -- knowledge of what we are converting.
    -- TODO Fix!
    local text = ''
    if invalue == nil then return text end
    local outvalue = cvt:convert(invalue, parms.in_unit, parms.out_unit)
    local round_to = parms.round_to
    local sigfig = parms.sigfig
    local disp = parms.disp
    if round_to then
        -- Ignore sigfig, disp.
        round_to = require_integer(round_to, 'Need value', 'round_to "%s" must be an integer')
        if round_to >= 0 then
            local fmt = '%.' .. string.format('%.0f', round_to) .. 'f'
            text = string.format(fmt, outvalue)
        else
            factor = 10^(-round_to)
            zeroes = string.sub(tonumber(factor), 2)
            text = string.format('%.0f', outvalue/factor) .. zeroes
        end
    elseif sigfig then
        -- Ignore disp.
        sigfig = require_integer(sigfig, 'Need value', 'sigfig "%s" must be an integer')
        if sigfig <= 0 then
            msg = 'sigfig "%s" must be positive'
            error(msg:format(parms.sigfig))
        end
        local fmt = '%.' .. string.format('%.0f', sigfig) .. 'g'
        text = string.format(fmt, outvalue)
    elseif disp then
        text = 'TODO: disp'
    else
        -- Default rounding.
        text = 'TODO: default rounding'
    end
    return text
end

local function process(parms)
    local cvt = converter(parms)
    local outext = cvtround(parms.value, parms, cvt)
    local outext2 = cvtround(parms.value2, parms, cvt)
    local intext = parms.in_text
    local intext2 = parms.in_text2
    local range = parms.range
    local wikitext
    if range == nil then
        wikitext = '%s %s (%s %s)'
        wikitext = wikitext:format(intext, parms.in_unit, outext, parms.out_unit)
    else
        wikitext = '%s%s%s %s (%s%s%s %s)'
        wikitext = wikitext:format(intext, range[1], intext2, parms.in_unit, outext, range[2], outext2, parms.out_unit)
    end
    return wikitext
end

-- Used by template {{convert2}}.
-- We will have to keep old {{convert}} for a long time, and run
-- {{convert2}} in parallel with {{convert}} while testing/developing.
local p = {}

function p.convert(frame)
    local pframe = frame:getParent()
    local parms = get_parms(pframe)
    return process(parms)
end

return p