Como analisar uma data parcial em ANTLR?
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
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];