Задание 2 cs193p Spring 2016 «Умный» Калькулятор. Решение. Обязательные и дополнительные пункты.

Содержание

Текст Домашнего Задания 2 на английском языке доступен на  iTunes в пункте “Programming: Project 2: Calculator Brain″На русском языке вы можете скачать здесь:

Задание 2 iOS 9.pdf

Задание 2 iOS 9.pdf

Задание 2 расширяет возможности калькулятора Calculator из Задания 1, позволяя ему вводить “переменные” и выполнять операцию Undo. Кроме того, вам необходимо внести в код все изменения, сделанные на лекции 3 (переменную program типа Property List и атрибуты управления доступом).  Кроме того, выполнение всех его пунктов позволит вам овладеть навыками работы с такими синтаксическими конструкциями как enum, Dictionary, Array, кортежи, Optional, String, Наблюдатели Свойств. 

Код Задания 2 представлен в двух вариантах:

  1. переменная как операнд — на Github для Xcode 7 и Swift 2.2.
  2. переменная как операция  — на Github для Xcode 7 и Swift 2.2.

Если вы установили Xcode 8, то для Swift 2.3 код находится на Github, а для Swift 3 — также на Github.

Ниже представлено решение всех обязательных и дополнительных пунктов Задания 2.

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

Все изменения, сделанные с калькулятором Calculator на лекции, должны быть внесены в код вашего Задания 1. Это включает как var program, так и правильную установку доступа (access control) всех методов и свойств. Сделайте полученный Calculator полностью функционирующим прежде, чем вы приступите к выполнению остальных обязательных заданий. И, как и в прошлый раз, “печатайте” изменения в коде, а не используйте copy / paste.

Все уже сделано в Задании 1, но в этом пункте я переименую мой класс ViewController в CalculatorViewController. Это переименование состоит из 3-х шагов:

  1. переименовываем класс ViewController в CalculatorViewController.

Screen Shot 2016-06-08 at 5.36.32 PM

2.  переименовываем файл ViewController в CalculatorViewController.

Screen Shot 2016-06-08 at 5.41.19 PM

3.  изменяем класс, привязанный к экранному фрагменту на storyboard

Screen Shot 2016-06-08 at 5.44.08 PM

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

Ничего не меняйте в non-private API в CalculatorBrain и продолжайте использовать Dictionary<String,Operation> в качестве основной внутренней структуры данных.

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

Ваш пользовательский интерфейс (UI) должен быть всегда синхронизирован с вашей Моделью (CalculatorBrain).

Все эти пункты выполнены в Задании 1.

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

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

func setOperand  (variableName: String)

var variableValues:  Dictionary<String, Double>

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

Вначале реализуем переменную как операнд, что и говорит название метода 

func setOperand  (variableName: String)

Добавляем указанный выше API в класс CalculatorBrain.
Screen Shot 2016-06-08 at 6.12.01 PM
Если словарь variableValues меняется, то result должен изменяться, отражая новые значения для “переменных”. Поэтому мы перезапускаем нашу program, то есть пересчитываем все, что мы ввели в калькулятор, но с новыми значениями переменных.
.   .   .   .   .   .   .   .  .   .   .   .   .   .   .

Screen Shot 2016-06-08 at 6.13.44 PM

Вы видите, что, если значение переменной variable отсутствует в словаре
var variableValues:  Dictionary<String, Double>, то берется значение 0, как значение по умолчанию с помощью оператора ??.
Описание var description должно продолжать работать правильно и должно показывать имя “переменной” (а не ее значение) всякий раз, когда ее вводят. Поэтому  
descriptionAccumulator = variable
Свойство var program, добавленное на лекции, также нуждается в изменении для поддержания “переменных”.

Screen Shot 2016-06-08 at 7.50.47 PM

В подсказке № 1 говорится, что «Даже если пользователи API класса CalculatorBrain вводят “переменную” с помощью метода с именем setOperand, нет причин, чтобы внутреняя реализация “переменных” в классе CalculatorBrain не использовала механизма “операций”, чтобы заставить это работать (возможно, это хорошая идея, так как внутри класса CalculatorBrain достаточно большая инфраструктура для управления операциями).» Мы рассмотрим этот вариант позже и это будет совсем другое приложение. А пока продолжим вариант реализации Задания, когда переменная представляется как операнд.
Код можно посмотреть на Github для Xcode 7 и Swift 2.2.

Если вы установили Xcode 8, то для Swift 2.3 код находится на Github, а для Swift 3 — также на Github.

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

