Javier Marsicano
Javier Marsicano

Reputation: 68

Java functional approach to join identical elements into a single one

I would like to use Streams API to process a call log and calculate the total billing amount for the same phone number. Here's the code that achieves it with a hybrid approach but I would like to use fully functional approach:

List<CallLog> callLogs = Arrays.stream(S.split("\n"))
                    .map(CallLog::new)
                    .sorted(Comparator.comparingInt(callLog -> callLog.phoneNumber))
                    .collect(Collectors.toList());

            for (int i = 0; i< callLogs.size() -1 ;i++) {
                if (callLogs.get(i).phoneNumber == callLogs.get(i+1).phoneNumber) {
                    callLogs.get(i).billing += callLogs.get(i+1).billing;
                    callLogs.remove(i+1);
                }
            }

Upvotes: 1

Views: 64

Answers (5)

Jan Held
Jan Held

Reputation: 654

You could group the billing amount by phoneNumber, like VLAZ said. An example implementation could look something like this:

import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;

public class Demo {

    public static void main(String[] args) {

        final String s = "555123456;12.00\n"
                + "555123456;3.00\n"
                + "555123457;1.00\n"
                + "555123457;2.00\n"
                + "555123457;5.00";

        final Map<Integer, Double> map = Arrays.stream(s.split("\n"))
                .map(CallLog::new)
                .collect(Collectors.groupingBy(CallLog::getPhoneNumber, Collectors.summingDouble(CallLog::getBilling)));

        map.forEach((key, value) -> System.out.printf("%d: %.2f\n", key, value));
    }

    private static class CallLog {

        private final int phoneNumber;
        private final double billing;

        public CallLog(int phoneNumber, double billing) {
            this.phoneNumber = phoneNumber;
            this.billing = billing;
        }

        public CallLog(String s) {
            final String[] strings = s.split(";");
            this.phoneNumber = Integer.parseInt(strings[0]);
            this.billing = Double.parseDouble(strings[1]);
        }

        public int getPhoneNumber() {
            return phoneNumber;
        }

        public double getBilling() {
            return billing;
        }
    }
}

which produces the following output:

555123456: 15.00
555123457: 8.00

Upvotes: 0

corlaez
corlaez

Reputation: 1423

What you are trying to do is to remove duplicate phone numbers, while adding their billing. The one thing streams are incompatible with are remove operations. So how can we do what you need without remove?

Well instead of sorting I would go with groupingBy phone numbers then I would map the list of groups of callLogs into callLogs with the billing already accumulated.

Upvotes: 0

Maxim Popov
Maxim Popov

Reputation: 1227

Map<Integer, Integer> result = Arrays.stream(S.split("\n"))
                    .map(CallLog::new)
                    .sorted(Comparator.comparingInt(callLog -> callLog.phoneNumber))
                    .collect(Collectors.toMap(
                        c -> c.phoneNumber(),
                        c -> c.billing(),
                        (a, b) -> a+b
                     ));

And if you want to have a 'List callLogs' as a result:

List<CallLog> callLogs = Arrays.stream(S.split("\n"))
                        .map(CallLog::new)
                        .collect(Collectors.toMap(
                            c -> c.phoneNumber(),
                            c -> c.billing(),
                            (a, b) -> a+b
                         ))
                        .entrySet()
                        .stream()
                        .map(entry -> toCallLog(entry.getKey(), entry.getValue()))
                        .sorted(Comparator.comparingInt(callLog -> callLog.phoneNumber))
                        .collect(Collectors.toList())

Upvotes: 1

Ryuzaki L
Ryuzaki L

Reputation: 40038

You can use Collectors.groupingBy to group CallLog object by phoneNumber with Collectors.summingInt to sum the billing of grouped elements

Map<Integer, Integer> likesPerType = Arrays.stream(S.split("\n"))
                                           .map(CallLog::new)
                 .collect(Collectors.groupingBy(CallLog::getPhoneNumber, Collectors.summingInt(CallLog::getBilling)));

Upvotes: 3

VLAZ
VLAZ

Reputation: 28986

You can save yourself the sorting -> collection to list -> iterating the list for values next to each other if you instead do the following

  1. Create all CallLog objects.
  2. Merge them by the phoneNumber field
    • combine the billing fields every time
  3. Return the already merged items

This can be done using Collectors.toMap(Function, Function, BinaryOperator) where the third parameter is the merge function that defines how items with identical keys would be combined:

Collection<CallLog> callLogs = Arrays.stream(S.split("\n"))
  .map(CallLog::new)
  .collect(Collectors.toMap( //a collector that will produce a map
    CallLog::phoneNumber,    //using phoneNumber as the key to group
    x -> x,                  //the item itself as the value
    (a, b) -> {              //and a merge function that returns an object with combined billing
      a.billing += b.billing;
      return a;
    }))
  .values(); //just return the values from that map

In the end, you would have CallLog items with unique phoneNumber fields whose billing field is equal to the combination of all billings of the previously duplicate phoneNumbers.

Upvotes: 0

Related Questions