How much time is left? rags-to-riches of Convert seconds to hours, minutes and seconds.

In that question, BigDecimal was chosen to be the model class, which doesn't seem like a right fit for temporal amounts.

Sure, doing arithmetic should be the simpler and faster way, but I thought of using the Duration class to make this more 'API-driven'.

DurationFormatter

public final class DurationFormatter {

    private DurationFormatter() {
        // empty
    }

    public static String displayReadable(long seconds) {
        return displayReadable(Duration.ofSeconds(seconds));
    }

    public static String displayReadable(Duration duration) {
        if (Objects.requireNonNull(duration).isNegative()) {
            throw new IllegalArgumentException("seconds must not be negative.");
        }
        long hours = duration.toHours();
        long minutes = duration.minusHours(hours).toMinutes();
        long leftover = duration.minusHours(hours).minusMinutes(minutes).getSeconds();
        return replaceLast(
                Stream.of(append(hours, ChronoUnit.HOURS),
                            append(minutes, ChronoUnit.MINUTES),
                            append(leftover, ChronoUnit.SECONDS))
                        .filter(s -&gt; !s.startsWith("0"))
                        .collect(Collectors.joining(", ")), ", ", " and");
    }

    private static String append(long value, ChronoUnit unit) {
        return String.format("%d %s", Long.valueOf(value),
                value == 1 ? singular(unit) : unit).toLowerCase();
    }

    private static String singular(ChronoUnit unit) {
        String toString = unit.toString();
        return toString.substring(0, toString.length() - 1);
    }

    private static String replaceLast(String input, String target, String replacement) {
        int lastIndex = input.lastIndexOf(target);
        return lastIndex == -1 ? input : input.substring(0, lastIndex) + replacement
                + input.substring(lastIndex + 1);
    }
}


DurationFormatterTest

public class DurationFormatterTest {

    enum TestCase {
        ZERO(0, ""),
        ONE_SEC(1, "1 second"),
        TWO_SECS(2, "2 seconds"),
        ONE_MIN(60, "1 minute"),
        TWO_MINS(120, "2 minutes"),
        ONE_MIN_ONE_SEC(61, "1 minute and 1 second"),
        ONE_HR(3600, "1 hour"),
        TWO_HRS(7200, "2 hours"),
        ONE_HR_ONE_SEC(3601, "1 hour and 1 second"),
        ONE_HR_ONE_MIN(3660, "1 hour and 1 minute"),
        ONE_HR_ONE_MIN_ONE_SEC(3661, "1 hour, 1 minute and 1 second"),
        MAX(Long.MAX_VALUE, "2562047788015215 hours, 30 minutes and 7 seconds");

        private final long seconds;
        private final String expected;

        private TestCase(long seconds, String expected) {
            this.seconds = seconds;
            this.expected = expected;
        }

        void verify() {
            assertThat(DurationFormatter.displayReadable(seconds), equalTo(expected));
        }

        @Override
        public String toString() {
            return String.format("%d seconds =&gt; \"%s\"", Long.valueOf(seconds),
                    expected);
        }
    }

    @DataProvider(name = "test-cases")
    public Iterator&lt;Object[]&gt; getTestCases() {
        return EnumSet.allOf(TestCase.class).stream().map(v -&gt; new Object[] { v })
                .iterator();
    }

    @Test(dataProvider = "test-cases")
    public void testCases(TestCase testCase) {
        testCase.verify();
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testNegativeValue() {
        try {
            DurationFormatter.displayReadable(-1);
        } catch (IllegalArgumentException e) {
            assertThat(e.getMessage(), equalTo("seconds must not be negative."));
            throw e;
        }
    }

    @Test(expectedExceptions = NullPointerException.class)
    public void testNullValue() {
        DurationFormatter.displayReadable(null);
    }
}
After sitting on this for... 8 hours, I realized singular() can, and probably should, use the replaceLast() method, to encourage code re-use at a very small expense of efficiency (which isn't the goal here anyways):

private static String singular(ChronoUnit unit) {
    return replaceLast(unit.toString(), "s", "");
}