2

I have a form where user can input date and time from keypad and LocalDateTime accepts input when time part of input is 24:00, converting it to next day.

My code

import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

val INPUT_DATETIME_PATTERN = "yyyyMMddHHmm"
val formatter = DateTimeFormatter.ofPattern(INPUT_DATETIME_PATTERN)
    
val datetime = "202308102400"

val localdatetime = LocalDateTime.parse(datetime, formatter)
println(datetime)
println(localdatetime.format(formatter))

Expected result:
LocalDateTime.parse throws an exception.
According to documentation, HH stands for hour-of-day (0-23), so 2400 is invalid. Exception is also thrown for input string 202308102401.

Actual result:
202308110000 is printed. Checked in a scratch in Android Studio and in Kotlin playground (link)

Algimantas
  • 159
  • 9
  • 1
    Could this help somehow? https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html#parsedExcessDays-- – Marco F. Aug 28 '23 at 14:21
  • Gosh, I had the feeling this is one of those issues that can be resolved by reading the docs *really* carefully. Thank you very much! – Algimantas Aug 29 '23 at 06:52

4 Answers4

4

Turns out it is my fault for not reading documentation with enough attention... Thanks Marco F. for pointing me at what I missed.

DateTimeResolver is created with ResolverStyle.SMART by default which performs sensible default for each field, e.g. interpreting 24:00 as "first moment of next day" or allows month-of-day values up to 31, silently converting to actual valid day values.

Correct code to get the expected behavior:

val INPUT_DATETIME_PATTERN = "uuuuMMddHHmm"
val formatter = DateTimeFormatter
    .ofPattern(INPUT_DATETIME_PATTERN)
    .withResolverStyle(ResolverStyle.STRICT)

val datetime = "202308102400"

val localdatetime = LocalDateTime.parse(datetime, formatter)
println(datetime)
println(localdatetime.format(formatter))

Note also that year specifier is changed from yyyy (year of era) to uuuu (year), because strict resolver requires an era to go with YearOfEra (source).

Algimantas
  • 159
  • 9
3

(In Java syntax, not Kotlin)

tl;dr

To reject such inputs, specify a strict resolver style.

DateTimeFormatter
    .ofPattern( "uuuuMMddHHmm" )
    .withResolverStyle( ResolverStyle.STRICT )

LocalDateTime accepts input when time part of input is 24:00, converting it to next day

Yes, it should do that. What you see is a feature, not a bug.

Using 24:00 on a 00-23 clock means “end of today” which is also “beginning of tomorrow”. So logically:

( 202308102400 = 202308110000 ) -> true

This specific behavior is documented.

24:00 does mean the next day

24:00 means the first moment of the next day. In contrast, 00:00 means the first moment of the specified day.

Conceptually, every day has two midnights, one at the beginning of the day, and one at the end of the day. To avoid this ambiguity, notice that the java.time classes avoid both the term and the concept of “midnight”. Instead the java.time classes focus on the first moment of the day.

Some industries differentiate between the two midnights by using 00:00 & 24:00. They use the 24:00 notation as a way of saying “at the end of the day today”.

So we see that 202308102400 is the same as 202308110000. The end of the day today is also the beginning of the day tomorrow.

So the behavior of LocalDateTime is semantically correct, returning to you the date of the next day along with the time-of-day of 00:00.

See the following code run at Ideone.com

    final DateTimeFormatter BASIC_ISO_LOCAL_DATE_TIME = DateTimeFormatter.ofPattern( "uuuuMMddHHmm" ) ;
    String input = "202308102400" ; 
    try {
        LocalDateTime ldt = LocalDateTime.parse ( input , BASIC_ISO_LOCAL_DATE_TIME ) ;
        System.out.println( "ldt.toString() = " + ldt ) ;
    } catch ( DateTimeParseException e ) {
        System.out.println( "Yuck. Your input is distasteful. " ) ;
    }

ldt.toString() = 2023-08-11T00:00

Check input with String#endsWith

If in your case you are certain the time 24:00 should be rejected, you could simply check tho string input for the presence of 2400 at the end.

if ( input.endsWith( "2400" ) { … }
else { LocalDateTime ldt = LocalDateTime.parse( input ) ; }

Resolver.STRICT

As you eventually found, to make the DateTimeFormatter object reject inputs with a time of 24:00, set the ResolverStyle to the enum object STRICT. Strict mode will tolerate only hours of 00-23.

    final DateTimeFormatter STRICT_BASIC_ISO_LOCAL_DATE_TIME = 
        DateTimeFormatter
            .ofPattern( "uuuuMMddHHmm" )
            .withResolverStyle( ResolverStyle.STRICT ) ;  // <— Reject 2400 as time of day. 

Catch DateTimeParseException to handle such unwelcome inputs.

    String input = "202308102400" ; 
    try {
        LocalDateTime ldt = LocalDateTime.parse ( input , STRICT_BASIC_ISO_LOCAL_DATE_TIME ) ;
        System.out.println( "ldt.toString() = " + ldt ) ;
    } catch ( DateTimeParseException e ) {
        System.out.println( "Yuck. Your input is distasteful. " ) ;
    }
Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
  • 1
    Yep, I figured it is likely a feature I don't want in my case and wanted a way to turn it off. It was that DateTimeFormater was created with ResolverStyle.SMART by default, while I needed ResolverStyle.STRICT. – Algimantas Aug 29 '23 at 09:11
  • A well-researched answer! All tye links in the answer are valuable. – Arvind Kumar Avinash Aug 29 '23 at 18:37
1

Change your line
val formatter = DateTimeFormatter.ofPattern(INPUT_DATETIME_PATTERN)
to
val formatter = DateTimeFormatter.ofPattern(INPUT_DATETIME_PATTERN).withResolverStyle(ResolverStyle.STRICT) See this question for more info: Using new Java 8 DateTimeFormatter to do strict date parsing Also, in your format change year part from 'yyyy' to 'uuuu'. It won't work without this.

Michael Gantman
  • 7,315
  • 2
  • 19
  • 36
-2
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

val INPUT_DATETIME_PATTERN = "yyyyMMddHHmm"
val formatter = DateTimeFormatter.ofPattern(INPUT_DATETIME_PATTERN)

val datetime = "202308102400"
val adjustedDatetime = datetime.replaceRange(8..9, "00")

val localdatetime = LocalDateTime.parse(adjustedDatetime, formatter)
println(datetime)
println(localdatetime.format(formatter))

This code adjusts the time part from "2400" to "0000" before parsing the datetime string and produces the expected output