Skip to content

Commit 0793a2b

Browse files
authored
Add isSorted and related matchers (#2490)
1 parent e2ddae9 commit 0793a2b

File tree

3 files changed

+224
-1
lines changed

3 files changed

+224
-1
lines changed

pkgs/matcher/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Remove some dynamic invocations.
44
* Add explicit casts from `dynamic` values.
55
* Require Dart 3.5
6+
* Add `isSorted` and related matchers for iterables.
67

78
## 0.12.17
89

pkgs/matcher/lib/src/iterable_matchers.dart

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ class _UnorderedEquals extends _UnorderedMatches {
132132

133133
/// Iterable matchers match against [Iterable]s. We add this intermediate
134134
/// class to give better mismatch error messages than the base Matcher class.
135-
abstract class _IterableMatcher extends FeatureMatcher<Iterable> {
135+
abstract class _IterableMatcher<T> extends FeatureMatcher<Iterable<T>> {
136136
const _IterableMatcher();
137137
}
138138

@@ -414,3 +414,119 @@ class _ContainsOnce extends _IterableMatcher {
414414
Description mismatchDescription, Map matchState, bool verbose) =>
415415
mismatchDescription.add(_test(item, matchState)!);
416416
}
417+
418+
/// Matches [Iterable]s which are sorted.
419+
Matcher isSorted<T extends Comparable<T>>() =>
420+
_IsSorted<T, T>((t) => t, (a, b) => a.compareTo(b));
421+
422+
/// Matches [Iterable]s which are [compare]-sorted.
423+
Matcher isSortedUsing<T>(Comparator<T> compare) =>
424+
_IsSorted<T, T>((t) => t, compare);
425+
426+
/// Matches [Iterable]s which are sorted by the [keyOf] property.
427+
Matcher isSortedBy<T, K extends Comparable<K>>(K Function(T) keyOf) =>
428+
_IsSorted<T, K>(keyOf, (a, b) => a.compareTo(b));
429+
430+
/// Matches [Iterable]s which are [compare]-sorted by their [keyOf] property.
431+
Matcher isSortedByCompare<T, K>(K Function(T) keyOf, Comparator<K> compare) =>
432+
_IsSorted(keyOf, compare);
433+
434+
class _IsSorted<T, K> extends _IterableMatcher<T> {
435+
final K Function(T) _keyOf;
436+
final Comparator<K> _compare;
437+
438+
_IsSorted(K Function(T) keyOf, Comparator<K> compare)
439+
: _keyOf = keyOf,
440+
_compare = compare;
441+
442+
@override
443+
bool typedMatches(Iterable<T> item, Map matchState) {
444+
var iterator = item.iterator;
445+
if (!iterator.moveNext()) return true;
446+
var previousElement = iterator.current;
447+
K previousKey;
448+
try {
449+
previousKey = _keyOf(previousElement);
450+
} catch (e) {
451+
addStateInfo(matchState, {
452+
'index': 0,
453+
'element': previousElement,
454+
'error': e,
455+
'keyError': true
456+
});
457+
return false;
458+
}
459+
460+
var index = 0;
461+
while (iterator.moveNext()) {
462+
final element = iterator.current;
463+
final K key;
464+
try {
465+
key = _keyOf(element);
466+
} catch (e) {
467+
addStateInfo(matchState,
468+
{'index': index, 'element': element, 'error': e, 'keyError': true});
469+
return false;
470+
}
471+
472+
final int comparison;
473+
try {
474+
comparison = _compare(previousKey, key);
475+
} catch (e) {
476+
addStateInfo(matchState, {
477+
'index': index,
478+
'first': previousElement,
479+
'second': element,
480+
'error': e,
481+
'compareError': true
482+
});
483+
return false;
484+
}
485+
486+
if (comparison > 0) {
487+
addStateInfo(matchState,
488+
{'index': index, 'first': previousElement, 'second': element});
489+
return false;
490+
}
491+
previousElement = element;
492+
previousKey = key;
493+
index++;
494+
}
495+
return true;
496+
}
497+
498+
@override
499+
Description describe(Description description) => description.add('is sorted');
500+
501+
@override
502+
Description describeTypedMismatch(Iterable<T> item,
503+
Description mismatchDescription, Map matchState, bool verbose) {
504+
if (matchState.containsKey('error')) {
505+
mismatchDescription
506+
.add('got error ')
507+
.addDescriptionOf(matchState['error'])
508+
.add(' at ')
509+
.addDescriptionOf(matchState['index']);
510+
511+
if (matchState.containsKey('compareError')) {
512+
return mismatchDescription
513+
.add(' when comparing ')
514+
.addDescriptionOf(matchState['first'])
515+
.add(' and ')
516+
.addDescriptionOf(matchState['second']);
517+
} else {
518+
return mismatchDescription
519+
.add(' when getting key of ')
520+
.addDescriptionOf(matchState['element']);
521+
}
522+
}
523+
524+
return mismatchDescription
525+
.add('found elements out of order at ')
526+
.addDescriptionOf(matchState['index'])
527+
.add(': ')
528+
.addDescriptionOf(matchState['first'])
529+
.add(' and ')
530+
.addDescriptionOf(matchState['second']);
531+
}
532+
}

pkgs/matcher/test/iterable_matchers_test.dart

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,4 +392,110 @@ void main() {
392392
'Actual: SimpleIterable:[3, 2, 1] '
393393
'Which: does not contain <5>');
394394
});
395+
396+
test('isSorted', () {
397+
final sorted = [4, 8, 15, 16, 23, 42];
398+
final mismatchAtStart = [8, 4, 15, 16, 23, 42];
399+
final mismatchInMiddle = [4, 8, 16, 15, 23, 42];
400+
final mismatchAtEnd = [4, 8, 15, 16, 42, 23];
401+
final singleElement = [42];
402+
final twoElementsSorted = [42, 143];
403+
final twoElementsUnsorted = [143, 42];
404+
405+
shouldPass(sorted, isSorted<num>());
406+
shouldFail(
407+
mismatchAtStart,
408+
isSorted<num>(),
409+
'Expected: is sorted '
410+
'Actual: [8, 4, 15, 16, 23, 42] '
411+
'Which: found elements out of order at <0>: <8> and <4>');
412+
shouldFail(
413+
mismatchInMiddle,
414+
isSorted<num>(),
415+
'Expected: is sorted '
416+
'Actual: [4, 8, 16, 15, 23, 42] '
417+
'Which: found elements out of order at <2>: <16> and <15>');
418+
shouldFail(
419+
mismatchAtEnd,
420+
isSorted<num>(),
421+
'Expected: is sorted '
422+
'Actual: [4, 8, 15, 16, 42, 23] '
423+
'Which: found elements out of order at <4>: <42> and <23>');
424+
shouldPass(singleElement, isSorted<num>());
425+
shouldPass(twoElementsSorted, isSorted<num>());
426+
shouldFail(
427+
twoElementsUnsorted,
428+
isSorted<num>(),
429+
'Expected: is sorted '
430+
'Actual: [143, 42] '
431+
'Which: found elements out of order at <0>: <143> and <42>');
432+
});
433+
434+
test('isSortedUsing', () {
435+
final sorted = [1, 2, 3];
436+
final unsorted = [1, 3, 2];
437+
final reverseSorted = [3, 2, 1];
438+
439+
int alwaysEqualCompare(int x, int y) => 0;
440+
int throwingCompare(int x, int y) => throw Error();
441+
442+
shouldPass(sorted, isSortedUsing((int x, int y) => x - y));
443+
shouldFail(
444+
unsorted,
445+
isSortedUsing((int x, int y) => x - y),
446+
'Expected: is sorted '
447+
'Actual: [1, 3, 2] '
448+
'Which: found elements out of order at <1>: <3> and <2>');
449+
shouldPass(reverseSorted, isSortedUsing((int x, int y) => y - x));
450+
451+
shouldPass(unsorted, isSortedUsing(alwaysEqualCompare));
452+
453+
shouldFail(
454+
sorted,
455+
isSortedUsing(throwingCompare),
456+
'Expected: is sorted '
457+
'Actual: [1, 2, 3] '
458+
'Which: got error <Instance of \'Error\'> at <0> '
459+
'when comparing <1> and <2>');
460+
});
461+
462+
test('isSortedBy', () {
463+
final sorted = ['y', 'zz', 'bbbb', 'aaaa'];
464+
final unsorted = ['y', 'bbbb', 'aaaa', 'zz'];
465+
final sortedDueToSameKey = ['zzz', 'abc', 'def', 'aaa'];
466+
467+
num throwingKey(String s) => throw Error();
468+
469+
shouldPass(sorted, isSortedBy<String, num>((String s) => s.length));
470+
shouldFail(
471+
unsorted,
472+
isSortedBy<String, num>((String s) => s.length),
473+
'Expected: is sorted '
474+
'Actual: [\'y\', \'bbbb\', \'aaaa\', \'zz\'] '
475+
'Which: found elements out of order at <2>: \'aaaa\' and \'zz\'');
476+
shouldPass(
477+
sortedDueToSameKey, isSortedBy<String, num>((String s) => s.length));
478+
479+
shouldFail(
480+
sorted,
481+
isSortedBy(throwingKey),
482+
'Expected: is sorted '
483+
'Actual: [\'y\', \'zz\', \'bbbb\', \'aaaa\'] '
484+
'Which: got error <Instance of \'Error\'> at <0> '
485+
'when getting key of \'y\'');
486+
});
487+
488+
test('isSortedByCompare', () {
489+
final sorted = ['aaaa', 'bbbb', 'zz', 'y'];
490+
final unsorted = ['y', 'bbbb', 'aaaa', 'zz'];
491+
492+
shouldPass(sorted,
493+
isSortedByCompare((String s) => s.length, (a, b) => b.compareTo(a)));
494+
shouldFail(
495+
unsorted,
496+
isSortedByCompare((String s) => s.length, (a, b) => b.compareTo(a)),
497+
'Expected: is sorted '
498+
'Actual: [\'y\', \'bbbb\', \'aaaa\', \'zz\'] '
499+
'Which: found elements out of order at <0>: \'y\' and \'bbbb\'');
500+
});
395501
}

0 commit comments

Comments
 (0)