Calculate String Module

Calculate-String Module

Methods / Functions:

Evaluate(String:string formula or formula)

  • Used to Evaluate A String Formula
  • Input may contain the string or number form of a number or formula and may also contain variables set using the SetVar() function.
  • Output is still in string form although solved and may need to be turned into a number with tonumber()
  • Example: Method.Evaluate("(10/2)^2") -- outputs "25"
  • Formulas Follow Normal Math Rules Except For Exponents which default to 2 if not followed by a number or variable.
  • Example 2: Method.Evaluate("(10/2)^") -- This is Equivalent to the above example

SetVar(VarName:string,Value:string or number)

  • Used to Set Variables For a calculation
  • Setting already set Variables overwrites them
  • Variables can contain Variables As long as they are set before the calculation that the ancestor variable is used in.
  • Example:
    Method.SetVar("Test","5")
    Method.Evaluate("Test^") -- outputs "25"

RemoveVar(VarName:string)

  • Used to Remove Variables
  • Example:
    Method.SetVar("Test","5") -- Set "Test"
    Method.RemoveVar("Test") -- Remove "Test"
    Method.Evaluate("Test^") -- outputs "Test^" and gives 2 warnings
If you find a bug feel free to let me know. This still in progress and may have some bugs.

Updates:

Update 1: Infinite Var Loop Fix, Renamed Solve To Evaluate.
Update 2: Removed Temp Wait From A Loop And Replaced it with a better yield option.
Update 3: Removed Some debug print calls.
Update 4: Invalid vars no long cause infinite loop.
Update 5: Added Proper erroring, added module settings, and fixed bugs

3 Likes

Very cool! I did something similar! But first I designed a algorithm that generates math questions then created a math question decoder that can solve worded arithmetic. This is the one for arithetmetic

function cm.InterpretMathQuestion(str)
	-- Get the shapes table from the ReplicatedStorage
	local Result=nil
    local new_query = str -- create a copy of the input string
    new_query=add_spaces_around_numbers(new_query)
   -- print(new_query)
   local str=new_query
	local addition,subtraction,multiplication,division=nil,nil,nil,nil
	local addition=mathquery(str,AddConstruct2,true,true,false)	
  -- print(addition)
    if addition==nil then
		subtraction=mathquery(str,SubtractConstruct2,true,true,false)	
	if subtraction==nil then	
			multiplication=mathquery(str,MultiplyConstruct2,true,true,false)	
	if multiplication==nil then		
				division=mathquery(str,DivideConstruct2,true,true,false)	
			end
		end
	end
	if multiplication or addition or subtraction or division then
	-- Create an array of keys from the Operators table
	local Keys = {}
	
	for k, v in pairs(Operators) do
		table.insert(Keys, k)
	end
	
	local function SplitQuery(query, num1)
		local parts = {}
		-- Find index of first number in query
		local num1Index = string.find(query, tostring(num1))
		-- Get substring before number 
		parts[1] = string.sub(query, 1, num1Index - 1)
		-- Get substring after number
		parts[2] = string.sub(query, num1Index + 1)
