Skip to content

Commit 1da4e42

Browse files
committed
feat(match-input): cut over to tokenizer, remove regex and moment
Step 10 of plan #177: - parseInput() now calls tokenize() + tokensToInput() directly - Deleted: getExpression(), getMonthPattern(), getWeekdayPattern(), extractText/Flags/Offset/Week/Tags, hasTemporalToken, resolveRelatedWeek, resolveNumberedWeek, resolveDayOfMonth, this.expr cache field - Removed: import moment (no moment calls remain in this file) - resolveRelatedWeekNM / resolveDayOfMonthNM are the authoritative week/month-day resolvers going forward #177
1 parent 11857a8 commit 1da4e42

1 file changed

Lines changed: 5 additions & 336 deletions

File tree

src/journal/match-input.ts

Lines changed: 5 additions & 336 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Logger } from "../util/logger";
22
import { isNullOrUndefined, isNotNullOrUndefined, getDayOfWeekForString } from "../util/";
33
import { Input, ParseConfidence } from "../model/input";
4-
import moment = require("moment");
54
import { getMonthForString, getCurrentISOWeek } from "../util/dates";
65

76
export type EntryGranularity = "daily" | "weekly";
@@ -27,7 +26,6 @@ interface TokenizeResult { tokens: Token[]; confidence: ParseConfidence; }
2726
export class MatchInput {
2827
public today: Date;
2928
private scopeExpression: RegExp = /\s#\w+\s/;
30-
private expr: RegExp | undefined;
3129
private _weekdayPatterns: RegExp[] | undefined;
3230
private _monthPatterns: RegExp[] | undefined;
3331

@@ -61,61 +59,12 @@ export class MatchInput {
6159
}
6260

6361
try {
64-
// --- regex path (authoritative during parallel phase) ---
65-
const parsedInput = new Input();
66-
67-
const res: RegExpMatchArray | null = inputString.match(this.getExpression());
68-
if (res === null) {
69-
throw new Error("cancel");
70-
}
71-
72-
this.logger.trace(Object.entries(res!.groups!).map(([key, value]) => `${key}: ${value}`).join(', '));
73-
74-
parsedInput.flags = this.extractFlags(res!);
75-
parsedInput.offset = this.extractOffset(res!);
76-
parsedInput.week = this.extractWeek(res!);
77-
parsedInput.text = this.extractText(res!);
62+
const { tokens, confidence } = this.tokenize(inputString);
63+
const parsedInput = this.tokensToInput(tokens);
7864
parsedInput.tags = this.extractTags(inputString);
65+
parsedInput.confidence = confidence;
7966

80-
const userProvidedTemporalToken = this.hasTemporalToken(res!);
81-
82-
if (parsedInput.hasFlags() && !parsedInput.hasMemo()) {
83-
throw new Error("No text found for memo or task");
84-
}
85-
86-
if (!parsedInput.hasFlags() && parsedInput.hasMemo()) {
87-
parsedInput.flags = "memo";
88-
}
89-
90-
if (!userProvidedTemporalToken && !parsedInput.hasWeek()) {
91-
if (this.granularity === "weekly") {
92-
parsedInput.week = moment().week();
93-
parsedInput.offset = NaN;
94-
} else {
95-
parsedInput.offset = 0;
96-
}
97-
}
98-
99-
// --- tokenizer path (parallel, not yet authoritative) ---
100-
try {
101-
const { tokens, confidence } = this.tokenize(inputString);
102-
const tokenInput = this.tokensToInput(tokens);
103-
tokenInput.tags = this.extractTags(inputString);
104-
tokenInput.confidence = confidence;
105-
106-
if (!this.inputsEqual(parsedInput, tokenInput)) {
107-
this.logger.debug(
108-
"tokenizer divergence for input '", inputString, "':",
109-
JSON.stringify({ regex: { offset: parsedInput.offset, week: parsedInput.week, flags: parsedInput.flags, text: parsedInput.text }, tokenizer: { offset: tokenInput.offset, week: tokenInput.week, flags: tokenInput.flags, text: tokenInput.text } })
110-
);
111-
} else {
112-
parsedInput.confidence = confidence;
113-
}
114-
} catch (tokErr) {
115-
this.logger.debug("tokenizer error for input '", inputString, "':", tokErr instanceof Error ? tokErr.message : String(tokErr));
116-
}
117-
118-
this.logger.trace("Tokenized input: ", JSON.stringify(parsedInput));
67+
this.logger.trace("Parsed input: ", JSON.stringify(parsedInput));
11968
return parsedInput;
12069

12170
} catch (error) {
@@ -128,14 +77,6 @@ export class MatchInput {
12877
}
12978
}
13079

131-
private inputsEqual(a: Input, b: Input): boolean {
132-
const nanEq = (x: number, y: number) => (isNaN(x) && isNaN(y)) || x === y;
133-
return nanEq(a.offset, b.offset)
134-
&& a.week === b.week
135-
&& a.flags === b.flags
136-
&& a.text === b.text;
137-
}
138-
13980

14081
/**
14182
* If tags are present in the input string, extract them if these are configured scopes
@@ -154,120 +95,6 @@ export class MatchInput {
15495

15596

15697

157-
private extractText(inputGroups: RegExpMatchArray): string {
158-
const text = inputGroups.groups!["text"];
159-
/* Groups
160-
10: text of memo
161-
*/
162-
return isNotNullOrUndefined(text) ? text : "";
163-
}
164-
165-
166-
/**
167-
* Returns true when the user explicitly typed a temporal token
168-
* (shortcut, offset, ISO date, weekday, week reference, or month + day).
169-
* Distinguishes "the user said today" from "the user said nothing and we
170-
* picked a default."
171-
*/
172-
private hasTemporalToken(inputGroups: RegExpMatchArray): boolean {
173-
const g = inputGroups.groups!;
174-
return isNotNullOrUndefined(g["shortcut"])
175-
|| isNotNullOrUndefined(g["offset"])
176-
|| isNotNullOrUndefined(g["iso"])
177-
|| isNotNullOrUndefined(g["weekday"])
178-
|| isNotNullOrUndefined(g["week"])
179-
|| isNotNullOrUndefined(g["weekNum"])
180-
|| (isNotNullOrUndefined(g["month"]) && isNotNullOrUndefined(g["dayOfMonth"]));
181-
}
182-
183-
private extractFlags(inputGroups: RegExpMatchArray): string {
184-
const flagPre = inputGroups.groups!["flag"];
185-
const flagPost = inputGroups.groups!["flagPost"];
186-
187-
if (isNotNullOrUndefined(flagPre)) { return flagPre; }
188-
if (isNotNullOrUndefined(flagPost)) { return flagPost; }
189-
return "";
190-
}
191-
192-
/**
193-
* Tries to extract the mentioned week
194-
*
195-
*
196-
*/
197-
extractWeek(inputGroups: RegExpMatchArray): number {
198-
let week = inputGroups.groups!["week"];
199-
let weekNum = inputGroups.groups!["weekNum"];
200-
let modifier = inputGroups.groups!["modifier"];
201-
202-
if (isNotNullOrUndefined(weekNum)) {
203-
return this.resolveNumberedWeek(weekNum);
204-
}
205-
206-
if (isNotNullOrUndefined(week)) {
207-
return this.resolveRelatedWeek(modifier);
208-
}
209-
210-
return -1;
211-
212-
}
213-
resolveRelatedWeek(modifier: string): number {
214-
let now = moment();
215-
216-
if (isNotNullOrUndefined(modifier) && modifier.match(/l|last/)) {
217-
return now.subtract(1, "week").week();
218-
}
219-
220-
if (isNotNullOrUndefined(modifier) && modifier.match(/n|next/)) {
221-
return now.add(1, "week").week();
222-
}
223-
224-
return now.week();
225-
}
226-
227-
/**
228-
*
229-
* @param weekAsNumber numbered week, e.g. "w13"
230-
*/
231-
resolveNumberedWeek(weekAsNumber: string): number {
232-
return parseInt(weekAsNumber);
233-
}
234-
235-
236-
private extractOffset(inputGroups: RegExpMatchArray): number {
237-
let shortcut = inputGroups.groups!["shortcut"];
238-
let offset = inputGroups.groups!["offset"];
239-
let iso = inputGroups.groups!["iso"];
240-
let weekday = inputGroups.groups!["weekday"];
241-
let modifier = inputGroups.groups!["modifier"];
242-
let dayOfMonth = inputGroups.groups!["dayOfMonth"];
243-
let month = inputGroups.groups!["month"];
244-
245-
if (isNotNullOrUndefined(shortcut)) {
246-
return this.resolveShortcutString(shortcut);
247-
}
248-
if (isNotNullOrUndefined(offset)) {
249-
return this.resolveOffsetString(offset);
250-
}
251-
if (isNotNullOrUndefined(iso)) {
252-
return this.resolveISOString(iso);
253-
}
254-
if (isNotNullOrUndefined(weekday)) {
255-
return this.resolveWeekday(weekday, modifier);
256-
}
257-
258-
if (isNotNullOrUndefined(month) && isNotNullOrUndefined(dayOfMonth)) {
259-
260-
return this.resolveDayOfMonth(month, dayOfMonth);
261-
}
262-
263-
264-
// default, we always return zero (as today)
265-
return 0;
266-
}
267-
268-
269-
270-
27198
private resolveOffsetString(inputString: string): number {
27299
if (inputString.startsWith("+", 0)) {
273100
return parseInt(inputString.substring(1, inputString.length));
@@ -381,20 +208,6 @@ export class MatchInput {
381208
return NaN;
382209
}
383210

384-
/**
385-
* Parses strings like "Jun 1" and returns the offset from today
386-
*
387-
* @param month
388-
* @param dayOfMonth
389-
* @returns
390-
*/
391-
private resolveDayOfMonth(month: string, dayOfMonth: string): number {
392-
let current = moment();
393-
let date = moment().month(getMonthForString(month)).date(parseInt(dayOfMonth));
394-
let diff = date.diff(current, "days");
395-
return diff;
396-
}
397-
398211

399212
// -------------------------------------------------------------------------
400213
// Tokenizer — Steps 2-5 of the plan
@@ -740,148 +553,4 @@ export class MatchInput {
740553
'يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو', 'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر',
741554
];
742555
}
743-
744-
/**
745-
* Takes any given string as input and tries to compute the offset from today's date.
746-
* It translates something like "next wednesday" into "4" (if next wednesday is in four days).
747-
*
748-
* @param {string} value the string to be processed
749-
* @returns {Q.Promise<number>} the resolved offeset
750-
* @memberof Parser
751-
*/
752-
private getExpression(): RegExp {
753-
/*
754-
v6 with week modifier https://regex101.com/r/sCtPOb/6
755-
(?:(task|todo)\s)?(?:(?:(today|tod|yesterday|yes|tomorrow|tom|0)(?:\s|$))|(?:((?:\+|\-)\d+)(?:\s|$))|(?:((?:\d{4}\-\d{1,2}\-\d{1,2})|(?:\d{1,2}\-\d{1,2})|(?:\d{1,2}))(?:\s|$))|(?:(next|last|n|l)?\s?(monday|tuesday|wednesday|thursday|friday|saturday|sunday|mon|tue|wed|thu|fri|sat|sun|montag|dienstag|mittwoch|donnerstag|freitag|samstag|sonntag)\s?))?(?:(task|todo)\s)?(.*)
756-
757-
v8 (with Month + Day) https://regex101.com/r/sCtPOb/7
758-
^(?:(?<flag>task|todo)\s)?(?:(?:(?:(?<shortcut>today|tod|yesterday|yes|tomorrow|tom|0)(?:\s|$)))|(?:(?<offset>(?:\+|\-)\d+)(?:\s|$))|(?:(?<iso>(?:\d{4}(?:\-|\\)\d{1,2}(?:\-|\\)\d{1,2})|(?:\d{1,2}(?:\-|\\)\d{1,2})|(?:\d{1,2}))(?:\s|$))|(?:(?<modifier>next|last|n|l)?\s?(?:(?<weekday>monday|tuesday|wednesday|thursday|friday|saturday|sunday|mon|tue|wed|thu|fri|sat|sun|montag|dienstag|mittwoch|donnerstag|freitag|samstag|sonntag)?|(?<week>w(?:eek)?(?:\s\D|$)))?\s?)|(?:w(?:eek)?\s?(?<weekNum>[1-5]?[0-9])(?:\s|$))|(?:(?<month>Jan|Feb|Mar|Apr|Apr(?:il)?|May|June?|July?|Aug(?:gust)?|Sep(?:tember)?|Oct(?:ober)?|Nov|Dec)+)+\s?(?<dayOfMonth>(?:[1-9]|1[0-9]|2[0-9]|3[0-1])(?:\s|$))+)?(?:(?<flagPost>task|todo)\s)?(?<text>.*)$
759-
760-
761-
762-
763-
Groups (see https://regex101.com/r/sCtPOb) (! // -> /)
764-
1: flag "task"
765-
2: shortcut "today"
766-
3: offset "+1"
767-
4: iso date "2012-12-23"
768-
5: month and day "12-23"
769-
6: day of month "23"
770-
7: weekday flag "next"
771-
8: weekday name "monday"
772-
9: flag "task"
773-
10: text of memo
774-
775-
776-
0:"..."
777-
1:task
778-
2:today
779-
3:+22
780-
4:11-24
781-
5:"next"
782-
6:"monday"
783-
7:"task"
784-
8:"hello world"
785-
*/
786-
if (isNullOrUndefined(this.expr)) {
787-
// Regular expression components
788-
const flagPattern = '(?<flag>task|todo)?\\s?';
789-
const shortcutPattern = '(?<shortcut>today|tod|yesterday|yes|tomorrow|tom|0)(?:\\s|$)';
790-
const offsetPattern = '(?<offset>(?:\\+|\\-)\\d+)(?:\\s|$)';
791-
const isoPattern = '(?<iso>(?:\\d{4}(?:\\-|\\/)\\d{1,2}(?:\\-|\\/)\\d{1,2})|(?:\\d{1,2}(?:\\-|\\/)\\d{1,2})|(?:\\d{1,2}))(?:\\s|$)';
792-
const modifierPattern = '(?<modifier>next|last|n\\b|l\\b)?\\s?';
793-
// const weekdayPattern = '(?<weekday>monday|tuesday|wednesday|thursday|friday|saturday|sunday|mon|tue|wed|thu|fri|sat|sun|montag|dienstag|mittwoch|donnerstag|freitag|samstag|sonntag)?';
794-
const weekdayPattern = this.getWeekdayPattern();
795-
const weekPattern = '(?<week>w(?:eek)?(?:\\s\\D|$))';
796-
const weekNumPattern = 'w(?:eek)?\\s?(?<weekNum>[1-5]?[0-9])(?:\\s|$)';
797-
// const monthPattern = '(?<month>Jan|Feb|Mar|Apr|Apr(?:il)?|May|June?|July?|Aug(?:gust)?|Sep(?:tember)?|Oct(?:ober)?|Nov|Dec)+';
798-
const monthPattern = this.getMonthPattern();
799-
const dayOfMonthPattern = '\\s?(?<dayOfMonth>(?:[1-9]|1[0-9]|2[0-9]|3[0-1])(?:\\s|$))+';
800-
const flagPostPattern = '(?<flagPost>task|todo)?\\s?';
801-
const textPattern = '(?<text>.*)';
802-
803-
//'(?<weekday>monday|tuesday|wednesday|thursday|friday|saturday|sunday|mon|tue|wed|thu|fri|sat|sun|montag|dienstag|mittwoch|donnerstag|freitag|samstag|sonntag|lun(?:di)?|mar(?:di)?|mer(?:credi)?|jeu(?:di)?|ven(?:dredi)?|sam(?:edi)?|dim(?:anche)?|lunes?|martes?|mié(?:rcoles)?|jueves?|viernes?|sáb(?:ado)?|dom(?:ingo)?|lunedì|martedì|mercoledì|giovedì|venerdì|sabato|domenica|segunda-feira|terça-feira|quarta-feira|quinta-feira|sexta-feira|sábado|domingo|maandag|dinsdag|woensdag|donderdag|vrijdag|zaterdag|zondag|понедельник|вторник|среда|четверг|пятница|суббота|воскресенье|xīngqī yī|xīngqī èr|xīngqī sān|xīngqī sì|xīngqī wǔ|xīngqī liù|xīngqī rì|getsuyōbi|kayōbi|suiyōbi|mokuyōbi|kin'yōbi|doyōbi|nichiyōbi|الإثنين|الثلاثاء|الأربعاء|الخميس|الجمعة|السبت|الأحد)?'
804-
805-
// Full regular expression
806-
const regExpPattern = `^${flagPattern}(?:${shortcutPattern}|${offsetPattern}|${isoPattern}|${modifierPattern}(?:${weekdayPattern}|${weekPattern})?\\s?|${weekNumPattern}|${monthPattern}${dayOfMonthPattern})?${flagPostPattern}${textPattern}$`;
807-
808-
// Compile the regular expression
809-
this.expr = new RegExp(regExpPattern, 'i');
810-
}
811-
812-
return this.expr!;
813-
}
814-
815-
816-
private getMonthPattern(): string {
817-
// Issue #170: wrap the alternation in a non-capturing group with a trailing
818-
// (?=\s|$) lookahead so e.g. "Marathon" doesn't match "Mar". The named
819-
// group keeps the same alternation; the boundary asserts without consuming.
820-
// Alternations are joined without leading whitespace so long alternatives
821-
// like "January" remain reachable (template-literal indentation otherwise
822-
// becomes part of the pattern and prefixes the first alternative on each line).
823-
const alternatives = [
824-
// English
825-
'Jan(?:uary)?', 'Feb(?:ruary)?', 'Mar(?:ch)?', 'Apr(?:il)?', 'May', 'June?', 'July?', 'Aug(?:ust)?', 'Sep(?:tember)?', 'Oct(?:ober)?', 'Nov(?:ember)?', 'Dec(?:ember)?',
826-
// German
827-
'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'Okt(?:ober)?', 'Dez(?:ember)?',
828-
// French
829-
'Janv(?:ier)?', 'Fév(?:rier)?', 'Mars', 'Avr(?:il)?', 'Juin', 'Juil(?:let)?', 'Août', 'Sept(?:embre)?', 'Oct(?:obre)?', 'Nov(?:embre)?', 'Déc(?:embre)?',
830-
// Spanish
831-
'Ene(?:ro)?', 'Feb(?:rero)?', 'Mar(?:zo)?', 'Abr(?:il)?', 'May(?:o)?', 'Jun(?:io)?', 'Jul(?:io)?', 'Ago(?:sto)?', 'Sep(?:tiembre)?', 'Oct(?:ubre)?', 'Nov(?:iembre)?', 'Dic(?:iembre)?',
832-
// Italian
833-
'Gen(?:naio)?', 'Feb(?:braio)?', 'Mag(?:gio)?', 'Giu(?:gno)?', 'Lug(?:lio)?', 'Set(?:tembre)?', 'Ott(?:obre)?', 'Dic(?:embre)?',
834-
// Portuguese
835-
'Jan(?:eiro)?', 'Fev(?:ereiro)?', 'Mar(?:ço)?', 'Mai(?:o)?', 'Jun(?:ho)?', 'Jul(?:ho)?', 'Set(?:embro)?', 'Out(?:ubro)?', 'Nov(?:embro)?', 'Dez(?:embro)?',
836-
// Dutch
837-
'Jan(?:uari)?', 'Feb(?:ruari)?', 'Mrt', 'Mei', 'Jun(?:i)?', 'Jul(?:i)?', 'Aug(?:ustus)?',
838-
// Russian
839-
'Янв(?:арь)?', 'Фев(?:раль)?', 'Мар(?:т)?', 'Апр(?:ель)?', 'Май', 'Июн(?:ь)?', 'Июл(?:ь)?', 'Авг(?:уст)?', 'Сен(?:тябрь)?', 'Окт(?:ябрь)?', 'Ноя(?:брь)?', 'Дек(?:абрь)?',
840-
// Chinese (Pinyin)
841-
'yīyuè', 'èryuè', 'sānyuè', 'sìyuè', 'wǔyuè', 'liùyuè', 'qīyuè', 'bāyuè', 'jiǔyuè', 'shíyuè', 'shíyīyuè', "shí'èryuè",
842-
// Japanese (Romaji)
843-
'ichigatsu', 'nigatsu', 'sangatsu', 'shigatsu', 'gogatsu', 'rokugatsu', 'shichigatsu', 'hachigatsu', 'kugatsu', 'jugatsu', 'juichigatsu', 'juunigatsu',
844-
// Arabic
845-
'يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو', 'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر',
846-
].join('|');
847-
return `(?:(?<month>${alternatives})(?=\\s|$))+`;
848-
}
849-
850-
private getWeekdayPattern(): string {
851-
// Issue #170: the inner alternation lists bare two-letter weekday prefixes
852-
// (do, di, fr, sa, ...) which would otherwise substring-match inside ordinary
853-
// words like "Don Julio" or "Doel halen". The (?=\s|$) lookahead at the tail
854-
// (placed inside the outer optional group so it only fires when the weekday
855-
// actually matched) requires the token to end on a word boundary.
856-
// Alternations are listed longest-first inside each locale and joined without
857-
// leading whitespace so the named group can match the full forms.
858-
const alternatives = [
859-
// English (full first, then 3-letter abbreviations)
860-
'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday',
861-
'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun',
862-
// German (full first, then 2-3 letter abbreviations)
863-
'montag', 'dienstag', 'mittwoch', 'donnerstag', 'freitag', 'samstag', 'sonntag',
864-
'mit', 'di', 'do', 'fr', 'sa', 'so',
865-
// French
866-
'lun(?:di)?', 'mar(?:di)?', 'mer(?:credi)?', 'jeu(?:di)?', 'ven(?:dredi)?', 'sam(?:edi)?', 'dim(?:anche)?',
867-
// Spanish
868-
'lunes?', 'martes?', 'mié(?:rcoles)?', 'jueves?', 'viernes?', 'sáb(?:ado)?', 'dom(?:ingo)?',
869-
// Italian
870-
'lunedì', 'martedì', 'mercoledì', 'giovedì', 'venerdì', 'sabato', 'domenica',
871-
// Portuguese
872-
'segunda-feira', 'terça-feira', 'quarta-feira', 'quinta-feira', 'sexta-feira', 'sábado', 'domingo',
873-
// Dutch
874-
'maandag', 'dinsdag', 'woensdag', 'donderdag', 'vrijdag', 'zaterdag', 'zondag',
875-
// Russian
876-
'понедельник', 'вторник', 'среда', 'четверг', 'пятница', 'суббота', 'воскресенье',
877-
// Chinese (Pinyin)
878-
'xīngqī yī', 'xīngqī èr', 'xīngqī sān', 'xīngqī sì', 'xīngqī wǔ', 'xīngqī liù', 'xīngqī rì',
879-
// Japanese (Romaji)
880-
'getsuyōbi', 'kayōbi', 'suiyōbi', 'mokuyōbi', "kin'yōbi", 'doyōbi', 'nichiyōbi',
881-
// Arabic
882-
'الإثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت', 'الأحد',
883-
].join('|');
884-
return `(?:(?<weekday>${alternatives})(?=\\s|$))?`;
885-
}
886-
887-
}
556+
}

0 commit comments

Comments
 (0)