parse-messy-schedule
Version:
parse recurring or one-off scheduled events from free-form text
236 lines (217 loc) • 7.11 kB
JavaScript
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()
}