1 /**
2     Removes elements from a range
3 */
4 module ddash.algorithm.pull;
5 
6 ///
7 unittest {
8     int[] arr = [1, 2, 3, 4, 5];
9     arr.pull(1, [2, 5]);
10     assert(arr == [3, 4]);
11 
12     import std.math: ceil;
13     double[] farr = [2.1, 1.2];
14     assert(farr.pull!ceil([2.3, 3.4]) == [1.2]);
15 
16     farr = [2.1, 1.2];
17     assert(farr.pull!((a, b) => ceil(a) == ceil(b))([2.3, 3.4]) == [1.2]);
18 
19     arr = [1, 2, 3, 4, 5];
20     assert(arr.pull!"a == b - 1"(4, 6) == [1, 2, 4]);
21 }
22 
23 ///
24 unittest {
25     struct A {
26         int x;
27         int y;
28     }
29 
30     auto arr = [A(1, 2), A(3, 4), A(5, 6)];
31     assert(arr.pullBy!"y"(2, 6) == [A(3, 4)]);
32 }
33 
34 import ddash.common;
35 
36 /**
37     Removes elements from a range.
38 
39     Params:
40         range = a mutable range
41         values = variables args of ranges and values to pull out
42         pred = a unary transoform predicate or binary equality predicate. Defaults to `==`.
43 
44     Returns:
45         Modified range
46 
47     Since:
48         0.0.1
49 */
50 ref pull(alias pred = null, Range, Values...)(return ref Range range, Values values)
51 if (from!"std.range".isInputRange!Range)
52 {
53     return range.pullBase!("", pred)(values);
54 }
55 
56 /**
57     Removes elements from a range by a publicly visible field of `ElemntType!Range`
58 
59     Params:
60         range = a mutable range
61         values = variables args of ranges and values to pull out
62         member = which member in `ElementType!Range` to pull by
63         pred = a unary transform predicate or binary equality predicate. Defaults to `==`.
64 
65     Returns:
66         Modified range
67 
68     Since:
69         0.0.1
70 */
71 ref pullBy(string member, alias pred = null, Range, Values...)(return ref Range range, Values values)
72 if (from!"std.range".isInputRange!Range)
73 {
74     return range.pullBase!(member, pred)(values);
75 }
76 
77 private ref pullBase(string member, alias pred, Range, Values...)(return ref Range range, Values values) {
78     import std.algorithm: canFind, remove;
79     import ddash.algorithm: concat, equal;
80     import ddash.common.valueby;
81     // elements from values will be lhs
82     alias reverseEqual = (a, b) => equal!pred(b, a);
83     auto unwanted = concat(values);
84     static if (member == "")
85     {
86         alias transform = (a) => a;
87     }
88     else
89     {
90         alias transform = (a) => a.valueBy!member;
91     }
92     range = range
93         .remove!(a => unwanted
94             .canFind!reverseEqual(transform(a))
95         );
96     return range;
97 }
98 
99 /**
100     Returns a range excluding the specified indices.
101 
102     The indices can be given as a range of indices, or single integral arguments or a mixture of both.
103     All index arguments will be $(DDOX_NAMED_REF algorithm.concat, `concat`)ed together and sorted
104     before returning a range that then goes through the original range exlusing the resulting concatenation
105     of indices.
106 
107     The running time is $(B O(n)) if `indices` is a single sorted range or a single index to remove, else
108     there's an additional cost of standard sort running time.
109 
110     Params:
111         range = an input range
112         indices = zero or more integral elements or ranges
113 
114     Returns:
115         A range excluding the supplied indices
116 
117     Benchmarks:
118         $(LI single args: `pullAt(8, 16, 14...)`)
119         $(LI single range: `pullAt([8, 16, 14...])`)
120         $(LI sorted range: `pullAt([8, 16, 14...].sort)`)
121         $(LI canFind range: `indices.filter!(canFind)`)
122         $(LI canFind sorted: `indices.sort.filter!(canFind)`)
123         ---
124         Benchmarking pullAt against filter/canFind:
125           numbers: [12, 11, 1, 9, 11, 4, 1, 4, 2, 7, 16, 8, 8, 9, 6, 15, 9, 0, 15, 2]
126           indices: [8, 16, 14, 11, 0, 16, 12, 10, 15, 17]
127         pullAt:
128           single args:    3 ms, 885 μs, and 6 hnsecs
129           single range:   1 ms and 610 μs
130           sorted range:   185 μs and 2 hnsecs
131           canFind range:  5 ms and 547 μs
132           canFind sorted: 4 hnsecs
133         pullAt (with .array):
134           single args:    8 ms, 765 μs, and 8 hnsecs
135           single range:   6 ms, 823 μs, and 8 hnsecs
136           sorted range:   6 ms, 571 μs, and 2 hnsecs
137           canFind range:  10 ms, 479 μs, and 2 hnsecs
138           canFind sorted: 9 ms, 330 μs, and 5 hnsecs
139         ---
140 
141     Since:
142         0.0.1
143 */
144 auto pullAt(Range, Indices...)(Range range, Indices indices)
145 if (from!"std.range".isInputRange!Range
146     && from!"std.meta".allSatisfy!(
147         from!"std.traits".isIntegral,
148         from!"bolts.meta".Flatten!Indices
149     )
150 ) {
151     import std.algorithm: sort;
152     import std.range: empty, popFront, front, isInputRange;
153     import std.array;
154     import ddash.algorithm: concat;
155     import bolts.range: isSortedRange;
156 
157     // If we only have one element or we are a sorted range then there's no need to sort
158     static if (Indices.length == 1 && (!isInputRange!(Indices[0]) || isSortedRange!(Indices[0])))
159     {
160         auto normalizedIndices = concat(indices);
161     }
162     else
163     {
164         auto normalizedIndices = concat(indices).array.sort;
165     }
166 
167     alias I = typeof(normalizedIndices);
168     static struct Result {
169         Range source;
170         I indices;
171         size_t currentIndex;
172         this(Range r, I i) {
173             this.source = r;
174             this.indices = i;
175             this.moveToNextElement;
176         }
177         void moveToNextElement() {
178             while (!this.source.empty && !this.indices.empty && this.indices.front == this.currentIndex) {
179                 this.currentIndex++;
180                 this.source.popFront;
181                 this.indices.popFront;
182             }
183         }
184         auto ref front() @property {
185             return this.source.front;
186         }
187         bool empty() @property {
188             return this.source.empty;
189         }
190         void popFront() {
191             this.currentIndex++;
192             this.source.popFront;
193 
194             this.moveToNextElement;
195         }
196     }
197     return Result(range, normalizedIndices);
198 }
199 
200 unittest {
201     assert([1, 2, 3, 4].pullAt(1, 2, 3).equal([1]));
202     assert([1, 2, 3, 4].pullAt(0, 3).equal([2, 3]));
203     assert([1, 2, 3, 4].pullAt(0, 5).equal([2, 3, 4]));
204     assert([1, 2, 3, 4].pullAt([2, 1]).equal([1, 4]));
205     assert([1, 2, 3, 4].pullAt([2, 1, 0, 3]).empty);
206 }