mirror of
https://github.com/dlang/phobos.git
synced 2025-05-01 15:40:36 +03:00
Merge pull request #3005 from Poita/Issue14183
Fix Issue 14183 - Changes to groupBy
This commit is contained in:
parent
99956f9060
commit
afd30b41b0
3 changed files with 136 additions and 72 deletions
|
@ -7,13 +7,15 @@ $(BOOKTABLE Cheat Sheet,
|
|||
|
||||
$(TR $(TH Function Name) $(TH Description))
|
||||
|
||||
$(T2 aggregate,
|
||||
$(D [[3, 1, 5], [2, 6, 4]].aggregate!max) returns a range containing
|
||||
the elements $(D 5) and $(D 6).)
|
||||
$(T2 cache,
|
||||
Eagerly evaluates and caches another range's $(D front).)
|
||||
$(T2 cacheBidirectional,
|
||||
As above, but also provides $(D back) and $(D popBack).)
|
||||
$(T2 chunkyBy,
|
||||
$(D chunkyBy!((a,b) => a[1] == b[1])([[1, 1], [1, 2], [2, 2], [2, 1]]))
|
||||
returns a range containing 3 subranges: the first with just
|
||||
$(D [1, 1]); the second with the elements $(D [1, 2]) and $(D [2, 2]);
|
||||
and the third with just $(D [2, 1]).)
|
||||
$(T2 each,
|
||||
$(D each!writeln([1, 2, 3])) eagerly prints the numbers $(D 1), $(D 2)
|
||||
and $(D 3) on their own lines.)
|
||||
|
@ -26,11 +28,6 @@ $(T2 filterBidirectional,
|
|||
$(T2 group,
|
||||
$(D group([5, 2, 2, 3, 3])) returns a range containing the tuples
|
||||
$(D tuple(5, 1)), $(D tuple(2, 2)), and $(D tuple(3, 2)).)
|
||||
$(T2 groupBy,
|
||||
$(D groupBy!((a,b) => a[1] == b[1])([[1, 1], [1, 2], [2, 2], [2, 1]]))
|
||||
returns a range containing 3 subranges: the first with just
|
||||
$(D [1, 1]); the second with the elements $(D [1, 2]) and $(D [2, 2]);
|
||||
and the third with just $(D [2, 1]).)
|
||||
$(T2 joiner,
|
||||
$(D joiner(["hello", "world!"], "; ")) returns a range that iterates
|
||||
over the characters $(D "hello; world!"). No new string is created -
|
||||
|
@ -66,10 +63,9 @@ import std.functional; // : unaryFun, binaryFun;
|
|||
import std.range.primitives;
|
||||
import std.traits;
|
||||
|
||||
///
|
||||
template aggregate(fun...) if (fun.length >= 1)
|
||||
{
|
||||
/**
|
||||
/* --Intentionally not ddoc--
|
||||
* Aggregates elements in each subrange of the given range of ranges using
|
||||
* the given aggregating function(s).
|
||||
* Params:
|
||||
|
@ -90,7 +86,6 @@ template aggregate(fun...) if (fun.length >= 1)
|
|||
return ror.map!(reduce!fun);
|
||||
}
|
||||
|
||||
///
|
||||
unittest
|
||||
{
|
||||
import std.algorithm.comparison : equal, max, min;
|
||||
|
@ -1374,8 +1369,8 @@ unittest
|
|||
auto g6 = a6.group;
|
||||
}
|
||||
|
||||
// Used by implementation of groupBy for non-forward input ranges.
|
||||
private struct GroupByChunkImpl(alias pred, Range)
|
||||
// Used by implementation of chunkBy for non-forward input ranges.
|
||||
private struct ChunkByChunkImpl(alias pred, Range)
|
||||
if (isInputRange!Range && !isForwardRange!Range)
|
||||
{
|
||||
alias fun = binaryFun!pred;
|
||||
|
@ -1398,11 +1393,31 @@ private struct GroupByChunkImpl(alias pred, Range)
|
|||
void popFront() { r.popFront(); }
|
||||
}
|
||||
|
||||
// Implementation of groupBy for non-forward input ranges.
|
||||
private struct GroupByImpl(alias pred, Range)
|
||||
private template ChunkByImplIsUnary(alias pred, Range)
|
||||
{
|
||||
static if (is(typeof(binaryFun!pred(ElementType!Range.init,
|
||||
ElementType!Range.init)) : bool))
|
||||
enum ChunkByImplIsUnary = false;
|
||||
else static if (is(typeof(
|
||||
unaryFun!pred(ElementType!Range.init) ==
|
||||
unaryFun!pred(ElementType!Range.init))))
|
||||
enum ChunkByImplIsUnary = true;
|
||||
else
|
||||
static assert(0, "chunkBy expects either a binary predicate or "~
|
||||
"a unary predicate on range elements of type: "~
|
||||
ElementType!Range.stringof);
|
||||
}
|
||||
|
||||
// Implementation of chunkBy for non-forward input ranges.
|
||||
private struct ChunkByImpl(alias pred, Range)
|
||||
if (isInputRange!Range && !isForwardRange!Range)
|
||||
{
|
||||
alias fun = binaryFun!pred;
|
||||
enum bool isUnary = ChunkByImplIsUnary!(pred, Range);
|
||||
|
||||
static if (isUnary)
|
||||
alias eq = binaryFun!((a, b) => unaryFun!pred(a) == unaryFun!pred(b));
|
||||
else
|
||||
alias eq = binaryFun!pred;
|
||||
|
||||
private Range r;
|
||||
private ElementType!Range _prev;
|
||||
|
@ -1414,8 +1429,8 @@ private struct GroupByImpl(alias pred, Range)
|
|||
{
|
||||
// Check reflexivity if predicate is claimed to be an equivalence
|
||||
// relation.
|
||||
assert(pred(r.front, r.front),
|
||||
"predicate " ~ pred.stringof ~ " is not reflexive");
|
||||
assert(eq(r.front, r.front),
|
||||
"predicate is not reflexive");
|
||||
|
||||
// _prev's type may be a nested struct, so must be initialized
|
||||
// directly in the constructor (cannot call savePred()).
|
||||
|
@ -1431,14 +1446,23 @@ private struct GroupByImpl(alias pred, Range)
|
|||
|
||||
@property auto front()
|
||||
{
|
||||
return GroupByChunkImpl!(pred, Range)(r, _prev);
|
||||
static if (isUnary)
|
||||
{
|
||||
import std.typecons : tuple;
|
||||
return tuple(unaryFun!pred(_prev),
|
||||
ChunkByChunkImpl!(eq, Range)(r, _prev));
|
||||
}
|
||||
else
|
||||
{
|
||||
return ChunkByChunkImpl!(eq, Range)(r, _prev);
|
||||
}
|
||||
}
|
||||
|
||||
void popFront()
|
||||
{
|
||||
while (!r.empty)
|
||||
{
|
||||
if (!fun(_prev, r.front))
|
||||
if (!eq(_prev, r.front))
|
||||
{
|
||||
_prev = r.front;
|
||||
break;
|
||||
|
@ -1448,12 +1472,19 @@ private struct GroupByImpl(alias pred, Range)
|
|||
}
|
||||
}
|
||||
|
||||
// Single-pass implementation of groupBy for forward ranges.
|
||||
private struct GroupByImpl(alias pred, Range)
|
||||
// Single-pass implementation of chunkBy for forward ranges.
|
||||
private struct ChunkByImpl(alias pred, Range)
|
||||
if (isForwardRange!Range)
|
||||
{
|
||||
import std.typecons : RefCounted;
|
||||
|
||||
enum bool isUnary = ChunkByImplIsUnary!(pred, Range);
|
||||
|
||||
static if (isUnary)
|
||||
alias eq = binaryFun!((a, b) => unaryFun!pred(a) == unaryFun!pred(b));
|
||||
else
|
||||
alias eq = binaryFun!pred;
|
||||
|
||||
// Outer range
|
||||
static struct Impl
|
||||
{
|
||||
|
@ -1482,8 +1513,8 @@ private struct GroupByImpl(alias pred, Range)
|
|||
mothership = origin;
|
||||
|
||||
// Note: this requires reflexivity.
|
||||
assert(pred(start.front, current.front),
|
||||
"predicate " ~ pred.stringof ~ " is not reflexive");
|
||||
assert(eq(start.front, current.front),
|
||||
"predicate is not reflexive");
|
||||
}
|
||||
|
||||
@property bool empty() { return groupNum == size_t.max; }
|
||||
|
@ -1494,7 +1525,7 @@ private struct GroupByImpl(alias pred, Range)
|
|||
current.popFront();
|
||||
|
||||
// Note: this requires transitivity.
|
||||
if (current.empty || !pred(start.front, current.front))
|
||||
if (current.empty || !eq(start.front, current.front))
|
||||
{
|
||||
if (groupNum == mothership.groupNum)
|
||||
{
|
||||
|
@ -1524,14 +1555,26 @@ private struct GroupByImpl(alias pred, Range)
|
|||
}
|
||||
|
||||
@property bool empty() { return impl.current.empty; }
|
||||
@property auto front() { return Group(impl); }
|
||||
|
||||
@property auto front()
|
||||
{
|
||||
static if (isUnary)
|
||||
{
|
||||
import std.typecons : tuple;
|
||||
return tuple(unaryFun!pred(impl.current.front), Group(impl));
|
||||
}
|
||||
else
|
||||
{
|
||||
return Group(impl);
|
||||
}
|
||||
}
|
||||
|
||||
void popFront()
|
||||
{
|
||||
// Scan for next group. If we're lucky, one of our Groups would have
|
||||
// already set .next to the start of the next group, in which case the
|
||||
// loop is skipped.
|
||||
while (!impl.next.empty && pred(impl.current.front, impl.next.front))
|
||||
while (!impl.next.empty && eq(impl.current.front, impl.next.front))
|
||||
{
|
||||
impl.next.popFront();
|
||||
}
|
||||
|
@ -1574,7 +1617,7 @@ unittest
|
|||
static assert(isForwardRange!RefFwdRange);
|
||||
|
||||
auto testdata = new RefFwdRange([1, 3, 5, 2, 4, 7, 6, 8, 9]);
|
||||
auto groups = testdata.groupBy!((a,b) => (a % 2) == (b % 2));
|
||||
auto groups = testdata.chunkBy!((a,b) => (a % 2) == (b % 2));
|
||||
auto outerSave1 = groups.save;
|
||||
|
||||
// Sanity test
|
||||
|
@ -1617,14 +1660,17 @@ unittest
|
|||
* reflexive ($(D pred(x,x)) is always true), symmetric
|
||||
* ($(D pred(x,y) == pred(y,x))), and transitive ($(D pred(x,y) && pred(y,z))
|
||||
* implies $(D pred(x,z))). If this is not the case, the range returned by
|
||||
* groupBy may assert at runtime or behave erratically.
|
||||
* chunkBy may assert at runtime or behave erratically.
|
||||
*
|
||||
* Params:
|
||||
* pred = Predicate for determining equivalence.
|
||||
* r = The range to be chunked.
|
||||
*
|
||||
* Returns: A range of ranges in which all elements in a given subrange are
|
||||
* equivalent under the given predicate.
|
||||
* Returns: With a binary predicate, a range of ranges is returned in which
|
||||
* all elements in a given subrange are equivalent under the given predicate.
|
||||
* With a unary predicate, a range of tuples is returned, with the tuple
|
||||
* consisting of the result of the unary predicate for each subrange, and the
|
||||
* subrange itself.
|
||||
*
|
||||
* Notes:
|
||||
*
|
||||
|
@ -1637,20 +1683,10 @@ unittest
|
|||
* $(XREF algorithm,group), which collapses adjacent equivalent elements into a
|
||||
* single element.
|
||||
*/
|
||||
auto groupBy(alias pred, Range)(Range r)
|
||||
auto chunkBy(alias pred, Range)(Range r)
|
||||
if (isInputRange!Range)
|
||||
{
|
||||
static if (is(typeof(binaryFun!pred(ElementType!Range.init,
|
||||
ElementType!Range.init)) : bool))
|
||||
return GroupByImpl!(pred, Range)(r);
|
||||
else static if (is(typeof(
|
||||
unaryFun!pred(ElementType!Range.init) ==
|
||||
unaryFun!pred(ElementType!Range.init))))
|
||||
return GroupByImpl!((a,b) => pred(a) == pred(b), Range)(r);
|
||||
else
|
||||
static assert(0, "groupBy expects either a binary predicate or "~
|
||||
"a unary predicate on range elements of type: "~
|
||||
ElementType!Range.stringof);
|
||||
return ChunkByImpl!(pred, Range)(r);
|
||||
}
|
||||
|
||||
/// Showing usage with binary predicate:
|
||||
|
@ -1666,13 +1702,13 @@ auto groupBy(alias pred, Range)(Range r)
|
|||
[2, 3]
|
||||
];
|
||||
|
||||
auto r1 = data.groupBy!((a,b) => a[0] == b[0]);
|
||||
auto r1 = data.chunkBy!((a,b) => a[0] == b[0]);
|
||||
assert(r1.equal!equal([
|
||||
[[1, 1], [1, 2]],
|
||||
[[2, 2], [2, 3]]
|
||||
]));
|
||||
|
||||
auto r2 = data.groupBy!((a,b) => a[1] == b[1]);
|
||||
auto r2 = data.chunkBy!((a,b) => a[1] == b[1]);
|
||||
assert(r2.equal!equal([
|
||||
[[1, 1]],
|
||||
[[1, 2], [2, 2]],
|
||||
|
@ -1694,7 +1730,7 @@ unittest
|
|||
{
|
||||
// Grouping by maximum adjacent difference:
|
||||
import std.math : abs;
|
||||
auto r3 = [1, 3, 2, 5, 4, 9, 10].groupBy!((a, b) => abs(a-b) < 3);
|
||||
auto r3 = [1, 3, 2, 5, 4, 9, 10].chunkBy!((a, b) => abs(a-b) < 3);
|
||||
assert(r3.equal!equal([
|
||||
[1, 3, 2],
|
||||
[5, 4],
|
||||
|
@ -1707,6 +1743,7 @@ unittest
|
|||
/* FIXME: pure @safe nothrow*/ unittest
|
||||
{
|
||||
import std.algorithm.comparison : equal;
|
||||
import std.typecons : tuple;
|
||||
|
||||
// Grouping by particular attribute of each element:
|
||||
auto range =
|
||||
|
@ -1720,31 +1757,33 @@ unittest
|
|||
[3, 3]
|
||||
];
|
||||
|
||||
auto byX = groupBy!(a => a[0])(range);
|
||||
auto byX = chunkBy!(a => a[0])(range);
|
||||
auto expected1 =
|
||||
[
|
||||
[[1, 1], [1, 1], [1, 2]],
|
||||
[[2, 2], [2, 3], [2, 3]],
|
||||
[[3, 3]]
|
||||
tuple(1, [[1, 1], [1, 1], [1, 2]]),
|
||||
tuple(2, [[2, 2], [2, 3], [2, 3]]),
|
||||
tuple(3, [[3, 3]])
|
||||
];
|
||||
foreach (e; byX)
|
||||
{
|
||||
assert(!expected1.empty);
|
||||
assert(e.equal(expected1.front));
|
||||
assert(e[0] == expected1.front[0]);
|
||||
assert(e[1].equal(expected1.front[1]));
|
||||
expected1.popFront();
|
||||
}
|
||||
|
||||
auto byY = groupBy!(a => a[1])(range);
|
||||
auto byY = chunkBy!(a => a[1])(range);
|
||||
auto expected2 =
|
||||
[
|
||||
[[1, 1], [1, 1]],
|
||||
[[1, 2], [2, 2]],
|
||||
[[2, 3], [2, 3], [3, 3]]
|
||||
tuple(1, [[1, 1], [1, 1]]),
|
||||
tuple(2, [[1, 2], [2, 2]]),
|
||||
tuple(3, [[2, 3], [2, 3], [3, 3]])
|
||||
];
|
||||
foreach (e; byY)
|
||||
{
|
||||
assert(!expected2.empty);
|
||||
assert(e.equal(expected2.front));
|
||||
assert(e[0] == expected2.front[0]);
|
||||
assert(e[1].equal(expected2.front[1]));
|
||||
expected2.popFront();
|
||||
}
|
||||
}
|
||||
|
@ -1752,6 +1791,7 @@ unittest
|
|||
/*FIXME: pure @safe nothrow*/ unittest
|
||||
{
|
||||
import std.algorithm.comparison : equal;
|
||||
import std.typecons : tuple;
|
||||
|
||||
struct Item { int x, y; }
|
||||
|
||||
|
@ -1772,11 +1812,11 @@ unittest
|
|||
auto arr = [ Item(1,2), Item(1,3), Item(2,3) ];
|
||||
static assert(isForwardRange!(typeof(arr)));
|
||||
|
||||
auto byX = groupBy!(a => a.x)(arr);
|
||||
auto byX = chunkBy!(a => a.x)(arr);
|
||||
static assert(isForwardRange!(typeof(byX)));
|
||||
|
||||
auto byX_subrange1 = byX.front.save;
|
||||
auto byX_subrange2 = byX.front.save;
|
||||
auto byX_subrange1 = byX.front[1].save;
|
||||
auto byX_subrange2 = byX.front[1].save;
|
||||
static assert(isForwardRange!(typeof(byX_subrange1)));
|
||||
static assert(isForwardRange!(typeof(byX_subrange2)));
|
||||
|
||||
|
@ -1786,32 +1826,38 @@ unittest
|
|||
assert(byX_subrange1.equal([ Item(1,3) ]));
|
||||
assert(byX_subrange2.equal([ Item(1,2), Item(1,3) ]));
|
||||
|
||||
auto byY = groupBy!(a => a.y)(arr);
|
||||
auto byY = chunkBy!(a => a.y)(arr);
|
||||
static assert(isForwardRange!(typeof(byY)));
|
||||
|
||||
auto byY2 = byY.save;
|
||||
static assert(is(typeof(byY) == typeof(byY2)));
|
||||
byY.popFront();
|
||||
assert(byY.front.equal([ Item(1,3), Item(2,3) ]));
|
||||
assert(byY2.front.equal([ Item(1,2) ]));
|
||||
assert(byY.front[0] == 3);
|
||||
assert(byY.front[1].equal([ Item(1,3), Item(2,3) ]));
|
||||
assert(byY2.front[0] == 2);
|
||||
assert(byY2.front[1].equal([ Item(1,2) ]));
|
||||
}
|
||||
|
||||
// Test non-forward input ranges.
|
||||
{
|
||||
auto range = refInputRange([ Item(1,1), Item(1,2), Item(2,2) ]);
|
||||
auto byX = groupBy!(a => a.x)(range);
|
||||
assert(byX.front.equal([ Item(1,1), Item(1,2) ]));
|
||||
auto byX = chunkBy!(a => a.x)(range);
|
||||
assert(byX.front[0] == 1);
|
||||
assert(byX.front[1].equal([ Item(1,1), Item(1,2) ]));
|
||||
byX.popFront();
|
||||
assert(byX.front.equal([ Item(2,2) ]));
|
||||
assert(byX.front[0] == 2);
|
||||
assert(byX.front[1].equal([ Item(2,2) ]));
|
||||
byX.popFront();
|
||||
assert(byX.empty);
|
||||
assert(range.empty);
|
||||
|
||||
range = refInputRange([ Item(1,1), Item(1,2), Item(2,2) ]);
|
||||
auto byY = groupBy!(a => a.y)(range);
|
||||
assert(byY.front.equal([ Item(1,1) ]));
|
||||
auto byY = chunkBy!(a => a.y)(range);
|
||||
assert(byY.front[0] == 1);
|
||||
assert(byY.front[1].equal([ Item(1,1) ]));
|
||||
byY.popFront();
|
||||
assert(byY.front.equal([ Item(1,2), Item(2,2) ]));
|
||||
assert(byY.front[0] == 2);
|
||||
assert(byY.front[1].equal([ Item(1,2), Item(2,2) ]));
|
||||
byY.popFront();
|
||||
assert(byY.empty);
|
||||
assert(range.empty);
|
||||
|
@ -1823,7 +1869,7 @@ version(none) // This requires support for non-equivalence relations
|
|||
unittest
|
||||
{
|
||||
import std.algorithm.comparison : equal;
|
||||
auto r = [1, 2, 3, 4, 5, 6, 7, 8, 9].groupBy!((x, y) => ((x*y) % 3) == 0);
|
||||
auto r = [1, 2, 3, 4, 5, 6, 7, 8, 9].chunkBy!((x, y) => ((x*y) % 3) == 0);
|
||||
assert(r.equal!equal([
|
||||
[1],
|
||||
[2, 3, 4],
|
||||
|
@ -1835,7 +1881,7 @@ unittest
|
|||
// Issue 13805
|
||||
unittest
|
||||
{
|
||||
[""].map!((s) => s).groupBy!((x, y) => true);
|
||||
[""].map!((s) => s).chunkBy!((x, y) => true);
|
||||
}
|
||||
|
||||
// joiner
|
||||
|
|
|
@ -59,14 +59,13 @@ $(TR $(TDNW Comparison)
|
|||
$(TR $(TDNW Iteration)
|
||||
$(TDNW $(SUBMODULE iteration))
|
||||
$(TD
|
||||
$(SUBREF iteration, aggregate)
|
||||
$(SUBREF iteration, cache)
|
||||
$(SUBREF iteration, cacheBidirectional)
|
||||
$(SUBREF iteration, chunkBy)
|
||||
$(SUBREF iteration, each)
|
||||
$(SUBREF iteration, filter)
|
||||
$(SUBREF iteration, filterBidirectional)
|
||||
$(SUBREF iteration, group)
|
||||
$(SUBREF iteration, groupBy)
|
||||
$(SUBREF iteration, joiner)
|
||||
$(SUBREF iteration, map)
|
||||
$(SUBREF iteration, reduce)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue