1

I tried this code:

public class TimePassed {
    private long seconds;
    private long minutes;
    private long hours;
    private long days;
    private long years;
    ...

    public TimePassed(double unixSeconds)  {
        Instant now = Instant.now();
        Instant ago = Instant.ofEpochSecond((long) unixSeconds);

        this.seconds = ChronoUnit.SECONDS.between(
            ago.atZone(ZoneId.systemDefault()),
            now.atZone(ZoneId.systemDefault()));  //6100
        this.minutes = ChronoUnit.MINUTES.between(
            ago.atZone(ZoneId.systemDefault()),
            now.atZone(ZoneId.systemDefault()));  //101
        this.hours = ChronoUnit.HOURS.between(
            ago.atZone(ZoneId.systemDefault()),
            now.atZone(ZoneId.systemDefault()));  //1
        this.days = ChronoUnit.DAYS.between(
            ago.atZone(ZoneId.systemDefault()),
            now.atZone(ZoneId.systemDefault()));  //0
        this.years = ChronoUnit.YEARS.between(
            ago.atZone(ZoneId.systemDefault()),
            now.atZone(ZoneId.systemDefault()));  //0
    }
}

However then the TimePassed object would have seconds = 6100 and minutes = 101 and hours = 1, while I want it to be hours = 1, minutes = 41, seconds = 40, so that 60*60 + 41*60 + 40 = 6100. Is it possible to do with java.time package? Because as of now I can only either get passed seconds, passed minutes or passed hours, etc. And neither would account for the other.

Tom
  • 16,842
  • 17
  • 45
  • 54
parsecer
  • 4,758
  • 13
  • 71
  • 140

2 Answers2

5

Java 9 answer: Duration.toXxxPart methods

Basic idea, not complete:

    Duration dur = Duration.between(ago, now);

    this.seconds = dur.toSecondsPart(); // 40
    this.minutes = dur.toMinutesPart(); // 41
    this.hours = dur.toHoursPart(); // 1
    this.days = dur.toDaysPart(); // 0

Tested with instants that are 6100 seoncds apart like yours from the question. The toXxxPart methods were introduced in Java 9. For Java 8 (or ThreeTen Backport) you will need to start from the coarser units, the days, and subtract them from the duration before getting the next finer unit. See this answer by lauhub for an example

The years and days are a bit tricky to get completely correct, though. To get only the days that exceed the whole years here’s the full code:

    ZoneId zone = ZoneId.systemDefault();
    ZonedDateTime agoZdt = ago.atZone(zone);
    ZonedDateTime nowZdt = now.atZone(zone);
    this.years = ChronoUnit.YEARS.between(agoZdt, nowZdt);
    ZonedDateTime afterWholeYears = agoZdt.plusYears(this.years);

    Duration dur = Duration.between(afterWholeYears, nowZdt);

    this.seconds = dur.toSecondsPart(); // 40
    this.minutes = dur.toMinutesPart(); // 41
    this.hours = dur.toHoursPart(); // 1
    this.days = dur.toDays(); // 0

I am on purpose reading ZoneId.systemDefault() only once just for the unlikely case that someone changes the default time zone setting underway.

Ole V.V.
  • 81,772
  • 15
  • 137
  • 161
  • It doesn't work the way I need to. For one example, it outputs `seconds = 118` and `minutes = 1`, while I'd need it to do `minutes = 1; seconds = 58`. – parsecer Nov 06 '19 at 18:35
  • I don't see `duration.toHoursPart` method, only `duraction.toHours` and no `duration.toSeconds` at all. EDIT: oh java 9 – parsecer Nov 06 '19 at 18:49
  • I edited with a short explanation for Java 8. @parsecer – Ole V.V. Nov 06 '19 at 18:55
2

Something like this should work:

ZoneId zone = ZoneId.systemDefault();

ZonedDateTime ago = ZonedDateTime.ofInstant(Instant.ofEpochSeconds(unixSeconds), zone);
ZonedDateTime now = ZonedDateTime.now(zone);

Period period = Period.between(ago.toLocalDate(), now.toLocalDate());

ZonedDateTime adjusted = ago.with(now.toLocalDate());
if (adjusted.isAfter(now)) {
    adjusted = adjusted.minusDays(1);
    period = period.minusDays(1);
}    

Duration duration = Duration.between(adjusted, now);
assert duration.toDaysPart() == 0;

years   = period.getYears();
months  = period.getMonths();
days    = period.getDays();
hours   = duration.toHoursPart();
minutes = duration.toMinutesPart();
seconds = duration.toSecondsPart();

Why/how this works:

  1. The difference in non-time fields (year, month, day) is computed by using a specialized Period type, which is intended exactly for this purpose. It uses LocalDate part of two date-times, which is safe, since they are both in the same time zone
  2. To know the difference in rest of the fields, we adjust the "ago" value, so that the days are exactly the same. If we happened to overshoot (which might happen if "ago" happened at earlier local time compared to "now"), we adjust to that by decreasing both the adjusted date-time and also the period by one day
  3. Then, we use Duration class to get the difference between time-based fields. Since there we query the difference between adjusted date-times, our duration will not be more than one day, to which I added the assertion for.
  4. Lastly, I used various methods available on both Period and Duration to obtain their "fields". Note that methods of Duration class that I used only available since Java 9, so if you don't have it yet, you'll have to use methods like toMinutes() and manually divide them by amount of, for example, minutes per hour and such:
    // those constants you'll have to define on your own, shouldn't be hard
    hours = duration.toHours() % Constants.HOURS_PER_DAY;

Alternatively, if you don't want to define constants, you can repeat the trick with adjusting the "ago" variable:

adjusted = ago.with(now.toLocalDate());
if (adjusted.isAfter(now)) {
    adjusted = adjusted.minusDays(1);
    period = period.minusDays(1);
}
hours = HOURS.between(adjusted, now);
adjusted = adjusted.withHour(now.getHour());
if (adjusted.isAfter(now)) {
    adjusted = adjusted.minusHour(1);
    hours -= 1;
}

minutes = MINUTES.between(adjusted, now);
adjusted = adjusted.withMinute(now.getMinute());
if (adjusted.isAfter(now)) {
    adjusted = adjusted.minusMinutes(1);
    minutes -= 1;
}
seconds = SECONDS.between(adjusted, now);
M. Prokhorov
  • 3,894
  • 25
  • 39
  • I don't see `duration.toHoursPart` method, only `duraction.toHours` and no `duration.toSeconds` at all – parsecer Nov 06 '19 at 18:49
  • @parsecer, as I mentioned, these were added to the class [since Java 9](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/Duration.html#toHoursPart()), and if you're still on Java 8, you'll have to write glue code. – M. Prokhorov Nov 06 '19 at 18:53
  • Nice and well-explained solution. As a detail, I am not necessarily convinced that your `assert` holds in all corner cases. When summer time (DST) ends, a day can be 25 hours. So I think that your `Duration` may end up for example 24 hours 30 minutes, which `Duration` will interpret as 1 day 30 minutes. – Ole V.V. Nov 06 '19 at 20:09
  • @OleV.V., that might be true, I thought about how this thing might behave when close to transitions, but haven't tested it. If what you say is true, then the assert is wrong, but then I don't know which answer to give: does being in the transition means we should report that two hours has passed when by most accounts it's only been one? Edit: As far as I remember, durations in those cases say "one hour passed", meaning Steven thought about us mere mortals, and the assert might not be wrong after all. – M. Prokhorov Nov 07 '19 at 05:40
  • Upd: when I said "Steven", I meant "Stephen", My bad. – M. Prokhorov Nov 07 '19 at 09:31