Грамматики (grammars) в Perl 6 — огромная бесконечная тема, не имеющая аналогов в других языках программирования. Эта часть языка отлично проработана и используется для парсинга самого Perl 6.
Я планирую возвращаться к этой теме время от времени, а сегодня мы начнем с разбора чисел. Разумеется, это можно сделать с помощью регулярных выражений, но и грамматики отлично подойдут.
Для начала создадим набор тестовых данных:
my @tests = < 1 -1 +1 123 -123 1.2 -1.2 10000000 -10000000 .23 -.23 1e2 1E2 1e-2 1e+2 -1E-2 1.2E3 .2E3 -.2E3 >;
Начнем писать грамматику с простейшего случая, когда все число является лишь последовательностью цифр:
grammar Number { token TOP { <number> } token number { <digit>+ } }
Грамматика начинается с главного токена TOP, который должен совпасть со всей строкой целиком. В данном случае этот токен содержит только токен number, который является быть последовательностью цифр. Правило digit встроено в язык.
Пройдемся по тестовым строкам и разберем их с помощью существующей грамматики:
for @tests -> $value { my $result = Number.parse($value); my $check = $result ?? '✓' !! '✗'; say "$check $value"; }
Запускаем программу и смотрим на результаты:
✓ 1 ✗ -1 ✗ +1 ✓ 123 ✗ -123 ✗ 1.2 ✗ -1.2 ✓ 10000000 ✗ -10000000 ✗ .23 ✗ -.23 ✗ 1e2 ✗ 1E2 ✗ 1e-2 ✗ 1e+2 ✗ -1E-2 ✗ 1.2E3 ✗ .2E3 ✗ -.2E3
Есть еще над чем поработать. Начнем с добавления необязательного знака:
grammar Number { token TOP { <number> } token number { <sign>? <digit>+ } token sign { '+' | '-' } }
Число успешных тестов немного увеличилось:
✓ 1 ✓ -1 ✓ +1 ✓ 123 ✓ -123 ✗ 1.2 ✗ -1.2 ✓ 10000000 ✓ -10000000 ✗ .23 ✗ -.23 ✗ 1e2 ✗ 1E2 ✗ 1e-2 ✗ 1e+2 ✗ -1E-2 ✗ 1.2E3 ✗ .2E3 ✗ -.2E3
Теперь можно добавить десятичную дробь и разделить целую и дробные части числа на отдельные токены. Само число будет собираться из этих частей, каждая из которых необязательна.
grammar Number { token TOP { <sign>? <number> } token number { | <comma> <fractional> | <integer> <comma> <fractional> | <integer> <comma> | <integer> } token sign { '+' | '-' } token integer { <digit>+ } token fractional { <digit>+ } token comma { '.' } }
Здесь я создал несколько альтернатив, чтобы не путаться с модификаторами у отдельных частей, и заодно перенес знак в стартовый токен. Одновременно стало видно, что не хватает тестов для редких, но допустимых случаев, когда у числа есть точка, но нет дробной части. Проверяем:
✓ 1 ✓ -1 ✓ +1 ✓ 123 ✓ -123 ✓ 1.2 ✓ -1.2 ✓ 10000000 ✓ -10000000 ✓ .23 ✓ -.23 ✗ 1e2 ✗ 1E2 ✗ 1e-2 ✗ 1e+2 ✗ -1E-2 ✗ 1.2E3 ✗ .2E3 ✗ -.2E3 ✓ 1. ✓ -2.
Отлично. Добавляем правила для разбора научной записи:
grammar Number { token TOP { <sign>? <number> [ ['e' | 'E'] <sign>? <integer> ]? } token number { | <comma> <fractional> | <integer> <comma> <fractional> | <integer> <comma> | <integer> } token sign { '+' | '-' } token integer { <digit>+ } token fractional { <digit>+ } token comma { '.' } }
Проверка показывает, что все тесты успешно проходят:
✓ 1 ✓ -1 ✓ +1 ✓ 123 ✓ -123 ✓ 1.2 ✓ -1.2 ✓ 10000000 ✓ -10000000 ✓ .23 ✓ -.23 ✓ 1e2 ✓ 1E2 ✓ 1e-2 ✓ 1e+2 ✓ -1E-2 ✓ 1.2E3 ✓ .2E3 ✓ -.2E3 ✓ 1. ✓ -2.
На сегодня это все. Созданная грамматика смогла разобрать все запланированные варианты. План на следующий раз — дополнить грамматику действиями (actions), чтобы разобранную строку превратить в полноценное число.