Getting the first and last time in milliseconds with Java 8 Time API
Asked Answered
A

3

5

I am converting my time calculations from self implemented code to Java 8 Time API.

I need to have the start and end time in milliseconds from a java.time.Year or java.time.Month class, which I plan to use later in another layer for JFreeChart.

I need functions like getFirstMillisecond() & getLastMilliSecond() from org.jfree.data.time.RegularTimePeriod class of JFreeChart.

I have already implemented code something like-

public static long getStartTimeInMillis(java.time.Year year, java.time.Month month) {       
    if (year != null && month != null) {
        return LocalDate.of(year.getValue(), month, 1).with(TemporalAdjusters.firstDayOfMonth()).
                atStartOfDay().atZone(TimeZone.getDefault().toZoneId()).toInstant().toEpochMilli();
    } else if (year != null) {
        return LocalDate.of(year.getValue(), java.time.Month.JANUARY, 1).with(TemporalAdjusters.firstDayOfMonth()).
                atStartOfDay().atZone(TimeZone.getDefault().toZoneId()).toInstant().toEpochMilli();
    }       
    return 0;
}

public static long getEndTimeInMillis(java.time.Year year, java.time.Month month) {
    if (year != null && month != null) {
        return LocalDate.of(year.getValue(), month, 1).with(TemporalAdjusters.lastDayOfMonth()).
                atTime(OffsetTime.MAX).toLocalDateTime().atZone(TimeZone.getDefault().toZoneId()).toInstant().toEpochMilli();
    } else if (year != null) {
        return  LocalDate.of(year.getValue(), java.time.Month.DECEMBER, 1).with(TemporalAdjusters.lastDayOfMonth()).
                atTime(OffsetTime.MAX).toLocalDateTime().atZone(TimeZone.getDefault().toZoneId()).toInstant().toEpochMilli();
    }       
    return 0;
}

But it looks really complicated to me. Is there any better/shorter way to get these values?

Antedate answered 23/12, 2015 at 9:45 Comment(5)
Why can you not use the JFreeChart-subclass Month instead?Airing
@MenoHochschild As I mentioned, the creation of the charts occurs in another layer, I don't want to import the packages and use JFreeChart classes only for the time API. The part which contains JFreeChart is a plugin in a large framework. So I wanted to create utility methods which every developer can use even if they don't need to create charts but only want to make time calculations.Antedate
Okay, I understand that JFreeChart requires milliseconds per design so your conversion utility can be used for that specific purpose (in chart layer), but why should users have a need for a conversion to milliseconds for other parts of your app? The smallest suitable unit here is MONTHS, not millisecs.Airing
@MenoHochschild We have a lot of legacy code which depends on different time classes like java.util.Calendar, java.util.Date, java.sql.Date and java.sql.Timestamp. Yeah, it is a mess, but I have no desire to refactor everything. I thought millisecs could be a good basis if any other developer needs some conversion.Antedate
It is worth to consider if your app design in the non-freechart-layer can instead require a combination of integer amount and unit enum/identifier. But okay, the costs of refactoring are to be taken into consideration, too.Airing
P
5

YearMonth

Yes, there is a slightly better way. Use the YearMonth class included with java.time.

Also, break up that call-chain into separate statements to make it more readable and easier to trace/debug. Trust the JVM to optimize on your behalf; only use call-chaining where it makes your code more readable and easier to understand.

Going through TimeZone to get the JVM’s current default time zone is unnecessary. Instead, call ZoneId.systemDefault().

Set up some input values.

// Inputs
Year year = Year.of ( 2015 );
Month month = Month.DECEMBER;

The core part of your method.

// Code for your method.
YearMonth yearMonth = year.atMonth ( month ); // Instantiate a YearMonth from a Year and a Month.
LocalDate localDate = yearMonth.atDay ( 1 ); // First day of month.
ZoneId zoneId = ZoneId.systemDefault (); // Or… ZoneId.of("America/Montreal");
ZonedDateTime zdt = localDate.atStartOfDay ( zoneId );
long millis = zdt.toInstant ().toEpochMilli ();

Dump to console.

System.out.println ( "year: " + year + " | month: " + month + " | yearMonth: " + yearMonth + " | zoneId:" + zoneId + " | zdt: " + zdt + " | millis: " + millis );

year: 2015 | month: DECEMBER | yearMonth: 2015-12 | zoneId:America/Los_Angeles | zdt: 2015-12-01T00:00-08:00[America/Los_Angeles] | millis: 1448956800000

Even better, pass the YearMonth instance to your method rather than the pair of Year and Month objects. If your other business logic is using the Year + Month pair, use YearMonth instead – that’s what it’s for.

Poodle answered 23/12, 2015 at 10:45 Comment(1)
That was really helpful. Thank you for your answer.Antedate
J
4

Aside from the questionable practices of returning 0 when given a null year and trusting system default timezone, you can rewrite your methods as follows:

public static long getStartTimeInMillis(java.time.Year year, java.time.Month month) {
    if (year == null) {
        return 0;
    }

    if (month == null) {
        month = Month.JANUARY;
    }

    return year.atMonth(month)
            .atDay(1)
            .atStartOfDay()
            .atZone(ZoneId.systemDefault())
            .toInstant()
            .toEpochMilli();

}

public static long getEndTimeInMillis(java.time.Year year, java.time.Month month) {
    if (year == null) {
        return 0;
    }

    if (month == null) {
        month = Month.JANUARY;
    }

    return year.atMonth(month)
            .atEndOfMonth()
            .atTime(LocalTime.MAX)
            .atZone(ZoneId.systemDefault())
            .toInstant()
            .toEpochMilli();
};

To expand, it is likely better to just throw a NullPointerException if year is null. If you are passed a null year, there's probably a bug in upstream code. Returning a meaningless 0 only pushes the bug further down the road and makes it harder to track down. This principle is called "fail fast".

Relying on system default timezone is a bad idea for serious production code because it tends to cause problems as servers might be configured unpredictably (e.g. GMT) or you can have issues when geographically distributed servers are in different timezones. It prevents headaches to give a careful consideration to the question of "what timezone am I computing all these times against?"

Johore answered 23/12, 2015 at 10:47 Comment(3)
OK I will change my timezone to 'ZoneId.systemDefault()' . But what would be a better practice for return value in your opinion? -62135773200000 for the default timestamp in milliseconds??Antedate
I added a clarification to the answer. See if that makes sense.Johore
I will take this into consideration. Thank you.Antedate
A
0

I would refactor like this

public static long getStartTimeInMillis(java.time.Year year, Optional<Month> monthOpt) {
    Month month = monthOpt.orElse(Month.JANUARY);

    if (year != null) {
        return LocalDate.of(year.getValue(), month, 1)
                .with(TemporalAdjusters.firstDayOfMonth())
                .atStartOfDay()
                .atZone(TimeZone.getDefault().toZoneId())
                .toInstant()
                .toEpochMilli();
    }

    return 0;
}
Adown answered 23/12, 2015 at 10:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.