简体   繁体   中英

Parse and retrieve timezone offset from date-time

Date format: " yyyy-MM-dd'T'HH:mm:ss.SSSZ "

Input date: " 2017-09-18T03:08:20.888+0200 "

Problem: I need retrieve timezone offset from the input String and print the parsed date in this timezone. In other words, I need output to be the same as the input.

SimpleDateFormat parses input date successfully and returns java.util.Date object. As we know , Date does not have timezone field. SimpleDateFormat converts parsed date to its timezone, which is, by default, system timezone. When I print this date, it is printed in System timezone.

Simple demo

private static void runDemoTask() throws ParseException {
    final String dateTimeTimezoneFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
    final SimpleDateFormat inputSdf = new SimpleDateFormat(dateTimeTimezoneFormat);
    final String inputDate = "2017-09-18T01:08:20.888+0200";

    Date parsedDate = inputSdf.parse(inputDate);

    final SimpleDateFormat outputSdf = new SimpleDateFormat(dateTimeTimezoneFormat);
    //outputSdf.setTimeZone("X_TIMEZONE_WHICH_I_NEED");
    String output = outputSdf.format(parsedDate);
    System.out.println(output);
}

Output

Mon Sep 18 00:08:20 GMT+01:00 2017

Note, output date has system timezone, which is different from input string.

Note, I will not use java.time , Joda Time and other libraries because I need to support existing code.


Possible unpleasant solution

I tried to use regular expression to retrieve sign and offset.

private static  String parseTimeZone(String input) {
    final int singGroup = 1;
    final int offsetGroup = 2;
    final String timezonePatternStr = "([+-])(\\d{4})$";
    final Pattern timezonePattern = Pattern.compile(timezonePatternStr);

    Matcher matcher = timezonePattern.matcher(input);
    if (matcher.find()) {
        String sign = matcher.group(singGroup);
        String offset = matcher.group(offsetGroup);
        System.out.println(sign + " " + offset);
    }

    return "";
} 

It prints

+ 0200

Thank you, guys: @Thomas, @ole-vv

final DateTimeFormatter inputSdf1 = DateTimeFormatter.ofPattern(dateTimeTimezoneFormat);
OffsetDateTime d = OffsetDateTime.parse(inputDate, inputSdf1);

ZoneOffset zo = d.getOffset();  //Offset from the input.
TimeZone tz = TimeZone.getTimeZone(zo.normalized());

outputSdf.setTimeZone(tz);
System.out.println(outputSdf.format(parsedDate));

tl;dr

TimeZone.getTimeZone(       // Convert from modern java.time type (`ZoneOffset`/`ZoneId`) to legacy type (`TimeZone`)
    OffsetDateTime.parse (  // Instantiate a `OffsetDateTime`, a moment on the timeline.
        "2017-09-18T03:08:20.888+0200" ,
        DateTimeFormatter.ofPattern( "uuuu-MM-dd'T'HH:mm:ss.SSSX" )
    ).getOffset()           // Extract a `ZoneOffset`, which is a subclass of `ZoneId`.
)

Convert directly from modern ZoneOffset to legacy TimeZone

The code seen here is similar to Answers by Yan Khonski, but using the variation of TimeZone.getTimeZone that directly converts from the modern java.time classes ( ZoneOffset & ZoneID ) to the legacy TimeZone class.

While there is no difference in the end result, this approach uses a an explicit conversion method. This is one of many new methods added to the old date-time classes for converting to/from java.time objects.

Using such a conversion method makes your code more self-documenting. Also makes more clear your awareness that you are consciously moving between the modern & legacy classes.

String input = "2017-09-18T03:08:20.888+0200";
DateTimeFormatter f = DateTimeFormatter.ofPattern( "uuuu-MM-dd'T'HH:mm:ss.SSSX" );

OffsetDateTime odt = OffsetDateTime.parse( input , f );  // Parse input.
ZoneOffset offset = odt.getOffset( );                    // Interrogate for the `ZoneOffset` object representing this moment’s offset-from-UTC (a number of hours/minutes/seconds).

TimeZone tz = TimeZone.getTimeZone( offset );            // Convert from modern java.time object (a `ZoneOffset`/`ZoneId`) to the legacy class `TimeZone`.

