Compact a comma delimited number list into ranges
I can only think about a custom collector... You can obviously create a method that would return this collector and the code would be really compact in this case, provided that the collector is hidden via a static factory method.
Notice how the combiner
is doing basically nothing, not good for parallel coding. I'm still trying to think of a good way to provide an implementation for it.
List<String> result = IntStream.of(1, 2, 3, 4, 5, 12, 13, 14, 19)
.boxed()
.collect(Collector.of(
() -> {
List<List<Integer>> list = new ArrayList<>();
list.add(new ArrayList<>());
return list;
},
(list, x) -> {
List<Integer> inner = list.get(list.size() - 1);
if (inner.size() == 0) {
inner.add(x);
} else {
int lastElement = inner.get(inner.size() - 1);
if (lastElement == x - 1) {
inner.add(x);
} else {
List<Integer> oneMore = new ArrayList<>();
oneMore.add(x);
list.add(oneMore);
}
}
},
(left, right) -> {
throw new IllegalArgumentException("No parallel!");
},
list -> {
return list.stream()
.map(inner -> {
if (inner.size() > 1) {
return inner.get(0) + "-" + inner.get(inner.size() - 1);
}
return "" + inner.get(0);
}).collect(Collectors.toList());
}));
System.out.println(result);
Now that we have seen several Stream variants, here the non-Stream variant for comparison:
private static StringBuilder appendRange(StringBuilder sb, int start, int previous) {
sb.append(start);
if(start!=previous) sb.append(previous-start>1? " - ": ", ").append(previous);
return sb;
}
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 12, 13, 14, 19);
StringBuilder sb = new StringBuilder();
int previous = list.get(0), start = previous;
for(int next: list.subList(1, list.size())) {
if(previous+1 != next) {
appendRange(sb, start, previous).append(", ");
start = next;
}
previous = next;
}
String result = appendRange(sb, start, previous).toString();
Edit
I'm sorry, I misunderstand your requirement since my English is so bad. Thank everybody of forgiveness. I'll give a configurable compress
method later to thank everybody.
After working, I found I can't apply your rule above by use stream easily: "the count of numbers in the range is 3 or more." . so I down to the traditional approach. I wish it can helped you.
// v--- "1-5, 12-14, 19"
String ranges = compress(asList(1,2,3,4,5, 12,13,14, 19)).collect(joining(", "));
// v--- ["1", "2"]
Stream<String> lessThan3 = compress(asList(1, 2));
// v--- ["1-4"]
Stream<String> step2 = compress(asList(1, 3, 4), 2, 3);
Build the range of Stream<String>
immediately by using Stream.Builder
.
static Stream<String> compress(List<Integer> numbers) {
return compress(numbers, 1, 3);
}
static Stream<String> compress(List<Integer> numbers, int step, int minSize) {
Builder<String> ranges = Stream.builder();
IntBuffer queue = IntBuffer.allocate(minSize + 1);
for (int it : numbers) {
int prev = queue.position() - 1;
if (prev >= 0 && queue.get(prev) + step < it) {
copy(queue, ranges, minSize);
queue.put(it);
} else {
if (queue.hasRemaining()) {
queue.put(it);
} else {
queue.put(prev, it);
}
}
}
return copy(queue, ranges, minSize).build();
}
static Builder<String> copy(IntBuffer queue, Builder<String> target, int minSize) {
queue.flip();
if (queue.limit() >= minSize) {
target.add(format("%d-%d", queue.get(0), queue.get(queue.limit() - 1)));
} else {
while (queue.hasRemaining()) target.add(Integer.toString(queue.get()));
}
queue.clear();
return target;
}
Edit2
Build the range of Stream<String>
lazily by using Spliterator
.
static Stream<String> compress(List<Integer> numbers, int step, int minSize) {
return compress(numbers, minSize, (prev, current) -> current - prev <= step);
}
static Stream<String> compress(List<Integer> numbers,
int minSize,
IntBiPredicate rule) {
return StreamSupport.stream(spliterator(numbers, minSize, rule), false);
}
static AbstractSpliterator<String> spliterator(List<Integer> numbers,
int minSize,
IntBiPredicate rule) {
return new AbstractSpliterator<String>(numbers.size(), ORDERED) {
private Iterator<Integer> data;
private Queue<String> queue;
private IntBuffer buff;
@Override
public boolean tryAdvance(Consumer<? super String> action) {
init();
return tryConsuming(action) || evaluate();
}
private void init() {
if (data != null) return;
data = numbers.iterator();
queue = new LinkedList<>();
buff = IntBuffer.allocate(minSize + 1);
}
private boolean tryConsuming(Consumer<? super String> action) {
if (queue.isEmpty()) return false;
action.accept(queue.poll());
return true;
}
private boolean evaluate() {
if (!data.hasNext()) {
return buff.position() > 0 && fill();
} else {
evaluateNext(data.next());
return true;
}
}
private void evaluateNext(int it) {
int prev = buff.position() - 1;
if (prev >= 0 && !rule.test(buff.get(prev), it)) {
fill();
buff.put(it);
} else {
if (!buff.hasRemaining()) {
buff.put(buff.position() - 1, it);
} else {
buff.put(it);
}
}
}
private boolean fill() {
buff.flip();
if (buff.limit() >= minSize) {
int min = buff.get(0);
int max = buff.get(buff.limit() - 1);
queue.add(format("%d-%d", min, max));
} else {
while (buff.hasRemaining()) {
queue.add(Integer.toString(buff.get()));
}
}
buff.clear();
return true;
}
};
}
interface IntBiPredicate {
boolean test(int first, int second);
}
Oldest
How about this? String
ranges are grouped by n/m
:
int m = 5 + 1;
// v--- "1-5, 12-14, 19"
String ranges =
Stream.of(1, 2, 3, 4, 5, 12, 13, 14, 19)
// v--- calculate ranges until grouping is done
.collect(collectingAndThen(
groupingBy(
// v--- group n by n/m
n -> n / m,
TreeMap::new,
// v--- summarizing the current group
summarizingInt(Integer::intValue)
),
summary -> summary.values()
.stream()
.map(
//create range string from IntSummaryStats ---v
it ->String.format(
it.getMin()==it.getMax()?"%d":"%d-%d",
it.getMin(),
it.getMax()
)
)
.collect(joining(", "))
));