Skip to content

Instantly share code, notes, and snippets.

@ramzesenok
Last active October 17, 2021 10:42
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ramzesenok/6c92bda6bfdf8cbde98224a3d75301a6 to your computer and use it in GitHub Desktop.
Save ramzesenok/6c92bda6bfdf8cbde98224a3d75301a6 to your computer and use it in GitHub Desktop.
Here's my attempt to create a calculator, that takes a string as an input, parses it and calculates the result. Key difficulties: taking the operator's precedence and parenthesis in account. Operates only on integer values. Solved using an AST
// MARK: - Data Structures
enum Operation: String {
case add = "+"
case subtract = "-"
case multiply = "*"
case divide = "/"
var action: (Int, Int) -> Int {
switch self {
case .add:
return (+)
case .subtract:
return (-)
case .multiply:
return (*)
case .divide:
return (/)
}
}
}
enum BracketType: String {
case opening = "("
case closing = ")"
}
// Tokens that represent raw incoming data
enum ParsingToken: Equatable {
case operation(Operation)
case number(Int)
case bracket(BracketType)
init?(rawValue: String) {
if let operation = Operation(rawValue: rawValue) {
self = .operation(operation)
} else if let bracketType = BracketType(rawValue: rawValue) {
self = .bracket(bracketType)
} else if let number = Int(rawValue) {
self = .number(number)
} else {
return nil
}
}
var rawValue: String {
switch self {
case .number(let number):
return "\(number)"
case .operation(let operation):
return operation.rawValue
case .bracket(let bracketType):
return bracketType.rawValue
}
}
}
// Tokens that represent incoming data in an operatable format
indirect enum ComputingToken {
case number(Int)
case operation(Operation)
case expression([ComputingToken])
var isOperation: Bool {
if case .operation = self {
return true
}
return false
}
var isExpression: Bool {
if case .expression = self {
return true
}
return false
}
}
// Binary tree that represents the computable structure of expression
indirect enum AST {
case node(val: Operation, left: AST, right: AST)
case leaf(val: Int)
func compute() -> Int {
switch self {
case .node(let operation, let left, let right):
return operation.action(left.compute(), right.compute())
case .leaf(let val):
return val
}
}
}
// MARK: - Parsing
func clean(tokens: inout [ParsingToken]) {
if tokens.filter({ $0 == .bracket(.opening) }).count != tokens.filter({ $0 == .bracket(.closing) }).count {
fatalError("Amount of opening brackets should equal to amount of closing brackets")
}
var i = 0
while i < tokens.count - 1 {
if case .number(let num1) = tokens[i] {
while i != tokens.count - 1, case .number(let num2) = tokens[i + 1] {
tokens[i] = .number(Int("\(num1)\(num2)")!)
tokens.remove(at: i + 1)
}
} else if case .bracket(.opening) = tokens[i], case .bracket(.closing) = tokens[i + 1] {
tokens.remove(at: i)
tokens.remove(at: i)
continue
} else if case .operation = tokens[i], case .operation = tokens[i + 1] {
fatalError("Expression should not contain two operations in a row")
}
i += 1
}
}
func createComputationTokens(from parsingTokens: [ParsingToken]) -> ComputingToken {
var computingTokens = [ComputingToken]()
var i = 0
while i < parsingTokens.count {
switch parsingTokens[i] {
case .number(let number):
computingTokens.append(.number(number))
case .operation(let operation):
computingTokens.append(.operation(operation))
case .bracket(let type):
switch type {
case .opening:
var innerOpeningBracketsCount = 0
var j = i + 1
while j < parsingTokens.count {
j += 1
if case .bracket(let type) = parsingTokens[j - 1] {
guard type == .closing else {
innerOpeningBracketsCount += 1
continue
}
if innerOpeningBracketsCount == 0 {
break // Found the last bracket of the current scope
}
innerOpeningBracketsCount -= 1
if innerOpeningBracketsCount < 0 {
fatalError("The brackets are in wrong order")
}
}
}
let currentScopeComputationalTokens = createComputationTokens(from: Array(parsingTokens[i+1..<j]))
i = j - 1
computingTokens.append(currentScopeComputationalTokens)
case .closing:
break
}
}
i += 1
}
return .expression(computingTokens)
}
func divideToBinaryRelations(_ token: ComputingToken) -> ComputingToken {
guard case .expression(var tokens) = token else {
fatalError("Cannot divide to binary relation a non-expression token")
}
if tokens.isBinaryRelation {
// Make sure inner expressions are binary too
for i in 0..<tokens.count where tokens[i].isExpression {
tokens[i] = divideToBinaryRelations(tokens[i])
}
return .expression(tokens)
}
func firstIndex(of operations: Operation...) -> Int? {
tokens.firstIndex(where: {
if case .operation(let operation) = $0 {
return operations.contains(operation)
}
return false
})
}
// Select first prioritized operation
var operationToLeaveIdx = firstIndex(of: .multiply, .divide) ?? firstIndex(of: .add, .subtract)
guard let idx = operationToLeaveIdx else {
fatalError("No operations were found")
}
guard idx > 0 && idx < tokens.count - 1 else {
fatalError("Not enough values to compute")
}
// Merge operation with the left and right neigbour expressions into a separate expression and remove the redundant expressions
tokens[idx - 1] = .expression([tokens[idx - 1], tokens[idx], tokens[idx + 1]])
tokens.remove(at: idx)
tokens.remove(at: idx)
return divideToBinaryRelations(.expression(tokens))
}
extension Array where Element == ComputingToken {
var isBinaryRelation: Bool {
self.count == 3 && self.filter(\.isOperation).count == 1 && self[1].isOperation
}
}
func compoundAST(from token: ComputingToken) -> AST {
guard case .expression(var tokens) = token else {
fatalError("Cannot compound a tree from a non-expression token")
}
guard tokens.isBinaryRelation, case .operation(let operation) = tokens[1] else {
fatalError("Cannot compound a tree from non-binary expression")
}
let left = tokens[0]
let right = tokens[2]
switch (left, right) {
case (.number(let leftNum), .number(let rightNum)):
return .node(val: operation, left: .leaf(val: leftNum), right: .leaf(val: rightNum))
case (.number(let leftNum), _):
return .node(val: operation, left: .leaf(val: leftNum), right: compoundAST(from: right))
case (_, .number(let rightNum)):
return .node(val: operation, left: compoundAST(from: left), right: .leaf(val: rightNum))
default:
return .node(val: operation, left: compoundAST(from: left), right: compoundAST(from: right))
}
}
// MARK: - Assembling
func calc(_ str: String) -> Int {
var rawTokens = str.map(String.init).flatMap(ParsingToken.init)
clean(tokens: &rawTokens)
let computationTokens = createComputationTokens(from: rawTokens)
let binaryRelatedExpressions = divideToBinaryRelations(computationTokens)
let tree = compoundAST(from: binaryRelatedExpressions)
return tree.compute()
}
calc("(5 + 3) / 4 + (3 * (6 - 2) + (1 * 2)) * 4") // 58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment