Help on coding an advanced calculator

Hey there!

My goal is to build a working chat / textbox calculator which would solve most of the mathematical operations.

However, I don’t really know how would I go about coding it.
I am stuck literally at the beginning (finding operators and calculating it).
I don’t want to write million if statements only to check what is the operator and to calculate it.
e.g:

if operator == "+" then
    print(num1 + num2)
elseif operator == "-" then
    print(num1 - num2)
elseif operator == "/" then
    print(num1 / num2)
-- etc...
end

I expect it to solve operations like these:
2 + 3,
10 + 6 - 2,
88 - 2 * 9,
30 / 2 * 5.25 - 2
50 ^ 70 / 1.5
I just wrote some random ones, but you get the point right?
Adding, subtracting, multiplying, diving, exponentation, operating on floats.

I tried googling for the solution, I also tried to look at Developer Hub and Scripting Helpers but I couldn’t seem to get any help. That’s why I am writing this.

Just to be clear: I don’t want any ready-to-use scripts.
I just need some help with this, tips, and a possible explanation on how would I easily code it.
It’s a challange I always wanted to do.

(Sorry for my bad english, I am still learning. I might have mistook operations with equations :sweat_smile:)

Thanks, any help is really appreciated!

4 Likes

There’s no harm in if statements. You’re trying to do different things depending on some condition – that’s what they’re for! EDIT: Also, there’s not a “million” operators – there’s like, ten.

That being said, you could make a map from operators to functions:

local function Add(a, b) return a + b end
local function Subtract(a, b) return a - b end
-- ... etc.

-- map from operator strings to functions accepting two numbers
local operatorFuncs = {
    ["+"] = Add,
    ["-"] = Subtract,
    -- can do anonymous funcs instead
    ["/"] = function(a, b) return a / b end
}

local function DoOperation(operator, a, b)
    -- look up which function to use
    local func = operatorFuncs[operator]

    if not func then error("Invalid operator") end

    return func(a, b)
end

Again though, you’re not saving much code compared to the following:

local function DoOperation(operator, a, b)
    if operator == "+" then
        return Add(a, b)
    elseif operator == "-" then
        return Subtract(a, b)
    elseif operator == "/" then
        return a / b
    else
        error("Invalid operator")
    end
end

This is the easy part, by the way :slight_smile: . The harder part is figuring out things like order of operations (doing multiplication before addition, supporting parentheses, etc.). The google term for that problem is “abstract syntax tree” if you wanna go down that rabbit hole!

3 Likes

Welcome to the world of pattern matching and syntax trees!

For your current desired input/output, we can use pattern matching to determine the operands and the operator like so:

