Parsing dates using java.time on Android

Having fun using java.time and Retrofit for ISO 8601 dates on Android

I was assigned a task to parse ISO 8601 dates with timezone information from an endpoint. I decided to use Java 8’s java.time library. I had to write a custom converter to use it with Retrofit because the endpoint provided the date in a different format. This is a summary of my journey.

What is java.time, and how do I use it with Android?

java.time aims to improve the date and time handling in Java. It was added to Java in JSR-310. It was included in Android as of API 26. But if you want to use it with earlier versions, you must use a library.

There are a couple of solutions, each with their own drawbacks. I settled on a library by Jake Wharton called ThreeTenABP. It has been optimized for the Android platform. Jake goes into his reasoning on the project’s main page. https://github.com/JakeWharton/ThreeTenABP

To get this to work with Retrofit, I used a library called ThreeTen-Backport-Gson-Adapter. It’s a library that provides json serialization/deserialization for java.time using Gson. https://github.com/aaronhe42/ThreeTen-Backport-Gson-Adapter

Let’s run some tests!

There are lots of ways to document your code. One of my favorites is with tests. A good test can express how to do something, and why you are doing it. It also allows a future developer to play around with the code in a safe environment.

All my examples are tests. I’ve included the classes under test with the test itself. It makes it easier to show here.

Make sure you click to expand the test, otherwise you can’t see it!

Setup environment

You can set these up in Android Studio, but you can also use IntelliJ Community Edition.

I like having IntelliJ around to run test code. Create a new Project with Gradle, and include Java and Kotlin. Then use the build.gradle file to include your dependencies just like you would in Android Studio.

My dependencies are below. Keep in mind that these will be old versions by the time you read this.

Click to see code
dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
    testCompile group: 'junit', name: 'junit', version: '4.12'

    /* Java 8 java.time (JSR-310) backport for Android */
    implementation 'com.jakewharton.threetenabp:threetenabp:1.2.0'
    implementation("org.aaronhe:threetenbp-gson-adapter:1.0.2") {
        exclude module: 'threetenbp'
    }
    testImplementation 'org.threeten:threetenbp:1.3.8'
}

java.time tests

I wrote some tests to see how java.time behaves. Since it’s a newer library, I figured I could encourage my team to use them to learn java.time in a playground-like environment.

Click to see java.time tests
import junit.framework.Assert.assertEquals
import org.junit.Test
import org.threeten.bp.LocalDateTime
import org.threeten.bp.ZoneId
import org.threeten.bp.ZoneOffset
import org.threeten.bp.ZonedDateTime
import org.threeten.bp.format.DateTimeFormatter

class JavaTimeTests {

    @Test
    fun test_basicParsing() {
        // GIVEN a date at 12:25am UTC
        val apiDate = LocalDateTime.parse("2019-01-18T00:25:00.0000000")
        val zonedDate = ZonedDateTime.of(apiDate, ZoneId.of("UTC"))

        // WHEN I convert my date to PST
        val zoneID = ZoneId.of("PST", ZoneId.SHORT_IDS)
        val inMyTimezone = zonedDate.withZoneSameInstant(zoneID)

        val isoDateTime = inMyTimezone.format(DateTimeFormatter.ISO_DATE_TIME)

        // THEN I get the expected date and time in PST (a day earlier)
        val isoDate = inMyTimezone.format(DateTimeFormatter.ISO_DATE)
        assertEquals("2019-01-17-08:00", isoDate.toString())

        // WHEN I format my date in a different pattern (using slashes)
        val patternDate = inMyTimezone.format(DateTimeFormatter.ofPattern("yyyy/MM/dd"))
        // THEN I receive a date use the expected pattern
        assertEquals("2019/01/17", patternDate.toString())

        // WHEN I format my date in a different patter (using dashes)
        val localDate = inMyTimezone.toLocalDate()
        // THEN I receive a date use the expected pattern
        assertEquals("2019-01-17", localDate.toString())
    }

    @Test
    fun test_convertDatesToUTC() {
        // GIVEN a string date and string timezone
        val usZone = ZoneId.of("America/Los_Angeles")
        val str = "1926-09-23 00:00"

        // WHEN I parse the string date to a ZonedDateTime
        val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
        val localDateAndTime = LocalDateTime.parse(str, formatter)
        val dateAndTimeInUS = ZonedDateTime.of(localDateAndTime, usZone)

        // THEN I get the expected date in PST
        // 0 AM PST
        assertEquals(0, dateAndTimeInUS.hour)

        // THEN I get the expected date in UTC
        // 8 AM UTC
        val utcDate = dateAndTimeInUS.withZoneSameInstant(ZoneOffset.UTC)

        assertEquals(8, utcDate.hour)
    }

}

Parsing dates with gson

I’ve included the smallest amount of code to see how to use gson to format the custom date.

  • I don’t show how to use retrofit here, I just use a gson builder to incorporate the converter factory
  • The Converter parses a custom shaped json object, noted in the test
