util/time.js

var AmpersandModel = require('ampersand-model');
var AmpersandColllection = require('ampersand-collection');
var moment = require('moment-timezone');

/*
 * Time is grouped by truncating; the resolution is determined in util-time.getResolution()
 * See [this table](http://momentjs.com/docs/#/durations/creating/) for accpetable values
 * when using a crossfilter dataset.
 */
function unitsForMilliseconds (milliseconds) {
  var count = milliseconds;
  if (count < 10000) { // 10 seconds
    return 'milliseconds';
  }
  count = count / 1000;

  if (count < 15 * 60) { // 15 minutes
    return 'seconds';
  }
  count = count / 60;

  if (count < 3 * 60) { // 3 hours
    return 'minutes';
  }
  count = count / 60;

  if (count < 3 * 24) { // 3 days
    return 'hours';
  }
  count = count / 24;

  if (count < 3 * 7) { // 3 weeks
    return 'days';
  }
  if (count < 7 * 52) { // 52 weeks
    return 'weeks';
  }
  if (count < 2 * 365) { // 2 years
    return 'months';
  }
  return 'years';
}

function getFormat (units) {
  var fmt;
  if (units === 'seconds') {
    fmt = 'mm:ss';
  } else if (units === 'minutes') {
    fmt = 'HH:mm';
  } else if (units === 'hours') {
    fmt = 'HH:00';
  } else if (units === 'days') {
    fmt = 'dddd do';
  } else if (units === 'weeks') {
    fmt = 'wo';
  } else if (units === 'months') {
    fmt = 'YY MMM';
  } else if (units === 'years') {
    fmt = 'YYYY';
  }
  return fmt;
}

function getDatetimeResolution (start, end) {
  var difference = end.diff(start);
  return unitsForMilliseconds(difference);
}

function getDurationResolution (min, max) {
  var length = moment.duration(max.as('milliseconds') - min.as('milliseconds'), 'milliseconds');
  return unitsForMilliseconds(length);
}

var TimePart = AmpersandModel.extend({
  props: {
    /**
     * The format string for momentjs
     * @memberof! TimePart
     * @type {string}
     */
    momentFormat: ['string', true],
    /**
     * The format string for postgresql
     * @memberof! TimePart
     * @type {string}
     */
    postgresFormat: ['string', true],
    /**
     * The human readable descprition of the datetime part
     * @memberof! TimePart
     * @type {string}
     */
    description: ['string', true],
    /**
     * Data type after conversion: 'continuous', or 'categorial'
     * @memberof! TimePart
     * @type {string}
     */
    type: ['string', true],
    /**
     * For continuous datetime parts (ie, day-of-year), the minimum value
     * @memberof! TimePart
     * @type {number}
     */
    min: ['number', true, 0],
    /**
     * For continuous datetime parts (ie, day-of-year), the maximum value
     * @memberof! TimePart
     * @type {number}
     */
    max: ['number', true, 1],
    /**
     * When true, calculate the minimum and maximum value from the
     * original datetime limits. Used for continuous datetime parts (ie, year)
     * @memberof! TimePart
     * @type {boolean}
     */
    calculate: ['boolean', true, false],
    /**
     * For categorial datetime parts (Mon, Tue, ..), the array of possible values
     * @memberof! TimePart
     * @type {String[]}
     */
    groups: ['array']
  }
});

var TimeParts = AmpersandColllection.extend({
  model: TimePart,
  indexes: ['description']
});

var timeParts = new TimeParts([
  { description: 'ISO8601', type: 'datetime', calculate: true },
  { postgresFormat: 'month', momentFormat: 'M', description: 'Month (1-12)', type: 'continuous', min: 1, max: 12 },
  { postgresFormat: 'quarter', momentFormat: 'Q', description: 'Quarter (1-4)', type: 'continuous', min: 1, max: 4 },
  { postgresFormat: 'day', momentFormat: 'D', description: 'Day of Month  (1-31)', type: 'continuous', min: 1, max: 31 },
  { postgresFormat: 'doy', momentFormat: 'DDD', description: 'Day of Year (1-365)', type: 'continuous', min: 1, max: 365 },
  { postgresFormat: 'dow', momentFormat: 'd', description: 'Day of Week (0-6)', type: 'continuous', min: 0, max: 6 },
  { postgresFormat: 'isodow', momentFormat: 'E', description: 'Day of Week ISO (1-7)', type: 'continuous', min: 1, max: 7 },
  { postgresFormat: 'week', momentFormat: 'W', description: 'Week of Year ISO  (1-53)', type: 'continuous', min: 1, max: 53 },
  { postgresFormat: 'year', momentFormat: 'Y', description: 'Year', type: 'continuous', calculate: true },
  { postgresFormat: 'hours', momentFormat: 'H', description: 'Hour (0-23)', type: 'continuous', min: 0, max: 23 },
  { postgresFormat: 'minute', momentFormat: 'm', description: 'Minute (0-59)', type: 'continuous', min: 0, max: 59 },
  { postgresFormat: 'second', momentFormat: 's', description: 'Second (0-59)', type: 'continuous', min: 0, max: 59 },
  { postgresFormat: 'milliseconds', momentFormat: 'SSS', description: 'Milliseconds (0-999)', type: 'continuous', min: 0, max: 999 },
  { postgresFormat: 'microseconds', momentFormat: 'SSSSSS', description: 'microseconds (0-999999)', type: 'continuous', min: 0, max: 999999 },
  { postgresFormat: 'epoch', momentFormat: 'X', description: 'Unix Timestamp', type: 'continuous', calculate: true },
  { postgresFormat: 'Mon', momentFormat: 'MMM', description: 'Month (Jan - Dec)', type: 'categorial', groups: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] },
  { postgresFormat: 'Month', momentFormat: 'MMMM', description: 'Month (January - December)', type: 'categorial', groups: ['January', 'Feburary', 'March', 'April', 'May', 'June', 'July', 'August', 'Septebmer', 'October', 'November', 'December'] },
  { postgresFormat: 'Dy', momentFormat: 'ddd', description: 'Day of Week (Sun-Sat)', type: 'categorial', groups: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] },
  { postgresFormat: 'Day', momentFormat: 'dddd', description: 'Day of Week (Sunday-Saturday)', type: 'categorial', groups: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] },
  { postgresFormat: 'AM', momentFormat: 'A', description: 'AM/PM', type: 'categorial', groups: ['AM', 'PM'] }
]);