Результат var result должен теперь правильно отражать значения “переменных” (из словаря variableValues) всякий раз, когда используются “переменные” (как в прошлом, так и в будущем, см. 8e ниже). Если у “переменной” нет значения в словаре, то используйте 0.0 как ее значение. Если словарь variableValues меняется, то result должен изменяться, отражая новые значения для “переменных”.

Реализацию можно посмотреть в обязательном пункте 4.

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

Ваше описание var description должно продолжать работать правильно и должно показывать имя “переменной” (а не ее значение) всякий раз, когда ее вводят.

Реализацию можно посмотреть в обязательном пункте 4.

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

Свойство var program, добавленное на лекции, также нуждается в изменении для поддержания “переменных”.

Реализацию можно посмотреть в обязательном пункте 4.
Код можно посмотреть на Github для Xcode 7 и Swift 2.2.

Если вы установили Xcode 8, то для Swift 2.3 код находится на Github, а для Swift 3 — также на Github.

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

  1. Добавьте две новых кнопки на UI калькулятора Calculator:→M и M. Не приносите в жертву никакие кнопки с требуемыми операциями из Задания 1 (хотя вы можете добавить больше операций, если хотите). Эти две кнопки будут соответственно устанавливать и получать переменную в CalculatorBrain, называемую M.
  • →M устанавливает значение переменной M в brain в текущее значение на display и показывает на display результат result, полученный из brain.
  • →M не должна выполнять setOperand.
  • Нажатие M должно setOperand(“M”) в brain и затем показывать на display результат result, полученный из brain.
  • →M и M являются механизмом Controller, а не механизмом Model (хотя они оба используют концепцию “переменных” в Model).
  • Это не выдающаяся кнопка “memory” на нашем калькуляторе, но она является хорошим инструментом для тестирования, правильно ли работает концепция “переменных”, реализованная выше.
  • Примеры …
  • 9 +  M = √ description имеет вид (9+M), display показывает 3, так как M не установлена (то есть равна 0)
  • 7 →Mdisplay теперь показывает 4 (квадратный корень 16), description все еще показывает (9+M)
  • +14  =display показывает 18, description теперь (9+M)+14

Располагаем на storyboard  кнопки →M  и M и с помощью CTRL-перетягивания создаем для них @IBAction функции setM и pushM

Screen Shot 2016-06-08 at 9.48.17 PM

Screen Shot 2016-06-08 at 9.48.45 PM

Хотя методы @IBAction в названии ориентируются на переменную M, реализация методов  setM и pushM предполагает произвольную переменную, которую мы читаем с заголовка кнопки:

 

Проверяем работоспособность на примерах, представленных в задании.
Код можно посмотреть на Github для Xcode 7 и Swift 2.2.

Если вы установили Xcode 8, то для Swift 2.3 код находится на Github, а для Swift 3 — также на Github.

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

Убедитесь, что кнопка  С вашего Задания 1 работает правильно в этом задании. В дополнение она должна убирать любые значения “переменной” M из variableValues Dictionary в CalculatorBrain ( а не устанавливать в 0 или какое-то другое значение). Это позволит вам тестировать случай “неустановленной” переменной.

Для придания гибкости API калькулятору — как нам советовали в подсказках — добавляем в CalculatorBrain еще два non-private метода clear  и  clearVariable

Screen Shot 2016-06-09 at 1.35.04 PM

Хочу отметить, что метод  removeAll() имеет параметр keepCapacity, который по умолчанию имеет значение true и означает сохранение области памяти, которую занимал массив. Нам же действительно нужно, чтобы массив «ушел» из «кучи», следовательно в нашем случае keepCapacity: false. Это эквивалентно коду
internalProgram = []
Используем обе этих функций в реализации кнопки С:

Screen Shot 2016-06-08 at 10.25.21 PM

Вот как выглядит Action ClearAll:

Screen Shot 2016-06-09 at 1.53.42 PM

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

Добавьте “Undo” к вашему калькулятору: в дополнительном пункте Задания 1 вы добавляли  кнопку  “backspace”, если пользователь ввел неверную цифру. Теперь мы говорим о комбинации “backspace” и реального “Undo” в единой кнопке. Если пользователь находится в середине ввода числа, то это кнопка работает как  backspace. Если пользователь не находится  в середине ввода числа, то должно сделать Undo последней вещи, которая была выполнена в CalculatorBrain.  Не отменяйте запоминания значений переменной (но ДЕЛАЙТЕ отмену установки “переменной” как операнда).

Добавляем в API CalculatorBrain новый метод undoLast ()

Screen Shot 2016-06-09 at 2.44.31 PM

И используем его в нашем Controller на кнопке «backspace»

Screen Shot 2016-06-09 at 2.48.42 PM

