7
\$\begingroup\$

LiveDate has the ability to display the current date either by asynchronously fetching the time from a server or by just using the client system date. It's only as accurate / reliable as the source being used.

It's pretty much finished, but before I definitely considered it that I'd like to get a full review of it.

Specifically I'd also appreciate a review on my error handling and the way I'm importing the dependencies into the IIFE by just passing in window. And sendAjaxRequest since this function dependency is optional depending on which method is used to update the time.

Any suggestions for improvement are welcome!

LiveDate.js

var LiveDate = (function (global) {
    'use strict';

    function LiveDate(settings) {
        this.element = settings.element;

        this.timeUrl = settings.timeUrl || null;
        this.format = settings.format || LiveDate.formats.ISO8601;
        this.offset = settings.offset || 0;
        this.errorHtml = settings.errorHtml || 'LiveDate Error';

        this.weekdayNames = {
            'long': settings.weekdayNames && settings.weekdayNames['long'] ? settings.weekdayNames['long'] : [
                'Sunday',
                'Monday',
                'Tuesday',
                'Wednesday',
                'Thursday',
                'Friday',
                'Saturday'
            ],
            'short': settings.weekdayNames && settings.weekdayNames['short'] ? settings.weekdayNames['short'] : [
                'Sun',
                'Mon',
                'Tue',
                'Wed',
                'Thu',
                'Fri',
                'Sat'
            ]
        };

        this.monthNames = {
            'long': settings.monthNames && settings.monthNames['long'] ? settings.monthNames['long'] : [
                'January',
                'February',
                'March',
                'April',
                'May',
                'June',
                'July',
                'August',
                'September',
                'October',
                'November',
                'December'
            ],
            'short': settings.monthNames && settings.monthNames['short'] ? settings.monthNames['short'] : [
                'Jan',
                'Feb',
                'Mar',
                'Apr',
                'May',
                'Jun',
                'Jul',
                'Aug',
                'Sep',
                'Oct',
                'Nov',
                'Dec'
            ]
        };

        this.start();
    }

    LiveDate.prototype.start = function () {
        var self = this;

        if (!this.timeUrl) {
            this._tickIntervalId = global.setInterval((function tick() {
                self._date = new global.Date(new global.Date().getTime() + self.offset);

                displayDate();

                return tick;
            }()), 200);

            return;
        }

        global.sendAjaxRequest({
            url: this.timeUrl,
            headers: { 'X-Requested-With': 'XMLHttpRequest' },
            cacheDocument: false,
            successHandler: successHandler,
            networkErrorHandler: networkErrorHandler
        });

        function successHandler(xhr) {
            if (!(xhr.status >= 200 && xhr.status <= 299)) {
                displayError();

                throw {
                    name: 'AjaxRequestFetchError',
                    context: 'Fetching the document from: ' + self.timeUrl + '.',
                    problem: 'HTTP ' + xhr.status + ' was returned but expected HTTP 200.',
                    solution: 'Double check the URL and make sure it\'s reachable.',
                    toString: function () { return this.name; }
                };
            }

            var requestTime = new global.Date().getTime(),
                previousTime = new global.Date().getTime(),
                serverTime = parseInt(xhr.responseText, 10);

            self._tickIntervalId = global.setInterval((function () {
                tick();

                return tick;
            }()), 200);

            function tick() {
                var NET_REINIT_WAIT_PERIOD = 10000;

                var currentTime = new global.Date().getTime();

                // Detecting client's system date changes to keep ours unaffected.
                // Interval delay increment taken into account as well (+500 ms).
                if ((currentTime - previousTime) < 0 || (currentTime - previousTime) >= 1500) {
                    self.stop();

                    // Introducing a delay particularly for PC's that've just been awoken to
                    // try and prevent an AjaxRequestFetchError from being thrown due to their
                    // Internet connection not being reinitialized yet.
                    global.setTimeout(function () {
                        self.start();
                    }, NET_REINIT_WAIT_PERIOD);

                    return;
                }

                self._date = new global.Date(serverTime + currentTime - requestTime + self.offset);

                displayDate();

                previousTime = currentTime;
            }
        }

        function networkErrorHandler(xhr) {
            displayError();

            throw {
                name: 'AjaxRequestNetworkError',
                context: 'Sending an AJAX request to: ' + self.timeUrl + '.',
                problem: 'Some type of network error or fetch termination occurred. (Error ' + xhr.status + ')',
                solution: 'For the solution, analyse the network level for problems.',
                toString: function () { return this.name; }
            };
        }

        function displayError() {
            self.element.innerHTML = self.errorHtml;
        }

        function displayDate() {
            self.element.innerHTML = self.formatAs(self.format);
        }
    };

    LiveDate.prototype.formatAs = function (format) {
        var self = this;

        function padNumber(number, width, character) {
            number = '' + number;
            character = character || 0;

            if (number.length < width) {
                return new global.Array(width - number.length + 1).join(character) + number;
            }

            return number;
        }

        var dateFormatter = {
            /* DAY */
            // Day of the month with leading zeros (01 to 31).
            d: function () {
                return padNumber(self._date.getUTCDate(), 2);
            },

            // Short textual representation of the day of the week (Mon).
            D: function () {
                return self.weekdayNames['short'][self._date.getUTCDay()];
            },

            // Day of the month without leading zeros (1 to 31).
            j: function () {
                return self._date.getUTCDate();
            },

            // Long textual representation of the day of the week (Monday).
            l: function () {
                return self.weekdayNames['long'][self._date.getUTCDay()];
            },

            /* MONTH */
            // Long textual representation of the month (January).
            F: function () {
                return self.monthNames['long'][self._date.getUTCMonth()];
            },

            // Numeric representation of the month with leading zeros (01 to 12).
            m: function () {
                return padNumber(self._date.getUTCMonth() + 1, 2);
            },

            // Short textual representation of the month (Jan).
            M: function () {
                return self.monthNames['short'][self._date.getUTCMonth()];
            },

            // Numeric representation of the month without leading zeros (1 to 12).
            n: function () {
                return self._date.getUTCMonth() + 1;
            },

            /* YEAR */
            // Short numeric representation of the year (15).
            y: function () {
                return ('' + self._date.getUTCFullYear()).slice(-2);
            },

            // Long numeric representation of the year (2015).
            Y: function () {
                return self._date.getUTCFullYear();
            },

            /* TIME */
            // Lowercase ante meridiem (a.m.) or post meridiem (p.m.).
            a: function () {
                return self._date.getUTCHours() < 12 ? 'a.m.' : 'p.m.';
            },

            // Uppercase ante meridiem (A.M.) or post meridiem (P.M.).
            A: function () {
                return this.a().toUpperCase();
            },

            // 12-hour format of the hour without leading zeros (1 to 12).
            g: function () {
                return self._date.getUTCHours() % 12 || 12;
            },

            // 24-hour format of the hour without leading zeros (0 to 23).
            G: function () {
                return self._date.getUTCHours();
            },

            // 12-hour format of the hour with leading zeros (01 to 12).
            h: function () {
                return padNumber(self._date.getUTCHours() % 12 || 12, 2);
            },

            // 24-hour format of the hour with leading zeros (00 to 23).
            H: function () {
                return padNumber(self._date.getUTCHours(), 2);
            },

            // Minutes with leading zeros (00 to 59).
            i: function () {
                return padNumber(self._date.getUTCMinutes(), 2);
            },

            // Seconds with leading zeros (00 to 59).
            s: function () {
                return padNumber(self._date.getUTCSeconds(), 2);
            },

            /* TIMEZONE */
            // Hour and minute difference to UTC without colon in-between and with leading zeros (+0000).
            O: function () {
                var sign = self.offset < 0 ? '-' : '+',
                    hours = global.Math.floor(global.Math.abs(self.offset) / 3600000),
                    minutes = self.offset / 3600000 % 1 * 60;

                return sign + padNumber(hours, 2) + padNumber(minutes, 2);
            },

            // Hour and minute difference to UTC with colon in-between and leading zeros (+00:00).
            P: function () {
                var O = this.O();

                return O.substr(0, 3) + ':' + O.substr(3);
            }
        };

        return format.replace(/(\\?)([a-z])/gi, function (match, isEscaped, character) {
            return !isEscaped && dateFormatter[character] ? dateFormatter[character]() : character;
        });
    };

    LiveDate.prototype.getTime = function () {
        return this._date.getTime();
    };

    LiveDate.prototype.stop = function () {
        global.clearInterval(this._tickIntervalId);

        delete this._tickIntervalId;
    };

    LiveDate.formats = {
        ATOM: 'Y-m-d\\TH:i:sP',
        ISO8601: 'Y-m-d\\TH:i:sP',
        RSS: 'D, d M Y H:i:s O',
        RFC822: 'D, d M y H:i:s O',
        RFC1036: 'D, d M y H:i:s O',
        RFC1123: 'D, d M Y H:i:s O',
        RFC2822: 'D, d M Y H:i:s O',
        RFC3339: 'Y-m-d\\TH:i:sP',
        W3C: 'Y-m-d\\TH:i:sP'
    };

    return LiveDate;
}(window));

