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

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

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


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



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

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

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]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 – номер месяца!..


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

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