3

I need to sort a List of items based on a List of Filters, which are ordered by priority. However, these Filters come from the API request body, so they can change.

I have a Filter class

public class Filter {
  private String fieldName;
  private String order;

  // Getters and Setters...
}

Some Filter Objects

Filter filter1 = new Filter("price", "desc");
Filter filter2 = new Filter("size", "asc");

My Item class goes like this:

public class Item {
  private String productName;
  private double size;
  private double price;

  // Getters and Setters...
}

Then I have to sort the Items like this:

If an Item.price equals the next Item, compare their size, and so on...

I already tried creating a Comparator for each Filter, but I'm unable to chain them, so each filter sorts the List on it's own, with no regard to the previous sorting method (sometimes flipping the whole List upside down).

I also tried implementing the Comparable interface on the Item class, but the interface method compareTo accepts only a single parameter(the next Item), but not a List of rules.

So given a List of Items like

List<Item> items = new ArrayList<Item>(
  new Item("foo", 10.0, 5.0),
  new Item("bar", 6.0, 15.0),
  new Item("baz", 7.0, 5.0)
);

And a List of Filters like

List<Filter> filters = new ArrayList<Filter>(
  new Filter("price", "desc"),
  new Filter("size", "asc")
);

I'd expect the result to be

List<Item> sortedItems = new ArrayList<Item>(
  new Item("bar", 6.0, 15.0),
  new Item("baz", 7.0, 5.0),
  new Item("foo", 10.0, 5.0)
);

Would you please help me? Thanks in advance!

IMPORTANT: I'm not having a problem with the comparison of the fields themselves. My problem is with making a dynamic Comparator that changes its comparisons based on a List of Filters.

2 Answers2

4

I believe the following should push you in the right direction in chaining the comparators for the item given variable filter comparisons. It uses reflection to call the getters and assumes that the comparisons are with doubles.

If they are not guaranteed to be doubles, then adapt the reflection casting to cast to something that works for all your use cases.

PropertyUtils.getProperty() is a part of Apache Commons BeanUtils and can be replaced by your choice of getting the values, whether it be through reflection or static comparators.

public class Filter {

    // properties, constructors, getters, setters ...

    public Comparator<Item> itemComparator() {
        return (item1, item2) -> {
            Double val1 = (Double) PropertyUtils.getProperty(item1, fieldName);
            Double val2 = (Double) PropertyUtils.getProperty(item2, fieldName);
            return (order.equals("asc") ? val1.compareTo(val2) : val2.compareTo(val1);
        };
    }

    public static Comparator<Item> chainedItemComparators(List<Filter> filters) {
        return filters.stream()
            .map(Filter::itemComparator)
            .reduce((item1, item2) -> 0, (f1, f2) -> f1.thenComparing(f2));
    }
}

To then use the chained comparator:

public static void main(String[] args) {
    List<Filter> filters = new ArrayList<>(Arrays.asList(
        new Filter("price", "desc"),
        new Filter("size", "asc")
    ));
    List<Item> items = new ArrayList<>(Arrays.asList(
        new Item("bar", 6.0, 15.0),
        new Item("baz", 7.0, 5.0),
        new Item("foo", 10.0, 5.0)
    ));
    items.sort(Filter.chainedItemComparators(filters));
}
theawesometrey
  • 389
  • 2
  • 11
1

You can compose comparators, given price-based and size-based initial implementations.

The following method creates an item comparator for a given filter:

//Preferring static implementations to reflection-based ones
//if there are too many fields, you may want to use a Map<String, Comparator<Item>>
static Comparator<Item> priceComparator = Comparator.comparing(Item::getPrice);
static Comparator<Item> sizeComparator = Comparator.comparingDouble(Item::getSize);

private static Comparator<Item> itemComparator(Filter filter) {
    Comparator<Item> comparator = "price".equals(filter.getFieldName()) ? 
                                     priceComparator : sizeComparator;

    if ("desc".equals(filter.getOrder()))
        return comparator.reversed();

    return comparator;
}

From that you can chain comparators from a list of filters:

public static void main(String args[]) {

    Comparator<Item> comparator = itemComparator(filters.get(0));
    for (Filter f : filters.subList(1, filters.size())) {
        comparator = comparator.thenComparing(itemComparator(f));
    }

    items.sort(comparator);
}

Testing that, I get the following (expected) output:

[[productName=bar, size=6.0, price=15.0],
 [productName=baz, size=7.0, price=5.0],
 [productName=foo, size=10.0, price=5.0]]
ernest_k
  • 44,416
  • 5
  • 53
  • 99
  • 1
    also it's possible to use https://commons.apache.org/proper/commons-collections/apidocs/org/apache/commons/collections4/ComparatorUtils.html for chained comparators – ema Feb 25 '21 at 21:13