From CloudModding MM Wiki

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

-- module providing various functions for handling numbers in radices 2..36

-- note: not using tonumber() since it's not as featurful as we need

local p = {};

local LU = require('libraryUtil');
local B32 = require('bit32');

-- this function parses an integer; it's global to allow use by other modules on
-- the site. This function will stop at the first non-numeric character
-- encountered.
function p.parse_int(numstr, radix)
	radix = radix or 0;
    -- some type-checking
    LU.checkType("parse_int", 1, numstr, "string");
    LU.checkType("parse_int", 2, radix, "number");

    assert(radix == math.floor(radix), "radix must be an integer, yet isn't");
    
    if radix ~= 0 then
        assert(2 <= radix, "radix must be at least 2 (and at most 36)");
        assert(radix <= 36, "radix must be at most 36 (and at least 2)");
    end

    -- now, the function

    local signage = 1;

    if mw.ustring.sub(numstr, 1, 1) == '-' then
        signage = -1;
        numstr = mw.ustring.sub(numstr, 2);
    end

    if radix == 0 then
        -- we'll need to figure out the radix on our own, then
        local ohprefix = mw.ustring.sub(numstr, 1, 2);
        if ohprefix == "0x" then
            radix = 16;
        elseif ohprefix == "0o" then
            radix = 8;
        elseif ohprefix == "0b" then
            radix = 2;
        elseif ohprefix == "0d" then
            radix = 10;
        end

        -- if we changed the radix, then we need to take the oh prefix out of
        -- the string, otherwise there's nothing to remove, and we default to
        -- base 10.

        if radix ~= 0 then
            numstr = mw.ustring.sub(numstr, 3);
        else
            radix = 10;
        end
    end

    -- and now we get to the conversion part
    local res = 0;

    -- loop in terms of codepoints so we're already set to do it if/when we
    -- expand the range of characters handled in the future (the oh prefix stuff
    -- earlier can be safely done assuming ASCII)
    for cp in mw.ustring.gcodepoint(numstr) do
        local thedigit;
        if (0x30 <= cp) and (cp <= 0x39) then -- digits 0..9
            thedigit = B32.band(cp, 0x0F);
        elseif (0x41 <= B32.band(cp, 0xDF)) and (B32.band(cp, 0xDF) <= 0x5A) then -- ASCII letters A..Z, any case
            thedigit = B32.band(cp, 0xDF) - 0x41 + 10;
        else -- some unrecognizable character, we have to stop
            break;
        end

        if thedigit >= radix then
            -- while we could count this an invalid character and break, you
            -- likely made a mistake, so we'll error instead
            error("Invalid digit for radix!");
        end

        res = res * radix + thedigit;
    end

    return res * signage;
end

-- XXX TODO a function to parse floats, when needed

-- writes a number, int or float, to a string
function p.write_num(thenum, radix)
    -- type-checking
    LU.checkType("write_num", 1, thenum, "number");
    LU.checkType("write_num", 2, radix, "number");

    assert(radix == math.floor(radix), "radix must be an integer, yet isn't");
    assert(2 <= radix, "radix must be at least 2 (and at most 36)");
    assert(radix <= 36, "radix must be at most 36 (and at least 2)");

    -- function
    local res = "";

    if thenum < 0 then
        res = res .. "-";
        thenum = math.abs(thenum);
    end

    local ipart = math.floor(thenum);
    local fpart = thenum - ipart;

    local charlist = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";

    -- it's easier to handle the integer part if we turn it into a number less
    -- than one first (note that floor(log()) + 1 is just a neat trick to get
    -- the length of an integer number in the chosen radix)
    local isize = math.floor(math.log(ipart)/math.log(radix)) + 1;
    ipart = ipart / (radix ^ isize);

    -- now to write out the integer part; we multiply the integer part by the
    -- radix, take the integer part of _that_, and subtract the written digit
    -- from ipart.
    for i = 1, isize do
        ipart = ipart * radix;
        res = res .. mw.ustring.sub(charlist, math.floor(ipart) + 1, math.floor(ipart) + 1);
        ipart = ipart - math.floor(ipart);
    end

    -- if there's a fractional part, get to that now
    if fpart ~= 0 then
        res = res .. ".";

        -- note that this is just like how we handled the integer part, only we
        -- didn't have to divide first.
        while fpart ~= 0 do
            fpart = fpart * radix;
            res = res .. mw.ustring.sub(charlist, math.floor(fpart) + 1, math.floor(fpart) + 1);
            fpart = fpart - math.floor(fpart);
        end
    end

    return res;
end

-- only this function can be #invoke'd, the ones above can't.
function p.ConvertInteger(frame)
    local the_num  = frame.args[1];
    local from_rad = tonumber(frame.args[2]) or 0;
    local to_rad = tonumber(frame.args[3]) or 10;

    return p.write_num(p.parse_int(the_num, from_rad), to_rad);
end

return p;