gene b.
gene b.

Reputation: 11996

Java Calendar/Date: AM and PM not setting correctly

I have a function that takes a custom string and converts it into a Date. My goal is to store today's date but with the custom Hours:Minutes supplied by the string.

For some reason, the debugger shows that the AM/PM are switched at the end (but the flow is correct). When I pass in 12:05am the Date object is stored as the PM value, whereas if I pass in 12:05pm the Date object is stored as the AM value. It should be the opposite.

Code:

public class DateUtils {

    private static final String AM_LOWERCASE = "am";
    private static final String AM_UPPERCASE = "AM";

    public static Date getDateFromTimeString(String timeStr) {

        Calendar calendar = Calendar.getInstance();

        if (StringUtils.hasText(timeStr)) {

            if (timeStr.indexOf(AM_LOWERCASE) != -1 || timeStr.indexOf(AM_UPPERCASE) != -1) {
                calendar.set(Calendar.AM_PM, Calendar.AM);
            } else {
                calendar.set(Calendar.AM_PM, Calendar.PM);
            }

            // Set custom Hours:Minutes on today's date, based on timeStr
            String[] timeStrParts = timeStr.replaceAll("[a-zA-Z]", "").split(":");
            calendar.set(Calendar.HOUR, Integer.valueOf(timeStrParts[0]));
            calendar.set(Calendar.MINUTE, Integer.valueOf(timeStrParts[1]));
            calendar.set(Calendar.SECOND, 0);
            calendar.set(Calendar.MILLISECOND, 0);
        }

        return calendar.getTime();

    }
}

The debugger shows that:

Input: 12:05am -> Sun Dec 17 12:05:00 EST 2017

Input: 12:05pm -> Mon Dec 18 00:05:00 EST 2017

It should be the opposite of that. If I were to write out these strings back using SimpleDateFormat I would see that Input 1 comes back as 12:05PM and Input 2 comes back as 12:05AM.

Also, for #2 the date shouldn't cycle forward a day. The Dates that should be stored are today's date in both cases, with either 12:05 AM or 12:05 PM.

Am I missing something? Goal:

12:05am   ->    Sun Dec 17 00:05:00 EST 2017
12:05pm   ->    Sun Dec 17 12:05:00 EST 2017

Upvotes: 2

Views: 1666

Answers (3)

Arvind Kumar Avinash
Arvind Kumar Avinash

Reputation: 79175

java.time

The java.util Date-Time API and their formatting API, SimpleDateFormat are outdated and error-prone. It is recommended to stop using them completely and switch to the modern Date-Time API*.

Avoid performing String operations

Performing String operations (e.g. regex match, to-upper-case, to-lower-case etc.) can be error-prone and incomplete. Instead use DateTimeFormatterBuilder which allows a rich set of options (e.g. case-insensitive parsing, specifying optional patterns, parsing with default values etc.).

Demo:

import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.util.Locale;
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        DateTimeFormatter dtf = new DateTimeFormatterBuilder()
                .parseCaseInsensitive()   //To parse in case-insensitive way e.g. AM, am
                .appendPattern("h:m[ ]a") // Notice single h and m and optional space in square bracket
                .toFormatter(Locale.ENGLISH);
        
        // Test
        Stream.of(
                    "08:20 am",
                    "08:20 pm",
                    "08:20 AM",
                    "08:20 PM",
                    "8:20 am",
                    "08:5 pm",
                    "08:5pm",
                    "8:5am"
        ).forEach(s -> System.out.println(LocalTime.parse(s, dtf)));
    }
}

Output:

08:20
20:20
08:20
20:20
08:20
20:05
20:05
08:05

Notice the single h and m which cater for both one-digit as well as two-digit hour and minute. Another thing to notice is the optional space specified using a square bracket. You can specify many optional patterns in a single DateTimeFormatter. Check this answer demonstrating some more powerful features of DateTimeFormatter.

Need today's date and a timezone along with time?

Use ZonedDateTime#of(LocalDate, LocalTime, ZoneId) to create one.

String strTime = "08:20 am";
ZoneId tz = ZoneId.of("America/Los_Angeles");
System.out.println(ZonedDateTime.of(LocalDate.now(tz), LocalTime.parse(strTime, dtf), tz));

ONLINE DEMO

Learn more about java.time, the modern Date-Time API* from Trail: Date Time.


* For any reason, if you have to stick to Java 6 or Java 7, you can use ThreeTen-Backport which backports most of the java.time functionality to Java 6 & 7. If you are working for an Android project and your Android API level is still not compliant with Java-8, check Java 8+ APIs available through desugaring and How to use ThreeTenABP in Android Project.

Upvotes: 1

Basil Bourque
Basil Bourque

Reputation: 339043

tl;dr

ZonedDateTime.of(
    LocalDate.now( ZoneId.of( "Africa/Casablanca" )  ) ,
    LocalTime.parse( "12:05am".toUppercase() , DateTimeFormatter.ofPattern( "hh:mma" , Locale.US ) )  ,
    ZoneId.of( "Africa/Casablanca" ) 
)

java.time

You are using troublesome old date-time classes that are now legacy, supplanted by java.time classes.

java.time.LocalTime

Your input strings use lowercase "am"/"pm" which are incorrect. Those letters are actually initial-letter abbreviations and so should be uppercase. We must force the uppercase to facilitate parsing.

DateTimeFormatter f = DateTimeFormatter.ofPattern( "hh:mma" , Locale.US ) ;
LocalTime lt = LocalTime.parse( "12:05am".toUppercase() , f ) ;

You are inappropriately trying to represent a time-of-day with a date-time class. Instead use LocalTime class as it is meant for a time-of-day with no date and no zone/offset.

Today

Getting the current date requires a time zone.

ZoneId z = ZoneId.of( "Pacific/Auckland" ) ; 
LocalDate ld = LocalDate.now( z );

Combine to get a ZonedDateTime.

ZonedDateTime zdt = ZonedDateTime.of( ld , lt , z ) ;

If that time-of-day on that date in that zone is not valid because of anomalies such as Daylight Saving Time, java.time automatically adjusts. Read the doc to be sure you understand and agree with the adjustment algorithm.

Upvotes: 2

Jim Garrison
Jim Garrison

Reputation: 86774

The problem is that the values for Calendar.HOUR range from 0 to 11, not 1 to 12. When you set the hour to 12 the calendar normalizes this to the opposite half of the day... i.e. you "overflowed" to the next day half.

public static void main(String[] args)
{
    Calendar c1 = Calendar.getInstance();
    System.out.printf("Initial: %s\n",c1.getTime().toString());
    c1.set(Calendar.AM_PM, Calendar.AM);
    System.out.printf("Set(AM): %s\n",c1.getTime().toString());
    c1.set(Calendar.HOUR, 12);
    System.out.printf("Set(12): %s\n\n",c1.getTime().toString());

    Calendar c2 = Calendar.getInstance();
    System.out.printf("Initial: %s\n",c2.getTime().toString());
    c2.set(Calendar.AM_PM, Calendar.PM);
    System.out.printf("Set(PM): %s\n",c2.getTime().toString());
    c2.set(Calendar.HOUR, 12);
    System.out.printf("Set(12): %s\n\n",c2.getTime().toString());
}

Output

Initial: Sun Dec 17 17:53:52 PST 2017
Set(AM): Sun Dec 17 05:53:52 PST 2017
Set(12): Sun Dec 17 12:53:52 PST 2017

Initial: Sun Dec 17 17:53:52 PST 2017
Set(PM): Sun Dec 17 17:53:52 PST 2017
Set(12): Mon Dec 18 00:53:52 PST 2017

All this aside, you should be using the new Time classes that have been part of Java since Java 8. They supersede the legacy classes (i.e. Calendar, Date, etc) that have been known to be sub-optimal for many years.

As suggested by @Andreas, here's how to parse it the modern way:

LocalTime.parse(
    "12:05am", 
     new DateTimeFormatterBuilder().
         parseCaseInsensitive().
         appendPatt‌​‌​ern("hh:mma").
         toFo‌​rm‌​atter(Locale.US)‌​);

Upvotes: 5

Related Questions