355 500 произведений, 25 200 авторов.

Электронная библиотека книг » Хэл Фултон » Программирование на языке Ruby » Текст книги (страница 18)
Программирование на языке Ruby
  • Текст добавлен: 24 сентября 2016, 06:40

Текст книги "Программирование на языке Ruby"


Автор книги: Хэл Фултон



сообщить о нарушении

Текущая страница: 18 (всего у книги 56 страниц) [доступный отрывок для чтения: 20 страниц]

6.2.3. Обход диапазона

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

(3..6).each {|x| puts x } # Печатаются четыре строки

                          # (скобки обязательны).

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

r1 = "7".."9"

r2 = "7".."10"

r1.each {|x| puts x } # Печатаются три строки.

r2.each {|x| puts x } # Ничего не печатается!

Предыдущие примеры похожи, но ведут себя по-разному. Отчасти причина в том, что границы второго диапазона – строки разной длины. Мы ожидаем, что в диапазон входят строки "7", "8", "9" и "10", но что происходит на самом деле?

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

А что сказать по поводу диапазонов чисел с плавающей точкой? Такой диапазон можно сконструировать и, конечно, проверить, попадает ли в него конкретное число. Это полезно. Но обойти такой диапазон нельзя, так как метод succ отсутствует.

fr = 2.0..2.2

fr.each {|x| puts x } # Ошибка!

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

6.2.4. Проверка принадлежности диапазону

Зачем нужен диапазон, если нельзя проверить, принадлежит ли ему конкретный объект? Эта задача легко решается с помощью метода include?:

r1 = 23456..34567

x = 14142

y = 31416

r1.include?(x) # false

r1.include?(у) # true

У этого метода есть также синоним member?.

А как он работает? Как интерпретатор определяет, принадлежит ли объект диапазону? Просто путем сравнения с границами (поэтому проверка принадлежности диапазону возможна лишь, если определен осмысленный оператор <=>). Следовательно, запись (a..b).include?(x) эквивалентна x >= a and x <= b. Еще раз предупреждаем: будьте осторожны со строковыми диапазонами!

s1 = "2".."5"

str = "28"

s1.include?(str) # true (неправильно!)

6.2.5. Преобразование в массив

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

r = 3..12

arr = r.to_a # [3,4,5,6,7,8,9,10,11,12]

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

6.2.6. Обратные диапазоны

Имеет ли смысл говорить об обратном диапазоне? И да, и нет. Следующий диапазон допустим:

r = 6..3

x = r.begin # 6

y = r.end   # 3

flag = r.end_excluded? # false

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

arr = r. to_a     # []

r.each {|x| p x } # Ни одной итерации.

y = 5

r.include?(у)     # false (для любого значения y)

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

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

string = "flowery

str1 = string[0..-2]  # "flower"

str2 = string[1..-2]  # "lower"

str3 = string[-5..-3] # "owe" (по существу, прямой диапазон)

6.2.7. Оператор переключения

Диапазон в составе условия обрабатывается особым образом. В этом случае .. называется оператором переключения (flip-flop operator), поскольку это, по существу, переключатель, который сохраняет свое состояние.

Такой прием, позаимствованный из языка Perl, бывает полезен. Но понять, как он работает, довольно трудно.

Представьте себе исходный текст программы на Ruby, в который встроена документация, ограниченная маркерами =begin и =end. Как бы вы подошли к задаче отыскания и вывода этих и только этих фрагментов? (Состояние переключается между «внутри раздела» и «вне раздела», отсюда и понятие переключения.) Решение, хотя интуитивно и не очевидное, дает следующий код:

loop do

 break if eof?

 line = gets

 puts line if (line=~/=begin/)..(line=~/=end/)

end

«Волшебство» объясняется принципом работы оператора переключения.

Во-первых, надо осознать, что «диапазон» сохраняет свое состояние, хотя оно и скрыто. Когда становится истинным условие, заданное в качестве левой границы, сам диапазон принимает значение true. Он сохраняет это состояние до тех пор пока не станет истинным условие на правой границе, и в этот момент состояние переключается в false.

Такое поведение полезно во многих случаях, в частности для разбора HTML-документов или конфигурационных файлов, разбитых на разделы, выбора диапазонов элементов из списков и т.д.

