cs193p – Assignment #1 Task #5

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.

FacebooktwitterredditpinterestlinkedintumblrmailFacebooktwitterredditpinterestlinkedintumblrmail

7 thoughts on “cs193p – Assignment #1 Task #5”

  1. 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…

  2. 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!

  3. 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.

  4. 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.

Leave a Reply

Your email address will not be published.