1. Locale, Date and Time

In almost all exercises we have screen outputs, and whoever writes in everyday life, for example Chinese, Spanish or Arabic, will probably prefer program outputs in this language as well. However, in some places a wrong format might appear, e.g. when decimal places in floating point numbers are not separated correctly. The decimal separator is only one of many examples of how different the standards are in the countries: Currencies sometimes precede and then follow the number; for dates, the format is year-month-day in some countries, day-month-year in others, and then month-day-year again.

This chapter focuses on exercises around internationalization (how to make programs language-independent in principle) and localization (adaptation to a specific language). After all, if our software is to be successful, it must of course run anywhere on the planet at any time of day. Java can easily accommodate many language specifics, and we’ll look at that in the exercises so that Captain CiaoCiao and Bonny Brain can also do their business anywhere and everyone understands their "language".

Prerequisites

  • know Locale class and constants, like Locale.US

  • be able to recognize the need for country-specific formatting

  • know temporal data types LocalDate, LocalDateTime, `Duration

Data types used in this chapter:

1.1. Languages and countries

In order for the Java library to parse and format floating point numbers and dates, as well as translate text, there is a datatype called Locale that represents a language with an optional region. We want to use this datatype to solve some exercises that show well where Locale occurs everywhere.

1.1.1. Apply country/language specific formatting for random number. ⭐

Bonny Brain is preparing a new email scam: Bitcoins are to be "sold" well below their price. It prepares subject lines for this, which look like this, for example

Buy 𝑩𝒊𝒕𝒄𝒐𝒊𝒏 for just $11,937.70 💰

Of course, the crew is planning a worldwide scam, and that’s where it’s important to format the number according to the rules of the different countries.

The printf(…​) method is overloaded, as is String.format(…​):

  • With Locale as first parameter.

  • Without Locale. If no Locale object is passed, the default locale applies. This leads to the fact that the Java Virtual Machine under lets say an operating system language Spanish, this language adopts and with the output with System.out.printf(…​) and a floating point number by default a comma as decimal separator is used. If you have an English-language operating system, the point is used as a separator by default, because decimal places are separated with a point in English-speaking countries.

Exercise:

  • Create a random number of type double between 10 000 (inclusive) and 12 000 (exclusive); decimal places are desired.

  • Use the method String.format(String format, Object... args) to format a floating point number with two decimal places. There should be a thousands separator.

  • Get all Locale objects of the system, and use them as arguments to the String.format(Locale l, String format, Object... args) method, so that the floating point number is formatted "locally" in each case. Output the string.

As a general rule, it can be stated that all methods implementing language-dependent formatting, or parsing strings, usually accept a Locale object as a parameter. There may be language-dependent methods without parameters in addition, but these are often overloaded methods that internally query the default language, and then pass to the method with the explicit Locale parameter.

1.2. Date and time classes

At first glance, it looks like the date consists only of year, month, and day. However, you would expect an API to be able to answer more questions: Is a day a Wednesday? If a party goes for three days on February 27, when does it end? When does week 12 start? If I leave Beijing at 09:30 on December 31, 2021 and arrive in Miami after 11 hours, what time is it when I get there?

The Java library has evolved over the years, and so there are various types for date and time calculation:

  • java.util.Date since Java 1.0

  • java.util.Calendar, java.util.GregorianCalendar, java.util.TimeZone since Java 1.1

  • package java.time since Java 8 with classes like LocalDate, LocalTime, LocalDateTime, Duration.

For a date with time part there are three possibilities at once; however Date and Calendar are no longer popular since Java 8, because they are causing a number of problems. However, these data types can still be found in many written examples, especially online. We should stay away from these "old" types, and therefore this section specifically trains how to use the current data types from java.time.

1.2.1. Formatting date output in different languages ⭐

September 19 marks the return of International Talk Like a Pirate Day. Bonny Brain is planning a party and preparing invitations, and the date is to be formatted for the languages Locale.CHINESE, Locale.ITALIAN and new Locale("th"); Germans, for example, write Day.Month.Year, but what about in the other languages?

Exercise:

  • Create a LocalDate object for September 19:

    LocalDate now = LocalDate.of( Year.now().getValue(), Month.SEPTEMBER, 19 );
  • Call the toString() method; what is the output?

  • Call format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)) on the LocalDate; what is the output?

  • There are four FormatStyle styles in total — try them all. Which pattern is shown?

  • On the DateTimeFormatter object you can call withLocale(Locale) and change the language; try this for different languages.

1.2.2. On which day does Sir Francis Beaufort celebrate his birthday? ⭐

Captain CiaoCiao celebrates the birthday of Sir Francis Beaufort every year, who was born on May 27, 1774.

Exercise:

  • Given a LocalDate with Francis` birthday:

    LocalDate beaufortBday = LocalDate.of( 1774, Month.MAY, 27 );
  • Starting from beaufortBday, develop a new LocalDate object with the current year, where the current year should not be hardcoded but dynamically obtained from the system.

  • Create an output on which day of the week Francis celebrates his birthday this year. In which form the weekday is output, i.e. number or string or which language, does not matter.

