11

I'd like to change the behaviour of the standard Date object. Years between 0..99 passed to the constructor should be interpreted as fullYear (no add of 1900). But my following function doesn't work

var oDateConst = Date.prototype.constructor; // save old contructor

Date.prototype.constructor = function () {
    var d = oDateConst.apply(oDateConst, arguments); // create object with it
    if ( ((arguments.length == 3) || (arguments.length == 6))
        && ((arguments[0] < 100) && (arguments[0] >= 0))) {
        d.setFullYear(arguments[0]);
    }
    return d;
}

Why does it never get called? How would you solve this problem?

Lauber Bernhard
  • 175
  • 1
  • 1
  • 6
  • The article http://pivotallabs.com/users/pjaros/blog/articles/1368-javascript-constructors-prototypes-and-the-new-keyword helped me a lot understanding how object creation in Javascript works. To override the `Date.prototype.constructor` doesn't help before object creation. Will try to re-write the `Date` function and let you know – Lauber Bernhard Dec 12 '12 at 15:18
  • The code `var oDateFnctn = oDateFnctn || Date; function Date(){ var d = new oDateFnctn(arguments); var ac = arguments.length; var ay = arguments[0]; if (((ac == 3) || (ac == 6)) && (ay < 100) && (ay >=0)) { d.setFullYear(ay); } return d; }` results in a **Uncaught RangeError: Maximum call stack size exceeded** error. I'm stuck now – Lauber Bernhard Dec 12 '12 at 15:27
  • 1
    Just **do not overwrite** the native constructors with **custom** behavior. Never. Build your own function for that. – Bergi Dec 12 '12 at 15:51
  • 1
    Forget [that article](http://pivotallabs.com/users/pjaros/blog/articles/1368-javascript-constructors-prototypes-and-the-new-keyword)!!! What it states for "step 2" is so completely wrong I did not care to read the rest of it. Have a look at [MDN's description of `new`](https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Operators/new) instead. – Bergi Dec 12 '12 at 15:56
  • 1
    Should probably at least be `.length` rather than `.lenght` – Esailija Dec 12 '12 at 12:02

4 Answers4

35

The reason it never gets called is because you're changing the constructor property on Date.prototype. However you're probably still creating a date using the code new Date(). So it never uses your constructor. What you really want to do is create your own Date constructor:

function MyDate() {
    var d = Date.apply(Date, arguments);
    if ((arguments.length == 3 || arguments.length == 6)
        && (arguments[0] < 100 && arguments[0] >= 0)) {
        d.setFullYear(arguments[0]);
    return d;
}

Then you can create your new date like this:

var d = MyDate();

Edit: Instead of using Date.apply I would rather use the following instantiate function which allows you to apply arguments to a constructor function:

var bind = Function.bind;
var unbind = bind.bind(bind);

function instantiate(constructor, args) {
    return new (unbind(constructor, null).apply(null, args));
}

This is how I would implement the new date constructor:

function myDate() {
    var date = instantiate(Date, arguments);
    var args = arguments.length;
    var arg = arguments[0];

    if ((args === 3 || args == 6) && arg < 100 && arg >= 0)
        date.setFullYear(arg);
    return date;
}

Edit: If you want to override the native Date constructor then you must do something like this:

Date = function (Date) {
    MyDate.prototype = Date.prototype;

    return MyDate;

    function MyDate() {
        var date = instantiate(Date, arguments);
        var args = arguments.length;
        var arg = arguments[0];

        if ((args === 3 || args == 6) && arg < 100 && arg >= 0)
            date.setFullYear(arg);
        return date;
    }
}(Date);
Community
  • 1
  • 1
Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299
  • Thank you What if I like to create the Date with `var d = Date()` instead of a own class? – Lauber Bernhard Dec 12 '12 at 14:41
  • 1
    I added an example on how to do that. See this fiddle: http://jsfiddle.net/ubmzs/ – Aadit M Shah Dec 12 '12 at 15:48
  • @AaditMShah Hi! Sorry I'm two years late to the party. I have the same need, but what was so special about the `clockwork.instantiate` method? The repository seems to not exist anymore so I cannot check myself. – Alpha Aug 18 '14 at 20:41
  • @Alpha At the time when I wrote the answer I used to maintain the clockwork library. Now I do not maintain it anymore because I realized that it sucks. So I edited the answer and provided a definition for the `instantiate` function. – Aadit M Shah Aug 19 '14 at 16:15
  • 1
    @Alpha So here's the demo of the new code: http://jsfiddle.net/ubmzs/12/. In addition I found a clone of my original repository in case you are interested: https://github.com/npmcomponent/aaditmshah-clockwork. Thank God that people fork code. Note to self: never delete information no matter what. What you may consider as garbage could be useful to other people. =) – Aadit M Shah Aug 19 '14 at 16:26
  • @AaditMShah I really appreciate you taking the time to look those up! Thanks! – Alpha Aug 19 '14 at 16:56
  • Is there any way to override the "constructor" called to create an object literal, as in `{a: 1}`, without having to do `Object({a:1})`? –  Sep 26 '14 at 04:51
  • @AaditMShah I am using your code to override Date native constructor, which works great but any idea why new Date() instaceof Date gives false? A plugin I´m using stops working because of that – juvian Jan 27 '15 at 18:42
  • @AaditMShah Thanks a lot! Working perfectly – juvian Jan 28 '15 at 17:26
  • @AaditMShah: But the overridden Date, wouldn't contain the date methods like UTC, parse, now. – sakthi Oct 24 '15 at 15:28
  • @AaditMShah: is there any way to add UTC, parse, now functionality to this overridden Date. – Dimuth Lochana Oct 14 '16 at 07:09
3

Piggybacking on Aadit M Shah's native date constructor override - this should be a reply but I don't have enough SO rep for that - as @sakthi mentioned, you'd lose your native Date methods by doing it that way. This sucks a bit because Date methods are non-enumerable, so you have to implement a bit of a hack to clone them.

First off, I'd recommend not doing it this way unless you have to. In my case, I was working in an application with a bunch of legacy code that was constructing dates using the format "m-d-yyyy", which works in chrome but not safari. I couldn't just do a find/replace in the app, because there were lots of instances where the date strings were being pulled from the service layer or the database. So, I decided to override the Date constructor in the case where there's a datestring argument in "m-d-yyyy" format. I wanted it to be as minimally-invasive as possible, so that it functions as a normal Date otherwise.

Here are my changes - it should allow you to override date with some changes to the constructor, but everything else the same. You're going to want to change the MyDate constructor before instantiate is called to do whatever you want the constructor to handle. This will happen BEFORE the system Date constructor gets applied.

var bind = Function.bind;
var unbind = bind.bind(bind);

function instantiate(constructor, args) {
    return new (unbind(constructor, null).apply(null, args));
}

Date = function (Date) {

    // copy date methods - this is a pain in the butt because they're mostly nonenumerable
    // Get all own props, even nonenumerable ones
    var names = Object.getOwnPropertyNames(Date);
    // Loop through them
    for (var i = 0; i < names.length; i++) {
        // Skip props already in the MyDate object
        if (names[i] in MyDate) continue;
        // Get property description from o
        var desc = Object.getOwnPropertyDescriptor(Date, names[i]);
        // Use it to create property on MyDate
        Object.defineProperty(MyDate, names[i], desc);
    }

    return MyDate;

    function MyDate() {
        // we only care about modifying the constructor if a datestring is passed in
        if (arguments.length === 1 && typeof (arguments[0]) === 'string') {
            // if you're adding other date transformations, add them here

            // match dates of format m-d-yyyy and convert them to cross-browser-friendly m/d/yyyy
            var mdyyyyDashRegex = /(\d{1,2})-(\d{1,2})-(\d{4})/g;
            arguments[0] = arguments[0].replace(mdyyyyDashRegex, function (match, p1, p2, p3) {
                return p1 + "/" + p2 + "/" + p3;
            });
        }

        // call the original Date constructor with whatever arguments are passed in here
        var date = instantiate(Date, arguments);

        return date;
    }
}(Date);

references:

Community
  • 1
  • 1
  • This is a great solution for completely replacing `Date` without impacting other code. The one thing I notice is that this will fail `new Date instanceof Date`, which could cause trouble in some cases. You can fix this by adding `MyDate.prototype = Date.prototype;` right before `return MyDate;` which according to my brief testing doesn't break any other functionality. – Dan Hlavenka Oct 07 '16 at 15:06
1

With reference to the technique mentioned in Matthew Albert's post, apart from the point which Dan Hlavenka posted, there is one more test scenario which fails. See the following code:

typeof Date() == typeof new Date()     //Should be false, but it returns true

In a legacy project, there is a chance the above scenario could break few scenarios. Apart from the above and what Dan Hlavenka pointed, I agree that this is the most complete solution so far.

0

Here is a solutions that is very flexible. It handles (I believe), all the different cases.

DateStub - Allows for stubbing out the Date function.
If you have 'new Date(...). ...' sprinkled throughout your code, and you want to test it, this is for you. It also works with 'moments'.

/**
 * DateStub - Allows for stubbing out the Date function.  If you have
 *      'new Date(...)....' sprinkled throughout your code,
 *      and you want to test it, this is for you.
 *
 * @param {string} arguments Provide 0 to any number of dates in string format.
 *
 * @return a date object corresponding to the arguments passed in.
 *      If you pass only one date in, this will be used by all 'new Date()' calls.
 *      This also provides support for 'Date.UTC()', 'Date.now()', 'Date.parse()'.
 *      Also, this works with the moments library.
 *
 * Examples:
    {   // Test with 1 argument
        Date = DateStub('1/2/2033');        // Override 'Date'

        expect(new Date().toString())
            .toEqual('Sun Jan 02 2033 00:00:00 GMT-0500 (EST)');
        expect(new Date().toString())
            .toEqual('Sun Jan 02 2033 00:00:00 GMT-0500 (EST)');

        Date = DateStub.JavaScriptDate;     // Reset 'Date'
    }
    {   // Call subsequent arguments, each time 'new Date()' is called
        Date = DateStub('1/2/1111', '1/2/1222'
                        , '1/2/3333', '1/2/4444');  // Override 'Date'

        expect(new Date().toString())
            .toEqual('Mon Jan 02 1111 00:00:00 GMT-0500 (EST)');
        expect(new Date().toString())
            .toEqual('Sun Jan 02 1222 00:00:00 GMT-0500 (EST)');
        expect(new Date().toString())
            .toEqual('Fri Jan 02 3333 00:00:00 GMT-0500 (EST)');
        expect(new Date().toString())
            .toEqual('Sat Jan 02 4444 00:00:00 GMT-0500 (EST)');

        Date = DateStub.JavaScriptDate;     // Reset 'Date'
    }
    {   // Test 'Date.now()'.  You can also use: 'Date.UTC()', 'Date.parse()'
        Date = DateStub('1/2/2033');

        expect(new Date(Date.now()).toString())
                .toEqual('Sun Jan 02 2033 00:00:00 GMT-0500 (EST)');

        Date = DateStub.JavaScriptDate;     // Reset 'Date'
    }
 *
 * For more info:  AAkcasu@gmail.com
 */

const DateStub =
    function () {
        function CustomDate(date) {
            if (!!date) { return new DateStub.JavaScriptDate(date); }
            return getNextDate();
        };
        function getNextDate() {
            return dates[index >= length ? index - 1 : index++];
        };

        if (Date.name === 'Date') {
            DateStub.prototype = Date.prototype;
            DateStub.JavaScriptDate = Date;

            // Add Date.* methods.
            CustomDate.UTC = Date.UTC;
            CustomDate.parse = Date.parse;
            CustomDate.now = getNextDate;
        }

        var dateArguments = (arguments.length === 0)
            ? [(new DateStub.JavaScriptDate()).toString()] : arguments
            , length = dateArguments.length
            , index = 0
            , dates = [];
        for (var i = 0; i < length; i++) {
            dates.push(new DateStub.JavaScriptDate(dateArguments[i]));
        }

        return CustomDate;
    };

module.exports = DateStub;

// If you have a test file, and are using node:
// Add this to the top:  const DateStub = require('./DateStub');