11import { Logger } from "../util/logger" ;
22import { isNullOrUndefined , isNotNullOrUndefined , getDayOfWeekForString } from "../util/" ;
33import { Input , ParseConfidence } from "../model/input" ;
4- import moment = require( "moment" ) ;
54import { getMonthForString , getCurrentISOWeek } from "../util/dates" ;
65
76export type EntryGranularity = "daily" | "weekly" ;
@@ -27,7 +26,6 @@ interface TokenizeResult { tokens: Token[]; confidence: ParseConfidence; }
2726export 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 | l a s t / ) ) {
217- return now . subtract ( 1 , "week" ) . week ( ) ;
218- }
219-
220- if ( isNotNullOrUndefined ( modifier ) && modifier . match ( / n | n e x t / ) ) {
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