Задание 2. Решение — продолжение (обязательные пункты 5-8) Swift 1.2 и Swift 2.0

Текст Домашнего задания на английском языке доступен на  iTunes в пункте “Developing iOS 8 app: Programming: Project 2″.  Текст Домашнего задания на русском  языке доступен на

Задание 2 iOS 8_new.pdf

Результаты выполнения задания можно обсуждать на форуме Swift[ru] . Ваше решение может быть лучше и интереснее. Выкладывайте его в Github, давайте ссылку на Dropbox или используйте другие системы управления версиями. Xcode  работает напрямую с Github.

Начало решения Задания 2 находится здесь.

Пункт 5 обязательный

Добавьте способности вашему CalculatorBrain  помещать “переменные”  в его  внутренний стэк. Сделайте это путем реализации следующего API в вашем CalculatorBrain …

func pushOperand (symbol:String) -> Double?

var variableValues:Dictionary<String, Double>

Это должно делать точно то, что говорят названия : первая функция “толкает” “переменную” во внутренний стэк CalculatorBrain (например, pushOperand (“x”)  будет помещать переменную с именем х), а вторая переменная ввиде словаря позволит пользователю CalculatorBrain установить любое значение для любой переменной (например, brain.variableValues [“x”] = 35.0) . pushOperand должен вернуть результат evaluate() после помещения переменной в стэк (как pushOperand делает в других случаях).

В нашей Модели CalculatorBrain добавляем в перечисление enum вариант для переменных Variable:

Screen Shot 2015-03-26 at 2.57.03 PM
Функция func pushOperand (symbol:String) -> Double? для переменных будет выглядеть точно также, как и уже существующая, только вместо операнда мы будем добавлять в стэк переменную

Screen Shot 2015-03-26 at 3.05.37 PM
Создадим словарь для хранения значений переменных

Screen Shot 2015-03-26 at 3.11.59 PM

Использование значений переменных в функции evaluate — это работа следующего пункта 6 Задания 2, так что пока вернем  nil :

Screen Shot 2015-03-26 at 3.17.34 PM
Внесенные нами изменения в Модель CalculatorBrain являются лишь подготовкой к использованию переменных, поэтому никаких изменений в работе мы не сможем пока увидеть.
Код можно посмотреть на Github.

Пункт 6 обязательный

Функция evaluate() должна использовать значение переменной (из словаря variableValues) всякий раз, когда “переменная” рассчитывается или вернуть nil, если при рассчете не находится подходящего значения для этой переменной.

Это означает, что мы должны вернуть значение из словаря  variableValues
Screen Shot 2015-03-26 at 3.30.45 PM
Мы можем проверить как работают переменные с помощью простого теста
Screen Shot 2015-03-26 at 3.34.28 PM
Код можно посмотреть на Github.

Пункт 7 обязательный

Реализуйте новую read-only (только get, нет set) переменную var для CalculatorBrain, чтобы описать содержимое “мозгов” как String

var description: String

  • Унарные операции должны быть показаны с использованием нотации “функция”. Например, ввод 10 cos должен отображаться в description как cos (10)
  • Бинарные операции должны быть показаны с использованием инфиксной (infix) нотации. Например, ввод 3 ↲ 5 — должен отображаться в description как   3 — 5. Убедитесь, что вы получили правильный порядок чисел.
  • Весь другой контент стэка (например, операнды, переменные, константы как π и  так далее) должны быть показаны “как есть”. Например 23.5 ⇒ 23.5π  ⇒ π (не 3.1415!), “переменная” x ⇒  x (не значение) и т.д.
  • Любые комбинации элементов стэка должны отображаться правильно. Например:      10 √ 3 +    ⇒ √ (10) +  3

3  ↲ 5 +  √  ⇒ √ ( 3 +  5)

3  ↲ 5 √  + √  6 ÷  ⇒√( 3 + √ (5))   ÷  6

  • Если есть пропущенные операнды, заменяем их на ?,  например,