Но лично мне такой синтаксис не нравится. Недовольны им и многие другие, включая и самого Маца. Возможно, в будущем эта возможность будет исключена из Ruby. Однако я покажу удобный способ реализовать ту же функциональность по-другому.

Что меня не устраивает в операторе переключения? В контексте предыдущего примера рассмотрим строку, начинающуюся с маркера =begin. Напомним, что оператор =~ не возвращает true или false, как можно было бы ожидать; он возвращает начальную позицию найденного соответствия (Fixnum) или nil, если соответствие не найдено. Следовательно, при вычислении выражений для строк, попадающих и не попадающих в диапазон, мы получаем 0 и nil соответственно.

Однако при попытке сконструировать диапазон от 0 до nil возникает ошибка, поскольку такой диапазон не имеет смысла:

range = 0..nil # Ошибка!

Далее, напомню, что в Ruby только false и nil дают значение «ложь» – все остальные объекты в логическом контексте вычисляются как «истина». А значит, следуя общей идеологии диапазон не должен вычисляться как «ложь».

puts "hello" if x..y

# Печатается "hello" для любого допустимого диапазона x..y.

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

loop do

 break if eof?

 line = gets

 start = line=~/=begin/

 stop = line=~/=end/

 puts line if start..stop

end

А что если сам диапазон поместить в переменную? Тоже не получится – проверка снова дает true.

loop do

 break if eof?

 line = gets

 range = (line=~/=begin/)..(line=~/=end/)

 puts line if range

end

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

И наконец, задумаемся о границах в операторе переключения. Они вычисляются каждый раз, но результат вычисления нельзя сохранить в переменной и затем просто подставить ее. В некотором смысле граничные точки оказываются похожи на объекты proc. Это не значения, а исполняемый код. Тот факт, что нечто, выглядящее как обычное выражение, на самом деле представляет собой proc, тоже не вызывает восторга.

И несмотря на все вышесказанное, функциональность-то полезная!.. Можно ли написать класс, который инкапсулирует ее, но при этом не будет таким «магическим»? Можно и даже не очень трудно. В листинге 6.1 приведен простой класс Transition, имитирующий поведение оператора переключения.

Листинг 6.1. Класс Transition

class Transition

 А, В = :А, :В

 T, F = true, false

 # state,p1,p2 => newstate, result

 Table = {[A,F,F]=>[A,F], [B,F,F]=>[B,T],

          [A,T,F]=>[B,T], [B,T,F]=>[B,T],

          [A,F,T]=>[A,F], [B,F,T]=>[A,T],

          [A,T,T]=>[A,T], [B,T,T]=>[A,T]}

 def initialize(proc1, proc2)

  @state = A

  @proc1, @proc2 = proc1, proc2

  check?

 end

 def check?

  p1 = @proc1.call ? T : F

  p2 = @proc2.call ? T : F

  @state, result = *Table[[@state,p1,p2]]

  return result

 end

end

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

Вот слегка измененный вариант того же подхода. Здесь метод initialize принимает proc и два произвольных выражения:

def initialize(var,flag1,flag2)

 @state = A

 @proc1 = proc { flag1 === var.call }

 @proc2 = proc { flag2 === var.call }

 check?

end

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

line = nil

trans = Transition.new(proc {line}, /=begin/, /=end/)

loop do break if eof? line = gets

 puts line if trans.check?

end

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

6.2.8. Нестандартные диапазоны

Рассмотрим пример диапазона, состоящего из произвольных объектов. В листинге 6.2 приведен класс для работы с римскими числами.

Листинг 6.2. Класс для работы с римскими числами

class Roman

 include Comparable

 I,IV,V,IX,X,XL,L,XC,C,CD,D,CM,M =

  1, 4, 5, 9, 10, 40, 50, 90, 100, 400, 500, 900, 1000

 Values = %w[M CM D CD С XC L XL X IX V IV I]

 def Roman.encode(value)

  return "" if self == 0

  str = ""

  Values.each do |letters|

   rnum = const_get(letters)

   if value >= rnum

    return(letters + str=encode(value-rnum))

   end

  end

  str

 end

 def Roman.decode(rvalue)

  sum = 0

  letters = rvalue.split('')

  letters.each_with_index do |letter,i|

   this = const_get(letter)

   that = const_get(letters[i+1]) rescue 0

   op = that > this ? :– : :+

   sum = sum.send(op,this)

  end

  sum

 end

 def initialize(value)

  case value

   when String

    @roman = value

    @decimal = Roman.decode(@roman)

   when Symbol

    @roman = value.to_s

    @decimal = Roman.decode(@roman)

   when Numeric

    @decimal = value

    @roman = Roman.encode(@decimal)

  end

 end

 def to_i

  @decimal

 end

 def to_s

  @roman

 end

 def succ

  Roman.new(@decima1 +1)

 end

 def <=>(other)

  self.to_i <=> other.to_i

 end

