Skip to content
fsm.js 16.2 KiB
Newer Older
var fsm = function () {
    const debugging = false;

    const debug = function (msg) {
	if (debugging) {
	    console.log(msg);
	}
    };

    /**
     * a fsm that tries to match string "abb". see function
     * matchesSubstringAbb.
     */
    const matchAbbMachine = function () {
	const stateWaitForA = "wait for a";
	const stateWaitForFirstB = "wait for 1st b";
	const stateWaitForSecondB = "wait for 2nd b";
	const stateSucceed = "succeed";

	this.currentState = "wait for a";
	this.readch = function (ch) {
	    switch (this.currentState) {
	    case stateWaitForA:
		if (ch === 'a') {
		    this.currentState = stateWaitForFirstB;
		    this.currentState = stateWaitForA;
	    case stateWaitForFirstB:
		if (ch === 'b') {
		    this.currentState = stateWaitForSecondB;
		} else if (ch === 'a') {
		    // do nothing, keep current state.
		} else {
		    this.currentState = stateWaitForA;
	    case stateWaitForSecondB:
		if (ch === 'b') {
		    this.currentState = stateSucceed;
		} else if (ch === 'a') {
		    this.currentState = stateWaitForFirstB;
		    this.currentState = stateWaitForA;
		}
		break;
	    case this.succeed:
		// do nothing, keep current state.
		break;
	    }
	    debug("readch " + ch + ", currentState is " + this.currentState);
	    return this.currentState;
	};
    };

    /**
     * return true if given string has substring "abb" in it.
     * implemented using fsm.
     */
    const matchesSubstringAbb = function (str) {
	const machine = new matchAbbMachine();
	for (i = 0; i < str.length; ++i) {
	    machine.readch(str[i]);
	    if (machine.currentState === "succeed") {
		// console.log("matches");
		return true;
	    }
	}
	return false;
    };

    /**
     * a fsm that implement a RPN calculator. see document in ./operational
     */
    const RPNCalculator = function () {
	// states
    	const stateIdle = "idle";
    	const stateWaitingForNumberOrAction = "waiting for number or action";

Yuanle Song's avatar
Yuanle Song committed
	// define constants for key name literals.
	const keyNames = {
	    num0: "num0",
	    num1: "num1",
	    num2: "num2",
	    num3: "num3",
	    num4: "num4",
	    num5: "num5",
	    num6: "num6",
	    num7: "num7",
	    num8: "num8",
	    num9: "num9",
	    dot: "dot",
	    backspace: "backspace",
	    times: "times",
	    divide: "divide",
	    plus: "plus",
	    minus: "minus",
	    sum_all: "sum-all",
	    square_root: "square-root",
	    power: "power",
	    swap: "swap",
	    undo: "undo",
	    "return": "return",
	    change_sign: "change-sign",
Yuanle Song's avatar
Yuanle Song committed
	    round: "round",
Yuanle Song's avatar
Yuanle Song committed
	// map keyName to short key name.
	const shortKeyNames = {
	    "num0": "0",
	    "num1": "1",
	    "num2": "2",
	    "num3": "3",
	    "num4": "4",
	    "num5": "5",
	    "num6": "6",
	    "num7": "7",
	    "num8": "8",
	    "num9": "9",
	    "dot": ".",
	    "backspace": "bksp",
	    "times": "*",
	    "divide": "/",
	    "plus": "+",
	    "minus": "-",
	    "sum-all": "Σ",
	    "square-root": "sqrt",
	    "power": "pow",
Yuanle Song's avatar
Yuanle Song committed
	    "swap": "swap",
	    "undo": "undo",
	    "return": "ret",
	    "change-sign": "chs",
Yuanle Song's avatar
Yuanle Song committed
	    round: "rond",
Yuanle Song's avatar
Yuanle Song committed
	};
	const msgTooFewElementsOnStack = "Too few elements on stack";
Yuanle Song's avatar
Yuanle Song committed
	const trailHistorySize = 100;
	const undoHistorySize = 100;

    	this.currentState = stateIdle;
    	this.numberStack = [];
    	this.currentNumber = "";
	this.lastError = null;
Yuanle Song's avatar
Yuanle Song committed
	this.trail = new fifo.BoundedQueue(trailHistorySize);
Yuanle Song's avatar
Yuanle Song committed
	this.snapshots = new fifo.BoundedStack(undoHistorySize);

	/**
	 * save current state of this FSM. You can use load to restore the FSM state.
	 */
	this.save = function () {
	    return {
		"currentState": this.currentState,
Yuanle Song's avatar
Yuanle Song committed
		"numberStack": this.numberStack.slice(0),
		"currentNumber": this.currentNumber,
Yuanle Song's avatar
Yuanle Song committed
		"trail": this.trail.clone(),
	    };
	};
	/**
	 * load saved data back to this FSM. current FSM status will be lost.
	 */
	this.load = function (data) {
	    this.currentState = data.currentState;
Yuanle Song's avatar
Yuanle Song committed
	    this.numberStack = data.numberStack.slice(0);
	    this.currentNumber = data.currentNumber;
Yuanle Song's avatar
Yuanle Song committed
	    this.trail = data.trail.clone();
	};
	/**
	 * set error msg. only the last error msg can be fetched via this.lastError.
	 */
	this.setErrorMsg = function (msg) {
	    console.log(msg);
	    this.lastError = msg;
	};
	/**
	 * clear error msg.
	 */
	this.clearErrorMsg = function () {
	    this.lastError = null;
	};
Yuanle Song's avatar
Yuanle Song committed
	/**
	 * do calculator for an unary operator.
	 * this handles not enough element in stack problem.
	 *
	 * it may modify this.numberStack and this.lastError.
	 *
	 * Return true on success, return false on failure.
	 * You may do early return if this function fails. this.lastError will
	 * be set when false is returned.
	 */
	this.doUnaryOperator = function (keyName, func) {
	    this.snapshots.push(this.save());

	    const num = this.numberStack.pop();
	    if (num === undefined) {
		this.setErrorMsg(msgTooFewElementsOnStack);
		this.snapshots.pop();
		return false;
	    }
	    const result = func(num);
	    if (result === undefined || result === null || result === false) {
		// this does consume the number. Just don't push result
		// to number stack.
		if (this.lastError === null) {
		    this.setErrorMsg("result of this operator is undefined or null");
		}
		return false;
	    }
	    this.numberStack.push(result);

	    this.trail.push([shortKeyName(keyName), result]);
	    return true;
	};
	/**
	 * do calculation for a binary operator.
	 * this handles not enough element in stack problem.
	 *
	 * it may modify this.numberStack and this.lastError.
	 *
	 * Return true on success, return false on failure.
	 * You may do early return if this function fails. this.lastError will
	 * be set when false is returned.
	 */
Yuanle Song's avatar
Yuanle Song committed
	this.doBinaryOperator = function (keyName, func) {
Yuanle Song's avatar
Yuanle Song committed
	    this.snapshots.push(this.save());

	    const num2 = this.numberStack.pop();
	    if (num2 === undefined) {
		this.setErrorMsg(msgTooFewElementsOnStack);
Yuanle Song's avatar
Yuanle Song committed
		this.snapshots.pop();
		return false;
	    }
	    const num1 = this.numberStack.pop();
	    if (num1 === undefined) {
		this.setErrorMsg(msgTooFewElementsOnStack);
		this.numberStack.push(num2);
Yuanle Song's avatar
Yuanle Song committed
		this.snapshots.pop();
		return false;
	    }
	    const result = func(num1, num2);
	    if (result === undefined || result === null || result === false) {
		// this does consume the two numbers. Just don't push result
		// to number stack.
		if (this.lastError === null) {
		    this.setErrorMsg("result of this operator is undefined or null");
		}
		return false;
	    }
	    this.numberStack.push(result);
Yuanle Song's avatar
Yuanle Song committed

	    this.trail.push([shortKeyName(keyName), result]);

	/**
	 * return true if given keyName is one of number keys.
	 * number keys include num0-9 and dot.
	 */
	const isNumberKey = function (keyName) {
	    return (keyNames[keyName] !== undefined) && (keyName.startsWith("num") || keyName === keyNames.dot);
	};
	/**
	 * operator key names.
	 */
	const operatorKeyList = [
	    keyNames.change_sign,
	    keyNames.plus, keyNames.minus, keyNames.times, keyNames.divide,
	    keyNames.swap, keyNames.undo, keyNames.sum_all, keyNames.square_root,
Yuanle Song's avatar
Yuanle Song committed
	    keyNames.power, keyNames.round,
	];
	const isOperator = function (keyName) {
	    return operatorKeyList.indexOf(keyName) !== -1;
	};
	/**
	 * keys that is not a simple operator.
	 */
	const miscKeyList = [keyNames["return"], keyNames.undo, keyNames.backspace];
	const isMiscKey = function (keyName) {
	    return miscKeyList.index(keyName) !== -1;
	};
	/**
	 * convert number key to a number string.
	 */
	const numKeyToNumString = function (keyName) {
	    console.assert(isNumberKey(keyName), "not a number key: " + keyName);
	    if (keyName === keyNames.dot) {
		return ".";
	    } else {
		return keyName.substring(3);
	    }
	};
	/**
	 * press keyName when existing number string is oldNumberString.
	 * return [newNumberString, errMsg]
	 * errMsg will be null if there is no error.
	 */
	const changeNumberString = function (keyName, oldNumberString) {
	    if (oldNumberString === "0" && keyName === keyNames.num0) {
		return [oldNumberString, "more 0s are ignored"];
	    } else if (oldNumberString === "" && keyName === keyNames.dot) {
		return ["0.", null];
	    } else if (keyName === keyNames.dot && oldNumberString.indexOf(".") !== -1) {
		// Note: do not use indexOf(keyNames.dot) in if condition,
		// because the keyName is dot, not ".".
		return [oldNumberString, "more dots are ignored"];
	    } else {
		return [oldNumberString + numKeyToNumString(keyName), null];
	    }
	};
	const testChangeNumberString = function () {
	    var r = changeNumberString(keyNames.dot, "");
	    console.assert(r[0] === "0.");
	    console.assert(r[1] === null);

	    r = changeNumberString(keyNames.num1, "");
	    console.assert(r[0] === "1");
	    console.assert(r[1] === null);

	    r = changeNumberString(keyNames.num1, "2");
	    console.assert(r[0] === "21");
	    console.assert(r[1] === null);

	    r = changeNumberString(keyNames.num0, "0");
	    console.assert(r[0] === "0");
	    console.assert(r[1] !== null);

	    r = changeNumberString(keyNames.num0, "12");
	    console.assert(r[0] === "120");
	    console.assert(r[1] === null);

	    r = changeNumberString(keyNames.dot, "1.2");
	    console.assert(r[0] === "1.2");
	    console.assert(r[1] !== null);
	};
Yuanle Song's avatar
Yuanle Song committed
	/**
	 * return short key name for given keyName.
	 * this is used to show operator in shorter form in trails.
	 */
	const shortKeyName = function (keyName) {
	    return shortKeyNames[keyName];
	};
Yuanle Song's avatar
Yuanle Song committed
	const printSnapshots = function (snapshots) {
	    debug("printSnapshots()");
	    const data = snapshots.data;
	    const l = data.length;
	    debug("len=" + l);
	    var i;
	    for (i = 0; i < l; ++i) {
		debug("snapshot[" + i + "]: " + data[i].currentState + ", " + data[i].numberStack);
	    }
	};
	const printSnapshot = function (snapshot) {
	    const msg = "snapshot: " + snapshot.currentState + ", " + snapshot.numberStack;
	    debug(msg);
	    return msg;
	};
    	this.sendKey = function (keyName) {
	    var num, num1, num2;
	    var numString, errMsg;
	    var r;

    	    switch (this.currentState) {
    	    case stateIdle:
    		if (isNumberKey(keyName)) {
		    r = changeNumberString(keyName, this.currentNumber);
		    numString = r[0];
		    errMsg = r[1];
		    if (errMsg === null) {
			this.currentNumber = numString;
Yuanle Song's avatar
Yuanle Song committed
			// change state
			this.currentState = stateWaitingForNumberOrAction;
Yuanle Song's avatar
Yuanle Song committed
			// idle state always succeed on number key input.
			console.assert(false, "this should not happen");
			this.setErrorMsg(errMsg);
Yuanle Song's avatar
Yuanle Song committed
			return;
		} else {
		    switch (keyName) {
Yuanle Song's avatar
Yuanle Song committed
		    case keyNames.change_sign:
Yuanle Song's avatar
Yuanle Song committed
			if (! this.doUnaryOperator(keyName, (num) => -num)) {
Yuanle Song's avatar
Yuanle Song committed
		    case keyNames.round:
			if (! this.doUnaryOperator(keyName, (num) => Math.round(num))) {
Yuanle Song's avatar
Yuanle Song committed
		    	break;
		    case keyNames.square_root:
			if (! this.doUnaryOperator(keyName, (num) => {
			    if (num < 0) {
				// consumes the number and show error to user.
				this.setErrorMsg("square root of negative not supported");
				this.snapshots.pop();
				return;
			    }
			    return Math.pow(num, 0.5);
			})) {
Yuanle Song's avatar
Yuanle Song committed
		    case keyNames.plus:
			if (! this.doBinaryOperator(keyName, (lhs, rhs) => lhs + rhs)) {
Yuanle Song's avatar
Yuanle Song committed
		    case keyNames.minus:
			if (! this.doBinaryOperator(keyName, (lhs, rhs) => lhs - rhs)) {
		    	break;
		    case keyNames.times:
Yuanle Song's avatar
Yuanle Song committed
			if (! this.doBinaryOperator(keyName, (lhs, rhs) => lhs * rhs)) {
		    	break;
		    case keyNames.divide:
Yuanle Song's avatar
Yuanle Song committed
			if (! this.doBinaryOperator(keyName, (lhs, rhs) => {
			    if (rhs === 0) {
				this.setErrorMsg("Division by 0");
				return null;
			    }
			    return lhs / rhs;
			})) {
			    return;
			}
		    case keyNames.power:
			if (! this.doBinaryOperator(keyName, (lhs, rhs) => Math.pow(lhs, rhs))) {
		    case keyNames.swap:
Yuanle Song's avatar
Yuanle Song committed
			this.snapshots.push(this.save());
		    	num2 = this.numberStack.pop();
			if (num2 === undefined) {
			    this.setErrorMsg(msgTooFewElementsOnStack);
Yuanle Song's avatar
Yuanle Song committed
			    this.snapshots.pop();
		    	num1 = this.numberStack.pop();
			if (num1 === undefined) {
			    this.setErrorMsg(msgTooFewElementsOnStack);
			    this.numberStack.push(num2);
Yuanle Song's avatar
Yuanle Song committed
			    this.snapshots.pop();
		    	this.numberStack.push(num2);
		    	this.numberStack.push(num1);
Yuanle Song's avatar
Yuanle Song committed
			// no changes on trail
		    	break;
		    case keyNames['return']:
			// duplicate number
Yuanle Song's avatar
Yuanle Song committed
			this.snapshots.push(this.save());
			num = this.numberStack.pop();
			if (num === undefined) {
			    this.setErrorMsg(msgTooFewElementsOnStack);
Yuanle Song's avatar
Yuanle Song committed
			    this.snapshots.pop();
			this.numberStack.push(num);
			this.numberStack.push(num);
Yuanle Song's avatar
Yuanle Song committed
			// no changes on trail.
		    case keyNames.backspace:
			// drop one number on stack
Yuanle Song's avatar
Yuanle Song committed
			this.snapshots.push(this.save());
			num = this.numberStack.pop();
			if (num === undefined) {
			    this.setErrorMsg(msgTooFewElementsOnStack);
Yuanle Song's avatar
Yuanle Song committed
			    this.snapshots.pop();
		    case keyNames.sum_all:
			// sum all numbers of stack
			this.snapshots.push(this.save());
			r = 0;
			num = this.numberStack.pop();
			if (num === undefined) {
			    this.setErrorMsg(msgTooFewElementsOnStack);
			    this.snapshots.pop();
			    return;
			}
			while (num !== undefined) {
			    r += num;
			    num = this.numberStack.pop();
			}
			this.numberStack.push(r);
			this.trail.push([shortKeyName(keyName), r]);
			break;
Yuanle Song's avatar
Yuanle Song committed
		    case keyNames.undo:
			printSnapshots(this.snapshots);

			r = this.snapshots.pop();
			if (r === undefined) {
			    this.setErrorMsg("No more undo history!");
			    return;
			} else {
			    console.log("undo: load saved state: " + printSnapshot(r));
			    this.load(r);
			    this.setErrorMsg("Undo!");
			    // this is a success msg, no need to do early return.
			}
			break;
		    default:
		    	console.assert(false, "action not implemented, key is " + keyName);
		    }
		}
    		break;
    	    case stateWaitingForNumberOrAction:
    		if (isNumberKey(keyName)) {
		    r = changeNumberString(keyName, this.currentNumber);
		    numString = r[0];
		    errMsg = r[1];
		    if (errMsg === null) {
			this.currentNumber = numString;
		    } else {
			this.setErrorMsg(errMsg);
Yuanle Song's avatar
Yuanle Song committed
			return;
		    // keep current state
		} else if (keyName === keyNames['return']) {
Yuanle Song's avatar
Yuanle Song committed
		    // commit number to stack
		    num = parseFloat(this.currentNumber);
		    this.currentNumber = "";
		    this.currentState = stateIdle;
Yuanle Song's avatar
Yuanle Song committed

Yuanle Song's avatar
Yuanle Song committed
		    r = this.save();
		    // console.log("save a snapshot: " + r.currentState
		    // 		+ ", " + r.numberStack + ", " + r.currentNumber);
		    this.snapshots.push(r);

		    this.numberStack.push(num);
Yuanle Song's avatar
Yuanle Song committed
		    this.trail.push([null, num]);
		} else if (keyName === keyNames.backspace) {
		    if (this.currentNumber.length > 0) {
			this.currentNumber = this.currentNumber.substring(0, this.currentNumber.length - 1);
                        if (this.currentNumber === "") {
                            this.currentNumber = "0";
                        }
			this.setErrorMsg("no digits to delete");
		    }
		} else if (isOperator(keyName)) {
		    // commit number
		    console.log("send return key automatically");
		    this.sendKey(keyNames['return']);
		    console.assert(this.currentState === stateIdle,
				   "auto commit number should set state to idle");
		    // consume and resend this key. notice the early return.
		    console.log("resend key " + keyName);
		    return this.sendKey(keyName);
		} else {
		    switch (keyName) {
		    default:
			console.assert(false, "action not implemented, key is " + keyName);
		    }
		}
    		break;
	    default:
		console.assert(false, "unknown state: " + this.currentState);
    	    }
    	    debug("sendKey " + keyName + ", currentState is " + this.currentState);
	    this.clearErrorMsg();
    	    return this.currentState;
    	};
	this.clearTrail = function () {
	    this.trail.clear();
	};
	this.clearStack = function () {
	    this.numberStack = [];
	};
	this.reset = function () {
	    this.currentState = stateIdle;
    	    this.numberStack = [];
    	    this.currentNumber = "";
	    this.lastError = null;
	    this.trail = new fifo.BoundedQueue(trailHistorySize);
	};
	const runTests = function () {
	    testChangeNumberString();
	};

	// run all tests;
	runTests();
	matchesSubstringAbb: matchesSubstringAbb,
	RPNCalculator: RPNCalculator,