src/js/model.js
/*! @file model.js */
/*
* part of the 'Transfinite Ordinal Calculator'
* author: Claudio Kressibucher
* license: GNU LGPL
*/
// --- This file contains the current calculation data on the stack,
// as well as the register contents (stored values)
/* JSHINT global declaration */
/*global oGui:true, util:true, config:true, Ord:true, ClassedError:true, Calculation:true, model:true */
/**
* @module calculator
* @description A simple calculator that performs arithmetic operations
* with ordinal numbers up to (but not including) epsilon_0.
*/
/**
* The model contains the data of the application.
* @class model
* @static
*/
var model = (function (){
/**
* The stack holds the data of the current expression.
* @property stack
* @type Array (of StackEl objects)
* @private
*/
var stack = [];
/**
* The registers contain the stored data.
* @property regs
* @type Array of Ord instances
* @private
*/
var regs = new Array( null, null ); // initialized with null, indicating that they weren't filled yet (will be represented by the ordinal 0, see methods recall and getRegister)
/**
* Caches the last comparison.
* @property cmpData
* @type model.ComparisonData
* @private
*/
var cmpData = null;
/**
* Flag to indicate if register's have changed since last call of
* method compareRegs()
* @property regDirty
* @type boolean
* @private
*/
var regDirty = true;
/**
* Enumerable values for the state of the model.
* @property enumState
* @type Object literal containing number values
*/
var enumState = { // will be referenced by public accessible property model.enumState
init: 0,
digit: 1,
optor: 2,
constant: 3, // omega, or after recalling a completed Ord object from registers
calc: 4, // after a performed calculation
leftBracket: 5,
rightBracket: 6, // useful to determine, if the brackets must be set during input...
stored: 9
};
/**
* Counts how many brackets are currently opened.
* @property numOfOpenBrackets
* @type number
* @private
*/
var numOfOpenBrackets = 0;
/**
* The state of the model, depending on the last added character.
* @property state
* @type number (enumState)
* @private
*/
var state = enumState.init;
/**
* To store a ClassedError object, and share it between methods.
* @property mErr
* @type ClassedError
* @private
*/
var mErr = null;
// functions (commented at declaration)
var isOperator;
var isNatural;
var subcalc;
var trySimplify;
var calculate;
var determineState;
var stackToString;
var logStack;
var updateComparisonData;
// primitive methods for addChar
var addNum, addConst, addOptor, addLeftBracket;
// variable handlers
var getRegister;
/**
* Check if a character is an operator.
* @method isOperator
* @param {string} ch The character (or short string) to be checked.
* @return {boolean} True if the parameter ch is an operator
* @private
*/
isOperator = function (ch){
switch(ch){
case '+':
case '*':
case '^':
return true;
default:
return false;
}
};
/**
* Check if the character is a natural number consisting of digits.
* @method isNatural
* @param {string} ch The character or little String to be tested
* @return {boolean} True if the parameter ch is a natural number
* @private
*/
isNatural = function (ch){
if (typeof(ch) !== "string"){
ch = ''+ch;
}
return ch.search(/^\d*$/) >= 0;
};
/**
* Performs a (sub-)calculation around an operator whose stack-index
* is given as parameter. The method looks for the two
* surrounding operands, extracts this operands, and then performs
* the calculation. If a calculation error occurrs, then subcalc will
* store it to the mErr propererty. Check for that in the calling function.
* @method subcalc
* @param {number} optorIx The index of the operator on the stack
* @return {Ord} The result object, or null if an error occurs.
* @private
*/
subcalc = function subcalc(optorIx){
var firstOp, secOp; // operands
var oCalc = null; // calculation object
var resAssert; // result of assert call
var res = null;
var err; // error from Calculation obect oCalc
// must have operands around...
if (optorIx <= 0 || optorIx >= stack.length-1) {
mErr = mErr || new ClassedError("stack error", ClassedError.types.progError);
return null;
}
// fetch first operand
firstOp = stack[optorIx-1];
// fetch second operand
secOp = stack[optorIx+1];
// test types
if (config.debug > 0){
resAssert = util.assert(firstOp instanceof model.Operand && secOp instanceof model.Operand);
if (resAssert === false){
oGui.dbgErr("model.subcalc: operands have wrong type...");
}
}
oCalc = new Calculation(firstOp, secOp, stack[optorIx]);
res = oCalc.simplify();
err = oCalc.getError();
if (err !== null){
// this is the central point, where every Calculation.simplify starts.
if (mErr === null){
if (err instanceof ClassedError)
mErr = err;
else
mErr = new ClassedError("", ClassedError.types.calcError);
}
res = null;
}
return res;
};
/**
* Tries simplification of previous operands. This is possible, if the last
* operator (which is delivered as parameter):
* <ul>
* <li>has a lower priority than the preceeding one</li>
* <li>has the same priority than the preceeding one and the operation is
* associativ or left-associativ</li>
* </ul>
* This method tries to calculate as many operands as possible by iteratively
* testing if the condition described above is true...<br />
* The method updates the stack itself. If an error occurs, mErr is used to
* store the Error object.
* @method trySimplify
* @param {string} op The last operator. Is not stored on the stack yet.
* @private
*/
trySimplify = function trySimplify(op){
var res; // intermediate result
var resOrd; // intermediate result wrapping Ord object
var preOptor;
var preType; // type of previous operator
while(stack.length > 2) { // must have at least 3 Elements: Operand, operator, and operand_2
preOptor = stack[stack.length-2];
preType = preOptor.getType();
if (preType === model.enumType.tOptor){
if (preOptor.getPrio() > op.getPrio() ||
preOptor.getPrio() === op.getPrio() && op.getSymbol() !== '^'){
// -- do simplification
res = subcalc(stack.length-2);
if (res === null) {
// mErr should be not null, but check for it...
if (mErr === null){
mErr = new ClassedError("", ClassedError.types.progError);
}
stack.splice(stack.length-3, 3); // remove the last three items from the stack
return;
}
// splice stack array: replace operands and operators with result "res".
resOrd = new model.Operand(model.enumType.tOrd);
resOrd.setValue(res);
stack.splice(stack.length-3, 3, resOrd);
} else {
break;
}
} else if (preType === model.enumType.tBracket){ // left bracket
util.assert(preOptor.getSymbol() === '(');
break;
} else {
oGui.dbgWarn("model.trySimplify: operator has wrong type");
mErr = mErr || new ClassedError("", ClassedError.types.progError);
return;
}
}
};
/**
* Calculates the expression from top of the stack until
* an open bracket or the end (bottom) of the stack is reached.<br />
* Uses mErr to store a ClassedError object if an error occurs. Check this
* property in the calling function.
* @method calculate
* @param {boolean} lookForBracket If true, an open bracket is expected. If no
* such is found, a debug error is generated.
* @private
*/
calculate = function calculate(lookForBracket){
var res; // intermediate result
var resOrd; // intermediate result wrapping Ord object
var optor;
var optorType; // type of previous operator
while(stack.length > 1) {
optor = stack[stack.length-2];
optorType = optor.getType();
if (optorType === model.enumType.tOptor){
if (!util.assert(stack.length > 2)){
oGui.dbgErr("model.calculate: operator at position 0");
mErr = mErr || new ClassedError("", ClassedError.types.progError);
break;
}
// -- do simplification
res = subcalc(stack.length-2);
if (res === null) {
mErr = mErr || new ClassedError("", ClassedError.types.progError); // mErr should already has an error object stored from subcalc, but don't rely on it
oGui.dbgLog("model.calculate: error in calculation, reduce last operation from stack");
stack.splice(stack.length-3, 3);
break;
}
// splice stack array: replace operands and operators with result "res".
resOrd = new model.Operand(model.enumType.tOrd);
resOrd.setValue(res);
stack.splice(stack.length-3, 3, resOrd);
if (stack.length === 1){
// bottom of stack reached
if (lookForBracket){
oGui.dbgWarn("model.calculate: an open bracket was expected, but " +
"the end of the stack is reached without the bracket.");
}
break;
}
} else if (optorType === model.enumType.tBracket){ // left bracket
// replace the '(' character with the last operand
stack.splice(stack.length-2, 2, stack[stack.length-1]);
numOfOpenBrackets--;
if (!lookForBracket){
oGui.dbgWarn("model.calculate: an unexpected open bracket was found. Calculation aborted.");
}
break;
} else {
oGui.dbgWarn("mode.calculate: operator has wrong type");
mErr = mErr || new ClassedError("", ClassedError.types.progError); // mErr should already has an error object stored from subcalc, but don't rely on it
break;
}
}
};
/**
* Get the content of the stack as String representation
* @method stackToString
* @param {boolean} htmlStyle If true, the return-String contains
* the special html-char ω instead of w and the last
* operand will be wrapped into a span tag.
* @return {string} The string representation of the stack
* @private
*/
stackToString = function (htmlStyle){
var i=0;
var a = [];
var bracket;
var opnd;
var innerOptor; // weekest operator in the operand (if an operand)
var sEl, prevEl, nextEl;
var sLen = stack.length;
var lastOp; // if the stakc element is the last element on the stack and an operand
while (i < sLen){
sEl = stack[i];
lastOp = (i === sLen-1) && (sEl instanceof model.Operand);
if (lastOp && htmlStyle){
a.push('<span class="lastOp">');
}
// determine if bracket is needed...
// Klammern ausgeben:
// - Wenn Stack-Element ein Operand ist mit einem inneren Operator, der schwaecher als einer der umgebenden Operatoren ist.
// - Nach Eingabe einer geschlossenen Klammer, wenn der letzte Operand (das Resultat des Klammerausdrucks) nicht der einzige ist (nur um letzten Operanden klammern!)
if (sEl.getType() !== model.enumType.tOrd){
bracket = false;
} else {
opnd = sEl.getOrd();
if (!opnd || opnd.isUndefined()){
oGui.dbgErr("undefined operand on stack");
continue;
}
if (opnd.isNatural()){
bracket = false;
} else if (state === enumState.rightBracket && // use case: stack = 2+(w+4 ; dann Eingabe ')': benoetigt Klammerung um 'w+4'!
sLen > 1 && i === sLen-1){
bracket = true;
} else {
innerOptor = null;
if (opnd.getNumOfSumds() > 1){
innerOptor = new model.Operator('+');
} else if (opnd.getSummand(0).getFactor() > 1){
innerOptor = new model.Operator('*');
} else if (!opnd.equalsOmega()) {
innerOptor = new model.Operator('^');
}
bracket = false;
if (innerOptor !== null) {
bracket = false;
// check previous element
if (i > 0){
prevEl = stack[i-1];
if (prevEl.getType() === model.enumType.tOptor &&
prevEl.getPrio() > innerOptor.getPrio()){
bracket = true;
}
}
// check next element
if (!bracket && i < sLen-1){
nextEl = stack[i+1];
if (nextEl.getType() === model.enumType.tOptor){
if (nextEl.getPrio() > innerOptor.getPrio() ||
nextEl.getPrio() === innerOptor.getPrio() && nextEl.getSymbol() === '^')
bracket = true;
}
}
} // else: opnd === omega, there is no need for a bracket around one single character
}
}
if (bracket){
a.push('(');
}
sEl.serialize(a, htmlStyle);
if (bracket){
a.push(')');
}
if (lastOp && htmlStyle){
a.push('</span>');
}
i++;
}
return a.join('');
};
/**
* Logs the content of the stack.
* @method logStack
* @private
*/
logStack = function (){
oGui.dbgLog("Stack: " + stackToString(false));
};
/**
* Primitive function to add a Digit to the stack.
* Helper method for addChar.
* @method addNum
* @param {string} ch The character representing the number to add
* @private
*/
addNum = function(ch){
var se = new model.Operand(model.enumType.tNatNum); // stack element
var oldDigit = null;
var oldValue;
var strTmp;
var re; // regexp
switch(state){
case enumState.init: // nothing added so far
case enumState.optor: // last char was an operator
case enumState.leftBracket:
se.setValue(ch);
break;
case enumState.digit:
if (stack.length > 0) {
oldDigit = stack[stack.length-1];
} else {
oGui.dbgWarn("model.addNum: state was 'digit', but stack is empty");
se.setValue(ch);
break;
}
if (oldDigit instanceof model.Operand && oldDigit.getType() === model.enumType.tNatNum){
try {
oldValue = oldDigit.getValue();
if (oldValue === 0){
se.setValue(ch);
} else {
// concatenate digits
strTmp = oldValue.toString();
// check if string is of type 1e+21 ...
re = /^[0-9]+$/;
if (!re.test(strTmp)){
oGui.dbgErr("string representation of a number characters that differ from digits...");
throw new ClassedError("number is too large...", ClassedError.types.inaccurate);
}
se.setValue(oldValue.toString() + ch);
}
stack.pop(); // replace top element with concatenated natural number
} catch (e){
if (e instanceof ClassedError){
mErr = mErr || e;
break;
} else {
throw e; // don't catch other errors than ClassedError
}
}
} else {
mErr = mErr || new ClassedError("", ClassedError.types.progError);
oGui.dbgErr("model.addNum: state was 'digit', top stack element was not an operand or not a nat. number");
}
break;
case enumState.constant:
case enumState.rightBracket:
mErr = mErr || new ClassedError("Not allowed to add a digit here", ClassedError.types.progError);
break;
case enumState.calc:
case enumState.stored:
// start new calculation
oGui.dbgLog("model.addNum: reset stack");
model.resetModel();
se.setValue(ch);
break;
default:
mErr = mErr || new ClassedError("", ClassedError.types.progError);
oGui.dbgErr("model.addNum: unknown state...");
} // end switch
if (mErr === null && se.isValueSet()) {
stack.push(se);
}
};
/**
* Primitive function to add a constant (omega) to
* the stack. Helper method for addChar. If an error occurs, it will
* be stored to the mErr property, so it can be handled later.
* @method addConst
* @param {string} ch The constant to add (normally it is 'w').
* @private
*/
addConst = function(ch){
var se = new model.Operand(model.enumType.tConst); // stack element
if (ch === 'w'){
ch = 'ω';
} else {
oGui.dbgErr('model.addConst(' + ch + ')');
mErr = mErr || new ClassedError("", ClassedError.types.progError);
return;
}
switch (state){
case enumState.init:
case enumState.optor:
case enumState.leftBracket:
se.setValue(ch);
break;
case enumState.digit:
mErr = mErr || new ClassedError("omega is not allowed here.", ClassedError.types.progError);
break;
case enumState.constant:
case enumState.rightBracket:
mErr = mErr || new ClassedError("omega is not allowed here.", ClassedError.types.progError);
break;
case enumState.calc:
case enumState.stored:
// start new calculation
oGui.dbgLog("model.addConst: reset stack");
model.resetModel();
se.setValue(ch);
break;
default:
mErr = mErr || new ClassedError("", ClassedError.types.progError);
} // end switch
if (mErr === null && se.isValueSet()) {
stack.push(se);
}
};
/**
* Primitive function to add an Operator to the
* stack. Helper method for addChar. Does not return anything.
* If an error occurred, then a ClassedError object is appended to
* the private property mErr. This error should be handled by the calling
* function.
* @method addOptor
* @param {string} ch The operator to add to the stack.
* @private
*/
addOptor = function(ch){
var se = new model.Operator(ch); // stack element
switch(state){
case enumState.init:
case enumState.optor:
case enumState.leftBracket:
mErr = mErr || new ClassedError("Not allowed to add an operator here", ClassedError.types.progError);
break;
case enumState.digit:
case enumState.constant:
case enumState.rightBracket:
trySimplify(se);
break;
case enumState.calc:
// do nothing (do not reset stack!) but pushing the optor on the stack
util.assert(stack.length === 1, "stack length should be 1 in model.state == calc");
break;
case enumState.stored:
// do nothing but pushing the optor on the stack
break;
default:
oGui.dbgErr("model.addOptor: unknown state...");
mErr = mErr || new ClassedError("", ClassedError.types.progError);
} // end switch
if (mErr === null){ // push se only, if no error has occurred
stack.push(se);
}
};
/**
* Primitive function to add a left bracket to
* the stack. Helper method for addChar.
* @method addLeftBracket
* @param {string} ch The bracket character, normally '('.
* @private
*/
addLeftBracket = function(ch){
var se = new model.Bracket(ch); // stack element
// var error = false;
switch(state){
case enumState.init:
case enumState.optor:
case enumState.leftBracket:
// do nothing
break;
case enumState.digit:
case enumState.constant:
case enumState.rightBracket:
// error
mErr = mErr || new ClassedError("Not allowed to add a left bracket here", ClassedError.types.progError);
break;
case enumState.calc:
case enumState.stored:
// reset stack
oGui.dbgLog("model.addLeftBracket: reset stack");
model.resetModel();
break;
default:
oGui.dbgErr("model.addLeftBracket: unknown state...");
mErr = mErr || new ClassedError("", ClassedError.types.progError);
} // end switch
if (mErr === null)
stack.push(se);
};
/**
* Tries to determine and set the correct state of the stack
* after an error in calculation.
* @method determineState
* @return {model.enumState} The state.
* @private
*/
determineState = function(){
var lastEl;
var newState;
var localErr; // used to inform the user, is handled inside this method so its locally. The return value is correct anyway, because in the case of an error, the model stack is resetted.
if (stack.length <= 0){
model.resetModel(); // sets state init
newState = model.getState();
} else {
lastEl = stack[stack.length-1];
switch(lastEl.getType()){
case model.enumType.tNatNum :
newState = enumState.digit;
break;
case model.enumType.tConst :
case model.enumType.tOrd :
newState = enumState.constant; // finite operand => handle as constant
break;
case model.enumType.tOptor :
newState = enumState.optor;
break;
case model.enumType.tBracket :
if (lastEl.isRight()){
stack.pop(); // remove right bracket
newState = determineState();
} else {
newState = enumState.leftBracket;
}
break;
default:
oGui.dbgWarn("cannot determine state of model because the last element on the stack" +
"has no valid type. To ensure consistent data, the stack will be cleared.");
localErr = new ClassedError("The Stack is resetted, because an error has occurred.", ClassedError.types.progError);
localErr.handleErr();
model.resetModel();
newState = model.getState();
}
}
return newState;
};
/**
* Get the operand stored in the register
* @method getRegister
* @param {number} regAddr The address of the register
* @return {model.Operand} The value stored in the register or null
* if an error occurs.
* @private
*/
getRegister = function(regAddr){
var regVal;
if (regs.length <= regAddr){
oGui.dbgErr("model.getRegister: regs.length <= regAddr");
mErr = mErr || new ClassedError("unvalid register address", ClassedError.types.progError);
return null;
}
regVal = regs[regAddr];
if (regVal === null){
regVal = new model.Operand(model.enumType.tOrd);
regVal.setValue( new Ord(0) );
}
return regVal;
};
updateComparisonData = function(){
if (regDirty){
var r1 = getRegister(0);
var r2 = getRegister(1);
if (mErr === null){
oGui.dbgLog("model.compareRegs: recompute comparison");
cmpData = new model.ComparisonData(r1.getOrd(), r2.getOrd());
regDirty = false;
} else {
oGui.dbgErr("Comparison failed: " + (mErr.message ? mErr.message : ""));
}
}
return cmpData; // check for null value in calling function
};
// The object to return as model. Contains the properties and methods
// that are public accessible
return {
// make the enumState object globally accesible
'enumState' : enumState,
/**
* Get the current state of the model.
* @method getState
* @param {boolean} bAsString If set, the state is returned as a string
* instead of the representing number (association defined in
* model.enumState)
* @return {number / string} The state of the model. A value of model.enumState.
*/
getState : function (bAsString){
if (bAsString){
switch (state){
case enumState.init:
return "init";
case enumState.digit:
return "digit";
case enumState.optor:
return "optor (Operator)";
case enumState.constant:
return "constant";
case enumState.calc:
return "calc";
case enumState.leftBracket:
return "leftBracket";
case enumState.rightBracket:
return "rightBracket";
case enumState.stored:
return "stored";
default: // return as number
return state;
}
}
return state;
},
/**
* Get the data in the representation needed by the gui
* to display the content of the model.
* @method getData
* @param {boolean} simple If set, the data is in a simple form,
* that means, no html, just w instead of omega and so on.
* If not specified, the html form is returned.
* @return {string} The data of the model
*/
getData : function (simple){
if (stack.length === 0){
return "0";
}
return (simple) ? stackToString(false) : stackToString(true);
},
/**
* Calculate the whole stack (if necessary) and store the
* resulting Ord object into a register
* @method store
* @param {number} regAddr The address of the register.
* Possible Values: 0 and 1
* @return {boolean} If the operation was successful
*/
store: function (regAddr){
var locErr = null;
var opnd;
var a = [];
if (regs.length > regAddr && regAddr >= 0){
// calculate before storing
// must do it recursively until all open brackets are closed and
// there is just one operand left on the stack
while(stack.length > 1){
calculate(numOfOpenBrackets > 0); // if open brackets left, then the argument lookForBracket is true, otherwise false
}
if (mErr !== null){
locErr = mErr;
mErr = null;
oGui.dbgErr("model.store() failed because calculation (method model.calculate()) failed");
} else { // go on and store stack to register
opnd = stack[stack.length-1];
if (opnd instanceof model.Operand) {
regs[regAddr] = opnd;
if (config.debug > 2){
opnd.serialize(a, false);
oGui.dbgLog("stored \"" + a.join('') + "\" to register " + regAddr);
}
} else {
oGui.dbgErr("model.store: top of stack is not an operand");
locErr = new ClassedError("", ClassedError.types.progError);
}
}
} else {
oGui.dbgErr("model.store: register with address " + regAddr + " doesn't exist");
locErr = new ClassedError("", ClassedError.types.progError);
}
if (locErr === null){ // successful
regDirty = true;
state = enumState.stored;
} else {
oGui.dbgErr("model.store: operation couldn't be completed");
locErr.handleErr();
}
oGui.update();
},
/**
* Recall a value from the register and put it on the stack.
* @method recall
* @param {number} regAddr The address of the register.
* Possible Values: 0 and 1
* @return {boolean} If the operation was successful
*/
recall: function (regAddr){
var locErr = null;
var regVal;
// get register value
if (regs.length <= regAddr || regAddr < 0){
oGui.dbgErr("model.recall: regs.length <= regAddr || regAddr < 0");
locErr = new ClassedError("", ClassedError.types.progError);
locErr.handleErr();
} else {
regVal = regs[regAddr];
if (regVal === null){
regVal = new model.Operand(model.enumType.tOrd);
regVal.setValue( new Ord(0) );
}
// reset stack before pushing value?
if (state === enumState.stored || state === enumState.calc){ // state calc shouldn't exist anymore, but for stability reasons it is checked too
model.resetModel();
}
// store register value on stack
stack.push(regVal);
state = enumState.constant;
oGui.update();
}
},
/**
* Sets the model back to state init and delete the stack's content.
* @method resetModel
*/
resetModel : function(){
stack = [];
state = enumState.init;
numOfOpenBrackets = 0;
oGui.update();
},
/**
* This method clears the registers. It expects that the
* model is in the init state, otherwise it will call
* resetModel itself to ensure that.
* @method clearRegister
*/
clearRegister : function(){
if (state !== enumState.init){ // should not be the case...
oGui.dbgWarn("model.clearRegister: clear also stack, but you " +
"shouldn't call this method with state !== init !");
model.resetModel();
state = enumState.init;
}
if (!model.regsEmpty()) {
regs = new Array( null, null );
regDirty = true; // registers have changed!
}
oGui.update();
},
/**
* Checks if the registers are empty. Empty means that
* they are in the original state; if an ordinal with
* the value 0 is stored in a register, then it is NOT
* considered empty!
* @method regsEmpty
* @return {boolean} True if registers are empty
*/
regsEmpty : function (){
return regs[0] === null && regs[1] === null;
},
/**
* Get the number of open (and not yet closed) brackets on
* the stack expression.
* @method getNumOfOpenBr
* @return {number} The number of open brackets
*/
getNumOfOpenBr : function(){
return numOfOpenBrackets;
},
/**
* Add a character to the stack. This method is normally called from
* the event handler of the input keys.
* @method addChar
* @param {string} ch The character to be added.
*/
addChar : function (ch){
var nextState = state; // default: should be overwritten
var tmpRet;
var locErr = null; // local error
var checkErr = function (){
if (mErr !== null){
mErr.handleErr();
mErr = null;
// set correct state of the model
state = determineState();
if (config.debug >= 3)
logStack();
// update gui
oGui.update();
return false;
}
return true;
};
if (isNatural(ch)){
addNum(ch);
if (!checkErr())
return;
nextState = enumState.digit;
} else if (isOperator(ch)){
addOptor(ch);
if (!checkErr())
return;
nextState = enumState.optor;
} else if (ch === 'w'){
addConst(ch);
if (!checkErr())
return;
nextState = enumState.constant;
} else if (ch === '('){
addLeftBracket('(');
if (!checkErr())
return;
numOfOpenBrackets++;
nextState = enumState.leftBracket;
} else if (ch === ')' || ch === '='){
if (state === enumState.leftBracket){
mErr = mErr || new ClassedError("Not allowed to add right bracket here", ClassedError.types.progError);
} else {
calculate(ch ===')');
if (!checkErr())
return;
if (ch === ')'){
nextState = enumState.rightBracket;
} else {
nextState = enumState.calc;
}
}
} else {
mErr = mErr || new ClassedError("Wrong character to add: " + ch, ClassedError.types.progError);
}
if (!checkErr())
return;
state = nextState;
if (config.debug >= 3)
logStack();
oGui.update();
},
/**
* This method performs an operation on the stack that is
* expected when pressing the backspace button. Which operation
* is actually performed depends on the state of the model.
* No return value
* @method backSpace
*/
backSpace: function backSpace(){
var lastStackEl;
// var success = true;
// var errMsg = null;
var errObj = null;
var progErrMsg = "Backspace operation could not be completed. Remove last element.";
var newState = state;
var tmp;
var sLen; // stacklength
switch (state){
case enumState.init:
case enumState.calc:
// errMsg = "The backspace key is not allowed in this context";
// success = false;
errObj = new ClassedError("The backspace key is not allowed in this context", ClassedError.types.progError);
break;
case enumState.digit:
lastStackEl = stack.pop();
tmp = null;
try {
tmp = lastStackEl.getValue(); // a natural number
} catch (e) {
if (e instanceof ClassedError){
errObj = errObj || e;
break;
} else {
throw e; // don't catch other errors than ClassedError
}
// don't push last stack element back to help fixing the error...
}
if (typeof(tmp) !== 'number'){
errObj = new ClassedError(progErrMsg, ClassedError.types.progError);
break;
}
// delete last digit or whole number, if there is only one digit
// (last stack element already popped)
if (tmp < 10){ // the whole operand
sLen = stack.length;
if (sLen <= 0){
newState = enumState.init;
} else {
tmp = stack[sLen-1];
if (tmp instanceof model.Operator){
newState = enumState.optor;
} else if (tmp instanceof model.Bracket){
newState = enumState.leftBracket;
} else {
errObj = new ClassedError(progErrMsg, ClassedError.types.progError);
break;
}
}
} else {
// no natural number division in JS: remove last digit in string representation
tmp = tmp.toString();
tmp = tmp.substring(0, tmp.length-1);
tmp = parseInt(tmp, 10);
lastStackEl = new model.Operand(model.enumType.tNatNum);
try {
lastStackEl.setValue(tmp);
stack.push(lastStackEl);
} catch (e){
if (e instanceof ClassedError){
errObj = errObj || e;
break;
} else {
throw e;
}
}
// state remains the same
}
break;
case enumState.optor:
lastStackEl = stack.pop();
sLen = stack.length;
if (! lastStackEl instanceof model.Operator || sLen <= 0){
errObj = new ClassedError(progErrMsg, ClassedError.types.progError);
} else {
tmp = stack[sLen-1];
if (! tmp instanceof model.Operand){
errObj = new ClassedError(progErrMsg, ClassedError.types.progError);
} else {
if (tmp.getType() === model.enumType.tNatNum){
newState = enumState.digit;
} else {
newState = enumState.constant;
}
}
}
// if (errObj !== null){
// stack.push(lastStackEl);
// }
break;
case enumState.constant: // last element on stack is an operand
case enumState.rightBracket: // last element on stack is a calculated operand
case enumState.stored: // last element on stack is an operand
case enumState.leftBracket: // similar to other cases above
lastStackEl = stack.pop(); // remove last operand
if (! lastStackEl instanceof model.Operand && ! lastStackEl instanceof model.Bracket){
errObj = new ClassedError(progErrMsg, ClassedError.types.progError);
break;
}
if (state === enumState.leftBracket){
numOfOpenBrackets--;
}
// set new state
sLen = stack.length;
if (sLen <= 0){
newState = enumState.init;
} else {
tmp = stack[sLen-1];
if (tmp instanceof model.Bracket){
newState = enumState.leftBracket;
} else if (tmp instanceof model.Operator){
newState = enumState.optor;
} else {
errObj = new ClassedError(progErrMsg, ClassedError.types.progError);
}
}
break;
default:
errObj = new ClassedError(progErrMsg, ClassedError.types.progError);
}
if (errObj === null){ // no error
state = newState;
} else {
errObj.handleErr();
}
if (config.debug >= 3)
logStack();
oGui.update();
},
/**
* Compare the register values and return a DocumentFragment
* containing the html to represent the comparison.
* @method compareRegs
* @return {DocumentFragment} HTML elements representing the comparison. Null if
* Comparison failed. (Note: error is already processed)
*/
compareRegs: function compareRegs(){
updateComparisonData();
if (cmpData === null){
mErr = mErr || new ClassedError("Comparison failed...", ClassedError.progError);
oGui.dbgErr('model.compareRegs: cmpData is null');
}
if (mErr !== null){
mErr.handleErr();
return null;
}
return cmpData.getDataAsHTML();
},
/**
* Returns an array with the two operands (as Ord instances).
* The left operand is placed at (array-)index 0, the right operand
* at index 1. If registers have changed, the comparisonData object
* of the model is updated.
* @method getCmpOpnds
* @return {Array of Ord} the Array
*/
getCmpOpnds: function (){
updateComparisonData();
if (cmpData === null){
mErr = mErr || new ClassedError("Comparison failed...", ClassedError.progError);
oGui.dbgErr('model.compareRegs: cmpData is null');
}
if (mErr !== null){
mErr.handleErr();
return null;
}
return cmpData.getOpnds();
},
/**
* Returns the result of the comparison between the two actual
* register values. If an error occurred, the value null is returned.
* If registers have changed, the comparisonData object
* of the model is updated.
* @method getCmpResult
* @return {number / null} the compare value. If the comparison succeeded,
* there are three possibilities:<br />
* <ul>
* <li>Return value is greater 0: The left operand is greater than the right one.</li>
* <li>Return value is equal to 0: The two operands are equal</li>
* <li>Return value is less than 0: The left operand is less than the right one.</li>
* </ul>
*/
getCmpResult: function (){
updateComparisonData();
if (cmpData === null){
mErr = mErr || new ClassedError("Comparison failed...", ClassedError.progError);
oGui.dbgErr('model.compareRegs: cmpData is null');
}
if (mErr !== null){
mErr.handleErr();
return null;
}
return cmpData.getResult();
},
/**
* Checks the state of the registers.
* @method regsDirty
* @return {boolean} If register values have changed since last call to the
* compareRegs method, then this method will return true.
*/
regsDirty: function regsDirty(){
return regDirty;
}
};
}());
//============================================================== //
//============ Enum model.enumType ============================= //
//============================================================== //
/**
* Enumerable values to define the type of a stack element.
* @property enumType
* @type object literal with number values
*/
model.enumType = {
tUnvalid : 0, // type of the abstract class StackEl: must be overridden!
tOrd : 1, // Elements with instances of Ord
tNatNum : 5, // Elements with natural numbers
tConst : 6, // Elements with constants (omega)
tOptor : 7, // Elements with operators
tBracket : 8 // Elements with left (opening) and right (closing) brackets
};
/**
* Defines an object which holds the data of a comparison of two
* Ord instances.
* @class ComparisonData
* @constructor ComparisionData
* @param {Ord} firstOpnd
* @param {Ord} secondOpnd
*/
model.ComparisonData = function ComparisonData(firstOpnd, secondOpnd){
/**
* @property opnd1 The first operand
* @type {Ord}
*/
this.opnd1 = firstOpnd;
/**
* @property opnd2 The second operand
* @type {Ord}
*/
this.opnd2 = secondOpnd;
util.assert(firstOpnd && secondOpnd && firstOpnd instanceof Ord && secondOpnd instanceof Ord);
/**
* @property result
* @type {number} first operand
*/
this.result = Calculation.compare(firstOpnd, secondOpnd);
};
/**
* Returns the result value if valid or null
* @method getResult
* @return {number / null} the compare value
*/
model.ComparisonData.prototype.getResult = function (){
if (typeof(this.result) === 'number'){
return this.result;
}
oGui.dbgLog("ComparisonData.getResult: the result is unvalid. return null");
return null; // the caller function should care that the user knows that the result is unvalid
};
/**
* Returns an array with the two operands (as Ord instances).
* The left operand is placed at (array-)index 0, the right operand
* at index 1.
* @method getOpnds
* @return {Array of Ord} the Array
*/
model.ComparisonData.prototype.getOpnds = function (){
return new Array( this.opnd1, this.opnd2 );
};
/**
* @method getDataAsHTML
* @return {DocumentFragment} A DocumentFragment with child nodes containing
* the values of the registers and the relation between them.
*/
model.ComparisonData.prototype.getDataAsHTML = function (){
var o1, o2, res;
var rfieldA, rfieldB;
var frag = document.createDocumentFragment();
var cmpValue = this.getResult();
if (cmpValue !== null){
o1 = document.createElement('div');
o1.setAttribute('class', 'cmpOpnd leftOpnd');
o1.innerHTML = '<div>' + this.opnd1.toString(true) + '</div>';
o2 = document.createElement('div');
o2.setAttribute('class', 'cmpOpnd rightOpnd');
o2.innerHTML = '<div>' + this.opnd2.toString(true) + '</div>';
res = document.createElement('div');
res.setAttribute('class', 'cmpRel');
if (this.result < 0){
res.innerHTML = '<div> < </div>';
} else if (this.result === 0){
res.innerHTML = '<div> = </div>';
} else {
res.innerHTML = '<div> > </div>';
}
frag.appendChild(o1);
frag.appendChild(res);
frag.appendChild(o2);
} else {
// an error has occurred, the comparison result is not valid!
res = document.createElement('div');
res.setAttribute('class', 'cmpRel cmpProgError');
res.innerHTML = '<div>Comparison Error</div>';
frag.appendChild(res);
}
return frag;
};