Как ранжировать коллекцию объектов

У меня, например, класс

class Person{
    Integer rank;
    Double profit;
    Person(Integer rank, Double profit){
        this.rank = rank;
        this.profit = profit;
    }
    Person(Double profit){
        this(0, profit);
    }
}

Я хочу ранжировать список отсортированных по прибыли лиц с оценкой рейтинга. Чтобы

rank(Arrays.asList(
    new Person(30), 
    new Person(20), 
    new Person(20), 
    new Person(10))
)

будет произведен в список

new Person(1, 30), 
new Person(2, 20), 
new Person(2, 20), 
new Person(3, 10)

Также я хочу сделать это с помощью пользовательских Collector (или что-то подобное) из Java 8 и не использовать простые циклы.

2 ответа

Решение

Давайте использовать специальный коллектор, которому мы передаем весь материал извне. Коллекционер получит рейтинг и конструктор от звонящего. Мы хотели бы назвать это так:

public List<Person> rank(List<Person> people) {
        return people
                .stream()
                .sorted(Comparator.<Person>comparingDouble(x -> x.profit).reversed())
                .collect(new IntegerRankingCollector<>(
                        Comparator.comparingDouble(p -> p.profit),  // how to differentiate rankings
                        p -> p.rank,    // where to get rank for an element which was already ranked
                        (p, rank) -> new Person(rank, p.profit)     // how to create an element from another element values and a rank
                ));
    }

Этот коллектор может быть реализован как Collectors.toList() но с помощью метода аккумулятора, который:

  1. получает ранг предыдущего элемента
  2. увеличивает его, если текущий ранг элемента должен отличаться от предыдущего ранга элемента
  3. создает элемент с новым рангом

Вот как это выглядит и должно работать для упорядоченных потоков:

public class IntegerRankingCollector<T> implements Collector<T, List<T>, List<T>> {
        ...

    public IntegerRankingCollector(Comparator<? super T> comparator, Function<T, Integer> ranker, BiFunction<T, Integer, T> creator) {
        this.comparator = comparator;
        this.ranker = ranker;
        this.creator = creator;
    }

    @Override
    public BiConsumer<List<T>, T> accumulator() {
        return (list, current) -> {
            ArrayList<T> right = new ArrayList<>();
            right.add(creator.apply(current, 1));
            combiner().apply(list, right);
        };
    }

    @Override
    public BinaryOperator<List<T>> combiner() {
        return (left, right) -> {
            int rankAdjustment = getRankAdjustment(left, right);
            for (T t : right)
                left.add(creator.apply(t, rankAdjustment + ranker.apply(t)));
            return left;
        };
    }

    private int getRankAdjustment(List<T> left, List<T> right) {
        Optional<T> lastElementOnTheLeft = optGet(left, left.size() - 1);
        Optional<T> firstElementOnTheRight = optGet(right, 0);

        if (!lastElementOnTheLeft.isPresent() || !firstElementOnTheRight.isPresent())
            return 0;
        else if (comparator.compare(firstElementOnTheRight.get(), lastElementOnTheLeft.get()) == 0)
            return ranker.apply(lastElementOnTheLeft.get()) - 1;
        else
            return ranker.apply(lastElementOnTheLeft.get());
    }

    private Optional<T> optGet(List<T> list, int index) {
        if (list == null || list.isEmpty())
            return Optional.empty();
        else
            return Optional.of(list.get(index));
    }

        ...
    }

Для полноты, это полный код класса. Я скопировал все остальное Collectors.toList:

public class IntegerRankingCollector<T> implements Collector<T, List<T>, List<T>> {

    private static final Set<Characteristics> CHARACTERISTICSS = Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH));
    private Comparator<? super T> comparator;
    private BiFunction<T, Integer, T> creator;
    private Function<T, Integer> ranker;

    public IntegerRankingCollector(Comparator<? super T> comparator, Function<T, Integer> ranker, BiFunction<T, Integer, T> creator) {
        this.comparator = comparator;
        this.ranker = ranker;
        this.creator = creator;
    }

    @Override
    public BiConsumer<List<T>, T> accumulator() {
        return (list, current) -> {
            ArrayList<T> right = new ArrayList<>();
            right.add(creator.apply(current, 1));
            combiner().apply(list, right);
        };
    }

    @Override
    public BinaryOperator<List<T>> combiner() {
        return (left, right) -> {
            int rankAdjustment = getRankAdjustment(left, right);
            for (T t : right)
                left.add(creator.apply(t, rankAdjustment + ranker.apply(t)));
            return left;
        };
    }

    private int getRankAdjustment(List<T> left, List<T> right) {
        Optional<T> lastElementOnTheLeft = optGet(left, left.size() - 1);
        Optional<T> firstElementOnTheRight = optGet(right, 0);

        if (!lastElementOnTheLeft.isPresent() || !firstElementOnTheRight.isPresent())
            return 0;
        else if (comparator.compare(firstElementOnTheRight.get(), lastElementOnTheLeft.get()) == 0)
            return ranker.apply(lastElementOnTheLeft.get()) - 1;
        else
            return ranker.apply(lastElementOnTheLeft.get());
    }

    private Optional<T> optGet(List<T> list, int index) {
        if (list == null || list.isEmpty())
            return Optional.empty();
        else
            return Optional.of(list.get(index));
    }


    @Override
    public Supplier<List<T>> supplier() {
        return ArrayList::new;
    }

    @Override
    public Function<List<T>, List<T>> finisher() {
        return l -> l;
    }

    @Override
    public Set<Characteristics> characteristics() {
        return CHARACTERISTICSS;
    }
}

Вы можете сделать это так:

  1. сначала отсортируйте список с помощью пользовательских Comparator;
  2. составить новый список с прибылью без дубликатов (distinct() метод);
  3. установить соответствующие ранги, используя forEach(),

Я надеюсь, что это будет полезно.

List<Person> l  = Arrays.asList(new Person(30.0), new Person(20.0), new Person(20.0), new Person(10.0));
Collections.sort(l,(Person o1, Person o2)->o1.profit.compareTo(o2.profit));
List<Double> p = l.stream().map(a -> a.profit).distinct().collect(Collectors.toList());
l.forEach(a -> a.setRank(p.indexOf(a.profit) + 1));
Другие вопросы по тегам