It is surprising that the basic mathematical operations so commonly used in programming are not available when the program is running. If your user enters a text string such as "11 + 3," there is no easy way to make Flash recognize this as addition and carry it out. The reason is that Actionscript is compiled. (Not into machine code, but into bytecode for a "virtual machine".) Javascript, which is interpreted, has an eval() function that takes any string and interprets it as code. To do the same in Flash, however, would require embedding a separate interpreter into the Flash player.
So, if we want to interpret a string as a meaningful expression, we need to write the code ourselves. Several years ago, I wrote a program to do just this. It was large and clunky, and the process was painful. I am aware of two other efforts in this direction, by Ralf Bokelberg and Eric Lin. Both their programs are as convoluted as my earlier attempt.
When I showed my friend Drew the da Capo music notation parser that I'm working on, he suggested that I should read "The Dragon Book," the classic textbook on compilers, by Aho, Sethi and Ullman. After reading just the first chapter, I sat down and wrote this program in under two hours. I've finally got the right approach!
The program is a direct translation of the following description, which is a "context-free grammar" represented in something close to BNF (Backus-Naur form), but modified to reflect some optimizations in the actual program structure (loops replacing recursion):
expression --> term ( ( '+' | '-' ) term )*
term --> subTerm ( ('*' | '/' ) subTerm)*
subTerm --> atom ( '^' subTerm)?
atom --> '-' subTerm
| '(' expression ')'
| constantName
| functionName '(' expression ')'
| number
constantName --> 'E' | 'PI' | 'LN10' | 'LN2' | 'LOG2E' | 'LOG10E' | SQRT2' | 'SQRT1_2'
functionName --> 'sin' | 'cos' | 'tan' | 'asin' | 'acos' | 'atan' | 'abs' | 'floor' | 'ceil' | 'log' | 'exp'
number --> (numChar)+
numChar --> '0'|'1'|'2'| ... |'9'|'.'
This says that an expression consists of a term, followed zero or more times by a '+' or '-' and another term. A term is, similarly, composed of subTerms. A subTerm is made of atoms. Now here's the spiffy part: an atom can be any expression enclosed in parentheses. So the functions getExpression(), getTerm(), getSubTerm() and getAtom() go off merrily calling each other recursively. A mathematical expression parses as a tree, but we never build the tree explicitly. Instead, the call stack reflects the tree structure. This is both an interpreter and a compiler. As an interpreter, the program keeps track of the expression's numerical value; as a compiler, it assembles an RPN form (Reverse Polish Notation) of the function, which can be computed rapidly multiple times (e.g., for graphing). Incorporating declarations of new functions is an exercise left for another weekend...
The preloaded expression comes from Lewis Carroll's poem The Hunting of the Snark.
"Taking Three as the subject to reason about--
A convenient number to state--
We add Seven, and Ten, and then multiply out
By One Thousand diminished by Eight.
"The result we proceed to divide, as you see,
By Nine Hundred and Ninety Two:
Then subtract Seventeen, and the answer must be
Exactly and perfectly true."
/***************************************************************\
ActionScript code to parse mathematical expressions
using numbers, parentheses, the operators
+, - *, /, ^, and standard functions of the Flash
Math object.
© 2005 Michael Kantor
You may use and/or adapt this code however you want.
Attribution, and a link to flashgizmo.com, is
appreciated, but not required.
\***************************************************************/
var i:Number;
var c:String;
var inString : String;
var junkString : String = " <-- Extra Junk"; // error string
var removeInt : Number;
var stack : Array;
var errorMessage : String = '';
parse_btn.addEventListener("click", clickFunction);
function clickFunction(){
inString = input_txt.text;
stack = [];
output_txt.text = parse();
stack_txt.text = stack.join("\n");
if(errorMessage != '') {
output_txt.text = errorMessage;
}
}
function parse():Number {
i = 0;
c = inString.charAt(i);
var v:Number = getExpression();
if(c != '') { // there's extra junk at the end
Selection.setFocus(input_txt);
Selection.setSelection(i, input_txt.text.length);
input_txt.text += junkString;
removeInt = setInterval(removeJunkString, 1000);
}
return(v);
}
function getExpression() {
var v:Number;
eatWhiteSpace();
if(c == '') return("Nothing");
v = getTerm();
while('x+-'.indexOf(c) > 0) {
if(c == '+') {
match('+');
v += getTerm();
stack.push("add");
}
if(c == '-') {
match('-');
v -= getTerm();
stack.push("sub");
}
eatWhiteSpace();
}
return(v);
}
function getTerm() {
var v:Number;
v = getSubTerm();
eatWhiteSpace();
while('x*/'.indexOf(c) > 0) {
if(c == '*') {
match('*');
v *= getSubTerm();
stack.push("mul");
}
if(c == '/') {
match('/');
v /= getSubTerm();
stack.push("div");
}
eatWhiteSpace();
}
return(v);
}
function getSubTerm() {
var v:Number;
v = getAtom();
eatWhiteSpace();
if(c == '^') {
match('^');
// this recursion treats ^ as right-associative
v = Math.pow(v, getSubTerm() );
stack.push("exp");
}
return(v);
}
function getAtom() {
var v:Number;
eatWhiteSpace();
if(c == '-') {
match('-');
v = getSubTerm();
stack.push("neg");
return(-1 * v);
}
if(c == '(') {
match('(');
v = getExpression();
match(')');
return(v);
}
if(isAlpha(c)) {
var f:String = getFunctionName();
if( !isNaN(Math[f]) ) { // a constant
stack.push(f);
return(Math[f]);
}
// otherwise, it's a function
match('(');
v = getExpression();
match(')');
stack.push(f);
return( Math[f](v) );
}
var s:String = '';
if('x+-'.indexOf(c) > 0) {
s += c;
match(c);
}
while(isNumChar(c)) { // isNumChar matches digits and decimal point
s += c;
match(c);
}
if(isNaN(Number(s))) {
trace("Error : '" + s + "' is not a number.");
return;
}
stack.push(Number(s));
return(Number(s))
}
function getFunctionName() {
var s : String = '';
while(isAlphaNumeric(c)) {
s += c;
match(c);
}
if (!isNaN( Math[s.toUpperCase()]) ) { // a number
return(s.toUpperCase());
}
if( Math[s.toLowerCase()] instanceof Function ) { // a function
return(s.toLowerCase());
}
error("Error: unknown function '" + s + "'");
return("???");
}
function eatWhiteSpace() {
while(c == ' ' || c == "\t") {
match(c);
}
}
function match(cc:String) {
if(inString.charAt(i) != cc) {
trace("Match error: expected '" + cc + "' but got '" + inString.charAt(i) + "'");
return;
}
// get the next character
c = inString.charAt(++i);
}
function isNumChar(c:String) {
return('x0123456789.'.indexOf(c) > 0);
}
function isAlpha(x) {
return(' 0123456789abcdefghijklmnopqrstuvwxyz'.indexOf(x) > 10);
}
function isAlphaNumeric(x) {
return(' 0123456789abcdefghijklmnopqrstuvwxyz'.indexOf(x) > 0);
}
function removeJunkString() {
clearInterval(removeInt);
var pos = input_txt.text.indexOf(junkString);
if(pos < 0) return;
input_txt.text = input_txt.text.substr(0, pos);
}
function error(msg) {
trace(msg);
if(errorMessage != '') {
errorMessage += " | ";
}
errorMessage += msg;
}
| © Michael J. Kantor 1/2005 | FlashGizmo.com |