Java Разница между текущей датой и прошлой датой в годах, месяцах, днях, часах, минутах, секундах

Я знаю, что об этом уже много раз спрашивали здесь, но я не смог найти ничего конкретного для моего случая. Я пытаюсь найти разницу между текущей датой и временем и предыдущей датой, каждая из которых имеет формат yyyy-MM-dd HH:mm:ss.s. Основываясь на ответе, данном здесь, я придумал следующий код :

SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.s");
String earliestRunTime = "2017-12-16 01:30:08.0";
Date currentDate = new Date();
log.info("Current Date: {}", format.format(currentDate));

try {
    Date earliestDate = format.parse(earliestRunTime);

    long diff = currentDate.getTime() - earliestDate.getTime();

    long diffSeconds = diff / 1000 % 60;
    long diffMinutes = diff / (60 * 1000) % 60;
    long diffHours = diff / (60 * 60 * 1000) % 24;
    long diffDays = diff / (24 * 60 * 60 * 1000) % 30;
    long diffMonths = diff / (30 * 24 * 60 * 60 * 1000) % 12;
    long diffYears = diff / (12 * 30 * 24 * 60 * 60 * 1000);

    return String.format("%s years, %s months, %s days, %s hours, %s minutes, %s seconds",
            diffYears, diffMonths, diffDays, diffHours, diffMinutes, diffSeconds);


    }
catch (Exception e) {
    e.printStackTrace();
    return e.getMessage();
}

Когда я запускаю код, JSON возвращает следующий результат:

lifetime: "41 years, -1 months, 14 days, 9 hours, 42 minutes, 37 seconds"

У меня тут два вопроса:

  1. Где я ошибаюсь в своих расчетах 41 год и отрицательное число?
  2. Есть ли лучший способ сделать это? Моя текущая настройка не учитывает високосные годы или 365-дневный год, и мне нужно это учитывать.

person SVill    schedule 24.04.2019    source источник
comment
Как должен выглядеть результат? Если разница составила 1 год, вы хотите напечатать 1 год, 12 месяцев, 365 дней или 1 год, 0 месяцев, 0 дней?   -  person NeplatnyUdaj    schedule 24.04.2019
comment
Просто мысль: если отчетность с точностью до секунды важна, но промежуток времени может включать месяцы с 28, 29, 30 или 31 днем, возможно, вы захотите переосмыслить свое использование -кейс.   -  person Solomon Slow    schedule 24.04.2019
comment
@NeplatnyUdaj В идеале разница должна выглядеть как 1 год, 0 месяцев, 0 дней.   -  person SVill    schedule 24.04.2019
comment
@SolomonSlow Хороший вопрос, я даже не думал об этом.   -  person SVill    schedule 24.04.2019
comment
Вы используете ужасные классы, которые много лет назад были вытеснены java.time. Никогда не используйте Date.   -  person Basil Bourque    schedule 25.04.2019
comment
Я вполне убежден, что этот вопрос должен быть дубликатом. Насколько тщательно вы искали?   -  person Ole V.V.    schedule 26.04.2019


Ответы (3)


Используя тот же подход, что и вы, вам нужно явно указать знаменатель как длинные значения. В настоящее время он предполагает, что они являются целыми числами, что вызывает числовое переполнение, то есть вычисленное значение слишком велико для целого числа. Это объясняет, почему вы получаете отрицательные/произвольные значения. Исправить просто:

SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.s");
String earliestRunTime = "2017-12-16 01:30:08.0";
Date currentDate = new Date();
log.info("Current Date: {}" + format.format(currentDate));

try {
    Date earliestDate = format.parse(earliestRunTime);

    long diff = currentDate.getTime() - earliestDate.getTime();

    long diffSeconds = diff / 1000L % 60L;
    long diffMinutes = diff / (60L * 1000L) % 60L;
    long diffHours = diff / (60L * 60L * 1000L) % 24L;
    long diffDays = diff / (24L * 60L * 60L * 1000L) % 30L;
    long diffMonths = diff / (30L * 24L * 60L * 60L * 1000L) % 12L;
    long diffYears = diff / (12L * 30L * 24L * 60L * 60L * 1000L);

    return String.format("%s years, %s months, %s days, %s hours, %s minutes, %s seconds",
            diffYears, diffMonths, diffDays, diffHours, diffMinutes, diffSeconds);


}
catch (Exception e) {
    e.printStackTrace();
    return e.getMessage();
}
person KevinThePepper23    schedule 24.04.2019

Где я ошибаюсь в своих расчетах 41 года и отрицательного числа?

Потому что знаменатель переполнится. Вам нужно использовать Long:

long diffMonths = diff / (30 * 24 * 60 * 60 * 1000L) % 12; //Notice the L at the end
long diffYears = diff / (12 * 30 * 24 * 60 * 60 * 1000L); //Notice the L at the end

Также обратите внимание, что 12 * 30 — очень плохое приближение количества дней в году.

