lumenize
Version:
Illuminating the forest AND the trees in your data.
320 lines (279 loc) • 14.7 kB
JavaScript
// Generated by CoffeeScript 1.10.0
(function() {
var OLAPCube, Time, TimeInStateCalculator, Timeline, ref, utils;
OLAPCube = require('./OLAPCube').OLAPCube;
ref = require('tztime'), utils = ref.utils, Time = ref.Time, Timeline = ref.Timeline;
TimeInStateCalculator = (function() {
/*
@class TimeInStateCalculator
Used to calculate how much time each uniqueID spent "in-state". You use this by querying a temporal data
model (like Rally's Lookback API) with a predicate indicating the "state" of interest. You'll then have a list of
snapshots where that predicate was true. You pass this in to the addSnapshots method of this previously instantiated
TimeInStateCalculator class.
Usage:
{TimeInStateCalculator} = require('../')
snapshots = [
{ id: 1, from: '2011-01-06T15:10:00.000Z', to: '2011-01-06T15:30:00.000Z', Name: 'Item A' }, # 20 minutes all within an hour
{ id: 2, from: '2011-01-06T15:50:00.000Z', to: '2011-01-06T16:10:00.000Z', Name: 'Item B' }, # 20 minutes spanning an hour
{ id: 3, from: '2011-01-07T13:00:00.000Z', to: '2011-01-07T15:20:00.000Z', Name: 'Item C' }, # start 2 hours before but overlap by 20 minutes of start
{ id: 4, from: '2011-01-06T16:40:00.000Z', to: '2011-01-06T19:00:00.000Z', Name: 'Item D' }, # 20 minutes before end of day
{ id: 5, from: '2011-01-06T16:50:00.000Z', to: '2011-01-07T15:10:00.000Z', Name: 'Item E' }, # 10 minutes before end of one day and 10 before the start of next
{ id: 6, from: '2011-01-06T16:55:00.000Z', to: '2011-01-07T15:05:00.000Z', Name: 'Item F' }, # multiple cycles over several days for a total of 20 minutes of work time
{ id: 6, from: '2011-01-07T16:55:00.000Z', to: '2011-01-10T15:05:00.000Z', Name: 'Item F modified' },
{ id: 7, from: '2011-01-06T16:40:00.000Z', to: '9999-01-01T00:00:00.000Z', Name: 'Item G' } # continues past the range of consideration in this test
]
granularity = 'minute'
tz = 'America/Chicago'
config = # default work days and holidays
granularity: granularity
tz: tz
endBefore: '2011-01-11T00:00:00.000'
workDayStartOn: {hour: 9, minute: 0} # 09:00 in Chicago is 15:00 in GMT
workDayEndBefore: {hour: 11, minute: 0} # 11:00 in Chicago is 17:00 in GMT # !TODO: Change this to 5pm when I change the samples above
validFromField: 'from'
validToField: 'to'
uniqueIDField: 'id'
trackLastValueForTheseFields: ['to', 'Name']
startOn = '2011-01-05T00:00:00.000Z'
endBefore = '2011-01-11T00:00:00.000Z'
tisc = new TimeInStateCalculator(config)
tisc.addSnapshots(snapshots, startOn, endBefore)
console.log(tisc.getResults())
* [ { id: 1,
* ticks: 20,
* to_lastValue: '2011-01-06T15:30:00.000Z',
* Name_lastValue: 'Item A' },
* { id: 2,
* ticks: 20,
* to_lastValue: '2011-01-06T16:10:00.000Z',
* Name_lastValue: 'Item B' },
* { id: 3,
* ticks: 20,
* to_lastValue: '2011-01-07T15:20:00.000Z',
* Name_lastValue: 'Item C' },
* { id: 4,
* ticks: 20,
* to_lastValue: '2011-01-06T19:00:00.000Z',
* Name_lastValue: 'Item D' },
* { id: 5,
* ticks: 20,
* to_lastValue: '2011-01-07T15:10:00.000Z',
* Name_lastValue: 'Item E' },
* { id: 6,
* ticks: 20,
* to_lastValue: '2011-01-10T15:05:00.000Z',
* Name_lastValue: 'Item F modified' },
* { id: 7,
* ticks: 260,
* to_lastValue: '9999-01-01T00:00:00.000Z',
* Name_lastValue: 'Item G' } ]
But we are not done yet. We can serialize the state of this calculator and later restore it.
savedState = tisc.getStateForSaving({somekey: 'some value'})
Let's incrementally update the original.
snapshots = [
{ id: 7, from: '2011-01-06T16:40:00.000Z', to: '9999-01-01T00:00:00.000Z', Name: 'Item G modified' }, # same snapshot as before still going
{ id: 3, from: '2011-01-11T15:00:00.000Z', to: '2011-01-11T15:20:00.000Z', Name: 'Item C modified' }, # 20 more minutes for id 3
{ id: 8, from: '2011-01-11T15:00:00.000Z', to: '9999-01-01T00:00:00.000Z', Name: 'Item H' } # 20 minutes in scope for new id 8
]
startOn = '2011-01-11T00:00:00.000Z' # must match endBefore of prior call
endBefore = '2011-01-11T15:20:00.000Z'
tisc.addSnapshots(snapshots, startOn, endBefore)
Now, let's restore from saved state into tisc2 and give it the same updates and confirm that they match.
tisc2 = TimeInStateCalculator.newFromSavedState(savedState)
tisc2.addSnapshots(snapshots, startOn, endBefore)
console.log(tisc2.meta.somekey)
* some value
console.log(JSON.stringify(tisc.getResults()) == JSON.stringify(tisc2.getResults()))
* true
Note, it's common to calculate time in state at granularity of hour and convert it to fractional days. Since it knocks
out non-work hours, this conversion is not as simple as dividing by 24. This code calculates the conversion factor
(workHours) for whatever workDayStartOn and workDayEndBefore you have specified even if your "workday" spans midnight.
startOnInMinutes = config.workDayStartOn.hour * 60
if config.workDayStartOn?.minute
startOnInMinutes += config.workDayStartOn.minute
endBeforeInMinutes = config.workDayEndBefore.hour * 60
if config.workDayEndBefore?.minute
endBeforeInMinutes += config.workDayEndBefore.minute
if startOnInMinutes < endBeforeInMinutes
workMinutes = endBeforeInMinutes - startOnInMinutes
else
workMinutes = 24 * 60 - startOnInMinutes
workMinutes += endBeforeInMinutes
workHours = workMinutes / 60
console.log(workHours) # Should say 2 because our work day was from 9am to 11am
* 2
You would simply divide the ticks by this `workHours` value to convert from ticks (in hours) to fractional days.
*/
function TimeInStateCalculator(config) {
/*
@constructor
@param {Object} config
@cfg {String} tz The timezone for analysis
@cfg {String} [validFromField = "_ValidFrom"]
@cfg {String} [validToField = "_ValidTo"]
@cfg {String} [uniqueIDField = "_EntityID"]
@cfg {String} granularity This calculator will tell you how many ticks fall within the snapshots you feed in.
This configuration value indicates the granularity of the ticks (i.e. Time.MINUTE, Time.HOUR, Time.DAY, etc.)
@cfg {String[]/String} [workDays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday']] List of days of the week that you work on. You can specify this as an Array of Strings
(['Monday', 'Tuesday', ...]) or a single comma seperated String ("Monday,Tuesday,...").
@cfg {Object[]} [holidays] An optional Array containing rows that are either ISOStrings or JavaScript Objects
(mix and match). Example: `[{month: 12, day: 25}, {year: 2011, month: 11, day: 24}, "2012-12-24"]`
Notice how you can leave off the year if the holiday falls on the same day every year.
@cfg {Object} [workDayStartOn] An optional object in the form {hour: 8, minute: 15}. If minute is zero it can be omitted.
If workDayStartOn is later than workDayEndBefore, then it assumes that you work the night shift and your work
hours span midnight. If tickGranularity is "hour" or finer, you probably want to set this; if tickGranularity is
"day" or coarser, probably not.
@cfg {Object} [workDayEndBefore] An optional object in the form {hour: 17, minute: 0}. If minute is zero it can be omitted.
The use of workDayStartOn and workDayEndBefore only make sense when the granularity is "hour" or finer.
Note: If the business closes at 5:00pm, you'll want to leave workDayEndBefore to 17:00, rather
than 17:01. Think about it, you'll be open 4:59:59.999pm, but you'll be closed at 5:00pm. This also makes all of
the math work. 9am to 5pm means 17 - 9 = an 8 hour work day.
@cfg {String[]} [trackLastValueForTheseFields] If provided, the last value of these fields will appear in the results.
This is useful if you want to filter the result by where the ended or if you want information to fill in the tooltip
for a chart.
*/
var cubeConfig, dimensions, fieldName, i, len, metricObject, metrics, ref1;
this.config = utils.clone(config);
if (this.config.validFromField == null) {
this.config.validFromField = "_ValidFrom";
}
if (this.config.validToField == null) {
this.config.validToField = "_ValidTo";
}
if (this.config.uniqueIDField == null) {
this.config.uniqueIDField = "_EntityID";
}
utils.assert(this.config.tz != null, "Must provide a timezone to this calculator.");
utils.assert(this.config.granularity != null, "Must provide a granularity to this calculator.");
dimensions = [
{
field: this.config.uniqueIDField
}
];
metrics = [
{
field: 'ticks',
as: 'ticks',
f: 'sum'
}
];
if (this.config.trackLastValueForTheseFields != null) {
ref1 = this.config.trackLastValueForTheseFields;
for (i = 0, len = ref1.length; i < len; i++) {
fieldName = ref1[i];
metricObject = {
f: 'lastValue',
field: fieldName
};
metrics.push(metricObject);
}
}
cubeConfig = {
dimensions: dimensions,
metrics: metrics
};
this.cube = new OLAPCube(cubeConfig);
this.upToDateISOString = null;
}
TimeInStateCalculator.prototype.addSnapshots = function(snapshots, startOn, endBefore) {
/*
@method addSnapshots
Allows you to incrementally add snapshots to this calculator.
@chainable
@param {Object[]} snapshots An array of temporal data model snapshots.
@param {String} startOn A ISOString (e.g. '2012-01-01T12:34:56.789Z') indicating the time start of the period of
interest. On the second through nth call, this should equal the previous endBefore.
@param {String} endBefore A ISOString (e.g. '2012-01-01T12:34:56.789Z') indicating the moment just past the time
period of interest.
@return {TimeInStateCalculator}
*/
var i, len, s, ticks, timeline, timelineConfig;
if (this.upToDateISOString != null) {
utils.assert(this.upToDateISOString === startOn, "startOn (" + startOn + ") parameter should equal endBefore of previous call (" + this.upToDateISOString + ") to addSnapshots.");
}
this.upToDateISOString = endBefore;
timelineConfig = utils.clone(this.config);
timelineConfig.startOn = new Time(startOn, Time.MILLISECOND, this.config.tz);
timelineConfig.endBefore = new Time(endBefore, Time.MILLISECOND, this.config.tz);
timeline = new Timeline(timelineConfig);
for (i = 0, len = snapshots.length; i < len; i++) {
s = snapshots[i];
ticks = timeline.ticksThatIntersect(s[this.config.validFromField], s[this.config.validToField], this.config.tz);
s.ticks = ticks.length;
}
this.cube.addFacts(snapshots);
return this;
};
TimeInStateCalculator.prototype.getResults = function() {
/*
@method getResults
Returns the current state of the calculator
@return {Object[]} Returns an Array of Maps like `{<uniqueIDField>: <id>, ticks: <ticks>, lastValidTo: <lastValidTo>}`
*/
var cell, fieldName, filter, i, id, j, len, len1, out, outRow, ref1, uniqueIDs;
out = [];
uniqueIDs = this.cube.getDimensionValues(this.config.uniqueIDField);
for (i = 0, len = uniqueIDs.length; i < len; i++) {
id = uniqueIDs[i];
filter = {};
filter[this.config.uniqueIDField] = id;
cell = this.cube.getCell(filter);
outRow = {};
outRow[this.config.uniqueIDField] = id;
outRow.ticks = cell.ticks;
if (this.config.trackLastValueForTheseFields != null) {
ref1 = this.config.trackLastValueForTheseFields;
for (j = 0, len1 = ref1.length; j < len1; j++) {
fieldName = ref1[j];
outRow[fieldName + '_lastValue'] = cell[fieldName + '_lastValue'];
}
}
out.push(outRow);
}
return out;
};
TimeInStateCalculator.prototype.getStateForSaving = function(meta) {
/*
@method getStateForSaving
Enables saving the state of this calculator. See class documentation for a detailed example.
@param {Object} [meta] An optional parameter that will be added to the serialized output and added to the meta field
within the deserialized calculator.
@return {Object} Returns an Ojbect representing the state of the calculator. This Object is suitable for saving to
to an object store. Use the static method `newFromSavedState()` with this Object as the parameter to reconstitute
the calculator.
*/
var out;
out = {
config: this.config,
cubeSavedState: this.cube.getStateForSaving(),
upToDateISOString: this.upToDateISOString
};
if (meta != null) {
out.meta = meta;
}
return out;
};
TimeInStateCalculator.newFromSavedState = function(p) {
/*
@method newFromSavedState
Deserializes a previously saved calculator and returns a new calculator. See class documentation for a detailed example.
@static
@param {String/Object} p A String or Object from a previously saved state
@return {TimeInStateCalculator}
*/
var calculator;
if (utils.type(p) === 'string') {
p = JSON.parse(p);
}
calculator = new TimeInStateCalculator(p.config);
calculator.cube = OLAPCube.newFromSavedState(p.cubeSavedState);
calculator.upToDateISOString = p.upToDateISOString;
if (p.meta != null) {
calculator.meta = p.meta;
}
return calculator;
};
return TimeInStateCalculator;
})();
exports.TimeInStateCalculator = TimeInStateCalculator;
}).call(this);