Разработка

Начнём с исполняемой части программы (но при этом, как обычно, расположим её в конце). Она включает в себя приём параметра из командной строки, вызов процедуры toRoman, которая делает все вычисления, вывод сообщений об ошибках в случае отсутствующего или недопустимого параметра и печать результата:

die "$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 должна возвратить неопределённое значение, если получит недопустимое значение параметра, не являющееся целым числом, или выходящее за пределы промежутка 0 4000 . В случае ошибки (отсутствующий или недопустимый параметр) выполнение программы завершается по команде die с выдачей соответствующего сообщения.

Исполняемой части будет предшествовать декларативная, состоящая из определений процедур toRoman и toRomanHelper.

Тело процедуры toRoman начинается с получения значения параметра в переменную $n и его проверки:

sub toRoman($)
{
	my $n=shift;
	return if $n!~m/^\d+$/ or $n>=4000;
	

Если значение не удовлетворяет необходимым требованиям (не состоит целиком из десятичных цифр или превышает 3999), возвращается неопределённое значение (undef). На наше счастье, «ленивое» вычисление логических выражений в Perl не допустит сравнения $n с 4000, если $n не прошло первую проверку (то есть истинным оказалось выражение $n!~m/^\d+$/). Поэтому важен именно такой порядок операндов в выражении $n!~m/^\d+$/ or $n>=4000.

Оставшаяся часть тела процедуры toRoman очень проста для понимания. К переменной $roman, изначально содержащей пустую строку, приписывается слева фрагмент римской записи числа, соответствующий очередной цифре и номеру разряда для этой цифры. Разряды начинают отсчитываться от нуля, и цифры перебираются справа налево. Для получения каждого фрагмента вызывается процедура toRomanHelper, получающая в качестве параметров цифру и номер её разряда:

	
	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, то это выглядело бы так:

my $i=('I', 'X', 'C', 'M')[$d];
my $v=('V', 'L', 'D')[$d];
my $x=('X', 'C', 'M')[$d];

Чуть короче такой код запишется с использованием оператора qw/…/, обсуждавшемся в разделе «Список слов в массиве»:

Perl
my $i=qw/I X C M/[$d]; my $v=qw/V L D/[$d]; my $x=qw/X C M/[$d];

Итак, начало процедуры toRomanHelper готово:

Perl
sub 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: получение и анализ параметра командной строки, обработка возможных ошибок, вычисление и печать результата:

die "$0: Требуется римское число\n"
	unless defined(my $roman=shift);
die "$0: Неправильное римское число: «$roman»\n"
	unless defined(my $decimal=parseRoman($roman));

print "$decimal\n";

Процедура parseRoman, производящая вычисления, решает для римской записи ту же задачу, что решала процедура toRoman для десятичной: разбивает запись на фрагменты, соответствующие десятичным цифрам, определяет числа, соответствующие этим фрагментам, складывает их и возвращает сумму. Если римская запись не проходит проверку, возвращается неопределённое значение. Проверка представляет собой сопоставление с уже полученным нами регулярным выражением:

sub 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):

sub parseRomanHelper($$)
{
	my $fragment=shift;
	return 0 unless $fragment;
	my $d=shift;
	

Если фрагмент оказался пустым, возвращается нуль, вне зависимости от номера разряда.

Следующим шагом должно быть получение числа, соответствующего фрагменту и номеру разряда. К примеру, для фрагмента XL должно получиться число 40, а для CM — 900. Тут можно было бы брать результат прямо из таблицы 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}$ 1 3 длина фрагмента
IV 44
V, VI, VII, VIII^VI{0,3}$ 5 8 длина фрагмента плюс 4
IX 99

Приведём код, соответствующий этой таблице:

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

Информатика-54© А. Н. Швец