Skip to content

Commit

Permalink
Merge pull request #1 from mitryp/feature/compare_sequentially
Browse files Browse the repository at this point in the history
Feature/Compare sequentially
  • Loading branch information
mitryp authored Mar 12, 2024
2 parents ea1aff2 + 5e92269 commit c6875cf
Show file tree
Hide file tree
Showing 10 changed files with 164 additions and 82 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 1.1.0

- Implemented `sortSequentially` comparator as a concise alternative to comparator chaining with `then`;
- Added `package:collection` dependency;
- **Marked `then`, `reversed` Comparator extension methods and `compareTransformed` function as deprecated - they
duplicate existing functionality of `package:collection`.**

## 1.0.0

- Initial version: implemented Java-like comparators in Dart.
72 changes: 30 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,23 @@
## Make your code readable with functional Java-like by-field comparators in Dart
## Make your sorts readable with functional by-field comparators

[![Dart Tests](https://github.com/mitryp/comparators/actions/workflows/dart.yml/badge.svg)](https://github.com/mitryp/comparators/actions/workflows/dart.yml?branch=master)
[![Pub package](https://img.shields.io/pub/v/comparators.svg)](https://pub.dev/packages/comparators)
[![Package publisher](https://img.shields.io/pub/publisher/comparators.svg)](https://pub.dev/packages/comparators/publisher)

The `comparators` package is a toolset for creating Java-like comparators in Dart, designed to provide a way
to compare objects by their fields. It also includes extensions to chain and
invert comparators.

> This package includes some functionality already included in the `collection` package: extensions to chain and inverse
> comparators.
>
> If you already use that package in your project and only need this functionality, you won't need this package.
to compare objects by their fields. It also provides a declarative way to combine comparators sequentially.

### Features

Import `comparators/comparators.dart` to use:
* By-field object comparators
* Field transformation before comparison
* Boolean comparison
* Combining comparators to break ties

Import `comparators/extensions.dart` to use:
* Comparator chaining
* Comparator reversing
Comparator extensions `then` and `reversed` from this package are **deprecated** and will be removed in 2.0.0.
Please, use the analogues from the `package:collection`: `then` and `inverse`.

In 2.0.0, this package will export those instead of the removed ones.

### Getting Started
To install the package, run `pub add comparators` or add the following line to your `pubspec.yaml`:
Expand All @@ -45,50 +38,45 @@ Comparison by a single field:
users.sort(compare((u) => u.username));
```

Comparison by a transformed field:
```dart
// this will sort the users by their username
// before comparing the usernames will be transformed with the provided transform
// in this case, it will lowercase the names to do a case insensitive comparison
users.sort(
compareTransformed<User, String>((u) => u.username, (name) => name.toLowerCase()),
);
```

Comparison by a boolean field:
```dart
users.sort(compareBool((u) => u.isActive));
```
When comparing boolean, the function will use the integer comparison and the following transformation:
`true => 1, false => 0`.

---
Combining comparators to compare sequentially:
```dart
users.sort(compareSequentially([
compare((user) => user.name),
compare((user) => user.surname),
compare((user) => user.country),
]));
```

> The comparators can be chained together and reverted with the Comparator extensions imported from
the `comparators/extensions.dart`.
In this example, user objects will be compared by a name first,
then by a surname, and by a country.

Multi-field comparison with chaining and reverting:
The same result could be achieved using comparator chaining from the `package:collection`,
but in a less declarative way:
```dart
// this will sort the users by their activity first, then by their email,
// and then by their username
users.sort(
// the users which active is set to true will come first in the list
compareBool<User>((u) => u.isActive).reversed.then(
// if both compared users have the same activity, the tie will be broken comparing by their email field
compare<User>((u) => u.email).then(
// and then by their username
compare<User>((u) => u.username),
),
),
compare((User user) => user.name).then(
compare((User user) => user.surname).then(
compare((User user) => user.country),
),
),
);
```

Also, note that the compiler cannot infer types in the example with chaining,
but can with `compareSequentially`.

---

### Issues and contributions

If you found any issues or would like to contribute to this package, feel free to do so at the project's
[GitHub](https://github.com/mitryp/comparators).

### Roadmap
- [x] Basic java-like field comparators
- [x] Comparator chaining/reversal
- [ ] List extensions
Contributions are welcome - start the discussion in the
[Issue tracker](https://github.com/mitryp/comparators/issues).
54 changes: 30 additions & 24 deletions example/comparators_example.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:collection/collection.dart' show ComparatorExtension;
import 'package:comparators/comparators.dart';
import 'package:comparators/extensions.dart';

final List<User> users = [
const User(
Expand Down Expand Up @@ -41,42 +41,48 @@ final List<User> users = [
];

void main() {
_printUsers();
var usersCopy = [...users];
void reset() => usersCopy = [...users];
void printUsers({String trailing = '\n'}) =>
print('${usersCopy.join('\n')}$trailing');

// this will sort the list by the username field of the User object
users.sort(compare((u) => u.username));

_printUsers();
printUsers();

// this will sort the users by their username
// before comparing the usernames will be transformed with the provided transform
// in this case, it will lowercase the names to do a case insensitive comparison
users.sort(
compareTransformed<User, String>(
(u) => u.username, (name) => name.toLowerCase()),
);
// this will sort the list by the username field of the User object
usersCopy.sort(compare((u) => u.username));

_printUsers();
printUsers();
reset();

// this will sort the users by their activity first, then by their email,
// and then by their username
users.sort(
// * `inverse` is from the `package:collection`
usersCopy.sort(compareSequentially([
// the users which active is set to true will come first in the list
compareBool<User>((u) => u.isActive).reversed.then(
// then users will be sorted by their email field
compare<User>((u) => u.email).then(
// and then by their username
compare<User>((u) => u.username),
// note that using `inverse` requires explicit type in the comparator
compareBool((User u) => u.isActive).inverse,
// then users will be sorted by their email field
compare((u) => u.email),
// and then by their username
compare((u) => u.username),
]));

printUsers();
reset();

// alternatively, it is possible to achieve the same result using `then` and `inverse` extension methods from the
// `package:collection`, but it is necessary to write types explicitly in most cases
usersCopy.sort(
compareBool((User u) => u.isActive).inverse.then(
compare((User u) => u.email).then(
compare((User u) => u.username),
),
),
);

_printUsers(trailing: '');
printUsers(trailing: '');
}

void _printUsers({String trailing = '\n'}) =>
print('${users.join('\n')}$trailing');

/// A class representing a user with an id, username, email and activity status.
class User {
final int id;
Expand Down
4 changes: 3 additions & 1 deletion lib/src/extensions/comparator_chaining.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
/// A [Comparator] extension adding [then] method to allow chaining the
/// A [Comparator] extension adding `then` method to allow chaining the
/// comparators together.
extension ComparatorChaining<T> on Comparator<T> {
/// Returns a new [Comparator] combining this one and the given
/// [nextComparator] in the lexicographical order.
///
/// Firstly, this comparator will be used, and if the result is 0, the
/// [nextComparator] will be used.
@Deprecated('It duplicates the functionality of \'then\' '
'from the \'package:collection\' and will be removed in v2.0.0')
Comparator<T> then(Comparator<T> nextComparator) => (a, b) {
final firstComparison = call(a, b);

Expand Down
4 changes: 3 additions & 1 deletion lib/src/extensions/comparator_reversing.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/// A [Comparator] extension adding the [reversed] getter to allow reversing the
/// A [Comparator] extension adding the `reversed` getter to allow reversing the
/// comparing operation.
extension ComparatorReversing<T> on Comparator<T> {
/// Returns a new [Comparator] that compares the elements in the reversed
Expand All @@ -7,5 +7,7 @@ extension ComparatorReversing<T> on Comparator<T> {
/// For example, if this comparator compares the elements in the ascending
/// order, the reversed comparator will compare the elements in the descending
/// order.
@Deprecated('It duplicates the \'inverse\' '
'from the \'package:collection\' and will be removed in v2.0.0')
Comparator<T> get reversed => (a, b) => call(b, a);
}
61 changes: 56 additions & 5 deletions lib/src/field_comparators.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:collection/collection.dart' show ComparatorExtension;
import 'package:comparators/src/util.dart';

import 'typedefs.dart';
Expand All @@ -23,9 +24,16 @@ import 'typedefs.dart';
/// );
/// // in this package, there is `compareBool` function which does the same same as in this example
/// ```
@Deprecated('It will be made private starting from v2.0.0')
Comparator<T> compareTransformed<T, R>(
FieldExtractor<T, R> fieldExtractor,
ComparableTransformer<R, Comparable> comparableTransformer,
) =>
_compareTransformed(fieldExtractor, comparableTransformer);

Comparator<T> _compareTransformed<T, R>(
FieldExtractor<T, R> fieldExtractor,
ComparableTransformer<R, Comparable> comparableTransformer,
) {
return (a, b) {
final valueA = comparableTransformer(fieldExtractor(a));
Expand All @@ -38,9 +46,6 @@ Comparator<T> compareTransformed<T, R>(
/// Returns a [Comparator] of type [T] comparing by the [Comparable] field of
/// type [R] extracted with the [fieldExtractor].
///
/// It is a shorthand for the [compareTransformed] comparator function for the
/// fields that are [Comparable] by themselves.
///
/// Example:
/// ```dart
/// // compare by a single field
Expand All @@ -58,7 +63,8 @@ Comparator<T> compareTransformed<T, R>(
/// );
/// ```
Comparator<T> compare<T>(FieldExtractor<T, Comparable> fieldExtractor) {
return compareTransformed<T, Comparable>(fieldExtractor, identityTransformer);
return _compareTransformed<T, Comparable>(
fieldExtractor, identityTransformer);
}

/// Returns a comparator for a boolean field extracted with the given
Expand All @@ -67,5 +73,50 @@ Comparator<T> compare<T>(FieldExtractor<T, Comparable> fieldExtractor) {
/// Internally it will use the integer comparison and the following
/// transformation: `true => 1, false => 0`.
Comparator<T> compareBool<T>(FieldExtractor<T, bool> fieldExtractor) {
return compareTransformed(fieldExtractor, boolTransformer);
return _compareTransformed(fieldExtractor, boolTransformer);
}

/// Returns a comparator that will compare the values of [T] using the [comparators] in their order in the iterable.
/// Next comparators are used as tie breakers for the previous ones.
///
/// This approach allows Dart to infer the comparator types from the context and does not require providing generics
/// explicitly for most cases.
///
/// Example:
/// ```dart
/// // chaining using `then` from the `package:collection`
/// users.sort(
/// compare<User>((user) => user.name).then(
/// compare<User>((user) => user.surname).then(
/// compare<User>((user) => user.country),
/// ),
/// ),
/// );
///
/// // with `compareSequentially`
/// users.sort(compareSequentially([
/// compare((user) => user.name),
/// compare((user) => user.surname),
/// compare((user) => user.country),
/// ]));
///
/// // using `inverse` from the `package:collection`
/// users.sort(compareSequentially([
/// // ...
/// compare((User user) => user.surname).inverse,
/// // ...
/// ]));
/// ```
Comparator<T> compareSequentially<T>(Iterable<Comparator<T>> comparators) {
if (comparators.isEmpty) {
throw StateError('An empty iterable of Comparators cannot be combined');
}

var combined = comparators.first;

for (final comparator in comparators.skip(1)) {
combined = combined.then(comparator);
}

return combined;
}
7 changes: 5 additions & 2 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
name: comparators
description: A package providing flexible Java-like comparator generators and extensions for Comparator functions.
version: 1.0.0
description: A package providing flexible Java-like comparator generators, featuring declarative comparator combining.
version: 1.1.0
repository: https://github.com/mitryp/comparators
issue_tracker: https://github.com/mitryp/comparators/issues

environment:
sdk: '>=2.17.0 <4.0.0'

dependencies:
collection: ^1.18.0

dev_dependencies:
lints: ^2.0.0
test: ^1.21.0
Expand Down
19 changes: 19 additions & 0 deletions test/comparators_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ void main() {
'`compareTransformed` works correctly',
() => repeat(times: testRuns, () {
final list = rList(rand);
// ignore: deprecated_member_use_from_same_package
final comparator = compareTransformed<NotComparable, NotComparable>(
(nc) => nc,
(nc) => nc.intValue,
Expand Down Expand Up @@ -72,5 +73,23 @@ void main() {
expect(isSorted(list, comparator: comparator), isTrue);
}),
);

test(
'`compareSequentially` works correctly',
() => repeat(times: testRuns, () {
final list = rList(rand);
final matcher = [...list];

final comparator = compareSequentially<NotComparable>([
compare((nc) => nc.intValue),
compareBool((nc) => nc.boolValue),
]);

list.sort(comparator);
matcher.sort(matcherComparator);

expect(list, orderedEquals(matcher));
}),
);
});
}
Loading

0 comments on commit c6875cf

Please sign in to comment.