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:
- Mind the punctuation for the full hours; o’clock is written as one word.
- Spacing between individual words is strictly limited to one space. Cardinal numbers greater than 20 are hyphenated (e.g. “twenty-one”).
- Input is always going to be a non-null string in the format \d{2}:\d{2}(\s?[ap]m)?
- Both 12h and 24h input may be present. In case of 12h input disregard the am/pm part.
- Remember that in 24h midnight is denoted as 00:00.
- 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]))) ); }); } }