Source: core/TemporalUtils.js

/**
 * Copyright (C) 2005-2016 Alfresco Software Limited.
 *
 * This file is part of Alfresco
 *
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * This is a mixin that provides time and date related utility functions.
 * 
 * @module alfresco/core/TemporalUtils
 * @mixes module:alfresco/core/Core
 * @author Richard Smith
 */
define(["dojo/_base/declare",
        "dojo/_base/lang",
        "alfresco/core/Core",
        "dojo/date/stamp",
        "dojo/query"], 
        function(declare, lang, AlfCore, stamp, query) {
    
   var cachedDateFormatsByI18nScope = {};

   return declare([AlfCore], {

      /**
       * An array of the i18n files to use with this widget.
       * 
       * @instance
       * @type {object[]}
       * @default [{i18nFile: "./i18n/TemporalUtils.properties"}]
       */
      i18nRequirements: [{i18nFile: "./i18n/TemporalUtils.properties"}],

      /**
       * Constructor
       * 
       * @param {object} config Config to mixin
       */
      constructor: function alfresco_core_TemporalUtils__constructor(config) {

         lang.mixin(this, config);
         
         // check already resolved dateFormats before doing
         var lookupKey = this.i18nScope || "default";
         if (cachedDateFormatsByI18nScope.hasOwnProperty(lookupKey))
         {
             this.dateFormats = JSON.parse(cachedDateFormatsByI18nScope[lookupKey]);
         }
         else
         {
             this.dateFormats = {};
             this.dateFormats.DAY_NAMES = (this.message("days.medium") + "," + this.message("days.long")).split(",");
             this.dateFormats.MONTH_NAMES = (this.message("months.short") + "," + this.message("months.long")).split(",");
             this.dateFormats.TIME_AM = this.message("date-format.am");
             this.dateFormats.TIME_PM = this.message("date-format.pm");
             this.dateFormats.masks = {};
             this.dateFormats.masks["default"] = this.message("date-format.default");
             this.dateFormats.masks.defaultDateOnly = this.message("date-format.defaultDateOnly");
             this.dateFormats.masks.shortDate = this.message("date-format.shortDate");
             this.dateFormats.masks.mediumDate = this.message("date-format.mediumDate");
             this.dateFormats.masks.longDate = this.message("date-format.longDate");
             this.dateFormats.masks.fullDate = this.message("date-format.fullDate");
             this.dateFormats.masks.shortTime = this.message("date-format.shortTime");
             this.dateFormats.masks.mediumTime = this.message("date-format.mediumTime");
             this.dateFormats.masks.longTime = this.message("date-format.longTime");
             this.dateFormats.masks.isoDate = "yyyy-mm-dd";
             this.dateFormats.masks.isoTime = "HH:MM:ss";
             this.dateFormats.masks.isoDateTime = "yyyy-mm-dd'T'HH:MM:ss";
             this.dateFormats.masks.isoFullDateTime = "yyyy-mm-dd'T'HH:MM:ss.lo";
             this.dateFormats.i18n = {
                dayNames: this.dateFormats.DAY_NAMES,
                monthNames: this.dateFormats.MONTH_NAMES
             };
             
             // cache
             cachedDateFormatsByI18nScope[lookupKey] = JSON.stringify(this.dateFormats);
         }
      },

      /**
       * Generate a relative time between two Date objects.
       *
       * @instance
       * @param {Date|String} from JavaScript Date object or ISO8601-formatted date string
       * @param {Date|String} [to] JavaScript Date object or ISO8601-formatted date string, defaults to now if not supplied
       * @return {String} Relative time description
       */
      getRelativeTime: function alfresco_core_TemporalUtils__getRelativeTime(from, to) {
         /*jshint maxstatements:false, maxcomplexity:false*/

         var originalFrom = from;

         if (lang.isString(from))
         {
            from = this.fromISO8601(from);
         }

         if (!(from instanceof Date))
         {
            return originalFrom;
         }

         if (typeof to === "undefined")
         {
            to = new Date();
         }
         else if (lang.isString(to))
         {
            to = this.fromISO8601(to);
         }

         var seconds_ago = ((to - from) / 1000),
             minutes_ago = Math.floor(seconds_ago / 60),
             _this = this,
             fnTime = function getRelativeTime_fnTime() {
                return "<span title='" + _this.formatDate(from) + "'>" + _this.message.apply(this, arguments) + "</span>";
             };

         if (minutes_ago <= 0)
         {
            return fnTime("relative.seconds", seconds_ago);
         }
         if (minutes_ago === 1)
         {
            return fnTime("relative.minute");
         }
         if (minutes_ago < 45)
         {
            return fnTime("relative.minutes", minutes_ago);
         }
         if (minutes_ago < 90)
         {
            return fnTime("relative.hour");
         }
         var hours_ago  = Math.round(minutes_ago / 60);
         if (minutes_ago < 1440)
         {
            return fnTime("relative.hours", hours_ago);
         }
         if (minutes_ago < 2880)
         {
            return fnTime("relative.day");
         }
         var days_ago  = Math.round(minutes_ago / 1440);
         if (minutes_ago < 43200)
         {
            return fnTime("relative.days", days_ago);
         }
         if (minutes_ago < 86400)
         {
            return fnTime("relative.month");
         }
         var months_ago  = Math.round(minutes_ago / 43200);
         if (minutes_ago < 525960)
         {
            return fnTime("relative.months", months_ago);
         }
         if (minutes_ago < 1051920)
         {
            return fnTime("relative.year");
         }
         var years_ago  = Math.round(minutes_ago / 525960);
         return fnTime("relative.years", years_ago);
      },

      /**
       * Convert an ISO8601 date string into a native JavaScript Date object
       *
       * @instance
       * @param {String} date ISO8601 formatted date string
       * @param {Boolean} [ignoreTime] Ignores any time (and therefore timezone) components.
       * @return {Date} JavaScript native Date object or null on errors
       */
      fromISO8601: function alfresco_core_TemporalUtils__fromISO8601(dateString, ignoreTime) {

         if (ignoreTime)
         {
            dateString = dateString.split("T")[0];
         }

         try
         {
            return stamp.fromISOString(dateString);
         }
         catch(e)
         {
            return null;
         }
      },

      /**
       * Convert a native JavaScript Date object into an ISO8601 date string
       *
       * @instance
       * @param {Date} date JavaScript native Date object
       * @return {String} ISO8601 formatted date string
       */
      toISO8601: function alfresco_core_TemporalUtils__toISO8601(date) {

         try
         {
            return stamp.toISOString(date);
         }
         catch(e)
         {
            return "";
         }
      },

      /**
       * Formats a date time into a more UI-friendly format
       *
       * @instance
       * @param {String|Date} [date] Date as ISO8601 compatible string or JavaScript Date Object. Today used if missing.
       * @param {String} [format] Mask to use to override default.
       * @return {String} Date formatted for UI
       */
      formatDate: function alfresco_core_TemporalUtils__formatDate(date, /*jshint unused:false*/ format) {

         var dateToUse = date;
         if (lang.isString(date))
         {
            // if we've got a date as an ISO8601 string, convert to date Object before proceeding - otherwise pass it through
            dateToUse = this.fromISO8601(date) || date;
         }
         try
         {
            return this.formatDate3rd(dateToUse, format);
         }
         catch(e)
         {
            return date;
         }
      },

      /**
       * Format a date object to a user-specified mask
       *
       * Original code:
       *    Date Format 1.1
       *    (c) 2007 Steven Levithan <stevenlevithan.com>
       *    MIT license
       *    With code by Scott Trenda (Z and o flags, and enhanced brevity)
       *
       * http://blog.stevenlevithan.com/archives/date-time-format
       * 
       * @instance
       * @return {String}
       */
      formatDate3rd: function alfresco_core_TemporalUtils__formatDate3rd() {
         /*jshint noarg:false*/

         /* dateFormat
          Accepts a date, a mask, or a date and a mask.
          Returns a formatted version of the given date.
          The date defaults to the current date/time.
          The mask defaults ``"ddd mmm d yyyy HH:MM:ss"``.
          */
         var _this = this,
             dateFormat = (function() {
            
            var token        = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloZ]|"[^"]*"|'[^']*'/g,
                timezone     = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g,
                timezoneClip = /[^-+\dA-Z]/g,
                fnPad = function(value, length) {
                   value = String(value);
                   length = parseInt(length, 10) || 2;
                   while (value.length < length)
                   {
                      value = "0" + value;
                   }
                   return value;
                };

            // Regexes and supporting functions are cached through closure
            return function (date, mask) {
               /*jshint maxcomplexity:false*/

               // Treat the first argument as a mask if it doesn't contain any numbers
               if (arguments.length === 1 &&
                  (typeof date === "string" || date instanceof String) &&
                  !/\d/.test(date))
               {
                  mask = date;
                  date = undefined;
               }

               if (typeof date === "string")
               {
                  date = date.replace(".", "");
               }

               date = date ? new Date(date) : new Date();
               if (isNaN(date))
               {
                  throw "invalid date";
               }

               mask = String(_this.dateFormats.masks[mask] || mask || _this.dateFormats.masks["default"]);

               var d = date.getDate(),
                   D = date.getDay(),
                   m = date.getMonth(),
                   y = date.getFullYear(),
                   H = date.getHours(),
                   M = date.getMinutes(),
                   s = date.getSeconds(),
                   L = date.getMilliseconds(),
                   o = date.getTimezoneOffset(),
                   flags = {
                      d:    d,
                      dd:   fnPad(d),
                      ddd:  _this.dateFormats.i18n.dayNames[D],
                      dddd: _this.dateFormats.i18n.dayNames[D + 7],
                      m:    m + 1,
                      mm:   fnPad(m + 1),
                      mmm:  _this.dateFormats.i18n.monthNames[m],
                      mmmm: _this.dateFormats.i18n.monthNames[m + 12],
                      yy:   String(y).slice(2),
                      yyyy: y,
                      h:    H % 12 || 12,
                      hh:   fnPad(H % 12 || 12),
                      H:    H,
                      HH:   fnPad(H),
                      M:    M,
                      MM:   fnPad(M),
                      s:    s,
                      ss:   fnPad(s),
                      l:    fnPad(L, 3),
                      L:    fnPad(L > 99 ? Math.round(L / 10) : L),
                      t:    H < 12 ? _this.dateFormats.TIME_AM.charAt(0) : _this.dateFormats.TIME_PM.charAt(0),
                      tt:   H < 12 ? _this.dateFormats.TIME_AM : _this.dateFormats.TIME_PM,
                      T:    H < 12 ? _this.dateFormats.TIME_AM.charAt(0).toUpperCase() : _this.dateFormats.TIME_PM.charAt(0).toUpperCase(),
                      TT:   H < 12 ? _this.dateFormats.TIME_AM.toUpperCase() : _this.dateFormats.TIME_PM.toUpperCase(),
                      Z:    (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""),
                      o:    (o > 0 ? "-" : "+") + fnPad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4)
                   };

               return mask.replace(token, function ($0) {
                  return ($0 in flags) ? flags[$0] : $0.slice(1, $0.length - 1);
               });
            };
         })();

         /**
          * Alfresco wrapper: delegate to wrapped code
          */
         return dateFormat.apply(arguments.callee, arguments);

      },

      /**
       * Converts a user input time string into a date object.
       * Accepted inputs include: 11am, 11PM, 11:00, 23:00, 11:23 am, 3 p.m., 08:00, 1100, 11, 8, 23.
       * Only accepts hours and minutes. Seconds are zeroed.
       *
       * @instance
       * @param {String} timeString User input time
       * @return {Date}
       */
      parseTime: function alfresco_core_TemporalUtils__parseTime(timeString) {
         /*jshint maxcomplexity:false*/

         var d = new Date(); // Today's date
         var time = timeString.toString().match(/^(\d{1,2})(?::?(\d\d))?\s*(a*)([p]?)\.*m?\.*$/i);

         // Exit early if we've not got a match, if the hours are greater than 24, or greater than 12 if AM/PM is specified, or minutes are larger than 59.
         if (time === null || typeof time === "undefined" || !time[1] || time[1] > 24 || (time[1] > 12 && (time[3]||time[4])) || (time[2] && time[2] > 59))
         {
            return null;
         }

         // Add 12?
         var add12 = false;

         // If we're PM:
         if (time[4])
         {
            add12 = true;
         }

         // if we've got EITHER AM or PM, the 12th hour behaves different:
         // 12am = 00:00 (which is the same as 24:00 if the date is ignored), 12pm = 12:00
         // if we don't have AM or PM, then default to 12 === noon (i.e. add nothing).
         if (time[1] === "12" && (time[3] || time[4]))
         {
            add12 = !add12;
         }

         d.setHours( parseInt(time[1], 10) + (add12 ? 12 : 0) );
         d.setMinutes( parseInt(time[2], 10) || 0 );
         d.setSeconds(0);

         return d;
      },

      /**
       * Render relative dates on the client
       *
       * Converts all ISO8601 dates within the specified container to relative dates.
       * (indicated by <span class="relativeTime">{date.iso8601}</span>)
       *
       * @instance
       * @param {String} id ID of HTML element containing dates for conversion
       */
      renderRelativeTime: function alfresco_core_TemporalUtils__renderRelativeTime(id) {
         query("span.relativeTime", id).forEach(function(node){
            node.innerHTML = this.getRelativeTime(node.innerHTML);
         });
      }

   });
});