3

Java ArrayList, filled with objects called packinglistrows which hold three key values ( ISBN, PalletNumber, Quantity), along with other properties.

I have this ArrayList with all the same ISBN values in it. I want to be able to merge the items with the same PalletNumbers quantity value.

For example:

ArrayList items = [ packinglistrow( 1234, 1, 10 ), packinglistrow( 1234, 2, 5), packinglistrow( 1234, 1, 15 ) ]

After merge the [0] and [2] objects are merged as they have the same ISBN and pallet number 1. Resulting in a merged object with the updated quantity:

ArrayList items = [ packinglistrow( 1234, 1, 25 ), packinglistrow( 1234, 2, 5) ]

Was thinking loop over and compare and add the different types to new ArrayList then loop over and merge, but there must be a neater way of doing this?

Thanks.

4
  • Java is an OO language. Create a class to hold these values with a method void merge(MyData other). Then have a List<MyData>. Looping is an option, combined with Map<String, MyData> eventually. But really, it would be simpler do to the merging with a class. Commented May 30, 2015 at 14:07
  • Thanks yeah good suggestion, a lack of experience showing there, making a class to do that merge makes sense. Commented May 30, 2015 at 14:10
  • Could you expand on the class option please? The way I can imagine a solution still seems a little too convoluted. As the objects will still be held in an array, they need merging before setting a key as value, maybe sort them by the pallet number then in order, test for same pallet number on consecutive and merge then. Commented May 30, 2015 at 14:24
  • It's a bit to long to fit in a comment, so I've added an answer. Commented May 30, 2015 at 14:40

3 Answers 3

3

First create a class to handle this datas. There's two important points to note. The equals and hashcode method are only based with the isbn and palletNumber values and there is a merge method that returns a new instance of PackingListRow with the quantities between this and an other instance you give as parameter.

class PackingListRow {
    private final String isbn;
    private final int palletNumber;
    private final int quantity;

    public PackingListRow(String isbn, int palletNumber, int quantity) {
        this.isbn = isbn;
        this.palletNumber = palletNumber;
        this.quantity = quantity;
    }

    public String getIsbn() {
        return isbn;
    }

    public int getPalletNumber() {
        return palletNumber;
    }

    public int getQuantity() {
        return quantity;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        PackingListRow that = (PackingListRow) o;
        return Objects.equals(palletNumber, that.palletNumber) &&
                Objects.equals(isbn, that.isbn);
    }

    @Override
    public int hashCode() {
        return Objects.hash(isbn, palletNumber);
    }

    @Override
    public String toString() {
        return "PackingListRow{" +
                "isbn='" + isbn + '\'' +
                ", palletNumber=" + palletNumber +
                ", quantity=" + quantity +
                '}';
    }

    public PackingListRow merge(PackingListRow other) {
        assert(this.equals(other));
        return new PackingListRow(this.isbn, this.palletNumber, this.quantity + other.quantity);
    }
}

Once you have that, you just need to create another new list that is initially empty. It will contains the merged values. For each instance in the initial list, you check whether it is already in the merged list. If yes, you modify the existing instance by calling merge, otherwise you just append it to the list. We end up with the following algorithm:

List<PackingListRow> list =
        Arrays.asList(new PackingListRow("1234", 1, 10), new PackingListRow("1234", 2, 5), new PackingListRow("1234", 1, 15));

List<PackingListRow> mergedList = new ArrayList<>();
for(PackingListRow p : list) {
    int index = mergedList.indexOf(p);
    if(index != -1) {
        mergedList.set(index, mergedList.get(index).merge(p));
    } else {
        mergedList.add(p);
    }
}

System.out.println(mergedList);

Which outputs:

[PackingListRow{isbn='1234', palletNumber=1, quantity=25}, PackingListRow{isbn='1234', palletNumber=2, quantity=5}]

With Java 8, I would maybe use a different strategy (at least you show there are multiple ways to solve the problem). I would create a static class that does the grouping for me:

class PackingListRow {
    private final String isbn;
    private final int palletNumber;
    private final int quantity;

    static class GroupPacking {

        private final String isbn;
        private final int palletNumber;

        public GroupPacking(PackingListRow p) {
            this.isbn = p.isbn;
            this.palletNumber = p.palletNumber;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            GroupPacking that = (GroupPacking) o;
            return Objects.equals(palletNumber, that.palletNumber) &&
                    Objects.equals(isbn, that.isbn);
        }

        @Override
        public int hashCode() {
            return Objects.hash(isbn, palletNumber);
        }
    }

    ....

    public PackingListRow merge(PackingListRow other) {
        assert (new GroupPacking(other).equals(new GroupPacking(this)));
        return new PackingListRow(this.isbn, this.palletNumber, this.quantity + other.quantity);
    }
}

Then you can use the Stream API. Given the original list, you get a Stream<PackingListRow> from which you collect the elements into a Map according by their GroupPacking instances (the keys). The value is simply the current PackingListRow instance. If you have two instances with the same GroupPacking value (according to equals/hashcode), you merge them. You finally get the values() of the map.