end

def Roman(val)

 Roman.new(val)

end

Сначала несколько слов о самом классе. Его конструктору можно передать строку, символ (представляющий число, записанное римскими цифрами) или Fixnum (число, записанное обычными арабскими цифрами). Внутри выполняется преобразование и сохраняются обе формы. Имеется вспомогательный метод Roman, это просто сокращенная запись вызова Roman.new. Методы класса encode и decode занимаются преобразованием из арабской формы в римскую и наоборот.

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

Метод to_i, конечно же, возвращает десятичное значение, a to_s – число, записанное римскими цифрами. Метод succ возвращает следующее римское число: например, Roman(:IV).succ вернет Roman(:V).

Оператор сравнения сравнивает десятичные эквиваленты. Мы включили с помощью директивы include модуль Comparable, чтобы получить доступ к операторам «меньше» и «больше» (реализация которых опирается на наличие метода сравнения <=>).

Обратите внимание на использование символов в следующем фрагменте:

op = that > this ? :– : :+

sum = sum.send(op,this)

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

if that > this

 sum -= this

else

 sum += this

end

Второй вариант длиннее, зато более понятен.

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

require 'roman'

y1 = Roman(:MCMLXVI)

y2 = Roman(:MMIX)

range = y1..y2 # 1966..2009

range.each {|x| puts x}      # Выводятся 44 строки.

epoch = Roman(:MCMLXX)

range.include?(epoch)        # true

doomsday = Roman(2038)

range.include?(doomsday)     # false

Roman(:V) == Roman(:IV).succ # true

Roman(:MCM) < Roman(:MM)     # true

6.3. Заключение

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

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

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

Глава 7. Дата и время

Знает ли кто-нибудь, что такое время на самом деле?

Чикаго, Чикаго IV

Один из самых сложных и противоречивых аспектов человеческой жизни – измерение времени. Чтобы приблизиться к истинному пониманию предмета, необходимо хорошо знать физику, астрономию, историю, юриспруденцию, бизнес и религию. Астрономам известно (в отличие от большинства из нас!), что солнечное и звездное время – не совсем одно и то же. Ведомо им и то, почему иногда к году добавляется «високосная секунда». Историки знают, что в октябре 1582 года, когда Италия переходила с григорианского календаря на юлианский, из календаря было изъято несколько дней. Немногим известна разница между астрономической и церковной Пасхой (почти всегда они совпадают). Многие не в курсе, что год, который не делится на 400 (например, 1900), високосным не является.

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

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

Среднее время по Гринвичу (Greenwich Mean Time, GMT) – устаревший термин, который теперь официально не употребляется. Новый глобальный стандарт называется «всеобщее скоординированное время» (Coordinated Universal Time, или UTC от французской аббревиатуры). GMT и UTC – по существу, одно и то же. По прошествии ряда лет разница между ними составит несколько секунд. В большинстве промышленных программ (в том числе в Ruby) эти системы измерения времени не различаются.

На летнее время переходят раз в полгода, сдвигая официальное время на один час. Поэтому обозначения часовых поясов в США обычно заканчиваются на ST (Standard Time – стандартное время) или DT (Daylight Time – летнее время). Это происходит в большинстве штатов США (если не во всех), да и во многих других странах.

Точка отсчета (epoch) – термин, пришедший из мира UNIX. В этой системе время обычно хранится как число секунд, прошедших с определенного момента (называемого точкой отсчета), а именно с полуночи 1 января 1970 года по Гринвичу.

(Отметим, что во временных поясах США точкой отсчета оказывается 31 декабря предыдущего года). Тем же словом обозначается не только начальный момент, но и время, прошедшее с этого момента.

