Technical details

Figure 321. The @Test annotation Slide presentation
public
@interface Test {
...
}
  • @interface defining an annotation.

  • Purpose: Adding meta information for automated detection of test methods.


Figure 322. The Assert class Slide presentation
public class Assert {

  public static void assertTrue(
    String message, boolean condition) { ...}

  public static void assertEquals(
    long expected, long actual) { ...}
...
}

Figure 323. Importing dependencies Slide presentation
<project ...>
...
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.13</version>

      <scope>test</scope>
    </dependency>
    ...
  </dependencies>
  ...
</project>

Local: /home/goik/.m2/repository/junit/junit/4.13/junit-4.13.jar

Remote: https://mvnrepository.com/artifact/junit/junit/4.13


Figure 324. Dependency archive content Slide presentation
> jar -tf junit-4.13.jar
META-INF/
META-INF/MANIFEST.MF
org/
org/junit/
...
org/junit/Assert.class
...

exercise No. 127

Turning seconds into weeks, part 2

Q:

This is a follow-up exercise to Turning seconds into weeks implementing a corresponding Timeperiod class. Consider the subsequent example:

final long seconds = 112223;
final Timeperiod tPeriod = new Timeperiod(112223); 
System.out.println(seconds + " seconds are equal to " + tPeriod ); 

This requires a constructor definition Timeperiod(long).

This requires a toString() method within your Timeperiod class matching Object.toString()'s method signature.

1 day, 7 hours, 10 minutes and 23 seconds

Since the toString() method may not be called at all it shall be implemented in a »on demand« fashion similar to Your personal String class . Hint: create a suitable attribute being null initially. If toString() is being called, first check for null and initialize it by the desired output if so required.

Furthermore individual weeks, days, hours, minutes and seconds shall be implemented as read-only values:

final Timeperiod tPeriod = new Timeperiod(2310983);
System.out.print("weeks = " + tPeriod.weeks);

tPeriod.days = 5;  // Expected compile time error:
                   // Cannot assign a value to final variable 'days'
weeks = 3

In other words: Instances of Timeperiod should be immutable objects. Once a Timeperiod instance has been created it shall be impossible to alter its internal state.

In addition supply a so called copy constructor to allow for creating a new instance from an existing one:

/**
 *  Clone a given instance.
 *
 * @param timeperiod Instance to be cloned.
 */
public Timeperiod(final Timeperiod timeperiod) { ... }

Use the following unit tests to check your implementation's correctness:

public class TimeperiodTest {

    // Helper methods for real tests.
    static void assertPeriodEqualImplement(
                          final int expectedSeconds,
                          final int expectedMinutes,
                          final int expectedHours,
                          final int expectedDays,
                          final int expectedWeeks,
                          final String expectedToString,
                          final Timeperiod period) {
        Assert.assertEquals(expectedSeconds, period.seconds);
        Assert.assertEquals(expectedMinutes, period.minutes);
        Assert.assertEquals(expectedHours, period.hours);
        Assert.assertEquals(expectedDays, period.days);
        Assert.assertEquals(expectedWeeks, period.weeks);

        Assert.assertEquals(expectedToString, period.toString());
    }

    static void assertPeriodEqual(
                          final int expectedSeconds,
                          final int expectedMinutes,
                          final int expectedHours,
                          final int expectedDays,
                          final int expectedWeeks,
                          final String expectedToString,
                          final Timeperiod period) {

        // Testing period in question
        assertPeriodEqualImplement(
                expectedSeconds, expectedMinutes, expectedHours, expectedDays, expectedWeeks, expectedToString,
                period);

        // Testing copy constructor
        final Timeperiod periodClone = new Timeperiod(period);

        Assert.assertTrue("A cloned instance must differ from its original", periodClone != period);

        assertPeriodEqualImplement(
                expectedSeconds, expectedMinutes, expectedHours, expectedDays, expectedWeeks, expectedToString,
                periodClone);
    }

    /**
     * Test constructor zero seconds.
     */
        @Test
        public void testZero() {
            assertPeriodEqual(
                    0,0,0,0,0,
                    "0 seconds",
                    new Timeperiod(0));
        }

