如何根据项目属性的总和对给定限制进行分区?

时间:2019-04-06 22:32:11

标签: java functional-programming 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的{​​{1}}),并且“集合”包含该年,月,日和每个文件的sizeInBytes之和小于modifiedDate

是否可以在避免循环并使用Stream API或其他可用函数构造的同时完成这两项操作?都可以在一个语句中完成吗?

1 个答案:

答案 0 :(得分:1)

您可以在StreamEx中尝试StreamEx.collapse(...)。以下是示例代码:

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);

下面是嵌套groupingBy的问题第二部分的解决方案:

// 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-Util

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))));