tell-me-when
Version:
human relative date and time parser
310 lines (282 loc) • 7.9 kB
text/typescript
import { GrammarNode } from './util/GrammarNode'
import * as EnglishGrammar from './en-US'
import { ParseNode } from './util/ParseNode'
import type { DateFn } from './util/DateFn'
import { ParseRootNode } from './util/ParseRootNode'
import * as base from './util/parse'
const { group, named, oneOf, longestOf } = GrammarNode
const { space, AmPmValue } = EnglishGrammar
const kanjiNumberMap = new Map([
['〇', '0'],
['一', '1'],
['二', '2'],
['三', '3'],
['四', '4'],
['五', '5'],
['六', '6'],
['七', '7'],
['八', '8'],
['九', '9'],
])
class YearNode extends EnglishGrammar.FullYearNode {
constructor(public wrapped: ParseNode) {
super(wrapped)
}
year(input: string) {
const yearText = input
.substring(this.from, this.to - 1)
.split('')
.map((char) => kanjiNumberMap.get(char) ?? char)
.join('')
return parseInt(yearText)
}
}
const JapaneseYear = named(
'JapaneseFullYear',
/([0-9〇一二三四五六七八九]{1,4})年/
).parseAs(YearNode)
class MonthNameNode extends EnglishGrammar.MonthNameNode {
static monthMap = new Map([
['一月', 0],
['二月', 1],
['三月', 2],
['四月', 3],
['五月', 4],
['六月', 5],
['七月', 6],
['八月', 7],
['九月', 8],
['十月', 9],
['十一月', 10],
['十二月', 11],
['睦月', 0],
['如月', 1],
['弥生', 2],
['卯月', 3],
['皐月', 4],
['水無月', 5],
['文月', 6],
['葉月', 7],
['長月', 8],
['神無月', 9],
['神在月', 9],
['霜月', 10],
['師走', 11],
])
month(input: string) {
const monthText = this.substringOf(input).replace('〇', '')
return MonthNameNode.monthMap.get(monthText)!
}
}
const Month = named('JapaneseMonth', /(1[0-2]|0?[1-9])月/).parseAs(
EnglishGrammar.MonthNumNode
)
const MonthName = named(
'JapaneseMonthName',
/((?:〇?[一二三四五六七八九]|十[一二]?)月|睦月|如月|弥生|卯月|皐月|水無月|文月|葉月|長月|神[無在]月|霜月|師走)/
).parseAs(MonthNameNode)
class DayOfMonthNode extends EnglishGrammar.DayOfMonthNumNode {
constructor(public wrapped: ParseNode) {
super(wrapped)
}
dayOfMonth(input: string) {
const dayText = input.substring(this.from, this.to - 1)
switch (dayText) {
case '十':
return 10
default:
return parseInt(
dayText
.replace(/^十/, '一')
.replace(/十$/, '〇')
.replace('十', '')
.split('')
.map((char) => kanjiNumberMap.get(char) ?? char)
.join('')
)
}
}
}
const DayOfMonth = named(
'JapaneseDayOfMonth',
/([12][0-9]|3[01]|0?[1-9]|[一二][〇一二三四五六七八九]|三[〇一]|[二三]?十[一二三四五六七八九]?|〇?[一二三四五六七八九])日/
).parseAs(DayOfMonthNode)
class DateNode extends EnglishGrammar.DateNode {
yearFns(input: string): DateFn[] | undefined {
return this.find(YearNode)?.dateFns(input)
}
monthFns(input: string): DateFn[] | undefined {
const month = (
this.find(MonthNameNode) || this.find(EnglishGrammar.MonthNumNode)
)?.month(input)
return month != null ? [['setMonth', month]] : undefined
}
day(input: string) {
return this.find(DayOfMonthNode)?.dayOfMonth(input)
}
}
const Date = named(
'JapaneseDate',
longestOf(
group(
JapaneseYear,
group(oneOf(Month, MonthName), DayOfMonth.maybe()).maybe()
),
group(oneOf(Month, MonthName), DayOfMonth.maybe()),
DayOfMonth
)
).parseAs(DateNode)
class HoursNode extends EnglishGrammar.HoursNode {
hours(input: string): number {
const hoursText = input.substring(this.from, this.to - 1)
switch (hoursText) {
case '零':
return 0
case '十':
return 10
default:
return parseInt(
hoursText
.replace(/^十/, '一')
.replace(/十$/, '〇')
.replace('十', '')
.split('')
.map((char) => kanjiNumberMap.get(char) ?? char)
.join('')
)
}
}
}
const Hours = named(
'JapaneseHours',
/(2[0-3]|[01]?[0-9]|二?十[一二三]?|十?[一二三四五六七八九]|零)時/
).parseAs(HoursNode)
class MinutesNode extends EnglishGrammar.MinutesNode {
minutes(input: string) {
const minutesText = input.substring(this.from, this.to - 1)
switch (minutesText) {
case '零':
return 0
case '十':
return 10
default:
return parseInt(
minutesText
.replace(/^十/, '一')
.replace(/十$/, '〇')
.replace('十', '')
.split('')
.map((char) => kanjiNumberMap.get(char) ?? char)
.join('')
)
}
}
}
const Minutes = named(
'JapaneseMinutes',
/([0-5]?[0-9]|[二三四五]?十[一二三四五六七八九]?|[一二三四五六七八九]|零)分/
).parseAs(MinutesNode)
class SecondsNode extends EnglishGrammar.SecondsNode {
seconds(input: string): number {
const secondsText = input.substring(this.from, this.to - 1)
switch (secondsText) {
case '零':
return 0
case '十':
return 10
default:
return parseInt(
secondsText
.replace(/^十/, '一')
.replace(/十$/, '〇')
.replace('十', '')
.split('')
.map((char) => kanjiNumberMap.get(char) ?? char)
.join('')
)
}
}
}
const Seconds = named(
'JapaneseSeconds',
/([0-5]?[0-9]|[二三四五]?十[一二三四五六七八九]?|[一二三四五六七八九]|零)秒/
).parseAs(SecondsNode)
class TimeNode extends EnglishGrammar.TimeNode {
hours(input: string) {
return this.find(HoursNode)?.hours(input)
}
minutes(input: string) {
return this.find(MinutesNode)?.minutes(input)
}
seconds(input: string) {
return this.find(SecondsNode)?.seconds(input)
}
amPm(input: string) {
return this.find(AmPmNode)?.amPm(input)
}
}
class AmPmNode extends EnglishGrammar.AmPmNode {
amPm(input: string) {
switch (this.substringOf(input)) {
case '午前':
return AmPmValue.AM
case '午後':
return AmPmValue.PM
default:
throw new Error(`unexpected`)
}
}
}
const AmPm = named('JapaneseAmPm', /午[前後]/).parseAs(AmPmNode)
const Time = named(
'Time',
longestOf(
group(AmPm.maybe(), Hours, group(Minutes, Seconds.maybe())),
group(AmPm.maybe(), Hours, group(Minutes, Seconds.maybe()).maybe())
)
).parseAs(TimeNode)
export class DateTimeNode extends EnglishGrammar.DateTimeNode {
date(input: string): DateFn[] | undefined {
return this.find(DateNode)?.dateFns(input)
}
time(input: string): DateFn[] | undefined {
return this.find(TimeNode)?.dateFns(input)
}
}
export const DateTime = named(
'JapaneseDateTime',
longestOf(Date, Time, group(Date, group(space.maybe(), Time)))
).parseAs(DateTimeNode)
export const Range = named(
'Range',
group(
named('RangeStart', DateTime),
group(
space.maybe(),
oneOf('から', '~', '-', 'ー', '~', '-'),
space.maybe()
),
named('RangeEnd', DateTime),
group(space.maybe(), 'まで').maybe()
)
).parseAs(EnglishGrammar.RangeNode)
export class RootNode extends ParseRootNode {
dateFns(input: string): DateFn[] {
return (
(this.find(EnglishGrammar.RangeNode) || this.find(DateTimeNode))?.dateFns(
input
) || []
)
}
}
export const Root = group(
space.maybe(),
oneOf(Range, DateTime),
space.maybe()
).parseAs(RootNode)
export function parse(input: string) {
return base.parse(input, { grammar: Root })
}
export function tellMeWhen(when: string, options?: { now?: Date }) {
return base.tellMeWhen(when, { ...options, grammar: Root })
}