Вычисление разницы дат за указанное количество дней с использованием класса LocalDate

Я использую openjdk версию 1.8.0_112-release для разработки, но вам также нужно будет поддерживать предыдущие версии JDK (pre-Java-8) - поэтому не можете использовать java.time.

Я пишу класс, чтобы рассчитать дату, чтобы увидеть, была ли сохраненная дата до текущей даты, что означает, что она истекла.

Однако я не уверен, что сделал это правильно. Я использую класс LocalDate для вычисления дней. Истечение рассчитывается начиная с даты и времени, когда пользователь нажал кнопку "Сохранить". Эта дата будет сохранена и будет выполнена проверка с этой сохраненной датой и временем и текущей датой и временем, то есть когда пользователь войдет в систему.

Это лучший способ сделать это? Я хотел бы придерживаться класса LocalDate.

import org.threeten.bp.LocalDate;

public final class Utilities {
    private Utilities() {}

    public static boolean hasDateExpired(int days, LocalDate savedDate, LocalDate currentDate) {
        boolean hasExpired = false;

        if(savedDate != null && currentDate != null) {
            /* has expired if the saved date plus the specified days is still before the current date (today) */
            if(savedDate.plusDays(days).isBefore(currentDate)) {
                hasExpired = true;
            }       
        }

        return hasExpired;
    }
}

Я использую класс следующим образом:

private void showDialogToIndicateLicenseHasExpired() {
    final LocalDate currentDate = LocalDate.now();
    final int DAYS_TO_EXPIRE = 3;
    final LocalDate savedDate = user.getSavedDate();

    if(hasDateExpired(DAYS_TO_EXPIRE, savedDate, currentDate)) {
        /* License has expired, warn the user */    
    }
}

Я ищу решение, которое будет учитывать часовые пояса. Если срок действия лицензии истекает через 3 дня, пользователь должен был перейти в другой часовой пояс. то есть они могут быть впереди или отставать по часам. Срок действия лицензии еще не закончен.

Ответ 1

Вы можете использовать ChronoUnit.DAYS (в пакете org.threeten.bp.temporal или в java.time.temporal, если вы используете родные классы java 8), чтобы вычислить количество дней между объектами 2 LocalDate:

if (savedDate != null && currentDate != null) {
    if (ChronoUnit.DAYS.between(savedDate, currentDate) > days) {
        hasExpired = true;
    }
}

Изменить (после объяснения баунти)

Для этого теста я использую threetenbp version 1.3.4

Как вы хотите, чтобы решение работало, даже если пользователь находится в другом часовом поясе, вы не должны использовать LocalDate, потому что этот класс не обрабатывает проблемы с часовым поясом.

Я считаю, что лучшим решением является использование класса Instant. Он представляет собой единый момент времени, независимо от того, в какой часовой зоне вы находитесь (на данный момент все в мире находятся в одном и том же мгновенном, хотя локальная дата и время могут отличаться в зависимости от того, где вы находитесь).

Фактически Instant всегда находится в UTC Time - стандартный независимый часовой пояс, поэтому очень подходит для ваш случай (так как вы хотите, чтобы вычисление не зависело от того, в каком часовом поясе находится пользователь).

Итак, ваши savedDate и currentDate должны быть Instant, и вы должны рассчитать разницу между ними.

Теперь, тонкая деталь. Вы хотите, чтобы истечение произошло через 3 дня. Для кода, который я сделал, я делаю следующие предположения:

  • 3 дня = 72 часа
  • 1 часть секунды через 72 часа, срок ее действия истек

Второе предположение важно для того, как я реализовал решение. Я рассматриваю следующие случаи:

  • currentDate составляет менее 72 часов после savedDate - не истек
  • currentDate ровно через 72 часа после savedDate - не истек (или истек? см. комментарии ниже)
  • currentDate составляет более 72 часов после savedDate (даже на долю секунды) - expired

Класс Instant имеет наносекундную точность, поэтому в случае 3 я считаю, что он истек, даже если он на 1 наносекунду через 72 часа:

import org.threeten.bp.Instant;
import org.threeten.bp.temporal.ChronoUnit;

public static boolean hasDateExpired(int days, Instant savedDate, Instant currentDate) {
    boolean hasExpired = false;

    if (savedDate != null && currentDate != null) {
        // nanoseconds between savedDate and currentDate > number of nanoseconds in the specified number of days
        if (ChronoUnit.NANOS.between(savedDate, currentDate) > days * ChronoUnit.DAYS.getDuration().toNanos()) {
            hasExpired = true;
        }
    }

    return hasExpired;
}

