models/time-transform.js

/**
 * TimeTransfrom defines a transformation on time or duration data
 *
 * @class TimeTransform
 */
var AmpersandModel = require('ampersand-model');
var moment = require('moment-timezone');

module.exports = AmpersandModel.extend({
  props: {
    /**
     * When datetime or durations are not in ISO8601 format, this format will be used to parse the datetime
     * Implementation depends on the dataset. Crossfilter dataset uses momentjs
     * @memberof! TimeTransform
     * @type {string}
     */
    format: 'string',

    /**
     * Timezone to use when parsing, for when timezone information is absent or incorrect.
     * @memberof! TimeTransform
     * @type {string}
     */
    zone: 'string',

    /**
     * Type of datetime object: a datetime or a duration
     * Do not use directly, but use `isDatetime` and `isDuration` methods.
     * @memberof! TimeTransform
     * @type {string}
     */
    type: {
      type: 'string',
      values: ['datetime', 'duration']
    },

    /**
     * For durations, sets the new units to use (years, months, weeks, days, hours, minutes, seconds, miliseconds). Data will be transformed.
     * For datetimes, reformats to a string using the momentjs or postgreSQL format specifiers.
     * This allows a transformation to day of the year, or day of week etc.
     * @memberof! TimeTransform
     * @type {string}
     */
    transformedFormat: 'string',

    /**
     * For datetimes, transform the date to this timezone.
     * @memberof! TimeTransform
     * @type {string}
     */
    transformedZone: 'string',

    /**
     * Controls conversion between datetimes and durations by adding or subtracting this date
     * @memberof! TimeTransform
     * @type {string}
     */
    transformedReference: 'string'
  },
  derived: {
    // properties for: base-value-time
    isDatetime: {
      deps: ['type'],
      fn: function () {
        return this.type === 'datetime';
      }
    },
    isDuration: {
      deps: ['type'],
      fn: function () {
        return this.type === 'duration';
      }
    },
    transformedType: {
      deps: ['type', 'transformedReference', 'transformedFormat'],
      fn: function () {
        if (this.type === 'duration') {
          if (this.transformedReference) {
            return 'datetime';
          } else {
            return 'continuous';
          }
        } else if (this.type === 'datetime') {
          if (this.transformedReference) {
            return 'continuous'; // ie. a duration
          } else if (this.transformedFormat) {
            return 'categorial'; // weekday, etc.
          } else {
            return 'datetime';
          }
        }
      }
    },
    // reference momentjs for duration <-> datetime conversion
    referenceMoment: {
      deps: ['transformedZone', 'transformedReference'],
      fn: function () {
        var tz;
        if (this.transformedZone) {
          tz = this.transformedZone;
        } else {
          tz = moment.tz.guess();
        }

        if (this.transformedReference) {
          return moment.tz(this.transformedReference, tz);
        }
        return null;
      }
    }
  },

  /**
   * @function
   * @memberof! TimeTransform
   * @param {Object} momentjs
   * @returns {Object} momentjs
   */
  transform: function transform (inval) {
    if (this.isDatetime) {
      if (this.referenceMoment) {
        // datetime -> duration
        return inval.diff(this.referenceMoment, this.transformedFormat, true);
      } else if (this.transformedZone) {
        // change time zone
        return inval.tz(this.transformedZone);
      } else if (this.transformedFormat) {
        // Print the momentjs object as string, and as it is now a categorial type, wrap it in an array
        // Format specification here http://momentjs.com/docs/#/displaying/format/
        return [inval.format(this.transformedFormat)];
      } else {
        return inval;
      }
    } else if (this.isDuration) {
      if (this.referenceMoment) {
        // duration -> datetime
        return this.referenceMoment.clone().add(inval);
      } else if (this.transformedFormat) {
        // change units
        return inval.as(this.transformedFormat);
      } else {
        // no change
        return inval;
      }
    } else {
      console.error('Time type not implemented for timeTransform', this);
    }
  },
  clear: function () {
    this.unset(['format', 'zone', 'type', 'transformedFormat', 'transformedZone', 'transformedReference']);
  }
});