Example:

  • For the year 2020 the output could be:

    WEDNESDAY
    3
    Wednesday

1.2.3. Get average duration of karaoke nights ⭐

Karaoke nights with rum, dancing and singing are popular among the crew. Often the parties go on until dawn, and this disturbs Bonny Brain because the crew is dozy the next day.

To find out how many hours the excesses go on average, Bonny Brain wants to keep a record of statistics. She writes down the start and end times and can calculate the average later. For example, the note sheets say "2022-03-12, 20:20 - 2022-03-12, 23:50".

Exercise:

  • Write a program that evaluates a string in the format above, finds the average party duration, and prints it out.

  • The program does not need to take into account time zones, leap seconds, or other special cases — a day can be exactly 24 hours long.

Example:

  • For the string

    2022-03-12, 20:20 - 2022-03-12, 23:50
    2022-04-01, 21:30 - 2022-04-02, 01:20

    the output should look like this:

    3 h 40 m

For time differences the class Duration helps.

1.2.4. Parse different date formats ⭐⭐⭐

A date can be specified as absolute or relative, and there are several ways to specify dates. A few examples:

2020-10-10
2020-12-2
1/3/1976
1/3/20
tomorrow
today
yesterday
1 day ago
2234 days ago

Exercise:

  • Write a method Optional<LocalDate> parseDate(String string) that recognizes the above formats.

  • If the string is in one of the formats, the method should parse the string, convert it to a LocalDate and return it in Optional.

  • If no format could be parsed, the return is Optional.empty().

1.3. Suggested solutions

1.3.1. Apply country/language specific formatting for random number.

com/tutego/exercise/util/RandomInEveryLocalePrinter.java
double random = ThreadLocalRandom.current().nextDouble( 10_000, 12_000 );
Locale[] locales = Locale.getAvailableLocales();
for ( Locale locale : locales )
  System.out.printf( locale, "%,.2f (%s)%n", random, locale.getDisplayName() );

The solution consists of four steps: In the first part, we generate a random number. In the second part we query all registered Locale objects as an array containing all supported languages of the Java library. We can traverse this array in the third step and output it in the fourth step.

System.out.printf(…​) expects a formatting string that puts the random number as a floating point number, just like the language name. The part in the formatting string for the number is %,.2f — the comma indicates the desire for a thousands separator, .2 indicates the two decimal places.

The important parameter in printf(…​) is the first one, which says that a Locale instance can be passed, which determines the formatting of the floating point number in the following.

1.3.2. Formatting date output in different languages

com/tutego/exercise/time/DateTimeFormatterDemo.java
LocalDate date = LocalDate.of( Year.now().getValue(), Month.SEPTEMBER, 19 );
System.out.println( date );

DateTimeFormatter formatterShort =
    DateTimeFormatter.ofLocalizedDate( FormatStyle.SHORT );
DateTimeFormatter formatterMedium =
    DateTimeFormatter.ofLocalizedDate( FormatStyle.MEDIUM );
DateTimeFormatter formatterLong =
    DateTimeFormatter.ofLocalizedDate( FormatStyle.LONG );