После реализации операции «Undo» нам необходимо проверить работоспособность ситуации, указанной в подсказке № 3:
«Некоторые вещи (подобные π или результат другого выражения) несколько хитроумно запоминать в M после того, как вы ввели выражение, которое вы хотите оценить (например, ввели cos(M), а затем пытаетесь установить M в π). Как только вы реализуете Undo, вы сможете это сделать (путем “undoing” этого выражения, полученного просто нажатием π для вычисления значения, которое вы хотите запомнить в M).»
Для  этого вводим:
 M cos  >⇒ description имеет вид cos(M) =display показывает 1, так как M не установлена (то есть равна 0)
Затем вводим:
 π →M  ⇒ description> имеет вид π =display показывает 3.141593
Undo   ⇒ description имеет вид cos (M) =display показывает -1, так как M установлена в  π
Код можно посмотреть на Github для Xcode 7 и Swift 2.2.

Если вы установили Xcode 8, то для Swift 2.3 код находится на Github, а для Swift 3 — также на Github.

Пункт 1 дополнительный

Ваш Калькулятор должен сообщать об ошибках. Например,  из отрицательного числа или деление на нуль. Есть несколько способов “обнаружения” этих ошибок  (может быть добавить ассоциированное значение к Unary/ BinaryOperation вариантам, которые являются функциями, которые обнаруживают ошибку, или, возможно, заставить функцию, которая ассоциируется с Unary/ BinaryOperation возвращать кортеж как с результатом result, так и с ошибкой error (если она есть) или ???). То, как вы будете сообщать о любых обнаруженных ошибках пользователям CalculatorBrain API, потребует от вас некоторых изменений API, но не заставляйте пользователей CalculatorBrain API иметь дело с ошибками, если они этого не хотят (то есть позвольте Controllers, которые хотят показывать errors, делать это, а другим, которые не хотят показывать ошибки,  позвольте иметь дело просто с NaN или +∞, появляющимися на вашем UI). Другими словами, не изменяйте никакие из существующих методов и свойств в non-private API CalculatorBrain для того, чтобы поддерживать эти возможности  (вместо этого добавьте методы / свойства, если это необходимо).

Необходимо уметь спрашивать операции BinaryOperation и UnaryOperation, какие ошибки (если они есть) будут генерироваться при передачи в них  операнда (ов).
Один из способов сделать это — иметь для BinaryOperation и UnaryOperation операций ассоциированное значение , которое представляют собой функцию, анализирующую потенциальные аргументы и возвращающую соответствующее String сообщение об  ошибке, если выполнение операции генерирует ошибку (или nil  в противном случае).
Большинство операций не могут сообщать о каких-то ошибках, да мы и не обязаны заставлять их это делать. В этом случае мы будем использовать  nil в качестве функции, тестирующей ошибки. Можно сделать Optional целиком тип функции, размещая тип функции в круглых скобках и ставя знак ? после круглых скобок. Например, ((Double, Double ) -> String?)? — это  Optional  функция, которая берет два Double и возвращает Optional <String>.
Добавляем к операциям BinaryOperation и UnaryOperation новое ассоциированное значение в виде типа тестирующей ошибку функции.

Screen Shot 2016-06-09 at 3.24.06 PM

 Здесь интересны два варианта:

  • первый — если сама тестирующая ошибки функция не установлена, то есть сама функция errorTest -это nil,
  • второй — функция errorTest не nil, но она декларирована так, что возвращает nil. Это очень удобно.

В нашей реализации только некоторые унарные операции типа lnx⁻¹, sin⁻¹cos⁻¹ и бинарная операции «деление»  получают тестирующую ошибку функцию.

Screen Shot 2016-06-09 at 4.12.09 PM

Ошибку будем фиксировать в private переменной error: String?

Screen Shot 2016-06-09 at 4.16.43 PM

.  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .
Прежде, чем выполнить унарную или бинарную операцию, тестируем полученные операнды на ошибку. Если ошибка обнаружена, то фиксируем ошибку в error
.  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .

Screen Shot 2016-06-09 at 7.23.58 PM

.  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .

Используем синтаксис “цепочек” Optionals и пишем функции validator c вопросительным ? знаком и здесь интересны два варианта:

  • первый — если  функция validator не установлена, например, для операций «sin«,»cos«,»tan«, то error равно nil,
  • второй — функция validator не nil, но она декларирована так, что возвращает nil в случае отсутствия ошибки. Это очень удобно.

