1. The Date and Time API

In Java, an Instant represents a point on the time line. The origin (called epoch) is arbitrarily set at midnight of the first of January 1970 Greenwich time (UTC). Instant.now() returns the current point on the time line, being the number of seconds since the epoch (stored as long) and the number of nano seconds (stored as int). Each day has 24x60x60 = 86400 seconds. A Duration is the amount of time between two instants. Both Instant and Duration are immutable.

An Instant does not have a human readable format. The two kinds of human time in the java API are local date/time and zoned time. Local date/time has a date and/or time of day, but no associated time zone. Examples:

calendar example LocalDate Local Date example. alarmclock example LocalTime Local Time Example.

Do not use zoned time, unless really necessary. Today’s date can be retrieved using LocalDate.now(), an arbitrary date with LocalDate.of(<year>, <month>, <day>) like LocalDate.of(2021, 5, 1) being first of May 2021. The amount of time between dates is called a Period. Check the java API to read about convenient methods of LocalDate and Period. An interesting one is the datesUntil(…​) method on LocalDate, which returns a Stream<LocalDate>.

LocalTime objects can be created like LocalDates, e.g.: LocalTime endOfLecture = LocalTime.now().plusHours(1);

Zoned times make use of time zomes. Java uses the IANA database: https://www.iana.org/time-zones To make a zoned time out of a LocalDateTime you can use the ZonedDateTime.of() method or do it like this: ZonedDateTime current = LocalDateTime.now().atZone(ZoneId.of("Europe/Amsterdam"));. Now you get a specific Instant in time. The other way around, you can can find the corresponding ZonedDateTime in another time zone using instant.atZone(…​).

To represent dates and times in readable and customizable formats, use the DateTimeFormatter class. It can use predefined standard formatters, Locale specific formatters and formatters with custom patterns.

This paragraph is based on Core Java Volume 2, 11th edition, chapter 6 by Cay Horstmann.

2. Number Formats

Some of you might have noticed that one of the teacher tests in the FXTriangulate exercise of week 8 fails on your machine.

This is because on the machine we developed, the locale is set to en_US. This causes the numbers to be formatted in the way that Double.parseDouble(String input) expects it. If you run the same tests on a machine with say a German Locale, the test that reads the length back from the label, use Double.parseDouble(), which is then surprised to find a comma instead of the decimal point, and fails with a format exception.

To solve that, modify the test in method tLength such that the line
double l = Double.parseDouble( ltext.split( ":" )[ 1 ] ); reads
double l = getDoubleConsideringLocale( ltext.split( ":" )[ 1 ] );, so that the whole test method reads

fxtriangulate.GUITest.tLength
    @ParameterizedTest
    @CsvSource( {
        "a,greenCircle,blueCircle,500.0,400.0,100.0,100.0,500.0",
        "b,redCircle,blueCircle,400.0,100.0,100.0,100.0,500.0",
        "c,greenCircle,redCircle,300.0,100.0,100.0,400.0,100.0", } )
    public void tLength( String line, String p1, String p2, double expected,
            double x1, double y1, double x2, double y2 ) throws ParseException {
        double xOrg = stage.getX();
        double yOrg = stage.getY();
        FxRobot rob = new FxRobot();
        rob.drag( '#' + p1 ).dropTo( xOrg + x1, yOrg + y1 );
        rob.drag( '#' + p2 ).dropTo( xOrg + x2, yOrg + y2 );
        String ltext = labelMap.get( line ).apply( triangulator ).getText();
        double l = getDoubleConsideringLocale( ltext.split( ":" )[ 1 ] ); (1)
        assertThat( l ).isCloseTo( expected, within( 0.1 ) );
//        fail( "method tLength reached end. You know what to do." );
    }
1 changed line.
Consider the local in which the number is written.
    /**
     * Use the default locale to parse a double value from a string.
     * @param input string
     * @return the double
     * @throws ParseException if the string does not parse to double.
     */
    static double getDoubleConsideringLocale( String input ) throws ParseException {
        return DecimalFormat.getNumberInstance().parse( input ).doubleValue(); (1)
    }

    /**
    * Use the given locale to parse a double value from a string.
    * @param locale to use.
    * @param input string.
    * @return the double.
    * @throws ParseException if the string does not parse to double.
    */
   static double getDoubleConsideringLocale( Locale locale, String input ) throws ParseException {
       return DecimalFormat.getNumberInstance(locale).parse( input ).doubleValue();
   }
1 DecimalFormat.getNumberInstance gets a format that understands the default locale.
Set the locale for the execution. Useful for tests.
          Locale.setDefault( Locale.GERMANY ); (1)