DateTimeFormatter formatterFull =
    DateTimeFormatter.ofLocalizedDate( FormatStyle.FULL );

DateTimeFormatter[] dateTimeFormatter = {
    formatterShort,
    formatterMedium,
    formatterLong,
    formatterFull,
    formatterShort.withLocale( Locale.CANADA_FRENCH ),
    formatterMedium.withLocale( Locale.CHINESE ),
    formatterLong.withLocale( Locale.ITALIAN ),
    formatterFull.withLocale( new Locale( "th" ) )
};

for ( DateTimeFormatter formatter : dateTimeFormatter )
  System.out.println( date.format( formatter ) );

The output of the program for the year 2021 is:

021-09-19
19.09.21
19.09.2021
19. September 2021
Sonntag, 19. September 2021
2021-09-19
2021年9月19日
19 settembre 2021
วันอาทิตย์ที่ 19 กันยายน ค.ศ. 2021

The temporal data types override the toString(…​) method, but it cannot be parameterized with a formatting type. Instead, LocalDate has the format(…​) method, which can be passed a DateTimeFormatter. There are three common ways to get a DateTimeFormatter:

  • You select a constant, like DateTimeFormatter.ISO_LOCAL_DATE.

  • You specify via a pattern precisely where e.g. the day or the month is placed and which separator symbols are used.

  • You choose a language-independent standard formatting, which is very practical for screen outputs. It returns DateTimeFormatter.ofLocalizedDate(…​).

The ofLocalizedDate(…​) method expects a parameter of type FormatStyle and the proposed solution builds four of these DateTimeFormatter instances and then calls the format(…​) method with just these formats.

The method name ofLocalizedDate(…​) already contains a hint about the localization. With withLocale(…​) any DateTimeFormatter can be associated with a Locale. All temporal data types are immutable, the return of the method is a new object with Locale set.

1.3.3. On which day does Sir Francis Beaufort celebrate his birthday?

In the solution, we proceed in two steps:

  1. Starting from beaufortBday we have to build a LocalDate with May 27th of this year and

  2. the day of the week must be retrieved.

To the first part:

com/tutego/exercise/time/SirFrancisBeaufortBirthday.java
LocalDate beaufortBday = LocalDate.of( 1774, Month.MAY, 27 );

// 1.
LocalDate beaufortBdayThisYear = beaufortBday.withYear( Year.now().getValue() );

// 2.
LocalDate beaufortBdayThisYear2 = LocalDate.of( LocalDate.now().getYear(),
                                                beaufortBday.getMonth(),
                                                beaufortBday.getDayOfMonth() );

// 3.
LocalDate beaufortBdayThisYear3 = LocalDate.now()
                                           .withMonth( beaufortBday.getMonthValue() )
                                           .withDayOfMonth( beaufortBday.getDayOfMonth() );

For the construction of a LocalDate object the proposed solution shows three variants:

  1. In the first variant we use a wither, i.e. a method with the prefix with instead of set, which returns a new object with a changed value. Java’s temporal data types are immutable, so there are no setters. withYear(int) returns a new LocalDate object with a changed year. For the current year we can use the Year datatype. The static method now() returns the current year, and since withYear(int) needs an integer, getValue() is needed on the Year object.

  2. Instead of using a wither, we can use the static factory method of(…​) to compose a new LocalDate object by taking the current year and retrieving the month and day from beaufortBday.

  3. In the third variant, we query LocalDate.now() for the current date with day, month, year, but get a new LocalDate object with the month set first and then a new LocalDate object with the day of the month using the two wither methods. This variant is not optimal, because two temporary LocalDate objects are created, which then end up in the automatic garbage collection again.

To the second part:

com/tutego/exercise/time/SirFrancisBeaufortBirthday.java
DayOfWeek dayOfWeek = beaufortBdayThisYear.getDayOfWeek();
System.out.println( dayOfWeek );
System.out.println( dayOfWeek.getValue() );
System.out.println( dayOfWeek.getDisplayName( TextStyle.FULL, Locale.GERMANY ) );

DateTimeFormatter formatter =
    DateTimeFormatter.ofPattern( "EEEE" /*, Locale.GERMANY */ );