Обратите внимание, что я использовал ChronoUnit.DAYS.getDuration().toNanos() для получения количества наносекунд за один день. Лучше полагаться на API, а не на жестко заданные большие числа, подверженные ошибкам.

Я провел несколько тестов, используя даты в том же часовом поясе и в разных. Я использовал метод ZonedDateTime.toInstant() для преобразования дат в Instant:

import org.threeten.bp.ZoneId;
import org.threeten.bp.ZonedDateTime;

// testing in the same timezone
ZoneId sp = ZoneId.of("America/Sao_Paulo");
// savedDate: 22/05/2017 10:00 in Sao Paulo timezone
Instant savedDate = ZonedDateTime.of(2017, 5, 22, 10, 0, 0, 0, sp).toInstant();
// 1 nanosecond before expires (returns false - not expired)
System.out.println(hasDateExpired(3, savedDate, ZonedDateTime.of(2017, 5, 25, 9, 59, 59, 999999999, sp).toInstant()));
// exactly 3 days (72 hours) after saved date (returns false - not expired)
System.out.println(hasDateExpired(3, savedDate, ZonedDateTime.of(2017, 5, 25, 10, 0, 0, 0, sp).toInstant()));
// 1 nanosecond after 3 days (72 hours) (returns true - expired)
System.out.println(hasDateExpired(3, savedDate, ZonedDateTime.of(2017, 5, 25, 10, 0, 0, 1, sp).toInstant()));

// testing in different timezones (savedDate in Sao Paulo, currentDate in London)
ZoneId london = ZoneId.of("Europe/London");
// In 22/05/2017, London will be in summer time, so 10h in Sao Paulo = 14h in London
// 1 nanosecond before expires (returns false - not expired)
System.out.println(hasDateExpired(3, savedDate, ZonedDateTime.of(2017, 5, 25, 13, 59, 59, 999999999, london).toInstant()));
// exactly 3 days (72 hours) after saved date (returns false - not expired)
System.out.println(hasDateExpired(3, savedDate, ZonedDateTime.of(2017, 5, 25, 14, 0, 0, 0, london).toInstant()));
// 1 nanosecond after 3 days (72 hours) (returns true - expired)
System.out.println(hasDateExpired(3, savedDate, ZonedDateTime.of(2017, 5, 25, 14, 0, 0, 1, london).toInstant()));

PS: для случая 2 (currentDateровно через 72 часа после сохранения) - не истек) - если вы хотите, чтобы это было expired, просто измените if выше, чтобы использовать >= вместо >:

if (ChronoUnit.NANOS.between(savedDate, currentDate) >= days * ChronoUnit.DAYS.getDuration().toNanos()) {
    ... // it returns "true" for case 2
}

Если вы не хотите точности наносекунд и просто хотите сравнить дни между датами, вы можете сделать так, как в @Ole V.V answer. Я считаю, что наши ответы очень похожи (и я подозреваемый, что коды эквивалентны, хотя я не уверен), но я не проверял достаточно случаев, чтобы проверить, отличаются ли они в какой-либо конкретной ситуации.

Ответ 2

Ваш код в основном прекрасен. Я бы сделал это в основном так же, просто с деталями или двумя разными.

Как уже отмечал Хьюго, я бы использовал java.time.LocalDate и отказаться от использования ThreeTen Backport (если только это не является особым требованием, чтобы ваш код мог работать на Java 6 или 7).

Часовой пояс

Вы должны решить, в какой часовой зоне вы считаете свои дни. Также я бы предпочел, если вы сделаете часовой пояс явным в вашем коде. Если ваша система будет использоваться только в вашем собственном часовом поясе, выбор будет простым, просто сделайте это явным. Например:

    final LocalDate currentDate = LocalDate.now(ZoneId.of("Asia/Hong_Kong"));

Пожалуйста, заполните соответствующий идентификатор зоны. Это также обеспечит правильную работу программы, даже если в один прекрасный день произойдет ее запуск на компьютере с неправильной настройкой часового пояса. Если ваша система является глобальной, вы можете использовать UTC, например:

    final LocalDate currentDate = LocalDate.now(ZoneOffset.UTC);

Аналогичным образом вы захотите сделать это, сохранив дату, когда пользователь нажал "Сохранить", чтобы ваши данные были согласованы.

72 часа

