вторник, 4 января 2011 г.

Использование класса MathParser

Этим постом я начну небольшую серию уроков-переводов. За основу взята 12 глава из книги "Flash and Math Applets: Learn by example", доступная здесь. Перевод этот неточный, специально переработанный для использования совместно с Flex SDK (в книге все примеры разработаны во Flash IDE), но достаточный для начала работы с описываемыми классами. Также хочу отметить, что на сайте книги есть документация к описываемым классам и множество других очень интересных примеров, которые могут оказаться полезными для flash-разработчиков, интересующихся математикой. Итак, первый пример...

Пример 1. Простые вычисления с помощью класса MathParser

Мы начнем с примера, разъясняющего работу с классом MathParser. В приложении, показанном на рисунке 1, пользователь вводит выражение, зависящее от значения переменной x, и численное значение этой переменной, при этом допустим такой синтаксис, как sqrt(2*pi) или e^2. Затем, для вычисления значения выражения, пользователь нажимает клавишу ввода.
Заметьте, что для вычисляемых выражений класс MathParser использует математические функции и константы языка ActionScript. Подобно Java, JavaScript, C++, и многим другим языкам программирования, математические функции ActionScript и константы поддерживают двойную степень точности (не уверен правильно ли я перевел термин double precision accuracy). Важно учитывать данный факт, т.к. это может привести к странным (неожиданным) результатам. Например, если вы попробуете вычислить значение выражения tan(pi/2), то (т.к. значение константы pi/2 не может быть выражено в численном виде абсолютно точно) вы получите значение тангенса при значении переменной x очень и очень близком к pi/2 (но не равном ему), а это очень большое число (хотя как мы знаем, что функция тангенса не определена при pi/2). Мораль здесь в том, что, чтобы создать точный научный калькулятор, нужно проделать намного больше работы, чем просто суметь вычислить значения встроенных функций и констант языка программирования.

Настройка сцены

Чтобы создать приложение, изображенное на рис. 1, сначала нам понадобится несколько элементов на сцене. Это должны быть 2 текстовых поля для ввода текста с именами inVal и inExp и динамическое текстовое поле txtResult. Остальную работу сделает скрипт.
Рис. 1. Простенький научный калькулятор (для просмотра щелкните по изображению)

Пишем код

Сначала мы импортируем класс MathParser, а также другие необходимые классы. Отметим, что папка flashandmath должна находиться в той же папке, что и исходный файл приложения.

