Это первое пособие по работе с языком РубиН — RubyN — RubyN(ational) русскоязычным расширением популярного языка программирования Ruby. В целях экономии времени этот раздел представляет видоизмененный оригинальный викиучебник по Ruby.


Человек создан для творчества, и я всегда знал, что люблю творить. Увы, я обделён талантом художника или музыканта. Зато умею писать программы.

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

Юкихиро Мацумото

Программы должны быть написаны так, чтобы их могли читать люди, и лишь иногда так, чтобы их могли выполнять машины.

Абельсон и Сассман

Ру́би — интерпретируемый язык программирования высокого уровня. Обладает независимой от операционной системы реализацией многопоточности, строгой динамической типизацией, «сборщиком мусора» и многими другими возможностями, поддерживающими много разных парадигм программирования, прежде всего классово-объектную. Руби был задуман в 1993 году (24 февраля) японцем Юкихиро Мацумото, стремившимся создать язык, совмещающий все качества других языков, способствующие облегчению труда программиста.

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

Учебник пока неполон. Читайте иноязычную литературу по Ruby и с новыми знаниями присоединяйтесь к написанию. Да-да, каждый может участвовать: научитесь работать в вики-среде, с остающимися вопросами выступайте на странице обсуждения учебника.

Отличия РубиНа от Ruby

править

Самое первое и главное отличие — при написании переменных, классов, методов Вы теперь не ограничены 128 первыми символами ANSI. Вам доступны все 256 8-битных символа. В данной реализации используется наиболее общеупотребительная кодировка Windows-1251.

Все ключевые слова, классы и методы Ruby полностью доступны в Рубине, но кроме того все они имеют русскоязычные эквиваленты. Например: для ~ for

В Рубине в оригинальный язык Ruby добавлено одно ключевое слово equivalent ~ эквивалент, которое по функциональной нагрузке весьма схоже с ключевым словом alias ~ дримя , но все же имеет существенное отличие.

В дальнейшем, если вы читаете что-либо о языке Ruby, то все это в полной мере относится и к языку РубиН. В случае отличий это всегда будет оговорено и будет использовано название РубиН.

Основные свойства Ruby

править

Интерпретируемый язык:

Простое и быстрое программирование:

Объектно-ориентированное программирование:

Удобства:
  • Неограниченный диапазон значений целых чисел
  • Модель обработки исключений
  • Все операторы возвращают значения, даже управляющие структуры
  • Динамическая загрузка
  • Механизм перехвата исключений
  • Поддержка потоков; как собственных, так и систем семейства Unix
Недостатки:
  • Неуправляемость некоторых процессов (таких, как выделение памяти), невозможность задания низкоуровневых структур данных или подпрограмм
  • Невозможность компиляции и сопутствующей ей оптимизации программы
  • Следствие двух первых недостатков — весьма низкая скорость запуска и выполнения программ

Начало работы

править

От читателя требуется общее знание компьютеров, включая навыки работы с файловой системой и текстовыми файлами. Прежде чем начать программировать на Руби, нужно установить интерпретатор и обустроить для себя удобную среду для создания программ. Для испытания кода из учебника можно воспользоваться сетевым интерактивным интерпретатором.

Об установке и настройке рабочей среды Руби читайте приложение Установка.

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

Чтобы запустить программу с именем TecToBa9_nporpamma.rb (программы на Руби обычно имеют расширение .rb), в Unix-e нужно вызвать консоль и набрать:

% ruby TecToBa9_nporpamma.rb

или если Вы используете кодировку Windows-1251

% rubyn тестовая_программа.рб 

а сделав соответствующую ссылку и

% рубин тестовая_программа.рб

Также возможна настройка популярнейшей UNIX-оболочки mc таким образом, что вы сможете стартовать программу просто нажав Enter на имени программы.

Чтобы запустить программу из Windows нужно дважды кликнуть на файл с программой. Чтобы окно не исчезало и были видны результаты надо перед местами выхода из программы поставить команду ожидания ввода

СТДВВ.ввс или  СТДВВ.ввстр или STDIN.getc
 

Вот и добрались до еще одного существенного отличия РубиНа от Ruby - вышеуказанное пожелание очень полезно, когда вы работаете с полностью отлаженной программой, что на практике встречается довольно редко, поэтому в РубиН добавлен атрибут командной строки -z, который позволяет увидеть до закрытия окна результат выполнения Вашей программы. Насладившись созерцанием столь приятной картинки Вы сможете нажать любую клавишу (под Виндами) или клавишу Enter (под UNIX), и только после этого Ваше окошко исчезнет.

 

Именам программ в Windows принято давать расширение .rbw или .рбо, что позволяет использовать Windows-версию интерпретатора rubynw, который надоедающее окошко просто не вызывает. Интерпретатор rubyn предназначен для работы в консоли. Для интерпретатора rubynw вставлять код СТДВВ.ввстр ~ STDIN.getc перед местами выхода бесполезно


Первая программа

править

Откройте редактор и напишите программу. По традиции первая программа должна просто выводить приветствие миру:

п "Привет, Мир!"
вывстр "Привет, Мир!"

Сохраните её в файл привет_мир.рб и запустите. На экране будет красоваться надпись:

"Привет, Мир!"
Привет, Мир!

На этом традиционную часть можно считать выполненной.

Полигон для испытания программ

править

Чтобы посмотреть результат выполнения большинства примеров из учебника, не обязательно создавать файл. Достаточно использовать полигон для испытания программ на Руби irb, который входит во все дистрибутивы и сам написан на Руби. Запускается он командой

% irb

Полигон — это рубиговорящая командная строка. Она показывает вам результат выполнения каждой строки кода:

% irb(main):001:0> [1,2,3,4]
% => [1, 2, 3, 4]

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

% irb(main):002:0> "некий текст".методы
% => ["клон", "gem", "strip!", "член?", ...]

если же есть желание как-то упорядочить, то сделайте

% irb(main):002:0> "некий текст".методы.сорт
% => ["%", "*", "+", "<", ...]

Или посчитаем:

% irb(main):002:0> 14 * 5
% => 70

Для выхода с полигона — набрать выйти или exit.



fxri: полигон и справочник

править
 
Внешний вид программы fxri

В последних версиях дистрибутива «Установка за один щелчок» для Windows появилась утилита fxri. Это оконная кроссплатформенная программа, вобравшая в себя функционал ri и irb. Буковки fx в начале означают использование библиотеки FXRuby. Последние две буквы ri означают Ruby Informer (Информатор о классах и методах в языке Руби).

У fxri три окна: в левом перечислены все методы c описаниями. Верхнее правое окно рассказывает о выбранном методе. Нижнее левое окно реализует irb, описанную выше.

Полигон в сети

править

Иногда интерпретатора Руби нет под рукой, приходится использовать сетевой полигон. Он позволяет выполнять код на Руби прямо из браузера.

puts hellow

Комментарии и демонстрация результата

править

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

В Ruby знаком начала комментария служит #. Все, что между ним и концом строки пропускается. Пример:

вывстр 2+2          # это комментарий
вывстр "Привет!"    # тоже комментарий

Результат иллюстрируемого кода будет располагаться после последовательности #->. Пример:

puts 2+2          #-> 4
puts "Привет"     #-> Привет

Вывод на экран

править

В Ruby есть много методов вывода: print, printf, p, puts, .display и другие. Но мы использовать будем два:

  • метод puts. После вывода строки осуществляет переход на новую. Если приходится выводить объект, не являющийся строкой, то puts вызывает метод .to_s для преобразования его к строке;
  • метод p. Имеет самое короткое название, из-за чего часто используется для отладки. Так же, как и puts, делает перевод на новую строку после вывода. Перед выводом любого объекта (в том числе и строки) на экран, вызывает метод .inspect.

Примеры вывода на экран:

puts [1,2,3,4]      #-> 1\n2\n3\n4 
                    # \n означает перевод строки-возврат каретки
p [1,2,3,4]         #-> [1,2,3,4]
puts "npuBeT!"      #-> npuBeT!
p "npuBeT!"         #-> "npuBeT!"
puts 5              #-> 5
p 5                 #-> 5

Как видно из примера, результаты во время вывода строк и массивов существенно различаются. Если вас не смущают кавычки в результате вывода, то смело используйте p, если смущают, то puts.

 

Иногда возникает ситуация, когда при попытке вывода на экран русскоязычной строки при помощи метода p выводится непонятный код вида

"\323\367\350\362\345 \320\363\341\350!"
Чтобы избежать таких ситуаций следует поместить в начало программы следующий программный код:
$KCODE = "utf-8"

Кодировка utf-8 используется в SciTE. При использовании других редакторов, может потребоваться смена кодировки на соответствующую (зависит от редактора и операционной системы)

Переменные

править

Переменные используются, чтобы сохранить промежуточный результат вычислений. Имя переменной в Ruby должно:

  • начинаться со строчной буквы или знака подчеркивания;
  • состоять из латинских букв, цифр и знака подчеркивания.

Примеры переменных:

maccuB
nepemeHHa9
gpyra9_nepemeHHa9
HenpaBuJIbHa9_nepemeHHa9   # неправильное имя. Начинается с прописной
3JIemeHT                   # неправильное имя. Начинается с цифры
3JIemeHT                   # а вот как можно
__                         # немного странное, но корректное имя переменной

Переменная может иметь имя не только латинское, но и русское. Для этого, правда, требуется, чтобы весь текст программы был написан в кодировке UTF-8, а интерпретатор запускался с параметром -KU

Присвоение делается знаком равенства (=), вот так:

maccuB = [1,2,3,4]
nepemeHHa9 = maccuB + [1,2]
gpyra9_nepemeHHa9 = nepemeHHa9 - maccuB

То, что справа от =, прежде чем стать значением переменной, обычно полностью вычисляется. Наша nepemeHHa9 будет содержать массив [1, 2, 3, 4, 1, 2], ибо именно таков результат действия maccuB + [1, 2]. Плюс (+) с массивами поступает именно так: прицепляет второй массив в хвост первого.

Переменные указывают на объект

править

Фокус-покус:

nogpyra = "Даша"
k_HAM_B_rocTu_ugeT = nogpyra
puts nogpyra                    #-> "Даша",  разумеется
k_HAM_B_rocTu_ugeT[0] = "М" # меняем первую (номер ноль) букву у переменной-строки
puts nogpyra              #-> "Маша"
                              # На первый взгляд, странно и неожиданно

Значение, возвращаемое первой переменной изменилось потому, что

Сами данные (объект) лежат где-то в другом месте. В виду этого естественно, что напрямую изменяя сам объект, указуемый переменной, все другие переменные, указывающие на этот объект, будут возвращать то изменённое значение (также будут изменяться).

Чтобы nogpyra наша осталась "Даша", надо в переменную занести её клон:

nogpyra = "Даша"
k_HAM_B_rocTu_ugeT = nogpyra.clone
k_HAM_B_rocTu_ugeT[0] = "М"
# Но изменили мы лишь клон. Дома в сохранности сидит настоящая:
puts nogpyra #-> "Даша"
 

Можно создавать копии объектов ещё методом .dup. Разницу между ними Вы потом поймёте

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

#...
k_HAM_B_rocTu_ugeT = "Аристарх"; # Создаётся новый объект, переменная переводится на него
p nogpyra #-> "Даша"

Типы данных

править

Данные любого типа в Ruby суть объекты тех или иных классов. Самые используемые встроенные типы данных: Fixnum (целые числа, меньшие  ), Bignum (целые числа, большие  ), Float (числа с плавающей точкой), Array (массивы), String (строки), Hash (ассоциативные массивы). Естественно, что это только базовые типы, но их вполне хватает для широкого спектра задач.

 
Все абстрактные типы данных

Числа в Ruby выглядят так:

5     # целое число
-12   # отрицательное целое число
4.5   # число с плавающей точкой
076   # восьмеричное число
0b010 # двоичное число
0x89  # шестнадцатиричное число

Не будем пока углубляться и мельком взглянем на другие типы данных.

Логический тип

править

Логические операции

править
название операции символ операции литерное обозначение
логическое «или» || or
логическое «и» && and
логическое «не» ! not
логическое «исключающее или» ^ xor

Логический (булев) тип — это вариация на тему «да» или «нет». В Ruby он представлен двумя предопределенными переменными true («истина» или «да») и false («ложь» или «нет»). Появляется логический тип в результате логических операций или вызова логических методов.

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

Методы сравнения

править
название метода символ
равно ==
не равно !=
меньше <
больше >
меньше или равно <=
больше или равно >=
Часто молодые программисты, когда надо написать «меньше или равно», пишут знак =< вместо <=. Запомнить правильное написание можно вслух проговорив «меньше или равно» и в этом же порядке писать < и =


Массивы

править

Разработчики Руби решили не реализовывать особых классов для динамических массивов, списков, стеков и тому подобного. Они все это реализовали в массивах — структурах данных типа (или класса, — в Руби всё равно) Array. Сделано это путем добавления специальных методов; например, методы .push и .pop для стека. Особенности массивов в Руби:

  • Нет ограничений (это общий принцип языка). Массивы могут быть сколь угодно длинными.
  • Динамичность: размер массива легко меняется.
  • Гетерогенность: один массив может хранить данные разных типов.
  • Библиотека итераторов на каждый случай жизни. Эта возможность позволяет не использовать циклов для обработки данных в массивах, а следовательно избегать множества ошибок, связанных с неосторожным обращением с циклами. Итераторы реализуются на высочайшем уровне.
  • Много других методов. Все элементарные задачи для массивов решаются вызовом нужного метода.

Массив лучше всего вообразить как гусеницу и притом поезд с лапками-запятыми вместо колёс.

[1, 0, 740, 14, 88]       # целочисленный массив
['a',"й", "6",'Br', "Это массив строк, о них Вы скоро узнаете"]
[[1,2],[3,4]]     # двумерный целочисленный массив. 
                  # Матрица -- это объект класса Matrix. 
                  # Двумерный массив -- это не матрица целых чисел
["1й элемент смешанного массива", "7.343", [4, "вепрь"], 
   [3, 67, 4326, 12, 3781357, 84221, "строка делает этот подмассив смешанным, но это не беда"]]
 maccuB=["Этот массив пойдёт в переменную maccuB", "Як-цуп-цоп, парви каридула"]

Ползёт он всегда влево; на левом же конце его локомотив — первый элемент. Первый потому, что элементы упорядочены. Если знаете порядковый номер элемента, то легко получить его значение:

maccuB[1] #=> "Як-цуп-цоп, парви каридула"

В мире поездов-гусениц счёт вагонов начинается с локомотива, а не со следующего за ним вагона. Таким образом локомотив — это как бы нулевой вагон.

maccuB[0] #=> "Этот массив пойдёт в переменную maccuB"

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

Строки

править

Строки — это ряды букв и других символов. В Ruby строки используют наработки языка Перл. Вот небольшой список их возможностей:

  • Нет ограничений. Длина строки может достигать поистине фантастических размеров.
  • Динамичность. Строки можно расширять или уменьшать (для этого есть методы + и []).
  • Любой объект преобразуется к строке (методы .inspect и .to_s есть у любого объекта).
  • Строка обладает обширной библиотекой методов, которые работают с правилами (это методы .gsub, .match, .scan, .split).
 

Правилаэто новое название регулярных выражений. В текущей версии Ruby они называются регулярными выражениями, но давайте смотреть в будущее. Perl 6 уже не имеет понятия «регулярное выражение», заменив его «правилами» и «грамматиками»;

  • Можно вставлять произвольный код на языке Ruby в строку. После выполнения код заместится результатом.

Строки начинаются и заканчиваются " (лапками) или ' (типографским апострофом). Пример:

"мама мыла раму"        # строка в надёжных лапках
'рама сопротивлялась'   # строка в апострофах

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

Ассоциативные массивы

править

Ассоциативные массивы подобны массивам упорядоченных пар. Работают они подобно словарям: фигурная скобка символизирует боковой вид на открытую книгу, а стрелка => покажет читателю связь каждого одного с чем-то другим. Вторая фигурная скобка говорит, что пора закрывать книгу.

{ "мама" => "мыла раму", 807 => "Это число улыбается!"}

Но можно и без фигурных скобок, одной стрелкой:

 "npeBeg" => "MegBeg"

Например:

puts maccuB["мама"] #-> мыла раму
puts maccuB["807"]  #-> nil
puts maccuB[807]    #-> Это число улыбается!
puts maccuB[1]      #-> nil
puts maccuB["npeBeg"]#-> MegBeg

Ассоциативные массивы оставляют возможность хранения данных разного типа только в ассоциативном виде.

Диапазоны значений

править

Чтобы было удобней получать подмассив или подстроку, был введен простенький тип данных — диапазон (класс Range). Диапазон формируется тремя элементами: начало, конец и тип протяженности (символ .. или ...). Начало и конец должны быть одного типа данных (одного класса) и быть перечислимыми, что значит, иметь метод .succ. Пример диапазонов:

'a'..'z'
'a'...'z'  # то же, что и 'a'..'y'
1..100
1...100    # то же, что и 1..99

Диапазон-мотоцикл (..) проезжает от первого указанного объекта к его .succ (succedent — «последующему»), и до последнего включительно. Три точки — то же, но мотоцикл остановился прямо перед последним элементом. Ещё раз:

1..991...100

 

Объекты, имеющие .succ называют последовательными: этим методом можно по текущему элементу достоверно определить следующий.

Классы и объекты

править

Самодельные и неабстрактные, составные типы данных называются классами. Если для вас это новость, то почитайте викиучебник об объектно-ориентированном программировании или статью в Википедии. Вообще, в Руби всё в конечном счёте принадлежит классу Object.

str = "Aз есмь строка"
str.class              #-> объект класса String
str.class.superclass   #-> подкласса класса Object

Классы можно определять и создавать по ним объекты. Внутри класса может быть много всего интересного, и у него может быть фамильное дерево, то есть классы Руби поддерживают наследование. Однако заметим, что множественное наследование в Руби не разрешается. И ещё много всего интересного можно сделать с классами и объектами. Но об этом позже.

Подробнее о числах

править

Изначально числа представлены тремя типами: два целых типа (классы Fixnum и Bignum) и один дробный (класс Float). Возможно подключение дополнительных типов, — например, комплексных и рациональных чисел, но пока ограничимся тремя.

Целые числа

править

Целые числа в Ruby не ограничены по величине, то есть могут хранить сколь угодно большие значения. Для обеспечения такого волшебного свойства было создано два класса. Один из них хранит числа меньшие   (по модулю), а второй — всё, что больше. По сути, для больших чисел создается массив из маленьких, а раз массив не имеет ограничений по длине, то и число получается неограниченным по значению.

Как только число типа 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 (подключаются рациональные и комплексные числа).

Семейный портрет чисел

править
 
Числовые типы данных

В отличие от большинства элементарных типов данных, числа обладают своей иерархией. Все числа в Руби наследованы от класса Numeric (Числовой). Поэтому, если хотите добавить новый метод ко всем числам, то нужно расширять именно этот класс. Далее идет деление чисел: Integer (целое), Float (дробное) и Complex (комплексное). При желании можно добавить и Rational (рациональное), но на данном семейном портрете оно отсутствует.

От класса Integer наследуются два класса: Fixnum (целое маленькое) и Bignum (целое большое). К первому относятся все числа, по модулю меньшие   , а ко второму — все остальные.


Арифметические операции

править

Арифметические операции в 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

Лучше проверить эти сведения самостоятельно. Для этого даже не обязательно устанавливать интерпретатор Ruby. Достаточно лишь зайти на сайт try ruby!.

Поразрядная арифметика

править
Знак операции Название
&
побитовое «и»
|
побитовое «или»
^
побитовое «исключающее или»
<<
побитовый сдвиг влево
>>
побитовый сдвиг вправо
~
побитовая инверсия

Операции побитовой арифметики заимствованы из языка Си. На этот раз без всяких экзотических особенностей.

6 & 4     #-> 4
6 | 4     #-> 6
6 ^ 4     #-> 2
6 << 4    #-> 96
6 >> 4    #-> 0 (чересчур намного сдвинули)
~4        #-> -5 (операция только над одним аргументом)

Здесь, вроде, всё понятно и без дополнительных пояснений. А если не понятно, то справочник по языку Си поможет.

Операции с присвоением

править

Часто можно встретить выражения вида:

ogHo_4ucJIo += gpyroe_4ucJIo

Это выполнение операции сразу с присваиванием. Вышеуказанная запись равнозначна следующей:

ogHo_4ucJIo = ogHo_4ucJIo + gpyroe_4ucJIo

Вполне естественно, что вместо операции + может использоваться любая другая, а вместо чисел могут быть другие типы данных.

cTpoka = "едем"
cTpoka += ", "
cTpoka *= 3
cTpoka    #-> "едем, едем, едем, "

maccuB = [1,2,3]
maccuB += [4,5]
maccuB    #-> [1,2,3,4,5]
 

При определении метода +, метод += вы получаете в подарок. Это правило касается всех бинарных операций, обозначаемых значками.

Методы явного преобразования типов

править
Метод Операция
to_f преобразовать в дробное
to_i преобразовать в целое
to_s преобразовать в строку
to_a преобразовать в массив

Методы преобразования типов в 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 (на 1 меньше 100). Во втором случае, метод rand возвращает дробное число в диапазоне от 0.0 до 1.0 включительно. Различие в результате обусловлено передаваемым параметром:

  • если передается параметр (в данном случае 100), то генерируется целое случайное число (в диапазоне 0...N-1, где N — передаваемый аргумент);
  • если параметр отсутствует, то генерируется дробное число в диапазоне от 0.0 до 1.0.

Есть способ предсказать весь ряд «случайных» чисел. Делается это при помощи метода srand. Ему передается целое число (идентификатор «случайной» последовательности). После этого, весь случайный ряд можно предугадать. Проведем эксперимент: берусь угадать массив, который будет сгенерен следующей программой.

srand 123
Array.new(5){ rand(100) }   #-> [69, 71, 28, 42, 22]

Если вы выполните данную программу у себя дома, то получите тот же самый массив. 123 — номер «случайной» последовательности. Измените его и массив изменится!

Если вызвать srand без параметра или не вызывать его вообще, то номер «случайной» последовательности выбирается случайным образом.

Хитрости

править

Задача: выдать целое число в двоичной системе счисления.

ucxogHoe_4ucJIo = 1234
puts sprintf("%b",ucxogHoe_4ucJIo) # метод sprintf заимствован из Си
puts ucxogHoe_4ucJIo.to_s(2)       # современный метод - означает "по основанию",
                                   # аргументом может служить не только 8 и 16, но и 5, 30...
                                   # На самом деле, основание системы в моей версии ruby не может
                                   # превышать 36, что вполне объяснимо - 10 цифр и 26 букв латинского
                                   # алфавита.

Поменять порядок цифр данного числа на обратный:

ucxogHoe_4ucJIo = 1234
puts ucxogHoe_4ucJIo.to_s.reverse  # метод reverse переворачивает строку

Получить значение N-го двоичного разряда данного целого числа:

ucxogHoe_4ucJIo, N = 1234, 5
puts ucxogHoe_4ucJIo[ N ]

Поменять целочисленные значения двух переменных без использования третьей переменной:

ucxogHoe_nepBoe, ucxogHoe_BTopoe = 134, 234
ucxogHoe_nepBoe, ucxogHoe_BTopoe = ucxogHoe_BTopoe, ucxogHoe_nepBoe
puts ucxogHoe_nepBoe, ucxogHoe_BTopoe

Округлить дробное число до двух разрядов:

gpo6Hoe_4ucJIo = 3.1415926535
puts ( gpo6Hoe_4ucJIo * 100 ).to_i.to_f / 100
puts (( gpo6Hoe_4ucJIo + 0.005) * 100 ).to_i / 100.0 
puts sprintf( "%.2f", gpo6Hoe_4ucJIo ).to_f    # полуСишный способ =)

На самом деле во второй строке оставляются 2 знака после запятой, а остальные просто отбрасываются безо всяких округлений, в то время как в третьей строке действительно происходит округление до двух знаков после запятой. Это легко проверить попытавшись округлить до 3 знаков после запятой:

gpo6Hoe_4ucJIo = 3.1415926535
puts ( gpo6Hoe_4ucJIo * 1000 ).to_i.to_f / 1000           # => 3.141
puts (( gpo6Hoe_4ucJIo + 0.0005) * 1000 ).to_i / 1000.0   # => 3.142
puts sprintf( "%.3f", gpo6Hoe_4ucJIo ).to_f               # => 3.142

