Skip to content

Support dates #16

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Serialize JavaScript
====================

Serialize JavaScript to a _superset_ of JSON that includes regular expressions and functions.
Serialize JavaScript to a _superset_ of JSON that includes regular expressions, dates and functions.

[![npm Version][npm-badge]][npm]
[![Dependency Status][david-badge]][david]
Expand All @@ -11,7 +11,7 @@ Serialize JavaScript to a _superset_ of JSON that includes regular expressions a

The code in this package began its life as an internal module to [express-state][]. To expand its usefulness, it now lives as `serialize-javascript` — an independent package on npm.

You're probably wondering: **What about `JSON.stringify()`!?** We've found that sometimes we need to serialize JavaScript **functions** and **regexps**. A great example is a web app that uses client-side URL routing where the route definitions are regexps that need to be shared from the server to the client.
You're probably wondering: **What about `JSON.stringify()`!?** We've found that sometimes we need to serialize JavaScript **functions**, **regexps** or **dates**. A great example is a web app that uses client-side URL routing where the route definitions are regexps that need to be shared from the server to the client.

The string returned from this package's single export function is literal JavaScript which can be saved to a `.js` file, or be embedded into an HTML document by making the content of a `<script>` element. **HTML charaters and JavaScript line terminators are escaped automatically.**

