1

As part of some logic, it is necessary in my program to turn a long Java timestamp (including year, month, etc.) to a 'short' Java time. This should correspond to exactly the same hours, minutes and seconds of the original time, but within 1 day of Jan 1 1970 (i.e. a value between 0 (00:00:00) and 86400000 (23:59:59)). An example is the conversion in the question.

In order the perform this, I thought the below code would work:

public int convertToTime(long fullTimeStamp) {
    Calendar c = Calendar.getInstance();
    c.setTimeInMillis(date);
    c.set(Calendar.DATE, 1);
    c.set(Calendar.MONTH, 0);
    c.set(Calendar.YEAR, 1970);
    return (int) c.getTimeInMillis();
}

The issue I am having is to do with timezones. In the UK we are currently in BST. After setting all the values with the function, the time remains the same numbers (e.g. 8.00am) but changes the timezone to GMT! 8.00am GMT is of course not the same as 8.00am BST, and is instead equal to 9.00am BST.

Adding some console output to the function demonstrates this issue:

public int convertToTime(long fullTimeStamp) {
    System.out.println(new Date(fullTimeStamp)); // correct

    Calendar c = Calendar.getInstance();
    c.setTimeInMillis(fullTimeStamp);

    System.out.println(c.getTime()); // correct

    c.set(Calendar.DATE, 1);
    c.set(Calendar.MONTH, 0);
    c.set(Calendar.YEAR, 1970);

    System.out.println(c.getTime()); // incorrect!

    return (int) c.getTimeInMillis();
}

Program output:

Wed Jun 19 12:15:00 BST 2013 // ok
Wed Jun 19 12:15:00 BST 2013 // this makes sense
Thu Jan 01 12:15:00 GMT 1970 // Calendar, stahp!

The desired behaviour is for the last part to read:

Thu Jan 01 11:15:00 GMT 1970
or
Thu Jan 01 12:15:00 BST 1970

Is this expected behaviour of the calendar? My understanding was that it keeps all the 'digits' the same that aren't modified, so if the value of HOUR_OF_DAY is 8, it should stay at 8, even if the timezone is modified.

I have tried setting the timezone on the calendar (before any values are set) to BST and GMT and exactly the same behaviour occurs. I also cannot manually add or remove milliseconds to delete all years after 1970 as I will have to handle leap years.

Aside from 'use Joda time (or some other time package)' does anyone have any other suggestions to perform this operation? I kind of need to get a quick fix in before experimenting with other packages if possible.

Thanks!

