Lesson Wednesday

In this lesson, we'll walk through integrating time-based values in the Java side of our application.

However, before we can learn about time in any programming language, let alone Java, we need to understand the differences in the way humans and computers interpret time.

Machine Time

As you already know, humans calculate time in years, months, days, hours and seconds. We say things like "last Tuesday", or "May 5th, 1990" to communicate time to one another. Machines, however, don't organize the concept of time into years, months, or days unless we explicitly instruct them to. By default, they measure time on one, continuous timeline. Machine times are calculated in the amount of time ( in seconds) since the epoch. The term epoch simply means a reference point in history.

Epoch dates differ between programming languages. We can take a peek at this guide from Wikipedia and see that Java's specific epoch date is January 1, 1970. So, Java actually measures time in the number of seconds since January 1, 1970. Pretty crazy, right?

We can see what these 'machine' timestamps look like by importing Java's Instant class into the REPL, and calling the following methods:

> import java.time.Instant;
Imported java.time.Instant

> Instant.now().getEpochSecond();
java.lang.Long res5 = 1474852514

Date and Time in Java

But Java actually includes many, many classes responsible for creating and interacting with dates and time. In fact, they have an entire tutorial dedicated solely to the different types of date and time formats. (You're not required to go through this tutorial; but it's a great resource if you would like a more advanced exploration into time classes in Java). But even that immersive tutorial doesn't cover them all!

As you could imagine, with all these different ways to represent and interact with time, it could be difficult to manage. Especially making sure your time objects are in a format both Java and SQL can handle. There are many different approaches to this. We can work with Java's Timestamp class, or with the Date class, or even the Calendar class.

But because we are going to be storing dates and times in our database, we not only need to consider how Java handles time, we also need to work with something SQL can store correctly. If you google "storing dates and times in postgres", you'll see that there are many approaches to storing time. But one of the simplest ones is not storing time as time, but as milliseconds. This makes it easier to time as a datatype long in our objects, and use the column type BIGINT to store our long values in the database.

Let's start by adding a property to our Review class, as this is where time is most useful.

Extending our Model

Let's add a long field to our model and update our constructor.

src/main/java/models/Review.java
public class Review  {

    private int id;
    private String writtenBy;
    private int rating;
    private String content;
    private int restaurantId;
    private long createdat;
    private String formattedCreatedAt;

    public Review(String writtenBy, int rating, String content, int restaurantId) {
        this.writtenBy = writtenBy;
        this.rating = rating;
        this.content = content;
        this.restaurantId = restaurantId;
        this.createdat = System.currentTimeMillis();
        setFormattedCreatedAt(); //we'll make me in a minute
    }
}

Let's generate getters and setters for this too. They need to look like this.

public long getCreatedat() {
    return createdat;
}

public void setCreatedat() {
    this.createdat = System.currentTimeMillis(); // It'll become clear soon why we need this explicit setter
}

public String getFormattedCreatedAt(){
    String someString;
    return someString; //more on this in a sec
}

public void setFormattedCreatedAt(){
    this.formattedCreatedAt = "some time"
}

Updating our Database

If we extend our model, we also need to update our database schema, as well as deleting our production database and forcing it to be rebuilt. Let's do that now.

Our database structure should look like this:

src/main/resources/db/create.sql
CREATE TABLE IF NOT EXISTS reviews (
 id int PRIMARY KEY auto_increment,
 writtenby VARCHAR,
 rating VARCHAR,
 content VARCHAR,
 restaurantid INTEGER,
 createdat BIGINT
);

The BIGINT column type is perfect for storing longs, as they are frequently longer (hah) than Integer types.

Let's delete our production database before we forget. Our testing database is created on the fly every time, and therefore doesn't need to be deleted for any schema changes to be included.

In a terminal prompt, run: cd ~ ls -a rm *.db

This sequence will delete all database files. If you don't want that, delete a specific one instead by amending the above command with the correct db name

Testing our our time

Time to write a test to verify that we are storing the time correctly.

Write a test that looks like this in your Sql2oReviewDaoTest:

