Skip to content

Commit

Permalink
New: (En) Disallow timeunit abbreviations in strict mode
Browse files Browse the repository at this point in the history
  • Loading branch information
Wanasit Tanakitrungruang committed Apr 9, 2023
1 parent 665bda6 commit 787c41b
Show file tree
Hide file tree
Showing 11 changed files with 143 additions and 23 deletions.
30 changes: 30 additions & 0 deletions src/locales/en/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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,
};

//-----------------------------
Expand Down Expand Up @@ -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 = {};
Expand Down
2 changes: 1 addition & 1 deletion src/locales/en/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
6 changes: 3 additions & 3 deletions src/locales/en/parsers/ENTimeUnitAgoFormatParser.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
12 changes: 10 additions & 2 deletions src/locales/en/parsers/ENTimeUnitCasualRelativeFormatParser.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
7 changes: 5 additions & 2 deletions src/locales/en/parsers/ENTimeUnitLaterFormatParser.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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 {
Expand Down
19 changes: 16 additions & 3 deletions src/locales/en/parsers/ENTimeUnitWithinFormatParser.ts
Original file line number Diff line number Diff line change
@@ -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;
}

Expand Down
12 changes: 12 additions & 0 deletions test/en/en_time_units_ago.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
18 changes: 18 additions & 0 deletions test/en/en_time_units_casual_relative.test.ts
Original file line number Diff line number Diff line change
@@ -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) => {
Expand Down Expand Up @@ -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));
Expand Down
23 changes: 13 additions & 10 deletions test/en/en_time_units_later.test.ts
Original file line number Diff line number Diff line change
@@ -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 () {
Expand Down Expand Up @@ -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);
Expand All @@ -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", () => {
Expand Down
13 changes: 12 additions & 1 deletion test/en/en_time_units_within.test.ts
Original file line number Diff line number Diff line change
@@ -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) => {
Expand Down Expand Up @@ -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");
});
24 changes: 23 additions & 1 deletion test/system.test.ts
Original file line number Diff line number Diff line change
@@ -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";

//-------------------------------------

Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 787c41b

Please sign in to comment.