Click to see code

// for EndpointFactory
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import org.aaronhe.threetengson.ThreeTenGsonAdapter

// for EndpointDate
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonParseException
import org.junit.Assert.assertEquals
import org.junit.Test
import org.threeten.bp.LocalDateTime
import org.threeten.bp.Month
import org.threeten.bp.ZoneId
import org.threeten.bp.ZonedDateTime
import org.threeten.bp.format.DateTimeParseException
import java.lang.reflect.Type

/**
 * The data class for the date
 */
data class EndpointDate(
    val dateTime: ZonedDateTime,
    val timeZone: String
)

/**
 * Deserialize an EndpointDate for Gson.  This will load the proper ZonedDateTime, in the given timeZone
 */
class EndpointDateDeserializer : JsonDeserializer<EndpointDate> {
    override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): EndpointDate {
        json?.let {
            val jsonObject = it.asJsonObject

            try {
                val apiDate = LocalDateTime.parse(jsonObject.get("dateTime").asString)
                val zoneString = jsonObject.get("timeZone").asString
                val zone = ZoneId.of(zoneString)

                val zonedDate = ZonedDateTime.of(apiDate, zone)
                return EndpointDate(zonedDate, zoneString)
            } catch (e: DateTimeParseException) {
                throw JsonParseException(e)
            }
        }
        throw IllegalArgumentException("unknown type: $typeOfT")
    }
}


/**
 * I just supply the gson builder
 */
object EndpointFactory {

    fun getGson(): Gson {
        val gsonBuilder = GsonBuilder()
        gsonBuilder.registerTypeAdapter(EndpointDate::class.java, EndpointDateDeserializer())
        return ThreeTenGsonAdapter.registerZonedDateTime(gsonBuilder).create()
    }
}


class DateParseTests {

    @Test
    fun test_parseEndpointDate() {

        // GIVEN a gson object with a deserializer that can parse EndpointDate objects
        val gson = EndpointFactory.getGson()

        // GIVEN json of an EndpointDate
        val json = """
            {
                "dateTime": "1564-04-23T11:25:00.0000000",
                "timeZone": "UTC"
            }
        """.trimIndent()

        // WHEN the json is parsed into a EndpointDate data class
        val endpointDate = gson.fromJson(json, EndpointDate::class.java)

        // THEN I get the expected date with proper time zone
        assertEquals(1564, endpointDate.dateTime.year)
        assertEquals(Month.APRIL, endpointDate.dateTime.month)
        assertEquals(23, endpointDate.dateTime.dayOfMonth)

        assertEquals(11, endpointDate.dateTime.hour)
        assertEquals(25, endpointDate.dateTime.minute)
        assertEquals("UTC", endpointDate.dateTime.zone.id)
    }
}

Conclusion

This was an interesting problem to solve. We had some weird timezone issues that kept popping up. Our app was going to be heavily reliant on time. I knew we needed to stop writing short-term hacks and come up with something more concrete. This process took a little more time to put in place, but it paid off in the end by clearing the road ahead of us.

Bug Update – Let’s get it started

I’m learning about cars through my 1974 VW Beetle. This is my initial report.

Condition when purchased

When I bought the car, it did not start. The battery was dead, and when we jump-started the car it would not run for very long. According to the owner the engine was fairly new, but had been sitting for close to 2 years. I had test driven so many of these cars that I could at least tell that the body was pretty solid. It had some minor rust spots but nothing major. Everything the owner had said was plausible. So I made a deal to have it towed to a garage, and base the price on what the mechanic’s inspection.

The mechanic confirmed almost everything the owner had said, with a couple exceptions. So we negotiated a price and then I had them fix those issues.

Current Condition

The mechanic gave me a summary of the issues to get the car running. I had them fix the minimum amount to get the car home.

The initial assessment:

  • Bad battery (immediately replaced)
  • Distributor and Ignition Coil
  • Bad gas from sitting
  • The ignition switch is very temperamental

For each part I replace, I plan to research and explain what it does. So I am learning about the car as I replace parts.

Everything we want to talk about in one picture

The Ignition Coil

An Ignition coil transforms the battery’s low voltage to a high voltage. This is required to create a spark for the spark plugs to ignite fuel. At it’s simplest form it’s an iron bar with 2 separate coils of wire wrapped around either end:

  1. The first, or primary wire is only wrapped around the iron a few hundred times
  2. The second, or secondary wire is wrapped around many thousands of turns more

When an electric current (from the battery) is passed to the primary, a magnetic field is generated. The field stores the energy and it builds up. When the current in the primary is interrupted, it transfers its energy to the secondary coil. This causes a spark to jump across the air gap between them.

As stated earlier, this spark is what is needed to ignite fuel in the engine’s cylinders. You might remember that an engine has more than one cylinder. That’s where the distributor comes in.

Read more:

The Distributor

The Distributor transfers the high voltage from the coil to the correct cylinder. Since there is more than one cylinder in the car, it must “distribute” the voltage to the correct cylinder at the correct time.

