Gopi
Gopi

Reputation: 939

Calendar.getTime IllegalArgumentException

import java.util.Calendar;

public class WeekYear {
    static String input = "202001";
    //static String format = "YYYYMM";

    public static void main(String[] args) throws ParseException {
        Calendar lCal = Calendar.getInstance();
        System.out.println(lCal.isLenient());
        lCal.setLenient(false);
        lCal.set(Calendar.YEAR, new Integer(input.substring(0, 4)).intValue());
        lCal.set(Calendar.WEEK_OF_YEAR, new Integer(input.substring(4, 6)).intValue());
        //lCal.setMinimalDaysInFirstWeek(5);
        System.out.println(lCal.isLenient());
        System.out.println(lCal.getTime());

        //lCal.set(Calendar.YEAR, new Integer(input.substring(0, 4)).intValue());
        //lCal.set(Calendar.WEEK_OF_YEAR, new Integer(input.substring(4, 6)).intValue());
        //System.out.println(lCal.getTime());
    }
}

When this code is executed on Nov 22nd, 2020 I get an IllegalArgumentException from Calendar.getTime(). But when executed on Nov 27, 2020 it works fine.

The documentation says:

The setLenient(boolean leniency) method in Calendar class is used to specify whether the interpretation of the date and time is to be lenient or not. Parameters: The method takes one parameter leniency of the boolean type that refers to the mode of the calendar.

Any explanation? I am not able to reproduce the issue even in my local now. Local time is set to CST

Exception Stack:

