Reputation: 15155
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
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:
FormatStyle.LONG
for japanese locale gives me 2017/01/23 13:28:37 EST
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:
HH
, hh
, H
or h
and change to use 12 or 24 hour format (I replaced keeping the same number of letters)z
(or Z
): to remove or add a timezoneh
after the hours (like HH'h'
which becomes 13h
), so I had to take care of this when replacingThe 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:
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).String
, you can do anything with it-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.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.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
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