Search code examples
kotlindate-formattingdate-comparisonthreetenbpdatetimeformatter

How to stabilize flaky DateTimeFormatter#ofLocalizedDateTime test?


Given the following ThreeTenBp based DateFormatter:

class DateFormatter private constructor() {

    private val dateShortTimeShortFormatter = 
        org.threeten.bp.format.DateTimeFormatter.ofLocalizedDateTime(
            FormatStyle.SHORT, FormatStyle.SHORT)

    fun getFormattedDateTimeShort(time: Long): String {
        return dateShortTimeShortFormatter.withZone(ZoneId.systemDefault())
                                          .format(Instant.ofEpochMilli(time))
    }

    /* ... */
}

I am running the following test on Ubuntu 20.04 in the GNOME Terminal shell (LANG=en_US.UTF-8):

class DateFormatterTest {

    private val systemTimezone = TimeZone.getDefault()
    private val systemLocale = Locale.getDefault()

    @Before
    fun resetTimeZone() {
        TimeZone.setDefault(TimeZone.getTimeZone("GMT+1"))
    }
    
    @After
    fun resetSystemDefaults() {
        Locale.setDefault(systemLocale)
        TimeZone.setDefault(systemTimezone)
    }


    @Test
    fun getFormattedDateTimeShort() {
        Locale.setDefault(Locale.US)
        assertThat(DateFormatter.newInstance().getFormattedDateTimeShort(1548115200000L))
                                              .isEqualTo("1/22/19 1:00 AM")    
    }

}

It succeeds.

When I run it in Android Studio 4.2 Beta 3 or Android Studio 2020.3.1 Canary 4 it fails with the following error:

org.junit.ComparisonFailure:
Expected :"1/22/19 1:00 AM"
Actual :"1/22/19, 1:00 AM"

Java news as of 16.01.2021

Based Ole V.V.'s comment and answer I figured out that the test behaves different in the shell depending on the Java version. Gradle picks up JAVA_HOME - therefore I need to update the environment variable. Please note that changing the symlink to the java executable via sudo update-alternatives --config java has no effect.

export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64

-> Test succeeds

export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64

-> Test fails

export JAVA_HOME=/usr/lib/jvm/java-14-openjdk-amd64

-> Process 'Gradle Test Executor 1' finished with non-zero exit value 134

Java/JRE 9 is not available for this Ubuntu version.

Solution: Java/JDK in Android Studio

In the IDE I can also change the JDK location for the project via File > Project Structure ... > SDK location:

JDK location

Once I change from the pre-configured JDK 11 (within the installation folder of the IDE) to JDK 8 - then the test succeeds!

Related


Solution

  • Locale data

    Date and time formats are in the locale data. So you have got different locale data in your Android Java and the Java on your Ubuntu. Java can get its locale data from different sources, and you can to some extent control which.

    I ran this on ThreeTen Backport and Java 9 (pardon my Java):

        Locale.setDefault(Locale.US);
        System.setProperty("user.timezone", "GMT+01:00");
        
        DateTimeFormatter dateShortTimeShortFormatter
                = org.threeten.bp.format.DateTimeFormatter.ofLocalizedDateTime(
                        FormatStyle.SHORT, FormatStyle.SHORT);
        
        String text = dateShortTimeShortFormatter.withZone(ZoneId.systemDefault())
                .format(Instant.ofEpochMilli(1548115200000L));
        
        System.out.println(text);
    

    Output was:

    1/22/19, 1:00 AM

    This has a comma between date and time and agrees with what you got in Android Studio.

    But I’m afraid that the success story ends here. I was thinking that if you would accept the comma, you could produce it in both environments, and your tests would pass. But I was unsuccessful trying to produce the same behaviour on Java 8 or lower.

    Desktop Java gets its locale data from up to four sources:

    There are four distinct sources for locale data, identified by the following keywords:

    • CLDR represents the locale data provided by the Unicode CLDR project.
    • HOST represents the current user's customization of the underlying operating system's settings. It works only with the user's default locale, and the customizable settings may vary depending on the operating system. However, primarily date, time, number, and currency formats are supported.
    • SPI represents the locale-sensitive services implemented by the installed Service Provider Interface (SPI) providers.
    • COMPAT (formerly called JRE) represents the locale data that is compatible with releases prior to JDK 9. JRE can still be used as the value, but COMPAT is preferred.

    The default in Java 9 is equivalent to CLDR,COMPAT,SPI (CLDR is for Common Locale Data Repository). So theoretically I should be able to get the same locale data in Java 8 by using this setting:

        System.setProperty("java.locale.providers", "CLDR,COMPAT,SPI");
    

    But no:

    1/22/19 1:00 AM

    There’s no comma here. It agrees with what you expected and seem to have got on your Ubuntu. The explanation that I know is that CLDR also comes in versions, so I gather that the CLDR version bundled with Java 8 differs from the one in Java 9.

    I am no Android developer. I don’t know from where Android gets its locale data or whether you can control that. You may want to go searching for options.

    Of course if you can upgrade Java on your Ubuntu to Java 9 or later, that does seem to give you behaviour consistent with Android Studio.

    Or you may consider renouncing on testing the exact format. Locale data are input to your program, not part of your program, and unit testing input doesn’t make sense in the end, it’s a contradiction in terms.

    Link

    CLDR Locale Data Enabled by Default from Internationalization Enhancements in JDK 9