Java:API:Time

From Juneday education
Jump to: navigation, search

Description

This chapter looks a little into the Java API for time.

Introductory examples

LocalTime used for a clock

We'll start with a small example of how to represent a time in Java's time API. The example is a small Java program which writes the current time to the terminal and updates it every second (like a clock).

The first class we'll introduce is java.time.LocalTime. This is an immutable class representing a time. LocalTime has nanosecond precision, but can be "truncated" e.g. seconds (in which case the fractions are set to 0).

To obtain a LocalTime representing the current time, you use the static factory method now():

LocalTime now = LocalTime.now();
System.out.println(now); // e.g. "10:49:41.263707"

The class LocalTime's being immutable means that we cannot change the state of an object of that class. Every method for manipulating a time returns a reference to a new time. See Strings are immutable for a comparison with Strings, which are also immutable. So, if we have a LocalTime and are interested in what the time would be in 45 minutes, we'd use the plusMinutes(long) method:

LocalTime afterTheLecture = LocalTime.now().plusMinutes(45);
System.out.println("This boring lectures end at " + afterTheLecture);

There are similar plus methods (and also minus methods) for seconds, minutes and hours.

Now, to our example with the small Java clock program. Our program needs to obtain a LocalTime every second and print it truncated to zeros (because looking at the nanoseconds require a really fast brain and really fast eyes). The way to truncate a LocalTime to seconds is to use the truncatedTo(TemporalUnit) method. The interface java.time.temporal.TemporalUnit represents units of time (like seconds, minutes, hours etc). The commonly used implementation for this is the class java.time.temporal.ChronoUnit is the class we will use, in order to specify that we want our time truncated to seconds:

LocalTime nowDownToSeconds = LocalTime.now().truncatedTo(ChronoUnit.SECONDS);
System.out.println(nowDownToSeconds); // e.g. "10:49:41"

In order to get a new time every second, we'll use a java.util.Timer and schedule that with a recurring java.util.TimerTask. How to use timers and timer tasks is a topic for another lecture, but we think that our code will be self explanatory. We'll explain the code briefly after the source code:

import java.time.LocalTime;
import java.time.temporal.ChronoUnit;
import java.util.Timer;
import java.util.TimerTask;

public class Java8Clock {
  public static void main(String[] args) {
    Timer timer = new Timer("Ticker");
    timer.schedule(new TimerTask(){
        @Override
        public void run() {
          System.out.print("\r%s" +
                           LocalTime.now()
                           .truncatedTo(ChronoUnit.SECONDS));
        }
      }, 0, 1000);
  }
}

Running the program will show the time in your terminal and update it every second (on the same line, in place).

The program works like this:

  • Create a Timer - an object which can do timing tasks
  • Schedule the timer to run a TimerTask every second (every 1000 milliseconds)
    • The task is to print a carriage return followed by the current time truncated down to seconds

What makes the program write the time and overwrite it the next second is the \r (carriage return). It simply moves the cursor to the beginning of the line before next output, but it doesn't write a newline character. So the next text will overwrite the previous one, giving the impression of a "clock" updating.

What makes the program tick (no pun intended), is the timer scheduled with the repeating timer task. The arguments to this overloaded version of schedule() are:

  • A TimerTask
  • How many milliseconds to wait before starting (0)
  • How many milliseconds before each run of the task (1000 - one second)

Try it!

LocalDate used for planning after-work

Next, we'll look at the class java.time.LocalDate which represents a date. The example we use here is a small application for telling us what date, in the future from today, is the last Monday of the month. Let's say we have a standing invitation to after-work festivities every last Monday of every month (because Friday is amateur night), and we are already looking forward to this event, but we can't remember what date is the next "last Monday"!

For this, we need to represent today's date as a LocalDate and the use this date to get hold of the date for the next last monday of a month (or today's date if we are lucky and it's the last Monday today already!).

To get hold of a LocalDate representing today is pretty straight forward:

LocalDate today = LocalDate.now();
System.out.println(today); // e.g. "2018-04-02"