local function calculate(str)
    if type(str) ~= 'string' then
        return error 'Need to provide a string.'
    end
    
    -- Pattern matching magic here
    local x, operator, y = str:match('(%d+)%s*(%S+)%s*(%d+)')
    -- The pattern above just means [{Capture a number}(ignore any spaces of any size){Capture anything but a space - i.e. it's a negated class}(ignore any spaces of any size){Capture any number}]
    -- Since our operator match can be anything but a space, we could even add a case for 'x plus y' etc
    x, y = tonumber(x), tonumber(y) -- make sure they're numbers
    if x and y then
        -- perform our operation
        if operator then
            if operator == '+' then
                return x + y
            elseif operator == '-' then
                return x - y
            elseif operator == '%' then
                return x % y
            elseif operator == '*' then
                return x * y
            elseif operator == '/' then
                return x / y
            elseif operator == '^' then
                return x ^ y
            end
        end
        return error 'No valid operator found.'
    else
        return error 'Valid operands not found.'
    end
end


--> Now let's test!
local operands = {
    x = 5;
    y = 5;
}

local operators = {'+', '-', '%', '*', '/', '^'}

for _, operator in next, operators do
    -- Let's make it into a string so the calculate function gets something like '5 + 5'
    local equation = operands.x .. operator .. operands.y
    local result = calculate(equation)
    print(
        ('Equation [%s] yielded %d as a result'):format(equation, result)
    )
end

--[[
Output:
Equation [5+5] yielded 10 as a result
Equation [5-5] yielded 0 as a result
Equation [5%5] yielded 0 as a result
Equation [5*5] yielded 25 as a result
Equation [5/5] yielded 1 as a result
Equation [5^5] yielded 3125 as a result
]]

Highly recommend you read up on Lua pattern matching here. You could probably expand this to parse the equation in chunks, such that you could build some form of order of operations, but I would imagine it wouldn’t be as effective as a syntax tree.

Now, if you want to get crazy and start adding in order of operators, and algebraic support similar to https://www.wolframalpha.com/ you’d need to look into syntax trees (just as @nicemike40 has suggested).

A really basic github exploring this can be found here (it is in Java, mind).

7 Likes

Either send a Post request to the relevant endpoint for this, or use string manipulation
e.g

local str = "5*5" -- example calculation
local a, op, b = str:match("(%d+).-(%p).-(%d+)")
print(loadstring("return "..a..op..b)()) -- as long as it's only 2 numbers involved
-- no need to handle operations for every individual case when you can use loadstring to do it for you (^, *, +, -, %), unless  you don't want to involve that

Edit: forgot to clarify that this simple approach is only to be used when you’re entirely certain the operation only involves one operator and 2 numbers - invalid operations aren’t accounted obviously as you can see, wasn’t the intent of this post too.

Ooh yeah using an existing free API is not a bad idea if you just want something that works. https://api.mathjs.org/ looks promising.

Also, I don’t think you should use string matching for this project. Split the string into tokens, then just check the individual tokens with tonumber or a operator lookup.

Hm, thanks.

I’ll look at that aswell.

I also think that splitting is a better idea if I would like to have multiple operators.

You can always just use ^ 2 for the exponential, you can always use math.cos sin tan, you can always use math.abs for absolute values, there is quite a lot, you can find most math functions on roblox wiki

Why the doubts of string matching? You can push it pretty far. Pattern matching uses e.g. XML parsing, HTML parsing and so forth - all utilising pattern matching (albeit, the latter does seem to use C libraries).

Further pattern example for your case:

local function pushOperation(operator, x, y)
    local res
    if operator == '+' then
        res = x + y
    elseif operator == '-' then
        res = x - y
    elseif operator == '%' then
        res = x % y
    elseif operator == '*' then
        res = x * y
    elseif operator == '/' then
        res = x / y
    elseif operator == '^' then
        res = x ^ y
    end
    return res
end

local function calculate(str)
    if type(str) ~= 'string' then
        return error 'Need to provide a string.'
    end
    
    local result
    for first, op, last in str:gmatch('(%b())([%S]+)(%b())') do
        for _, side in next, {first, last} do
            local x, operator, y = side:match('(%d+)%s*(%S+)%s*(%d+)')
            x, y = tonumber(x), tonumber(y)
            
            local res
            if x and y then
                res = pushOperation(operator, x, y)
            end
            
            if res then
                if result then
                    result = pushOperation(op, result, res)
                else
                    result = res
                end
            end
        end
    end
    
    return result or 0
end


--> Now let's test!
local operands = {
    x = 5;
    y = 5;
}

local operators = {'+', '-', '*', '/', '^'}

for _, operator in next, operators do
    local equation = string.rep('(' ..operands.x .. operator .. operands.y .. ')', 2, operators[math.random(1, #operators)])
    local result = calculate(equation)
    print(
        ('Equation [%s] yielded %s as a result'):format(equation, tostring(result))
    )
end

Equation [(5+5)^(5+5)] yielded 10000000000.0 as a result
Equation [(5-5)-(5-5)] yielded 0 as a result
Equation [(55)/(55)] yielded 1.0 as a result
Equation [(5/5)/(5/5)] yielded 1.0 as a result
Equation [(5^5)^(5^5)] yielded inf as a result

Granted, you’re not going to account for every case through one expression. You’re going to need to parse and interpret the string if you’re trying to build this from the ground up. Imo, splitting the string doesn’t seem particularly helpful, as you’ll have to parse before and after each split, taking into account every other split.

If your goal isn’t to build this from scratch as I interpreted from your OP, @XxELECTROFUSIONxX’s suggestion of posting to an endpoint is likely best. His idea of parsing via loadstring is doable, but you’d need to sandbox and sanitise input, however… this would also require some parsing and interpretation.

1 Like

I managed to finally do the calculator’s basics. Now I would only need to implement the abstract syntax tree which will be a lot harder from what I see, but atleast basics are finally done.

For those who still struggle with it:

I managed to do it using while loop and splitting the variable result which was the given string (math operation).
Then I calculated one by one everything using gsub (replacing the first piece of operation e.g 5 + 5 with 10) and continuing until ‘operator’ and number ‘y’ were nil.

So 5 + 5 * 2 would go like this:
5 + 5 * 2
10 * 2
20

Is this the best method? I don’t know, but I would probably have to change it a little bit when it comes with syntax tree.

Good luck

And thanks to everyone for help!

True, but I would probably not choose that method.
I believe split is easier, I am not that good in patterns so I don’t even understand what is

str:gmatch('(%b())([%S]+)(%b())')

For separating words, or splitting an XML tag into names and tokens, regex will be useful.

For this though, the only steps are:

  1. Tokenize
  2. Build AST
  3. Evaluate AST

The initial tokenizing could use gmatch with a regex to skip whitespace and record whole words, but the other steps don’t use it.

For separating words, or splitting an XML tag into names and tokens, regex will be useful.
For this though, the only steps are:

  1. Tokenize
  2. Build AST
  3. Evaluate AST
    The initial tokenizing could use gmatch with a regex to skip whitespace and record whole words , but the other steps don’t use it.

I agree with you, hence my initial tagging of your name :slight_smile: I didn’t say that solving an ambigious equation could be done using pattern matching alone, I just didn’t agree that splitting the string is superior in this case when pattern matching can be utilised effectively

I have a module that parses math expressions.

https://www.roblox.com/library/5005473860/Luaxp-Expression-Parser

Example:

local m = require([path].luaxp)

print(m.evaluate("pow(2, 8) - 1")) -- 255

Credits are given inside the module script.

I also have a full GLR parser able to parse all context-free grammars which has the full Lua syntax baked in and the ability to add semantic actions to construct a custom AST.

Woah, how many time did you spend coding that?
Seems impressing. :slightly_smiling_face:

YES!
I DID IT.
Sorry, I forgot to break the loop after if’s…

So now my calculator fully works.
Thanks to everyone for help!

You should also add the ability to solve for variables

Do you have tutorial on how to use it?

To help people in the future;

If you don’t want to spam if statements you could write some code like this

operations = {}

operations["+"] = function(a,b) return a+b end
operations["-"] = function(a,b) return a-b end
operations["*"] = function(a,b) return a*b end
operations["/"] = function(a,b) return a/b end
operations["^"] = function(a,b) return a^b end
operations["%"] = function(a,b) return a%b end

After you set a table like this say you had to do 2 ^ 3 you would just execute

resault = operations["^"](2,3)

This is a very clean and forward thinking way to write your code.