simple-dfa
Version:
Allow construction, validation, and manual step-through of a simple DFA.
538 lines (476 loc) • 20.7 kB
JavaScript
"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;