Unable to obtain LocalDate from TemporalAccessor for week based string
Asked Answered
M

5

5

I'm trying to parse a simple string in the format "YYYYww" (e.g. 201901) into a LocalDate, but none of my attempts succeed.

I attempted to parse it by simply using the pattern "YYYYww" and also through manually appending the values to the FormatterBuilder. Since my input string does not contain a day, I also configured the formatter to default to Sunday.

Here's the code that fails for me, running Java 8 (IBM JRE 8.0.5.25).

public static void main(String[] args) {
    formatter1(); // Unable to obtain LocalDate from TemporalAccessor
    formatter2(); // Text '201901' could not be parsed at index 0
    formatter3(); // Text '201901' could not be parsed at index 6
}

public static void formatter1() {
    DateTimeFormatter formatter = new DateTimeFormatterBuilder()
            .appendValue(WeekFields.ISO.weekBasedYear(), 4, 4, SignStyle.NEVER)
            .appendValue(WeekFields.ISO.weekOfYear(), 2, 2, SignStyle.NEVER)
            .parseDefaulting(WeekFields.ISO.dayOfWeek(), DayOfWeek.SUNDAY.getValue())
            .toFormatter();

    LocalDate.parse("201901", formatter);
}

public static void formatter2() {
    DateTimeFormatter formatter = new DateTimeFormatterBuilder()
            .appendPattern("YYYYww")
            .parseDefaulting(WeekFields.ISO.dayOfWeek(), DayOfWeek.SUNDAY.getValue())
            .toFormatter();

    LocalDate.parse("201901", formatter);
}

public static void formatter3() {
    DateTimeFormatter formatter = new DateTimeFormatterBuilder()
            .parseLenient()
            .appendPattern("YYYYww")
            .parseDefaulting(WeekFields.ISO.dayOfWeek(), DayOfWeek.SUNDAY.getValue())
            .toFormatter();

    LocalDate.parse("201901", formatter);
}

As seen in the example code I get different error messages, with especially the first example confusing me, since the TemporalAccessor contains the week-based-year, the week of the year and the week-day, which should be enough to construct a LocalDate.

Exception in thread "main" java.time.format.DateTimeParseException: Text '201901' could not be parsed: Unable to obtain LocalDate from TemporalAccessor: {WeekOfYear[WeekFields[MONDAY,4]]=1, WeekBasedYear[WeekFields[MONDAY,4]]=2019, DayOfWeek=7},ISO of type java.time.format.Parsed
    at java.time.format.DateTimeFormatter.createError(DateTimeFormatter.java:1931)
    at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1866)
    at java.time.LocalDate.parse(LocalDate.java:411)
    at Main.formatter1(Main.java:22)
    at Main.main(Main.java:10)
Caused by: java.time.DateTimeException: Unable to obtain LocalDate from TemporalAccessor: {WeekOfYear[WeekFields[MONDAY,4]]=1, WeekBasedYear[WeekFields[MONDAY,4]]=2019, DayOfWeek=7},ISO of type java.time.format.Parsed
    at java.time.LocalDate.from(LocalDate.java:379)
    at java.time.LocalDate$$Lambda$7.000000001061ED20.queryFrom(Unknown Source)
    at java.time.format.Parsed.query(Parsed.java:237)
    at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1862)
    ... 3 more
Memling answered 13/8, 2019 at 13:42 Comment(5)
A small but useful trick for debugging such things btw. is calling TemporalAccessor a = formatter.parse("..."); that way you get the in between step, and it's a bit easier to walk through why things happens as they do :)Handbag
I cannot reproduce. Both your methods formatter2 and formatter3 work flawlessly on my Java 11. To get formatter1 to work, just replace WeekFields.ISO.weekOfYear() with WeekFields.ISO.weekOfWeekBasedYear()Domesticity
@OleV.V. Sorry, I'm running on Java 8 (IBM)Memling
I have reproduced on (Oracle) Java 1.8.0_101. formatter2() throws java.time.format.DateTimeParseException: Text '201901' could not be parsed at index 0. formatter3() instead throws java.time.format.DateTimeParseException: Text '201901' could not be parsed at index 6. I find it very interesting and cannot explain immediately. (Adding your Java version in the question will allow me to retract my downvote.)Domesticity
Very interesting, so it seems like it has been fixed in newer versions of Java. Thanks for confirming, I have added my version in the question.Memling
D
3

This works on Java 8 and later:

    DateTimeFormatter formatter = new DateTimeFormatterBuilder()
            .appendValue(WeekFields.ISO.weekBasedYear(), 4)
            .appendValue(WeekFields.ISO.weekOfWeekBasedYear(), 2)
            .parseDefaulting(WeekFields.ISO.dayOfWeek(), DayOfWeek.SUNDAY.getValue())
            .toFormatter();

    System.out.println(LocalDate.parse("201901", formatter));

Output is:

2019-01-06

I have tested on Oracle Java 1.8.0_101. I haven’t got any IBM Java installed.

The key to making it work is using weekOfWeekBasedYear() instead of weekOfYear(). My other changes are cosmetic. WeekFields.ISO.weekOfYear() defines week numbers by counting the Mondays in the year, which is not what you wanted since week 1 according to ISO may start on a Monday near the end of the previous year (December 29 through 31). Therefore it also doesn’t work well with week-based year, and Java refuses to use the two together to identify a week.

On Java 9 both of your formatter2() and formatter3() work too (tested on Oracle Java 9.0.4). It seems there has been a bug in Java 8 where adjacent field parsing didn’t work for YYYYww. I have searched the Oracle Java Bug Database without finding the bug description, though.

ISO 8601

