1

I have a requirement to

  • Save and retrieve the date in GMT timezone (date should be converted to String). So, if user saves date 10/10/2017 23:05, that will be saved as 10/11/2017 4:05 (5 hours ahead if saved in CST time for e.g.) in DB.
  • While retrieving and presenting the date to UI, it should show as 10/10/2017 23:05 for CST users.
  • Also, need to verify a function to know if the date needs to be shown in US/Non-US date format (dd/MM/YYYY vs mm/DD/YYYY).

To achieve this, I have coded below snippets, however is not yielding the required result. It is storing the value 10/11/2017 4:05, however, when presenting to US, i.e. getting value/ refreshing the page, its adding 5 more hours. Removed exceptions and other unnecessary code to make it simple:

public class DatetoString implements Serializable
{
    private final DateFormat dateFormatter = createDateFormatter();

    // Sets Date to model
    public void setTypedValue(final Object val)
    {
        final String dateValue;
        String dateTimeFormat = BooleanUtils.isFalse(getUSDateFormatConfig()) ? "dd/MM/yyyy HH:mm" : "MM/dd/yyyy HH:mm";
        DateFormat df = new SimpleDateFormat(dateTimeFormat);
        df.setTimeZone(TimeZone.getTimeZone("GMT"));

        Date singleDate = (Date) df.parse(val.toString());
        dateValue = dateFormatter.format(singleDate);
        model.setValue(dateValue.toString());
        // Other code..
    }

    // Retrieves date from model
    public Object getTypedValue()
    {
        final Object result;
        String dateValue = model.iterator().next().getValue();

        String dateTimeFormat = BooleanUtils.isFalse(getUSDateFormatConfig()) ? "dd/MM/yyyy HH:mm" : "MM/dd/yyyy HH:mm";
        DateFormat df = new SimpleDateFormat(dateTimeFormat);
        df.setTimeZone(TimeZone.getTimeZone("GMT"));

        Date singleDate = (Date) df.parse(dateValue);
        result = dateFormatter.format(singleDate);
        return result;
    }