print(parts[1]..parts[2])
		return parts[1], parts[2]
	end

	local function GetNumber(query)
		local Num1, Num2 = nil, nil
		local identifier=nil
		local part1,part2=nil,nil
		for word in string.gmatch(query, "%S+") do
			if tonumber(word) then 
				if not Num1 then
					Num1 = tonumber(word) 
					part1,part2=SplitQuery(query,Num1)
				else
					if identifier==nil then
						
					end
					Num2 = tonumber(word)
					break -- stop after 2 numbers found
				end
			end
		end
		return Num1,Num2
	end
	-- Pick two random numbers between 1 and 10
	local Num1,Num2=GetNumber(str)
	-- Pick a random operator from the Operators table using the Keys array
	if Num1 and Num2 then
	local Key=nil 
	if addition~=nil then
			Key=AddConstruct[1]
			Keys=AddConstruct
	elseif subtraction~=nil then
			Key=SubtractConstruct[1]
			Keys=SubtractConstruct
	elseif multiplication~=nil then
			Key=MultiplyConstruct[1]
			Keys=MultiplyConstruct
	elseif division~=nil then
			Key=DivideConstruct[1]
			Keys=DivideConstruct
	end
	if Key~=nil then
			local Operator = Operators[Key]
	Key= Keys[mathrandom(1, #Keys)]
	local Symbol = Key
	local Key2
	local Operator2
	-- Get the symbol of the operator (same as the key)
	-- Calculate the answer by applying the operator function to the numbers
                local Answer = Operator(Num1, Num2)
                local closing={". The solution would be ",". The answer would be ",". This gives us a result of ",". This is equal to "}
                 -- local Subjects = game.ReplicatedStorage.Items.Food.Icons:GetChildren()

                -- Pick a random subject from the Subjects table
                -- local Subject = Subjects[mathrandom(1, #Subjects)].Name

	local Question = Opening[mathrandom(1,#Opening)]..Num1..Symbol..Num2..closing[mathrandom(1,#closing)]..Answer	
		--print(Question,Answer)
	-- Return the word problem and the answer as variables
            
                Result=Question
		end	
		end	
	end
	return Result
end

function cm.CreateWordProblem(Num1, Symbol, Num2)
	-- Get the subjects table from the ReplicatedStorage
	local Subjects = game.ReplicatedStorage.Items.Food.Icons:GetChildren()

	-- Pick a random subject from the Subjects table
	local Subject = Subjects[mathrandom(1, #Subjects)].Name

	-- Define the opening phrases for the word problem
	local Opening = {
		"If I have ", 
		"If I possess ", 
		"So we have ", 
		"So starting with ",
		"Imagine I have ",
		"Suppose I own ",
		"Let's say we have ",
		"Given that I start with "
	}
	local Answer
	-- Define the phrases for each operator
	local AddConstruct = {
		". Then, I add ", 
		". Furthermore, I gain ", 
		". In addition to that I have ", 
		". We obtain ", 
		". Then we acquire ",
		". After that, I receive ",
		". Besides that, I find ",
		". We also get ",
		". Then we collect "
	}
	local SubtractConstruct = {
		". I lose ", 
		". Taking away ", 
		". Say we no longer have ", 
		". So some of them have disappeared that is ",
		". I give away ",
		". Removing ",
		". Suppose we use up ",
		". So a few of them are gone that is ",
		". I donate "
	}
	local MultiplyConstruct = {
		". Then, I multiply them by ", 
		". Furthermore, I duplicate them ", 
		". In addition to that I get them times ", 
		". We increase those groups by a factor of ", 
		". After that, I replicate them all by ",
		". Besides that, I boost them all by ",
		". We multiply them by ",
	}
	local DivideConstruct = {
		". Then, I divide them by ", 
		". Furthermore, I split them amongst ", 
		". In addition to that I share them among ", 
		". We separate them into groups of ", 
		". Then we distribute amongst ",
		". After that, I seperate them into groups of ",
		". Besides that, I divide them equally among ",
		". We split them into groups of ",
		". Then we allocate by "
	}

	-- Define the closing phrases for the word problem
	local ClosingPhrase = {
		" So, how many "..Subject.." do I have left?",
		" Then, how many "..Subject.." do we have?",
		" What's there that remains of the "..Subject.."?",
		" Now what do we have left of the "..Subject.."?",
		" So, what is the final count of the "..Subject.."?",
		" Then, what is the total number of the "..Subject.."?",
		" How many "..Subject.." are left in the end?",
		" What is the final amount of the "..Subject.." left?"}

	-- Pick a random phrase for the opening and the operator
	local OpeningPhrase = Opening[mathrandom(1, #Opening)]
	local OperatorPhrase = ""
	if Symbol == "+" then
		OperatorPhrase = AddConstruct[mathrandom(1, #AddConstruct)]
	elseif Symbol == "-" then
		OperatorPhrase = SubtractConstruct[mathrandom(1, #SubtractConstruct)]
	elseif Symbol == "*" then
		OperatorPhrase = MultiplyConstruct[mathrandom(1, #MultiplyConstruct)]
	elseif Symbol == "/" then
		OperatorPhrase = DivideConstruct[mathrandom(1, #DivideConstruct)]
	end


	-- Calculate the answer by applying the operator to the numbers
	local Answer = nil
	if Symbol == "+" then
		Answer = Num1 + Num2
	elseif Symbol == "-" then
		Answer = Num1 - Num2
	elseif Symbol == "*" then
		Answer = Num1 * Num2
	elseif Symbol == "/" then
		Answer = Num1 / Num2
	end

	return Opening[mathrandom(1,#Opening)]..Num1.." "..Subject..OperatorPhrase..Num2.."."..ClosingPhrase[mathrandom(1,#ClosingPhrase)],Answer
end

1 Like

Not sure I would ever find a practical use case scenario, but this is some wizard level stuff. Good work!

1 Like

Shouldn’t this be something like Evaluate? Solve implies that it can handle basic algebra (as in something like 2x-3=5, solve for x), which it does not.

I find the decision (?) to just treat ^ without an operand after it as ^2 to be kinda strange, it should probably just throw a warning since it was almost certainly not intended to be that way.

The module seems to take an abnormally large amount of time doing something whenever you enter a string with an undefined variable. It eventually throws a huge amount of warnings along with a script timeout error.

Not sure if this is intentional, but the expression 5*3) does not throw a warning, even though there’s a stray close parenthesis, and (5*3 throws a warning. Definitely an interesting module, though.

2 Likes

Thanks! Im making a game that allows the user to set up custom UIs and wanted the players to be able to use some basic math for the object properties so I made this. It basically emulates roblox’s basic math but with strings. I also added variables and exponents defaulting to 2 just for some extra options since i might wanna use the variables later on, though i’m not using them right now.

Thank you. :slight_smile: Yours looks nice too, though it follows a bit of a different model for the calculations.

  1. Actually Evaluate does sound like a better method name than solve, thank you.
  2. I was actually going to make all or the operators have a default but it was causing problems to I ended up just doing the exponents. I may change it later, we will see, but for now i am doing it to default to 0. also, I think i saw it default to 2 somewhere else as well, but i’m not sure where.
  3. can you show the string you inputed so i can debug it?
  4. I had a warning system in place before but it wasn’t very good so I scrapped it and replaced everything with warn() and I will hopefully be able to fix that soon, maybe next.

Also, I just fixed a bug with infinite string loops, that may be the issue you were having. I am gonna change Solve() and then save the fix in a minute.

I made one for Geometry as well I ought to look at your code and apply PEMDAS principles when decoding the calculation currently mine only solves for the first two numbers. If I could organize each pair of numbers in the calculation into an ordered array I can do a for loop for each component of the calculation and provide a worded step by step solution which would be soo cool.

A little insight to mine is that it has a bunch of sample phrases that describes the worded calculation method then this is used to decode the calculation and in practice it works very well it also works with the non worded arithmetic. It’s for an AI project.

Nice :slight_smile:

Adding PEMDAS to this was actually a pain lol.

I will try to go through and add some comments on the process later to explain the evaluation process of the code, if i remember.

sin(2) can show the issue I believe.

1 Like

thanks, I thought i had fixed that issue already, I will try to work on a fix asap.

edit: i think i had something stopping it before but accidentally removed it.

Thank you, the problem is now fixed. As of now, an unset variable will be ignored. This may be changed in the future once proper erroring is added.

This is very inspiring! I’ll be checking it out soon! as well as adding exponents and PEMDAS and do some long arithmetic with worded step by step descriptions. also here’s a standalone function that generates Geometry Questions of varying difficulty.

function cm.CreateGeometryQuestion(Num1,Num2,Shape)
	-- Get the shapes table from the ReplicatedStorage
	--local Shapes = game.ReplicatedStorage.Items.Shapes.Icons:GetChildren()

	-- Pick a random shape from the Shapes table
	--local Shape = Shapes[mathrandom(1, #Shapes)].Name

	-- Define the opening phrases for the word problem
	local Opening = {
		"If I have a ", 
		"If I possess a ", 
		"So we have a ", 
		"So starting with a ",
		"Imagine I have a ",
		"Suppose I own a ",
		"Let's say we have a ",
		"Given that I start with a "
	}

	-- Define the phrases for each shape
	local SquareConstruct = {
		"square with side length of ", 
		"square with edge width of ", 
		"square with each side measuring ", 
	}
	local RectangleConstruct = {
		"rectangle with length of ", 
		"and a width of ", 
	}
	local CircleConstruct = {
		"circle with radius of ", 
		" with diameter of ", 
	}
	local TriangleConstruct = {
		"triangle with base of ", 
		"and a height of ", 
	}

	-- Define the questions for each shape
	local SquareQuestion = {
		"What is the area of the square?",
		"What is the perimeter of the square?",
	}
	local RectangleQuestion = {
		"What is the area of the rectangle?",
		"What is the perimeter of the rectangle?",
	}
	local CircleQuestion = {
		"What is the area of the circle?",
		"What is the circumference of the circle?",
       -- "circumference"
	}
	local TriangleQuestion = {
		"What is the area of the triangle?",
		"What is the perimeter of the triangle?",
	}

	-- Define some random numbers for the dimensions
	--local Num1 = mathrandom(1, 10)
	--local Num2 = mathrandom(1, 10)

	-- Pick a random phrase for the opening and the shape
	local OpeningPhrase = Opening[mathrandom(1, #Opening)]
	local ShapePhrase = ""
	local QuestionPhrase = ""
	if Shape == "Square" then
		ShapePhrase = SquareConstruct[mathrandom(1, #SquareConstruct)] .. Num1 .. " units."
		QuestionPhrase = SquareQuestion[mathrandom(1, #SquareQuestion)]
	elseif Shape == "Rectangle" then
		ShapePhrase = RectangleConstruct[1]..Num1.." units and "..RectangleConstruct[2]..Num2.." units."
		QuestionPhrase = RectangleQuestion[mathrandom(1, #RectangleQuestion)]
	elseif Shape == "Circle" then
		ShapePhrase = CircleConstruct[mathrandom(1, 2)] .. Num1 .. " units."
		QuestionPhrase = CircleQuestion[mathrandom(1, #CircleQuestion)]
	elseif Shape == "Triangle" then
		ShapePhrase = TriangleConstruct[1] .. Num1 .. " units and " .. TriangleConstruct[2] .. Num2 .. " units."
		QuestionPhrase = TriangleQuestion[mathrandom(1, #TriangleQuestion)]
	end

	-- Calculate the answer by applying the formula to the numbers
	local Answer = nil
	if Shape == "Square" then
		if QuestionPhrase == SquareQuestion[1] then -- area
			Answer = Num1 * Num1
		elseif QuestionPhrase == SquareQuestion[2] then -- perimeter
			Answer = Num1 * 4
		end
	elseif Shape == "Rectangle" then
		if QuestionPhrase == RectangleQuestion[1] then -- area
			Answer = Num1 * Num2
		elseif QuestionPhrase == RectangleQuestion[2] then -- perimeter
			Answer = (Num1 + Num2) * 2
		end
	elseif Shape == "Circle" then
-- Define a function to calculate the circumference of a circle, given the radius
local function circumference(radius)
  -- Use the formula C = 2 * pi * r, where C is the circumference, pi is a constant, and r is the radius
  local C = 2 * math.pi * radius
  -- Return the result
  return C
end

		if QuestionPhrase == CircleQuestion[1] then -- area
			Answer = math.pi * Num1 * Num1 -- assuming radius
		elseif QuestionPhrase == CircleQuestion[2] then -- circumference
			Answer =  circumference(Num1)--math.pi * Num1 * 2 -- assuming radius
		end
	elseif Shape == "Triangle" then
		if QuestionPhrase == TriangleQuestion[1] then -- area
			Answer = (Num1 * Num2) / 2 -- assuming base and height
		elseif QuestionPhrase == TriangleQuestion[2] then -- perimeter
			Answer = nil -- cannot be calculated without knowing all sides or angles
		end
	end

	-- Format the word problem as a string and round the answer to two decimal places
	local WordProblem = OpeningPhrase .. ShapePhrase .. " " .. QuestionPhrase
	if Answer then
		Answer = math.floor(Answer * 100 + 0.5) / 100 -- round to two decimal places
	else
		Answer = "Cannot be calculated"
	end

	-- Return the word problem and the answer as variables
	return WordProblem, Answer
end
--Decode the Geometry question
function cm.InterpretGeometryQuestion(str)
	local Answer=nil
	local Shape
 local Opening = {
		"If I have a ", 
		"If I possess a ", 
		"So we have a ", 
		"So starting with a ",
		"Imagine I have a ",
		"Suppose I own a ",
		"Let's say we have a ",
		"Given that I start with a "
	}
local RectangleConstruct = {
		"with length of ", 
		"width of ", 
	}	
	local open=mathquery(str,Opening,false,true,false)
	local square,rectangle,circle,triangle=nil,nil,nil,nil
	local qsquare,qrectangle,qtriangle,qcircle
	 square=mathquery(str,SquareConstruct,true,true,false)		
	if not square then
	rectangle=mathquery(str,RectangleConstruct,true,true,false)	
		if not rectangle then	
        print("Circle")
		circle=mathquery(str,CircleConstruct,true,true,false)	
			if not circle then	
	triangle=mathquery(str,TriangleConstruct,false,true,false)	
			end
		end 
	end

	if square then qsquare=mathquery(str,SquareQuestion,false,true,false)	
	elseif rectangle then	qrectangle=mathquery(str,RectangleQuestion,false,true,false)	
	elseif circle then	
		qcircle=mathquery(str,CircleQuestion,false,true,false)	
	elseif triangle then	
		qtriangle=mathquery(str,TriangleQuestion,false,true,false)
	end	
if square or rectangle or circle or triangle then	
	local score=0
	local QuestionPhrase = nil
	if open~=nil then score=score+1 end
	if square~=nil and qsquare~=nil then
		Shape="Square"
		QuestionPhrase = qsquare
	elseif rectangle~=nil and qrectangle~=nil  then
		Shape="Rectangle"
		QuestionPhrase = qrectangle
	elseif circle~=nil and qcircle~=nil then
		Shape="Circle"
		QuestionPhrase = qcircle
	elseif triangle~=nil and qtriangle~=nil then
		Shape="Triangle"
		 QuestionPhrase = qtriangle
	end	
local function SplitQuery(query, num1)
		local parts = {}
		-- Find index of first number in query
		local num1Index = string.find(query, tostring(num1))
		-- Get substring before number 
		parts[1] = string.sub(query, 1, num1Index - 1)
		-- Get substring after number
		parts[2] = string.sub(query, num1Index + 1)
		return parts[1], parts[2]
	end
	
	local function GetNumber(query)
		local Num1, Num2 = nil, nil
		local identifers={"length","width","diameter","radius"}
		local identifier=nil
		local part1,part2=nil,nil
		for word in string.gmatch(query, "%w+") do
			if tonumber(word) then 
				if not Num1 then
					Num1 = tonumber(word) 
                    print(Num1)
					part1,part2=SplitQuery(query,Num1)
					identifier=mathquery(part1,identifers,false,true,false)
				else
				if identifier==nil then
						identifier=mathquery(part2,identifers,false,true,false)
					end
                    
					Num2 = tonumber(word)
                    print(Num2)
					break -- stop after 2 numbers found
				end
			end
		end
		return Num1,Num2,identifier
	end
	local part1
	local	Num1,Num2,identfier=GetNumber(str)
    
	-- Pick a random phrase for the opening and the shape
	local OpeningPhrase=open
		if OpeningPhrase==nil then	OpeningPhrase= Opening[mathrandom(1, #Opening)] end
	local ShapePhrase = ""	
	if Shape == "Square" then
		ShapePhrase = square .. Num1 .. " units."
	elseif Shape == "Rectangle" then
		part1=mathquery(identfier,RectangleConstruct,false,true,false)
	local part2	
		if part1==RectangleConstruct[1] then
			part2=RectangleConstruct[2]
		else part2=RectangleConstruct[1]	
		end
	   
		ShapePhrase = "rectangle "--rectangle
                            ..Num1.." units and "
                                            ..part2
                                                   ..Num2.." units."
	
	elseif Shape == "Circle" then
			part1=mathquery(identfier,CircleConstruct,false,true,false)
		if part1==nil then
        part1=circle
        end
		ShapePhrase = part1 .. Num1 .. " units."

	elseif Shape == "Triangle" then
		
		ShapePhrase = TriangleConstruct[1] .. Num1 .. " units and " .. TriangleConstruct[2] .. Num2 .. " units."
	end
	-- Calculate the answer by applying the formula to the numbers
	local Answer = nil
	if Shape == "Square" then
		if QuestionPhrase == SquareQuestion[1] then -- area
			Answer = Num1 * Num1
		elseif QuestionPhrase == SquareQuestion[2] then -- perimeter
			Answer = Num1 * 4
		end
	elseif Shape == "Rectangle" then
		if QuestionPhrase == RectangleQuestion[1] then -- area
			Answer = Num1 * Num2
		elseif QuestionPhrase == RectangleQuestion[2] then -- perimeter
			Answer = (Num1 + Num2) * 2
		end
	elseif Shape == "Circle" then
		if QuestionPhrase == CircleQuestion[1] then -- area
			Answer = math.pi * Num1 * Num1 -- assuming radius
		elseif QuestionPhrase == CircleQuestion[2] then -- circumference
			Answer = math.pi * Num1 * 2 -- assuming diameter
		end
	elseif Shape == "Triangle" then
		if QuestionPhrase == TriangleQuestion[1] then -- area
			Answer = (Num1 * Num2) / 2 -- assuming base and height
		elseif QuestionPhrase == TriangleQuestion[2] then -- perimeter
			Answer = nil -- cannot be calculated without knowing all sides or angles
		end
		end
		
	if Answer then
		Answer = math.floor(Answer * 100 + 0.5) / 100 -- round to two decimal places
		Answer	= OpeningPhrase .. ShapePhrase .. " " .. QuestionPhrase.." The answer is "..Answer.."."
	else
		Answer = nil
	end
	-- Return the word problem and the answer as variables
		return  Answer
	else return nil	
	end	
end

function mathquery(query,group)
   
    local words=cm.splitString(query,true)
    for i,v in group do
    
        if  string.match(query,v) then
           -- print(v)
            return v
        end
    end
local importance={"circle","square","rectangle","triangle"}
local geomclass={"area","circumference","perimeter","radius","height","diameter","length","width","base"}  
--  for t,o in words do 
--print(group)
       for y,p in group do

 local gwords=cm.splitString(p,true)
 for i,v in importance do 
local imp=false 
  for q,w in geomclass do
 local geom=false
    
   for t,o in words do 
   for tt,oo in gwords do 
 --words=cm.splitString(query,true)
    if string.match(o,w)  and  string.match(oo,w) then
    geom=true
    end
    if  string.match(o,v) and  string.match(oo,v) then
    imp=true
    end
    
        if geom and imp then
           print(v)
            return p
        end
    end
end
end
end
end
1 Like