System.out.println( beaufortBdayThisYear.format( formatter ) );

The proposed solution shows different variants of how to output the day of the week.

  1. A LocalDate object provides various getters. getDayOfWeek() returns the enumeration DayOfWeek, and we get the day in the week. DayOfWeek is an enumeration type, and since all enumerations have a toString() method, the output is, for example, WEDNESDAY. If a numeric value is desired, getValue() from DayOfWeek will return a number such as 3 for Wednesday.

  2. DayOfWeek provides the convenient method getDisplayName(TextStyle style, Locale locale); it returns a sting formatted in the local language for the day of the week.

  3. Another variant does not work via DayOfWeek, but uses the format(…​) method provided by LocalDate. This must be passed a DateTimeFormatter, and if we use the pattern EEEE, then that stands for the day of the week. A language for the day of the week can be specified optionally, if it is missing, the default language of the operating system is taken as default.

1.3.4. Get average duration of karaoke nights

com/tutego/exercise/time/AverageDuration.java
String input = "2022-03-12, 20:20 - 2022-03-12, 23:50\n" +
               "2022-04-01, 21:30 - 2022-04-02, 01:20";

DateTimeFormatter formatter =
    DateTimeFormatter.ofPattern( "yyyy-MM-dd, HH:mm" );
Scanner scanner = new Scanner( input ).useDelimiter( " - |\\n" );
Duration totalDuration = Duration.ZERO;

int lines;
for ( lines = 0; scanner.hasNext(); lines++ ) {
  String start = scanner.next();
  String end   = scanner.next();  // potential NoSuchElementException

  // potential DateTimeParseException
  LocalDateTime startDateTime = LocalDateTime.parse( start, formatter );
  LocalDateTime endDateTime   = LocalDateTime.parse( end, formatter );

  Duration duration = Duration.between( startDateTime, endDateTime );
  totalDuration = totalDuration.plus( duration );
}

Duration averageDuration = totalDuration.dividedBy( lines );
System.out.printf( "%d h %02d m", averageDuration.toHours(),
                                  averageDuration.toMinutesPart() );

Several steps are necessary to solve the task. First of all, the program has to extract all start and end values from the large string. Then the strings have to be transferred to the corresponding temporal data types. Then, the differences of these start and end times must be calculated and summed. Finally, we divide the sum by the number of entries and the task is solved.

The detection of the start and end time points can be done by the Scanner. The Scanner is initialized with white space as separator by default. We change this and set a minus sign and a newline as separators. Repeated calls to the next() method will return the start and end values in succession.

Once we have extracted the string, we pass it to the parse(…​) method of the LocalDateTime class. Since our string does not follow an ISO standard, we must pass a DateTimeFormatter to parse(…​). We built this before with the method ofPattern(…​), where the passed string corresponds exactly to the pattern we used to format the date with the time information. The symbol strings YYYY, MM, dd etc. can be taken from the Javadoc.

With two LocalDateTime objects we can determine the difference — we do not do this by hand, but resort to the Duration class. There are two classes in the Java Date-Time API that are used for the difference of temporal data types: The Duration class stores the intervals of two time values in fractions of seconds, while Period is used for date values and works on the basis of days. Period can be used well if, for example, leap days are to be taken into account in the difference; with Duration a day is exactly 24 hours long, which corresponds to 86 400 seconds. For our calculation, Duration is just right.

Conveniently, a Duration object can be built directly with the between(…​) method, and you pass the start and end values. Like all temporal data types, Duration is immutable. To add the differences, we access the plus(…​) method and store the result back in the totalDuration variable. Finally, we count up one more variable for the total number of date-time pairs, and it goes back to the loop test to see if any more dates follow.

Two runtime errors can occur in the program: Once, because there is an odd number of date-times in String, and of course, because the formatting of the date-time specification is wrong. We do not catch these exceptions, they cause the program to abort.

If all values are correct, the program ends with the calculation of the average duration. The method dividedBy(…​) helps to divide the total summed duration by the number of occurring date-time pairs. The resulting object of type Duration is then asked for the number of hours and minutes and the whole result is formatted.

