Files
2026-11-04 15:26:32 +09:00

339 lines
10 KiB
Lua

--[[-- Gui - Calculator
Add a calculator to do quick calculation.
]]
local Gui = require("modules/exp_gui")
local Roles = require("modules/exp_roles")
local format_number = require("util").format_number
--- @class ExpGui_Calculator.elements
local Elements = {}
local calc_function = setmetatable({
ln = math.log,
log = function(x, b) return math.log(x, b or 10) end,
round = function(x) return x >= 0 and math.floor(x + 0.5) or math.ceil(x - 0.5) end,
trunc = function(x) return (math.modf(x)) end,
sign = function(x) return x > 0 and 1 or (x < 0 and -1 or 0) end,
pi = function(_) return math.pi end,
}, {
__index = math
})
local calc_function_operation = {
["+"] = {1, function(a, b) return a + b end},
["-"] = {1, function(a, b) return a - b end},
["*"] = {2, function(a, b) return a * b end},
["/"] = {2, function(a, b) if b == 0 then error({"", {"exp-gui_calculator.error"}, {"exp-gui_calculator.div_zero"}}, 0) end return a / b end},
["%"] = {2, function(a, b) if b == 0 then error({"", {"exp-gui_calculator.error"}, {"exp-gui_calculator.mod_zero"}}, 0) end return a % b end},
}
local calc_si_suffix = {
k = 1e3,
m = 1e6,
g = 1e9,
t = 1e12,
p = 1e15
}
local calc_number_base = {
x = {value = 16, digits = "%x"},
b = {value = 2, digits = "[01]"},
o = {value = 8, digits = "[0-7]"},
}
--- Function label
Elements.calculator_function_label = Gui.define("calculator/function_label")
:draw{
type = "label",
caption = {"exp-gui_calculator.tooltip"},
style = "heading_2_label",
}
--- Input textfield
Elements.calculator_input_textfield = Gui.define("calculator/input_textfield")
:draw{
type = "textfield",
style = "console_input_textfield",
}
:style{
width = 400,
}
--- Output label
Elements.calculator_result_label = Gui.define("calculator/result_label")
:draw{
type = "label",
caption = "",
name = "calculator_result_label",
style = "heading_2_label",
}
--- Info label
Elements.calculator_info_label = Gui.define("calculator/info_label")
:draw{
type = "label",
caption = "[virtual-signal=signal-info]",
tooltip = {"exp-gui_calculator.info_tooltip"},
style = "heading_2_label",
}
--- Container added to the screen
Elements.container = Gui.define("calculator/container")
:draw(function(def, parent)
local screen_frame = Gui.elements.screen_frame(parent, nil, true)
Elements.calculator_function_label(screen_frame)
Elements.calculator_input_textfield(screen_frame)
Elements.calculator_result_label(screen_frame)
Elements.calculator_info_label(screen_frame)
return Gui.elements.screen_frame.get_root_element(screen_frame)
end)
--- Add a button to create the container
Gui.toolbar.create_button{
name = "open_calculator",
sprite = "entity/arithmetic-combinator",
tooltip = {"exp-gui_calculator.tooltip"},
visible = function(player, element)
return Roles.player_has_permission(player, "exp-gui_calculator")
end
}:on_click(function(def, player, element, event)
Elements.container(player.gui.screen)
end)
--- Parse Value
function Elements.parse(expression)
local cursor = 1
local function skip_space()
while expression:sub(cursor, cursor) == " " do
cursor = cursor + 1
end
end
local function factorial(n)
if n < 0 then
return error({"", {"exp-gui_calculator.error"}, {"exp-gui_calculator.factorial_neg", n}}, 0)
elseif n > 170 then
return error({"", {"exp-gui_calculator.error"}, {"exp-gui_calculator.factorial_large", n}}, 0)
end
if n ~= math.floor(n) then
return error({"", {"exp-gui_calculator.error"}, {"exp-gui_calculator.factorial_int", n}}, 0)
end
local result = 1
for i = 2, n do
result = result * i
end
return result
end
local parse_expr
local function parse_atom()
skip_space()
local character = expression:sub(cursor, cursor)
if character == "|" then
cursor = cursor + 1
---@diagnostic disable-next-line: need-check-nil
local value = parse_expr(1)
skip_space()
if expression:sub(cursor, cursor) ~= "|" then
return error({"", {"exp-gui_calculator.error"}, {"exp-gui_calculator.expected_bar"}}, 0)
end
cursor = cursor + 1
return math.abs(value)
elseif character == "(" then
cursor = cursor + 1
---@diagnostic disable-next-line: need-check-nil
local value = parse_expr(1)
skip_space()
if expression:sub(cursor, cursor) ~= ")" then
return error({"", {"exp-gui_calculator.error"}, {"exp-gui_calculator.expected_paren"}}, 0)
end
cursor = cursor + 1
return value
elseif character == "" then
return error({"", {"exp-gui_calculator.error"}, {"exp-gui_calculator.unexpected_end"}}, 0)
elseif character:match("%a") then
skip_space()
local start = cursor
while expression:sub(cursor, cursor):match("%w") do
cursor = cursor + 1
end
local name = expression:sub(start, cursor - 1)
local fn = calc_function[name]
skip_space()
if expression:sub(cursor, cursor) == "(" then
if not fn then
return error({"", {"exp-gui_calculator.error"}, {"exp-gui_calculator.unknown_fn"}}, 0)
end
cursor = cursor + 1
---@diagnostic disable-next-line: need-check-nil
local argument = parse_expr(1)
skip_space()
if expression:sub(cursor, cursor) ~= ")" then
return error({"", {"exp-gui_calculator.error"}, {"exp-gui_calculator.expected_paren"}}, 0)
end
cursor = cursor + 1
return fn(argument)
end
if fn then
return error({"", {"exp-gui_calculator.error"}, {"exp-gui_calculator.expected_open", name}}, 0)
end
return error({"", {"exp-gui_calculator.error"}, {"exp-gui_calculator.unknown_id", name}}, 0)
else
skip_space()
if expression:sub(cursor, cursor) == "0" then
local base = calc_number_base[expression:sub(cursor + 1, cursor + 1):lower()]
if base then
local literal_start = cursor
cursor = cursor + 2
local digit_start = cursor
while expression:sub(cursor, cursor):match(base.digits) do
cursor = cursor + 1
end
if cursor == digit_start or expression:sub(cursor, cursor):match("%w") then
while expression:sub(cursor, cursor):match("%w") do
cursor = cursor + 1
end
return error({"", {"exp-gui_calculator.error"}, {"exp-gui_calculator.invalid_num", expression:sub(literal_start, cursor - 1)}}, 0)
end
return tonumber(expression:sub(digit_start, cursor - 1), base.value)
end
end
local literal = expression:match("^(%d+%.?%d*[eE][%+%-]?%d+)", cursor) or expression:match("^(%d+%.?%d*)", cursor)
if not literal then
return error({"", {"exp-gui_calculator.error"}, {"exp-gui_calculator.expected_num"}}, 0)
end
cursor = cursor + literal:len()
local num = tonumber(literal)
if not num then
return error({"", {"exp-gui_calculator.error"}, {"exp-gui_calculator.invalid_num", literal}}, 0)
end
return num
end
end
local function parse_factor()
skip_space()
if expression:sub(cursor, cursor) == "-" then
cursor = cursor + 1
return -parse_factor()
elseif expression:sub(cursor, cursor) == "+" then
cursor = cursor + 1
return parse_factor()
end
local base = parse_atom()
skip_space()
local suffix = calc_si_suffix[expression:sub(cursor, cursor)]
if suffix then
cursor = cursor + 1
base = base * suffix
end
skip_space()
if expression:sub(cursor, cursor) == "!" then
cursor = cursor + 1
base = factorial(base)
end
skip_space()
if expression:sub(cursor, cursor + 1) == "**" then
cursor = cursor + 2
return base ^ parse_factor()
elseif expression:sub(cursor, cursor) == "^" then
cursor = cursor + 1
return base ^ parse_factor()
end
return base
end
parse_expr = function(min_prec)
local lhs = parse_factor()
while true do
skip_space()
local entry = calc_function_operation[expression:sub(cursor, cursor)]
if not entry or entry[1] < min_prec then
break
end
cursor = cursor + 1
---@diagnostic disable-next-line: need-check-nil
lhs = entry[2](lhs, parse_expr(entry[1] + 1))
end
return lhs
end
local ok, result = pcall(function()
local value = parse_expr(1)
skip_space()
if cursor <= #expression then
error({"", {"exp-gui_calculator.error"}, {"exp-gui_calculator.trailing", expression:sub(cursor, expression:len())}}, 0)
end
return value
end)
return ok and ("= " .. format_number(result, false)) or (type(result) == "table" and result or {"", {"exp-gui_calculator.error"}, tostring(result)})
end
--- Value change
function Elements.change(event)
event.element.parent.calculator_result_label.caption = event.text == "" and "" or Elements.parse(event.text:lower())
end
local e = defines.events
return {
elements = Elements,
events = {
[e.on_gui_text_changed] = Elements.change,
},
}