4

I'm new to Java. I have a time I am getting from a web-page, this is in the "hh:mm" format (not 24 hour). This comes to me as a string. I then want to combine this string with todays date in order to make a Java Date I can use.

In C#:

string s = "5:45 PM";
DateTime d;
DateTime.TryParse(s, out d);

in Java I have attempted:

String s = "5:45 PM";
Date d = new Date(); // Which instantiates with the current date/time.

String[] arr = s.split(" ");
boolean isPm = arr[1].compareToIgnoreCase("PM") == 0;

arr = arr[0].split(":");
int hours = Integer.parseInt(arr[0]);
d.setHours(isPm ? hours + 12 : hours);
d.setMinutes(Integer.parseInt(arr[1]));
d.setSeconds(0);

Is there a better way to achieve what I want?

MoonKnight
  • 23,214
  • 40
  • 145
  • 277

2 Answers2

14

Is there a better way to achieve what I want?

Absolutely - in both .NET and in Java, in fact. In .NET I'd (in a biased way) recommend using Noda Time so you can represent just a time of day as a LocalTime, parsing precisely the pattern you expect.

In Java 8 you can do the same thing with java.time.LocalTime:

import java.time.*;
import java.time.format.*;

public class Test {
    public static void main(String[] args) {
        String text = "5:45 PM";
        DateTimeFormatter format = DateTimeFormatter.ofPattern("h:mm a");
        LocalTime time = LocalTime.parse(text, format);
        System.out.println(time);
    }
}

Once you've parsed the text you've got into an appropriate type, you can combine it with other types. For example, to get a ZonedDateTime in the system time zone, using today's date and the specified time of day, you might use:

ZonedDateTime zoned = ZonedDateTime.now().with(time);

That uses the system time zone and clock by default, making it hard to test - I'd recommend passing in a Clock for testability.

(The same sort of thing is available in Noda Time, but slightly differently. Let me know if you need details.)

I would strongly recommend against using java.util.Date, which just represents an instant in time and has an awful API.

The key points here are:

  • Parse the text with a well-specified format
  • Parse the text into a type that represents the information it conveys: a time of day
  • Combine that value with another value which should also be carefully specified (in terms of clock and time zone)

All of these will lead to clear, reliable, testable code. (And the existing .NET code doesn't meet any of those bullet points, IMO.)

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • Wait, Jon, this does not give me the equivelent of my .NET code. I want to have todays date, with the time ammended to the string value I want. So for today "16/08/2017 15:45" preferable as a date object so I can perform operation against it. This merely gives "17:45" when dumped to console. – MoonKnight Aug 16 '17 at 12:32
  • 1
    @MoonKnight: A `Date` object is an *instant in time*. What time zone do you want that in? `Date` is almost always the wrong type to use. I'll edit my answer a bit more. – Jon Skeet Aug 16 '17 at 12:36
  • @MoonKnight: Expanded quite a bit. – Jon Skeet Aug 16 '17 at 12:40
  • Nice. I merely want a snapshot, but I agree with all the points you make regarding Date. Thanks very much for taking time to write sure a detailed respose, it is most appreciated. I take everything you have said on board. Thanks again. – MoonKnight Aug 16 '17 at 12:45
3

To parse the time, you can do as explained in @Jon Skeet's answer:

String input = "5:45 PM";
DateTimeFormatter parser = DateTimeFormatter.ofPattern("h:mm a", Locale.ENGLISH);
LocalTime time = LocalTime.parse(input, parser);

Note that I also used a java.util.Locale because if you don't specify it, it'll use the system's default locale - and some locales can use different symbols for AM/PM field. Using an explicit locale avoids this corner-case (and the default locale can also be changed, even at runtime, so it's better to use an explicit one).

To combine with the today's date, you'll need a java.time.LocalDate (to get the date) and combine with the LocalTime, to get a LocalDateTime:

// combine with today's date
LocalDateTime combined = LocalDate.now().atTime(time);

Then you can format the LocalDateTime using another formatter:

DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm");
System.out.println(combined.format(fmt));

The output is:

16/08/2017 17:45


If you want to convert the LocalDateTime to a java.util.Date, you must take care of some details.

A java.util.Date represents the number of milliseconds since 1970-01-01T00:00Z (aka Unix Epoch). It's an instant (a specific point in time). Check this article for more info.

So, the same Date object can represent different dates or times, depending on where you are: think that, right now, at this moment, everybody in the world are in the same instant (the same number of milliseconds since 1970-01-01T00:00Z), but the local date and time is different in each part of the world.

A LocalDateTime represents this concept of "local": it's a date (day, month and year) and a time (hour, minute, second and nanosecond), but without any relation to a specific timezone.

The same LocalDateTime object can represent different instants in time in different timezones. So, to convert it to a Date, you must define in what timezone you want it.

One option is to use the system's default timezone:

// convert to system's default timezone
ZonedDateTime atDefaultTimezone = combined.atZone(ZoneId.systemDefault());
// convert to java.util.Date
Date date = Date.from(atDefaultTimezone.toInstant());

But the default can vary from system/environment, and can also be changed, even at runtime. To not depend on that and have more control over it, you can use an explicit zone:

// convert to a specific timezone
ZonedDateTime zdt = combined.atZone(ZoneId.of("Europe/London"));
// convert to java.util.Date
Date date = Date.from(zdt.toInstant());

Note that I used Europe/London. The API uses IANA timezones names (always in the format Region/City, like America/Sao_Paulo or Europe/Berlin). Avoid using the 3-letter abbreviations (like CST or PST) because they are ambiguous and not standard.

You can get a list of available timezones (and choose the one that fits best your system) by calling ZoneId.getAvailableZoneIds().

And there's also the corner cases of Daylight Saving Time (when a LocalDateTime can exist twice or can't exist due to overlaps and gaps). In this case, Jon's solution using ZonedDateTime avoids this problem).

  • 1
    Fantastic answer, thanks very much for your time. I have learnt a lot from this. All the best. – MoonKnight Aug 16 '17 at 13:50
  • @MoonKnight You're welcome, glad to help! May I recommend this [tutorial](https://docs.oracle.com/javase/tutorial/datetime), it's a good starting point for `java.time` API. Cheers! –  Aug 16 '17 at 13:55
  • You should instead use `Locale.ROOT`, as it is *["used as the language/country neutral locale for the locale sensitive operations"](https://docs.oracle.com/en/java/javase/14/docs/api/java.base/java/util/Locale.html#ROOT)*. – MC Emperor Nov 28 '20 at 14:30