Как проанализировать частичную дату в ANTLR?

#parsing #antlr #antlr4

#синтаксический анализ #antlr #antlr4

Вопрос:

Я делаю первые шаги по использованию antlr4 и пытаюсь проанализировать частичную дату в европейском формате DD.MM.YYYY .

Я хочу распознать обычную дату, подобную 15.05.2020 or 7.5.20 , но также и даты, которые содержат только месяц и год, подобные 05.2020 or 5.20 , и в дополнение к этим датам, которые содержат только год, подобный 2020 or 20 . В моем приложении я хочу иметь доступ ко всем частям даты (день, месяц и год), в которых некоторые части могут быть пустыми / нулевыми.

Вот моя грамматика на данный момент.

 grammar LogicalDateExpressions;

stmt    :   date EOF
        ;

date    :   (YEAR)
        |   (MONTH DOT YEAR)
        |   (DAY DOT MONTH DOT YEAR)
        ;

YEAR    :   ([12] [0-9] [0-9] [0-9])
        |   ([0-9] [0-9])
        ;

MONTH   :   ('0'? [1-9])
        |   ('1' [012])
        ;

DAY     :   ('0'? [1-9])
        |   ([12][0-9])
        |   ('3'[01])
        ;

DOT     :   '.';
WS      :  [ trnu000C]  -> skip;
  

Эта грамматика работает с одним годом ( 2020 ), но не распознает комбинацию месяц-год ( 05.2020 ). grun -tokens сказал мне следующее.

 [@0,0:1='05',<YEAR>,1:0]
[@1,2:2='.',<'.'>,1:2]
[@2,3:6='2020',<YEAR>,1:3]
[@3,9:8='<EOF>',<EOF>,2:0]
line 1:2 mismatched input '.' expecting <EOF>
  

Итак, с моим небольшим разбором я решил, что проблема в правиле синтаксического date анализа, и я переписал его на

 date : (
          (DAY DOT)?      
          MONTH DOT     
       )?
       YEAR               
     ;
  

Но я все равно получил ту же ошибку. Затем я подумал, что, возможно, мне нужно изменить порядок правил лексера. Итак, вместо ГОДА -> МЕСЯЦА -> ДЕНЬ, я написал их ДЕНЬ -> МЕСЯЦ -> ГОД. Но grun сказал мне.

 [@0,0:1='05',<DAY>,1:0]
[@1,2:2='.',<'.'>,1:2]
[@2,3:6='2020',<YEAR>,1:3]
[@3,9:8='<EOF>',<EOF>,2:0]
line 1:3 mismatched input '2020' expecting MONTH
  

Я также попытался изменить порядок or’ed альтернатив в правиле синтаксического date анализа, но это тоже не сработало. Затем я попытался изменить правила лексера ДЕНЬ, МЕСЯЦ, ГОД, чтобы сделать их правилами синтаксического анализа (день, месяц, год). После получения некоторых ошибок, потому что, по-видимому, обозначение [0-9] не разрешено в правилах синтаксического анализа, я изменил грамматику на это.

 date    :   (year)
        |   (month DOT year)
        |   (day DOT month DOT year)
        ;

[...]
  
year    :   (('1'|'2') DIGIT DIGIT DIGIT)
        |   (DIGIT DIGIT)
        ;

month   :   ('0'? DIGIT_NO_ZERO)
        |   ('1' ('0'|'1'|'2'))
        ;

day     :   ('0'? DIGIT_NO_ZERO)
        |   (('1'|'2') DIGIT)
        |   ('3' ('0'|'1'))
        ;

[...]

DIGIT         :   [0-9];
DIGIT_NO_ZERO :   [1-9];
  

Это тоже был облом. grun сказал мне.

 [@0,0:0='0',<'0'>,1:0]
[@1,1:1='5',<DIGIT>,1:1]
[@2,2:2='.',<'.'>,1:2]
[@3,3:3='2',<'2'>,1:3]
[@4,4:4='0',<'0'>,1:4]
[@5,5:5='2',<'2'>,1:5]
[@6,6:6='0',<'0'>,1:6]
[@7,9:8='<EOF>',<EOF>,2:0]
line 1:1 no viable alternative at input '05'
  

Насколько я понимаю, язык, который я ищу, является обычным. И каждый ввод однозначен. Итак, я попытался поместить всю «логику» в лексер, и мне это удалось со следующей грамматикой.

 grammar LogicalDateExpressions;

stmt :   date EOF
     ;

date :   DT
     ;

DT   :  (
            ((('0'? [1-9])|([12][0-9])|('3'[01])) DOT)? // Day
            (('0'? [1-9])|('1' [012])) DOT              // Month
        )?
        ((DIGIT DIGIT DIGIT DIGIT)|(DIGIT DIGIT))       // Year
    ;

DIGIT   :   [0-9];
DOT     :   '.';
WS      :  [ trnu000C]  -> skip;
  

Он анализирует каждый ввод, который я ему даю. Но проблема в том, что каждый ввод — это просто DT .

 [@0,0:6='05.2020',<DT>,1:0]
[@1,9:8='<EOF>',<EOF>,2:0]
  

Я не могу различить день, месяц и год в посетителе / слушателе, потому что метки не разрешены в правилах lexer.


Итак, мой вопрос в том, в чем проблема с первой заданной грамматикой и что мне нужно изменить, чтобы она работала?

Взглянув на вывод токена из grun, я думаю, что могу понять проблему, каждый ввод за день, месяц и / или год может быть неоднозначным, но в целом ввод в сочетании с точками не должен быть. Как я могу сообщить об этом antlr?

Ответ №1:

Итак, мой вопрос в том, в чем проблема с первой заданной грамматикой и что мне нужно изменить, чтобы она работала?

Проблема в том, что лексер не управляется синтаксическим анализатором. Это означает, что когда анализатор пытается сопоставить токены DAY DOT MONTH и входные 01.01 данные, лексер создаст не a DAY и a MONTH для этих двух 01 , а два MONTH токена. Вот как работает лексер ANTLR: попробуйте захватить как можно больше символов для токена, и когда есть 2 или более токенов, которые соответствуют одним и тем же символам (например 01 , могут совпадать оба DAY и MONTH ), пусть сначала «выиграет» определенный токен (который является MONTH токеном). Обойти это невозможно.

Что вы могли бы сделать, это что-то вроде этого (непроверенный):

 date
 : year
 | month DOT year
 | day DOT month DOT year
 ;

day
 : N_01_12
 | N_13_31
 ;

month
 : N_01_12
 ;

year
 : N_01_12
 | N_13_31
 | N_32_99
 | N_1000_2999
 ;

N_01_12
 : '0'? D    // 01-09
 | '1' [0-2] // 10-12
 ;

N_13_31
 : '1' [3-9] // 13-19
 | '2' D     // 20-29
 | '3' [01]  // 30-31
 ;

N_32_99
 : '3' [2-9] // 32-39
 | [4-9] D   // 40-99
 ;

N_1000_2999
 : [12] D D D // 1000-2999
 ;

fragment D : [0-9];
  

Комментарии:

1. спасибо за объяснение и код, я думаю, что понимаю. Кстати. код работает отлично. В любом случае, правильно ли это с технической точки зрения, если я скажу, что лексер в моем примере неоднозначен?

2. Я бы так это не назвал, хотя я бы понял, что люди так думают. Из-за того, как работает лексер ANTLR (совпадение жадное, и в случае ничьей выигрывает первое правило), двусмысленности на самом деле нет. И, конечно, пожалуйста!