ISO 8601, the standard which lays down the definition of weeks that you are using, also defines a format for a week. It goes like 2012-W48. So unless you have a good and very specific reason not to, this is the format that you should use, especially in your exchange of data with any external system. It also happens to eliminate the adjacent fields parsing problem on Java 8.

    DateTimeFormatter formatter = new DateTimeFormatterBuilder()
            .appendPattern("YYYY-'W'ww")
            .parseDefaulting(WeekFields.ISO.dayOfWeek(), DayOfWeek.SUNDAY.getValue())
            .toFormatter();

    System.out.println(LocalDate.parse("2019-W01", formatter));

2019-01-06

Link

Domesticity answered 26/8, 2019 at 12:25 Comment(0)
H
2

As far as I know you cannot do that. There are as far as I know two issues with your code. The first is that a LocalDate does not have enough information, since your code cannot know which weekday it is supposed to parse to.

The second is the use of the slightly strange fields. If you just use the Chrono fields you should be fine. You might also be able to adjust your pattern to contain either a space or a dash, as it seems the parsing doesn't like it being in one number - not sure why though

You can for instance do as follows:

DateTimeFormatter formatter = new DateTimeFormatterBuilder()
        .appendValue(ChronoField.YEAR, 4, 4, SignStyle.NEVER)
        .appendValue(ChronoField.ALIGNED_WEEK_OF_YEAR, 2, 2, SignStyle.NEVER)
        .parseDefaulting(ChronoField.DAY_OF_WEEK, DayOfWeek.SUNDAY.getValue())
        .toFormatter();

LocalDate d = LocalDate.parse("201933", formatter);

However, using week numbers doesn't seem like a good option. If you can go with months as @butiri-dan suggests you are most likely better off.

Edit: Per suggestion of @carlos-heuberger it's now using ChronoField.DAY_OF_WEEK in the parseDefaulting-setep

Handbag answered 13/8, 2019 at 14:12 Comment(5)
This seems to work, however I wonder if the ChronoFields are the correct fields to use. Unfortunately the department needing this application works based on these week codes, so I have to work with them.Memling
Judging by the documentation I'd say yes: docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/… but it's not overly specific, and it does indeed leave one wondering if there is a better option :)Handbag
I would use ChronoField.DAY_OF_WEEK (instead of WeekFields day) and, for week based year/weeks, IsoFields.WEEK_BASED_YEAR and IsoFields.WEEK_OF_WEEK_BASED_YEARSewoll
Good point, will update the answer accordingly, thanks @carlos-heubergerHandbag
Very interesting, wasn't aware of IsoFields before. Using IsoFields.WEEK_BASED_YEAR and IsoFields.WEEK_OF_WEEK_BASED_YEAR instead of the respective ChronoFields results in different handling of the first week however. If you change the defaulting day from Sunday to Monday in my example, using IsoFields results in "2018-12-31" being parsed, while using ChronoField.YEAR and ChronoField.ALIGNED_WEEK_OF_YEAR is parsed to "2019-01-07". Makes me wonder if using ChronoField really is correct in this case.Memling
C
1

You can use YearMonth

public static void main(String[] args) {
    System.out.println(YearMonth.parse("201901", DateTimeFormatter.ofPattern("yyyyMM")));
}

Output

2019-01
Cage answered 13/8, 2019 at 14:8 Comment(2)
A good idea, however, if I understand the question correctly, he's trying to use week numbers, not months :/Handbag
Unfortunately my input is not year and month, but year and week.Memling
H
0

Well first week of 2019 doesn't give you a particular date. You need to add a specific day of the week. I ran this:

System.out.println(LocalDate.parse("2019-01-01", DateTimeFormatter.ofPattern("YYYY-ww-ee")));

and got "2018-12-30" (that's because January 1st of 2019 fell on Tuesday i.e. the third day of the week, so the first day of the first week of 2019 is actually "2018-12-30"). Note that for some reason without dashes "-" it didn't work. To read about the formatter options go here: DateTimeFormatter

Hollenbeck answered 13/8, 2019 at 17:58 Comment(0)
O
0

tl;dr

Use org.threeten.extra.YearWeek class.

YearWeek.parse (
                "201901" ,
                new DateTimeFormatterBuilder ( )
                        .parseCaseInsensitive ( )
                        .appendValue ( IsoFields.WEEK_BASED_YEAR , 4 , 10 , SignStyle.EXCEEDS_PAD )
                        .appendValue ( IsoFields.WEEK_OF_WEEK_BASED_YEAR , 2 )
                        .toFormatter ( )
        )
        .atDay ( DayOfWeek.MONDAY )
        .toString ( )

2018-12-31

YearWeek class

To work with weeks in a year, use a class to represent that concept.

You neglected to define “week". Perhaps you meant the standard ISO 8601 week. Week # 1 contains the first Thursday of the calendar year, with a week-based year composed of exactly 52 or 53 complete 7-day weeks, running from Monday-Sunday. So the first/last weeks may have a few days from the previous/successive calendar years.

You will find a class to represent a standard ISO 8601 week, YearWeek, in the ThreeTen-Extra library, an open-source project, available free of cost.

Define a formatting pattern to match your input, in a DateTimeFormatter object via DateTimeFormatterBuilder.

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

Parse your input, producing a YearWeek object.

String input = "201901";
YearWeek yearWeek = YearWeek.parse ( input , PARSER );

Verify by generating text in standard ISO 8601 format.

String input = "201901";
YearWeek yearWeek = YearWeek.parse ( input , PARSER );

2019-W01

Determine the date for the first day, Monday, in that week. The YearWeek object can return a LocalDate object for that date. Specify which day of the week you desire via the DayOfWeek enum.

LocalDate localDate = yearWeek.atDay ( DayOfWeek.MONDAY );

2018-12-31

Oliviaolivie answered 27/10 at 1:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.