List<PackingListRow> mergedList =
                new ArrayList<>(list.stream().collect(toMap(PackingListRow.GroupPacking::new, p -> p, PackingListRow::merge)).values());
Sign up to request clarification or add additional context in comments.

2 Comments

this is one of the very good explanation and showcasing implementation. Saved my day +1
could you please tell me what is the way if I want a different DTO (PackingListRow{isbn='1234', quantity=25}) after merging ? other criterias are same I just want less fields.
0

Here you have working example (Java8 + Google Guava):

package com.rgrebski.test;

import com.google.common.base.*;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Multimaps;
import org.assertj.core.api.Assertions;
import org.testng.annotations.*;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class PackingListRowMergeTest {

    @Test
    public void test() {
        //given
        List<PackingListRow> packingListRow = ImmutableList.of(
                new PackingListRow(1234, 1, 10),
                new PackingListRow(1234, 2, 5),
                new PackingListRow(1234, 1, 15)
        );

        //when
        List<PackingListRow> mergedPackingListRow = Multimaps.index(packingListRow, groupByPaletNumbers())
                .asMap()
                .values()
                .stream()
                .map(packingListRowCollectionToSinglePackingListRowWithQuantitySum())
                .collect(Collectors.toList());

        //then
        Assertions.assertThat(mergedPackingListRow).containsExactly(
                new PackingListRow(1234, 1, 25),
                new PackingListRow(1234, 2, 5)
                );
    }

    protected java.util.function.Function<Collection<PackingListRow>, PackingListRow> packingListRowCollectionToSinglePackingListRowWithQuantitySum() {
        return new java.util.function.Function<Collection<PackingListRow>, PackingListRow>() {
            @Override
            public PackingListRow apply(Collection<PackingListRow> packingListRows) {
                int quantitySum = packingListRows.stream().flatMapToInt(packingListRow -> IntStream.of(packingListRow.getQuantity())).sum();
                PackingListRow firstPackingListRow = packingListRows.stream().findFirst().get();

                return new PackingListRow(firstPackingListRow.getIsbn(), firstPackingListRow.getPaletNumber(), quantitySum);
            }
        };
    }

    private Function<? super PackingListRow, Integer> groupByPaletNumbers() {
        return new Function<PackingListRow, Integer>() {
            @Override
            public Integer apply(PackingListRow input) {
                return input.getPaletNumber();
            }
        };
    }

    private static class PackingListRow {

        private int isbn;
        private int paletNumber;
        private int quantity;

        public PackingListRow(int isbn, int paletNumber, int quantity) {

            this.isbn = isbn;
            this.paletNumber = paletNumber;
            this.quantity = quantity;
        }


        public int getIsbn() {
            return isbn;
        }

        public int getPaletNumber() {
            return paletNumber;
        }

        public int getQuantity() {
            return quantity;
        }


        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            PackingListRow that = (PackingListRow) o;

            return Objects.equal(this.isbn, that.isbn) &&
                    Objects.equal(this.paletNumber, that.paletNumber) &&
                    Objects.equal(this.quantity, that.quantity);
        }

        @Override
        public int hashCode() {
            return Objects.hashCode(isbn, paletNumber, quantity);
        }

        @Override
        public String toString() {
            return MoreObjects.toStringHelper(this)
                    .add("isbn", isbn)
                    .add("paletNumber", paletNumber)
                    .add("quantity", quantity)
                    .toString();
        }
    }
}

Comments

0

It seems reasonable to create an object just for ISBN & pallet number, but keep the quantity separately as you consider them equal if ISBN & pallet number are equal. So the object might look like this:

public class PackingListRow {
    private final String isbn;
    private final int palletNumber;

    public PackingListRow(String isbn, int palletNumber) {
        this.isbn = isbn;
        this.palletNumber = palletNumber;
    }

    @Override
    public int hashCode() {
        return palletNumber * 31 + ((isbn == null) ? 0 : isbn.hashCode());
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        PackingListRow other = (PackingListRow) obj;
        if (isbn == null) {
            if (other.isbn != null)
                return false;
        } else if (!isbn.equals(other.isbn))
            return false;
        if (palletNumber != other.palletNumber)
            return false;
        return true;
    }

    @Override
    public String toString() {
        return isbn+":"+palletNumber;
    }
}

After that you can store the results in the Map object and add items via method like this:

public static void addItem(Map<PackingListRow, Integer> items, PackingListRow row, 
                           int quantity) {
    Integer oldQuantity = items.get(row);
    items.put(row, oldQuantity == null ? quantity : quantity+oldQuantity);
}

Or much simpler if you are using Java 8:

public static void addItem(Map<PackingListRow, Integer> items, PackingListRow row, 
                           int quantity) {
    items.merge(row, quantity, Integer::sum);
}

Testing on your sample data:

public static void main(String[] args) {
    Map<PackingListRow, Integer> items = new HashMap<PackingListRow, Integer>();
    addItem(items, new PackingListRow("1234", 1), 10);
    addItem(items, new PackingListRow("1234", 2), 5);
    addItem(items, new PackingListRow("1234", 1), 15);
    System.out.println(items);
}

The output is:

{1234:1=25, 1234:2=5}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.