Skip to content

Commit

Permalink
Manually compute the hours, minutes, seconds, and milliseconds when s…
Browse files Browse the repository at this point in the history
…erializing dates in Safari < 2.0.2 and Opera >= 10.53. Use strict equality when checking for cyclic structures. Lazy-define `isPropertyOf` only if `Object#hasOwnProperty` is not present.
  • Loading branch information
Kit Cambridge committed Apr 24, 2012
1 parent 9cd3335 commit a4e637a
Showing 1 changed file with 92 additions and 70 deletions.
162 changes: 92 additions & 70 deletions lib/json3.js
Expand Up @@ -15,25 +15,36 @@
// implementations.
serialized = '{"result":[1,true,false,null,"\\u0000\\b\\n\\f\\r\\t"]}',

// The `Date#getUTC{FullYear, Month, Date}` methods return nonsensical
// results for certain dates in Opera >= 10.54. Based on work by @Yaffle.
value = new Date(-3509827334573292), floor, Months,
getDay = (typeof value.getUTCFullYear != "function" || value.getUTCFullYear() != -109252 ||
typeof value.getUTCMonth != "function" || value.getUTCMonth() !== 0 ||
typeof value.getUTCDate != "function" || value.getUTCDate() != 1) &&
// A mapping between the months of the year and the number of days
// between January 1st and the first of the respective month.
(floor = Math.floor, Months = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334],
// Calculates the number of days between the Unix epoch and the first
// day of the given month.
function (year, month) {
return Months[month] + 365 * (year - 1970) + floor((year - 1969 + (month = +(month > 1))) / 4) - floor((year - 1901 + month) / 100) + floor((year - 1601 + month) / 400);
}),

// Feature tests to determine whether the native `JSON.stringify` and `parse`
// implementations are spec-compliant. Based on work by Ken Snyder.
stringifySupported = typeof exports.stringify == "function" && !getDay, Escapes, toPaddedString, quote, serialize,
parseSupported = typeof exports.parse == "function", fromCharCode, Unescapes, Parser, walk;
parseSupported = typeof exports.parse == "function", fromCharCode, Unescapes, Parser, walk,

// Test the `Date#getUTC*` methods. Based on work by @Yaffle.
value = new Date(-3509827334573292), floor, Months, getDay;

try {
// The `getUTCFullYear`, `Month`, and `Date` methods return nonsensical
// results for certain dates in Opera >= 10.53.
value = value.getUTCFullYear() == -109252 && value.getUTCMonth() === 0 && value.getUTCDate() == 1 &&
// Safari < 2.0.2 stores the internal millisecond time value correctly,
// but clips the values returned by the date methods to the range of
// signed 32-bit integers ([-2 ** 31, 2 ** 31 - 1]).
value.getUTCHours() == 10 && value.getUTCMinutes() == 37 && value.getUTCSeconds() == 6 && value.getUTCMilliseconds() == 708;
} catch (exception) {}

// Define additional utility methods if the `Date` methods are buggy.
if (!value) {
floor = Math.floor;
// A mapping between the months of the year and the number of days between
// January 1st and the first of the respective month.
Months = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
// Internal: Calculates the number of days between the Unix epoch and the
// first day of the given month.
getDay = function (year, month) {
return Months[month] + 365 * (year - 1970) + floor((year - 1969 + (month = +(month > 1))) / 4) - floor((year - 1901 + month) / 100) + floor((year - 1601 + month) / 400);
};
}

