• 2.1. Представление обычных строк
  • 2.2. Альтернативная нотация для представления строк
  • 2.3. Встроенные документы
  • 2.4. Получение длины строки
  • 2.5. Построчная обработка
  • 2.6. Побайтовая обработка
  • 2.7. Специализированное сравнение строк
  • 2.8. Разбиение строки на лексемы
  • 2.9. Форматирование строк
  • 2.10. Строки в качестве объектов ввода/вывода
  • 2.11. Управление регистром
  • 2.12. Вычленение и замена подстрок
  • 2.13. Подстановка в строках
  • 2.14. Поиск в строке
  • 2.15. Преобразование символов в коды ASCII и обратно
  • 2.16. Явные и неявные преобразования
  • 2.17. Дописывание в конец строки
  • 2.18. Удаление хвостовых символов новой строки и прочих
  • 2.19. Удаление лишних пропусков
  • 2.20. Повтор строк
  • 2.21. Включение выражений в строку
  • 2.22. Отложенная интерполяция
  • 2.23. Разбор данных, разделенных запятыми
  • 2.24. Преобразование строки в число (десятичное или иное)
  • 2.25. Кодирование и декодирование строк в кодировке rot13
  • 2.26. Шифрование строк
  • 2.27. Сжатие строк
  • 2.28. Подсчет числа символов в строке
  • 2.29. Обращение строки
  • 2.30. Удаление дубликатов
  • 2.31. Удаление заданных символов
  • 2.32. Печать специальных символов
  • 2.33. Генерирование последовательности строк
  • 2.34. Вычисление 32-разрядного CRC
  • 2.35. Вычисление МD5-свертки строки
  • 2.36. Вычисление расстояния Левенштейна между двумя строками
  • 2.37. base64-кодирование и декодирование
  • 2.38. Кодирование и декодирование строк (uuencode/uudecode)
  • 2.39. Замена символов табуляции пробелами и сворачивание пробелов в табуляторы
  • 2.40. Цитирование текста
  • 2.41. Заключение
  • Глава 2. Строки

    Когда-то элементарными кирпичиками мироздания считались атомы, потом протоны, потом кварки. Теперь таковыми считаются струны[7].

    (Дэвид Гросс, профессор теоретической физики,) (Принстонский университет)

    В начале 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. Кодирование и декодирование строк в кодировке rot13

    Rot13 — наверное, самый слабый из известных человечеству шифров. Исторически он просто препятствовал «случайному» прочтению текста. Он часто встречается в конференциях 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 | Добавить материал | Нашёл ошибку | Наверх