Изменить: я понимаю из вашего комментария, что вы хотите измерить 3 дня, то есть 72 часа, со времени сохранения, чтобы определить, истек ли срок действия лицензии. Для этого a LocalDate не дает вам достаточно информации. Это только дата без часового времени, например, 26 мая 2017 года. Есть и другие варианты:

  • Instant - это момент времени (с точностью до наносекунд, даже). Это простое решение, чтобы убедиться, что истечение произойдет через 72 часа независимо от того, перемещается ли пользователь в другой часовой пояс.
  • ZonedDateTime представляет собой дату и время и часовой пояс, например 29 мая 2017 года. 19: 21: 33,783 при смещении GMT + 08: 00 [Asia/Hong_Kong]. Если вы хотите напомнить пользователю, когда было сохранено время, ZonedDateTime, вы можете предоставить эту информацию в часовом поясе, в котором была рассчитана дата сохранения.
  • Наконец, OffsetDateTime тоже будет работать, но, похоже, он не дает вам много преимуществ двух других, поэтому я не буду использовать эту возможность.

Поскольку во всех часовых поясах один и тот же момент, вы не указываете часовой пояс при получении текущего момента времени:

    final Instant currentDate = Instant.now();

Добавление 3 дней к Instant немного отличается от LocalDate, но остальная часть логики одинаков:

public static boolean hasDateExpired(int days, Instant savedDate, Instant currentDate) {
    boolean hasExpired = false;

    if(savedDate != null && currentDate != null) {
        /* has expired if the saved date plus the specified days is still before the current date (today) */
        if (savedDate.plus(days, ChronoUnit.DAYS).isBefore(currentDate)) {
            hasExpired = true;
        }       
    }

    return hasExpired;
}

Использование ZonedDateTime, с другой стороны, похоже на LocalDate в коде:

    final ZonedDateTime currentDate = ZonedDateTime.now(ZoneId.of("Asia/Hong_Kong"));

Если вы хотите установить текущий часовой пояс из JVM, где выполняется программа:

    final ZonedDateTime currentDate = ZonedDateTime.now(ZoneId.systemDefault());

Теперь, если вы объявите public static boolean hasDateExpired(int days, ZonedDateTime savedDate, ZonedDateTime currentDate), вы можете сделать так:

        /* has expired if the saved date plus the specified days is still before the current date (today) */
        if (savedDate.plusDays(days).isBefore(currentDate)) {
            hasExpired = true;
        }       

Это приведет к правильному сравнению, даже если два объекта ZonedDateTime находятся в двух разных часовых поясах. Поэтому независимо от того, будет ли пользователь переезжать в другой часовой пояс, у него не будет меньше и не больше часов до истечения срока действия лицензии.

Ответ 3

Ответ Хьюго и ответ Оле В.В. Оба правильны, и тот, что Ole V.V. является наиболее важным, поскольку часовой пояс имеет решающее значение для определения текущей даты.

Period

Другим полезным классом для этой работы является класс Period. Этот класс представляет собой промежуток времени, не привязанный к временной шкале, как количество лет, месяцев и дней.

Обратите внимание, что этот класс не подходит для представления истекшего времени, необходимого для этого Вопроса, потому что это представление "chunked" как годы, затем месяцы, а затем любые оставшиеся дни. Поэтому, если LocalDate.between( start , stop ) использовалось в течение нескольких недель, результат может быть примерно как "два месяца и три дня". Обратите внимание, что по этой причине этот класс не реализует интерфейс Comparable, поскольку нельзя сказать, что одна пара месяцев больше или меньше другой пары, если мы не знаем, какие конкретные месяцы задействованы.

Мы можем использовать этот класс для представления двухдневного льготного периода, упомянутого в Вопросе. Это делает наш код более самодокументированным. Лучше передать объект такого типа, чем передать простое целое число.

Period grace = Period.ofDays( 2 ) ;

LocalDate start = LocalDate.of( 2017 , Month.JANUARY , 23 ).plusDays( grace ) ;
LocalDate stop = LocalDate.of( 2017 , Month.MARCH , 7 ) ;

Мы используем ChronoUnit для расчета прошедших дней.

int days = ChronoUnit.DAYS.between( start , stop ) ;

Duration

Кстати, класс Duration похож на Period тем, что он представляет собой промежуток времени, не привязанный к график. Но Duration представляет общее количество целых секунд плюс дробная секунда, разрешенная в наносекундах. Из этого вы можете рассчитать несколько общих 24-часовых дней (не дней, основанных на дате), часов, минут, секунд и дробной секунды. Имейте в виду, что дни не всегда 24 часа; здесь, в Соединенных Штатах, в настоящее время они могут составлять 23, 24 или 25 часов из-за летнего времени.

Этот вопрос касается дней, основанных на дате, а не комков 24 часов. Поэтому класс Duration здесь не подходит.


О java.time