1 Set the locale to GERMANY if it isn’t already. Similar for other languages.

3. Testing Localized Exceptions

The standard way of testing exceptions with assertj is explained in week01.

To get to the localized message, which contains the message as translated by the locale framework is a bit more involved.

Luckily, AssertJ allows you to extract information from a Throwable, by using an extractor function. Now the Lambda bells should ring.

To make a long story very short: here is an example:
    String[] keys = keyWords.split( "\\|");
    assertThatThrownBy( () -> {
                MainSimulation.main( args );
    } ).isExactlyInstanceOf( exceptionMap.get( expectionClassName ) )
            .extracting( e -> e.getLocalizedMessage() ) (1)
            .asString()           (2)
            .contains( keys ); (3)
1 extract using Function<? super Throwable,​T>, e → getLocalizedMessage() in this case.
2 Get the assertion for in String. Do not use toString(), because that produces a String, not an AbstractStringAssert.
3 And use the assert to check that the string contains the required key information.
assertjtypehints
Figure 1. If you turn on type hints in NetBeans-IDE (or in intelij) you can see what the type is on which you call contains(keys)

4. Additional Pointers

  • If you haven’t read the Horstmann book but you need an introduction into Internationalization, read this tutorial from DZONE here. Make sure to read the bit about Resource Bundles, as you use them in the exercise for this week.

  • Jakob Jenkov also has a tutorial on Java Internationalization

Internationalization (I18N) of a Pub

In the pub (Spanish beer)

In the pub Spanish

For this exercise, you are expanding on the pub exercise again. This time, enums and exceptions are all implemented and tested. The focus this week is on Internationalization! We want to make sure that the pub can run in different languages.

Open the project folder in Netbeans. Let’s first look at the resources of the project. You will notice a bunch of new files in the resources folder. The files contain the translations of error messages into Dutch, German and Spanish. For example, the inthepub_nl_NL.properties file contains

A good way of working is to create languages files for other that the default (typically English) to have the English text as comment and as a hint for the translator. Then the key value (the string at the left side of the equals sign) do not have to contain the full english texts, which might need escaping, such as when the key contains whitespace. In this exercise we banned the white space in the key altogether, and derive the keys from the default message with

    String msg = getMessage(); (1)
    String key = msg.toLowerCase().replaceAll( "\\s", "_" ); (2)
    String lMessage = bundle.containsKey( key ) ? bundle.getString( key ) : msg; (3)
1 get the original message.
2 translate to lower case and replace spaces with underscores, which yields the key.
3 If that key is found, use its message, otherwise use the default, non-internationalized version.
The dutch file
#hello=Hello
hello=Hallo
#good_morning=Good Morning
good_morning=Goede Morgen
#you_drank_too_much=You drank too much
you_drank_too_much=U hebt te veel gedronken
#beer_is_sold_out=beer is sold out
beer_is_sold_out=Het bier is uitverkocht
#you_are_too_young_to_drink=you are too young to drink
you_are_too_young_to_drink=U bent te jong voor drank

So when the pub raises an EmptyStockException and the Locale is Dutch, it should display the Dutch error message, not the English one. You don’t need to touch the language properties files, unless you want to add more languages (or perhaps our Spanish is too horrible!).

In this exercise you work on the files:

  1. LanguageTest.java, which is your main file for testing your implementation. Remember, work test-driven! What you should check is that the Exceptions are thrown with a message in the correct language.

  2. PubException.java, this is a new Exception that all other Exceptions inherit. It is the one responsible for translating the error messages. Your implementation goes here.

  3. HablasEspañol.java, which provides a main method for you to play around with the languages. Feel free to change this code to your liking to familiarize yourself with Internationalization, it is not tested.

  4. MainSimulationTest.java to complete the simulation tests. The business code is given. Push up the coverage.

If you want to run the program, you can use the run.sh file that we provided in the project’s repository. Simply execute it to test the pub simulation. Alternatively, simply type java -Duser.language=nl -Duser.country=NL -cp target/classes/ pub.ParlezEspañol in your command line interface. Of course, this will throw errors as long as you haven’t written your implementation!

Where should I start?

Start with the PubException.java class and implement the constructor so that the compilor errors will go away. Then, move to LanguageTest.java and work on the rest of the exercise, first writing the tests and then implementing in the PubException class.

Windows run command
@echo off
rem @author David Greven - https://github.com/grevend
if not exist target/classes call mvn clean compile
chcp 65001
cls
java -Duser.language=nl -Duser.country=NL -cp target/classes/ pub.HablasEspañol
java -Duser.language=es -Duser.country=ES -cp target/classes/ pub.HablasEspañol
pause

Exercises in this part