Como analisar uma data parcial em ANTLR?

Sep 30 2020

Estou dando meus primeiros passos para usar o antlr4 e tentar analisar uma data parcial no formato europeu DD.MM.YYYY.

Desejo reconhecer uma data normal como 15.05.2020ou, 7.5.20mas também datas que contêm apenas mês e ano como 05.2020ou 5.20e além daquelas datas que contêm apenas fora de um ano como 2020ou 20. Em meu aplicativo, desejo ter acesso a todas as partes de uma data (dia, mês e ano) em que algumas partes podem estar vazias / nulas.

Aqui está minha gramática até agora.

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      :  [ \t\r\n\u000C]+ -> skip;

Esta gramática funciona com um único ano ( 2020), mas não reconhece uma combinação mês-ano ( 05.2020). grun -tokensme disse o seguinte.

[@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>

Então, com o meu conhecimento, descobri que a regra do analisador dateé o problema e reescrevi

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

Mas ainda recebo o mesmo erro. Então pensei que talvez eu precisasse reordenar as regras do Lexer. Então, em vez de ANO -> MÊS -> DIA, escrevi DIA -> MÊS -> ANO. Mas grunme disse.

[@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

Também tentei alterar a ordem das alternativas or'ed na regra do analisador, datemas também não funcionou. Então tentei mudar as regras do lexer DAY, MONTH, YEAR para torná-las regras do parser (dia, mês, ano). Depois de obter alguns erros porque aparentemente a notação [0-9] não é permitida nas regras do analisador, mudei a gramática para isso.

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];

Isso também foi uma chatice. grunme disse.

[@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'

Pelo que entendi, o idioma que procuro é regular. E cada entrada é inequívoca. Então, tentei colocar toda a "lógica" no lexer e tive sucesso com a seguinte gramática.

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      :  [ \t\r\n\u000C]+ -> skip;

Ele analisa cada entrada que eu dou. Mas o problema é que cada entrada é apenas um DT.

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

Não consigo distinguir entre o dia, o mês e o ano em um visitante / ouvinte porque rótulos não são permitidos em regras lexer.


Portanto, minha pergunta é onde está o problema com a primeira gramática fornecida e o que preciso alterar para que funcione?

Olhando para a saída de token do grun, acho que posso entender o problema, cada entrada para um dia, mês e / ou ano pode ser ambígua, mas como uma entrada inteira em conjunto com os pontos, não deveria ser. Como posso saber isso?

Respostas

1 BartKiers Sep 30 2020 at 15:02

Portanto, minha pergunta é onde está o problema com a primeira gramática fornecida e o que preciso alterar para que funcione?

O problema é que o lexer não é controlado pelo analisador. O que isso significa é que quando o analisador tenta combinar os tokens DAY DOT MONTHe a entrada é 01.01, o lexer não criará um DAYe um MONTHpara esses dois 01, mas dois MONTHtokens. É assim que o lexer do ANTLR funciona: tente pegar o máximo de caracteres para um token, e quando houver 2 ou mais tokens que correspondam aos mesmos caracteres (como 01pode ser correspondido por ambos DAYe MONTH), deixe o token definido primeiro "vencer" (que é o MONTHtoken). Não há maneira de contornar isso.

O que você poderia fazer é algo assim (não testado):

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];