Bryan
Bryan

Reputation: 15155

Localized Date/Time with More Control

I am using the ThreeTen-Backport (specifically ThreeTenABP) to display a timestamp in my project. I would like the displayed timestamp to be displayed in a localized format (based on the Locale of the system); which is easy enough using either of the DateTimeFormatter.ofLocalizedDateTime() methods:

DateTimeFormatter formatter = DateTimeFormatter
        .ofLocalizedDateTime(FormatStyle.LONG)
        .withLocale(Locale.getDefault())
        .withZone(ZoneId.systemDefault());

String timestamp = formatter.format(Instant.now());

The issue is that I do not have much control over the output of the formatter with only four FormatStyle types (SHORT, MEDIUM, LONG, FULL). I am curious if there is a way to have much more fine-tuned control over the output, without losing the localization formatting.


Using the previous code, the resulting timestamp for the "en_US" locale would be:

"January 23, 2017 1:28:37 PM EST"

While the result for the "ja_JP" locale would be:

"2017年1月23日 13:28:37 GMT-5:00"

As you can see, each of the locales utilize a specific pattern, and use a default of either the 12 or 24 hour format. I would like to maintain the localized pattern, but change things like whether or not the time zone is displayed, or if the 12 or 24 hour format is used.

For example; if I could set both locales to use the 12 hour format, and remove the time zone; the results would look like this:

"January 23, 2017 1:28:37 PM"

"2017年1月23日 1:28:37午後"

Upvotes: 5

Views: 2408

Answers (2)

user7605325
user7605325

Reputation:

The problem with FormatStyle's (AFAIK) is that they use predefined patterns. Although, it's possible to get them and manipulate/change the pattern to fit your needs.

I'm not using Android specific environment, so I'm not sure how well this code will work for you. I'm using Java JDK 1.7.0_79 and ThreeTen Backport 1.3.4. I'm also using America/New_York timezone for my tests - I guess it corresponds to EST.

I noticed some differences from your environment:

  • using FormatStyle.LONG for japanese locale gives me 2017/01/23 13:28:37 EST
  • I had to use FormatStyle.FULL to get 2017年1月23日 13時28分37秒 EST

But I think this doesn't invalidate my tests.

First I used java.text.DateFormat class to get the localized pattern as a String. Then I did some replacements to this String, according to the configuration wanted:

  • look for HH, hh, H or h and change to use 12 or 24 hour format (I replaced keeping the same number of letters)
  • remove or add z (or Z): to remove or add a timezone
  • avoid literals: some patterns (like in pt_BR locale) have a literal h after the hours (like HH'h' which becomes 13h), so I had to take care of this when replacing

The code to create the formatter is:

// creates a formatter with the specified style, locale and zone
// there are options to use 12 or 24 hour format and include or not a timezone
public DateTimeFormatter getFormatter(FormatStyle style, Locale locale, ZoneId zone,
                                      boolean use24HourFormat, boolean useTimezone) {
    // get the format correspondent to the style and locale
    DateFormat dateFormat = DateFormat.getDateTimeInstance(style.ordinal(), style.ordinal(), locale);

    // *** JDK 1.7.0_79 returns SimpleDateFormat ***
    // If Android returns another type, check if it's possible to get the pattern from this type
    if (dateFormat instanceof SimpleDateFormat) {
        // get the pattern String for the locale
        String pattern = ((SimpleDateFormat) dateFormat).toPattern();

        if (use24HourFormat) {
            if (pattern.contains("hh")) { // check the "hh" hour format
                // hh not surrounded by ' (to avoid literals)
                pattern = pattern.replaceAll("((?<!\')hh)|(hh(?!\'))", "HH");
            } else { // check the "h" hour format
                // h not surrounded by ' (to avoid literals)
                pattern = pattern.replaceAll("((?<!\')h)|(h(?!\'))", "H");
            }
        } else {
            if (pattern.contains("HH")) { // check the "HH" hour format
                // HH not surrounded by ' (to avoid literals)
                pattern = pattern.replaceAll("((?<!\')HH)|(HH(?!\'))", "hh");
            } else { // check the "H" hour format
                // H not surrounded by ' (to avoid literals)
                pattern = pattern.replaceAll("((?<!\')H)|(H(?!\'))", "h");
            }
        }

        if (useTimezone) {
            // checking if already contains a timezone (the naive way)
            if (!pattern.contains("z") && !pattern.contains("Z")) {
                // I'm adding z in the end, but choose whatever pattern you want for the timezone (it can be Z, zzz, and so on)
                pattern += " z";
            }
        } else {
            // 1 or more (z or Z) not surrounded by ' (to avoid literals)
            pattern = pattern.replaceAll("((?<!\')[zZ]+)|([zZ]+(?!\'))", "");
        }

        // create the formatter for the locale and zone, with the customized pattern
        return DateTimeFormatter.ofPattern(pattern, locale).withZone(zone);
    }

    // can't get pattern string, return the default formatter for the specified style/locale/zone
    return DateTimeFormatter.ofLocalizedDateTime(style).withLocale(locale).withZone(zone);
}

Some usage examples (my default Locale is pt_BR - Brazilian Portuguese):

ZoneId zone = ZoneId.of("America/New_York");
Instant instant = ZonedDateTime.of(2017, 1, 23, 13, 28, 37, 0, zone).toInstant();
FormatStyle style = FormatStyle.FULL;