    @Test
    public void testMinute() {
        assertPeriodEqual(0,1,0,0,0,
                "1 minute and 0 seconds",
                new Timeperiod(60));
        assertPeriodEqual(50,0,0,0,0,
                "50 seconds",
                new Timeperiod(50));
        assertPeriodEqual(12,1,0,0,0,
                "1 minute and 12 seconds",
                new Timeperiod(72));
        assertPeriodEqual(2,5,0,0,0,
                "5 minutes and 2 seconds",
                new Timeperiod(302));
    }

    @Test
    public void testHour() {
        assertPeriodEqual(0,0,2,0,0,
                "2 hours, 0 minutes and 0 seconds",
                new Timeperiod(7200));
        assertPeriodEqual(59,59,0,0,0,
                "59 minutes and 59 seconds",
                new Timeperiod(3599));
        assertPeriodEqual(40,1,1,0,0,
                "1 hour, 1 minute and 40 seconds",
                new Timeperiod(3700));
    }

    @Test
    public void testVarious() {
        assertPeriodEqual(1,3,4,1,6,
                "6 weeks, 1 day, 4 hours, 3 minutes and 1 second",
                new Timeperiod(3729781));

        assertPeriodEqual(23,56,17,5,3,
                "3 weeks, 5 days, 17 hours, 56 minutes and 23 seconds",
                new Timeperiod(2310983));
    }
}

A:

Our implementation basically reads:

public class Timeperiod {

   // Constructors, toString() and other methods
   //                 ...

   public final int seconds, minutes, hours, days, weeks; // Instance state
}

The final modifier ensures instances being immutable thus requiring all values to be set within any constructor being defined. We thus decompose the desired number of seconds into weeks, days, hours, minutes and remaining seconds:

public Timeperiod(int seconds) {

  weeks = seconds / SECONDS_PER_WEEK;
  seconds = seconds % SECONDS_PER_WEEK;        // remaining seconds without weeks

  days = seconds / SECONDS_PER_DAY;
  seconds %= SECONDS_PER_DAY;                  // remaining seconds without days

  hours = seconds / SECONDS_PER_HOUR;
  seconds %= SECONDS_PER_HOUR;                 // remaining seconds without minutes

  minutes = seconds / SECONDS_PER_MINUTE;
  this.seconds = seconds % SECONDS_PER_MINUTE; // remaining seconds
}

Notice the this.seconds qualification being required to disambiguate the constructor parameter variable Timeperiod(int seconds) scope from the instance member variable scope Timeperiod.seconds.

The toString() method could be defined straightforwardly:

public String toString() {
   final int largestNonZeroComponent;

        if (0 < weeks) {
            largestNonZeroComponent = 5;       // weeks, days, hours, minutes and seconds
        } else if (0 < days) {
            largestNonZeroComponent = 4;       // days, hours, minutes and seconds
        } else if (0 < hours) {
            largestNonZeroComponent = 3;       // hours, minutes and seconds
        } else if (0 < minutes) {
            largestNonZeroComponent = 2;       // minutes and seconds
        } else  {
            largestNonZeroComponent = 1;       // only seconds, potentially zero as well
        }

        final StringBuffer buffer = new StringBuffer();
        switch (largestNonZeroComponent) {
            case 5: addSingularOrPlural(buffer, weeks, "week", ", ");
            case 4: addSingularOrPlural(buffer, days, "day", ", ");
            case 3: addSingularOrPlural(buffer, hours, "hour", ", ");
            case 2: addSingularOrPlural(buffer, minutes, "minute", ", ");
            case 1: addSingularOrPlural(buffer, seconds, "second", " and ");
        }
        return buffer.toString();
}

This solution provides the desired result. However if being called repeatedly it causes a performance penalty recalculating an identical value time and again. We thus refine it by using a lazy initialization mechanism. In the first step we rename our current method implementation having just private access:

private String toStringImplement() {
   final int largestNonZeroComponent;

        if (0 < weeks) {
            largestNonZeroComponent = 5; 
            ...

        return buffer.toString();
}

We now re-implement our desired toString() method in a lazy initialization fashion by introducing an additional private attribute:

public class Timeperiod {

    // Tedious calculation, will be be initialized on-demand only
    private String toStringValue = null;

             ...

    public String toString() {
        // Calculation is tedious, thus performing it only on-demand.
        //
        if (null == toStringValue) { // Called for the first time, not yet initialized?
            toStringValue = toStringImplement();
        }
        return toStringValue;
    }
}

See Timeperiod for a complete solution including a copy constructor implementation.