Skip to content
fsm.js 7.04 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";

	// keynames
	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",
	    swap: "swap",
	    undo: "undo",
	    "return": "return",
	    change_sign: "change-sign",
	};

    	this.currentState = stateIdle;
    	this.numberStack = [];
    	this.currentNumber = "";
	this.lastError = null;

	/**
	 * save current state of this FSM. You can use load to restore the FSM state.
	 */
	this.save = function () {
	    return {
		"currentState": this.currentState,
		"numberStack": this.numberStack,
		"currentNumber": this.currentNumber,
	    };
	};
	/**
	 * load saved data back to this FSM. current FSM status will be lost.
	 */
	this.load = function (data) {
	    this.currentState = data.currentState;
	    this.numberStack = data.numberStack;
	    this.currentNumber = data.currentNumber;
	};
	/**
	 * set error msg. only the last error msg can be fetched via this.lastError.
	 */
	this.setErrorMsg = function (msg) {
	    console.log(msg);
	    this.lastError = msg;
	};

	/**
	 * return true if given keyName is one of number keys.
	 * number keys include num0-9 and dot.
	 */
	const isNumberKey = function (keyName) {
	    return keyName.startsWith("num") || keyName === keyNames.dot;
	};
	/**
	 * operator key names.
	 */
	const operatorKeyList = [
	    keyNames.change_sign,
	    keyNames.plus, keyNames.minus, keyNames.times, keyNames.divide,
	    keyNames.swap,
	];
	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);
	    }
	};
    	this.sendKey = function (keyName) {
	    var num, num1, num2;

    	    switch (this.currentState) {
    	    case stateIdle:
    		if (isNumberKey(keyName)) {
		    // TODO handle errors and special cases
		    this.currentNumber += numKeyToNumString(keyName);
		    this.currentState = stateWaitingForNumberOrAction;
		} else {
		    switch (keyName) {
		    case keyNames.plus:
		    	this.numberStack.push(this.numberStack.pop() + this.numberStack.pop());
		    	break;
		    case keyNames.minus:
			num2 = this.numberStack.pop();
			num1 = this.numberStack.pop();
		    	this.numberStack.push(num1 - num2);
		    	break;
		    case keyNames.change_sign:
		    	this.numberStack.push(-this.numberStack.pop());
		    	break;
		    case keyNames.times:
		    	this.numberStack.push(this.numberStack.pop() * this.numberStack.pop());
		    	break;
		    case keyNames.divide:
			num2 = this.numberStack.pop();
			num1 = this.numberStack.pop();
		    	this.numberStack.push(num1 / num2);
		    	break;
		    case keyNames.swap:
		    	num2 = this.numberStack.pop();
		    	num1 = this.numberStack.pop();
		    	this.numberStack.push(num2);
		    	this.numberStack.push(num1);
		    	break;
		    case keyNames['return']:
			// duplicate number
			num = this.numberStack.pop();
			this.numberStack.push(num);
			this.numberStack.push(num);
		    	break;
		    default:
		    	console.assert(false, "action not implemented, key is " + keyName);
		    }
		}
    		break;
    	    case stateWaitingForNumberOrAction:
    		if (isNumberKey(keyName)) {
		    // TODO handle errors and special cases
		    this.currentNumber += numKeyToNumString(keyName);
		    // keep current state
		} else if (keyName === keyNames['return']) {
		    this.numberStack.push(parseFloat(this.currentNumber));
		    this.currentNumber = "";
		    this.currentState = stateIdle;
		} else if (keyName === keyNames.backspace) {
		    if (this.currentNumber.length > 0) {
			this.currentNumber = this.currentNumber.substring(0, this.currentNumber.length - 1);
		    } else {
			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);
    	    return this.currentState;
    	};
    };

	matchesSubstringAbb: matchesSubstringAbb,
	RPNCalculator: RPNCalculator,