UNPKG

@davidwells/parse-time

Version:

Parse messy time strings into JavaScript Date objects

453 lines (427 loc) 14.5 kB
// Fork of https://github.com/substack/parse-messy-time/blob/master/index.js const MONTHS = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', ] const DAYS = [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', ] var hmsre = RegExp('(\\d+\\.?\\d*(?:[:h]\\d+\\.?\\d*(?:[:m]\\d+\\.\\d*s?)?)?)') var tokre = RegExp( '\\s+|(\\d+(?:st|th|nd|rd|th))\\b' + '|' + hmsre.source + '([A-Za-z]+)' + '|([A-Za-z]+)' + hmsre.source, ) /** * Parses a time string, number, or Date object into a JavaScript Date object * @param {string|number|Date} str - The time to parse. Can be a string (e.g. "tomorrow", "next monday"), * a Unix timestamp (10 or 13 digits), or a Date object * @param {Object} [opts={}] - Options for parsing * @param {Date|number|string} [opts.now=new Date()] - Reference date to use for relative time calculations * @returns {Date} A JavaScript Date object representing the parsed time * @throws {Error} Throws an error if the input string is empty, undefined, or null * @example * // Parse relative times * parseTime('in 2 hours') // Returns a date 2 hours from now * parseTime('3 days ago') // Returns a date 3 days before now * parseTime('next week') // Returns a date 1 week from now * * // Parse specific dates and times * parseTime('January 15th 2024 at 3:30pm') // Returns 2024-01-15 15:30:00 * parseTime('Dec 25 2023 midnight') // Returns 2023-12-25 00:00:00 * parseTime('July 4 2023 noon') // Returns 2023-07-04 12:00:00 * * // Parse special keywords * parseTime('today at 5pm') // Returns today at 5:00 PM * parseTime('yesterday noon') // Returns yesterday at 12:00 PM * parseTime('tomorrow at midnight') // Returns tomorrow at 00:00 AM * * // Parse with reference date * const nowDate = new Date('2023-01-01T12:00:00Z') * parseTime('in 2 hours', { now: nowDate }) // Returns 2023-01-01T14:00:00.000Z */ function parseTime(str, opts) { if (!opts) opts = {} /* if str is empty string or undefined or null throw error */ if (str === '' || str === undefined || str === null) { throw new Error('Invalid time string') } /* If str === unix timestamp return value */ if (typeof str === 'number' && str.toString().length === 10) { return new Date(str * 1000) } /* If str === unix timestamp return value */ if (typeof str === 'number' && str.toString().length === 13) { return new Date(str) } /* If str is already a date return value */ if (str instanceof Date) { return str } var now = opts.now || new Date() if (typeof now === 'number' || typeof now === 'string') now = new Date(now) if (typeof str === 'string' && str === 'never') { return new Date('9999-12-31') } var ago = false var tokens = str.split(tokre).filter(Boolean).map(lc) var res = {} // By default set ms to 0 res.ms = 0 for (var i = 0; i < tokens.length; i++) { var t = tokens[i] var next = tokens[i + 1] var prev = tokens[i - 1] var m // console.log('t', t) // console.log('next', next) // console.log('prev', prev) if ((m = /(\d+)(st|nd|rd|th)/i.exec(t))) { if (next === 'of') { next = tokens[i + 2] i++ } res.date = Number(m[1]) if (monthish(next)) { res.month = next i++ } } else if ((m = /(\d+)(st|nd|rd|th)?/i.exec(next)) && monthish(t)) { res.month = t res.date = Number(m[1]) i++ if (/^\d+$/.test(tokens[i + 1])) { // year res.year = Number(tokens[i + 1]) i++ } } else if ((m = hmsre.exec(t)) && isunit(next)) { if (tokens[i - 1] === 'in') { for (var j = i; j < tokens.length; j += 2) { if (tokens[j] === 'and') j-- else if ((m = hmsre.exec(tokens[j])) && ishunit(tokens[j + 1])) { addu(parseh(tokens[j]), nunit(tokens[j + 1])) } else if ((m = /^(\d+\.?\d*)/.exec(tokens[j])) && isdunit(tokens[j + 1])) { daddu(Number(m[1]), nunit(tokens[j + 1])) } else break } i = j } else { for (var j = i + 2; j < tokens.length; j++) { if (tokens[j] === 'ago') { ago = true break } } if (j === tokens.length) continue for (var k = i; k < j; k++) { if ((m = hmsre.exec(tokens[k])) && ishunit(tokens[k + 1])) { subu(parseh(tokens[k]), nunit(tokens[k + 1])) } else if ((m = /^(\d+\.?\d*)/.exec(tokens[k])) && isdunit(tokens[k + 1])) { dsubu(Number(m[1]), nunit(tokens[k + 1])) } } i = j } } else if (/noon/.test(t)) { res.hours = 12 res.minutes = 0 res.seconds = 0 } else if (/midnight/.test(t)) { res.hours = 0 res.minutes = 0 res.seconds = 0 // console.log('set it', res) } else if (/\d+[:h]\d+/.test(t) || /^(am|pm)/.test(next)) { var hms = parseh(t, next) if (hms[0] !== null) res.hours = hms[0] if (hms[1] !== null) res.minutes = hms[1] if (hms[2] !== null) res.seconds = hms[2] } else if ((m = /^(\d+)/.exec(t)) && monthish(next)) { var x = Number(m[1]) if (res.year === undefined && x > 31) res.year = x else if (res.date === undefined) res.date = x if (res.month === undefined) res.month = next i++ } else if (monthish(t) && (m = /^(\d+)/.exec(next))) { var x = Number(m[1]) if (res.year === undefined && x > 31) res.year = x else if (res.date === undefined) res.date = x if (res.month === undefined) res.month = t i++ } else if ((m = /^(\d+)/.exec(t)) && monthish(prev)) { var x = Number(m[1]) if (res.year === undefined) res.year = x else if (res.hours === undefined) res.hours = x } else if ((m = /^[`'\u00b4\u2019](\d+)/.exec(t))) { res.year = Number(m[1]) } else if (/^\d{4}[\W_]\d{1,2}[\W_]\d{1,2}/.test(t)) { var yms = t.split(/[\W_]/) res.year = Number(yms[0]) res.month = Number(yms[1]) - 1 res.date = Number(yms[2]) } else if ((m = /^(\d+)/.exec(t))) { var x = Number(m[1]) if (res.hours === undefined && x < 24) res.hours = x else if (res.date === undefined && x <= 31) res.date = x else if (res.year === undefined && x > 31) res.year = x else if (res.year == undefined && res.hours !== undefined && res.date !== undefined) { res.year = x } else if (res.hours === undefined && res.date !== undefined && res.year !== undefined) { res.hours = x } else if (res.date === undefined && res.hours !== undefined && res.year !== undefined) { res.date = x } } else if (/^today$/.test(t) && res.date === undefined) { res.date = now.getDate() res.month = MONTHS[now.getMonth()] res.year = now.getFullYear() } else if (/^now$/.test(t) && res.date === undefined) { res.hours = now.getHours() res.minutes = now.getMinutes() res.seconds = now.getSeconds() res.date = now.getDate() res.month = MONTHS[now.getMonth()] res.year = now.getFullYear() res.ms = now.getMilliseconds() } else if (/^to?m+o?r+o?w?/.test(t) && res.date === undefined) { var tomorrow = new Date(now.valueOf() + 24 * 60 * 60 * 1000) res.date = tomorrow.getDate() if (res.month === undefined) { res.month = MONTHS[tomorrow.getMonth()] } if (res.year === undefined) { res.year = tomorrow.getFullYear() } } else if (/^yesterday/.test(t) && res.date === undefined) { var yst = new Date(now.valueOf() - 24 * 60 * 60 * 1000) res.date = yst.getDate() if (res.month === undefined) { res.month = MONTHS[yst.getMonth()] } if (res.year === undefined) { res.year = yst.getFullYear() } } else if (t === 'next' && dayish(next) && res.date === undefined) { setFromDay(next, 7) i++ } // Add case for "next week" else if (t === 'next' && next === 'week' && res.date === undefined) { res.hours = now.getHours() res.minutes = now.getMinutes() res.seconds = now.getSeconds() res.date = now.getDate() + 7 res.month = MONTHS[now.getMonth()] res.year = now.getFullYear() i++ } else if (t === 'last' && dayish(next) && res.date === undefined) { setFromDay(next, -7) i++ } else if (dayish(t) && res.date === undefined) { setFromDay(t, 0) } } if (res.year < 100) { var y = now.getFullYear() var py = y % 100 if (py + 10 < res.year) { res.year += y - py - 100 } else res.year += y - py } if (res.month && typeof res.month !== 'number') { res.month = nmonth(res.month) } var out = new Date(now) // console.log('out1', out) out.setHours(res.hours === undefined ? 0 : res.hours) out.setMinutes(res.minutes === undefined ? 0 : res.minutes) out.setSeconds(res.seconds === undefined ? 0 : res.seconds) out.setMilliseconds(res.ms) // console.log('out2', out) var monthSet = res.month if (typeof res.month === 'number') { out.setMonth(res.month) } else if (res.month) { monthSet = MONTHS.indexOf(res.month) out.setMonth(monthSet) } if (res.date !== undefined) out.setDate(res.date) if (monthSet !== undefined && out.getMonth() !== monthSet) { out.setMonth(monthSet) } if (res.year) { out.setYear(res.year) // console.log('out', out) } else if (out < now && !ago && Math.abs(out.getMonth() + 12 - now.getMonth()) % 12 >= 1) { out.setYear(now.getFullYear() + 1) // console.log('out', out) } return out /** * Sets the date, month and year in the result object based on a day name and offset * @param {string} t - The day name to set (e.g. 'monday', 'tuesday', etc) * @param {number} x - Number of weeks to offset from the target day (0 for this week, 7 for next week, -7 for last week) * @private */ function setFromDay(t, x) { var dayi = DAYS.indexOf(nday(t)) var xdays = ((7 + dayi - now.getDay()) % 7) + x var d = new Date(now.valueOf() + xdays * 24 * 60 * 60 * 1000) res.date = d.getDate() if (res.month === undefined) { res.month = MONTHS[d.getMonth()] } if (res.year === undefined) { res.year = d.getFullYear() } } function opu(hms, u, op) { if (u == 'hours') { res.hours = op(now.getHours(), hms[0]) res.minutes = op(now.getMinutes(), hms[1] === null ? 0 : hms[1]) res.seconds = op(now.getSeconds(), hms[2] === null ? 0 : hms[2]) } else if (u == 'minutes') { if (res.hours === undefined) res.hours = now.getHours() res.minutes = op(now.getMinutes(), hms[0] === null ? 0 : hms[0]) res.seconds = op(now.getSeconds(), hms[1] === null ? 0 : hms[1]) } else if (u == 'seconds') { if (res.hours === undefined) res.hours = now.getHours() if (res.minutes === undefined) res.minutes = now.getMinutes() res.seconds = op(now.getSeconds(), hms[0] === null ? 0 : hms[0]) } } function subu(hms, u) { opu(hms, u, sub) } function addu(hms, u) { opu(hms, u, add) } function dopu(n, u, op) { if (res.hours === undefined) res.hours = now.getHours() if (res.minutes === undefined) res.minutes = now.getMinutes() if (res.seconds === undefined) res.seconds = now.getSeconds() if (u === 'days') { res.date = op(now.getDate(), n) } else if (u === 'weeks') { res.date = op(now.getDate(), n * 7) } else if (u === 'months') { res.month = op(now.getMonth(), n) } else if (u === 'years') { res.year = op(now.getFullYear(), n) } } function dsubu(n, u) { dopu(n, u, sub) } function daddu(n, u) { dopu(n, u, add) } } function dateToUTCMidnight(now) { return new Date(Date.UTC( now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), 0, 0, 0 )) } function add(a, b) { return a + b } function sub(a, b) { return a - b } function lc(s) { return String(s).toLowerCase() } function ishunit(s) { var n = nunit(s) return n === 'hours' || n === 'minutes' || n === 'seconds' } function isdunit(s) { var n = nunit(s) return n === 'days' || n === 'weeks' || n === 'months' || n === 'years' } function isunit(s) { return Boolean(nunit(s)) } function nunit(s) { if (/^(ms|millisecs?|milliseconds?)$/.test(s)) return 'milliseconds' if (/^(s|secs?|seconds?)$/.test(s)) return 'seconds' if (/^(m|mins?|minutes?)$/.test(s)) return 'minutes' if (/^(h|hrs?|hours?)$/.test(s)) return 'hours' if (/^(d|days?)$/.test(s)) return 'days' if (/^(w|wks?|weeks?)$/.test(s)) return 'weeks' if (/^(mo|mnths?|months?)$/.test(s)) return 'months' if (/^(y|yrs?|years?)$/.test(s)) return 'years' } function monthish(s) { return Boolean(nmonth(s)) } function dayish(s) { return /^(mon|tue|wed|thu|fri|sat|sun)/i.test(s) } function nmonth(s) { if (/^jan/i.test(s)) return 'January' if (/^feb/i.test(s)) return 'February' if (/^mar/i.test(s)) return 'March' if (/^apr/i.test(s)) return 'April' if (/^may/i.test(s)) return 'May' if (/^jun/i.test(s)) return 'June' if (/^jul/i.test(s)) return 'July' if (/^aug/i.test(s)) return 'August' if (/^sep/i.test(s)) return 'September' if (/^oct/i.test(s)) return 'October' if (/^nov/i.test(s)) return 'November' if (/^dec/i.test(s)) return 'December' } function nday(s) { if (/^mon/i.test(s)) return 'Monday' if (/^tue/i.test(s)) return 'Tuesday' if (/^wed/i.test(s)) return 'Wednesday' if (/^thu/i.test(s)) return 'Thursday' if (/^fri/i.test(s)) return 'Friday' if (/^sat/i.test(s)) return 'Saturday' if (/^sun/i.test(s)) return 'Sunday' } function parseh(s, next) { var m = /(\d+\.?\d*)(?:[:h](\d+\.?\d*)(?:[:m](\d+\.?\d*s?\.?\d*))?)?/.exec(s) var hms = [Number(m[1]), null, null] if (/^am/.test(next) && hms[0] == 12) hms[0] -= 12 if (/^pm/.test(next) && hms[0] < 12) hms[0] += 12 if (m[2]) hms[1] = Number(m[2]) if (m[3]) hms[2] = Number(m[3]) if (hms[0] > floorup(hms[0])) { hms[1] = floorup((hms[0] - floorup(hms[0])) * 60) hms[0] = floorup(hms[0]) } if (hms[1] > floorup(hms[1])) { hms[2] = floorup((hms[1] - floorup(hms[1])) * 60) hms[1] = floorup(hms[1]) } return hms } function floorup(x) { return Math.floor(Math.round(x * 1e6) / 1e6) } module.exports = { MONTHS, DAYS, parseTime }