3 ↲  + ⇒ ? +  3

  • Если в стэке несколько законченных выражений, разделяем их запятыми. Например, 3 ↲ 5 + √ π cos ⇒ (3+5), cos(π). Выражения должны располагаться в историческом порядке с наиболее старыми вначале строки и с недавно push / perform в конце.
  • Ваш description  должен правильно представлять математическое выражение. Например,  3  ↲ 5 ↲ 4  + × не должно быть  3 × 5 +  4 оно должно быть  3 × (5 +  4). Другими словами, вы кое где должны добавить круглые скобки вокруг бинарных операций. При этом постарайтесь минимизировать количество круглых скобок насколько это возможно (но результат должен быть математически корректным). Посмотрите дополнительное задание, если вы действительно хотите сделать это правильно.

По аналогии с парой методов для  evaluate

private func evaluate(ops: [Op]) -> (result: Double?, remainingOps: [Op])
func evaluate() -> Double?

создадим для description вспомогательный private метод и non-private переменную

private func description(ops: [Op]) -> (result: String?, remainingOps: [Op])
var description: String

Вспомогательный метод private func description начинает с последнего элемента стэка и работает рекурсивно. Для операндов мы возвращаем значение, отформатированное уже известным нам специфическим numberFormatter (). Для операций-констант – в нашем случае π – возвращаем символ константы symbol. Для унарной операции возвращаем символ константы symbol с операндом, окруженном круглыми скобками. Для бинарной операции возвращаем операнды, разделенные символом операции symbol. Кроме того, мы заключаем в круглые скобки первый операнд, если оставшийся стэк состоит только из одного или двух элементов. Для переменной возвращаем ее symbol. Если стэк не рассчитался, возвращаем знак ? вопроса.

Screen Shot 2015-08-23 at 10.17.56 PM
Для представления операнда использован класс NumberFormatter для форматирования десятичных чисел и разделяемая переменная (singleton) formatter.
Screen Shot 2015-08-23 at 10.28.48 PM
Если в стэке несколько законченных выражений, их нужно разделить запятыми.
В Задании 2 дана подсказка № 8:
“Если в стэке несколько законченных выражений, разделяем их запятыми”. Эту часть для description лучше реализовать  внутри кода для  var description ( а не в его рекурсивном вспомогательнои методе).

Здесь есть два варианта.
Первый более простой использует конструкцию do…while  действительно в get{} переменной  var description
Screen Shot 2015-03-27 at 5.05.04 PM
Второй вариант связан с созданием еще одной вспомогательной более высокого ранга рекурентной функции descParts, которая будет выделять и разделять запятой законченные выражения, вычисленные с помощью рекурентной функции description 
Screen Shot 2015-03-27 at 5.12.31 PM
и  переменной  var description1, чей get{} абсолютная калька с evaluate()
Screen Shot 2015-03-27 at 5.33.14 PM
В нашем классе ViewController в set {} вычисляемой переменной displaValue, в которой устанавливается значение результата вычислений, мы метку history можем наполнить другим содержанием, просто заменив результат вызова brain.displayStack() на brain.description или brain.description1.

Screen Shot 2015-08-24 at 6.38.10 AM

Я оставила c обучающей целью в коде как description, так и description1.

Для тестирования необходимо разместить тесты в файле CalculatorBrainTests и вместо запуска приложения кнопкой Run, нужно использовать Test

Screen Shot 2015-08-24 at 8.57.18 AM

Для проверки новых методов создадим ряд тестовых случаев в файле CalculatorBrainTests:

