DateTimeFormatter weekday seems off by one
Asked Answered
G

2

14

I'm porting an existing application from Joda-Time to Java 8 java.time.

I ran into a problem where parsing a date/time string that contains a 'day of week' value triggered an exception in my unit tests.

When parsing:

2016-12-21 20:50:25 Wednesday December +0000 3

using format:

yyyy'-'MM'-'dd' 'HH':'mm':'ss' 'EEEE' 'MMMM' 'ZZ' 'e

I get:

java.time.format.DateTimeParseException: 
Text '2016-12-21 20:50:25 Wednesday December +0000 3' 
could not be parsed: Conflict found: 
Field DayOfWeek 3 differs from DayOfWeek 2 derived from 2016-12-21

When letting the DateTimeFormatter indicate what it expects:

String logline     = "2016-12-21 20:50:25 Wednesday December +0000";
String format      = "yyyy'-'MM'-'dd' 'HH':'mm':'ss' 'EEEE' 'MMMM' 'ZZ";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format).withLocale(Locale.ENGLISH);;
ZonedDateTime dateTime = formatter.parse(logline, ZonedDateTime::from);

format      = "yyyy'-'MM'-'dd' 'HH':'mm':'ss' 'EEEE' 'MMMM' 'ZZ' 'e";
formatter = DateTimeFormatter.ofPattern(format).withLocale(Locale.ENGLISH);
System.out.println(formatter.format(dateTime));

I now get this output:

2016-12-21 20:50:25 Wednesday December +0000 4

So effectively the root cause of the problem is that the e flag in Joda-Time considers Monday to be 1 yet the Java 8 java.time considers Monday to be 0.

Now for the patterns that java.time.DateTimeFormatter supports I find in both the Oracle documentation and in JSR-310 this:

e/c     localized day-of-week       number/text       2; 02; Tue; Tuesday; T

This explicit example of 2 and 'Tuesday' leads me to believe that Wednesday should also in java.time be 3 instead of 4.

What is wrong here? Do I misunderstand? Is this a bug in Java 8?

Gmt answered 21/9, 2017 at 10:8 Comment(2)
What timezone/locale are you using, as the doc says it's localized day of the week, some locales start the week of Sunday as opposed to MondayDotty
To clarify I added the .withLocale(Locale.ENGLISH); in the code.Gmt
F
13

There's a difference on how Joda-Time and java.time interprets the pattern e.


In Joda-Time, the e pattern designates the numeric value of day-of-week:

Symbol  Meaning        Presentation  Examples
------  -----------    ------------  -------
e       day of week    number        2

So, using e is equivalent to getting the day of the week from a date object:

// using org.joda.time.DateTime and org.joda.time.format.DateTimeFormat
DateTime d = new DateTime(2016, 12, 21, 20, 50, 25, 0, DateTimeZone.UTC);
DateTimeFormatter fmt = DateTimeFormat.forPattern("e").withLocale(Locale.ENGLISH);
System.out.println(d.toString(fmt)); // 3
System.out.println(d.getDayOfWeek()); // 3
System.out.println(d.dayOfWeek().getAsText(Locale.ENGLISH)); // Wednesday

Note that both the formatter and getDayOfWeek() return 3. The getDayOfWeek() method returns a value defined in DateTimeConstants class, and Wednesday's value is 3 (the third day of the week according to ISO's definition).


In java.time API, the pattern e has a different meaning:

Pattern  Count  Equivalent builder methods
-------  -----  --------------------------
e        1      append special localized WeekFields element for numeric day-of-week

It uses the localized WeekFields element, and this can vary according to the locale. The behaviour might be different when compared to the getDayOfWeek() method:

ZonedDateTime z = ZonedDateTime.of(2016, 12, 21, 20, 50, 25, 0, ZoneOffset.UTC);
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("e", Locale.ENGLISH);
System.out.println(z.format(fmt)); // 4
System.out.println(z.getDayOfWeek()); // WEDNESDAY
System.out.println(z.getDayOfWeek().getValue()); // 3

Note that the formatter uses the localized day of week for English locale, and the value is 4, while calling getDayOfWeek().getValue() returns 3.

That's because e with English locale is equivalent to using a java.time.temporal.WeekFields:

// using localized fields
WeekFields wf = WeekFields.of(Locale.ENGLISH);
System.out.println(z.get(wf.dayOfWeek())); // 4

While getDayOfWeek() is equivalent to using ISO's definition:

// same as getDayOfWeek()
System.out.println(z.get(WeekFields.ISO.dayOfWeek())); // 3

That's because ISO's definition uses Monday as the first day of the week, while WeekFields with English locale uses Sunday:

// comparing the first day of week
System.out.println(WeekFields.ISO.getFirstDayOfWeek()); // MONDAY
System.out.println(wf.getFirstDayOfWeek()); // SUNDAY

So the e pattern might behave differently or not to getDayOfWeek(), according to the locale set in the formatter (or the JVM default locale, if none is set). In French locale, for example, it behaves just like ISO, while in some arabic locales, the first day of the week is Saturday:

WeekFields.of(Locale.FRENCH).getFirstDayOfWeek(); // MONDAY
WeekFields.of(new Locale("ar", "AE")).getFirstDayOfWeek(); // SATURDAY

According to javadoc, the only patterns that return a numeric value for the day of week seem to be the localized ones. So, to parse the input 2016-12-21 20:50:25 Wednesday December +0000 3, you can use a java.time.format.DateTimeFormatterBuilder and join the date/time pattern with a java.time.temporal.ChronoField to indicate the numeric value of the day of week (the ISO non-locale sensitive field):

String input = "2016-12-21 20:50:25 Wednesday December +0000 3";
DateTimeFormatter parser = new DateTimeFormatterBuilder()
    // date/time pattern
    .appendPattern("yyyy-MM-dd HH:mm:ss EEEE MMMM ZZ ")
    // numeric day of week
    .appendValue(ChronoField.DAY_OF_WEEK)
    // create formatter with English locale
    .toFormatter(Locale.ENGLISH);

ZonedDateTime date = ZonedDateTime.parse(input, parser);

Also note that you don't need to quote the -, : and space characters, so the pattern becomes more clear and readable (IMO).

I also set the English locale, because if you don't set, it'll use the JVM default locale, and it's not guaranteed to always be English. And it can also be changed without notice, even at runtime, so it's better to specify one, specially if you already know in what language the input is.


Update: probably the ccccc pattern should work, as it's equivalent to appendText(ChronoField.DAY_OF_WEEK, TextStyle.NARROW_STANDALONE) and in my tests (JDK 1.8.0_144) it returns (and also parses) 3:

DateTimeFormatter parser = DateTimeFormatter
    .ofPattern("yyyy-MM-dd HH:mm:ss EEEE MMMM ZZ ccccc", Locale.ENGLISH);
ZonedDateTime date = ZonedDateTime.parse(input, parser);
Fiacre answered 21/9, 2017 at 12:17 Comment(2)
Thanks. This explanation really helps. I have the quoting of all of those fields like this because the expression is generated from a strftime formatted input string. I have to figure out a way to append these values to make it all work right. See github.com/nielsbasjes/logparser/blob/master/httpdlog/…Gmt
@NielsBasjes You're welcome, glad to help! I've made some tests and maybe the pattern ccccc should be what you need. I've updated the answer.Fiacre
J
1

In Locale.ENGLISH Wednesday is the 4th day of week, as week starts on Sunday. You can check first day of week with

WeekFields.of(Locale.ENGLISH).getFirstDayOfWeek(); //it's SUNDAY
Jacklight answered 21/9, 2017 at 10:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.