Expand All @@ -36,6 +36,7 @@ serialize({
bool : true,
nil : null,
undef: undefined,
date: new Date("Thu, 28 Apr 2016 22:02:17 GMT"),

fn: function echo(arg) { return arg; },
re: /([^\s]+)/g
Expand All @@ -45,7 +46,7 @@ serialize({
The above will produce the following string output:

```js
'{"str":"string","num":0,"obj":{"foo":"foo"},"arr":[1,2,3],"bool":true,"nil":null,"fn":function echo(arg) { return arg; },"re":/([^\\s]+)/g}'
'{"str":"string","num":0,"obj":{"foo":"foo"},"arr":[1,2,3],"bool":true,"nil":null,date:new Date("2016-04-28T22:02:17.156Z"),"fn":function echo(arg) { return arg; },"re":/([^\\s]+)/g}'
```

Note: to produced a beautified string, you can pass an optional second argument to `serialize()` to define the number of spaces to be used for the indentation.
Expand Down
42 changes: 36 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ var isRegExp = require('util').isRegExp;

// Generate an internal UID to make the regexp pattern harder to guess.
var UID = Math.floor(Math.random() * 0x10000000000).toString(16);
var PLACE_HOLDER_REGEXP = new RegExp('"@__(FUNCTION|REGEXP)-' + UID + '-(\\d+)__@"', 'g');
var PLACE_HOLDER_REGEXP = new RegExp('"@__(FUNCTION|REGEXP|DATE)-' + UID + '-(\\d+)__@"', 'g');

var IS_NATIVE_CODE_REGEXP = /\{\s*\[native code\]\s*\}/g;
var UNSAFE_CHARS_REGEXP = /[<>\/\u2028\u2029]/g;
Expand All @@ -25,13 +25,31 @@ var UNICODE_CHARS = {
'\u2029': '\\u2029'
};

// We can‘t just instanceof Date since dates are already converted to strings
// because of native Date.prototype.JSON (which use toISOString)
var DATE_LENGTH = new Date().toISOString().length
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The toISOString() method returns a string in simplified extended ISO format (ISO 8601), which is always 24 characters long: YYYY-MM-DDTHH:mm:ss.sssZ.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString

Can you just set this to 24?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, but this is more clear the way I coded it (imo) and it's computed only once per runtime, so no big deal (and clearer) imo.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree. I think setting it to 24 with a comment that it'll always be 24 is a better approach. But this is likely moot given my two comments above: #16 (comment)

function isDate(d) {
try {
return (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the try/catch might bring some slowness here, not sure.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't use the try/catch we must validate the date. It seems that d.getTime() will not throw, so we could test using isNaN().

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use isFinite() to see if it's a valid date without this try/catch.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will try that.

// testing length first to avoid new Date() for every string
d.length === DATE_LENGTH &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this detection process seems to be very flaky, specially because Date() constructor is really flexible. e.g.:

new Date("2016-04-29              ")
> Fri Apr 29 2016 00:00:00 GMT-0400 (EDT)

Maybe you should use a regexp to detect, then call Date() on the value to validate, and if it throw, you should probably prevent swallowing the error, otherwise this will be crazy to debug.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The date is automatically converted to ISOString by Date.prototype.toJSON before this code is executed. So this is not a problem. That's why I tested the length, because all date will be already converted to the same format at this point.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand, I'm just saying that you should probably apply a regexp to prevent other arbitrary strings with the same length to be treated as dates just because they have the exact same length. Otherwise an error will throw.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify, the part that I don't like here is the comparison with DATE_LENGTH as the solely way to detect serialized dates.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for performance reason, we should avoid regex. That will be very expensive to use regex on all strings without a fast and easy way to detect non date string. Obviously, we need to ensure a date is valid. That's why I mentionned using new Date(string).getTime() which is not throwing error (from my test) but that return NaN for invalid date.

Copy link
Collaborator

@ericf ericf May 5, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another approach:

let currentObj = obj;
JSON.stringify(obj, function (key, value) {
  // Do other value type checks...

  if (typeof value === 'string' && currentObj[key] instanceof Date) {
    return '@__DATE-' + UID + '-' + (dates.push(value) - 1) + '__@';
  }

  if (value && typeof value === 'object') {
    currentObj = value;
  }

  return value;
});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this one better, I forgot that we can check back into the original value if we want to.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot that we can check back into the original value if we want to.

Are you sure about that? How does it works for key in depth?! (eg: { some: { thing: { d:, key will be d, so currentObj[key] will be inaccurate).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a depth-first traversal so I don't see why it wouldn't work:

var obj = {
    foo: {
        bar: 'bar'
    },
    baz: {
        date: new Date(0)
    }
};

var currentObj = obj;
var output = JSON.stringify(obj, function (key, value) {
    console.log(key);

    if (typeof value === 'string' && currentObj[key] instanceof Date) {
        return 'DATE!';
    }

    if (value && typeof value === 'object') {
        currentObj = value;
    }

    return value;
});

console.log(output);
foo
bar
baz
date
{"foo":{"bar":"bar"},"baz":{"date":"DATE!"}}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I missed the assignation after the test. So yeah that might work :)

d === ((new Date(d)).toISOString())
)
}
// Invalid Date might throw "RangeError: invalid date" when calling toISOString
catch(e) {}

return false
}

module.exports = function serialize(obj, space) {
var functions = [];
var regexps = [];
var dates = [];
var str;

// Creates a JSON string representation of the object and uses placeholders
// for functions and regexps (identified by index) which are later
// for functions, regexps and dates (identified by index) which are later
// replaced.
str = JSON.stringify(obj, function (key, value) {
if (typeof value === 'function') {
Expand All @@ -42,6 +60,10 @@ module.exports = function serialize(obj, space) {
return '@__REGEXP-' + UID + '-' + (regexps.push(value) - 1) + '__@';
}

if (typeof value === 'string' && isDate(value)) {
return '@__DATE-' + UID + '-' + (dates.push(value) - 1) + '__@';
}

return value;
}, space);

Expand All @@ -58,14 +80,22 @@ module.exports = function serialize(obj, space) {
return UNICODE_CHARS[unsafeChar];
});

if (functions.length === 0 && regexps.length === 0) {
if (
functions.length === 0 &&
regexps.length === 0 &&
dates.length === 0
) {
return str;
}

// Replaces all occurrences of function and regexp placeholders in the JSON
// string with their string representations. If the original value can not
// be found, then `undefined` is used.
// Replaces all occurrences of function, regexp and date placeholders in the
// JSON string with their string representations.
// If the original value can not be found, then `undefined` is used.
return str.replace(PLACE_HOLDER_REGEXP, function (match, type, valueIndex) {
if (type === 'DATE') {
return "new Date(\"" + dates[valueIndex] + "\")";
}

if (type === 'REGEXP') {
return regexps[valueIndex].toString();
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "serialize-javascript",
"version": "1.2.0",
"description": "Serialize JavaScript to a superset of JSON that includes regular expressions and functions.",
"description": "Serialize JavaScript to a superset of JSON that includes regular expressions, dates and functions.",
"main": "index.js",
"scripts": {
"benchmark": "node test/benchmark/serialize.js",
Expand Down
19 changes: 19 additions & 0 deletions test/unit/serialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,23 @@ describe('serialize( obj )', function () {
expect(eval(serialize('</script>'))).to.equal('</script>');
});
});

describe('dates', function () {
it('should serialize dates', function () {
var d = new Date('2016-04-28T22:02:17.156Z');
expect(serialize(d)).to.be.a('string').equal('new Date("2016-04-28T22:02:17.156Z")');
});

it('should deserialize a date', function () {
var d = eval(serialize(new Date('2016-04-28T22:02:17.156Z')));
expect(d).to.be.a('Date');
expect(d.toISOString()).to.equal('2016-04-28T22:02:17.156Z');
});

it('should deserialize a string that is not a valid date', function () {
var d = eval(serialize('2016-04-28T25:02:17.156Z'));
expect(d).to.be.a('string');
expect(d).to.equal('2016-04-28T25:02:17.156Z');
});
})
});