[objc]
class CalculatorBrainTests: XCTestCase {
private var brain = CalculatorBrain()

func testPushOperandVariable() {
XCTAssertNil(brain.pushOperand("x"))
brain.variableValues = ["x": 5.2]
XCTAssertEqual(5.2, brain.pushOperand("x")!)
XCTAssertEqual(10.4, brain.performOperation("+")!)
}

func testDescription() {
// cos(10)
brain = CalculatorBrain()
XCTAssertEqual(brain.pushOperand(10)!, 10)
XCTAssertTrue(brain.performOperation("cos")! — 0.839 < 0.1)
XCTAssertEqual(brain.displayStack(), "10.0 cos")

// 3 — 5
brain = CalculatorBrain()
XCTAssertEqual(brain.pushOperand(3)!, 3)
XCTAssertEqual(brain.pushOperand(5)!, 5)
XCTAssertEqual(brain.performOperation("−")!, -2)
XCTAssertEqual(brain.description, "3 − 5")

// 23.5
brain = CalculatorBrain()
XCTAssertEqual(brain.pushOperand(23.5)!, 23.5)
XCTAssertEqual(brain.description, "23.5")

// π
brain = CalculatorBrain()
XCTAssertEqual(brain.performOperation("π")!, M_PI)
XCTAssertEqual(brain.description, "π")

// x
brain = CalculatorBrain()
XCTAssertNil(brain.pushOperand("x"))
XCTAssertEqual(brain.description, "x")

// √(10) + 3
brain = CalculatorBrain()
XCTAssertEqual(brain.pushOperand(10)!, 10)
XCTAssertTrue(brain.performOperation("√")! — 3.162 < 0.1)
XCTAssertEqual(brain.pushOperand(3)!, 3)
XCTAssertTrue(brain.performOperation("+")! — 6.162 < 0.1)
XCTAssertEqual(brain.description, "√(10) + 3")

// √(3 + 5)
brain = CalculatorBrain()
XCTAssertEqual(brain.pushOperand(3)!, 3)
XCTAssertEqual(brain.pushOperand(5)!, 5)
XCTAssertEqual(brain.performOperation("+")!, 8)
XCTAssertTrue(brain.performOperation("√")! — 2.828 < 0.1)
XCTAssertEqual(brain.description, "√(3 + 5)")

// 3 + (5 + 4)
brain = CalculatorBrain()
XCTAssertEqual(brain.pushOperand(3)!, 3)
XCTAssertEqual(brain.pushOperand(5)!, 5)
XCTAssertEqual(brain.pushOperand(4)!, 4)
XCTAssertEqual(brain.performOperation("+")!, 9)
XCTAssertEqual(brain.performOperation("+")!, 12)
XCTAssertEqual(brain.description, "3 + (5 + 4)")

// √(3 + √(5)) ÷ 6
brain = CalculatorBrain()
XCTAssertEqual(brain.pushOperand(3)!, 3)
XCTAssertEqual(brain.pushOperand(5)!, 5)
XCTAssertTrue(brain.performOperation("√")! — 2.236 < 0.1)
XCTAssertTrue(brain.performOperation("+")! — 5.236 < 0.1)
XCTAssertTrue(brain.performOperation("√")! — 2.288 < 0.1)
XCTAssertEqual(brain.pushOperand(6)!, 6)
XCTAssertTrue(brain.performOperation("÷")! — 0.381 < 0.1)
XCTAssertEqual(brain.description, "√(3 + √(5)) ÷ 6")

// ? + 3
brain = CalculatorBrain()
XCTAssertEqual(brain.pushOperand(3)!, 3)
XCTAssertNil(brain.performOperation("+"))
XCTAssertEqual(brain.description, "? + 3")

// √(3 + 5), cos(π)
brain = CalculatorBrain()
XCTAssertEqual(brain.pushOperand(3)!, 3)
XCTAssertEqual(brain.pushOperand(5)!, 5)
XCTAssertEqual(brain.performOperation("+")!, 8)
XCTAssertTrue(brain.performOperation("√")! — 2.828 < 0.1)
XCTAssertEqual(brain.performOperation("π")!, M_PI)
XCTAssertEqual(brain.performOperation("cos")!, -1)
XCTAssertEqual(brain.description, "√(3 + 5), cos(π)")

// 3 * (5 + 4)
brain = CalculatorBrain()
XCTAssertEqual(brain.pushOperand(3)!, 3)
XCTAssertEqual(brain.pushOperand(5)!, 5)
XCTAssertEqual(brain.pushOperand(4)!, 4)
XCTAssertEqual(brain.performOperation("+")!, 9)
XCTAssertEqual(brain.performOperation("×")!, 27)
XCTAssertEqual(brain.description, "3 × (5 + 4)")
}
}
[/objc]

