javaillegalargumentexceptionweek-numberjava.util.calendarjava-calendar

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)
`````````

Solution

  • 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:

    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" ) ;