Please note, this blog entry is from a previous course. You might want to check out the current one.
Add a String property to your CalculatorBrain called description which returns a description of the sequence of operands and operations that led to the value returned by result. “=“ should never appear in this description, nor should “…”.
Let’s add a public string property called description, and compute it – like the result property – from another new private string property – the description accumulator. When no operation is pending, we can just return the accumulator otherwise we will have to do a little bit more:
var description: String { get { if pending == nil { return descriptionAccumulator } else { return ... } } } private var descriptionAccumulator = "0"
Setting a new operator should also set the description accumulator. The format “%g” makes sure to trim trailing zeros for integer values:
func setOperand(operand: Double) { ... descriptionAccumulator = String(format:"%g", operand) }
For constants just use its symbol for the description accumulator:
func performOperation(symbol: String) { ... case .Constant(let value): ... descriptionAccumulator = symbol ... }
Unary operations might need a different description depending on the kind of operation, thus we expand its declaration by a new function using a string (the current operand) as parameter and returning a string with the description:
private enum Operation { ... case UnaryOperation((Double) -> Double, (String) -> String) ... }
Now each unary operation can provide a suitable formatted description function:
private var operations: Dictionary<String,Operation> = [ ... "±" : Operation.UnaryOperation({ -$0 }, { "-(" + $0 + ")"}), "√" : Operation.UnaryOperation(sqrt, { "√(" + $0 + ")"}), "x²" : Operation.UnaryOperation({ pow($0, 2) }, { "(" + $0 + ")²"}), "x³" : Operation.UnaryOperation({ pow($0, 3) }, { "(" + $0 + ")³"}), "x⁻¹" : Operation.UnaryOperation({ 1 / $0 }, { "(" + $0 + ")⁻¹"}), "sin" : Operation.UnaryOperation(sin, { "sin(" + $0 + ")"}), "cos" : Operation.UnaryOperation(cos, { "cos(" + $0 + ")"}), "tan" : Operation.UnaryOperation(tan, { "tan(" + $0 + ")"}), "sinh" : Operation.UnaryOperation(sinh, { "sinh(" + $0 + ")"}), "cosh" : Operation.UnaryOperation(cosh, { "cosh(" + $0 + ")"}), "tanh" : Operation.UnaryOperation(tanh, { "tanh(" + $0 + ")"}), "ln" : Operation.UnaryOperation(log, { "ln(" + $0 + ")"}), "log" : Operation.UnaryOperation(log10, { "log(" + $0 + ")"}), "eˣ" : Operation.UnaryOperation(exp, { "e^(" + $0 + ")"}), "10ˣ" : Operation.UnaryOperation({ pow(10, $0) }, { "10^(" + $0 + ")"}), "x!" : Operation.UnaryOperation(factorial, { "(" + $0 + ")!"}), ... ]
… and use this methods to set the description accumulator:
func performOperation(symbol: String) { ... case .UnaryOperation(let function, let descriptionFunction): ... descriptionAccumulator = descriptionFunction(descriptionAccumulator) ... }
Same with binary operation – only difference – we need two operators:
private enum Operation { ... case BinaryOperation((Double, Double) -> Double, (String, String) -> String) ... } private var operations: Dictionary<String,Operation> = [ ... "×" : Operation.BinaryOperation(*, { $0 + " × " + $1 }), "÷" : Operation.BinaryOperation(/, { $0 + " ÷ " + $1 }), "+" : Operation.BinaryOperation(+, { $0 + " + " + $1 }), "-" : Operation.BinaryOperation(-, { $0 + " - " + $1 }), "xʸ" : Operation.BinaryOperation(pow, { $0 + " ^ " + $1 }), ... ]
In addition store the description information (the function and the current operand) in the pending-information structure, to use it when both operands are known:
private struct PendingBinaryOperationInfo { ... var descriptionFunction: (String, String) -> String var descriptionOperand: String } func performOperation(symbol: String) { ... case .BinaryOperation(let function, let descriptionFunction): ... pending = PendingBinaryOperationInfo(binaryFunction: function, firstOperand: accumulator, descriptionFunction: descriptionFunction, descriptionOperand: descriptionAccumulator) ... } private func executePendingBinaryOperation() { ... descriptionAccumulator = pending!.descriptionFunction(pending!.descriptionOperand, descriptionAccumulator) ... }
Because the description should also show pending operation, adjust the description getter to do so:
var description: String { ... } else { return pending!.descriptionFunction(pending!.descriptionOperand, pending!.descriptionOperand != descriptionAccumulator ? descriptionAccumulator : "") } } }
To get a correct descriptions we need to add the operator precedence – due to laziness only for binary operations:
private enum Operation { ... case BinaryOperation((Double, Double) -> Double, (String, String) -> String, Int) ... } private var operations: Dictionary<String,Operation> = [ ... "×" : Operation.BinaryOperation(*, { $0 + " × " + $1 }, 1), "÷" : Operation.BinaryOperation(/, { $0 + " ÷ " + $1 }, 1), "+" : Operation.BinaryOperation(+, { $0 + " + " + $1 }, 0), "-" : Operation.BinaryOperation(-, { $0 + " - " + $1 }, 0), "xʸ" : Operation.BinaryOperation(pow, { $0 + " ^ " + $1 }, 2), ... ]
A helper variable holds the precedence of the previous operation:
private var currentPrecedence = Int.max
When the precedence of a new operation is higher than the previous one, add brackets:
func performOperation(symbol: String) { ... case .BinaryOperation(let function, let descriptionFunction, let precedence): ... if currentPrecedence < precedence { descriptionAccumulator = "(" + descriptionAccumulator + ")" } currentPrecedence = precedence ... }
Every time the description gets set and the there is no pending operation, reset the current precedence:
private var descriptionAccumulator = "0" { didSet { if pending == nil { currentPrecedence = Int.max } } }
… and because we have now output of the description yet, test it using unit tests:
func testDescription() { // a. touching 7 + would show “7 + ...” (with 7 still in the display) let brain = CalculatorBrain() brain.setOperand(7) brain.performOperation("+") XCTAssertEqual(brain.description, "7 + ") XCTAssertEqual(brain.result, 7.0) // b. 7 + 9 would show “7 + ...” (9 in the display) //brain.setOperand(9) // entered but not pushed to model XCTAssertEqual(brain.description, "7 + ") XCTAssertEqual(brain.result, 7.0) // c. 7 + 9 = would show “7 + 9 =” (16 in the display) brain.setOperand(9) brain.performOperation("=") XCTAssertEqual(brain.description, "7 + 9") XCTAssertEqual(brain.result, 16.0) // d. 7 + 9 = √ would show “√(7 + 9) =” (4 in the display) brain.performOperation("√") XCTAssertEqual(brain.description, "√(7 + 9)") XCTAssertEqual(brain.result, 4.0) // e. 7 + 9 √ would show “7 + √(9) ...” (3 in the display) brain.setOperand(7) brain.performOperation("+") brain.setOperand(9) brain.performOperation("√") XCTAssertEqual(brain.description, "7 + √(9)") XCTAssertEqual(brain.result, 3.0) // f. 7 + 9 √ = would show “7 + √(9) =“ (10 in the display) brain.performOperation("=") XCTAssertEqual(brain.description, "7 + √(9)") XCTAssertEqual(brain.result, 10.0) // g. 7 + 9 = + 6 + 3 = would show “7 + 9 + 6 + 3 =” (25 in the display) brain.setOperand(7) brain.performOperation("+") brain.setOperand(9) brain.performOperation("=") brain.performOperation("+") brain.setOperand(6) brain.performOperation("+") brain.setOperand(3) brain.performOperation("=") XCTAssertEqual(brain.description, "7 + 9 + 6 + 3") XCTAssertEqual(brain.result, 25.0) // h. 7 + 9 = √ 6 + 3 = would show “6 + 3 =” (9 in the display) brain.setOperand(7) brain.performOperation("+") brain.setOperand(9) brain.performOperation("=") brain.performOperation("√") brain.setOperand(6) brain.performOperation("+") brain.setOperand(3) brain.performOperation("=") XCTAssertEqual(brain.description, "6 + 3") XCTAssertEqual(brain.result, 9.0) // i. 5 + 6 = 7 3 would show “5 + 6 =” (73 in the display) brain.setOperand(5) brain.performOperation("+") brain.setOperand(6) brain.performOperation("=") //brain.setOperand(73) // entered but not pushed to model XCTAssertEqual(brain.description, "5 + 6") XCTAssertEqual(brain.result, 11.0) // j. 7 + = would show “7 + 7 =” (14 in the display) brain.setOperand(7) brain.performOperation("+") brain.performOperation("=") XCTAssertEqual(brain.description, "7 + 7") XCTAssertEqual(brain.result, 14.0) // k. 4 × π = would show “4 × π =“ (12.5663706143592 in the display) brain.setOperand(4) brain.performOperation("×") brain.performOperation("π") brain.performOperation("=") XCTAssertEqual(brain.description, "4 × π") XCTAssertTrue(abs(brain.result - 12.5663706143592) < 0.001) // m. 4 + 5 × 3 = could also show “(4 + 5) × 3 =” if you prefer (27 in the display) brain.setOperand(4) brain.performOperation("+") brain.setOperand(5) brain.performOperation("×") brain.setOperand(3) brain.performOperation("=") XCTAssertEqual(brain.description, "(4 + 5) × 3") XCTAssertEqual(brain.result, 27.0) }
The complete code for task #5 is available on GitHub.
Thank you so much for this, I was stuck !
It’s actually brilliant, I wonder why I didn’t think of mimicking the accumulator’s behaviour for description…
Thanks for this. I just wanted to confirm I wasn’t crazy as this task was about 20x more work than all the other tasks combined. I feel like there should have been some hint that this task is essentially the entire assignment. Thanks for the guidance!
Hi, thank you so much for the walkthrough! I have a question about the description variable.
Why did you return pending!.descriptionFunction(pending!.descriptionOperand, pending!.descriptionOperand != descriptionAccumulator ? descriptionAccumulator : “”), instead of just pending!.descriptionFunction(pending!.descriptionOperand, “”)? My thoughts is that if pending is not nil, the second operand for the binary function will always be empty.
Aha I have figured out why 🙂
… so there was a reason after all!
Honestly, I have no idea (which happens with undocumented code after some time …)! If your version works, perfect!
Thank you very much, great solution!
However I believe, we need to add this line of code
pending!.descriptionOperand = descriptionAccumulator
inside of this condition
if currentPrecedence < precedence {
descriptionAccumulator = "(" + descriptionAccumulator + ")"
// should be inserted here
}
so, then everything should be fine and all conditions will be met.