|
||||
|
Глава 2. Строки
В начале 1980-х годов один профессор информатики, начиная первую лекцию по структурам данных, не представился студентам, не сказал, как называется курс, не рассказал о его программе и не порекомендовал никаких учебников — а вместо этого сходу спросил: «Какой тип данных самый важный?» Было высказано несколько предположений. Когда профессор услышал слово «указатели», он выразил удовлетворение, но все-таки не согласился со студентом, а высказал свое мнение: «Самым важным является тип символ». У него были на то основания. Компьютерам предназначено быть нашими слугами, а не хозяевами, а человеку понятны только символьные данные. (Есть, конечно, люди, которые без труда читают и двоичные данные, но о них мы сейчас говорить не будем.) Символы, а стало быть, и строки, позволяют человеку общаться с компьютером. Любую информацию, в том числе и текст на естественном языке, можно закодировать в виде строк. Как и в других языках, строка в Ruby — просто последовательность символов. Подобно другим сущностям, строки являются полноценными объектами. В программах приходится выполнять разнообразные операции над строками: конкатенировать, выделять лексемы, анализировать, производить поиск и замену и т.д. Язык Ruby позволяет все это делать без труда. Почти всюду в этой главе предполагается, что байт — это символ. Но при работе в многоязычной среде это не совсем так. Вопросы интернационализации обсуждаются в главе 4. 2.1. Представление обычных строкСтрока в Ruby — это последовательность 8-битовых байтов. Она не завершается нулевым символом, как в С, и, следовательно, может содержать такие символы. В строке могут быть символы с кодами больше 0xFF, но такие строки имеют смысл, лишь если выбран некоторый набор символов (кодировка). Дополнительную информацию о кодировках вы найдете в главе 4. Простейшая строка в Ruby заключается в одиночные кавычки. Такие строки воспринимаются буквально; в качестве управляющих символов в них распознаются только экранированная одиночная кавычка ( \') и экранированный символ обратной косой черты ( \\): s1 = 'Это строка' # Это строка. s2 = 'Г-жа О\'Лири' # Г-жа О'Лири. s3 = 'Смотри в С:\\TEMP' # Смотри в C:\TEMP. Строки, заключенные в двойные кавычки, обладают большей гибкостью. В них допустимо много других управляющих последовательностей, в частности для представления символов забоя, табуляции, возврата каретки и перевода строки. Можно также включать произвольные символы, представленные восьмеричными цифрами: s1 = "Это знак табуляции: (\t)" s2 = "Несколько символов забоя: xyz\b\b\b" s3 = "Это тоже знак табуляции: \011" s4 = "А это символы подачи звукового сигнала: \а \007" Внутри строки, заключенной в двойные кавычки, могут встречаться даже выражения (см. раздел 2.21). 2.2. Альтернативная нотация для представления строкИногда встречаются строки, в которых много метасимволов, например одиночных и двойных кавычек и т.д. В этом случае можно воспользоваться конструкциями %qи %Q. Вслед за ними должна идти строка, обрамленная с обеих сторон символами-ограничителями; лично я предпочитаю квадратные скобки ( []). При этом %qведет себя как одиночные кавычки, a %Q -как двойные. S1 = %q[Как сказал Магритт, "Ceci n'est pas une pipe."] s2 = %q[Это не табуляция: (\t)] # Равнозначно 'Это не табуляция: \t' s3 = %Q[А это табуляция: (\t)] # Равнозначно "А это табуляция: \t" В обоих вариантах можно применять и другие ограничители, помимо квадратных скобок: круглые, фигурные, угловые скобки. s1 = %q(Билл сказал: "Боб сказал: 'This is a string.'") s2 = %q{Дpyгaя строка.} s3 = %q<B этой строке есть специальные символы '" [ ] (){}.> Допустимы также непарные ограничители. В этом качестве может выступать любой символ, кроме букв, цифр и пропусков (пробелов и им подобных), который имеет визуальное представление и не относится к числу перечисленных выше парных ограничителей. s1 = %q:"Я думаю, что это сделала корова г-жи О'Лири," сказал он.: s2 = %q*\r - это control-M, a \n - это control-J.* 2.3. Встроенные документыДля представления длинной строки, занимающей несколько строк в тексте, можно, конечно, воспользоваться обычными строками в кавычках: str = "Три девицы под окном Пряли поздно вечерком..." Но тогда отступ окажется частью строки. Можно вместо этого воспользоваться встроенным документом, изначально предназначенным для многострочных фрагментов. (Идея и сам термин заимствованы из более старых языков.) Синтаксически он начинается с двух знаков <<, за которыми следует концевой маркер, нуль или более строк текста и в завершение тот же самый концевой маркер в отдельной строке: str = <<EOF Три девицы под окном Пряли поздно вечерком... EOF Но следите внимательно, чтобы после завершающего концевого маркера не было пробелов. В текущей версии Ruby маркер в такой ситуации не распознается. Встроенные документы могут быть вложенными. В примере ниже показано, как передать методу три представленных таким образом строки: some_method(<<str1, <<str2, <<str3) первый кусок текста... str1 второй кусок... str2 третий кусок текста. str3 По умолчанию встроенный документ ведет себя как строка в двойных кавычках, то есть внутри него интерпретируются управляющие последовательности и интерполируются выражения. Но если концевой маркер заключен в одиночные кавычки, то и весь документ ведет себя как строка в одиночных кавычках: str = <<'EOF' Это не знак табуляции: \t а это не символ новой строки: \n EOF Если концевому маркеру встроенного документа предшествует дефис, то маркер может начинаться с красной строки. При этом удаляются только пробелы из той строки, на которой расположен сам маркер, но не из предшествующих ей строк документа. str = <<-EOF Каждая из этих строк начинается с пары пробелов. EOF Опишу стиль, который нравится лично мне. Предположим, что определен такой метод margin: class String def margin arr = self.split("\n") # Разбить на строки. arr.map! {|x| x.sub!(/\s*\|/,"")) # Удалить начальные символы. str = arr.join("\n") # Объединить в одну строку. self.replace(str) # Подменить исходную строку. end end Для ясности я включил подробные комментарии. В этом коде применяются конструкции, которые будут рассмотрены ниже —как в этой, так и в последующих главах. Используется этот метод так: str = <<end.margin |Этот встроенный документ имеет "левое поле" |на уровне вертикальной черты в каждой строке. | | Можно включать цитаты, | делать выступы и т.д. end В качестве концевого маркера естественно употребить слово end. (Впрочем, это дело вкуса. Выглядит такой маркер как зарезервированное слово end, но на самом деле этот выбор ничуть не хуже любого другого.) Каждая строка начинается с символа вертикальной черты, но эти символы потом отбрасываются вместе с начальными пробелами. 2.4. Получение длины строкиДля получения длины строки служит метод length. У него есть синоним size. str1 = "Карл" x = str1.length # 4 str2 = "Дойль" x = str2.size # 5 2.5. Построчная обработкаСтрока в Ruby может содержать символы новой строки. Например, можно прочитать в память файл и сохранить его в виде одной строки. Применяемый по умолчанию итератор each в этом случае перебирает отдельные строки: str = "Когда-то\nдавным-давно...\nКонец\n" num = 0 str.each do |line| num += 1 print "Строка #{num}: #{line}" end Выполнение этого кода дает следующий результат: Строка 1: Когда-то Строка 2: давным-давно... Строка 3: Конец Альтернативно можно было бы воспользоваться методом each_with_index. 2.6. Побайтовая обработкаПоскольку на момент написания этой книги язык Ruby еще не поддерживал интернационализацию в полной мере, то символ и байт —по существу одно и то же. Для последовательной обработки символов пользуйтесь итератором each_byte: str = "ABC" str.each_byte {|char| print char, " " } #Результат: 65 66 67. В текущей версии Ruby строку можно преобразовать в массив односимвольных строк с помощью метода scan, которому передается простое регулярное выражение, соответствующее одному символу: str = "ABC" chars = str.scan(/./) chars.each {|char| print char, " " } #Результат: ABC. 2.7. Специализированное сравнение строкВ язык Ruby уже встроен механизм сравнения строк: строки сравниваются в привычном лексикографическом порядке (то есть на основе упорядочения, присущего данному набору символов). Но при желании можно задать собственные правила сравнения любой сложности. Предположим, например, что мы хотим игнорировать английские артикли a, an и the, если они встречаются в начале строки, а также не обращать внимания на большинство знаков препинания. Для этого следует переопределить встроенный метод <=>(он вызывается из методов <, <=, >и >=). В листинге 2.1 показано, как это сделать. Листинг 2.1. Специализированное сравнение строк class String alias old_compare <=> def <=>(other) a = self.dup b = other.dup # Удалить знаки препинания. a.gsub!(/[\,\.\?\!\:\;]/, "") b.gsub!(/[\,\.\?\!\:\;]/, "") # Удалить артикли из начала строки. a.gsub!(/^(a |an | the )/i, "") b.gsub!(/^(a |an | the )/i, "") # Удалить начальные и хвостовые пробелы. a.strip! b.strip! # Вызвать старый метод <=>. # a.old_compare(b) end end title1 = "Calling All Cars" title2 = "The Call of the Wild" # При стандартном сравнении было бы напечатано "yes". if title1 < title2 puts "yes" else puts "no" # А теперь печатается "no". end Обратите внимание, что мы «сохранили» старый метод <=>с помощью ключевого слова aliasи в конце вызвали его. Если бы мы вместо этого воспользовались методом <, то был бы вызван новый метод <=>, что привело бы к бесконечной рекурсии и в конечном счете к аварийному завершению программы. Отметим также, что оператор ==не вызывает метод <=>(принадлежащий классу-примеси Comparable). Это означает, что для специализированной проверки строк на равенство пришлось бы отдельно переопределить метод ==. Но в рассмотренном случае в этом нет необходимости. Допустим, что мы хотим сравнивать строки без учета регистра. Для этого есть встроенный метод casecmp; надо лишь добиться, чтобы он вызывался при сравнении. Вот как это можно сделать: class String def <=>(other) casecmp(other) end end Есть и более простой способ: class String alias <=> casecmp(other) end Но это не все. Надо еще переопределить оператор ==, чтобы он вел себя точно так же: class String def ==(other) casecmp(other) == 0 end end Теперь все строки будут сравниваться без учета регистра. И при всех операциях сортировки, которые определены в терминах метода <=>, регистр тоже не будет учитываться. 2.8. Разбиение строки на лексемыМетод splitразбивает строку на части и возвращает массив лексем. Ему передаются два параметра: разделитель и максимальное число полей (целое). По умолчанию разделителем является пробел, а точнее, значение специальной переменной $;или ее англоязычного эквивалента $FIELD_SEPARATOR. Если же первым параметром задана некоторая строка, то она и будет использоваться в качестве разделителя лексем. s1 = "Была темная грозовая ночь." words = s1.split # ["Была", "темная", "грозовая", "ночь] s2 = "яблоки, груши, персики" list = s2.split(", ") # ["яблоки", "груши", "персики"] s3 = "львы и тигры и медведи" zoo = s3.split(/ и /) # ["львы", "тигры", "медведи"] Второй параметр ограничивает число возвращаемых полей, при этом действуют следующие правила: 1. Если параметр опущен, то пустые поля в конце отбрасываются. 2. Если параметр — положительное число, то будет возвращено не более указанного числа полей (если необходимо, весь «хвост» строки помещается в последнее поле). Пустые поля в конце сохраняются. 3. Если параметр — отрицательное число, то количество возвращаемых полей не ограничено, а пустые поля в конце сохраняются. Ниже приведены примеры: str = "alpha,beta,gamma,," list1 = str.split(",") # ["alpha","beta","gamma"] list2 = str.split(",",2) # ["alpha", "beta,gamma,,"] list3 = str.split(",",4) # ["alpha", "beta", "gamma", ","] list4 = str.split(",",8) # ["alpha", "beta", "gamma", "", "") list5 = str.split(",",-1) # ["alpha", "beta", "gamma", "", ""] Для сопоставления строки с регулярным выражением или с другой строкой служит метод scan: str = "I am a leaf on the wind..." # Строка интерпретируется буквально, а не как регулярное выражение. arr = str.scan("а") # ["а","а","а"] # При сопоставлении с регулярным выражением возвращаются все соответствия. arr = str.scan(/\w+/) # ["I", "am", "a", "leaf", "on", "the", "wind"] # Можно задать блок. str.scan(/\w+/) {|x| puts x } Класс StringScannerиз стандартной библиотеки отличается тем, что сохраняет состояние сканирования, а не выполняет все за один раз: require 'strscan' str = "Смотри, как я парю!" ss = StringScanner.new(str) loop do word = ss.scan(/\w+/) # Получать по одному слову. break if word.nil? puts word sep = ss.scan(/\W+/) # Получить следующий фрагмент, # не являющийся словом. break if sep.nil? end 2.9. Форматирование строкВ Ruby, как и в языке С, для этой цели предназначен метод sprintf. Он принимает строку и список выражений, а возвращает строку. Набор спецификаторов в форматной строке мало чем отличается от принятого в функции sprintf(или printf) из библиотеки С. name = "Боб" age =28 str = sprintf("Привет, %s... Похоже, тебе %d лет.", name, age) Спрашивается, зачем нужен этот метод, если можно просто интерполировать значения в строку с помощью конструкции #{expr}? А затем, что sprintfпозволяет выполнить дополнительное форматирование - например, задать максимальную ширину поля или максимальное число цифр после запятой, добавить или подавить начальные нули, выровнять строки текста по левой или правой границе и т.д. str = sprintf("%-20s %3d", name, age) В классе Stringесть еще метод %, который делает почти то же самое. Он принимает одно значение или массив значений любых типов: str = "%-20s %3d" % [name, age] # To же, что и выше Имеются также методы ljust, rjustи center; они принимают длину результирующей строки и дополняют ее до указанной длины пробелами, если это необходимо. str = "Моби Дик" s1 = str.ljust(12) # "Моби Дик" s2 = str.center(12) # " Моби Дик " s3 = str.rjust(12) # " Моби Дик" Можно задать и второй параметр, который интерпретируется как строка заполнения (при необходимости она будет урезана): str = "Капитан Ахав" s1 = str.ljust(20,"+") # "Капитан Ахав++++++++" s2 = str.center(20,"-") # "----Капитан Ахав----" s3 = str.rjust(20,"123") # "12312312Капитан Ахав" 2.10. Строки в качестве объектов ввода/выводаПомимо методов sprintfи scanf, есть еще один способ имитировать ввод/вывод в строку: класс StringIO. Из-за сходства с объектом IOмы рассмотрим его в главе, посвященной вводу/выводу (см. раздел 10.1.24). 2.11. Управление регистромВ классе Stringесть множество методов управления регистром. В этом разделе мы приведем их краткий обзор. Метод downcaseпереводит символы всей строки в нижний регистр, а метод upcase— в верхний: s1 = "Бостонское чаепитие" s2 = s1.downcase # "бостонское чаепитие" s3 = s2.upcase # "БОСТОНСКОЕ ЧАЕПИТИЕ" Метод capitalizeпредставляет первый символ строки в верхнем регистре, а все остальные - в нижнем: s4 = s1.capitalize # "Бостонское чаепитие" s5 = s2.capitalize # "Бостонское чаепитие" s6 = s3.capitalize # "Бостонское чаепитие" Метод swapcaseизменяет регистр каждой буквы на противоположный: s7 = "ЭТО БЫВШИЙ попугай." s8 = s7.swapcase # "это бывший ПОПУГАЙ." Начиная с версии 1.8, в язык Ruby включен метод casecmp, который работает аналогично стандартному методу <=>, но игнорирует регистр: n1 = "abc".casecmp("xyz") # -1 n2 = "abc".casecmp("XYZ") # -1 n3 = "ABC".casecmp("xyz") # -1 n4 = "ABC".casecmp("abc") # 0 n5 = "xyz".casecmp("abc") # 1 У каждого из перечисленных методов имеется аналог, осуществляющий модификацию «на месте» ( upcase!, downcase!, capitalize!, swapcase!). He существует встроенных методов, позволяющих узнать регистр буквы, но это легко сделать с помощью регулярных выражений: if string=~ /[a-z]/ puts "строка содержит символы в нижнем регистре" end if string =~ /[A-Z]/ puts "строка содержит символы в верхнем регистре" end if string =~ /[A-Z]/ and string =~ /а-z/ puts "строка содержит символы в разных регистрах" end if string[0..0] =~ /[A-Z]/ puts "строка начинается с прописной буквы" end Отметим, что все эти методы не учитывают местные особенности (locale). 2.12. Вычленение и замена подстрокВ Ruby к подстрокам можно обращаться разными способами. Обычно применяются квадратные скобки, как для массивов, но внутри скобок может находиться пара объектов класса Fixnum, диапазон, регулярное выражение или строка. Ниже мы рассмотрим все варианты. Если задана пара объектов класса Fixnum, то они трактуются как смещение от начала строки и длина, а возвращается соответствующая подстрока. str = "Шалтай-Болтай" sub1 = str[7,4] # "Болт" sub2 = str[7,99] # "Болтай" (выход за границу строки допускается) sub3 = str[10,-4] # nil (отрицательная длина) Важно помнить, что это именно смещение и длина (число символов), а не начальное и конечное смещение. Если индекс отрицателен, то отсчет ведется от конца строки. В этом случае индекс начинается с единицы, а не с нуля. Но при нахождении подстроки указанной длины все равно берутся символы правее, а не левее начального: str1 = "Алиса" sub1 = str1[-3,3] # "иса" str2 = "В Зазеркалье" sub3 = str2[-8,6] # "зеркал" Можно задавать диапазон. Он интерпретируется как диапазон позиций внутри строки. Диапазон может включать отрицательные числа, но в любом случае нижняя граница не должна быть больше верхней. Если диапазон «инвертированный» или нижняя граница оказывается вне строки, возвращается nil: str = "Уинстон Черчилль" sub1 = str[8..13] # "Черчил" sub2 = str[-4..-1] # "илль" sub3 = str[-1..-4] # nil sub4 = str[25..30] # nil Если задано регулярное выражение, то возвращается строка, соответствующая образцу. Если соответствия нет, возвращается nil: str = "Alistair Cooke" sub1 = str[/1..t/] # "list" sub2 = str[/s.*r/] # "stair" sub3 = str[/foo/] # nil Если задана строка, то она и возвращается, если встречается в качестве подстроки в исходной строке; в противном случае возвращается nil: str = "theater" sub1 = str["heat"] # "heat" sub2 = str["eat"] # "eat" sub3 = str["ate"] # "ate" sub4 = str["beat"] # nil sub5 = str["cheat"] # nil Наконец, в тривиальном случае, когда в качестве индекса задано одно число Fixnum, возвращается ASCII-код символа в соответствующей позиции (или nil, если индекс выходит за границы строки): str = "Aaron Burr" ch1 = str[0] # 65 ch1 = str[1] # 97 ch3 = str[99] # nil Важно понимать, что все описанные выше способы могут использоваться не только для доступа к подстроке, но и для ее замены: str1 = "Шалтай-Болтай" str1[7,3] = "Хва" # "Шалтай-Хватай" str2 = "Алиса" str2[-3,3] = "ександра" # "Александра" str3 = "В Зазеркалье" str3[-9,9] = "стеколье" # "В Застеколье" str4 = "Уинстон Черчилль" str4[8..11] = "X" # "Уинстон Хилль" str5 = "Alistair Cooke" str5[/e$/] ="ie Monster" # "Alistair Cookie Monster" str6 = "theater" str6["er"] = "re" # "theatre" str7 = "Aaron Burr" str7[0] = 66 # "Baron Burr" Присваивание выражения, равного nil, не оказывает никакого действия. 2.13. Подстановка в строкахМы уже видели, как выполняются простые подстановки. Методы subи gsubпредоставляют более развитые средства, основанные на сопоставлении с образцом. Имеются также варианты sub!и gsub!, позволяющие выполнить подстановку «на месте». Метод subзаменяет первое вхождение строки, соответствующей образцу, другой строкой или результатом вычисления блока: s1 = "spam, spam, and eggs" s2 = s1.sub(/spam/,"bacon") # "bacon, spam, and eggs" s3 = s2.sub(/(\w+), (\w+),/,'\2, \1,') # "spam, bacon, and eggs" s4 = "Don't forget the spam." s5 = s4.sub(/spam/) { |m| m.reverse } # "Don't forget the maps." s4.sub!(/spam/) { |m| m.reverse } # s4 теперь равно "Don't forget the maps." Как видите, в подставляемой строке могут встречаться специальные символы \1, \2и т.д. Но такие специальные переменные, как $&(или ее англоязычная версия $MATCH), не допускаются. Если употребляется форма с блоком, то допустимы и специальные переменные. Если вам нужно лишь получить сопоставленную с образцом строку, то она будет передана в блок как параметр. Если эта строка вообще не нужна, то параметр, конечно, можно опустить. Метод gsub(глобальная подстановка) отличается от subлишь тем, что заменяются все вхождения, а не только первое: s5 = "alfalfa abracadabra" s6 = s5.gsub(/a[bl]/,"xx")# "xxfxxfa xxracadxxra" s5.gsub!(/[lfdbr]/) { |m| m.upcase + "-" } # s5 теперь равно "aL-F-aL-F-a aB-R-acaD-aB-R-a" Метод Regexp.last_matchэквивалентен действию специальной переменной $&(она же $MATCH). 2.14. Поиск в строкеПомимо различных способов доступа к подстрокам, есть и другие методы поиска в строке. Метод indexвозвращает начальную позицию заданной подстроки, символа или регулярного выражения. Если подстрока не найдена, возвращается nil: str = "Albert Einstein" pos1 = str.index(?E) # 7 pos2 = str.index("bert") # 2 pos3 = str.index(/in/) # 8 pos4 = str.index(?W) # nil pos5 = str.index("bart") # nil pos6 = str.index(/Wein/) # nil Метод rindexначинает поиск с конца строки. Но номера позиций отсчитываются тем не менее от начала: str = "Albert Einstein" pos1 = str.rindex(?E) # 7 pos2 = str.rindex("bert") # 2 pos3 = str.rindex(/in/) # 13 (найдено самое правое соответствие) pos4 = str.rindex(?W) # nil pos5 = str.rindex("bart") # nil pos6 = str.rindex(/wein/) # nil Метод include?сообщает, встречается ли в данной строке указанная подстрока или один символ: str1 = "mathematics" flag1 = str1.include? ?e # true flag2 = str1.include? "math" # true str2 = "Daylight Saving Time" flag3 = str2.include? ?s # false flag4 = str2.include? "Savings" # false Метод scanмногократно просматривает строку в поисках указанного образца. Будучи вызван внутри блока, он возвращает массив. Если образец содержит несколько (заключенных в скобки) групп, то массив окажется вложенным: str1 = "abracadabra" sub1 = str1.scan(/а./) # sub1 теперь равно ["ab","ас","ad","ab"] str2 = "Acapulco, Mexico" sub2 = str2.scan(/(.)(c.)/) # sub2 теперь равно [ ["A","ca"], ["l","со"], ["i","со"] ] Если при вызове задан блок, то метод поочередно передает этому блоку найденные значения: str3 = "Kobayashi" str3.scan(/["aeiou]+[aeiou]/) do |x| print "Слог: #{x}\n" end Этот код выводит такой результат: Слог: Ko Слог: ba Слог: уа Слог: shi 2.15. Преобразование символов в коды ASCII и обратноВ Ruby символ представляется целым числом. Это поведение изменится в версии 2.0, а возможно и раньше. В будущем предполагается хранить символы в виде односимвольных строк. str = "Martin" print str[0] # 77 Если в конец строки дописывается объект типа Fixnum, то он предварительно преобразуется в символ: str2 = str << 111 # "Martino" 2.16. Явные и неявные преобразованияНа первый взгляд, методы to_sи to_strмогут вызвать недоумение. Ведь оба преобразуют объект в строковое представление, так? Но есть и различия. Во-первых, любой объект в принципе можно как-то преобразовать в строку, поэтому почти все системные классы обладают методом to_s. Однако метод to_strв системных классах не реализуется никогда. Как правило, метод to_strприменяется для объектов, очень похожих на строки, способных «замаскироваться» под строку. В общем, можете считать, что метод to_s— это явное преобразование, а метод to_str— неявное. Я уже сказал, что ни в одном системном классе не определен метод to_str(по крайней мере, мне о таких классах неизвестно). Но иногда они вызывают to_str(если такой метод существует в соответствующем классе). Первое, что приходит на ум, — подкласс класса String; но на самом деле объект любого класса, производного от String, уже является строкой, так что определять метод to_strизлишне. А вот пример из реальной жизни. Класс Pathnameопределен для удобства работы с путями в файловой системе (например, конкатенации). Но путь естественно отображается на строку (хотя и не наследует классу String). require 'pathname' path = Pathname.new("/tmp/myfile") name = path.to_s # "/tmp/myfile" name = path.to_str # "/tmp/myfile" (Ну и что?) # Вот где это оказывается полезно... heading = "Имя файла равно " + path puts heading# " Имя файла равно /tmp/myfile" В этом фрагменте мы просто дописали путь в конец обычной строки "Имя файла равно". Обычно такая операция приводит к ошибке во время выполнения, поскольку оператор +ожидает, что второй операнд — тоже строка. Но так как в классе Pathnameесть метод to_str, то он вызывается. Класс Pathname«маскируется» под строку, то есть может быть неявно преобразован в String. На практике методы to_sи to_strобычно возвращают одно и то же значение, но это необязательно. Неявное преобразование должно давать «истинное строковое значение» объекта, а явное можно расценивать как «принудительное» преобразование. Метод putsобращается к методу to_sобъекта, чтобы получить его строковое представление. Можно считать, что это неявный вызов явного преобразования. То же самое справедливо в отношении интерполяции строк. Вот пример: class Helium def to_s "He" end def to_str "гелий" end end e = Helium.new print "Элемент " puts e # Элемент He. puts "Элемент " + e # Элемент гелий. puts "Элемент #{e}" # Элемент He. Как видите, разумное определение этих методов в собственном классе может несколько повысить гибкость применения. Но что сказать об идентификации объектов, переданных методам вашего класса? Предположим, например, что вы написали метод, который ожидает в качестве параметра объект String. Вопреки философии «утипизации», так делают часто, и это вполне оправдано. Например, предполагается, что первый параметр метода File.new— строка. Решить эту проблему просто. Если вы ожидаете на входе строку, проверьте, имеет ли объект метод to_str, и при необходимости вызывайте его. def set_title(title) if title.respond_to? :to_str title = title.to_str end # ... end Ну а если объект не отвечает на вызов метода to_str? Есть несколько вариантов действий. Можно принудительно вызвать метод to_s; можно проверить, принадлежит ли объект классу Stringили его подклассу; можно, наконец, продолжать работать, понимая, что при попытке выполнить операцию, которую объект не поддерживает, мы получим исключение ArgumentError. Короткий путь к цели выглядит так: title = title.to_str rescue title Он опирается на тот факт, что при отсутствии реализации метода to_strвозникнет исключение. Разумеется, модификаторы rescueмогут быть вложенными: title = title.to_str rescue title.to_s rescue title # Обрабатывается маловероятный случай, когда отсутствует даже метод to_s. С помощью неявного преобразования можно было бы сделать строки и числа практически эквивалентными: class Fixnum def to_str self.to_s end end str = "Число равно " + 345 # Число равно 345. Но я не рекомендую так поступать: «много хорошо тоже нехорошо». В Ruby, как и в большинстве языков, строки и числа — разные сущности. Мне кажется, что ясности ради преобразования, как правило, должны быть явными. И еще: в методе to_strнет ничего волшебного. Предполагается, что он возвращает строку, но если вы пишете такой метод сами, ответственность за то, что он действительно так и поступает, ложится на вас. 2.17. Дописывание в конец строкиДля конкатенации строк применяется оператор <<. Он «каскадный», то есть позволяет выполнять подряд несколько операций над одним и тем же операндом-приемником. str = "А" str << [1,2,3].to_s << " " << (3.14).to_s # str теперь равно "А123 3.14". Если число типа Fixnumпринадлежит диапазону 0..255, то оно будет преобразовано в символ: str = "Marlow" str << 101 << ", Christopher" # str теперь равно "Marlowe, Christopher". 2.18. Удаление хвостовых символов новой строки и прочихЧасто бывает необходимо удалить лишние символы в конце строки. Типичный пример — удаление символа новой строки после чтения строки из внешнего источника. Метод chopудаляет последний символ строки (обычно это символ новой строки). Если перед символом новой строки находится символ перевода каретки ( \r), он тоже удаляется. Причина такого поведения заключается в том, что разные операционные системы неодинаково трактуют понятие «новой строки». В UNIX-подобных системах новая строка представляется символом \n. А в DOS и Windows для этой цели используется пара символов \r\n. str = gets.chop # Прочитать, удалить символ новой строки. s2 = "Some string\n" # "Some string" (нет символа новой строки). s3 = s2.chop! # s2 теперь тоже равно "Some string". s4 = "Other string\r\n" s4.chop! # "Other string" (нет символа новой строки). Обратите внимание, что при вызове варианта chop!операнд-источник модифицируется. Важно еще отметить, что последний символ удаляется, даже если это не символ новой строки: str = "abcxyz" s1 = str.chop # "abcxy" Поскольку символ новой строки присутствует не всегда, иногда удобнее применять метод chomp: str = "abcxyz" str2 = "123\n" str3 = "123\r" str4 = "123\r\n" s1 = str.chomp # "abcxyz" s2 = str2.chomp # "123" # Если установлен стандартный разделитель записей, то удаляется не только # \n, но также \r и \r\n. s3 = str3.chomp # "123" s4 = str4.chomp # "123" Как и следовало ожидать, имеется также метод chomp!для замены «на месте». Если методу chompпередана строка-параметр, то удаляются перечисленные в ней символы, а не подразумеваемый по умолчанию разделитель записей. Кстати, если разделитель записей встречается в середине строки, то он не удаляется: str1 = "abcxyz" str2 = "abcxyz" s1 = str1.chomp("yz") # "abcx" s2 = str2.chomp("x") # "abcxyz" 2.19. Удаление лишних пропусковМетод stripудаляет пропуски в начале и в конце строки, а вариант strip!делает то же самое «на месте». str1 = "\t \nabc \t\n" str2 = str1.strip # "abc" str3 = str1.strip! # "abc" #str1 теперь тоже равно "abc". Под пропусками, как обычно, понимаются пробелы, символы табуляции и перевода на новую строку. Чтобы удалить пропуски только в начале или только в конце строки, применяйте методы lstripи rstrip: str = " abc " s2 = str.lstrip # "abc " s3 = str.rstrip # " abc" Имеются также варианты lstrip!и rstrip!для удаления «на месте». 2.20. Повтор строкВ Ruby оператор (или метод) умножения перегружен так, что в применении к строкам выполняет операцию повторения. Если строку умножить на n, то получится строка, состоящая из n конкатенированных копий исходной: etc = "Etc. "*3 # "Etc. Etc. Etc. " ruler = " + " + (". "*4+"5" + "."*4+" + ")*3 # "+....5....+....5....+....5....+" 2.21. Включение выражений в строкуЭто легко позволяет сделать синтаксическая конструкция #{}. Нет нужды думать о преобразовании, добавлении и конкатенации; нужно лишь интерполировать переменную или другое выражение в любое место строки: puts "#{temp_f} по Фаренгейту равно #{temp_c} по Цельсию" puts "Значение определителя равно #{b*b — 4*а*с}." puts "#{word} это #{word.reverse} наоборот." Внутри фигурных скобок могут находиться даже полные предложения. При этом возвращается результат вычисления последнего выражения. str = "Ответ равен #{ def factorial(n) n==0 ? 1 : n*factorial(n-1) end answer = factorial(3) * 7}, естественно." # Ответ равен 42, естественно. При интерполяции глобальных переменных, а также переменных класса и экземпляра фигурные скобки можно опускать: print "$gvar = #$gvar и ivar = #@ivar." Интерполяция не производится внутри строк, заключенных в одиночные кавычки (поскольку их значение не интерпретируется), но применима к заключенным в двойные кавычки встроенным документам и к регулярным выражениям. 2.22. Отложенная интерполяцияИногда желательно отложить интерполяцию значений в строку. Идеального способа решить эту задачу не существует, но можно воспользоваться блоком: str = proc {|x,у,z| "Числа равны #{x}, #{у} и #{z}" } s1 = str.call(3,4,5) # Числа равны 3, 4 и 5. s2 = str.call(7,8,9) # Числа равны 7, 8 и 9. Другое, более громоздкое решение состоит в том, чтобы сохранить строку, заключенную в одиночные кавычки, потом «обернуть» ее двойными кавычками и вычислить: str = '#{name} - мое имя, а #{nation} - моя родина' name, nation = "Стивен Дедал", "Ирландия" s1 = eval('"' + str + '"') # Стивен Дедал - мое имя, а Ирландия - моя родина. Можно также передать evalдругую функцию привязки: bind = proc do name,nation = "Гулливер Фойл", "Земля" binding end.call # Надуманный пример; возвращает привязанный контекст блока s2 = eval('"' + str + '"',bind) # Гулливер Фойл - мое имя, а Земля - моя родина. У техники работы с evalесть свои «причуды». Например, будьте осторожны, вставляя управляющие последовательности, скажем \n. 2.23. Разбор данных, разделенных запятымиДанные, разделенные запятыми, часто встречаются при программировании. Это в некотором роде «наибольший общий делитель» всех форматов обмена данными. Например, так передаются данные между несовместимыми СУБД или приложениями, которые не поддерживают никакого другого общего формата. Будем предполагать, что данные представляют собой строки и числа, а все строки заключены в кавычки. Еще предположим, что все символы должным образом экранированы (например, запятые и кавычки внутри строки). Задача оказывается простой, поскольку такой формат данных подозрительно напоминает встроенные в Ruby массивы данных разных типов. Достаточно заключить все выражение в квадратные скобки, чтобы получить массив. string = gets.chop! #Предположим, что прочитана такая строка: #"Doe, John", 35, 225, "5'10\"", "555-0123" data = eval("[" + string + "]") # Преобразовать в массив. data.each {|x| puts "Значение = #{x}"} Этот код выводит такой результат: Значение = Doe, John Значение =35 Значение =225 Значение = 5' 10" Значение = 555-0123 Более общее решение дает стандартная библиотека CSV. Есть также усовершенствованный инструмент под названием FasterCSV. Поищите его в сети, он не входит в стандартный дистрибутив Ruby. 2.24. Преобразование строки в число (десятичное или иное)Есть два основных способа преобразовать строку в число: методы Integerи Floatмодуля Kernelи методы to_iи to_fкласса String. (Имена, начинающиеся с прописной буквы, например Integer, обычно резервируются для специальных функций преобразования.) Простой случай тривиален, следующие два предложения эквивалентны: x = "123".to_i # 123 y = Integer("123") # 123 Но если в строке хранится не число, то поведение этих методов различается: x = junk".to_i # Молча возвращает 0. y = Integer("junk") # Ошибка. Метод to_iпрекращает преобразование, как только встречает первый символ, не являющийся цифрой, а метод Integerв этом случае возбуждает исключение: x = "123junk".to_i # 123 y = Integer("123junk") # Ошибка. Оба метода допускают наличие пропусков в начале и в конце строки: x = " 123 ".to_i # 123 y = Integer(" 123 ") # 123 Преобразование строки в число с плавающей точкой работает аналогично: x = "3.1416".to_f # 3.1416 y = Float("2.718") # 2.718 Оба метода понимают научную нотацию: x = Float("6.02е23") # 6.02е23 y = "2.9979246е5".to_f # 299792.46 Методы to_iи Integerтакже по-разному относятся к системе счисления. По умолчанию, естественно, подразумевается система по основанию 10, но другие тоже допускаются (это справедливо и для чисел с плавающей точкой). Говоря о преобразовании из одной системы счисления в другую, мы всегда имеем в виду строки. Ведь целое число неизменно хранится в двоичном виде. Следовательно, преобразование системы счисления — это всегда преобразование одной строки в другую. Здесь мы рассмотрим преобразование из строки (обратное преобразование рассматривается в разделах 5.18 и 5.5). Числу в тексте программы может предшествовать префикс, обозначающий основание системы счисления. Префикс 0bобозначает двоичное число, 0— восьмеричное, а 0x— шестнадцатеричное. Метод Integerтакие префиксы понимает, а метод to_i— нет: x = Integer("0b111") # Двоичное - возвращает 7. y = Integer("0111") # Восьмеричное - возвращает 73. z = Integer("0x111") # Шестнадцатеричное - возвращает 291. x = "0b111".to_i # 0 y = "0111".to_i # 0 z = "0x111".to_i # 0 Однако у метода to_iесть необязательный второй параметр для указания основания. Обычно применяют только четыре основания: 2, 8, 10 (по умолчанию) и 16. Впрочем, префиксы не распознаются даже при определении основания. x = "111".to_i(2) # 7 y = "111".to_i(8) # Восьмеричное - возвращает 73. z = "111".to_i(16) # Шестнадцатеричное - возвращает 291. x = "0b111".to_i # 0 y = "0111".to_i # 0 z = "0x111".to_i # 0 Из-за «стандартного» поведения этих методов цифры, недопустимые при данном основании, обрабатываются по-разному: x = "12389".to_i(8) # 123 (8 игнорируется). y = Integer("012389") # Ошибка (8 недопустима). Хотя полезность этого и сомнительна, метод to_iпонимает основания вплоть до 36, когда в представлении числа допустимы все буквы латинского алфавита. (Возможно, это напомнило вам о base64-кодировании; дополнительную информацию по этому поводу вы найдете в разделе 2.37.) x = "123".to_i(5) # 66 y = "ruby".to_i (36) # 1299022 Для преобразования символьной строки в число можно также воспользоваться методом scanfиз стандартной библиотеки, которая добавляет его в модуль Kernel, а также классы IOи String: str = "234 234 234" x, y, z = str.scanf("%d %o %x") # 234, 156, 564 Метод scanfреализует всю имеющую смысл функциональность стандартных функций scanf, sscanfи fscanfиз библиотеки языка С. Но строки, представляющие двоичные числа, он не обрабатывает. 2.25. Кодирование и декодирование строк в кодировке rot13Rot13 — наверное, самый слабый из известных человечеству шифров. Исторически он просто препятствовал «случайному» прочтению текста. Он часто встречается в конференциях Usenet; например, так можно закодировать потенциально обидную шутку или сценарий фильма «Звездные войны. Эпизод 13» накануне премьеры. Принцип кодирования состоит в смещении символов относительно начала алфавита (латинского) на 13: А превращается в N, В — в О и т.д. Строчные буквы смещаются на ту же величину; цифры, знаки препинания и прочие символы игнорируются. Поскольку 13 — это ровно половина от 26 (число букв в латинском алфавите), то функция является обратной самой себе, то есть ее повторное применение восстанавливает исходный текст. Ниже приведена реализация этого метода, добавленного в класс String, никаких особых комментариев она не требует: class String def rot13 self.tr("A-Ma-mN-Zn-z","N-Zn-zA-Ma-m") end end joke = "Y2K bug" joke13 = joke.rot13 # "L2X oht" episode2 = "Fcbvyre: Naanxva qbrfa'g trg xvyyrq." puts episode2.rot13 2.26. Шифрование строкИногда нежелательно, чтобы строки можно было легко распознать. Например, пароли не следует хранить в открытом виде, какими бы ограничительными ни были права доступа к файлу. В стандартном методе crypt применяется стандартная функция с тем же именем для шифрования строки по алгоритму DES. Она принимает в качестве параметра «затравку» (ее назначение то же, что у затравки генератора случайных чисел). На платформах, отличных от UNIX, параметр может быть иным. Ниже показано тривиальное приложение, которое запрашивает пароль, знакомый любителям Толкиена: coded = "hfCghHIE5LAM." puts "Говори, друг, и жми Enter!" print "Пароль: " password = gets.chop if password.crypt("hf") == coded puts "Добро пожаловать!" else puts "Кто ты, орк?" end Стоит отметить, что на такое шифрование не стоит полагаться в серверных Web-приложениях, поскольку пароль, введенный в поле формы, все равно передаётся по сети в открытом виде. В таких случаях проще всего воспользоваться протоколом SSL (Secure Sockets Layer). Разумеется, никто не запрещает пользоваться шифрованием на сервере, но по другой причине — чтобы защитить пароль в хранилище, а не во время передачи по сети. 2.27. Сжатие строкДля сжатия строк и файлов применяется библиотека Zlib. Зачем может понадобиться сжимать строки? Возможно, чтобы ускорить ввод/вывод из базы данных, оптимизировать использование сети или усложнить распознавание строк. В классах Deflateи Inflateимеются методы класса deflateи inflateсоответственно. У метода deflate(он выполняет сжатие) есть дополнительный параметр, задающий режим сжатия. Он определяет компромисс между качеством сжатия и скоростью. Если значение равно BEST_COMPRESSION, то строка сжимается максимально, но это занимает сравнительно много времени. Значение BEST_SPEEDзадает максимальную скорость, но при этом строка сжимается хуже. Подразумеваемое по умолчанию значение DEFAULT_COMPRESSIONвыбирает компромиссный режим. require 'zlib' include Zlib long_string = ("abcde"*71 + "defghi"*79 + "ghijkl"*113)*371 # long_string состоит из 559097 символов. s1 = Deflate.deflate(long_string,BEST_SPEED) # 4188 символов. s3 = Deflate.deflate(long_string) # 3568 символов s2 = Deflate.deflate(long_string,BEST_COMPRESSION) # 2120 символов Неформальные эксперименты показывают, что скорость отличается примерно в два раза, а плотность сжатия — в обратной пропорции на ту же величину. И скорость, и плотность сильно зависят от состава строки. Разумеется, на скорость влияет и имеющееся оборудование. Имейте в виду, что существует пороговое значение длины строки. Если строка короче, то сжимать ее практически бесполезно (если только вы не хотите сделать ее нечитаемой). В этом случае неизбежные накладные расходы могут даже привести к тому, что сжатая строка окажется длиннее исходной. 2.28. Подсчет числа символов в строкеМетод countподсчитывает число вхождений в строку символов из заданного набора: s1 = "abracadabra" a = s1.count("с") # 1 b = s1.count("bdr") # 5 Строковый параметр ведет себя как простое регулярное выражение. Если он начинается с символа ^, то берется дополнение к списку: c = s1.count("^а") # 6 d = s1.count ("^bdr") # 6 Дефис обозначает диапазон символов: e = s1.count("a-d") # 9 f = s1.count("^a-d") # 2 2.29. Обращение строкиДля обращения строки служит метод reverse(или его вариант для обращения «на месте» reverse!): s1 = "Star Trek" s2 = s1.reverse # "kerT ratS" si.reverse! # si теперь равно "kerT ratS" Пусть требуется обратить порядок слов (а не символов). Тогда можно сначала воспользоваться методом String#split, который вернет массив слов. В классе Arrayтоже есть метод reverse, поэтому можно обратить массив, а затем с помощью метода joinобъединить слова в новую строку: phrase = "Now here's a sentence" phrase.split(" ").reverse.join(" ") # "sentence a here's Now" 2.30. Удаление дубликатовЦепочки повторяющихся символов можно сжать до одного методом squeeze: s1 = "bookkeeper" s2 = s1.squeeze # "bokeper" s3 = "Hello..." s4 = s3.squeeze # "Helo." Если указан параметр, то будут удаляться только дубликаты заданных в нем символов: s5 = s3.squeeze(".") # "Hello." Этот параметр подчиняется тем же правилам, что и параметр метода count(см. раздел 2.28), то есть допускаются дефис и символ ^. Имеется также метод squeeze!. 2.31. Удаление заданных символовМетод deleteудаляет из строки те символы, которые включены в список, переданный в качестве параметра: s1 = "To be, or not to be" s2 = s1.delete("b") # "To e, or not to e" s3 = "Veni, vidi, vici!" s4 = s3.delete(",!") # "Veni vidi vici" Этот параметр подчиняется тем же правилам, что и параметр метода count(см. раздел 2.28), то есть допускаются символы -(дефис) и ^(каре). Имеется также метод delete!. 2.32. Печать специальных символовМетод dumpпозволяет получить графическое представление символов, которые обычно не печатаются вовсе или вызывают побочные эффекты: s1 = "Внимание" << 7 << 7 << 7 # Добавлено три символа ASCII BEL. puts s1.dump # Печатается: Внимание\007\007\007 s2 = "abc\t\tdef\tghi\n\n" puts s2.dump # Печатается: abc\t\tdef\tghi\n\n s3 = "Двойная кавычка: \"" puts s3.dump # Печатается: Двойная кавычка: \" При стандартном значении переменной $KCODEметод dumpдает такой же эффект, как вызов метода inspectдля строки. Переменная $KCODEрассматривается в главе 4. 2.33. Генерирование последовательности строкИзредка бывает необходимо получить «следующую» строку. Так, следующей для строки "aaa"будет строка "aab"(затем "aac", "aad"и так далее). В Ruby для этой цели есть метод succ: droid = "R2D2" improved = droid.succ # "R2D3" pill = "Vitamin B" pill2 = pill.succ # "Vitamin C" He рекомендуется применять этот метод, если точно не известно, что начальное значение предсказуемо и разумно. Если начать с какой-нибудь экзотической строки, то рано или поздно вы получите странный результат. Существует также метод upto, который в цикле вызывает succ, пока не будет достигнуто конечное значение: "Files, A".upto "Files, X" do | letter | puts "Opening: #{letter}" end # Выводится 24 строки. Еще раз подчеркнем, что эта возможность используется редко, да и то на ваш страх и риск. Кстати, метода, возвращающего «предшествующую» строку, не существует. 2.34. Вычисление 32-разрядного CRCКонтрольный код циклической избыточности (Cyclic Redundancy Checksum, CRC) — хорошо известный способ получить «сигнатуру» файла или произвольного массива байтов. CRC обладает тем свойством, что вероятность получения одинакового кода для разных входных данных равна 1/2**N, где N — число битов результата (чаще всего 32). Вычислить его позволяет библиотека zlib, написанная Уэно Кацухиро (Ueno Katsuhiro). Метод crc32вычисляет CRC для строки, переданной в качестве параметра. require 'zlib' include Zlib crc = crc32("Hello") # 4157704578 crc = crc32(" world!",crc) # 461707669 crc = crc32("Hello world!") # 461707669 (то же, что и выше) В качестве необязательного второго параметра можно передать ранее вычисленный CRC. Результат получится такой, как если бы конкатенировать обе строки и вычислить CRC для объединения. Это полезно, например, когда нужно вычислить CRC файла настолько большого, что прочитать его можно только по частям. 2.35. Вычисление МD5-свертки строкиАлгоритм MD5 вырабатывает 128-разрядный цифровой отпечаток или дайджест сообщения произвольной длины. Это разновидность свертки, то есть функция шифрования односторонняя, так что восстановить исходное сообщение по дайджесту невозможно. Для Ruby имеется расширение, реализующее MD5; интересующиеся могут найти его в каталоге ext/md5стандартного дистрибутива. Для создания нового объекта MD5 есть два эквивалентных метода класса: newи md5: require 'md5' hash = MD5.md5 hash = MD5.new Есть также четыре метода экземпляра: clone, digest, hexdigestи update. Метод cloneпросто копирует существующий объект, а метод updateдобавляет новые данные к объекту: hash.update("Дополнительная информация...") Можно создать объект и передать ему данные за одну операцию: secret = MD5.new("Секретные данные") Если задан строковый аргумент, он добавляется к объекту путем обращения к методу update. Повторные обращения эквивалентны одному вызову с конкатенированными аргументами: # Эти два предложения: сryptic.update("Данные...") cryptic.update(" еще данные.") # ... эквивалентны одному такому: cryptic.update("Данные... еще данные.") Метод digestвозвращает 16-байтовую двоичную строку, содержащую 128-разрядный дайджест. Но наиболее полезен метод hexdigest, который возвращает дайджест в виде строки в коде ASCII, состоящей из 32 шестнадцатеричных символов, соответствующих 16 байтам. Он эквивалентен следующему коду: def hexdigest ret = '' digest.each_byte {|i| ret << sprintf{'%02x' , i) } ret end secret.hexdigest # "b30e77a94604b78bd7a7e64ad500f3c2" Короче говоря, для получения MD5-свертки нужно написать: require 'md5' m = MD5.new("Секретные данные").hexdigest 2.36. Вычисление расстояния Левенштейна между двумя строкамиРасстояние между строками важно знать в индуктивном обучении (искусственный интеллект), криптографии, исследовании структуры белков и других областях. Расстоянием Левенштейна называется минимальное число элементарных модификаций, которым нужно подвергнуть одну строку, чтобы преобразовать ее в другую. Элементарными модификациями называются следующие операции: del(удаление одного символа), ins(замена символа) и sub(замена символа). Замену можно также считать комбинацией удаления и вставки ( indel). Существуют разные подходы к решению этой задачи, но не будем вдаваться в технические детали. Достаточно знать, что реализация на Ruby (см. листинг 2.2) позволяет задавать дополнительные параметры, определяющие стоимость всех трех операций модификации. По умолчанию за базовую принимается стоимость одной операции indel(стоимость вставки = стоимость удаления). Листинг 2.2. Расстояние Левенштейна class String def levenshtein(other, ins=2, del=2, sub=1) # ins, del, sub - взвешенные стоимости. return nil if self.nil? return nil if other.nil? dm = [] # Матрица расстояний. # Инициализировать первую строку. dm[0] = (0..self.length).collect { |i| i * ins } fill = [0] * (self.length - 1) # Инициализировать первую колонку. for i in 1..other.length dm[i] = [i * del, fill.flatten] end # Заполнить матрицу. for i in 1..other.length for j in 1..self.length # Главное сравнение. dm[i][j] = [ dm[i-1][j-1] + (self[j-1] == other[i-1] ? 0 : sub), dm[i][j-1] * ins, dm[i-1][j] + del ].min end end # Последнее значение в матрице и есть # расстояние Левенштейна между строками. dm[other.length][self.length] end end s1 = "ACUGAUGUGA" s2 = "AUGGAA" d1 = s1.levenshtein(s2) # 9 s3 = "Pennsylvania" s4 = "pencilvaneya" d2 = s3.levenshtein(s4) # 7 s5 = "abcd" s6 = "abcd" d3 = s5.levenshtein(s6) # 0 Определив расстояние Левенштейна, мы можем написать метод similar?, вычисляющий меру схожести строк. Например: class String def similar?(other, thresh=2) if self.levenshtein(other) < thresh true else false end end end if "polarity".similar?("hilarity") puts "Электричество - забавная штука!" end Разумеется, можно было бы передать методу similar?три взвешенные стоимости, которые он в свою очередь передал бы методу levenshtein. Но для простоты мы не стали этого делать. 2.37. base64-кодирование и декодированиеАлгоритм base64 часто применяется для преобразования двоичных данных в текстовую форму, не содержащую специальных символов. Например, в конференциях так обмениваются исполняемыми файлами. Простейший способ осуществить base64-кодирование и декодирование — воспользоваться встроенными возможностями Ruby. В классе Arrayесть метод pack, который возвращает строку в кодировке base64 (если передать ему параметр "m"). А в классе stringесть метод unpack, который декодирует такую строку: str = "\007\007\002\abdce" new_string = [str].pack("m") # "BwcCB2JkY2U=" original = new_string.unpack("m") # ["\a\a\002\abdce"] Отметим, что метод unpackвозвращает массив. 2.38. Кодирование и декодирование строк (uuencode/uudecode)Префикс uuв этих именах означает UNIX-to-UNIX. Утилиты uuencodeи uudecode— это проверенный временем способ обмена данными в текстовой форме (аналогичный base64). str = "\007\007\002\abdce" new_string = [str].pack("u") # '(P<"!V)D8V4'' original = new_string.unpack("u") # ["\a\a\002\abdce"] Отметим, что метод unpackвозвращает массив. 2.39. Замена символов табуляции пробелами и сворачивание пробелов в табуляторыБывает, что имеется строка с символами табуляции, а мы хотели бы преобразовать их в пробелы (или наоборот). Ниже показаны два метода, реализующих эти операции: class String def detab(ts=8) str = self.dup while (leftmost = str.index("\t")) != nil space = " "* (ts-(leftmost%ts)) str[leftmost]=space end str end def entab(ts=8) str = self.detab areas = str.length/ts newstr = "" for a in 0..areas temp = str[a*ts..a*ts+ts-1] if temp.size==ts if temp =~ /+/ match=Regexp.last_match[0] endmatch = Regexp.new(match+"$") if match.length>1 temp.sub!(endmatch,"\t") end end end newstr += temp end newstr end end foo = "Это всего лишь тест. " puts foo puts foo.entab(4) puts foo.entab(4).dump Отметим, что этот код не распознает символы забоя. 2.40. Цитирование текстаИногда бывает необходимо напечатать длинные строки текста, задав ширину поля. Приведенный ниже код решает эту задачу, разбивая текст по границам слов и учитывая символы табуляции (но символы забоя не учитываются, а табуляция не сохраняется): str = <<-EOF When in the Course of human events it becomes necessary for one people to dissolve the political bands which have connected them with another, and to assume among the powers of the earth the separate and equal station to which the Laws of Nature and of Nature's God entitle them, a decent respect for the opinions of mankind requires that they should declare the causes which impel them to the separation. EOF max = 20 line = 0 out = [""] input = str.gsub(/\n/, " ") words = input.split(" ") while input ! = "" word = words.shift break if not word if out[line].length + word.length > max out[line].squeeze!(" ") line += 1 out[line] = "" end out[line] << word + " " end out.each {|line| puts line} # Печатает 24 очень коротких строки. Библиотека Format решает как эту, так и много других схожих задач. Поищите ее в сети. 2.41. ЗаключениеМы обсудили основы представления строк (заключенных в одиночные или двойные кавычки). Поговорили о том, как интерполировать выражения в строку в двойных кавычках; узнали, что в таких строках допустимы некоторые специальные символы, представленные управляющими последовательностями. Кроме того, мы познакомились с конструкциями %qи %Q, которые позволяют нам по своему вкусу выбирать ограничители. Наконец, рассмотрели синтаксис встроенных документов, унаследованных из старых продуктов, в том числе командных интерпретаторов в UNIX. В этой главе были продемонстрированы все наиболее важные операции, которые программисты обычно выполняют над строками: конкатенация, поиск, извлечение подстрок, разбиение на лексемы и т.д. Мы видели, как можно кодировать строки (например, по алгоритму base64) и сжимать их. Пришло время перейти к тесно связанной со строками теме — регулярным выражениям. Регулярные выражения — это мощное средства сопоставления строк с образцами. Мы рассмотрим их в следующей главе. Примечания:7 В английском языке словом «string» обозначается как «строка», так и «струна» (Прим. перев.) |
|
||
Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх |
||||
|