// Test `JSON.stringify`.
if (stringifySupported) {
Expand Down Expand Up @@ -84,17 +95,15 @@
case exports.stringify(null, value) === "1":
case exports.stringify([1, 2], null, 1) == "[\n 1,\n 2\n]":
// JSON 2, Prototype <= 1.7, and older WebKit builds incorrectly
// serialize extended years. Safari < 2.0.2 restricts date time values
// to the range `[(-2 ** 31), (2 ** 31) - 1]`, so the serialization
// tests will fail in case JSON 3 is loaded twice.
case (value = new Date(-8.64e15)).getUTCFullYear() != -271821 || exports.stringify(value) == '"-271821-04-20T00:00:00.000Z"':
// serialize extended years.
case exports.stringify(new Date(-8.64e15)) == '"-271821-04-20T00:00:00.000Z"':
// The milliseconds are optional in ES 5, but required in 5.1.
case (value = new Date(8.64e15)).getUTCFullYear() != 275760 || exports.stringify(value) == '"+275760-09-13T00:00:00.000Z"':
case exports.stringify(new Date(8.64e15)) == '"+275760-09-13T00:00:00.000Z"':
// Firefox <= 11.0 incorrectly serializes years prior to 0 as negative
// four-digit years instead of six-digit years. Credits: @Yaffle.
case exports.stringify(new Date(-621987552e5)) == '"-000001-01-01T00:00:00.000Z"':
// Safari <= 5.1.5 and Opera >= 10.54 incorrectly serialize
// millisecond values less than 1000. Credits: @Yaffle.
// Safari <= 5.1.5 and Opera >= 10.53 incorrectly serialize millisecond
// values less than 1000. Credits: @Yaffle.
case exports.stringify(new Date(-1)) == '"1969-12-31T23:59:59.999Z"':
stringifySupported = false;
}
Expand Down Expand Up @@ -136,59 +145,57 @@

