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"
7.4. Определение даты Пасхи
Дату этого праздника всегда было сложно вычислить, так как она привязана к лунному календарю. Солнечный год не делится нацело на лунные месяцы, поэтому даты, основанные на таком исчислении времени, будут из года в год меняться.
Представленный ниже алгоритм хорошо известен с давних времен. Мы видели его реализацию на языках BASIC, Pascal и С. А теперь перевели и на Ruby:
def easter(year)
с = year/100
n = year - 19*(year/19)
k = (c-17)/25
i = с - c/4 - (c-k)/3 + 19*n + 15
i = i - 30*(i/30)
i = i - (i/28)* (1 -(i/28)*(29/(i + 1))*((21-n)/11))
j = year + year/4 + i + 2 - с + c/4
j = j - 7*(j/7)
l = i - j
month = 3 + (1+40)/44
day = l + 28 - 31*(month/4)
[month, day]
end
date = easter 2001 # Найти месяц и день для 2001 года,
date = [2001] + date # Добавить в начало год.
t = Time.local *date # Передать параметры Time.local.
puts t # Sun Apr 15 01:00:00 GMT-8:00 2001
Кто-то, прочитав этот раздел о Пасхе, непременно спросит: "Церковная или астрономическая?" Честно говоря, не знаю. Если вам удастся выяснить, сообщите всем нам.
Я бы с удовольствием объяснил вам этот алгоритм, только вот сам его не понимаю… Что-то надо принимать на веру, а в данном случае это особенно уместно!
7.5. Вычисление n-ого дня недели в месяце
Иногда, зная год и месяц, хочется вычислить дату, скажем, третьего понедельника или второго вторника в этом месяце. Такую задачу решает код в листинге 7.1.
Чтобы найти n-ое вхождение данного дня недели, мы передаем n в качестве первого параметра. Второй параметр - номер дня недели (0 - воскресенье, 1 - понедельник и т.д.). Третий и четвертый параметры - месяц и год соответственно.
Листинг 7.1. Вычисление n-ого дня недели в месяце
def nth_wday(n, wday, month, year)
if (!n.between? 1,5) or
(!wday.between? 0,6) or
(!month.between? 1,12) raise ArgumentError
end
t = Time.local year, month, 1
first = t.wday
if first == wday
fwd = 1
elsif first < wday
fwd = wday - first + 1
elsif first > wday
fwd = (wday+7) - first + 1
end
target = fwd + (n-1)*7
begin
t2 = Time.local year, month, target
rescue ArgumentError
return nil
end
if t2.mday == target
t2
else
nil
end
end
Странный код в конце текста метода призван скорректировать давнюю традицию, принятую в функциях работы с датами. Если вы думаете, что попытка создать объект для представления 31 ноября приведет к ошибке, то разочарую вас. Почти все системы молчаливо преобразуют эту дату в 1 декабря. Если вы давным-давно программируете в UNIX, то, наверное, полагаете, что так и должно быть. Другие сочтут это ошибкой.
Не станем спорить о том, что должна делать системная библиотека и должен ли Ruby изменить это поведение. Но мы не хотим, чтобы наша процедура продолжала эту традицию. Если вы ищете, к примеру, пятую пятницу в ноябре 2000 года, то она вернет nil (а не 1 декабря 2000 года).
7.6. Преобразование из секунд в более крупные единицы
Иногда нужно преобразовать заданное число секунд в дни, часы, минуты и секунды. Это можно сделать следующим образом:
def sec2dhms(seсs)
time = seсs.round # Отбрасываем микросекунды.
sec = time % 60 # Извлекаем секунды.
time /= 60 # Отбрасываем секунды.
mins = time % 60 # Извлекаем минуты.
time /= 60 # Отбрасываем минуты.
hrs = time % 24 # Извлекаем часы.
time /= 24 # Отбрасываем часы.
days = time # Дни (последний остаток).
[days, hrs, mins, sec] # Возвращаем массив [d,h,m,s].
end
t = sec2dhms(1000000) # Миллион секунд равно...
puts "#{t[0]} days," # 11 дней,
puts "#{t[1]} hours," # 13 часов,
puts "#{t[2]} minutes," # 46 минут
puts " and #{t[3]} seconds." # и 40 секунд.
Можно было пойти и дальше. Но неделю вряд ли назовешь полезной единицей, месяц не слишком точно определен, а год не всегда содержит одно и то же число дней.
Ниже приведена также обратная функция:
def dhms2sec(days,hrs=0,min=0,sec=0)
days*86400 + hrs*3600 + min*60 + sec
end
7.7. Вычисление промежутка времени, прошедшего от точки отсчета
По разным причинам может понадобиться перейти от внутреннего (традиционного) представления времени к стандартному. В системе время хранится как число секунд, прошедших с точки отсчета.
Метод класса Time.at создает новый объект Time, зная, сколько секунд прошло с точки отсчета:
epoch = Time.at(0) # Найти точку отсчета (1 января 1970 GMT)
newmil = Time.at(978307200) # Счастливого миллениума! (1 января 2001)
Обратная функция - это метод экземпляра to_i, который преобразует дату в целое число.
now = Time.now # 16 Nov 2000 17:24:28
sec = now.to_i # 974424268
Если нужны микросекунды, и система поддерживает такую точность, то можно воспользоваться методом to_f для преобразования в число с плавающей точкой.
7.8. Високосные секунды
Вот снова день исчез, как ветра легкий стон,
Из нашей жизни, друг, навеки выпал он.
Но я, покуда жив, тревожиться не стану
О дне, что отошел, и дне, что не рожден.Омар Хайям, "Рубаи"
Хотите иметь дело с "високосными" секундами? Мой совет: не делайте этого.
Високосные секунды - это не миф. Одна была добавлена в 2005 году; его последняя минута состояла из 61 секунды, а не из 60. Библиотечные функции уже много лет учитывают возможность появления минут, состоящих из 61 секунды. Но наш опыт показывает, что большинство операционных систем високосные секунды игнорирует. Говоря "большинство", мы имеем в виду все, с которыми когда-либо сталкивались.
Известно, например, что високосная секунда была добавлена в конец последнего дня 1998 года. Вслед за моментом 23:59:59 наступил редкий момент 23:59:60. Но стандартная библиотека языка С, которой пользуется Ruby, этого в расчет не принимает.
t0 = Time.gm(1998, 12, 31, 23, 59, 59)
t1 = t0 + 1
puts t1 # Fri Jan 01 00:00:00 GMT 1999
Быть может (хотя и маловероятно), Ruby скорректирует эту ситуацию. Но во время работы над данной книгой таких планов не было.
7.9. Определение порядкового номера дня в году
Порядковый номер дня в году иногда еще называют юлианской датой, хотя это не имеет прямого отношения к юлианскому календарю, давно вышедшему из употребления. Многие считают, что такое название неправильно, поэтому мы им больше пользоваться не будем.
Но как ни называй порядковый номер дня, иногда хочется его узнать, то есть получить число от 1 до 366. В Ruby это просто - достаточно вызвать метод yday:
t = Time.now
day = t.yday # 315
7.10. Контроль даты и времени
В разделе 7.5 было показано, что стандартные функции не проверяют корректность даты, а "переносят" ее вперед, если необходимо. Например, 31 ноября становится 1 декабря.
Иногда такое поведение даже желательно. А если нет, то спешу обрадовать: стандартная библиотека Date не считает такие даты правильными. Мы можем воспользоваться этим фактом для контроля переданной даты.
class Time
def Time.validate(year, month=1, day=1,
hour=0, min=0, sec=0, usec=0)
require "date"
begin
d = Date.new(year,month,day)
rescue
return nil
end
Time.local(year,month,day,hour,min,sec,usec)
end
end
t1 = Time.validate(2000,11,30) # Создается корректный объект.
t2 = Time.validate(2000,11,31) # Возвращается nil.
Здесь не мудрствуя лукаво мы просто возвращаем nil, если переданные параметры не соответствуют правильной дате (полагаясь на вердикт, вынесенный классом Date). Мы оформили этот метод как метод класса Time по аналогии с другими методами создания объектов.
Отметим, что класс Date может работать и с датами, предшествующими точке отсчета, то есть дата 31 мая 1961 года с точки зрения этого класса вполне допустима. Но при попытке передать такие значения классу Time возникнет исключение ArgumentError. Мы не пытаемся его перехватить, полагая, что это лучше делать на том же уровне пользовательского кода, где обрабатывались бы исключения, скажем, от метода Time.local.
Раз уж зашла речь о Time.local, то отметим, что мы воспользовались именно этим методом. Захоти мы работать со временем по Гринвичу, нужно было бы вызывать метод gmt. Лучше реализовать оба варианта.
7.11. Определение недели в году
Что такое "порядковый номер недели", не вполне ясно. Разные компании, коалиции, правительственные учреждения и органы стандартизации по-разному определяют это понятие. Путаница связана с тем, что год может начинаться с любого дня недели. Все зависит от того, хотим ли мы учитывать неполные недели. К тому же в одних странах неделя начинается с воскресенья, в других - с понедельника.
В этом разделе мы предложим три варианта. Первые два основаны на методе strftime класса Time. Спецификатор %U отсчитывает недели, начинающиеся с воскресенья, а спецификатор %W - начинающиеся с понедельника.
Третью возможность предоставляет класс Date. В нем имеется метод доступа cweek, который возвращает порядковый номер недели, следуя определению из стандарта ISO 8601 (согласно которому первой считается неделя, содержащая первый вторник года).
Если все это вам не подходит, можете придумать собственный алгоритм. Все три решения включены в один фрагмент кода:
require "date"
# Посмотрим, в какую неделю попадает 1 мая в 2002 и 2005 годах.
t1 = Time.local(2002,5,1)
d1 = Date.new(2002,5,1)
week1a = t1.strftime("%U").to_i # 17
week1b = t1.strftime("%W").to_i # 17
week1c = d1.cweek #18
t2 = Time.local(2005,5,1)
d2 = Date.new(2005,5,1)
week2a = t2.strftime("%U").to_i # 18
week2b = t2.strftime("%W").to_i # 18
week2c = d2.cweek # 17