Подробнее о массивах

править

Массивы — это тип данных, с которым вам придется работать постоянно. Облик большинства программ зависит именно от правильного (читай «изящного») использования массивов.

Способы создания массива

править

Массив создается как минимум тремя способами. Первый способ:

[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'  ]

Плюсы расставлены лишь для красоты. Но вернемся к отрицательной индексации. Каков ее смысл? Чтобы его пояснить, давайте решим задачку: дан массив, требуется получить предпоследний элемент.

maccuB = [ 'a', 'b', 'c', 'd', 'e']
maccuB[ maccuB.size - 2 ] #-> 'd'

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

maccuB = [ 'a', 'b', 'c', 'd', 'e']
maccuB[ -2 ]     #-> 'd'

Индекс −2 значит «второй с конца элемент массива». Вот так и появилась отрицательная индексация. Теперь давайте разберемся с диапазонами. Оказывается, в них тоже можно использовать отрицательную индексацию. Вот как можно получить все элементы массива кроме первого и последнего:

maccuB = [ 'a', 'b', 'c', 'd', 'e']
maccuB[ 1..-2 ]  #-> ['b', 'c', 'd']

Или так:

maccuB = [ 'a', 'b', 'c', 'd', 'e']
maccuB[ 1...-1 ]  #-> ['b', 'c', 'd']

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

О двумерных массивах

править

Для Ruby двумерный массив — это не более чем массив, содержащий одномерные массивы. Вот несколько примеров двумерных массивов:

[[1],[2,3],[4]]                                    # разная длина элементов-массивов
[[1,2],[3,4]]                                      # одинаковая длинна
[["прива","Привет"],["пока","Всего хорошего"]]     # двумерный массив (классика)
[["прива","Привет"],[1,["пока","Всего хорошего"]]] # гибрид двух-трех-мерного массива


Методы работы с массивами

править

Разнообразие и полезность методов у массивов создаёт впечатление, что все сложные алгоритмы уже реализованы. Это не так, программистам Руби дана действительно обширная библиотека методов. Здесь мы рассмотрим лишь самые употребимые; остальные ищите в справочнике.

Получение размера массива

править

В Ruby массивы динамические: в каждый конкретный момент времени неизвестно сколько в нем элементов. Чтобы не плодить тайн подобного рода и был реализован метод .size:

[1,'считайте',3,'количество',5,6,'зяпятых',2,5].size   #-> 9

Мы явно указали массив, но на его месте могла стоять переменная:

maccuB = [1,'считайте',3,'количество',5,6,'запятых',2,5]
maccuB.size               #-> 9
 

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

 

Метод .size есть у многих классов. Например, у ассоциативных массивов и строк. И даже у целых чисел.

Поиск максимального/минимального элемента

править

Вспомните сколько усилий вам приходилось прилагать, чтобы найти максимальный элемент? А сколько раз вы повторяли этот кусок кода в своих программах? Ну а в Ruby поиск максимального элемента осуществляется при помощи метода .max, а в более сложных случаях при помощи метода .max_by (начиная с версии 1.9). Вот как это выглядит:

['у','попа','была','собака'].max                          #-> 'у'       максимальный по значению
['у','попа','была','собака'].max_by{ |elem| elem.size }   #-> 'собака'  максимальный по размеру строки

Методы .min и .min_by работают аналогично:

['у','попа','была','собака'].min                         #-> 'была'   минимальный по значению
['у','попа','была','собака'].min_by{ |elem| elem.size }  #-> 'y'        минимальный по размеру строки

Ну как? А в Руби эти методы уже давно.

Упорядочение

править

Не буду травить душу долгими байками. Чтобы упорядочить массив, нужно вызвать метод .sort или .sort_by (начиная с версии 1.8).

['у','попа','была','собака'].sort
   #-> ['была','попа','собака','у'] сортировка по значению
['у','попа','была','собака'].sort_by{ |elem| elem.size }
   #-> ['у','попа','была','собака'] сортировка по размеру строки

Остается только добавить, что массивы упорядочиваются по возрастанию. Если вам надо по убыванию, то придется писать собственный метод сортировки пузырьком. Шутка! По правде же, есть много способов выстроить массив по убыванию. Пока мы будем использовать метод .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]

Поэтому объединение массивов можно записать как ( nepBblu_maccuB + BTopou_maccuB ).uniq. Но проще, конечно, объединять палкой.

Сплющивание массивов

править

Метод .flatten делает из многомерного массива простой, длинный одномерный массив. Он как бы расплющивает его. Например, чтобы найти в двумерном массиве наибольший элемент, мы сперва расплющим массив, а потом найдём максимум методом .max:

maccuB = [[1,2],[3,4]]
maccuB.flatten.max    #-> 4

Расплющивание происходит в несколько этапов. Сначала происходит удаление всех квадратных скобок.

[[1,2],[3,4]]

А потом, две квадратные скобки добавляются слева и справа. Но делать это надо быстро, чтобы элементы не успели разбежаться.

[1,2,3,4]

Вот и все! У нас они разбежаться не успели. Повторите данное упражнение на других массивах (двумерных, трехмерных и т.д).

Удаление неопределенных(nil) элементов

править

Функцию удаления nil элементов массива выполняет метод .compact например:

maccuB = [1,nil,2,nil,3]
maccuB.compact    #-> [1,2,3]

Транспонирование двумерного массива

править

Задача: дан двумерный массив. Вывести одномерный массив с максимумами каждого из столбцов. Хм… посмотрим сперва, как эта задача решается для строчек, а не столбцов:

maccuB_2D = [[1,2],[3,4]]
maccuB_2D.map{ |maccuB| maccuB.max } #-> [2,4]

Чтобы решить задачу в первоначальном варианте, нам надо лишь предварительно транспонировать массив (поменять местами строки и столбцы):

maccuB_2D = [[1,2],[3,4]]
maccuB_2D.transpose.map{ |maccuB| maccuB.max } #-> [3,4]

Метод .transpose как раз и занимается транспонированием. Это позволяет с легкостью решать задачи про столбцы приемами, схожими с задачами про строки.

Размножение массивов

править

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

["много", "денег", "прячет", "теща"] * 2
    #-> ["много", "денег", "прячет", "теща", "много", "денег", "прячет", "теща"]

Того же самого эффекта можно добиться сцепив массив необходимое количество раз:

maccuB = ["много", "денег", "прячет", "теща"]
maccuB + maccuB #-> ["много", "денег", "прячет", "теща", "много", "денег", "прячет", "теща"]

Заметили, что есть некоторая параллель с целыми числами? Умножение можно заменить сложением и наоборот!

Функциональность стека

править

Часто и во многих алгоритмах надо добавить элемент в конец массива:

maccuB = [1,2,3,4,5]
maccuB[ maccuB.size ] = 6
maccuB  #-> [1,2,3,4,5,6]

И если уж добавили, то надо как-то его и удалить. Делается это примерно так:

maccuB = [1,2,3,4,5,6]
maccuB[0...-1] #-> [1,2,3,4,5]

Но как всегда, эти задачи возникали слишком часто и их решили реализовать в виде методов. Методы назвали .push («втолкнуть» в конец массива) и .pop («вытолкнуть» элемент из массива):

maccuB = [1,2,3,4,5]
maccuB.push( 6 )
maccuB      #-> [1,2,3,4,5,6]
maccuB.pop  #-> 6
maccuB      #-> [1,2,3,4,5]

Функциональность очереди и списка

править

Чтобы можно было использовать массив в качестве очереди и/или списка, потребуется сделать всего лишь пару методов. Первый из них добавляет элемент в начало массива, а второй удаляет элемент из начала. Давайте посмотрим, как это делается универсальными методами [], []= и +:

maccuB = [1,2,3,4,5]

# добавим элемент в начало массива
maccuB = [6] + maccuB
maccuB     #-> [6,1,2,3,4,5]
maccuB[0]  #-> 6

# удалим элемент из начала массива
maccuB = maccuB[1..-1]
maccuB     #-> [1,2,3,4,5]

Теперь посмотрим, какие методы реализуют точно такую же функциональность:

maccuB = [1,2,3,4,5]

# добавляем элемент в начало массива
maccuB.unshift( 6 ) #-> [6,1,2,3,4,5]

# удаляем из начала массива
maccuB.shift        #-> 6
maccuB              #-> [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 и включите в ваш класс mixin Enumerable. В нем находится реализация методов, таких как .inject, .each_with_index и т. п.

Логические методы

править
Логический метод — это метод, результатом которого является логическое выражение (true или false).

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

maccuB = [1,2,2,3]
puts maccuB.methods.grep(/\?$/)

Для удобства, можно упорядочить полученный список:

maccuB = [1,2,2,3]
puts maccuB.methods.grep(/\?$/).sort

Есть элемент в массиве?

править

Как узнать, есть ли некоторый элемент в массиве? Попробуем решить эту задачу при помощи метода .size и итератора .find_all:

maccuB = [1,2,3,4,5,6,7]
uckomoe = 5                # число, которое мы будем искать
maccuB.find_all{ |elem| elem == uckomoe }.size != 0  #-> true 
                                                     # это значит, что такое число есть

Использование связки из трех методов (!=, .find_all и .size) для такой задачи — возмутительно! Разработчики не могли с этим долго мириться и реализовали метод специально для этой задачи. Имя ему — .include?. Перепишем нашу задачу, но на этот раз будем использовать правильный метод:

maccuB = [1,2,3,4,5,6,7]
uckomoe = 5                # число, которое мы будем искать
maccuB.include?( uckomoe ) #-> true
                           # что бы это значило?

Мутный горизонт скрывает берег,
Ветер мокр, холоден и лют.
Есть ли в озере акулы, я проверю
Методом логическим .include?

o3epo = ["правый берег", "ветер", "вода", "вода", "вода", "окунь", "вода", "вода", "левый берег"] 
o3epo.include?("акула") #-> false

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

Массив пустой?

править

Если вы хотите задать массиву вопрос «ты пуст ли?», но боитесь обидеть, то можете пойти окружным путем. Например, спросить у него: ты равен пустому массиву?

nycTou_maccuB = []
noJIHbIu_maccuB = [1,2,2,3]

nycTou_maccuB == []   #-> true
noJIHbIu_maccuB == [] #-> false

Еще можно задать вопрос: твой размер равен нулю?

nycTou_maccuB = []
noJIHbIu_maccuB = [1,2,2,3]

nycTou_maccuB.size == 0   #-> true
noJIHbIu_maccuB.size == 0 #-> false

Но наш вам совет: не стоит искать обходных путей. Спросите его напрямую: .empty? («пуст?»):

nycTou_maccuB = []
noJIHbIu_maccuB = [1,2,2,3]

nycTou_maccuB.empty?     #-> true
noJIHbIu_maccuB.empty?   #-> false


Итераторы

править

Массивы — эти эшелоны переменных, эти ожерелья запятых и элементов, — часто приходится проходить целиком, обследуя каждый элемент. Да и не только массивы, но и любые последовательности чего-нибудь.

В старину люди делали это циклами. В Руби у списочных структур данных есть встроенные методы, которые проходят весь ряд поэлементно, но, в отличие от циклов:

  • не зацикливаются: счётчик цикла нельзя докручивать;
  • выполняются заведомое число раз;
  • их много, и каждый делает своё дело.
Имя им — итераторы.

Изменение всех элементов массива

править

Изменить все элементы массива можно по-всякому. Начнём с обнуления:

maccuB = ['шифровка','Штирлица','в','Центр','секретно']
maccuB.map{ 0 }             #-> [0,0,0,0,0]


Используется итератор .map, за которым следует блок, — кусочек кода, схваченный лапками-фигурными скобками. .map последовательно проходит maccuB и выполняет блок заново для каждого элемента. То, что выходит из блока, итератор .map делает очередным элементом нового массива.

Можно дать элементу .map иное задание. Для этого зажимаем в фигурные скобы блока иной код:

maccuB = [1,2,3,4,5]
maccuB.map{ |elem|  elem**2 }  #-> [1,4,9,16,25]

Прежде, чем блоку выдать квадрат очередного элемента, ему нужно знать этот элемент. Итератор .map даёт ему значение элемента, словно фотографию, обрамлённую слева и справа вертикальными чертами |. Чтобы блок смог взять эту фотографию, обязательно дать ей имя. В нашем случае это elem, но подходят и такие названия:

  • _3JleMeHt
  • x
  • y
  • a_He_x0tuTe_li_4awe4Ku_4a9_c_LuMoHom

Вы уже, наверное, хорошо поняли, что в итераторах массивы обрабатываются по очереди; двери вагона расходятся, появляется элемент. Блок даёт ему прозвище, выполняет код в лапках-фигурных скобках. Затем переходит к следующему вагону, и там всё сначала.

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

То имя, что показывается в раздвижных дверях, — это не сам элемент, это лишь его копия. Фотография. Голограмма. Это даже не другая переменная, это не переменная вообще. Бессмысленно присваивать новое значение фотографии:

maccuB = [1,2,3,4,5]
maccuB.map{ |elem|  elem = elem**2 } # присваивание не имеет смысла: elem несёт лишь значение элемента, не являясь тем

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

maccuB = [1,2,3,4,5]
maccuB.map{ |elem| elem**2 }  #-> [1,4,9,16,25]
maccuB       #-> [1,2,3,4,5] - неизменный первый поезд.

Можно присвоить его новой переменной maccuB_c_kBagpaTaMu. А можно заместить им существующую переменную maccuB:

maccuB = [1,2,3,4,5]
maccuB = maccuB.map{ |elem| elem**2 }  #-> [1,4,9,16,25]
maccuB       #-> [1,4,9,16,25]

Это общее явление Руби: методы (здесь — итераторы) не меняют объект (массив), с которым работают. Они лишь выдают результат, который потом можно использовать как аргумент или присвоить переменной.

Явление было воспето в фольклоре:

Метод .map все изменяет,
Как кто пожелает
И обижается на тех,
Кто результат не сохраняет.

Отбор элементов по признаку

править

Вот как итератор .find_all выберет из массива все чётные элементы:

maccuB = [1,2,3,4,5]
maccuB.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
Метода вам не сыскать!


Мысль: красивее делить лишь последнюю цифру(так в третьем классе учат четность определять :)). Тем более, когда нужно определить четность factorial(100) :). В любом случае базовый класс нужно расширить чем-то вроде метода .chetnoe?.(с)Вовчик

Ошибка: (elem&1).zero? проверит на нечетность. Для проверки на четность надо !(elem&1).zero?

Ошибка комментатора: как раз наоборот. Разберем: (elem&1) — битовая операция, вернет первый бит числа (составляющую 20). То есть результатом будет 0, если elem — число четное, и 1 в ином случае. 0.zero? #true 1.zero? #false

Суммирование/произведение/агрегация элементов

править

Очень часто возникает задача найти сумму/произведение всех элементов массива. Для этих целей (начиная с версии 1.8) традиционно используется итератор .inject. Для демонстрации его работы, давайте найдем сумму элементов массива:

maccuB = [1,2,3,4,5]
maccuB.inject( 0 ){ |result, elem|   result + elem } #-> 15

Рассмотрим все по порядку. Начнем с нуля. Его следует расшифровывать как result = 0 перед началом работы итератора, то есть это начальное значение переменной result (переменной промежуточного результата).

Далее идет объявление двух переменных. Первая из них (result) будет хранить промежуточный результат. Вторая (elem) — фотография текущего элемента массива (или последовательности), мы такую уже видели.

После объявления описан алгоритм работы итератора. В данном случае ему предписано каждый элемент массива складывать с промежуточной суммой: result + elem.


Учитывая эти два замечания, напишем код, который является неправильным:

maccuB = [1,2,3,4,5]
maccuB.inject( 0 ){ |result, elem| result = result + elem } #-> 15
 

Имена переменных result и elem созданы богатым воображением автора. В ваших программах они могут называться иначе.

Изменим имена переменных:

maccuB = [1,2,3,4,5]
maccuB.inject( 0 ){ |pe3yJibTaT, nepemeHHa9| pe3yJibTaT + nepemeHHa9 } #-> 15

Невероятно, но от изменения имен переменных, результат остается прежним. Помните это!

Для полноты картины решим еще одну задачку. На этот раз будем искать произведение всех элементов массива:

maccuB = [1,2,3,4,5]
maccuB.inject( 1 ){ |pe3yJibTaT, nepemeHHa9| pe3yJibTaT * nepemeHHa9 } #-> 120

Чтобы закрепить материал, решите задачу: найти произведение всех положительных элементов массива. Подсказка: используйте метод .find_all.

Элементов надо кучу
Перемножить иль сложить?
Есть .inject на этот случай,
Его бы вам употребить.

Разбиение надвое

править

Итератор .partition делит массив на две части по некоторому бинарному признаку (чётности, положительности, наличию высшего образования и тому подобным). Вот как разделить массив на две части по признаку кратности трём:

a = [1,2,3,4,5,6,7,8,9]
a.partition{ |x| (x%3).zero? } #-> [ [3,6,9], [1,2,4,5,7,8] ]

В результате работы итератора получился массив, состоящий из двух элементов-массивов. Первый элемент-массив содержит все элементы, которые удовлетворяют условию, а второй, которые не удовлетворяют. Обратите внимание, как проверяется кратность трем. Ничего не напоминает? Например, итератор .find_all? Нет? Ну и ладно!

Есть интересная хитрость, позволяющая разместить массив, полученный .partition, в две разные переменные:

a = [1,2,3,4,5,6,7,8,9]
one, two = a.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, то есть до появления логических итераторов:

maccuB = [1,2,2,3]
maccuB.inject( true ){ |result, elem|
    result && ( elem > 2 )
} #-> false
 

В примере используется так называемый механизм презумпции виновности. Переменной result присваивается значение true. Логическое умножение изменяет значение переменной result на false.

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

maccuB = [1,2,2,3]
maccuB.all?{ |elem| elem > 2 } #-> false

Несмотря на то, что код получился короче, результат остался прежним: утверждение, что все элементы массива больше двух, ложно.

Хотя бы один элемент удовлетворяет условию?

править

Вслед за квантором общности (он же — логический итератор .all?), из математической логики был перенесен и квантор существования —  . На языке Ruby он называется .any?. Чтобы оценить его по достоинству, посмотрим решение задачи без его участия. Проверим, содержит ли maccuB хотя бы один элемент больший двух:

maccuB = [1,2,2,3]
maccuB.inject( false ){ |result, elem|
    result || ( elem > 2 )
} #-> true 
 

В данном примере используется так называемый механизм презумпции невиновности. Переменной result присваивается значение false. В результате логического сложения, происходит изменение значения переменной result на true.

Теперь, тоже самое, но через логический итератор .any?:

maccuB = [1,2,2,3]
maccuB.any?{ |elem| elem > 2 } #-> true

Естественно, что с появлением логических итераторов, реализация задач математической логики (в рамках языка Ruby) стала удобней.

Хитрости

править

Вот так можно сгенерировать «хороший пароль» — произвольную последовательность из чисел или латинских букв, общей длиной в 8 символов.

cumBoJIbI = ['a'..'z','A'..'Z','0'..'9'].map{ |range| range.to_a }.flatten
puts (0...8).map{ cumBoJIbI[ rand( cumBoJIbI.size ) ] }.join

Перемешать упорядоченный массив:

maccuB = [1,2,3,4,5,6,7]
maccuB.sort_by{ rand }    #-> перемешанный массив

Выстроить элементы массива по убыванию без использования .reverse:

maccuB = [2,1,3,5,6,7,4]
maccuB.sort{ |x,y| y <=> x }    #-> [7,6,5,4,3,2,1]

Задачи про массивы

править
 

Помимо нижеследующих задач, советуем вам взглянуть на задачи из нашего сборника задач

Одномерные

править
  1. Вывести индексы массива в том порядке, в котором соответствующие им элементы образуют возрастающую последовательность.
  2. В численном массиве найти сумму отрицательных элементов.
  3. Найти все индексы, по которым располагается максимальный элемент.
  4. В массиве переставить в начало элементы, стоящие на четной позиции, а в конец — стоящие на нечётной.
Одномерные целочисленные
править
  1. Найти все элементы, большие среднего арифметического элементов.
  2. К четным элементам прибавить первый элемент, а к нечетным — последний. Первый и последний элемент не изменять.
  3. Заменить все положительные элементы на значение минимального.
  4. Найти произведение всех четных элементов массива.
  5. Найти количество минимальных элементов.
  6. Вывести индексы элементов, меньших своего левого соседа.

[Пример решения [1]]

Двумерные

править
  1. Поменять первый и последний столбец массива местами.
  2. Упорядочить N-ый столбец.
  3. Упорядочить строки, содержащие максимальный элемент.
  4. Упорядочить строки, если они не отсортированы и перемешать, если они отсортированы.
  5. Упорядочить строки массива по значению элемента главной диагонали в каждой из строк (в исходном массиве).
  6. Найти номера строк, элементы которых упорядочены по возрастанию.

[Частичное решение [2]]

Двумерные целочисленные
править
  1. Найти максимальный элемент для каждого столбца, а затем получить произведение этих элементов.
  2. Найти минимум в двумерном массиве.
  3. Найти произведение положительных элементов.
  4. Найти сумму положительных элементов, больших К.
  5. Вычислить сумму и среднее арифметическое элементов главной диагонали.
  6. Найти номера строк, все элементы которых — нули.

Подробнее об ассоциативных массивах

править

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

 

Индексные массивы чаще всего называют просто «массивами», а ассоциативные массивы — «хешами» или «словарями».

Хеши можно представить как массив пар: ключ => значение. Но в отличие от массива, хеш неупорядочен: нельзя заранее сказать, какая пара будет первой, а какая последней. Правда, удобство использования массива это шибко не умаляет. Более того, поскольку в Ruby переменные не типизированы и методам с похожей функциональностью дают похожие имена, то использование хеша чаще всего равносильно использованию массива.

Несмотря на мощь хеша, использовать его не всегда целесообразно. Бывают задачи, решаемые с хешами легко и удобно, но таких задач мало. Чаще всего хватает использования массива. Но представление о классе задач для хеша надо иметь.

Давайте создадим хеш, где в качестве ключа будем использовать целое число:

xew = {5=>3,1=>6,3=>2}
xew[ 5 ]  #-> 3
xew[ 2 ]  #-> nil      это значит, что объект отсутствует
xew[ 3 ]  #-> 2

А вот так будет выглядеть та же самая программа, если мы будем использовать массив:

maccuB = [nil, 6, nil, 2, nil, 3]
maccuB[ 5 ]  #-> 3
maccuB[ 2 ]  #-> nil
maccuB[ 3 ]  #-> 2
Первый случай применимости хеша: если в массиве намечаются обширные незаполненные (то есть заполненные nil) области, то целесообразнее использовать хеш с целочисленным индексом.

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

Продолжим поиски случаев применимости хеша и, на этот раз подсчитаем, сколько раз каждое число повторяется в данном целочисленном массиве. Решение массивом:

maccuB = [1,2,1,2,3,2,1,2,4,5]
maccuB.uniq.map{ |i| [i, maccuB.find_all{ |j| j == i }.size ] }
  #-> [[1, 3], [2, 4], [3, 1], [4, 1], [5, 1]]

Алгоритм получается ужасным. Не буду утомлять излишними терминами, а замечу, что по одному и тому же массиву итераторы (в количестве двух штук) пробегают много раз. А ведь достаточно одной «пробежки». Понятное дело, что такая программа не сделает вам чести. В качестве упражнения, предалагаю вам решить эту задачу другим, более оптимальным, способом.

Теперь рассмотрим решение этой же задачи, но с применением хеша:

maccuB = [1,2,1,2,3,2,1,2,4,5]
maccuB.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]+=i имеет в качестве результата целое число (учитывая, что массив целочисленный), а не хеш. Следовательно, параметру result автоматически будет присвоено целое число (см. описание итератора .inject). На следующей итерации мы будем обращаться к result, как к хешу, хотя там уже будет храниться число. Хорошо, если программа выдаст ошибку, а если нет? Проверьте это самостоятельно.

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

Второй случай применимости хеша: если требуется подсчитать число элементов массива, то целессобразнее применять хеш. Кстати, вместо подсчета количества, можно использовать конкатенацию массивов или строк. Но это уже более сложные задачи, которые будут рассмотрены позже.

Теперь представим, что мы работаем системными администраторами. У нас есть список DNS-имен и IP-адреса. Каждому DNS-имени соответствует только один IP-адрес. Как нам это соответствие записать в виде программы? Попробуем это сделать при помощи массива:

