Java: Phone Bill Calculator

A Java 8 solution to an exercise about calculating phone bills.

Java: Phone Bill Calculator

Yesterday I was browsing the Java subreddit and I noticed an interesting blog post by a developer at North Concepts. He had received a Scala solution to a programming problem and solved it using the companies proprietary software "Data Pipeline". The solution showcases the ease of performing the kata using their libraries, but I didn't think it would be a particularly hard solution to approach from a vanilla Java perspective, so I put a solution together.

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

class PhoneBillCalculator {

	private static final Integer FIVE_MINUTE_CUT_OFF = 300;
	private static final Integer CENTS_PER_SECOND = 3;
	private static final Integer CENTS_PER_STARTED_MINUTE = 150;

	private static final Comparator<List<PhoneCall>> PHONE_CALL_LIST_COMPARATOR = (l1, l2) -> {
		int comparedValue = -1 * l1.stream().map(x -> x.seconds).reduce(0, (x, y) -> x + y)
				.compareTo(l2.stream().map(x -> x.seconds).reduce(0, (x, y) -> x + y));
		return comparedValue == 0 //This is important! The duration between two sets of calls could be the same, but the cost calc could be very different!
				? l1.stream().findFirst().get().phoneNumber.replace("-", "")
				.compareTo(l2.stream().findFirst().get().phoneNumber.replace("-", ""))
				: comparedValue;
	};

	public int calculatePhoneBill(String csvString) {
		return Arrays.stream(csvString.split("\r?\n"))
				.map(x -> x.split(","))
				.map(Arrays::asList)
				.map(PhoneCall::new) //Stream<PhoneCall>
				.collect(Collectors.groupingBy(x -> x.phoneNumber)) //Map<String, List<PhoneCall>>
				.values() //Only deal with the grouped phone numbers
				.stream()
				.sorted(PHONE_CALL_LIST_COMPARATOR) //Sort by longest duration/phone number
				.skip(1) //Remove the longest total duration
				.flatMap(List::stream) //flat map to a list of calls
				.reduce(0, (curCost, call) -> curCost + costCalc(call) , (curCost, addCost) -> curCost + addCost); //Return the total cost of all calls, calculated individually
	}

	private int costCalc(PhoneCall phoneCall) {
		return phoneCall.totalSeconds >= FIVE_MINUTE_CUT_OFF
				? ((phoneCall.hours * 60) + (phoneCall.minutes) + (phoneCall.seconds > 0 ? 1 : 0)) * CENTS_PER_STARTED_MINUTE
				: ((phoneCall.minutes * 60) + (phoneCall.seconds)) * CENTS_PER_SECOND;
	}

	private static class PhoneCall {
		final String phoneNumber;
		final int hours;
		final int minutes;
		final int seconds;
		final int totalSeconds;

		PhoneCall(List<String> values) {
			phoneNumber = values.get(1);
			String[] durationArray = values.get(0).split(":");
			hours = Integer.valueOf(durationArray[0]);
			minutes = Integer.valueOf(durationArray[1]);
			seconds = Integer.valueOf(durationArray[2]);
			totalSeconds = (hours * 3600) + (minutes * 60) + (seconds);
		}
	}
}

It's not a perfect solution, but it's easy to understand. It is important to note that the comparator MUST sort by the smaller of the two phone numbers (numerically) because while two numbers could have the same duration their total costs could be wildly different.

public static void main(String... args) {
	String INPUT = "00:01:07,400-234-090\n" +
            "00:06:07,701-080-080\n" +
            "00:05:00,400-234-090";
    System.out.println(new PhoneBillCalculator().calculatePhoneBill(INPUT));
}

In this example, 400-234-090 and 701-080-080 have the same duration, 00:06:07, but their total costs are very different. Because calls over 5 minutes charge you for whole minutes as soon as you start a minute, 00:06:07 comes out to 1050 cents (7 * 150), but 00:05:00 and 00:01:07 comes out to 951 cents ((60 * 150) + (67 * 3)).

Anyway, this was a fun little exercise and I'd love to see other solutions!

About the author
Photo by Roman Mager on Unsplash