Last active
January 17, 2025 11:49
-
-
Save mjf/c63f06b7e90a88fa20c8177c22e93f2e to your computer and use it in GitHub Desktop.
Radix functions in Lua (including example usage and a test suite)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
local function from_radix(digits, base) | |
local ip = 0 | |
local fp = 0 | |
local dp = nil | |
for i, v in ipairs(digits) do | |
if v == "." then | |
dp = i | |
break | |
end | |
if type(v) ~= "number" then | |
error("Invalid digit in input") | |
end | |
if v < 0 or v >= base then | |
error("Digit out of range for given base") | |
end | |
end | |
if dp then | |
for i = 1, dp - 1 do | |
ip = ip * base + digits[i] | |
end | |
for i = dp + 1, #digits do | |
fp = fp + digits[i] / (base ^ (i - dp)) | |
end | |
else | |
for i = 1, #digits do | |
ip = ip * base + digits[i] | |
end | |
end | |
return ip + fp | |
end | |
local function to_radix(x, base, frac_digits) | |
if base < 2 then | |
error("Base must be at least 2") | |
end | |
if frac_digits and frac_digits < 0 then | |
error("Number of fractional digits must be non-negative") | |
end | |
local result = {} | |
local ip = math.floor(x) | |
local fp = x - ip | |
repeat | |
table.insert(result, 1, ip % base) | |
ip = math.floor(ip / base) | |
until ip == 0 | |
if frac_digits and frac_digits > 0 then | |
table.insert(result, ".") | |
for _ = 1, frac_digits do | |
fp = fp * base | |
local digit = math.floor(fp) | |
table.insert(result, digit) | |
fp = fp - digit | |
end | |
end | |
return result | |
end | |
local function clock_from_moment(tee) | |
local seconds = math.fmod(tee, 86400) | |
if seconds < 0 then | |
seconds = seconds + 86400 | |
end | |
local hours = math.floor(seconds / 3600) | |
seconds = math.fmod(seconds, 3600) | |
local minutes = math.floor(seconds / 60) | |
seconds = math.fmod(seconds, 60) | |
return {hours, minutes, math.floor(seconds)} | |
end | |
local function time_from_clock(hms) | |
if #hms ~= 3 then | |
error("Clock time must have 3 elements: {hours, minutes, seconds}") | |
end | |
for _, v in ipairs(hms) do | |
if type(v) ~= "number" then | |
error("Clock time elements must be numbers") | |
end | |
end | |
if hms[1] < 0 or hms[1] > 23 then | |
error("Hours must be between 0 and 23") | |
end | |
if hms[2] < 0 or hms[2] > 59 then | |
error("Minutes must be between 0 and 59") | |
end | |
if hms[3] < 0 or hms[3] > 59 then | |
error("Seconds must be between 0 and 59") | |
end | |
return ((hms[1] * 3600) + (hms[2] * 60) + hms[3]) / 86400 | |
end | |
local function table_tostring(t) | |
if type(t) ~= "table" then | |
return tostring(t) | |
end | |
local s = "{" | |
for k, v in pairs(t) do | |
s = s .. "[" .. tostring(k) .. "] = " .. tostring(v) .. ", " | |
end | |
if #s > 1 then | |
s = s:sub(1, #s - 2) | |
end | |
return s .. "}" | |
end | |
local function float_equal(a, b, tolerance) | |
tolerance = tolerance or 1e-9 | |
return math.abs(a - b) < tolerance | |
end | |
local function assert_eq(actual, expected, message) | |
local success = false | |
if type(actual) == type(expected) then | |
if type(actual) == "table" then | |
local actual_count = 0 | |
local expected_count = 0 | |
for _ in pairs(actual) do | |
actual_count = actual_count + 1 | |
end | |
for _ in pairs(expected) do | |
expected_count = expected_count + 1 | |
end | |
if actual_count == expected_count then | |
success = true | |
for k, exp_v in pairs(expected) do | |
local v = actual[k] | |
if type(v) == "number" and type(exp_v) == "number" then | |
if not float_equal(v, exp_v) then | |
success = false | |
break | |
end | |
elseif tostring(v) ~= tostring(exp_v) then | |
success = false | |
break | |
end | |
end | |
end | |
elseif type(actual) == "number" then | |
success = float_equal(actual, expected) | |
else | |
success = actual == expected | |
end | |
end | |
if not success then | |
error( | |
"Assertion failed: " .. message .. "\n" .. | |
"Expected: " .. table_tostring(expected) .. "\n" .. | |
"Got: " .. table_tostring(actual) | |
) | |
end | |
end | |
local tests = { | |
{function() | |
return from_radix({1, 0, 1, ".", 1, 1}, 2) | |
end, 5.75, "Binary 101.11"}, | |
{function() | |
return to_radix(5.75, 2, 5) | |
end, {1, 0, 1, ".", 1, 1, 0, 0, 0}, "Decimal 5.75 to binary"}, | |
{function() | |
return to_radix(12345, 16) | |
end, {3, 0, 3, 9}, "Decimal 12345 to hex"}, | |
{function() | |
return to_radix(0.3, 2, 10) | |
end, {0, ".", 0, 1, 0, 0, 1, 1, 0, 0, 1, 1}, "Decimal 0.3 to binary"}, | |
{function() | |
return clock_from_moment(3723) | |
end, {1, 2, 3}, "3723 seconds to clock time"}, | |
{function() | |
return clock_from_moment(0) | |
end, {0, 0, 0}, "0 seconds to clock time"}, | |
{function() | |
return clock_from_moment(86400) | |
end, {0, 0, 0}, "86400 seconds to clock time"}, | |
{function() | |
return clock_from_moment(86461) | |
end, {0, 1, 1}, "86461 seconds to clock time"}, | |
{function() | |
return clock_from_moment(123456) | |
end, {10, 17, 36}, "123456 seconds to clock time"}, | |
{function() | |
return time_from_clock({1, 2, 3}) | |
end, 0.043090277777778, "Clock time 1:2:3 to fraction of day"}, | |
{function() | |
return time_from_clock({0, 0, 0}) | |
end, 0, "Clock time 0:0:0 to fraction of day"}, | |
{function() | |
return time_from_clock({12, 0, 0}) | |
end, 0.5, "Clock time 12:0:0 to fraction of day"}, | |
{function() | |
return time_from_clock({23, 59, 59}) | |
end, 0.99998842592593, "Clock time 23:59:59 to fraction of day"}, | |
{function() | |
return time_from_clock({1, 10, 56}) | |
end, 0.049259259259259, "Clock time 1:10:56 to fraction of day"} | |
} | |
local function run_tests() | |
local passed = 0 | |
local total = #tests | |
for i, test in ipairs(tests) do | |
local description = test[3] | |
local actual = test[1]() | |
local expected = test[2] | |
assert_eq(actual, expected, description) | |
print("Test " .. i .. ": " .. description .. " - PASSED") | |
passed = passed + 1 | |
end | |
print("\nPassed " .. passed .. " of " .. total .. " tests.") | |
end | |
run_tests() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment