2

We have an application where we accept the datetime format from the customer.

We recently started migrating to Java 8 new date time API's because they are thread safe. Look at the below code you can see before preparing the formatter we are setting A and P to be considered as AM & PM Strings. We get the formatter from customer & we simply apply the below logic to display the date time.

String[] AMPM = new String[2];
AMPM[0] = "A";
AMPM[1] = "P";
DateFormatSymbols dfs = new DateFormatSymbols(locale);
dfs.setAmPmStrings(AMPM);
sdf = new SimpleDateFormat(format, dfs);  

But with new Java 8 date time API I nowhere see how to define this. But after some googling I can see we have to use DateTimeFormatterBuilder but this is only useful if I am building the pattern myself; here we are receiving the full date time pattern from customer.

Any pointers on how to do this using Java 8.

Dungeon Hunter
  • 19,827
  • 13
  • 59
  • 82
  • This has been asked before. I don’t remember the exact title or wording, but go search. – Ole V.V. Jun 07 '17 at 12:02
  • The pattern you're receiving, it's always the same? Does it always have the AM/PM field? –  Jun 07 '17 at 12:10
  • [This answer](https://stackoverflow.com/a/24004917/5772882) may help. The question is about parsing, but I think the solution would work for formatting too. – Ole V.V. Jun 07 '17 at 12:11
  • @Hugo the patterns might be differing they wont be same everytime – Dungeon Hunter Jun 07 '17 at 12:12
  • But how much it varies? Can you add some examples? I suppose there's a limited list of different patterns. –  Jun 07 '17 at 12:18
  • @hugo it can be any date time pattern. to give you more background we have scripting support in our product, there if customer uses any date time formatting functions underlying this java code gets executed. – Dungeon Hunter Jun 07 '17 at 12:25
  • Well, I've added some ideas to my answer below. –  Jun 07 '17 at 13:09

1 Answers1

4

You can use DateTimeFormatterBuilder.appendText() method that takes a map with custom values.

According to javadoc, the field AMPM_OF_DAY can have the value 0 for AM and 1 for PM, so you just create a map with these values and pass it to the formatter:

import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;
import java.time.LocalTime;

// create map with custom values (0=AM, 1=PM, mapping to values "A" and "P")
Map<Long, String> map = new HashMap<>();
map.put(0L, "A"); // AM mapped to "A"
map.put(1L, "P"); // PM mapped to "P"

DateTimeFormatter formatter = new DateTimeFormatterBuilder()
    // hour/minute/second (change to whatever pattern you need)
    .appendPattern("hh:mm:ss ")
    // use the mapped values
    .appendText(ChronoField.AMPM_OF_DAY, map)
    // create formatter
    .toFormatter();

// testing
System.out.println(formatter.format(LocalTime.of(10, 30, 45))); // 10:30:45 A
System.out.println(formatter.format(LocalTime.of(22, 30, 45))); // 10:30:45 P

The output will be:

10:30:45 A
10:30:45 P

And it also works to parse a String:

System.out.println(LocalTime.parse("10:20:30 A", formatter)); // 10:20:30
System.out.println(LocalTime.parse("10:20:30 P", formatter)); // 22:20:30

The output will be:

10:20:30
22:20:30


If you don't control the pattern, you can look for a letter a (the pattern corresponding to the AM/PM field) and replace it by the custom version.

One alternative is to split the format String, using a as the delimiter, then you don't lose the rest of the pattern:

// format with day/month/year hour:minute:second am/pm timezone
String format = "dd/MM/yyyy hh:mm:ss a z";

// split the format ("a" is the AM/PM field)
String[] formats = format.split("a");

DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder()
    // first part of the pattern (everything before "a")
    .appendPattern(formats[0])
    // use the AM/PM mapped values
    .appendText(ChronoField.AMPM_OF_DAY, map);

// if there's something after the "a", add it
if (formats.length > 1) {
    builder.appendPattern(formats[1]);
}

// create formatter
DateTimeFormatter formatter = builder.toFormatter();

//test
ZonedDateTime z = ZonedDateTime.of(2017, 5, 1, 10, 20, 30, 0, ZoneId.of("America/Sao_Paulo"));
// AM replaced by "A"
System.out.println(formatter.format(z)); // 01/05/2017 10:20:30 A BRT

This algorithm also works if the a is in the beginning or end, or even if there's no a (in this case, I'm adding in the end, I'm not sure if it must be added if not present).

// "a" in the end
String format = "hh:mm:ss a";
DateTimeFormatter formatter = // use same code above
System.out.println(formatter.format(LocalTime.of(15, 30))); // 03:30:00 P

// "a" in the beginning
String format = "a hh:mm:ss";
DateTimeFormatter formatter = // use same code above
System.out.println(formatter.format(LocalTime.of(15, 30))); // P 03:30:00

// no "a" (field is added in the end)
String format = "HH:mm:ss";
DateTimeFormatter formatter = // use same code above
System.out.println(formatter.format(LocalTime.of(15, 30))); // 03:30:00P

You can make some improvements, like if the format doesn't contain a, then don't add it (or add with a space before it), or whatever you want.


PS: this code doesn't handle literals (text inside ') like "dd/MM/yyyy 'at' hh:mm:ss a" (the "at" is a literal and shouldn't be removed/replaced).

I'm not a regex expert (so I'm not 100% sure if it works for all cases), but you can do something like this:

String[] formats = format.split("(?<!\'[^\']{0,20})a|a(?![^\']{0,20}\')");

This regex will ignore a inside quoted text. I have to use {0,20} quantifier because lookahead and lookbehinds (the (? patterns) don't accept *, so this will work if the quoted text before or after the "a" has at most 20 characters (just change this value to what you think it fits best your use cases).

Some tests:

String format = "\'Time: at\' hh:mm:ss a";
DateTimeFormatter formatter = // use same code above, but with the modified regex split
System.out.println(formatter.format(LocalTime.of(15, 30))); // Time: at 03:30:00 P

String format = "dd/MM/yyyy \'at\' hh:mm:ss a z";
DateTimeFormatter formatter = // use same code above, but with the modified regex split
ZonedDateTime z = ZonedDateTime.of(2017, 5, 1, 10, 20, 30, 0, ZoneId.of("America/Sao_Paulo"));
System.out.println(formatter.format(z)); // 01/05/2017 at 10:20:30 A BRT