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