refactor(fromJson/date filter): move date string logic to date filter

Breaks angular.fromJson which doesn't deserialize date strings into date objects.

This was done to make fromJson compatible with JSON.parse.

If you do require the old behavior - if at all neeeded then because of
json deserialization of XHR responses - then please create a custom
$http transform:

$httpProvider.defaults.transformResponse.push(function(data) {
  // recursively parse dates from data object here
  // see code removed in this diff for hints
});

Closes #202
This commit is contained in:
Igor Minar 2012-03-26 23:38:20 -07:00
parent bb2fa6f63f
commit ac4318a2fa
5 changed files with 211 additions and 226 deletions

View file

@ -1,7 +1,5 @@
'use strict';
var array = [].constructor;
/**
* @ngdoc function
* @name angular.toJson
@ -35,46 +33,11 @@ function toJson(obj, pretty) {
function fromJson(json, useNative) {
if (!isString(json)) return json;
var obj;
if (useNative && window.JSON && window.JSON.parse) {
obj = JSON.parse(json);
} else {
obj = parseJson(json, true)();
}
return transformDates(obj);
// TODO make forEach optionally recursive and remove this function
// TODO(misko): remove this once the $http service is checked in.
function transformDates(obj) {
if (isString(obj) && 15 <= obj.length && obj.length <= 24) {
return jsonStringToDate(obj);
} else if (isArray(obj) || isObject(obj)) {
forEach(obj, function(val, name) {
obj[name] = transformDates(val);
});
}
return obj;
}
return (useNative && window.JSON && window.JSON.parse)
? JSON.parse(json)
: parseJson(json, true)();
}
var R_ISO8061_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/;
function jsonStringToDate(string){
var match;
if (match = string.match(R_ISO8061_STR)) {
var date = new Date(0),
tzHour = 0,
tzMin = 0;
if (match[9]) {
tzHour = int(match[9] + match[10]);
tzMin = int(match[9] + match[11]);
}
date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3]));
date.setUTCHours(int(match[4]||0) - tzHour, int(match[5]||0) - tzMin, int(match[6]||0), int(match[7]||0));
return date;
}
return string;
}
function jsonDateToString(date){
if (!date) return date;

View file

@ -288,7 +288,8 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+
* (e.g. `"h o''clock"`).
*
* @param {(Date|number|string)} date Date to format either as Date object, milliseconds (string or
* number) or ISO 8601 extended datetime string (yyyy-MM-ddTHH:mm:ss.SSSZ).
* number) or various ISO 8601 datetime string formats (e.g. yyyy-MM-ddTHH:mm:ss.SSSZ and it's
* shorter versions like yyyy-MM-ddTHH:mmZ, yyyy-MM-dd or yyyyMMddTHHmmssZ).
* @param {string=} format Formatting rules (see Description). If not specified,
* `mediumDate` is used.
* @returns {string} Formatted string or the input if input is not recognized as date/millis.
@ -317,6 +318,27 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+
*/
dateFilter.$inject = ['$locale'];
function dateFilter($locale) {
var R_ISO8601_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/;
function jsonStringToDate(string){
var match;
if (match = string.match(R_ISO8601_STR)) {
var date = new Date(0),
tzHour = 0,
tzMin = 0;
if (match[9]) {
tzHour = int(match[9] + match[10]);
tzMin = int(match[9] + match[11]);
}
date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3]));
date.setUTCHours(int(match[4]||0) - tzHour, int(match[5]||0) - tzMin, int(match[6]||0), int(match[7]||0));
return date;
}
return string;
}
return function(date, format) {
var text = '',
parts = [],

View file

@ -373,146 +373,176 @@ angular.mock.$LogProvider = function() {
};
/**
* @ngdoc object
* @name angular.mock.TzDate
* @description
*
* *NOTE*: this is not an injectable instance, just a globally available mock class of `Date`.
*
* Mock of the Date type which has its timezone specified via constroctor arg.
*
* The main purpose is to create Date-like instances with timezone fixed to the specified timezone
* offset, so that we can test code that depends on local timezone settings without dependency on
* the time zone settings of the machine where the code is running.
*
* @param {number} offset Offset of the *desired* timezone in hours (fractions will be honored)
* @param {(number|string)} timestamp Timestamp representing the desired time in *UTC*
*
* @example
* !!!! WARNING !!!!!
* This is not a complete Date object so only methods that were implemented can be called safely.
* To make matters worse, TzDate instances inherit stuff from Date via a prototype.
*
* We do our best to intercept calls to "unimplemented" methods, but since the list of methods is
* incomplete we might be missing some non-standard methods. This can result in errors like:
* "Date.prototype.foo called on incompatible Object".
*
* <pre>
* var newYearInBratislava = new TzDate(-1, '2009-12-31T23:00:00Z');
* newYearInBratislava.getTimezoneOffset() => -60;
* newYearInBratislava.getFullYear() => 2010;
* newYearInBratislava.getMonth() => 0;
* newYearInBratislava.getDate() => 1;
* newYearInBratislava.getHours() => 0;
* newYearInBratislava.getMinutes() => 0;
* </pre>
*
*/
angular.mock.TzDate = function (offset, timestamp) {
var self = new Date(0);
if (angular.isString(timestamp)) {
var tsStr = timestamp;
(function() {
var R_ISO8061_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/;
self.origDate = angular.fromJson(angular.toJson({date:timestamp})).date;
timestamp = self.origDate.getTime();
if (isNaN(timestamp))
throw {
name: "Illegal Argument",
message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string"
};
} else {
self.origDate = new Date(timestamp);
function jsonStringToDate(string){
var match;
if (match = string.match(R_ISO8061_STR)) {
var date = new Date(0),
tzHour = 0,
tzMin = 0;
if (match[9]) {
tzHour = int(match[9] + match[10]);
tzMin = int(match[9] + match[11]);
}
date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3]));
date.setUTCHours(int(match[4]||0) - tzHour, int(match[5]||0) - tzMin, int(match[6]||0), int(match[7]||0));
return date;
}
return string;
}
var localOffset = new Date(timestamp).getTimezoneOffset();
self.offsetDiff = localOffset*60*1000 - offset*1000*60*60;
self.date = new Date(timestamp + self.offsetDiff);
function int(str) {
return parseInt(str, 10);
}
self.getTime = function() {
return self.date.getTime() - self.offsetDiff;
};
self.toLocaleDateString = function() {
return self.date.toLocaleDateString();
};
/**
* @ngdoc object
* @name angular.mock.TzDate
* @description
*
* *NOTE*: this is not an injectable instance, just a globally available mock class of `Date`.
*
* Mock of the Date type which has its timezone specified via constroctor arg.
*
* The main purpose is to create Date-like instances with timezone fixed to the specified timezone
* offset, so that we can test code that depends on local timezone settings without dependency on
* the time zone settings of the machine where the code is running.
*
* @param {number} offset Offset of the *desired* timezone in hours (fractions will be honored)
* @param {(number|string)} timestamp Timestamp representing the desired time in *UTC*
*
* @example
* !!!! WARNING !!!!!
* This is not a complete Date object so only methods that were implemented can be called safely.
* To make matters worse, TzDate instances inherit stuff from Date via a prototype.
*
* We do our best to intercept calls to "unimplemented" methods, but since the list of methods is
* incomplete we might be missing some non-standard methods. This can result in errors like:
* "Date.prototype.foo called on incompatible Object".
*
* <pre>
* var newYearInBratislava = new TzDate(-1, '2009-12-31T23:00:00Z');
* newYearInBratislava.getTimezoneOffset() => -60;
* newYearInBratislava.getFullYear() => 2010;
* newYearInBratislava.getMonth() => 0;
* newYearInBratislava.getDate() => 1;
* newYearInBratislava.getHours() => 0;
* newYearInBratislava.getMinutes() => 0;
* </pre>
*
*/
angular.mock.TzDate = function (offset, timestamp) {
var self = new Date(0);
if (angular.isString(timestamp)) {
var tsStr = timestamp;
self.getFullYear = function() {
return self.date.getFullYear();
};
self.origDate = jsonStringToDate(timestamp)
self.getMonth = function() {
return self.date.getMonth();
};
timestamp = self.origDate.getTime();
if (isNaN(timestamp))
throw {
name: "Illegal Argument",
message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string"
};
} else {
self.origDate = new Date(timestamp);
}
self.getDate = function() {
return self.date.getDate();
};
var localOffset = new Date(timestamp).getTimezoneOffset();
self.offsetDiff = localOffset*60*1000 - offset*1000*60*60;
self.date = new Date(timestamp + self.offsetDiff);
self.getHours = function() {
return self.date.getHours();
};
self.getMinutes = function() {
return self.date.getMinutes();
};
self.getSeconds = function() {
return self.date.getSeconds();
};
self.getTimezoneOffset = function() {
return offset * 60;
};
self.getUTCFullYear = function() {
return self.origDate.getUTCFullYear();
};
self.getUTCMonth = function() {
return self.origDate.getUTCMonth();
};
self.getUTCDate = function() {
return self.origDate.getUTCDate();
};
self.getUTCHours = function() {
return self.origDate.getUTCHours();
};
self.getUTCMinutes = function() {
return self.origDate.getUTCMinutes();
};
self.getUTCSeconds = function() {
return self.origDate.getUTCSeconds();
};
self.getDay = function() {
return self.date.getDay();
};
//hide all methods not implemented in this mock that the Date prototype exposes
var unimplementedMethods = ['getMilliseconds', 'getUTCDay',
'getUTCMilliseconds', 'getYear', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds',
'setMinutes', 'setMonth', 'setSeconds', 'setTime', 'setUTCDate', 'setUTCFullYear',
'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds',
'setYear', 'toDateString', 'toJSON', 'toGMTString', 'toLocaleFormat', 'toLocaleString',
'toLocaleTimeString', 'toSource', 'toString', 'toTimeString', 'toUTCString', 'valueOf'];
angular.forEach(unimplementedMethods, function(methodName) {
self[methodName] = function() {
throw Error("Method '" + methodName + "' is not implemented in the TzDate mock");
self.getTime = function() {
return self.date.getTime() - self.offsetDiff;
};
});
return self;
};
self.toLocaleDateString = function() {
return self.date.toLocaleDateString();
};
//make "tzDateInstance instanceof Date" return true
angular.mock.TzDate.prototype = Date.prototype;
self.getFullYear = function() {
return self.date.getFullYear();
};
self.getMonth = function() {
return self.date.getMonth();
};
self.getDate = function() {
return self.date.getDate();
};
self.getHours = function() {
return self.date.getHours();
};
self.getMinutes = function() {
return self.date.getMinutes();
};
self.getSeconds = function() {
return self.date.getSeconds();
};
self.getTimezoneOffset = function() {
return offset * 60;
};
self.getUTCFullYear = function() {
return self.origDate.getUTCFullYear();
};
self.getUTCMonth = function() {
return self.origDate.getUTCMonth();
};
self.getUTCDate = function() {
return self.origDate.getUTCDate();
};
self.getUTCHours = function() {
return self.origDate.getUTCHours();
};
self.getUTCMinutes = function() {
return self.origDate.getUTCMinutes();
};
self.getUTCSeconds = function() {
return self.origDate.getUTCSeconds();
};
self.getUTCMilliseconds = function() {
return self.origDate.getUTCMilliseconds();
};
self.getDay = function() {
return self.date.getDay();
};
//hide all methods not implemented in this mock that the Date prototype exposes
var unimplementedMethods = ['getMilliseconds', 'getUTCDay',
'getYear', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds',
'setMinutes', 'setMonth', 'setSeconds', 'setTime', 'setUTCDate', 'setUTCFullYear',
'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds',
'setYear', 'toDateString', 'toJSON', 'toGMTString', 'toLocaleFormat', 'toLocaleString',
'toLocaleTimeString', 'toSource', 'toString', 'toTimeString', 'toUTCString', 'valueOf'];
angular.forEach(unimplementedMethods, function(methodName) {
self[methodName] = function() {
throw Error("Method '" + methodName + "' is not implemented in the TzDate mock");
};
});
return self;
};
//make "tzDateInstance instanceof Date" return true
angular.mock.TzDate.prototype = Date.prototype;
})();
/**

View file

@ -72,9 +72,8 @@ describe('json', function() {
});
it('should serialize UTC dates', function() {
var date = jsonStringToDate('2009-10-09T01:02:03.027Z');
var date = new angular.mock.TzDate(-1, '2009-10-09T01:02:03.027Z');
expect(toJson(date)).toEqual('"2009-10-09T01:02:03.027Z"');
expect(fromJson('"2009-10-09T01:02:03.027Z"').getTime()).toEqual(date.getTime());
});
it('should prevent recursion', function() {
@ -156,17 +155,7 @@ describe('json', function() {
expect(fromJson('{}', true)).toEqual({});
expect(spy).toHaveBeenCalled();
});
it('should convert timestamp strings to Date objects', function() {
expect(fromJson('"2010-12-22T17:23:17.974Z"', true) instanceof Date).toBe(true);
expect(fromJson('["2010-12-22T17:23:17.974Z"]', true)[0] instanceof Date).toBe(true);
expect(fromJson('{"t":"2010-12-22T17:23:17.974Z"}', true).t instanceof Date).toBe(true);
expect(fromJson('{"t":["2010-12-22T17:23:17.974Z"]}', true).t[0] instanceof Date).toBe(true);
expect(fromJson('{"t":{"t":"2010-12-22T17:23:17.974Z"}}', true).t.t instanceof Date).toBe(true);
});
});
}
@ -220,45 +209,6 @@ describe('json', function() {
});
describe('iso 8061 date', function() {
it('should read/write to date', function() {
var date = new Date('Sep 10 2003 13:02:03 GMT');
expect(jsonDateToString(date)).toBe('2003-09-10T13:02:03.000Z');
expect(jsonStringToDate(jsonDateToString(date)).getTime()).toBe(date.getTime());
});
it('should convert to date', function() {
//full ISO8061
expect(jsonStringToDate('2003-09-10T13:02:03.000Z')).toEqual(new Date('Sep 10 2003 13:02:03 GMT'));
expect(jsonStringToDate('2003-09-10T13:02:03.000+00:00')).toEqual(new Date('Sep 10 2003 13:02:03 GMT'));
expect(jsonStringToDate('20030910T033203-0930')).toEqual(new Date('Sep 10 2003 13:02:03 GMT'));
//no millis
expect(jsonStringToDate('2003-09-10T13:02:03Z')).toEqual(new Date('Sep 10 2003 13:02:03 GMT'));
//no seconds
expect(jsonStringToDate('2003-09-10T13:02Z')).toEqual(new Date('Sep 10 2003 13:02:00 GMT'));
//no minutes
expect(jsonStringToDate('2003-09-10T13Z')).toEqual(new Date('Sep 10 2003 13:00:00 GMT'));
//no time
expect(jsonStringToDate('2003-09-10')).toEqual(new Date('Sep 10 2003 00:00:00 GMT'));
expect(jsonStringToDate('2011-12-28T13:02:09-08:00')).toEqual(new Date('Dec 28 2011 21:02:09 GMT'));
});
it('should parse date', function() {
var date = jsonStringToDate('2003-09-10T13:02:03.000Z');
expect(jsonDateToString(date)).toBe('2003-09-10T13:02:03.000Z');
expect(jsonStringToDate('str')).toBe('str');
});
});
describe('string', function() {
it('should quote', function() {
expect(quoteUnicode('a')).toBe('"a"');

View file

@ -267,14 +267,34 @@ describe('filters', function() {
toEqual('12:05 PM');
});
it('should be able to parse ISO 8601 dates/times using', function() {
var isoString = '2010-09-03T05:05:08.872Z';
expect(date(isoString)).
toEqual(date(isoString, 'mediumDate'));
});
it('should parse format ending with non-replaced string', function() {
expect(date(morning, 'yy/xxx')).toEqual('10/xxx');
});
it('should support various iso8061 date strings as input', function() {
var format = 'yyyy-MM ss';
//full ISO8061
expect(date('2003-09-10T13:02:03.000Z', format)).toEqual('2003-09 03');
expect(date('2003-09-10T13:02:03.000+00:00', format)).toEqual('2003-09 03');
expect(date('2003-09-10T13:02:03-08:00', format)).toEqual('2003-09 03');
expect(date('20030910T033203-0930', format)).toEqual('2003-09 03');
//no millis
expect(date('2003-09-10T13:02:03Z', format)).toEqual('2003-09 03');
//no seconds
expect(date('2003-09-10T13:02Z', format)).toEqual('2003-09 00');
//no minutes
expect(date('2003-09-10T13Z', format)).toEqual('2003-09 00');
//no time
expect(date('2003-09-10', format)).toEqual('2003-09 00');
});
});
});