Структура java.time встроена в Java 8 и более поздних версий. Эти классы вытесняют неприятные старые legacy классы времени, такие как java.util.Date, Calendar и SimpleDateFormat.

Проект Joda-Time, теперь режим обслуживания, советуем перейти на классы java.time.

Чтобы узнать больше, см. Учебник Oracle. И поиск Qaru для многих примеров и объяснений. Спецификация JSR 310.

Где получить классы java.time?

  • Java SE 8, Java SE 9, а затем
    • Встроенный.
    • Часть стандартного Java API с объединенной реализацией.
    • Java 9 добавляет некоторые незначительные функции и исправления.
  • Java SE 6 и Java SE 7
    • Большая часть функциональных возможностей java.time обратно переносится на Java 6 и 7 в ThreeTen-Backport.
  • Android

Проект ThreeTen-Extra расширяет java.time с помощью дополнительных классов. Этот проект является доказательством возможных будущих дополнений к java.time. Здесь вы можете найти полезные классы, такие как Interval, YearWeek, YearQuarter и больше.

Ответ 4

Я думаю, гораздо лучше использовать это:

Duration.between(currentDate.atStartOfDay(), savedDate.atStartOfDay()).toDays() > days;

Duration, помещенный в пакет java.time.

Ответ 5

Поскольку этот вопрос не получает "достаточных ответов", я добавил еще один ответ:

Я использовал "SimpleDateFormat.setTimeZone(TimeZone.getTimeZone(" UTC ")); для установки часовой пояс в UTC. Таким образом, больше нет часового пояса (все дата/время будут установлены на UTC).

savedDate установлен в UTC.

dateTimeNow также устанавливается в UTC, с количеством истекших "дней" (отрицательное число), добавленных к dateTimeNow.

Новая дата expiresDate использует длинные миллисекунды с dateTimeNow

Проверить, если savedDate. до (expiresDate)


package com.chocksaway;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;

public class ExpiredDate {
    private static final long DAY_IN_MS = 1000 * 60 * 60 * 24;

    private static boolean hasDateExpired(int days, java.util.Date savedDate) throws ParseException {
        SimpleDateFormat dateFormatUtc = new SimpleDateFormat("yyyy-MMM-dd HH:mm:ss");
        dateFormatUtc.setTimeZone(TimeZone.getTimeZone("UTC"));

        // Local Date / time zone
        SimpleDateFormat dateFormatLocal = new SimpleDateFormat("yyyy-MMM-dd HH:mm:ss");

        // Date / time in UTC
        savedDate = dateFormatLocal.parse( dateFormatUtc.format(savedDate));

        Date dateTimeNow = dateFormatLocal.parse( dateFormatUtc.format(new Date()));

        long expires = dateTimeNow.getTime() + (DAY_IN_MS * days);

        Date expiresDate = new Date(expires);

        System.out.println("savedDate \t\t" + savedDate + "\nexpiresDate \t" + expiresDate);

        return savedDate.before(expiresDate);
    }

    public static void main(String[] args) throws ParseException {
        Calendar cal = Calendar.getInstance();
        cal.add(Calendar.DATE, 0);

        if (ExpiredDate.hasDateExpired(-2, cal.getTime())) {
            System.out.println("expired");
        } else {
            System.out.println("not expired");
        }

        System.out.print("\n");

        cal.add(Calendar.DATE, -3);

        if (ExpiredDate.hasDateExpired(-2, cal.getTime())) {
            System.out.println("expired");
        } else {
            System.out.println("not expired");
        }
    }
}

Запуск этого кода дает следующий вывод:

savedDate       Mon Jun 05 15:03:24 BST 2017
expiresDate     Sat Jun 03 15:03:24 BST 2017
not expired

savedDate       Fri Jun 02 15:03:24 BST 2017
expiresDate     Sat Jun 03 15:03:24 BST 2017
expired

Все даты/время - UTC. Первый срок не истек. Второе истекло (savedDate до expiresDate).

Ответ 6

ПОЦЕЛУЙ

public static boolean hasDateExpired(int days, java.util.Date savedDate) {
    long expires = savedDate().getTime() + (86_400_000L * days);
    return System.currentTimeMillis() > expires;
}

Работает на старой JRE просто отлично. Date.getTime() дает миллисекунды UTC, поэтому часовой пояс даже не фактор. Магия 86'400'000 - это количество миллисекунд в день.

Вместо использования java.util.Date вы можете упростить это, если вы просто используете long for savedTime.

Ответ 7

Я создал простой класс утилиты ExpiredDate с TimeZone (например, CET), expiredDate, expireDays и разницу InHoursMillis.

