• 7.1. Определение текущего момента времени
  • 7.2. Работа с конкретными датами (после точки отсчета)
  • 7.3. Определение дня недели
  • 7.4. Определение даты Пасхи
  • 7.5. Вычисление n-ого дня недели в месяце
  • 7.6. Преобразование из секунд в более крупные единицы
  • 7.7. Вычисление промежутка времени, прошедшего от точки отсчета
  • 7.8. Високосные секунды
  • 7.9. Определение порядкового номера дня в году
  • 7.10. Контроль даты и времени
  • 7.11. Определение недели в году
  • 7.12. Проверка года на високосность
  • 7.13. Определение часового пояса
  • 7.14. Манипулирование временем без даты
  • 7.15 Сравнение моментов времени
  • 7.16 Прибавление интервала к моменту времени
  • 7.17. Вычисление разности между двумя моментами времени
  • 7.18. Работа с конкретными датами (до точки отсчета)
  • 7.19. Взаимные преобразования объектов Date, Time и DateTime
  • 7.20. Извлечение даты и времени из строки
  • 7.21. Форматирование и печать даты и времени
  • 7.22. Преобразование часовых поясов
  • 7.23. Определение числа дней в месяце
  • 7.24. Разбиение месяца на недели
  • 7.25. Заключение
  • Глава 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
    используется местное поясное время. Поэтому у него есть синоним 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. Високосные секунды

    Вот снова день исчез, как ветра легкий стон,
    Из нашей жизни, друг, навеки выпал он.
    Но я, покуда жив, тревожиться не стану
    О дне, что отошел, и дне, что не рожден.[10]
    (Омар Хайям, «Рубаи»)

    Хотите иметь дело с «високосными» секундами? Мой совет: не делайте этого.

    Високосные секунды — это не миф. Одна была добавлена в 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 | Добавить материал | Нашёл ошибку | Наверх