var DurationUnit = AmpersandModel.extend({
  props: {
    /**
     * The descriptive name of the time unit
     * @memberof! DurationUnit
     * @type {string}
     */
    description: ['string'],
    /**
     * Momentjs parsing format
     * @memberof! DurationUnit
     * @type {string}
     */
    momentFormat: ['string'],
    /**
     * Postgres parsing format
     * @memberof! DurationUnit
     * @type {string}
     */
    postgresFormat: ['string'],
    /**
     * Conversion factor to seconds
     * @memberof! DurationUnit
     * @type {string}
     */
    seconds: ['number']
  }
});

var DurationUnits = AmpersandColllection.extend({
  indexes: ['description'],
  model: DurationUnit
});

var durationUnits = new DurationUnits([
  {
    description: 'ISO8601',
    seconds: 1
  }, {
    description: 'millenium',
    momentFormat: 'millenium',
    postgresFormat: 'millenium',
    seconds: 100 * 365.25 * 24 * 60 * 60
  }, {
    description: 'century',
    momentFormat: 'century',
    postgresFormat: 'century',
    seconds: 100 * 365.25 * 24 * 60 * 60
  }, {
    description: 'decades',
    momentFormat: 'decades',
    postgresFormat: 'decade',
    seconds: 10 * 365.25 * 24 * 60 * 60
  }, {
    description: 'years',
    momentFormat: 'years',
    postgresFormat: 'year',
    seconds: 365.25 * 24 * 60 * 60
  }, {
    description: 'quarters',
    momentFormat: '',
    postgresFormat: 'quarter',
    seconds: 365.25 * 8 * 60 * 60
  }, {
    description: 'months',
    momentFormat: 'months',
    postgresFormat: 'month',
    seconds: 30 * 24 * 60 * 60
  }, {
    description: 'weeks',
    momentFormat: 'weeks',
    postgresFormat: 'week',
    seconds: 7 * 24 * 60 * 60
  }, {
    description: 'days',
    momentFormat: 'days',
    postgresFormat: 'day',
    seconds: 24 * 60 * 60
  }, {
    description: 'hours',
    momentFormat: 'hours',
    postgresFormat: 'hour',
    seconds: 60 * 60
  }, {
    description: 'minutes',
    momentFormat: 'minutes',
    postgresFormat: 'minute',
    seconds: 60
  }, {
    description: 'seconds',
    momentFormat: 'seconds',
    postgresFormat: 'second',
    seconds: 1
  }, {
    description: 'milliseconds',
    momentFormat: 'milliseconds',
    postgresFormat: 'milliseconds',
    seconds: 0.001
  }, {
    description: 'microseconds',
    momentFormat: 'microseconds',
    postgresFormat: 'microseconds',
    seconds: 0.000001
  }
]);

var TimeZone = AmpersandModel.extend({
  props: {
    /**
     * The descriptive name of the time zone
     * @memberof! TimeZone
     * @type {string}
     */
    description: ['string'],
    /**
     * The time zone format
     * @memberof! TimeZone
     * @type {string}
     */
    format: ['string']
  }
});

var TimeZones = AmpersandColllection.extend({
  indexes: ['description'],
  model: TimeZone
});

var timeZones = new TimeZones();
timeZones.add({
  description: 'ISO8601',
  format: 'ISO8601'
});

moment.tz.names().forEach(function (tz) {
  timeZones.add({
    description: tz,
    format: tz
  });
});

module.exports = {
  timeParts: timeParts,
  timeZones: timeZones,
  durationUnits: durationUnits,
  getDatetimeResolution: getDatetimeResolution,
  getDurationResolution: getDurationResolution,
  getFormat: getFormat
};