Я использую java.util.Date и Date.before(expiredDate):

Чтобы узнать, умножена ли дата() на expiryDays plus (разница в часовом поясе, умноженная на expiryDays), до expiredDate.

Любая дата, которая старше, чем expiredDate, "истек".

Новая дата создается добавлением (i) + (ii):

(я). Я использую количество миллисекунд в день (DAY_IN_MS = 1000 * 60 * 60 * 24), которое умножается на число (число) expireDays.

+

(II). Чтобы иметь дело с другим TimeZone, я нахожу количество миллисекунд между часовым поясом Default (для меня BST), а TimeZone (например, CET) передается в ExpiredDate. Для CET разница составляет один час, что составляет 3600000 миллисекунд. Это умножается на (число) expireDays.

Новая дата возвращается из parseDate().

Если новая дата до истечения срока действия expiredDate → устарела до значения True.   dateTimeWithExpire.before(expiredDate);

Я создал 3 теста:

  • Установите дату истечения срока действия 7 дней, а expireDays = 3

    Истек срок действия (7 дней больше 3 дней)

  • Установите дату/время истечения срока действия и истечет срок до 2 дней

    Истек срок действия - поскольку часовой пояс CET добавляет два часа (один час в день) к dateTimeWithExpire

  • Установите дату истечения срока действия 1 день, а expireDays = 2 (1 день - менее 2 дней)

expired истинно


package com.chocksaway;

import java.text.ParseException;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;

public class ExpiredDate {
    /**
     * milliseconds in a day
    */
    private static final long DAY_IN_MS = 1000 * 60 * 60 * 24;
    private String timeZone;
    private Date expiredDate;
    private int expireDays;
    private int differenceInHoursMillis;

    /**
     *
     * @param timeZone - valid timezone
     * @param expiredDate - the fixed date for expiry
     * @param expireDays - the number of days to expire
    */
    private ExpiredDate(String timeZone, Date expiredDate, int expireDays) {
        this.expiredDate = expiredDate;
        this.expireDays = expireDays;
        this.timeZone = timeZone;

        long currentTime = System.currentTimeMillis();
        int zoneOffset =   TimeZone.getTimeZone(timeZone).getOffset(currentTime);
        int defaultOffset = TimeZone.getDefault().getOffset(currentTime);

        /**
         * Example:
         *     TimeZone.getTimeZone(timeZone) is BST
         *     timeZone is CET
         *
         *     There is one hours difference, which is 3600000 milliseconds
         *
         */

        this.differenceInHoursMillis = (zoneOffset - defaultOffset);
    }


    /**
     *
     * Subtract a number of expire days from the date
     *
     * @param dateTimeNow - the date and time now
     * @return - the date and time minus the number of expired days
     *           + (difference in hours for timezone * expiryDays)
     *
     */
    private Date parseDate(Date dateTimeNow) {
        return new Date(dateTimeNow.getTime() - (expireDays * DAY_IN_MS) + (this.differenceInHoursMillis * expireDays));
    }


    private boolean hasDateExpired(Date currentDate) {
        Date dateTimeWithExpire = parseDate(currentDate);

        return dateTimeWithExpire.before(expiredDate);
    }


    public static void main(String[] args) throws ParseException {

        /* Set the expiry date 7 days, and expireDays = 3
        *
        * Not expired
        */

        Calendar cal = Calendar.getInstance();
        cal.add(Calendar.DATE, -7);

        ExpiredDate expired = new ExpiredDate("CET", cal.getTime(), 3);

        Date dateTimeNow = new Date();

        if (expired.hasDateExpired(dateTimeNow)) {
            System.out.println("expired");
        } else {
            System.out.println("NOT expired");

        }

        /* Set the expiry date / time, and expireDays to 2 days
         *  Not expired - because the CET timezone adds two hours to the dateTimeWithExpire
        */


        cal = Calendar.getInstance();
        cal.add(Calendar.DATE, -2);

        expired = new ExpiredDate("CET", cal.getTime(), 2);

        dateTimeNow = new Date();

        if (expired.hasDateExpired(dateTimeNow)) {
            System.out.println("expired");
        } else {
            System.out.println("NOT expired");

        }

        /* Set the expiry date 1 days, and expireDays = 2
        *
        * expired
        */

        cal = Calendar.getInstance();
        cal.add(Calendar.DATE, -1);

        expired = new ExpiredDate("CET", cal.getTime(), 2);

        dateTimeNow = new Date();

        if (expired.hasDateExpired(dateTimeNow)) {
            System.out.println("expired");
        } else {
            System.out.println("NOT expired");

        }

     } 
 }