package 
{
 import flashandmath.as3.parsers.*;

 import flash.display.Sprite;
 import flash.display.StageAlign;
 import flash.display.StageScaleMode;
 import flash.events.Event;
 import flash.events.KeyboardEvent;
 import flash.text.TextField;
 import flash.text.TextFieldAutoSize;
 import flash.text.TextFieldType;
 import flash.text.TextFormat;
 import flash.text.TextFormatAlign;
 import flash.ui.Keyboard;
 import teormech.utils.TextUtils;
 
 public class Main extends Sprite 
 {
 ... 
 
Затем мы создадим два анализатора синтаксиса, один для разбора выражений, зависящих от значения переменной x, а второй – для выражений, не содержащих переменных (т.е. для вычисления значения переменной x, при котором мы хотим найти значение первого выражения). Заметьте, что обязательным параметром конструктора класса MathParser является массив, содержащий имена переменных, которыми могут быть практически любые строки (кроме имен функций и констант). Для анализатора выражения, не содержащего переменных, мы, в качестве значения обязательного аргумента конструктора, передаем пустой массив [ ]. Класс MathParser входит в пакет flashandmath.as3.parsers, который мы импортировали ранее.
var mpExp:MathParser = new MathParser(["x"]); 
var mpVal:MathParser = new MathParser([ ]);
В классе MathParser синтаксический анализ выражения и вычисление значение выражения выделено в отдельные методы. Такое разделение обусловлено тем, что во многих приложениях необходимо разбирать выражение единожды, а вычислять его значение многократно (например, при разных значениях переменных). Для анализа выражений, мы объявляем две переменные, являющиеся экземплярами класса CompiledObject. Этот класс также входит в состав пакета flashandmath.as3.parsers.
var compobjExp:CompiledObject = new CompiledObject(); 
var compobjVal:CompiledObject = new CompiledObject(); 
Вычисление значения выражения вызывается нажатием на клавишу Enter (при нажатии на любую другую клавишу ничего происходить не будет). Данную задачу выполняет функция compute:

// Данная строка строка взята из тела функции init()
stage.addEventListener( KeyboardEvent.KEY_DOWN, compute); 
...
private function compute(kevt:KeyboardEvent):void { 
 if (kevt.keyCode != Keyboard.ENTER) { 
  return; 
 } 

 //  «Захват» содержимого текстовых полей
 var stVal:String = inVal.text; 
 var stExpression:String = inExp.text; 

 // «Компиляция» выражений, введенных пользователем
 compobjVal = mpVal.doCompile(stVal); 
 compobjExp = mpExp.doCompile(stExpression); 

 // Проверка на наличие синтаксических ошибок 
 // во введенных выражениях.
 // При возникновении ошибки в соответствующем 
 // текстовом поле выводится сообщение
 if (compobjVal.errorStatus == 1) { 
  txtResult.text = compobjVal.errorMes; 
  stage.focus = inVal; 
  return; 
 } 
 if (compobjExp.errorStatus == 1) { 
  txtResult.text = compobjExp.errorMes; 
  stage.focus = inExp; 
  return; 
 } 

 // Вычисление значения выражения переменной
 var xVal:Number = mpVal.doEval( compobjVal.PolishArray, [ ]); 
 // Вычисление значение выражения, 
 // зависящего от значения переменной
 var resVal:Number = 
  mpExp.doEval( compobjExp.PolishArray, [xVal]); 
 // Вывод ответа с точностью то десятых долей.
 txtResult.text = resVal.toFixed(10); 
}
Для вычисления сначала числового значения переменной x, а затем самого выражения с подстановленным значением переменной мы использовали метод doEval. Отметим, что первым аргументом метода doEval является свойство PolishArray соответствующего ему объекта класса CompiledObject, а вторым аргументом - массив чисел, которые нужно передать в выражение.

Так как выражение для анализатора mpVal не содержит переменных, то ему мы передали пустой массив, а для анализатора mpExp мы передали массив с одним элементом (содержащим значение этой переменной), т.к. это выражение содержит одну переменную.

Исходники к этому примеру можно взять здесь.

Пример 2. Использование класса MathParser для создания простой математической викторины

У класса MathParser может быть множество вариантов применения. Здесь мы рассмотрим простой пример, в котором осуществляется проверка ответа студента на вопросы с произвольно генерируемыми исходными данными (см. рис.2).

Рис. 2. Проверьте свои знания по школьной математике (для проверки щелкните по картинке)

Идея, лежащая в основе проверки вводимого ответа, такова: программа берет numpoints значений переменной (в данном случае переменной x), лежащих в промежутке между значениями min и max, и сравнивает результаты вычисления «правильного» выражения (которое выводит сама программа) с результатами вычисления выражения, введенного пользователем при выбранных значениях переменной. Если все результаты вычислений отличаются не более чем на величину tolerance, то ответ считается правильным. Хотя это и ненадежный механизм для проверки алгебраических ответов, он достаточно хорошо работает в данных условиях, когда мы знаем, что ответы являются простыми многочленами. Устанавливая значения переменных numpoints, min, max, и tolerance в начале программы, мы можем более точно ее настраивать для каждого конкретного случая.

Настройки сцены

Чтобы создать описываемое приложение, Вам понадобится расположить на сцене несколько объектов: это ярлыки (labels), динамические текстовые поля (txtInitial, txtRecursivePart, txtResponseBox) и текстовое поле для ввода текста пользовательского ответа txtUserAnswer, а также кнопки btnCheck и btnNext.

Пишем код

Сперва мы импортируем необходимые классы (MathParser в их числе) и далее задаем значения переменных-параметров нашей программы, предназначенных для проверки ответа:
package 
{
 import com.bit101.components.Label;
 import com.bit101.components.PushButton;
 import com.bit101.components.Style;
 import flash.display.Sprite;
 import flash.display.StageAlign;
 import flash.display.StageScaleMode;
 import flash.events.Event;
 import flash.events.KeyboardEvent;
 import flash.events.MouseEvent;
 import flash.text.TextField;
 import flash.text.TextFieldAutoSize;
 import flash.text.TextFieldType;
 import flash.text.TextFormat;
 import flash.text.TextFormatAlign;
 import flash.ui.Keyboard;
 import teormech.utils.TextUtils;
 
 import flashandmath.as3.parsers.*;
 
 public class Main extends Sprite 
 {
  
  private var txtPts:TextField;
  private var txtUserAnswer:TextField;
  private var txtResponseBox:TextField;
  
  private var btnCheck:PushButton;
  private var btnNext:PushButton;
  
  private var numpoints:Number = 12; 
  private var tolerance:Number = 0.0000001;
  private var min:Number = 2;
  private var max:Number = 5;

  private var stPt1:String;
  private var stPt2:String;
  private var stUserAnswer:String;
  private var stCorrectAnswer:String;
  ...
Функция nextProblem задает случайные исходные данные и правильный ответ для задачи. В качестве значений параметров задачи a, b, x1 и x2 выбран интервал целых чисел [-2; 5]. Эти параметры используются для определения значений y1 и y2 (так, чтобы через точки (x1; y1) и (x2; y2) проходила прямая, определяемая уравнением y = a*x + b) и построения правильного ответа – переменной stCorrectAnswer.
private function nextProblem():void
{
 var a:int;
 var b:int;
 var x1:int;
 var y1:int;
 var x2:int;
 var y2:int;
 /* 
 Выбираем случайные значения параметров для задачи, 
 лежащие в диапазоне {-2; 5}
 */
 a = 5 - Math.floor(8*Math.random());
 b = 5 - Math.floor(8*Math.random());
 x1 = 5 - Math.floor(8*Math.random());
 x2 = 5 - Math.floor(8*Math.random());
 if (x1 == x2) {
  x2 = x1 + 1;
 }
 y1 = a*x1 + b;
 y2 = a*x2 + b;
 
 stCorrectAnswer = String(a) + "*x + " + String(b);
   
 txtPts.text = "(" + String(x1) + ", " + String(y1) + 
 ") and (" + String(x2) + ", " + String(y2) + ").";
 
 txtResponseBox.text = "";
 txtUserAnswer.text = "";
}
Функция checkAnswer производит анализ ответа пользователя и правильного ответа как выражения, зависящего от переменной x, а затем сравнивает результаты их вычисления при различных значениях переменной x:
private function checkAnswer():void 
{
 // Проверка – был ли введен ответ. Если нет, 
 // то ничего не делаем 
 var stUserAnswer:String = txtUserAnswer.text;
 
 if (stUserAnswer.length == 0) {
  return;
 }

 // Осуществляем анализ введенного пользователем 
 // и правильного выражений
 var procFun:MathParser = new MathParser(["x"]);
 var corrCO:CompiledObject= procFun.doCompile(stCorrectAnswer);
 var userCO:CompiledObject = procFun.doCompile(stUserAnswer); 

 // Сообщаем об ошибке, если какое-либо из выражений 
 // содержит синтаксическую ошибку
 if (userCO.errorStatus == 1) {
  txtResponseBox.text = 
   "Ошибка синтаксиса в пользовательском ответе. " + 
   userCO.errorMes;
  return;
 }
   
 if (corrCO.errorStatus == 1) {
  txtResponseBox.text = 
   "Ошибка синтаксиса в программном ответе. " + 
   corrCO.errorMes;
  return;
 }
 /* Выбираем значения переменной x и интервала [min; max] 
 и находим значения y для выражений введенных пользователем 
 и правильного ответа. Если ответы различаются более, чем 
 на величину переменной tolerance, то мы знаем, что ответ
 неправильный.
 */  
 var thisX:Number;
 var userY:Number;
 var corrY:Number;
 var i:Number;
 
 for (i = 0; i < numpoints; i++) 
 {
  thisX = min + i*(max - min)/numpoints;
  userY = procFun.doEval(userCO.PolishArray,[thisX]);
  corrY = procFun.doEval(corrCO.PolishArray,[thisX]);
  
  if (Math.abs(userY - corrY) > tolerance) 
  {
   var incorrectTextFormat:TextFormat = 
    new TextFormat("Arial", "18", 0xff0000);
   txtResponseBox.text = "Неправильно";
   txtResponseBox.setTextFormat(incorrectTextFormat);
   return;
  }
 }
 // Если же мы не нашли ошибки, то ответ правильный  
 txtResponseBox.text = "Правильно!";
 txtResponseBox.setTextFormat(txtResponseBox.defaultTextFormat);
}
Далее мы добавляем обработчики событий, реагирующие на нажатие кнопки Проверить и клавиши Enter и вызывающие функцию проверки ответа:
btnCheck.addEventListener( MouseEvent.CLICK, btnCheckPressed); 
stage.addEventListener( KeyboardEvent.KEY_DOWN, keyPressed); 
...
private function btnCheckPressed(e:MouseEvent):void { 
 checkAnswer(); 
} 

function keyPressed(evt:KeyboardEvent):void { 
 // Проверка ответа срабатывает только при нажатии
 // на ENTER
 if (evt.keyCode == Keyboard.ENTER) { 
  checkAnswer(); 
 } 
}
Нажатие на кнопку Следующая задача должно вести к генерации условий следующей задачи путем вызова функции nextProblem. Также мы вызывает эту функцию при запуске приложения (внутри функции init.
nextProblem();
Исходники к этому примеру лежат здесь.

UPD: В исходных классах пакета flashandmath у многих переменных отсутствует типизация, если работать во Flash IDE, то никаких проблем не возникает, но если вы разрабатываете приложения с помощью Flex SDK, то компилятор ругается на отсутствие типизации переменных и идентификаторов, поэтому мне пришлось потратить немного времени для устранения этой проблемки. Классы пакета flashandmath, включенные в исходники к примерам отличаются от официальных лишь добавлением типизации.

Комментариев нет:

Отправить комментарий