Exception in thread "main" java.lang.IllegalArgumentException: year: 2020 -> 2019
at java.util.GregorianCalendar.computeTime(GregorianCalendar.java:2829)
at java.util.Calendar.updateTime(Calendar.java:3393)
at java.util.Calendar.getTimeInMillis(Calendar.java:1782)
at java.util.Calendar.getTime(Calendar.java:1755)
at WildDog.main(WildDog.java:13)
`````````

Upvotes: 0

Views: 1242

Answers (2)

Anonymous
Anonymous

Reputation: 86379

Basil Bourque has already provided a very good answer. Here’s one that doesn’t require an external dependency (provided you are using Java 8 or later).

java.time

    WeekFields wf = WeekFields.of(Locale.US);
    DateTimeFormatter yearWeekFormatter = new DateTimeFormatterBuilder()
            .appendValue(wf.weekBasedYear(), 4)
            .appendValue(wf.weekOfWeekBasedYear(), 2)
            .parseDefaulting(ChronoField.DAY_OF_WEEK, DayOfWeek.SUNDAY.getValue())
            .toFormatter();

    String input = "202001";
    LocalDate date = LocalDate.parse(input, yearWeekFormatter);
    System.out.println(date);

Output is:

2019-12-29

Assuming American weeks where Sunday is the first day of the week and week 1 is the week containing January 1, this is correct: week 1 of 2020 begins on Sunday, December 29, 2019. If you want weeks defined in some other way, just use a different WeekFields object.

I recommend that you don’t use the Calendar class. That class was always poorly designed and is now long outdated. Instead I am using java.time, the modern Java date and time API.

Any explanation?

With thanks to user85421 for how to reproduce. You are first creating a Calendar object (really an instance of GregorianCalendar) representing the current day, in your example Nov 22nd, 2020, a Sunday (apparently having set your computer clock nearly a year ahead). You are then setting its year to 2020 (no change) and its week number to 1. However, as we saw above, this would change the date to December 29, 2019, and thus create a conflict with the year that you set to 2020. Therefore GregorianCalendar decides that you are asking the impossible and throws the exception. The stack trace that I got was:

java.lang.IllegalArgumentException: YEAR: 2020 -> 2019
    at java.base/java.util.GregorianCalendar.computeTime(GregorianCalendar.java:2826)
    at java.base/java.util.Calendar.updateTime(Calendar.java:3395)
    at java.base/java.util.Calendar.getTimeInMillis(Calendar.java:1782)
    at java.base/java.util.Calendar.getTime(Calendar.java:1755)
    at ovv.misc.Test.main(Test.java:17)

In your second example you were running your program on Nov 27, 2020, a Friday. This time the date is changed to Friday, January 3, 2020, so still within year 2020, and therefore there is no conflict and hence no exception.

The explanation presumes that your default locale is one where week 1 of the year is defined as the week that contains January 1. I have ran your code in my own locale after setting my computer’s time to Nov 22, 2020, and my time zone to America/Chicago. No exception was seen (output included Sun Jan 05 13:54:27 CST 2020). My locale follows the international standard, ISO. Monday is the first day of the week, and week 1 is the first week that has at least 4 days of the new year in it. So week 1 of 2020 is from Monday, December 30, 2019, through Sunday, January 5. I suppose that on a Monday or Tuesday I could reproduce your problem in this locale too, I haven’t tried.

PS How to parse an integer

Just a tip, to parse a string into an int, just use Integer.parseInt(yourString). No need to create a new Integer object.

Link

Oracle tutorial: Date Time explaining how to use java.time.

Upvotes: 2

Basil Bourque
Basil Bourque

Reputation: 340070

tl;dr

Never use Calendar, now legacy, supplanted by java.time classes such as ZonedDateTime.

Use a purpose-built class, YearWeek from the ThreeTen-Extra project, to track standard ISO 8601 weeks.

Custom formatter

Define a DateTimeFormatter object to match your non-standard input string.

org.threeten.extra.YearWeek
.parse(
    "202001" ,
    new DateTimeFormatterBuilder()
    .parseCaseInsensitive()
    .appendValue( IsoFields.WEEK_BASED_YEAR, 4, 10, SignStyle.EXCEEDS_PAD)
    .appendValue(IsoFields.WEEK_OF_WEEK_BASED_YEAR, 2)
    .toFormatter()
)
.toString()

2020-W01

Standard formatter

Or manipulate your input string to comply with the ISO 8601 standard format, inserting a -W in the middle between the week-based-year and the week. The java.time classes and the ThreeTen-Extra classes all use the ISO 8601 formats by default when parsing/generating strings.

String input = "202001";
String inputModified = input.substring( 0 , 4 ) + "-W" + input.substring( 4 );
YearWeek yearWeek = YearWeek.parse( inputModified ) ;

yearWeek.toString(): 2020-W01

Avoid legacy date-time classes

Do not waste your time trying to understand Calendar. This terrible class was supplanted years ago by the modern java.time classes defined in JSR 310.

Definition of week

You must specify your definition of a week. Do you mean week number 1 contains the first day of the year? Or week # 1contains a certain day of the week? Or week # 1 is the first calendar week to consist entirely of dates in the new year? Or perhaps an industry-specific definition of week? Some other definition?

One of the confusing things about Calendar is that its definition of a week shifts by Locale. This one of many reasons to avoid that legacy class.

Week-based year

Depending on your definition of week, the year of a week may not be the calendar year of some dates on that week. A week-based year may overlap with calendar years.

Standard weeks and week-based year

For example, the standard ISO 8601 week defines a week as:

  • Starting on Monday, and
  • Week # 1 contains the first Thursday of the calendar year.

So there are 52 or 53 whole weeks in every week-based year. Of course, that means some dates from the previous and/or following calendar years may appear in the first/last weeks of our week-based year.

org.threeten.extra.YearWeek

One problem is that you are trying to represent a year-week with a class that represents a moment, a date with time of day in the context of a time zone.

Instead, use a purpose-built class. You can find one in the ThreeTen-Extra library, YearWeek. This library extends the functionality of the java.time classes built into Java 8 and later.

With that class I would think that we could define a DateTimeFormatter to parse your input using the formatting pattern YYYYww where the YYYY means a 4-digit year of week-based-year, and the ww means the two-digit week number. Like this:

// FAIL
String input = "202001" ; 
DateTimeFormatter f = DateTimeFormatter.ofPattern( "YYYYww" ) ;
YearWeek yearWeek = YearWeek.parse( input , f ) ;

But using that formatter throws an DateTimeParseException for reasons that escape me.

Exception in thread "main" java.time.format.DateTimeParseException: Text '202001' could not be parsed: Unable to obtain YearWeek from TemporalAccessor: {WeekOfWeekBasedYear[WeekFields[SUNDAY,1]]=1, WeekBasedYear[WeekFields[SUNDAY,1]]=2020},ISO of type java.time.format.Parsed

Caused by: java.time.DateTimeException: Unable to obtain YearWeek from TemporalAccessor: {WeekOfWeekBasedYear[WeekFields[SUNDAY,1]]=1, WeekBasedYear[WeekFields[SUNDAY,1]]=2020},ISO of type java.time.format.Parsed

Caused by: java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: WeekBasedYear

Alternatively, we can use DateTimeFormatterBuilder to build up a DateTimeFormatter from parts. By perusing the OpenJDK source code for Java 13 for DateTimeFormatter.ISO_WEEK_DATE I was able to cobble together this formatter that seems to work.

DateTimeFormatter f =  
        new DateTimeFormatterBuilder()
        .parseCaseInsensitive()
        .appendValue( IsoFields.WEEK_BASED_YEAR, 4, 10, SignStyle.EXCEEDS_PAD)
        .appendValue(IsoFields.WEEK_OF_WEEK_BASED_YEAR, 2)
        .toFormatter()
;

Using that:

String input = "202001" ; 
YearWeek yearWeek = YearWeek.parse( input , f ) ;

ISO 8601

Educate the publisher of your data about the ISO 8601 standard defining formats for representing date-time values textually.

To generate a string in standard format representing the value of our YearWeek, call toString.

String output = yearWeek.toString() ;

2020-W01

And parsing a standard string.

YearWeek yearWeek = YearWeek.parse( "2020-W01" ) ;

Upvotes: 2

Related Questions