What Time Is It? in Java


The challenge

How many times have we been asked this simple question in our daily lives by family, friends and strangers alike?

In this challenge you take a look at your watch and answer this question in proper English. Sometimes you have your watch in 24h format and others in 12h. The AM/PM part of the time is always disregarded as the asker knows whether it’s morning or afternoon.

Requirements:
  1. Mind the punctuation for the full hours; o’clock is written as one word.
  2. Spacing between individual words is strictly limited to one space. Cardinal numbers greater than 20 are hyphenated (e.g. “twenty-one”).
  3. Input is always going to be a non-null string in the format \d{2}:\d{2}(\s?[ap]m)?
  4. Both 12h and 24h input may be present. In case of 12h input disregard the am/pm part.
  5. Remember that in 24h midnight is denoted as 00:00.
  6. There may or may not be a space between the minutes and the am/pm part in 12h format.
Examples:
toHumanTime("05:28 pm"); // twenty-eight minutes past five
toHumanTime("12:00");    // twelve o'clock
toHumanTime("03:45am");  // quarter to four
toHumanTime("07:15");    // quarter past seven
toHumanTime("23:30");    // half past eleven
toHumanTime("00:01");    // one minute past twelve
toHumanTime("17:51");    // nine minutes to six 

The solution in Java code

Option 1:

public class TimeFormatter {

  private final static String[] NUMBERS = {
    "o'clock",
    "one", "two", "three", "four", "five", 
    "six", "seven", "eight", "nine", "ten",
    "eleven", "twelve", "thirteen", "fourteen", "quarter", 
    "sixteen", "seventeen", "eighteen", "nineteen", "twenty",
    "twenty-one", "twenty-two", "twenty-three", "twenty-four", "twenty-five", 
    "twenty-six", "twenty-seven", "twenty-eight", "twenty-nine", "half" };
    
  private static String minuteStr(final int m) {
    return NUMBERS[m] + ((m == 15 || m == 30) ? "" : m == 1 ? " minute" : " minutes");
  }
  
  private static int hr12(final int h) {
    int hr = h % 12;
    return hr == 0 ? 12 : hr;
  }
  
  public static String toHumanTime(final String time) {
    final String[] parts = time.split("[\\s:aApP]");
    final int hr = Integer.valueOf(parts[0]), min = Integer.valueOf(parts[1]);
    if (min == 0) return String.format("%s %s", NUMBERS[hr12(hr)], NUMBERS[0]);    
    if (min <= 30) return String.format("%s past %s", minuteStr(min), NUMBERS[hr12(hr)]);
    return String.format("%s to %s", minuteStr(60 - min), NUMBERS[hr12(hr+1)]);
  }
  
}

Option 2:

public class TimeFormatter {
    public static String toHumanTime(String time) {
        String stringH = time.replaceAll(":.+$", "");
        String stringM = time.replaceAll("^(\\d.)", "");
        String stringM1 = stringM.replaceAll("\\D", "");
        return printWords(Integer.valueOf(stringH), Integer.valueOf(stringM1));
    }

    private static String printWords(int h, int m) {
        String nums[] = {"twelve", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten",
                "eleven", "twelve", "thirteen", "fourteen", "fifteen" ,"sixteen", "seventeen", "eighteen", "nineteen", "twenty",
                "twenty-one", "twenty-two", "twenty-three", "twenty-four", "twenty-five", "twenty-six",
                "twenty-seven", "twenty-eight", "twenty-nine"};

        if (m == 0) return nums[h%12] + " o'clock";
        else if (m == 1) return "one minute past " + nums[h%12];
        else if (m == 59) return "one minute to " + nums[(h % 12) + 1];
        else if (m == 15) return "quarter past " + nums[h%12];
        else if (m == 30) return "half past " + nums[h%12];
        else if (m == 45) return "quarter to " + nums[(h % 12) + 1];
        else if (m <= 30) return nums[m] + " minutes past " + nums[h%12];
        //  else if (m > 30)
        return nums[60 - m] + " minutes to " + nums[(h % 12) + 1];
    }
}

Test cases to validate our solution

import static org.junit.Assert.*;
import org.junit.Test;

public class TimeFormatterTest {

