Анализируя ваши ожидания (в псевдокоде)
startDate.plus(Period.between(startDate, endDate)) == endDate
мы должны обсудить несколько тем:
- как обрабатывать отдельные единицы, например месяцы или дни?
- как определяется сложение длительности (или «периода»)?
- как определить временное расстояние (длительность) между двумя датами?
- как вычитается длительность (или "период") определен?
Давайте сначала посмотрим на единицы. Дни не проблема, поскольку они являются наименьшей возможной календарной единицей, и каждая календарная дата отличается от любой другой даты. в полных целых числах дней. Таким образом, в псевдокоде мы всегда равны, если положительный или отрицательный:
startDate.plus(ChronoUnit.Days.between(startDate, endDate)) == endDate
Месяцы сложны, поскольку григорианский календарь определяет календарные месяцы различной длины. Таким образом, может возникнуть ситуация, когда добавление любого целого числа месяцев к дате может привести к неверной дате:
[2019-08-31] + P1M = [2019-09-31]
Решение java.time
сократить дату окончания до действительной - здесь [2019-09-30] - является разумным и соответствует ожиданиям большинства пользователей, поскольку окончательная дата все еще сохраняет расчетный месяц. Однако это добавление , включая исправление на конец месяца, НЕ обратимо , см. Обратную операцию, называемую вычитанием:
[2019-09-30] - P1M = [2019-08-30]
Результат также является разумным, потому что а) основное правило сложения месяца состоит в том, чтобы максимально сохранить дневной день месяца и б) [2019-08-30] + P1M = [2019-09-30].
Что такое точное добавление длительности (периода)?
В java.time
, Period
представляет собой композициюпредметы, состоящие из лет, месяцев и дней с любыми целыми частичными суммами. Таким образом, добавление Period
может быть разрешено добавлением частичных сумм к начальной дате. Поскольку годы всегда могут быть преобразованы в 12-кратные месяцы, мы можем сначала объединить годы и месяцы, а затем суммировать их за один шаг, чтобы избежать странных побочных эффектов в високосные годы. Дни могут быть добавлены на последнем шаге. Разумный план, как в java.time
.
Как определить правильное Period
между двумя датами?
Давайте сначала обсудим случай, когда продолжительность положительна, означая, что начальная дата предшествует конечной дате. Затем мы всегда можем определить продолжительность, сначала определив разницу в месяцах, а затем в днях. Этот порядок важен для достижения компонента месяца, потому что в противном случае каждая продолжительность между двумя датами будет состоять только из дней. Используя даты вашего примера:
[2019-09-30] + P1M1D = [2019-10-31]
Технически, дата начала сначала перемещается вперед на вычисленную разницу в месяцах междуначало и конец. Затем дельта дня как разница между перенесенной датой начала и датой окончания добавляется к перенесенной дате начала. Таким образом, мы можем рассчитать продолжительность как P1M1D в примере. Пока все разумно.
Как вычесть длительность?
Наиболее интересным моментом в предыдущем примере сложения является то, что случайно в конце месяца НЕТкоррекция. Тем не менее java.time
не может выполнить обратное вычитание. Сначала вычитаются месяцы, а затем дни:
[2019-10-31] - P1M1D = [2019-09-29]
Если вместо этого java.time
попытался изменить шагив добавлении до этого естественным выбором было бы сначала вычесть дни, а затем месяцы . С этим измененным заказом мы получили бы [2019-09-30]. Измененный порядок в вычитании поможет, если на соответствующем шаге сложения не было коррекции на конец месяца. Это особенно верно, если день месяца любой начальной или конечной даты не превышает 28 (минимально возможная длина месяца). К сожалению, java.time
определил другой дизайн для вычитания Period
, что приводит к менее последовательным результатам.
Является ли добавление продолжительности обратимым в вычитании?
Сначала мы должны понять, что предложенный измененный порядок вычитания продолжительности из данной календарной даты не гарантируетобратимость сложения. Пример счетчика, к которому добавлена поправка на конец месяца:
[2011-03-31] + P3M1D = [2011-06-30] + P1D = [2011-07-01] (ok)
[2011-07-01] - P3M1D = [2011-06-30] - P3M = [2011-03-30] :-(
Изменение заказа не является плохим, поскольку дает более согласованные результаты. Но как вылечить оставшиеся недостатки? Остался только один способ - изменить расчет продолжительности. Вместо использования P3M1D мы можем видеть, что продолжительность P2M31D будет работать в обоих направлениях:
[2011-03-31] + P2M31D = [2011-05-31] + P31D = [2011-07-01] (ok)
[2011-07-01] - P2M31D = [2011-05-31] - P2M = [2011-03-31] (ok)
Таким образом, идея состоит в том, чтобы изменить нормализацию вычисленной продолжительности. Это можно сделать, посмотрев, является ли добавление вычисленной дельты месяца обратимым на шаге вычитания, т.е. избегает необходимости коррекции на конец месяца. java.time
, к сожалению, не предлагает такого решения. Это не ошибка, но может рассматриваться как ограничение дизайна.
Альтернативы?
Я улучшил свою библиотеку времени Time4J с помощью обратимыхметрики, которые раскрывают идеи, приведенные выше. Смотрите следующий пример:
PlainDate d1 = PlainDate.of(2011, 3, 31);
PlainDate d2 = PlainDate.of(2011, 7, 1);
TimeMetric<CalendarUnit, Duration<CalendarUnit>> metric =
Duration.inYearsMonthsDays().reversible();
Duration<CalendarUnit> duration =
metric.between(d1, d2); // P2M31D
Duration<CalendarUnit> invDur =
metric.between(d2, d1); // -P2M31D
assertThat(d1.plus(duration), is(d2)); // first invariance
assertThat(invDur, is(duration.inverse())); // second invariance
assertThat(d2.minus(duration), is(d1)); // third invariance