UNPKG

simple-dfa

Version:

Allow construction, validation, and manual step-through of a simple DFA.

538 lines (476 loc) 20.7 kB
"use strict"; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var uuid = require("uuid"); var clone = require("clone"); /** * A Deterministic Finite Automata that allows omission of transitions. * The machine will simply reject upon inability to transition or accept. * It allows manual control of operations via the start and step methods. * ** ** Example Construction: ** ** new SimpleDFA( ** ["1","2","3","4"], ** ["a", "b", "c"], ** { ** 1: {'a': '2', 'b': '2', 'c': '3'}, ** 2: {'b': '3', 'c': '3'}, ** 3: {'a': '1', 'b': '1', 'c': '1'}, ** }, ** "1", ** ["2","3","4"] ** ); * * @class SimpleDFA */ var SimpleDFA = function () { /** * Creates an instance of SimpleDFA. Any symbols or states specified in transitions * but not elsewhere will be added. * * @param {Array} states - set of ids for states * @param {Array} alphabet - set of characters to include in alphabet * @param {Object} transitions - object mapping source state + symbol -> destination state * @param {String} startState - member of states to be marked as start * @param {Array} acceptStates - subset of states to be marked as accept * * @throws if states, alphabet, or acceptStates isn't an Array * @throws if states is defined but startState isn't * @throws if states is defined but startState isn't a member * @throws if acceptStates isn't a subset of states * * @throws if states contains non-string or duplicate ID * @throws if alphabet contains non-character or duplicate symbol */ function SimpleDFA() { var states = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; var alphabet = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; var transitions = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; var _this = this; var startState = arguments[3]; var acceptStates = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : []; _classCallCheck(this, SimpleDFA); // Check parameter types if (!(states instanceof Array)) throw "SimpleDFA states parameter must be an array."; if (!(alphabet instanceof Array)) throw "SimpleDFA alphabet parameter must be an array."; if (!(acceptStates instanceof Array)) throw "SimpleDFA acceptStates parameter must be an array."; if (states.length > 0 && !startState) throw "If SimpleDFA states parameter exists, then start state must be specified."; // Check and create states object this.states = {}; states.forEach(function (id) { if (!(typeof id === "string")) throw "SimpleDFA states parameter must contain only State IDs."; if (_this.states[id]) throw "SimpleDFA states parameter contains states with duplicate id: " + id; _this.states[id] = { start: startState === id, accept: acceptStates.includes(id) }; }); if (states.length > 0 && !this.states[startState]) throw "SimpleDFA states parameter doesn't contain specified start state: " + startState; this.startState = startState; // Check and clone alphabet var temp = {}; alphabet.forEach(function (symbol) { if (typeof symbol !== "string" || symbol.length !== 1) throw "SimpleDFA alphabet parameter must only contain single-letter strings."; if (temp[symbol]) throw "SimpleDFA alphabet parameter contains duplicate symbol: " + symbol + ".";else temp[symbol] = true; }); this.alphabet = clone(alphabet); // Check and create transitions object temp = {}; Object.keys(transitions).forEach(function (sourceStateID) { if (!_this.states[sourceStateID]) addState(sourceStateID); Object.keys(transitions[sourceStateID]).forEach(function (symbol) { if (!_this.states[transitions[sourceStateID][symbol]]) addState(transitions[sourceStateID][symbol]); if (!_this.alphabet.includes(symbol)) _this.alphabet.push(symbol); }); }); this.transitions = clone(transitions); } /** * Add a symbol to the SimpleDFA's alphabet. * * @param {String} symbol - character to add to alphabet * @returns true if alphabet was modified * @throws if symbol is not a single character * @throws if SimpleDFA is currently running on an input */ _createClass(SimpleDFA, [{ key: "addSymbol", value: function addSymbol(symbol) { if (this.input || this.active || this.position) throw "Cannot edit SimpleDFA while running on an input."; if (typeof symbol !== "string" || symbol.length !== 1) throw "Only allowed to add single characters to alphabet."; var result = false; if (!this.alphabet.includes(symbol)) { this.alphabet.push(symbol); result = true; } if (result && this.observer) { this.observer.notify(); } return result; } /** * Remove a symbol from the SimpleDFA's alphabet. * If it is the trigger for any transitions then remove those as well. * * @param {String} symbol - character to remove from alphabet * @returns true if alphabet/transitions were modified * @throws if symbol is not a single character * @throws if SimpleDFA is currently running on an input */ }, { key: "removeSymbol", value: function removeSymbol(symbol) { var _this2 = this; if (this.input || this.active || this.position) throw "Cannot edit SimpleDFA while running on an input."; if (typeof symbol !== "string" || symbol.length !== 1) throw "Only allowed to remove single characters from alphabet."; var result = false; if (this.alphabet.includes(symbol)) { this.alphabet.splice(this.alphabet.indexOf(symbol), 1); // remove from alphabet result = true; Object.keys(this.transitions).forEach(function (sourceStateID) { // Delete trigger from transition delete _this2.transitions[sourceStateID][symbol]; // Delete transition if it contains no triggers if (Object.keys(_this2.transitions[sourceStateID]).length === 0) delete _this2.transitions[sourceStateID]; }); } if (result && this.observer) { this.observer.notify(); } return result; } /** * Add a state to the SimpleDFA by ID. If state already exists then nothing happens. * If states was empty then this state will be marked as the start state. * * @param {String} stateID - id of state to add * @returns true if states was modified * @throws if stateID is non-string * @throws if SimpleDFA is currently running on an input */ }, { key: "addState", value: function addState(stateID) { if (this.input || this.active || this.position) throw "Cannot edit SimpleDFA while running on an input."; if (typeof stateID !== "string") throw "Only allowed to add states by ID."; var result = false; if (!this.states[stateID]) { result = true; if (!this.startState) this.startState = stateID; this.states[stateID] = { start: this.startState === stateID, accept: false }; } if (result && this.observer) { this.observer.notify(); } return result; } /** * Mark specified state as the start state. * * @param {String} stateID - id of state to mark as start * @returns true if startState was modified * @throws if stateID is non-string * @throws if state with stateID doesn't exist * @throws if SimpleDFA is currently running on an input */ }, { key: "markStart", value: function markStart(stateID) { if (this.input || this.active || this.position) throw "Cannot edit SimpleDFA while running on an input."; if (typeof stateID !== "string") throw "Can only mark states as start by ID."; if (!this.states[stateID]) throw "State " + stateID + " doesn't exist; cannot mark as start."; var result = false; if (this.startState !== stateID) { result = true; this.states[stateID].start = true; this.states[this.startState].start = false; this.startState = stateID; if (this.observer) { this.observer.notify(); } } return result; } /** * Toggle the accept property of specified state. * * @param {String} stateID - id of state to toggle accept property * @returns true always * @throws if stateID is non-string * @throws if state with stateID doesn't exist * @throws if SimpleDFA is currently running on an input */ }, { key: "toggleAccept", value: function toggleAccept(stateID) { if (this.input || this.active || this.position) throw "Cannot edit SimpleDFA while running on an input."; if (typeof stateID !== "string") throw "Can only mark states as accept by ID."; if (!this.states[stateID]) throw "State " + stateID + " doesn't exist; cannot toggle its accept."; this.states[stateID].accept = !this.states[stateID].accept; if (this.observer) { this.observer.notify(); } return true; } /** * Check if specified state is connected to any others. * If state doesn't exist in graph simply returns false. * (Helpful to check if confirmation needed to delete). * * @param {String} stateID - id of state of which to check connectivity * @returns true if state with stateID is source or destination of any transitions * @throws if stateID is non-string */ }, { key: "isConnected", value: function isConnected(stateID) { var _this3 = this; if (typeof stateID !== "string") throw "Can only check state's connections by state ID."; var result = false; Object.keys(this.transitions).forEach(function (sourceStateID) { if (sourceStateID === stateID) result = true; // State is source of transition else { Object.keys(_this3.transitions[sourceStateID]).forEach(function (symbol) { if (_this3.transitions[sourceStateID][symbol] === stateID) result = true; // State is destination of transition }); } }); return result; } /** * Remove specified state from the graph. Removing the start state is * not allowed. Has side effect of removing any transitions to/from this state. * * @param {String} stateID - id of state to remove * @returns true if graph was modified * @throws if stateID is non-string * @throws if state with stateID is start state * @throws if SimpleDFA is currently running on an input */ }, { key: "removeState", value: function removeState(stateID) { var _this4 = this; if (this.input || this.active || this.position) throw "Cannot edit SimpleDFA while running on an input."; if (typeof stateID !== "string") throw "Can only remove states by ID."; if (this.startState === stateID) throw "Cannot delete start state."; var result = false; if (this.states[stateID]) { result = true; delete this.states[stateID]; // Delete transitions from state delete this.transitions[stateID]; // Delete transitions to state Object.keys(this.transitions).forEach(function (sourceStateID) { Object.keys(_this4.transitions[sourceStateID]).forEach(function (symbol) { if (_this4.transitions[sourceStateID][symbol] === stateID) delete _this4.transitions[sourceStateID][symbol]; }); }); } if (result && this.observer) { this.observer.notify(); } return result; } /** * Add a transition to the SimpleDFA. If the source, destination, or symbol * don't exist in the graph then they are added. * * @param {string} source - source state * @param {string} dest - destination state * @param {string} symbol - single character to trigger transition * @returns true if graph was modified * @throws if source or dest is non-string * @throws if symbol is not a single character * @throws if SimpleDFA is currently running on an input */ }, { key: "addTransition", value: function addTransition(source, dest, symbol) { if (this.input || this.active || this.position) throw "Cannot edit SimpleDFA while running on an input."; if (typeof source !== "string") throw "Can only add transitions between states by state ID."; if (typeof dest !== "string") throw "Can only add transitions between states by state ID."; if (typeof symbol !== "string" || symbol.length !== 1) throw "Only allowed use single symbol as trigger for transition."; if (!this.states[source]) this.addState(source); if (!this.states[dest]) this.addState(dest); if (!this.alphabet.includes(symbol)) this.addSymbol(symbol); if (!this.transitions[source]) this.transitions[source] = {}; var result = this.transitions[source][symbol] !== dest; this.transitions[source][symbol] = dest; if (result && this.observer) { this.observer.notify(); } return result; } /** * Remove a transition from the SimpleDFA. If the source, destination, symbol, * or transition doesn't exist in the graph then nothing happens and false is returned. * * @param {string} source - source state * @param {string} dest - destination state * @param {string} symbol - single character that triggered transition * @returns true if graph was modified * @throws if source or dest is non-string * @throws if symbol is not a single character * @throws if SimpleDFA is currently running on an input */ }, { key: "removeTransition", value: function removeTransition(source, dest, symbol) { if (this.input || this.active || this.position) throw "Cannot edit SimpleDFA while running on an input."; if (typeof source !== "string") throw "Can only remove transitions between states by state ID."; if (typeof dest !== "string") throw "Can only remove transitions between states by state ID."; if (typeof symbol !== "string" || symbol.length !== 1) throw "Only recognized single symbols as triggers for transition."; var result = false; if (this.transitions[source] && this.transitions[source][symbol] && this.transitions[source][symbol] === dest) { result = true; delete this.transitions[source][symbol]; if (Object.keys(this.transitions[source]).length === 0) delete this.transitions[source]; } if (result && this.observer) { this.observer.notify(); } return result; } /** * Tests if the input contains only symbols in this SimpleDFA's alphabet. * @param {String} input - string to check validity of * @returns true if input is valid for this SimpleDFA * @throws if input is non-string */ }, { key: "validateInput", value: function validateInput(input) { if (typeof input !== "string") throw "Can only test validity of strings."; return input.replace(new RegExp(this.alphabet.reduce(function (a, e) { return a + '|' + e; }), 'g'), '').length === 0; } /** * Starts the SimpleDFA with the given input string. The active state will be the start * state, and no transitions will be made until step() is called. * * @param {String} input - string to feed into SimpleDFA * @returns ** { ** state: (start state), ** symbol: (first symbol), ** accept: (if start state is accept) ** } * @throws if input is invalid * @throws if SimpleDFA is already running on an input */ }, { key: "start", value: function start(input) { if (this.input || this.active || this.position) throw "Cannot start SimpleDFA while already running on an input."; if (!this.validateInput(input)) throw "Input is invalid: " + input; this.input = input; this.position = 0; this.active = { state: this.startState, symbol: this.input[this.position], accept: this.states[this.startState].accept }; var result = clone(this.active); if (this.observer) { this.observer.notify(result); } return result; } /** * Stops the SimpleDFA, allowing edits to be made. * If it wasn't running then nothing happens. */ }, { key: "stop", value: function stop() { this.input = this.active = this.position = undefined; if (this.observer) { this.observer.notify({}); } } /** * Perform a single step in the SimpleDFA on the given input string. * ** Returns: ** If active is transition (active.source is defined): ** { ** state: (destination of transition), ** symbol: (next symbol), ** accept: (destination's accept property) ** } ** If active is a state: ** And symbols remain in input: ** And transition exists for next symbol: ** { ** source: (source of transition), ** symbol: (symbol used to enter transition), ** } ** And no transition exists for next symbol: ** { ** state: (state where failed), ** symbol: (symbol with no transition), ** reject: true, ** } ** And no symbols remain in input: ** { ** <previous-active> (endless steps just return state where succeeded or failed) ** } * * Note that SimpleDFA has reached end of input when result's symbol is undefined. * The final state and acceptance policy can be found in the result. * * If the SimpleDFA lacks a transition required by the input then reject is set in result. * The final state and symbol can be found in the result. * * @returns value as specified above * @throws if SimpleDFA is not running */ }, { key: "step", value: function step() { if (this.input === undefined || !this.active || this.position === undefined) throw "Cannot perform step when SimpleDFA isn't running."; var result = this.active; // Currently in transition if (this.active.source) { var dest = this.transitions[this.active.source][this.active.symbol]; result = { state: dest, // destination symbol: this.input[++this.position], // undefined if input was accepted accept: this.states[dest].accept }; } // Currently in state, with symbols to read else if (!result.reject && this.position < this.input.length) { // Transition exists if (this.transitions[this.active.state] && this.transitions[this.active.state][this.active.symbol]) { result = { source: this.active.state, symbol: this.active.symbol }; } // Transition doesn't exist else { result = { state: this.active.state, symbol: this.active.symbol, reject: true }; } } // If in state but no symbols to return then previous active is returned. this.active = clone(result); if (this.observer) { this.observer.notify(result); } return result; } }, { key: "registerObserver", value: function registerObserver(observer) { this.observer = observer; } }]); return SimpleDFA; }(); module.exports = SimpleDFA;