    @Test
    public void basicTests() {
        assertEquals("twenty-eight minutes past five", TimeFormatter.toHumanTime("05:28 pm"));
        assertEquals("twelve o'clock", TimeFormatter.toHumanTime("12:00"));
        assertEquals("quarter to four", TimeFormatter.toHumanTime("03:45am"));
        assertEquals("quarter past seven", TimeFormatter.toHumanTime("07:15"));
        assertEquals("half past eleven", TimeFormatter.toHumanTime("23:30"));
        assertEquals("one minute past twelve", TimeFormatter.toHumanTime("00:01"));
        assertEquals("nine minutes to six", TimeFormatter.toHumanTime("17:51"));
    }
}

Additional test cases

import org.junit.Test;

import java.util.Random;
import java.util.stream.IntStream;

import static org.junit.Assert.assertEquals;

public class TimeFormatterTest {

    private static final String[] cardinals = new String[]{
        "one","two","three","four","five","six","seven","eight","nine","ten",
        "eleven","twelve","thirteen","fourteen","quarter","sixteen","seventeen","eighteen","nineteen","twenty"
    };
    private static final Random random = new Random(System.currentTimeMillis());

    private String format(int h, int m) {
        boolean is24format = random.nextBoolean();
        boolean isAm = random.nextBoolean();
        String extraSpace = random.nextBoolean() ? " " : "";
        String a = "";
        if (is24format) {
            if (!isAm) h += 12;
            if (h == 24) h = 0;
        } else {
            a = extraSpace + (isAm ? "am" : "pm");
        }
        return String.format("%02d:%02d%s", h, m, a);
    }

    private String calcMinutes(int m) {
        StringBuilder minutes = new StringBuilder();
        if (m <= 20) {
            minutes.append(cardinals[m-1]);
        } else {
            String hyphenated = String.join("-", cardinals[19], cardinals[m-21]);
            minutes.append(hyphenated);
        }
        minutes.append(m == 15 ? "" : (" minute" + (m == 1 ? "" : "s")));
        return minutes.toString();
    }

    @Test
    public void basicTests() {
        assertEquals("twenty-eight minutes past five", TimeFormatter.toHumanTime("05:28 pm"));
        assertEquals("twelve o'clock", TimeFormatter.toHumanTime("12:00"));
        assertEquals("quarter to four", TimeFormatter.toHumanTime("03:45am"));
        assertEquals("quarter past seven", TimeFormatter.toHumanTime("07:15"));
        assertEquals("half past eleven", TimeFormatter.toHumanTime("23:30"));
        assertEquals("one minute past twelve", TimeFormatter.toHumanTime("00:01"));
        assertEquals("nine minutes to six", TimeFormatter.toHumanTime("17:51"));
    }

    @Test
    public void shouldTranslateCorrectlyTimeStrings() {
        IntStream.rangeClosed(1,12).forEach(hour -> {
            String translatedHour = cardinals[hour-1];
            String expectedFull = translatedHour + " o'clock";
            assertEquals(expectedFull, TimeFormatter.toHumanTime(format(hour, 0)));

            String expectedPastQuarter = "quarter past " + translatedHour;
            assertEquals(expectedPastQuarter, TimeFormatter.toHumanTime(format(hour, 15)));

            String expectedHalf = "half past " + translatedHour;
            assertEquals(expectedHalf, TimeFormatter.toHumanTime(format(hour, 30)));

            String translatedNextHour = cardinals[hour%12];
            String expectedToQuarter = "quarter to " + translatedNextHour;
            assertEquals(expectedToQuarter, TimeFormatter.toHumanTime(format(hour, 45)));

            // minutes past ...[hour]
            IntStream.of(1,2,3,4,5,6,7,8,9,10,11,12,13,14,16,17,18,19,20,21,22,23,24,25,26,27,28,29)
                .mapToObj(m -> {
                    String minutes = calcMinutes(m);
                    return new Object[]{minutes + " past " + cardinals[hour-1], m};
                })
                .forEachOrdered(expected ->
                    assertEquals(expected[0], TimeFormatter.toHumanTime(format(hour, (Integer)expected[1])))
                );

            // minutes to ...[nextHour]
            IntStream.of(31,32,33,34,35,36,37,38,39,40,41,42,43,44,46,47,48,49,50,51,52,53,54,55,56,57,58,59)
                .mapToObj(m -> {
                    String minutes = calcMinutes(60-m);
                    return new Object[]{minutes + " to " + cardinals[hour%12], m};
                })
                .forEachOrdered(expected ->
                    assertEquals(expected[0], TimeFormatter.toHumanTime(format(hour, (Integer)expected[1])))
                );
        });
    }
}