HTML page

Глава 2 Числа

Введение

Числа составляют основные типы данных практически в любом языке программирования, однако даже с ними могут возникнуть неожиданные сложности. Случайные числа, числа с дробной частью, числовые последовательности и преобразования строк в числа - все это вызывает немалые затруднения. Peri старается по возможности облегчить вам жизнь, и его средства для работы с числами не являются исключением из этого правила. Если скалярное значение интерпретируется в программе как число, то Peri преобразует его в числовую форму. Читаете ли вы числовые данные из файла, извлекаете отдельные цифры из строки или иным образом получаете числа из бесчисленных текстовых источников Внешнего Мира, - вам не приходится преодолевать препятствия в виде неудобных ограничений других языков на пути преобразования ASCII-строк в числа. Если строка используется в числовом контексте (например, в математическом выражении), Peri старается интерпретировать ее как число, однако у него нет возможности сообщить о том, что строка в действительности не соответствует числу. Встречая не-числовой символ, Peri прекращает интерпретацию строки, при этом не-числовые строки считаются равными нулю, поэтому "А7" преобразуется в О, а "7 А" - в 7 (хотя флаг -w предупредит вас о некорректных преобразованиях). Иногда (например, при проверке вводимых данных) требуется узнать, соответствует ли строка числу. Мы покажем как это делается в рецепте 2.1. В рецепте 2.16 объясняется, как получить число из строк с шестнадцатерич-ными или восьмеричными представлениями чисел - например, "Oxff". Peri автоматически преобразует литералы в программном коде (поэтому $а = 3 + Oxff присвоит $а значений 258), но это не относится к данным, прочитанным программой. Вы не можете прочитать "Oxff" в $Ь и затем написать $а = 3 + $b, чтобы присвоить $а 258. А если трудностей с целыми числами окажется недостаточно, числа с плавающей запятой преподнесут целый букет новых проблем. Во внутреннем представлении дробные числа хранятся в формате с плавающей запятой. Они представляют вещественные числа лишь приближенно, с ограниченной точностью. Для представления бесконечного множества вещественных чисел используется конечное пространство, обычно состоящее из 64 бит или около того. Потеря точности неизбежна. Числа, прочитанные из файла или встретившиеся в программе в виде литералов, преобразуются из десятичного представления (например, 0.1) во внутреннее. Невозможно точно представить 0.1 в виде двоичного числа с плавающей запятой - подобно тому, как 1/3 невозможно точно представить в виде конечного десятичного числа. Следовательно, двоичное представление 0.1 в действительности отличается от 0.1. Для 20 десятичных разрядов оно равно 0.10000000000000000555. При выполнении арифметических операций с двоичными представлениями чисел с плавающей запятой накапливаются ошибки. Значение выражения 3*0.1 не совпадает с двоичной кодировкой числа 0.3. Это означает, что числа с плавающей запятой в Peri нельзя просто сравнивать с помощью ==. Работе с ними посвящены рецепты 2.2 и 2.3. В рецепте 2.4 показано, как преобразовать ASCII-строку с двоичным представлением числа (например, "1001") в целое (9 для приведенного примера) и обратно. Рецепт 2.5 описывает три способа выполнения некоторой операции с каждым элементом последовательного множества целых чисел. Преобразование чисел в римскую запись и обратно продемонстрировано в рецепте 2.6. Случайным числам посвящено сразу несколько рецептов. Функция Peri rand возвращает число с плавающей запятой от 0 до 1 или от 0 до своего аргумента. Мы покажем, как получить случайное число в конкретном интервале, как сделать их "еще более случайными" и как заставить rand генерировать новый набор случайных чисел при каждом запуске программы. Глава завершается рецептами, относящимися к тригонометрии, логарифмам, умножению матриц, комплексным числам. Заодно вы найдете ответ на часто встречающийся вопрос: "Как включить в выводимое число запятую?"

2.1. Проверка строк на соответствие числам

Проблема

Требуется проверить, соответствует ли строка допустимому числу. Эта проблема часто возникает при проверке входных данных (например, в сценариях CGI).

Решение

Сравните строку с регулярным выражением, которое совпадает со всеми интересующими вас разновидностями чисел:
if ($stnng =~ /PATTERN/) {
# является числом
} else {
# не является числом
}
.

Комментарий

Все зависит от того, что именно понимать под числом. Даже простые на первый взгляд понятия - например, целое - заставят вас поломать голову над тем, какие строки следует отнести к этой категории. Например, что делать с начальным -для положительных чисел? Разрешить, сделать обязательным или запретить? А числа с плавающей запятой представляются таким огромным количеством способов, что у вас в голове перегреется процессор. Сначала решите, какие символы допустимы, а какие - пет. Затем сконструируйте для отобранных символов регулярное выражение. Ниже приведены некоторые стандартные конструкции для самых распространенных ситуаций (что-то вроде полуфабрикатов для нашей поваренной книги).
# Содержит нецифровые символы
warn "has nondigits"
if /\D/;
# He является натуральным числом
warn "not a natural number"
unless /"\d+$/; # Отвергает -3
# He является целым числом
warn "not an integer"
unless /"-'''\d+$/; # Отвергает +3
warn "not an integer"
unless /"[+-]?\d+$/;
# He является десятичным числом
warn "not a decimal number"
unless /"-?\d+\.?\d*$/; # Отвергает .2
warn "not a decimal number"
unless /~-?(?:d+(?:\.\d)?|\.\d+)$/,
# He является вещественным числом С warn "not a C float"
unless /--([+-]?)(^=\d|\.\d)\d*(\.\d*)'?([Ee]([+-]Ad+))?$/:
В этих шаблонах не обрабатываются особые случаи Infinity и NaN в записи IEEE. Если вы не боитесь, что члены комитета IEEE придут к вашему компьютеру и начнут бить вас по голове копиями соответствующих стандартов, вероятно, об этих странных "числах" можно забыть. Для строк с начальными или конечными пробелами эти шаблоны не подходят. Либо вставьте в них соответствующую логику, либо вызовите функцию trim из рецепта 1.14. В POSIX-системах Peri поддерживает функцию POSIX: :strtod. Ее семантика чрезвычайно громоздка, поэтому мы приведем функцию getnum для упрощения доступа. Эта функция получает строку и возвращает либо преобразованное число, либо undef для строк, не соответствующих вещественным числам С. Интерфейсная функция is_numeric упрощает вызов getnum в ситуациях, когда вы просто хотите спросить: "Это вещественное число"?
sub getnum {
USE POSIX qw(strtod);
my $str = shift;
$str =~ s/\s+$//;
$! = 0;
my($num, $unparsed) = strtod($str);
if (($str eq '') | ($unparsed != 0) || $!) {
return;
} else {
return $num;
}
}
sub is_numeric { defined scalar &getnum }

