Skip to content

Commit

Permalink
naming improvements; starting to break util functions out into pure m…
Browse files Browse the repository at this point in the history
…odule; testing improvements
  • Loading branch information
StoneCypher committed May 27, 2017
1 parent 7dd738f commit 7a9ec47
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 69 deletions.
8 changes: 4 additions & 4 deletions package.json
@@ -1,15 +1,15 @@
{
"name": "jssm",
"version": "1.2.0",
"version": "1.4.0",
"engines": {
"node": ">=6.0.0"
},
"description": "A Javascript state machine with a simple API. Well tested, and typed with Flowtype. MIT License.",
"main": "dist/jssm.es5.browserified.js",
"scripts": {
"nyc-test": "nyc ava src/js/jssm-tests.js",
"test": "ava src/js/jssm-tests.js",
"test-verbose": "ava src/js/jssm-tests.js -v",
"nyc-test": "nyc ava src/js/jssm*-tests.js",
"test": "ava src/js/jssm*-tests.js",
"test-verbose": "ava src/js/jssm*-tests.js -v",
"clean": "rimraf build -f && rimraf dist -f",
"babel": "babel src/js -d build/",
"setver": "node ./set_version.js",
Expand Down
18 changes: 13 additions & 5 deletions src/js/jssm-tests.js
Expand Up @@ -33,7 +33,7 @@ describe('Simple stop light', async it => {
it(`has state "${c}"`, t => t.is(r_states.includes(c), true))
);

const r_names = light.named_transitions();
const r_names = light.list_named_transitions();
it('has the right named transition count', t => t.is(r_names.size, 3));
trs.map(t => t.name).map(a =>
it(`has named transition "${a}"`, t => t.is(r_names.has(a), true))
Expand Down Expand Up @@ -163,9 +163,9 @@ describe('Complex stop light', async it => {
{ action:'power_off', from:'yellow', to:'off' },
{ action:'power_off', from:'green', to:'off' },

{ name:'switch_warn', from:'green', to:'yellow' },
{ name:'switch_halt', from:'yellow', to:'red' },
{ name:'switch_go', from:'red', to:'green' }
{ name:'switch_warn', action:'proceed', from:'green', to:'yellow' },
{ name:'switch_halt', action:'proceed', from:'yellow', to:'red' },
{ name:'switch_go', action:'proceed', from:'red', to:'green' }

]

Expand All @@ -177,18 +177,23 @@ describe('Complex stop light', async it => {
it(`has state "${c}"`, t => t.is(r_states.includes(c), true))
);

const r_names = light2.named_transitions();
const r_names = light2.list_named_transitions();
it('has the right named transition count', t => t.is(r_names.size, 4));
['turn_on', 'switch_warn', 'switch_halt', 'switch_go'].map(a =>
it(`has named transition "${a}"`, t => t.is(r_names.has(a), true))
);

it('has the right exit actions for red', t => t.deepEqual(['power_off', 'proceed'], light2.list_exit_actions_for('red')));


it.describe('- `transition` walkthrough', async it2 => {

it2('machine starts off', t => t.is("off", light2.state()));
it2('off refuses green', t => t.is(false, light2.transition('green')));
it2('off refuses yellow', t => t.is(false, light2.transition('yellow')));

it2('off refuses proceed', t => t.is(false, light2.action('proceed')));

it2('off accepts red', t => t.is(true, light2.transition('red')));
it2('off is now red', t => t.is("red", light2.state()));
it2('red refuses yellow', t => t.is(false, light2.transition('yellow')));
Expand All @@ -213,6 +218,9 @@ describe('Complex stop light', async it => {
it2('yellow accepts red', t => t.is(true, light2.transition('red')));
it2('back to red', t => t.is("red", light2.state()));

it2('proceed is true', t => t.is(true, light2.action('proceed')));
it2('light is now green', t => t.is("green", light2.state()));

});

});
Expand Down
16 changes: 16 additions & 0 deletions src/js/jssm-util-tests.js
@@ -0,0 +1,16 @@

import {test, describe} from 'ava-spec';

const jssm = require('../../build/jssm.es5.js');





describe('seq/1', async it => {

it('(0) generates []', t => t.deepEqual([], jssm.seq(0) ));
it('(1) generates [0]', t => t.deepEqual([0], jssm.seq(1) ));
it('(2) generates [0,1]', t => t.deepEqual([0,1], jssm.seq(2) ));

});
55 changes: 55 additions & 0 deletions src/js/jssm-util.js
@@ -0,0 +1,55 @@

const rand_select = (options : Array<any>, probability_property : string = 'probability') => {

if (!Array.isArray(options)) { throw new TypeError('options must be a non-empty array of objects'); }
if (!(typeof options[0] === 'object')) { throw new TypeError('options must be a non-empty array of objects'); }

const frand = cap => Math.random() * cap,
prob_sum = options.reduce( (acc, val:any) => acc + val[probability_property], 0 ),
rnd = frand(prob_sum);

var cursor = 0,
cursor_sum = 0;

while ((cursor_sum += (options:any)[cursor++][probability_property]) <= rnd) { }
return options[cursor-1];

};





const seq = (n : number) =>

(new Array(n)).fill(true).map( (_,i) => i );





const histograph = (a : Array<any>) =>

a.sort().reduce( (m,v) => ( m.set(v, (m.has(v)? m.get(v)+1 : 1)) , m), new Map() );





const sample_select = (n : number, options : Array<mixed>, probability_property : string) =>

seq(n).map(i => rand_select(options, probability_property));





const histo_key = (n : number, options : Array<mixed>, probability_property : string, extract : string) =>

histograph(sample_select(n, options, probability_property).map( (s:any) => s[extract]));





export { seq, histograph, histo_key, rand_select, sample_select };
96 changes: 36 additions & 60 deletions src/js/jssm.js
Expand Up @@ -13,6 +13,12 @@ const version = null; // replaced from package.js in build




import { seq, rand_select, histograph } from './jssm-util.js';




class machine<mNT, mDT> {


Expand Down Expand Up @@ -205,115 +211,83 @@ todo comeback



transitions() : Array< JssmTransition<mNT, mDT> > {
list_transitions() : Array< JssmTransition<mNT, mDT> > {
return this._edges;
}

named_transitions() : Map<mNT, number> {
list_named_transitions() : Map<mNT, number> {
return this._named_transitions;
}

actions() : Array<mNT> {
list_actions() : Array<mNT> {
return [... this._actions.keys()];
}



transition_id(from: mNT, to: mNT) {
get_transition_by_id(from: mNT, to: mNT) {
return this._edge_map.has(from)? (this._edge_map.get(from) : any).get(to) : undefined;
}

transition_for(from: mNT, to: mNT) : ?JssmTransition<mNT, mDT> {
const id = this.transition_id(from, to);
lookup_transition_for(from: mNT, to: mNT) : ?JssmTransition<mNT, mDT> {
const id = this.get_transition_by_id(from, to);
return (id === undefined)? undefined : this._edges[id];
}



transitions_for(whichState : mNT) : JssmTransitionList<mNT> {
return {entrances: this.entrances_for(whichState), exits: this.exits_for(whichState)};
list_transitions_for(whichState : mNT) : JssmTransitionList<mNT> {
return {entrances: this.list_entrances_for(whichState), exits: this.list_exits_for(whichState)};
}

entrances_for(whichState : mNT) : Array<mNT> {
list_entrances_for(whichState : mNT) : Array<mNT> {
return (this._states.get(whichState) || {}).from; // return undefined if it doesn't exist by asking for a member of an empty obj
}

exits_for(whichState : mNT) : Array<mNT> {
list_exits_for(whichState : mNT) : Array<mNT> {
return (this._states.get(whichState) || {}).to;
}

probable_exits_for(whichState : mNT) : Array< JssmTransition<mNT, mDT> > {

const wstate_to : Array<mNT> = ((this._states.get(whichState) || {to: []}).to),
wtf = wstate_to.map(ws => this.transition_for(this.state(), ws)).filter(defined => defined);
wtf = wstate_to.map(ws => this.lookup_transition_for(this.state(), ws)).filter(defined => defined);

return (wtf:any) || []; // :any because .transition_for can return `undefined`, which doesn't match this return spec

// const wstate_a = ((this._states.get(whichState) || {to: []}).to).map(ws => this.transition_for(this.state(), ws));
// if (wstate_a) { return wstate_a.map(ex => this.transition_for(this.state(), ex) ); }
// const wstate_a = ((this._states.get(whichState) || {to: []}).to).map(ws => this.lookup_transition_for(this.state(), ws));
// if (wstate_a) { return wstate_a.map(ex => this.lookup_transition_for(this.state(), ex) ); }

}

probabilistic_transition() : boolean {
const selected = this.rand_select(this.probable_exits_for(this.state()));
const selected = rand_select(this.probable_exits_for(this.state()));
return this.transition( selected.to );
}

probabilistic_walk(n : number) : Array<mNT> {
return this.seq(n-1)
.map(i => {
const state_was = this.state();
this.probabilistic_transition();
return state_was;
})
.concat([this.state()]);
return seq(n-1)
.map(i => {
const state_was = this.state();
this.probabilistic_transition();
return state_was;
})
.concat([this.state()]);
}

probabilistic_histo_walk(n : number) : Map<any, number> {
return this.histograph(this.probabilistic_walk(n));
return histograph(this.probabilistic_walk(n));
}



rand_select(options : Array<any>, probability_property : string = 'probability') {

if (!Array.isArray(options)) { throw new TypeError('options must be a non-empty array of objects'); }
if (!(typeof options[0] === 'object')) { throw new TypeError('options must be a non-empty array of objects'); }

const frand = cap => Math.random() * cap,
prob_sum = options.reduce( (acc, val:any) => acc + val[probability_property], 0 ),
rnd = frand(prob_sum);

var cursor = 0,
cursor_sum = 0;

while ((cursor_sum += (options:any)[cursor++][probability_property]) <= rnd) { }
return options[cursor-1];

}

seq(n : number) { return (new Array(n)).fill(true).map( (_,i) => i ); }

histograph(a : Array<any>) {
return a.sort().reduce( (m,v) => ( m.set(v, (m.has(v)? m.get(v)+1 : 1)) , m), new Map() );
}

sample_select(n : number, options : Array<mixed>, probability_property : string) {
return this.seq(n).map(i => this.rand_select(options, probability_property));
}

histo_key(n : number, options : Array<mixed>, probability_property : string, extract : string) {
return this.histograph(this.sample_select(n, options, probability_property).map( (s:any) => s[extract]));
}


actions_for(whichState : mNT) : Array<mNT> {
const wstate = this._reverse_actions.get(whichState);
if (wstate) { return [... (wstate || new Map()).keys()]; }
else { throw new Error(`No such state ${JSON.stringify(whichState)}`); }
}

action_found_on_states(whichState : mNT) : Array<mNT> {
list_states_having_action(whichState : mNT) : Array<mNT> {
return [... ((this._actions.get(whichState) || new Map()).keys() || [])]; // wasteful
}
/*
Expand All @@ -326,7 +300,7 @@ todo comeback
}
*/

action_exits_for(whichState : mNT) : Array<mNT> {
list_exit_actions_for(whichState : mNT) : Array<mNT> {
return [... (this._reverse_actions.get(whichState) || new Map()).values()] // wasteful
.map ( (edgeId:number) => this._edges[edgeId] )
.filter ( (o:JssmTransition<mNT, mDT>) => o.from === whichState )
Expand All @@ -345,7 +319,7 @@ todo comeback


is_unenterable(whichState : mNT) : boolean {
return this.entrances_for(whichState).length === 0;
return this.list_entrances_for(whichState).length === 0;
}

has_unenterables() : boolean {
Expand All @@ -359,7 +333,7 @@ todo comeback
}

state_is_terminal(whichState : mNT) : boolean {
return this.exits_for(whichState).length === 0;
return this.list_exits_for(whichState).length === 0;
}

has_terminals() : boolean {
Expand Down Expand Up @@ -446,7 +420,7 @@ todo comeback
// todo whargarbl implement hooks
// todo whargarbl implement data stuff
// todo major incomplete whargarbl comeback
return (this.transition_for(this.state(), newState) !== undefined);
return (this.lookup_transition_for(this.state(), newState) !== undefined);
}

valid_force_transition(newState : mNT, newData? : mDT) : boolean {
Expand Down Expand Up @@ -505,8 +479,10 @@ todo comeback

export {

version,

machine,

version
seq, rand_select, histograph

};

0 comments on commit 7a9ec47

Please sign in to comment.