lumenize
Version:
Illuminating the forest AND the trees in your data.
326 lines (295 loc) • 15.1 kB
HTML
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>The source code</title>
<link href="../resources/prettify/prettify.css" type="text/css" rel="stylesheet" />
<script type="text/javascript" src="../resources/prettify/prettify.js"></script>
<style type="text/css">
.highlight { display: block; background-color: #ddd; }
</style>
<script type="text/javascript">
function highlight() {
document.getElementById(location.hash.replace(/#/, "")).className = "highlight";
}
</script>
</head>
<body onload="prettyPrint(); highlight();">
<pre class="prettyprint lang-js">/* <CoffeeScript>
{utils, Time} = require('tztime')
functions = require('./functions').functions # !TODO: Do we need this here?
{arrayOfMaps_To_CSVStyleArray, csvStyleArray_To_ArrayOfMaps} = require('./dataTransform') # !TODO: Do we need this here?
INFINITY = '9999-01-01T00:00:00.000Z'
class Store
</CoffeeScript> */
<span id='Lumenize-Store-method-constructor'><span id='Lumenize-Store-cfg-defaultValues'><span id='Lumenize-Store-cfg-tz'><span id='Lumenize-Store-cfg-idField'><span id='Lumenize-Store-cfg-validToField'><span id='Lumenize-Store-cfg-validFromField'><span id='Lumenize-Store-cfg-uniqueIDField'><span id='Lumenize-Store'> /**
</span></span></span></span></span></span></span></span> * @class Lumenize.Store
*
* __An efficient, in-memory, datastore for snapshot data.__
*
* Note, this store takes advantage of JavaScript's prototype inheritance to store snapshots in memory. Since the next snapshot might
* only have one field different from the prior one, this saves a ton of space. There is some concern that this will
* slow down certain operations because the JavaScript engine has to search all fields in the current level before bumping up
* to the next. However, there is some evidence that modern JavaScript implementations handle this very efficiently.
*
* However, this choice means that each row in the snapshots array doesn't have all of the fields.
*
* Store keeps track of all of the fields it has seen so you can flatten a row(s) if necessary.
*
* Example:
*
* {Store} = require('../')
*
* snapshotCSVStyleArray = [
* ['RecordID', 'DefectID', 'Created_Date', 'Severity', 'Modified_Date', 'Status'],
* [ 1, 1, '2014-06-16', 5, '2014-06-16', 'New'],
* [ 100, 1, '2014-06-16', 5, '2014-07-17', 'In Progress'],
* [ 1000, 1, '2014-06-16', 5, '2014-08-18', 'Done'],
* ]
*
* defects = require('../').csvStyleArray_To_ArrayOfMaps(snapshotCSVStyleArray)
*
* config =
* uniqueIDField: 'DefectID'
* validFromField: 'Modified_Date'
* idField: 'RecordID'
* defaultValues:
* Severity: 4
*
* store = new Store(config, defects)
*
* console.log(require('../').table.toString(store.snapshots, store.fields))
* # | Modified_Date | _ValidTo | _PreviousValues | DefectID | RecordID | Created_Date | Severity | Status |
* # | ------------------------ | ------------------------ | --------------- | -------- | -------- | ------------ | -------- | ----------- |
* # | 2014-06-16T00:00:00.000Z | 2014-07-17T00:00:00.000Z | [object Object] | 1 | 1 | 2014-06-16 | 5 | New |
* # | 2014-07-17T00:00:00.000Z | 2014-08-18T00:00:00.000Z | [object Object] | 1 | 100 | 2014-06-16 | 5 | In Progress |
* # | 2014-08-18T00:00:00.000Z | 9999-01-01T00:00:00.000Z | [object Object] | 1 | 1000 | 2014-06-16 | 5 | Done |
*
* That's pretty boring. We pretty much got out what we put in. There are a few things to notice though. First,
* Notice how the _ValidTo field is automatically set. Also, notice that it added the _PreviousValues field. This is
* a record of the immediately proceeding values for the fields that changed. In this way, the records not only
* represent the current snapshot; they also represent the state transition that occured to get into this snapshot
* state. That's what stateBoundaryCrossedFilter and other methods key off of.
*
* Also, under the covers, the prototype of each snapshot is the prior snapshot and only the fields that changed
* are actually stored in the next snapshot. So:
*
* console.log(store.snapshots[1] is store.snapshots[2].__proto__)
* # true
*
* The Store also keeps the equivalent of a database index on uniqueIDField and keeps a pointer to the last snapshot
* for each particular uniqueIDField. This provides a convenient way to do per entity analysis.
*
* console.log(store.byUniqueID['1'].snapshots[0].RecordID)
* # 1
*
* console.log(store.byUniqueID['1'].lastSnapshot.RecordID)
* # 1000
*
* @constructor
*
* @param {Object} config See Config options for details.
* @param {Object[]} [snapshots] Optional parameter allowing the population of the Store at instantiation.
*
* @cfg {String} [uniqueIDField = "_EntityID"] Specifies the field that identifies unique entities.
* @cfg {String} [validFromField = "_ValidFrom"]
* @cfg {String} [validToField = "_ValidTo"]
* @cfg {String} [idField = "_id"]
* @cfg {String} [tz = "GMT"]
* @cfg {Object} [defaultValues = {}] In some datastores, null numeric fields may be assumed to be zero and null
* boolean fields may be assumed to be false. Lumenize makes no such assumption and will crash if a field value
* is missing. the defaultValues becomes the root of prototype inheritance hierarchy.
*
*/
/* <CoffeeScript>
</CoffeeScript> */
<span id='Lumenize-Store-property-snapshots'> /**
</span> * @property snapshots
* @member Lumenize.Store
* An Array of Objects
*
* The snapshots in compressed (via JavaScript inheritance) format
*/
/* <CoffeeScript>
</CoffeeScript> */
<span id='Lumenize-Store-property-fields'> /**
</span> * @property fields
* @member Lumenize.Store
* An Array of Strings
*
* The list of all fields that this Store has ever seen. Use to expand each row.
*/
/* <CoffeeScript>
</CoffeeScript> */
<span id='Lumenize-Store-property-byUniqueID'> /**
</span> * @property byUniqueID
* @member Lumenize.Store
* This is the database equivalent of an index by uniqueIDField.
*
* An Object in the form:
*
* {
* '1234': {
* snapshots: [...],
* lastSnapshot: <points to last snapshot for this uniqueID>
* },
* '7890': {
* ...
* },
* ...
* }
*/
/* <CoffeeScript>
@config = utils.clone(@userConfig)
unless @config.uniqueIDField?
@config.uniqueIDField = '_EntityID'
unless @config.validFromField?
@config.validFromField = '_ValidFrom'
unless @config.validToField?
@config.validToField = '_ValidTo'
unless @config.tz?
@config.tz = 'GMT'
unless @config.defaultValues?
@config.defaultValues = {}
@config.defaultValues[@config.validFromField] = new Time(1, Time.MILLISECOND).toString()
unless @config.idField?
@config.idField = '_id'
@snapshots = []
@fields = [@config.validFromField, @config.validToField, '_PreviousValues', @config.uniqueIDField]
@lastValidFrom = new Time(1, Time.MILLISECOND).toString()
@byUniqueID = {}
@addSnapshots(snapshots)
addSnapshots: (snapshots) ->
</CoffeeScript> */
<span id='Lumenize-Store-method-addSnapshots'> /**
</span> * @method addSnapshots
* @member Lumenize.Store
* Adds the snapshots to the Store
* @param {Object[]} snapshots
* @chainable
* @return {Store} Returns this
*
*/
/* <CoffeeScript>
snapshots = utils._.sortBy(snapshots, @config.validFromField)
for s in snapshots
uniqueID = s[@config.uniqueIDField]
utils.assert(uniqueID?, "Missing #{@config.uniqueIDField} field in submitted snapshot: \n" + JSON.stringify(s, null, 2))
dataForUniqueID = @byUniqueID[uniqueID]
unless dataForUniqueID?
# First time we've seen this uniqueID
dataForUniqueID =
snapshots: []
lastSnapshot: @config.defaultValues
@byUniqueID[uniqueID] = dataForUniqueID
if s[@config.validFromField] < dataForUniqueID.lastSnapshot[@config.validFromField]
throw new Error("Got a new snapshot for a time earlier than the prior last snapshot for #{@config.uniqueIDField} #{uniqueID}.")
# Eventually, we may have to handle this case. I should be able to enable _nextSnapshot and stitch a snapshot in between two existing ones
else if s[@config.validFromField] is dataForUniqueID.lastSnapshot[@config.validFromField]
for key, value of s
dataForUniqueID.lastSnapshot[key] = value
else
validFrom = s[@config.validFromField]
validFrom = new Time(validFrom, null, @config.tz).getISOStringInTZ(@config.tz)
utils.assert(validFrom >= dataForUniqueID.lastSnapshot[@config.validFromField], "validFromField (#{validFrom}) must be >= lastValidFrom (#{dataForUniqueID.lastSnapshot[@config.validFromField]}) for this entity" ) # !TODO: Deal with out of order snapshots
utils.assert(validFrom >= @lastValidFrom, "validFromField (#{validFrom}) must be >= lastValidFrom (#{@lastValidFrom}) for the Store")
validTo = s[@config.validTo]
if validTo?
validTo = new Time(validTo, null, @config.tz).getISOStringInTZ(@config.tz)
else
validTo = INFINITY
priorSnapshot = dataForUniqueID.lastSnapshot
# Build new Snapshot for adding
newSnapshot = {}
newSnapshot._PreviousValues = {}
for key, value of s
unless key in [@config.validFromField, @config.validToField, '_PreviousValues', @config.uniqueIDField]
unless key in @fields
@fields.push(key)
unless value == priorSnapshot[key]
newSnapshot[key] = value
unless key in [@config.idField]
if priorSnapshot[key]?
newSnapshot._PreviousValues[key] = priorSnapshot[key]
else
newSnapshot._PreviousValues[key] = null
newSnapshot[@config.uniqueIDField] = uniqueID
newSnapshot[@config.validFromField] = validFrom
newSnapshot[@config.validToField] = validTo
if s._PreviousValues?
newSnapshot._PreviousValues = s._PreviousValues
newSnapshot.__proto__ = priorSnapshot
# Update priorSnapshot
if priorSnapshot[@config.validToField] is INFINITY
priorSnapshot[@config.validToField] = validFrom
# priorSnapshot._NextSnapshot = newSnapshot # Adding link to next snapshot in case we want to do smart insertion later
# Update metadata
dataForUniqueID.lastSnapshot = newSnapshot
@lastValidFrom = validFrom
# Add the newSnapshot to the arrays
@byUniqueID[uniqueID].snapshots.push(newSnapshot)
@snapshots.push(newSnapshot)
return this
filtered: (filter) ->
</CoffeeScript> */
<span id='Lumenize-Store-method-filtered'> /**
</span> * @method filtered
* @member Lumenize.Store
* Returns the subset of the snapshots that match the filter
* @param {Function} filter
* @return {Object[]} An array of snapshots. Note, they will not be flattened so they have references to their prototypes
*/
/* <CoffeeScript>
result = []
for s in @snapshots
if filter(s)
result.push(s)
return result
stateBoundaryCrossedFiltered: (field, values, valueToTheRightOfBoundary, forward = true, assumeNullIsLowest = true) ->
</CoffeeScript> */
<span id='Lumenize-Store-method-stateBoundaryCrossedFiltered'> /**
</span> * @method stateBoundaryCrossedFiltered
* @member Lumenize.Store
* Returns the subset of the snapshots where the field transitions from the left of valueToTheRightOfBoundary to
* the right (inclusive)
* @param {String} field
* @param {String[]} values
* @param {String} valueToTheRightOfBoundary
* @param {Boolean} [forward = true] When true (the default), this will return the transitions from left to right
* However, if you set this to false, it will return the transitions right to left.
* @param {Boolean} [assumeNullIsLowest = true] Set to false if you don't want to consider transitions out of null
* @return {Object[]} An array or snapshots. Note, they will not be flattened so they have references to their prototypes
*/
/* <CoffeeScript>
index = values.indexOf(valueToTheRightOfBoundary)
utils.assert(index >= 0, "stateToTheRightOfBoundary must be in stateList")
left = values.slice(0, index)
if assumeNullIsLowest
left.unshift(null)
right = values.slice(index)
if forward
filter = (s) -> s._PreviousValues.hasOwnProperty(field) and s._PreviousValues[field] in left and s[field] in right
else
filter = (s) -> s._PreviousValues.hasOwnProperty(field) and s._PreviousValues[field] in right and s[field] in left
return @filtered(filter)
stateBoundaryCrossedFilteredBothWays: (field, values, valueToTheRightOfBoundary, assumeNullIsLowest = true) ->
</CoffeeScript> */
<span id='Lumenize-Store-method-stateBoundaryCrossedFilteredBothWays'> /**
</span> * @method stateBoundaryCrossedFilteredBothWays
* @member Lumenize.Store
* Shortcut to stateBoundaryCrossedFiltered for when you need both directions
* @param {String} field
* @param {String[]} values
* @param {String} valueToTheRightOfBoundary
* @param {Boolean} [assumeNullIsLowest = true] Set to false if you don't want to consider transitions out of null
* @return {Object} An object with two root keys: 1) forward, 2) backward. The values are the arrays that are returned
* from stateBoundaryCrossedFiltered
*/
/* <CoffeeScript>
forward = @stateBoundaryCrossedFiltered(field, values, valueToTheRightOfBoundary, true, assumeNullIsLowest)
backward = @stateBoundaryCrossedFiltered(field, values, valueToTheRightOfBoundary, false, assumeNullIsLowest)
return {forward, backward}
exports.Store = Store
</CoffeeScript> */</pre>
</body>
</html>