maccuB = [['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'
maccuB.find_all{ |key, value| key == dns_name }[0][-1]
   #-> "192.168.0.3"

В данном примере было использовано два интересных приема:

  • Если в двумерном массиве заранее известное количество столбцов (в нашем случае — два), то каждому из столбцов (в рамках любого итератора) можно дать свое имя (в нашем случае: key и value). Если бы мы такого имени не давали, то вышеописанное решение выглядело бы так:
maccuB.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". Как раз это нам и было нужно.

Теперь ту же самую задачу решим, используя хеш:

xew = {'comp1.mydomen.ru' => '192.168.0.3',
   'comp2.mydomen.ru' => '192.168.0.1', 'comp3.mydomen.ru' => '192.168.0.2'}
xew[ 'comp1.mydomen.ru' ]   #-> "192.168.0.3"

Нет ни одного итератора и следовательно, не сделано ни одной «пробежки» по массиву.

Третий случай применимости хеша: когда требуется сопоставить один набор данных с другим, то целесообразнее использовать хеш.

Вполне естественно, что существуют и другие «случаи применимости хеша», но вероятность столкнуться с ними в реальной работе намного меньше. Вышеописанных трех «случаев» должно хватить надолго.

В заключении, как и было обещанно, приводится решение задачи с использованием метода .update:

maccuB = [1,2,1,2,3,2,1,2,4,5]
maccuB.inject( {} ){ |result, i| result.update( { i=>1 } ){ |key,old,new| old+new } }
   #-> {5=>1, 1=>3, 2=>4, 3=>1, 4=>1}
   

Описание метода .update будет дано ниже. На данном этапе, попытайтесь угадать принцип работы метода .update.

Что используется в качестве ключей?

править

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


maccuB_1 = [ "а", "б" ]
maccuB_2 = [ "в", "г" ]
xew = { maccuB_1 => 100, maccuB_2 => 300 }
xew[ maccuB_1 ]        #-> 100
maccuB_1[0] = "я"
xew[ maccuB_1 ]        #-> nil
xew.rehash             #-> {["я", "б"]=>100, ["в", "г"]=>300}
xew[ maccuB_1 ]        #-> 100

В данном примере ключами хеша (xew) являются два массива (maccuB_1 и maccuB_2). Одному из них (maccuB_1) мы изменили нулевой элемент (с «а» на «я»). После этого доступ к значению был потерян. После выполнения метода .rehash все встало на свои места.

 

Как Руби отслеживает изменение ключа в ассоциативном массиве? Очень просто: с помощью метода .hash, который генерирует "контрольную сумму" объекта в виде целого числа. Например: [1,2,3].hash #-> 25

Способы создания ассоциативного массива

править

При создании ассоциативного массива важно ответить на несколько вопросов:

  • Какие данные имеются?
  • Какого типа эти данные?
  • Что будет ключом, а что значением?

Ответы определят способ создания хеша.

Из одномерного массива

править

Положим, что у нас в наличии индексный массив, где ключ и значение записаны последовательно. Тогда мы можем использовать связку методов * и Hash[]:

maccuB = [1, 4, 5, 3, 2, 2]
Hash[ *maccuB ]          #-> {5=>3, 1=>4, 2=>2}

Элементы, стоящие на нечетной позиции (в данном случае: 1, 5 и 2) стали ключами, а элементы, стоящие на четной позиции (то есть: 4, 3 и 2), стали значениями.

Из двумерного массива

править

Если повезло меньше и нам достался двумерный массив с элементами вида [["ключ_1", "значение_1"], ["ключ_2", "значение_2"], ["ключ_3", "значение_3"], ... ], то его надо сплющить (.flatten) и тем задача будет сведена к предыдущей:

maccuB = [[1,4],[5,3],[2,2]]
Hash[ *maccuB.flatten ]   #-> {5=>3, 1=>4, 2=>2}

Каждый нулевой элемент подмассива станет ключом, а каждый первый — значением.

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

[["ключ_1","ключ_2","ключ_3", ... ], ["значение_1","значение_2","значение_3", ... ]]

Вспоминаем методы работы с массивами. Там был метод .transpose (транспонирование массива), вызов которого сведет задачу к предыдущей.

maccuB = [[1,5,2],[4,3,2]]
Hash[ *maccuB.transpose.flatten ]   #-> {5=>3, 1=>4, 2=>2}

Нет данных

править

Если нет данных, то лучше записать хеш как пару фигурных скобок:

xew = {}
xew[1] = 4
xew[5] = 3
xew[2] = 2
xew        #-> {5=>3, 1=>4, 2=>2}

И уже по ходу дела разобраться, что к чему.

Известен только тип значений

править

Сведения о типе значений использовать следует так: создать хеш, в котором будет определен элемент по умолчанию. Элементом по умолчанию должен быть нулевой элемент соответствующего типа, то есть для строки это будет пустая строка (""), для массива — пустой массив ([]), а для числа — нуль (0 или 0.0). Это делается, чтобы к пустому элементу можно было что-то добавить и при этом не получить ошибку.

xew = Hash.new( "" )
xew[ "песенка про зайцев" ] += "В темносинем лесу"
xew[ "песенка про зайцев" ] += ", где трепещут осины"
xew  #-> { "песенка про зайцев"=>"В темносинем лесу, где трепещут осины" }

Или ещё пример:

xew = Hash.new( 0 )
xew[ "зарплата" ] += 60
xew[ "зарплата" ] *= 21
xew  #-> {"зарплата"=>1260}

Но как известно из любого правила есть исключение: использовать нулевой элемент, когда значение будет записываться умножением, нежелательно. Потому как, даже не будучи npopokом (Ых-хы-ыыы), можно предсказать результат. Он будет равен нулю.

Всё известно и дано

править

Если вам изначально известны все ключи и значения, то и записывайте их сразу в виде хеша:

{ "март" => 400, "январь" => 350, "февраль" => 200 }
Не изобретайте велосипед и поступайте как можно проще.

Методы работы с ассоциативными массивами

править

Когда речь пойдет о методах, которые присутствуют в ассоциативных массивах, то частенько будет возникать чувство дежавю. Во всяком случае, учить заново итераторы вам не придется. Вполне естественно, что появятся новички, но их будет немного. Тем не менее, прилежный преподаватель первым делом представляет новичков группе. Поэтому и мы начнем с тех методов, которые будут необходимы нам при работе с ассоциативными массивами, но отсутствуют у индексных.

Получение массива значений и массива ключей

править

Для получения отдельно массива ключей или значений существуют методы .keys и .values.

{1 => 4, 5 => 3, 2 => 2}.keys    #-> [1, 2, 5]
{1 => 4, 5 => 3, 2 => 2}.values  #-> [4, 3, 2]


Замена ключей на значения

править

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

xew = {"первый ключ" => 4, "второй ключ" => 5}
xew.invert #-> {4 => "первый ключ", 5 => "второй ключ"}


xew = {"первый ключ" => 10, "второй ключ" => 10}
xew.invert #-> {10 => "второй ключ"}

Небольшая хитрость: xew.invert.invert возвратит нам хеш с уникальными значениями.

Обновление пары

править

Что вы делаете, если хотите обновить какую-то программу или игру? Правильно, устанавливаете апдейт. А вот чтобы обновить значение в ассоциативном массиве используется метод .update. Пример использования этого метода в боевых условиях мы уже приводили в начале раздела. Если вы помните, то мы считали сколько раз повторяется каждое число. Наверняка, вы немного подзабыли его решение (у программистов есть привычка не помнить константы). Позволю себе его вам напомнить:

maccuB = [1,2,1,2,3,2,1,2,4,5]
maccuB.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

Счетчик итератора — это переменная в которую итератор записывает текущий элемент последовательности.

Здесь вроде бы все понятно. Запись стала менее страшной, но все равно вызывает дрожь. Будем это исправлять!

result.update( { i=>1 } ){ |key,old,new| old+new }

Раньше мы не встречались с такой записью. Но ничего страшного в ней нет. Это что-то типа поля боя. Нам выдали вооружение и необходимо провести некий маневр. В нашем случае, арсенал у нас внушительный: key, old и new. Бой начинается при некоторых условиях. Наш бой начнется, когда при добавлении очередной пары (переданной в предыдущей части страшной записи) обнаружится, что такой ключ уже есть в хеше. Нам предлагается описать наши действия именно в таком случае. Что же это за действия?

result.update( { i=>1 } ){ |key,old,new| old+new }

Всего лишь сложение old и new. Ничего не говорит? Тогда расскажу, что значат переменные key, old и new. В переменную key передается значение текущего ключа, в old — старое значение ключа (old по английски значит «старый»), а в переменную new — добавляемое значение ключа (new по английски значит «новый»).

Теперь переведем запись old+new на русский: в случае обнаружения ключа в хеше, нам необходимо сложить старое значение с новым. Если помните, то новое значение равняется единице, то есть в случае когда ключ, хранимый в i уже есть в хеше result, то к старому значению просто добавляется единица. Вот и все… а вы боялись.

 

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

Интересно, сколько читателей сможет прочитать эту строку и не зациклиться на предыдущей?

Размер ассоциативного массива

править

Ну вот, с новичками мы познакомились, теперь можно переходить к старым знакомым. Помните, как мы находили размер массива? Вот и с хешами точно также:

xew = {5=>1, 1=>3, 2=>4, 3=>1, 4=>1}
xew.size  #-> 5
 

Стоит уточнить, что если в индексных массивах под размером понимается количество элементов, то в ассоциативноя массиве это количество пар вида ключ => значение. В остальном же это наш старый добрый .size

Удаление пары по ключу

править

О том, как добавлять элементы в массив мы знаем, а вот про удаление — нет! Необходимо это исправить. Чем мы сейчас и займемся.

xew = {5=>1, 1=>3, 2=>4, 3=>1, 4=>1}
xew.delete( 5 )  #-> 1
xew              #-> {1=>3, 2=>4, 3=>1, 4=>1}
xew.delete( 5 )  #-> nil

Как вы, наверно, уже догадались, удалением пары по ключу занимается метод .delete. Ему передается ключ от пары, которую следует удалить.

 
  • Метод .delete возвращает значение, которое соответствовало ключу в удаляемой паре
  • Если в хеше отсутствует пара с передаваемым ключем, то метод .delete возвращает nil
  • Напоминаем, что nil — это символ пустоты

Удаление произвольной пары

править

Многие программисты удивляются, когда узнают, что ассоциативные массивы имеют метод .shift. Связано это удивление с тем, что у индексных массивов он удаляет первый элемент, возвращая его во время удаления. А вот как понять, какая пара является первой? И что такое первый в неупорядоченной последовательности пар?

 

Ответ кроется в отсутствии метода-напарника .pop, так как если нельзя удалить последний элемент, то под .shift понимается удаление произвольной пары. Вот такое вот нехитрое доказательство

Давайте посмотрим его в действии:

xew = {5=>3,1=>6,3=>2}
xew.shift     #-> [5,3]
xew           #-> {1=>6, 3=>2}

Обратите внимание, что метод .shift возвращает удаляемую пару в виде индексного массива [ ключ, значение ].


Однажды нерадивому студенту был задан вопрос: как упорядоченны ключи в ассоциативном массиве? На что он дал радостный ответ «по возрастанию» и получил заслуженного «гуся». Не повторяйте его ошибку! Помните, что пары в ассоциативных массивах неупорядоченны.

Преобразовать в индексный массив

править

Чуть ранее уже говорилось, что в большинстве случаев индексные массивы удобней ассоциативных.

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

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

Чтобы преобразовать ассоциативный массив в индексный, надо использовать метод to_a. Его используют все, кто не может запомнить методов работы с хешами.

xew = {"гаечный ключ" => 10, "разводной ключ" => 22}
xew.to_a  #-> [ ["гаечный ключ", 10], [разводной ключ", 22 ] ]

Способ преобразования таков. Сперва пара и преобразуется в массив:

{["гаечный ключ" => 10], ["разводной ключ" => 22]}

Затем «стрелку» заменяем на запятую

{["гаечный ключ", 10], ["разводной ключ", 22]}

и фигурные скобки выпрямляем, так что теперь их можно заправить в стэплер.

[["гаечный ключ", 10], ["разводной ключ", 22]]

Упорядочение хеша

править

Да, множество пар в хеше неупорядоченно. Но это можно исправить, разве что результат потом будет не хешем, а двумерным массивом.

xew = {"гаечный ключ" => 4, "разводной ключ" => 10}
xew.sort #-> [ ["гаечный ключ", 4], ["разводной ключ", 10] ]
 

Сначала хеш упорядочивается по ключам, а потом, в случаях равнозначных ключей, — по значениям.

В методе .sort_by передаются два значения:

xew = {"гаечный ключ" => 4, "разводной ключ" => 10}
xew.sort_by{ |key,value| value } #-> [ ["гаечный ключ", 4], ["разводной ключ", 10] ]

Здесь мы упорядочили хеш по значению.

Поиск максимальной/минимальной пары

править

Максимальная пара в хеше ищется точно также, как и максимальный элемент в массиве

xew = {"гаечный ключ" => 10, "разводной ключ" => 22}
xew.max   #-> ["разводной ключ", 22]
xew.min   #-> ["гаечный ключ", 10]

но с небольшими особенностями:

  • результат поиска — массив из двух элементов вида [ключ, значение]
  • сначала поиск происходит по ключу, а в случае совпадения ключей — по значению

Несколько больше возможностей приобрели методы .max_by и min_by


xew = {"гаечный ключ" => 10, "разводной ключ" => 22}
xew.max_by{ |key,value| value }  #-> ["разводной ключ", 22]
xew.min_by{ |array| array[0] }   #-> ["гаечный ключ", 10]

Также, как и в методе .sort_by есть возможность по разному получать текущую пару: в виде массива или двух переменных.

Логические методы

править

Работа логических методов похожа на допрос с пристрастием. Помните, как в детективах во время теста на детекторе лжи, главный герой восклицал: «Отвечать только да или нет!» Если перевести это на язык Ruby, то это будет звучать примерно так: «Отвечать только true или false!»

В детективах набор вопросов стандартен:

  • знали ли вы мистера Х?
  • вы были на месте преступления?
  • убивали ли мистера Х

На Ruby примерно тоже самое:

  • ты пустой?
  • есть ли такой элемент?
  • ты массив?
  • уверен, что не строка?

Но давайте рассмотрим их подробней.

Хеш пустой?

править

Зададим вопрос «Хеш пустой?», но используя известный нам лексикон. Для начала спросим «Пустой хеш тебе не брат-близнец

nycTou_xew = {}
noJIHbIu_xew = { "гаечный" => 20, "замочный" => "английский", "разводной" => 34 }

nycTou_xew == {}   #-> true
noJIHbIu_xew == {} #-> false

Можно спросить по другому — «Размер у тебя не нулевой?»

nycTou_xew = {}
noJIHbIu_xew = { "гаечный" => 20, "замочный" => "английский", "разводной" => 34 }

nycTou_xew.size.zero?    #-> true
noJIHbIu_xew.size.zero?  #-> false

Но давайте будем задавать правильные вопросы

nycTou_xew = {}
noJIHbIu_xew = { "гаечный" => 20, "замочный" => "английский", "разводной" => 34 }

nycTou_xew.empty?    #-> true
noJIHbIu_xew.empty?  #-> false

а то еще примут нас за приезжих…

 

Обратите внимание, что метод .empty? полностью повторяет такой же метод у индексных массивов

Есть такой ключ?

править

Если вам нужно узнать у хеша ответ на вопрос «Есть у тебя такой ключ?», но вы не знаете как это правильно спросить, то скорее всего вы зададите вопрос в два этапа: «какие ключи у тебя есть?» и «есть среди них такой ключ?»

kapmaH = { "гаечный" => 20, "замочный" => "английский", "разводной" => 34 }
kapmaH.keys.include?( "гаечный" )    #-> true

В данном примере у нас в kapmaH`e нашелся «гаечный» ключ.

Но лучше задавать вопрос напрямую, это покажет ваше прекрасное знание языка.

kapmaH = { "гаечный" => 20, "замочный" => "английский", "разводной" => 34 }
kapmaH.key?( "гаечный" )            #-> true

или в стиле индексных массивов

kapmaH = { "гаечный" => 20, "замочный" => "английский", "разводной" => 34 }
kapmaH.include?( "гаечный" )        #-> true

Это несколько сократит первоначальное предложение, но тогда можно перепутать хеш с массивом.

 

Этот же вопрос можно задать методами: .member? и .has_key?

Есть такое значение?

править

Давайте подумаем, как задать вопрос «Есть такое значение?» хешу. Скорее всего, мы опять зададим вопрос в два этапа: «какие значения есть?» и «есть ли среди них нужное нам?»

kapmaH = { "гаечный" => 20, "замочный" => "английский", "разводной" => 34 }
kapmaH.values.include?( "гаечный" )    #-> false - ой, забыл сменить
kapmaH.values.include?( "английский" ) #-> true

Но аборигены говорят иначе и задают вопрос напрямую

kapmaH = { "гаечный" => 20, "замочный" => "английский", "разводной" => 34 }
kapmaH.value?( "английский" ) #-> true
 

Задать вопрос "Есть такое значение?" можно не только при помощи метода .value?, но и при помощи более длинного .has_value?

Итераторы

править

У ассоциативных массивов есть следующие итераторы:

  • .find_all — поиск всех элементов, которые удовлетворяют логическому условию
  • .map — изменение всех элементов по некоторому алгоритму
  • .inject — сложение, перемножение и агрегация элементов массива

Набор итераторов точно такой же, как и у индексных массивов — сказывается их родство. Вот только ведут себя они несколько иначе:

  • результатом является двумерный массив (как после метода .to_a)
  • в качестве счетчика (переменной в фотографии) передается массив вида [ключ, значение]
  • можно развернуть массив вида [ключ, значение] в две переменные
  • в итераторе .inject развернуть массив не получится

Рассматривать заново работу каждого итератора в отдельности скучно. Поэтому мы будем рассматривать работу всех итераторов сразу.

xew = {"гаечный ключ" => 4, "разводной ключ" => 10}

xew.find_all{ |array| array[1] < 5 }             
  #-> [ ["гаечный ключ", 4] ]

xew.map { |array| "#{array[0]} на #{array[1]}" } 
  #-> ["гаечный ключ на 4", "разводной ключ на 10"]

xew.inject(0){ |result, array| result + array[1] }
  #-> 14

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

 

Есть подозрение, что перед работой любого из итераторов вызывается метод .to_a. Уж больно работа итераторов в хешах напоминает работу с двумерным массивом

Теперь посмотрим, как можно развернуть array в две переменные. Делается это простой заменой array на key, value

xew = {"гаечный ключ" => 4, "разводной ключ" => 10}

xew.find_all{ |key, value| value < 5 }             
  #-> [ ["гаечный ключ", 4] ]

xew.map { |key, value| "#{key} на #{value}" } 
  #-> ["гаечный ключ на 4", "разводной ключ на 10"]

xew.inject(0){ |result, key, value| result + value }
  #-> Ошибка в методе '+' невозможно сложить nil и число типа Fixnum

Обратите внимание, что развертка массива прошла успешно только в первых двух итераторах. В третьем возникла ошибка. Давайте выясним, откуда там взялся nil. Дело в том, что развернуть массив не удалось и теперь он стал называться не array, а key. Переменная value осталась «не у дел» и ей присвоилось значение nil. Чтобы это исправить, достаточно поставить круглые скобки:

xew.inject(0){ |result, (key, value)| result + value }
  #-> 14


 

Итератор .map, вызванный без аргументов, аналогичен методу .to_a: просто раскладывает хеш в двумерный массив.

Хитрости

править

Одному программисту надоело писать xew["key"] и он захотел сделать так, чтобы можно было написать xew.key.

class Hash
    def method_missing( id )
        self[ id.id2name ]
    end
end

xew = {"hello" => "привет","bye" => "пока"}
xew.hello  #-> "привет"
xew.bye    #-> "пока"

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

Задачи

править
  1. Дан массив слов. Необходимо подсчитать сколько раз встречается каждое слово в массиве.

Подробнее о строках

править
 

Строка — это упорядоченная последовательность символов, которая располагается между ограничительными символами

Строковый тип является самым популярным в любом языке программирования. Ведь без него невозможно написать любую программу (особенно учитывая, что любая программа — это строка). При выводе на экран или записи в файл, любой тип данных преобразовывается к строке (явно или неявно). Это значит, что в конечном итоге все сводится к строковому типу. Кстати, и ввод данных тоже осуществляется в виде строки (и только потом преобразовывается в другие типы).

Студенты 4-го курса МЭТТ ГАИ поступили на подготовительные курсы в МГИУ. Там им начали преподавать основы программирования на Ruby. И одна из заданных им задач была: «Дано число, необходимо поменять порядок цифр на обратный». Задача сложная, но наши студенты об этом не знали и решили ее преобразованием к строке: ucxogHoe.to_s.reverse. Преподаватели были поражены и впредь запретили им использовать преобразования к строке в своих программах. И все потому, что это сильно упрощало решение и давало студентам огромное преимущество перед остальными слушателями курсов.

Язык Ruby унаследовал работу со строками из языка Perl (признанного лидера по работе со строками). В частности такой мощный инструмент как «правила» (rules).

 

Раньше «правило» называлось «регулярным выражением», но в виду того, что на данный момент механизм «правил» вышел далеко за пределы регулярной грамматики, то решено его было переименовать (дабы не смущать умы образованной общественности)

Но наследование не подразумевает бездумного копирования. В частности, «правила», в рамках Ruby, получили объектно-ориентированную реализацию, что позволяет применять к ним различные методы. Помимо «правил», присутствует великое множество методов работы со строками. Причем некоторые из них являются нашими старыми знакомыми (+, *, [] и т. д.). Работают они несколько иначе, но некоторая параллель с массивами все же присутствует. Следует упомянуть два очень интересных момента:

  • Cтроки — это универсальный тип данных, так как в строку можно преобразовать любой другой тип данных. А также, строку можно преобразовать в любой другой тип данных (ведь изначально любой код программы — это строка).
  • Cтроки очень удобно преобразовывать в массив и обратно (методы .join и .split). Поэтому работа со строками практически такая же удобная, как и с массивами.

Способы создания строки

править

Строка создается при помощи ограничительных символов. Для этих целей чаще всего используются « (лапка) и ' (типографский апостроф/минута/одиночная кавычка). Их смысл различен. Строка в минутах гарантирует, что в ней будет содержаться текст такой же, как в коде программы, без изменений. Строка в лапках будет проходить предварительное преобразование. Будут раскрыты конструкции „вставка“ и „специальный символ“.

Зажатые в лапках работать хотят,
а строки в минутах лениво сопят.

Давайте будем называть строки в минутах „ленивыми“, а строки в лапках — „работящими“.

 

«Вставка» — это хитрая конструкция, которая вставляется между ограничительными символами (внутрь строки). Она состоит из комбинации решетки и двух ушек ( #{ 'здесь был Вася' } ). Внутри нее можно писать не только 'Здесь был Вася', но и любой программный код. Результат программного кода будет преобразован к строке и вставлен вместо «вставки»

„Вставка“ жизнью заправляет:
Код программный выполняет,
Тихо результат считает,
Вместо „вставки“ подставляет.


Специальный символ начинается с знака \ (обратная косая черта). Самые популярные из них: \n (переход на новую строку), \t (табуляция), \\ (обратная косая черта) и \» (двойная кавычка).


Для чего нужны работящие и ленивые строки?

править

Скорее всего вы будете редко вспоминать про то, что существуют работящие и ленивые строки. Тем более, что это различие действительно только на момент создания строки. Рядовой программист пользуется либо работящими, либо ленивыми строками. Давайте посмотрим, как выглядит код программиста, который использует только ленивые строки:

moe_4ucJIo = 18
mou_maccuB = [1,2,3,4]
puts 'Мое число = ' + moe_4ucJIo.to_s + ', а мой массив длины ' + mou_maccuB.size.to_s

Обратите внимание, что перед сцеплением (умные дяди называют это конкатенацией) необходимо все данные преобразовывать к строке методом .to_s. «Вставка» позволяет этого избежать. Вот как будет выглядеть та же самая программа с использованием «вставки»:

moe_4ucJIo = 18
mou_maccuB = [1,2,3,4]
puts "Мое число = #{moe_4ucJIo}, а мой массив длины #{mou_maccuB.size}"

Программа стала не только меньше, но и лучше читаться. Исчезли многочисленные сцепления.

 

Если внутри «вставки» надо создать строку, то экранировать кавычки не стоит. Внутренности вставки не являются частью строки, а значит «живут» по своим законам.

mou_maccuB = [1,2,3,4]
puts "Повторенье -- мать ученья. Мой массив = #{mou_maccuB.join(\",\")}"

Программа вызовет ошибку, так как внутри «вставки» было использовано экранирование кавычек. Правильный пример будет выглядеть так:

mou_maccuB = [1,2,3,4]
puts "Повторенье -- мать ученья. Мой массив = #{mou_maccuB.join(",")}"



Методы работы со строками

править

Методы строк умеют:

  • преобразовывать входные данные в красивый вид
  • красиво оформить выходные данные
  • дезертировать в массивы

Допустим, вы нашли максимальный элемент массива. И вам надо вывести результат на экран. Вы можете поступить вот так:

maccuB = [4, 4, 2, 5, 2, 7]
puts maccuB.max   #-> 7

Несмотря на правильность решения, вывод результата выглядит некрасиво. Гораздо профессиональней будет написать вот такую программу:

maccuB = [4, 4, 2, 5, 2, 7]
puts "Максимальный элемент maccuB'a = #{maccuB.max}"   
   #-> Максимальный элемент maccuB'a = 7

Заметили, насколько привлекательней стал вывод результата, когда мы задействовали строки?

 

Заниматься оформлением выходных данных стоит только непосредственно перед выводом результата. В остальное время, использование оформительских элементов будет только мешать

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

ucxogHoe_4ucJIo = 123
puts "В двоичном виде -> %b" % ucxogHoe_4ucJIo  
   #-> В двоичном виде -> 1111011

Мы задействовали метод % (аналог sprintf в Си), который осуществляет форматирование строки. Эту же задачу можно решить несколько иначе.

ucxogHoe_4ucJIo = 123
puts "В двоичном виде -> #{ ucxogHoe_4ucJIo.to_s(2) }"
   #-> В двоичном виде -> 1111011 

Со вторым вариантом вы могли уже встречаться в разделе, посвященном числам (подраздел Хитрости).

 

Методы sprintf и printf в Ruby также присутствуют, но по непонятным причинам используются крайне редко. Чаще всего они заменяются на методы % и puts

Арифметика строк

править

Кто бы мог подумать, но строки можно складывать, умножать и если постараться, то и делить. Естественно, что это не арифметические операции чисел, а методы со своим особенным поведением. Рассмотрим сложение, которое в строках работает как сцепление (конкатенация):

cynpyr = "Саша"
cynpyra = "Маша"
cynpyr + " плюс " + cynpyra + " = семья!" 
   #-> "Саша плюс Маша = семья!"

Вот такое вот романтическое сцепление сердец!

Переходим к умножению. Умножают строки только на целое число. Строка, которую умножают просто копируется указанное число раз. Давайте напишем речь для кукушки, чтобы она не сбилась и не накукукала нам слишком мало.

ocTaJIocb_JIeT = 100
"Ky-ky! " * ocTaJIocb_JIeT
    #-> "Ky-ky! Ky-ky! Ky-ky! Ky-ky! Ky-ky! Ky-ky! Ky-ky!Ky-ky! Ky-ky! "
    

Теперь за свое будущее можно не беспокоиться! Хотя нам может попасться неграмотная кукушка…

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

pe4b_kykywku = "Ky-ky! Ky-ky! Ky-ky! Ky-ky! Ky-ky! Ky-ky! Ky-ky! ... Ky-ky! Ky-ky! "
( pe4b_kykywku / " " ).size   #-> 100
 
  • В нашем примере метод .size считает число элементов массива, который получился в результате деления
  • Скобки нужны для того, чтобы вызвать .size от результата деления, а не от " "

Деление в примере работает также как и метод .split, о котором речь пойдет чуть позже. А чтобы оно заработало у вас, необходимо добавить небольшой код в начало программы:

class String
    alias / :split
end

Этот код как раз и говорит о том, что деление и .split — одно и тоже.

 

Деление стало нужно, когда метод * для массивов получил возможность работать, как .join (преобразовать массив в строку, расположив элементы через разделитель). В виду того, что .join и .split работают вместе точно также, как умножение и деление, то появилась идея заставить работать деление как .split

Преобразование в массив или путешествие туда и обратно

править

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


"Ky-ky".split('-')   #-> ["Ky","ky"]
"Ky-ky".split('y')   #-> ["K","-k"]

За преобразование строки в массив отвечает метод .split. Ему передается разделитель, по которому будет происходить деление строки на элементы. В нашем случае это '-'. Теперь вернем все на место. Для этого мы будем использовать метод .join из арсенала массивов:

["Ky","ky"].join('-') #-> "Ky-ky"
["Ky","ky"].join(',') #-> "Ky,ky"
["Ky","ky"].join      #-> "Kyky"
["Ky","ky"].to_s      #-> "Kyky"

 
  • Разделитель будет вставлен между элементами исходного массива
  • Метод .join вызванный без разделителя дает такой же результат, как и метод .to_s

Чуть ранее мы упоминали, что можно использовать умножение вместо .join. Давайте посмотрим, как это выглядит:

["Ky","ky"] * '-'  #-> "Ky-ky"
["Ky","ky"] * 3    #-> ["Ky","ky","Ky","ky","Ky","ky"]

Длина строки

править

Длина строки ищется точно также, как и длина массива или хеша, то есть методом .size:

"Во дворе дрова, а в дровах трава!".size #-> 33
"Три".size    #-> 3

Получение подстрок

править

Получение подстрок работает точно также, как и получение подмассива. С тем лишь отличием, что нумерация идет не по элементам, а по символам. Это логично, особенно, если учесть, что для строки элементом является символ.

cTpoka = "Во дворе дрова, а в дровах трава!"
cTpoka[27..-1]  #-> "трава!"
cTpoka[27...-1] #-> "трава"
cTpoka[9...14]  #-> "дрова"
cTpoka[9..13]   #-> "дрова"

 

Отрицательная нумерация тоже работает, то есть последний элемент имеет индекс -1, предпоследний -2 и так далее

Чтобы получить один единственный символ, необходимо получить подстроку длиной 1.

cTpoka = "Во дворе дрова, а в дровах трава!"
cTpoka[3..3] #-> "д"
cTpoka[3]    #-> 228


Если мы указываем не диапазон, а число, то метод [] выдает нам не символ, а его целочисленный код.

cTpoka = "Во дворе дрова, а в дровах трава!"
cTpoka[3]     #-> 228
cTpoka[3].chr #-> "д"
 

Для преобразования целочисленного кода обратно в символ, используется метод .chr

Строка-перевертыш

править

Иногда хочется перевернуть строку задом наперед. Причины могут быть разные. Например, вы ищите палиндром (число, которое можно перевернуть без ущерба для его значения). Занимается этим благородным делом метод .reverse. Давайте сформируем сообщение для Иных по другую сторону Сумрака:

"Ночной дозор! Всем выйти из Сумрака!".reverse
    #-> "!акармуС зи итйыв месВ !розод йончоН"

Вот примерно такой бред надо прокричать, чтобы вас поняли. Кстати, попробуйте быстро проговорить полученный текст.


Меняю шило на мыло!

править

Для того, чтобы заменить "шило" на "мыло" используется не газета «Из рук в руки», а методы .sub и .gsub:

"шило в мешке не утаишь".sub("шило","мыло")    #-> "мыло в мешке не утаишь"

Естественно, что менять можно не только шило и мыло, но и другие данные. Например, возраст. Девушка Ирина утверждает, что ей 18, но я-то знаю, что ей 26. Давайте восстановим истину (в ущерб своему здоровью):

"Ирине 18 лет".sub("18","26")   #-> "Ирине 26 лет"

Заметили, что мы используем только метод .sub? Давайте теперь рассмотрим работу метода .gsub и его отличие от .sub. На этот раз мы будем исправлять текст, авторы которого забыли про правило «ЖИ, ШИ — пиши через И»

cTpoka = "жыло-было шыбко шыпящее жывотное"
cTpoka.sub("жы","жи")   #-> "жило-было шыбко шыпящее жывотное"
cTpoka.gsub("жы","жи")  #-> "жило-было шыбко шыпяшее животное"
cTpoka.gsub("жы","жи").gsub("шы","ши") 
   #-> "жило-было шибко шипящее животное"


Одну ошибку лишь исправил
Ленивый метод .sub
Все остальные устранил
Трудолюбивый .gsub

 
  • Название метода .sub произошло от английского substring (подстрока)
  • Название метода .gsub корнями уходит в язык Perl. В нем, для осуществления всевозможных замен, использовалась конструкция s///g, где модификатор /g означал все возможные замены (от английского global — всеобщий, глобальный)

Сканируем текст на оШЫбки

править

Давайте, найдем и посчитаем оШЫбки. Искать мы будем методом .scan

cTpoka = "жыло-было шыбко шыпящее жывотное"
cTpoka.scan("шы")       #-> ["шы","шы"]
cTpoka.scan("шы").size  #-> 2
cTpoka.scan("жы").size  #-> 2
cTpoka.scan("жы").size + cTpoka.scan("шы").size  #-> 4
 

Метод .scan находит все указанные подстроки и возвращает их в виде массива строк. В данном примере, метод .size считает количество элементов массива, возвращаемого .scan

Ужас, в одном предложении целых четыре ошибки. Будем отчислять!

Нашел оШЫбку метод .scan,
В массив ее запомнил.
Учителям он свыше дан!
Зачем его я вспомнил?!

Правила работы со строками

править

Правила — это образцы к которым можно примерять строки. Правила обладают своим собственным языком, который позволяет описывать одну, две, сотню и вообще любое количество строк. Это своеобразная упаковка для множества строк в одну компактную запись.

Правила в Ruby ограничиваются символами / (наклонная черта). Примеры правил:

/(жы|шы)/
/\w+@[\w\.]+\w+/i

Страшно? А мне нет. На самом деле работа с правилами очень проста. Главное привыкнуть и попрактиковаться.

Правила состоят из:

  • Символьных классов. Перечисление символов, которые может содержать строка
  • Квантификаторов. Количество символов
  • Альтернатив. Пречисление всевозможных вариантов
  • Группировок. Возможность выделить несколько групп, которые могут обрабатываться отдельно
  • Модификаторов. Изменение поведения правила. Например, игнорирование регистра символов
 

Правила в рамках учебника будут описаны очень сжато. Многие тонкости освещены не будут, поэтому для освоения "фигур высшего пилотажа" необходимо прочитать специализированную литературу. Например, книгу "Регулярные выражения"

Символьный класс
править

Символьный класс — просто конечный набор символов. Он ограничивается квадратными скобками и содержит перечисление символов, которые можно вместо него подставить. Заменяется он всего на один символ, входящий в этот класс. Примеры символьных классов:

/[абвгде]/      #-> простое перечисление символов
/[а-яА-Я]/      #-> все русские буквы
/[0-9a-z]/      #-> цифры и строчная латиница
/[^0-9]/        #-> все символы, кроме цифр
 
  • Можно использовать - (дефис) для указания диапазонов символов
  • Если первый символ класса (идущий сразу после открывающейся квардратной скобки) ^ (крышка), то это означает символ, который отсутствует в данном классе
  • Некоторые популярные классы имеют короткую запись

Краткие записи популярных символьных классов

Короткая
запись
Полная
запись
Описание
\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] любой символ, кроме перевода строки
 

Взгляните на примеры правил! Правда, они стали понятней? По крайней мере второе...

Квантификатор
править

Квантификатор показывает сколько раз может повторяться предыдущий символ (группа, альтернатива или еще какая нечисть). Квантификатор ограничивается парой ушек (фигурными скобками). Примеры квантификаторов:

/\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(/[а-я]+/){ |cJIoBo| cJIoBo.reverse }
     #-> "заР, авд, ирт!"

Нашей переменной мы дали название cJIoBo, что вполне согласуется с нашим geJIom.

Правильный поиск

править

Вот здесь метод .scan может развернуться в полную силу. Хотите получить массив всех русских слов в тексте? Запросто:

"Раз, два, три!".scan(/[А-Яа-я]+/)
     #-> ["Раз","два","три"]

Хотите получить все знаки препинания? Нет ничего проще:

"Раз, два, три!".scan(/[,\.;:! ]+/)
     #-> [", ",", ","!"]


Например, ниже записана программа, которая занимается поиском адресов электронной почты.

cTpoka = "495-506-13 56 nata@rambler.ru(34) 1.5.1232 12.14.56 31.декабря.9999"
cTpoka.scan(/(?:[-a-z_\.])*@(?:[-a-z])*(?:\.[a-z]{2,4})+/) #-> ["nata@rambler.ru"]

Выполните ее, посмотрите результат, а потом замените любую из группировок (?:…) на (…) и снова взгляните на рузультат.

Ну со .scan должно быть все понятно. А вот то, что метод [] начинает тоже правильно искать — пока нет.

"Раз, два, три!"[/[А-Яа-я]+/] #-> "Раз"


Очень полезно использовать [] в ситуациях, когда надо узнать ответ на вопрос «Есть хотя бы одна подстрока, которая удовлетворяет правилу?» или получить первое (или единственное) совпадение с правилом.

 

Существует древнее поверье, что если использовать одно и тоже правило для .scan и .split, то получаются две части текста, из которых реально получить исходный.

TekcT.scan( npaBuJIo ) + TekcT.split( npaBuJIo ) = TekcT

Это значит, что если метод .split использует правило, описывающие все знаки припинания, то результатом будет текст без знаков припинания. А вот если это же правило будет использовать метод .scan, то в результате мы получим все знаки препинания без текста


Жадность

править

Речь пойдет о жадности среди квантификаторов. Возьмем некоторый квантификатор {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/

Методы преобразования к строке

править

Руби сам преобразует типы для некоторых простых операций. Например, при включении строки в другую он воспользуется имеющимся у объекта методом .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

Задачи

править
  1. Дана строка слов, разделёных пробелами. Вывести длиннейшее слово.
  2. Дана строка, содержащая кириллицу, латиницу и цифры. Вывести все слова, длина которых равна средней.
  3. Найти в строке первое целиком кириллическое слово.
  4. Дан текст (строка с переносами). Найти все слова, содержащие лишь три буквы «о».
    • Только для русских слов.
    • Для французских и русских слов.
    • Для любого регистра буквы «о».
  5. Найти в тексте время в формате «часы: минуты: секунды».
  6. Найти все слова без повторяющихся букв (например, «Лисп» или «Ruby», но не «Паскаль» или «Java»).
    • Только для русскоязычных слов.
    • Не учитывайте цифры в словах.
  7. Найти в тексте слова, содержащие две прописные буквы и исправить.
    • Решите задачу для слов и в кириллице, и в латинице.
  8. Найти в тексте даты формата «день.месяц.год».
    • Найдите дату, где день ограничен числом 31, а месяц 12. Год ограничивайте четырёхзначными числами.
    • Распознавайте месяц в виде «31.марта.2001».
  9. Дан текст. Найдите все URL адреса и вычлените из них только корневой домен (например, из http://ru.wikibooks.org/wiki/Ruby сделайте http://ru.wikibooks.org ).

Подробнее о методах

править

Все функции в Ruby являются методами, то есть свойственны объектам. При программировании на это можно не обращать внимания, поскольку любая программа на Руби уже является определением класса. У методов могут быть обязательные или необязательные параметры. Методы разграничиваются фигурными скобками или ключевыми словами def и end.

Создание метода

править

Благодаря тому, что указание класса-носителя метода необязательно, на Ruby можно программировать в функциональном стиле, не заботясь о создании класса-«носителя» для каждой группы методов. Метод создается с помощью ключевых слов def ... end

def sum(a, b) 
  return a + b
end
sum(10, 2) #-> 12

Ruby по умолчанию возвратит из метода результат последнего выполненного выражения, поэтому в конце метода или в условных конструкциях слово return можно опускать. Поскольку методы могут быть переопределены в процессе выполнения программы, можно «на ходу» переписать метод так:

def sum(a, b)
    a + b
end
sum(10, 2) #=> 12


Указание значений по умолчанию

править

У методов могут быть необязательные аргументы; для этого им нужно присвоить значение, которое следует применять «по умолчанию»

def sum(a, b = 5)
  a + b
end
sum(10, 2) #-> 12
sum(10) #-> 15

Методы с восклицательным и вопросительным знаком

править

В Ruby при создании методов можно применять простейшую пунктуацию. Два стандартных приема применения такой пунктуации — восклицательный и вопросительный знак в конце метода. Методы с вопросительным знаком традионно работают как предикаты, то есть возвращают true или false. Пример методов-предикатов, — методы массива. Например, в Java подобные методы начинались бы со слова is: isVolatile(), isEnabled

Обычно программист, чтобы проверить, пуст ли массив, посмотрит его длину:

arr = []
if arr.length == 0
  puts "empty"
else
  puts "not empty"
end

У массива в Ruby есть метод-предикат .empty?, возвращающий true если массив пуст, и метод-предикат .any?, возвращающий true если массив содержит хотя бы один элемент.

arr = []
if arr.empty?
  puts "empty"
else
  puts "not empty"
end

Если вы реализуете программу, которой будут пользоваться другие, считается хорошим тоном реализовывать методы-предикаты.

Еще одна их прелесть — сочетание с модификаторами выражения:

arr = [1,2 ,3]
p "Array has something" if arr.any?

Методы с восклицательным знаком на конце меняют объект, к которому привязаны.

string = "   Some string with spaces     "
string.strip! #-> "Some string with spaces" - возвращает результат операции...

string        #-> "Some string with spaces" #=> ...и меняет состояние объекта-адресата

Методы присвоения

править

Другие особые варианты пунктуации — знак равенства и арифметические знаки.

Знак равенства в конце названия метода означает, что этот метод присваивает свойству объекта значение:

class Bottle
  def capacity
    @capacity
  end
  
  def capacity=(new_cap)
    @capacity = new_cap
  end
end

bytilka = Bottle.new
bytilka.capacity = 10 #-> 10, автоматически преобразуется в вызов метода capacity=

Операторы

править

Операторы (умножение, деление, возведение в степень и так далее — вплоть до сравнения!) — тоже методы. Например:

class BeHuK
  def+(another)
   12 + another
  end
end

meTeJIka = BeHuK.new
meTeJIka + 10        #-> 22

Это применяется, например, во встроенном в Ruby объекте Time. При прибавлении к нему целого числа он возвращает новый объект Time с добавленным количеством секунд:

t = Time.now #-> Sun Jun 11 20:29:51 
t + 60             #-> Sun Jun 11 20:30:51 - на минуту позже

То же самое характерно для имеющегося в стандартной библиотеке класса Date, но, в отличие от Time, он считает дни вместо секунд

require 'date'
d = Date.today #-> Sun Jun 11 
d + 1 #-> Mon Jun 12 - на день позже

«Поглощение» аргументов метода

править

Можно «свернуть» аргументы с помощью звездочки — тогда метод получит массив в качестве аргумента

def sum(*members)
  members[0] + members[1]
end
sum(10, 2) #-> 12

Поскольку теперь наш метод принимает неограниченное количество элементов, мы можем пользоваться ими как массивом и в теле функции

def sum(*members)
  initial = 0 
  members.collect{ | item | initial += item }
  initial
end
sum(10, 2)         #-> 12 
sum(10, 2, 12, 34) #-> 58

Можно разделить аргументы на обязательные и необязательные, просто пометив последний аргумент «звездочкой». Если методу будут переданы только обязательные аргументы, в переменной «со звездочкой» в теле метода будет пустой массив.

Звездочкой полезно пользоваться и когда нужно передать методу аргументы, но не хочется указывать их по отдельности. Следуя тому же примеру:

array_to_sum = [10, 2, 12, 34]
sum(*array_to_sum) #-> 58

Подробнее о блоках

править

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

Ruby позволяет создавать анонимные методы и передавать их функциям — такие анонимные методы называются **блоками**. Очень большое количество функций Ruby основано на использовании блоков — например, итераторы (такие как each и map). Блок — это фактически «функция в функции» — программист определяет операцию, которую необходимо выполнить, но непосредственно ее выполнение осуществляет метод, которому блок передается.

Зачем они нужны

править

Блоки позволяют избавиться от очень большого количества операций, которые для каждого программиста являются привычными — а именно…

  • Поддержание индекса в цикле
  • Забота об итераторах как отдельных объектах
  • Закрытие ресурса после его использования
  • Забота о контексте, в котором выполняется операция

Как создать блок

править

Блок передается методу через конструкцию do... end или фигурные скобки. Общепринятым является использовать фигурные скобки, если вызов блока умещается на одну строку программы. Для демонстрации работы блока, мы будем использовать метод .map. Этот метод принимает блок и выполняет его строго заданное число раз.

При передаче блока методу блок следует за скобками аргументов.

 puts (1..3).map(){ "Bay!" } # выводит Bay! три раза

Поскольку при отсутствии аргументов скобки необязательны, простейшая запись такова:

puts (1..3).map{ "Bay!" } # выводит Bay! три раза

Важно помнить, что блок использует методы и переменные, указанные при его создании, то есть блок захватывает контекст, но переменные определенные в блоке остаются для него локальными!

puts (1..3).map{ word = 'Bay!'; word } #-> выводит Bay! три раза
# поскольку блок знает про переменную word и она определена в нем
puts word # вызывает сообщение об ошибке - вне блока об этой переменной ничего не известно

Как уже упоминалось, если блок многострочный целесообразней пользоваться формой с do ... end

 puts (1..3).map do
   random_number = rand()
   "Bay - случайный номер!\n"+
   random_number.to_s
 end

Блоки принимают аргументы

править

Другое замечательное свойство блоков — они, как и функции, могут принимать аргументы. В таком случае метод, которому передан блок, сам «решает», что этот блок получит в качестве аргумента. Например, уже продемонстрированный метод .map еще и передает блоку аргумент, который можно захватить следующим образом:

puts (1..3).map do | index |
  index
end

В данном случае при каждом выполнении блока переменная index будет устанавливаться на положение итератора, начиная с 1! Аргументы метода указываются после открывающей фигурной скобки или после слова do через запятую, и разграничиваются двумя вертикальми чертами. Чтобы не перепутать черту со строчной латинской L, принято «отбивать» аргументы блока от вертикальной черты пробелами.

method { | argument | .. }

Свои методы с блоками

править

Ключевое слово yield в методе открывает раздвижные двери, впускающие аргумент[ы] в блок.

def pa3_u_gBa 
 yield "u pa3"
 yield "u gBa"
end

pa3_u_gBa { | cJloBa | puts "!!! " + cJloBa }  #-> !!! u pa3
                                            #-> !!! u gBa

При этом строка будет передаваться блоку в переменную cJloBa при каждом выполнении.

Если блок обязателен, следует пометить его как последний аргумент метода и в начале аргумента добавить амперсанд:

def twice(&block)
 yield "u pa3"
 yield "u gBa"
end

twice #-> Ошибка LocalJumpError - отсутствует блок

Последнее утверждение не совсем верно. Даже совсем не верно. Указания переменной блока недостаточно для контроля наличия входного блока. Дело в том, что в случае, если блок не вызывается, то и ошибки не будет:

def func(a,&block)
 return a if a
 yield "u pa3"
 yield "u gBa"
end
func true #-> true
func false #-> LocalJumpError: no block given

Более того, вызов функции pa3_u_gBa без указания блока также приведет к ошибке. Таким образом, гораздо лучше вместо введения обязательного параметра задавать блок по-умолчанию:

def func(a,&block)
 return a if a
 block ||= lambda{ | cJloBa | puts "!!! " + cJloBa }
 block.call("u pa3")
 block.call("u gBa")
end

func true #-> true
func false #-> !!! u pa3
         #-> !!! u gBa

func(false) { | cJloBa | puts "??? " + cJloBa } #-> ??? u pa3
                                        #-> ??? u gBa

Здесь lambda — пустая функция, а block.call — явный способ вызова блока кода на выполнение.

Блок можно также передать другому методу, просто указав его как последний аргумент с амперсандом:

def writing_to(file, &block)
 File.open(file, 'w', &block)
end

Некоторые применения блоков

править

Блоки — одна из главных особенностей Ruby. Уметь ими пользоваться — ключ к очень коротким и очень понятным программам, делающим очень много.

Типичное применение блока — когда после выполнений некой операции нужно «вынести мусор»: закрыть открытый ресурс или отсоединиться от сети. Предположим, что мы пишем метод для интернет-системы. При этом мы хотим выполнить несколько операций, но чтобы их выполнить, нужно подключить пользователя к Сети. После того, как операции завершились, надо его так же незаметно отключить.

connected { download_email }

В данном случае мы пишем только блок с download_email, все заботы по открытию (а главное — закрытию) соединения возьмет на себя метод connected:

def connected
  connect_to_internet
  result = yield
  disconnect
  result
end

В данном случае мы сохраняем то, что вернул блок в метод, закрываем соединение и возвращаем результат блока как свой собственный. Чаще всего о методах, принимающих блоки, можно говорить как о деепричастном обороте — например «соединившись», «внутри_транзакции», «с файлом», «трижды».

Если воспользоваться встроенной проверкой исключений, то метод принимает такой вид:

def connected
  connect_to_internet
  begin
    result = yield
  ensure
    disconnect
  end
  result
end

Тогда даже если метод вызовет ошибку, соединение все равно будет закрыто.

Методы, которых не было

править

Экспериментально замечено, что во время сессии у студентов в разы повышается способность к изобретениям различного рода. Иногда, удается направить эту энергию в мирное русло: некоторые студенты во время сдачи зачета начинают придумывать свои методы. Естественно, что «придуманные методы» они реализовать не могут, но с этим замечательно справляются их преподаватели. Некоторым методам даже дают имена студентов, которые приложили свое незнание к их созданию. Многие из таких методов включают в последующие версии языка.

Ширяевский .size

править

Студент МЭТТ Ширяев Денис, на одном из зачетов предложил использовать метод .size в качестве итератора. Он использовал его для подсчета количества элементов массива, удовлетворяющих условию. По сути, он предложил укоротить связку .find_all{ ... }.size. Вот как будет выглядеть программа подсчета количества четных элементов массива:

maccuB = [1,2,3,4,5,6]
maccuB.size{ |i| (i%2).zero? }   #-> 3

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

class Array
   def size( &block )
       block ? inject( 0 ){ |count,elem| (yield elem ) ? count + 1 : count } : length
   end
end

Метод реализован только для массивов, но возможно его добавление к хешам или строкам.

Случайное число из диапазона

править

Студенты часто возмущаются, почему чтобы получить случайное число от 3 до 6 нужно писать нечто невнятное вида:

3 + rand( 4 )

Откуда чего берется? Почему нельзя написать проще? Например вот так:

(3..6).rand

Действительно, почему? Давайте добавим такую функциональность к классу Range:

class Range
    def rand
        first + Kernel.rand( last - first  + ( exclude_end? ? 0 : 1 ) )
    end
end

Для проверки можно выполнить следующий код:

p Array.new(100){ (3..6).rand }.uniq.sort   #-> [3,4,5,6]

Что и требовалось реализовать! Кстати, данная реализация имеет один изъян: для строковых диапазонов, метод Range#rand будет выдавать ошибку. Решается проблема достаточно просто. Надо реализовать Array#rand (получение случайного элемента массива), а внутри Range#rand вызывать связку .to_a.rand. Теперь тоже самое, но на Ruby:

class Array
    def rand
        self[ Kernel.rand( size ) ]
    end
end

class Range
    def rand
        to_a.rand
    end
end

Для проверки выполним следующий код:

p Array.new(100){ ('a'..'c').rand }.uniq.sort  #-> ["a","b","c"]

Странно, но видимо все работает!

Способы расширения библиотеки методов

править

Как добавить метод к массиву/строке/венику?

править

Важно помнить, что в Ruby все типы являются объектами, даже сами классы. Каждый класс до конца выполнения программы остается открытым, а это значит, что в любой тип можно добавить собственные методы (или изменить поведение существующих). Каждый класс можно определять постепенно, в нескольких частях программы:

class BeHuK
  def mecTu
  end
end

BeHuK.instance_methods #-> [..., "mecTu", ...]

class BeHuK
  def noMblTb_B_yHuta3e(yHuTa3)
  end
end

BeHuK.instance_methods #-> [..., "mecTu", ..., "noMblTb_B_yHuta3e", ...]


Добавленные методы становятся доступны немедленно, в том числе для уже созданнных экземпляров типа. Стоит помнить, что методы в Ruby — на самом деле «сообщения», и у каждого метода есть «приемник», то есть объект, которому сообщение отправлено. Метод по умолчанию ищет другие методы в экземпляре класса, поскольку приемником для него является self.

Простейший пример — добавление метода классу String, выводящий только согласные буквы из строки:

 str = "Crazy fox jumps over a lazy dog"
 class String
   def consonants
     cons = []
     self.scan /[BCDFGHJKLMNPRSTbcdfghjklmnprst]/ do | m |
       cons << m
     end
     cons.uniq.join
   end
 end
 
 str.consonants #-> "Crfjmpsldg"


У объектов в Ruby есть методы класса и методы экземпляра — в нашем примере consonants — это именно методы экземпляра. При создании нового класса или изменении существующего создать метод класса можно, начав его имя с имени класса или с self и точки:

 str = "Crazy fox jumps over a lazy dog"
 class String
   def self.consonants_from(str)
     cons = []
     str.scan /[BCDFGHJKLMNPRSTVWZbcdfghjklmnprstvwz]/ do | m |
       cons << m
     end
     cons.uniq.join
   end
 end
 
 String.consonants_from(str) #-> "Crfjmpsvlzdg"

Одним из специфических свойств Ruby является то, что классы сами по себе — экземпляры класса Class, и с ними можно работать как с обычными объектами. Специальный синтаксис для доступа к методам класса в Ruby не нужен. Классы можно хранить в переменных, передавать методам и так далее.


Проиллюстрируем это простым примером. Как мы знаем, у класса File есть метод open. Создадим метод у класса File, дающий нам доступ к временному файлу, создаваемому в момент выполнения кода. Это такой же метод, но открывающий только файлы из директории /tmp:

class File
  def self.temporary(&block)
    # определим директорию в которой в данный момент запущена программа
    # методы dirname и expand_path в данном случае - File.dirname и File.expand_path
    my_dir = self.dirname(self.expand_path(__FILE__))
    base = basename(__FILE__, '.rb') # -> имя файла с программой без расширения .rb
    stamp = "#{base}_#{Time.now.to_i}.tmp" # -> системное время в секундах и расширение tmp

    # File.join соединит фрагменты пути обратным слешем в Windows и прямым слешем на UNIX
    path = join(my_dir, stamp)
    self.open(path, 'w', &block)
  end
end

File.temporary { |f| f << "Some info" } # #<File:/Tests/(irb)_1151198720.tmp (closed)>
 

Для управления временными файлами в Руби существует класс Tempfile - помимо других достоинств он гарантирует, что созданные временные файлы по завершении программы будут удалены. Так что даже в этом случае велосипед изобретать не стоит!

Если к классу надо добавить много методов сразу, то при описании класса можно выйти на уровень его объекта-класса. Это свойство в Ruby называется eigenclass (нем. eigen = «свой, особый»). Подозревая, что многие из читателей незнакомы с математическим понятием собственного значения/вектора/пространства, мы кратко и по-программистски назовём eigenclass эйгенклассом.

Добавим к классу File метод myself, который дает быстрый доступ к текущему файлу с исходным кодом:

class Manager
  class << self
    def create
     ...
    end
     
    def manage
     ...
    end
  end
end

Если нужно добавить метод только к конкретному экземпляру, нужно выйти на его эйгенкласс:

str = "Crazy fox jumps over a lazy dog"
other_string = "Three black witches"

def str.vowels
    vowels = []
    scan /[AEIOUYaeiuoy]/ do | m |
      vowels << m
    end
    vowels.uniq.join
  end
end

str.vowels #-> "ayoue"
other_string.vowels #-> NoMethodError: у этой строки нету метода `vowels'...

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

Программист-разрушитель

править

Как ни странно, изредка программисту приходится взять на себя позицию разрушителя — удалить существующий метод или константу. Метод undef позволяет сделать это:

class BeHuK
  def mecTu
    "МетёмЪ!"
  end
end

class BeHuk_6epe3oBblu < BeHuK
  def xJlecTaTb(cnuHa)
  end
  def noMo4uTb_B_Ta3uke(ta3uk)
  end
  undef mecTu
end

venik = BeHuK.new
beriozovy_venik = BeHuk_6epe3oBblu.new

venik.mecTu #-> "МетёмЪ!"
beriozovy_venik.mecTu #-> Ошибка NoMethodError - такого метода нет, хоть он и был унаследован

Уничтожение класса несколько сложнее, но тоже возможно

Object.send(:remove_const, :BeHuK)

После этого BeHuK будет существовать только для объекта-экземпляра

class BeHuK
end

meTeJIka = BeHuK.new

Object.send(:remove_const, :BeHuK)

BeHuK # => Ошибка NameError: неизвестная константа BeHuK

meTeJIka.class #=> BeHuK, все еще существует для экземпляра

Это свойство Ruby крайне полезно, если нужно создать класс, наследующий от другого, но при этом имеющий другого родителя. Например:

# в чужой программе...
class Connection < Socket
  # ... много-много методов connection...#
end

conn = Connection.new()

# в нашей программе...
Object.send(:remove_const, :Connection)

class Connection < EncryptedSocket
  # ... такие-же методы как у connection, но работающие с шифрованным соединением...#
end

# в итоге чужая программа будет использовать созданный нами Connection!


История из жизни

При разработке своего Rails-приложения мной применялся класс OrderedHash, который работает как стандартный хеш, но при этом имеет упорядоченные ключи. Это позволяет, к примеру, удобно сгруппировать новости по датам, сохраняя порядок дат. В какой-то момент моя программа перестала работать. Почему? В Rails был, для внутренних нужд, добавлен другой класс OrderedHash, но при этом он не соответствовал моему (и даже не соответствовал обычному Hash — некоторых методов в нем просто не было! Благодаря remove_const мне удалось просто выгрузить их класс и заменить его своим. А тесты в комплекте чужой библиотеки позволили удостовериться, что я ничего не испортил и она с моим «внедренным» классом функционирует нормально.

Julik 01:52, 25 июня 2006

Как написать свой итератор?

править

Как написать свой класс?

править

Наследовать или перемешать?

править

Как сделать свою библиотеку методов?

править

Матрицы и вектора

править

Давно хотел написать историю про то, как использовать матрицы и вектора в Ruby, но руки дошли только сейчас. Мое знакомство с реализацией матричного исчисления в Ruby началось еще в мою бытность студентом МГИУ. И надо сказать, что это знакомство было неудачным. Дело в том, что простейшая программа вычисления определителя матрицы

require 'matrix'
p Matrix[[1,-2,3],[3,4,-5],[2,4,1]].det   #-> -50

давала неверный результат (правильный ответ = 62). Как выяснилось позднее, эта проблема связана со спецификой целочисленной арифметики в Ruby (<одна вторая в Руби — нуль>). Предположив это, я решил, что проблема легко решится, если я преобразую элементы матрицы к дробному типу (классу Float):

require 'matrix'
p Matrix[[1.0,-2.0,3.0],[3.0,4.0,-5.0],[2.0,4.0,1.0]].det   #-> 62.0

И в некоторых случаях это меня действительно выручало. Но не всегда… стоило появиться делению на 3, как появлялись ненужные остатки и погрешности. И чем больше исходная матрица, тем больше вероятность появления таких остатков. В некоторых случаях это было несущественным, а в остальных приходилось прибегать к услугам специальных математических пакетов (например,Maxima). Было жутко неудобно (я тогда писал курсовой, который решал все варианты контрольной работы по предмету <Математическое программирование>. Да простит меня преподаватель, который так и не понял секрета тотальной успеваемости группы) и обидно за такую «кривую реализацию».

На этом бы история и закончилась (как позже я узнал, на этом она заканчивалась для многих), но мне в руки попалась книга Programming Ruby 2ed с описанием возможностей стандартной библиотеки версии 1.8.2. Именно там (на стр. 671) я наткнулся на описание библиотеки mathn. Уникальность ее состоит в том, что она существенно расширяет возможности стандартных чисел, добавляя к ним рациональные числа (класс Rational) и комплексные числа (класс Complex).

 

Проще говоря, появляется возможность делить числа без погрешностей (класс Rational) и возможность извлекать квадратный корень из отрицательного числа (класс Complex)

Одновременно с этим она добавляет матричное и векторное исчисления (правда, почему-то в книге дополнительно подключали complex и matrix). И после этого матричное счисление начинает работать «из коробки» и еще как работать!!! Хотите обратую матрицу? Пожалуйста! Хотите определитель? Нет ничего проще! Обидно только одно… программу к тому времени я уже написал и эти возможности мне были не нужны.

Чуть позднее, один из моих студентов написал мне письмо с просьбой объяснить как <работать с матрицами в Руби>? При этом он задал всего три вопроса:

  1. Как создать новую матрицу?
  2. Как послать в матрицу 2-мерный массив?
  3. Как поменять элемент матрицы?

Для того, чтобы начать наше знакомство с матрицами, я отвечу сперва на них.

Как создать новую матрицу?

править

Самый простейший способ создать матрицу — использовать метод <батарейка> (метод [] выглядит как индикатор заряда батареи на сотовом телефоне):

require 'mathn'
Matrix[[1, -2, 3], [3, 4, -5], [2, 4, 1]]

Этот способ создания мы уже с вами видели раньше. Обратите внимание, что для использования матриц необходимо подключать библиотеку mathn.

Как послать в матрицу 2-мерный массив?

править

Немножко перефразируем вопрос: <как преобразовать двумерный массив в матрицу>? Пусть ответ будет неожиданным, но это делается при помощи того-же метода <батарейка>:

require 'mathn'
maccuB = [[1, -2, 3], [3, 4, -5], [2, 4, 1]]
Matrix[ *maccuB ]

Оператор * преобразует maccuB в список параметров, что позволяет существенно упросить процесс создания матрицы. Более того, это единственно удобный метод ее создания. Все остальные методы создания матрицы (например, метод .new) работают не столь изящно и интуитивно.

Как изменить элемент матрицы?

править

И вот тут всплывает <недоделанность> матриц. Метода для изменения элемента матрицы в них нет! Для того, чтобы изменить элемент матрицы, надо преобразовать матрицу в массив, изменить элемент массива и преобразовать массив в матрицу. Примерно вот так (меняем элемент с индексом 0,0):

require 'mathn'
maTpuLLa =  Matrix[[1,-2,3],[3,4,-5],[2,4,1]]
maccuB = maTpuLLa.to_a
maccuB[0][0] = 0
p Matrix[ *maccuB ] #-> Matrix[[0, -2, 3], [3, 4, -5], [2, 4, 1]]

Исправляем оплошность разработчиков

править

Для начала, рассматриваем поближе библиотеку matrix (исходник или описание в fxri) и выясняем, что для получения значения элемента используется метод <батарейка> с двумя аргументами. Вполне закономерно использовать метод <батарейка равно> (то есть []=) для изменения элемента. Сейчас мы его и реализуем:

require 'mathn'
class Matrix
  def []=( i, j, value )
    @rows[i][j] = value
  end
end

maTpuLLa =  Matrix[[1,-2,3],[3,4,-5],[2,4,1]]
maTpuLLa[0,0] = 5
p maTpuLLa  #-> Matrix[[5, -2, 3], [3, 4, -5], [2, 4, 1]]

Ну вот как-то примерно так… Почему не могли этого сделать разработчики, я так и не понял. Скорее всего по идеологическим соображениям (<не дело, чтобы матрицы вели себя как простые массивы>).

Методы работы с матрицами

править

Из методов работы с матрицами наиболее важные это: вычисление определителя (.det), вычисление обратной матрицы (.inv), поиск минора матрицы (.minor), преобразование матрицы в массив (.to_a), умножение (*), сложение (+), вычитание (-) и деление (/) матриц. Без остальных методов можно обойтись. Поэтому изучите их самостоятельно.

Зачем нужны вектора?

править

Во-первых, для Ruby вектор — это объект класса Vector. Подключается он одновременно с матрицами (класс Matrix). Во-вторых, очень похож на массив, но с одним существенным отличием (cобственно это отличие и определяет полезность вектора): массивы и вектора по разному складываются и вычитаются. Давайте рассмотрим небольшой пример:

require 'mathn'
maccuB = [1,2,3,4,5]
BekTop = Vector[ *maccuB ]
p BekTop + BekTop   #-> Vector[2, 4, 6, 8, 10]
p maccuB + maccuB   #-> [1, 2, 3, 4, 5, 1, 2, 3, 4, 5]
 

Обратите внимание, что BekTop создается точно также, как и матрица. По сути, вектор - это матрица, которая состоит лишь из одной строки. А матрица, в свою очередь - это массив векторов

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

Хитрости

править

Задачи

править

Решение систем линейных уравнений методом Гаусса

править

Как я уже рассказывал раньше, первая моя программа на Ruby (декабрь 2002 года) — реализация симплексного алгоритма для решения задач оптимизации. Весь фокус в том, что я эту программу нашел! Это 11 Кбайт и 363 строчки (140 + 223) программного кода. Кстати, если найду подходящее для нее место, то обязательно надо будет опубликовать.

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

require 'mathn'
ypaBHeHue = [Vector[1,2,1,1],Vector[1,5,6,2],Vector[1,5,7,10]]

Почему был выбран именно массив векторов, а не матрица или двумерный массив? Дело в том, что в методе Гаусса приходится выполнять такие векторные операции, как: вычитание векторов и деление вектора на скаляр. Поэтому смысла создавать матрицу (векторных операций не предусмотрено) или двумерный массив (придется реализовывать эти операции) — нет! Кстати, вполне возможно создать массив векторов и из двумерного массива (что мы и сделаем в следующем примере). Итак, приступим к реализации прямого хода метода Гаусса:

require 'mathn'
ypaBHeHue = [[1,2,1,1],[1,5,6,2],[1,5,7,10]].map{ |array| Vector[ *array ] }
# Прямой ход метода Гаусса  
ypaBHeHue[0] /= ypaBHeHue[0][0]

И вот тут нас ждет неприятный сюрприз: у векторов не реализован метод /. Смотрим в файл matrix.rb (который мирно лежит в директории со стандартными библиотеками). Действительно нет! Метод * (умножить) есть, а разделить — нет. Ну и ладно, мы — программисты не гордые… сами напишем!

require 'mathn'

class Vector
  def /( arg )
    self * (1/arg)
  end
end

ypaBHeHue = [[1,2,1,1],[1,5,6,2],[1,5,7,10]].map{ |array| Vector[ *array ] }
# Прямой ход метода Гаусса  
ypaBHeHue[0] /= ypaBHeHue[0][0]

Во! Теперь работает (только со скалярами, но нам больше и не надо). Заканчиваем реализовывать прямой проход метода Гаусса:

require 'mathn'

class Vector
  def /( arg )
    self * (1/arg)
  end
end

ypaBHeHue = [[1,2,1,1],[1,5,6,2],[1,5,7,10]].map{ |array| Vector[ *array ] }
# Прямой ход метода Гаусса  
ypaBHeHue[0] /= ypaBHeHue[0][0]
ypaBHeHue[1] -= ypaBHeHue[0]*ypaBHeHue[1][0]
ypaBHeHue[2] -= ypaBHeHue[0]*ypaBHeHue[2][0]

ypaBHeHue[1] /= ypaBHeHue[1][1]
ypaBHeHue[2] -= ypaBHeHue[1]*ypaBHeHue[2][1]

ypaBHeHue[2] /= ypaBHeHue[2][2]

p ypaBHeHue  #-> [Vector[1,2,1,1], Vector[0,1,5/3,1/3], Vector[0,0,1,8]]

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

require 'mathn'

class Vector
  def /( arg )
    self * (1/arg)
  end
end

ypaBHeHue = [[1,2,1,1],[1,5,6,2],[1,5,7,10]].map{ |array| Vector[ *array ] }
# Прямой ход метода Гаусса  
ypaBHeHue[0] /= ypaBHeHue[0][0]
ypaBHeHue[1] -= ypaBHeHue[0]*ypaBHeHue[1][0]
ypaBHeHue[2] -= ypaBHeHue[0]*ypaBHeHue[2][0]

ypaBHeHue[1] /= ypaBHeHue[1][1]
ypaBHeHue[2] -= ypaBHeHue[1]*ypaBHeHue[2][1]

ypaBHeHue[2] /= ypaBHeHue[2][2]

# Обратный ход метода Гаусса
ypaBHeHue[1] -= ypaBHeHue[2]*ypaBHeHue[1][2]
ypaBHeHue[0] -= ypaBHeHue[2]*ypaBHeHue[0][2]

ypaBHeHue[0] -= ypaBHeHue[1]*ypaBHeHue[0][1]

p ypaBHeHue  #-> [Vector[1,0,0,19], Vector[0,1,0,-13], Vector[0,0,1,8]]

Ну вот и все… вроде как решение получили. Но было бы замечательно, если бы выводилось не все уравнение, а только столбец свободных членов. Задачка простенькая, но важная. Давайте ее решим:

p ypaBHeHue.map{ |vector| vector.to_a }.transpose[-1]  #-> [19,-13,8]

Теперь задачу можно считать решенной! Жаль только, что программа работает только для уравнения 3х4. Надо бы добавить несколько итераторов, чтобы они самостоятельно определяли размерность уравнения. Для этого нужно проследить чередование индексов и записать для них итераторы (хоть я и недолюбливаю .each, но для данного случая он пришелся как нельзя кстати). Расписывать преобразование я не буду, так как не вижу в этом большой надобности. Поэтому, смотрите сразу то, что у меня получилось:

require 'mathn'

class Vector
  def /( arg )
    self * (1/arg)
  end
end

ypaBHeHue = Matrix[[1,2,1,1],[1,5,6,2],[1,5,7,10]].row_vectors

# Прямой ход метода Гаусса
(0...ypaBHeHue.size).each{ |i|
  ypaBHeHue[i] /= ypaBHeHue[i][i]
  (i+1...ypaBHeHue.size).each{ |j| ypaBHeHue[j] -= ypaBHeHue[i]*ypaBHeHue[j][i] }
}

# Обратный ход метода Гаусса
(1...ypaBHeHue.size).to_a.reverse.each{ |i|
  (0...i).each{ |j| ypaBHeHue[j] -= ypaBHeHue[i]*ypaBHeHue[j][i] }
}

p ypaBHeHue.map{ |vector| vector[-1] }  #-> [19,-13,8]

Обратите внимание, что ypaBHeHue задается через матрицу (которая, не без помощи метода .row_vectors, преобразуется в массив векторов). Также обратите внимание, что получить последний столбец можно посредством итератора .map и метода «батарейка».

Кстати, реализация симплексного алгоритма подразумевает еще несколько интересных задач:

  1. Если какой либо элемент главной диагонали нулевой, то необходимо переставить строки и/или столбцы, чтобы это исправить.
  2. Преобразование целевой функции и системы уравнений из текстового формата (2*x1-x2-x4 > min и x1-2*x2-2*x3-x4 <= 10) в YAML
  3. Первичная обработка целевой функции (если она стремится к min, то необходимо умножить вектор целевой функции на −1) и уравнений системы (если равенство не строгое, то преобразовать его в строгое, путем добавления новой неизвестной: x1-2*x2-2*x3-x4 <= 10 > x1-2*x2-2*x3-x4+x5 = 10).
  4. Поиск ведущего элемента.
  5. Смена ведущего элемента.
  6. Вывод всего уравнения в читабельном виде. Лучше всего в виде таблицы.

Буду рад, если кто-то опубликует их решение.

Работа с файлами

править

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

 

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

 

В своих операционных системах фирма Microsoft ввела понятие "двоичный файл", но оно порождает больше проблем, чем удобств. Особенно при создании кроссплатформенных приложений

Как организована работа с файлами? В самом общем случае работа с файлами состоит из следующих этапов:

  1. Открытие файла. Сущность этого этапа состоит в создании объекта класса File.
  2. Запись или чтение. Вызываются привычные нам методы вывода на экран и не совсем привычные — ввода.
  3. Закрытие файла. Во время закрытия файла происходят действия с файловой системой. С объектом, который создается при открытии файла ничего не происходит, но после этого он указывает на закрытый файл и производить операции чтения/записи при помощи него уже нельзя.

Существует масса способов реализации работы с файлами:

  1. Чтение при помощи класса IO. Класс IO имеет множество методов, которые позволяют производить чтение из текстовых файлов (с «двоичными файлами» лучше так не работать). Если нужно считать весь текстовый файл, то лучше пользоваться методами класса IO.
  2. Перенаправление потоков. Существует три предопределенных переменных: $stdio, $stdin и $stderr. Если им присвоить объект класса File (создаваемый во время открытия файла), то весь вывод пойдет в файл, который присвоили переменной $stdout. Весь ввод будет браться из файла, который присволи переменной $stdin, а все ошибки будут сохраняться в файле, который присвоили переменной $stderr. Если нужно работать только с одним файлом на чтение и одним файлом на запись, то обычно используют этот способ. Также очень удобно использовать перенаправление потока ошибок (переменная $stderr) для программ, которые работают в составе пакетных файлов и используют только интерфейс командной строки.
  3. Универсальный способ. Используется в ситуациях, когда нельзя использовать предыдущие два способа.

Подведем небольшой итог:

  1. Если нужно считать весь файл целиком, то надо использовать методы класса IO.
  2. Если нужно работать только с одним файлом на чтение и только одиним файлом на запись, то надо использовать перенаправление потока.
  3. Если нельзя применить два вышеперечисленных способа, то надо использовать универсальный способ работы с файлами.

Чтение при помощи класса IO

править

Для чтения файла целиком используется метод .read. Он считывает весь файл в виде строки. Во время его использования не стоит задумывать об открытии/закрытии файла, так как эти операции скрыты внутри метода.

config = IO.read('config.yaml')
config.class   #-> String


В примере можно увидеть, как считывается файл config.yaml в переменную config. Вторая строчка примера показывает, что, при использовании метода .read, файл считывается в виде строки (класс String). Теперь к переменной config можно применять любые методы из богатого строкового арсенала.

В качестве упражнения предлагаю вам считывать и выводить на экран любые файлы, которые вы только найдете у себя на диске.


Перенаправление потока

править

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

$stdin = File.open('входные данные.txt')

После этого, он сильно бьет себя по лбу и выкрикивает нечто вроде «Я идиот!» И все от осознания того, что процесс откладки давно можно было упростить таким способом.

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

$stdout = File.open('выходные данные.txt','w')
$stderr = File.open('сообщения об ошибках.txt','a')
 

Вторым параметром метода .open передается модификатор доступа, то есть кодовое слово по которому метод .open может предположить то, что вы будете делать с этим файлом. В нашем примере мы использовали модификатор w (от англ. write - писать), который говорит о том, что мы будем только писать в файл. Причем каждый раз файл будет перезаписываться. При помощи модификатора a (от англ. append - добавлять) мы указали, что мы будем добавлять данные в файл, а не перезаписывать, как в случае с w

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

$stdout = File.open('выходные данные.txt','w')
$stderr = File.open('сообщения об ошибках.txt','a')

puts 'Очень важные данные'
puts 'которые будут сохранены в файл'
raise 'Принудительно вызываем ошибку'
 

Метод raise — для принудительного вызова ошибки.

Теперь можете смело экспериментировать! Для начала, попробуйте поменять модификаторы w и a местами.

Универсальный способ работы с файлами

править

Универсальным способом я назвал способ с использованием метода File.open. Дело в том, что при помощи него можно осуществлять не только считывание, запись и перезапись, но и их закрытие файлов (чего нельзя сделать при использовании способа с переменными $stdout, $stdin и $stderr). Это позволяет несколько раз (за время выполнения программы) осуществлять операции открытия-закрытия файла. В виду того, что эта возможность нужна далеко не всегда, то и используется этот способ только тогда, когда использование всех предыдущих невозможно. Чтение из файла 'входные данные.txt' при помощи универсального метода будет выглядеть следующим образом:

cTpoka = File.open('входные данные.txt','r'){ |file| file.read }

Модификатор доступа ('r') указывать необязательно, так как он устанавливается по умолчанию. Поэтому следующий код тоже верен:

cTpoka = File.open('входные данные.txt'){ |file| file.read }

Если необходимо записать данные, то нужно использовать модификатор доступа 'a' (добавление к концу файла) или 'w' (добавление в файл с его предварительной очисткой). Само собой разумеется, что запись данных в файл осуществляется методами puts, write и т. д.

File.open('выходные данные.txt','w'){ |file| file.write cTpoka }
File.open('выходные данные.txt','a'){ |file| file.puts cTpoka }

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

Хитрости

править

Задачи

править

Как написать троян?

править

Однажды, один из студентов (Geka) попросил меня рассказать о том, как создать простейшее клиент-серверное приложение на Ruby. Перед тем, как подойти ко мне он уже облазил несколько форумов, но у него все равно осталось много вопросов. Отчаявшись он наконец подошел ко мне…

В ходе противостояния греков и троянцев была разработана и впервые реализована операция по дезинформации противника. Греки построили огромного деревянного коня, в котором разместили небольшое войско, и поставили его под ворота Трои. В итоге сооружение было перемещено в город и греки одержали победу. Концепция «троянского коня» оказалась настолько действенной, что до сих пор используется всеми разведывательными службами мира. Кроме того, этот метод широко используется хакерами в целях получения нужной информации о своих «жертвах».

Построение серверной части

править

На руках у него уже была серверная часть программы, которая позволяла манипулировать удаленной файловой системой (как потом оказалось, ему ее презентовал Invalid). Давайте на нее посмотрим…

require 'socket'
server = TCPServer.new('localhost', 3000)
while (srv = server.accept)
  str = srv.gets.chomp.split(' ')
  cmd = str[0]
  arg = str[1]
  case cmd
    when ".."
        Dir.chdir("..")
        srv.print "OK!"
    when "ls"
        srv.print Dir.entries(".").join("\n")
    when "cd"
        begin
            Dir.chdir( arg )
            srv.print "OK!"
        rescue
            srv.print "No such file or directory - #{ arg }"
        end
    when "md"
        Dir.mkdir( arg )
        srv.print "OK!" 
    when "rmd"
        begin
            Dir.rmdir( arg )
            srv.print "OK!"
        rescue
            srv.print "No such file or directory - #{ arg }"
        end
    when "shutdown"
        break
    else
        srv.print "Bad Command!"
  end  
  srv.close
end

Программка конечно хорошая, но сдается мне, что она не работает. Первое на что следует обратить внимание — это закрытие соединения после выполнения каждой команды. При работе с этим сервером через telnet будут возникать определенные проблемы. Давайте предельно упростим специализированный код этой программы и посмотрим на структуру клиент-серверного приложения.

require 'socket'
TCPServer.open('localhost', 3000){ |server|
    if (session = server.accept)
        session.print "Do6po no}i{aJIoBaTb Ha cepBep\r\n"
        session.print "BbI mo}i{eTe Ha6paTb komaHgbI: ls <dir> | cd <dir> | shutdown\r\n"
        loop{
            cmd, arg = *session.gets.chomp.split
            case cmd
                when "ls"
                    begin
                        session.print Dir[ arg || "*" ].map{ |str| str + "\r\n" }
                    rescue
                        session.print "HeT Takoro qpau'JIa uJIu gupekTopuu - #{ arg.inspect }\r\n"
                    end
                when "cd"
                    begin
                        Dir.chdir( arg )
                        session.print "OK!\r\n"
                    rescue
                        session.print "HeT Takoro qpau'JIa uJIu gupekTopuu - #{ arg.inspect }\r\n"
                    end
                when "shutdown"
                    session.close
                    break
                else
                    session.print "HeBepHa9 komaHga!\r\n"
            end
        }
    end
}
 

Для того, чтобы соединиться с этим сервером, необходимо выполнить команду telnet и набрать o localhost 3000. После успешного соединения можете набирать команды ls, cd или shutdown

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

После небольших манипуляций с кодом стал виден базовый каркас сервера. Он состоит из следующих блоков:

  • require 'socket'

Для того, чтобы работать с классом TCPServer (и не только с ним) необходимо подключить библиотеку socket.

  • TCPServer.open('localhost',3000){ |server| ... }

Этот программный код создает сервер, который будет прослушивать порт 3000. В качестве порта может использоваться любой другой (например, 31337). Менять имя хоста (localhost) не нужно, если только у Вас не несколько сетевых интерфейсов. Если у Вас их все таки несколько, то ничего по поводу смены хоста Вам объяснять не надо. Вы и так все, скорее всего, знаете …

  • if (session = server.accept) ... end

При помощи такой нехитрой комбинации ловится соединение с сервером. Обратите внимание, что в примере студента использовалась конструкция while. Ее пришлось упразднить, так как большой необходимости в ней не было. Как только вызов server.accept возвращает значение, то это означает, что к серверу подсоединился клиент. В переменную session записывается указатель на соединение. С ним-то мы и будем работать дальше…

  • cmd, arg = *session.gets.chomp.split

Данный код интересен тем, что в программе студента для его реализации задействовано аж три строчки. И все лишь потому, что он не знал, что такое оператор * (звездочка). Этот оператор преобразует массив в список параметров. Тем самым он освобождает нас от нуждого присваивания. Сам же код получает от клиента строку, которая интерпретируется им как «команда и аргумент, разделенные пробелом». Обратите внимание, что работа идет с переменной session, а не server.

  • case cmd ... end

Ветвление case необходимо для обработки команд. В зависимости от значения переменной cmd будут выполнены те или иные действия.

  • Остальной код описывать не буду, так как каркасом клиент-серверного приложения он не является, а реализует функциональность конкретного приложения.
 

Для того, чтобы прекратить работу сервера необходимо нажать комбинацию клавиш Ctrl+C или Ctrl+Break. Команду отключения данный сервер не поддерживает. Команда shutdown относится к клиенту

Несмотря на то, что данный сервер вполне рабочий, у него есть один существенный недостаток — он не работает для нескольких клиентов. Для того, чтобы это реализовать необходимо обрабатывать каждое соединение с клиентом в отдельном потоке. Кто изучал мультипроцессорное программирование, тот понимает о чем речь. Вот только не стоит сразу кидаться в книжный магазин за необходимой литературой. В составе дистрибутива Ruby уже есть замечательная библиотека gserver, которая как раз и занимается тем, что реализует обработку запросов клиентов в отдельных потоках. Для демонстрации ее работы перепишем предыдущую программу под gserver.

   require 'gserver'
   class Tpo9H < GServer
       def serve( session )
           session.print "Do6po no}i{aJIoBaTb Ha cepBep\r\n"
           session.print "BbI mo}i{eTe Ha6paTb komaHgbI: ls <dir> | cd <dir> | shutdown\r\n"
           loop{
               cmd, arg = *session.gets.chomp.split
               case cmd
                   when "ls"
                       begin
                           session.print Dir[ arg || "*" ].map{ |str| str + "\r\n" }
                       rescue
                           session.print "HeT Takoro qpau'JIa uJIu gupekTopuu - #{ arg.inspect }\r\n"
                       end
                   when "cd"
                       begin
                           Dir.chdir( arg )
                           session.print "OK!\r\n"
                       rescue
                           session.print "HeT Takoro qpau'JIa uJIu gupekTopuu - #{ arg.inspect }\r\n"
                       end
                   when "shutdown"
                       session.close
                       break
                   else
                       session.print "HeBepHa9 komaHga!\r\n"
               end
           }
       end
   end
   tpo9H = Tpo9H.new( 31337 )
   tpo9H.audit = true
   tpo9H.start
   tpo9H.join

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

  • require 'gserver'

Вместо библиотеки socket мы подключаем библиотеку gserver. Для нас это означает то, что работать с классом TCPServer напрямую мы не будем. Использовать мы будем класс GServer.

  • class Tpo9H < GServer

Этот код создает класс Tpo9H, который наследует функциональность от класса GServer. Можно было бы конечно просто расширить класс GServer, но так хотелось «хакерское» название класса, что я не смог удержаться и произвел наследование.

  • def serve( session )

Метод serve (название какое-то незаконченное… но на англ. означает «обслуживать») используется классом GServer как обработчик сессии с клиентом. При выходе из метода, сессия автоматически закрывается. Обратите внимание, что переменная session является параметром метода. Весь код обработки сессии взят из предыдущей программы без изменений.

  • tpo9H = Tpo9H.new( 31337 )

Создается экземпляр класса Tpo9H. В качестве порта для «прослушки» указан 31337. Почему не 3000? Потому, что захотелось! При желании можете его сменить на любой другой.

  • tpo9H.audit = true

Если пропустить эту строчку, то сервер будет работать молча. Настолько молча, что Вы не сможете понять работает он или нет? Надпись об удачном запуске сервера будет выглядеть примерно так:

[Mon Oct 23 23:33:12 2006] Tpo9H 127.0.0.1:3000 start

Если она не появилась, то появится какая-то другая, которая известит Вас об синтаксической ошибке, которую Вы допустили при переписывании кода.

  • tpo9H.start

Этой командой мы стартуем сервер.

  • tpo9H.join

Класс GServer (и его наследники) прослушивает порт в фоновом режиме. Но если программа завершается, то завершатся и все нити (Thread’s). Поэтому надо чем-то занять программу на время работы сервера. Вот и было решено ожидать завершения прослушивающей нити сервера. Эта команда останавливает выполнение программы (за исключением нитей в фоне) до окончания прослушивания экземпляром созданного класса.

Построение клиентской части

править

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

Хотя Geka и имел в своем распоряжении клиентскую программу, ее мы рассматривать не будем, так как с тех пор его сервер претерпел некоторые изменения и прежняя клиентская часть не может работать с нынешней серверной. Мы напишем новую клиентскую часть «с нуля».

Для соединения с сервером по протоколу TCP/IP используется класс TCPSocket из библиотеки socket. Для того, чтобы наша задача была конкретней, мы заставим сервер (при помощи нашего клиента) выполнить следующие команды: ls, cd .., ls и shutdown. Вывод результата выполнения этих команд мы будем осуществлять на экран, но пользователь не получит возможности изменять эту последовательность действий (кроме как исправив программу). Он увидит только результат. Логика здесь следующая: зачем заставлять пользователя вводить команды, если это может делать программа? Если же пользователю нужна другая последовательность команд, то пусть использует telnet или правит клиентскую часть под свои нужды.

Итак, давайте наберем клиентский код, который будет соединяться с нашей серверной частью. Скорее всего он будет выглядеть вот так:

   require 'socket'
   TCPSocket.open('localhost',31337){ |client|
       2.times{ puts client.gets.chomp }
       client.puts "ls"
       puts client.read
       client.puts "cd .."
       puts client.gets
       client.puts "ls"
       puts client.read
       client.puts "shutdown"
   }

Все замечательно, но программа не работает. Не вся, конечно… Она выводит приглашение к диалогу и все, дальше виснет. Это связано с тем, что используется метод .read, который считывает весь поток целиком, пока не встретит символ EOF. Его, то наш сервер как раз и не передает. Не будем пока спешить и править сервер, а применим один нечестный прием: будем использовать не метод .read, а метод .sysread( n ). Почему прием нечестный? Да все потому, что метод .sysread( n ) считывает n-первых символов из потока. Так как мы не знаем сколько нам надо считать символов, то мы зададим в качестве n очень большое число. Например, 5000. Если символов в потоке меньше, чем 5000, то .sysread( n ) считает столько, сколько есть. Эту особенность мы и используем. Разве можно сказать, что такой прием честный?

   require 'socket'
   TCPSocket.open('localhost',31337){ |client|
       2.times{ puts client.gets.chomp }
       client.puts "ls"
       puts client.sysread(5000)
       client.puts "cd .."
       puts client.gets
       client.puts "ls"
       puts client.sysread(5000)
       client.puts "shutdown"
   }

Уже лучше… по крайней мере программа работает. Но давайте поразмышляем над ситуацией, которая произошла с методом .read. Если немного подправить сервер и выдавать после каждой передачи этот символ, то программа с .read могла бы с успехом работать. Какова здесь мораль? А мораль в том, что для успешной работы необходимо с сервера передавать сигнал, который означал бы «последняя строка, которую я передаю клиенту». Чтобы клиент не пытался читать данные с сервера, а начинал их передачу. Вполне естественно, что добавление такого сигнала означает модификацию сервера. В качестве сигнала последней строки мы будем использовать строку +ОК. Почему именно такую? Просто видел ее где-то, вот и решил использовать. Если хотите, то можете использовать свою строку. Только не забудьте об этом, когда будете писать программу-клиент. Вот модифицированный сервер:

   require 'gserver'
   class Tpo9H < GServer
       def serve( session )
           session.puts "Do6po no}i{aJIoBaTb Ha cepBep\r\n"
           session.puts "BbI mo}i{eTe Ha6paTb komaHgbI: ls <dir> | cd <dir> | shutdown\r\n"
           loop{
               cmd, arg = *session.gets.chomp.split
               case cmd
                   when "ls"
                       begin
                           session.puts Dir[ arg || "*" ].map{ |str| str + "\r\n" }
                           
                       rescue
                           session.puts "HeT Takoro qpau'JIa uJIu gupekTopuu - #{ arg.inspect }\r\n"
                       end
                   when "cd"
                       begin
                           Dir.chdir( arg )
                           session.puts "OK!\r\n"
                       rescue
                           session.puts "HeT Takoro qpau'JIa uJIu gupekTopuu - #{ arg.inspect }\r\n"
                       end
                   when "shutdown"
                       session.close
                       break
                   else
                       session.puts "HeBepHa9 komaHga!\r\n"
               end
               session.puts "+OK"
           }
       end
   end
   tpo9H = Tpo9H.new( 31337 )
   tpo9H.audit = true
   tpo9H.start
   tpo9H.join
   

Была добавлена лишь одна команда (хотя, самые внимательные могут заметить, что еще и метод .print был заменен на .puts) — session.puts "+OK". Она будет выполнятся после каждой передачи данных от сервера к клиенту. Тем самым мы будем извещать клиента о том, что передача завершается. Теперь займемся переписыванием клиента. Необходимо исправить код там, где происходит чтение, чтобы он учитывал строки +OK.

   require 'socket'
   TCPSocket.open('localhost',31337){ |client|
       2.times{ puts client.gets.chomp }
       client.puts "ls"
       loop{
           str = client.gets.chomp
           if str[/^\+OK/]
               break
           else
               puts str
           end
       }
       client.puts "cd .."
       loop{
           str = client.gets.chomp
           str[/^\+OK/] ? break : puts( str )
       }
       client.puts "ls"
       while !(str = client.gets.chomp)[/^\+OK/]
           puts str
       end
       client.puts "shutdown"
   }

Код стал выглядеть ужасней, зато стал более честным. Стоит отметить, что предложено три варианта обработки строки +OK. Правда, отличаются они лишь в деталях и функционально делают одно и то же. Давайте рассмотрим подробней решения, которые были реализованы в ходе исправления:

  • loop{ ... }

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

  • if str[/^\+OK/] then ... else ... end

При помощи команды str[/^\+OK/] мы проверяем наличие строки +OK в переменной str. Если проверка прошла успешно, то происходит выход из бесконечного цикла. Если нет, то продолжается получение данных и вывод их на экран.

  • str[/^\+OK/] ? break : puts( str )

Это всего лишь иная запись кода: if str[/^\+OK/] then ... else ... end. Зато более короткая…

  • while !(str = client.gets.chomp)[/^\+OK/] do ... end

Условный оператор и бесконечный цикл были объединены в цикл с предусловием. Условия выхода тоже самое, но оно совмещено с чтением и присваиванием. Выглядит жутковато, но это вполне работоспособный код.

Из всех предложенных вариантов вы вольны выбирать любой… но мне не нравится вообще весь клиент. Как-то он сильно разросся и теперь выглядит монстрозно. Конечно же, есть возможность загнать чтение в отдельный метод, но мы этого делать не будем. Хотя… где наша не пропадала?! Давайте вынесем код отсылки команды и получения ответа в отдельный код. Естественно, что это будет метод для класса TCPSocket (который мы будем расширять). Назовем мы его .cmd.

   class TCPSocket
       def cmd( command, regexp = /^\+OK/ )
           self.puts command
           while !(str = self.gets.chomp)[ regexp ]
               yield str
           end
       end
   end

Итак, теперь все экземпляры класса TCPSocket приобрели метод .cmd, который отсылает команды и принимает результат. Непонятными могут быть следующие моменты:

  • self.puts command

Переменная self содержит указатель на текущий объект, то есть она будет указывать на объект, который хранится в переменной client (так как вызываться метод будет именно для этого объекта).

  • yield str

Данная команда передает значение переменной str в блок. Наличие этой команды подразумевает наличие блока во время вызова метода (имеется в виду .cmd). Блок позволит нам обрабатывать результат так, как мы хотим. Не ограничивая себя выводом результата на экран.

Теперь остается исправить клиент и посмотреть на него в действии:

   TCPSocket.open('localhost',31337){ |client|
       2.times{ puts client.gets.chomp }
       client.cmd("ls"){ |str| puts str }
       client.cmd("cd .."){ |str| puts str }
       client.cmd("ls"){ |str| puts str }
       client.cmd("shutdown"){ |str| puts str }
   }

Обратите внимание на код:

   client.cmd("ls"){ |str| puts str }

Метод .cmd работает как итератор. Последовательно передавая пришедшие строки в блок для дальнейшей обработки.

Чуть не забыл. Данный код выдает ошибку вида:

   client_001.rbw:18:in `cmd': private method `chomp' called for nil:NilClass (NoMethodError)

Это все потому, что передача команды shutdown не подразумевает ответа. А это неправильно. В очередной раз исправим сервер, чтобы избавится от этой ошибки:

   require 'gserver'
   class Tpo9H < GServer
       def serve( session )
           session.puts "Do6po no}i{aJIoBaTb Ha cepBep\r\n"
           session.puts "BbI mo}i{eTe Ha6paTb komaHgbI: ls <dir> | cd <dir> | shutdown\r\n"
           loop{
               cmd, arg = *session.gets.chomp.split
               case cmd
                   when "ls"
                       begin
                           session.puts Dir[ arg || "*" ].map{ |str| str + "\r\n" }
                       rescue
                           session.puts "HeT Takoro qpau'JIa uJIu gupekTopuu - #{ arg.inspect }\r\n"
                       end
                   when "cd"
                       begin
                           Dir.chdir( arg )
                           session.puts "CmeHa gupekTopuu Ha #{arg} npowJIa yga4Ho!\r\n"
                       rescue
                           session.puts "HeT Takoro qpau'JIa uJIu gupekTopuu - #{ arg.inspect }\r\n"
                       end
                   when "shutdown"
                       session.puts "+OK"
                       session.close
                       break
                   else
                       session.puts "HeBepHa9 komaHga!\r\n"
               end
               session.puts "+OK"
           }
       end
   end
   tpo9H = Tpo9H.new( 31337 )
   tpo9H.audit = true
   tpo9H.start
   tpo9H.join

На этом можно было бы и закончить, если бы не одно НО: метод .cmd уже реализован в рамках класса Net::Telnet. И не переписать наш клиент под этот класс было бы неправильно. Переписываем:

   require 'net/telnet'
   client = Net::Telnet.new( 'Host' => 'localhost', 'Port' => 31337, "Prompt" => /^\+OK/n )
   client.cmd("ls"){ |str| puts str }
   client.cmd("cd .."){ |str| puts str }
   client.cmd("ls"){ |str| puts str }
   client.close

Вот теперь, уж точно все!

 

Вскоре, после публикации данной главы в учебнике, Geka прислал клиентскую часть, которую он реализовал в две строчки:

system( "telnet " + gets )
loop{ system( gets ){ |str| puts str } }

Способ не совсем честный, но нет причин о нем не рассказать. Данный способ использует метод system, который вызывает внешнюю программу (в данном случае telnet). Далее все введенное с клавиатуры уходит в программу telnet, а выдаваемое на экран берется из результата работы этой программы.

После запуска данной программы надо ввести имя хоста и порт. Далее, можно вводить команды, которые поддерживает сервер (в нашем случае ls, cd и shutdown).

Как создать сетевой блокнот?

править

Идея написать подобную программу появилась после прочтения статьи Создаем свой online-блокнот. Продемонстрированная там программа предельно проста, но на ее примере очень удобно показать работу с ERB. А учитывая тот факт, что ERB используется в составе инструментария Руби на Рельсах, то ценность этой статьи становится очевидной.

Первое приближение

править

В первом приближении мы попытаемся реализовать ту же самую функциональность, что и описана в статье. Вот только PHP подразумевает наличие веб-сервера, который будет заниматься интерпретацией его команд. В нашем же примере мы самостоятельно поднимем веб-сервер (написанный на Ruby), чтобы не заморачиваться с настройкой стороннего.

Смысл всей программы следующий: надо создать страницу с окном ввода и кнопкой «Сохранить», при нажатии на которую происходит сохранение текста из окна ввода в файл notepad.txt. Ввод осуществляется через браузер по адресу http://localhost:8080.

 

Сервер будем запускать на порт 8080, так как стандартный для веб-серверов порт 80 у меня занял Skype. При желании, порт можно легко поменять

Теперь, собственно, сама программа:

   require 'webrick'
   server = WEBrick::HTTPServer.new( :Port => 8080 )
   server.mount_proc('/'){ |req,resp|
       File.open('notepad.txt','w'){ |f| f.write( req.query["text"] ) } if req.query["text"]
       resp['Content-Type'] = 'text/html'
       resp.body = %& <html><body><center><form method="post">
        <textarea name="text" rows="4" cols="40">#{IO.read('notepad.txt')}</textarea>
        <br /><input type="submit" name="update" value="Сохранить" />
        </form></center></body></html>& 
   }
   server.start

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

  • require 'webrick'
Подключение библиотеки WEBrick для построения серверов (в том числе и веб-серверов).
  • :Port => 8080
Как уже было сказано ранее, порт 80 у меня занят. Поэтому приходится искать другой. Исключительной магической силой порт 8080 не обладает. Поэтому, при желании, его можно сменить на другой.
  • server.mount_proc('/')
На виртуальную корневую директорию мы вешаем процедурный сервлет. Он будет заниматься обработкой запроса на адрес http://localhost:8080/, то есть обращение к виртуальной корневой директории. Чтобы изменить запрос, на который будет откликаться сервлет, достаточно заменить строку '/' на другую, например '/notepad'. Тогда, адрес сервлета будет http://localhost:8080/notepad. Только, зачем писать больше?
  • { |req,resp| ... }
Процедурному (как и любому другому) сервлету передается в блок две переменные. Переменная req содержит информацию о запросе (какой браузер запрашивает, какие переменные передаются и так далее), а при помощи переменной resp формируется ответ (какой тип информации передается, информация для отображения и так далее).
  • ... if req.query["text"]
Постфиксная запись условного оператора if. Блок, перед if будет выполняться только в том случае, когда сервлету будет передаваться переменная text. Метод .query возвращает ассоциативный массив в виде {имя_переменной => значение}.
  • File.open('notepad.txt','w'){ |f| f.write( req.query["text"] ) }
Открываем файл notepad.txt для записи и пишем туда значение переменной text (передается сервлету в теле запроса).
  • resp['Content-Type'] = 'text/html'
Устанавливаем тип передаваемых нами данных. Обычно тип определяется по расширению файла, но процедурный сервлет про файл, а тем более про его расширение, ничего не слышал. Вот и приходится указывать тип передаваемых данных через параметр Content-Type.
  • resp.body = %& ... &
При помощи метода .body= мы передаем HTML-код в качестве ответа на запрос. Сам HTML-код передается в виде строки, заключенной в %& ... & . Это альтернативный способ задания строки. После символа % идет символ, который будет замыкающим (в нашем случае & ). Такой способ задания строки используется в случаях, когда строка содержит много кавычек и апострофов (чтобы не заниматься их экранированием).
  • server.start
При помощи метода .start мы запускаем веб-сервер.
 

Для того, чтобы прекратить работу веб-сервера, надо его просто выгрузить. Это делается при помощи комбинации клавиш Ctrl+Break или Ctrl+C

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

Добавляем ERB

править

Теперь приступим к ERB. Это шаблонная библиотека, которая позволяет осуществлять вставку в произвольный текст любого Ruby-кода. Для этого имеются два основных тега:

  • <% ... %>
Код внутри тега выполняется. Во время обработки он заменяется на пустую строку.
  • <%= ... %>
Код внутри тега выполняется. Результат последнего вычисления преобразуется в строку и вставляется вместо тега.


Не знаю почему, но ERB для меня это «PHP, только на Ruby». Эта фраза обладает столь магическим свойством, что после нее собеседнику все становится понятным. Вот и вам советую воспринимать ERB как «PHP на Ruby».

 

Библиотека WEBrick поддерживает PHP-скрипты благодаря CGI-сервлету

Давайте посмотрим, как может выглядеть наша программа в которой используется ERB-шаблон:

   require 'webrick'
   require 'erb'
   server = WEBrick::HTTPServer.new( :Port => 8080 )
   server.mount_proc('/'){ |req,resp|
       File.open('notepad.txt','w'){ |f| f.write req.query["text"] } if req.query["text"]
       resp['Content-Type'] = 'text/html'
       wa6JIoH = %& <html><body><center><form method="post">
        <textarea name="text" rows="4" cols="40"><%=IO.read('notepad.txt')%></textarea>
        <br /><input type="submit" name="update" value="Сохранить" />
        </form></center></body></html>&
       resp.body = ERB.new( wa6JIoH ).result
   }
   server.start

Что изменилось? Изменений немного, но они все же есть:

  • require 'erb'
Подключаем библиотеку ERB.
  • wa6JIoH = %& ... <%=IO.read('notepad.txt')%> ... &
Создаем переменную wa6JIoH и присваиваем ей строку, которая по совместительству является ERB-шаблоном. Внутри строки можно заметить тег <%= ... %> внутри которого осуществляется считывание файла notepad.txt. Результат считывания будет вставлен вместо тега <%= ... %> во время обработки ERB-шаблона.
  • resp.body = ERB.new( wa6JIoH ).result

Создаем объект ERB и передаем туда подготовленный ERB-шаблон. Обрабатываем его методом .result и результирую строку передаем методу .body=, который подставляет ее в качестве ответа на запрос.

И что мы получаем в результате? Подключили «лишнюю» библиотеку и создали «лишнюю» переменную? Не будем спешить с выводами. Использование библиотеки ERB позволяет вынести шаблон во внешний файл. Тем самым мы очищаем Ruby-код от HTML-кода.

«Кесарю — кесарево!» (с) Иешуа Ганоций

Выносим ERB-шаблон во внешний файл

править

ERB-шаблон существенно портит красоту нашего Ruby-кода. Поэтому, решение вынести шаблон во внешний файл вполне оправдано. Тем более, что это позволит нам править ERB-шаблон отдельно от программы. Более того, внесенные в шаблон изменения будут вступать в силу без перезагрузки сервера.

Файл с ERB-шаблоном (index.html) будет выглядеть следующим образом:

<html>
<body>
 <center>
  <form method="post">
   <textarea name="text" rows="4" cols="40"><%=IO.read('notepad.txt')%></textarea><br />
   <input type="submit" name="update" value="Сохранить" />
  </form>
 </center>
</body>
</html>

Переменную шаблон мы убираем, а вместо нее вставим считывание файла c ERB-шаблоном (index.html).

   require 'webrick'
   require 'erb'
   server = WEBrick::HTTPServer.new( :Port => 8080 )
   server.mount_proc('/'){ |req,resp|
       File.open('notepad.txt','w'){ |f| f.write req.query["text"] } if req.query["text"]
       resp['Content-Type'] = 'text/html'
       resp.body = ERB.new( IO.read('index.html') ).result
   }
   server.start

Вот, уже другое дело! Можно было бы этим и ограничиться, если бы в библиотеке WEBrick отсутствовал бы ERB-сервлет… Но он есть!

ERB-шаблон превращается в ERB-сервлет

править

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

   require 'webrick'
   server = WEBrick::HTTPServer.new( :Port => 8080 )
   server.mount('/',WEBrick::HTTPServlet::ERBHandler,'index.html')
   server.start
 

Самые внимательные читатели заметили, что строка require 'erb' волшебным образом испарилась. Связано это с тем, что реализация ERB-сервлета уже подключает библиотеку ERB

Но, чтобы добиться столь существенного уменьшения кода программы, пришлось немного изменить файл index.html (с ERB-шаблоном):

   <% File.open('notepad.txt','w'){ |f| f.write query["text"] } if query["text"] %>
    <html><body><center><form method=post>
    <textarea name="text" rows="4" cols="40"><%=IO.read('notepad.txt')%></textarea>
    <br /><input type="submit" name="update" value="Сохранить" />
    </form></center></body></html>

Как можно заметить, в самое начало шаблона был добавлен тег, который осуществляет сохранение содержимого переменной text в файл notepad.txt. Код тега был перенесен из программы практически один к одному. За одним только исключением: к переменной text мы теперь обращаемся через переменную query, а не через req.query. Вот и все отличие!

На этом все… из чего состоит наша программа теперь?

  • notepad.rb. Программа-сервер. Назвать файл можно на свое усмотрение. Главное, чтобы работал. Содержит логику, которая осуществляет конфигурирование и запуск сервера.
  • index.html. ERB-шаблон. В нем содержится вся логика программы, кроме реализованной в программе-сервере.
  • notepad.txt. Файл данных. В нем содержатся записи, которые мы вводим и отображаем посредством нашей программы.

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

Гнезда, которые свили не птицы

править
 
Примерное содержание главы

Как пропинговать компьютер в сети?

править

Открываем новую серию статей, которые будут рассказывать про использование встроенных библиотек Ruby. Первая статья будет посвящена написанию утилиты ping (в очень упрощенной форме). Смотрим в стантартную библиотеку и обнаруживаем файлик ping.rb. Смотрим в него и обнаруживаем метод pingecho. Метод используется следующим образом:

require 'ping'
host = ARGV[0] || "localhost" 
printf("%s alive? - %sn", host, Ping::pingecho(host, 5))

Данный метод имеет один маленький недостаток. Он не отслеживает никаких ошибок кроме Timeout::Error и Errno::ECONNREFUSED. Меня это немного смутило и поэтому я убрал явное указание на Timeout. Получился примерно такой метод:

require 'socket'
require 'timeout'
def ping( host, service = "echo",timeout = 5)
    begin 
        timeout( timeout ){ 
            TCPSocket.open( host, service){} 
        } 
    rescue Errno::ECONNREFUSED 
        true 
    rescue false 
    else true 
    end 
end 
p ping(ARGV[0] || "localhost")

Итак, давайте разберем, что делает наш метод. Он создает соединение посредством класса TCPSocket и тут же закрывает его. Если соединение проходит слишком долго (хост не существует в сети) или произошла какая-то другая ошибка (не поддерживается протокол или еще что-то), то метод возвращает false. Если удаленный хост явно отверг наш запрос или принял его, то мы возвращаем true. Как видите, ничего сложного в этом нет.

Простейший датчик работы службы

править

Летним воскресным утром мне захотелось сделать что-то приятное и красивое… Я написал письмо в конференцию о моих светлых идеях, но не смог его отправить. SMTP-сервер безбожно висел. И решило мое больное воображение написать программку, которая документировала бы подобные ситуации. В общем мне нужен был простейший документ (лог работы службы), который бы доказывал, что наши сетевые админы зря едят свой хлеб (а на самом деле я добрый). Для начала я определился с информацией, которая мне была нужна. А именно:

  • текущее время;
  • время отклика;
  • баннер службы.

Ее я решил писать в логи следующим образом… Каждый день будет создаваться файл лога и каждый час в него будет писаться информация о работе службы. В качестве планировщика заданий использовался виндовый Cron, который запускал мою программу в нужное время. Для начала я написал программу, которая соединяется со службой SMTP и получает от него баннер:

require 'socket' 

request = ""
begin_time = Time.now 
t = TCPSocket.open('mail.scli.ru', 'smtp'){
request = t.gets.chomp 
t.close 
end_time = Time.now 
File.open( Time.now.strftime('d:/logs/smtp/%Y_%m_%d.smtp'), File::APPEND | File::CREAT | File::WRONLY ){ |f| 
    f.puts( "#{sprintf( '%02d',Time.now.hour )} | #{end_time-begin_time} | #{request}" ) 
}

Как и следовало ожидать, программа не работала. Она подвисала и не хотела ничего писать в файл. Висела она на строчке:

request = t.gets.chomp

Чтобы разобраться с проблемой, пришлось читать книжку. Слава богу, что под рукой оказалась книга TCP/IP. Учебный курс. В ней на с.345 черным по серому была начертана схема взаимодействия SMTP протокола. Как оказалось, чтобы получить баннер от службы, надо послать команду NOOP. Переписываем наш фрагмент программы.

require 'socket' 

request = "" 
begin_time = Time.now 
t = TCPSocket.open('mail.scli.ru', 'smtp') 
t.puts('NOOP') 
request = t.gets.chomp 
t.close 
end_time = Time.now 
File.open( Time.now.strftime('d:/logs/smtp/%Y_%m_%d.smtp'), File::APPEND | File::CREAT | File::WRONLY ){ |f| 
    f.puts( "#{sprintf( '%02d',Time.now.hour )} | #{end_time-begin_time} | #{request}" ) 
}

Великолепно! Программа работает… но иногда зависает. И тогда меня посетила еще одна мысль: на соединение отводить всего одну секунду (не уложился — сам дурак). Если соединение зависало, то в файл записывалось timeout. Чтобы «отводить время на выполнение операции» нужно задействовать библиотеку timeout. Она у меня входила в состав дистрибутива. Итак, переписываем нашу программу:

require 'timeout'
require 'socket'

request = ""
beachmark = ""
begin 
    beachmark = Time.now 
    timeout(1){
        t = TCPSocket.open('mail.scli.ru', 'smtp') 
        t.puts('NOOP') 
        request = t.gets.chomp t.close 
    } 
    beachmark = Time.now - beachmark 
rescue 
    beachmark = 'timeout'
end 
File.open( Time.now.strftime('d:/logs/smtp/%Y_%m_%d.smtp'), File::APPEND | File::CREAT | File::WRONLY ){ |f| 
    f.puts( "#{sprintf( '%02d',Time.now.hour )} | #{beachmark} | #{request}" ) 
}

Все бы хорошо, но вот beachmark хотелось бы считать «по-взрослому», а именно при помощи пакета beachmark. И снова переписываем код:

require 'timeout'
require 'socket'
require 'benchmark'

request = ""
beachmark = ""
begin
    beachmark = Benchmark.measure{ 
        timeout(1){ 
            t = TCPSocket.open('mail.scli.ru', 'smtp')
            t.puts('NOOP') 
            request = t.gets.chomp
            t.close
        }
    }.to_s
rescue 
    beachmark = 'timeout'
end 
File.open( PATH_LOG_SMTP + Time.now.strftime('d:/logs/smtp/%Y_%m_%d.smtp'), File::APPEND | File::CREAT | File::WRONLY ){ |f| 
    f.puts( "#{sprintf( '%02d',Time.now.hour )} | #{beachmark} | #{request}" ) 
}

И тут мы замечаем, что Beachmark очень многословен. Он выдает информацию в виде:

user system total ( real )

Нам нужен только real. Все остальное — для более детального анализа. Поэтому немного доработаем результат блока measure:

beachmark = Benchmark.measure{ 
    timeout(1){ 
        t = TCPSocket.open('mail.scli.ru', 'smtp') 
        t.puts('NOOP') 
        request = t.gets.chomp
        t.close
    }
}.real

Или можно просто вместо Benchmark.measure использовать Benchmark.realtime. Теперь надо бы разделить ошибки по таймауту и ошибки соединения. Для этого надо добавить лишь еще один блок ;rescue. Помимо этого мне не понадобится все сообщение от службы. Мне и кода сообщения достаточно. Итак, смотрим, что получилось:

require 'socket'
require 'benchmark'
require 'timeout'

request = ""
beachmark = ""
begin
    timeout(1){ 
        beachmark = Benchmark.measure{ 
            request = TCPSocket.open('mail.scli.ru', 'smtp'){ |t| 
                t.puts('NOOP') 
                t.gets.chomp
            }[0..2] 
        }.real 
    } 
rescue Timeout::Error 
    beachmark = 'timeout'
rescue 
    beachmark = ' '
    request = 'error'
end 
File.open( Time.now.strftime('d:/logs/smtp/%Y_%m_%d.smtp'), File::APPEND | File::CREAT | File::WRONLY ){ |f| 
    f.puts( "#{sprintf( '%02d',Time.now.hour )} | #{beachmark} | #{request}" ) 
}

А теперь вопрос: как переписать программу так, чтобы она могла тестировать не только SMTP, но и HTTP, FTP, POP3 и т.д? Но это уже для самостоятельного изучения.

Датчик обновления сайта

править

Ни для кого не секрет, что админы — это жутко ленивый народ. Но начальство, как назло, не хочет платить деньги за просто так. Ему нужны отчеты! Представляю на Ваш суд датчик обновления новостной ленты сайта. Для начала следует как следует поставить себе задачу:

  • Есть сайт и админ его частенько обновляет.
  • Будем рассматривать только случай добавления новостей. Все остальные разделы можно контролировать аналогично.
  • В качестве тестового сайта выберем http://www.minjust.ru. Но, с небольшой доработкой, программа может быть адаптирована и для любого другого сайта.
  • Обновление сайта просходит примерно 1-2 раза в день. Не больше, но может быть и меньше (это зависит от ЦОС Минюста РФ). Для более частого обновления придётся существенно дорабатывать программу.
  • Новости располагаются на первой странице сайта.

Итак, для чего нам нужен датчик? Допустим Вы админ сайта и постоянно добавляете новости. По окончании недели (месяца, года, столетия, …) от Вас требуют отчет о проделанной работе. Вам приходится заходить на сайт и смотреть те новости, которые Вы добавили за последний период. Муторно и неэффективно. Намного приятнее постоянно вести записи о добавленных новостях (при помощи программы, конечно) и по завершении периода просто сделать соответствующую выборку.

Итак, немного об алгоритме программы… Обычно новостей на главной странице строго определенное количество. На нашем тестовом сайте из ровно пять. У нас есть файл в котором мы храним дату добавления новости и заголовок новости. Разделитель у нас может быть произвольным, но в качестве примера будет использован набор символов ' ^_^ '. Вообще для данной задачи даже разделитель не очень-то и нужен (дата состоит из строго определенного количества символов и записывается в строго определенном формате)… но универсальность превыше всего!

Каждый раз при запуске программы, мы скачиваем заглавную страницу сайта и выдираем оттуда даты и заголовки новостей. Потом читаем файл с подобным же набором даных. Читаем весь файл, хотя можно читать только последние n-строк. Но мы будем создавать каждый месяц новый файл и поэтому особой загрузки памяти происходить не должно. Далее сравниваем эти два набора данных и с помощью пересечения и вычитания множеств мы получаем те данные, которых до сих пор нет в файле. Как раз эти данные мы и добавляем в конец файла. ВСЕ! Просто и лаконично. Теперь код, который выполняет поставленную задачу:

require 'net/http'
 
 h = Net::HTTP.new( 'www.minjust.ru', 80 )
 resp, data = h.get( '/index.html' )
 dates = data.scan(/<DIV ALIGN="RIGHT" STYLE="font-size : x-small;">(d{4}-d{2}-d{2})</div>/).map{ |ar| ar[0] }
 texts = data.scan(/<DIV CLASS="TITLE2">(.*?)</div>/m ).map{ |ar| 
     ar[0].gsub("n",' ').gsub("r"," ").gsub(' '*2, ' ').strip
 }
 File.open( Time.now.strftime('log/%Y_%m.log'), File::APPEND | File::CREAT | File::RDWR ){ |f| 
     from_inet = (0...5).inject([]){ |result, i| result + [ dates[i] + ' ^_^ ' + texts[i] ] }
     from_file = f.readlines.map{ |str| str.chomp }.compact
     f.puts( ( from_inet - (from_file & from_inet) ).sort )
 }

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

Приемо-передача файлов

править

Как скачать HTML-страницу

править

Для скачивания HTML-страниц обычно используется протокол HTTP. Адрес страницы задается в виде URL. В Руби существует несколько способов скачать страницу по протоколу HTTP. Начнем с самого древнего — класса Net::HTTP (древнее только Socket’ы, но про них мы умолчим). Для определенности мы будем скачивать этот учебник и сохранять его в виде файла RubyBook.html.

   require 'net/http'
   
   Net::HTTP.open('ru.wikibooks.org'){ |http|
       File.open('RubyBook.html','w'){ |file|
           file.write http.get('/wiki/Ruby').body
       }
   }

Недостатком использования класса Net::HTTP является то, что URL разбивается на две части: адрес сервера (ru.wikibooks.org) и адрес страницы (/wiki/Ruby). Такое разбиение удобно только в том случае, когда необходимо скачивать несколько HTML-страниц с одного сервера. А вот как раз такие ситуации возникают крайне редко. Чаще приходится запрашивать страницы с различных серверов. Для этих целей и был придуман метод open из библиотеки open-uri. Он самостоятельно разбирает URL и производит нужный запрос (не только HTTP).

require 'open-uri'

File.open('RubyBook.html','w'){ |file|
    file.write open( 'http://ru.wikibooks.org/wiki/Ruby' ).read
}


Запрос заголовка

править

Во время скачивания передается не только сама страница (тело сообщения или на англ. body), но и техническая информация (заголовок или на англ. head). Мы еще не раз будем свидетелями такого поведения в протоколах сети Интернет. В заголовке содержится информация об успешности запроса (код возврата), типе передаваемых данных, кодировке, HTTP-сервере и так далее. Для примера, мы будем производить HTTP-запрос и получать ответ только в виде заголовка. Запись заголовка в файл производить не будем, так как в реальной практике этот прием практически не используется. Сначала «потренируемся на кошках», то есть на классе Net::HTTP:

require 'net/http'

Net::HTTP.open('ru.wikibooks.org'){ |http|
    p http.head('/wiki/Ruby')
}

Как и обещалось ранее, вывод заголовка мы делаем на экран. А запрос заголовка, вместо тела сообщения, осуществляется простой заменой метода .get на метод .head. А как тогда получить заголовок и тело сообщения одновременно? Очень просто:

require 'net/http'

Net::HTTP.open('ru.wikibooks.org'){ |http|
   head, body = http.get('/wiki/Ruby')
}

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

Теперь рассмотрим, как выглядит чтение заголовков в методе open библиотеки open-uri:

require 'open-uri'

p open( 'http://ru.wikibooks.org/wiki/Ruby' ).meta

Замена метода .read на метод .meta позволяет нам дотянуться до заголовка. Заголовок имеет вид ассоциативного массива (объект класса Hash), где ключем является имя параметра (по стандарту MIME), а значением — значение параметра. Читать одновременно заголовок и тело сообщения можно вот так:

require 'open-uri'

open( 'http://ru.wikibooks.org/wiki/Ruby/' ){ |f|
       p f.meta
       p f.read
}

Мы использовали свойство метода .open прикреплять к себе блок.

Работа через прокси

править

Прокси-сервер — это сервер, перенаправляющий запросы другим серверам. Обычно таковой используется для скрытия истинного IP-адреса или для контроля за трафиком.

Необходимость HTTP-запроса через прокси возникает, когда соединение с целевым сервером напрямую невозможно (например, администратор сети посчитал, что соединение посредством шлюза дает ему слишком мало возможностей контроля за трафиком). Использование подобного запроса маловероятно, но необходимо знать о возможности посылки http-запроса через прокси. Начнем с класса Net::HTTP::Proxy:

require 'net/http'

Net::HTTP::Proxy('you.proxy.host',8808).start('ru.wikibooks.org'){ |http|
 p http.get('/wiki/Ruby').body
}

Добавив всего лишь небольшой фрагмент кода мы получили работу через прокси. В нашем случае использовался прокси-сервер с адресом you.proxy.host, который предоставляет прокси-доступ через порт 8808.

Теперь посмотрим, как эта же самая функциональность можеть быть реализована с использованием метода open (из библиотеки open-uri).

require 'open-uri'
   
p open( 'http://ru.wikibooks.org/wiki/Ruby',:proxy => 'http://you.proxy.host:8808/').read

Добавился второй параметр (ассоциативный массив с единственной парой) в вызове метода open. К слову сказать, во втором параметре можно указать множество параметров запроса (например, название браузера).

 

Реализация соединения через прокси у метода open более удобна, так как в зависимости от внешних факторов можно соединяться как через прокси, так и напрямую. В случае Net::HTTP и Net::HTTP::Proxy используются два разных класса и подобные трюки затруднительны (хотя и возможны).

Вам письмо или You have got mail

править

Запускаем свой веб-сервер

править

Для того, чтобы создать простейший веб-сервер не нужно реализовывать протокол HTTP на уровне сокетов. Достаточно знать, какую библиотеку использовать. В нашем случае это будет библиотека WEBrick (http://www.webrick.org), которая уже включена в дистрибутив Руби. Возможности библиотеки настолько широкие, что для их описания потребуется создание отдельного учебника. Мы рассмотрим лишь часть из них, зато на реальном жизненном примере.

 

Библиотека WEBrick (переводится как «куча паутины») отнюдь не лучшая реализация библиотеки для построения веб-сервера. Сейчас набирает обороты библиотека Mongrel (переводится как «полукровка», что указывает на смешанную структуру библиотеки. В ней критичные участки кода реализованы не на Руби, а на Си. Это в лучшую сторону сказалось на скорости работы данного симбиоза), которая имеет гораздо лучшую реализацию, но для ее использования нужно скачивать и устанавливать библиотечные файлы, так как в дистрибутив она пока не включена. Именно поэтому построение нашего первого веб-сервера будет проходить на базе WEBrick

Секретарша начальника Управления производственного планирования ЗИЛ попросила меня реализовать программу, которая облегчала бы ей процесс создания служебных записок. В частности, требовалось по кодам подразделений выставлять фамилии и должность начальников этих подразделений. Без программы это сущая морока, особенно, если необходимо отправлять «служебку» необходимо в несколько подразделений. У меня не было особенного настроения, но я решил сделать два дела одновременно: заслужить благодарность секретарши и попутно продемонстрировать процесс создания web-серверного приложения. Благодарность я уже получил, поэтому приступаю ко второй части.

Для того, чтобы проверить работает ли библиотека WEBrick я выполнил следующий код:

   require 'webrick'
   WEBrick::HTTPServer.new( :DocumentRoot => 'public_html', :Port => 8080 ).start

После успешного запуска сервера, я запустил Firefox и набрал адрес: http://localhost:8080/. И тут я узнал, что директория public_html пуста. Горевал я по этому поводу не долго и быстро создал файл public_html/index.html примерно следующего содержания:

   <html><body><h1>Сервер работает!</h1></body></html>

После обновления страницы на экране красовалась крупная надпись: «Сервер работает!» Все! Мы с вами написали свой первый веб-сервер.


Настало время разобраться в том, как это все работает:

  • require 'webrick'
Подключение библиотеки WEBrick. Ничего интересного и неожиданного здесь не произошло.
  • WEBrick::HTTPServer.new( :DocumentRoot => 'public_html', :Port => 8080 ).start
  • Библиотека WEBrick реализована в виде модуля с одноименным названием, поэтому до любого класса приходится «дотягиваться» через префикс: WEBrick::.
  • HTTPServer — это название класса, который используется для создания веб-серверов.
  • Метод .new создает экземпляр класса WEBrick::HTTPServer. Методу передается два параметра (на самом деле — один ассоциативный массив с двумя парами):
  • :DocumentRoot => 'public_html' указывает на то, что веб-сервер будет просматривать директорию ./public_html для выдачи html-страниц (и не только) по запросу. Если этот параметр не указывать, то сервер просто не будет работать.
  • :Port => 8080 задает порт 8080 для «прослушивания» сервером. Если этот параметр не указывать, то сервер будет прослушивать порт 80 (стандарт для веб-серверов). Но случилось так, что «слушать» 80-порт он у меня отказался, так как там «обосновался» Skype. Вот и пришлось в срочном порядке придумывать новый. Ничего умнее, как продублировать число 80 мне в голову не пришло. Если фантазия у вас развита лучше, то придумайте любой другой порт.
  • Метод .start запускает сервер. В отличии от класса GServer, дополнительные манипуляции с методами .sleep и loop не требуются.
 

Очень часто необходимо осуществить передачу данных между компьютерами по сети. Для этого вполне подойдет WEBrick. Быстро напишите веб-сервер, который будет просматривать нужную директорию. Теперь, для вас это больше не проблема!

Но вернемся к заказанной мне программе. Она будет состоять из двух частей: обычная html-страница (index.html) и сервлет (/input). Страница public_html/index.html будет содержать форму в которую будут вводится исходные данные. Ее задачей будет формирование запроса для сервлета. Сервлет /input будет получать номера подразделений, а выдавать их названия и ФИО действующих начальников. Вот код public_html/index.html:

<nowiki><HTML><BODY><form </nowiki><u>action='/input' method='post'</u><nowiki>><div align=center>
    <table border=0 bgcolor=#000000 width=350><tr><td><table border=0 bgcolor=#CCCCFF width=100%>
    <tr><td align=right>Лит-1 - 01</td><td><INPUT </nowiki><u>TYPE="checkbox" NAME="check_01"</u><nowiki>></td>
    <td><INPUT TYPE="checkbox" NAME="check_02"></td><td>02 - ГЛЦЧ</td></tr>
    <tr><td align=right>ГКЦ - 04</td><td><INPUT TYPE="checkbox" NAME="check_04"></td>
    <td><INPUT TYPE="checkbox" NAME="check_05"></td><td>05 - Прессовый</td></tr>
    <tr><td align=right>РПЦ - 06</td><td><INPUT TYPE="checkbox" NAME="check_06"></td>
    <td><INPUT TYPE="checkbox" NAME="check_09"></td><td>09 - Моторный</td></tr>
    <tr><td align=right>Кузовной - 10</td><td><INPUT TYPE="checkbox" NAME="check_10"></td>
    <td><INPUT TYPE="checkbox" NAME="check_11"></td><td>11 - ПСК</td></tr>
    <tr><td align=right>МСК-1 - 12</td><td><INPUT TYPE="checkbox" NAME="check_12"></td>
    <td><INPUT TYPE="checkbox" NAME="check_14"></td><td>14 - ПОиСА</td></tr>
    <tr><td align=right>МСЦ-3 - 17</td><td><INPUT TYPE="checkbox" NAME="check_17"></td>
    <td><INPUT TYPE="checkbox" NAME="check_18"></td><td>18 - МСЦ-2</td></tr>
    <tr><td align=right>Нормаль - 19</td><td><INPUT TYPE="checkbox" NAME="check_19"></td>
    <td><INPUT TYPE="checkbox" NAME="check_20"></td><td>20 - Арматурный</td></tr>
    <tr><td align=right>АСК - 21</td><td><INPUT TYPE="checkbox" NAME="check_21"></td>
    <td><INPUT TYPE="checkbox" NAME="check_22"></td><td>22 - Термический</td></tr>
    <tr><td align=right>РААЗ - 25</td><td><INPUT TYPE="checkbox" NAME="check_25"></td>
    <td><INPUT TYPE="checkbox" NAME="check_27"></td><td>27 - МЗАА</td></tr>
    <tr><td align=right>УО - 30</td><td><INPUT TYPE="checkbox" NAME="check_30"></td>
    <td><INPUT TYPE="checkbox" NAME="check_34"></td><td>34 - ЗИЛтехоснастка</td></tr>
    <tr><td align=right>УКЭР - 57</td><td><INPUT TYPE="checkbox" NAME="check_57"></td>
    <td><INPUT TYPE="checkbox" NAME="check_58"></td><td>58 - ПенЗА</td></tr>
    <tr><td align=right>СААЗ - 61</td><td><INPUT TYPE="checkbox" NAME="check_61"></td>
    <td><INPUT TYPE="checkbox" NAME="check_62"></td><td>62 - ТУ</td></tr>
    <tr><td align=right>УКК - 64</td><td><INPUT TYPE="checkbox" NAME="check_64"></td>
    <td><INPUT TYPE="checkbox" NAME="check_67"></td><td>67 - УМТС</td></tr>
    <tr><td align=right>УСК - 74</td><td><INPUT TYPE="checkbox" NAME="check_74"></td>
    <td><INPUT TYPE="checkbox" NAME="check_76"></td><td>76 - ЦИТ</td></tr>
    <tr><td align=right>УСХ - 81</td><td><INPUT TYPE="checkbox" NAME="check_81"></td>
    <td><INPUT TYPE="checkbox" NAME="check_82"></td><td>82 - ПЗА</td></tr>
    <tr><td align=right>РЗАА - 85</td><td><INPUT TYPE="checkbox" NAME="check_85"></td>
    <td><INPUT TYPE="checkbox" NAME="check_"></td><td>&nbsp;</td></tr>
    <tr><td colspan=4 align=center><INPUT TYPE="submit" NAME="Сформировать шапку"></td></tr>
    <tr><td align=right>Рассказов</td><td><INPUT TYPE="checkbox" NAME="check_001"></td>
    <td><INPUT TYPE="checkbox" NAME="check_002"></td><td>Коновалова</td></tr>
    <tr><td align=right>Принцев</td><td><INPUT TYPE="checkbox" NAME="check_003"></td>
    <td><INPUT TYPE="checkbox" NAME="check_004"></td><td>Сорокин</td></tr>
    <tr><td align=right>Журавлев</td><td><INPUT TYPE="checkbox" NAME="check_005"></td>
    <td><INPUT TYPE="checkbox" NAME="check_006"></td><td>Корабельников</td></tr>
    <tr><td align=right>Фет</td><td><INPUT TYPE="checkbox" NAME="check_007"></td>
    <td><INPUT TYPE="checkbox" NAME="check_008"></td><td>Ярков</td></tr>
    <tr><td align=right>Болотин</td><td><INPUT TYPE="checkbox" NAME="check_009"></td>
    <td><INPUT TYPE="checkbox" NAME="check_010"></td><td>Шрамов</td></tr>
    <tr><td align=right>Поленов</td><td><INPUT TYPE="checkbox" NAME="check_011"></td>
    <td><INPUT TYPE="checkbox" NAME="check_012"></td><td>Копылов</td></tr>
    </table></td></tr></table></div></form></BODY></HTML></nowiki>
 

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

Хотя код и довольно объемный, но я решил поместить его целиком, чтобы программа у вас работала точно также, как и у меня. Первое, на что следует обратить внимание, это параметры тега FORM. Именно он занимается тем, что подготавливает данные для сервлета и переправляет их ему.

  • action='/input'
Адрес, по которому будут передаваться данные. Это может быть как CGI-приложение, так и сервлет. В нашем случае это сервлет /input.
  • method='post'
В протоколе HTTP используется два метода передачи данных: POST и GET. Отличаются они тем, что POST не отображает передаваемые данные в адресной строке браузера. Возьмите за правило использовать метод POST.

Внутри тега FORM присутствует великое множество тегов INPUT, которые как раз и формируют данные. Каждый из тегов INPUT имеет два параметра: NAME и TYPE.

  • TYPE="checkbox"
Параметр TYPE указывает на тип формируемых данных. В данном случае это checkbox (переключатель). Он имеет два значения: «on» (включен) и «off» (выключен). Значение «off» обычно не передается. Получается своеобразный булевский тип.
  • NAME="check_01"
Все данные передаются в виде пары имя=значение. Параметром NAME задается имя. Значение же задается пользователем.

Вот как выглядит страница index.html после обработки браузером (Firefox):

 
Вид страницы public_html/index.html в Firefox

Теперь рассмотрим серверную часть программы (сервлет /input), которая будет обрабатывать запросы (сформированные файлом public/index.html). Сервлет является частью сервера. Поэтому листинг сервера будет одновременно и листингом сервлета.

<u>bosses = </u>{
    'check_01' => "\tНачальнику Лит-1\tБокову Ю.В.",
    'check_02' => "\tНачальнику ГЛЦЧ\tНазарову А.В.",
    'check_04' => "\tНачальнику ГКЦ\tЛасунину Б.Д.",
    'check_05' => "\tНачальнику Прессового корпуса\tЯшнову Ю.М.",
    'check_06' => "\tНачальнику РПЦ\tШепелеву Е.И.",
    'check_09' => "\tДиректору МАП\tБагатурии Р.С.",
    'check_10' => "\tНачальнику Кузовного корпуса\tАшмарину А.Г.",
    'check_11' => "\tНачальнику ПСК\tАнаньву А.С.",
    'check_12' => "\tНачальнику МСК-1\tМиролюбову В.П.",
    'check_14' => "\tДиректору ПОиСА\tСаттарову М.Д.",
    'check_17' => "\tНачальнику МСЦ-3\tБородуле П.Н.",
    'check_18' => "\tНачальнику МСЦ-2\tГрищенкову А.И.",
    'check_19' => "\tНачальнику цеха \"Нормаль\"\tАфонину А.Н.",
    'check_20' => "\tНачальнику Арматурного цеха\tДавыдову В.И.",
    'check_21' => "\tНачальнику АСК\tБорисюку В.Д.",
    'check_22' => "\tНачальнику Термического цеха\tВерташову Н.А.",
    'check_25' => "\tДиректору ЗАО РААЗ\tСавчуку В.Ф.",
    'check_27' => "\tДиректору ЗАО МЗАА\tСоловьеву Н.И.",
    'check_30' => "\tНачальнику УО\tЮру А.Е.",
    'check_34' => "\tДиректору ООО \"ЗИЛтехоснастка\"\tТимофееву Г.П.",
    'check_57' => "\tГлавному конструктору АМО ЗИЛ-начальнику УКЭР\tРыбину Е.Л.",
    'check_58' => "\tДиректору ЗАО ПенЗА\tГудкову В.И.",
    'check_61' => "\tДиректору ЗАО СААЗ\tНовикову В.А.",
    'check_64' => "\tНачальнику УКК\tМинину Д.С.",
    'check_62' => "\tНачальнику ТУ\tУстинкину В.В.",
    'check_67' => "\tНачальнику УМТС\tМелешкину В.Д.",
    'check_74' => "\tНачальнику УСК\tТарабрину В.В.",
    'check_76' => "\tДиректору ЦИТ\tИгнатьеву В.П.",
    'check_81' => "\tНачальнику УСХ\tТурчину Н.В.",
    'check_82' => "\tДиректору ЗАО ПЗА\tПлешакову И.В.",
    'check_85' => "\tДиректору ОАО РЗАА\tДобрынину Ю.Г.",
    'check_001' => "\tНачальнику УК\tРассказову А.А.",
    'check_002' => "\tНачальнику ЭУ\tКоноваловой С.Н.",
    'check_003' => "\tИсполнительному директору АМО ЗИЛ\tПринцеву И.В.",
    'check_004' => "\tДиректору по экономике и финансам АМО ЗИЛ\tСорокину А.В.",
    'check_005' => "\tДиректору по производству АМО ЗИЛ\tЖуравлеву В.С.",
    'check_006' => "\tКоммерческому директору АМО ЗИЛ\tКорабельникову Е.В.",
    'check_007' => "\tДиректору по дочерним и зависимым обществам\tФету О.Л.",
    'check_008' => "\tГлавному инженеру АМО ЗИЛ\tЯркову Г.А.",
    'check_009' => "\tДиректору по качеству АМО ЗИЛ\tБолотину Ю.М.",
    'check_010' => "\tДиректору ЗАО \"Торговый дом ЗИЛ\"\tШрамову В.П.",
    'check_011' => "\tДиректору ООО ВТФ \"ЗИЛ-экспорт\"\tПоленову А.Ю.",
    'check_012' => "\tДиректору Прессового производства\tКопылову Ю.П."
    }  
    require 'webrick'
    server = WEBrick::HTTPServer.new( :Port => 8080 )
    server.mount_proc('/'){ |req,resp| resp.body = IO.read('public_html/index.html') }
    server.mount_proc('/input'){ |req,resp|
        resp.body = %!<nowiki><html><body><div align=center><form action='/' method='post'>
    <textarea rows='5' cols='60'>#{req.query.map{ |key,value| bosses[ key ] }.compact.join("\n") }
    </textarea><br /><input type='submit' value='Повторим?'></form></div></body></html></nowiki>!
    }
    server.start

Начинаем разбираться с кодом веб-сервера:

  • bosses = { ... }
Создается ассоциативный массив, который будет использоваться при обработке данных. Ключем является имя «переключателя», а значением — строка, на которую надо этот ключ заменить. Путем такой замены мы будем формировать результат.
  • require 'webrick'
Подключаем библиотеку WEBrick. По-хорошему ее надо было подключать перед инициализацией ассоциативного массива. Но не хотелось разбивать на части код, относящийся к реализации веб-сервера.
  • server = WEBrick::HTTPServer.new( :Port => 8080 )
Создаем экземпляр класса WEBrick::HTTPServer. На этот раз мы его сохраняем в переменную server. Это необходимо для подключения сервлетов. Параметр :DocumentRoot мы не указываем, так как файл public_html/index.html мы тоже сделаем сервлетом. Поэтому подключать для этого всю папку public_html просто глупо.
  • server.mount_proc('/'){ |req,resp| resp.body = IO.read('public_html/index.html') }
  • При помощи метода .mount_proc('/') мы закрепили сервлет на корневую директорию, то есть при запросе http://localhost:8080/ будет вызываться именно он.
  • Параметры req и resp означают запрос сервлету (req) и ответ сервлета (resp), то есть сервлет получает переменную req и в качестве результата должен сформировать переменную resp (resp.body = ).
  • В качестве результата сервлет возвращает содержимое файла public_html/index.html. По хорошему, надо было бы считать этот файл при запуске сервера, чтобы сэкономить время обработки сервлета.
  • server.mount_proc('/input'){ |req,resp| ... }
Таким образом мы создаем сервлет /input, который будет обрабатывать запросы. Его тело мы рассмотрим в несколько заходов:
  • При помощи resp.body = ... мы формируем выходные данные (строка с HTML-кодом).
  • HTML-код: <html><body><div align=center><form action='/' method='post'><textarea rows='5' cols='60'>#{..}</textarea><br /><input type='submit' value='Повторим?'></form></div></body></html>; просто создает окружения для наших выходных данных. В частности видно, что наши выходные данные будут помещаться в тег TEXTAREA. Это сделано для того, чтобы удобней было копировать данные. Вот примерно так это будет выглядеть в браузере:
 
Пример работы сервлета /input
  • Код обработки данных: req.query.map{ |key,value| bosses[ key ] }.compact.join("\n"); получает список передаваемых параметров в виде ассоциативного массива (метод .query) и заменяет имена переменных на значения из ассоциативного массива bosses. Далее идет преобразование в строку при помощи метода .join("\n").
  • server.start
Запуск сервера.


Вот почти такую программу я и презентовал секретарше Анюте. «Почти» потому, что фамилии должны идти строго в определенном порядке (чего нельзя добиться в ассоциативном массиве), но для того, чтобы посмотреть реализацию веб-сервера — продемонстрированной версии программы должно быть достаточно. Смело меняйте код и создавайте свои веб-сервера!

Сервлетки в рубиновом соусе

править

Мы уже неоднократно упоминали понятие сервлет, но особенно на нем не останавливались. Давайте сейчас рассмотрим типовые сервлеты, которые имеются в стандартной поставке WEBrick

  • Файловый сервлет
Реализует взаимосвязь запроса с реальным файлом (или директорией). В самом первом примере мы использовали файловый сервлет, когда передавали :DocumentRoot => 'public_html' в качестве параметра методу .new. Это было равнозначно созданию файлового сервлета на корневой директории веб-сервера. Функциональность файлового сервлета описана в классе WEBrick::HTTPServlet::FileHandler.
  • CGI-сервлет
Реализует взаимосвязь запроса с CGI-приложением, то есть считывается первая строка и вытаскивается оттуда путь к интерпретатору. Далее производится запуск интерпретатора, которому параметром передается вызванный файл. Строка для CGI-приложений на Ruby будет выглядеть приемерно так:
  • #!/usr/bin/ruby
для операционных систем семейства Unix
  • #!c:/ruby/bin/ruby.exe
для операционных систем семейства Windows
Кстати, я столкнулся с тем, что под Windows CGI-сервлеты отказывались работать. Это связано с тем, что WEBrick считает, что для того, чтобы запустить скрипт достаточно просто его вызывать (./sctipt_name) и он сам запустится. Понятное дело, что в Windows такое работать не будет. Поэтому мне пришлось немножко переписать часть библиотеки WEBrick, которая отвечает за запуск CGI-программ. Для того, чтобы мои изменение стали доступны и вам, я написал небольшую программку, которая устанавливает себя куда надо:
File.open(Config::CONFIG['rubylibdir']+'/webrick/httpservlet/cgi_runner.rb','w'){|file| 
  file.puts IO.read($0).split('#'*10).last 
}
exit
##########
#
# cgi_runner.rb -- CGI launcher.
#
# Author: IPR -- Internet Programming with Ruby -- writers
# Copyright (c) 2000 TAKAHASHI Masayoshi, GOTOU YUUZOU
# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
# reserved.
# Copyright (c) 2007 Rubynovich
#
# $IPR: cgi_runner.rb,v 2.0 2007/02/23 18:53:15 Rubynovich Exp $

STDIN.binmode
STDOUT.reopen(open(STDIN.sysread(STDIN.sysread(8).to_i), "w"))
STDERR.reopen(open(STDIN.sysread(STDIN.sysread(8).to_i), "w"))

ENV.clear
ENV.update( Marshal.restore(STDIN.sysread(STDIN.sysread(8).to_i)) )

if IO.read(ENV["SCRIPT_FILENAME"],50) =~ /^#!(.*)$/
  exec($1,ENV["SCRIPT_FILENAME"])
else
  exec(ENV["SCRIPT_FILENAME"])
end
Немного поясню. Первая строчка — это программа, которая записывает нижеследующий код в папку с библиотеками.
Функциональность CGI-сервлета описана в классе WEBrick::HTTPServlet::CGIHandler
  • ERB-сервлет
Реализует взаимосвязь запроса с ERB-шаблонами, в которые вставляются предварительно подготовленные данные.
  • Процедурный сервлет
Этим сервлетом мы уже пользовались, когда писали .mount_proc. Он обеспечивает взаимосвязь запроса и процедуры обработки (описанной внутри блока метода .mount_proc).

Для того, чтобы было проще пользоваться всеми видами сервлетов WEBrick я написал небольшую программку, которая создает метод mount_file (для файлового сервлета), mount_erb (для ERB-сервлета) и mount_cgi (для CGI-сервлета). Как вы могли заметить mount_proc уже существует (собственно, его название и послужило прототипом для остальных). Вот эта программа:

require 'webrick'
module WEBrick
  class HTTPServer
    ['ERB','CGI','File'].each{ |handler|
      class_eval( "def mount_#{handler.downcase}(point,file)\nmount(point,HTTPServlet::#{handler}Handler,file)\nend" )
    }
  end
end

Теперь можно использовать эти сервлеты следующим образом:

require 'webrick'
module WEBrick
  class HTTPServer
    ['ERB','CGI','File'].each{ |handler|
      class_eval( "def mount_#{handler.downcase}(point,file)\nmount(point,HTTPServlet::#{handler}Handler,file)\nend" )
    }
  end
end

server = WEBrick::HTTPServer.new( :Port => 8090 )
server.mount_file('/','html')
server.mount_cgi('/hello','cgi-bin/hello.exe' )
server.mount_cgi('/hello.cgi','cgi-bin/hello.rb' )
server.start

Ну вот вроде и все, что можно сказать по сервлетам.

Приложения

править
Задачник
Сборник задач
Справочник
Справочник по базовым классам
Лицензия
Перевод Лицензии Руби
Установка
Установка Руби
Жаргон
Придумываем слова с рубинами и рельсами
Фольклор
Народное творчество любителей Руби
Избранное с RubyNews
К разграблению. Полезные статьи перемещаются в основную часть учебника, затем это приложение удалим.
Идеология
программирования вообще и на Руби в частности

Дальнейшее чтение

править

Русскоязычные ресурсы

править
  • Все о Ruby on Rails Рускоязычный ресурс по Ruby.
  • RubyInUse Территория общения русскоязычных рубистов.
  • Форум Ruby on rails. Русскоязычный форум написанный и посвященный Ruby on Rails, здесь всегда отвечают на ваши вопросы, для зарегистрированных доступна тематическая PDF библиотека.
  • Учебные материалы МФТИ. Коллекция учебных материалов по Ruby на сайте МФТИ

Русскоязычная литература

править

Иноязычная литература

править
  • Why’s (Poignant) Guide to Ruby(англ.) — эта книга достойна чтения, даже если Вам не нужно знание Руби. Просто шедевр. Распространяется бесплатно. Там ещё богатая подсветка текста; эх, нам бы такую.. Частичный перевод здесь.
  • Programming Ruby Дэйва Томаса(англ.). Многие пытались, но не перевели на русский. Первая редакция книги содержится в пакете «Установка за один щелчок» для Windows. Владельцы других ОС смогут найти её в Сети (например, в виде набора вебстраниц). Вторую редакцию книги можно купить на Amazon.com в бумажном или электронном виде.

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