diff --git a/README.md b/README.md index d2db4267..436b0ce6 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,22 @@ chrono.parseDate('Friday', referenceDate, { forwardDate: true }); // Fri Aug 31 2012 12:00:00 GMT+0900 (JST) -- The following Friday ``` +`useShorts` (boolean) parses short-hands (currently only `en` locale) such as s, m, d, etc. + +```javascript +const referenceDate = new Date(2012, 7, 25); +// Sat Aug 25 2012 00:00:00 GMT+0200 (CEST) + +chrono.parseDate('in 25 minutes', referenceDate); +// Sat Aug 25 2012 00:25:00 GMT+0200 (CEST) -- adds 25 minutes + +chrono.parseDate('in 25m', referenceDate); +// null -- does not understand 25m + +chrono.parseDate('in 25m', referenceDate, { useShorts: true }); +// Sat Aug 25 2012 00:25:00 GMT+0200 (CEST) -- allows 25m to parse as 25 minutes +``` + ### Parsed Results and Components #### ParsedResult diff --git a/src/index.ts b/src/index.ts index 979574a9..deae7138 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,7 @@ export function parseDate(text: string, ref?: Date, option?: ParsingOption): Dat export interface ParsingOption { forwardDate?: boolean; + useShorts?: boolean; debug?: DebugHandler | DebugConsume; timezones?: { string: number }; } diff --git a/src/locales/en/constants.ts b/src/locales/en/constants.ts index 261f54ac..472d1966 100644 --- a/src/locales/en/constants.ts +++ b/src/locales/en/constants.ts @@ -2,6 +2,7 @@ import { OpUnitType } from "dayjs"; import { matchAnyPattern } from "../../utils/pattern"; import { findMostLikelyADYear } from "../../calculation/years"; import { TimeUnits } from "../../utils/timeunits"; +import { DictionaryLike } from "../../utils/pattern"; export const WEEKDAY_DICTIONARY: { [word: string]: number } = { sunday: 0, @@ -158,6 +159,17 @@ export const TIME_UNIT_DICTIONARY: { [word: string]: OpUnitType } = { years: "year", }; +export const SHORT_TIME_UNIT_DICTIONARY: { [letter:string]: OpUnitType } = { + ms: "millisecond", + s: "second", + m: "minute", + d: "d", + w: "week", + wk: "week", +} + +export const FULL_TIME_UNIT_DICTIONARY = { ...TIME_UNIT_DICTIONARY, ...SHORT_TIME_UNIT_DICTIONARY } + //----------------------------- export const NUMBER_PATTERN = `(?:${matchAnyPattern( @@ -223,29 +235,48 @@ export function parseYear(match: string): number { //----------------------------- -const SINGLE_TIME_UNIT_PATTERN = `(${NUMBER_PATTERN})\\s*(${matchAnyPattern(TIME_UNIT_DICTIONARY)})\\s*`; +const SINGLE_TIME_PATTERN = matchAnyPattern(TIME_UNIT_DICTIONARY); +const SINGLE_TIME_UNIT_PATTERN = createUnitPattern(SINGLE_TIME_PATTERN); const SINGLE_TIME_UNIT_REGEX = new RegExp(SINGLE_TIME_UNIT_PATTERN, "i"); - const SINGLE_TIME_UNIT_PATTERN_NO_CAPTURE = SINGLE_TIME_UNIT_PATTERN.replace(/\((?!\?)/g, "(?:"); + +const FULL_UNIT_PATTERN = matchAnyPattern(FULL_TIME_UNIT_DICTIONARY); +const FULL_TIME_UNIT_PATTERN = createUnitPattern(FULL_UNIT_PATTERN); +const FULL_TIME_UNIT_REGEX = new RegExp(FULL_TIME_UNIT_PATTERN, "i"); +const FULL_TIME_UNIT_PATTERN_NO_CAPTURE = FULL_TIME_UNIT_PATTERN.replace(/\((?!\?)/g, "(?:"); + + + export const TIME_UNITS_PATTERN = `(?:(?:about|around)\\s*)?` + `${SINGLE_TIME_UNIT_PATTERN_NO_CAPTURE}\\s*(?:,?\\s*${SINGLE_TIME_UNIT_PATTERN_NO_CAPTURE})*`; -export function parseTimeUnits(timeunitText): TimeUnits { +export const FULL_TIME_UNITS_PATTERN = + `(?:(?:about|around)\\s*)?` + + `${FULL_TIME_UNIT_PATTERN_NO_CAPTURE}\\s*(?:,?\\s*${FULL_TIME_UNIT_PATTERN_NO_CAPTURE})*`; + +export function parseTimeUnits(timeunitText, useShorts = false): TimeUnits { const fragments = {}; let remainingText = timeunitText; - let match = SINGLE_TIME_UNIT_REGEX.exec(remainingText); + const REGEX = useShorts ? FULL_TIME_UNIT_REGEX : SINGLE_TIME_UNIT_REGEX; + const DICT = useShorts ? FULL_TIME_UNIT_DICTIONARY : TIME_UNIT_DICTIONARY; + + let match = REGEX.exec(remainingText); while (match) { - collectDateTimeFragment(fragments, match); + collectDateTimeFragment(fragments, match, DICT); remainingText = remainingText.substring(match[0].length); - match = SINGLE_TIME_UNIT_REGEX.exec(remainingText); + match = REGEX.exec(remainingText); } return fragments; } -function collectDateTimeFragment(fragments, match) { +function collectDateTimeFragment(fragments, match, DICT: DictionaryLike) { const num = parseNumberPattern(match[1]); - const unit = TIME_UNIT_DICTIONARY[match[2].toLowerCase()]; + const unit = DICT[match[2].toLowerCase()]; fragments[unit] = num; } + +function createUnitPattern(pattern: string): string { + return `(${NUMBER_PATTERN})\\s*(${pattern})\\s*`; +} \ No newline at end of file diff --git a/src/locales/en/parsers/ENTimeUnitAgoFormatParser.ts b/src/locales/en/parsers/ENTimeUnitAgoFormatParser.ts index e9204ca7..e974feba 100644 --- a/src/locales/en/parsers/ENTimeUnitAgoFormatParser.ts +++ b/src/locales/en/parsers/ENTimeUnitAgoFormatParser.ts @@ -1,10 +1,15 @@ import { ParsingContext } from "../../../chrono"; -import { parseTimeUnits, TIME_UNITS_PATTERN } from "../constants"; +import { FULL_TIME_UNITS_PATTERN, parseTimeUnits, TIME_UNITS_PATTERN } from "../constants"; import { ParsingComponents } from "../../../results"; import { AbstractParserWithWordBoundaryChecking } from "../../../common/parsers/AbstractParserWithWordBoundary"; import { reverseTimeUnits } from "../../../utils/timeunits"; -const PATTERN = new RegExp("" + "(" + TIME_UNITS_PATTERN + ")" + "(?:ago|before|earlier)(?=(?:\\W|$))", "i"); +function createRegExp(pattern: string): RegExp { + return new RegExp("" + "(" + pattern + ")" + "(?:ago|before|earlier)(?=(?:\\W|$))", "i"); +} + +const PATTERN = createRegExp(TIME_UNITS_PATTERN); +const FULL_PATTERN = createRegExp(FULL_TIME_UNITS_PATTERN); const STRICT_PATTERN = new RegExp("" + "(" + TIME_UNITS_PATTERN + ")" + "ago(?=(?:\\W|$))", "i"); @@ -13,12 +18,13 @@ export default class ENTimeUnitAgoFormatParser extends AbstractParserWithWordBou super(); } - innerPattern(): RegExp { - return this.strictMode ? STRICT_PATTERN : PATTERN; + innerPattern(context: ParsingContext): RegExp { + return this.strictMode ? STRICT_PATTERN : + context.option.useShorts ? FULL_PATTERN : PATTERN; } innerExtract(context: ParsingContext, match: RegExpMatchArray) { - const timeUnits = parseTimeUnits(match[1]); + const timeUnits = parseTimeUnits(match[1], context.option.useShorts); const outputTimeUnits = reverseTimeUnits(timeUnits); return ParsingComponents.createRelativeFromRefDate(context.refDate, outputTimeUnits); diff --git a/src/locales/en/parsers/ENTimeUnitCasualRelativeFormatParser.ts b/src/locales/en/parsers/ENTimeUnitCasualRelativeFormatParser.ts index a0ed9598..eb96c709 100644 --- a/src/locales/en/parsers/ENTimeUnitCasualRelativeFormatParser.ts +++ b/src/locales/en/parsers/ENTimeUnitCasualRelativeFormatParser.ts @@ -1,17 +1,24 @@ -import { TIME_UNITS_PATTERN, parseTimeUnits } from "../constants"; +import { TIME_UNITS_PATTERN, parseTimeUnits, FULL_TIME_UNITS_PATTERN } from "../constants"; import { ParsingContext } from "../../../chrono"; import { ParsingComponents } from "../../../results"; import { AbstractParserWithWordBoundaryChecking } from "../../../common/parsers/AbstractParserWithWordBoundary"; import { reverseTimeUnits } from "../../../utils/timeunits"; +const PATTERN = createRegExp(TIME_UNITS_PATTERN); +const FULL_PATTERN = createRegExp(FULL_TIME_UNITS_PATTERN); + +function createRegExp(pattern: string): RegExp { + return new RegExp(`(this|last|past|next|\\+|-)\\s*(${pattern})(?=\\W|$)`, "i"); +} + export default class ENTimeUnitCasualRelativeFormatParser extends AbstractParserWithWordBoundaryChecking { - innerPattern(): RegExp { - return new RegExp(`(this|last|past|next|\\+|-)\\s*(${TIME_UNITS_PATTERN})(?=\\W|$)`, "i"); + innerPattern(context: ParsingContext): RegExp { + return context.option.useShorts ? FULL_PATTERN : PATTERN; } innerExtract(context: ParsingContext, match: RegExpMatchArray): ParsingComponents { const prefix = match[1].toLowerCase(); - let timeUnits = parseTimeUnits(match[2]); + let timeUnits = parseTimeUnits(match[2], context.option.useShorts); switch (prefix) { case "last": case "past": diff --git a/src/locales/en/parsers/ENTimeUnitLaterFormatParser.ts b/src/locales/en/parsers/ENTimeUnitLaterFormatParser.ts index 47e0aab6..ff319091 100644 --- a/src/locales/en/parsers/ENTimeUnitLaterFormatParser.ts +++ b/src/locales/en/parsers/ENTimeUnitLaterFormatParser.ts @@ -1,12 +1,17 @@ import { ParsingContext } from "../../../chrono"; -import { parseTimeUnits, TIME_UNITS_PATTERN } from "../constants"; +import { FULL_TIME_UNITS_PATTERN, parseTimeUnits, TIME_UNITS_PATTERN } from "../constants"; import { ParsingComponents } from "../../../results"; import { AbstractParserWithWordBoundaryChecking } from "../../../common/parsers/AbstractParserWithWordBoundary"; -const PATTERN = new RegExp( - "" + "(" + TIME_UNITS_PATTERN + ")" + "(later|after|from now|henceforth|forward|out)" + "(?=(?:\\W|$))", - "i" -); +const PATTERN = createRegExp(TIME_UNITS_PATTERN); +const FULL_PATTERN = createRegExp(FULL_TIME_UNITS_PATTERN); + +function createRegExp(pattern: string) { + return new RegExp( + "" + "(" + pattern + ")" + "(later|after|from now|henceforth|forward|out)" + "(?=(?:\\W|$))", + "i" + ); +} const STRICT_PATTERN = new RegExp("" + "(" + TIME_UNITS_PATTERN + ")" + "(later|from now)" + "(?=(?:\\W|$))", "i"); const GROUP_NUM_TIMEUNITS = 1; @@ -16,12 +21,13 @@ export default class ENTimeUnitLaterFormatParser extends AbstractParserWithWordB super(); } - innerPattern(): RegExp { - return this.strictMode ? STRICT_PATTERN : PATTERN; + innerPattern(context: ParsingContext): RegExp { + return this.strictMode ? STRICT_PATTERN : + context.option.useShorts ? FULL_PATTERN : PATTERN; } innerExtract(context: ParsingContext, match: RegExpMatchArray) { - const fragments = parseTimeUnits(match[GROUP_NUM_TIMEUNITS]); + const fragments = parseTimeUnits(match[GROUP_NUM_TIMEUNITS], context.option.useShorts); return ParsingComponents.createRelativeFromRefDate(context.refDate, fragments); } } diff --git a/src/locales/en/parsers/ENTimeUnitWithinFormatParser.ts b/src/locales/en/parsers/ENTimeUnitWithinFormatParser.ts index 12745b53..694c805c 100644 --- a/src/locales/en/parsers/ENTimeUnitWithinFormatParser.ts +++ b/src/locales/en/parsers/ENTimeUnitWithinFormatParser.ts @@ -1,22 +1,29 @@ -import { TIME_UNITS_PATTERN, parseTimeUnits } from "../constants"; +import { TIME_UNITS_PATTERN, parseTimeUnits, FULL_TIME_UNITS_PATTERN } from "../constants"; import { ParsingContext } from "../../../chrono"; import { ParsingComponents } from "../../../results"; import { AbstractParserWithWordBoundaryChecking } from "../../../common/parsers/AbstractParserWithWordBoundary"; +const PATTERN = createRegExp(TIME_UNITS_PATTERN); +const FULL_PATTERN = createRegExp(FULL_TIME_UNITS_PATTERN); + +function createRegExp(pattern: string): RegExp { + return new RegExp( + `(?:within|in|for)\\s*(?:(?:about|around|roughly|approximately|just)\\s*(?:~\\s*)?)?` + + "(" + + pattern + + ")" + + `(?=\\W|$)`, + "i" + ); +} + export default class ENTimeUnitWithinFormatParser extends AbstractParserWithWordBoundaryChecking { - innerPattern(): RegExp { - return new RegExp( - `(?:within|in|for)\\s*(?:(?:about|around|roughly|approximately|just)\\s*(?:~\\s*)?)?` + - "(" + - TIME_UNITS_PATTERN + - ")" + - `(?=\\W|$)`, - "i" - ); + innerPattern(context: ParsingContext): RegExp { + return context.option.useShorts ? FULL_PATTERN : PATTERN; } innerExtract(context: ParsingContext, match: RegExpMatchArray): ParsingComponents { - const timeUnits = parseTimeUnits(match[1]); + const timeUnits = parseTimeUnits(match[1], context.option.useShorts); return ParsingComponents.createRelativeFromRefDate(context.refDate, timeUnits); } } diff --git a/src/locales/nl/parsers/NLTimeUnitWithinFormatParser.ts b/src/locales/nl/parsers/NLTimeUnitWithinFormatParser.ts index babb774c..af9d27e0 100644 --- a/src/locales/nl/parsers/NLTimeUnitWithinFormatParser.ts +++ b/src/locales/nl/parsers/NLTimeUnitWithinFormatParser.ts @@ -1,15 +1,21 @@ -import { TIME_UNITS_PATTERN, parseTimeUnits } from "../../en/constants"; +import { TIME_UNITS_PATTERN, parseTimeUnits, FULL_TIME_UNITS_PATTERN } from "../../en/constants"; import { ParsingContext } from "../../../chrono"; import { ParsingComponents } from "../../../results"; import { AbstractParserWithWordBoundaryChecking } from "../../../common/parsers/AbstractParserWithWordBoundary"; +const PATTERN = createRegExp(TIME_UNITS_PATTERN); +const FULL_PATTERN = createRegExp(FULL_TIME_UNITS_PATTERN); + +function createRegExp(pattern: string): RegExp { + return new RegExp(`(?:binnen|in)\\s*` + "(" + pattern + ")" + `(?=\\W|$)`, "i"); +} export default class ENTimeUnitWithinFormatParser extends AbstractParserWithWordBoundaryChecking { - innerPattern(): RegExp { - return new RegExp(`(?:binnen|in)\\s*` + "(" + TIME_UNITS_PATTERN + ")" + `(?=\\W|$)`, "i"); + innerPattern(context: ParsingContext): RegExp { + return context.option.useShorts ? FULL_PATTERN : PATTERN; } innerExtract(context: ParsingContext, match: RegExpMatchArray): ParsingComponents { - const timeUnits = parseTimeUnits(match[1]); + const timeUnits = parseTimeUnits(match[1], context.option.useShorts); return ParsingComponents.createRelativeFromRefDate(context.refDate, timeUnits); } } diff --git a/src/utils/pattern.ts b/src/utils/pattern.ts index e6176092..d3b3dbd4 100644 --- a/src/utils/pattern.ts +++ b/src/utils/pattern.ts @@ -1,4 +1,4 @@ -type DictionaryLike = string[] | { [word: string]: unknown } | Map; +export type DictionaryLike = string[] | { [word: string]: unknown } | Map; export function patternWithWordBreak(regExp: RegExp): RegExp { return RegExp("" + regExp.source);