Ruby/Для начинающих
Подробнее о числах
правитьИзначально числа представлены тремя типами: два целых типа (классы Fixnum
и Bignum
) и один с плавающей запятой (класс Float
). Возможно подключение дополнительных типов, например, комплексных и рациональных чисел, но пока ограничимся тремя.
Целые числа
правитьЦелые числа в Ruby не ограничены по величине, то есть могут хранить сколь угодно большие значения. Для обеспечения такого волшебного свойства было создано два класса. Один из них хранит числа меньше (по модулю), а второй — всё, что больше. По сути, для больших чисел создаётся массив из маленьких, а раз массив не имеет ограничений по длине, то и число получается неограниченным по значению.
Как ни странно, определяется как (2**30).class #=> Bignum
Однако, целое число, меньшее (по модулю) определяется как ((2**30)-1).class #=> Fixnum
(-(2**30)+1).class #=> Fixnum
Так-то! |
Как только число типа Fixnum
становится больше или равным (по модулю), то оно преобразуется к классу Bignum
. Если число типа Bignum
становится меньше , то оно преобразуется к типу Fixnum
.
При записи целых чисел сначала указывается знак числа (знак +
обычно не пишется). Далее идёт основание системы счисления, в которой задаётся число (если оно отлично от десятичной): 0
— для восьмеричной, 0x
— для шестнадцатеричной, 0b
— для двоичной. Затем идёт последовательность цифр, выражающих число в данной системе счисления. При записи чисел можно использовать символ подчёркивания, который игнорируется при обработке. Чтобы закрепить вышесказанное, посмотрим примеры целых чисел:
# тип Fixnum
123_456 # подчёркивание игнорируется
-567 # отрицательное число
0xbad # шестнадцатеричное число
0377 # восьмеричное
-0b101010 # отрицательное двоичное
0b0101_0101 # подчёркивание игнорируется
# тип Bignum
123_456_789_123_456 # подчёркивание игнорируется
-123_456_789_123_456 # отрицательное
07777777777777777777 # восьмеричное большое
Как видно из примеров, маленькие целые (Fixnum
) и больши́е целые (Bignum
) отличаются только значением.
Числа с плавающей запятой
правитьЧисла с плавающей запятой задаются только в десятичной системе счисления, при этом для отделения дробной части используется символ .
(точка). Для задания чисел с плавающей запятой может быть применена и экспоненциальная форма записи: два различных представления 0.1234e2
и 1234e-2
задают одно и то же число 12.34
.
# тип Float
-12.34 # отрицательное число с плавающей запятой
0.1234е2 # экспоненциальная форма для числа 12.34
1234е-2 # экспоненциальная форма для числа 12.34
Следует упомянуть, что чи́сла с плавающей запятой имеют фиксированный диапазон значений в отличие от целых чисел. Этот недостаток легко устраняется подключением библиотеки mathn
(подключаются рациональные и комплексные числа).
Семейный портрет чисел
правитьВ отличие от большинства элементарных типов данных, числа обладают своей иерархией. Все числа в Ruby наследованы от класса Numeric
(числовой). Поэтому, если хотите добавить новый метод ко всем числам, то нужно расширять именно этот класс. Далее идёт деление чисел: Integer
(целое), Float
(число с плавающей запятой) и Complex
(комплексное). При желании можно добавить и Rational
(рациональное), но на данном семейном портрете оно отсутствует.
От класса Integer
наследуются два класса: Fixnum
(фиксированное целое) и Bignum
(большое целое). К первому относятся все числа, по модулю меньшие , а ко второму — все остальные.
Fixnum
автоматически становитсяBignum
по превышении по модулю. И наоборот, падая ниже,Bignum
преобразуется вFixnum
.- Из отрицательного числа можно получить корень, когда подключена библиотека
mathn
. Он будет типаComplex
. - Как только число типа
Complex
лишается мнимой части, то оно становится либоInteger
(Fixnum
илиBignum
), либоFloat
(в зависимости от типа действительной части). Если подключена библиотекаmathn
, может получиться число типаRational
. - Если в результате арифметических действий в числе типа
Rational
знаменатель приравнивается1
, то оно преобразуется к числуInteger
.
Арифметические операции
правитьАрифметические операции в Ruby обычны: сложение (+
), вычитание (-
), умножение (*
), деление (/
), получение остатка от деления (%
), возведение в степень (**
).
6 + 4 #=> 10
6 - 4 #=> 2
6 * 4 #=> 24
6 / 4 #=> 1
6 % 4 #=> 2
6 ** 4 #=> 1296
Эти операции используются как числами с плавающей запятой, так и целыми числами (а также рациональными дробями и комплексными).
Порядок вычисления обычный. Для изменения приоритета применяются круглые скобки:
2 + 2 * 2 #=> 6
(2 + 2) * 2 #=> 8
Первое, что бросается в глаза, — результат арифметической операции двух целых чисел всегда будет целым. Особенно это видно при делении:
1/3 #=> 0
2/3 #=> 0
3/3 #=> 1
Если все аргументы арифметического выражения целые числа, то результат будет целым, если хотя бы одно число с плавающей запятой, то результат будет числом с плавающей запятой.
Одна вторая в Ruby ноль,
А три вторые — единица.
Запомнить надо эту соль,
Чтоб результату не дивиться.
Посмотрим, каковы результаты, когда одно из чисел является числом с плавающей запятой.
6.0 + 4 #=> 10.0
6 - 4.0 #=> 2.0
6.0 * 4.0 #=> 24.0
6.0 / 4 #=> 1.5 (одно из чисел с плавающей запятой, значит результат с плавающей запятой)
6.0 % 4 #=> 2.0
6 ** 4.0 #=> 1296.0
Лучше проверить эти сведения самостоятельно.
Поразрядная арифметика
правитьЗнак операции | Название |
---|---|
&
|
Побитовое «и» |
|
|
Побитовое «или» |
^
|
Побитовое «исключающее или» |
<<
|
Побитовый сдвиг влево |
>>
|
Побитовый сдвиг вправо |
~
|
Побитовая инверсия |
Операции побитовой арифметики заимствованы из языка Си. На этот раз без всяких экзотических особенностей.
6 & 4 #=> 4
6 | 4 #=> 6
6 ^ 4 #=> 2
6 << 4 #=> 96
6 >> 4 #=> 0 (чересчур намного сдвинули)
~4 #=> -5 (операция только над одним аргументом)
Здесь, вроде, всё понятно и без дополнительных пояснений. А если непонятно, то справочник по языку Си поможет.
Операции с присваиванием
правитьЧасто можно встретить выражения вида:
number_one += number_two
Это выполнение операции сразу с присваиванием. Вышеуказанная запись равнозначна следующей:
number_one = number_one + number_two
Вполне естественно, что вместо операции +
может использоваться любая другая, а вместо чисел могут быть другие типы данных.
string = "едем"
string += ", "
string *= 3
string #=> "едем, едем, едем, "
array = [1, 2, 3]
array += [4, 5]
array #=> [1, 2, 3, 4, 5]
При определении метода +
метод +=
вы получаете в подарок. Это правило касается всех бинарных операций, обозначаемых значками.
Методы явного преобразования типов
правитьМетод | Операция |
---|---|
to_f |
Преобразовать в число с плавающей запятой |
to_i |
Преобразовать в целое число |
to_s |
Преобразовать в строку |
to_a |
Преобразовать в массив (до версии 1.9+) |
Методы преобразования типов в Ruby традиционно начинаются с приставки to_
. Последующая буква — это сокращение от названия класса, в который происходит преобразование (f
— Float
— число с плавающей запятой, i
— Integer
— целое, s
— String
— строка, a
— Array
— массив). Посмотрим их действие на примере:
7.to_f #=> 7.0
7.9.to_i #=> 7
7.to_s #=> "7"
"7".to_a #=> ["7"]
Случайное число
правитьЧасто требуется получить случайное число. Пример:
rand(100) #=> 86
rand #=> 0.599794231588021
В первом случае метод rand
возвращает целое число в диапазоне от 0
до 99
(на единицу меньше 100). Во втором случае метод rand
возвращает число с плавающей запятой в диапазоне от 0.0
до 1.0
включительно. Различие в результате обусловлено передаваемым параметром:
- если передаётся параметр (в данном случае
100
), то генерируется целое случайное число (в диапазоне0..N-1
, гдеN
— передаваемый аргумент); - если параметр отсутствует, то генерируется число с плавающей запятой в диапазоне от
0.0
до1.0
.
Есть способ предсказать весь ряд «случайных» чисел. Делается это при помощи метода srand
. Ему передаётся целое число (идентификатор «случайной» последовательности). После этого весь случайный ряд можно предугадать. Проведём опыт: берусь угадать массив, который будет создан следующей программой.
srand 123
Array.new(5){ rand(100) } #=> [66, 92, 98, 17, 83]
Если вы выполните данную программу у себя, то получите тот же самый массив. 123 — номер «случайной» последовательности. Измените его и массив изменится!
Если вызвать srand
без параметра или не вызывать его вообще, то номер «случайной» последовательности выбирается случайным образом.
Хитрости
правитьЗадача: выдать целое число в двоичной системе счисления.
start_number = 1234
puts sprintf("%b", start_number) # метод sprintf заимствован из Си
puts start_number.to_s(2) # современный метод — означает «по основанию»,
# аргументом может служить не только 8 и 16, но и 5, 30…
# На самом деле, основание не может превышать 36,
# что вполне объяснимо — 10 цифр и 26 букв латинского алфавита.
Поменять порядок цифр данного числа на обратный:
start_number = 1234
puts start_number.to_s.reverse # метод reverse переворачивает строку
Получить значение N-го двоичного разряда данного целого числа:
start_number, N = 1234, 5
puts start_number[N]
Поменять целочисленные значения двух переменных без использования третьей переменной:
number_one, number_two = 134, 234
number_one, number_two = number_two, number_one
Округлить число с плавающей запятой до двух разрядов:
float_integer = 3.1415926535
puts (float_integer * 100).to_i.to_f / 100
puts ((float_integer + 0.005) * 100).to_i / 100.0
puts sprintf("%.2f", float_integer).to_f # полуСишный способ =)
На самом деле во второй строке оставляются два знака после запятой, а остальные просто отбрасываются безо всяких округлений, в то время как в третьей строке действительно происходит округление до двух знаков после запятой. Это легко проверить попытавшись округлить до трёх знаков после запятой:
float_integer = 3.1415926535
puts (float_integer * 1000).to_i.to_f / 1000 #=>3.141
puts ((float_integer + 0.0005) * 1000).to_i / 1000.0 #=>3.142
puts sprintf("%.3f", float_integer).to_f #=>3.142
Но всё же лучше:
float_integer = 3.1415926535
float_integer.round 3 #=>3.142 (возможно, что round округляет только до целых)
Подробнее о массивах
правитьМассивы — это тип данных, с которым вам придётся работать постоянно. Облик большинства программ зависит именно от правильного (читай: «изящного») использования массивов.
Способы создания массива
правитьМассив создаётся как минимум тремя способами. Первый способ:
[1, 2, 3, 4, 5, 6]
Вы просто перечисляете элементы массива через запятую, а границы массива обозначаете квадратными скобками. С таким методом создания массива мы уже встречались. А теперь попробуем второй способ, через вызов метода .new
класса Array
:
Array.new(6){ |index| index + 1 } #=> [1, 2, 3, 4, 5, 6]
Параметром метода .new
является количество элементов будущего массива (в данном случае это число 6). В фигурных скобках указано, как мы будем заполнять массив. В данном случае значение элемента массива будет больше на единицу его индекса. Третий способ заключается в создании объекта типа Range
(диапазон) и вызове метода .to_a
:
(1..6).to_a #=> [1, 2, 3, 4, 5, 6]
Есть ещё много способов, но эти три используются чаще всего.
Диапазоны
правитьМетодом to_a
очень удобно создавать из диапазона массив, содержащий упорядоченные элементы данного диапазона.
(1..10).to_a #=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
("a".."d").to_a #=> ["a", "b", "c", "d"]
Раз уж речь зашла о диапазонах, то давайте посмотрим, как они позволяют получать подмассивы. И насколько изящно у них это получается. Рассмотрим массив:
["a", "b", "c", "d", "e"]
Традиционно нумерация массива начинается с нуля и возрастает по одному:
["a" , "b" , "c" , "d" , "e" ]
Такая нумерация называется в Ruby положительной индексацией. «Хм, — скажете вы, — а есть ещё и отрицательная?» Да, есть!
["a" , "b" , "c" , "d" , "e" ]
Плюсы расставлены лишь для красоты. Но вернёмся к отрицательной индексации. Каков её смысл? Чтобы его пояснить, давайте решим задачку: дан массив, требуется получить предпоследний элемент.
array = ["a", "b", "c", "d", "e"]
array[array.size - 2] #=> "d"
В данном случае мы использовали метод .size
, который возвращает размер массива. Разработчики заметили, что вызов array.size
приходится писать довольно часто, и решили от него избавиться. Вот что получилось:
array = ["a", "b", "c", "d", "e"]
array[-2] #=> "d"
Индекс -2
значит «второй с конца элемент массива». Вот так и появилась отрицательная индексация. Теперь давайте разберёмся с диапазонами. Оказывается, в них тоже можно использовать отрицательную индексацию. Вот как можно получить все элементы массива кроме первого и последнего:
array = ["a", "b", "c", "d", "e"]
array[1..-2] #=> ["b", "c", "d"]
Или так:
array = ["a", "b", "c", "d", "e"]
array[1...-1] #=> ["b", "c", "d"]
Второй вариант с тремя точками, что автоматически приближает правую границу диапазона на одну позицию влево.
О двумерных массивах
правитьДля Ruby двумерный массив — это не более чем массив, содержащий одномерные массивы. Вот несколько примеров двумерных массивов:
[[1], [2, 3], [4]] # разная длина элементов-массивов
[[1, 2], [3, 4]] # одинаковая длина
[["прива", "Привет"], ["пока", "Всего хорошего"]] # двумерный массив (классика)
[["прива", "Привет"], [1, ["пока", "Всего хорошего"]]] # гибрид двух-трёх-мерного массива
- Двумерность массива средствами языка не отслеживается. Вполне могут возникнуть гибриды разномерных массивов.
- Подмассивы внутри двумерного массива могут иметь произвольную длину.
- Элементы из двумерного массива достаются последовательно: сначала элемент-массив, потом элемент.
Методы работы с массивами
правитьРазнообразие и полезность методов у массивов создаёт впечатление, что все сложные алгоритмы уже реализованы. Это не так, но программистам Ruby дана действительно обширная библиотека методов. Здесь мы рассмотрим лишь самые употребимые, остальные ищите в справочнике.
Получение размера массива
правитьВ Ruby массивы динамические: в каждый конкретный момент времени неизвестно, сколько в нём элементов. Чтобы не плодить тайн подобного рода и был реализован метод .size
:
[1, "считайте", 3, "количество", 5, 6, "запятых", 2, 5].size #=> 9
Мы явно указали массив, но на его месте могла стоять переменная:
array = [1, "считайте", 3, "количество", 5, 6, "запятых", 2, 5]
array.size #=> 9
Метод .size
есть у многих классов. Например, у ассоциативных массивов и строк. И даже у целых чисел.
Поиск максимального/минимального элемента
правитьВспомните сколько усилий вам приходилось прилагать, чтобы найти максимальный элемент? А сколько раз вы повторяли этот кусок кода в своих программах? Ну а в Ruby поиск максимального элемента осуществляется при помощи метода .max
, а в более сложных случаях при помощи метода .max_by
. Вот как это выглядит:
["у", "попа", "была", "собака"].max #=> "у" максимальный по значению
["у", "попа", "была", "собака"].max_by{ |elem| elem.size } #=> "собака" максимальный по размеру строки
Методы .min
и .min_by
работают аналогично:
["у", "попа", "была", "собака"].min #=> "была" минимальный по значению
["у", "попа", "была", "собака"].min_by{ |elem| elem.size } #=> "у" минимальный по размеру строки
Ну как? А в Ruby эти методы уже давно. Подробнее о том, как работает хеш "под капотом" можно дополнительно прочесть в статье.
Упорядочение
правитьЧтобы упорядочить массив, нужно вызвать метод .sort
или .sort_by
(начиная с версии 1.8).
["у", "попа", "была", "собака"].sort
#=> ["была", "попа", "собака", "у"] сортировка по значению
["у", "попа", "была", "собака"].sort_by{ |elem| elem.size }
#=> ["у", "попа", "была", "собака"] сортировка по размеру строки
Для двумерных массивов:
[[1,0], [16,6], [2,1], [4,5],[4,0],[5,6]].sort_by {|elem| elem[1]}
#=> [[1, 0], [4, 0], [2, 1], [4, 5], [16, 6], [5, 6]] сортировка "внешних" элементов по значению "внутренних"
[[1,0], [16,6], [2,1], [4,5],[4,0],[5,6]].sort_by {|elem| elem[0]}
#=> [[1, 0], [2, 1], [4, 0], [4, 5], [5, 6], [16, 6]]
Остается только добавить, что массивы упорядочиваются по возрастанию. Если вам надо по убыванию, то придётся писать собственный метод сортировки пузырьком. Шутка! По правде же, есть много способов выстроить массив по убыванию. Пока мы будем использовать метод .reverse
, обращающий массив.
Обращение массива
правитьОбращение массива — это изменение порядка элементов на обратный, то есть первый элемент становится последним, второй элемент — предпоследним и так далее.
Для обращения массива существует метод .reverse
. Применим его к предыдущим примерам, чтобы получить сортировку по убыванию:
["у", "попа", "была", "собака"].sort.reverse #=> ["у", "собака", "попа", "была"]
["у", "попа", "была", "собака"].sort_by{ |elem| elem.size }.reverse #=> ["собака", "была", "попа", "у"]
Метод .reverse
мы просто прицепили в конец предыдущего примера. Так можно выстроить произвольную цепочку допустимых методов; выполняться они будут по очереди, начиная с самого левого, то есть самого первого в цепочке.
Сложение/вычитание массивов
правитьДля сложения массивов, строк и чисел используется метод +
:
[1, 2, 3, 4] + [5, 6, 7] + [8, 9] #=> [1, 2, 3, 4, 5, 6, 7, 8, 9]
Плюс берёт массив справа и, будто это железнодорожный состав, прицепляет его к хвосту первого массива. Это называется конкатенацией.
Вычитаются массивы методом -
, но происходит это сложнее, чем расцепление вагонов:
[1, 1, 2, 2, 3, 3, 3, 4, 5] - [1, 2, 4] #=> [3, 3, 3, 5]
Из первого массива удаляются все элементы, имеющиеся во втором, независимо от их количества. Остальные элементы остаются без изменений, сохраняют относительные позиции.
Объединение и пересечение массивов (как множеств)
правитьОчень часто приходится решать задачи, в которых нужно оперировать множествами. У массивов припасено для этих целей два метода: |
(объединение) и &
(пересечение).
Рассмотрим объединение множеств в действии:
[1, 2, 3, 4, 5, 5, 6] | [0, 1, 2, 3, 4, 5, 7] #=> [1, 2, 3, 4, 5, 6, 0, 7]
Объединение получается вот так. Сначала массивы сцепляются:
[1, 2, 3, 4, 5, 5, 6, 0, 1, 2, 3, 4, 5, 7]
Затем, начиная с первого вагона, инспектор идёт от вагона к вагону, удаляя элементы, которые уже встречались. После зачистки получается настоящее логическое объединение.
На деле это выглядит так:
[1, 2, 3, 4, 5, -5-, 6, 0, -1-, -2-, -3-, -4-, -5-, 7]
Зачёркнутые числа — это удаленные элементы (дубликаты). Переходим от слов к делу:
[1, 2, 3, 4, 5, 5, 6] & [0, 2, 1, 3, 5, 4, 7] #=> [1, 2, 3, 4, 5]
При пересечении двух массивов, из первого удаляются все элементы, отсутствующие во втором. А из второго, отсутствующие в первом. При этом относительный порядок остающихся элементов первого массива сохраняется.
[1, 2, 3, 4, 5, -5-, -6-] & [-0-, 2, 1, 3, 5, 4, -7-]
Всё просто. Важно лишь помнить, что |
и &
не изменяют ни первый, ни второй исходные массивы. Они через описанные процедуры создают новый массив. Чтобы тот не уехал от вас, нужно присвоить его; но не себе, а переменной, приготовленной слева от =
.
В итоге пересечения или объединения множеств получается массив, не содержащий дубликатов.
Удаление дубликатов
правитьДля удаления дубликатов (повторяющихся элементов массива) в Ruby используется метод .uniq
:
[1, 2, 3, 4, 5, 5, 6, 0, 1, 2, 3, 4, 5, 7].uniq #=> [1, 2, 3, 4, 5, 6, 0, 7]
Процесс зачистки массива от дубликатов такой же, как и в объединении.
[1, 2, 3, 4, 5, -5-, 6, 0, -1-, -2-, -3-, -4-, -5-, 7]
Поэтому объединение массивов можно записать как
(array1 + array2).uniq
Но проще, конечно, объединять палкой.
Сплющивание массивов
правитьМетод .flatten
делает из многомерного массива простой, длинный одномерный массив. Он как бы расплющивает его. Например, чтобы найти в двумерном массиве наибольший элемент, мы сперва расплющим массив, а потом найдём максимум методом .max
:
array = [[1, 2], [3, 4]]
array.flatten.max #=> 4
Расплющивание происходит в несколько этапов. Сначала происходит удаление всех квадратных скобок.
-[[- 1, 2 -]-, -[- 3, 4 -]]-
А потом, две квадратные скобки добавляются слева и справа. Но делать это надо быстро, чтобы элементы не успели разбежаться.
[1, 2, 3, 4]
Вот и всё! У нас они разбежаться не успели. Повторите данное упражнение на других массивах (двумерных, трёхмерных и так далее).
Удаление неопределённых (nil) элементов
правитьФункцию удаления элементов nil
массива выполняет метод .compact
например:
array = [1, nil, 2, nil, 3]
array.compact #=> [1, 2, 3]
Транспонирование двумерного массива
правитьЗадача: дан двумерный массив. Вывести одномерный массив с максимумами каждого из столбцов. Хм… посмотрим сперва, как эта задача решается для строчек, а не столбцов:
array2D = [[1, 2], [3, 4]]
array2D.map{ |array| array.max } #=> [2, 4]
Метод .map
— это итератор, который позволяет нам делать что-нибудь с каждым объектом, на который указывает массив. Подробнее о них ниже в этой главе.
Чтобы решить задачу в первоначальном варианте, нам надо лишь предварительно транспонировать массив (поменять местами строки и столбцы):
array2D = [[1, 2], [3, 4]]
array2D.transpose.map{ |array| array.max } #=> [3, 4]
Метод .transpose
как раз и занимается транспонированием. Это позволяет с лёгкостью решать задачи про столбцы приёмами, схожими с задачами про строки.
Размножение массивов
правитьРечь пойдёт не о почковании, а о методе, который позволяет умножать массив на целое число. В результате такого умножения мы получим массив, состоящий из нескольких копий элементов исходного массива.
["много", "денег", "прячет", "тёща"] * 2
#=> ["много", "денег", "прячет", "тёща", "много", "денег", "прячет", "тёща"]
Того же самого эффекта можно добиться сцепив массив необходимое количество раз:
array = ["много", "денег", "прячет", "тёща"]
array + array #=> ["много", "денег", "прячет", "тёща", "много", "денег", "прячет", "тёща"]
Заметили, что есть некоторая параллель с целыми числами? Умножение можно заменить сложением и наоборот!
Функциональность стека
правитьЧасто и во многих алгоритмах надо добавить элемент в конец массива:
array = [1, 2, 3, 4, 5]
array[array.size] = 6
array #=> [1, 2, 3, 4, 5, 6]
И если уж добавили, то надо как-то его и удалить. Делается это примерно так:
array = [1, 2, 3, 4, 5, 6]
array[0...-1] #=> [1, 2, 3, 4, 5]
Но как всегда, эти задачи возникали слишком часто и их решили реализовать в виде методов. Методы назвали .push
(«втолкнуть» в конец массива) и .pop
(«вытолкнуть» элемент из массива):
array = [1, 2, 3, 4, 5]
array.push(6)
array #=> [1, 2, 3, 4, 5, 6]
array << 7 #=> [1, 2, 3, 4, 5, 6, 7], другой синтаксис
array.pop #=> 7
array #=> [1, 2, 3, 4, 5, 6]
Функциональность очереди и списка
правитьЧтобы можно было использовать массив в качестве очереди и/или списка, потребуется сделать всего лишь пару методов. Первый из них добавляет элемент в начало массива, а второй удаляет элемент из начала. Давайте посмотрим, как это делается универсальными методами []
, []=
и +
:
array = [1, 2, 3, 4, 5]
# добавим элемент в начало массива
# способ № 1
array = [6] + array
array #=> [6, 1, 2, 3, 4, 5]
array[0] #=> 6
# способ № 2
array[1..array.size] = array[0..-1] #=> [1, 1, 2, 3, 4, 5]
array[0] = 6
array #=> [6, 1, 2, 3, 4, 5]
# удалим элемент из начала массива
array = array[1..-1]
array #=> [1, 2, 3, 4, 5]
Теперь посмотрим, какие методы реализуют точно такую же функциональность:
array = [1, 2, 3, 4, 5]
# добавляем элемент в начало массива
array.unshift(6) #=> [6, 1, 2, 3, 4, 5]
# удаляем из начала массива
array.shift #=> 6
array #=> [1, 2, 3, 4, 5]
Удаляющий метод — .shift
(«сдвинуть»), а метод, добавляющий элемент в начало массива, называется .unshift
(непереводимое слово, означающее нечто противоположное «сдвинуть обратно»).
Мнемограмма для методов стека/очереди/списка
правитьМнемограмма для методов .shift
, .unshift
, .pop
и .push
:
.unshift(0) .push(6)
[1, 2, 3, 4, 5]
.shift .pop
Методы с параметром (сверху) добавляют элемент в массив, а методы без параметра (снизу) — удаляют. По желанию можно дорисовать стрелочки.
Метод .shift
сдвигает влево,
Метод .pop
— направо.
Метод .push
к концу цепляет,
А .unshift
— к началу.
Замечание. Имена методов unshift
/shift
неинтуитивны. Во-первых, они не напоминают о том, что работа идёт с головой массива, а не с хвостом, во-вторых ничего не говорят о том, идёт заполнение или опустошение стека. Можно создать для этих методов псевдонимы с говорящими именами, например, feed
/spit
(кормить/выплевывать):
class Array
alias feed :unshift
alias spit :shift
end
Создание своих классов, работающих как массивы
правитьЕсли потребуется написание своего класса, который работает, как описанные массивы, то возникают некоторые тонкости. Дело в том, что реализация всех описанных методов займёт жуткое количество времени и сил. На самом деле, для реализации большинства описанных методов, достаточно реализовать .each
.
Где это может понадобиться? Например, вы реализуете класс, который умеет читать из файла записи определённой структуры. Основную его логику занимает именно чтение нужного формата, кеширование, разбор, десериализация и тому подобное.
Просто реализуйте .each
и включите в ваш класс примесь Enumerable
. В нём находится реализация методов, таких как .inject
, .each_with_index
и тому подобные.
Логические методы
правитьЛогический метод — это метод, результатом которого является логическое выражение (true
или false
).
По японской традиции, имена логических методов принято заканчивать ?
(вопросительным знаком). Это позволяет также получить список логических методов, вызываемых в данном случае: просто отобрать из всех имеющихся методов те, что кончаются на ?
. Делается это при помощи небольшой вспомогательной программы:
array = [1, 2, 2, 3]
puts array.methods.grep(/\?$/)
Для удобства, можно упорядочить полученный список:
array = [1, 2, 2, 3]
puts array.methods.grep(/\?$/).sort
Есть ли элемент в массиве?
правитьКак узнать, есть ли некоторый элемент в массиве? Попробуем решить эту задачу при помощи метода .size
и итератора .find_all
:
array = [1, 2, 3, 4, 5, 6, 7]
required = 5 # число, которое мы будем искать
array.find_all{ |elem| elem == required }.size != 0 #=> true
# это значит, что такое число есть
Использование связки из трёх методов (!=
, .find_all
и .size
) для такой задачи — возмутительно! Разработчики не могли с этим долго мириться и реализовали метод специально для этой задачи. Имя ему — .include?
. Перепишем нашу задачу, но на этот раз будем использовать правильный метод:
array = [1, 2, 3, 4, 5, 6, 7]
required = 5 # число, которое мы будем искать
array.include?(required) #=> true
# что бы это значило?
Мутный горизонт скрывает берег,
Ветер мокр, холоден и лют.
Есть ли в озере акулы, я проверю
Методом логическим .include?
.
lake = ["правый берег", "ветер", "вода", "вода", "вода", "окунь", "вода", "вода", "левый берег"]
lake.include?("акула") #=> false
Опытным путём мы доказали, что акулы в озере не водятся.
Массив пустой?
правитьЕсли вы хотите задать массиву вопрос «пуст ли ты?», но боитесь обидеть, то можете пойти окружным путём. Например, спросить у него: ты равен пустому массиву?
empty_array = []
filled_array = [1, 2, 2, 3]
empty_array == [] #=> true
filled_array == [] #=> false
Ещё можно задать вопрос: твой размер равен нулю?
empty_array = []
filled_array = [1, 2, 2, 3]
empty_array.size == 0 #=> true
filled_array.size == 0 #=> false
Но наш вам совет: не стоит искать обходных путей. Спросите его напрямую: .empty?
(«пуст?»):
empty_array = []
filled_array = [1, 2, 2, 3]
empty_array.empty? #=> true
filled_array.empty? #=> false
И наоборот
правитьВ Ruby принято избегать отрицания условия. Например, если вам нужно сделать что-то, если массив не пуст, можно воспользоваться методом, обратным empty?
. Этот метод называется any?
.
array = [1, 2, 4]
array.length > 0 #=> true
array.empty? #=> false
array.any? #=> true
Итераторы
правитьМассивы — эти эшелоны переменных, эти ожерелья запятых и элементов — часто приходится проходить целиком, обследуя каждый элемент. Да и не только массивы, но и любые последовательности чего-нибудь.
В старину люди делали это циклами. В Ruby у списочных структур данных есть встроенные методы, которые проходят весь ряд поэлементно, но, в отличие от циклов:
- не зацикливаются: счётчик цикла нельзя докручивать;
- выполняются заведомое число раз;
- их много, и каждый делает своё дело.
Имя им — итераторы.
Изменение всех элементов массива
правитьИзменить все элементы массива можно по-всякому. Начнём с обнуления:
array = ["шифровка", "Штирлица", "в", "Центр", "секретно"]
array.map{ 0 } #=> [0, 0, 0, 0, 0]
В приведенном примере каждый элемент массива будет заменён нулем независимо от того, чем является этот элемент. Например, при попытке обнулить таким образом двумерный массив [[1, 2], [3, 4]]
, в результате получим [0, 0]
.
Используется итератор .map
, за которым следует замыкание, — кусочек кода, схваченный лапками-фигурными скобками. .map
последовательно проходит array
и выполняет замыкание заново для каждого элемента. То, что выходит из замыкания, итератор .map
делает очередным элементом нового массива.
Можно дать элементу .map
иное задание. Для этого зажимаем в фигурные скобы замыкания иной код:
array = [1, 2, 3, 4, 5]
array.map{ |elem| elem ** 2 } #=> [1, 4, 9, 16, 25]
Прежде, чем замыканию выдать квадрат очередного элемента, ему нужно знать этот элемент. Итератор .map
даёт ему значение элемента, словно фотографию, обрамлённую слева и справа вертикальными чертами |
. Чтобы замыкание смогло взять эту фотографию, обязательно нужно дать ей имя. В нашем случае это elem
, но подходят и такие названия:
_element
x
y
a
b
Но недопустимы названия вроде Like_This
или 3This
. Правила именования тут такие же, как и для обычных переменных.
Вы уже, наверное, хорошо поняли, что в итераторах массивы обрабатываются по очереди; двери вагона расходятся, появляется элемент. Замыкание даёт ему прозвище, выполняет код в лапках-фигурных скобках. Затем переходит к следующему вагону, и там всё сначала.
Но ещё важно помнить, что элементы не уходят из первого вагона: замыкание лишь осматривает каждый элемент, берёт его значение, но не меняет. Всё, что получается в результате работы замыкания, садится в очередной вагон другого поезда.
То имя, что показывается в раздвижных дверях, — это не сам элемент, это лишь его копия. Фотография. Голограмма. Это даже не другая переменная, это не переменная вообще. Бессмысленно присваивать новое значение фотографии:
array = [1, 2, 3, 4, 5]
array.map{ |elem| elem = elem**2 }
# присваивание не имеет смысла: elem несёт лишь значение элемента, не являясь им
Из итератора .map
выезжает другой поезд, у которого вместо соответствующего элемента первого поезда сидит результат вычисления замыкания. Используйте этот массив как-нибудь, иначе поезд уедет.
array = [1, 2, 3, 4, 5]
array.map{ |elem| elem**2 } #=> [1, 4, 9, 16, 25]
array #=> [1, 2, 3, 4, 5] — неизменный первый поезд
Можно присвоить его новой переменной array_of_squares
. А можно заместить им существующую переменную array
:
array = [1, 2, 3, 4, 5]
array = array.map{ |elem| elem**2 } #=> [1, 4, 9, 16, 25]
array #=> [1, 4, 9, 16, 25]
Это общее явление Ruby: методы (здесь — итераторы) не меняют объект (массив), с которым работают. Они лишь выдают результат, который потом можно использовать как аргумент или присвоить переменной.
Явление было воспето в фольклоре:
Метод .map
всё изменяет,
Как кто пожелает
И обижается на тех,
Кто результат не сохраняет.
Для того, чтобы сохранить результат выполнения метода в исходную переменную, нужно добавить к названию метода восклицательный знак.
array = [1, 2, 3, 4, 5]
array.map!{ |elem| elem**2 } #=> [1, 4, 9, 16, 25]
array #=> [1, 4, 9, 16, 25]
Такой приём работает и с многими другими методами в языке, которые только возвращают результат своего выполнения. Однако при использовании цепочек методов каждый должен быть с восклицательным знаком, иначе разорвётся цепочка копирований.
array = [1, 2, 3, 4, 5, nil]
array.compact.map!{ |elem| elem**2 } #=> [1, 4, 9, 16, 25]
array #=> [1, 2, 3, 4, 5, nil]
array.compact!.map!{ |elem| elem**2 } #=> [1, 4, 9, 16, 25]
array #=> [1, 4, 9, 16, 25]
В первом случае операция .compact создала копию массива, тогда как .compact! заменила первоначальные значения результатами, полученными от .map!
Отбор элементов по признаку
правитьВот как итератор .find_all
выберет из массива все чётные элементы:
array = [1, 2, 3, 4, 5]
array.find_all{ |elem| elem % 2 == 0 } #=> [2, 4]
elem % 2 == 0
— это вопрос «чётен ли elem
?». Ответом, как всегда, будет true
или false
. Ведь «чётность» — это равенство нулю (== 0
) остатка деления (%
) на 2
.
Кстати, равенство нулю можно проверять и при помощи метода .zero?
. А чётность тоже можно проверить разными способами:
(elem % 2).zero?
(elem & 1).zero?
(elem[0]).zero? # Этот вариант круче всех
Если на вопрос, заданный в замыкании, ответ true
, то |elem|
(значение очередного элемента исходного массива), заносится в новый массив, который в итоге будет выводом из итератора .find_all
.
Выражение в замыкании для .find_all
должно быть логическим, то есть принимать значение true
или false
.
Если нужно элементы
По условию искать,
То полезнее .find_all
Метод вам не отыскать!
Также есть методы .odd?
и .even?
для более наглядной реализации таких вещей, нечетный и четный соответственно.
array = [1, 2, 3, 4, 5]
array.find_all{ |elem| elem.even? } #=> [2, 4]
Суммирование/произведение/агрегация элементов
правитьОчень часто возникает задача найти сумму/произведение всех элементов массива. Для этих целей традиционно используется итератор .inject
. Для демонстрации его работы, давайте найдем сумму элементов массива:
array = [1, 2, 3, 4, 5]
array.inject(0){ |result, elem| result + elem } #=> 15
Рассмотрим все по порядку. Начнём с нуля. Его следует расшифровывать как result = 0
перед началом работы итератора, то есть это начальное значение переменной result
(переменной промежуточного результата).
Далее идёт объявление двух переменных. Первая из них (result
) будет хранить промежуточный результат. Вторая (elem
) — фотография текущего элемента массива (или последовательности), мы такую уже видели.
После объявления описан алгоритм работы итератора. В данном случае ему предписано каждый элемент массива складывать с промежуточной суммой: result + elem
.
Учитывая эти два замечания, напишем код, который является неправильным:
array = [1, 2, 3, 4, 5]
array.inject(0){ |result, elem| result = result + elem } #=> 15
Имена переменных result
и elem
созданы богатым воображением автора. В ваших программах они могут называться иначе.
Невероятно, но от изменения имён переменных результат не меняется. Помните это!
Для полноты картины решим ещё одну задачку. На этот раз будем искать произведение всех элементов массива:
array = [1, 2, 3, 4, 5]
array.inject(1){ |result, elem| result * elem } #=> 120
А вот так можем вычислить факториал для заданного числа (n!):
n = 9
(1..n).to_a.inject(){ |one, two| one * two } #=> 362880
Чтобы закрепить материал, решите задачу: найти произведение всех положительных элементов массива. Подсказка: используйте метод .find_all
.
Элементов надо кучу
Перемножить иль сложить?
Есть .inject
на этот случай,
Его бы вам употребить.
Разбиение надвое
правитьИтератор .partition
делит массив на две части по некоторому бинарному признаку (чётности, положительности, наличию высшего образования и тому подобным). Вот как разделить массив на две части по признаку кратности трём:
array = [1, 2, 3, 4, 5, 6, 7, 8, 9]
array.partition{ |x| (x % 3).zero? } #=> [[3, 6, 9], [1, 2, 4, 5, 7, 8]]
В результате работы итератора получился массив, состоящий из двух элементов-массивов. Первый элемент-массив содержит все элементы, которые удовлетворяют условию, а второй, которые не удовлетворяют. Обратите внимание, как проверяется кратность трём. Ничего не напоминает? Например, итератор .find_all
? Нет? Ну и ладно.
Есть интересная хитрость, позволяющая разместить массив, полученный .partition
, в две разные переменные:
array = [1, 2, 3, 4, 5, 6, 7, 8, 9]
one, two = array.partition{ |x| (x % 3).zero? }
one #=> [3, 6, 9]
two #=> [1, 2, 4, 5, 7, 8]
Этот метод называется множественным присваиванием (multiple assignment) и широко используется в ситуациях, когда из метода надо вернуть более одного значения. Но об этом позднее.
Логические итераторы
правитьВ версии 1.8 появилось несколько логических методов: .all?
и .any?
. Они положили начало такому классу методов, как логические итераторы.
Логический итератор — это итератор (метод, обрабатывающий все элементы последовательности), возвращающий значение логического типа — true
или false
.
Конечно же, идея логических итераторов долгое время летала в ноосфере. Существовали итераторы, которые являлись условно-логическими: они возвращали nil
в случае неудачи и какой-либо объект — в случае удачи. В логическом контексте поведение таких итераторов можно было посчитать логическим (false
→ nil
, а true
→ число/строка/любой объект). Примером условно-логического итератора служит метод .detect
.
Все ли элементы удовлетворяют условию?
правитьВ математической логике такой «итератор» называется квантором общности, обозначается символом . На языке Ruby он называется .all?
. По сложившейся традиции, давайте посмотрим, как решалась бы эта задача до версии 1.8, то есть до появления логических итераторов:
array = [1, 2, 2, 3]
array.inject(true){ |result, elem| result && (elem > 2) } #=> false
В примере используется так называемый механизм презумпции виновности. Переменной result
присваивается значение true
. Логическое умножение изменяет значение переменной result
на false
.
Данная программа проверяет, все ли элементы array
больше двух.
Ещё один вариант решения той же задачи:
array = [1, 2, 2, 3]
array.find_all{ |elem| elem <= 2 }.size.zero? #=> false
Давайте решим эту же задачу при помощи новоявленного логического итератора:
array = [1, 2, 2, 3]
array.all?{ |elem| elem > 2 } #=> false
Несмотря на то, что код получился короче, результат остался прежним: утверждение, что все элементы массива больше двух, ложно.
Хотя бы один элемент удовлетворяет условию?
правитьВслед за квантором общности (он же — логический итератор .all?
), из математической логики был перенесён и квантор существования — . На языке Ruby он называется .any?
. Чтобы оценить его по достоинству, посмотрим решение задачи без его участия. Проверим, содержит ли array
хотя бы один элемент больший двух:
array = [1, 2, 2, 3]
array.inject(false){ |result, elem| result || (elem > 2) } #=> true
В данном примере используется так называемый механизм презумпции невиновности. Переменной result
присваивается значение false
. В результате логического сложения, происходит изменение значения переменной result
на true
.
Теперь тоже самое, но через логический итератор .any?
:
array = [1, 2, 2, 3]
array.any?{ |elem| elem > 2 } #=> true
Естественно, что с появлением логических итераторов, реализация задач математической логики (в рамках языка Ruby) стала удобней.
Хитрости
правитьВот так можно сгенерировать «хороший пароль» — произвольную последовательность из чисел или латинских букв, общей длиной в 8 символов.
symbols = ["a".."z", "A".."Z", "0".."9"].map{ |range| range.to_a }.flatten
puts (0...8).map{ symbols.sample }.join
Метод sample возвращает случайный элемент из массива.
Перемешать упорядоченный массив:
array = [1, 2, 3, 4, 5, 6, 7]
array.sort_by{ rand } #=> перемешанный массив
Выстроить элементы массива по убыванию без использования .reverse
:
array = [2, 1, 3, 5, 6, 7, 4]
array.sort{ |x, y| y <=> x } #=> [7, 6, 5, 4, 3, 2, 1]
Задачи про массивы
правитьПомимо приведённых ниже задач, советуем вам взглянуть на задачи из нашего сборника задач.
Возможные варианты решений на Обсуждение:Ruby/Подробнее о массивах
Одномерные
править- Вывести индексы массива в том порядке, в котором соответствующие им элементы образуют возрастающую последовательность.
- В численном массиве найти сумму отрицательных элементов.
- Найти все индексы, по которым располагается максимальный элемент.
- В массиве переставить в начало элементы, стоящие на чётной позиции, а в конец — стоящие на нечётной.
Двумерные
править- Поменять первый и последний столбец массива местами.
- Упорядочить N-ый столбец.
- Упорядочить строки, содержащие максимальный элемент.
- Упорядочить строки, если они не отсортированы и перемешать, если они отсортированы.
- Упорядочить строки массива по значению элемента главной диагонали в каждой из строк (в исходном массиве).
- Найти номера строк, элементы которых упорядочены по возрастанию.
Двумерные целочисленные
править- Найти максимальный элемент для каждого столбца, а затем получить произведение этих элементов.
- Найти минимум в двумерном массиве.
- Найти произведение положительных элементов.
- Найти сумму положительных элементов, больших К.
- Вычислить сумму и среднее арифметическое элементов главной диагонали.
- Найти номера строк, все элементы которых — нули.
Подробнее об ассоциативных массивах
правитьРазличают два типа массивов: индексные, у которых в качестве индекса только целое число, и ассоциативные, где индексом может быть любой объект.
Индексные массивы чаще всего называют просто «массивами», а ассоциативные массивы — «хешами» или «словарями». |
Хеши можно представить как массив пар: ключ=>значение
. Но в отличие от массива, хеш неупорядочен: нельзя заранее сказать, какая пара будет первой, а какая последней. Правда, удобство использования массива это не умаляет. Более того, поскольку в Ruby переменные не типизированы и методам с похожей функциональностью дают похожие имена, то использование хеша чаще всего равносильно использованию массива.
Несмотря на мощь хеша, использовать его не всегда целесообразно. Бывают задачи, решаемые с хешами легко и удобно, но таких задач мало. Чаще всего хватает использования массива. Но представление о классе задач для хеша надо иметь.
Давайте создадим хеш, где в качестве ключа будем использовать целое число:
hash = {5=>3, 1=>6, 3=>2}
hash[5] #=> 3
hash[2] #=> nil это значит, что объект отсутствует
hash[3] #=> 2
А вот так будет выглядеть та же самая программа, если мы будем использовать массив:
array = [nil, 6, nil, 2, nil, 3]
array[5] #=> 3
array[2] #=> nil
array[3] #=> 2
Первый случай применимости хеша: если в массиве намечаются обширные незаполненные (то есть заполненные nil
) области, то целесообразнее использовать хеш с целочисленным индексом.
Использовать хеш в данном случае лучше потому, что, формально, хеш для данного примера состоит из трёх значащих пар, а массив — из шести элементов, из которых лишь три элемента значащие. Исходя из этого, можно заключить, что массив будет хранить избыточную информацию, а хеш — только нужную.
Продолжим поиски случаев применимости хеша и на этот раз подсчитаем, сколько раз каждое число повторяется в данном целочисленном массиве. Решение массивом:
array = [1, 2, 1, 2, 3, 2, 1, 2, 4, 5]
array.uniq.map{ |i| [i, array.find_all{ |j| j == i }.size] } #=> [[1, 3], [2, 4], [3, 1], [4, 1], [5, 1]]
Алгоритм получается ужасным. Не буду утомлять излишними терминами, а замечу, что по одному и тому же массиву итераторы (в количестве двух штук) пробегают много раз. А ведь достаточно одной «пробежки». Понятное дело, что такая программа не сделает вам чести. В качестве упражнения, предлагаю вам решить эту задачу другим, более оптимальным, способом.
Теперь рассмотрим решение этой же задачи, но с применением хеша:
array = [1, 2, 1, 2, 3, 2, 1, 2, 4, 5]
array.inject(Hash.new{ 0 }){ |result, i|
result[i] += 1
result
} #=> {5=>1, 1=>3, 2=>4, 3=>1, 4=>1}
Удалось избавиться от лишних методов и обойтись лишь одной «пробежкой» итератора по массиву.
Начальный хеш был создан хитроумной комбинацией Hash.new{0}
, что в переводе на русский означает примерно следующее: «создадим пустой хеш, в котором любому несуществующему ключу будет соответствовать 0
». Это нужно, чтобы суммирование (метод +
) не выдавало ошибку вида: «не могу сложить nil
и число типа Fixnum
». В качестве упражнения, предлагаю вам заменить комбинацию Hash.new{ 0 }
на {}
и посмотреть, чем это чревато.
Зачем нужно дописывать result
? Дело в том, что комбинация result[i] += 1
имеет в качестве результата целое число (учитывая, что массив целочисленный), а не хеш. Следовательно, параметру result
автоматически будет присвоено целое число (см. описание итератора .inject
). На следующей итерации мы будем обращаться к result
, как к хешу, хотя там уже будет храниться число. Хорошо, если программа выдаст ошибку, а если нет? Проверьте это самостоятельно.
В качестве упражнения, предлагаю вам переписать программу без вышеописанных двух особенностей (используйте метод .update
). Решение будет опубликовано ниже.
Второй случай применимости хеша: если требуется подсчитать число элементов массива, то целессобразнее применять хеш. Кстати, вместо подсчета количества, можно использовать конкатенацию массивов или строк. Но это уже более сложные задачи, которые будут рассмотрены позже.
Теперь представим, что мы работаем системными администраторами. У нас есть список DNS-имён и IP-адреса. Каждому DNS-имени соответствует только один IP-адрес. Как нам это соответствие записать в виде программы? Попробуем это сделать при помощи массива:
array = [["comp1.mydomen.ru", "192.168.0.3"],
["comp2.mydomen.ru", "192.168.0.1"],
["comp3.mydomen.ru", "192.168.0.2"]]
Всё бы ничего, но чтобы найти IP-адрес по DNS имени, придётся перелопатить весь массив в поиске нужного DNS:
dns_name = "comp1.mydomen.ru"
array.find_all{ |key, value| key == dns_name }[0][-1] #=> "192.168.0.3"
В данном примере было использовано два интересных приёма:
- Если в двумерном массиве заранее известное количество столбцов (в нашем случае — два), то каждому из столбцов (в рамках любого итератора) можно дать своё имя (в нашем случае:
key
иvalue
). Если бы мы такого имени не давали, то вышеописанное решение выглядело бы так:
array.find_all{ |array| array[0] == dns_name }[0][-1] #=> "192.168.0.3"
Без именования столбцов, внутри итератора вы будете работать с массивом (в двумерном массиве каждый элемент — массив, а любой итератор «пробегает» массив поэлементно). Это высказывание действительно, когда «пробежка» осуществляется по двумерному массиву.
- Метод
.find_all
возвращает двумерный массив примерно следующего вида:[["comp1.mydomen.ru", "192.168.0.3"]]
, чтобы получить строку"192.168.0.3"
необходимо избавиться от двумерности. Делается это при помощи метода[]
, который вызывается два раза (понижает размерность c двух до нуля). Метод[0]
возвращает в результате —["comp1.mydomen.ru", "192.168.0.3"]
, а метод[-1]
—"192.168.0.3"
. Как раз это нам и было нужно.
Теперь ту же самую задачу решим, используя хеш:
hash = {"comp1.mydomen.ru"=>"192.168.0.3",
"comp2.mydomen.ru"=>"192.168.0.1",
"comp3.mydomen.ru"=>"192.168.0.2"}
hash["comp1.mydomen.ru"] #=> "192.168.0.3"
Нет ни одного итератора и, следовательно, не сделано ни одной «пробежки» по массиву.
Третий случай применимости хеша: когда требуется сопоставить один набор данных с другим, то целесообразнее использовать хеш.
Вполне естественно, что существуют и другие случаи применимости хеша, но вероятность столкнуться с ними в реальной работе намного меньше. Вышеописанных трёх случаев должно хватить надолго.
В заключении, как и было обещано, приводится решение задачи с использованием метода .update
:
array = [1, 2, 1, 2, 3, 2, 1, 2, 4, 5]
array.inject({}){ |result, i| result.update({ i=>1 }){ |key, old, new| old+new }}
#=> {5=>1, 1=>3, 2=>4, 3=>1, 4=>1}
Описание метода .update
будет дано ниже. На данном этапе попытайтесь угадать принцип работы метода .update
.
Что используется в качестве ключей?
правитьВ качестве ключей ассоциативного массива можно использовать любые типы. Например, другие ассоциативные массивы, строки, числа, символы или просто объекты любых классов.
Если состояние объектов-ключей изменилось, то хешу необходимо вызвать метод .rehash
.
array1 = ["а", "б"]
array2 = ["в", "г"]
hash = {array1=>100, array2=>300}
hash[array1] #=> 100
array1[0] = "я"
hash[array1] #=> nil
hash.rehash #=> {["я", "б"]=>100, ["в", "г"]=>300}
hash[array1] #=> 100
В данном примере ключами хеша (hash
) являются два массива (array1
и array2
). Одному из них (array1
) мы изменили нулевой элемент (с "а"
на "я"
). После этого доступ к значению был потерян. После выполнения метода .rehash
всё встало на свои места.
Как Ruby отслеживает изменение ключа в ассоциативном массиве? Очень просто: с помощью метода .hash
, который генерирует «контрольную сумму» объекта в виде целого числа. Например:
[1, 2, 3].hash #=> 25
Способы создания ассоциативного массива
правитьПри создании ассоциативного массива важно ответить на несколько вопросов:
- Какие данные имеются?
- Какого типа эти данные?
- Что будет ключом, а что — значением?
Ответы определят способ создания хеша.
Из одномерного массива
правитьПоложим, что у нас в наличии индексный массив, где ключ и значение записаны последовательно. Тогда мы можем использовать связку методов *
и Hash[]
:
array = [1, 4, 5, 3, 2, 2]
Hash[*array] #=> {1=>4, 5=>3, 2=>2}
Элементы, стоящие на нечётной позиции (в данном случае: 1, 5 и 2) стали ключами, а элементы, стоящие на чётной позиции (то есть: 4, 3 и 2), стали значениями.
Из двумерного массива
правитьЕсли повезло меньше и нам достался двумерный массив с элементами вида [["ключ_1", "значение_1"], ["ключ_2", "значение_2"], ["ключ_3", "значение_3"], …]
, то его надо сплющить (.flatten
) и тем задача будет сведена к предыдущей:
array = [[1, 4], [5, 3], [2, 2]]
Hash[*array.flatten] #=> {1=>4, 5=>3, 2=>2}
Каждый нулевой элемент подмассива станет ключом, а каждый первый — значением.
Но может случиться так, что двумерный массив будет состоять из двух подмассивов: подмассива ключей и подмассива значений:
[["ключ_1", "ключ_2", "ключ_3", …], ["значение_1", "значение_2", "значение_3", …]]
Вспоминаем методы работы с массивами. Там был метод .transpose
(транспонирование массива), вызов которого сведёт задачу к предыдущей.
array = [[1, 5, 2], [4, 3, 2]]
Hash[*array.transpose.flatten] #=> {1=>4, 5=>3, 2=>2}
Нет данных
правитьЕсли нет данных, то лучше записать хеш как пару фигурных скобок:
hash = {}
hash[1] = 4
hash[5] = 3
hash[2] = 2
hash #=> {1=>4, 5=>3, 2=>2}
И уже по ходу дела разобраться, что к чему.
Известен только тип значений
правитьСведения о типе значений использовать следует так: создать хеш, в котором будет определён элемент по умолчанию. Элементом по умолчанию должен быть нулевой элемент соответствующего типа, то есть для строки это будет пустая строка (""
), для массива — пустой массив ([]
), а для числа — нуль (0
или 0.0
). Это делается, чтобы к пустому элементу можно было что-то добавить и при этом не получить ошибку.
hash = Hash.new("")
hash["песенка про зайцев"] += "В тёмно-синем лесу, "
hash["песенка про зайцев"] += "где трепещут осины"
hash #=> {"песенка про зайцев"=>"В темно-синем лесу, где трепещут осины"}
Или ещё пример:
hash = Hash.new(0)
hash["зарплата"] += 60
hash["зарплата"] *= 21
hash #=> {"зарплата"=>1260}
Но, как известно, из любого правила есть исключение: использовать нулевой элемент, когда значение будет записываться умножением, нежелательно. Потому как, даже не будучи пророком, можно предсказать результат. Он будет равен нулю.
Всё известно и дано
правитьЕсли вам изначально известны все ключи и значения, то и записывайте их сразу в виде хеша, одним из способов:
{"март"=>400, "январь"=>350, "февраль"=>200} #=> на выходе такой же текст
{fox: 1, wolf: 2, dragon: 3} #=> {:fox=>1, :wolf=>2, :dragon=>3} обратите внимание на знак ':', он говорит что fox - это не строка,
# а чтото вроде перечисления (Enum), как в языке Си.
Не изобретайте велосипед и поступайте как можно проще.
Методы работы с ассоциативными массивами
правитьКогда речь пойдёт о методах, которые присутствуют в ассоциативных массивах, то частенько будет возникать чувство дежавю. Во всяком случае, учить заново итераторы вам не придётся. Вполне естественно, что появятся новички, но их будет немного. Тем не менее, прилежный преподаватель первым делом представляет новичков группе. Поэтому и мы начнем с тех методов, которые будут необходимы нам при работе с ассоциативными массивами, но отсутствуют у индексных.
Получение массива значений и массива ключей
правитьДля получения отдельно массива ключей или значений существуют методы .keys
и .values
.
{1=>4, 5=>3, 2=>2}.keys #=> [1, 2, 5]
{1=>4, 5=>3, 2=>2}.values #=> [4, 3, 2]
Ассоциативные массивы в Ruby неупорядоченны: массивы могут иметь любой порядок элементов.
Замена ключей на значения
правитьЧтобы поменять местами ключи и значения ассоциативного массива, следует применять метод .invert
. Этот метод возвращает ассоциативный массив с ключами, заменёнными значениями, и значениями, заменёнными ключами.
hash = {"первый ключ"=>4, "второй ключ"=>5}
hash.invert #=> {4=>"первый ключ", 5=>"второй ключ"}
Поскольку ключи в ассоциативных массивах уникальны, то ключи с одинаковыми значениями будут отброшены:
hash = {"первый ключ"=>10, "второй ключ"=>10}
hash.invert #=> {10=>"второй ключ"}
Небольшая хитрость: hash.invert.invert
возвратит нам хеш с уникальными значениями.
Обновление пары
правитьЧто вы делаете, если хотите обновить какую-то программу или игру? Правильно, устанавливаете апдейт. Вы не поверите, но для обновления значения в ассоциативном массиве используется метод .update
. Странно, да? Пример использования этого метода в «боевых» условиях мы уже приводили в начале раздела. Если вы помните, то мы считали, сколько раз повторяется каждое число. Наверняка, вы немного подзабыли его решение (у программистов есть привычка не помнить константы). Позволю себе его вам напомнить:
array = [1, 2, 1, 2, 3, 2, 1, 2, 4, 5]
array.inject({}){ |result, i| result.update({i=>1}){ |key, old, new| old + new } }
#=> {5=>1, 1=>3, 2=>4, 3=>1, 4=>1}
Страшноватая запись. Поэтому будем разбирать её по частям.
result.update({i=>1}){ |key, old, new| old + new }
Сразу после названия метода (в нашем случае .update
) идёт передача параметра. Страшная запись {i=>1}
— это не что иное, как ещё один хеш. Ключ его хранится в переменной i
(счётчик итератора .inject
), а в качестве значения выбрана единица. Зачем? Расскажу чуть позже.
Не обязательно писать именно {i=>1}
. Можно «сократить» фигурные скобки и записать i=>1
.
Счётчик итератора — это переменная в которую итератор записывает текущий элемент последовательности.
Здесь вроде бы все понятно. Запись стала менее страшной, но всё равно вызывает дрожь. Будем это исправлять!
… { |key, old, new| … } …
Раньше мы не встречались с такой записью. Но ничего страшного в ней нет. Это что-то вроде по́ля боя. Нам выдали вооружение и необходимо провести некий манёвр. В нашем случае, арсенал у нас внушительный: key
, old
и new
. Бой начинается при некоторых условиях. Наш бой начнется, когда при добавлении очередной пары (переданной в предыдущей части страшной записи) обнаружится, что такой ключ уже есть в хеше. Нам предлагается описать наши действия именно в таком случае. Что же это за действия?
… { … old + new } …
Всего лишь сложение old
и new
. Ничего не говорит? Тогда расскажу, что значат переменные key
, old
и new
. В переменную key
передаётся текущий ключ, в old
— старое значение по ключу (англ. old — старый), а в переменную new
— добавляемое значение по ключу (англ. new — новый).
Теперь переведём запись old + new
на русский: в случае обнаружения ключа в хеше, нам необходимо сложить старое значение с новым. Если помните, то новое значение равняется единице, то есть в случае когда ключ, хранимый в i
уже есть в хеше result
, то к старому значению просто добавляется единица. Вот и всё… а вы боялись.
Рекомендуется перечитать данную главу ещё раз, так как вы её немного не поняли. |
Интересно, сколько читателей сможет прочитать эту строку и не зациклиться на предыдущей?
Слияние двух массивов
правитьДля слияния двух массивов можно использовать тот же метод .update
или его алиас .merge
или .merge!
:
hash1 = {3 => "a", 4 => "c"}
hash2 = {5 => "r", 7 => "t"}
hash1.merge!(hash2) #=> {5=>"r", 7=>"t", 3=>"a", 4=>"c"}
Если во втором массиве ключ будет совпадать с каким-либо ключем из первого массива, значение будет заменено на значение из второго массива.
Размер ассоциативного массива
правитьНу вот, с новичками мы познакомились, теперь можно переходить к старым знакомым. Помните, как мы находили размер массива? Вот и с хешами точно также:
hash = {5=>1, 1=>3, 2=>4, 3=>1, 4=>1}
hash.size #=> 5
Стоит уточнить, что если в индексных массивах под размером понимается количество элементов, то в ассоциативном массиве это количество пар вида ключ=>значение
. В остальном же это наш старый добрый .size
.
Удаление пары по ключу
правитьО том, как добавлять элементы в массив мы знаем, а вот про удаление — нет. Необходимо это исправить. Чем мы сейчас и займёмся.
hash = {5=>1, 1=>3, 2=>4, 3=>1, 4=>1}
hash.delete(5) #=> 1
hash #=> {1=>3, 2=>4, 3=>1, 4=>1}
hash.delete(5) #=> nil
Как вы, наверно, уже догадались, удалением пары по ключу занимается метод .delete
. Ему передаётся ключ от пары, которую следует удалить.
Метод .delete
возвращает значение, которое соответствовало ключу в удаляемой паре. Если в хеше отсутствует пара с передаваемым ключом, то метод .delete
возвращает nil
. Напоминаем, что nil
— это символ пустоты.
Удаление произвольной пары
правитьМногие программисты удивляются, когда узнаю́т, что ассоциативные массивы имеют метод .shift
. Связано это удивление с тем, что у индексных массивов он удаляет первый элемент, возвращая его во время удаления. А вот как понять, какая пара является первой? И что такое первый в неупорядоченной последовательности пар?
Ответ кроется в отсутствии метода-напарника .pop
, так как если нельзя удалить последний элемент, то под .shift
понимается удаление произвольной пары. Вот такое вот нехитрое доказательство.
Давайте посмотрим его в действии:
hash = {5=>3, 1=>6, 3=>2}
hash.shift #=> [5, 3]
hash #=> {1=>6, 3=>2}
Обратите внимание, что метод .shift
возвращает удаляемую пару в виде индексного массива [ключ, значение]
.
Не стоит обольщаться по поводу того, что метод .shift
возвращает первую пару. Помните, что ассоциативные массивы неупорядоченны.
Преобразовать в индексный массив
правитьЧуть ранее уже говорилось, что в большинстве случаев индексные массивы удобней ассоциативных.
Чтобы преобразовать ассоциативный массив в индексный, надо использовать метод to_a
. Его используют все, кто не может запомнить методов работы с хешами.
hash = {"гаечный ключ"=>10, "разводной ключ"=>22}
hash.to_a #=> [["гаечный ключ", 10], ["разводной ключ", 22]]
Способ преобразования таков. Сперва пары (ключ=>значение) преобразуются в массив:
{["гаечный ключ"=>10], ["разводной ключ"=>22]}
Затем «стрелку» заменяем на запятую:
{["гаечный ключ", 10], ["разводной ключ", 22]}
и фигурные скобки выпрямляем, так что теперь их можно заправить в стэплер.
[["гаечный ключ", 10], ["разводной ключ", 22]]
Упорядочение хеша
правитьДа, множество пар в хеше неупорядоченно. Но это можно исправить, разве что результат потом будет не хешем, а двумерным массивом.
hash = {"гаечный ключ"=>4, "разводной ключ"=>10}
hash.sort #=> [["гаечный ключ", 4], ["разводной ключ", 10]]
В методе .sort_by
передаются два значения:
hash = {"гаечный ключ"=>4, "разводной ключ"=>10}
hash.sort_by{ |key, value| value } #=> [["гаечный ключ", 4], ["разводной ключ", 10]]
Здесь мы упорядочили хеш по значению.
Сначала хеш упорядочивается по ключам, а потом, в случаях равнозначных ключей при использовании sort_by
, — по значениям.
Поиск максимальной/минимальной пары
правитьМаксимальная пара в хеше ищется точно также, как и максимальный элемент в массиве
hash = {"гаечный ключ"=>10, "разводной ключ"=>22}
hash.max #=> ["разводной ключ", 22]
hash.min #=> ["гаечный ключ" , 10]
но с небольшими особенностями:
- результат поиска — массив из двух элементов вида
[ключ, значение]
, - сначала поиск происходит по ключу, а в случае равноправных ключей при использовании
max_by
иmin_by
— по значению.
Несколько больше возможностей приобрели методы max_by
и min_by
:
hash = {"гаечный ключ"=>10, "разводной ключ"=>22}
hash.max_by{ |key, value| value } #=> ["разводной ключ", 22]
hash.min_by{ |array| array[0] } #=> ["гаечный ключ" , 10]
Также, как и в методе sort_by
есть возможность по разному получать текущую пару: в виде массива или двух переменных.
Логические методы
правитьРабота логических методов похожа на допрос с пристрастием. Помните, как в детективах во время теста на детекторе лжи, главный герой восклицал: «Отвечать только „да“ или „нет“!» Если перевести это на язык Ruby, то это будет звучать примерно так: «Отвечать только true
или false
!»
В детективах набор вопросов стандартен:
- Знали ли вы мистера X?
- Вы были на месте преступления?
- Убивали ли мистера X?
- …
На Ruby примерно тоже самое:
- Ты пустой?
- Есть ли такой элемент?
- Ты массив?
- Уверен, что не строка?
Но давайте рассмотрим их подробней.
Хеш пустой?
правитьЗададим вопрос «Хеш пустой?», но используя известный нам лексикон. Для начала спросим «Пустой хеш тебе не брат-близнец?»
empty_hash = {}
filled_hash = {"гаечный"=>20, "замочный"=>"английский", "разводной"=>34}
empty_hash == {} #=> true
filled_hash == {} #=> false
Можно спросить по другому: «Размер у тебя не нулевой?»
empty_hash = {}
filled_hash = {"гаечный"=>20, "замочный"=>"английский", "разводной"=>34}
empty_hash .size.zero? #=> true
filled_hash.size.zero? #=> false
Но давайте будем задавать правильные вопросы
empty_hash = {}
filled_hash = {"гаечный"=>20, "замочный"=>"английский", "разводной"=>34}
empty_hash .empty? #=> true
filled_hash.empty? #=> false
а то ещё примут нас за приезжих…
Обратите внимание, что метод .empty?
полностью повторяет такой же метод у индексных массивов.
Есть такой ключ?
правитьЕсли вам нужно узнать у хеша ответ на вопрос «есть у тебя такой ключ?», но вы не знаете как это правильно спросить, то скорее всего вы зададите вопрос в два этапа: «какие ключи у тебя есть?» и «есть среди них такой ключ?»
pocket = {"гаечный"=>20, "замочный"=>"английский", "разводной"=>34}
pocket.keys.include?("гаечный") #=> true
В данном примере у нас в pocket
нашёлся "гаечный"
ключ.
Но лучше задавать вопрос напрямую, это покажет ваше прекрасное знание языка.
pocket = {"гаечный"=>20, "замочный"=>"английский", "разводной"=>34}
pocket.key?("гаечный") #=> true
или в стиле индексных массивов
pocket = {"гаечный"=>20, "замочный"=>"английский", "разводной"=>34}
pocket.include?("гаечный") #=> true
Это несколько сократит первоначальное предложение, но тогда можно перепутать хеш с массивом.
Этот же вопрос можно задать методами: .member?
и .has_key?
.
Есть такое значение?
правитьДавайте подумаем, как задать вопрос «есть такое значение?» хешу. Скорее всего, мы опять зададим вопрос в два этапа: «какие значения есть?» и «есть ли среди них нужное нам?»
pocket = {"гаечный"=>20, "замочный"=>"английский", "разводной"=>34}
pocket.values.include?("гаечный") #=> false — ой, забыл сменить
pocket.values.include?("английский") #=> true
Но аборигены говорят иначе и задают вопрос напрямую:
pocket = {"гаечный"=>20, "замочный"=>"английский", "разводной"=>34}
pocket.value?("английский") #=> true
Задать вопрос «Есть такое значение?» можно не только при помощи метода .value?
, но и при помощи более длинного .has_value?
.
Итераторы
правитьУ ассоциативных массивов есть следующие итераторы:
.find_all
— поиск всех элементов, которые удовлетворяют логическому условию;.map
— изменение всех элементов по некоторому алгоритму;.inject
— сложение, перемножение и агрегация элементов массива.
Набор итераторов точно такой же, как и у индексных массивов — сказывается их родство. Вот только ведут себя они несколько иначе:
- Результатом является двумерный массив (как после метода
.to_a
). - В качестве счётчика (переменной в фотографии) передаётся массив вида
[ключ, значение]
. - Можно развернуть массив вида
[ключ, значение]
в две переменные. - В итераторе
.inject
развернуть массив можно используя запись.inject{|result, (key, value)| }
.
Рассматривать заново работу каждого итератора в отдельности скучно. Поэтому мы будем рассматривать работу всех итераторов сразу.
hash = {"гаечный ключ"=>4, "разводной ключ"=>10}
hash.find_all{ |array| array[1] < 5 }
#=> [["гаечный ключ", 4]]
hash.map { |array| "#{array[0]} на #{array[1]}" }
#=> ["гаечный ключ на 4", "разводной ключ на 10"]
hash.inject(0){ |result, array| result + array[1] }
#=> 14
Обратите внимание на то, что в качестве счётчика передаётся массив из двух элементов. В наших примерах счётчик итератора мы назвали array
. В своих программах вы вольны называть его как угодно.
Есть подозрение, что перед работой любого из итераторов вызывается метод .to_a
. Уж больно работа итераторов в хешах напоминает работу с двумерным массивом.
Теперь посмотрим, как можно развернуть array
в две переменные. Делается это простой заменой array
на key, value
:
hash = {"гаечный ключ"=>4, "разводной ключ"=>10}
hash.find_all{ |key, value| value < 5 }
#=> [["гаечный ключ", 4]]
hash.map{ |key, value| "#{key} на #{value}" }
#=> ["гаечный ключ на 4", "разводной ключ на 10"]
hash.inject(0){ |result, key, value| result + value }
#=> Ошибка в методе "+": невозможно сложить nil и число типа Fixnum
Обратите внимание, что развёртка массива прошла успешно только в первых двух итераторах. В третьем возникла ошибка. Давайте выясним, откуда там взялся nil
. Дело в том, что развернуть массив не удалось, и теперь он стал называться не array
, а key
. Переменная value
осталась «не у дел», и ей присвоилось значение nil
. Чтобы это исправить, достаточно поставить круглые скобки:
hash.inject(0){ |result, (key, value)| result + value }
#=> 14
Ассоциативный массив, как и индексный массив, имеет метод .map
, который передаёт замыканию ключ и соответствующее ему значение. При этом в замыкание на самом деле передаётся массив с ключом и значением, но Ruby «разворачивает» их в две переменные при передаче замыканию.
Итератор .map
, в свою очередь, возвращает индексный массив с результатами замыкания — по элементу массива на каждый ключ:
hash = {"гаечный ключ"=>4, "разводной ключ"=>10}
hash.map { | key, value | "#{key} на #{value}" } #=> ["гаечный ключ на 4", "разводной ключ на 10"]
hash.map #=> [["гаечный ключ", 4], ["разводной ключ", 10]]
Итератор .map
, вызванный без аргументов, аналогичен методу .to_a
: просто раскладывает хеш в двумерный массив.
Хитрости
правитьОдному программисту надоело писать hash["key"]
и он захотел сделать так, чтобы можно было написать hash.key
.
class Hash
def method_missing(id)
self[id.id2name]
end
end
hash = {"hello"=>"привет", "bye"=>"пока"}
hash.hello #=> "привет"
hash.bye #=> "пока"
Естественно, что ключи в таком хеше могут содержать только латиницу, символ подчёркивания и цифры (везде, кроме первого символа). Иначе говоря, удовлетворять всем требованиям, которые мы предъявляем к именам методов и именам переменных.
Задачи
править- Дан массив слов. Необходимо подсчитать, сколько раз встречается каждое слово в массиве.
Подробнее о строках
правитьСтрока — это упорядоченная последовательность символов, которая располагается между ограничительными символами.
Строковый тип является самым популярным в любом языке программирования. Ведь без него невозможно написать любую программу (особенно учитывая, что любая программа — это строка). При выводе на экран или записи в файл, любой тип данных преобразуется к строке (явно или неявно). Это значит, что в конечном итоге всё сводится к строковому типу. Кстати, и ввод данных тоже осуществляется в виде строки (и только потом преобразуется в другие типы).
Студенты четвёртого курса МЭТТ ГАИ поступили на подготовительные курсы в МГИУ. Там им начали преподавать основы программирования на Ruby. И одна из заданных им задач была: «Дано число, необходимо поменять порядок цифр на обратный». Задача сложная, но наши студенты об этом не знали и решили её преобразованием к строке: |
Язык Ruby унаследовал работу со строками из языка Perl (признанного лидера по работе со строками). В частности такой мощный инструмент как правила (rules).
Но наследование не подразумевает бездумного копирования. В частности, правила, в рамках Ruby, получили объектно-ориентированную реализацию, что позволяет применять к ним различные методы. Помимо правил, присутствует великое множество методов работы со строками. Причём некоторые из них являются нашими старыми знакомыми (+
, *
, []
и так далее). Работают они несколько иначе, но некоторая параллель с массивами все же присутствует.
Следует упомянуть два очень интересных момента:
- Cтроки — это универсальный тип данных, так как в строку можно преобразовать любой другой тип данных. Также строку можно преобразовать в любой другой тип данных (ведь изначально любой код программы — это строка).
- Cтроки очень удобно преобразовывать в массив и обратно (методы
.join
и.split
). Поэтому работа со строками практически столь же удобная, как и с массивами.
Если работа со строками обходится без преобразования в массив, то программа либо очень простая, либо бесполезная.
Способы создания строки
правитьСтрока создаётся при помощи ограничительных символов. Для этих целей чаще всего используются "
(программистская кавычка) и '
(машинописный апостроф). Их смысл различен. Строка в апострофах гарантирует, что в ней будет содержаться тот же текст, что и в коде программы, без изменений. Строка в кавычках будет проходить предварительное преобразование. Будут раскрыты конструкции «вставка» и «специальный символ».
Давайте будем называть строки в апострофах «ленивыми», а строки в кавычках — «работящими».
Вставка — это хитрая конструкция, которая вставляется между ограничительными символами внутри строки. Она состоит из комбинации октоторпа и двух фигурных скобок (#{ 'здесь был Вася' }
). Внутри неё можно писать не только 'Здесь был Вася'
, но и любой программный код. Результат программного кода будет преобразован к строке и вставлен на место вставки.
Вставка жизнью заправляет:
Код программный выполняет,
Тихо результат считает,
Вместо вставки подставляет.
«Вставка» работает только в момент создания строки́. После создания придётся придумывать другие способы подстановки данных в стро́ки.
Специальный символ начинается со знака \
(обратная косая черта). Самые популярные из них: \n
(переход на новую строку), \t
(табуляция), \\
(обратная косая черта) и \"
(двойная кавычка).
Хотя специальный символ и пишется, как два знака, но на деле это всего один символ. Доказать это можно выполнением простенького кода: "\n".size #=> 1
.
Для чего нужны работящие и ленивые строки?
правитьСкорее всего вы будете редко вспоминать про то, что существуют работящие и ленивые строки. Тем более, что это различие действительно только на момент создания строки. Рядовой программист пользуется либо работящими, либо ленивыми строками. Давайте посмотрим, как выглядит код программиста, который использует только ленивые строки:
my_number = 18
my_array = [1, 2, 3, 4]
puts 'Моё число = ' + my_number.to_s + ', а мой массив длины ' + my_array.size.to_s
Обратите внимание, что перед сцеплением (умные дяди называют это конкатенацией) необходимо все данные преобразовывать к строке методом .to_s
. Вставка позволяет этого избежать. Вот, как будет выглядеть та же самая программа с использованием вставки:
my_number = 18
my_array = [1, 2, 3, 4]
puts "Моё число = #{my_number}, а мой массив длины #{my_array.size}"
Программа стала не только меньше, но и лучше читаться. Исчезли многочисленные сцепления.
Если внутри вставки надо создать строку, то экранировать кавычки не сто́ит. Внутренности вставки не являются частью строки́, а значит живут по своим законам.
my_array = [1, 2, 3, 4]
puts "Повторенье — мать ученья. Мой массив = #{my_array.join(\", \")}"
Программа вызовет ошибку, так как внутри вставки было использовано экранирование кавычек. Правильный пример будет выглядеть так:
my_array = [1, 2, 3, 4]
puts "Повторенье — мать ученья. Мой массив = #{my_array.join(", ")}"
Нет необходимости в экранировании символов внутри вставки. |
Методы работы со строками
правитьМетоды строк умеют:
- преобразовывать входные данные в красивый вид,
- красиво оформить выходные данные,
- дезертировать в массивы.
Допустим, вы нашли максимальный элемент массива. И вам надо вывести результат на экран. Вы можете поступить вот так:
array = [4, 4, 2, 5, 2, 7]
puts array.max #=> 7
Несмотря на правильность решения, вывод результата выглядит некрасиво. Гораздо профессиональней будет написать вот такую программу:
array = [4, 4, 2, 5, 2, 7]
puts "Максимальный элемент array = #{array.max}" #=> Максимальный элемент array = 7
Заметили, насколько привлекательней стал вывод результата, когда мы задействовали стро́ки?
Заниматься оформлением выходных данных сто́ит только непосредственно перед выводом результата. В остальное время использование оформительских элементов будет только мешать.
Всем известно, что чи́сла внутри компьютера хранятся в двоичной системе. Но вот парадокс: получить двоичное представление числа иногда очень сложно. В частности, для того, чтобы получить двоичную запись числа, необходимо создать строку и записывать результат в неё. Это значит, что результатом преобразования в другую систему счисления (из десятичной) будет строка. Давайте посмотрим, как эта задача решается:
my_number = 123
puts "В двоичном виде: %b" % my_number #=> В двоичном виде: 1111011
Мы задействовали метод %
(аналог sprintf
в Си), который осуществляет форматирование строки́. Эту же задачу можно решить несколько иначе.
my_number = 123
puts "В двоичном виде: #{my_number.to_s(2)}" #=> В двоичном виде: 1111011
Со вторым вариантом вы могли уже́ встречаться в разделе, посвящённом числам (подраздел Хитрости).
Методы sprintf
и printf
в Ruby также присутствуют, но используются крайне редко. Чаще всего они заменяются на методы %
и puts
.
Арифметика строк
правитьКто бы мог подумать, но строки можно складывать, умножать и, если постараться, то и делить. Естественно, что это не арифметические операции чисел, а методы со своим особенным поведением. Рассмотрим сложение, которое в строках работает как сцепление (конкатенация):
boy = "Саша"
girl = "Маша"
puts boy + " + " + girl + " = любовь!" #=> "Саша + Маша = любовь!"
# или так:
puts "#{boy} + #{girl} = любовь!" #=> "Саша + Маша = любовь!"
Вот такое вот романтическое сцепление сердец!
Переходим к умножению. Умножают стро́ки только на целое число. Строка, которую умножают просто копируется указанное число раз. Давайте напишем речь для кукушки, чтобы она не сбилась и не накукукала нам слишком мало.
years_left = 100
"Ку-ку! " * years_left
#=> "Ку-ку! Ку-ку! Ку-ку! Ку-ку! Ку-ку! Ку-ку! Ку-ку! … Ку-ку! Ку-ку! "
Теперь за своё будущее можно не беспокоиться! Хотя нам может попасться неграмотная кукушка…
С делением совсем другая история. Сейчас его в языке просто нет, но этот недостаток уже ощущают многие. Смысл деления состоит в том, чтобы преобразовать строку в массив, разбив её по разделителю. Давайте преобразуем речь кукушки в массив и посмотрим, сколько же лет она нам насчитала:
cuckoo_talk = "Ку-ку! Ку-ку! Ку-ку! Ку-ку! Ку-ку! Ку-ку! Ку-ку! … Ку-ку! Ку-ку! "
(cuckoo_talk / " ").size #=> 10
В нашем примере метод .size
считает число элементов массива, который получился в результате деления.
Скобки нужны для того, чтобы вызвать .size
от результата деления, а не от " "
.
Деление в примере работает также как и метод .split
, о котором речь пойдёт чуть позже. А чтобы оно заработало у вас, необходимо добавить небольшой код в начало программы:
class String
alias / :split
end
Этот код как раз и говорит о том, что деление и .split
— одно и тоже.
Деление стало нужно, когда метод *
для массивов получил возможность работать, как .join
(преобразовать массив в строку, расположив элементы через разделитель). В виду того, что .join
и .split
работают вместе точно также, как умножение и деление, то появилась идея заставить работать деление как .split
.
Преобразование в массив или путешествие туда и обратно
правитьСлучилось так, что итераторы в строках работают настолько неуклюже, что рассмотрение их в рамках учебника будет пропущено. Следовательно, для того, чтобы задействовать механизм итераторов, мы будем преобразовывать строки в массивы. После того, как итераторы нам станут не нужны, то мы вернёмся к строкам.
"Ку-ку".split('-') #=> ["Ку", "ку"]
"Ку-ку".split('y') #=> ["К", "-к"]
Обратите внимание, что разделитель из результата исчезает.
За преобразование строки́ в массив отвечает метод .split
. Ему передаётся разделитель, по которому будет происходить деление строки на элементы. В нашем случае это '-'
. Теперь вернём всё на место. Для этого мы будем использовать метод .join
из арсенала массивов:
["Ку", "ку"].join('-') #=> "Ку-ку"
["Ку", "ку"].join(', ') #=> "Ку, ку"
["Ку", "ку"].join #=> "Куку"
["Ку", "ку"].to_s #=> "Куку"
Кукушка кукует,
строку генерит.
Из строчки массив
Получу через .split
.
- Разделитель будет вставлен между элементами исходного массива.
- Метод
.join
, вызванный без разделителя, даёт такой же результат, как и метод.to_s
.
Чуть ранее мы упоминали, что можно использовать умножение вместо .join
. Давайте посмотрим, как это выглядит:
["Ку", "ку"] * '-' #=> "Ку-ку"
["Ку", "ку"] * 3 #=> ["Ку", "ку", "Ку", "ку", "Ку", "ку"]
Если умножать массив на целое число, то будет размножение массива (прямо как умножение в строках), а не преобразование в строку.
Длина строки
правитьДлина строки определяется точно также, как и длина массива или хеша, то есть методом .size
:
"Во дворе дрова, а в дровах трава!".size #=> 33
"Три".size #=> 3
Следует помнить, что пробел является символом, хотя и не отображается на экране. |
Ограничительные кавычки или апострофы в количество символов не входят! Они предназначены лишь для того, чтобы Ruby понял, где начинается и заканчивается строка. |
Получение подстрок
правитьПолучение подстрок работает точно также, как и получение подмассива. С тем лишь отличием, что нумерация идёт не по элементам, а по символам. Это логично, особенно если учесть, что для строки элементом является символ.
string = "Во дворе дрова, а в дровах трава!"
string[27..-1] #=> "трава!"
string[27...-1] #=> "трава"
string[9...14] #=> "дрова"
string[9..13] #=> "дрова"
Отрицательная нумерация тоже работает, то есть последний элемент имеет индекс -1
, предпоследний -2
и так далее.
Помните, что три точки в диапазоне дают подстроку без крайнего правого элемента. Кто-то шлагбаумом балуется!
Чтобы получить один единственный символ, необходимо получить подстроку длины 1.
string = "Во дворе дрова, а в дровах трава!"
string[3..3] #=> "д"
string[3] #=> 190
Для получения подстроки или символа из строки необходимо всегда указывать диапазон. Даже если в диапазоне всего один элемент.
Если мы указываем не диапазон, а число, то метод []
выдаёт нам не символ, а его целочисленный код.
string = "Во дворе дрова, а в дровах трава!"
string[3] #=> 190
string[3].chr #=> "д"
Для преобразования целочисленного кода обратно в символ, используется метод .chr
.
В последних версиях Ruby код string[3]
уже выдает искомую "д" и без использования метода .chr
. Но - зачем рисковать, если можно сразу указать точный диапазон?
Строка-перевёртыш
правитьИногда хочется перевернуть строку задом наперед. Причины могут быть разные. Например, вы ищете палиндром (число, которое можно перевернуть без ущерба для его значения). Занимается этим благородным делом метод .reverse
.
Узнаем настоящее имя Алукарда из аниме Hellsing:
"Алукард.".reverse
#=> ".дракулА"
Не стоит путать метод .reverse
для массивов с методом .reverse
для строк. В массивах меняется порядок элементов, а в строках — символов.
Меняю шило на мыло!
правитьДля того, чтобы заменить "шило"
на "мыло"
используется не газета «Из рук в руки», а методы .sub
и .gsub
:
"шило в мешке не утаишь".sub("шило", "мыло")
#=> "мыло в мешке не утаишь"
Естественно, что менять можно не только шило и мыло, но и другие данные. Например, возраст. Девушка Ирина утверждает, что ей 18, но мы-то знаем, что ей 26. Давайте восстановим истину, возможно — в ущерб своему здоровью:
"Ирине 18 лет.".sub("18", "26") #=> "Ирине 26 лет."
Заметили, что мы используем только метод .sub
? Давайте теперь рассмотрим работу метода .gsub
и его отличие от .sub
. На этот раз мы будем исправлять текст, авторы которого — недоучки ( или белорусы ), забывшие про правило «Жи, Ши — пиши через 'и'»
string = "жыло-было шыбко шыпящее жывотное"
string.sub("жы", "жи") #=> "жило-было шыбко шыпящее жывотное"
string.gsub("жы", "жи") #=> "жило-было шыбко шыпящее животное"
string.gsub("жы", "жи").gsub("шы", "ши") #=> "жило-было шибко шипящее животное"
Метод .sub
производит только одну замену, а .gsub
— все возможные.
Название метода .sub
произошло от английского «substitute» — «замена», созвучное же название метода .gsub
отличается от него только словом «global».
Сканируем текст на ошибки
правитьДавайте найдем и посчитаем ошибки. Искать мы будем методом .scan
.
string = "жыло-было шыбко шыпящее жывотное"
string.scan("шы") #=> ["шы", "шы"]
string.scan("шы").size #=> 2
string.scan("жы").size #=> 2
string.scan("жы").size + string.scan("шы").size #=> 4
Метод .scan
находит все указанные подстроки и возвращает их в виде массива строк. В данном примере, метод .size
считает количество элементов массива, возвращаемого .scan
.
Ужас, в одном предложении целых четыре ошибки. Будем отчислять!
Нашел ошибку метод
.scan
,
В массив её запомнил.
Учителям он свыше дан!
Зачем его я вспомнил?!
Правила (они же регулярные выражения) работы со строками.
правитьПравила — это образцы к которым можно примерять строки. Правила обладают своим собственным языком, который позволяет описывать одну, две, сотню и вообще любое количество строк. Это своеобразная упаковка для множества строк в одну компактную запись.
Правила в Ruby ограничиваются символами /
(косая черта). Примеры правил:
/(жы|шы)/
/\w+@[\w\.]+\w+/i
Страшно? А зря. На самом деле работа с правилами очень проста. Главное привыкнуть и попрактиковаться.
Составные части правил:
- Символьные классы
- Перечисление символов, которые может содержать строка.
- Квантификаторы
- Количество символов.
- Альтернативы
- Перечисление всевозможных вариантов.
- Группировки
- Возможность выделить несколько групп, которые могут обрабатываться отдельно.
- Модификаторы
- Изменение поведения правила. Например, игнорирование регистра символов.
Правила в рамках учебника будут описаны очень сжато. Многие тонкости освещены не будут, поэтому для освоения «фигур высшего пилотажа» необходимо прочитать специализированную литературу. Например, книгу «Регулярные выражения»
Символьный класс.
правитьСимвольный класс — просто конечный набор символов. Он ограничивается квадратными скобками и содержит перечисление символов, которые можно вместо него подставить. Заменяется он всего на один символ, входящий в этот класс. Примеры символьных классов:
/[абвгде]/ #=> простое перечисление символов
/[а-яА-ЯЁё]/ #=> все русские буквы
/[0-9a-z]/ #=> цифры и строчная латиница
/[^0-9]/ #=> все символы, кроме цифр
Замечание: В таблице Unicode символы 'Ё' и 'ё' стоят немного отдельно от остальных символов русского алфавита.
- Можно использовать
-
(дефис) для указания диапазонов символов. - Если первый символ класса (идущий сразу после открывающейся квардратной скобки) —
^
(циркумфлекс), то это означает символ, который отсутствует в данном классе. - Некоторые популярные классы имеют короткую запись.
Короткая запись |
Полная запись |
Описание |
---|---|---|
\s |
[\f\t\n\r] |
Пробельный символ |
\S |
[^\f\t\n\r] |
Любой символ, кроме пробельного |
\d |
[0-9] |
Цифра |
\D |
[^0-9] |
Любой символ, кроме цифры |
\w |
[a-zA-Z0-9] |
Латиница или цифра |
\W |
[^a-zA-Z0-9] |
Любой символ, кроме латиницы или цифры |
. |
[^\n\r] |
Любой символ, кроме перевода строки |
\b |
|
Граница слова |
\B |
|
Не граница слова |
\A |
|
Начало строки |
\Z |
|
Конец строки |
Взгляните на примеры правил! Правда, они стали понятней? По крайней мере второе…
Квантификатор
правитьПоказывает, сколько раз может повторяться предыдущий символ, группа, альтернатива, etc. Квантификатор ограничивается фигурными скобками.
Примеры квантификаторов:
/\w{3}/ #=> три латинских буквы или цифры
/\d{1, 3}/ #=> одна, две или три цифры
/[а-яА-ЯЁё]{3,}/ #=> русское слово длиной три символа и больше
- Квантификатор с одним параметром называется точным и указывает точное количество повторений.
- Квантификатор с двумя агрументами называется конечным и указывает конечный диапазон, в котором варьируется количество повторений.
- Квантификатор без второго параметра (но с запятой) называется бесконечным и ограничивает количество повторений лишь снизу.
- Некоторые популярные квантификаторы имеют короткую запись.
Короткая запись |
Полная запись |
Описание |
---|---|---|
* |
{0, } |
Любое количество |
+ |
{1, } |
Один и более |
? |
{0, 1} |
Есть или нет |
Снова посмотрите на примеры правил. Теперь вам они понятны? Если нет, то перечитайте две предыдущие главы — в них основа правил.
Альтернатива
правитьАльтернатива нужна, когда необходимо объединить несколько правил в одно. При этом совпадение засчитывается, когда есть совпадение хотя бы с одним правилом. Желательно альтернативу заключать внутрь группировки (круглые скобки). Правила, входящие в альтернативу, разделяются |
(вертикальной чертой, которая и является альтернативой). Примеры альтернатив:
/(жы|шы)/ #=> или "жы", или "шы"
/(\w+|[а-яА-Я]+)/ #=> или слово на латинице, или русское
Вместо альтернативы можно задействовать логические итераторы .any?
и .all?
внутри .inject
. Получается более гибкая конструкция.
В данном примере продемонстрирована альтернатива с группировкой. В принципе альтернатива может существовать и без неё, но так возникает меньше ошибок у начинающих.
Группировка
правитьГруппировка используется, когда необходимо обрабатывать результат частями. Например, при обработке ссылок в HTML-документе удобно отдельно обрабатывать текст ссылки и URL. Группировка также как и альтернатива, заключается в круглые скобки. Более того, альтернатива обрабатывается как группировка. Доступ к результату совпадения каждой группировки осуществляется посредством специальных переменных $1
, $2
, …, $9
. Подробнее группировки будут рассмотрены в подразделе «Правильная замена». Пример использования группировки:
"2+7*3".gsub(/(\d+)\*(\d+)/){ $1.to_i * $2.to_i } #=> "2+21"
Почти калькулятор!
Существует много видов группировок. Например, (?:…)
— группировка без сохранения результата в «долларовую переменную» или (?!…)
— негативная группировка. В любом случае они ограничиваются парой круглых скобок.
Фиксирующая директива
правитьФиксирующие директивы — это символы, которые привязывают правило к некоторому признаку. Например, к концу или началу строки.
/^\d+/ #=> строка начинается с числа
/\w+$/ #=> последнее слово на латинице или число
/^$/ #=> пустая строка
Насколько видно из примеров,
^
— привязка к началу строки,$
— привязка к концу строки.
Фиксирующих директив гораздо больше двух. Об остальных читайте в специализированной литературе.
Модификатор
правитьМодификатор предназначен для изменения поведения правила. Он размещается сразу же после правила (после последней наклонной черты). Пример использования модификатора:
/(hello|world)/i #=> или "hello", или "world". Причём независимо от регистра
/\s+/mix #=> несколько подряд идущих пробельных символов
Бывают следующие модификаторы:
- multiline — перенос строки считается простым символом,
- ignorcase — поиск без учёта регистра,
- extended — игнорировать пробельные символы.
Игнорирование регистра работает только для латиницы.
- Можно применять любое количество модификаторов и в любом порядке.
- Обратите внимание, что модификаторы образуют слово mix.
Теперь можно не бояться страшных правил.
Правильное разбиение
правитьРазбиение называется «правильным» тогда, когда в качестве аргумента метода .split
используется правило. Например, можно разбить текст по знакам препинания. Для этого необходимо выполнить следующий код.
"Раз, два, три!".split(/[, \.?!]+/) #=> ["Раз", "два", "три"]
Обратите внимание, что в результирующем массиве знаки препинания отсутствуют.
Правильная замена
правитьС правильной заменой не всё так просто. Дело в том, что методы .sub
и .gsub
совместно с правилами становятся итераторами, которые последовательно обрабатывают каждое совпадение с правилом. Чтобы это увидеть в действии, давайте решим задачу исправления ошибок:
"Жыло-было шыбко шыпящее жывотное".gsub(/(ж|ш)ы/){ $1 + "и" }
#=> "Жыло-было шибко шипящее животное"
Опаньки, а первое слово не исправилось! Видимо дело в том, что слово Жыло
начинается с прописной буквы. Сейчас исправим:
"Жыло-было шыбко шыпящее жывотное".gsub(/(Ж|Ш|ж|ш)ы/){ $1 + "и" }
#=> "Жило-было шибко шипящее животное"
Вот, теперь гораздо лучше. Как мы этого добились? Давайте разберёмся. Начнём с регулярного выражения:
/(Ж|Ш|ж|ш)ы/
Оно состоит из двух частей:
- альтернативы с группировкой —
(Ж|Ш|ж|ш)
, - символа —
ы
.
В альтернативе мы указали буквы с которых начинается неправильный слог. Символ просто добавляется к букве из альтернативы.
Зачем была использована группировка? Для пояснения причины, рассмотрим код в фигурных скобках:
{ $1 + "и" }
Вот для того, чтобы можно было использовать переменную $1
(результат первой группировки) мы и задействовали группировку. В данном случае, в $1
сохраняется первая буква слога, которая в результате исправления оШЫбки не меняется.
- Для того, чтобы получить доступ к результату первой группировки, надо обратиться к переменной
$1
(один доллар), ко второй —$2
(два доллара) и так далее до переменной$9
(девять долларов). - Переменные
$1
—$9
заимствованы из языка Perl.
Можно ли было решить эту же задачу иначе? Конечно можно!
"Жыло-было шыбко шыпящее жывотное".gsub(/([ЖШжш])ы/){ $1 + "и" }
#=> "Жило-было шибко шипящее животное"
На этот раз мы просто задействовали символьный класс вместо альтернативы, который описывает первую букву слога с оШЫбкой.
Есть ещё пару интересных моментов, которые вам необходимо знать. Во время предыдущего примера вас могли посетить следующие вопрос: а как получить весь текст, который совпал с правилом? Неужели необходимо делать всеобщую группировку?
Ответ на этот вопрос однозначный — нет! Достаточно придумать название переменной (которая будет содержать совпавший текст) и правильно описать внутри ушек:
"Раз, два, три!".gsub(/[а-я]+/){ |word| word.reverse }
#=> "заР, авд, ирт!"
Правильный поиск
правитьВот здесь метод .scan
может развернуться в полную силу. Хотите получить массив всех русских слов в тексте? Запросто:
"Раз, два, три!".scan(/[А-Яа-я]+/) #=> ["Раз", "два", "три"]
Хотите получить все знаки препинания? Нет ничего проще:
"Раз, два, три!".scan(/[, \.;:!]+/) #=> [", ", ", ", "!"]
Если необходимо в метод .scan
передавать правило с группировкой, то желательно использовать группировку без сохранения результата, то есть (?:…)
. Иначе результатом метода .scan
будет совпадение с группировкой, а не с правилом.
Например, ниже записана программа, которая занимается поиском адресов электронной почты.
string = "495-506-13 56 nata@rambler.ru(34) 1.5.1232 12.14.56 31.декабря.9999"
string.scan(/(?:[-a-z_\d])+@(?:[-a-z])*(?:\.[a-z]{2,4})+/) #=> ["nata@rambler.ru"]
Выполните её, посмотрите результат, а потом замените любую из группировок (?:…)
на (…)
и снова взгляните на результат.
Ну со .scan
должно быть всё понятно. А вот то, что метод []
начинает тоже правильно искать — пока нет.
"Раз, два, три!"[/[А-Яа-я]+/] #=> "Раз"
Если методу []
передать в качестве параметра правило, то он вернёт либо совпадение с правилом, либо nil
.
Очень полезно использовать []
в ситуациях, когда надо узнать ответ на вопрос «есть хотя бы одна подстрока, которая удовлетворяет правилу?» или получить первое (или единственное) совпадение с правилом.
Существует древнее поверье, что если использовать одно и тоже правило для .scan
и .split
, то получаются две части текста, из которых реально получить исходный.
Text.scan(rule) + Text.split(rule) = Text
Это значит, что если метод .split
использует правило, описывающие все знаки припинания, то результатом будет текст без знаков припинания. А вот если это же правило будет использовать метод .scan
, то в результате мы получим все знаки препинания без текста.
Рекомендуется использовать метод []
вместо метода =~
(заимствованного из Perl), так как []
более функционален.
Жадность
правитьРечь пойдёт о жадности среди квантификаторов. Возьмем некоторый квантификатор {n, m}
и посмотрим как он работает.
Нежадные квантификаторы иногда называют щедрыми. |
Сперва он начинает искать последовательность длины m
(вот так жадность), и если правило не срабатывает, он начинает уменьшать длину последовательности вплоть до n
. Так работают обычные жадные кванторы.
Но иногда жадные кванторы не могут справиться с задачей. Например, в файле на языке HTML мы осуществляем поиск ссылок (тег <a>
). Правило с жадным квантором найдёт начало первой ссылки и конец последней. Весь остальной текст оно примет за текст ссылки. Понятно, что работать верно оно будет только в двух случаях: когда в тексте нет ссылок или когда ссылка только одна.
Для решения вышеописанной проблемы и был придуман так называемый щедрый квантификатор. От жадного он отличается обратным ходом обработки, то есть длину последовательности он не уменьшает от m
к n
, а наоборот, увеличивает от n
до m
. Научить щедрости квантификатор можно знаком вопроса ?
после любого жадного квантификатора.
"Раз, два, три!".scan(/[А-Яа-я]+?/)
#=> ["Р", "а", "з", "д", "в", "а", "т", "р", "и"]
"Жуй жвачку, жывотное!".gsub(/([жЖшШ]??)ы/){ $1 + 'и' }
#=> "Жуй жвачку, животное!"
С рождения квантификаторы жадные. Щедрость — обретаемый признак. |
На самом деле, жадный квантификатор называется жадным (или - максимальным) потому, что он пытается забрать все себе, а щедрый (минимальный) - стремится отдать все другим. И потому с рождения квантификаторы жадны, а щедрость — обретаемый признак. Подробнее вы можете узнать об этом в книге Джона Фридла «Регулярные выражения» |
Хитрости
правитьПеренос по словам
правитьНесколько лет назад (ещё при жизни http://ruby-forum.ru) решали мы интересную задачу: как реализовать автоматический перенос на новую строку (wrap). Для тех, кто не застал те времена, уточню задание: дан текст, необходимо, вставить переносы таким образом, чтобы каждая из полученных строк была меньше n
(для определённости n = 80
). Недавно я осознал, что не могу решить эту задачу тем способом, который был нами тогда найден. Я его просто не помнил… Решение нашлось быстро, достаточно было вспомнить, что на английском языке эта задача именуется коротким и ёмким словом wrap.
class String
def wrap(col = 80)
gsub(/(.{1,#{col}})( +|$\n?)|(.{1,#{col}})/, "\\1\\3\n")
end
end
Немного о структуре кода. Метод .wrap
реализован для экземпляров класса String
. Также стоит обратить внимание на то, что внутри правила (регулярного выражения) возможна «вставка» (как в «рабочих строках»). Используется сей метод следующим образом:
p "wrapping text with regular expressions".wrap(10)
#=> "wrapping\ntext with\nregular\nexpression\ns\n"
Теперь давайте разберёмся с правилом. Чтобы не смущать неокрепшие умы, заменим вставку на 80. Правило станет короче и менее страшным.
(.{1, 80})( +|$\n?)|(.{1, 80})
Очевидно, что оно состоит из четырёх частей:
(.{1, 80})
— строка длиной от 1 до 80 символов (любых). Результат группировки записывается в$1
(один доллар) или"\\1"
.( +|$\n?)
— пробелы или конец строки. Результат группировки записывается в$2
(два доллара) или"\\2"
. Обратите внимание на запись$\n?
, которая означает «конец строки ($
), после которого может идти перенос (\n
)». Обратите внимание, что$2
мы не используем и поэтому можно использовать(?:)
(группировку без сохранения результата).|
— или.(.{1, 80})
— строка длиной от 1 до 80 символов (любых). Результат группировки записывается в$3
(три доллара) или"\\3"
.
В результате работы этого правила произойдёт сопоставление с группировками 1 и 2 или 3. В первом случае будет обрабатываться строка, слова в которой по длине не превышают 80. Во втором случае строка будет принудительно усечена до 80 символов. Другими словами, мы пытаемся сделать перенос по словам, но если у нас не получается, то мы будем делать перенос так, как у нас получится.
Представленное решение не идеально. В частности, слова с дефисом хорошо бы переносить именно по дефису (при этом оставляя его на первой строке). Про остальные изыски (перенос по слогам и так далее) вы можете догадаться самостоятельно. Эта задача может решаться бесконечно, предусматривая всё большее количество различных вариантов. Плюс ко всему, возможны комбинации с другими задачами (найти длину самого длинного слова и осуществить перенос по этой длине).
Источник (англоязычный): http://macromates.com/blog/archives/2006/06/28/wrapping-text-with-regular-expressions/
Методы преобразования к строке
правитьRuby сам преобразует типы для некоторых простых операций. Например, при включении строки в другую он воспользуется имеющимся у объекта методом .to_s
:
class Container
def to_s
"контейнер"
end
end
cont = Container.new
p "Это #{cont}" #=> "Это контейнер"
Если нужно, чтобы ваши объекты упорядочивались и сравнивались с обычными строками, следует применять примесь Comparable
и единственный специальный метод to_str
. Наличие этого метода у вашего объекта — знак для Ruby, что для сравнения следует применять не встроенный в String
метод, а ваш.
class Container
include Comparable
def to_str
"контейнер"
end
def to_s
"контейнер"
end
def <=>(other)
to_s <=> other.to_s
end
end
cont = Container.new
"контейнер" == cont #=> true
Задачи
править- Дана строка слов, разделёных пробелами. Вывести длиннейшее слово.
- Дана строка, содержащая кириллицу, латиницу и цифры. Вывести все слова, длина которых равна средней.
- Найти в строке первое целиком кириллическое слово.
- Дан текст (строка с переносами). Найти все слова, содержащие лишь три буквы «о».
- Только для русских слов.
- Для французских и русских слов.
- Для любого регистра буквы «о».
- Найти в тексте время в формате «часы:минуты:секунды».
- Найти все слова без повторяющихся букв (например, «Лисп» или «Ruby», но не «Паскаль» или «Java»).
- Только для русскоязычных слов.
- Не учитывайте цифры в словах.
- Найти в тексте слова́, содержащие две прописные буквы, и исправить.
- Решите задачу для слов и в кириллице, и в латинице.
- Найти в тексте даты формата «день.месяц.год».
- Найдите дату, где день ограничен числом 31, а месяц 12. Год ограничивайте четырёхзначными числами.
- Распознавайте месяц в виде «31.марта.2001».
- Дан текст. Найдите все URL адреса и вычлените из них ссылку на корневую страницу сайта (например, из http://ru.wikibooks.org/wiki/Ruby сделайте http://ru.wikibooks.org).