Текст книги "Программирование на языке Ruby"
Автор книги: Хэл Фултон
Жанр:
Программирование
сообщить о нарушении
Текущая страница: 15 (всего у книги 56 страниц) [доступный отрывок для чтения: 20 страниц]
Если вы распространяете вместе со своей программой справочники сообщений, то лучше собрать пакет с помощью системы RubyGems или библиотеки setup.rb
. Дополнительную информацию по этому поводу вы найдете в разделе 17.2.
При установке пакета, собранного RubyGems, справочники сообщений копируются в каталоги вида:
(gem-packages-installed-dir)/myapp-x.x.x/data/locale/
Такие каталоги уже включены в путь поиска для библиотеки gettext
, поэтому ваша программа будет локализована даже без явной установки переменной окружения GETTEXT_PATH
.
В случае сборки пакета с помощью библиотеки setup.rb
справочники сообщений помещаются в каталог (system-dir)/share/locale/
. И в этом случае локализация достигается без установки переменной GETTEXT_PATH
.
Напомним, что описанная библиотека не является оберткой набора утилит gettext
от GNU. Однако файлы сообщений совместимы, поэтому при желании вы можете пользоваться средствами сопровождения GNU. Понятно, что во время выполнения программы все эти инструменты не нужны, то есть пользователь не обязан их устанавливать на свой компьютер.
4.4. Заключение
В этой главе мы рассмотрели один из самых сложных аспектов программирования – проблему интернационализации кода. При этом нам понадобился материал из двух предыдущих глав, так как интернационализация тесно связана со строками и регулярными выражениями.
Мы видели, что в Ruby некоторые задачи решаются просто благодаря наличию библиотеки jcode
и сопутствующих инструментов. Заодно мы познакомились с наборами символов вообще и с набором Unicode в частности.
Мы узнали, что регулярные выражения в общем случае лучше поддерживают Unicode, чем средства работы со строками, а также рассмотрели методы pack
и unpack
с точки зрения полезности для манипулирования Unicode-строками.
Наконец, мы довольно подробно остановились на справочниках сообщений. Мы поняли, для чего они нужны, как их создавать и поддерживать.
Детально рассмотрев вопрос о строках и регулярных выражениях, вернемся на главную дорогу. Глава 5 посвящена численному анализу в языке Ruby.
Глава 5. Численные методы
Дважды [члены Парламента] задавали мне вопрос: «А скажите, мистер Бэббидж, если вы заложите в эту машину неверные числа, то получите правильный результат?» Не могу даже представить себе, насколько извращенно должен мыслить человек, задающий такие вопросы.
Чарльз Бэббидж
Числа – самый первичный тип данных, естественный для любого компьютера. Придется сильно постараться, чтобы найти такую область знания, в которой нет места числам. Будь вы бухгалтером или конструктором воздухоплавательных аппаратов, без чисел вам не обойтись. В этой главе мы обсудим различные способы обработки, преобразования и анализа числовых данных.
Как и всякий современный язык, Ruby прекрасно умеет работать с любыми числами – как целыми, так и с плавающей точкой. В нем есть полный набор ожидаемых математических операторов и функций, а вместе с тем и кое-какие приятные сюрпризы: классы Bignum
, BigDecimal
и Rational
.
Помимо средств для манипуляции числами, имеющихся в системной и стандартной библиотеках, мы рассмотрим более специфические темы (тригонометрия, математический анализ и статистика). Примеры приведены не только для справки, но и как образцы кода на языке Ruby, иллюстрирующие принципы, изложенные в других частях книги.
5.1. Представление чисел в языке Ruby
Если вы знакомы с любым другим языком программирования, то представление чисел в Ruby не вызовет у вас никакого удивления. Объект класса Fixnum
может представлять число со знаком или без знака:
237 # Число без знака (положительное).
+237 # То же, что и выше.
-237 # Отрицательное число.
Если число длинное, то между любыми цифрами можно вставлять знак подчеркивания. Это сделано исключительно для удобства, назначении константы никак не сказывается. Обычно подчерки вставляются в те же места, где бухгалтеры вставляют пробелы:
1048576 # Число в обычной записи.
1_048_576 # То же самое значение.
Целые числа можно представлять и в других системах счисления (по основанию 2, 8 и 16). Для этого в начале ставятся префиксы 0b
, 0
и 0х
соответственно.
0b10010110 # Двоичное.
0b1211 # Ошибка!
01234 # Восьмеричное (основание 8).
01823 # Ошибка!
0xdeadbeef # Шестнадцатеричное (основание 16) .
0xDEADBEEF # То же самое.
0xdeadpork # Ошибка!
В числах с плавающей точкой десятичная точка должна присутствовать, а показатель степени, возможно со знаком, необязателен:
3.14 # Число пи, округленное до сотых.
-0.628 # -2*pi, поделенное на 10, округленное до тысячных.
6.02е23 # Число Авогадро.
6.626068е-34 # Постоянная Планка.
В классе Float
есть константы, определяющие минимальные и максимальные значения чисел с плавающей точкой. Они машиннозависимы. Вот некоторые наиболее важные:
Float::MIN # 2.2250738585072е-308 (на конкретной машине)
Float::МАХ # 1.79769313486232е+308
Float::EPSILON # 2.22044604925031е-16
5.2. Основные операции над числами
Обычные операции сложения, вычитания, умножения и деления в Ruby, как и во всех распространенных языках программирования, обозначаются операторами +
, -
, *
, /
. Операторы в большинстве своем реализованы в виде методов (и потому могут быть переопределены).
Возведение в степень обозначается оператором **
, как в языках BASIC и FORTRAN. Эта операция подчиняется обычным математическим правилам.
а = 64**2 # 4096
b = 64**0.5 # 8.0
с = 64**0 # 1
d = 64**-1 # 0.015625
При делении одного целого числа на другое дробная часть результата отбрасывается. Это не ошибка, так и задумано. Если вы хотите получить результат с плавающей точкой, позаботьтесь о том, чтобы хотя бы один из операндов был числом c плавающей точкой.
3 / 3 # 3
5 / 3 # 1
3 / 4 # 0
3.0 / 4 # 0.75
3 / 4.0 # 0.75
3.0 / 4.0 # 0.75
Если вы работаете с переменными и сомневаетесь относительно их типа, воспользуйтесь приведением типа к Float
или методом to_f
:
z = x.to_f / у z = Float(x) / y
См. также раздел 5.17 «Поразрядные операции над числами».
5.3. Округление чисел с плавающей точкой
Кирк: Какие, вы говорите, у нас шансы выбраться отсюда?
Спок: Трудно сказать точно, капитан. Приблизительно 7824.7 к одному.
Стар Трек, «Миссия милосердия»
Метод round
округляет число с плавающей точкой до целого:
pi = 3.14159
new_pi = pi.round # 3
temp = -47.6
temp2 = temp.round # -48
Иногда бывает нужно округлить не до целого, а до заданного числа знаков после запятой. В таком случае можно воспользоваться функциями sprintf
(которая умеет округлять) и eval
:
pi = 3.1415926535
pi6 = eval(sprintf("%8.6f",pi)) # 3.141593
pi5 = eval(sprintf("%8.5f",pi)) # 3.14159
pi4 = eval(sprintf("%8.4f",pi)) # 3.1416
Это не слишком красиво. Поэтому инкапсулируем оба вызова функций в метод, который добавим в класс 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.2fn", x) # x = 345.68
printf("x = %9.2en", x) # x = 3.457e+02
printf("i = %5dn i) # i = 123
printf("i = %05dn", i) # i = 00123
printf("i = %-5dn, 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Е112(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.30330423168E116(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).
5.10. Перемножение матриц
Стандартная библиотека matrix
предназначена для выполнения операций над числовыми матрицами. В ней определено два класса: Matrix
и Vector
.
Следует также знать о прекрасной библиотеке NArray
, которую написал Масахиро Танака (Masahiro Tanaka) – ее можно найти на сайте www.rubyforge.org. Хотя эта библиотека не относится к числу стандартных, она широко известна и очень полезна. Если вы предъявляете повышенные требования к быстродействию, нуждаетесь в особом представлении данных или желаете выполнять быстрое преобразование Фурье, обязательно ознакомьтесь с этим пакетом. Впрочем, для типичных применений стандартной библиотеки matrix
должно хватить, поэтому именно ее мы и рассмотрим.
Чтобы создать матрицу, мы, конечно же, обращаемся к методу класса. Сделать это можно несколькими способами. Самый простой – вызвать метод Matrix.[]
и перечислить строки в виде массивов. Ниже мы записали вызов на нескольких строчках, но, разумеется, это необязательно:
m = Matrix[[1,2,3],
[4,5,6],
[7,8,9]]
Вместо этого можно вызвать метод rows, передав ему массив массивов (в таком случае «дополнительные» скобки необходимы). Необязательный параметр сору, по умолчанию равный true, указывает, надо ли скопировать переданные массивы или просто сохранить на них ссылки. Оставляйте значение true, если нужно защитить исходные массивы от изменения, и задавайте false, если это несущественно.
Row1 = [2,3]
row2 = [4,5]
m1 = Matrix.rows([row1,row2]) # copy=true
m2 = Matrix.rows([row1,row2],false) # He копировать.
row1[1] = 99 # Теперь изменим row1.
p m1 # Matrix[[2, 3], [4, 5]]
p m2 # Matrix[[2, 99], [4, 5]]
Можно задать матрицу и путем перечисления столбцов, если воспользоваться методом columns
. Ему параметр сору
не передается, потому что столбцы в любом случае расщепляются, так как во внутреннем представлении матрица хранится построчно:
m1 = Matrix.rows([[1,2],[3,4]])
m2 = Matrix.columns([[1,3],[2,4]]) # m1 == m2
Предполагается, что все матрицы прямоугольные, но это не проверяется. Если вы создадите матрицу, в которой отдельные строки или столбцы длиннее либо короче остальных, то можете получить неверные или неожиданные результаты.
Некоторые специальные матрицы, особенно квадратные, конструируются проще. Так, тождественную матрицу конструирует метод identity
(или его синонимы I
и unit
):
im1 = Matrix.identity(3) # Matrix[[1,0,0],[0,1,0],[0,0,1]]
im2 = Matrix.I(3) # То же самое.
im3 = Matrix.unit(3) # То же самое.
Более общий метод scalar
строит диагональную матрицу, в которой все элементы на диагонали одинаковы, но не обязательно равны 1:
sm = Matrix.scalar(3,8) # Matrix[[8,0,0],[0,8,0],[0,0,8]]
Еще более общим является метод diagonal
, который формирует диагональную матрицу с произвольными элементами (ясно, что параметр, задающий размерность, в этом случае не нужен).
dm = Matrix.diagonal(2,3,7) # Matrix[[2,0,0],[0,3,0],[0,0,7]]
Метод zero
создает нулевую матрицу заданной размерности (все элементы равны 0):
zm = Matrix.zero(3) # Matrix[[0,0,0],[0,0,0],[0,0,0]]
Понятно, что методы identity
, scalar
, diagonal
и zero
создают квадратные матрицы.
Чтобы создать матрицу размерности 1×N или N×1, воспользуйтесь методом row_vector или column_vector соответственно.
а = Matrix.row_vector(2,4,6,8) # Matrix[[2,4,6,8]]
b = Matrix.column_vector(6,7,8,9) # Matrix[[6],[7],[8],[9]]
К отдельным элементам матрицы можно обращаться, указывая индексы в квадратных скобках (оба индекса заключаются в одну пару скобок). Отметим, что не существует метода []=
. По той же причине, по которой его нет в классе Fixnum: матрицы – неизменяемые объекты (такое решение было принято автором библиотеки).
m = Matrix[[1,2,3],[4,5,6]]
puts m[1,2] # 6
Индексация начинается с 0, как и для массивов в Ruby. Возможно, это противоречит вашему опыту работы с матрицами, но индексация с 1 в качестве альтернативы не предусмотрена. Можно реализовать эту возможность самостоятельно:
# Наивный подход... не поступайте так!
class Matrix
alias bracket []
def [] (i,j)
bracket(i-1,j-1)
end
end
m = Matrix[[1,2,3],[4,5,6],[7,8,9]]
p m[2,2] # 5
На первый взгляд, этот код должен работать. Большинство операций над матрицами даже будет давать правильный результат при такой индексации. Так в чем же проблема? В том, что мы не знаем деталей внутренней реализации класса Matrix
. Если в нем для доступа к элементам матрицы всегда используется собственный метод []
, то все будет хорошо. Но если где-нибудь имеются прямые обращения к внутреннему массиву или применяются иные оптимизированные решения, то возникнет ошибка. Поэтому, решившись на такой трюк, вы должны тщательно протестировать новое поведение.
К тому же необходимо изменить методы row
и vector
. В них индексы тоже начинаются с 0, но метод []
не вызывается. Я не проверял, что еще придется модифицировать.
Иногда необходимо узнать размерность или форму матрицы. Для этого есть разные методы, например row_size
и column_size
.
Метод row_size
возвращает число строк в матрице. Что касается метода column_size
, тут есть одна тонкость: он проверяет лишь размер первой строки. Если по каким-либо причинам матрица не прямоугольная, то полученное значение бессмысленно. Кроме того, поскольку метод square?
(проверяющий, является ли матрица квадратной) обращается к row_size
и column_size
, его результат тоже нельзя считать стопроцентно надежным.
m1 = Matrix[[1,2,3],[4,5,6],[7,8,9]]
m2 = Matrix[[1,2,3],[4,5,6],[7,8]]
m1.row_.size # 3
m1.column_size # 3 m2.row_size # 3
m2.column_size # 3 (неправильно)
m1.square? # true
m2.square? # true (неправильно)
Решить эту мелкую проблему можно, например, определив метод rectangular?
.
class Matrix
def rectangular?
arr = to_a
first = arr[0].size
arr[1..-1].all? {|x| x.size == first }
end
end
Можно, конечно, модифицировать метод square?
, так чтобы сначала он проверял, является ли матрица прямоугольной. В таком случае нужно будет изменить метод column_size
, чтобы он возвращал nil
для непрямоугольной матрицы.
Для вырезания части матрицы имеется несколько методов. Метод row_vectors
возвращает массив объектов класса Vector
, представляющих строки (см. обсуждение класса Vector
ниже.) Метод column_vectors
работает аналогично, но для столбцов. Наконец, метод minor
возвращает матрицу меньшего размера; его параметрами являются либо четыре числа (нижняя и верхняя границы номеров строк и столбцов), либо два диапазона.
m = Matrix[[1,2,3,4],[5,6,7,8],[6,7,8,9]]
rows = m.row_vectors # Три объекта Vector.
cols = m.column_vectors # Четыре объекта Vector.
m2 = m.minor(1,2,1,2) # Matrix[[6,7,],[7,8]]
m3 = m.minor(0..1,1..3) # Matrix[[[2,3,4],[6,7,8]]
К матрицам применимы обычные операции: сложение, вычитание, умножение и деление. Для выполнения некоторых из них должны соблюдаться ограничения на размеры матриц-операндов; в противном случае будет возбуждено исключение (например, при попытке перемножить матрицы размерностей 3×3 и 4×4).
Поддерживаются стандартные преобразования: inverse
(обращение), transpose
(транспонирование) и determinant
(вычисление определителя). Для целочисленных матриц определитель лучше вычислять с помощью библиотеки mathn
(раздел 5.12).
Класс Vector
– это, по существу, частный случай одномерной матрицы. Его объект можно создать с помощью методов []
или elements
; в первом случае параметром является развернутый массив, а во втором – обычный массив и необязательный параметр сору
(по умолчанию равный true
).
arr = [2,3,4,5]
v1 = Vector[*arr] # Vector[2,3,4,5]
v2 = Vector.elements(arr) # Vector[2,3,4,5]
v3 = Vector.elements(arr,false) # Vector[2,3,4,5]
arr[2] = 7 # теперь v3 – Vector[2,3,7,5].
Метод covector
преобразует вектор длины N в матрицу размерности N×1 (выполняя попутно транспонирование).
v = Vector[2,3,4]
m = v.covector # Matrix[[2,3,4]]
Поддерживается сложение и вычитание векторов одинаковой длины. Вектор можно умножать на матрицу и на скаляр. Все эти операции подчиняются обычным математическим правилам.
v1 = Vector[2,3,4]
v2 = Vector[4,5,6]
v3 = v1 + v2 # Vector[6,8,10]
v4 = v1*v2.covector # Matrix![8,10,12],[12,15,18],[16,20,24]]
v5 = v1*5 # Vector[10,15,20]
Имеется метод inner_product
(скалярное произведение):
v1 = Vector[2,3,4]
v2 = Vector[4,5,6]
x = v1.inner_product(v2) # 47
Дополнительную информацию о классах Matrix
и vector
можно найти в любом справочном руководстве, например воспользовавшись командной утилитой ri
, или на сайте ruby-doc.org.