Skip to content

Commit

Permalink
More cases:
Browse files Browse the repository at this point in the history
last march on the 24th at midday
last week on tuesday at noon
  • Loading branch information
alanclarke committed May 18, 2020
1 parent f74dd7d commit e5a0ebf
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 50 deletions.
98 changes: 48 additions & 50 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,25 @@ const wordsToNumbers = require('words-to-numbers').default

// in 2 years and 5 weeks
const PATTERNS = {
WEEKDAYS: toPattern(WEEKDAYS),
// avoid confusion between 'monday' and 'month'
WEEKDAYS: toPattern(WEEKDAYS).replace('mon(', 'mon(?!th)('),
MONTHS: toPattern(MONTHS),
ORDINAL: '(?:st|th|rd|nd)',
PERIOD: '(sec(?:ond)|min(?:ute)?|h(?:ou)?r|day|week|month|year|2)s?',
PERIOD: '(sec(?:ond)|min(?:ute)?|h(?:ou)?r|day|week|month|year)s?',
QUANTITY: '(an|a|the|\\d+(?:\\.\\d+)?)'
}

module.exports = function parseHumanDate (input, options = {}) {
const { now = Date.now() } = options
let date, match, periods
const text = wordsToNumbers(input)
let text = wordsToNumbers(input)

if (typeof text !== 'string') return null

// words to numbers changes the word 'second' to 2, which isn't what you want
// when refering to 'one second'
text = text.replace(/(?<=(a|1|last|next|this)\s+)2/i, 'second')

// times 'at 2'
match = text.match(new RegExp([
'(?:at)', '([0-2]?\\d)', ':?', '(\\d\\d)?', '([ap]m)?'
Expand Down Expand Up @@ -62,35 +68,6 @@ module.exports = function parseHumanDate (input, options = {}) {
if (text === 'midnight') setTime(date, 0, 0)
}

// day 'tomorrow'
match = text.match(new RegExp('^' + [
'(today|now|yesterday|tomorrow)'
].join('\\s*'), 'i'))
if (match) {
date = date || new Date(now)
const [, text] = match
if (text === 'yesterday') date.setTime(date.getTime() - ms('1d'))
if (text === 'tomorrow') date.setTime(date.getTime() + ms('1d'))
}

// absolute date
match = text.match(new RegExp([
'(?:on)?', '(the|at)?', '([0-3]?\\d)(?:st|nd|rd|th)?', '(?:of)?', PATTERNS.MONTHS + '?', '(\\d+)?'
].join('\\s*'), 'i'))
if (match) {
let [, prefix, day, month, year] = match
day = parseNumber(day)
const isDate = day &&
prefix !== 'at' &&
(prefix === 'the' || month)
if (isDate) {
date = date || new Date(now)
date.setDate(day)
if (month) setMonth(date, 'this', month)
if (year) date.setYear(Number(year))
}
}

// n period
periods = parsePeriods(text)
if (periods) {
Expand All @@ -104,7 +81,10 @@ module.exports = function parseHumanDate (input, options = {}) {
const index = text.indexOf(suffix)
periods = parsePeriods(text.substr(0, index))
if (/^(last|next)$/.test(reference)) reference = `${reference} ${periods[0].period}`
const ref = parseHumanDate(reference, options)
const ref = parseHumanDate(reference, {
...options,
now: date ? date.getTime() : options.now
})
if (ref) {
date = ref
for (const p of periods) {
Expand All @@ -115,6 +95,7 @@ module.exports = function parseHumanDate (input, options = {}) {
addPeriod(date, period, n)
}
}
return date
}
} else if (suffix === 'ago') {
date = date || new Date(now)
Expand All @@ -131,14 +112,42 @@ module.exports = function parseHumanDate (input, options = {}) {
}
}

// last period / next period
// day 'tomorrow'
match = text.match(new RegExp('^' + [
'(today|now|yesterday|tomorrow)'
].join('\\s*'), 'i'))
if (match) {
date = date || new Date(now)
const [, text] = match
if (text === 'yesterday') date.setTime(date.getTime() - ms('1d'))
if (text === 'tomorrow') date.setTime(date.getTime() + ms('1d'))
}

// absolute date
match = text.match(new RegExp([
'(?:on)?', '(the|at)?', '([0-3]?\\d)(?:st|nd|rd|th)?', '(?:of)?', PATTERNS.MONTHS + '?', '(\\d+)?'
].join('\\s*'), 'i'))
if (match) {
let [, prefix, day, month, year] = match
day = parseNumber(day)
const isDate = day &&
prefix !== 'at' &&
(prefix === 'the' || month)
if (isDate) {
date = date || new Date(now)
date.setDate(day)
if (month) setMonth(date, 'this', month)
if (year) date.setYear(Number(year))
}
}

// last period / next period
match = text.match(new RegExp([
'(last|next)', PATTERNS.PERIOD
].join('\\s*') + '$', 'i'))
].join('\\s*'), 'i'))
if (match) {
date = date || new Date(now)
let [, direction, period] = match
if (period === '2') period = 'second'
const [, direction, period] = match
if (direction === 'last') {
addPeriod(date, period, -1)
} else {
Expand All @@ -152,18 +161,13 @@ module.exports = function parseHumanDate (input, options = {}) {
// next / last week on tuesday
match = text.match(new RegExp('^' + [
'(last|next|this)?', '(week)?', '(?:on)?', PATTERNS.WEEKDAYS, '(last|next|this)?', '(week)?'
].join('\\s*') + '$', 'i'))
].join('\\s*'), 'i'))
if (match) {
date = date || new Date(now)
let [, type, period, day, type2, period2] = match
type = type || type2 || 'this'
period = period || period2
if (period === 'week') {
if (type === 'next') {
addPeriod(date, 'week', 1)
} else if (type === 'last') {
addPeriod(date, 'week', -1)
}
type = 'this'
}
setWeekday(date, type, day)
Expand All @@ -175,18 +179,13 @@ module.exports = function parseHumanDate (input, options = {}) {
// last year on march / next year on march
match = text.match(new RegExp('^' + [
'(last|next|this)?', '(year)?', '(?:in)?', PATTERNS.MONTHS, '(last|next|this)?', '(year)?'
].join('\\s*') + '$', 'i'))
].join('\\s*'), 'i'))
if (match) {
date = date || new Date(now)
let [, type, period, month, type2, period2] = match
type = type || type2 || 'this'
period = period || period2
if (period === 'year') {
if (type === 'next') {
addPeriod(date, 'year', 1)
} else if (type === 'last') {
addPeriod(date, 'year', -1)
}
type = 'this'
}
setMonth(date, type, month)
Expand All @@ -200,7 +199,6 @@ function parsePeriods (text) {
return periods
? periods.map(p => {
let [n, period] = p.split(/\s+/)
if (period === '2') period = 'second'
n = parseNumber(n, { a: 1, an: 1, the: 1 })
return { n, period }
})
Expand Down
2 changes: 2 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ parseDate(text, options) // returns a new javascript date
parseDate('today')
parseDate('tomorrow')
parseDate('the day after tomorrow at twelve')
parseDate('last march on the 24th at midday')
parseDate('yesterday')
parseDate('on the 3rd of march 2014')
parseDate('on the 5th of may')
Expand All @@ -38,6 +39,7 @@ parseDate('next week on tuesday at 4am')
parseDate('tuesday last week')
parseDate('this tuesday')
parseDate('last tuesday')
parseDate('last week on tuesday at noon')
parseDate('one hour and one day before yesterday')
parseDate('next march')
parseDate('march last year')
Expand Down
2 changes: 2 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const cases = {
'at midnight': d => new Date(d.getFullYear(), d.getMonth(), d.getDate()),
midday: d => new Date(d.getFullYear(), d.getMonth(), d.getDate(), 12),
'on the 12th of march at midday': d => new Date(d.getFullYear(), 2, 12, 12),
'last march on the 24th at midday': d => new Date(d.getFullYear(), 2, 24, 12),
'midday on the 12th of march': d => new Date(d.getFullYear(), 2, 12, 12),
'tomorrow at 2pm': d => new Date(d.getFullYear(), d.getMonth(), d.getDate() + 1, 14),
'the day after tomorrow at twelve': d => new Date(d.getFullYear(), d.getMonth(), d.getDate() + 2, 12),
Expand Down Expand Up @@ -148,6 +149,7 @@ const cases = {
'next week on tuesday': d => new Date(d.getTime() + ms('1w') + ms('1d')),
'next week on friday': d => new Date(d.getTime() + ms('1w') + ms('4d')),
'last week on tuesday': d => new Date(d.getTime() - ms('1w') + ms('1d')),
'last week on tuesday at noon': d => new Date(d.getFullYear(), d.getMonth(), d.getDate() - 6, 12),
'last week on friday': d => new Date(d.getTime() - ms('1w') + ms('4d')),
'tuesday this week': d => new Date(d.getTime() + ms('1d')),
'tuesday next week': d => new Date(d.getTime() + ms('1w') + ms('1d')),
Expand Down

0 comments on commit e5a0ebf

Please sign in to comment.