Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sort with one element at the end

I have a list of objects and I want to sort it alphabetically by an attribute. But I want to add an exception rule if this attribute matches a specific string. For example:

public class Car {
  String name;
}
List<Car> cars = asList(
    new Car("Unassigned"), 
    new Car("Nissan"), 
    new Car("Yamaha"), 
    new Car("Honda"));

List<Car> sortedCars = cars
  .stream
  .sorted(Comparator.comparing(Car::getName))
  .collect(Collectors.toList());

If cars.name == "Unassigned" then this car should remain at the end of the list and the result would be:

[Car<Honda>, Car<Nissan>, Car<Yamaha>, Car<Unassigned>]
like image 640
bpereira Avatar asked Mar 11 '19 13:03

bpereira


3 Answers

  List<Car> sortedCars = cars
        .stream()
        .sorted(Comparator.comparing(
            Car::getName,
            Comparator.comparing((String x) -> x.equals("Unassigned"))
                      .thenComparing(Comparator.naturalOrder())))
        .collect(Collectors.toList());

There are a lot of things going on here. First I am using Comparator.comparing(Function, Comparator); then (String x) -> x.equals("Unassigned") which actually compares a Boolean (that is Comparable); then the fact that (String x) is used - as this type witness is used to correctly infer the types...

like image 114
Eugene Avatar answered Nov 13 '22 01:11

Eugene


The most direct and and easy-to-read solution is probably to write a custom comparator that implements your sorting logic.

You can still use the Comparator.comparing method to make it a bit prettier, though:

public static final String UNASSIGNED = "Unassigned";

List<Car> cars = List.of(
    new Car("Unassigned"), 
    new Car("Nissan"), 
    new Car("Yamaha"), 
    new Car("Honda"));

List<Car> sortedCars = cars.stream()
    .sorted(Comparator.comparing(Car::getName, (name1, name2) -> {
            if (name1.equals(name2)) return 0;
            if (name1.equals(UNASSIGNED)) return 1;
            if (name2.equals(UNASSIGNED)) return -1;
            return name1.compareTo(name2);
    }))
    .collect(toList());

It is possible to extract the "at-the-end" functionality to a separate comparable combinator method. Like this:

List<Car> sortedCars = cars.stream()
    .sorted(Comparator.comparing(Car::getName, withValueAtEnd(UNASSIGNED)))
    .collect(toList());

public static <T extends Comparable<T>> Comparator<T> withValueAtEnd(T atEnd) {
  return withValueAtEnd(atEnd, Comparator.naturalOrder());
}

public static <T> Comparator<T> withValueAtEnd(T atEnd, Comparator<T> c) {
    return (a, b) -> {
        if (a.equals(atEnd)) return 1;
        if (b.equals(atEnd)) return -1;
        return c.compare(a, b);
    };
}

Also, it's good style to use a named constant for special values like your "Unassigned".


Also, note that if you don't need to keep the unsorted cars list, then you can sort that list in place instead of using a stream:

cars.sort(UNASSIGNED_COMPARATOR);
like image 10
Lii Avatar answered Nov 13 '22 01:11

Lii


You could just replace "Unassigned" with an end-of-alphabet string in your comparator.

Comparator.comparing(car -> car.getName().equals("Unassigned") ? "ZZZ" : car.getName())
like image 1
MikeFHay Avatar answered Nov 13 '22 03:11

MikeFHay