UNPKG

parse-messy-schedule

Version:

parse recurring or one-off scheduled events from free-form text

236 lines (217 loc) 7.11 kB
var parset = require('parse-messy-time') var nums1to4 = '(1st|2nd|3rd|4th|first|second|third|fourth)' var numNames = { first: '1st', second: '2nd', third: '3rd', fourth: '4th' } var re = {} re.time = /(\d+:\d+|\d+(?::\d+)?\s*(?:pm|am))/ re.every = RegExp( '(?:' + re.time.source + '\\s+)?' + '(every|each)?\\s+(?:(other)\\s+|' + nums1to4 + '(?:(?:\\s*,\\s*|\\s+(and|through)\\s+)' + nums1to4 + ')?' + '\\s+)?' + '(?:((?:mon|tues?|wed(?:nes)?|thurs?|fri|sat(?:ur)?|sun)(?:days?)?' + '|tomorrow|day)\\b)' + '(?:\\s+(.+?))?' + '\\s*$', 'i' ) re.months = RegExp('(jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)' + '|may|june?|july?|aug(?:ust)?|sep(?:t|tember)?|oct(?:ober)?' + '|nov(?:ember)|dec(?:ember)?)', 'i' ) var months = [ 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'july', 'aug', 'sep', 'oct', 'nov', 'dec' ] re.evmonth = RegExp( '(?:(every|each)\\s+)?(' + '(\\d+)(?:st|nd|rd|th)?\\s+' + re.months.source + '|' + re.months.source + '\\s+(\\d+)(?:st|nd|rd|th)?\\b)' ) re.nth = RegExp('(\\d+)\\s*(?:st|nd|rd|th)\\b') re.titleBreak = RegExp( '\\b(each|every|tomorrow|' + '(?:mon|tues?|wed(?:nes)?|thurs?|fri|sat(?:ur)?|sun)(?:days?)?' + ')\\b', 'i' ) re.starting = /\b(?:starting|from)\s+(.+)/ re.until = /\b(?:until|to)\s+(.+)/ function nthf (s) { var m = re.nth.exec(s) if (m) return { n: m[1], time: s.replace(re.nth, '').trim() } } function monthf (s) { var m = re.evmonth.exec(s) if (m) return { date: fix(Number(m[3] || m[6])), month: months.indexOf(String(m[4] || m[5]).slice(0,3).toLowerCase()), time: s.replace(re.evmonth, '').trim() } function fix (n) { return isNaN(n) ? undefined : n } } function everyf (s, now) { if (!now) now = new Date var m = re.every.exec(s) if (!m) return m var time = m[1] || m[8] || null if (time) time = time.replace(/\s+(starting|until).*/, '') var ut = time && !re.time.test(m[10]) ? ' ' + time : '' if (ut) ut = ut.replace(/\b(starting|until).*/, '') var mst = re.starting.exec(s) if (mst) mst = mst[1].split(/\buntil\b/i)[0] var ust = re.until.exec(s) if (ust) ust = ust[1].split(/\bstarting\b/i)[0] return { every: Boolean(m[2] || /days$/i.test(m[7])), other: Boolean(m[3]), numbered: m[4] ? normNum( m[4] && m[5] === 'through' && m[6] ? thru(m[4], m[6]) : m[6] ? [ m[4], m[6] ] : [ m[4] ] ) : null, time: time, day: String(m[7]).toLowerCase(), starting: mst ? parset(mst + ut, { now: now }) : null, until: ust ? parset(ust + ut, { now: now }) : null, index: m.index } } function normNum (xs) { return xs.map(function (x) { x = (x || '').toLowerCase() return numNames[x] || x }) } function thru (a, b) { var m = normNum([ a, b ]) var ns = Object.keys(numNames) .map(function (n) { return numNames[n] }) .sort() ns.forEach(function (n) { if (n > m[m.length-2] && n < m[m.length-1]) { m.splice(m.length-1, 0, n) } }) return m } function countWeeks (a, b) { return Math.round((b.getTime() - a.getTime()) / 1000 / 60 / 60 / 24 / 7) } module.exports = Mess function Mess (str, opts) { if (!(this instanceof Mess)) return new Mess(str, opts) if (!opts) opts = {} this._every = everyf(str, opts.created) this._nth = nthf(str) this._month = monthf(str) this._created = opts.created this.title = this._every ? str.slice(0, this._every.index).trim().replace(/"/g, '') : null if (this.title) { var esp = str.split(re.titleBreak) var mstr = esp.slice(0,-4).join('').trim() if (mstr.length > this.title.length) { this.title = mstr var m = /(['"])([^\1]+)\1/.exec(this.title) if (m) this.title = m[2] } } this.oneTime = Boolean(!(this._every && this._every.every)) this.range = [ new Date(-864e13), new Date(864e13) ] if (this.oneTime) { var t = this.next(this._created) if (!t) t = this.prev(this._created) if (!t) { t = parset(str) } this.range[0] = t this.range[1] = t } if (this._every && this._every.starting) { this.range[0] = this._every.starting } if (this._every && this._every.until) { this.range[1] = this._every.until } if (this.range[0] > this.range[1] && this.range[0].getFullYear()-1 === this.range[1].getFullYear()) { this.range[0].setYear(this.range[0].getFullYear()-1) } } Mess.prototype.next = function (base) { return this._advance(1, base) } Mess.prototype.prev = function (base) { return this._advance(-1, base) } Mess.prototype._advance = function (dir, base) { if (!base) base = new Date if (typeof base === 'string') base = parset(base, { now: this._created }) if (this._every && this._every.numbered) { //... } else if (this._every && this._every.every) { var tt = this._every.time ? ' at ' + this._every.time : '' var t = base if (this._every.starting && t < this._every.starting) { t = this._every.starting } else if (this._every.until && t > this._every.until) { t = this._every.until } else if (this._every.day === 'day') { t = parset(tt, { now: base }) } else if (!this._every.time || this._every.time.split(/\s+/).length <= 4) { t = parset('this ' + this._every.day + tt, { now: base }) } if (((dir > 0 && t <= base) || (dir < 0 && t >= base)) && this._every.day === 'day') { t.setDate(t.getDate() + 1 * dir) } else if ((dir > 0 && t <= base) || (dir < 0 && t >= base)) { t.setDate(t.getDate() + 7 * dir) } if ((this._every.starting || this._created) && this._every.other) { var x = this._every.starting || this._created var w = ((countWeeks(x, t) % 2) + 2) % 2 if (w % 2 !== 0) t.setDate(t.getDate() + 7 * dir) } if (this._every.until && tgt(t, this._every.until)) return null if (this._every.starting && tlt(t, this._every.starting)) return null return t } else if (this._every && (this._created || this._every.starting)) { var x = this._every.starting || this._created var tt = this._every.time ? ' at ' + this._every.time : '' var t = parset('this ' + this._every.day + tt, { now: x }) if (dir > 0 && t <= base) return null else if (dir < 0 && t >= base) return null return t } else if (this._month) { var t = this._month.time ? parset(this._month.time, { now: base }) : base if ((this._month.month < base.getMonth() || (this._month.month === base.getMonth() && this._month.date <= base.getDate()))) { t.setFullYear(t.getFullYear() + dir) } t.setMonth(this._month.month) t.setDate(this._month.date) return t } else if (this._nth) { var t = this._nth.time ? parset(this._nth.time, { now: base }) : base t.setDate(this._nth.n) if (dir > 0 && base.getDate() >= t.getDate()) { t.setMonth(t.getMonth() + 1) } else if (dir < 0 && base.getDate() <= t.getDate()) { t.setMonth(t.getMonth() - 1) } return t } } function tlt (a, b) { return a.getTime() + 1000 < b.getTime() } function tgt (a, b) { return a.getTime() - 1000 > b.getTime() }