Как разделить коллекцию на основе суммирования свойств элемента до заданного предела?

#java #functional-programming #java-stream

#java #функциональное программирование #java-stream

Вопрос:

Как я могу разбить коллекцию на N блоков, основываясь на суммировании одного из полей каждого элемента в коллекции, до заданного максимального значения?

Например. учитывая следующее:

 class FileObject { public long sizeInBytes; }
Collection<FileObject> files;
long MAX_SIZE_THRESHOLD = 1024 * 1024 * 100; // 100 MB
  

Я хотел бы преобразовать элементы в Collection<Collection<FileObject>> с наименьшим количеством внутренних коллекций и удовлетворить предикату, что для каждой коллекции сумма sizeInBytes каждого элемента меньше MAX_SIZE_THRESHOLD .

Принимая это во внимание, в дополнение к вышеуказанному требованию, если FileObject будет расширено, чтобы содержать временную метку, я также хотел бы разделить результат по годам, месяцам и дням.

Например.

 class FileObject { public long sizeInBytes; public long modifiedDate; }
  

Я бы хотел, чтобы конечный результат выглядел как:

 Map<Integer, Map<Integer, Map<Integer, Collection<FileObject>>>>
  

где ключами в картах являются: год, месяц и день (соответствующие FileObject ‘s modifiedDate ), а Коллекция содержит все файлы в течение этого года, месяца, дня и где сумма байтов каждого файла меньше MAX_SIZE_THRESHOLD .

Можно ли выполнить обе операции, избегая циклов и используя функциональные конструкции, доступные с помощью Stream API или другого? Можно ли выполнить оба в одном операторе?

Комментарии:

1. одиночный оператор — нет, пользовательский коллектор — да, но в любом случае это было бы похоже на то, что вы бы написали с циклом; параллельно вам пришлось бы объединить Map s, кажется, и это дорого

Ответ №1:

Вы можете попробовать StreamEx.collapse(...) в StreamEx. Вот примеры кодов:

 final long MAX_SIZE_THRESHOLD = 12; // only for test purpose.

// create the sample file objects with random size for test.
Collection<FileObject> files =
    new Random().longs(0, 1000).limit(50).mapToObj(n -> new FileObject(n % 15, n))
    .collect(Collectors.toList());

// here is the final solution you can try
final MutableLong remaining = MutableLong.of(MAX_SIZE_THRESHOLD);

List<List<FileObject>> result = StreamEx.of(files).collapse((a, b) -> {
  if (b.sizeInBytes <= remaining.value() - a.sizeInBytes) {
    remaining.subtract(a.sizeInBytes);
    return true;
  } else {
    remaining.setValue(MAX_SIZE_THRESHOLD);
    return false;
  }
}, Collectors.toList()).toList();

result.forEach(System.out::println);
  

И вот решение с помощью nested groupingBy для части 2 вашего вопроса:

 // import static java.util.stream.Collectors.*
Map<Integer, Map<Integer, Map<Integer, List<FileObject>>>> result2 = files.stream()
    .filter(f -> f.sizeInBytes < MAX_SIZE_THRESHOLD)
    .collect(groupingBy(f -> f.getYear(), 
                        groupingBy(f -> f.getMonth(), 
                                        groupingBy(f -> f.getDay(), toList()))));

result2.entrySet().forEach(System.out::println);
  

Наконец, вот FileObject , который я использовал для тестирования:

 static class FileObject {
  public long sizeInBytes;
  public long modifiedDate;

  public FileObject(long sizeInBytes, long modifiedDate) {
    this.sizeInBytes = sizeInBytes;
    this.modifiedDate = modifiedDate;
  }

  public int getYear() {
    return (int) modifiedDate / 100; // only for test purpose
  }

  public int getMonth() {
    return (int) (modifiedDate % 100) / 10; // only for test purpose
  }

  public int getDay() {
    return (int) modifiedDate % 10; // only for test purpose
  }

  @Override
  public String toString() {
    return sizeInBytes   "-"   modifiedDate;
  }
}
  

Обновлено на основе комментариев:

Вам понадобится Collectors.collectAndThen .

 Function<List<FileObject>, List<List<FileObject>>> finisher = fileObjs -> {
  MutableLong remaining2 = MutableLong.of(MAX_SIZE_THRESHOLD);
  return StreamEx.of(fileObjs).collapse((a, b) -> {
    if (b.sizeInBytes <= remaining2.value() - a.sizeInBytes) {
      remaining2.subtract(a.sizeInBytes);
      return true;
    } else {
      remaining2.setValue(MAX_SIZE_THRESHOLD);
      return false;
    }
  }, toList()).toList();
};

Map<Integer, Map<Integer, Map<Integer, List<List<FileObject>>>>> result4 = files.stream()
    .collect(groupingBy(f -> f.getYear(),
        groupingBy(f -> f.getMonth(), 
            groupingBy(f -> f.getDay(), collectingAndThen(toList(), finisher)))));
  

И тип результата должен быть Map<Integer, Map<Integer, Map<Integer, List<List<FileObject>>>>> , а не Map<Integer, Map<Integer, Map<Integer, List<FileObject>>>> .

Кстати, если вы не хотите писать finisher функцию (я этого не делаю :-)), попробуйте мою библиотеку: abacus-common:

 Function<List<FileObject>, List<List<FileObject>>> finisher2 = fileObjs -> Seq.of(fileObjs)
    .split(MutableLong.of(0), (f, sizeSum) -> sizeSum.addAndGet(f.sizeInBytes) <= MAX_SIZE_THRESHOLD,
        sizeSum -> sizeSum.setValue(0));

// import static com.landawn.abacus.util.stream.Collectors.MoreCollectors.*;
StreamEx.of(files)
    .toMap(f -> f.getYear(),
        groupingBy(f -> f.getMonth(),
            groupingBy(f -> f.getDay(), collectingAndThen(toList(), finisher2))));
  

Комментарии:

1. StreamEx::collapse выглядит очень интересно, спасибо! Что касается второй части вопроса, то я искал, чтобы общий размер List<FileObject> был меньше MAX_SIZE_THRESHOLD, а не каждого отдельного FileObject. Другими словами, включение первой части вопроса.