Last active
October 17, 2021 10:42
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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