Дата и время
Модуль Dates предоставляет два типа для работы с датами: Date и DateTime, представляющие точность дня и миллисекунды соответственно; оба являются подтипами абстрактного типа TimeType. Мотивация для разных типов проста: некоторые операции намного проще, как с точки зрения кода, так и с точки зрения рассуждений, когда не нужно учитывать сложность большей точности. Например, так как тип Date разрешается только с точностью до даты (т.е. без часов, минут или секунд), обычные соображения по часовым поясам, переходу на летнее/зимнее время и високосным секундам не нужны и не учитываются.
Оба типа Date и DateTime являются по сути неизменяемыми Int64 обёртками. Единственное instant поле любого типа фактически является типом UTInstant{P}, который представляет собой непрерывно возрастающую временную шкалу машины, основанную на UT секунде [1]. Тип DateTime является независимым от часового пояса (в терминологии Python) или аналогичен LocalDateTime в Java 8. Дополнительные функции часовых поясов можно добавить с помощью пакета TimeZones.jl, который компилирует базу данных часовых поясов IANA. И Date, и DateTime основаны на стандарте ISO 8601, который следует пролептическому григорианскому календарю. Отметим, что стандарт ISO 8601 особым образом обращается с датами до н.э. В общем, последний день эры до н.э., 1-12-31 до н.э., был последован 1-1-1 н.э., следовательно, года ноль не существует. Однако стандарт ISO указывает, что 1 до н.э. - это год ноль, поэтому 0000-12-31 - это день перед 0001-01-01, а год -0001 (да, минус один для года) - это 2 до н.э., год -0002 - 3 до н.э. и т.д.
| [1] | Понятие UT секунды на самом деле довольно фундаментально. В основном существует два разных понятия времени, которые обычно принимаются, одно основано на физическом вращении Земли (один полный оборот = 1 день), другое основано на СИ секунде (фиксированное, постоянное значение). Это радикально разные вещи! Подумайте об этом, «UT секунда», как определено относительно вращения Земли, может иметь разную абсолютную длину в зависимости от дня! В любом случае, тот факт, что Date и DateTime основаны на UT секундах, является упрощающим, но честным предположением, чтобы избежать таких вещей, как високосные секунды и всех их сложностей. Эта основа времени формально называется UT или UT1. Основание типов на UT секунде в основном означает, что каждая минута имеет 60 секунд, а каждый день - 24 часа, и приводит к более естественным вычислениям при работе с датами календаря. |
Конструкторы
Типы Date и DateTime могут быть построены с помощью целочисленных или Period типов, с помощью парсинга или с помощью корректоров (подробнее об этом позже):
julia> DateTime(2013) 2013-01-01T00:00:00 julia> DateTime(2013,7) 2013-07-01T00:00:00 julia> DateTime(2013,7,1) 2013-07-01T00:00:00 julia> DateTime(2013,7,1,12) 2013-07-01T12:00:00 julia> DateTime(2013,7,1,12,30) 2013-07-01T12:30:00 julia> DateTime(2013,7,1,12,30,59) 2013-07-01T12:30:59 julia> DateTime(2013,7,1,12,30,59,1) 2013-07-01T12:30:59.001 julia> Date(2013) 2013-01-01 julia> Date(2013,7) 2013-07-01 julia> Date(2013,7,1) 2013-07-01 julia> Date(Dates.Year(2013),Dates.Month(7),Dates.Day(1)) 2013-07-01 julia> Date(Dates.Month(7),Dates.Year(2013)) 2013-07-01
Парсинг Date или DateTime выполняется с помощью строк формата. Строки формата работают, определяя «слоты» с разделителями или фиксированной ширины, содержащие период для парсинга, и передавая текст для парсинга и строку формата конструктору Date или DateTime в форме Date("2015-01-01","y-m-d") или DateTime("20150101","yyyymmdd").
Слоты с разделителями отмечаются указанием разделителя, который парсер должен ожидать между двумя последующими периодами; таким образом, "y-m-d" позволяет парсеру понять, что между первым и вторым слотами в строке даты, например, "2014-07-16", должен находиться символ -. Символы y, m, и d позволяют парсеру узнать, какие периоды нужно обработать в каждом слоте.
Слоты фиксированной ширины задаются повторением символа периода количество раз, соответствующее ширине, без разделителей между символами. Таким образом, "yyyymmdd" соответствует строке даты, например, "20140716". Парсер различает слот фиксированной ширины по отсутствию разделителя, отмечая переход "yyyymm" от одного символа периода к другому.
Поддержка парсинга месяца в текстовой форме также поддерживается символами u и U для сокращенных и полных названий месяцев соответственно. По умолчанию поддерживаются только английские названия месяцев, поэтому u соответствует «Янв», «Фев», «Март» и т.д. А U соответствует «Январь», «Февраль», «Март» и т.д. Аналогично другим функциям сопоставления имя => значение dayname() и monthname(), пользовательские локали могут быть загружены путем передачи сопоставления locale=>Dict{String,Int} в словари MONTHTOVALUEABBR и MONTHTOVALUE для сокращенных и полных названий месяцев соответственно.
Замечание по производительности парсинга: использование функции Date(date_string,format_string) приемлемо, если она вызывается несколько раз. Однако, если нужно обработать много строк дат с одинаковым форматом, намного эффективнее предварительно создать Dates.DateFormat и передать его вместо исходной строки формата.
julia> df = Dates.DateFormat("y-m-d");
julia> dt = Date("2015-01-01",df)
2015-01-01
julia> dt2 = Date("2015-01-02",df)
2015-01-02
Полный набор тестов и примеров парсинга и форматирования доступен в файле tests/dates/io.jl.
Продолжительности/Сравнения
Вычисление длительности между двумя Date или DateTime очевидно, учитывая их внутреннее представление как UTInstant{Day} и UTInstant{Millisecond} соответственно. Разница между Date возвращается в количестве Day, а DateTime - в количестве Millisecond. Аналогично, сравнение TimeType сводится к простому сравнению базовых машинных моментов времени (которые, в свою очередь, сравнивают внутренние значения Int64).
julia> dt = Date(2012,2,29)
2012-02-29
julia> dt2 = Date(2000,2,1)
2000-02-01
julia> dump(dt)
Date
instant: UTInstant{Day}
periods: Day
value: Int64 734562
julia> dump(dt2)
Date
instant: UTInstant{Day}
periods: Day
value: Int64 730151
julia> dt > dt2
true
julia> dt != dt2
true
julia> dt + dt2
Operation not defined for TimeTypes
julia> dt * dt2
Operation not defined for TimeTypes
julia> dt / dt2
Operation not defined for TimeTypes
julia> dt - dt2
4411 days
julia> dt2 - dt
-4411 days
julia> dt = DateTime(2012,2,29)
2012-02-29T00:00:00
julia> dt2 = DateTime(2000,2,1)
2000-02-01T00:00:00
julia> dt - dt2
381110402000 milliseconds
Функции доступа
Поскольку типы Date и DateTime хранятся как одиночные значения Int64, части или поля даты могут быть получены с помощью функций доступа. Функции с маленькой буквой возвращают поле как целое число:
julia> t = Date(2014,1,31) 2014-01-31 julia> Dates.year(t) 2014 julia> Dates.month(t) 1 julia> Dates.week(t) 5 julia> Dates.day(t) 31
В то время как функции с большой буквой возвращают то же значение в соответствующем типе Period:
julia> Dates.Year(t) 2014 years julia> Dates.Day(t) 31 days
Предоставлены составные методы, так как они обеспечивают эффективность, если одновременно требуется несколько полей:
julia> Dates.yearmonth(t) (2014,1) julia> Dates.monthday(t) (1,31) julia> Dates.yearmonthday(t) (2014,1,31)
Также можно получить доступ к базовому значению UTInstant или целочисленному значению:
julia> dump(t)
Date
instant: UTInstant{Day}
periods: Day
value: Int64 735264
julia> t.instant
UTInstant{Day}(735264 days)
julia> Dates.value(t)
735264
Функции запроса
Функции запроса предоставляют календарную информацию о TimeType. Они включают информацию о дне недели:
julia> t = Date(2014,1,31) 2014-01-31 julia> Dates.dayofweek(t) 5 julia> Dates.dayname(t) "Friday" julia> Dates.dayofweekofmonth(t) 5 # 5th Friday of January
Месяц года:
julia> Dates.monthname(t) "January" julia> Dates.daysinmonth(t) 31
А также информацию об TimeType году и квартале:
julia> Dates.isleapyear(t) false julia> Dates.dayofyear(t) 31 julia> Dates.quarterofyear(t) 1 julia> Dates.dayofquarter(t) 31
Методы dayname() и monthname() также могут принимать необязательный параметр locale ключевое слово, которое может быть использовано для возврата имени дня или месяца для других языков/локалей:
julia> const french_daysofweek = Dict(1=>"Lundi",2=>"Mardi",3=>"Mercredi",4=>"Jeudi",5=>"Vendredi",6=>"Samedi",7=>"Dimanche"); # Load the mapping into the Dates module under locale name "french" julia> Dates.VALUETODAYOFWEEK["french"] = french_daysofweek; julia> Dates.dayname(t;locale="french") "Vendredi"
Аналогично для функции monthname(), в VALUETOMONTH должна быть загружена таблица соответствий locale=>Dict{Int,String}.
Арифметика TimeType-Period
При использовании любого языка/фреймворка для работы с датами и временем полезно ознакомиться с тем, как обрабатывается арифметика периодов, так как существуют некоторые сложные моменты (хотя для типов с точностью до дня проблем значительно меньше).
Модуль Dates пытается следовать простому принципу, пытаясь по возможности минимально изменять данные при выполнении арифметических операций с Period. Этот подход также часто называют календарной арифметикой или тем, что вы, скорее всего, предположили бы, если бы вас спросили о таком же расчете в разговоре. Почему все эти сложности? Давайте рассмотрим классический пример: прибавим 1 месяц к 31 января 2014 года. Какой будет ответ? Javascript ответит 3 марта (предполагает 31 день). PHP ответит 2 марта (предполагает 30 дней). Фактически, правильного ответа нет. В модуле Dates результатом будет 28 февраля. Как это определяется? Мне нравится представлять это как классическую азартную игру 7-7-7 в казино.
Теперь представьте, что вместо 7-7-7 слоты представляют Год-Месяц-День, или в нашем примере, 2014-01-31. Если вы попросите добавить 1 месяц к этой дате, то слот месяца увеличивается, и теперь у нас есть 2014-02-31. Затем проверяется, больше ли значение дня, чем последнее допустимое значение дня в новом месяце; если больше (как в приведенном выше примере), значение дня корректируется до последнего допустимого значения (28). К каким последствиям приводит такой подход? Попробуйте добавить еще один месяц к нашей дате, 2014-02-28 + Month(1) == 2014-03-28. Что? Вы ожидали последнее число марта? Нет, к сожалению, помните слоты 7-7-7. Поменяются как можно меньше слотов, поэтому мы сначала увеличиваем слот месяца на 1, 2014-03-28, и вуаля, операция завершена, так как это допустимая дата. С другой стороны, если мы добавим 2 месяца к нашей исходной дате, 2014-01-31, то получим 2014-03-31, как и ожидалось. Другим последствием этого подхода является потеря ассоциативности, когда задается определенный порядок (т.е. добавление в разном порядке приводит к разным результатам). Например:
julia> (Date(2014,1,29)+Dates.Day(1)) + Dates.Month(1) 2014-02-28 julia> (Date(2014,1,29)+Dates.Month(1)) + Dates.Day(1) 2014-03-01
Что происходит в этом примере? В первой строке мы добавляем 1 день к 29 января, что дает 2014-01-30; затем мы добавляем 1 месяц, получая 2014-02-30, которая затем корректируется до 2014-02-28. Во втором примере мы сначала добавляем 1 месяц, получая 2014-02-29, которая корректируется до 2014-02-28, а затем добавляем 1 день, что дает 2014-03-01. Один из принципов проектирования в этом случае заключается в том, что при наличии нескольких периодов операции будут упорядочены по типам периодов, а не по их значениям или порядковому номеру; это означает, что Year всегда будет добавлено первым, затем Month, затем Week и т. д. Таким образом, следующее действительно приводит к ассоциативности и просто работает:
julia> Date(2014,1,29) + Dates.Day(1) + Dates.Month(1) 2014-03-01 julia> Date(2014,1,29) + Dates.Month(1) + Dates.Day(1) 2014-03-01
Сложно? Возможно. Что делать обычному пользователю Dates? В конечном счете, следует помнить, что явное навязывание определенной ассоциативности при работе с месяцами может привести к неожиданным результатам, но в противном случае все должно работать как ожидается. К счастью, это в основном и есть все необычные случаи в арифметике дат и периодов при работе со временем в UT (избегая «радостей» работы с переходом на летнее время, добавлением високосных секунд и т. д.).
Функции корректировки
Как удобны бы ни были арифметические операции с датами и периодами, часто необходимые расчеты с датами имеют скорее календарный или временной характер, чем представляют собой фиксированное количество периодов. Праздники являются идеальным примером; большинство из них подчиняются правилам, таким как «День памяти = последний понедельник мая» или «День благодарения = четверг 4-го ноября». Эти временные выражения оперируют правилами, относящимися к календарю, такими как первое или последнее число месяца, следующий вторник или первое и третье среды и т. д.
Модуль Dates предоставляет API функций корректировки через несколько удобных методов, которые помогают просто и лаконично выражать временные правила. Первая группа методов корректировки обрабатывает первое и последнее число недели, месяца, квартала и года. Каждый из них принимает на вход одну TimeType и возвращает или корректирует до первого или последнего числа желаемого периода, относительного к входу.
# Adjusts the input to the Monday of the input's week julia> Dates.firstdayofweek(Date(2014,7,16)) 2014-07-14 # Adjusts to the last day of the input's month julia> Dates.lastdayofmonth(Date(2014,7,16)) 2014-07-31 # Adjusts to the last day of the input's quarter julia> Dates.lastdayofquarter(Date(2014,7,16)) 2014-09-30
Два последующих метода высшего порядка, tonext() и toprev(), обобщают работу с временными выражениями, принимая в качестве первого аргумента DateFunction вместе с начальной TimeType. DateFunction — это просто функция, обычно анонимная, которая принимает на вход одну TimeType и возвращает Bool, true указывающие на выполнение критерия корректировки. Например:
julia> istuesday = x->Dates.dayofweek(x) == Dates.Tuesday # Returns true if the day of the week of x is Tuesday (anonymous function) julia> Dates.tonext(istuesday, Date(2014,7,13)) # 2014-07-13 is a Sunday 2014-07-15 # Convenience method provided for day of the week adjustments julia> Dates.tonext(Date(2014,7,13), Dates.Tuesday) 2014-07-15
Это полезно при использовании синтаксиса do-блоков для более сложных временных выражений:
julia> Dates.tonext(Date(2014,7,13)) do x
# Return true on the 4th Thursday of November (Thanksgiving)
Dates.dayofweek(x) == Dates.Thursday &&
Dates.dayofweekofmonth(x) == 4 &&
Dates.month(x) == Dates.November
end
2014-11-27
Окончательным методом в API функций корректировки является функция recur(). recur() векторизует процесс корректировки, принимая начальную и конечную дату (необязательно с указанием StepRange), а также DateFunction, чтобы указать все допустимые даты/моменты, которые необходимо вернуть в указанном диапазоне. В этом случае DateFunction часто называют функцией «включения», потому что она определяет (возвращая true), какие даты/моменты должны быть включены в возвращаемый вектор дат.
# Pittsburgh street cleaning; Every 2nd Tuesday from April to November
# Date range from January 1st, 2014 to January 1st, 2015
julia> dr = Dates.Date(2014):Dates.Date(2015);
julia> recur(dr) do x
Dates.dayofweek(x) == Dates.Tue &&
Dates.April <= Dates.month(x) <= Dates.Nov &&
Dates.dayofweekofmonth(x) == 2
end
8-element Array{Date,1}:
2014-04-08
2014-05-13
2014-06-10
2014-07-08
2014-08-12
2014-09-09
2014-10-14
2014-11-11
Дополнительные примеры и тесты доступны в test/dates/adjusters.jl.
Типы периодов
Периоды представляют собой человеческое представление о дискретных, иногда нерегулярных временных промежутках. Рассмотрим 1 месяц; в днях он может представлять значение 28, 29, 30 или 31 в зависимости от года и месяца. Или год может представлять 365 или 366 дней в случае високосного года. Типы Period — это простые Int64 оболочки, которые создаются путем обертывания любого типа, преобразуемого в Int64, т.е. Year(1) или Month(3.0). Арифметические операции между Period одного типа ведут себя как целые числа, и доступны ограниченные арифметические операции Period-Real.
julia> y1 = Dates.Year(1) 1 year julia> y2 = Dates.Year(2) 2 years julia> y3 = Dates.Year(10) 10 years julia> y1 + y2 3 years julia> div(y3,y2) 5 years julia> y3 - y2 8 years julia> y3 * y2 20 years julia> y3 % y2 0 years julia> y1 + 20 21 years julia> div(y3,3) # mirrors integer division 3 years
Округление
Значения Date и DateTime могут быть округлено до заданной точности (например, 1 месяц или 15 минут) с помощью floor(), ceil() или round():
julia> floor(Date(1985, 8, 16), Dates.Month) 1985-08-01 julia> ceil(DateTime(2013, 2, 13, 0, 31, 20), Dates.Minute(15)) 2013-02-13T00:45:00 julia> round(DateTime(2016, 8, 6, 20, 15), Dates.Day) 2016-08-07T00:00:00
В отличие от числового метода round(), который в случае равенства округляет к ближайшему четному числу по умолчанию, метод TimeType round() использует режим округления RoundNearestTiesUp. (Трудно представить, что бы означало округление к ближайшему «четному» TimeType). Более подробную информацию о доступных режимах RoundingMode можно найти в справочнике API.
Округление, как правило, ведет себя так, как ожидается, но есть несколько случаев, когда ожидаемое поведение не очевидно.
Округление Эпохи
Во многих случаях заданная точность округления (например, Dates.Second(30)) делится без остатка на следующий наибольший период (в данном случае, Dates.Minute(1)). Однако поведение округления в тех случаях, когда это не так, может вызвать путаницу. Какой ожидаемый результат округления DateTime до ближайших 10 часов?
julia> round(DateTime(2016, 7, 17, 11, 55), Dates.Hour(10)) 2016-07-17T12:00:00
Это может показаться запутанным, учитывая, что час (12) не делится нацело на 10. Причиной выбора 2016-07-17T12:00:00 является то, что он составляет 17 676 660 часов после 0000-01-01T00:00:00, а 17 676 660 делится на 10.
Поскольку значения Date и DateTime в Julia представляются в соответствии со стандартом ISO 8601, 0000-01-01T00:00:00 был выбран в качестве базового (или «эпохи округления») значения, с которого начинается подсчёт дней (и миллисекунд) при вычислениях округления. (Обратите внимание, что это немного отличается от внутреннего представления Date в Julia, использующего обозначение Rata Die; но поскольку стандарт ISO 8601 наиболее заметен для конечного пользователя, 0000-01-01T00:00:00 был выбран в качестве эпохи округления вместо 0000-12-31T00:00:00, используемого во внутренней реализации, для минимизации путаницы.)
Единственное исключение из использования 0000-01-01T00:00:00 в качестве эпохи округления — округление до недель. Округление до ближайшей недели всегда возвращает понедельник (первый день недели, как определено в ISO 8601). По этой причине мы используем 0000-01-03T00:00:00 (первый день первой недели года 0000, как определено в ISO 8601) в качестве основы при округлении до числа недель.
Вот похожий случай, в котором ожидаемое поведение не обязательно очевидно: что происходит, когда мы округляем до ближайшей P(2), где P — тип Period? В некоторых случаях (в частности, когда P <: Dates.TimePeriod) ответ очевиден:
julia> round(DateTime(2016, 7, 17, 8, 55, 30), Dates.Hour(2)) 2016-07-17T08:00:00 julia> round(DateTime(2016, 7, 17, 8, 55, 30), Dates.Minute(2)) 2016-07-17T08:56:00
Это кажется очевидным, потому что две единицы каждого из этих периодов по-прежнему делятся равномерно на следующий период более крупного порядка. Но в случае двух месяцев (которые по-прежнему делятся равномерно на один год) ответ может быть неожиданным:
julia> round(DateTime(2016, 7, 17, 8, 55, 30), Dates.Month(2)) 2016-07-01T00:00:00
Почему округлять до первого числа июля, хотя это месяц 7 (нечетное число)? Ключ в том, что месяцы индексируются с 1 (первый месяц имеет индекс 1), в отличие от часов, минут, секунд и миллисекунд (первый из которых имеет индекс 0).
Это означает, что округление DateTime до четного кратного секунд, минут, часов или лет (поскольку спецификация ISO 8601 включает нулевой год) приведет к DateTime с четным значением в этом поле, в то время как округление DateTime до четного кратного месяцев приведет к тому, что поле месяца будет иметь нечетное значение. Поскольку и месяцы, и годы могут содержать неравное число дней, нет уверенности, что округление до четного числа дней приведет к четному значению в поле дней.
Дополнительную информацию о методах, экспортируемых из модуля Dates, см. в справочнике API.
© 2009–2016 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/release-0.5/manual/dates/