/** * Module Dependencies */ var debug = require('debug')('date:parser') var date = require('./date') var norm = require('./norm') /** * Days */ var days = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'] var months = ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december' ] /** * Regexs */ // 5, 05, 5:30, 5.30, 05:30:10, 05:30.10, 05.30.10, at 5 var rMeridiem = /^(\d{1,2})([:.](\d{1,2}))?([:.](\d{1,2}))?\s*([ap]m)/ var rHourMinute = /^(\d{1,2})([:.](\d{1,2}))([:.](\d{1,2}))?/ var rAtHour = /^at\s?(\d{1,2})$/ var rDays = /\b(sun(day)?|mon(day)?|tues(day)?|wed(nesday)?|thur(sday|s)?|fri(day)?|sat(urday)?)s?\b/ var rMonths = /^((\d{1,2})\s*(st|nd|rd|th))\s(day\s)?(of\s)?(january|february|march|april|may|june|july|august|september|october|november|december)/i var rPast = /\b(last|yesterday|ago)\b/ var rDayMod = /\b(morning|noon|afternoon|night|evening|midnight)\b/ var rAgo = /^(\d*)\s?\b(second|minute|hour|day|week|month|year)[s]?\b\s?ago$/ /** * Expose `parser` */ module.exports = parser /** * Initialize `parser` * * @param {String} str * @return {Date} * @api publics */ function parser (str, offset) { if (!(this instanceof parser)) return new parser(str, offset) if (typeof offset == 'string') offset = parser(offset) // CFG preprocessing into normalized format, // get {str, tokens, normals} // !future: return multiple parsed times, some from it var prepro = norm(str, offset) // console.log(prepro) // reset the str to prepro str str = prepro.str // if proprocessed doesn't leave any str to be processed (non-date-time) format, check normals if (!str) { if (prepro.normals.length) { // if there's normal date parsed already, // !return the first return new Date(prepro.normals[0]) } else { // otherwise go back to below to return proper Error str = str } } var d = offset || new Date this.date = new date(d) this.original = str this.str = str.toLowerCase() this.stash = [] this.tokens = [] while (this.advance() !== 'eos') debug('tokens %j', this.tokens) this.nextTime(d) if (this.date.date == d) throw new Error('Invalid date') return this.date.date } /** * Advance a token */ parser.prototype.advance = function () { var tok = this.eos() || this.space() || this._next() || this.last() || this.dayByName() || this.monthByName() || this.timeAgo() || this.ago() || this.yesterday() || this.tomorrow() || this.noon() || this.midnight() || this.night() || this.evening() || this.afternoon() || this.morning() || this.tonight() || this.meridiem() || this.hourminute() || this.athour() || this.week() || this.month() || this.year() || this.second() || this.minute() || this.hour() || this.day() || this.number() || this.string() || this.other() this.tokens.push(tok) return tok } /** * Lookahead `n` tokens. * * @param {Number} n * @return {Object} * @api private */ parser.prototype.lookahead = function (n) { var fetch = n - this.stash.length if (fetch == 0) return this.lookahead(++n) while (fetch-- > 0) this.stash.push(this.advance()) return this.stash[--n] } /** * Lookahead a single token. * * @return {Token} * @api private */ parser.prototype.peek = function () { return this.lookahead(1) } /** * Fetch next token including those stashed by peek. * * @return {Token} * @api private */ parser.prototype.next = function () { var tok = this.stashed() || this.advance() return tok } /** * Return the next possibly stashed token. * * @return {Token} * @api private */ parser.prototype.stashed = function () { var stashed = this.stash.shift() return stashed } /** * Consume the given `len`. * * @param {Number|Array} len * @api private */ parser.prototype.skip = function (len) { this.str = this.str.substr(Array.isArray(len) ? len[0].length : len) } /** * EOS */ parser.prototype.eos = function () { if (this.str.length) return return 'eos' } /** * Space */ parser.prototype.space = function () { var captures if (captures = /^([ \t]+)/.exec(this.str)) { this.skip(captures) return this.advance() } } /** * Second */ parser.prototype.second = function () { var captures if (captures = /^s(ec|econd)?s?/.exec(this.str)) { this.skip(captures) return 'second' } } /** * Minute */ parser.prototype.minute = function () { var captures if (captures = /^m(in|inute)?s?/.exec(this.str)) { this.skip(captures) return 'minute' } } /** * Hour */ parser.prototype.hour = function () { var captures if (captures = /^h(r|our)s?/.exec(this.str)) { this.skip(captures) return 'hour' } } /** * Day */ parser.prototype.day = function () { var captures if (captures = /^d(ay)?s?/.exec(this.str)) { this.skip(captures) return 'day' } } /** * Day by name */ parser.prototype.dayByName = function () { var captures var r = new RegExp('^' + rDays.source) if (captures = r.exec(this.str)) { var day = captures[1] this.skip(captures) this.date[day](1) return captures[1] } } /** * Month by name */ parser.prototype.monthByName = function () { var captures if (captures = rMonths.exec(this.str)) { var day = captures[2] var month = captures[6] this.date.date.setMonth((months.indexOf(month))) if (day) this.date.date.setDate(parseInt(day)) this.skip(captures) return captures[0] } } parser.prototype.timeAgo = function () { var captures if (captures = rAgo.exec(this.str)) { var num = captures[1] var mod = captures[2] this.date[mod](-num) this.skip(captures) return 'timeAgo' } } /** * Week */ parser.prototype.week = function () { var captures if (captures = /^w(k|eek)s?/.exec(this.str)) { this.skip(captures) return 'week' } } /** * Month */ parser.prototype.month = function () { var captures if (captures = /^mon(th)?(es|s)?\b/.exec(this.str)) { this.skip(captures) return 'month' } } /** * Week */ parser.prototype.year = function () { var captures if (captures = /^y(r|ear)s?/.exec(this.str)) { this.skip(captures) return 'year' } } /** * Meridiem am/pm */ parser.prototype.meridiem = function () { var captures if (captures = rMeridiem.exec(this.str)) { this.skip(captures) this.time(captures[1], captures[3], captures[5], captures[6]) return 'meridiem' } } /** * Hour Minute (ex. 12:30) */ parser.prototype.hourminute = function () { var captures if (captures = rHourMinute.exec(this.str)) { this.skip(captures) this.time(captures[1], captures[3], captures[5], this._meridiem) return 'hourminute' } } /** * At Hour (ex. at 5) */ parser.prototype.athour = function () { var captures if (captures = rAtHour.exec(this.str)) { this.skip(captures) this.time(captures[1], 0, 0, this._meridiem) this._meridiem = null return 'athour' } } /** * Time set helper */ parser.prototype.time = function (h, m, s, meridiem) { var d = this.date var before = d.clone() if (meridiem) { // convert to 24 hour h = ('pm' == meridiem && 12 > h) ? +h + 12 : h; // 6pm => 18 h = ('am' == meridiem && 12 == h) ? 0 : h; // 12am => 0 } m = (!m && d.changed('minutes')) ? false : m s = (!s && d.changed('seconds')) ? false : s d.time(h, m, s) } /** * Best attempt to pick the next time this date will occur * * TODO: place at the end of the parsing */ parser.prototype.nextTime = function (before) { var d = this.date var orig = this.original if (before <= d.date || rPast.test(orig)) return this // If time is in the past, we need to guess at the next time if (rDays.test(orig)) { d.day(7) } else if ((before - d.date) / 1000 > 60) { // If it is a month in the past, don't add a day if (rMonths.test(orig)) { d.day(0) } else { d.day(1) } } return this } /** * Yesterday */ parser.prototype.yesterday = function () { var captures if (captures = /^(yes(terday)?)/.exec(this.str)) { this.skip(captures) this.date.day(-1) return 'yesterday' } } /** * Tomorrow */ parser.prototype.tomorrow = function () { var captures if (captures = /^tom(orrow)?/.exec(this.str)) { this.skip(captures) this.date.day(1) return 'tomorrow' } } /** * Noon */ parser.prototype.noon = function () { var captures if (captures = /^noon\b/.exec(this.str)) { this.skip(captures) var before = this.date.clone() this.date.date.setHours(12, 0, 0) return 'noon' } } /** * Midnight */ parser.prototype.midnight = function () { var captures if (captures = /^midnight\b/.exec(this.str)) { this.skip(captures) var before = this.date.clone() this.date.date.setHours(0, 0, 0) return 'midnight' } } /** * Night (arbitrarily set at 7pm) */ parser.prototype.night = function () { var captures if (captures = /^night\b/.exec(this.str)) { this.skip(captures) this._meridiem = 'pm' var before = this.date.clone() this.date.date.setHours(19, 0, 0) return 'night' } } /** * Evening (arbitrarily set at 5pm) */ parser.prototype.evening = function () { var captures if (captures = /^evening\b/.exec(this.str)) { this.skip(captures) this._meridiem = 'pm' var before = this.date.clone() this.date.date.setHours(17, 0, 0) return 'evening' } } /** * Afternoon (arbitrarily set at 2pm) */ parser.prototype.afternoon = function () { var captures if (captures = /^afternoon\b/.exec(this.str)) { this.skip(captures) this._meridiem = 'pm' var before = this.date.clone() if (this.date.changed('hours')) return 'afternoon' this.date.date.setHours(14, 0, 0) return 'afternoon' } } /** * Morning (arbitrarily set at 8am) */ parser.prototype.morning = function () { var captures if (captures = /^morning\b/.exec(this.str)) { this.skip(captures) this._meridiem = 'am' var before = this.date.clone() if (!this.date.changed('hours')) this.date.date.setHours(8, 0, 0) return 'morning' } } /** * Tonight */ parser.prototype.tonight = function () { var captures if (captures = /^tonight\b/.exec(this.str)) { this.skip(captures) this._meridiem = 'pm' return 'tonight' } } /** * Next time */ parser.prototype._next = function () { var captures if (captures = /^next/.exec(this.str)) { this.skip(captures) var d = new Date(this.date.date) var mod = this.peek() // If we have a defined modifier, then update if (this.date[mod]) { this.next() // slight hack to modify already modified this.date = date(d) this.date[mod](1) } else if (rDayMod.test(mod)) { this.date.day(1) } return 'next' } } /** * Last time */ parser.prototype.last = function () { var captures if (captures = /^last/.exec(this.str)) { this.skip(captures) var d = new Date(this.date.date) var mod = this.peek() // If we have a defined modifier, then update if (this.date[mod]) { this.next() // slight hack to modify already modified this.date = date(d) this.date[mod](-1) } else if (rDayMod.test(mod)) { this.date.day(-1) } return 'last' } } /** * Ago */ parser.prototype.ago = function () { var captures if (captures = /^ago\b/.exec(this.str)) { this.skip(captures) return 'ago' } } /** * Number */ parser.prototype.number = function () { var captures if (captures = /^(\d+)/.exec(this.str)) { var n = captures[1] this.skip(captures) var mod = this.peek() // If we have a defined modifier, then update if (this.date[mod]) { if ('ago' == this.peek()) n = -n this.date[mod](n) } else if (this._meridiem) { // when we don't have meridiem, possibly use context to guess this.time(n, 0, 0, this._meridiem) this._meridiem = null } else if (this.original.indexOf('at') > -1) { this.time(n, 0, 0, this._meridiem) this._meridiem = null } return 'number' } } /** * String */ parser.prototype.string = function () { var captures if (captures = /^\w+/.exec(this.str)) { this.skip(captures) return 'string' } } /** * Other */ parser.prototype.other = function () { var captures if (captures = /^./.exec(this.str)) { this.skip(captures) return 'other' } }