What went wrong in your code?
Your bug is in this line:
calendar.set(Calendar.HOUR_OF_DAY, timestamp.charAt(1) + 1);
Characters are represented as numbers in computers. It’s a confusing fact of Java that characters and numbers can be used interchangeably in many cases. If timestamp.charAt(1)
is the char '9'
as in your example, it is represented as the number 57. When you add 1, you get 58. When you set the hour of day to 58 — if you had expected the Calendar
class with default settings to report an error, you were wrong, this a confusing thing about that class and just one of the many reasons why we recommend you avoid using it. It just keeps counting hours into the next days and coincidentally ends up at the right hour of day, 10, only two days later (two days is 48 hours, and 48 + 10 = 58).
Other recommendation:
- Don’t “hand parse” your time string. Leave parsing to a library class.
- Don’t convert to Central European Time by adding an hour. It’s error-prone. During summer time (DST) it will give an incorrect result. It’s not portable to other time zones. Instead again leave the conversion to the library classes.
How to fix?
Basil Bourque already showed the good way to solve your problem using java.time, the modern Java date and time API. I’d like to show you that with java.time you can also include the fraction of second without much trouble if you like, though.
static DateTimeFormatter timeFormatter = new DateTimeFormatterBuilder()
.appendPattern("HHmmss")
.appendFraction(ChronoField.NANO_OF_SECOND, 3, 4, true)
.toFormatter(Locale.ROOT);
static ZoneId zone = ZoneId.of("Europe/Paris");
public static ZonedDateTime utcToLocalTimeFromLock(String timestamp) {
return LocalDate.now(ZoneOffset.UTC)
.atTime(LocalTime.parse(timestamp, timeFormatter))
.atOffset(ZoneOffset.UTC)
.atZoneSameInstant(zone);
}
Let’s try:
System.out.println(utcToLocalTimeFromLock(timestamp));
Output when running just now:
2018-12-11T10:12:15+01:00[Europe/Paris]
The appendFraction
method takes a minimum and maximum number of decimals after the point, so I specified 3 and 4, respectively. Depending on your needs you may specify a minimum down to 0 and a maximum up to 9 digits.
Of course replace your own time zone if it didn’t happen to Europe/Paris.
If you indispensably need an old-fashioned Date
object for a legacy API that you don’t want to upgrade just now:
public static Date utcToLocalTimeFromLock(String timestamp) {
Instant inst= LocalDate.now(ZoneOffset.UTC)
.atTime(LocalTime.parse(timestamp, timeFormatter))
.atOffset(ZoneOffset.UTC)
.toInstant();
return Date.from(inst);
}
Tue Dec 11 10:12:15 CET 2018
If using the backport (ThreeTen Backport and/or ThreeTenABP, see below) for conversion from Instant
to Date
instead of Date.from(inst)
use this:
return DateTimeUtils.toDate(inst);
Because a Date
doesn’t have a time zone, no time zone conversion is necessary in this case. Only because I happen to be in Central European Time zone too, does the output agree with what you expect — it will be the time in the JVM’s time zone.
Question: Can I use java.time on Android?
Yes, java.time works nicely on older and newer Android devices. It just requires at least Java 6.
- In Java 8 and later and on newer Android devices (from API level 26) the modern API comes built-in.
- In Java 6 and 7 get the ThreeTen Backport, the backport of the new classes (ThreeTen for JSR 310; see the links at the bottom).
- On (older) Android use the Android edition of ThreeTen Backport. It’s called ThreeTenABP. And make sure you import the date and time classes from
org.threeten.bp
with subpackages.
Links