Есть ли лучший способ сделать это?

Да. Используйте API-интерфейс Duration из Java 8. https://www.mkyong.com/java8/java-8-period-and-duration-examples/

Трудно дать точный ответ, потому что вопрос немного расплывчатый. Например, если один год был високосным, и вы сравнивали даты 2020/03/28 и 2021/03/28, каким должен быть результат? 1 год или 1 год, 1 день? (2020 год високосный, поэтому после 28 марта будет еще и 29 марта)

person NeplatnyUdaj    schedule 24.04.2019
comment
Високосный год следует обрабатывать как 1 год и 1 день. - person SVill; 24.04.2019

  1. Где я ошибаюсь в своих расчетах 41 год и отрицательное число?

Помимо использования общеизвестно проблемного и давно устаревшего класса SimpleDateFormat и столь же устаревшего класса Date, в вашем коде есть следующие ошибки:

  • Вы анализируете 08.0 как 8 секунд 0 секунд. На моем JDK-11 SimpleDateFormat выбирает 0 секунд и отбрасывает 8 секунд, которые я считаю правильными. SimpleDateFormat не может разобрать один десятичный знак в секундах (только ровно три десятичных знака), поэтому решение этой ошибки полностью отбрасывает SimpleDateFormat.
  • Как уже говорили другие, у вас есть переполнение int в ваших умножениях. Например, 30 * 24 * 60 * 60 * 1000 должно дать 2 592 000 000, но поскольку int не может содержать это число, вместо этого вы получите -1 702 967 296. Поскольку это отрицательное число, следующее деление дает отрицательное число месяцев.
  • Как указал Соломон Слоу в комментарии, в месяце может быть 28, 29, 30 или 31 день. Устанавливая для всех месяцев значение 30 дней, вы рискуете получить неверное количество дней и месяцев, а в конце также и годы. Когда я запустил ваш код сегодня, правильным ответом было бы 1 год, 4 месяца, 13 дней, но вместо этого я получил 19 дней, что на 6 дней больше.
  • Вы не учитываете летнее время (DST) и другие временные аномалии. Это может привести к тому, что в сутках будет, например, 23 или 25 часов, что приведет к ошибке.

Или резюмируя: Ваша ошибка заключалась в том, что вы пытались сделать расчет «вручную». Математика даты и времени слишком сложна и подвержена ошибкам, чтобы сделать это. Вместо этого вы всегда должны оставлять это хорошо зарекомендовавшим себя библиотечным классам.

  1. Есть ли лучший способ сделать это? Моя текущая настройка не учитывает високосные годы или 365-дневный год, и мне нужно это учитывать.

Да, есть гораздо лучший способ. Лучшим способом может быть использование класса PeriodDuration из проекта ThreeTen Extra, см. ссылку ниже. Я не собираюсь устанавливать эту библиотеку на свой компьютер прямо сейчас, поэтому просто покажу хорошее и современное решение с использованием встроенных классов:

    DateTimeFormatter dtf = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss.S");
    LocalDateTime currentDateTime = LocalDateTime.now(ZoneId.of("Australia/Sydney"));
    String earliestRunTime = "2017-12-16 01:30:08.0";
    LocalDateTime earliestDateTime = LocalDateTime.parse(earliestRunTime, dtf);

    // We want to find a period (years, months, days) and a duration (hours, minutes, seconds).
    // To do that we cut at the greatest possible whole number of days
    // and then measure the period before the cut and the duration after it.
    LocalDateTime cut = earliestDateTime.plusDays(
            ChronoUnit.DAYS.between(earliestDateTime, currentDateTime));

    Period p = Period.between(earliestDateTime.toLocalDate(), cut.toLocalDate());
    Duration d = Duration.between(cut, currentDateTime);

    String result = String.format("%s years, %s months, %s days, %s hours, %s minutes, %s seconds",
            p.getYears(), p.getMonths(), p.getDays(),
            d.toHours(), d.toMinutesPart(), d.toSecondsPart());

    System.out.println(result);

Когда я только что запустил код, я получил:

1 год, 4 месяца, 13 дней, 19 часов, 26 минут, 7 секунд

В java.time, современном API даты и времени Java, Period — это количество лет, месяцев и дней, а Duration — это количество часов, минут, секунд и долей секунды (вплоть до наносекунд). Поскольку вы хотели оба, я использую оба класса.

Методы toXxxPart Duration, которые я использую, были введены в Java 9. Если вы используете Java 8 (или ThreeTen Backport), печать минут и секунд немного сложнее. Найдите java format duration или аналогичный, чтобы узнать, как это сделать.

Я все еще не учитываю летнее время. Для этого нам нужно знать часовой пояс самой ранней строки времени выполнения, а затем использовать ZonedDateTime вместо LocalDateTime. В противном случае код был бы очень похож.

Ссылки

person Ole V.V.    schedule 29.04.2019