if (!stringifySupported || !parseSupported) {
// Internal: Determines if a property is a direct property of the given
// object.
isPropertyOf = function (property) {
var members = {}, method = members.hasOwnProperty, constructor;
if (method && getClass.call(method) == "[object Function]") {
// Delegate to the native `Object#hasOwnProperty` method if available.
isPropertyOf = method;
} else if ((members.__proto__ = null, members.__proto__ = {
// The internal *proto* property cannot be set multiple times in recent
// versions of Mozilla Firefox and SeaMonkey.
"toString": 1
}, members).toString != getClass) {
// Safari <= 2.0.3 doesn't implement `Object#hasOwnProperty`, but
// supports the mutable *proto* property.
isPropertyOf = function (property) {
// Capture and break the object's prototype chain (see section 8.6.2
// of the ES 5.1 spec). The parenthesized expression prevents an
// unsafe transformation by the Closure Compiler.
var original = this.__proto__, result = property in (this.__proto__ = null, this);
// Restore the original prototype chain.
this.__proto__ = original;
return result;
};
} else {
// Capture a reference to the top-level `Object` constructor.
constructor = members.constructor;
// Use the `constructor` property to simulate `Object#hasOwnProperty` in
// other environments.
isPropertyOf = function (property) {
var parent = (this.constructor || constructor).prototype;
return property in this && !(property in parent && this[property] === parent[property]);
};
}
members = method = null;
return isPropertyOf.call(this, property);
};
// object. Delegates to the native `Object#hasOwnProperty` method.
if (!(isPropertyOf = {}.hasOwnProperty)) {
isPropertyOf = function (property) {
var members = {}, constructor;
if ((members.__proto__ = null, members.__proto__ = {
// The *proto* property cannot be set multiple times in recent
// versions of Firefox and SeaMonkey.
"toString": 1
}, members).toString != getClass) {
// Safari <= 2.0.3 doesn't implement `Object#hasOwnProperty`, but
// supports the mutable *proto* property.
isPropertyOf = function (property) {
// Capture and break the object's prototype chain (see section 8.6.2
// of the ES 5.1 spec). The parenthesized expression prevents an
// unsafe transformation by the Closure Compiler.
var original = this.__proto__, result = property in (this.__proto__ = null, this);
// Restore the original prototype chain.
this.__proto__ = original;
return result;
};
} else {
// Capture a reference to the top-level `Object` constructor.
constructor = members.constructor;
// Use the `constructor` property to simulate `Object#hasOwnProperty` in
// other environments.
isPropertyOf = function (property) {
var parent = (this.constructor || constructor).prototype;
return property in this && !(property in parent && this[property] === parent[property]);
};
}
members = null;
return isPropertyOf.call(this, property);
};
}

// Internal: Normalizes the `for...in` iteration algorithm across
// environments. Each enumerated key is yielded to a `callback` function.
forEach = function (object, callback) {
var size = 0, members, property, forEach;
var size = 0, Properties, members, property, forEach;

// Tests for bugs in the current environment's `for...in` algorithm. The
// `valueOf` property inherits the non-enumerable flag from
// `Object.prototype` in JScript <= 5.8 and Gecko <= 1.0.
function Properties() {
(Properties = function () {
this.valueOf = 0;
}
Properties.prototype.valueOf = 0;
}).prototype.valueOf = 0;

// Iterate over a new instance of the `Properties` class.
members = new Properties();
for (property in members) {
// Ignore all other properties inherited from `Object.prototype`.
// Ignore all properties inherited from `Object.prototype`.
if (isPropertyOf.call(members, property)) {
size++;
}
Expand Down Expand Up @@ -292,34 +299,49 @@
// Internal: Recursively serializes an object. Implements the
// `Str(key, holder)`, `JO(value)`, and `JA(value)` operations.
serialize = function (property, object, callback, properties, whitespace, indentation, stack) {
var value = object[property], className, date, year, month, results, element, index, length, prefix, any;
var value = object[property], className, year, month, date, time, hours, minutes, seconds, milliseconds, results, element, index, length, prefix, any;
if (typeof value == "object" && value) {
if (getClass.call(value) == "[object Date]" && !isPropertyOf.call(value, "toJSON")) {
if (value > -1 / 0 && value < 1 / 0) {
// Dates are serialized according to the `Date#toJSON` method
// specified in ES 5.1 section 15.9.5.44. See section 15.9.1.15
// for the ISO 8601 date time string format.
if (getDay) {
// Manually compute the year, month, and date if the `getUTC*`
// methods are buggy. Adapted from @Yaffle's `date-shim`
// project.
// Manually compute the year, month, date, hours, minutes,
// seconds, and milliseconds if the `getUTC*` methods are
// buggy. Adapted from @Yaffle's `date-shim` project.
date = floor(value / 864e5);
for (year = floor(date / 365.2425) + 1970 - 1; getDay(year + 1, 0) <= date; year++);
for (month = floor((date - getDay(year, 0)) / 30.42); getDay(year, month + 1) <= date; month++);
date = 1 + date - getDay(year, month);
// The `time` value specifies the time within the day (see ES
// 5.1 section 15.9.1.2). The formula `(A % B + B) % B` is used
// to compute `A modulo B`, as the `%` operator does not
// correspond to the `modulo` operation for negative numbers.
time = (value % 864e5 + 864e5) % 864e5;
// The hours, minutes, seconds, and milliseconds are obtained by
// decomposing the time within the day. See section 15.9.1.10.
hours = floor(time / 36e5) % 24;
minutes = floor(time / 6e4) % 60;
seconds = floor(time / 1e3) % 60;
milliseconds = time % 1e3;
} else {
year = value.getUTCFullYear();
month = value.getUTCMonth();
date = value.getUTCDate();
hours = value.getUTCHours();
minutes = value.getUTCMinutes();
seconds = value.getUTCSeconds();
milliseconds = value.getUTCMilliseconds();
}
// Serialize extended years correctly.
value = (year <= 0 || year >= 1e4 ? (year < 0 ? "-" : "+") + toPaddedString(6, year < 0 ? -year : year) : toPaddedString(4, year)) +
"-" + toPaddedString(2, month + 1) + "-" + toPaddedString(2, date) +
// Months, dates, hours, minutes, and seconds should have two
// digits; milliseconds should have three.
"T" + toPaddedString(2, value.getUTCHours()) + ":" + toPaddedString(2, value.getUTCMinutes()) + ":" + toPaddedString(2, value.getUTCSeconds()) +
"T" + toPaddedString(2, hours) + ":" + toPaddedString(2, minutes) + ":" + toPaddedString(2, seconds) +
// Milliseconds are optional in ES 5.0, but required in 5.1.
"." + toPaddedString(3, value.getUTCMilliseconds()) + "Z";
"." + toPaddedString(3, milliseconds) + "Z";
} else {
value = null;
}
Expand Down Expand Up @@ -353,7 +375,7 @@
// Check for cyclic structures. This is a linear search; performance
// is inversely proportional to the number of unique nested objects.
for (length = stack.length; length--;) {
if (stack[length] == value) {
if (stack[length] === value) {
// Cyclic structures cannot be serialized by `JSON.stringify`.
throw TypeError();
}
Expand Down

0 comments on commit a4e637a

Please sign in to comment.