-
Notifications
You must be signed in to change notification settings - Fork 8
/
EKEnumerable.h
525 lines (386 loc) · 14.8 KB
/
EKEnumerable.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
//
// EKEnumerable.h
// EnumeratorKit
//
// Created by Adam Sharp on 13/05/13.
// Copyright (c) 2013 Adam Sharp. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <AvailabilityMacros.h>
/**
The `EKEnumerable` protocol and mixin provide a number of methods for
traversing, searching and sorting collection classes. To include the
`EKEnumerable` mixin, collection classes need to:
1. Adopt the `EKEnumerable` protocol
2. Implement `+load` and call `[self includeEKEnumerable]`
3. Implement the required `-each:` and `-initWithEnumerable:` methods in
the `EKEnumerable` protocol.
For example:
// MyCollection.h
@interface MyCollection : NSObject <EKEnumerable>
@end
// MyCollection.m
@implementation
+ (void)load
{
[self includeEKEnumerable];
}
- (instancetype)initWithEnumerable:(id<EKEnumerable>)enumerable
{
return [self initWithArray:[enumerable asArray]];
}
- (instancetype)each:(void (^)(id))block
{
for (id obj in self.data) {
block(obj);
}
return self;
}
@end
*/
@protocol EKEnumerable <NSObject>
@required
#pragma mark - Initialisation
/** @name Initialisation **/
/**
Called by `EnumeratorKit` to initialize a new intance of a collection
containing the transformed or filtered results after applying some operation.
For example, the default implementation of `-map:` calls this initializer and
passes in an enumerable containing the mapped values. Your implementation of
this method should use the values in the enumerable to initialize the new
instance.
@param enumerable An enumerable containing the values to initialize this instance.
*/
- (instancetype)initWithEnumerable:(id<EKEnumerable>)enumerable;
#pragma mark - Traversal
/** @name Traversal */
/**
Collection classes must implement this method to traverse each element
in the collection, applying the block to each element.
Example:
NSArray *greetings = @[@"Hello", @"Hi"];
[numbers each:^(NSString *greeting){
NSLog(@"%@, world", greeting);
}];
@param block A block that accepts a single object.
@return Your implementation of this method should return `self`.
*/
- (instancetype)each:(void (^)(id obj))block;
@optional
/**
Iterate the block over each element in the collection, calling the
block with two arguments: the object and its index (starting at 0).
Usage:
NSMutableArray *array = [NSMutableArray array];
[@[@"peach", @"pear", @"plum"] eachWithIndex:^(NSString *fruit, NSUInteger i){
[array addObject:[NSString stringWithFormat:@"%d: %@", i, fruit]];
}];
// => @[@"0: peach", @"1: pear", @"2: plum"]
@param block A block that accepts an object and an `NSUInteger` index.
@return This method returns `self` after the enumeration is complete.
*/
- (instancetype)eachWithIndex:(void (^)(id obj, NSUInteger i))block;
#pragma mark Transformations
/** @name Transformations */
/**
Return a new enumerable with the results of applying `block` to each element
in the receiver. `nil` values are automatically boxed as `NSNull`.
Usage:
Animal *dog = [Animal animalWithName:@"Spike"];
Animal *cat = [Animal animalWithName:@"Princess"];
[@[dog, cat] map:^(Animal *pet){
return pet.name;
}];
// => @[@"Spike", @"Princess"];
@param block A block that accepts a single object and returns an object.
*/
- (instancetype)map:(id (^)(id obj))block;
/**
Map `block` over each element in the receiver, while passing the index of each element.
@param block A block that accepts an object and an `NSUInteger` index.
*/
- (instancetype)mapWithIndex:(id (^)(id obj, NSUInteger i))block;
/**
Maps `block` over the receiver, combining each enumerable into a single enumerable.
`nil` values are automatically converted into an empty enumerable by calling
calling `+new` on the receiver's class.
Usage:
[@[@0, @1, @2] flattenMap:^(NSNumber *i){
return @[i, i.stringValue];
}];
// => @[@0, @"0", @1, @"1", @2, @"2"]
@param block A block accepting a single object and returning an enumerable.
*/
- (instancetype)flattenMap:(id<EKEnumerable> (^)(id obj))block;
/**
Transform an enumerable into an dictionary by transforming each element into
a dictionary entry.
Usage:
NSDictionary *apple = @{ @"id": @1, @"name": @"Apple", @"grows_on": @"tree" };
NSDictionary *grape = @{ @"id": @2, @"name": @"Grape", @"grows_on": @"vine" };
NSDictionary *fruits = [@[banana, apple] mapDictionary:^(NSDictionary *fruit){
return @{ fruit[@"name"]: fruit };
}];
fruits[@"Apple"][@"grows_on"]; // => @"tree"
**Note:** If the block returns a key that already exists in the
dictionary (i.e., if two elements return the same key while iterating),
the last value for that key will end up in the resulting dictionary.
Later values in the enumeration for the same key will replace earlier
values for that key.
@param block A block that maps objects to entries. The dictionary returned
by this block must not contain more than a single entry.
*/
- (NSDictionary *)mapDictionary:(NSDictionary *(^)(id obj))block;
/**
Takes the key returned by the block and "wraps" the element with it
as a key-value pair.
`wrap:` is a shorthand for this usage of `mapDictionary:`
[collection mapDictionary:^(id obj){
return @{ obj[@"key"]: obj };
}];
which becomes
[collection wrap:^(id obj){
return obj[@"key"];
}];
**Note:** Like `mapDictionary:`, if the block returns duplicate keys,
later values in the enumeration for the same key will replace earlier
values for that key.
@param block A block that returns a unique key for an object.
*/
- (NSDictionary *)wrap:(id<NSCopying> (^)(id obj))block;
/**
Applies the block to each item in the collection, using the result as
the item's key. Returns a dictionary of arrays grouped by the set of
keys returned by the block.
[@[@3, @1, @2] groupBy:^(NSNumber *num){
if (num.integerValue % 2 == 0) {
return @"even";
}
else {
return @"odd";
}
}];
// => @{ @"even": @[@2], @"odd": @[@3, @1] }
@param block A block whose return value will be used as a key to group
the item by.
*/
- (NSDictionary *)groupBy:(id<NSCopying> (^)(id obj))block;
/**
Applies the block to each item in the collection, using the result as
the item's key. Returns an array of pairs of the form
@[ key, @[first match, second match] ]
This method behaves like `groupBy:`, except the original order of the
items is unchanged (this is particularly useful if the original collection
is sorted).
For example, with the array `@[@"foo", @"bar"]`, if we were to chunk by
each item's first character:
[@[@"foo", @"bar"] chunk:^(NSString *string){
return [string substringToIndex:1];
}];
// => @[
@[@"f", @[@"foo"]],
@[@"b", @[@"bar"]]
]
Every "chunk" of items that returns the same value from the block is
grouped together:
[@[@"foo", @"bar", @"baz"] chunk:^(NSString *string){
return [string substringToIndex:1];
}];
// => @[
@[@"f", @[@"foo"]],
@[@"b", @[@"bar", @"baz"]]
]
@param block A block whose return value will be used as a key to group
the item by.
*/
- (NSArray *)chunk:(id (^)(id obj))block;
/**
Accumulates a result by applying the block to each element in turn.
Each time the block is executed, its return value becomes the new value
of the accumulator.
In this form of `reduce:`, as opposed to `reduce:withBlock:`, the first
element of the collection will become the initial value of the
accumulator, and every other element in the collection will be passed
to the block in turn.
For example, take an array of numbers for which we need to calculate
the maximum:
NSArray *numbers = @[@5, @1, @100, @13, @28, @123, @321, @10, @99, @4];
// at each step, returns the new maximum
[numbers reduce:^(NSNumber *max, NSNumber *num){
return num.integerValue > max.integerValue ? num : max;
}];
// => @321
1. The first element, `@5`, becomes the first value of `max`
2. The block is called with `max = @5` and `num = @1`. `max` is still
greater, so it is returned.
3. The block is called with `max = @5` and `num = @100`. This time,
`num` is greater, and so the block returns `@100`.
4. The block is called with `max = @100` and `num = @13`. It returns
`@100`.
5. And so on, until the maximum value of `@321` is returned.
@param block A block that accepts an accumulator value, and each
successive object in the collection.
@return The result of the transformation.
*/
- (id)reduce:(id (^)(id memo, id obj))block;
/**
Accumulates a result by applying the block to each element in turn.
Each time the block is executed, its return value becomes the new value
of the accumulator. The final result may be a new collection or a
discrete object.
In a variation on the example in `reduce:`, instead of returning the
maximum value in the array, we can instead return an array showing
how the maximum value changed across the enumeration:
NSArray *numbers = @[@5, @1, @100, @13, @28, @123, @321, @10, @99, @4];
[numbers reduce:[numbers take:1] withBlock:^(NSArray *maximums, NSNumber *num){
if (num.integerValue > maximums.lastObject.integerValue) {
[maximums addObject:num];
}
return maximums;
}];
// => @[@5, @100, @123, @321]
1. We use `[numbers take:1]` as the initial, which returns a new array
containing the first number (`@[@5]`). At each step, we test if the
number is greater than the last object in the array (the current
maximum).
2. For the first and second iterations, `maximums` is unchanged, as the
max value hasn't increased yet.
3. For `num = @100`, we have a new max value, so it is added to the
array.
4. And so on, until the final array of maximum numbers is copied and
returned.
@param initial The initial accumulator value to be passed to the block.
If the object conforms to `NSMutableCopying`, a mutable copy of it
will be taken, to allow for the construction of new collections.
Passing `nil` causes the first element to become the initial value,
and thus is equivalent to calling `reduce:`.
@param block A block that accepts an accumulator value, and each
successive object in the collection.
@return The result of the transformation. This may be a new collection,
or a discrete object. If the result conforms to `NSCopying`, it is
copied first.
*/
- (id)reduce:(id)initial withBlock:(id (^)(id memo, id obj))block;
/**
Accumulates a result by "injecting" the operation between successive
pairs of elements.
Usage:
NSArray *letters = @[@"H", @"e", @"l", @"l", @"o"];
[letters inject:@selector(stringByAppendingString:)];
// => @"Hello"
This is equivalent to the following, using `reduce:`:
[letters reduce:^(NSString *s, NSString *letter){
return [s stringByAppendingString:letter];
}];
// => @"Hello"
@param binaryOperation A selector that takes a single parameter, and
is expected to return an instance of the same type as the receiver.
@return Returns the result of "injecting" the operation between each
element of the collection.
*/
- (id)inject:(SEL)binaryOperation;
#pragma mark Searching and filtering
/** @name Searching and filtering */
/**
Applies the block to each element in the collection and returns a new
collection with only the elements for which the block returns `YES`.
Usage:
- (NSArray *)finishedOperations
{
return [self.operations select:^(NSOperation *op){
return op.isFinished;
}];
}
@param block A block that accepts an an object and returns `YES` if
the object should be kept, and `NO` if it should be removed.
@return A filtered array containing all the objects for which the
block returned `YES`.
*/
- (instancetype)select:(BOOL (^)(id obj))block;
/**
Alias for `select:`.
*/
- (instancetype)filter:(BOOL (^)(id obj))block;
/**
Like `select:`, but instead returns the elements for which the block
returns `NO`. Thus, if the block returns `YES`, the element is removed
from the collection.
Usage:
- (NSArray *)nonEmptyStrings
{
return [self.strings reject:^(NSString *s){
return s.length == 0;
}];
}
@param block A block that accepts an an object and returns `NO` if
the object should be kept, and `YES` if it should be removed.
@return A filtered array containing all the objects for which the
block returned `NO`.
*/
- (instancetype)reject:(BOOL (^)(id obj))block;
/**
Find the first element in a collection for which the block returns `YES`.
Usage:
NSArray *numbers = @[@1, @3, @5, @6, @9];
// look for an even number
[numbers find:^BOOL(NSNumber *number) {
return number.integerValue % 2 == 0;
}];
// => @6
@param block A block that accepts each successive object and returns
a `BOOL` result.
@return Returns the first object for which the block returns `YES`, if
any. If no object is found, returns `nil`.
*/
- (id)find:(BOOL (^)(id obj))block;
/**
Check if any value in a collection passes the block.
Usage:
NSArray *numbers = @[@1, @3, @5, @7, @9];
// look for an even number
[numbers any:^BOOL(NSNumber *number) {
return number.integerValue % 2 == 0;
}];
// => @NO
@param block A block that accepts each successive object and returns
a `BOOL` result.
@return Returns `YES` if an object is found for which the block returns `YES`.
If no object is found, returns `NO`.
*/
- (BOOL)any:(BOOL (^)(id obj))block;
/**
Check if all values in a collection pass the block.
Usage:
NSArray *numbers = @[@1, @3, @5, @7, @9];
// Check if all numbers are odd
[numbers all:^BOOL(NSNumber *obj) {
return obj.integerValue % 2 != 0;
}];
// => @YES
@param block A block that accepts each successive object and returns
a `BOOL` result.
@return Returns `YES` if the block returns `YES` for all objects in the
collection. If the block returns `NO` for at least one object, returns `NO`.
*/
- (BOOL)all:(BOOL (^)(id obj))block;
#pragma mark Sorting
/** @name Sorting */
/** Sorts the collection by sending `compare:` to successive pairs of elements. */
- (NSArray *)sort;
/** Sorts the collection using the given comparator. */
- (NSArray *)sortWith:(NSComparator)comparator;
/** Sort the collection, using the return values of the block as the sort key for comparison. */
- (NSArray *)sortBy:(id (^)(id obj))block;
#pragma mark Other methods
/** @name Other methods */
/** Take elements from a collection */
- (instancetype)take:(NSInteger)number;
/** Get an array */
- (NSArray *)asArray;
@end
#pragma mark - EKEnumerable mixin
@interface EKEnumerable : NSObject <EKEnumerable>
@end
@interface NSObject (includeEKEnumerable)
+ (void)includeEKEnumerable;
@end