src/test/java/dao/Sql2oReviewDaoTest.java
@Test
public void timeStampIsReturnedCorrectly() throws Exception {
    Restaurant testRestaurant = setupRestaurant();
    restaurantDao.add(testRestaurant);
    Review testReview = new Review("Captain Kirk", 3, "foodcoma!", testRestaurant.getId());
    reviewDao.add(testReview);

    long creationTime = testReview.getCreatedat();
    long savedTime = reviewDao.getAll().get(0).getCreatedat();
    assertEquals(creationTime, reviewDao.getAll().get(0).getCreatedat());
}

If all goes well, this test should pass! Our times should be the same. Cool.

Improving our display

But here we can already see a potential dilemma that I have hinted at above - our creation time in Milliseconds is being retrieved correctly, but it isn't displaying very well. Who cares about time in milliseconds (not humans). We need it to be more readable. It would be great if we could return the time in ms and also a nicely formatted code to be used.

Converting millisecond time into humanly readable time is a very common practice. We'll employ a very handy DateFormatter to help us with this.

Understanding SimpleDateFormatter

SimpleDateFormatter does what the name says - it allows us to convert times into different formats fairly easily. It can transform dates into strings, and vice versa. It uses a specific pattern to understand just how you want time to display. This pattern is highly customizable and used very widely.

Let's learn more about SimpleDateFormatter by turning milliseconds into the following format: MM/DD/YYYY @ H:MM AM/PM or '06/23/1980 @ 1:35 AM`

Implementing SimpleDateFormatter

Let's use SimpleDateFormatter by creating a second getter method for our createdat field that returns the time in a formatted way. Its a good idea to keep the method that returns milliseconds as well - we'll need both.

This should do the trick:

src/main/java/models/Review.java
[...]
public String getFormattedCreatedAt(){
    Date date = new Date(createdat);
    String datePatternToUse = "MM/dd/yyyy @ K:mm a"; //see https://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html
    SimpleDateFormat sdf = new SimpleDateFormat(datePatternToUse);
    return sdf.format(date);
}

public void setFormattedCreatedAt(){
    Date date = new Date(this.createdat);
    String datePatternToUse = "MM/dd/yyyy @ K:mm a";
    SimpleDateFormat sdf = new SimpleDateFormat(datePatternToUse);
    this.formattedCreatedAt = sdf.format(date);
}

Make sure you understand what we are getting up to here.

  • First, we are making a new Date type object using our milliseconds stored in createdat.
  • Then, we are defining a specific pattern we want to apply to the time format using predefined codes. Check the link and review them, or experiment with your own pattern. There are many options to choose from that are more and less concise.
  • Next, we are booting up a new SimpleDateFormat object with out pattern as an argument.
  • Finally, we run the method format() on our SimpleDateFormat object, using our date as an argument. The method returns a String.

SimpleDateFormat can run a couple of different ways, but they all follow this structure more or less. Let's test that we are converting things correctly by writing an additional test with a known time instead of the system time.

We'll learn exactly why we need the setFormattedCreatedAt when we create our frontend routes for this functionality, but first, let's extend the test we just wrote to check and see if our date is correct.

src/test/java/dao/Sql2oReviewTest.java
@Test
public void timeStampIsReturnedCorrectly() throws Exception {
    Restaurant testRestaurant = setupRestaurant();
    restaurantDao.add(testRestaurant);
    Review testReview = new Review("Captain Kirk", 3, "foodcoma!", testRestaurant.getId());
    reviewDao.add(testReview);

    long creationTime = testReview.getCreatedat();
    long savedTime = reviewDao.getAll().get(0).getCreatedat();
    String formattedCreationTime = testReview.getFormattedCreatedAt();
    String formattedSavedTime = reviewDao.getAll().get(0).getFormattedCreatedAt();
    assertEquals(formattedCreationTime,formattedSavedTime);
    assertEquals(creationTime, reviewDao.getAll().get(0).getCreatedat());
}

If you set a breakpoint at the beginning of this test, and run your tests in debug mode, you'll see that we are doing everything right - our time is getting saved correctly, and our converter is formatting the time correctly too. Awesome. We can now use this getter method to display the time correctly in our queries to the API.

Let's test this with Postman.

Testing

Next, we'll learn about sorting custom Objects in Java, and apply that knowledge to sorting Reviews newest to oldest.