ОК, это будет длинный и сложный ответ (ура!).
В целом вы на правильном пути, но есть несколько мелких ошибок.
Концептуализация дат
К настоящему времени (я надеюсь) уже достаточно хорошо известно, что Date
представляет собой мгновение во времени.Что не так хорошо известно, так это обратное.Date
представляет момент, но что представляет значение календаря?
Когда вы думаете об этом, «день» или «час» (или даже «месяц») представляют собой диапазон .Это значения, которые представляют все возможные моменты времени между моментом начала и моментом окончания.
По этой причине я считаю наиболее полезным думать о диапазонах , когда я имею дело с календаремценности.Другими словами, спрашивая «каков диапазон для этой минуты / часа / дня / недели / месяца / года?»и т.д.
С этим, я думаю, вы поймете, что вам легче понять, что происходит.Вы уже поняли Range<Int>
, верно?Так что Range<Date>
также интуитивно понятен.
К счастью, есть API на Calendar
для получения диапазона.К сожалению, мы не можем просто использовать Range<Date>
из-за удержания API со времен Objective-C.Но мы получаем NSDateInterval
, что фактически одно и то же.
Вы запрашиваете диапазоны, запрашивая у Calendar
диапазон часов / дней / недель / месяцев / лет, который содержит определенный момент,вот так:
let now = Date()
let calendar = Calendar.current
let today = calendar.dateInterval(of: .day, for: now)
Вооружившись этим, вы можете получить диапазон всего, что угодно:
let units = [Calendar.Component.second, .minute, .hour, .day, .weekOfMonth, .month, .quarter, .year, .era]
for unit in units {
let range = calendar.dateInterval(of: unit, for: now)
print("Range of \(unit): \(range)")
}
В то время, когда я пишу это, он печатает:
Range of second: Optional(2018-10-04 19:50:10 +0000 to 2018-10-04 19:50:11 +0000)
Range of minute: Optional(2018-10-04 19:50:00 +0000 to 2018-10-04 19:51:00 +0000)
Range of hour: Optional(2018-10-04 19:00:00 +0000 to 2018-10-04 20:00:00 +0000)
Range of day: Optional(2018-10-04 06:00:00 +0000 to 2018-10-05 06:00:00 +0000)
Range of weekOfMonth: Optional(2018-09-30 06:00:00 +0000 to 2018-10-07 06:00:00 +0000)
Range of month: Optional(2018-10-01 06:00:00 +0000 to 2018-11-01 06:00:00 +0000)
Range of quarter: Optional(2018-10-01 06:00:00 +0000 to 2019-01-01 07:00:00 +0000)
Range of year: Optional(2018-01-01 07:00:00 +0000 to 2019-01-01 07:00:00 +0000)
Range of era: Optional(0001-01-03 00:00:00 +0000 to 139369-07-19 02:25:04 +0000)
Вы можете увидеть, как отображаются соответствующие единицы измерения.Здесь нужно указать на две вещи:
Этот метод возвращает необязательный (DateInterval?
), потому что могут быть странные ситуации, когда он не работает.Я не могу думать ни о чём, кроме макушки головы, но календари - переменчивые вещи, так что не забывайте об этом.
Просьба о диапазоне эры обычно является бессмысленной операцией, дажехотя эпохи имеют жизненно важное значение для правильного построения дат.Трудно иметь большую точность с периодами за пределами определенных календарей (в первую очередь, японского календаря), так что значение, говорящее «Общая эра (CE или AD), начатое 3 января года 1», вероятно, неверно, но мы не можемдействительно делать что-нибудь об этом.Кроме того, конечно, мы не знаем, когда закончится текущая эра, поэтому мы просто предполагаем, что она длится вечно.
Печать Date
значений
Это приноситя к следующему пункту, о печати дат.Я заметил это в вашем коде, и я думаю, что это может быть источником некоторых ваших проблем.Я приветствую ваши усилия по объяснению мерзости перехода на летнее время, но я думаю, что вы увлеклись чем-то, что на самом деле не является ошибкой, но вот что:
Dates
всегда печать в UTC.
всегда всегда всегда.
Это потому, что они являются моментами во времени, и они являются одним и тем же моментом независимо от того,в Калифорнии, Джибути, Хатманду или где-то еще.Значение Date
не имеет часовой пояс.Это действительно просто смещение от другого известного момента времени.Однако печать 560375786.836208
в виде Date's
не кажется настолько полезной для людей, поэтому создатели API заставили его печатать как григорианскую дату относительно часового пояса UTC.
Если вы хотитенапечатайте дату, даже для отладки , вам почти наверняка понадобится DateFormatter
.
DateComponents.date
Это не проблема с вашимкод, скажем так, но это скорее хедз-ап.Свойство .date
в DateComponents
не гарантирует возврат первого момента, соответствующего указанным компонентам.Это не гарантирует возврат последнего момента, который соответствует компонентам.Все, что он делает, это гарантирует, что, если он сможет найти ответ, он даст вам Date
где-то в диапазоне указанных единиц.
Я не думаю, что вы технически полагаетесь на это в своем коде, но это полезно знать, и я видел это недоразумение, вызывающее ошибки в программном обеспечении.
Имея все это в виду, давайте посмотрим на то, что вы хотите сделать:
- Вы хотите знать диапазон года
- Вы хотите знать всемесяцы в году
- Вы хотите знать все дни месяца
- Вы хотите знать неделю, в которую входит первый день месяца
Итак, исходя из того, что мы узнали о диапазонах, теперь это должно быть довольно просто, и, что интересно, мы можем сделать все это без единого значения DateComponents
.В приведенном ниже коде для краткости я буду принудительно распаковывать значения.В вашем реальном коде вы, вероятно, захотите улучшить обработку ошибок.
Диапазон года
Это просто:
let yearRange = calendar.dateInterval(of: .year, for: now)!
Месяцы в году
Это немного сложнее, но не так уж плохо.
var monthsOfYear = Array<DateInterval>()
var startOfCurrentMonth = yearRange.start
repeat {
let monthRange = calendar.dateInterval(of: .month, for: startOfCurrentMonth)!
monthsOfYear.append(monthRange)
startOfCurrentMonth = monthRange.end.addingTimeInterval(1)
} while yearRange.contains(startOfCurrentMonth)
По сути, мы собираемся начать с первого момента года и спросить календарь на месяцэто содержит тот момент.Получив это, мы получим последний момент этого месяца и добавим к нему одну секунду, чтобы (якобы) перенести его на следующий месяц.Затем мы получим диапазон месяца, который содержит это мгновение, и так далее.Повторяйте, пока у нас не будет значения, которого больше нет в году, что означает, что мы агрегировали все диапазоны месяцев в начальном году.
Когда я запускаю это на своей машине, я получаю такой результат:
[
2018-01-01 07:00:00 +0000 to 2018-02-01 07:00:00 +0000,
2018-02-01 07:00:00 +0000 to 2018-03-01 07:00:00 +0000,
2018-03-01 07:00:00 +0000 to 2018-04-01 06:00:00 +0000,
2018-04-01 06:00:00 +0000 to 2018-05-01 06:00:00 +0000,
2018-05-01 06:00:00 +0000 to 2018-06-01 06:00:00 +0000,
2018-06-01 06:00:00 +0000 to 2018-07-01 06:00:00 +0000,
2018-07-01 06:00:00 +0000 to 2018-08-01 06:00:00 +0000,
2018-08-01 06:00:00 +0000 to 2018-09-01 06:00:00 +0000,
2018-09-01 06:00:00 +0000 to 2018-10-01 06:00:00 +0000,
2018-10-01 06:00:00 +0000 to 2018-11-01 06:00:00 +0000,
2018-11-01 06:00:00 +0000 to 2018-12-01 07:00:00 +0000,
2018-12-01 07:00:00 +0000 to 2019-01-01 07:00:00 +0000
]
Опять же, важно отметить, что эти даты указаны в UTC, хотя я нахожусь в горном часовом поясе.Из-за этого время смещается между 07:00 и 06:00, потому что часовой пояс горы отстает от UTC на 7 или 6 часов, в зависимости от того, наблюдаем ли мы летнее время.Таким образом, эти значения точны для часового пояса моего календаря.
Дни в месяце
Это похоже на предыдущий код:
var daysInMonth = Array<DateInterval>()
var startOfCurrentDay = currentMonth.start
repeat {
let dayRange = calendar.dateInterval(of: .day, for: startOfCurrentDay)!
daysInMonth.append(dayRange)
startOfCurrentDay = dayRange.end.addingTimeInterval(1)
} while currentMonth.contains(startOfCurrentDay)
И когда я запускаю это,Я получаю это:
[
2018-10-01 06:00:00 +0000 to 2018-10-02 06:00:00 +0000,
2018-10-02 06:00:00 +0000 to 2018-10-03 06:00:00 +0000,
2018-10-03 06:00:00 +0000 to 2018-10-04 06:00:00 +0000,
... snipped for brevity ...
2018-10-29 06:00:00 +0000 to 2018-10-30 06:00:00 +0000,
2018-10-30 06:00:00 +0000 to 2018-10-31 06:00:00 +0000,
2018-10-31 06:00:00 +0000 to 2018-11-01 06:00:00 +0000
]
Неделя с началом месяца
Давайте вернемся к массиву monthsOfYear
, который мы создали выше.Мы будем использовать это, чтобы вычислить недели для каждого месяца:
for month in monthsOfYear {
let weekContainingStart = calendar.dateInterval(of: .weekOfMonth, for: month.start)!
print(weekContainingStart)
}
И для моего часового пояса это напечатает:
2017-12-31 07:00:00 +0000 to 2018-01-07 07:00:00 +0000
2018-01-28 07:00:00 +0000 to 2018-02-04 07:00:00 +0000
2018-02-25 07:00:00 +0000 to 2018-03-04 07:00:00 +0000
2018-04-01 06:00:00 +0000 to 2018-04-08 06:00:00 +0000
2018-04-29 06:00:00 +0000 to 2018-05-06 06:00:00 +0000
2018-05-27 06:00:00 +0000 to 2018-06-03 06:00:00 +0000
2018-07-01 06:00:00 +0000 to 2018-07-08 06:00:00 +0000
2018-07-29 06:00:00 +0000 to 2018-08-05 06:00:00 +0000
2018-08-26 06:00:00 +0000 to 2018-09-02 06:00:00 +0000
2018-09-30 06:00:00 +0000 to 2018-10-07 06:00:00 +0000
2018-10-28 06:00:00 +0000 to 2018-11-04 06:00:00 +0000
2018-11-25 07:00:00 +0000 to 2018-12-02 07:00:00 +0000
Одна вещь, которую вы заметите, это то, чтоэто воскресенье как первый день недели.Например, на скриншоте у вас есть неделя с 1 февраля, начинающаяся 29 числа.
Это поведение регулируется свойством firstWeekday
в Calendar
.По умолчанию это значение, вероятно, равно 1
(в зависимости от вашей локали), что означает, что недели начинаются в воскресенье.Если вы хотите, чтобы ваш календарь выполнял вычисления, когда неделя начинается в понедельник , тогда вы измените значение этого свойства на 2
:
var weeksStartOnMondayCalendar = calendar
weeksStartOnMondayCalendar.firstWeekday = 2
for month in monthsOfYear {
let weekContainingStart = weeksStartOnMondayCalendar.dateInterval(of: .weekOfMonth, for: month.start)!
print(weekContainingStart)
}
Теперь, когда я запускаю этокод, я вижу, что неделя, содержащая 1 февраля, «начинается» 29 января:
2018-01-01 07:00:00 +0000 to 2018-01-08 07:00:00 +0000
2018-01-29 07:00:00 +0000 to 2018-02-05 07:00:00 +0000
2018-02-26 07:00:00 +0000 to 2018-03-05 07:00:00 +0000
2018-03-26 06:00:00 +0000 to 2018-04-02 06:00:00 +0000
2018-04-30 06:00:00 +0000 to 2018-05-07 06:00:00 +0000
2018-05-28 06:00:00 +0000 to 2018-06-04 06:00:00 +0000
2018-06-25 06:00:00 +0000 to 2018-07-02 06:00:00 +0000
2018-07-30 06:00:00 +0000 to 2018-08-06 06:00:00 +0000
2018-08-27 06:00:00 +0000 to 2018-09-03 06:00:00 +0000
2018-10-01 06:00:00 +0000 to 2018-10-08 06:00:00 +0000
2018-10-29 06:00:00 +0000 to 2018-11-05 07:00:00 +0000
2018-11-26 07:00:00 +0000 to 2018-12-03 07:00:00 +0000
Заключительные мысли
Со всем этим у вас есть все инструменты, которые вынеобходимо создать тот же пользовательский интерфейс, который показывает Calendar.app.
В качестве дополнительного бонуса весь код, который я разместил здесь, работает независимо от вашего календаря, часового пояса и локали .Он будет работать для создания пользовательских интерфейсов для японского календаря, коптских календарей, еврейского календаря, ISO8601 и т. Д. Он будет работать в любом часовом поясе и в любой локали.
Кроме того, имеявсе подобные значения DateInterval
, вероятно, облегчат реализацию вашего приложения, потому что выполнение проверки "содержит диапазон" - это проверка, которую вы хотите делать при создании приложения календаря.
Единственное, что нужно сделать, это убедиться, что вы используете DateFormatter
при рендеринге этих значений в String
.Пожалуйста, не извлекайте отдельные компоненты даты.
Если у вас есть дополнительные вопросы, опубликуйте их как новые вопросы и @mention
меня в комментарии.