UNPKG

lumenize

Version:

Illuminating the forest AND the trees in your data.

331 lines (285 loc) 14.5 kB
// 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);