Смотри также -----------
Описание синтаксиса регулярных выражений в perlre(1), страница руководства strtod(3); документация по стандартному модулю POSIX.

2.2. Сравнение чисел с плавающей запятой

Проблема

Арифметика с плавающей занятой не является абсолютно точной. Сравнивая два числа, вы хотите узнать, совпадают ли они до определенного десятичного разряда. Как правило, именно так следует сравнивать числа с плавающей запятой.

Решение

Воспользуйтесь функцией sprintf и отформатируйте числа до определенного десятичного разряда, после чего сравните полученные строки:
# equal(NUM1, NUM2, ACCURACY); возвращает true, если NUM1 и NUM2 # совпадают на ACCURACY десятичных разрядов.

sub equal {
my ($A, $B, $dp) = @_;
return sprintf("%.${dp}g", $A) eq sprintf("%.${dp}g", $A);
}
Альтернативное решение - преобразовать числа в целые, умножая их на соответствующий коэффициент. Комментарий Процедура equal понадобилась из-за того, что в компьютерах многие числа с плавающей запятой представляются с ограниченной точностью. Дополнительная информация приведена в разделе "Введение". При фиксированном количестве цифр в дробной части (например, в денежных суммах) проблему можно решить преобразованием в целое число. Если сумма 3.50 будет храниться в виде 350, а не 3.5, необходимость в числах с плавающей запятой отпадает. Десятичная точка снова появляется в выводимых данных:

$wage = 536; # $5,36/час
$week = 40 * $wage; # $214.40
printf("0ne week's wage is: \$%.2f\n", $week/100);

One week's wage is: $214.40 Редко требуется сравнивать числа более чем до 15 разряда.
> Смотри также -------------------------------- Описание функции sprintf в perlfunc(1); описание переменной $# в странице руководства perlvar(1); документация по стандартному модулю Math::BigFloat Функция sprintf используется в рецепте 2.3. Также обращайтесь к разделу 4.2.2 тома 2 "Искусство программирования".

2.3. Округление чисел с плавающей запятой

Проблема

Число с плавающей запятой требуется округлить до определенного разря да. Проблема связана с теми же погрешностями представления, которые затрудняют сравнение чисел (см. рецепт 2.2), а также возникает в ситуациях, когда точность ответа намеренно снижается для получения более наглядного результата.

Решение

Для получения непосредственного вывода воспользуйтесь функциями
Peri sprint или printf:
$rounded = sprintf("%formatf", $unrounded);

Комментарий

Округление серьезно отражается на работе некоторых алгоритмов, потому ис пользуемый метод должен быть точно указан. В особо важных приложениях (например, в финансовых вычислениях или системах наведения ракет) грамотный программист реализует свою собственную функцию округления, не полагаясь на встроенную логику языка (или ее отсутствие). Однако во многих ситуациях можно просто воспользоваться функцией sprintf. Формат f позволяет указать количество разрядов, до которого округляется аргумент. Peri округляет последний разряд вверх, если следующая цифра равна 5 и более, и вниз в противном случае.

$а = 0.255
$b = sprintf("%.2f", $a);
print "Unrounded: $a\nRounded: %.2f\n", $a;
Unrounded:
0.255 Rounded:
0.26 Unrounded:
0.255 Rounded: 0.26 Существуют три функции, предназначенные для округления чисел с плавающей запятой до целых: int, cell и floor. Встроенная функция Peri int возвращает целую часть числа с плавающей запятой (при вызове без аргумента она использует $_). Функции модуля POSIX floor и ceil округляют аргументы вверх и вниз, соответственно, до ближайшего целого.
use POSIX;
print "number\tint\floor\tceil\n";
@а = { 3.3 , 3.5 , 3.7 , -3,3};
foreach (@a) {
printf( "% .1f\t% .1f\t% ,1f\t% .1f\n", $_, int($_), floor($_), ceil($_) );
}
number int floor ceil

3.3        3.0 3.0 4.0
3.5         3.0 3.0 4.0
3.7        3.0 3.0 4.0
-3.3        3.0 -4.0 -3.0

> Смотри также Описание функций sprintf и int в perlfunc(1) описание функций floor и ceil в документации по стандартному модулю POSIX. Методика использования sprintf для округления представлена в рецепте 2.2.
copyright 2000 Soft group