From 4513e9c73edb580937f25281b936825c0fea5504 Mon Sep 17 00:00:00 2001 From: theKnightsOfRohan Date: Sat, 25 Jan 2025 19:03:21 -0600 Subject: [PATCH] refactor(parser): extract util functions --- lua/hexer/parse_utils.lua | 83 ++++++++++++++++++++ lua/hexer/parser.lua | 110 +++++---------------------- lua/hexer/tests/parse_utils_spec.lua | 51 +++++++++++++ lua/hexer/tests/parser_spec.lua | 92 ++++++---------------- 4 files changed, 173 insertions(+), 163 deletions(-) create mode 100644 lua/hexer/parse_utils.lua create mode 100644 lua/hexer/tests/parse_utils_spec.lua diff --git a/lua/hexer/parse_utils.lua b/lua/hexer/parse_utils.lua new file mode 100644 index 0000000..80b6d30 --- /dev/null +++ b/lua/hexer/parse_utils.lua @@ -0,0 +1,83 @@ +local M = {} + +---Integer in decimal form to binary string +---@param input integer +---@return string +function M.itob(input) + if input == 0 then + return "0" + end + + if input < 0 then + input = math.abs(input) + end + + local result = "" + + while input > 0 do + local remainder = input % 2 + result = remainder .. result + input = math.floor(input / 2) + end + + return result +end + +---Checks if the string has the given number format header and returns the start of the value if it does, otherwise 1 +---@param str string +---@param header string[] the list of single-character format specifiers +---@return integer +function M.check_header(str, header) + for _, ch in ipairs(header) do + if str:sub(1, 1) == ch then + return 2 + end + + -- Should never occur, but just in case + if str:sub(1, 1) == '0' and str:sub(2, 2) == ch then + return 3 + end + end + + return 1 +end + +-- Check whether a passed string is wrapped by quotes +---@param str string +---@return boolean +function M.is_string_wrapped(str) + return #str >= 3 and ( + (str:sub(1, 1) == '"' and str:sub(str:len()) == '"') or + (str:sub(1, 1) == "'" and str:sub(str:len()) == "'") + ) +end + +---String in decimal form to integer +---@param input string +---@return integer +function M.stoi(input) + return tonumber(input, 10); +end + +---String in decimal form to integer +---@param input string +---@return integer +function M.xtoi(input) + return tonumber(input, 16); +end + +---String in decimal form to integer +---@param input string +---@return integer +function M.btoi(input) + return tonumber(input, 2); +end + +---String in decimal form to integer +---@param input string +---@return integer +function M.otoi(input) + return tonumber(input, 8); +end + +return M diff --git a/lua/hexer/parser.lua b/lua/hexer/parser.lua index ef28afb..c6a2363 100644 --- a/lua/hexer/parser.lua +++ b/lua/hexer/parser.lua @@ -1,4 +1,8 @@ -local M = {} +local M = { + ---@private + _utils = require("hexer.parse_utils") +} + ---@class HexerChar ---@field ascii string @@ -9,136 +13,56 @@ local M = {} ---@alias HexerItem HexerChar[] --- Check whether a passed string is wrapped by quotes ----@param str string ----@return boolean -function M._is_string_wrapped(str) - return #str >= 3 and ( - (str:sub(1, 1) == '"' and str:sub(str:len()) == '"') or - (str:sub(1, 1) == "'" and str:sub(str:len()) == "'") - ) -end - ---Parse an input given by the user into a HexerItem, last resorts to string ---@param input string ---@return HexerItem function M.parse_input(input) local value = tonumber(input) if value ~= nil then - return { M._parse_from_int(value) } + return { M.parse_from_int(value) } end - if #input == 1 then return { M._parse_from_int(input:byte(1)) } end + if #input == 1 then return { M.parse_from_int(input:byte(1)) } end local head, tail = 1, input:len() local orig_head = head - head = M._check_header(input, { 'x', 'X' }) - if head ~= orig_head then return { M._parse_from_int(M._xtoi(input:sub(head))) } end + head = M._utils.check_header(input, { 'x', 'X' }) + if head ~= orig_head then return { M.parse_from_int(M._utils.xtoi(input:sub(head))) } end - head = M._check_header(input, { 'b', 'B' }) - if head ~= orig_head then return { M._parse_from_int(M._btoi(input:sub(head))) } end + head = M._utils.check_header(input, { 'b', 'B' }) + if head ~= orig_head then return { M.parse_from_int(M._utils.btoi(input:sub(head))) } end - head = M._check_header(input, { 'o', 'O' }) - if head ~= orig_head then return { M._parse_from_int(M._otoi(input:sub(head))) } end + head = M._utils.check_header(input, { 'o', 'O' }) + if head ~= orig_head then return { M.parse_from_int(M._utils.otoi(input:sub(head))) } end ---@type HexerItem local item = {} - if M._is_string_wrapped(input) then + if M._utils.is_string_wrapped(input) then head = head + 1 tail = tail - 1 end for i = head, tail do - item[i] = M._parse_from_int(input:byte(i)) + item[i] = M.parse_from_int(input:byte(i)) end return item end ----String in decimal form to integer ----@param input string ----@return integer -function M._stoi(input) - return tonumber(input, 10); -end - ----String in decimal form to integer ----@param input string ----@return integer -function M._xtoi(input) - return tonumber(input, 16); -end - ----String in decimal form to integer ----@param input string ----@return integer -function M._btoi(input) - return tonumber(input, 2); -end - ----String in decimal form to integer ----@param input string ----@return integer -function M._otoi(input) - return tonumber(input, 8); -end - ----Integer in decimal form to binary string ----@param input integer ----@return string -function M._itob(input) - if input == 0 then - return "0" - end - - if input < 0 then - input = math.abs(input) - end - - local result = "" - - while input > 0 do - local remainder = input % 2 - result = remainder .. result - input = math.floor(input / 2) - end - - return result -end - ----Checks if the string has the given number format header and returns the start of the value if it does, otherwise 1 ----@param str string ----@param header string[] the list of single-character format specifiers ----@return integer -function M._check_header(str, header) - for _, ch in ipairs(header) do - if str:sub(1, 1) == ch then - return 2 - end - - -- Should never occur, but just in case - if str:sub(1, 1) == '0' and str:sub(2, 2) == ch then - return 3 - end - end - - return 1 -end - ---Create a HexerChar from a given integer ---@param value integer ---@return HexerChar -function M._parse_from_int(value) +function M.parse_from_int(value) ---@type HexerChar local item = { value = value, hex = "0x" .. string.format("%X", value), octal = "0o" .. string.format("%o", value), ascii = string.format("%c", value), - binary = "0b" .. M._itob(value), + binary = "0b" .. M._utils.itob(value), } return item diff --git a/lua/hexer/tests/parse_utils_spec.lua b/lua/hexer/tests/parse_utils_spec.lua new file mode 100644 index 0000000..47989ed --- /dev/null +++ b/lua/hexer/tests/parse_utils_spec.lua @@ -0,0 +1,51 @@ +local Utils = require("hexer.parse_utils") + +describe("Parse Utils", function() + it("should be able to detect whether a passed string is quoted", function() + local strs = { + ["''"] = false, + ['""'] = false, + ["potato"] = false, + ["'p'"] = true, + ['"p"'] = true, + ["'potato'"] = true, + ['"potato"'] = true + } + + local res + + for str, q in pairs(strs) do + res = Utils.is_string_wrapped(str) == q + assert(res, ("%s expected to be %s but got %s"):format(str, q, res)) + end + end) + + it("should see if a given string contains the header", function() + local strs = { "0b0011", "B0011" } + local headers = { { "b", "B" }, { "x", "X" } } + + local val_pos = Utils.check_header(strs[1], headers[1]) + + assert(val_pos == 3, + ("String %s with headers %s expected pos %s but got %s"):format(strs[1], vim.inspect(headers[1]), 3, val_pos) + ) + + val_pos = Utils.check_header(strs[1], headers[2]) + + assert(val_pos == 1, + ("String %s with headers %s expected pos %s but got %s"):format(strs[1], vim.inspect(headers[2]), 1, val_pos) + ) + + val_pos = Utils.check_header(strs[2], headers[1]) + + assert(val_pos == 2, + ("String %s with headers %s expected pos %s but got %s"):format(strs[2], vim.inspect(headers[1]), 2, val_pos) + ) + + val_pos = Utils.check_header(strs[2], headers[2]) + + assert(val_pos == 1, + ("String %s with headers %s expected pos %s but got %s"):format(strs[2], vim.inspect(headers[2]), 1, val_pos) + ) + end) +end) diff --git a/lua/hexer/tests/parser_spec.lua b/lua/hexer/tests/parser_spec.lua index 893df84..ed83429 100644 --- a/lua/hexer/tests/parser_spec.lua +++ b/lua/hexer/tests/parser_spec.lua @@ -1,80 +1,24 @@ local Parser = require("hexer.parser") describe("Parser", function() - it("should be able to detect whether a passed string is quoted", function() - local strs = { - ["''"] = false, - ['""'] = false, - ["potato"] = false, - ["'p'"] = true, - ['"p"'] = true, - ["'potato'"] = true, - ['"potato"'] = true - } - - local res - - for str, q in pairs(strs) do - res = Parser._is_string_wrapped(str) == q - assert(res, ("%s expected to be %s but got %s"):format(str, q, res)) - end - end) - - it("should correctly see if a given string contains the header", function() - local strs = { "0b0011", "B0011" } - local headers = { { "b", "B" }, { "x", "X" } } - - local val_pos = Parser._check_header(strs[1], headers[1]) - - assert(val_pos == 3, - ("String %s with headers %s expected pos %s but got %s"):format(strs[1], vim.inspect(headers[1]), 3, val_pos) - ) + local target = { + value = 69, + ascii = "E", + binary = "0b1000101", + hex = "0x45", + octal = "0o105", + } - val_pos = Parser._check_header(strs[1], headers[2]) - assert(val_pos == 1, - ("String %s with headers %s expected pos %s but got %s"):format(strs[1], vim.inspect(headers[2]), 1, val_pos) - ) - - val_pos = Parser._check_header(strs[2], headers[1]) - - assert(val_pos == 2, - ("String %s with headers %s expected pos %s but got %s"):format(strs[2], vim.inspect(headers[1]), 2, val_pos) - ) - - val_pos = Parser._check_header(strs[2], headers[2]) - - assert(val_pos == 1, - ("String %s with headers %s expected pos %s but got %s"):format(strs[2], vim.inspect(headers[2]), 1, val_pos) - ) - end) - - it("should be able to correctly parse from a decimal value", function() - local parsed = Parser._parse_from_int(69) - - ---@type HexerChar - local target = { - value = 69, - ascii = "E", - binary = "0b1000101", - hex = "0x45", - octal = "0o105", - } + it("should be able to parse from a decimal value", function() + local parsed = Parser.parse_from_int(69) for k, v in pairs(target) do assert(v == parsed[k], string.format("Key %s: %s != %s", tostring(k), tostring(v), tostring(parsed[k]))) end end) - it("should be able to correctly parse from string inputs", function() - local target = { - value = 69, - ascii = "E", - binary = "0b1000101", - hex = "0x45", - octal = "0o105", - } - + it("should be able to parse from a decimal string", function() local parsed = Parser.parse_input("69") assert(#parsed == 1, string.format("Decimal string: expected length 1, is instead %s", tostring(#parsed))) @@ -82,8 +26,10 @@ describe("Parser", function() for k, v in pairs(target) do assert(v == parsed[1][k], string.format("Decimal string: %s != %s", tostring(v), tostring(parsed[1][k]))) end + end) - parsed = Parser.parse_input("E") + it("should be able to parse from an ascii character", function() + local parsed = Parser.parse_input("E") assert(parsed) assert(#parsed == 1, string.format("Ascii character: expected length 1, is instead %s", tostring(#parsed))) @@ -92,8 +38,10 @@ describe("Parser", function() assert(v == parsed[1][k], string.format("Ascii character: expected %s, actual %s", tostring(v), tostring(parsed[1][k]))) end + end) - parsed = Parser.parse_input("0b1000101") + it("should be able to parse from a binary string", function() + local parsed = Parser.parse_input("0b1000101") assert(parsed) assert(#parsed == 1, string.format("Binary: expected length 1, is instead %s", tostring(#parsed))) @@ -102,8 +50,10 @@ describe("Parser", function() assert(v == parsed[1][k], string.format("Binary: expected %s, actual %s", tostring(v), tostring(parsed[1][k]))) end + end) - parsed = Parser.parse_input("0x45") + it("should be able to parse from a hex string", function() + local parsed = Parser.parse_input("0x45") assert(parsed) assert(#parsed == 1, string.format("Hexadecimal: expected length 1, is instead %s", tostring(#parsed))) @@ -112,8 +62,10 @@ describe("Parser", function() assert(v == parsed[1][k], string.format("Hexadecimal: expected %s, actual %s", tostring(v), tostring(parsed[1][k]))) end + end) - parsed = Parser.parse_input("0o105") + it("should be able to parse from a octal string", function() + local parsed = Parser.parse_input("0o105") assert(parsed) assert(#parsed == 1, string.format("Octal: expected length 1, is instead %s", tostring(#parsed)))