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])))
                );
        });
    }
}