diff --git a/src/locales/en/constants.ts b/src/locales/en/constants.ts index 89406620..44a852bd 100644 --- a/src/locales/en/constants.ts +++ b/src/locales/en/constants.ts @@ -134,6 +134,25 @@ export const ORDINAL_WORD_DICTIONARY: { [word: string]: number } = { "thirty-first": 31, }; +export const TIME_UNIT_DICTIONARY_NO_ABBR: { [word: string]: OpUnitType | QUnitType } = { + second: "second", + seconds: "second", + minute: "minute", + minutes: "minute", + hour: "hour", + hours: "hour", + day: "d", + days: "d", + week: "week", + weeks: "week", + month: "month", + months: "month", + quarter: "quarter", + quarters: "quarter", + year: "year", + years: "year", +}; + export const TIME_UNIT_DICTIONARY: { [word: string]: OpUnitType | QUnitType } = { s: "second", sec: "second", @@ -167,6 +186,9 @@ export const TIME_UNIT_DICTIONARY: { [word: string]: OpUnitType | QUnitType } = yr: "year", year: "year", years: "year", + // Also, merge the entries from the full-name dictionary. + // We leave the duplicated entries for readability. + ...TIME_UNIT_DICTIONARY_NO_ABBR, }; //----------------------------- @@ -238,7 +260,15 @@ export function parseYear(match: string): number { const SINGLE_TIME_UNIT_PATTERN = `(${NUMBER_PATTERN})\\s{0,3}(${matchAnyPattern(TIME_UNIT_DICTIONARY)})`; const SINGLE_TIME_UNIT_REGEX = new RegExp(SINGLE_TIME_UNIT_PATTERN, "i"); +const SINGLE_TIME_UNIT_NO_ABBR_PATTERN = `(${NUMBER_PATTERN})\\s{0,3}(${matchAnyPattern( + TIME_UNIT_DICTIONARY_NO_ABBR +)})`; + export const TIME_UNITS_PATTERN = repeatedTimeunitPattern(`(?:(?:about|around)\\s{0,3})?`, SINGLE_TIME_UNIT_PATTERN); +export const TIME_UNITS_NO_ABBR_PATTERN = repeatedTimeunitPattern( + `(?:(?:about|around)\\s{0,3})?`, + SINGLE_TIME_UNIT_NO_ABBR_PATTERN +); export function parseTimeUnits(timeunitText): TimeUnits { const fragments = {}; diff --git a/src/locales/en/index.ts b/src/locales/en/index.ts index af1fb77f..a9e9c78e 100644 --- a/src/locales/en/index.ts +++ b/src/locales/en/index.ts @@ -82,7 +82,7 @@ export function createConfiguration(strictMode = true, littleEndian = false): Co { parsers: [ new SlashDateFormatParser(littleEndian), - new ENTimeUnitWithinFormatParser(), + new ENTimeUnitWithinFormatParser(strictMode), new ENMonthNameLittleEndianParser(), new ENMonthNameMiddleEndianParser(), new ENWeekdayParser(), diff --git a/src/locales/en/parsers/ENTimeUnitAgoFormatParser.ts b/src/locales/en/parsers/ENTimeUnitAgoFormatParser.ts index b5e4b258..a00d40cc 100644 --- a/src/locales/en/parsers/ENTimeUnitAgoFormatParser.ts +++ b/src/locales/en/parsers/ENTimeUnitAgoFormatParser.ts @@ -1,11 +1,11 @@ import { ParsingContext } from "../../../chrono"; -import { parseTimeUnits, TIME_UNITS_PATTERN } from "../constants"; +import { parseTimeUnits, TIME_UNITS_NO_ABBR_PATTERN, 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})\\s{0,5}(?:ago|before|earlier)(?=(?:\\W|$))`, "i"); -const STRICT_PATTERN = new RegExp(`(${TIME_UNITS_PATTERN})\\s{0,5}ago(?=(?:\\W|$))`, "i"); +const PATTERN = new RegExp(`(${TIME_UNITS_PATTERN})\\s{0,5}(?:ago|before|earlier)(?=\\W|$)`, "i"); +const STRICT_PATTERN = new RegExp(`(${TIME_UNITS_NO_ABBR_PATTERN})\\s{0,5}(?:ago|before|earlier)(?=\\W|$)`, "i"); export default class ENTimeUnitAgoFormatParser extends AbstractParserWithWordBoundaryChecking { constructor(private strictMode: boolean) { diff --git a/src/locales/en/parsers/ENTimeUnitCasualRelativeFormatParser.ts b/src/locales/en/parsers/ENTimeUnitCasualRelativeFormatParser.ts index 7f456092..505e2596 100644 --- a/src/locales/en/parsers/ENTimeUnitCasualRelativeFormatParser.ts +++ b/src/locales/en/parsers/ENTimeUnitCasualRelativeFormatParser.ts @@ -1,14 +1,22 @@ -import { TIME_UNITS_PATTERN, parseTimeUnits } from "../constants"; +import { TIME_UNITS_PATTERN, parseTimeUnits, TIME_UNITS_NO_ABBR_PATTERN } from "../constants"; import { ParsingContext } from "../../../chrono"; import { ParsingComponents } from "../../../results"; import { AbstractParserWithWordBoundaryChecking } from "../../../common/parsers/AbstractParserWithWordBoundary"; import { reverseTimeUnits } from "../../../utils/timeunits"; const PATTERN = new RegExp(`(this|last|past|next|after|\\+|-)\\s*(${TIME_UNITS_PATTERN})(?=\\W|$)`, "i"); +const PATTERN_NO_ABBR = new RegExp( + `(this|last|past|next|after|\\+|-)\\s*(${TIME_UNITS_NO_ABBR_PATTERN})(?=\\W|$)`, + "i" +); export default class ENTimeUnitCasualRelativeFormatParser extends AbstractParserWithWordBoundaryChecking { + constructor(private allowAbbreviations: boolean = true) { + super(); + } + innerPattern(): RegExp { - return PATTERN; + return this.allowAbbreviations ? PATTERN : PATTERN_NO_ABBR; } innerExtract(context: ParsingContext, match: RegExpMatchArray): ParsingComponents { diff --git a/src/locales/en/parsers/ENTimeUnitLaterFormatParser.ts b/src/locales/en/parsers/ENTimeUnitLaterFormatParser.ts index d9e172b4..69fe4942 100644 --- a/src/locales/en/parsers/ENTimeUnitLaterFormatParser.ts +++ b/src/locales/en/parsers/ENTimeUnitLaterFormatParser.ts @@ -1,5 +1,5 @@ import { ParsingContext } from "../../../chrono"; -import { parseTimeUnits, TIME_UNITS_PATTERN } from "../constants"; +import { parseTimeUnits, TIME_UNITS_NO_ABBR_PATTERN, TIME_UNITS_PATTERN } from "../constants"; import { ParsingComponents } from "../../../results"; import { AbstractParserWithWordBoundaryChecking } from "../../../common/parsers/AbstractParserWithWordBoundary"; @@ -8,7 +8,10 @@ const PATTERN = new RegExp( "i" ); -const STRICT_PATTERN = new RegExp("" + "(" + TIME_UNITS_PATTERN + ")" + "(later|from now)" + "(?=(?:\\W|$))", "i"); +const STRICT_PATTERN = new RegExp( + "" + "(" + TIME_UNITS_NO_ABBR_PATTERN + ")" + "(later|from now)" + "(?=(?:\\W|$))", + "i" +); const GROUP_NUM_TIMEUNITS = 1; export default class ENTimeUnitLaterFormatParser extends AbstractParserWithWordBoundaryChecking { diff --git a/src/locales/en/parsers/ENTimeUnitWithinFormatParser.ts b/src/locales/en/parsers/ENTimeUnitWithinFormatParser.ts index 30e313c4..3f7388ea 100644 --- a/src/locales/en/parsers/ENTimeUnitWithinFormatParser.ts +++ b/src/locales/en/parsers/ENTimeUnitWithinFormatParser.ts @@ -1,21 +1,34 @@ -import { TIME_UNITS_PATTERN, parseTimeUnits } from "../constants"; +import { TIME_UNITS_PATTERN, parseTimeUnits, TIME_UNITS_NO_ABBR_PATTERN } from "../constants"; import { ParsingContext } from "../../../chrono"; import { ParsingComponents } from "../../../results"; import { AbstractParserWithWordBoundaryChecking } from "../../../common/parsers/AbstractParserWithWordBoundary"; +const PATTERN_WITHOUT_PREFIX = new RegExp( + `(?:(?:about|around|roughly|approximately|just)\\s*(?:~\\s*)?)?(${TIME_UNITS_PATTERN})(?=\\W|$)`, + "i" +); + const PATTERN_WITH_PREFIX = new RegExp( `(?:within|in|for)\\s*` + `(?:(?:about|around|roughly|approximately|just)\\s*(?:~\\s*)?)?(${TIME_UNITS_PATTERN})(?=\\W|$)`, "i" ); -const PATTERN_WITHOUT_PREFIX = new RegExp( - `(?:(?:about|around|roughly|approximately|just)\\s*(?:~\\s*)?)?(${TIME_UNITS_PATTERN})(?=\\W|$)`, +const PATTERN_WITH_PREFIX_STRICT = new RegExp( + `(?:within|in|for)\\s*` + + `(?:(?:about|around|roughly|approximately|just)\\s*(?:~\\s*)?)?(${TIME_UNITS_NO_ABBR_PATTERN})(?=\\W|$)`, "i" ); export default class ENTimeUnitWithinFormatParser extends AbstractParserWithWordBoundaryChecking { + constructor(private strictMode: boolean) { + super(); + } + innerPattern(context: ParsingContext): RegExp { + if (this.strictMode) { + return PATTERN_WITH_PREFIX_STRICT; + } return context.option.forwardDate ? PATTERN_WITHOUT_PREFIX : PATTERN_WITH_PREFIX; } diff --git a/test/en/en_time_units_ago.test.ts b/test/en/en_time_units_ago.test.ts index 5787233d..889c1d0c 100644 --- a/test/en/en_time_units_ago.test.ts +++ b/test/en/en_time_units_ago.test.ts @@ -300,6 +300,18 @@ test("Test - Before with reference", () => { }); }); +test("Test - Strict mode", function () { + testSingleCase(chrono.strict, "5 minutes ago", new Date(2012, 7, 10, 12, 14), (result, text) => { + expect(result.start.get("hour")).toBe(12); + expect(result.start.get("minute")).toBe(9); + expect(result.start).toBeDate(new Date(2012, 7, 10, 12, 9)); + }); + + testUnexpectedResult(chrono.strict, "5m ago", new Date(2012, 7, 10, 12, 14)); + testUnexpectedResult(chrono.strict, "5hr before", new Date(2012, 7, 10, 12, 14)); + testUnexpectedResult(chrono.strict, "5 h ago", new Date(2012, 7, 10, 12, 14)); +}); + test("Test - Negative cases", function () { testUnexpectedResult(chrono, "15 hours 29 min"); testUnexpectedResult(chrono, "a few hour"); diff --git a/test/en/en_time_units_casual_relative.test.ts b/test/en/en_time_units_casual_relative.test.ts index 347829cf..59107743 100644 --- a/test/en/en_time_units_casual_relative.test.ts +++ b/test/en/en_time_units_casual_relative.test.ts @@ -1,5 +1,6 @@ import * as chrono from "../../src"; import { testSingleCase, testUnexpectedResult } from "../test_util"; +import ENTimeUnitCasualRelativeFormatParser from "../../src/locales/en/parsers/ENTimeUnitCasualRelativeFormatParser"; test("Test - Positive time units", () => { testSingleCase(chrono, "next 2 weeks", new Date(2016, 10 - 1, 1, 12), (result, text) => { @@ -141,6 +142,23 @@ test("Test - Minus '-' sign", () => { }); }); +test("Test - Without custom parser without abbreviations", function () { + const custom = chrono.en.strict.clone(); + custom.parsers.push(new ENTimeUnitCasualRelativeFormatParser(false)); + + testUnexpectedResult(custom, "-3y"); + testUnexpectedResult(custom, "last 2m"); + + testSingleCase(custom, "-2 hours 5 minutes", new Date(2016, 10 - 1, 1, 12), (result, text) => { + expect(result.text).toBe(text); + expect(result.start.get("year")).toBe(2016); + expect(result.start.get("month")).toBe(10); + expect(result.start.get("day")).toBe(1); + expect(result.start.get("hour")).toBe(9); + expect(result.start.get("minute")).toBe(55); + }); +}); + test("Test - Negative cases", () => { testUnexpectedResult(chrono.casual, "3y", new Date(2015, 7 - 1, 10, 12, 14)); testUnexpectedResult(chrono.casual, "1 m", new Date(2015, 7 - 1, 10, 12, 14)); diff --git a/test/en/en_time_units_later.test.ts b/test/en/en_time_units_later.test.ts index 15289777..1d20dde5 100644 --- a/test/en/en_time_units_later.test.ts +++ b/test/en/en_time_units_later.test.ts @@ -1,5 +1,5 @@ import * as chrono from "../../src/"; -import { testSingleCase } from "../test_util"; +import { testSingleCase, testUnexpectedResult } from "../test_util"; import { Meridiem } from "../../src/"; test("Test - Later Expression", function () { @@ -262,6 +262,16 @@ test("Test - From now Expression", () => { }); test("Test - Strict mode", function () { + testSingleCase(chrono, "the min after", new Date(2012, 7, 10, 12, 14), (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("the min after"); + expect(result.start.get("hour")).toBe(12); + expect(result.start.get("minute")).toBe(15); + expect(result.start.get("meridiem")).toBe(Meridiem.PM); + + expect(result.start).toBeDate(new Date(2012, 7, 10, 12, 15)); + }); + testSingleCase(chrono.strict, "15 minutes from now", new Date(2012, 7, 10, 12, 14), (result, text) => { expect(result.text).toBe(text); expect(result.start.get("hour")).toBe(12); @@ -279,15 +289,8 @@ test("Test - Strict mode", function () { expect(result.start).toBeDate(new Date(2012, 7, 10, 13, 5)); }); - testSingleCase(chrono, "the min after", new Date(2012, 7, 10, 12, 14), (result) => { - expect(result.index).toBe(0); - expect(result.text).toBe("the min after"); - expect(result.start.get("hour")).toBe(12); - expect(result.start.get("minute")).toBe(15); - expect(result.start.get("meridiem")).toBe(Meridiem.PM); - - expect(result.start).toBeDate(new Date(2012, 7, 10, 12, 15)); - }); + testUnexpectedResult(chrono.strict, "15m from now"); + testUnexpectedResult(chrono.strict, "15s later"); }); test("Test - After with reference", () => { diff --git a/test/en/en_time_units_within.test.ts b/test/en/en_time_units_within.test.ts index 94fe363b..ccad6073 100644 --- a/test/en/en_time_units_within.test.ts +++ b/test/en/en_time_units_within.test.ts @@ -1,5 +1,6 @@ import * as chrono from "../../src"; -import { testSingleCase } from "../test_util"; +import { testSingleCase, testUnexpectedResult } from "../test_util"; +import { Meridiem } from "../../src"; test("Test - The normal within expression", () => { testSingleCase(chrono, "we have to make something in 5 days.", new Date(2012, 7, 10), (result) => { @@ -327,3 +328,13 @@ test("Test - Time units' certainty", () => { expect(result.start.isCertain("minute")).toBeFalsy(); }); }); + +test("Test - Strict mode", function () { + testSingleCase(chrono, "in 2hour", new Date(2016, 10 - 1, 1, 14, 52), (result, text) => { + expect(result.start.get("hour")).toBe(16); + expect(result.start.get("minute")).toBe(52); + }); + + testUnexpectedResult(chrono.strict, "in 15m"); + testUnexpectedResult(chrono.strict, "within 5hr"); +}); diff --git a/test/system.test.ts b/test/system.test.ts index 1f14f882..c186d58f 100644 --- a/test/system.test.ts +++ b/test/system.test.ts @@ -1,8 +1,10 @@ import * as chrono from "../src/"; -import { testSingleCase } from "./test_util"; +import { testSingleCase, testUnexpectedResult } from "./test_util"; import { Meridiem } from "../src"; import UnlikelyFormatFilter from "../src/common/refiners/UnlikelyFormatFilter"; import SlashDateFormatParser from "../src/common/parsers/SlashDateFormatParser"; +import ENWeekdayParser from "../src/locales/en/parsers/ENWeekdayParser"; +import ENTimeUnitCasualRelativeFormatParser from "../src/locales/en/parsers/ENTimeUnitCasualRelativeFormatParser"; //------------------------------------- @@ -146,6 +148,26 @@ test("Test - Remove a refiner example", () => { }); }); +test("Test - Replace a parser example", () => { + const custom = chrono.en.casual.clone(); + testSingleCase(custom, "next 5m", new Date(2016, 10 - 1, 1, 14, 52), (result, text) => { + expect(result.start.get("hour")).toBe(14); + expect(result.start.get("minute")).toBe(57); + }); + testSingleCase(custom, "next 5 minutes", new Date(2016, 10 - 1, 1, 14, 52), (result, text) => { + expect(result.start.get("hour")).toBe(14); + expect(result.start.get("minute")).toBe(57); + }); + + const index = custom.parsers.findIndex((r) => r instanceof ENTimeUnitCasualRelativeFormatParser); + custom.parsers[index] = new ENTimeUnitCasualRelativeFormatParser(false); + testUnexpectedResult(custom, "next 5m"); + testSingleCase(custom, "next 5 minutes", new Date(2016, 10 - 1, 1, 14, 52), (result, text) => { + expect(result.start.get("hour")).toBe(14); + expect(result.start.get("minute")).toBe(57); + }); +}); + test("Test - Compare with native js", () => { const testByCompareWithNative = (text) => { const expectedDate = new Date(text);