Здесь действительно очень удачно работают два перечисленных выше варианта. Если у вашей операции, например, «sin» или «cos» нет тестирующей функции, то error = nil и сообщение об ошибке не будет выдано. Если у вас есть тестирующая функция, как у операции ««, а аргумент не дает «Отрицательное значение», то есть второй из вышеперечисленных вариантов, то также возвращается nil и сообщение об ошибке не будет. Именно это двоякое свойство определенной нами функции validator является очень удобным при обработке ошибок.
Бинарная операция может быть в отложенном состоянии, поэтому тестирующую функцию нужно запомнить в отложенной структуре PendingBinaryOperationInfo

Screen Shot 2016-06-09 at 4.25.35 PM

.  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .

Screen Shot 2016-06-09 at 4.33.03 PM

а затем использовать при вычислении на предмет проверки ошибки:

Screen Shot 2016-06-09 at 7.39.35 PM

И опять используем синтаксис “цепочек” Optionals и пишем функции validator c вопросительным ? знаком и опять здесь удачно работают два перечисленных выше варианта. Если у вашей операции, например, «+» или «» нет тестирующей функции, то error = nil и сообщение об ошибке не будет выдано. Если у вас есть тестирующая функция, как у операции деления «÷«,а аргументы не дают «Деление на ноль», то есть второй из вышеперечисленных вариантов, то также возвращается nil и сообщение об ошибке не будет. Именно это двоякое свойство определенной нами функции validator позволяет нам вести обработку ошибок одной строкой.
Для того, чтобы передать обнаруженную или не обнаруженную ошибку в CalculatorViewController, сформируем result в CalculatorBrain в виде кортежа, который содержит вычисленное значение accumulator типа Double и сообщение об ошибке error типа String?:

Screen Shot 2016-06-09 at 7.58.31 PM

В CalculatorViewController добавим переменную кортеж resultValue, которая будет «встречать» результирующее значение result из CalculatorBrain, производить необходимую обработку кортежа с помощью оператора switch c паттернами  соответствия образцу (pattern matching) и осуществлять всю необходимую настройку UI:

Screen Shot 2016-06-09 at 8.11.30 PM

Для соответствующих кнопок, где результат отображается на дисплее, вместо dysplayValue, устанавливаем значение для новой переменной resultValue:
кнопка с операциями

Screen Shot 2016-06-09 at 8.17.53 PM

кнопка backspace

Screen Shot 2016-06-09 at 8.20.49 PM

кнопка →M

Screen Shot 2016-06-09 at 8.23.27 PM

кнопка M

Screen Shot 2016-06-09 at 8.26.55 PM

Пример работы. Если мы с помощью переменной M наберем функцию (sin(M)+M)÷M, то получим «Деление на ноль», так как если значение M не присвоено, то оно равно 0. Eсли мы затем пошлем в переменную M значение π/2, то значение выражения (sin(M)+M)÷M будет равно 1.63662.

Screen Shot 2016-06-09 at 8.50.35 PM

Код можно посмотреть на Github для Xcode 7 и Swift 2.2.

Если вы установили Xcode 8, то для Swift 2.3 код находится на Github, а для Swift 3 — также на Github.

Представление переменной как операции

Даже если пользователи API класса CalculatorBrain вводят “переменную” с помощью метода с именем setOperand, нет причин, чтобы внутреняя реализация “переменных” в классе CalculatorBrain не использовала механизма “операций”, чтобы заставить это работать (возможно, это хорошая идея, так как внутри класса CalculatorBrain достаточно большая инфраструктура для управлеия операциями). 

Добавляем в enum Operation  вариант для переменных, не требующий никаких ассоциированных значений:

Screen Shot 2016-06-09 at 9.31.23 PM

В этом случае метод setOperand, для переменных будет выглядеть следующим образом.

Screen Shot 2016-06-09 at 9.34.11 PM

Добавляем  в словарь операций operation символ переменной variable как ключ, тип операции Operation.Variable как значение. И выполняем эту операцию с помощью метода performOperation, в который внесены необходимые изменения для Operation.Variable.

Screen Shot 2016-06-09 at 9.47.28 PM

Если у “переменной” нет значения в словаре, то используйте 0.0 как ее значение:
accumulator = variableValues[symbol] ?? 0
Описание var description должно показывать имя “переменной” (а не ее значение) всякий раз, когда ее вводят, поэтому
descriptionAccumulator = symbol
Свойство var program, добавленное на лекции, в данном не нуждается в изменении для поддержания “переменных”, так как переменная выступает как обычная операция

Screen Shot 2016-06-09 at 9.53.21 PM

Остальные пункты Задания 2 абсолютно идентичны тому, что изложено выше.
Код можно посмотреть на Github для Xcode 7 и Swift 2.2.

Если вы установили Xcode 8, то для Swift 2.3 код находится на Github, а для Swift 3 — также на Github.