Example

// Server Synced Date
var serverLiveDate = new LiveDate({
    timeUrl: 'time.php',
    element: document.getElementById('server-date'),
    format: 'l j F Y - <\\b>H:i:s</\\b>',
    offset: 720000 // UTC+2
});

// Client System Synced Date
var clientLiveDate = new LiveDate({
    element: document.getElementById('client-date')
});
\$\endgroup\$
7
  • \$\begingroup\$ I don't understand about the practical use of this code, can you explain a typical use case? \$\endgroup\$
    – Vignesh
    Commented Apr 22, 2015 at 14:08
  • 1
    \$\begingroup\$ @Vignesh Isn't it somewhat self-explanatory? \$\endgroup\$ Commented Apr 22, 2015 at 14:20
  • \$\begingroup\$ The code is pretty self explanatory, But I haven't came across a situation where I needed to fetch time from the server (beginner). So I am curious about a real use case. \$\endgroup\$
    – Vignesh
    Commented Apr 22, 2015 at 14:25
  • 2
    \$\begingroup\$ @Vignesh Because JavaScript's Date object is only as reliable as the client system date, which you have no control of. So if their system date is inaccurate, so will the time returned by Date be. If you need a solution that displays the "real" time that's not affected by how the client system date is set, you can get the date from a (reliable) server. For instance, you might have a service that displays different live dates from all over the world. But if you rely on the client system date and it's not set correctly, every date shown by your service will be incorrect as well. \$\endgroup\$ Commented Apr 22, 2015 at 14:36
  • \$\begingroup\$ Does this take into account that the browser and server may be in different time zones? If it doesn't, maybe it isn't very useful (I'm still a little unsure about the use case for this module). \$\endgroup\$ Commented Apr 25, 2015 at 17:19

1 Answer 1

1
\$\begingroup\$

Few things:

  • Your default settings should sit in an object and be referred to with this.something = settings.something || defaults.something. It just makes the code read better.
  • The use of var self = this should be superseded by calls to .bind(this) or .call(this) on the function object. (global.setInterval(function tick() { ... }.call(this))).
  • There's an implicit dependency on sendAjaxRequest. Pass it as an argument to the IIFE.
  • You have too many tick functions. It's unclear which is used where without digging into the scope.
  • Always throw instances of Error. If you need to implement a custom error type, do so.
  • Don't handle the error (displayError()) and then throw an error. Throw the error, and handle it in the relevant catch block.

Also, what's the need for sendAjaxRequest here? How does it do things better than just making a new XMLHttpRequest object and working with that?

A few WTFs when going over your code:

global.setInterval((function () {
    tick(); // wat

    return tick; // WAT
}()), 200);

and

function successHandler(xhr) { // Success!
    if (!(xhr.status >= 200 && xhr.status <= 299)) {
        displayError(); // Doesn't look very successful to me...

and

function networkErrorHandler(xhr) {
    displayError(); // Deja vuu. DRY please.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.