Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java Calendar.add gives wrong year

Tags:

java

calendar

My program takes the current date and then, in a loop, adds a week to that date and prints out the new date. Something like:

Calendar cal = Calendar.getInstance();
for (int i=0; i < 52; i++) {
cal.add(Calendar.DATE, 7);
// print date out
}

The add method works the way I expect it to until it reaches Dec 30, at which point the year jumps from 2012 to 2013.

so, using today's date of 4/16/2012, i tested a few different inputs:

this - cal.add(Calendar.DATE, 38*7);
yields- "date:1/7/2013"
this - cal.add(Calendar.DATE, 37*7);
yields- "date:12/31/2013"
this - cal.add(Calendar.DATE, 37*7-1);
yields- "date:12/30/2013"
this - cal.add(Calendar.DATE, 37*7-2);
yields- "date:12/29/2012"

so i notice that the year is correct up until dec 30 and dec 31, and then it corrects itself again when it gets back to january. is there a reason why it does this? does it have anything to do with 2012 being a leap year or am i misunderstanding the add method

like image 694
user1337183 Avatar asked Apr 16 '12 20:04

user1337183


2 Answers

Did you use SimpleDateFormat to print the date and use YYYY to produce the year? If so, that is where the problem lies. Because YYYY produces the week-year and not the calendar year. And as 30/12/2012 is in calendar week 1 of 2013, YYYY produces 2013. To get the calendar year, use yyyy in your SimpleDateFormat format string.

See https://bugs.openjdk.java.net/browse/JDK-8194625

like image 64
revilovs Avatar answered Oct 17 '22 01:10

revilovs


tl;dr

Use modern java.time classes, never the terrible legacy classes such as Calendar.

LocalDate                      // Represent a date-only value with `LocalDate`, without time-of-day and without time zone.
.now(                          // Capture the current date.
    ZoneId.systemDefault()     // Specify your desired/expected time zone explicitly.
)                              // Returns a `LocalDate` object.
.plusWeeks( 1 )                // Add a week, producing a new `LocalDate` object with values based on the original, per the immutable objects pattern.
.toString()                    // Generate text representing this date value in standard ISO 8601 format of YYYY-MM-DD.

2019-01-23

java.time

The modern approach uses the java.time classes.

The Calendar and GregorianCalendar classes are terrible, badly designed with flaws. Avoid them. Now replaced specifically by the ZonedDateTime class.

LocalDate

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

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.

If no time zone is specified, the JVM implicitly applies its current default time zone. That default may change at any moment during runtime(!), so your results may vary. Better to specify your desired/expected time zone explicitly as an argument.

Specify a proper time zone name in the format of Continent/Region, such as America/Montreal, Africa/Casablanca, or Pacific/Auckland. Never use the 2-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 ) ;

To generate text representing that date value in standard ISO 8601 format, simply call toString.

String output = today.toString() ;

Date math is easy, with various plus… & minus… methods.

LocalDate weekLater = today.plusWeeks( 1 ) ;

You can also define a span of time as a Period or Duration. Then add that.

Period p = Period.ofWeeks( 1 ) ;
LocalDate weekLater = today.plus( p ) ;

Your example

Let's test out your example dates.

LocalDate ld = LocalDate.of( 2012 , Month.APRIL , 16 ) ;

Period period38Weeks = Period.ofWeeks( 38 ) ;
Period period37Weeks = Period.ofWeeks( 37 ) ;
Period period37WeeksLess1Days = period37Weeks.minusDays( 1 ) ;
Period period37WeeksLess2Days = period37Weeks.minusDays( 2 ) ;

LocalDate later_38 = ld.plus( period38Weeks ) ;
LocalDate later_37 = ld.plus( period37Weeks ) ;
LocalDate later_37_1 = ld.plus( period37WeeksLess1Days ) ;
LocalDate later_37_2 = ld.plus( period37WeeksLess2Days ) ;

Run code live at IdeOne.com. No problems. The 38th week is in 2013, while week 37 dates are in 2012.

later_38.toString(): 2013-01-07

later_37.toString(): 2012-12-31

later_37_1.toString(): 2012-12-30

later_37_2.toString(): 2012-12-29


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?

  • Java SE 8, Java SE 9, Java SE 10, Java SE 11, and later - Part of the standard Java API with a bundled implementation.
    • Java 9 adds some minor features and fixes.
  • Java SE 6 and Java SE 7
    • Most of the java.time functionality is back-ported to Java 6 & 7 in ThreeTen-Backport.
  • Android
    • Later versions of Android bundle implementations of the java.time classes.
    • For earlier Android (<26), the ThreeTenABP project adapts ThreeTen-Backport (mentioned above). See How to use ThreeTenABP….

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.

like image 28
Basil Bourque Avatar answered Oct 17 '22 02:10

Basil Bourque