This is done with a rotor system spinning inside the cap of the distributor. There is a contact inside for each cylinder. As the rotor spins past each contact, the electrical pulse is transferred to this contact. This transferred pulse is sent to the spark plug attached to the correct cylinder.

As you can imagine, the timing of this process is very critical. These parts will wear out because of the high voltage involved. Weird things start happening when the timing is off. When you get a tune-up, one of the things they will replace is the rotor and wires.

In modern cars they do not use distributors anymore to distribute the energy from the coil. Instead they use smaller coils – one for each spark plug or one coil for every 2 spark plugs.

I thought it was interesting that in cases where 1 coil serves 2 spark plugs, they use a technique called “wasted spark”. The coil generates 2 sparks per cycle to both cylinders. Yet, only one works to ignite fuel, the other is wasted since the cylinder isn’t ready.

Read more:

Bad gas

How does gas go bad? It has to do with the complicated recipe used to make it. I’m going to gloss over the details, but it’s a somewhat interesting process. I recently watched an episode of Modern Marvels about gasoline. I would recommend you do anything else than watch a TV show about gasoline. But if you’re bored you might find it interesting.

If you store gas for too long, the mixture tends to break down. Some hydrocarbons evaporate out of the gas. Heat, oxygen, and humidity also influence the mixture. After time it starts to form solids called gum, which can start blocking the fuel lines.

Read more:

Getting rid of bad gas

It may not be enough to drive off the bad gas – the fuel has left deposits all throughout your system. Water may be present from heat changes and humidity. Water will separate from gasoline, and it’s bad for engines. For example. it can freeze in supply lines as low temperatures.

You could have the old gas flushed out of your system, but it is an expensive process.

A cheaper method is to use a fuel additive, such as a product called Seafoam. It will clean the fuel system of fuel deposits. It seems to have a solid fan base of people who swear by it. There are other brands that do the same thing, and I’m not interested in promoting one product. The term “fuel additive” is a vague term, there are other additives that have different benefits. So I’ll use the terms “fuel additive” and “Seafoam” interchangeably.

How do fuel additives work?

It is not easy finding how an additive like Seafoam works. I found some people who try to make a home-brewed version. I have no interest in making my own additive, but it was helpful to understand how it works.

The mixture of the additives are 3 basic ingredients:

  1. Pale Oil – a kind of mineral oil that assists in lubrication
  2. Naphtha – a liquid hydrocarbon mixture that assists with cleaning. It has a variety of uses, such as kerosene or to dilute heavy crude oil when shipping. I believe it helps clear gum and varnish by diluting them, and by making the gas burn a little hotter
  3. IPA (or Isopropyl alcohol). It’s a drying agent. It causes water in gasoline to become soluble, and will be consumed with the fuel when burned.

How to remove bad gas

This is the process required for cleaning my fuel system:

  1. Replace your fuel filter
  2. Add some fuel additive, such as Seafoam, and fill the tank with a high-octane gasoline
  3. Run through the tank
  4. Add more Seafoam, re-fill the tank with high-octane gas, and replace the fuel filter
  5. Run through the tank again
  6. Re-fill the tank with normal gas, and replace the fuel filter

Essentially you run through two tanks of fresh gas, replacing the fuel filter in between each tank.

Read more:

Fuel filter

The function of a fuel filter is straight forward. It’s attached to your fuel line between the fuel tank and the engine. It keeps dirt and other large particles out of your engine.

The ignition switch

I’m living with it for now, but it is rather temperamental. While trying to start the car, I may need to wiggle the key, sit and wait, or recite poetry.

There is a chance that the real issue is that the car sat for so long. It may only need to be driven for a while and the ignition will work itself out fine. We will see.

VW Beetle

Picture of a 1974 Black Volkswagen Beetle

I bought a car that is older than I am – a 1974 Volkswagen Beetle.

I got it for a few reasons:

  • I always thought they looked cool
  • I thought it would be neat to own a car older than me
  • I wanted to learn about cars and how they work
  • I don’t drive much, I commute by bike most of the time, but sometimes I need a car

So I decided to sell my reliable car and switch to a not-so-reliable Beetle.

I got the car at a good price. It wasn’t my first choice for year or color. But the body had very little rust – something that is very hard for me to fix, and the price was right. This car was 1/4 the price of ones that have everything I desired. So I made some compromises.

Another benefit is that I can make changes to the car without hurting its value. The worst case scenario is that I am out a couple thousand dollars. The car’s shell is almost worth that amount.

There are a lot of articles, forums, videos, and TV shows that can help you research what to look for when buying a Beetle. If you are looking for some advice in that regard, this is it – you should trust those sources over me.

I plan to document the car here with a Bug category. I want to learn how to work on cars, and I want to explain what I learned here.

Stay tuned for an update. Spoiler alert! I had to have the car towed to a shop.

The first accessory I installed – a soccer ball antenna topper