Программирование на языке Ruby - Хэл Фултон 23 стр.


Это не слишком красиво. Поэтому инкапсулируем оба вызова функций в метод, который добавим в класс Float:

class Float

def roundf(places)

temp = self.to_s.length

sprintf("%#{temp}.#{places}f",self).to_f

end

end

Иногда требуется округлять до целого по-другому. Традиционное округление n+0.5 с избытком со временем приводит к небольшим ошибкам; ведь n+0.5 все-таки ближе к n+1, чем к n. Есть другое соглашение: округлять до ближайшего четного числа, если дробная часть равна 0.5. Для реализации такого правила можно было бы расширить класс Float, добавив в него метод round2:

class Float

def round2

whole = self.floor

fraction = self - whole

if fraction == 0.5

if (whole % 2) == 0

whole

else

whole+1

end

else

self.round

end

end

end

a = (33.4).round2 # 33

b = (33.5).round2 # 34

с = (33.6).round2 # 34

d = (34.4).round2 # 34

e = (34.5).round2 # 34

f = (34.6).round2 # 35

Видно, что round2 отличается от round только в том случае, когда дробная часть в точности равна 0.5. Отметим, кстати, что число 0.5 можно точно представить в двоичном виде. Не так очевидно, что этот метод правильно работает и для отрицательных чисел (попробуйте!). Отметим еще, что скобки в данном случае необязательны и включены в запись только для удобства восприятия.

Ну а если мы хотим округлять до заданного числа знаков после запятой, но при этом использовать метод "округления до четного"? Тогда нужно добавить в класс Float также метод roundf2:

class Float

# Определение round2 такое же, как и выше.

def roundf2(places)

shift = 10**places

(self * shift).round2 / shift.to_f

end

end

a = 6.125

b = 6.135

x = a.roundf2(a) #6.12

y = b.roundf2(b) #6.13

У методов roundf и roundf2 есть ограничение: большое число с плавающей точкой может стать непредставимым при умножении на большую степень 10. На этот случай следовало бы предусмотреть проверку ошибок.

5.4. Сравнение чисел с плавающей точкой

Печально, но факт: в компьютере числа с плавающей точкой представляются неточно. В идеальном мире следующий код напечатал бы "да", но на всех машинах где мы его запускали, печатается "нет":

x = 1000001.0/0.003

y = 0.003*x

if y == 1000001.0

puts "да"

else

puts "нет"

end

Объясняется это тем, что для хранения числа с плавающей точкой выделено конечное число битов, а с помощью любого, сколь угодно большого, но конечного числа битов нельзя представить периодическую десятичную дробь с бесконечным числом знаков после запятой.

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

Ниже показан простой способ выполнения сравнения с "поправкой", когда числа считаются равными, если отличаются не более чем на величину, задаваемую программистом:

class Float

EPSILON = 1e-6 # 0.000001

def == (x)

(self-x).abs < EPSILON

end

end

x = 1000001.0/0.003

y = 0.003*x

if y == 1.0 # Пользуемся новым оператором ==.

puts "да" # Теперь печатается "да".

else

puts "нет"

end

В зависимости от ситуации может понадобиться задавать разные погрешности. Для этого определим в классе Float новый метод equals?. (При таком выборе имени мы избежим конфликта со стандартными методами equal? и eql?; последний, кстати, вообще не следует переопределять).

class Float

EPSILON = 1e-6

def equals?(x, tolerance=EPSILON)

(self-x).abs < tolerance

end

end

flag1 = (3.1416).equals? Math::PI # false

flag2 = (3.1416).equals?(Math::PI, 0.001) # true

Можно также ввести совершенно новый оператор для приближенного сравнения, назвав его, например, =~.

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

5.5. Форматирование чисел для вывода

Для вывода числа в заданном формате применяется метод printf из модуля Kernel. Он практически не отличается от одноименной функции в стандартной библиотеке С. Дополнительную информацию см. в документации по методу printf.

x = 345.6789

i = 123

printf("x = %6.2f\n", x) # x = 345.68

printf("x = %9.2e\n", x) # x = 3.457e+02

