Moment String Interpret (v2)

Revision 2 of this benchmark created by Matt on


Description

Optimizations for Moment string interpretation

Preparation HTML

<script src="https://momentjs.com/downloads/moment.min.js"></script>
<script src="https://momentjs.com/downloads/moment-timezone.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>

Setup

var DEFAULT_TZ = 'Asia/Tokyo';
    
    var ISO_8601_DATE_REGEXP = /^(\d{4})(?:-(\d{2})(?:-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?((?:\-|\+)(?:\d{2}):(?:\d{2})))?)?)?$/;
    
    var DEFAULT_DATE_FORMATS = {
        'HH:mm:ss': {
            regex: /^(\d{2}):(\d{2}):(\d{2})$/,
            match: ['hour', 'minute', 'second']
        },
        "M/D/YYYY": {
            regex: /^([01]?\d)\/([0-3]?\d)\/(\d{4})$/,
            match: ["month", "day", "year"]
        },
        "M/D/YYYY HH:mm A": {
            regex: /^([01]?\d)\/([0-3]?\d)\/(\d{4}) (\d{2}):(\d{2}) ([aApP][mM])$/,
            match: ["month", "day", "year", "hour", "minute", "ampm"]
        }
    };
    
    function oldCompare(date1, date2)
    {
        date1 = toMoment(date1);
        date2 = toMoment(date2);
        return date1 && date2 && (date1.valueOf() - date2.valueOf());
    }
    
    function newCompare(date1, date2)
    {
        if(!date1 || !date2)
        {
            return 0;
        }
        var date1IsString = isString(date1);
        var date2IsString = isString(date2);
        var match1 = date1IsString && date1.match(ISO_8601_DATE_REGEXP);
        var match2 = date2IsString && date2.match(ISO_8601_DATE_REGEXP);
        var date1Timestamp = NaN;
        var date2Timestamp = NaN;
        if(date1IsString && date2IsString && match1 && match2)
        {
            var date1Tz = match1[match1.length - 1] || '+00:00';
            date1Timestamp = match1[1] + (match1[2] || '01') + (match1[3] || '01') + (match1[4] || '00')
                + (match1[5] || '00') + (match1[6] || '00');
            var date2Tz = match2[match2.length - 1] || '+00:00';
            date2Timestamp = match2[1] + (match2[2] || '01') + (match2[3] || '01') + (match2[4] || '00')
                + (match2[5] || '00') + (match2[6] || '00');
            if(!date1Tz || !date2Tz)
            {
                date1Timestamp = NaN;
                date2Timestamp = NaN;
            }
            else if(date1Tz != date2Tz)
            {
                var hasTz = date1.indexOf('T') >= 0;
                date1 = new Date(date1);
                if(!hasTz)
                {
                    date1.setTime(date1.getTime() + date1.getTimezoneOffset()*60000);
                }
                hasTz = date2.indexOf('T') >= 0;
                date2 = new Date(date2);
                if(!hasTz)
                {
                    date2.setTime(date2.getTime() + date2.getTimezoneOffset()*60000);
                }
    
                date1Timestamp = date1.valueOf();
                date2Timestamp = date2.valueOf();
            }
        }
        else
        {
            if(match1)
            {
                hasTz = date1.indexOf('T') >= 0;
                date1 = new Date(date1);
                if(!hasTz)
                {
                    date1.setTime(date1.getTime() + date1.getTimezoneOffset()*60000);
                }
            }
            if(match2)
            {
                hasTz = date2.indexOf('T') >= 0;
                date2 = new Date(date2);
                if(!hasTz)
                {
                    date2.setTime(date2.getTime() + date2.getTimezoneOffset()*60000);
                }
            }
            date1Timestamp = moment.isMoment(date1) || isDate(date1) ? date1.valueOf() : NaN;
            date2Timestamp = moment.isMoment(date2) || isDate(date2) ? date2.valueOf() : NaN;
        }
        if(isNaN(date1Timestamp))
        {
            if(date1 = moment.isMoment(date1) ? date1 : toMoment(date1))
            {
                date1Timestamp = date1.valueOf();
            }
            else
            {
                return false;
            }
        }
        if(isNaN(date2Timestamp))
        {
            if(date2 = moment.isMoment(date2) ? date2 : toMoment(date2))
            {
                date2Timestamp = date2.valueOf();
            }
            else
            {
                return false;
            }
        }
        return date1Timestamp - date2Timestamp;
    }
    
    function toMoment(date, formats, tz)
    {
        var found = false;
        tz = tz || DEFAULT_TZ;
        if(date === false || date === null)
        {
            return false;
        }
        if(isString(date))
        {
            if(date == 'now')
            {
                return moment.tz(tz);
            }
            date = $.trim(date);
            var found;
            if(found = date.match(ISO_8601_DATE_REGEXP))
            {
                var hasTz = date.indexOf('T') >= 0;
                var tmpDate = new Date(date);
                date = moment.tz(tmpDate, tz);
                if(!hasTz)
                {
                    date.add(date.zone(), 'minutes');
                }
            }
            else
            {
                formats = $.extend({}, DEFAULT_DATE_FORMATS, formats || {});
                $.each(formats, function(format, regex){
                    if(found = date.match(regex.regex || regex))
                    {
                        if(regex.match)
                        {
                            var dateObj = {year: 1970, month: 1, day: 1, hour: 0, minute: 0, second: 0};
                            for(var i = 0; i < regex.match.length; i++)
                            {
                                dateObj[regex.match[i]] = found[i + 1];
                            }
                            if(dateObj.ampm && dateObj.hour)
                            {
                                if(dateObj.hour == 12)
                                {
                                    if(dateObj.ampm.charAt(0).toLowerCase() == 'a')
                                    {
                                        dateObj.hour = 0;
                                    }
                                }
                                else if(dateObj.ampm.charAt(0).toLowerCase() == 'p')
                                {
                                    dateObj.hour = parseInt(dateObj.hour) + 12;
                                }
                            }
                            var tmpDate = new Date(dateObj.year, dateObj.month - 1, dateObj.day, dateObj.hour, dateObj.minute, dateObj.second);
                            date = moment.tz(tmpDate, tz);
                            date.subtract(tmpDate.getTimezoneOffset() - date.zone(), 'minutes');
                        }
                        else
                        {
                            date = moment.tz(date, format, tz);
                        }
                        return false;
                    }
                });
            }
            if(!found)
            {
                return false;
            }
        }
        else if(moment.isMoment(date))
        {
            date = date.clone().tz(tz);
        }
        else
        {
            date = moment.tz(date, tz);
        }
        return moment.isMoment(date) && date.isValid() && date;
    }
    
    function isString(value){return typeof value == 'string';}
    function isDate(value){
      return Object.prototype.toString.call(value) === '[object Date]';
    }
    
    if (!Date.prototype.printf)
    {
        Date.prototype.printf = (function ()
        {
            function pad(number)
            {
                var r = String(number);
                if (r.length === 1)
                {
                    r = '0' + r;
                }
                return r;
            }
    
            return function (format)
            {
                var d = this.getDate(),
                    D = this.getDay(),
                    m = this.getMonth(),
                    y = this.getFullYear(),
                    h = this.getHours(),
                    M = this.getMinutes(),
                    s = this.getSeconds(),
                    tzOff = this.getTimezoneOffset(),
                    U = Math.floor(this.getTime() / 1000),
                    self = this,
                    escaped = false,
                    leap = ((0 == (y % 4)) && ((0 != (y % 100)) || (0 == (y % 400))));
                return $.map(format.split(''),function (part)
                {
                    if (escaped)
                    {
                        escaped = false;
                        return part;
                    }
                    switch (part)
                    {
                        case '\\':
                            escaped = true;
                            return "";
                        // Day
                        case "d":
                            return pad(d);
                        case "D":
                            return ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][D];
                        case "j":
                            return d;
                        case "l":
                            return ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][D];
                        case "N":
                            return D == 0 ? 7 : D;
                        case "S":
                            return ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10];
                        case "w":
                            return D;
                        case "z":
                            var ret = 0;
                            $.each([31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30], function (key, val)
                            {
                                if (key == m)
                                {
                                    return false;
                                }
                                ret += val;
                            });
                            return ret + (d - 1);
                        // Week
                        case "W":
                            return function (date)
                            {
                                date.setDate(d - (D + 6) % 7 + 3); // Nearest Thu
                                var ms = date.valueOf(); // GMT
                                date.setMonth(0);
                                date.setDate(4); // Thu in Week 1
                                return Math.round((ms - date.valueOf()) / (7 * 864e5)) + 1;
                            }(self);
                        // Month
                        case "F":
                            return ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August',
                                'September', 'October', 'November', 'December'][m];
                        case "m":
                            return pad(m + 1);
                        case "M":
                            return ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][m];
                        case "n":
                            return m + 1;
                        case "t":
                            return  [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][m];
                        // Year
                        case "L":
                            return leap ? 1 : 0;
                        //case "o": return y; // almost always the same as Y but not really so leave it for someone else
                        case "y":
                            return String(y).substr(2, 2);
                        case "Y":
                            return y;
                        // Time
                        case "a":
                            return h < 12 ? 'am' : 'pm';
                        case "A":
                            return h < 12 ? 'AM' : 'PM';
                        case "g":
                            return (h - 1) % 12 + 1;
                        case "G":
                            return h;
                        case "h":
                            return pad((h - 1) % 12 + 1);
                        case "H":
                            return pad(h);
                        case "i":
                            return pad(M);
                        case "s":
                            return pad(s);
                        // Timezone
                        case "O":
                            var abs = Math.abs(tzOff);
                            return (tzOff <= 0 ? "+" : "-") + pad(Math.floor(abs / 60)) + pad(abs % 60);
                        case "P":
                            var abs = Math.abs(tzOff);
                            return (tzOff <= 0 ? "+" : "-") + pad(Math.floor(abs / 60)) + ":" + pad(abs % 60);
                        case "Z":
                            return tzOff * 60;
                        // Full Date/Time
                        case "c":
                            return self.printf("Y-m-d\\TH:i:sP");
                        case "r":
                            return self.printf("D, d M Y H:i:s O");
                        case "U":
                            return U;
                        default:
                            return part;
                    }
                }).join('');
            };
        })();
    }
    function printfParams(moment){
        var split = moment.format("d Do YYYY M").split(' ');
        var leap = ((0 == (split[2] % 4)) && ((0 != (split[2] % 100)) || (0 == (split[2] % 400))));
        return {
            N: split[0] == 0 ? 7 : split[0],
            S: '[' + split[1].substr(-2) + ']',
            t: [null, 31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][split[3]],
            L: leap ? 1 : 0
        };
    }
    $.extend(moment, {
        convertPrintfToFormat: function(format){
            var newFormat = '';
            var extras = false;
            var addSeparator = false;
            for(var i = 0; i < format.length; i++)
            {
                var char = format[i];
                if(char == '\\')
                {
                    newFormat += char + format[++i];
                    continue;
                }
                if(char == '[')
                {
                    newFormat += "\\" + char;
                    continue;
                }
                var prevAddSeparator = addSeparator;
                addSeparator = true;
                var newChar = null;
                switch(char)
                {
                    // Day
                    case "d": newChar = 'DD'; break;
                    case "D": newChar = 'ddd'; break;
                    case "j": newChar = 'D'; break;
                    case "l": newChar = 'dddd'; break;
                    case "N": newChar = (extras || (extras = printfParams(this)))[char]; addSeparator = false; break;
                    case "S": newChar = (extras || (extras = printfParams(this)))[char]; addSeparator = false; break;
                    case "w": newChar = 'd'; break;
                    case "z": newChar = 'DDD'; break;
    
                    // Week
                    case 'W': newChar = 'w'; break;
    
                    // Month
                    case 'F': newChar = 'MMMM'; break;
                    case 'm': newChar = 'MM'; break;
                    case 'M': newChar = 'MMM'; break;
                    case 'n': newChar = 'M'; break;
                    case "t": newChar = (extras || (extras = printfParams(this)))[char]; addSeparator = false; break;
    
                    // Year
                    case "L": newChar = (extras || (extras = printfParams(this)))[char]; addSeparator = false; break;
                    // case "o": ???
                    case 'Y': newChar = 'YYYY'; break;
                    case 'y': newChar = 'YY'; break;
                    
                    // Time
                    case 'a': newChar = 'a'; break;
                    case 'A': newChar = 'A'; break;
                    // case "B": ???
                    case 'g': newChar = 'h'; break;
                    case 'G': newChar = 'H'; break;
                    case 'h': newChar = 'hh'; break;
                    case 'H': newChar = 'HH'; break;
                    case 'i': newChar = 'mm'; break;
                    case 's': newChar = 'ss'; break;
                    // case "u": newChar =  'SSS000'; break; <-- Close but not truly accurate
                    
                    // Timezone
                    case "e": newChar = 'zz'; break;
                    case "I": newChar = this.isDST() ? 1 : 0; addSeparator = false; break;
                    case "O": newChar = 'ZZ'; break;
                    case "P": newChar = 'Z'; break;
                    case "T": newChar = 'z'; break;
                    case "Z": newChar = this.zone() * 60; addSeparator = false; break;
    
                    // Full Date/Time
                    case "c": newChar = 'YYYY-MM-DD\\THH:mm:ssZ'; break;
                    case "r": newChar = 'ddd, DD MMM YYYY HH:mm:ss ZZ'; break;
                    case "U": newChar = 'X'; break;
                    default:
                        addSeparator = false;
                        if(char.match(/[A-Za-z]/))
                        {
                            newChar = '[' + char + ']';
                        }
                        else
                        {
                            newChar = char;
                        }
                        break;
                }
                if(addSeparator && prevAddSeparator)
                {
                    newFormat += '[]';
                }
                newFormat += newChar;
            }
            return newFormat;
        }
    });
    $.extend(moment.fn, {
        printf: function(format){
            return this.format(moment.convertPrintfToFormat(format));
        }
    });
    
    var firstDate = new Date('2014-02-23T01:02:03-05:00');
    var firstMoment = toMoment(new Date('2014-02-23T01:02:03-05:00'));
    
    var secondDate = new Date('2015-03-24T02:03:04-05:00');
    var secondMoment = toMoment(new Date('2015-03-24T02:03:04-05:00'));