Код можно скачать на Github.

Пункт 8 обязательный

Модифицируйте метку UILabel, которую вы добавили на прошлой неделе, чтобы она показывала description  вашего  CalculatorBrain. В конце разместите знак “=” и разместите метку UILabel так, чтобы дисплей выглядел как будто бы он является результатом этого “=” . Это знак “=” был дополнительным пунктом задания на прошлой неделе, но сейчас это обязательный пункт.

Этот пункт выполнен нами в Задании 1 как дополнительный. Напомним, что мы добавляем знак «=» при нажатии любой кнопки с операцией и успешного ее выполнения (не возвращается nil) и строку « = Error» в случае аварийного завершения операции или нехватки операндов.

 

Screen Shot 2015-08-22 at 10.08.59 PM

В set {} вычисляемой переменной displaValue мы каждый раз заново вычисляем содержимое метки history и наполняем ее другим содержанием , просто заменив результат вызова brain.displayStack() на brain.description или brain.description1.(смотри Пункт 7 обязательный).

Screen Shot 2015-08-24 at 6.38.10 AM
Код для Swift 1.2  и iOS 8 можно посмотреть на Github.

Код для Swift 2.0  и iOS 9 можно посмотреть на Github.
Продолжение следует…

Задание 2. Решение — продолжение (обязательные пункты 5-8) Swift 1.2 и Swift 2.0: 12 комментариев

  1. в 7 пункте: » Для бинарной операции возвращаем операнды, разделенные символом операции symbol. Кроме того, мы заключаем в круглые скобки первый операнд, если оставшийся стэк состоит только из одного или двух элементов.»
    — Для чего мы заключаем первый операнд в скобки и как это связано с оставшимся стеком? 🙂

    • Заметьте, что приведенная вами цитата касается только перевода нашего стэка, записанного в форме обратной польской записи (RPN), не требующей применения скобок вообще, в инфиксную форму со скобками. Инфиксная форма нужна только для представления описания стэка в привычной для пользователя форме var description и не влияет на вычисления нашего калькулятора.
      Рассмотрите Стэк 3 ↵ 4 ↵ 5 × ÷ . Его описание должен давать 3 ÷ (5 × 4), а не 3 ÷ 5 × 4. Когда вы приступаете к бинарной операции ÷, у вас в Стэке — два элемента — 3, 5 х 4. Для правильного описания вы должны взять первый аргумент, которым является 5 х 4 в круглые скобки, иначе выражение будет арифметически некорректным.

      • Благодарю) Долго не мог составить полную картину как работает функция, очень помог вывод в печать- так легче понять что где происходит и как считается, тем кто запутался- рекомендую!
        Татьяна, остался вопрос- почему разница remainingOps.count — op1Evaluation.remainingOps.count > 2
        должна быть больше именно 2? Что это значит?
        Не могу понять по какому принципу ставятся скобки(то что они там нужны для корректности знаю, а вот как рассчитать что их нужно поставить- нет)

        • Круглые скобки необходимы в бинарной операции operand2 op operand1, если первый аргумент operand1 является результатом также бинарной операции, например
          3*(4+5). В этом выражении operand1 = 4+5, operand2 =3, op *. Стэк для этого выражения будет 3 4 5 + *. Когда мы работаем с операцией op = *,то длина оставшегося стэка — 4 (3 4 5 +). После того, как я получу operand1 = 4+5, длина оставшегося стэка будет 1 (осталась только 3). Разница между длиной стэка перед вычислением первого операнда и после = 4-1=3. Следовательно, мы ставим круглые скобки.
          Теперь рассмотрим получение результата 4+5, когда operand1 = 5, operand2 =4, op +. Для операции + длина оставшегося стэка (3 4 5) равна 3, после выбора первого операнда длина стэка (3 4) 1, и разница 3-1 =2 — круглые скобки не ставим.
          На самом мы должны были бы всегда заключать первый операнд в круглые скобки на случай, если следующей операцией в стэке окажется бинарная операция определенного типа, тогда мы бы получили (3)*(4 +(5)). Но то, что мы используем здесь этот примитивный алгоритм remainingOps.count — op1Evaluation.remainingOps.count > 2 помогает нам избавиться от скобок в простейших случаях 4+5 или 4*5 или 4*cos(5). То есть когда первый операнд operand1 — это либо следующий за операцией элемент стэке, то есть число, либо результат одинарной операции, например, cos(5). В остальных случаях operand1 будет заключаться в круглые скобки, даже в таком случае 3 + (4 +5), что является избыточным.
          Далее вы встретите пункт задания 2, который попросит вас минимизировать количество круглых скобок, то есть вместо 3 + (4 +5) вы должны дать 3 + 4 + 5. Этот алгоритм основан на приоритете операций, но не только.
          Так что перед вами простейший алгоритм удаления избыточных круглых скобок в инфиксном описании стэка. Он позволяет избавиться от круглых скобок только в случае, если operand1 — число или результат бинарной операции.
          Вообще удаление избыточных скобок — очень не простая задача, но наделяя операции некоторыми определенными свойствами удается решить ее очень элегантно.

          • Благодарю за столь исчерпывающий ответ, он мне очень помог! Я понял как это работает: если первый операнд — бинарная операция то из стэка минусуются 3 значения(operand1, operand2 и op), соответственно и разница длины стэка будет больше 2, то есть 3 и мы ставим скобки.
            Если первый операнд — число — разница в стеке — 1, если унарная операция — разница 2, скобки не ставятся.
            Двигаюсь вперед:)

          • Поздравляю. Но это только частичное решение задачи удаления избыточных скобок. Будет дополнительный пункт Задания 2, в котором вам придется решить эту задачу в общем виде.

  2. Скажите, пожалуйста, что значит знак «-» в XCTAssertTrue(brain.performOperation(«cos»)! — -0.839 < 0.1)
    после brain.performOperation("cos")!
    Первый раз встречаю такую запись)

    • Это ошибка — я боролась с интерпретацией знаков <> WordPress.
      Нужно так
      func testDescription() {
      // cos(10)
      brain = CalculatorBrain()
      XCTAssertEqual(brain.pushOperand(10)!, 10)
      XCTAssertTrue(brain.performOperation("cos")! - 0.839 < 0.1) XCTAssertEqual(brain.displayStack(), "10.0 cos")

      Исправила. Спасибо за замечание.

      • Я сверяю с вашего проекта на Github, там все правильно) Просто не могу понять эту проверку, как она работает. Вызываем performOperation(«cos»)! и потом «-» -0.839 < 0.1
        Что означают "-" и -0.839 < 0.1 ?

        • Вычисляется cos(10) — это -0.839, если аргумент в радианах, затем сравнивается с заранее известным результатом, то есть -0.839. И если разница не превосходит 0.1 (закладываемся грубо на операции с числами с плавающей точкой), то считаем, что результат верный.
          Таким образом, там все таки два знака «-» — было правильно, так как
          cos(10) — — 0.839 = 0.0000715.. что, конечно, меньше 0.1
          Нужно рассматривать не одну строку, а целый блок кода

          brain = CalculatorBrain()
          XCTAssertEqual(brain.pushOperand(10)!, 10) // в стэке 10
          XCTAssertTrue(brain.performOperation(«cos»)! — 0.839 < 0.1) // вычисляем булевское выражение cos(10)--0.839 < 0.1 и проверяем на True XCTAssertEqual(brain.displayStack(), "10.0 cos") // проверяем описание стэка на равенство строке "10.0 cos"

Обсуждение закрыто.