printf("i = %5d\n\ i) # i = 123

printf("i = %05d\n", i) # i = 00123

printf("i = %-5d\n\, i) # i = 123

Чтобы сохранить результат в строке, а не печатать его немедленно, воспользуйтесь методом sprintf. При следующем обращении возвращается строка:

str = sprintf ("%5.1f",x) # "345.7"

Наконец, в классе String есть метод %, решающий ту же задачу. Слева от знака % должна стоять форматная строка, а справа - единственный аргумент (или массив значений), результатом является строка.

# Порядок вызова: 'формат % значение'

str = "%5.1f" % x # "345.7"

str = "%6.2f, %05d" % [x,i] # "345.68, 00123"

5.6. Вставка разделителей при форматировании чисел

Возможно, есть и более удачные способы достичь цели, но приведенный ниже код работает. Мы инвертируем строку, чтобы было удобнее выполнять глобальную замену, а в конце инвертируем ее еще раз:

def commas(x)

str = x.to_s.reverse

str.gsub!(/([0-9]{3})/,"\\1,")

str.gsub(/,$/,"").reverse

end

puts commas(123) # "123"

puts commas(1234) # "1,234"

puts commas(12345) # "12,435"

puts commas(123456) # "123,456"

puts commas(1234567) # "1,234,567"

5.7. Работа с очень большими числами

Управлять массами все равно что управлять немногими: дело в частях и в числе.

Сунь-Цзы

При необходимости Ruby позволяет работать с произвольно большими целыми числами. Переход от Fixnum к Bignum производится автоматически, прозрачно для программиста. В следующем разделе результат оказывается настолько большим, что преобразуется из объекта Fixnum в Bignum:

num1 = 1000000 # Один миллион (10**6)

num2 = num1*num1 # Один триллион (10**12)

puts num1 # 1000000

puts num1.class # Fixnum

puts num2 # 1000000000000

puts num2.class # Bignum

Размер Fixnum зависит от машинной архитектуры. Вычисления с объектами Bignum ограничены только объемом памяти и быстродействием процессора. Конечно, они потребляют больше памяти и выполняются несколько медленнее, тем не менее операции над очень большими целыми (сотни знаков) реальны.

5.8. Использование класса BigDecimal

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

Чтобы оценить преимущества, рассмотрим следующий простой фрагмент кода, в котором используются числа с плавающей точкой:

if (3.2 - 2.0) == 1.2

puts "равны"

else

puts "не равны" # Печатается "не равны"!

end

В подобной ситуации на помощь приходит класс BigDecimal. Однако в случае бесконечных периодических дробей проблема остается. Другой подход обсуждается в разделе 5.9 "Работа с рациональными числами".

Объект BigDecimal инициализируется строкой. (Объекта типа Float было бы недостаточно, поскольку погрешность вкралась бы еще до начала конструирования BigDecimal.) Метод BigDecimal эквивалентен BigDecimal.new; это еще один особый случай, когда имя метода начинается с прописной буквы. Поддерживаются обычные математические операции, например + и *. Отметим, что метод to_s может принимать в качестве параметра форматную строку. Дополнительную информацию вы найдете на сайте ruby-doc.org.

require 'bigdecimal'

x = BigDecimal("3.2")

y = BigDecimal("2.0")

z = BigDecimal("1.2")

if (x - y) == z

puts "равны" # Печатается "равны"!

else

puts "не равны"

end

а = x*y*z

a.to_s # "0.768Е1" (по умолчанию: научная нотация)

a.to_s("F") # "7.68" (обычная запись)

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

x = BigDecimal ("1.234",10)

y = BigDecimal("1.234",15)

x.precs # [8, 16]

y.precs # [8, 20]

В каждый момент число использованных байтов может оказаться меньше максимального. Максимум может также оказаться больше запрошенного вами (поскольку BigDecimal пытается оптимизировать использование внутренней памяти). У обычных операций (сложение, вычитание, умножение и деление) есть варианты принимающие в качестве дополнительного параметра число значащих цифр. Если результат содержит больше значащих цифр, чем указано, производится округление до заданного числа знаков.

a = BigDecimal("1.23456")

b = BigDecimal("2.45678")

# В комментариях "BigDecimal:objectid" опущено.

c = a+b # <'0.369134Е1\12(20)>

c2 = a.add(b,4) # <'0.3691Е1',8(20)>

d = a-b # <'-0.122222E1',12(20)>

d2 = a.sub(b,4) # <'-0.1222E1',8(20)>

e = a*b # <'0.30330423168E1\16(36)>

e2 = a.mult(b,4) # <'0.3033E1',8(36)>

f = a/b # <'0.502511417383729922907221E0',24(32)>

f2 = a.div(b,4) # <'0.5025E0',4(16)>

В классе BigDecimal определено и много других функций, например floor, abs и т.д. Как и следовало ожидать, имеются операторы % и **, а также операторы сравнения, к примеру <. Оператор == не умеет округлять свои операнды - эта обязанность возлагается на программиста.

В модуле BigMath определены константы E и PI с произвольной точностью. (На самом деле это методы, а не константы.) Там же определены функции sin, cos, exp и пр.; все они принимают число значащих цифр в качестве параметра. Следующие подбиблиотеки являются дополнениями к BigDecimal.

bigdecimal/math Модуль BigMath

bigdecimal/jacobian Методы для вычисления матрицы Якоби

bigdecimal/ludcmp Модуль LUSolve, разложение матрицы в произведение верхнетреугольной и нижнетреугольной

bigdecimal/newton Методы nlsolve и norm

В настоящей главе эти подбиблиотеки не описываются. Для получения дополнительной информации обратитесь к сайту ruby-doc.org или любому подробному справочному руководству.

5.9. Работа с рациональными числами

Класс Rational позволяет (во многих случаях) производить операции с дробями с "бесконечной" точностью, но лишь если это настоящие рациональные числа (то есть частное от деления двух целых чисел). К иррациональным числам, например π или e, он неприменим.

Для создания рационального числа мы вызываем специальный метод Rational (еще один из немногих методов, имя которого начинается с прописной буквы; обычно такие методы служат для преобразования данных или инициализации).

r = Rational(1,2) # 1/2 или 0.5

s = Rational(1,3) # 1/3 или 0.3333...

t = Rational(1,7) # 1/7 или 0.14...

u = Rational(6,2) # "то же самое, что" 3.0

z = Rational(1,0) # Ошибка!

Результатом операции над двумя рациональными числами, как правило, снова является рациональное число.

r+t # Rational(9, 14)

r-t # Rational(5, 14)

r*s # Rational(1, 6)

r/s # Rational(3, 2)

Вернемся к примеру, на котором мы демонстрировали неточность операций над числами с плавающей точкой (см. раздел 5.4). Ниже мы выполняем те же действия над рациональными, а не вещественными числами и получаем "математически ожидаемый" результат:

x = Rational(1000001,1)/Rational(3,1000)

y = Rational(3,1000)*x

if y == 1000001.0

puts "да" # Теперь получаем "да"!

else

puts "нет"

end

Конечно, не любая операция дает рациональное же число в качестве результата:

x = Rational (9,16) # Rational(9, 16)

Math.sqrt(x) # 0.75

x**0.5 # 0.75

x**Rational(1,2) # 0.75

Однако библиотека mathn в какой-то мере изменяет это поведение (см. раздел 5.12).

Назад Дальше