1.3.5. Parse different date formats

The challenge with the task is that there is not one format given in which each date is formatted, but rather we have different formats. For a certain number of different formats, the Java library actually provides support, with the DateTimeFormatterBuilder. With the method appendPattern(…​) a pattern can be specified to determine a format. You put the format in square brackets to express that it is optional. Example:

DateTimeFormatter formatter = new DateTimeFormatterBuilder()
    .appendPattern( "[d/M/yyy]" )
    .appendPattern( "[yyyy-MM-dd]" )
    .parseDefaulting( ChronoField.MONTH_OF_YEAR, YearMonth.now().getMonthValue() )
    .parseDefaulting( ChronoField.DAY_OF_MONTH, LocalDate.now().getDayOfMonth() )
    .toFormatter();

In our case, such a configured DateTimeFormatter cannot be used, because relative information like "yesterday", "today" or "tomorrow" cannot be expressed. We could in principle realize a part via such a configured DateTimeFormatter, but the proposed solution proceeds differently.

com/tutego/exercise/time/ParseDatePattern.java
public static Optional<LocalDate> parseDate( String string ) {
  LocalDate now = LocalDate.now();

  Collection<Function<String, LocalDate>> parsers = Arrays.asList(
      input -> LocalDate.parse( input, DateTimeFormatter.ofPattern( "yyyy-M-d" ) ),
      input -> LocalDate.parse( input, DateTimeFormatter.ofPattern( "d/M/yyyy" ) ),
      input -> LocalDate.parse( input, DateTimeFormatter.ofPattern( "d/M/yy" ) ),
      input -> input.equalsIgnoreCase( "yesterday" ) ? now.minusDays( 1 ) : null,
      input -> input.equalsIgnoreCase( "today" ) ? now : null,
      input -> input.equalsIgnoreCase( "tomorrow" ) ? now.plusDays( 1 ) : null,
      input -> new Scanner( input ).findAll( "(\\d+) days? ago" )
                                   .map( matchResult -> matchResult.group( 1 ) )
                                   .mapToInt( Integer::parseInt )
                                   .mapToObj( now::minusDays )
                                   .findFirst().orElse( null ) );

  for ( Function<String, LocalDate> parser : parsers ) {
    try { return Optional.of( parser.apply( string ) ); }
    catch ( Exception e ) { /* Ignore */ }
  }
  return Optional.empty();
}

Ultimately, our task is to transform a String into a LocalDate. But this is nothing else than a mapping, which we can express in Java by the datatype Function. We can write a Function that tries to recognize a certain format. In the best case the mapping works and we can map a String to a LocalDate, in the worst case there is an Exception, or we program this Function to return null. The reason for this generalization, which may seem unusual at first glance, is that it allows us to collect all function objects later and try them out one by one. In programming, we should always consider whether we can generalize things — from the specific to the general.

We can identify three different types of mappings:

  • The first three mappings try to recognize a pattern directly, and make use of the DateTimeFormatter.

  • The second type of functions detects whether today, yesterday or tomorrow was in question. We fall back on the local variable now, which is initialized when the method is called. For testing this is inconvenient, so in practice one would write an internal method for test cases where one can introduce a LocalDate as a reference point.

  • The third Function is the most complex, because it has to detect a relative reference with a given number of days. Several methods work together here. The Scanner can recognize a string and can simply return the find for the number of days. The result is in a MatchResult: we extract the specification of the days, convert it to an integer, get a new LocalDate object where this number of days has been reduced, and return the result. If the pattern did not match, we get the return null.

For each possible format there is now a Function, and we add new mappings if more formats are to be recognized, this is busywork. The actual recognition works by traversing all mappings; the functions are applied to the string in order until a function returns a valid result. To do this, the functions are first added to a list.If the application failed and there is an exception, the catch block catches the exception and selects the next function from the list. The relative references all answer null, which through the Optional.of(…​) leads to a NullPointerException, which is caught so that the next function can be used.

In the end, there are only two outputs: either a function responded with a valid LocalDate, and we get out of the try block with return, or there were only exceptions, and the for loop could not detect a candidate — in which case the method is exited with an Optional.empty().