Начнём с исполняемой части программы (но при этом, как обычно, расположим её
в конце). Она включает в себя приём параметра из командной строки, вызов
процедуры toRoman
, которая делает все вычисления, вывод
сообщений об ошибках в случае отсутствующего или недопустимого параметра
и печать результата:
Perldie "$0: Требуется натуральное число от 1 до 3999\n" unless defined(my $decimal=shift); die "$0: Неправильное число: «$decimal»\n" unless defined(my $roman=toRoman($decimal)); print "$roman\n";
Для краткости мы совместили объявления и присваивания переменным
$decimal
и $roman
с проверкой их
определённости. Здесь необходимо заметить, что процедура
toRoman
должна возвратить неопределённое значение, если
получит недопустимое значение параметра, не являющееся целым числом, или
выходящее за пределы промежутка
.
В случае ошибки (отсутствующий или недопустимый параметр) выполнение программы
завершается по команде die
с выдачей соответствующего
сообщения.
Исполняемой части будет предшествовать декларативная, состоящая из определений
процедур toRoman
и toRomanHelper
.
Тело процедуры toRoman
начинается с получения значения
параметра в переменную $n
и его проверки:
Perlsub toRoman($) { my $n=shift; return if $n!~m/^\d+$/ or $n>=4000; …
Если значение не удовлетворяет необходимым требованиям (не состоит целиком из
десятичных цифр или превышает ),
возвращается неопределённое значение (undef
). На наше
счастье, «ленивое» вычисление логических выражений в Perl не допустит сравнения
$n
с , если
$n
не прошло первую проверку (то есть истинным оказалось
выражение $n!~m/^\d+$/
). Поэтому важен именно
такой порядок операндов в выражении $n!~m/^\d+$/ or
$n>=4000
.
Оставшаяся часть тела процедуры toRoman
очень проста для
понимания. К переменной $roman
, изначально содержащей пустую
строку, приписывается слева фрагмент римской записи числа, соответствующий
очередной цифре и номеру разряда для этой цифры. Разряды начинают отсчитываться
от нуля, и цифры перебираются справа налево. Для получения каждого фрагмента
вызывается процедура toRomanHelper
, получающая в качестве
параметров цифру и номер её разряда:
Perl… my $roman=''; for(my $d=0; $n; $n=int($n/10)) { $roman=toRomanHelper($n % 10, $d++).$roman; } return $roman; }
Теперь определим процедуру toRomanHelper
. Вспомним о том,
что составление каждого фрагмента римской записи числа происходит по одной
и той же схеме. Различия заключаются лишь в наборе используемых римских цифр
в зависимости от номера разряда. Для нулевого разряда это I
,
V
, X
, для первого — X
,
L
, C
, для второго — C
,
D
, M
, и наконец, для третьего —
M
. Учитывая этот факт, будем составлять фрагменты римской
записи не из конкретных римских цифр, а из значений переменных
$i
, $v
, $x
.
В соответствии с номером разряда заполним эти переменные конкретными римскими
цифрами. Проще всего, пожалуй, было бы брать значения каждой из этих переменных
из соответствующего массива, используя номер разряда как индекс в нём. Если
номер разряда содержится в переменной $d
, то это
выглядело бы так:
Perlmy $i=('I', 'X', 'C', 'M')[$d]; my $v=('V', 'L', 'D')[$d]; my $x=('X', 'C', 'M')[$d];
Чуть короче такой код запишется с использованием оператора qw/…/, обсуждавшемся в разделе «Список слов в массиве»:
Perlmy $i=qw/I X C M/[$d]; my $v=qw/V L D/[$d]; my $x=qw/X C M/[$d];
Итак, начало процедуры toRomanHelper
готово:
Perlsub toRomanHelper($$) { my $n=shift; my $d=shift; my $i=qw/I X C M/[$d]; my $v=qw/V L D/[$d]; my $x=qw/X C M/[$d]; …
Содержательная часть процедуры очень проста и вряд ли нуждается в пояснениях:
Perl… return $i x $n if $n>=0 and $n<=3; return ($i x (5-$n)).$v if $n==4; return $v.($i x ($n-5)) if $n>=5 and $n<=8; return $i.$x; }
Программа parseroman.pl построена совершенно аналогично программе toroman.pl: получение и анализ параметра командной строки, обработка возможных ошибок, вычисление и печать результата:
Perldie "$0: Требуется римское число\n" unless defined(my $roman=shift); die "$0: Неправильное римское число: «$roman»\n" unless defined(my $decimal=parseRoman($roman)); print "$decimal\n";
Процедура parseRoman
, производящая вычисления, решает для
римской записи ту же задачу, что решала процедура toRoman
для десятичной: разбивает запись на фрагменты, соответствующие десятичным
цифрам, определяет числа, соответствующие этим фрагментам, складывает их
и возвращает сумму. Если римская запись не проходит проверку, возвращается
неопределённое значение. Проверка представляет собой сопоставление с уже
полученным нами регулярным выражением:
Perlsub parseRoman($) { if(shift=~m/^(M{0,3})(D?C{0,3}|C[DM])(L?X{0,3}|X[LC])(V?I{0,3}|I[VX])$/) { return parseRomanHelper($1, 3) +parseRomanHelper($2, 2) +parseRomanHelper($3, 1) +parseRomanHelper($4, 0); } return; }
Для определения чисел, соответствующих фрагментам, вызывается процедура
parseRomanHelper
, и при её вызове передаются в качестве
параметров фрагмент и номер разряда от нуля до трёх. Фрагменты захватываются
«группами захвата» и попадают в переменные $1
(третий
разряд), $2
(второй), $3
(первый)
и $4
(нулевой).
Определение процедуры parseRomanHelper
начинается
с получения параметров — фрагмента ($fragment
) и номера
разряда ($d
):
Perlsub parseRomanHelper($$) { my $fragment=shift; return 0 unless $fragment; my $d=shift; …
Если фрагмент оказался пустым, возвращается нуль, вне зависимости от номера разряда.
Следующим шагом должно быть получение числа, соответствующего фрагменту
и номеру разряда. К примеру, для фрагмента XL
должно
получиться число ,
а для CM
— . Тут
можно было бы брать результат прямо из таблицы 31.1. «Запись десятичных разрядов римскими цифрами», используя номер разряда
$d
как номер столбца и определяя номер строки, в которой
окажется данный фрагмент. Осталось возвратить найденный номер строки,
умноженный на десять в степени $d
.
Строго говоря, для поиска в таблице 31.1. «Запись десятичных разрядов римскими цифрами» не требуется даже знать номер разряда. Обратите внимание на то, что в таблице все заполненные ячейки уникальны, за исключением тех, что расположены в нулевой строке (но нулевая строка содержит в каждом разряде пустые фрагменты, соответствующие нулю; с этим случаем мы уже разобрались).
Похоже, это тот самый случай, когда для упрощения алгоритма стоит передать
в процедуру parseRomanHelper
избыточный параметр — номер
разряда, вместо того, чтобы поручать этой процедуре определять его
самостоятельно. Посмотрим ещё раз внимательно на таблицу. её столбцы устроены
совершенно одинаково и различаются только используемыми для записи римскими
цифрами. Не лучше ли поиск по всей таблице (двумерному массиву) заменить на
поиск только лишь в её нулевом столбце, выполнив предварительно в фрагменте
замену римских цифр на I
, V
,
X
? Естественно, какие именно римские цифры будут заменяться
на эти перечисленные, будет зависеть от значения $d
:
Perl… if($d==1) { $fragment=~tr/XLC/IVX/; } elsif($d==2) { $fragment=~tr/CDM/IVX/; } elsif($d==3) { $fragment=~tr/M/I/; } …
Затем найдём множитель, отвечающий номеру разряда $d
,
и сохраним результат в той же переменной:
Perl… $d=10**$d; …
Осталось разобрать по отдельности четыре случая:
фрагмент | шаблон | десятичная цифра | способ вычисления |
---|---|---|---|
I , II , III | ^I{1,3}$ | длина фрагмента | |
IV | |||
V , VI , VII , VIII | ^VI{0,3}$ | длина фрагмента плюс | |
IX |
Приведём код, соответствующий этой таблице:
Perl… return $d*length($fragment) if $fragment=~m/^I{1,3}$/; return $d*4 if $fragment eq 'IV'; return $d*(4+length($fragment)) if $fragment=~m/^VI{0,3}$/; return $d*9; }