Грамматики (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), чтобы разобранную строку превратить в полноценное число.