Расчет разницы дат за указанное количество дней с помощью класса 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 дня, и пользователь должен был отправиться в другой часовой пояс. то есть они могут быть впереди или позади, основываясь на часах. Срок действия лицензии еще должен истечь.

7 ответов

Решение

Ты можешь использовать ChronoUnit.DAYSorg.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 версии 1.3.4

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

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

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

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

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

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

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

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

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

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 часа после saveDate - не истек) - если вы хотите, чтобы это истекло, просто измените if выше, чтобы использовать >= вместо >:

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

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

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

Как уже заметил Хьюго, я бы использовал 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 часа, от времени сохранения, чтобы определить, истек ли срок действия лицензии. Для этого 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 объекты находятся в двух разных часовых поясах. Поэтому, независимо от того, путешествует ли пользователь в другой часовой пояс, он / она не получит ни меньше, ни больше часов до истечения срока действия лицензии.

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

Period

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

Обратите внимание, что этот класс не подходит для представления истекшего времени, необходимого для этого Вопроса, потому что это представление "разбивается" на годы, затем месяцы, а затем любые оставшиеся дни. Так что если 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 и более поздние версии. Эти классы вытесняют проблемные старые классы даты и времени, такие как java.util.Date, Calendar & SimpleDateFormat,

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

Чтобы узнать больше, смотрите Oracle Tutorial. И поиск переполнения стека для многих примеров и объяснений. Спецификация 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 и многое другое.

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

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

Duration класс размещен в java.time пакет.

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

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

Сохраненная дата установлена ​​в UTC.

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

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

Проверьте, если сохраненоДата.до(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

Все даты / время указаны по Гринвичу. Первый не истек. Вторая истекла (сохраненная дата предшествует expiresDate).

Я построил простой служебный класс ExpiredDate с TimeZone (например, CET), expiredDate, expireDays и diffInHoursMillis.

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

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

Любая дата старше expiredDate является "истекшей".

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

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

+

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

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

Если новая Дата предшествует expiredDate -> set expired to True. dateTimeWithExpire.before(expiredDate);

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

  1. Установите срок действия 7 дней, а expireDays = 3

    Не истек (7 дней больше 3 дней)

  2. Установите дату и время истечения, и expireDays равными 2 дням

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

  3. Установите срок действия 1 день, а expireDays = 2 (1 день меньше 2 дней)

истек истина


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");

        }

     } 
 }

ПОЦЕЛУЙ

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 для saveTime.

Другие вопросы по тегам