Source: fuzzytime.js

(function(root) {
    'use strict';

    /**
     * Constructs a new FuzzyTime instance.
     * @global
     * @class FuzzyTime
     * @author Robert Bost <bostrt at gmail dot com>
     * @license MIT
     */
    var FuzzyTime = function(){
        this.ats = [];
        this.befores = [];
        this.afters = [];
    };

    /**
     * Enumeration of time units supported by FuzzyTimeJS.
     * @memberOf FuzzyTime
     * @enum {number}
     * @readonly
     */
    FuzzyTime.Unit = {
        YEAR : 6,
        MONTH: 5,
        WEEK: 4,
        DAY: 3,
        HOUR: 2,
        MINUTE: 1,
        SECOND: 0
    };

    /**
     * Table storing conversion between IS0 8601 abbreviations, {@link FuzzyTime.Unit}s,
     * string format variables, and the time period/unit in seconds.
     * @private
     * @ignore
     */
    FuzzyTime.conversions = [
        {argument : '%y', multiplier: 31536000, unit: FuzzyTime.Unit.YEAR, abbrev: 'Y'},
        {argument : '%M', multiplier: 2592000, unit: FuzzyTime.Unit.MONTH, abbrev: 'M'},
        {argument : '%w', multiplier: 604800, unit: FuzzyTime.Unit.WEEK, abbrev: 'W'},
        {argument : '%d', multiplier: 86400, unit: FuzzyTime.Unit.DAY, abbrev: 'D'},
        {argument : '%h', multiplier: 3600, unit: FuzzyTime.Unit.HOUR, abbrev: 'h'},
        {argument : '%m', multiplier: 60, unit: FuzzyTime.Unit.MINUTE, abbrev: 'm'},
        {argument : '%s', multiplier: 1, unit: FuzzyTime.Unit.SECOND, abbrev: 's'}
    ];

    /**
     * Get the days in the month of the given date.
     * @param {Date} date The date that you want to count the days in the month of.
     * @returns {number} days in the month in the given date
     * @private
     */
    FuzzyTime.getDaysInMonth = function(date) {
        switch(date.getMonth()) {
            case 0:
            case 2:
            case 4:
            case 6:
            case 7:
            case 9:
            case 11:
                return 31;
            case 3:
            case 5:
            case 8:
            case 10:
                return 30;
            case 1:
                return isLeapYear(date) ? 29 : 28;
        }
    };

    /**
     * Check if the given date is a leap year.
     * @private
     * @param {Date} date The date that you want to check for a leap year.
     * @returns {boolean} True if date is a leap year. False otherwise.
     */
     FuzzyTime.isLeapYear = function(date) {
        var year = date.getFullYear();
        if (year % 400 === 0) {
            return true;
        } else if (year % 100 === 0) {
            return false;
        } else if (year % 4 === 0) {
            return true;
        } else {
            return false;
        }
    };

    /**
     * Get the {@link FuzzyTime.Unit} enum of a given ISO 8601 duration unit.
     * @memberOf FuzzyTime
     * @private
     * @param {string} string The ISO 8601 Duration unit (e.g. "Y", "M", "S", etc)
     * {@link FuzzyTime.Unit} of.
     * @returns {FuzzyTime.Unit} time unit enum
     */
    FuzzyTime.findFuzzyTimeUnit = function(string) {
        for (var i in FuzzyTime.conversions) {
            if (FuzzyTime.conversions[i].abbrev === string) {
                return FuzzyTime.conversions[i].unit;
            }
        }
    };

    /**
     * Convert an IS0 8601 Duration into something the FuzzyTimeJS library can work
     * with a little easier.
     *
     * @memberOf FuzzyTime
     * @private
     * @param {string} delta An IS0 8601 Duration
     * @returns {list} The duration in an array of
     * maps like:
     * <pre>[
     *   {time: 1, unit: FuzzyTime.Unit.MINUTE},
     *   {time: 30, unit: FuzzyTime.Unit.SECOND},
     *   ...
     * ]</pre>
     */
    FuzzyTime.parseDuration = function(delta) {
        var lowerDelta = delta.toLowerCase().trim();
        var period, time, value = "";
        delta = [];
        time = lowerDelta.split("t")[1];
        period = lowerDelta.split("t")[0].split("p").join("");

        if (period != null && period.length > 0) {
            for (var i = 0; i < period.length; i++) {
                if (/[a-z]/.test(period[i])) {
                    // We've hit the Unit part
                    delta.push({time: parseFloat(value), unit: FuzzyTime.findFuzzyTimeUnit(period[i].toUpperCase())});
                    value = "";
                } else {
                    value += period[i];
                }
            }
        }
        
        if (time != null && time.length > 0) {
            for (var i = 0; i < time.length; i++) {
                if (/[a-z]/.test(time[i])) {
                    // We've hit the Unit part
                    delta.push({time: parseFloat(value), unit: FuzzyTime.findFuzzyTimeUnit(time[i])});
                    value = "";
                } else {
                    value += time[i];
                }
            }
        }
        
        return delta;
    };


    /**
     * Adds the given time value to the given date. The time value's unit is
     * specified by the fuzzyTimeUnit parameter.
     * @private
     * @memberOf FuzzyTime
     * @param {Date} date - A date you want to add time to
     * @param {float} timeValue - The time value you want to add to the given date
     * @param {FuzzyTime.Unit} fuzzyTimeUnit - The unit of time you want to add to given date
     * @returns {Date} A date with all given time added to it.
     */
    FuzzyTime.addTime = function(date, timeValue, fuzzyTimeUnit) {
        var dateCopy = new Date(date);
        var timeValueInt = Math.floor(timeValue);
        var decimal = 0;

        //console.log("addTimeString ( " + date + ", " + timeValue + ", " + fuzzyTimeUnit);

        if (timeValueInt < timeValue) {
            // The Math.floor call we did earlier caused the decimal place to be dropped...making
            // the timeValueInt variable be smaller than original timeValue.
            // Add the remaining decimal part of timestring.
            decimal = timeValue - timeValueInt;
        }

        switch(fuzzyTimeUnit) {
            case FuzzyTime.Unit.YEAR:
                dateCopy.setYear(dateCopy.getFullYear() + timeValueInt);
                if (decimal > 0) {
                    // Take the new current year and add some days!
                    var daysInYear = FuzzyTime.isLeapYear(dateCopy) ? 366 : 365;
                    decimal = Math.round(decimal*1000)/1000;
                    dateCopy = FuzzyTime.addTime(dateCopy, decimal * daysInYear, FuzzyTime.Unit.DAY);
                }
                break;
            case FuzzyTime.Unit.MONTH:
                dateCopy.setMonth(dateCopy.getMonth() + timeValueInt);
                if (decimal > 0) {
                    // Add some dayzzeee
                    var daysInMonth = FuzzyTime.getDaysInMonth(dateCopy);
                    dateCopy = FuzzyTime.addTime(dateCopy, decimal * daysInMonth, FuzzyTime.Unit.DAY);
                }
                break;
            case FuzzyTime.Unit.WEEK:
                dateCopy.setDate(dateCopy.getDate() + (timeValueInt * 7));
                if (decimal > 0) {
                    // Add some days.
                    dateCopy = FuzzyTime.addTime(dateCopy, decimal * 7, FuzzyTime.Unit.DAY);
                }
                break;
            case FuzzyTime.Unit.DAY:
                dateCopy.setDate(dateCopy.getDate() + timeValueInt);
                if (decimal > 0) {
                    // Add some hours
                    decimal = Math.round(decimal*1000)/1000;
                    dateCopy = FuzzyTime.addTime(dateCopy, decimal * 24, FuzzyTime.Unit.HOUR);
                }
                break;
            case FuzzyTime.Unit.HOUR:
                dateCopy.setHours(dateCopy.getHours() + timeValueInt);
                if (decimal > 0) {
                    // Add some minutes
                    decimal = Math.round(decimal*1000)/1000;
                    dateCopy = FuzzyTime.addTime(dateCopy, decimal * 60, FuzzyTime.Unit.MINUTE);
                }
                break;
            case FuzzyTime.Unit.MINUTE:
                dateCopy.setMinutes(dateCopy.getMinutes() + timeValueInt);
                if (decimal > 0) {
                    // Add some minutes
                    decimal = Math.round(decimal*1000)/1000;
                    dateCopy = FuzzyTime.addTime(dateCopy, decimal * 60, FuzzyTime.Unit.SECOND);
                }
                break;
            case FuzzyTime.Unit.SECOND:
                dateCopy.setSeconds(dateCopy.getSeconds() + Math.round(timeValueInt));
                break;
        }

        return dateCopy;
    };

    /**
     * Configure this FuzzyTime to return the given string format (with values plugged
     * in) when {@link FuzzyTime#build} is called and the given time delta has been
     * reached.
     * @param {string} format The string format
     * @param {(string|list)} delta The time delta at which to display. This can be either
     * an ISO 8601 Duration (string) or a list of maps like:
     * <pre>[
     *   {time: 1, unit: FuzzyTime.Unit.MINUTE},
     *   {time: 30, unit: FuzzyTime.Unit.SECOND},
     *   ...
     * ]</pre>
     * @instance
     * @memberOf FuzzyTime
     */
    FuzzyTime.prototype.at = function(format, delta) {
        if (typeof delta === 'string') {
            // Parse string as ISO 8601 duration
            this.ats.push({format: format, delta: FuzzyTime.parseDuration(delta)});
        } else {
            this.ats.push({format: format, delta: delta});
        }
    };

    /**
     * Configures this FuzzyTime instance to return the given string format (with
     * values plugged in) any time {@link FuzzyTime#build} is called and
     * <b>before</b> the given time delta has passed.
     * @param {string} format The string format
     * @param {(string|list)} delta The time delta at which to display. This can be either
     * an ISO 8601 Duration (string) or a list of maps like:
     * <pre>[
     *   {time: 1, unit: FuzzyTime.Unit.MINUTE},
     *   {time: 30, unit: FuzzyTime.Unit.SECOND},
     *   ...
     * ]</pre>
     * @instance
     * @memberOf FuzzyTime
     */
    FuzzyTime.prototype.before = function(format, delta) {
        if (typeof delta === 'string') {
            // Parse string as ISO 8601 duration
            this.befores.push({format: format, delta: FuzzyTime.parseDuration(delta)});
        } else {
            this.befores.push({format: format, delta: delta});
        }
    };

    /**
     * Configures this FuzzyTime instance to return the given string format (with
     * values plugged in) any time that {@link FuzzyTime#build} is called and
     * <b>after</b> the given time delta has passed
     *
     * @param {string} format The string format
     * @param {(string|list)} delta The time delta at which to display. This can be either
     * an ISO 8601 Duration (string) or a list of maps like:
     * <pre>[
     *   {time: 1, unit: FuzzyTime.Unit.MINUTE},
     *   {time: 30, unit: FuzzyTime.Unit.SECOND},
     *   ...
     * ]</pre>
     * @memberOf FuzzyTime
     * @instance
     */
    FuzzyTime.prototype.after = function(format, delta) {
        if (typeof delta === 'string') {
            // Parse string as ISO 8601 duration
            this.afters.push({format: format, delta: FuzzyTime.parseDuration(delta)});
        } else {
            this.afters.push({format: format, delta: delta});
        }
    };

    /**
     * Populates the variables in a string template/format.
     * @memberOf FuzzyTime
     * @protected
     * @ignore
     * @param {number} seconds - The time delta in seconds
     * @param {string} format - The format that needs to have variables populated
     */
    FuzzyTime.prototype.buildString = function(seconds, format) {
        // Loop over all possible template arguments
        for (var idx in FuzzyTime.conversions) {
            var curr = FuzzyTime.conversions[idx];
            // Check if the string format contains the current argument we are
            // looping over
            if (format.search(curr.argument) >= 0) {
                // The format contains the argument we're looping over.
                // Convert the remaining seconds to the argument's unit (year, min, etc.)
                var argVal = Math.floor(seconds / curr.multiplier);
                seconds = seconds - argVal * curr.multiplier;
                format = format.replace(curr.argument, argVal);
                // Recurse, need to continue replacing argument.
                return this.buildString(seconds, format);
            }
        }

        return format;
    };

    /**
     * Build a "fuzzy" interpretation of the difference between the provided dates
     * based on previous configurations (using {@link FuzzyTime#at}, {@link FuzzyTime#before}, and
     * {@link FuzzyTime#after}).
     * @param {date} date - The starting date
     * @param {date=} referenceDate - The ending date. If nothing is provided
     * for this parameter, it defaults to <b>now</b> (i.e. <tt>new Date()</tt>).
     * @memberOf FuzzyTime
     * @instance
     * @returns {string} The "fuzzy" timestamp for the differene between the two
     * provided dates (if only one date is provided, <i>now</i> is used for 
     * second date). If there is no "at", "before", or "after" configuration
     * that applies to the difference between the dates provided, the locale
     * string of the <b>date</b> parameter is provided.
     */
    FuzzyTime.prototype.build = function(date, referenceDate) {
        if (typeof referenceDate === 'undefined' || referenceDate === null) {
            referenceDate = new Date();
        }
        
        var delta = Math.round((referenceDate - date)/1000);

        // Check "at"
        for (var atIdx in this.ats) {
            var xDate = new Date(date);
            var at = this.ats[atIdx];
            
            for (var i in at.delta) {
                xDate = FuzzyTime.addTime(xDate, at.delta[i].time, at.delta[i].unit);
            }

            // Is the difference between the two provided dates equal
            // to the "at" configuration we're looping on?
            if (xDate.toUTCString() === referenceDate.toUTCString()) {
                return this.buildString(delta, at.format);
            }
        }

        // FIXME: The `date < refreferenceDate` does not allow for future date deltas.
        if (delta > 0 && date < referenceDate) {
            // Check "before"
            
            var returnVar = null,
                    xDateDelta = null;
            for (var beforeIdx in this.befores) {
                var xDate = new Date(date);
                var before = this.befores[beforeIdx];

                for (var i in before.delta) {
                    xDate = FuzzyTime.addTime(xDate, before.delta[i].time, before.delta[i].unit);
                }

                // Is the difference between the two provided dates less than
                // the "before" configuration we're looping on?
                if (!(date > referenceDate) && xDate >= referenceDate) {
                    if (xDateDelta === null) {
                        xDateDelta = xDate - referenceDate;
                    }

                    if (xDateDelta >= (xDate - referenceDate)) {
                        xDateDelta = xDate - referenceDate;
                        returnVar = this.buildString(delta, before.format);
                    }
                }
            }

            if (returnVar !== null) {
                return returnVar;
            }

            xDateDelta = null;
            returnVar = null;
            
            // Check "after"
            for (var afterIdx in this.afters) {
                var xDate = new Date(date);
                var after = this.afters[afterIdx];

                for (var i in after.delta) {
                    xDate = FuzzyTime.addTime(date, after.delta[i].time, after.delta[i].unit);
                }

                if (xDate <= referenceDate) {
                    if (xDateDelta === null) {
                        xDateDelta = xDate - referenceDate;
                    }

                    if (xDateDelta >= (xDate - referenceDate)) {
                        xDateDelta = xDate - referenceDate;
                        returnVar = this.buildString(delta, after.format);
                    }
                }
            }

            if (returnVar !== null) {
                return returnVar;
            }
        }

        // There were not any "at", "before", or "after" configurations
        // that apply to the date difference. Return the exact date...
        return date.toLocaleString();
    };

    // Export the FuzzyTime class to the world
    if (typeof define !== 'undefined' && define.amd) {
        define(FuzzyTime);
    } else if (typeof exports === 'object') {
        module.exports = FuzzyTime;
    } else {
        root.FuzzyTime = FuzzyTime;
    }
    
})(this);