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