Dump to console.

System.out.println( "odt.toString(): " + odt );
System.out.println( "offset.toString(): " + offset );
System.out.println( "tz.toString(): " + tz );

odt.toString(): 2017-09-18T03:08:20.888+02:00

offset.toString(): +02:00

tz.toString(): sun.util.calendar.ZoneInfo[id="GMT+02:00",offset=7200000,dstSavings=0,useDaylight=false,transitions=0,lastRule=null]


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 .

SimpleDateFormat extends DateFormat and thus internally uses a Calendar . When parsing the date that calendar is being updated so you can get the timezone from it after parsing:

 
 
 
  
  //use the timezone of the internally stored calendar outputSdf.setTimeZone( inputSdf.getTimezone() );
 
  

That also shows why DateFormat is not threadsafe.

EDIT:

It seems the internal calendar's timezone isn't updated but the ZONE_OFFSET field is. Hence you could do something like this:

int zoneOffset = inputSdf.getCalendar().get( Calendar.ZONE_OFFSET );
//length check etc. left for you
String matchingZoneId = TimeZone.getAvailableIDs( zoneOffset )[0];
outputSdf.setTimeZone( TimeZone.getTimeZone( matchingZoneId ) );

Note that you can't just set the zone offset of the output format since that won't update the timezone reference which is used when formatting.

As you can see doing it this way looks a little "hacky" and thus you should think hard on whether you really need the timezone. In most cases you'd define the output timezone in a different way anyways, eg by getting the user's location, input, etc.

Hardcore solution!

Parse timezone and retrieve hours and minutes and sign and build a timezone!

public class ParseTimeZone {

    private static final String TIME_ZONE_REGEX = "([+-])(\\d{2})[:]?(\\d{2})$";
    private static final Pattern TIME_ZONE_PATTERN = Pattern.compile(TIME_ZONE_REGEX);

    private static final int SING_GROUP = 1;
    private static final int HOURS_GROUP = 2;
    private static final int MINUTES_GROUP = 3;

    private static final String PLUS = "+";
    private static final String MINUS = "-";

    private static final int MINUTES_IN_HOUR = 60;
    private static final int SECONDS_IN_MINUTE = 60;

    public static void main(String[] args) {
        final String inputDate = "2017-09-18T01:08:20.888Z";
        parseTimeZone(inputDate);
    }

    private static String parseTimeZone(String input) {
        input = fixDateStringWithZeroZoneOffset(input);
        Matcher matcher = TIME_ZONE_PATTERN.matcher(input);
        if (!matcher.find()) {
            return "";
        }

        String sign = matcher.group(SING_GROUP);
        String hours = matcher.group(HOURS_GROUP);
        String minutes = matcher.group(MINUTES_GROUP);

        int offsetSeconds = calculateOffsetSeconds(sign, hours, minutes);

        ZoneOffset zoneOffset = ZoneOffset.ofTotalSeconds(offsetSeconds);
        System.out.println(zoneOffset);

        TimeZone timeZone = TimeZone.getTimeZone(zoneOffset);
        System.out.println(timeZone);

        return "";
    }

    private static int calculateOffsetSeconds(String signStr, String hoursStr, String minutesStr) {
        try {
            int hours = Integer.parseInt(hoursStr);
            int minutes = Integer.parseInt(minutesStr);
            int sign = parseSign(signStr);

            int seconds = sign * ((hours * MINUTES_IN_HOUR + minutes) * SECONDS_IN_MINUTE);
            return seconds;

        } catch (NumberFormatException e) {
            throw new RuntimeException("It should not happen because it matches the regular expression. There should be numbers.", e);
        }
    }

    private static int parseSign(String sign) {
        if (sign.equals(PLUS)) {
            return 1;
        } else if (sign.equals(MINUS)) {
            return -1;
        } else {
            throw new RuntimeException("Offset sign should be + or -.");
        }
    }

    private static String fixDateStringWithZeroZoneOffset(String dateString) {
        if (dateString.endsWith("Z")) {
            return dateString.replaceAll("Z$", "+0000");
        }
        return dateString;
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM