2

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
gene b.
  • 10,512
  • 21
  • 115
  • 227
  • 2
    Why are you parsing time yourself? It's so very easy to mess up, like you just did. Use the built-in parser. You should also use the new Java 8 Time API, instead of the old flawed Date API. `LocalTime.parse("12:05am", new DateTimeFormatterBuilder().parseCaseInsensitive().appendPatt‌​‌​ern("hh:mma").toFo‌​rm‌​atter(Locale.US)‌​)` – Andreas Dec 18 '17 at 01:55
  • 2
    Any particular reason why you still use the long outdated `Calendar` class? Today we have so much better in [`java.time`, the modern Java date and time API](https://docs.oracle.com/javase/tutorial/datetime/). For your convenience (and not least that of those who will maintain your code) I recommend looking into the [comment by @Andreas](https://stackoverflow.com/questions/47861076/java-calendar-date-am-and-pm-not-setting-correctly#comment82687263_47861076) and [the answer by Basil Bourque](https://stackoverflow.com/a/47864426/5772882). – Ole V.V. Dec 18 '17 at 08:45

3 Answers3

5

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)‌​);
Andreas
  • 154,647
  • 11
  • 152
  • 247
Jim Garrison
  • 85,615
  • 20
  • 155
  • 190
  • 3
    Up-voted, but you're only showing what went wrong, not how to fix it, e.g. `LocalTime.parse("12:05am", new DateTimeFormatterBuilder().parseCaseInsensitive().appendPatt‌​ern("hh:mma").toForm‌​atter(Locale.US))` – Andreas Dec 18 '17 at 01:56
  • True, but one has to allow the OP _some_ scope to learn stuff on their own. I've added your suggestion (with attribution, thanks) – Jim Garrison Dec 18 '17 at 02:01
  • Much appreciated both Jim + Andreas! – gene b. Dec 18 '17 at 02:13
2

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.

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
1

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.

Arvind Kumar Avinash
  • 71,965
  • 6
  • 74
  • 110