lumenize
Version:
Illuminating the forest AND the trees in your data.
331 lines (285 loc) • 14.5 kB
JavaScript
// Generated by CoffeeScript 1.10.0
(function() {
var INFINITY, Store, Time, arrayOfMaps_To_CSVStyleArray, csvStyleArray_To_ArrayOfMaps, functions, ref, ref1, utils,
indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
ref = require('tztime'), utils = ref.utils, Time = ref.Time;
functions = require('./functions').functions;
ref1 = require('./dataTransform'), arrayOfMaps_To_CSVStyleArray = ref1.arrayOfMaps_To_CSVStyleArray, csvStyleArray_To_ArrayOfMaps = ref1.csvStyleArray_To_ArrayOfMaps;
INFINITY = '9999-01-01T00:00:00.000Z';
Store = (function() {
/*
@class 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
*/
/*
@property snapshots
An Array of Objects
The snapshots in compressed (via JavaScript inheritance) format
*/
/*
@property fields
An Array of Strings
The list of all fields that this Store has ever seen. Use to expand each row.
*/
/*
@property byUniqueID
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': {
...
},
...
}
*/
function Store(userConfig, snapshots) {
this.userConfig = userConfig;
/*
@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.
*/
this.config = utils.clone(this.userConfig);
if (this.config.uniqueIDField == null) {
this.config.uniqueIDField = '_EntityID';
}
if (this.config.validFromField == null) {
this.config.validFromField = '_ValidFrom';
}
if (this.config.validToField == null) {
this.config.validToField = '_ValidTo';
}
if (this.config.tz == null) {
this.config.tz = 'GMT';
}
if (this.config.defaultValues == null) {
this.config.defaultValues = {};
}
this.config.defaultValues[this.config.validFromField] = new Time(1, Time.MILLISECOND).toString();
if (this.config.idField == null) {
this.config.idField = '_id';
}
this.snapshots = [];
this.fields = [this.config.validFromField, this.config.validToField, '_PreviousValues', this.config.uniqueIDField];
this.lastValidFrom = new Time(1, Time.MILLISECOND).toString();
this.byUniqueID = {};
this.addSnapshots(snapshots);
}
Store.prototype.addSnapshots = function(snapshots) {
/*
@method addSnapshots
Adds the snapshots to the Store
@param {Object[]} snapshots
@chainable
@return {Store} Returns this
*/
var dataForUniqueID, i, key, len, newSnapshot, priorSnapshot, s, uniqueID, validFrom, validTo, value;
snapshots = utils._.sortBy(snapshots, this.config.validFromField);
for (i = 0, len = snapshots.length; i < len; i++) {
s = snapshots[i];
uniqueID = s[this.config.uniqueIDField];
utils.assert(uniqueID != null, ("Missing " + this.config.uniqueIDField + " field in submitted snapshot: \n") + JSON.stringify(s, null, 2));
dataForUniqueID = this.byUniqueID[uniqueID];
if (dataForUniqueID == null) {
dataForUniqueID = {
snapshots: [],
lastSnapshot: this.config.defaultValues
};
this.byUniqueID[uniqueID] = dataForUniqueID;
}
if (s[this.config.validFromField] < dataForUniqueID.lastSnapshot[this.config.validFromField]) {
throw new Error("Got a new snapshot for a time earlier than the prior last snapshot for " + this.config.uniqueIDField + " " + uniqueID + ".");
} else if (s[this.config.validFromField] === dataForUniqueID.lastSnapshot[this.config.validFromField]) {
for (key in s) {
value = s[key];
dataForUniqueID.lastSnapshot[key] = value;
}
} else {
validFrom = s[this.config.validFromField];
validFrom = new Time(validFrom, null, this.config.tz).getISOStringInTZ(this.config.tz);
utils.assert(validFrom >= dataForUniqueID.lastSnapshot[this.config.validFromField], "validFromField (" + validFrom + ") must be >= lastValidFrom (" + dataForUniqueID.lastSnapshot[this.config.validFromField] + ") for this entity");
utils.assert(validFrom >= this.lastValidFrom, "validFromField (" + validFrom + ") must be >= lastValidFrom (" + this.lastValidFrom + ") for the Store");
validTo = s[this.config.validTo];
if (validTo != null) {
validTo = new Time(validTo, null, this.config.tz).getISOStringInTZ(this.config.tz);
} else {
validTo = INFINITY;
}
priorSnapshot = dataForUniqueID.lastSnapshot;
newSnapshot = {};
newSnapshot._PreviousValues = {};
for (key in s) {
value = s[key];
if (key !== this.config.validFromField && key !== this.config.validToField && key !== '_PreviousValues' && key !== this.config.uniqueIDField) {
if (indexOf.call(this.fields, key) < 0) {
this.fields.push(key);
}
if (value !== priorSnapshot[key]) {
newSnapshot[key] = value;
if (key !== this.config.idField) {
if (priorSnapshot[key] != null) {
newSnapshot._PreviousValues[key] = priorSnapshot[key];
} else {
newSnapshot._PreviousValues[key] = null;
}
}
}
}
}
newSnapshot[this.config.uniqueIDField] = uniqueID;
newSnapshot[this.config.validFromField] = validFrom;
newSnapshot[this.config.validToField] = validTo;
if (s._PreviousValues != null) {
newSnapshot._PreviousValues = s._PreviousValues;
}
newSnapshot.__proto__ = priorSnapshot;
if (priorSnapshot[this.config.validToField] === INFINITY) {
priorSnapshot[this.config.validToField] = validFrom;
}
dataForUniqueID.lastSnapshot = newSnapshot;
this.lastValidFrom = validFrom;
this.byUniqueID[uniqueID].snapshots.push(newSnapshot);
this.snapshots.push(newSnapshot);
}
}
return this;
};
Store.prototype.filtered = function(filter) {
/*
@method filtered
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
*/
var i, len, ref2, result, s;
result = [];
ref2 = this.snapshots;
for (i = 0, len = ref2.length; i < len; i++) {
s = ref2[i];
if (filter(s)) {
result.push(s);
}
}
return result;
};
Store.prototype.stateBoundaryCrossedFiltered = function(field, values, valueToTheRightOfBoundary, forward, assumeNullIsLowest) {
var filter, index, left, right;
if (forward == null) {
forward = true;
}
if (assumeNullIsLowest == null) {
assumeNullIsLowest = true;
}
/*
@method stateBoundaryCrossedFiltered
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
*/
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 = function(s) {
var ref2, ref3;
return s._PreviousValues.hasOwnProperty(field) && (ref2 = s._PreviousValues[field], indexOf.call(left, ref2) >= 0) && (ref3 = s[field], indexOf.call(right, ref3) >= 0);
};
} else {
filter = function(s) {
var ref2, ref3;
return s._PreviousValues.hasOwnProperty(field) && (ref2 = s._PreviousValues[field], indexOf.call(right, ref2) >= 0) && (ref3 = s[field], indexOf.call(left, ref3) >= 0);
};
}
return this.filtered(filter);
};
Store.prototype.stateBoundaryCrossedFilteredBothWays = function(field, values, valueToTheRightOfBoundary, assumeNullIsLowest) {
var backward, forward;
if (assumeNullIsLowest == null) {
assumeNullIsLowest = true;
}
/*
@method stateBoundaryCrossedFilteredBothWays
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
*/
forward = this.stateBoundaryCrossedFiltered(field, values, valueToTheRightOfBoundary, true, assumeNullIsLowest);
backward = this.stateBoundaryCrossedFiltered(field, values, valueToTheRightOfBoundary, false, assumeNullIsLowest);
return {
forward: forward,
backward: backward
};
};
return Store;
})();
exports.Store = Store;
}).call(this);