Для выполнения большинства операций используется класс Time. Классы Date и DateTime обеспечивают дополнительную гибкость.

7.1. Определение текущего момента времени

Самый главный вопрос при манипуляциях с датами и временем: какой сегодня день и сколько сейчас времени? В Ruby при создании объекта класса Time без параметров устанавливаются текущие дата и время.

t0 = Time.new

Синонимом служит

Time.now: t0 = Time.now

Отметим, что разрешающая способность системного таймера на разных машинах различна. Иногда это микросекунды; в таком случае два объекта Time, созданных подряд, могут фиксировать разное время.

7.2. Работа с конкретными датами (после точки отсчета)

Большинству программ нужно работать только с датами, относящимися к будущему или недавнему прошлому. Для таких целей класса Time достаточно. Наиболее интересны методы mktime, local, gm и utc.

Метод mktime создает новый объект Time на основе переданных параметров. Параметры задаются по убыванию длительности промежутка: год, месяц, день, часы, минуты, секунды, микросекунды. Все параметры, кроме года, необязательны; по умолчанию предполагается минимально возможное значение. В некоторых машинных архитектурах микросекунды игнорируются. Час выражается числом от 0 до 23.

t1 = Time.mktime(2001)               # 1 января 2001 года, 0:00:00

t2 = Time.mktime(2001,3)

t3 = Time.mktime(2001,3,15)

t4 = Time.mktime(2001,3,15,21)

t5 = Time.mktime(2001,3,15,21,30)

t6 = Time.mktime(2001,3,15,21,30,15) # 15 марта 2001 года, 21:30:15

Отметим, что в методе mktime используется местное поясное время. Поэтому у него есть синоним Time.local.

t7 = Time.local(2001,3,15,21,30,15) # 15 марта 2001 года, 21:30:15

Метод Time.gm, по сути, делает то же самое, но в нем предполагается время GMT (или UTC). Поскольку автор книги проживает в центральном часовом поясе США, то разница составляет 8 часов:

t8 = Time.gm(2001,3,15,21,30,15) # March 15, 2001 21:30:15 pm

# Это 13:30:15 по центральному времени!

У этого метода есть синоним Time.utc:

t9 = Time.utc(2001,3,15,21,30,15) # March 15, 2001 21:30:15 pm

# Снова 13:30:15 по центральному времени.

Отметим одну важную вещь. Все эти методы могут принимать и альтернативный набор параметров. Метод экземпляра to_a (который преобразует время в массив отдельных компонентов) возвращает набор значений в следующем порядке: секунды, минуты, часы, день, месяц, год, день недели (0..6), порядковый номер дня в году (1..366), летнее время (true или false), часовой пояс (строка). Поэтому такие вызовы тоже допустимы:

t0 = Time.local(0,15,3,20,11,1979,2,324,false,"GMT-8:00")

t1 = Time.gm(*Time.now.to_a)

Однако, глядя на первый пример, не думайте, что вы сможете изменить вычисляемые параметры, например день недели (в данном случае 2 означает вторник). Такое действие противоречило бы принципам организации календаря, поэтому на созданном объекте Time оно никак не отражается. 20 ноября 1979 года был вторник, и никакой код не сможет этого изменить.

И наконец, отметим, что есть много способов задать время некорректно, например указав тринадцатый месяц или 35-й день месяца. При любой подобной попытке возникнет исключение ArgumentError.

7.3. Определение дня недели

Есть несколько способов определить день недели. Во-первых, метод экземпляра to_a возвращает массив, содержащий всю информацию о моменте времени. Можно обратиться к его седьмому элементу; это число от 0 до 6, причем 0 соответствует воскресенью, а 6 – субботе.

time = Time.now

day = time.to_a[6] # 2 (вторник)

Еще лучше воспользоваться методом экземпляра wday:

day = time.wday # 2 (вторник)

Но и тот, и другой способ не очень удобны. Иногда нужно получить день недели в виде числа, но чаще нас интересует его название в виде строки. Для этого можно обратиться к методу strftime. Его название знакомо программистам на С. Он распознает около двадцати спецификаторов, позволяя по-разному форматировать дату и время (см. раздел 7.21).

day = time.strftime("%а") # "Tue"

Можно получить и полное название:

long = time.strftime("%А") # "Tuesday"


    Ваша оценка произведения:

Популярные книги за неделю