Test runner

Ready to run.

Testing in
TestOps/sec
New ISO-8601 w/Same Timezone
newCompare('2014-02-23T01:02:03-05:00', '2015-03-24T02:03:04-05:00');
ready
Old ISO-8601 w/Same Timezone
oldCompare('2014-02-23T01:02:03-05:00', '2015-03-24T02:03:04-05:00');
ready
New ISO-8601 w/Different Timezone
newCompare('2014-02-23T01:02:03-05:00', '2015-03-24T02:03:04-06:00');
ready
Old ISO-8601 w/Different Timezone
oldCompare('2014-02-23T01:02:03-05:00', '2015-03-24T02:03:04-06:00');
ready
New ISO and Date
newCompare('2014-02-23T01:02:03-05:00', secondDate);
ready
Old ISO and Date
oldCompare('2014-02-23T01:02:03-05:00', secondDate);
ready
New ISO and Moment
newCompare('2014-02-23T01:02:03-05:00', secondMoment);
ready
Old ISO and Moment
oldCompare('2014-02-23T01:02:03-05:00', secondMoment);
ready
New Date and Moment
newCompare(firstDate, secondMoment);
ready
Old Date and Moment
oldCompare(firstDate, secondMoment);
ready
New 2 Moments
newCompare(firstMoment, secondMoment);
ready
Old 2 Moments
oldCompare(firstMoment, secondMoment);
ready
New 2 Dates
newCompare(firstDate, secondDate);
ready
Old 2 Dates
oldCompare(firstDate, secondDate);
ready
New ISO and Date
newCompare('2014-02-23T01:02:03-05:00', secondDate);
ready
Old ISO and Date
oldCompare('2014-02-23T01:02:03-05:00', secondDate);
ready
New non-ISO
newCompare('02/23/2013 01:02 AM', '03/24/2014 02:03 AM');
ready
Old non-ISO
oldCompare('02/23/2013 01:02 AM', '03/24/2014 02:03 AM');
ready

Revisions

You can edit these tests or add more tests to this page by appending /edit to the URL.