// US locale, 24-hour format, with timezone
DateTimeFormatter formatter = getFormatter(style, Locale.US, zone, true, true);
System.out.println(formatter.format(instant)); // Monday, January 23, 2017 13:28:37 PM EST
// US locale, 24-hour format, without timezone
formatter = getFormatter(style, Locale.US, zone, true, false);
System.out.println(formatter.format(instant)); // Monday, January 23, 2017 13:28:37 PM 
// US locale, 12-hour format, with timezone
formatter = getFormatter(style, Locale.US, zone, false, true);
System.out.println(formatter.format(instant)); // Monday, January 23, 2017 1:28:37 PM EST
// US locale, 12-hour format, without timezone
formatter = getFormatter(style, Locale.US, zone, false, false);
System.out.println(formatter.format(instant)); // Monday, January 23, 2017 1:28:37 PM 

// japanese locale, 24-hour format, with timezone
formatter = getFormatter(style, Locale.JAPAN, zone, true, true);
System.out.println(formatter.format(instant)); // 2017年1月23日 13時28分37秒 EST
// japanese locale, 24-hour format, without timezone
formatter = getFormatter(style, Locale.JAPAN, zone, true, false);
System.out.println(formatter.format(instant)); // 2017年1月23日 13時28分37秒 
// japanese locale, 12-hour format, with timezone
formatter = getFormatter(style, Locale.JAPAN, zone, false, true);
System.out.println(formatter.format(instant)); // 2017年1月23日 1時28分37秒 EST
// japanese locale, 12-hour format, without timezone
formatter = getFormatter(style, Locale.JAPAN, zone, false, false);
System.out.println(formatter.format(instant)); // 2017年1月23日 1時28分37秒 

// pt_BR locale, 24-hour format, with timezone
formatter = getFormatter(style, Locale.getDefault(), zone, true, true);
System.out.println(formatter.format(instant)); // Segunda-feira, 23 de Janeiro de 2017 13h28min37s EST
// pt_BR locale, 24-hour format, without timezone
formatter = getFormatter(style, Locale.getDefault(), zone, true, false);
System.out.println(formatter.format(instant)); // Segunda-feira, 23 de Janeiro de 2017 13h28min37s 
// pt_BR locale, 12-hour format, with timezone
formatter = getFormatter(style, Locale.getDefault(), zone, false, true);
System.out.println(formatter.format(instant)); // Segunda-feira, 23 de Janeiro de 2017 01h28min37s EST
// pt_BR locale, 12-hour format, without timezone
formatter = getFormatter(style, Locale.getDefault(), zone, false, false);
System.out.println(formatter.format(instant)); // Segunda-feira, 23 de Janeiro de 2017 01h28min37s 

Notes:

  • The method DateFormat.getDateTimeInstance returns a SimpleDateFormat, but I'm not sure if it works the same way in Android. You can check if it returns a different type and if it's possible to get the pattern String from this class (if it's not, then I don't know another way of doing it).
  • I created options to change the hour format (12 or 24) and include or remove a timezone. But you can create as many options as you need - once you have the pattern String, you can do anything with it
  • My regular expressions are (IMO) a little bit ugly. But I'm not a regex expert and I'm not sure how they can be improved. I've tested them with some locales and they seem to be fine, but there might be some particular case where they fail (although I haven't tested enough to find it)
  • If you want to add the offset (like -05:00) you can use the xxx pattern (instead of z). If you want the offset with GMT (like GMT-05:00) you can use the ZZZZ pattern.
  • I'm not sure if all patterns have the z as the timezone (they can use Z or x), so you might change the code to look for the other patterns. In my tests I didn't find anything different from z, but anyway I'd recommend a double check on this just to make sure.
  • I'm checking if the pattern already has a timezone by doing pattern.contains("z") - a very naive/silly way because it doesn't handle a z literal (inside quotes). Maybe this could be changed to use a regex as well (although I didn't find a locale with a pattern that has a z literal).

Upvotes: 1

ProgrammersBlock
ProgrammersBlock

Reputation: 6304

You can get the format string of the Locale with DateTimeFormatterBuilder.getLocalizedDateTimePattern. Once you have that string then you can manipulate it with the DateTimeFormatter.ofPattern method.

    String fr = DateTimeFormatterBuilder.getLocalizedDateTimePattern(FormatStyle.LONG, FormatStyle.FULL, IsoChronology.INSTANCE, Locale.FRANCE);
    //d MMMM yyyy HH' h 'mm z
    String ge = DateTimeFormatterBuilder.getLocalizedDateTimePattern(FormatStyle.LONG, FormatStyle.FULL, IsoChronology.INSTANCE, Locale.GERMAN);
    //d. MMMM yyyy HH:mm' Uhr 'z
    String ca = DateTimeFormatterBuilder.getLocalizedDateTimePattern(FormatStyle.LONG, FormatStyle.FULL, IsoChronology.INSTANCE, Locale.CANADA);
    //MMMM d, yyyy h:mm:ss 'o''clock' a z
    String en = DateTimeFormatterBuilder.getLocalizedDateTimePattern(FormatStyle.LONG, FormatStyle.FULL, IsoChronology.INSTANCE, Locale.ENGLISH);
    //MMMM d, yyyy h:mm:ss a z

In DateTimeFormatter you can specify individual units of the date with symbolic characters and the method ofPattern. The number of symbolic characters that you use per unit can also affect what gets displayed:

  • M will get you the month in digits.
  • MM will get you months as two digits, even if the month is less than 10.
  • MMM should get you the month name.

See the section "Patterns for Formatting and Parsing" on DateTimeFormatter documentation.

The pattern below gives you a four digit year, two digit month, and two digit day.

LocalDate localDate = LocalDate.now(); //For reference
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy MM dd");
String formattedString = localDate.format(formatter);

Upvotes: 1

Related Questions