diff --git a/src/dateparser/dateparser.js b/src/dateparser/dateparser.js new file mode 100644 index 0000000000..85fc203352 --- /dev/null +++ b/src/dateparser/dateparser.js @@ -0,0 +1,126 @@ +angular.module('ui.bootstrap.dateparser', []) + +.service('dateParser', ['$locale', 'orderByFilter', function($locale, orderByFilter) { + + this.parsers = {}; + + var formatCodeToRegex = { + 'yyyy': { + regex: '\\d{4}', + apply: function(value) { this.year = +value; } + }, + 'yy': { + regex: '\\d{2}', + apply: function(value) { this.year = +value + 2000; } + }, + 'y': { + regex: '\\d{1,4}', + apply: function(value) { this.year = +value; } + }, + 'MMMM': { + regex: $locale.DATETIME_FORMATS.MONTH.join('|'), + apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); } + }, + 'MMM': { + regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'), + apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); } + }, + 'MM': { + regex: '0[1-9]|1[0-2]', + apply: function(value) { this.month = value - 1; } + }, + 'M': { + regex: '[1-9]|1[0-2]', + apply: function(value) { this.month = value - 1; } + }, + 'dd': { + regex: '[0-2][0-9]{1}|3[0-1]{1}', + apply: function(value) { this.date = +value; } + }, + 'd': { + regex: '[1-2]?[0-9]{1}|3[0-1]{1}', + apply: function(value) { this.date = +value; } + }, + 'EEEE': { + regex: $locale.DATETIME_FORMATS.DAY.join('|') + }, + 'EEE': { + regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|') + } + }; + + this.createParser = function(format) { + var map = [], regex = format.split(''); + + angular.forEach(formatCodeToRegex, function(data, code) { + var index = format.indexOf(code); + + if (index > -1) { + format = format.split(''); + + regex[index] = '(' + data.regex + ')'; + format[index] = '$'; // Custom symbol to define consumed part of format + for (var i = index + 1, n = index + code.length; i < n; i++) { + regex[i] = ''; + format[i] = '$'; + } + format = format.join(''); + + map.push({ index: index, apply: data.apply }); + } + }); + + return { + regex: new RegExp('^' + regex.join('') + '$'), + map: orderByFilter(map, 'index') + }; + }; + + this.parse = function(input, format) { + if ( !angular.isString(input) ) { + return input; + } + + format = $locale.DATETIME_FORMATS[format] || format; + + if ( !this.parsers[format] ) { + this.parsers[format] = this.createParser(format); + } + + var parser = this.parsers[format], + regex = parser.regex, + map = parser.map, + results = input.match(regex); + + if ( results && results.length ) { + var fields = { year: 1900, month: 0, date: 1, hours: 0 }, dt; + + for( var i = 1, n = results.length; i < n; i++ ) { + var mapper = map[i-1]; + if ( mapper.apply ) { + mapper.apply.call(fields, results[i]); + } + } + + if ( isValid(fields.year, fields.month, fields.date) ) { + dt = new Date( fields.year, fields.month, fields.date, fields.hours); + } + + return dt; + } + }; + + // Check if date is valid for specific month (and year for February). + // Month: 0 = Jan, 1 = Feb, etc + function isValid(year, month, date) { + if ( month === 1 && date > 28) { + return date === 29 && ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0); + } + + if ( month === 3 || month === 5 || month === 8 || month === 10) { + return date < 31; + } + + return true; + } +}]); diff --git a/src/dateparser/test/dateparser.spec.js b/src/dateparser/test/dateparser.spec.js new file mode 100644 index 0000000000..4090b41ecd --- /dev/null +++ b/src/dateparser/test/dateparser.spec.js @@ -0,0 +1,96 @@ +describe('date parser', function () { + var dateParser; + + beforeEach(module('ui.bootstrap.dateparser')); + beforeEach(inject(function (_dateParser_) { + dateParser = _dateParser_; + })); + + function expectParse(input, format, date) { + expect(dateParser.parse(input, format)).toEqual(date); + } + + describe('wih custom formats', function() { + it('should work correctly for `dd`, `MM`, `yyyy`', function() { + expectParse('17.11.2013', 'dd.MM.yyyy', new Date(2013, 10, 17, 0)); + expectParse('31.12.2013', 'dd.MM.yyyy', new Date(2013, 11, 31, 0)); + expectParse('08-03-1991', 'dd-MM-yyyy', new Date(1991, 2, 8, 0)); + expectParse('03/05/1980', 'MM/dd/yyyy', new Date(1980, 2, 5, 0)); + expectParse('10.01/1983', 'dd.MM/yyyy', new Date(1983, 0, 10, 0)); + expectParse('11-09-1980', 'MM-dd-yyyy', new Date(1980, 10, 9, 0)); + expectParse('2011/02/05', 'yyyy/MM/dd', new Date(2011, 1, 5, 0)); + }); + + it('should work correctly for `yy`', function() { + expectParse('17.11.13', 'dd.MM.yy', new Date(2013, 10, 17, 0)); + expectParse('02-05-11', 'dd-MM-yy', new Date(2011, 4, 2, 0)); + expectParse('02/05/80', 'MM/dd/yy', new Date(2080, 1, 5, 0)); + expectParse('55/02/05', 'yy/MM/dd', new Date(2055, 1, 5, 0)); + expectParse('11-08-13', 'dd-MM-yy', new Date(2013, 7, 11, 0)); + }); + + it('should work correctly for `M`', function() { + expectParse('8/11/2013', 'M/dd/yyyy', new Date(2013, 7, 11, 0)); + expectParse('07.11.05', 'dd.M.yy', new Date(2005, 10, 7, 0)); + expectParse('02-5-11', 'dd-M-yy', new Date(2011, 4, 2, 0)); + expectParse('2/05/1980', 'M/dd/yyyy', new Date(1980, 1, 5, 0)); + expectParse('1955/2/05', 'yyyy/M/dd', new Date(1955, 1, 5, 0)); + expectParse('02-5-11', 'dd-M-yy', new Date(2011, 4, 2, 0)); + }); + + it('should work correctly for `MMM`', function() { + expectParse('30.Sep.10', 'dd.MMM.yy', new Date(2010, 8, 30, 0)); + expectParse('02-May-11', 'dd-MMM-yy', new Date(2011, 4, 2, 0)); + expectParse('Feb/05/1980', 'MMM/dd/yyyy', new Date(1980, 1, 5, 0)); + expectParse('1955/Feb/05', 'yyyy/MMM/dd', new Date(1955, 1, 5, 0)); + }); + + it('should work correctly for `MMMM`', function() { + expectParse('17.November.13', 'dd.MMMM.yy', new Date(2013, 10, 17, 0)); + expectParse('05-March-1980', 'dd-MMMM-yyyy', new Date(1980, 2, 5, 0)); + expectParse('February/05/1980', 'MMMM/dd/yyyy', new Date(1980, 1, 5, 0)); + expectParse('1949/December/20', 'yyyy/MMMM/dd', new Date(1949, 11, 20, 0)); + }); + + it('should work correctly for `d`', function() { + expectParse('17.November.13', 'd.MMMM.yy', new Date(2013, 10, 17, 0)); + expectParse('8-March-1991', 'd-MMMM-yyyy', new Date(1991, 2, 8, 0)); + expectParse('February/5/1980', 'MMMM/d/yyyy', new Date(1980, 1, 5, 0)); + expectParse('1955/February/5', 'yyyy/MMMM/d', new Date(1955, 1, 5, 0)); + expectParse('11-08-13', 'd-MM-yy', new Date(2013, 7, 11, 0)); + }); + }); + + describe('wih predefined formats', function() { + it('should work correctly for `shortDate`', function() { + expectParse('9/3/10', 'shortDate', new Date(2010, 8, 3, 0)); + }); + + it('should work correctly for `mediumDate`', function() { + expectParse('Sep 3, 2010', 'mediumDate', new Date(2010, 8, 3, 0)); + }); + + it('should work correctly for `longDate`', function() { + expectParse('September 3, 2010', 'longDate', new Date(2010, 8, 3, 0)); + }); + + it('should work correctly for `fullDate`', function() { + expectParse('Friday, September 3, 2010', 'fullDate', new Date(2010, 8, 3, 0)); + }); + }); + + describe('with edge case', function() { + it('should not work for invalid number of days in February', function() { + expect(dateParser.parse('29.02.2013', 'dd.MM.yyyy')).toBeUndefined(); + }); + + it('should work for 29 days in February for leap years', function() { + expectParse('29.02.2000', 'dd.MM.yyyy', new Date(2000, 1, 29, 0)); + }); + + it('should not work for 31 days for some months', function() { + expect(dateParser.parse('31-04-2013', 'dd-MM-yyyy')).toBeUndefined(); + expect(dateParser.parse('November 31, 2013', 'MMMM d, yyyy')).toBeUndefined(); + }); + }); +});