SimpleDateFormat hour string off by one on certain *dates*. How? Why?

I'm using SimpleDateFormat to turn my GregorianCalendar instances into nice strings. I'm having a weird issue where in certain dates the time string is off by one, but not always.

Normally, if my GregorianCalendar has values like HOUR_OF_DAY=0,MINUTE=11 then I get a string like "1:11 AM". But then I change the YEAR, MONTH, or DAY and now HOUR_OF_DAY=0,MINUTE=11 gives me the string "2:11 AM".

This is my time to string formatting function (datetime is the GregorianCalendar):

public String toTimeString() {
    Log.i(TAG, "Datetime: "+ datetime.toString() + "Formatted as: " + SimpleDateFormat.getTimeInstance(SimpleDateFormat.SHORT).format(datetime.getTime()));
    return SimpleDateFormat.getTimeInstance(SimpleDateFormat.SHORT).format(datetime.getTime());
}

This gives me these logs:

Working good at first:

Datetime: java.util.GregorianCalendar[time=1426637512725,areFieldsSet=true,lenient=true,zone=GMT,firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YEAR=2015,MONTH=2,WEEK_OF_YEAR=12,WEEK_OF_MONTH=3,DAY_OF_MONTH=18,DAY_OF_YEAR=77,DAY_OF_WEEK=4,DAY_OF_WEEK_IN_MONTH=3,AM_PM=0,HOUR=0,HOUR_OF_DAY=0,MINUTE=11,SECOND=52,MILLISECOND=725,ZONE_OFFSET=0,DST_OFFSET=0]
Formatted as: 1:11 AM

then I change the month from March to April (year and day do the same) and now this:

Datetime: java.util.GregorianCalendar[time=1429315912725,areFieldsSet=true,lenient=true,zone=GMT,firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YEAR=2015,MONTH=3,WEEK_OF_YEAR=16,WEEK_OF_MONTH=3,DAY_OF_MONTH=18,DAY_OF_YEAR=108,DAY_OF_WEEK=7,DAY_OF_WEEK_IN_MONTH=3,AM_PM=0,HOUR=0,HOUR_OF_DAY=0,MINUTE=11,SECOND=52,MILLISECOND=725,ZONE_OFFSET=0,DST_OFFSET=0]
Formatted as: 2:11 AM

How could this possibly be?

This off-by-one behavior only seems to happen in future months, not this month or last. 2015 and 2016 are fine, but 2017 causes it the problem, too. The actual date seems to trigger it when it's a week or more into the future (regardless of year).

I can change year, month, and date in different UI widgets, switching the time-string formatting from normal to off-by-one and back. I control the hour with a TimePicker that calls the string formatter on TimePicker.OnTimeChangedListener(). I don't think it's relevant, since it works when month and day are set to some values, but here is that code just in case. As you can see, there is some off-by-one handling, as GregorianCalendar stores HOUR_OF_DAY as a 0-23 value, and the TimePicker uses values 1-24 matching what the widget actually displays. I think I've done this right, but would be happy to find out the problem is a mistake here.

private void setUpTimePicker() {
    timePicker.setCurrentHour(schedule.getHourOfDay() + 1);
    timePicker.setCurrentMinute(schedule.getMinute());

    timePicker.setOnTimeChangedListener(new TimePicker.OnTimeChangedListener() {
        public void onTimeChanged(TimePicker view, int hourOfDay, int minute) {
            schedule.setHourOfDay(hourOfDay - 1);
            schedule.setMinute(minute);
            timeText.setText(schedule.toTimeString());
        }
    });
}

The time of day formatted to a string is always right (0 = 1am, 17 = 5pm, etc for every hour of the day) or always wrong (0 = 2am, 17 = 6pm, ect for every hour of the day) based on the other calendar fields. The consistency makes me think that it's not a thread issue.

Could it be a timezone issue like every other similar question? I can't imagine how. My GregorianCalandar instances are always created with this function:

public static TimeZone TIMEZONE = TimeZone.getTimeZone("GMT");

private static GregorianCalendar makeCalendar(){
    GregorianCalendar now = (GregorianCalendar) GregorianCalendar.getInstance(TIMEZONE); 
    now.setFirstDayOfWeek(Calendar.SUNDAY); 
    return now;
}

Any thoughts would be very appreciated! I've exhausted everything that I can think of, including sleeping on it for a few days. Thanks in advance to anyone who answers!!

UPDATE

Jon found the problem, but since the code change is hidden in comments, I'll copy it here, too.

The problem was that SimpleDateFormat is using the system timezone, not the timezone of the given GregorianCalendar, like I had assumed. I fixed this by changing this line:

return SimpleDateFormat.getTimeInstance(SimpleDateFormat.SHORT).format(datetime.getTime());

to this:

SimpleDateFormat sdf = new SimpleDateFormat("hh:mm a", LOCALE);
        sdf.setTimeZone(TIMEZONE);
        return sdf.format(datetime.getTime());

Then I removed the +1 and -1 from the TimePicker and it works!

Jon Skeet
people
quotationmark

You're setting the time zone of the calendar you're using, but that's not what you pass into the SimpleDateFormat - you're just using the default system time zone when you format here:

SimpleDateFormat.getTimeInstance(SimpleDateFormat.SHORT).format(datetime.getTime())

I would suggest that:

  • Ideally, move on to Joda Time or java.time from Java 8. The java.util classes are awful. I realize you're working on Android, so java.time is out, but is Joda Time an option for you?
  • Take the date picker out of the equation: try hard to come up with a short but complete program demonstrating the problem - just a console app which (say) tries to format 5am on the 1st of every month from the start of 2010 to the end of 2019. (Try to reproduce it outside Android, for simplicity.)
  • Whenever you format or parse (or do anything, basically), work out exactly what time zone you're interested in, and specify it explicitly. (I'd recommend using UTC rather than GMT to indicate "no offset from UTC" as otherwise some people will think you mean the UK time zone.)

people

See more on this question at Stackoverflow