Last active
November 15, 2019 09:33
-
-
Save farskid/a3c35f31bd3c02a76ba257d25f02c8ec to your computer and use it in GitHub Desktop.
Speaking statecharts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const statechart2 = { | |
id: "zoneMachine", | |
initial: "preparing", | |
context: { | |
error: undefined, | |
zone: undefined | |
}, | |
states: { | |
preparing: { | |
invoke: { | |
src: "detectLocationSupport", | |
id: "detectLocationSupport", | |
onDone: { | |
target: "ready" | |
}, | |
onError: { | |
target: "error" | |
} | |
} | |
}, | |
ready: { | |
initial: "zone_idle", | |
states: { | |
zone_idle: { | |
on: { | |
DETECT_ZONE: { | |
target: "zone_pending" | |
} | |
} | |
}, | |
zone_pending: { | |
invoke: { | |
id: "detectZone", | |
src: "detectZone", | |
onDone: { | |
target: "zone_available", | |
actions: ["setZone"] | |
}, | |
onError: { | |
target: "zone_error", | |
actions: ["setError"] | |
} | |
} | |
}, | |
zone_available: { | |
initial: "not_watching", | |
on: { | |
REFRESH: { | |
target: "zone_pending" | |
}, | |
LOCATION_CHANGED: { | |
target: ".history", | |
actions: ["setZone", "sendNotif"] | |
} | |
}, | |
states: { | |
history: { | |
type: "history", | |
history: "deep" | |
}, | |
not_watching: { | |
on: { | |
WATCH_LOCATION: { | |
target: "watching" | |
} | |
} | |
}, | |
watching: { | |
invoke: { | |
id: "watchLocation", | |
src: "watchLocationService" | |
} | |
} | |
} | |
}, | |
zone_error: { | |
on: { | |
DETECT_ZONE: { | |
target: "zone_pending" | |
} | |
} | |
} | |
} | |
}, | |
error: { | |
type: "final" | |
} | |
} | |
} | |
const statechart1 = { | |
id: "dragging-box", | |
initial: "released", | |
context: { | |
mousePositionInBox: { x: 0, y: 0 }, | |
boxPositionInContainer: { x: 0, y: 0 } | |
}, | |
states: { | |
released: { | |
on: { | |
GRAB: { | |
target: "grabbed" | |
} | |
} | |
}, | |
grabbed: { | |
entry: [ | |
"saveMousePositionInBox", | |
"saveBoxPositionInContainer", | |
"prepareBoxStyles", | |
"moveBox" | |
], | |
on: { | |
MOVE: "dragging" | |
} | |
}, | |
dragging: { | |
entry: [ | |
"saveBoxPositionInContainer", | |
"moveBox" | |
], | |
on: { | |
MOVE: "dragging", | |
RELEASE: "released" | |
} | |
} | |
} | |
} | |
// language exceptions | |
var exceptions = { | |
"are": "were", | |
"eat": "ate", | |
"go": "went", | |
"have": "had", | |
"inherit": "inherited", | |
"is": "was", | |
"run": "ran", | |
"sit": "sat", | |
"visit": "visited" | |
} | |
// grammatically predictable rules | |
function getPastTense(verb) { | |
if (exceptions[verb]) { | |
return exceptions[verb]; | |
} | |
if ((/e$/i).test(verb)) { | |
return verb + 'd'; | |
} | |
if ((/[aeiou]c/i).test(verb)) { | |
return verb + 'ked'; | |
} | |
// for american english only | |
if ((/el$/i).test(verb)) { | |
return verb + 'ed'; | |
} | |
if ((/[aeio][aeiou][dlmnprst]$/).test(verb)) { | |
return verb + 'ed'; | |
} | |
if ((/[aeiou][bdglmnprst]$/i).test(verb)) { | |
return verb.replace(/(.+[aeiou])([bdglmnprst])/, '$1$2$2ed'); | |
} | |
return verb + 'ed'; | |
} | |
"use strict"; | |
const rules = [ | |
// [long vowel][consonant] | |
{ | |
test: /([uao]m[pb]|[oa]wn|ey|elp|[ei]gn|ilm|o[uo]r|[oa]ugh|igh|ki|ff|oubt|ount|awl|o[alo]d|[iu]rl|upt|[oa]y|ight|oid|empt|act|aud|e[ea]d|ound|[aeiou][srcln]t|ept|dd|[eia]n[dk]|[ioa][xk]|[oa]rm|[ue]rn|[ao]ng|uin|eam|ai[mr]|[oea]w|[eaoui][rscl]k|[oa]r[nd]|ear|er|[^aieouyfm]it|[aeiouy]ir|[^aieouyfm]et|ll|en|vil|om|[^rno].mit|rup|bat|val|.[^skxwb][rvmchslwngb]el)$/, | |
transform: function(vb, to) { | |
if (to === "VBG") | |
return vb + 'ing'; | |
return vb; | |
} | |
}, | |
// [consonant][y] | |
{ | |
test: /[^aeiou]y$/, | |
transform: function(vb, to) { | |
var base = vb.substr(0, vb.length - 1); | |
if (to === "VBG") | |
return vb + 'ing'; | |
return vb; | |
} | |
}, | |
// [consonant][e] | |
{ | |
test: /[^aeiouy]e$/, | |
transform: function(vb, to) { | |
var base = vb.substr(0, vb.length - 1); | |
if (to === "VBG") | |
return base + 'ing'; | |
return vb; | |
} | |
}, | |
// [short vowel][consonant] | |
{ | |
test: /([^dtaeiuo][aeiuo][ptlgnm]|ir|cur|ug|[hj]ar|[^aouiey]ep|[^aeiuo][oua][db])$/, | |
transform: function(vb, to) { | |
if (to === "VBG") | |
return vb + vb[vb.length - 1] + 'ing'; | |
return vb; | |
} | |
}, | |
// [sibilant] | |
{ | |
test: /([ieao]ss|[aeiouy]zz|[aeiouy]ch|nch|rch|[aeiouy]sh|[iae]tch|ax)$/, | |
transform: function(vb, to) { | |
if (to === "VBG") | |
return vb + 'ing'; | |
return vb; | |
} | |
}, | |
// [e][e] | |
{ | |
test: /(ee)$/, | |
transform: function(vb, to) { | |
if (to === "VBG") | |
return vb + 'ing'; | |
return vb; | |
} | |
}, | |
// [i][e] | |
{ | |
test: /(ie)$/, | |
transform: function(vb, to) { | |
if (to === "VBG") | |
return vb.substr(0, vb.length - 2) + 'ying'; | |
return vb; | |
} | |
}, | |
// [u][e] | |
{ | |
test: /(ue)$/, | |
transform: function(vb, to) { | |
if (to === "VBG") | |
return vb.substr(0, vb.length - 1) + 'ing'; | |
return vb; | |
} | |
}, | |
// default (regular) | |
{ | |
test: /./, | |
transform: function(vb, to) { | |
if (to === "VBG") | |
return vb + 'ing'; | |
return vb; | |
} | |
} | |
]; | |
function solve(input, to) { | |
for (let i = 0; i < rules.length; i++) { | |
let rule = rules[i]; | |
if (rule.test.test(input)) | |
return rule.transform(input, to); | |
} | |
return ""; | |
} | |
function speak(txt) { | |
const synth = window.speechSynthesis; | |
const utterThis = new SpeechSynthesisUtterance(txt); | |
utterThis.pitch = 1; | |
utterThis.rate = 1; | |
/* utterThis.voice = "Google US English" */ | |
; | |
synth.speak(utterThis); | |
} | |
function getStateEventKeys(sNode) { | |
if (!sNode.on) return []; | |
return Object.keys(sNode.on); | |
} | |
function getStateEvents(sNode) { | |
return sNode.on; | |
} | |
function getEventName(evt) { | |
return typeof evt === "string" ? evt : evt.target; | |
} | |
function getStateEntryActions(sNode) { | |
const { | |
entry | |
} = sNode; | |
const entryType = typeof entry; | |
if (entryType === "string") { | |
return [entry] | |
} else if (Array.isArray(entry)) { | |
return entry | |
} else { | |
return [] | |
} | |
} | |
function getRecurringStateEvents(sKey, sNode) { | |
const events = getStateEvents(sNode); | |
let found = []; | |
for (const evt in events) { | |
if (sKey === getEventName(events[evt])) { | |
found.push(evt) | |
} | |
} | |
return found; | |
} | |
function parse(sc) { | |
const steps = []; | |
const id = sc.id; | |
const initialState = sc.initial; | |
const states = sc.states; | |
const stateKeys = Object.keys(states).sort((a) => a === initialState ? -1 : 1) | |
steps.push(`${id} is ${initialState} at first.`, "\n"); | |
stateKeys.forEach(sKey => { | |
const sNode = states[sKey]; | |
const eventKeys = getStateEventKeys(sNode); | |
const events = getStateEvents(sNode); | |
const entryActions = getStateEntryActions(sNode); | |
const recurringEvents = getRecurringStateEvents(sKey, sNode); | |
const eventKeysBesidesRecurrings = eventKeys.filter(evt => !recurringEvents.includes(evt)); | |
// announce entry actions | |
if (sNode.entry !== undefined) { | |
steps.push(`as soon as it's ${sKey}, we ${entryActions.slice(0,-1).join(", ")} and ${entryActions.slice().pop()}.`) | |
} | |
// announce event names | |
if (eventKeysBesidesRecurrings.length > 1) { | |
steps.push(`when it's ${sKey}, it can be ${eventKeys.map(s => s.toLowerCase()).map(getPastTense).join(" and ")}.`); | |
} else if (eventKeys.length === 1) { | |
steps.push(`when it's ${sKey}, it can only be ${eventKeys.map(s => s.toLowerCase()).map(getPastTense)}.`); | |
} | |
// Recurring events | |
recurringEvents.forEach(evt => { | |
steps.push(`as long as it's ${solve(evt.toLowerCase(), 'VBG')}, we keep it in ${sKey}`) | |
if (sNode.entry !== undefined) { | |
steps.push(`which means continuously ${entryActions.slice(0,-1).join(", ")} and ${entryActions.slice().pop()}.`) | |
} | |
}); | |
// Nested states | |
if (states[sKey].states) { | |
console.log(`encountered nested states for state: ${sKey}`) | |
steps.push(parse(states[sKey])) | |
} | |
// announce transitions | |
//eventKeys.forEach((evt, _, self) => { | |
//steps.push(`on event ${evt}, it will transition to state: ${getEventName(events[evt])}`) | |
//}); | |
steps.push("\n") | |
}) | |
return steps; | |
} | |
console.log(parse(statechart1).join("\n")) | |
/* parse(statechart).forEach(speak) */ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment