|
||||
|
Глава 7. Дата и время
Один из самых сложных и противоречивых аспектов человеческой жизни — измерение времени. Чтобы приблизиться к истинному пониманию предмета, необходимо хорошо знать физику, астрономию, историю, юриспруденцию, бизнес и религию. Астрономам известно (в отличие от большинства из нас!), что солнечное и звездное время — не совсем одно и то же. Ведомо им и то, почему иногда к году добавляется «високосная секунда». Историки знают, что в октябре 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используется местное поясное время. Поэтому у него есть синоним T ime.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 7.12. Проверка года на високосностьВ классе Dateесть два метода класса julian_leap?и gregorian_leap?, но только последний применим к относительно недавнему времени. Есть также метод leap?, но это просто синоним gregorian_leap?. require "date" flag1 = Date.julian_leap? 1700 # true flag2 = Date.gregorian_leap? 1700 # false flag3 = Date.leap? 1700 # false Любой ребенок знает первое правило проверки на високосность: год должен делиться на 4. Меньшее число людей знают второе правило: год не должен делиться на 100. И уж совсем немногие знают про исключение из второго правила: если год делится на 400, то он високосный. Таким образом, последний год тысячелетия является високосным, только если делится на 400; так, 1900 год не был високосным, а 2000 был. (Эта поправка необходима, потому что в году не ровно 365.25 дней, а приблизительно 365.2422.) В классе Timeнет аналогичного метода, но при желании его легко можно добавить. class Time def Time.leap? Year if year % 400 == 0 true elsif year % 100 == 0 false elsif year % 4 == 0 true else false end end Я привел этот код только для того, чтобы прояснить алгоритм; конечно, гораздо проще вызвать метод Date.leap?. В моей реализации это метод класса по аналогии с классом Date, но можно было бы сделать его и методом экземпляра. 7.13. Определение часового поясаМетод zoneкласса Timeвозвращает название часового пояса в виде строки: z1 = Time.gm(2000,11,10,22,5,0).zone # "GMT-6:00" z2 = Time.local(2000,11,10,22,5,0).zone # "GMT-6:00" К сожалению, время хранится относительно текущего часового пояса, а не того, для которого был создан объект. При желании можно скорректировать его самостоятельно. 7.14. Манипулирование временем без датыИногда нужно работать с временем дня в виде строки. На помощь снова приходит метод strftime. Можно «разбить» время на часы, минуты и секунды t = Time.now puts t.strftime("%H:%M:%S") # Печатается 22:07:45 А можно только на часы и минуты (прибавив 30 секунд, мы даже можем округлить до ближайшей минуты): puts t.strftime("%Н:%М") # Печатается 22:07 puts (t+30).strftime("%Н:%М") # Печатается 22:08 Наконец, со стандартного 24-часового представления можно переключиться на 12-часовой формат, добавив признак перехода через полдень (АМ/РМ): puts t.strftime("%I:%М %p") # Печатается 10:07 PM Есть и другие возможности — поэкспериментируйте! 7.15 Сравнение моментов времениК классу Timeподмешан модуль Comparable, поэтому моменты времени можно сравнивать непосредственно: t0 = Time.local(2000,11,10,22,15) # 10 Nov 2000 22:15 t1 = Time.local(2000,11,9,23,45) # 9 Nov 2000 23:45 t2 = Time.local(2000,11,12,8,10) # 12 Nov 2000 8:10 t3 = Time.local(2000,11,11,10,25) # 11 Nov 2000 10:25 if t0 < t1 then puts "t0 < t1" end if t1 != t2 then puts "t1 != t2" end if t1 <= t2 then puts "t1 <= t2" end if t3.between?(t1,t2) puts "t3 находится между t1 и t2" end # Все четыре предложения if возвращают true. 7.16 Прибавление интервала к моменту времениМожно получить новый момент времени, прибавив к существующему интервал. Последний представляется целым числом, которое интерпретируется как число секунд. t0 = Time.now t1 = t0 + 60 # Ровно одна минута с момента t0. t2 = t0 + 3600 # Ровно один час с момента t0. t3 = t0 + 86400 # Ровно один день с момента t0. Может пригодиться функция dhms2sec(определена в разделе 7.6). Напомним, что по умолчанию параметры, соответствующие часам, минутам и секундам, равны 0. t4 = t0 + dhms2sec(5,10) # 5 дней, 10 часов в будущем. t5 = t0 + dhms2sec(22,18,15) # 22 дня, 18 часов, 15 минут в будущем. t6 = t0 - dhms2sec(7) # Ровно неделю назад. Не забывайте, что для получения момента времени в прошлом нужно вычитать, как при вычислении t6в примере выше. 7.17. Вычисление разности между двумя моментами времениМожно вычислить интервал между двумя моментами времени. В результате вычитания одного объекта Timeиз другого получаем число секунд: today = Time.local(2000,11,10) yesterday = Time.local(2000,11,9) cliff = today - yesterday # 86400 секунд. И снова оказывается полезной функция sec2dhms, которая определена в разделе 7.6. past = Time. Local(1998,9,13,4,15) now = Time.local(2000,11,10,22,42) diff = now - past unit = sec2dhms(diff) puts "#{unit[0]} дней," # 789 дней, puts "#{unit[1]} часов," # 18 часов, puts "#{unit[2]} минут" # 27 минут puts "и #{unit[3]} секунд." # и 0 секунд. 7.18. Работа с конкретными датами (до точки отсчета)В стандартной библиотеке Dateесть одноименный класс для работы с датами, предшествующими полуночи 1 января 1970 года. Несмотря на некоторое перекрытие с классом Time, между ними есть существенные различия. Самое главное состоит в том, что класс Dateвообще игнорирует время, то есть работает с точностью до одного дня. Кроме того, класс Dateстроже контролирует ошибки, чем класс Time: попытка обратиться к 31 июня (или к 29 февраля невисокосного года) приведет к исключению. Код даже «знает» о различных датах перехода на григорианский календарь в Италии и Англии (в 1582 и 1752 году соответственно) и может обнаружить «несуществующие» даты, появившиеся в результате такого перехода. Эта стандартная библиотека — паноптикум интересного и местами загадочного кода. К сожалению, у нас нет места для более подробного разговора о ней. 7.19. Взаимные преобразования объектов Date, Time и DateTimeВ Ruby есть три основных класса для работы с датами и временем: Time, Dateи DateTime. Опишем их особенности: • Класс Timeпреимущественно обертывает соответствующие функции из стандартной библиотеки языка С. Они, как правило, опираются на точку отсчета в UNIX и потому не способны представлять моменты времени раньше 1970 года. • Класс Dateсоздан для преодоления недостатков класса Time. Он без труда справляется с датами в более отдаленном прошлом — например, позволяет представить день рождения Леонардо да Винчи (15 апреля 1452 года), и, кроме того, знает о реформе календаря. Но у него есть свои слабые места: он работает только с датами, игнорируя время. • Класс DateTimeнаследует Dateи пытается компенсировать отсутствующие в нем возможности. Он может представлять даты не хуже Dateи время не хуже Time. Часто его способ представления даты и времени оказывается наилучшим. Однако не думайте, что объект DateTime— это просто объект Date, к которому механически присоединен объект Time. На самом деле в классе DateTimeотсутствуют такие методы, как usec, dst?и некоторые другие. Итак, у нас есть три класса. К сожалению, не существует стандартного способа преобразовать один из них в любой другой. По мере развития Ruby подобные шероховатости будут устраняться. А пока обойдемся методами, приведенными в листинге 7.2. Спасибо Кирку Хейнсу (Kirk Haines). Листинг 7.2. Преобразования между классами, представляющими даты и времяclass Time def to_date Date.new(year, month, day) rescue NameError nil end def to_datetime DateTime.new(year, month, day, hour, min, sec) rescue NameError nil end end class DateTime def to_time Time.local(year,month,day,hour,min,sec) end end class Date def to_time Time.local(year,month,day) end end Эти методы пропускают наверх все исключения, кроме NameError. Зачем нужно его перехватывать? Потому что могло случиться так, что программа не затребовала (с помощью директивы require) библиотеку date(напомним, что классы Dateи DateTimeвходят в эту стандартную библиотеку, а не являются системными). В таком случае методы to_datetimeи to_dateвозвращают nil. 7.20. Извлечение даты и времени из строкиДата и время могут быть представлены в виде строки самыми разными способами: в полной или сокращенной форме, с разной пунктуацией, различным порядком компонентов и т.д. Из-за такого разнообразия очень сложно написать код, интерпретирующий символьную строку как дату. Рассмотрим несколько примеров: s1 = "9/13/98 2:15am" s2 = "1961-05-31" s3 = "11 July 1924" s4 = "April 17, 1929" s5 = "20 July 1969 16:17 EDT" s6 = "Mon Nov 13 2000" s7 = "August 24, 79" # День разрушения Помпеи. s8 = "8/24/79" К счастью, большую часть работы за нас уже проделали. В модуле ParseDateесть единственный класс с таким же именем, а в нем — единственный метод parsedate. Он возвращает массив компонентов даты в следующем порядке: год, месяц, день, час, минута, секунда, часовой пояс, день недели. Вместо полей, которые не удалось распознать, возвращается nil. require "parsedate.rb" include ParseDate p parsedate(s1) # [98, 9, 13, 2, 15, nil, nil, nil] p parsedate(s2) # [1961, 5, 31, nil, nil, nil, nil, nil] p parsedate(s3) # [1924, 7, 11, nil, nil, nil, nil, nil] p parsedate(s4) # [1929, 4, 17, nil, nil, nil, nil, nil] p parsedate(s5) # [1969, 7, 20, 16, 17, nil, "EDT", nil] p parsedate(s6) # [2000, 11, 13, nil, nil, nil, nil, 1] p parsedate(s7) # [79, 8, 24, nil, nil, nil, nil, nil] p parsedate(s8,true) # [1979, 8, 24, nil, nil, nil, nil, nil] Последние две строки иллюстрируют назначение второго параметра parsedate, который называется guess_year. Из-за привычки записывать год двумя цифрами может возникнуть неоднозначность. Последние две строки интерпретируются по-разному; при разборе s8мы установили значение guess_yearравным true, вследствие чего программа сочла, что имеется в виду четырехзначный год. С другой стороны, s7— это дата извержения Везувия в 79 году, так что двузначный год был употреблен сознательно. Правило применения параметра guess_yearтаково: если год меньше 100 и guess_yearравно true, преобразовать в четырехзначный год. Преобразование выполняется так: если год больше либо равен 70, прибавить к нему 1900, в противном случае прибавить 2000. Таким образом, 75 преобразуется в 1975, а 65 — в 2065. Такое правило применяется программистами повсеместно. А что сказать о строке s1, в которой, вероятно, имелся в виду 1998 год? Не все потеряно, если полученное число передается другому фрагменту программы, который интерпретирует его как 1998. Учтите, что parsedateпрактически не контролирует ошибки. Например, если подать ему на вход дату, в которой день недели установлен некорректно, то он несоответствия не обнаружит. Это всего лишь анализатор — со своей работой он справляется неплохо, а требовать от него большего было бы неправильно. Следует особо отметить склонность этого кода к «американизмам». Когда американец пишет 3/4/2001, он обычно имеет в виду 4 марта 2001 года. В Европе и большинстве других мест это означает 3 апреля. Но если при записи всех дат применяется одно и то же соглашение, ничего страшного не произойдет. Ведь возвращается просто массив, и ничто не мешает вам мысленно переставить первый и второй элементы. Кстати, имейте в виду, что вышеописанным образом интерпретируется даже такая дата, как 15/3/2000, хотя нам совершенно очевидно, что 15 — это день, а не месяц. Метод же parsedate«на голубом глазу» сообщит, что 15 — номер месяца!.. 7.21. Форматирование и печать даты и времениДля получения канонического представления даты и времени служит метод asctime; У него есть синоним ctime. Аналогичный результат дает метод to_s. Точно такая же строка будет напечатана, если просто передать объект, представляющий дату и время, методу puts. С помощью метода strftimeкласса Timeможно отформатировать дату и время почти произвольным образом. В этой главе мы уже встречали спецификаторы %a, %A, %U, %W, %H, %M, %S, %Iи %p, а ниже приведены оставшиеся: %bСокращенное название месяца ( "Jan") %BПолное название месяца ( "January") %cПредпочтительное представление локальной даты и времени %dДень месяца ( 1..31) %jПорядковый номер дня в году ( 1..366); так называемая «юлианская дата» %mНомер месяца ( 1..12) %wНомер дня недели ( 0..6) %xПредпочтительное представление даты без времени %yГод в двузначном формате (без указания века) %YГод в четырехзначном формате %ZНазвание часового пояса %%Знак %(процент) Дополнительную информацию вы найдете в справочном руководстве по языку Ruby. 7.22. Преобразование часовых поясовОбычно приходится работать только с двумя часовыми поясами: GMT (или UTC) и тем, в котором вы находитесь. Метод gmtimeпреобразует время к поясу GMT (модифицируя сам вызывающий объект). У него есть синоним utc. Может быть, вы думаете, что можно просто преобразовать момент времени в массив, подменить часовой пояс и выполнить обратное преобразование? Проблема в том, что все методы класса, к примеру localи gm(а также их синонимы mktimeи utc), готовы создавать объект Timeтолько в предположении, что указано либо местное время, либо время по Гринвичу. Есть обходной путь для преобразования часового пояса. Но предполагается, что вы заранее знаете разницу во времени. Взгляните на следующий фрагмент: mississippi = Time.local(2000,11,13,9,35) # 9:35 am CST california = mississippi - 2*3600 # Минус два часа. time1 = mississippi.strftime("%X CST") # 09:35:00 CST time2 = california.strftime("%X PST") # 07:35:00 PST Спецификатор %xв методе strftimeпросто выводит время в формате hh:mm:ss. 7.23. Определение числа дней в месяцеВ текущей версии Ruby еще нет встроенной функции для этой цели. Но ее можно без труда написать самостоятельно: require 'date' def month_days(month,year=Date.today.year) mdays = [nil,31,28,31,30,31,30,31,31,30,31.30,31] mdays[2] = 29 if Date.leap?(year) mdays[month] end days = month_days(5) # 31 (May) days = month_days(2,2000) # 29 (February 2000) days = month_days(2,2100) # 28 (February 2000) 7.24. Разбиение месяца на неделиПредставьте, что нужно разбить месяц на недели, например чтобы напечатать календарь. Эту задачу решает приведенный ниже код. Возвращаемый массив состоит из подмассивов, по семь элементов в каждом. При этом первому элементу каждого внутреннего массива соответствует воскресенье. Начальные элементы для первой недели и конечные для второй могут быть равны nil. def calendar(month,year) days = month_days(month,year) t = Time.mktime(year,month,1) first = t.wday list = *1..days weeks = [[]] week1 = 7 - first week1.times { weeks[0] << list.shift } nweeks = list.size/7 + 1 nweeks.times do |i| weeks[i+1] ||= [] 7.times do break if list.empty? weeks[i+1] << list.shift end end pad_first = 7-weeks[0].size pad_first.times { weeks[0].unshift(nil) } pad_last = 7-weeks[0].size pad_last.times { weeks[-1].unshift(nil) } weeks end arr = calendar(12,2008) # [[nil, 1, 2, 3, 4, 5, 6], # [7, 8, 9, 10, 11, 12, 13], # [14, 15, 16, 17, 18, 19, 20], # [21, 22, 23, 24, 25, 26, 27], # [28, 29, 30, 31, nil, nil, nil]] Чтобы было понятнее, распечатаем этот массив массивов: def print_calendar(month,year) weeks = calendar(month,year) weeks.each do |wk| wk.each do |d| item = d.nil? ? " "*4 : " %2d " % d print item end puts end puts end # Выводится: # 1 2 3 4 5 6 # 7 8 9 10 11 12 13 # 14 15 16 17 18 19 20 # 21 22 23 24 25 26 27 # 28 29 30 31 7.25. ЗаключениеВ этой главе мы рассмотрели класс Time, который является оберткой для функций из стандартной библиотеки языка С. Были показаны его возможности и ограничения. Мы также узнали, зачем существуют классы Dateи DateTimeи какую функциональность они предоставляют. Мы научились выполнять преобразования между этими классами и добавили несколько собственных полезных методов. На этом обсуждение даты и времени завершается. Переходим к массивам, хэшам и другим перечисляемым структурам в Ruby. Примечания:1 Огромное спасибо (яп.) 10 Пер. О. Румер. — Прим. ред. |
|
||
Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх |
||||
|