Now, just as with LocalTime, LocalDate objects are immutable. So any attempt to modify the date will return a reference to a new date (just as with LocalTime and String and other immutable classes). But how do we query the date for the next last Monday of this month (or if it's passed, the next month)?

We can use a java.time.temporal.TemporalAdjuster as the argument to LocalDate's with() method. A TemporalAdjuster is an object implementing the java.time.temporal.TemporalAdjuster interface, allowing us to choose an external strategy for adjusting the date. Now, there are a couple of prefab temporal adjusters in the utility class java.time.temporal.TemporalAdjusters class.

Specifically, we have the lastInMonth(DayOfWeek) method, which does exactly what we want. DayOfWeek is an enum and we can use it like this:

LocalDate today = LocalDate.now();
LocalDate afterWork = today.with(TemporalAdjusters.lastInMonth(DayOfWeek.MONDAY));

So the full program will become:

import java.time.LocalDate;
import java.time.temporal.TemporalAdjusters;
import java.time.DayOfWeek;

public class AfterWork {

  public static void main(String[] args) {
    LocalDate today = LocalDate.now();
    System.out.println("Today is " + today);
    System.out.println("Next after work is at " +
                       today.with(TemporalAdjusters
                                  .lastInMonth(DayOfWeek.MONDAY)));
  }
  
}

But what if we are on the very last Monday today? We should check that and give a heads-up:

    LocalDate afterWork =
      today.with(TemporalAdjusters.lastInMonth(DayOfWeek.MONDAY))
    if (today.equals(afterWork)) {
      System.out.println("Today!");
    }

But what if we are passed it? They we should get the next month's last Monday's date instead!

    if (today.isAfter(afterWork)) {
      System.out.println("We are past the last Monday :(");
      LocalDate nextAw =
        today
        .plusMonths(1)
        .with(TemporalAdjusters.lasstInMonth(DayOfWeek.MONDAY));
      System.out.println("Next month, it is, then: " + nextAw);
    }

The full program will look something like:

import java.time.LocalDate;
import java.time.temporal.TemporalAdjusters;
import java.time.temporal.TemporalAdjuster;
import java.time.DayOfWeek;
import java.time.format.TextStyle;
import java.util.Locale;

public class AfterWork {

  public static void main(String[] args) {
    
    LocalDate today = LocalDate.now();
    String weekDayToday = today
      .getDayOfWeek()
      .getDisplayName(TextStyle.FULL, Locale.getDefault());

    System.out.println("Today is " + weekDayToday + " " + today);

    TemporalAdjuster lastMonday =
      TemporalAdjusters.lastInMonth(DayOfWeek.MONDAY);
    LocalDate afterWork = 
      today.with(lastMonday);

    if (today.equals(afterWork)) {
      System.out.println("Today!");
    } else if (today.isAfter(afterWork)) {
      System.out.println("We missed it this month.");
      afterWork = today.plusMonths(1)
        .with(lastMonday);
      System.out.println("Next month, it is: " + afterWork);
    } else {
      System.out.println("Next AW at: " + afterWork);
    }
  }
}

The Duration class

The time API contains a class Duration which lets us represent a duration in time (seconds and nano seconds). This is very useful. With a Duration object, we can store how much time (in seconds, nano seconds etch) exists between for instance two LocalTime points.

To demonstrate a use of the Duration class, we'll start with some code snippets:

import java.time.Duration;
import java.time.LocalTime;

public class Examples {
  public static void main(String[] args) {
    LocalTime start = LocalTime.parse("13:15");
    LocalTime end = LocalTime.parse("14:10");
    Duration lectureLength = Duration.between(start, end);
    System.out.println("There are " + lectureLength.toMinutes() +
                       " minutes between " + start + " and " + end);
  }
}

The result of compiling and running the above snippet would be:

$ javac Examples.java && java Examples
There are 55 minutes between 13:15 and 14:10
$

Now, don't make the beginner's mistake of measuring the duration between to times across midnight. You see, the difference between 23:55 and 00:05 in minutes is -1430 minutes. Why is this? There are 24 x 60 minutes in 24 hours. Since a LocalTime is a time on a 24 hour clock, the maximum duration between two such times is 1440 minutes. The duration between a time and a time before that time, will render a negative number.

The duration between two LocalTime objects looks at the time points as a number of seconds of the day from 00:00. So, 23:55 is to the Duration object 1435 minutes into the day, whereas 00:05 is to the Duration object 5 minutes into the day. Now, 5 - 1435 = -1430.

So, how would we correctly calculate the duration between two times, while also handling the case that the start time is "after" the end time (i.e. a start time of 23:55 is seen as after the end time of 00:05)?

One strategy is to check programmatically whether the end time is before the start time:

LocalTime start = ...
LocalTime end = ...
int minutesBetween;
if (end.isBefore(start)) { // we have passed midnight!
  minutesBetween = (int)Duration.ofHours(24).minus(Duration.between(end, start)),toMinutes(); // note the order
} else {
  minutesBetween = (int)Duration.between(start, end).toMinutes();
}

The reason why the above works, is that instead of ending up with e.g. -1430 minutes (5 - 1435 = -1430) we discover that e.g. the end time 00:05 is "before" the start time of e.g. 23:55, so we do an alternative calculation. We take a full day (24 hours) duration (1440 minutes), and then we subtract the opposite duration of 00:05 to 23:55.

So we do this: minutesBetween = 1440 - 1430; // 10 minutes.

Exercise on Durations

We found an exercise on using LocalTime and Duration in a book, which we found interesting, because it looks easier than it actually is. We are here presenting a version of the exercise with slight modifications.

The exercise is about writing a small program which can calculate whether a TV show with a known start time and a known end time will fit onto a VHS cassette or not. The length (in minutes) of the cassette should be provided by the user, as well as how many minutes you already have recorded on the cassette. The program should calculate how many minutes long the TV show is, and answer with whether that show will fit on the remainder of the free space on the cassette.

Our version of the requirements are as follows:

  • Your program should be written in a Java class called VHS (defined in a file VHS.java and contain the main method)
  • Your program should check that the user gives exactly four (4) arguments, or exit with an exit code of one (1) with the following error message written to standard err (System.err.println works fine):
Usage: java VHS <length> <used> <start-time> <end-time>
Where:
  length is the length of the cassette in minutes
  used is the length of already recorded shows on the cassette
       in minutes
  start-time is a time on the format HH:MM
  end-time is a time on the format HH:MM
Example: 
  java VHS 180 90 22:10 23:55 
  • The program should be able to handle shows spanning over midnight, e.g between 23:00 and 01:30
  • Your program should handle malformed input (parse errors) and give appropriate error messages and exit codes
    • an exit code of two (2) is fine for all kinds of parse errors
    • The following arguments are examples which will produce a parse error:
      • java VHS apa 90 22:10 23:55
      • java VHS 180 apa 22:10 23:55
      • java VHS 180 90 22-10 23:55
      • java VHS 180 90 22:10 23colon55
  • The program should answer on this format:
    • You have 90 minutes left on the tape. The show is 95 minutes long. It won't fit on the cassette.
    • You have 45 minutes left on the tame. The show is 40 minutes long. It will fit on the cassette.

The book we found the exercise in has a suggested solution which doesn't handle shows that start before midnight and end after midnight. Explain what probably is wrong with that solution (why it won't work), and how you would fix the code.