Overlord_Dave
  • 894
  • 10
  • 27
  • Maybe you can try using java.sql.Time. – Raphaël Jun 19 '13 at 09:11
  • this is a possible solution: [link](http://stackoverflow.com/questions/2891361/java-how-to-set-timezone-of-a-java-util-date) – Gergely Králik Jun 19 '13 at 09:11
  • Why do you need this, exactly? – Rajesh J Advani Jun 19 '13 at 09:12
  • You can perform this with 2 SimpleDateFormat. The first one corresponds to the original format, the second one has only "HH:mm:ss". – Arnaud Denoyelle Jun 19 '13 at 09:12
  • @RajeshJAdvani I need to do this to perform calculations regarding time periods within a day - an example would be "if the valid period is 1100 -> 2000, is the period 1234->2123 within this?". This is simple enough until periods can be inverse, i.e. 2000 -> 1100 represents an 'overnight' validity (1100->2359 and then 0000 -> 1059). I spend a long time fiddling with the logic and found only considering the 'time' without an associated 'date' is the easiest way to perform calculations. – Overlord_Dave Jun 19 '13 at 09:15
  • Maybe it helps already if you start working with `Date` only - at least for type safety. I'm also quite sure that you can set the timezone on `Date` objects – Joshua Jun 19 '13 at 09:17
  • As @JonSkeet says below, once you start changing the day/year/month, you start getting into questions of milleseconds since *when*? You'll have issues during the boundary condition when daylight savings time kicks in. The better approach would be to calculate this by writing a function to count milliseconds since 00:00:00 of the first day in question, and use that in your calculations. – Rajesh J Advani Jun 19 '13 at 09:18
  • 1
    86400000 would be 24:00:00. 86400000 should be an exclusive upper bound, not an inclusive one. – Jon Skeet Jun 19 '13 at 09:18

2 Answers2

5

I think you're running foul of a little-known fact about the UK time zone: at the Unix epoch, we were actually in UTC+1. Java is getting the time of day right (within the UK time zone), but the name wrong - it shouldn't be specifying GMT, but BST. This isn't BST as in British Summer Time; it's BST as in British Standard Time. Yes, it's that mad.

From the relevant wikipedia article:

An inquiry during the winter of 1959–60, in which 180 national organisations were consulted, revealed a slight preference for a change to all-year GMT+1, but the length of summer time was extended as a trial rather than the domestic use of Greenwich Mean Time abolished.[8] A further inquiry during 1966–67 led the government of Harold Wilson to introduce the British Standard Time experiment, with Britain remaining on GMT+1 throughout the year. This took place between 27 October 1968 and 31 October 1971, when there was a reversion to the previous arrangement.

It's worth bearing in mind that your original problem statement is somewhat ambiguous: you're taking in a long, which is just the millis since the Unix epoch - but then you're trying to interpret it in terms of the hour of day, which immediately begs the question of which time zone you need to interpret it in. Have you made that decision? If so, you should document it very carefully, and make sure your code complies with it.

Ultimately, my recommendations are:

  • If you can possibly use Joda Time, do so. It will save you hours and hours of heartache.
  • If you're trying to calendar calculations like this, consider changing the time zone of the calendar to UTC before doing anything else; it will save you some heartache
  • Avoid using Date.toString() where possible - you could use a DateFormatter with the time zone set to UTC, and then you would see the expected results

As user2340612's answer states, to get just the "millisecond of UTC day" you can use simple arithmetic - but not quite with the values given. I would use:

long timeOfDay = millisecondsSinceUnixEpoch % TimeUnit.DAYS.toMillis(1);

... but this only works if you're interested in the UTC time of day for the given instant. (It will also give a negative result for negative input, but you may not care about that.)

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • Will `millisecondsSinceUnixEpoch % TimeUnit.DAYS.toMillis(1);` definitely work? Are all days, handling leap years, leap seconds, etc. of the same length? I think I initially used this conversion but encountered problems (which helpfully I cannot remember) – Overlord_Dave Jun 19 '13 at 09:19
  • @Overlord_Dave sadly not - that's why you should work with `Date`, `Calendar` and corresponding formatters only and not with raw timestamps – Joshua Jun 19 '13 at 09:22
  • @Joshua As a side note, is there really any difference between working with `Date` and a `long` timestamp? Jus' sayin'. And yes, I do realise working with timestamps may have been an issue but I want to try and avoid changing a lot of code if possible. – Overlord_Dave Jun 19 '13 at 09:25
  • @Overlord_Dave: Leap years make no difference to the time of day, and leap seconds aren't included in any Java implementation I've used. Daylight saving transitions are irrelevant when you're considering UTC. – Jon Skeet Jun 19 '13 at 09:32
0

If you need a timestamp between 0 and 86399999 (which is 23:59:59.999) you can get the current timestamp and calculate the remainder of the division between it and 86400000:

desired_time = cur_time % 86400000

But you'll miss the summer time, if present.

user2340612
  • 10,053
  • 4
  • 41
  • 66
  • 1
    That's not going to give the desired result, as it's expecting the day to be 86400001 milliseconds long. Basically 86400000 should be an *exclusive* upper bound. – Jon Skeet Jun 19 '13 at 09:18
  • you are right! I was wrong because the user said that 23:59:59 was 86400000 :) – user2340612 Jun 19 '13 at 09:20