7

I'm trying to create a date range to cover a full month, i.e.

[startDate; endDate]

As such, I have a reference date and try to create a new date from it. I'm having problem with the "endDate" because I want it to be near the end of the day (i.e. 23:59:59).

The code I'm using is the following:

  public static Date previousMonthLastDate(Date referenceDate) {
    Calendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
    calendar.setTime(referenceDate);
    calendar.add(Calendar.MONTH, -1); // move to the previous month
    int lastDay = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);
    calendar.set(Calendar.DAY_OF_MONTH, lastDay);
    // set the time to be the end of the day
    calendar.set(Calendar.HOUR_OF_DAY, 23);
    calendar.set(Calendar.MINUTE, 59);
    calendar.set(Calendar.SECOND, 59);

    return calendar.getTime();
  }

This code is working as expected on the Android emulator. However, running it on a real phone gives the wrong date. As such, I'm assuming it is some kind of timezone problem.

On the phone, instead of giving say 31/August/2010, it gives 01/September/2010. This value seams to be set after the line of code that sets the HOUR_OF_DAY to 23.

Any ideas?

Thanks.

MyName
  • 2,136
  • 5
  • 26
  • 37
  • 1
    Well I just fixed it... the time (HOUR_OF_DAY) must be set to 22 and not 23, because the field is zero based. Not sure why it worked on the emulator and not on the cell phone though. – MyName Sep 08 '10 at 21:33
  • Even if HOUR_OF_DAY is 0 based, 23 is still a valid hour, only 24 is out of range. – Damien Sep 08 '10 at 21:38
  • Damien: Maybe its because "23:59:59" is on the next day and so it assumes that it's actually the next day? – MyName Sep 08 '10 at 21:48
  • Either way, it should behave the same in the emulator and in the device. I suggest filing a bug report. – Mike Baranczak Sep 08 '10 at 23:31
  • BTW, a range in date-time work is generally better represented using the Half-Open approach where the beginning is inclusive while the ending is *exclusive*. So a month starts at first moment of first day and runs up to but *not including* the first moment of the fist day of the next month. Benefits include not trying to resolve the end to any particular resolution ( milliseconds, microseconds, nanoseconds). – Basil Bourque Nov 02 '15 at 17:04
  • FYI, the troublesome old date-time classes such as `java.util.Date`, `java.util.Calendar`, and `java.text.SimpleDateFormat` are now legacy, supplanted by the [*java.time*](https://docs.oracle.com/javase/10/docs/api/java/time/package-summary.html) classes. Much of the *java.time* functionality is back-ported to Java 6 & Java 7 in the [***ThreeTen-Backport***](http://www.threeten.org/threetenbp/) project. Further adapted for earlier Android in the [***ThreeTenABP***](https://github.com/JakeWharton/ThreeTenABP) project. See [*How to use ThreeTenABP…*](http://stackoverflow.com/q/38922754/642706). – Basil Bourque Apr 20 '18 at 06:18

6 Answers6

6

I working in something like that:

With this code, I set the day in interval:

Date day = new Date()

With this code, I get interval:

Calendar startDate = Calendar.getInstance();
Calendar endDate = Calendar.getInstance();

// Set time
startDate.setTime(day);
endDate.setTime(day);

startDate.set(Calendar.DAY_OF_MONTH, 1);
startDate.set(Calendar.HOUR_OF_DAY, 0);
startDate.set(Calendar.MINUTE, 0);
startDate.set(Calendar.SECOND, 0);
startDate.set(Calendar.MILLISECOND, 0);

endDate.set(Calendar.DAY_OF_MONTH, endDate.getActualMaximum(Calendar.DAY_OF_MONTH));
endDate.set(Calendar.HOUR_OF_DAY, 23);
endDate.set(Calendar.MINUTE, 59);
endDate.set(Calendar.SECOND, 59);
Eduardo Cuomo
  • 17,828
  • 6
  • 117
  • 94
6

I can't answer why it's happening, but have you tried setting it to the first day of the next month and subtracting one second/millisecond?

calendar.set(Calendar.DAY_OF_MONTH, 1);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
calendar.add(Calendar.MILLISECOND, -1);
waxwing
  • 18,547
  • 8
  • 66
  • 82
  • Either that or simply use an inequality (i.e. before the first day of the next month) in any calculations. That way you don't get confused by leap seconds or clock-change days... Of course, if you are only displaying the value, and not using it in any calculations, then the answer here is good. – Bill Michell Sep 09 '10 at 13:49
3

If you want the timezone to depend on the phone settings, you shouldn't force a timezone when creating your calendar. Just use:

Calendar calendar = Calendar.getInstance();
Damien
  • 2,254
  • 1
  • 22
  • 30
2

The Calendar API gives you this functionality out of the box, here is a trick to get it done :

// Get the instance of the Calendar.
Calendar calendar = Calendar.getInstance();
// Set the date of the First day of Month
calendar.set(Calendar.DAY_OF_MONTH, 1);
// Roll it to previous day of Year to get the Last day of Month
calendar.roll(Calendar.DAY_OF_YEAR, -1);

Edit : You also have to set the Hours, Minutes, Seconds and Milliseconds.

mmansoor
  • 580
  • 5
  • 11
0
calendar.set(Calendar.DAY_OF_MONTH, calendar.getActualMaximum(Calendar.DAY_OF_MONTH));
Alecs
  • 2,900
  • 1
  • 22
  • 25
0

tl;dr

ZoneId z = ZoneId.of( "America/Montreal" );
LocalDate today = LocalDate.now( z );
ZonedDateTime start = today.with( TemporalAdjusters.firstDayOfMonth() )
                           .atStartOfDay( z ) ;
ZonedDateTime stop = today.with( TemporalAdjusters.firstDayOfNextMonth() )
                          .atStartOfDay( z ) ;
  • Determining a date requires a time zone.
  • Better to specify explicitly than rely implicitly on JVM’s current default time zone.

Avoid legacy classes

You are using troublesome old date-time classes, now legacy, supplanted by the java.time classes.

Using java.time

This work is much easier with the java.time classes.

LocalDate

The LocalDate class represents a date-only value without time-of-day and without time zone.

Time zone

A time zone is crucial in determining a date. For any given moment, the date varies around the globe by zone. For example, a few minutes after midnight in Paris France is a new day while still “yesterday” in Montréal Québec.

Specify a proper time zone name in the format of continent/region, such as America/Montreal, Africa/Casablanca, or Pacific/Auckland. Never use the 3-4 letter abbreviation such as EST or IST as they are not true time zones, not standardized, and not even unique(!).

ZoneId z = ZoneId.of( "America/Montreal" );
LocalDate today = LocalDate.now( z );

Always specify the time zone explicitly. If omitted, the JVM’s current default time zone is implicitly applied. This default can be changed at any moment by any code of any app within the JVM. So if crucial, ask the user for the desired/expected time zone. If not crucial, you can ask for the default explicitly to make your intentions clear in your code rather than the ambiguity of relying on the implicit default.

ZoneId z = ZoneId.systemDefault();  // Get JVM’s current default time zone.
LocalDate today = LocalDate.now( z );

TemporalAdjuster

The TemporalAdjuster interface provides for classes that can adjust date-time values. The TemporalAdjusters class provides several handy implementations.

LocalDate firstOfThisMonth = today.with( TemporalAdjusters.firstDayOfMonth() );

ZonedDateTime

To turn that date-only into a date-with-time-of-day, apply a time zone ZoneId to get a ZonedDateTime object.

Do not assume the first moment of the day is 00:00:00. Because of anomalies such as Daylight Saving Time (DST), the first moment might be something like the time 01:00:00. Let java.time figure this out by calling atStartOfDay.

ZonedDateTime zdtStartOfMonth = firstOfThisMonth.atStartOfDay( z );

Half-Open

You are taking the wrong tack by trying to determine the last moment of the month. That last moment has an infinitely divisible fractional second. Trying to resolve to a particular granularity is ill-advised as different systems use different granularities. Old Java date-time classes use milliseconds, some databases such as Postgres use microseconds, the java.time classes use nanoseconds, and other systems use still other variations.

The wiser approach commonly used in date-time work for defining spans of time is Half-Open, where the beginning is inclusive while the ending is exclusive. This means a month begins at the first moment of the day of the first of the month and runs up to, but not including, the first moment of the first of the following month.

LocalDate firstOfNextMonth = today.with( TemporalAdjusters.firstOfNextMonth() );

Adjust into a time zone to get a specific moment.

ZonedDateTime zdtStartOfNextMonth = firstOfNextMonth.atStartOfDay( z );

The logic for comparing a moment to this span of time is “Is this moment (a) equal to or after the beginning, and (b) less than the ending?”. Notice the lack of "or is equal" in part 'b'. That means we are running up to, but not including, the ending.

Also, a shorter way of saying part 'a' is “not before the beginning”. So we can ask more simply, “Is this moment not before the beginning AND is before the ending?”.

ZonedDateTime moment = ZonedDateTime.now( z );
Boolean spanContainsMoment = ( ! moment.isBefore( zdtStartOfMonth ) ) && ( moment.isBefore( zdtStartOfNextMonth ) ) ;

By the way, the standard ISO 8601 format for formatting a textual representation of a span of time uses a slash character to join the beginning and ending.

String output = zdtStartOfMonth.toString() + "/" + zdtStartOfNextMonth.toString() ;

Interval

You can represent this span of time using the Interval class in the ThreeTen-Extra library. That class tracks the beginning and ending in UTC as Instant objects. You can extract an Instant from a ZonedDateTime object.

Interval interval = Interval.of( zdtStartOfMonth.toInstant() , zdtStartOfNextMonth.toInstant() );

YearMonth

By the way, you may find the YearMonth class useful in your work.


About java.time

The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.

You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.* classes.

Where to obtain the java.time classes?

The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154