Suggested layout to get you started:

import java.time.*;

public class VHS {

  /* We use int, since we are not aware of shows on TV whose
   * number of minute duration requires a long variable.
   */
  private static int duration(LocalTime start, LocalTime end) {
    /* Calculate the duration between start and end and
     * return it as an int of minutes.
     * the toMinutes() method in Duration has return-type long
     */
    return (int) ask-the-duration-for-its-minutes;
  }
  
  public static void main(String[] args) {

    if (args.length != 4) {
      System.err.println("Usage: java VHS <length> <used> <start-time> <end-time>");
      System.err.println("Where:");
      System.err.println(" length is the length of the cassette in minutes");
      System.err.println(" used is the length of already recorded shows on the cassete");
      System.err.println("      in minutes");
      System.err.println(" start-time is a time on the format HH:MM");
      System.err.println(" end-time is a time on the format HH:MM");
      System.err.println("Example:");
      System.err.println("  java VHS 180 90 22:10 23:55");
      System.exit(1);
    }

    // put parse error exception handling around this section
    // NOTE: if you declare variables inside a try-catch block, they
    // are local to that block and not usable after the block!
    int tapeLength = parse the args[0] to int
    int used = parse the args[1] to int;
    int minutesLeft = calculate how many minutes are left on tape;
    LocalTime start = parse the args[2] to a LocalTime;
    LocalTime end = parse the args[3] to a LocalTime;
    int showDuration = duration(start, end);
    // end parse error exception handling

    // NOTE again - if e.g minutesLeft was declared inside the try-catch
    // it will be local to that block and out of scope here - handle that.
    System.out.printf("You have %d minutes left on the tape.", minutesLeft);
    System.out.printf(" The show is %d minutes long.\nIt %s fit on the tape.\n",
                      showDuration,
                      (showDuration > (minutesLeft) ? "won't" : "will"));
  }
}

Examples of test runs on our suggested solution (which works, unlike the solution found via the book):

$ javac VHS.java && java VHS 180 90 23:00 00:31
You have 90 minutes left on the tape. The show is 91 minutes long.
It won't fit on the tape.
$ javac VHS.java && java VHS 180 90 23:00 00:30
You have 90 minutes left on the tape. The show is 90 minutes long.
It will fit on the tape.
$ javac VHS.java && java VHS 180 90 23:00 00:29
You have 90 minutes left on the tape. The show is 89 minutes long.
It will fit on the tape.
$

Challenge

Make the program a little more object oriented. Create a class ArgumentParser whose constructor takes a String[] args. An object of class ArgumentParser should have the following instance methods:

  • public boolean hasParseErrors() - true if all arguments were parsed, false otherwise
  • public int exitCode() return 0 for parse ok, 1 for wrong arguments and 2 for parse error
  • public String parseErrorMessage() - An error message if one of the arguments wouldn't parse (or there are wrong number of arguments etc)
  • public int tapeLength() - args[0] as an int
  • public int used() - args[1] as an int
  • public LocalTime start() - args[2] as a LocalTime
  • public LocalTime end() - args[3] as a LocalTime

The logic of your program's main method now can be changed to something like the following pseudo code:

ArgumentParser parser = new ArgumentParser(args);
if (parser.hasParseErrors()) {
  System.err.println(parser.parseErrorMessage());
  System.exit(parser.exitCode());
}
int duration = duration(parser.start(), parser.end());
if (parser.tapeLength() - parser.used() > duration) {
  // handle show will fit
} else {
  // handle show won't fit
}

Video and presentations

Links

Source code

External links

The Timer and TimerTask stuff (unrelated to Java's Time API but good to know):

Chapter links

  • Previous/Next links will be put here once we have an idea of what comes before and what comes after