    private DateFormat createDateFormatter()
    {
        final DateFormat result = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss");
        result.setTimeZone(TimeZone.getTimeZone("GMT"));
        return result;
    }
}
ChilBud
  • 185
  • 5
  • 26
  • Is it also a requirement to use the long outdated classes `Date`, `DateFormat`, `SimpleDateFormat` and `TimeZone`? Asking because you’d be much better off using [the modern Java date and time API known as JSR-310 or `java.time`](https://docs.oracle.com/javase/tutorial/datetime/). – Ole V.V. Oct 21 '17 at 07:39
  • 1
    I appreciate your attempt to reduce your code example to a minimum. If there’s a way you could isolate your problem into a self-contained, runnable example, I could try running it and probably more easily spot your problem. See [Short, Self Contained, Correct (Compilable), Example](http://www.sscce.org). – Ole V.V. Oct 21 '17 at 07:43
  • 1
    It seems to me you are doing similar conversions in `setTypedValue` and `getTypedValue`. Shouldn’t you do opposite conversions? I would suppose that in `getTypedValue` you should use `dateFormatter` (the final instance variable) for parsing from GMT and then a formatter using local time zone (not GMT) for formatting? – Ole V.V. Oct 21 '17 at 07:50
  • 1
    Minor points, you don’t need to cast the return value from `df.parse()` in any of the two places you are doing that, since it is already a `Date`. You don’t need to call `toString()` on `dateValue` since it is already a `String`, so the call will just return the same `String` again. – Ole V.V. Oct 21 '17 at 15:18
  • @OleV.V. Thank you for your comments. I am working with a legacy application which uses old date time formats. Also, I have integrated bootstrap date component (https://eonasdan.github.io/bootstrap-datetimepicker/) to show date and time in single field. I will try your suggestions and update on what worked. Thanks for the suggestions – ChilBud Oct 22 '17 at 02:09
  • 1
    You can easily (and I recommend you do) use a modern `DateTimeFormatter` with strings in old formats, there’s really no reason to do something else. I don’t know Bootstrap Datetimepicker, but even if it gives you, say, an old-fashioned `Date` object, you just first thing convert it to a modern `Instant` using `oldFashionedDate.toInstant()` and work using the modern classes from there. – Ole V.V. Oct 22 '17 at 06:55
  • @OleV.V. - Your comment "I would suppose that in getTypedValue you should use dateFormatter (the final instance variable) for parsing from GMT and then a formatter using local time zone (not GMT) for formatting?" Worked for me. changed to `Date singleDate = dateFormatter.parse(dateValue); result = df.format(singleDate);` Please add this comment as an answer and i will mark it. – ChilBud Oct 23 '17 at 05:00

2 Answers2

6

java.time

You are using terrible old date-time classes that are troublesome, confusing, and poorly designed. They are now legacy, supplanted by the java.time classes. Avoid Date, Calendar, SimpleDateFormat, and such.

Use real time zones

By CST did you mean Central Standard Time or China Standard Time?

Specify a proper time zone name in the format of continent/region, such as America/Montreal, Africa/Casablanca, or Pacific/Auckland. Never use the 3-4 letter abbreviation such as EST or IST as they are not true time zones, not standardized, and not even unique(!).

ZoneId z = ZoneId.of( "America/Chicago" );

Confirm time zone with user

If the time zone is critical for your work, you must confirm which zone was intended by their input. There are ways to guess at the zone or detect a default, but where important, make the zone part of your data-entry along with the date and the time-of-day. You can present a list from which they choose, or let them input a string name.

Ditto for Locale (discussed below). You can guess, but if critical, ask.

Parse and assemble

Save and retrieve the date in GMT timezone (date should be converted to String). So, if user saves date 10/10/2017 23:05, that will be saved as 10/11/2017 4:05 (5 hours ahead if saved in CST time for e.g.) in DB.

Parse the user input as a LocalDate and LocalTime using a DateTimeFormatter.

In real work you would add try-catch to capture DateTimeParseException thrown by faulty user input.

DateTimeFormatter fDate = DateTimeFormatter.ofPattern( "MM/dd/uuuu" ) ;
LocalDate ld = LocalDate.parse( inputDate , f ) ;

DateTimeFormatter fTime = DateTimeFormatter.ISO_LOCAL_TIME ; 
LocalTime lt = LocalTime.parse( inputTime , f ) ;

Combine, and specify a time zone to get a ZonedDateTime object.

ZonedDateTime zdt = ZonedDateTime.of( ld , lt , z ) ;

Adjust to UTC by extracting an Instant which is always in UTC by definition. Same moment, same point on the timeline, but viewed through the lens of a different wall-clock.

Instant instant = zdt.toInstant() ;

Database

Persist to your database, in a column of type TIMESTAMP WITH TIME ZONE. The other type WITHOUT ignores any time zone or offset-from-UTC information and is most definitely not what you want to track actual moments in time.

myPreparedStatement.setObject( … , instant ) ;

Retrieve from database.

Instant instant = myResultSet.getObject( … , Instant.class ) ;

While retrieving and presenting the date to UI, it should show as 10/10/2017 23:05 for CST users.

Adjust into whatever time zone the user expects/desires.

ZoneId z = ZoneId.of( "Asia/Kolkata" ) ;  // Or "America/Chicago" or "America/Winnipeg" etc.
ZonedDateTime zdt = instant.atZone( z ) ;

Generate textual representation

Also, need to verify a function to know if the date needs to be shown in US/Non-US date format (dd/MM/YYYY vs mm/DD/YYYY).

Likewise, when generating text to represent that moment, automatically localize with whatever Locale the user expects/desires.

To localize, specify:

  • FormatStyle to determine how long or abbreviated should the string be.
  • Locale to determine (a) the human language for translation of name of day, name of month, and such, and (b) the cultural norms deciding issues of abbreviation, capitalization, punctuation, separators, and such.

Example:

Locale l = Locale.FRANCE ;  // Or Locale.US etc.
DateTimeFormatter f = DateTimeFormatter.ofLocalizedDateTime( FormatStyle.LONG ).withLocale( l ) ;
String output = zdt.format( f ) ;

Note that Locale and time zone are orthogonal, unrelated and separate. You can have a French-speaking clerk in Morocco who is tracking a customer's delivery in India. So the moment is stored in UTC in the database running on a server in Canada, exchanged between database and other components in UTC, adjusted into India time zone to address the perspective of customer receiving delivery, and localized to French for reading by the user in Morocco.


About java.time

The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.

Where to obtain the java.time classes?

The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
  • Very nicely explained @Basil Bourque. Application that I am working on is legacy application and unfortunately (at least for now) have to make changes as it supports. I will try with the knowledge provided by you and update what worked for me. Thanks again for the awesome explanation on date and time.. – ChilBud Oct 22 '17 at 02:07
  • You have provided me very valuable information. I will try these after discussing with my lead. As of now, changing below code worked in getTypedValue() method - `Date singleDate = dateFormatter.parse(dateValue); result = df.format(singleDate);` – ChilBud Oct 23 '17 at 05:01
  • 1
    @ChilBud You may be ignoring the crucial issue of time zone. When not specified explicitly, a time zone is applied implicitly. So your results my be unexpected or may vary with runtime deployment conditions. I never use those those legacy classes so I cannot advise on their behavior with default zone. I *strongly* recommend avoiding those old classes. – Basil Bourque Oct 23 '17 at 06:11
  • Sure @Basil Bourque. I understand that drawbacks of using legacy classes. We are changing those in the next GO.. Thank you. – ChilBud Oct 26 '17 at 02:44
3

java.time

I agree wholeheartedly with Basil Bourque’s thorough and very knowledgeable answer. That your formats are old, has nothing to do with using the old and outdated date and time classes. Using the modern ones would lead to code that comes more naturally, and it would be easier to avoid problems like the one you are asking about. Also use time zone names in the format region/city, and beware that your JVM’s default time zone setting may be changed during runtime by other programs running in the same JVM.

EDIT: I didn’t want to spoil it by providing the code from the outset, but now you have solved your problem, for anyone reading along, here it is:

private static final DateTimeFormatter storeFormatter 
        = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss");
private static final DateTimeFormatter usDisplayFormatter 
        = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm");
private static final DateTimeFormatter internationalDisplayFormatter 
        = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm");

private ZoneId userTimeZone = ZoneId.of("America/Rosario");

/** Sets Date to model */
public void setTypedValue(final Object val)
{
    DateTimeFormatter parseFormatter = isUSDateFormatConfig()
            ? usDisplayFormatter : internationalDisplayFormatter;
    final String dateValue = LocalDateTime.parse(val.toString(), parseFormatter)
            .atZone(userTimeZone)
            .withZoneSameInstant(ZoneOffset.UTC)
            .format(storeFormatter);
    model.setValue(dateValue);
    // Other code..
}

/** Retrieves date from model */
public Object getTypedValue()
{
    String dateValue = model.iterator().next().getValue();
    DateTimeFormatter displayFormatter = isUSDateFormatConfig()
            ? usDisplayFormatter : internationalDisplayFormatter;

    final Object result = LocalDateTime.parse(dateValue, storeFormatter)
            .atOffset(ZoneOffset.UTC)
            .atZoneSameInstant(userTimeZone)
            .format(displayFormatter);
    return result;
}

I called setTypedValue("10/29/2017 21:30"), and the date-time was stored as 10/30/2017 00:30:00. I was able to retrieve it as both 10/29/2017 21:30 in the US and 29/10/2017 21:30 outside.

For now I have hardcoded the user’s time zone as America/Rosario just to demonstrate the use of the region/city format. Instead of the userTimeZone variable you may of course use ZoneId.systemDefault(), but as I said, this may be changed under your feet by other programs running in the same JVM.

If you wanted to modernize your user interface, you could use DateTimeFormatter.ofLocalizedDateTime() instead of the hardcoded display formats, as also mentioned by Basil Bourque.

What was wrong in your code?

It seems to me that in your code in the question you are doing similar conversions in setTypedValue and getTypedValue. Shouldn’t you do opposite conversions? I would suppose that in getTypedValue you should use dateFormatter (the final instance variable) for parsing from GMT and then a formatter using local time zone (not GMT) for formatting.

Minor points:

  • You don’t need to cast the return value from df.parse() in any of the two places you are doing that, since it is already declared that that method returns a Date.
  • You don’t need to call toString() on dateValue since it is already declared a String, so the call will just return the same String again.
Ole V.V.
  • 81,772
  • 15
  • 137
  • 161