diff --git a/README.md b/README.md index 93f934b4..4e15b6d4 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ const chrono = require('chrono-node'); ### What's changed in the v2 For Users * Chrono’s default now handles only international English. While in the previous version, it tried to parse with all known languages. -* The current fully supported languages are `en`, `ja`, `fr`, and `nl` (`de`, `pt`, and `zh.hant` are partially supported). +* The current fully supported languages are `en`, `ja`, `fr`, `nl` and `ru` (`de`, `pt`, and `zh.hant` are partially supported). For contributors and advanced users * The project is rewritten in TypeScript diff --git a/src/common/casualReferences.ts b/src/common/casualReferences.ts index 968384da..1c7d219f 100644 --- a/src/common/casualReferences.ts +++ b/src/common/casualReferences.ts @@ -34,6 +34,15 @@ export function yesterday(reference: ReferenceWithTimezone): ParsingComponents { return component; } +export function theDayBeforeYesterday(reference: ReferenceWithTimezone): ParsingComponents { + let targetDate = dayjs(reference.instant); + const component = new ParsingComponents(reference, {}); + targetDate = targetDate.add(-2, "day"); + assignSimilarDate(component, targetDate); + implySimilarTime(component, targetDate); + return component; +} + /** * The following day with dayjs.assignTheNextDay() */ @@ -44,6 +53,15 @@ export function tomorrow(reference: ReferenceWithTimezone): ParsingComponents { return component; } +export function theDayAfterTomorrow(reference: ReferenceWithTimezone): ParsingComponents { + let targetDate = dayjs(reference.instant); + const component = new ParsingComponents(reference, {}); + targetDate = targetDate.add(2, "day"); + assignSimilarDate(component, targetDate); + implySimilarTime(component, targetDate); + return component; +} + export function tonight(reference: ReferenceWithTimezone, implyHour = 22): ParsingComponents { const targetDate = dayjs(reference.instant); const component = new ParsingComponents(reference, {}); @@ -52,3 +70,53 @@ export function tonight(reference: ReferenceWithTimezone, implyHour = 22): Parsi assignSimilarDate(component, targetDate); return component; } + +export function lastNight(reference: ReferenceWithTimezone, implyHour = 0): ParsingComponents { + let targetDate = dayjs(reference.instant); + const component = new ParsingComponents(reference, {}); + if (targetDate.hour() < 6) { + targetDate = targetDate.add(-1, "day"); + } + assignSimilarDate(component, targetDate); + component.imply("hour", implyHour); + return component; +} + +export function evening(reference: ReferenceWithTimezone, implyHour = 20): ParsingComponents { + const component = new ParsingComponents(reference, {}); + component.imply("meridiem", Meridiem.PM); + component.imply("hour", implyHour); + return component; +} + +export function yesterdayEvening(reference: ReferenceWithTimezone, implyHour = 20): ParsingComponents { + let targetDate = dayjs(reference.instant); + const component = new ParsingComponents(reference, {}); + targetDate = targetDate.add(-1, "day"); + assignSimilarDate(component, targetDate); + component.imply("hour", implyHour); + component.imply("meridiem", Meridiem.PM); + return component; +} + +export function midnight(reference: ReferenceWithTimezone): ParsingComponents { + const component = new ParsingComponents(reference, {}); + component.imply("hour", 0); + component.imply("minute", 0); + component.imply("second", 0); + return component; +} + +export function morning(reference: ReferenceWithTimezone, implyHour = 6): ParsingComponents { + const component = new ParsingComponents(reference, {}); + component.imply("meridiem", Meridiem.AM); + component.imply("hour", implyHour); + return component; +} + +export function noon(reference: ReferenceWithTimezone): ParsingComponents { + const component = new ParsingComponents(reference, {}); + component.imply("meridiem", Meridiem.AM); + component.imply("hour", 12); + return component; +} diff --git a/src/common/parsers/AbstractParserWithWordBoundary.ts b/src/common/parsers/AbstractParserWithWordBoundary.ts index 14a95aa4..ccffab54 100644 --- a/src/common/parsers/AbstractParserWithWordBoundary.ts +++ b/src/common/parsers/AbstractParserWithWordBoundary.ts @@ -15,13 +15,17 @@ export abstract class AbstractParserWithWordBoundaryChecking implements Parser { private cachedInnerPattern?: RegExp = null; private cachedPattern?: RegExp = null; + patternLeftBoundary(): string { + return "(\\W|^)"; + } + pattern(context: ParsingContext): RegExp { const innerPattern = this.innerPattern(context); if (innerPattern == this.cachedInnerPattern) { return this.cachedPattern; } - this.cachedPattern = new RegExp(`(\\W|^)${innerPattern.source}`, innerPattern.flags); + this.cachedPattern = new RegExp(`${this.patternLeftBoundary()}${innerPattern.source}`, innerPattern.flags); this.cachedInnerPattern = innerPattern; return this.cachedPattern; } diff --git a/src/common/parsers/AbstractTimeExpressionParser.ts b/src/common/parsers/AbstractTimeExpressionParser.ts index d21f87f2..5681266c 100644 --- a/src/common/parsers/AbstractTimeExpressionParser.ts +++ b/src/common/parsers/AbstractTimeExpressionParser.ts @@ -3,23 +3,23 @@ import { ParsingComponents, ParsingResult } from "../../results"; import { Meridiem } from "../../index"; // prettier-ignore -function primaryTimePattern(primaryPrefix: string, primarySuffix: string) { +function primaryTimePattern(leftBoundary: string, primaryPrefix: string, primarySuffix: string, flags: string) { return new RegExp( - "(^|\\s|T|\\b)" + + leftBoundary + `${primaryPrefix}` + "(\\d{1,4})" + "(?:" + - "(?:\\.|\\:|\\:)" + + "(?:\\.|:|:)" + "(\\d{1,2})" + "(?:" + - "(?:\\:|\\:)" + + "(?::|:)" + "(\\d{2})" + "(?:\\.(\\d{1,6}))?" + ")?" + ")?" + "(?:\\s*(a\\.m\\.|p\\.m\\.|am?|pm?))?" + `${primarySuffix}`, - "i" + flags ); } @@ -57,6 +57,14 @@ export abstract class AbstractTimeExpressionParser implements Parser { this.strictMode = strictMode; } + patternFlags(): string { + return "i"; + } + + primaryPatternLeftBoundary(): string { + return "(^|\\s|T|\\b)"; + } + primarySuffix(): string { return "(?=\\W|$)"; } @@ -394,7 +402,12 @@ export abstract class AbstractTimeExpressionParser implements Parser { return this.cachedPrimaryTimePattern; } - this.cachedPrimaryTimePattern = primaryTimePattern(primaryPrefix, primarySuffix); + this.cachedPrimaryTimePattern = primaryTimePattern( + this.primaryPatternLeftBoundary(), + primaryPrefix, + primarySuffix, + this.patternFlags() + ); this.cachedPrimaryPrefix = primaryPrefix; this.cachedPrimarySuffix = primarySuffix; return this.cachedPrimaryTimePattern; diff --git a/src/index.ts b/src/index.ts index ad7b0aac..18985a04 100644 --- a/src/index.ts +++ b/src/index.ts @@ -106,7 +106,8 @@ import * as ja from "./locales/ja"; import * as pt from "./locales/pt"; import * as nl from "./locales/nl"; import * as zh from "./locales/zh"; -export { de, fr, ja, pt, nl, zh }; +import * as ru from "./locales/ru"; +export { de, fr, ja, pt, nl, zh, ru }; /** * A shortcut for {@link en | chrono.en.strict} diff --git a/src/locales/ru/constants.ts b/src/locales/ru/constants.ts new file mode 100644 index 00000000..f3830c24 --- /dev/null +++ b/src/locales/ru/constants.ts @@ -0,0 +1,344 @@ +import { OpUnitType, QUnitType } from "dayjs"; +import { matchAnyPattern, repeatedTimeunitPattern } from "../../utils/pattern"; +import { findMostLikelyADYear } from "../../calculation/years"; +import { TimeUnits } from "../../utils/timeunits"; + +export const REGEX_PARTS = { + leftBoundary: "([^\\p{L}\\p{N}_]|^)", + rightBoundary: "(?=[^\\p{L}\\p{N}_]|$)", + flags: "iu", +}; + +export const WEEKDAY_DICTIONARY: { [word: string]: number } = { + воскресенье: 0, + воскресенья: 0, + вск: 0, + "вск.": 0, + понедельник: 1, + понедельника: 1, + пн: 1, + "пн.": 1, + вторник: 2, + вторника: 2, + вт: 2, + "вт.": 2, + среда: 3, + среды: 3, + среду: 3, + ср: 3, + "ср.": 3, + четверг: 4, + четверга: 4, + чт: 4, + "чт.": 4, + пятница: 5, + пятницу: 5, + пятницы: 5, + пт: 5, + "пт.": 5, + суббота: 6, + субботу: 6, + субботы: 6, + сб: 6, + "сб.": 6, +}; + +export const FULL_MONTH_NAME_DICTIONARY: { [word: string]: number } = { + январь: 1, + января: 1, + январе: 1, + февряль: 2, + февряля: 2, + февряле: 2, + март: 3, + марта: 3, + марте: 3, + апрель: 4, + апреля: 4, + апреле: 4, + май: 5, + мая: 5, + мае: 5, + июнь: 6, + июня: 6, + июне: 6, + июль: 7, + июля: 7, + июле: 7, + август: 8, + августа: 8, + августе: 8, + сентябрь: 9, + сентября: 9, + сентябре: 9, + октябрь: 10, + октября: 10, + октябре: 10, + ноябрь: 11, + ноября: 11, + ноябре: 11, + декабрь: 12, + декабря: 12, + декабре: 12, +}; + +export const MONTH_DICTIONARY: { [word: string]: number } = { + ...FULL_MONTH_NAME_DICTIONARY, + янв: 1, + "янв.": 1, + фев: 2, + "фев.": 2, + мар: 3, + "мар.": 3, + апр: 4, + "апр.": 4, + авг: 8, + "авг.": 8, + сен: 9, + "сен.": 9, + окт: 10, + "окт.": 10, + ноя: 11, + "ноя.": 11, + дек: 12, + "дек.": 12, +}; + +export const INTEGER_WORD_DICTIONARY: { [word: string]: number } = { + один: 1, + одна: 1, + одной: 1, + одну: 1, + две: 2, + два: 2, + двух: 2, + три: 3, + трех: 3, + трёх: 3, + четыре: 4, + четырех: 4, + четырёх: 4, + пять: 5, + пяти: 5, + шесть: 6, + шести: 6, + семь: 7, + семи: 7, + восемь: 8, + восемьми: 8, + девять: 9, + девяти: 9, + десять: 10, + десяти: 10, + одиннадцать: 11, + одиннадцати: 11, + двенадцать: 12, + двенадцати: 12, +}; + +export const ORDINAL_WORD_DICTIONARY: { [word: string]: number } = { + первое: 1, + первого: 1, + второе: 2, + второго: 2, + третье: 3, + третьего: 3, + четвертое: 4, + четвертого: 4, + пятое: 5, + пятого: 5, + шестое: 6, + шестого: 6, + седьмое: 7, + седьмого: 7, + восьмое: 8, + восьмого: 8, + девятое: 9, + девятого: 9, + десятое: 10, + десятого: 10, + одиннадцатое: 11, + одиннадцатого: 11, + двенадцатое: 12, + двенадцатого: 12, + тринадцатое: 13, + тринадцатого: 13, + четырнадцатое: 14, + четырнадцатого: 14, + пятнадцатое: 15, + пятнадцатого: 15, + шестнадцатое: 16, + шестнадцатого: 16, + семнадцатое: 17, + семнадцатого: 17, + восемнадцатое: 18, + восемнадцатого: 18, + девятнадцатое: 19, + девятнадцатого: 19, + двадцатое: 20, + двадцатого: 20, + "двадцать первое": 21, + "двадцать первого": 21, + "двадцать второе": 22, + "двадцать второго": 22, + "двадцать третье": 23, + "двадцать третьего": 23, + "двадцать четвертое": 24, + "двадцать четвертого": 24, + "двадцать пятое": 25, + "двадцать пятого": 25, + "двадцать шестое": 26, + "двадцать шестого": 26, + "двадцать седьмое": 27, + "двадцать седьмого": 27, + "двадцать восьмое": 28, + "двадцать восьмого": 28, + "двадцать девятое": 29, + "двадцать девятого": 29, + "тридцатое": 30, + "тридцатого": 30, + "тридцать первое": 31, + "тридцать первого": 31, +}; + +export const TIME_UNIT_DICTIONARY: { [word: string]: OpUnitType | QUnitType } = { + сек: "second", + секунда: "second", + секунд: "second", + секунды: "second", + секунду: "second", + секундочка: "second", + секундочки: "second", + секундочек: "second", + секундочку: "second", + мин: "minute", + минута: "minute", + минут: "minute", + минуты: "minute", + минуту: "minute", + минуток: "minute", + минутки: "minute", + минутку: "minute", + час: "hour", + часов: "hour", + часа: "hour", + часу: "hour", + часиков: "hour", + часика: "hour", + часике: "hour", + часик: "hour", + день: "d", + дня: "d", + дней: "d", + суток: "d", + сутки: "d", + неделя: "week", + неделе: "week", + недели: "week", + неделю: "week", + недель: "week", + недельке: "week", + недельки: "week", + неделек: "week", + месяц: "month", + месяце: "month", + месяцев: "month", + месяца: "month", + квартал: "quarter", + квартале: "quarter", + кварталов: "quarter", + год: "year", + года: "year", + году: "year", + годов: "year", + лет: "year", + годик: "year", + годика: "year", + годиков: "year", +}; + +//----------------------------- + +export const NUMBER_PATTERN = `(?:${matchAnyPattern( + INTEGER_WORD_DICTIONARY +)}|[0-9]+|[0-9]+\\.[0-9]+|пол|несколько|пар(?:ы|у)|\\s{0,3})`; + +export function parseNumberPattern(match: string): number { + const num = match.toLowerCase(); + if (INTEGER_WORD_DICTIONARY[num] !== undefined) { + return INTEGER_WORD_DICTIONARY[num]; + } + if (num.match(/несколько/)) { + return 3; + } else if (num.match(/пол/)) { + return 0.5; + } else if (num.match(/пар/)) { + return 2; + } else if (num === "") { + return 1; + } + return parseFloat(num); +} + +//----------------------------- + +export const ORDINAL_NUMBER_PATTERN = `(?:${matchAnyPattern(ORDINAL_WORD_DICTIONARY)}|[0-9]{1,2}(?:го|ого|е|ое)?)`; +export function parseOrdinalNumberPattern(match: string): number { + let num = match.toLowerCase(); + if (ORDINAL_WORD_DICTIONARY[num] !== undefined) { + return ORDINAL_WORD_DICTIONARY[num]; + } + + num = num.replace(/(?:st|nd|rd|th)$/i, ""); + return parseInt(num); +} + +//----------------------------- + +const year = "(?:\\s+(?:году|года|год|г|г.))?"; +export const YEAR_PATTERN = `(?:[1-9][0-9]{0,3}${year}\\s*(?:н.э.|до н.э.|н. э.|до н. э.)|[1-2][0-9]{3}${year}|[5-9][0-9]${year})`; +export function parseYear(match: string): number { + if (/(год|года|г|г.)/i.test(match)) { + match = match.replace(/(год|года|г|г.)/i, ""); + } + + if (/(до н.э.|до н. э.)/i.test(match)) { + //Before Common Era + match = match.replace(/(до н.э.|до н. э.)/i, ""); + return -parseInt(match); + } + + if (/(н. э.|н.э.)/i.test(match)) { + //Common Era + match = match.replace(/(н. э.|н.э.)/i, ""); + return parseInt(match); + } + + const rawYearNumber = parseInt(match); + return findMostLikelyADYear(rawYearNumber); +} + +//----------------------------- + +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"); + +export const TIME_UNITS_PATTERN = repeatedTimeunitPattern(`(?:(?:около|примерно)\\s{0,3})?`, SINGLE_TIME_UNIT_PATTERN); + +export function parseTimeUnits(timeunitText): TimeUnits { + const fragments = {}; + let remainingText = timeunitText; + let match = SINGLE_TIME_UNIT_REGEX.exec(remainingText); + while (match) { + collectDateTimeFragment(fragments, match); + remainingText = remainingText.substring(match[0].length).trim(); + match = SINGLE_TIME_UNIT_REGEX.exec(remainingText); + } + return fragments; +} + +function collectDateTimeFragment(fragments, match) { + const num = parseNumberPattern(match[1]); + const unit = TIME_UNIT_DICTIONARY[match[2].toLowerCase()]; + fragments[unit] = num; +} diff --git a/src/locales/ru/index.ts b/src/locales/ru/index.ts new file mode 100644 index 00000000..c8b0dcac --- /dev/null +++ b/src/locales/ru/index.ts @@ -0,0 +1,84 @@ +/** + * Chrono components for Russian support (*parsers*, *refiners*, and *configuration*) + * + * @module + */ + +import RUTimeUnitWithinFormatParser from "./parsers/RUTimeUnitWithinFormatParser"; +import RUMonthNameLittleEndianParser from "./parsers/RUMonthNameLittleEndianParser"; +import RUMonthNameParser from "./parsers/RUMonthNameParser"; +import RUTimeExpressionParser from "./parsers/RUTimeExpressionParser"; +import RUTimeUnitAgoFormatParser from "./parsers/RUTimeUnitAgoFormatParser"; +import RUMergeDateRangeRefiner from "./refiners/RUMergeDateRangeRefiner"; +import RUMergeDateTimeRefiner from "./refiners/RUMergeDateTimeRefiner"; + +import { includeCommonConfiguration } from "../../configurations"; +import RUCasualDateParser from "./parsers/RUCasualDateParser"; +import RUCasualTimeParser from "./parsers/RUCasualTimeParser"; +import RUWeekdayParser from "./parsers/RUWeekdayParser"; +import RURelativeDateFormatParser from "./parsers/RURelativeDateFormatParser"; + +import { ParsedResult, ParsingOption } from "../../index"; +import { Chrono, Configuration } from "../../chrono"; +import SlashDateFormatParser from "../../common/parsers/SlashDateFormatParser"; +import RUTimeUnitCasualRelativeFormatParser from "./parsers/RUTimeUnitCasualRelativeFormatParser"; + +/** + * Chrono object configured for parsing *casual* Russian + */ +export const casual = new Chrono(createCasualConfiguration()); + +/** + * Chrono object configured for parsing *strict* Russian + */ +export const strict = new Chrono(createConfiguration(true)); + +/** + * A shortcut for ru.casual.parse() + */ +export function parse(text: string, ref?: Date, option?: ParsingOption): ParsedResult[] { + return casual.parse(text, ref, option); +} + +/** + * A shortcut for ru.casual.parseDate() + */ +export function parseDate(text: string, ref?: Date, option?: ParsingOption): Date { + return casual.parseDate(text, ref, option); +} + +/** + * Create a default *casual* {@Link Configuration} for Russian chrono. + * It calls {@Link createConfiguration} and includes additional parsers. + */ +export function createCasualConfiguration(): Configuration { + const option = createConfiguration(false); + option.parsers.unshift(new RUCasualDateParser()); + option.parsers.unshift(new RUCasualTimeParser()); + option.parsers.unshift(new RUMonthNameParser()); + option.parsers.unshift(new RURelativeDateFormatParser()); + option.parsers.unshift(new RUTimeUnitCasualRelativeFormatParser()); + return option; +} + +/** + * Create a default {@Link Configuration} for Russian chrono + * + * @param strictMode If the timeunit mentioning should be strict, not casual + */ +export function createConfiguration(strictMode = true): Configuration { + return includeCommonConfiguration( + { + parsers: [ + new SlashDateFormatParser(true), + new RUTimeUnitWithinFormatParser(), + new RUMonthNameLittleEndianParser(), + new RUWeekdayParser(), + new RUTimeExpressionParser(strictMode), + new RUTimeUnitAgoFormatParser(), + ], + refiners: [new RUMergeDateTimeRefiner(), new RUMergeDateRangeRefiner()], + }, + strictMode + ); +} diff --git a/src/locales/ru/parsers/RUCasualDateParser.ts b/src/locales/ru/parsers/RUCasualDateParser.ts new file mode 100644 index 00000000..f9b4511d --- /dev/null +++ b/src/locales/ru/parsers/RUCasualDateParser.ts @@ -0,0 +1,44 @@ +import { ParsingContext } from "../../../chrono"; +import { ParsingComponents, ParsingResult } from "../../../results"; +import { AbstractParserWithWordBoundaryChecking } from "../../../common/parsers/AbstractParserWithWordBoundary"; +import * as references from "../../../common/casualReferences"; +import { REGEX_PARTS } from "../constants"; + +const PATTERN = new RegExp( + `(?:с|со)?\\s*(сегодня|вчера|завтра|послезавтра|позавчера)${REGEX_PARTS.rightBoundary}`, + REGEX_PARTS.flags +); + +export default class RUCasualDateParser extends AbstractParserWithWordBoundaryChecking { + patternLeftBoundary(): string { + return REGEX_PARTS.leftBoundary; + } + + innerPattern(context: ParsingContext): RegExp { + return PATTERN; + } + + innerExtract(context: ParsingContext, match: RegExpMatchArray): ParsingComponents | ParsingResult { + const lowerText = match[1].toLowerCase(); + const component = context.createParsingComponents(); + + switch (lowerText) { + case "сегодня": + return references.today(context.reference); + + case "вчера": + return references.yesterday(context.reference); + + case "завтра": + return references.tomorrow(context.reference); + + case "послезавтра": + return references.theDayAfterTomorrow(context.reference); + + case "позавчера": + return references.theDayBeforeYesterday(context.reference); + } + + return component; + } +} diff --git a/src/locales/ru/parsers/RUCasualTimeParser.ts b/src/locales/ru/parsers/RUCasualTimeParser.ts new file mode 100644 index 00000000..4a9b3fc8 --- /dev/null +++ b/src/locales/ru/parsers/RUCasualTimeParser.ts @@ -0,0 +1,56 @@ +import { ParsingContext } from "../../../chrono"; +import { AbstractParserWithWordBoundaryChecking } from "../../../common/parsers/AbstractParserWithWordBoundary"; +import * as references from "../../../common/casualReferences"; +import { assignSimilarDate } from "../../../utils/dayjs"; +import dayjs from "dayjs"; +import { REGEX_PARTS } from "../constants"; + +const PATTERN = new RegExp( + "(сейчас|прошлым\\s*вечером|прошлой\\s*ночью|следующей\\s*ночью|сегодня\\s*ночью|этой\\s*ночью|ночью|этим утром|утром|утра|в\\s*полдень|вечером|вечера|в\\s*полночь)" + + REGEX_PARTS.rightBoundary, + REGEX_PARTS.flags +); +export default class RUCasualTimeParser extends AbstractParserWithWordBoundaryChecking { + patternLeftBoundary(): string { + return REGEX_PARTS.leftBoundary; + } + + innerPattern() { + return PATTERN; + } + + innerExtract(context: ParsingContext, match: RegExpMatchArray) { + let targetDate = dayjs(context.refDate); + const lowerText = match[0].toLowerCase(); + const component = context.createParsingComponents(); + + if (lowerText === "сейчас") { + return references.now(context.reference); + } + if (lowerText === "вечером" || lowerText === "вечера") { + return references.evening(context.reference); + } + if (lowerText.endsWith("утром") || lowerText.endsWith("утра")) { + return references.morning(context.reference); + } + if (lowerText.match(/в\s*полдень/)) { + return references.noon(context.reference); + } + if (lowerText.match(/прошлой\s*ночью/)) { + return references.lastNight(context.reference); + } + if (lowerText.match(/прошлым\s*вечером/)) { + return references.yesterdayEvening(context.reference); + } + if (lowerText.match(/следующей\s*ночью/)) { + const daysToAdd = targetDate.hour() < 22 ? 1 : 2; + targetDate = targetDate.add(daysToAdd, "day"); + assignSimilarDate(component, targetDate); + component.imply("hour", 0); + } + if (lowerText.match(/в\s*полночь/) || lowerText.endsWith("ночью")) { + return references.midnight(context.reference); + } + return component; + } +} diff --git a/src/locales/ru/parsers/RUMonthNameLittleEndianParser.ts b/src/locales/ru/parsers/RUMonthNameLittleEndianParser.ts new file mode 100644 index 00000000..a2198e6f --- /dev/null +++ b/src/locales/ru/parsers/RUMonthNameLittleEndianParser.ts @@ -0,0 +1,72 @@ +import { ParsingContext } from "../../../chrono"; +import { ParsingResult } from "../../../results"; +import { findYearClosestToRef } from "../../../calculation/years"; +import { MONTH_DICTIONARY, REGEX_PARTS } from "../constants"; +import { YEAR_PATTERN, parseYear } from "../constants"; +import { ORDINAL_NUMBER_PATTERN, parseOrdinalNumberPattern } from "../constants"; +import { matchAnyPattern } from "../../../utils/pattern"; +import { AbstractParserWithWordBoundaryChecking } from "../../../common/parsers/AbstractParserWithWordBoundary"; + +// prettier-ignore +const PATTERN = new RegExp( + `(?:с)?\\s*(${ORDINAL_NUMBER_PATTERN})` + + `(?:` + + `\\s{0,3}(?:по|-|–|до)?\\s{0,3}` + + `(${ORDINAL_NUMBER_PATTERN})` + + ")?" + + `(?:-|\\/|\\s{0,3}(?:of)?\\s{0,3})` + + `(${matchAnyPattern(MONTH_DICTIONARY)})` + + "(?:" + + `(?:-|\\/|,?\\s{0,3})` + + `(${YEAR_PATTERN}(?![^\\s]\\d))` + + ")?" + + REGEX_PARTS.rightBoundary, + REGEX_PARTS.flags +); + +const DATE_GROUP = 1; +const DATE_TO_GROUP = 2; +const MONTH_NAME_GROUP = 3; +const YEAR_GROUP = 4; + +export default class RUMonthNameLittleEndianParser extends AbstractParserWithWordBoundaryChecking { + patternLeftBoundary(): string { + return REGEX_PARTS.leftBoundary; + } + + innerPattern(): RegExp { + return PATTERN; + } + + innerExtract(context: ParsingContext, match: RegExpMatchArray): ParsingResult { + const result = context.createParsingResult(match.index, match[0]); + + const month = MONTH_DICTIONARY[match[MONTH_NAME_GROUP].toLowerCase()]; + const day = parseOrdinalNumberPattern(match[DATE_GROUP]); + if (day > 31) { + // e.g. "[96 Aug]" => "9[6 Aug]", we need to shift away from the next number + match.index = match.index + match[DATE_GROUP].length; + return null; + } + + result.start.assign("month", month); + result.start.assign("day", day); + + if (match[YEAR_GROUP]) { + const yearNumber = parseYear(match[YEAR_GROUP]); + result.start.assign("year", yearNumber); + } else { + const year = findYearClosestToRef(context.refDate, day, month); + result.start.imply("year", year); + } + + if (match[DATE_TO_GROUP]) { + const endDate = parseOrdinalNumberPattern(match[DATE_TO_GROUP]); + + result.end = result.start.clone(); + result.end.assign("day", endDate); + } + + return result; + } +} diff --git a/src/locales/ru/parsers/RUMonthNameParser.ts b/src/locales/ru/parsers/RUMonthNameParser.ts new file mode 100644 index 00000000..ad05f373 --- /dev/null +++ b/src/locales/ru/parsers/RUMonthNameParser.ts @@ -0,0 +1,61 @@ +import { FULL_MONTH_NAME_DICTIONARY, MONTH_DICTIONARY, REGEX_PARTS } from "../constants"; +import { ParsingContext } from "../../../chrono"; +import { findYearClosestToRef } from "../../../calculation/years"; +import { matchAnyPattern } from "../../../utils/pattern"; +import { YEAR_PATTERN, parseYear } from "../constants"; +import { AbstractParserWithWordBoundaryChecking } from "../../../common/parsers/AbstractParserWithWordBoundary"; + +const PATTERN = new RegExp( + `((?:в)\\s*)?` + + `(${matchAnyPattern(MONTH_DICTIONARY)})` + + `\\s*` + + `(?:` + + `[,-]?\\s*(${YEAR_PATTERN})?` + + ")?" + + "(?=[^\\s\\w]|\\s+[^0-9]|\\s+$|$)", + REGEX_PARTS.flags +); + +const MONTH_NAME_GROUP = 2; +const YEAR_GROUP = 3; + +/** + * The parser for parsing month name and year. + * - Январь, 2012 + * - Январь 2012 + * - Январь + */ +export default class RUMonthNameParser extends AbstractParserWithWordBoundaryChecking { + patternLeftBoundary(): string { + return REGEX_PARTS.leftBoundary; + } + + innerPattern(): RegExp { + return PATTERN; + } + + innerExtract(context: ParsingContext, match: RegExpMatchArray) { + const monthName = match[MONTH_NAME_GROUP].toLowerCase(); + + // skip some unlikely words "янв", "фер", .. + if (match[0].length <= 3 && !FULL_MONTH_NAME_DICTIONARY[monthName]) { + return null; + } + + const result = context.createParsingResult(match.index, match.index + match[0].length); + result.start.imply("day", 1); + + const month = MONTH_DICTIONARY[monthName]; + result.start.assign("month", month); + + if (match[YEAR_GROUP]) { + const year = parseYear(match[YEAR_GROUP]); + result.start.assign("year", year); + } else { + const year = findYearClosestToRef(context.refDate, 1, month); + result.start.imply("year", year); + } + + return result; + } +} diff --git a/src/locales/ru/parsers/RURelativeDateFormatParser.ts b/src/locales/ru/parsers/RURelativeDateFormatParser.ts new file mode 100644 index 00000000..03b7cfe3 --- /dev/null +++ b/src/locales/ru/parsers/RURelativeDateFormatParser.ts @@ -0,0 +1,75 @@ +import { REGEX_PARTS, TIME_UNIT_DICTIONARY } from "../constants"; +import { ParsingContext } from "../../../chrono"; +import { ParsingComponents } from "../../../results"; +import dayjs from "dayjs"; +import { AbstractParserWithWordBoundaryChecking } from "../../../common/parsers/AbstractParserWithWordBoundary"; +import { matchAnyPattern } from "../../../utils/pattern"; + +const PATTERN = new RegExp( + `(в прошлом|на прошлой|на следующей|в следующем|на этой|в этом)\\s*(${matchAnyPattern( + TIME_UNIT_DICTIONARY + )})(?=\\s*)${REGEX_PARTS.rightBoundary}`, + REGEX_PARTS.flags +); + +const MODIFIER_WORD_GROUP = 1; +const RELATIVE_WORD_GROUP = 2; + +export default class RURelativeDateFormatParser extends AbstractParserWithWordBoundaryChecking { + patternLeftBoundary(): string { + return REGEX_PARTS.leftBoundary; + } + + innerPattern(): RegExp { + return PATTERN; + } + + innerExtract(context: ParsingContext, match: RegExpMatchArray): ParsingComponents { + const modifier = match[MODIFIER_WORD_GROUP].toLowerCase(); + const unitWord = match[RELATIVE_WORD_GROUP].toLowerCase(); + const timeunit = TIME_UNIT_DICTIONARY[unitWord]; + + if (modifier == "на следующей" || modifier == "в следующем") { + const timeUnits = {}; + timeUnits[timeunit] = 1; + return ParsingComponents.createRelativeFromReference(context.reference, timeUnits); + } + + if (modifier == "в прошлом" || modifier == "на прошлой") { + const timeUnits = {}; + timeUnits[timeunit] = -1; + return ParsingComponents.createRelativeFromReference(context.reference, timeUnits); + } + + const components = context.createParsingComponents(); + let date = dayjs(context.reference.instant); + + // This week + if (timeunit.match(/week/i)) { + date = date.add(-date.get("d"), "d"); + components.imply("day", date.date()); + components.imply("month", date.month() + 1); + components.imply("year", date.year()); + } + + // This month + else if (timeunit.match(/month/i)) { + date = date.add(-date.date() + 1, "d"); + components.imply("day", date.date()); + components.assign("year", date.year()); + components.assign("month", date.month() + 1); + } + + // This year + else if (timeunit.match(/year/i)) { + date = date.add(-date.date() + 1, "d"); + date = date.add(-date.month(), "month"); + + components.imply("day", date.date()); + components.imply("month", date.month() + 1); + components.assign("year", date.year()); + } + + return components; + } +} diff --git a/src/locales/ru/parsers/RUTimeExpressionParser.ts b/src/locales/ru/parsers/RUTimeExpressionParser.ts new file mode 100644 index 00000000..f51145f6 --- /dev/null +++ b/src/locales/ru/parsers/RUTimeExpressionParser.ts @@ -0,0 +1,64 @@ +import { ParsingContext } from "../../../chrono"; +import { ParsingComponents } from "../../../results"; +import { Meridiem } from "../../../index"; +import { AbstractTimeExpressionParser } from "../../../common/parsers/AbstractTimeExpressionParser"; +import { REGEX_PARTS } from "../constants"; + +export default class RUTimeExpressionParser extends AbstractTimeExpressionParser { + constructor(strictMode) { + super(strictMode); + } + + patternFlags(): string { + return REGEX_PARTS.flags; + } + + primaryPatternLeftBoundary(): string { + return "(^|\\s|T|(?:[^\\p{L}\\p{N}_]))"; + } + + followingPhase(): string { + return "\\s*(?:\\-|\\–|\\~|\\〜|до|и|по|\\?)\\s*"; + } + + primaryPrefix(): string { + return "(?:(?:в|с)\\s*)??"; + } + + primarySuffix(): string { + return `(?:\\s*(?:утра|вечера|после полудня))?(?!\\/)${REGEX_PARTS.rightBoundary}`; + } + + extractPrimaryTimeComponents(context: ParsingContext, match: RegExpMatchArray): null | ParsingComponents { + const components = super.extractPrimaryTimeComponents(context, match); + if (components) { + if (match[0].endsWith("вечера")) { + const hour = components.get("hour"); + if (hour >= 6 && hour < 12) { + components.assign("hour", components.get("hour") + 12); + components.assign("meridiem", Meridiem.PM); + } else if (hour < 6) { + components.assign("meridiem", Meridiem.AM); + } + } + + if (match[0].endsWith("после полудня")) { + components.assign("meridiem", Meridiem.PM); + const hour = components.get("hour"); + if (hour >= 0 && hour <= 6) { + components.assign("hour", components.get("hour") + 12); + } + } + + if (match[0].endsWith("утра")) { + components.assign("meridiem", Meridiem.AM); + const hour = components.get("hour"); + if (hour < 12) { + components.assign("hour", components.get("hour")); + } + } + } + + return components; + } +} diff --git a/src/locales/ru/parsers/RUTimeUnitAgoFormatParser.ts b/src/locales/ru/parsers/RUTimeUnitAgoFormatParser.ts new file mode 100644 index 00000000..8e8efc18 --- /dev/null +++ b/src/locales/ru/parsers/RUTimeUnitAgoFormatParser.ts @@ -0,0 +1,24 @@ +import { ParsingContext } from "../../../chrono"; +import { parseTimeUnits, REGEX_PARTS, 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}назад(?=(?:\\W|$))`, REGEX_PARTS.flags); + +export default class RUTimeUnitAgoFormatParser extends AbstractParserWithWordBoundaryChecking { + patternLeftBoundary(): string { + return REGEX_PARTS.leftBoundary; + } + + innerPattern(): RegExp { + return PATTERN; + } + + innerExtract(context: ParsingContext, match: RegExpMatchArray) { + const timeUnits = parseTimeUnits(match[1]); + const outputTimeUnits = reverseTimeUnits(timeUnits); + + return ParsingComponents.createRelativeFromReference(context.reference, outputTimeUnits); + } +} diff --git a/src/locales/ru/parsers/RUTimeUnitCasualRelativeFormatParser.ts b/src/locales/ru/parsers/RUTimeUnitCasualRelativeFormatParser.ts new file mode 100644 index 00000000..673657e6 --- /dev/null +++ b/src/locales/ru/parsers/RUTimeUnitCasualRelativeFormatParser.ts @@ -0,0 +1,34 @@ +import { TIME_UNITS_PATTERN, parseTimeUnits, REGEX_PARTS } 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( + `(эти|последние|прошлые|следующие|после|через|\\+|-)\\s*(${TIME_UNITS_PATTERN})${REGEX_PARTS.rightBoundary}`, + REGEX_PARTS.flags +); + +export default class RUTimeUnitCasualRelativeFormatParser extends AbstractParserWithWordBoundaryChecking { + patternLeftBoundary(): string { + return REGEX_PARTS.leftBoundary; + } + + innerPattern(): RegExp { + return PATTERN; + } + + innerExtract(context: ParsingContext, match: RegExpMatchArray): ParsingComponents { + const prefix = match[1].toLowerCase(); + let timeUnits = parseTimeUnits(match[2]); + switch (prefix) { + case "последние": + case "прошлые": + case "-": + timeUnits = reverseTimeUnits(timeUnits); + break; + } + + return ParsingComponents.createRelativeFromReference(context.reference, timeUnits); + } +} diff --git a/src/locales/ru/parsers/RUTimeUnitWithinFormatParser.ts b/src/locales/ru/parsers/RUTimeUnitWithinFormatParser.ts new file mode 100644 index 00000000..e3f04f13 --- /dev/null +++ b/src/locales/ru/parsers/RUTimeUnitWithinFormatParser.ts @@ -0,0 +1,24 @@ +import { TIME_UNITS_PATTERN, parseTimeUnits, REGEX_PARTS } from "../constants"; +import { ParsingContext } from "../../../chrono"; +import { ParsingComponents } from "../../../results"; +import { AbstractParserWithWordBoundaryChecking } from "../../../common/parsers/AbstractParserWithWordBoundary"; + +const PATTERN = `(?:(?:около|примерно)\\s*(?:~\\s*)?)?(${TIME_UNITS_PATTERN})${REGEX_PARTS.rightBoundary}`; +const PATTERN_WITH_PREFIX = new RegExp(`(?:в течение|в течении)\\s*` + PATTERN, REGEX_PARTS.flags); + +const PATTERN_WITHOUT_PREFIX = new RegExp(PATTERN, "i"); + +export default class RUTimeUnitWithinFormatParser extends AbstractParserWithWordBoundaryChecking { + patternLeftBoundary(): string { + return REGEX_PARTS.leftBoundary; + } + + innerPattern(context: ParsingContext): RegExp { + return context.option.forwardDate ? PATTERN_WITHOUT_PREFIX : PATTERN_WITH_PREFIX; + } + + innerExtract(context: ParsingContext, match: RegExpMatchArray): ParsingComponents { + const timeUnits = parseTimeUnits(match[1]); + return ParsingComponents.createRelativeFromReference(context.reference, timeUnits); + } +} diff --git a/src/locales/ru/parsers/RUWeekdayParser.ts b/src/locales/ru/parsers/RUWeekdayParser.ts new file mode 100644 index 00000000..dea5d4c3 --- /dev/null +++ b/src/locales/ru/parsers/RUWeekdayParser.ts @@ -0,0 +1,63 @@ +import { ParsingContext } from "../../../chrono"; +import { ParsingComponents } from "../../../results"; +import { REGEX_PARTS, WEEKDAY_DICTIONARY } from "../constants"; +import { matchAnyPattern } from "../../../utils/pattern"; +import { AbstractParserWithWordBoundaryChecking } from "../../../common/parsers/AbstractParserWithWordBoundary"; +import { toDayJSWeekday } from "../../../calculation/weeks"; + +const PATTERN = new RegExp( + "(?:(?:,|\\(|()\\s*)?" + + "(?:в\\s*?)?" + + "(?:(эту|этот|прошлый|прошлую|следующий|следующую|следующего)\\s*)?" + + `(${matchAnyPattern(WEEKDAY_DICTIONARY)})` + + "(?:\\s*(?:,|\\)|)))?" + + "(?:\\s*на\\s*(этой|прошлой|следующей)\\s*неделе)?" + + REGEX_PARTS.rightBoundary, + REGEX_PARTS.flags +); + +const PREFIX_GROUP = 1; +const WEEKDAY_GROUP = 2; +const POSTFIX_GROUP = 3; + +export default class RUWeekdayParser extends AbstractParserWithWordBoundaryChecking { + innerPattern(): RegExp { + return PATTERN; + } + + patternLeftBoundary(): string { + return REGEX_PARTS.leftBoundary; + } + + innerExtract(context: ParsingContext, match: RegExpMatchArray): ParsingComponents { + const dayOfWeek = match[WEEKDAY_GROUP].toLowerCase(); + const offset = WEEKDAY_DICTIONARY[dayOfWeek]; + const prefix = match[PREFIX_GROUP]; + const postfix = match[POSTFIX_GROUP]; + let modifierWord = prefix || postfix; + modifierWord = modifierWord || ""; + modifierWord = modifierWord.toLowerCase(); + + let modifier = null; + if (modifierWord == "прошлый" || modifierWord == "прошлую" || modifierWord == "прошлой") { + modifier = "last"; + } else if ( + modifierWord == "следующий" || + modifierWord == "следующую" || + modifierWord == "следующей" || + modifierWord == "следующего" + ) { + modifier = "next"; + } else if (modifierWord == "этот" || modifierWord == "эту" || modifierWord == "этой") { + modifier = "this"; + } + + const date = toDayJSWeekday(context.refDate, offset, modifier); + return context + .createParsingComponents() + .assign("weekday", offset) + .imply("day", date.date()) + .imply("month", date.month() + 1) + .imply("year", date.year()); + } +} diff --git a/src/locales/ru/refiners/RUMergeDateRangeRefiner.ts b/src/locales/ru/refiners/RUMergeDateRangeRefiner.ts new file mode 100644 index 00000000..4c9a900d --- /dev/null +++ b/src/locales/ru/refiners/RUMergeDateRangeRefiner.ts @@ -0,0 +1,13 @@ +import AbstractMergeDateRangeRefiner from "../../../common/refiners/AbstractMergeDateRangeRefiner"; + +/** + * Merging before and after results (see. AbstractMergeDateRangeRefiner) + * This implementation should provide Russian connecting phases + * - c 06.09.1989 [до|по] 11.12.1996 + * - c пятницы и до среды + */ +export default class xf extends AbstractMergeDateRangeRefiner { + patternBetween(): RegExp { + return /^\s*(и до|и по|до|по|-)\s*$/i; + } +} diff --git a/src/locales/ru/refiners/RUMergeDateTimeRefiner.ts b/src/locales/ru/refiners/RUMergeDateTimeRefiner.ts new file mode 100644 index 00000000..0664a82c --- /dev/null +++ b/src/locales/ru/refiners/RUMergeDateTimeRefiner.ts @@ -0,0 +1,13 @@ +import AbstractMergeDateTimeRefiner from "../../../common/refiners/AbstractMergeDateTimeRefiner"; + +/** + * Merging date-only result and time-only result (see. AbstractMergeDateTimeRefiner). + * This implementation should provide English connecting phases + * - 2020-02-13 [в] 6:00 + * - Завтра [,] 7:00 + */ +export default class RUMergeDateTimeRefiner extends AbstractMergeDateTimeRefiner { + patternBetween(): RegExp { + return new RegExp("^\\s*(T|в|,|-)?\\s*$"); + } +} diff --git a/test/ru/ru_casual.test.ts b/test/ru/ru_casual.test.ts new file mode 100644 index 00000000..7d4e7579 --- /dev/null +++ b/test/ru/ru_casual.test.ts @@ -0,0 +1,147 @@ +import * as chrono from "../../src/"; +import { testSingleCase, testUnexpectedResult } from "../test_util"; + +test("Test - Single Expression", () => { + testSingleCase(chrono.ru.casual, "Дедлайн сегодня", new Date(2012, 7, 10, 17, 10), (result) => { + expect(result.index).toBe(8); + expect(result.text).toBe("сегодня"); + expect(result.start).toBeDate(new Date(2012, 7, 10, 17, 10)); + }); + + testSingleCase(chrono.ru.casual, "Дедлайн завтра", new Date(2012, 7, 10, 17, 10), (result) => { + expect(result.index).toBe(8); + expect(result.text).toBe("завтра"); + expect(result.start).toBeDate(new Date(2012, 7, 11, 17, 10)); + }); + + testSingleCase(chrono.ru.casual, "Дедлайн послезавтра", new Date(2012, 7, 10, 17, 10), (result) => { + expect(result.index).toBe(8); + expect(result.text).toBe("послезавтра"); + expect(result.start).toBeDate(new Date(2012, 7, 12, 17, 10)); + }); + + testSingleCase(chrono.ru.casual, "Дедлайн вчера", new Date(2012, 7, 10, 17, 10), (result) => { + expect(result.index).toBe(8); + expect(result.text).toBe("вчера"); + expect(result.start).toBeDate(new Date(2012, 7, 9, 17, 10)); + }); + + testSingleCase(chrono.ru.casual, "Дедлайн позавчера", new Date(2012, 7, 10, 17, 10), (result) => { + expect(result.index).toBe(8); + expect(result.text).toBe("позавчера"); + expect(result.start).toBeDate(new Date(2012, 7, 8, 17, 10)); + }); + + testSingleCase(chrono.ru.casual, "Дедлайн сейчас", new Date(2012, 7, 10, 8, 9, 10, 11), (result) => { + expect(result.index).toBe(8); + expect(result.text).toBe("сейчас"); + expect(result.start).toBeDate(result.refDate); + expect(result.start).toBeDate(new Date(2012, 7, 10, 8, 9, 10, 11)); + }); + + testSingleCase(chrono.ru.casual, "Дедлайн утром", new Date(2012, 7, 10, 8, 9, 10, 11), (result) => { + expect(result.index).toBe(8); + expect(result.text).toBe("утром"); + expect(result.start).toBeDate(new Date(2012, 7, 10, 6, 0, 0, 0)); + }); + + testSingleCase(chrono.ru.casual, "Дедлайн этим утром", new Date(2012, 7, 10, 8, 9, 10, 11), (result) => { + expect(result.index).toBe(8); + expect(result.text).toBe("этим утром"); + expect(result.start).toBeDate(new Date(2012, 7, 10, 6, 0, 0, 0)); + }); + + testSingleCase(chrono.ru.casual, "Дедлайн в полдень", new Date(2012, 7, 10, 8, 9, 10, 11), (result) => { + expect(result.index).toBe(8); + expect(result.text).toBe("в полдень"); + expect(result.start).toBeDate(new Date(2012, 7, 10, 12, 0, 0, 0)); + }); + + testSingleCase(chrono.ru.casual, "Дедлайн прошлым вечером", new Date(2012, 7, 10, 8, 9, 10, 11), (result) => { + expect(result.index).toBe(8); + expect(result.text).toBe("прошлым вечером"); + expect(result.start).toBeDate(new Date(2012, 7, 9, 20, 0, 0, 0)); + }); + + testSingleCase(chrono.ru.casual, "Дедлайн вечером", new Date(2012, 7, 10, 8, 9, 10, 11), (result) => { + expect(result.index).toBe(8); + expect(result.text).toBe("вечером"); + expect(result.start).toBeDate(new Date(2012, 7, 10, 20, 0, 0, 0)); + }); + + testSingleCase(chrono.ru.casual, "Дедлайн прошлой ночью", new Date(2012, 7, 10, 8, 9, 10, 11), (result) => { + expect(result.index).toBe(8); + expect(result.text).toBe("прошлой ночью"); + expect(result.start).toBeDate(new Date(2012, 7, 10, 0, 0, 0, 0)); + }); + + testSingleCase(chrono.ru.casual, "Дедлайн прошлой ночью", new Date(2012, 7, 10, 2, 9, 10, 11), (result) => { + expect(result.index).toBe(8); + expect(result.text).toBe("прошлой ночью"); + expect(result.start).toBeDate(new Date(2012, 7, 9, 0, 0, 0, 0)); + }); + + testSingleCase(chrono.ru.casual, "Дедлайн сегодня ночью", new Date(2012, 7, 10, 2, 9, 10, 11), (result) => { + expect(result.index).toBe(8); + expect(result.text).toBe("сегодня ночью"); + expect(result.start).toBeDate(new Date(2012, 7, 10, 0, 0, 0, 0)); + }); + + testSingleCase(chrono.ru.casual, "Дедлайн этой ночью", new Date(2012, 7, 10, 2, 9, 10, 11), (result) => { + expect(result.index).toBe(8); + expect(result.text).toBe("этой ночью"); + expect(result.start).toBeDate(new Date(2012, 7, 10, 0, 0, 0, 0)); + }); + + testSingleCase(chrono.ru.casual, "Дедлайн ночью", new Date(2012, 7, 10, 2, 9, 10, 11), (result) => { + expect(result.index).toBe(8); + expect(result.text).toBe("ночью"); + expect(result.start).toBeDate(new Date(2012, 7, 10, 0, 0, 0, 0)); + }); + + testSingleCase(chrono.ru.casual, "Дедлайн в полночь", new Date(2012, 7, 10, 2, 9, 10, 11), (result) => { + expect(result.index).toBe(8); + expect(result.text).toBe("в полночь"); + expect(result.start).toBeDate(new Date(2012, 7, 10, 0, 0, 0, 0)); + }); +}); + +test("Test - Combined Expression", () => { + testSingleCase(chrono.ru.casual, "Дедлайн вчера вечером", new Date(2012, 7, 10, 12), (result) => { + expect(result.index).toBe(8); + expect(result.text).toBe("вчера вечером"); + expect(result.start).toBeDate(new Date(2012, 7, 9, 20)); + }); + + testSingleCase(chrono.ru.casual, "Дедлайн завтра утром", new Date(2012, 8, 10, 14), (result) => { + expect(result.index).toBe(8); + expect(result.text).toBe("завтра утром"); + expect(result.start).toBeDate(new Date(2012, 8, 11, 6)); + }); +}); + +test("Test - Casual date range", () => { + testSingleCase(chrono.ru.casual, "Событие с сегодня и до послезавтра", new Date(2012, 7, 4, 12), (result) => { + expect(result.index).toBe(8); + expect(result.text).toBe("с сегодня и до послезавтра"); + expect(result.start).toBeDate(new Date(2012, 7, 4, 12)); + expect(result.end).toBeDate(new Date(2012, 7, 6, 12)); + }); + + testSingleCase(chrono.ru.casual, "Событие сегодня-завтра", new Date(2012, 7, 10, 12), (result) => { + expect(result.index).toBe(8); + expect(result.text).toBe("сегодня-завтра"); + expect(result.start).toBeDate(new Date(2012, 7, 10, 12)); + expect(result.end).toBeDate(new Date(2012, 7, 11, 12)); + }); +}); + +test("Test - Random negative text", () => { + testUnexpectedResult(chrono.ru, "несегодня"); + + testUnexpectedResult(chrono.ru, "зявтра"); + + testUnexpectedResult(chrono.ru, "вчеера"); + + testUnexpectedResult(chrono.ru, "январ"); +}); diff --git a/test/ru/ru_month.test.ts b/test/ru/ru_month.test.ts new file mode 100644 index 00000000..76e92204 --- /dev/null +++ b/test/ru/ru_month.test.ts @@ -0,0 +1,70 @@ +import * as chrono from "../../src"; +import { testSingleCase } from "../test_util"; + +test("Test - Month-Year expression", function () { + testSingleCase(chrono.ru, "Сентябрь 2012", (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("Сентябрь 2012"); + expect(result.start).toBeDate(new Date(2012, 9 - 1, 1, 12)); + }); + + testSingleCase(chrono.ru, "сен 2012", (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("сен 2012"); + expect(result.start).toBeDate(new Date(2012, 9 - 1, 1, 12)); + }); + + testSingleCase(chrono.ru, "сен. 2012", (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("сен. 2012"); + expect(result.start).toBeDate(new Date(2012, 9 - 1, 1, 12)); + }); + + testSingleCase(chrono.ru, "сен-2012", (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("сен-2012"); + expect(result.start).toBeDate(new Date(2012, 9 - 1, 1, 12)); + }); +}); + +test("Test - Month-Only expression", function () { + testSingleCase(chrono.ru, "в январе", new Date(2020, 11 - 1, 22), (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("в январе"); + expect(result.start).toBeDate(new Date(2021, 1 - 1, 1, 12)); + }); + + testSingleCase(chrono.ru, "в янв", new Date(2020, 11 - 1, 22), (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("в янв"); + expect(result.start).toBeDate(new Date(2021, 1 - 1, 1, 12)); + }); + + testSingleCase(chrono.ru, "май", new Date(2020, 11 - 1, 22), (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("май"); + expect(result.start).toBeDate(new Date(2021, 5 - 1, 1, 12)); + }); +}); + +test("Test - Month expression in context", function () { + testSingleCase(chrono.ru, "Это было в сентябре 2012 перед новым годом", (result) => { + expect(result.index).toBe(9); + expect(result.text).toBe("в сентябре 2012"); + expect(result.start).toBeDate(new Date(2012, 9 - 1, 1, 12)); + }); +}); + +test("Test - year 90's parsing", () => { + testSingleCase(chrono.ru, "авг 96", new Date(2012, 7, 10), (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("авг 96"); + expect(result.start).toBeDate(new Date(1996, 8 - 1, 1, 12)); + }); + + testSingleCase(chrono.ru, "96 авг 96", new Date(2012, 7, 10), (result) => { + expect(result.index).toBe(3); + expect(result.text).toBe("авг 96"); + expect(result.start).toBeDate(new Date(1996, 8 - 1, 1, 12)); + }); +}); diff --git a/test/ru/ru_month_name_little_endian.test.ts b/test/ru/ru_month_name_little_endian.test.ts new file mode 100644 index 00000000..8723fea5 --- /dev/null +++ b/test/ru/ru_month_name_little_endian.test.ts @@ -0,0 +1,145 @@ +import * as chrono from "../../src"; +import { testSingleCase, testUnexpectedResult } from "../test_util"; + +test("Test - Single expression", () => { + testSingleCase(chrono.ru, "10.08.2012", new Date(2012, 7, 10), (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("10.08.2012"); + expect(result.start).toBeDate(new Date(2012, 8 - 1, 10, 12)); + }); + + testSingleCase(chrono.ru, "10 августа 2012", new Date(2012, 7, 10), (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("10 августа 2012"); + expect(result.start).toBeDate(new Date(2012, 8 - 1, 10, 12)); + }); + + testSingleCase(chrono.ru, "третье фев 82", new Date(2012, 7, 10), (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("третье фев 82"); + expect(result.start).toBeDate(new Date(1982, 2 - 1, 3, 12)); + }); + + testSingleCase(chrono.ru, "Дедлайн 10 августа", new Date(2012, 7, 10), (result) => { + expect(result.index).toBe(8); + expect(result.text).toBe("10 августа"); + expect(result.start).toBeDate(new Date(2012, 8 - 1, 10, 12)); + }); + + testSingleCase(chrono.ru, "Дедлайн Четверг, 10 января", new Date(2012, 7, 10), (result) => { + expect(result.index).toBe(8); + expect(result.text).toBe("Четверг, 10 января"); + expect(result.start).toBeDate(new Date(2013, 1 - 1, 10, 12)); + }); +}); + +test("Test - Single expression with separators", () => { + testSingleCase(chrono.ru, "10-августа 2012", new Date(2012, 7, 8), (result, text) => { + expect(result.text).toBe(text); + expect(result).toBeDate(new Date(2012, 8 - 1, 10, 12, 0)); + }); + + testSingleCase(chrono.ru, "10-августа-2012", new Date(2012, 7, 8), (result, text) => { + expect(result.text).toBe(text); + expect(result).toBeDate(new Date(2012, 8 - 1, 10, 12, 0)); + }); + + testSingleCase(chrono.ru, "10/августа 2012", new Date(2012, 7, 8), (result, text) => { + expect(result.text).toBe(text); + expect(result).toBeDate(new Date(2012, 8 - 1, 10, 12, 0)); + }); + + testSingleCase(chrono.ru, "10/августа/2012", new Date(2012, 7, 8), (result, text) => { + expect(result.text).toBe(text); + expect(result).toBeDate(new Date(2012, 8 - 1, 10, 12, 0)); + }); +}); + +test("Test - Range expression", () => { + testSingleCase(chrono.ru, "10 - 22 августа 2012", new Date(2012, 7, 10), (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("10 - 22 августа 2012"); + expect(result.start).toBeDate(new Date(2012, 8 - 1, 10, 12)); + expect(result.end).toBeDate(new Date(2012, 8 - 1, 22, 12)); + }); + + testSingleCase(chrono.ru, "с 10 по 22 августа 2012", new Date(2012, 7, 10), (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("с 10 по 22 августа 2012"); + + expect(result.start).toBeDate(new Date(2012, 8 - 1, 10, 12)); + expect(result.end).toBeDate(new Date(2012, 8 - 1, 22, 12)); + }); + + testSingleCase(chrono.ru, "10 августа - 12 сентября", new Date(2012, 7, 10), (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("10 августа - 12 сентября"); + + expect(result.start).toBeDate(new Date(2012, 8 - 1, 10, 12)); + expect(result.end).toBeDate(new Date(2012, 9 - 1, 12, 12)); + }); + + testSingleCase(chrono.ru, "10 августа - 12 сентября 2013", new Date(2012, 7, 10), (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("10 августа - 12 сентября 2013"); + + expect(result.start).toBeDate(new Date(2013, 8 - 1, 10, 12)); + expect(result.end).toBeDate(new Date(2013, 9 - 1, 12, 12)); + }); +}); + +test("Test - Combined expression", () => { + testSingleCase(chrono.ru, "5 мая 12:00", new Date(2012, 7, 10), (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("5 мая 12:00"); + expect(result.start).toBeDate(new Date(2012, 5 - 1, 5, 12, 0)); + }); +}); + +test("Test - Ordinal Words", () => { + testSingleCase(chrono.ru, "двадцать пятое мая", new Date(2012, 1, 10), (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("двадцать пятое мая"); + expect(result.start).toBeDate(new Date(2012, 5 - 1, 25, 12, 0)); + }); + testSingleCase(chrono.ru, "двадцать пятое мая 2020 года", new Date(2012, 1, 10), (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("двадцать пятое мая 2020 года"); + expect(result.start).toBeDate(new Date(2020, 5 - 1, 25, 12, 0)); + }); +}); + +test("Test - little endian date followed by time", () => { + testSingleCase(chrono.ru, "24го октября, 9:00", new Date(2017, 7 - 1, 7, 15), (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("24го октября, 9:00"); + expect(result.start).toBeDate(new Date(2017, 10 - 1, 24, 9)); + }); +}); + +test("Test - year 90's parsing", () => { + testSingleCase(chrono.ru, "03 авг 96", new Date(2012, 7, 10), (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("03 авг 96"); + expect(result.start).toBeDate(new Date(1996, 8 - 1, 3, 12)); + }); +}); + +test("Test - Forward Option", () => { + testSingleCase(chrono.ru.casual, "22-23 фев в 7", new Date(2016, 3 - 1, 15), { forwardDate: true }, (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("22-23 фев в 7"); + expect(result.start).toBeDate(new Date(2017, 2 - 1, 22, 7)); + expect(result.end).toBeDate(new Date(2017, 2 - 1, 23, 7)); + }); +}); + +test("Test - Impossible Dates (Strict Mode)", function () { + testUnexpectedResult(chrono.ru.strict, "32 августа 2014", new Date(2012, 7, 10)); + + testUnexpectedResult(chrono.ru.strict, "29 февраля 2014", new Date(2012, 7, 10)); + + testUnexpectedResult(chrono.ru.strict, "32 августа", new Date(2012, 7, 10)); + + testUnexpectedResult(chrono.ru.strict, "29 февраля", new Date(2013, 7, 10)); +}); diff --git a/test/ru/ru_relative.test.ts b/test/ru/ru_relative.test.ts new file mode 100644 index 00000000..527a7e20 --- /dev/null +++ b/test/ru/ru_relative.test.ts @@ -0,0 +1,74 @@ +import * as chrono from "../../src"; +import { testSingleCase } from "../test_util"; + +test("Test - 'This' expressions", () => { + testSingleCase(chrono.ru, "на этой неделе", new Date(2017, 11 - 1, 19, 12), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2017, 11 - 1, 19, 12)); + }); + + testSingleCase(chrono.ru, "в этом месяце", new Date(2017, 11 - 1, 19, 12), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2017, 11 - 1, 1, 12)); + }); + + testSingleCase(chrono.ru, "в этом месяце", new Date(2017, 11 - 1, 1, 12), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2017, 11 - 1, 1, 12)); + }); + + testSingleCase(chrono.ru, "в этом году", new Date(2017, 11 - 1, 19, 12), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2017, 1 - 1, 1, 12)); + }); +}); + +test("Test - Past relative expressions", () => { + testSingleCase(chrono.ru, "на прошлой неделе", new Date(2016, 10 - 1, 1, 12), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2016, 9 - 1, 24, 12)); + }); + + testSingleCase(chrono.ru, "в прошлом месяце", new Date(2016, 10 - 1, 1, 12), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2016, 9 - 1, 1, 12)); + }); + + testSingleCase(chrono.ru, "в прошлом году", new Date(2016, 10 - 1, 1, 12), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2015, 10 - 1, 1, 12)); + }); +}); + +test("Test - Future relative expressions", () => { + testSingleCase(chrono.ru, "на следующей неделе", new Date(2016, 10 - 1, 1, 12), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2016, 10 - 1, 8, 12)); + }); + + testSingleCase(chrono.ru, "в следующем месяце", new Date(2016, 10 - 1, 1, 12), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2016, 11 - 1, 1, 12)); + }); + + testSingleCase(chrono.ru, "в следующем квартале", new Date(2016, 10 - 1, 1, 12), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2017, 1 - 1, 1, 12)); + }); + + testSingleCase(chrono.ru, "в следующем году", new Date(2016, 10 - 1, 1, 12), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2017, 10 - 1, 1, 12)); + }); +}); diff --git a/test/ru/ru_time_exp.test.ts b/test/ru/ru_time_exp.test.ts new file mode 100644 index 00000000..b15f4e8d --- /dev/null +++ b/test/ru/ru_time_exp.test.ts @@ -0,0 +1,111 @@ +import * as chrono from "../../src"; +import { testSingleCase, testUnexpectedResult } from "../test_util"; + +test("Test - Time expression", function () { + testSingleCase(chrono.ru, "20:32:13", new Date(2016, 10 - 1, 1, 8), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2016, 10 - 1, 1, 20, 32, 13)); + }); +}); + +test("Test - Time range expression", function () { + testSingleCase(chrono.ru, "10:00:00 - 21:45:01", new Date(2016, 10 - 1, 1, 8), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2016, 10 - 1, 1, 10)); + expect(result.end).toBeDate(new Date(2016, 10 - 1, 1, 21, 45, 1)); + }); +}); + +test("Test - Casual time number expression", function () { + testSingleCase(chrono.ru, "в 11 утра", new Date(2016, 10 - 1, 1, 8), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2016, 10 - 1, 1, 11)); + }); + + testSingleCase(chrono.ru, "в 11 вечера", new Date(2016, 10 - 1, 1, 8), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2016, 10 - 1, 1, 23)); + }); +}); + +test("Test - Time range's meridiem handling", function () { + testSingleCase(chrono.ru, "с 10 до 11 утра", new Date(2016, 10 - 1, 1, 8), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2016, 10 - 1, 1, 10)); + expect(result.end).toBeDate(new Date(2016, 10 - 1, 1, 11)); + }); + testSingleCase(chrono.ru, "с 10 до 11 вечера", new Date(2016, 10 - 1, 1, 8), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2016, 10 - 1, 1, 22)); + expect(result.end).toBeDate(new Date(2016, 10 - 1, 1, 23)); + }); +}); + +test("Test - Parsing causal positive cases", function () { + testSingleCase(chrono.ru.casual, "в 1", (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("в 1"); + expect(result.start.get("hour")).toBe(1); + }); + + testSingleCase(chrono.ru.casual, "в 12", (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("в 12"); + expect(result.start.get("hour")).toBe(12); + }); + + testSingleCase(chrono.ru.casual, "в 12.30", (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("в 12.30"); + expect(result.start.get("hour")).toBe(12); + expect(result.start.get("minute")).toBe(30); + }); +}); + +test("Test - Parsing negative cases : [year-like] pattern", function () { + testUnexpectedResult(chrono.ru, "2020"); + + testUnexpectedResult(chrono.ru, "2020 "); +}); + +test("Test - Parsing negative cases : 'at [some numbers]'", function () { + testUnexpectedResult(chrono.ru, "Температура 101,194 градусов!"); + + testUnexpectedResult(chrono.ru, "Температура 101 градусов!"); + + testUnexpectedResult(chrono.ru, "Температура 10.1"); +}); + +test("Test - Parsing negative cases : 'at [some numbers] - [some numbers]'", function () { + testUnexpectedResult(chrono.ru, "Это в 10.1 - 10.12"); + + testUnexpectedResult(chrono.ru, "Это в 10 - 10.1"); +}); + +test("Test - Parsing negative cases (Strict)", function () { + testUnexpectedResult(chrono.ru.strict, "Это в 101,194 телефон!"); + + testUnexpectedResult(chrono.ru.strict, "Это в 101 стул!"); + + testUnexpectedResult(chrono.ru.strict, "Это в 10.1"); + + testUnexpectedResult(chrono.ru.strict, "Это в 10"); + + testUnexpectedResult(chrono.ru.strict, "2020"); +}); + +test("Test - Parsing negative cases : 'at [some numbers] - [some numbers]' (Strict)", function () { + testUnexpectedResult(chrono.ru.strict, "Это в 10.1 - 10.12"); + + testUnexpectedResult(chrono.ru.strict, "Это в 10 - 10.1"); + + testUnexpectedResult(chrono.ru.strict, "Это в 10 - 20"); + + testUnexpectedResult(chrono.ru.strict, "7-730"); +}); diff --git a/test/ru/ru_time_units_ago.test.ts b/test/ru/ru_time_units_ago.test.ts new file mode 100644 index 00000000..4b9cc840 --- /dev/null +++ b/test/ru/ru_time_units_ago.test.ts @@ -0,0 +1,48 @@ +import * as chrono from "../../src/"; +import { testSingleCase, testUnexpectedResult } from "../test_util"; + +test("Test - Single Expression", function () { + testSingleCase(chrono.ru, "5 дней назад что-то было", new Date(2012, 7 - 1, 10), (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("5 дней назад"); + expect(result.start).toBeDate(new Date(2012, 7 - 1, 5)); + }); + + testSingleCase(chrono.ru, "5 минут назад что-то было", new Date(2012, 7 - 1, 10), (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("5 минут назад"); + expect(result.start).toBeDate(new Date(2012, 7 - 1, 9, 23, 55)); + }); + + testSingleCase(chrono.ru, "полчаса назад что-то было", new Date(2012, 7 - 1, 10), (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("полчаса назад"); + expect(result.start).toBeDate(new Date(2012, 7 - 1, 9, 23, 30)); + }); +}); + +test("Test - Nested time ago", function () { + testSingleCase(chrono.ru, "5 дней 2 часа назад что-то было", new Date(2012, 7 - 1, 10), (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("5 дней 2 часа назад"); + expect(result.start).toBeDate(new Date(2012, 7 - 1, 4, 22)); + }); + + testSingleCase(chrono.ru, "5 минут 20 секунд назад что-то было", new Date(2012, 7 - 1, 10), (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("5 минут 20 секунд назад"); + expect(result.start).toBeDate(new Date(2012, 7 - 1, 9, 23, 54, 40)); + }); + + testSingleCase(chrono.ru, "2 часа 5 минут назад что-то было", new Date(2012, 7 - 1, 10), (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("2 часа 5 минут назад"); + expect(result.start).toBeDate(new Date(2012, 7 - 1, 9, 21, 55)); + }); +}); + +test("Test - Negative cases", function () { + testUnexpectedResult(chrono.ru, "15 часов 29 мин"); + testUnexpectedResult(chrono.ru, "несколько часов"); + testUnexpectedResult(chrono.ru, "5 дней"); +}); diff --git a/test/ru/ru_time_units_casual_relative.test.ts b/test/ru/ru_time_units_casual_relative.test.ts new file mode 100644 index 00000000..b6d63c92 --- /dev/null +++ b/test/ru/ru_time_units_casual_relative.test.ts @@ -0,0 +1,112 @@ +import * as chrono from "../../src"; +import { testSingleCase } from "../test_util"; + +test("Test - Positive time units", () => { + testSingleCase(chrono.ru, "следующие 2 недели", new Date(2016, 10 - 1, 1, 12), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2016, 10 - 1, 15, 12)); + }); + + testSingleCase(chrono.ru, "следующие 2 дня", new Date(2016, 10 - 1, 1, 12), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2016, 10 - 1, 3, 12)); + }); + + testSingleCase(chrono.ru, "следующие два года", new Date(2016, 10 - 1, 1, 12), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2018, 10 - 1, 1, 12)); + }); + + testSingleCase(chrono.ru, "следующие 2 недели 3 дня", new Date(2016, 10 - 1, 1, 12), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2016, 10 - 1, 18, 12)); + }); + + testSingleCase(chrono.ru, "через пару минут", new Date(2016, 10 - 1, 1, 12), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2016, 10 - 1, 1, 12, 2)); + }); + + testSingleCase(chrono.ru, "через полчаса", new Date(2016, 10 - 1, 1, 12), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2016, 10 - 1, 1, 12, 30)); + }); + + testSingleCase(chrono.ru, "через 2 часа", new Date(2016, 10 - 1, 1, 12), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2016, 10 - 1, 1, 14)); + }); + + testSingleCase(chrono.ru, "через три месяца", new Date(2016, 10 - 1, 1, 12), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2017, 1 - 1, 1, 12)); + }); + + testSingleCase(chrono.ru, "через неделю", new Date(2016, 10 - 1, 1, 12), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2016, 10 - 1, 8, 12)); + }); + + testSingleCase(chrono.ru, "через месяц", new Date(2016, 10 - 1, 1, 12), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2016, 11 - 1, 1, 12)); + }); + + testSingleCase(chrono.ru, "через год", new Date(2020, 11 - 1, 22, 12, 11, 32, 6), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2021, 11 - 1, 22, 12, 11, 32, 6)); + }); +}); + +test("Test - Negative time units", () => { + testSingleCase(chrono.ru, "прошлые 2 недели", new Date(2016, 10 - 1, 1, 12), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2016, 9 - 1, 17, 12)); + }); + + testSingleCase(chrono.ru, "прошлые два дня", new Date(2016, 10 - 1, 1, 12), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2016, 9 - 1, 29, 12)); + }); +}); + +test("Test - Plus '+' sign", () => { + testSingleCase(chrono.ru.casual, "+15 минут", new Date(2012, 7 - 1, 10, 12, 14), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2012, 7 - 1, 10, 12, 29)); + }); + + testSingleCase(chrono.ru.casual, "+15мин", new Date(2012, 7 - 1, 10, 12, 14), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2012, 7 - 1, 10, 12, 29)); + }); + + testSingleCase(chrono.ru.casual, "+1 день 2 часа", new Date(2012, 7 - 1, 10, 12, 14), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2012, 7 - 1, 11, 14, 14)); + }); +}); + +test("Test - Minus '-' sign", () => { + testSingleCase(chrono.ru.casual, "-3 года", new Date(2015, 7 - 1, 10, 12, 14), (result, text) => { + expect(result.index).toBe(0); + expect(result.text).toBe(text); + expect(result.start).toBeDate(new Date(2012, 7 - 1, 10, 12, 14)); + }); +}); diff --git a/test/ru/ru_time_units_within.test.ts b/test/ru/ru_time_units_within.test.ts new file mode 100644 index 00000000..2fb11bdf --- /dev/null +++ b/test/ru/ru_time_units_within.test.ts @@ -0,0 +1,16 @@ +import * as chrono from "../../src"; +import { testSingleCase } from "../test_util"; + +test("Test - The normal within expression", () => { + testSingleCase(chrono.ru, "будет сделано в течение минуты", new Date(2012, 7, 10), (result) => { + expect(result.index).toBe(14); + expect(result.text).toBe("в течение минуты"); + expect(result.start).toBeDate(new Date(2012, 7, 10, 0, 1)); + }); + + testSingleCase(chrono.ru, "будет сделано в течение 2 часов.", new Date(2012, 7, 10), (result) => { + expect(result.index).toBe(14); + expect(result.text).toBe("в течение 2 часов"); + expect(result.start).toBeDate(new Date(2012, 7, 10, 2)); + }); +}); diff --git a/test/ru/ru_weekday.test.ts b/test/ru/ru_weekday.test.ts new file mode 100644 index 00000000..c2d043e0 --- /dev/null +++ b/test/ru/ru_weekday.test.ts @@ -0,0 +1,52 @@ +import * as chrono from "../../src"; +import { testSingleCase } from "../test_util"; + +test("Test - Single Expression", function () { + testSingleCase(chrono.ru.casual, "понедельник", new Date(2012, 7, 9), (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("понедельник"); + expect(result.start).toBeDate(new Date(2012, 7, 6, 12)); + }); + + testSingleCase(chrono.ru.casual, "Дедлайн в пятницу...", new Date(2012, 7, 9), (result) => { + expect(result.index).toBe(8); + expect(result.text).toBe("в пятницу"); + expect(result.start).toBeDate(new Date(2012, 7, 10, 12)); + }); + + testSingleCase(chrono.ru.casual, "Дедлайн в прошлый четверг!", new Date(2012, 7, 9), (result) => { + expect(result.index).toBe(8); + expect(result.text).toBe("в прошлый четверг"); + expect(result.start).toBeDate(new Date(2012, 7, 2, 12)); + }); + + testSingleCase(chrono.ru.casual, "Дедлайн в следующий вторник", new Date(2015, 3, 18), (result) => { + expect(result.index).toBe(8); + expect(result.text).toBe("в следующий вторник"); + expect(result.start).toBeDate(new Date(2015, 3, 21, 12)); + }); +}); + +test("Test - Weekday With Casual Time", function () { + testSingleCase(chrono.ru.casual, "Позвони в среду утром", new Date(2015, 3, 18), (result) => { + expect(result.index).toBe(8); + expect(result.text).toBe("в среду утром"); + expect(result.start).toBeDate(new Date(2015, 3, 15, 6)); + }); +}); + +test("Test - Weekday Overlap", function () { + testSingleCase(chrono.ru.casual, "воскресенье, 7 декабря 2014", new Date(2012, 7, 9), (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("воскресенье, 7 декабря 2014"); + expect(result.start).toBeDate(new Date(2014, 12 - 1, 7, 12)); + }); +}); + +test("Test - forward dates only option", () => { + testSingleCase(chrono.ru.casual, "В понедельник?", new Date(2012, 7, 9), { forwardDate: true }, (result) => { + expect(result.index).toBe(0); + expect(result.text).toBe("В понедельник"); + expect(result.start).toBeDate(new Date(2012, 7, 13, 12)); + }); +});