Skip to content

Commit

Permalink
Fix: Hyphanated (or slash) dates started by year intepretation
Browse files Browse the repository at this point in the history
  • Loading branch information
Wanasit Tanakitrungruang committed Aug 10, 2024
1 parent 23abdda commit d61f933
Show file tree
Hide file tree
Showing 6 changed files with 64 additions and 30 deletions.
23 changes: 13 additions & 10 deletions src/common/parsers/SlashDateFormatParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,22 @@ export default class SlashDateFormatParser implements Parser {
extract(context: ParsingContext, match: RegExpMatchArray): ParsingResult {
// Because of how pattern is executed on remaining text in `chrono.ts`, the character before the match could
// still be a number (e.g. X[X/YY/ZZ] or XX[/YY/ZZ] or [XX/YY/]ZZ). We want to check and skip them.
if (match[OPENING_GROUP].length == 0 && match.index > 0 && match.index < context.text.length) {
const previousChar = context.text[match.index - 1];
if (previousChar >= "0" && previousChar <= "9") {
const index = match.index + match[OPENING_GROUP].length;
const indexEnd = match.index + match[0].length - match[ENDING_GROUP].length;
if (index > 0) {
const textBefore = context.text.substring(0, index);
if (textBefore.match("\\d/?$")) {
return;
}
}
if (indexEnd < context.text.length) {
const textAfter = context.text.substring(indexEnd);
if (textAfter.match("^/?\\d")) {
return;
}
}

const index = match.index + match[OPENING_GROUP].length;
const text = match[0].substr(
match[OPENING_GROUP].length,
match[0].length - match[OPENING_GROUP].length - match[ENDING_GROUP].length
);
const text = context.text.substring(index, indexEnd);

// '1.12', '1.12.12' is more like a version numbers
if (text.match(/^\d\.\d$/) || text.match(/^\d\.\d{1,2}\.\d{1,2}\s*$/)) {
Expand All @@ -61,14 +65,13 @@ export default class SlashDateFormatParser implements Parser {

// MM/dd -> OK
// MM.dd -> NG
if (!match[YEAR_GROUP] && match[0].indexOf("/") < 0) {
if (!match[YEAR_GROUP] && text.indexOf("/") < 0) {
return;
}

const result = context.createParsingResult(index, text);
let month = parseInt(match[this.groupNumberMonth]);
let day = parseInt(match[this.groupNumberDay]);

if (month < 1 || month > 12) {
if (month > 12) {
if (day >= 1 && day <= 12 && month <= 31) {
Expand Down
5 changes: 3 additions & 2 deletions src/locales/en/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import ENTimeUnitWithinFormatParser from "./parsers/ENTimeUnitWithinFormatParser
import ENMonthNameLittleEndianParser from "./parsers/ENMonthNameLittleEndianParser";
import ENMonthNameMiddleEndianParser from "./parsers/ENMonthNameMiddleEndianParser";
import ENMonthNameParser from "./parsers/ENMonthNameParser";
import ENCasualYearMonthDayParser from "./parsers/ENCasualYearMonthDayParser";
import ENYearMonthDayParser from "./parsers/ENYearMonthDayParser";
import ENSlashMonthFormatParser from "./parsers/ENSlashMonthFormatParser";
import ENTimeExpressionParser from "./parsers/ENTimeExpressionParser";
import ENTimeUnitAgoFormatParser from "./parsers/ENTimeUnitAgoFormatParser";
Expand Down Expand Up @@ -55,7 +55,6 @@ export default class ENDefaultConfiguration {
new ENMonthNameLittleEndianParser(),
new ENMonthNameMiddleEndianParser(/*shouldSkipYearLikeDate=*/ littleEndian),
new ENWeekdayParser(),
new ENCasualYearMonthDayParser(),
new ENSlashMonthFormatParser(),
new ENTimeExpressionParser(strictMode),
new ENTimeUnitAgoFormatParser(strictMode),
Expand All @@ -65,6 +64,8 @@ export default class ENDefaultConfiguration {
},
strictMode
);
options.parsers.unshift(new ENYearMonthDayParser(/*strictMonthDateOrder=*/ strictMode));

// These relative-dates consideration should be done before other common refiners.
options.refiners.unshift(new ENMergeRelativeFollowByDateRefiner());
options.refiners.unshift(new ENMergeRelativeAfterDateRefiner());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import { AbstractParserWithWordBoundaryChecking } from "../../../common/parsers/
- YYYY.MM.DD
*/
const PATTERN = new RegExp(
`([0-9]{4})[\\.\\/\\s]` +
`(?:(${matchAnyPattern(MONTH_DICTIONARY)})|([0-9]{1,2}))[\\.\\/\\s]` +
`([0-9]{4})[-\\.\\/\\s]` +
`(?:(${matchAnyPattern(MONTH_DICTIONARY)})|([0-9]{1,2}))[-\\.\\/\\s]` +
`([0-9]{1,2})` +
"(?=\\W|$)",
"i"
Expand All @@ -23,23 +23,34 @@ const MONTH_NAME_GROUP = 2;
const MONTH_NUMBER_GROUP = 3;
const DATE_NUMBER_GROUP = 4;

export default class ENCasualYearMonthDayParser extends AbstractParserWithWordBoundaryChecking {
export default class ENYearMonthDayParser extends AbstractParserWithWordBoundaryChecking {
constructor(private strictMonthDateOrder: boolean) {
super();
}

innerPattern(): RegExp {
return PATTERN;
}

innerExtract(context: ParsingContext, match: RegExpMatchArray) {
const month = match[MONTH_NUMBER_GROUP]
const year = parseInt(match[YEAR_NUMBER_GROUP]);
let day = parseInt(match[DATE_NUMBER_GROUP]);
let month = match[MONTH_NUMBER_GROUP]
? parseInt(match[MONTH_NUMBER_GROUP])
: MONTH_DICTIONARY[match[MONTH_NAME_GROUP].toLowerCase()];

if (month < 1 || month > 12) {
if (this.strictMonthDateOrder) {
return null;
}
if (day >= 1 && day <= 12) {
[month, day] = [day, month];
}
}
if (day < 1 || day > 31) {
return null;
}

const year = parseInt(match[YEAR_NUMBER_GROUP]);
const day = parseInt(match[DATE_NUMBER_GROUP]);

return {
day: day,
month: month,
Expand Down
3 changes: 0 additions & 3 deletions src/locales/en/refiners/ENExtractYearSuffixRefiner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ const YEAR_GROUP = 1;
export default class ENExtractYearSuffixRefiner implements Refiner {
refine(context: ParsingContext, results: ParsingResult[]): ParsingResult[] {
results.forEach(function (result) {
context.debug(() => {
console.log("ENExtractYearSuffixRefiner", result.text, result.start);
});
if (!result.start.isDateWithUnknownYear()) {
return;
}
Expand Down
9 changes: 5 additions & 4 deletions test/en/en_slash.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ test("Test - Parsing Offset Expression", function () {
});
});

test("Test - Single Expression", function () {
test("Test - Single Expression (MM/dd/yyyy)", function () {
testSingleCase(chrono, "8/10/2012", new Date(2012, 7, 10), (result) => {
expect(result.start).not.toBeNull();
expect(result.start.get("year")).toBe(2012);
Expand Down Expand Up @@ -63,7 +63,7 @@ test("Test - Single Expression", function () {
});
});

test("Test - Single Expression Little-Endian", function () {
test("Test - Single Expression Little-Endian (dd/MM/yyyy)", function () {
testSingleCase(chrono.en.GB, "8/10/2012", new Date(2012, 7, 10), (result) => {
expect(result.start).not.toBeNull();
expect(result.start.get("year")).toBe(2012);
Expand Down Expand Up @@ -113,7 +113,7 @@ test("Test - Single Expression Little-Endian with Month name", function () {
});
});

test("Test - Single Expression Shorten (month/year)", () => {
test("Test - Single Expression Shorten (mm/yyyy)", () => {
testSingleCase(chrono, "The event is going ahead (04/2016)", new Date(2012, 7, 10), (result) => {
expect(result.start).not.toBeNull();
expect(result.start.get("year")).toBe(2016);
Expand All @@ -139,7 +139,7 @@ test("Test - Single Expression Shorten (month/year)", () => {
});
});

test("Test - Single Expression Shorten (date/month)", () => {
test("Test - Single Expression Shorten (dd/mm)", () => {
testSingleCase(chrono, "8/10", new Date(2012, 7, 10), (result) => {
expect(result.start).not.toBeNull();
expect(result.start.get("year")).toBe(2012);
Expand Down Expand Up @@ -239,6 +239,7 @@ test("Test - Impossible Dates and Unexpected Results", function () {
testUnexpectedResult(chrono, "06/-31/2022");
testUnexpectedResult(chrono, "18/13/2022");
testUnexpectedResult(chrono, "15/28/2022");
testUnexpectedResult(chrono, "4/13/1");
});

test("Test - forward dates only option", function () {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { testSingleCase, testUnexpectedResult } from "../test_util";
import * as chrono from "../../src";

test("Test - Single Expression Start with Year", function () {
test("Test - Single Expression (yyyy/MM/dd)", function () {
testSingleCase(chrono, "2012/8/10", new Date(2012, 7, 10), (result) => {
expect(result.start).not.toBeNull();
expect(result.start.get("year")).toBe(2012);
Expand Down Expand Up @@ -41,7 +41,7 @@ test("Test - Single Expression Start with Year", function () {
});
});

test("Test - Single Expression Start with Year and Month Name", function () {
test("Test - Single Expression with month name (yyyy/MMM/dd)", function () {
testSingleCase(chrono, "2012/Aug/10", new Date(2012, 7, 10), (result) => {
expect(result.start).not.toBeNull();
expect(result.start.get("year")).toBe(2012);
Expand Down Expand Up @@ -73,8 +73,29 @@ test("Test - Single Expression Start with Year and Month Name", function () {
});
});

test("Test - Negative year-month-day like pattern", function () {
testUnexpectedResult(chrono, "2012-80-10", new Date(2012, 7, 10));
test("Test - Allow swap date/month order in casual mode", () => {
testUnexpectedResult(chrono.strict, "2024/13/1");
testUnexpectedResult(chrono.strict, "2024-13-01");

testSingleCase(chrono.casual, "2024/13/1", new Date(2012, 7, 10), (result) => {
expect(result.start.get("year")).toBe(2024);
expect(result.start.get("month")).toBe(1);
expect(result.start.get("day")).toBe(13);
});

testSingleCase(chrono.casual, "2024-13-01", new Date(2012, 7, 10), (result) => {
expect(result.start.get("year")).toBe(2024);
expect(result.start.get("month")).toBe(1);
expect(result.start.get("day")).toBe(13);
});
});

test("Test - Not parse unlikely xxxx-xx-xx pattern", function () {
testUnexpectedResult(chrono, "2012/80/10", new Date(2012, 7, 10));
testUnexpectedResult(chrono, "2012 80 10", new Date(2012, 7, 10));
});

test("Test - Not parse impossible dates", function () {
testUnexpectedResult(chrono, "2014-08-32");
testUnexpectedResult(chrono, "2014-02-30");
});

0 comments on commit d61f933

Please sign in to comment.