Introduce a new lastBy() function (#223)

Similar to groupBy(), except that it only keeps the latest value
corresponding to a given key.

Prior art: [Kotlin](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.sequences/associate-by.html)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 806a8b0..f36e987 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 ## 1.16.1-dev
 
+* Add a top-level `lastBy()` function that converts an `Iterable` to a `Map` by
+  grouping its elements using a function, keeping the last element for each
+  computed key. Also available as an extension method on `Iterable`.
+
 ## 1.16.0
 
 * Add an `Iterable.slices` extension method.
diff --git a/lib/src/functions.dart b/lib/src/functions.dart
index 64a38ee..8f60b26 100644
--- a/lib/src/functions.dart
+++ b/lib/src/functions.dart
@@ -41,6 +41,13 @@
   return result;
 }
 
+/// Associates the elements in [values] by the value returned by [key].
+///
+/// Returns a map from keys computed by [key] to the last value for which [key]
+/// returns that key.
+Map<T, S> lastBy<S, T>(Iterable<S> values, T Function(S) key) =>
+    {for (var element in values) key(element): element};
+
 /// Groups the elements in [values] by the value returned by [key].
 ///
 /// Returns a map from keys computed by [key] to a list of all values for which
diff --git a/lib/src/iterable_extensions.dart b/lib/src/iterable_extensions.dart
index ac82112..88f320d 100644
--- a/lib/src/iterable_extensions.dart
+++ b/lib/src/iterable_extensions.dart
@@ -7,6 +7,7 @@
 import 'package:collection/src/utils.dart';
 
 import 'algorithms.dart';
+import 'functions.dart' as functions;
 
 /// Extensions that apply to all iterables.
 ///
@@ -353,6 +354,12 @@
     return null;
   }
 
+  /// Associates the elements in [this] by the value returned by [key].
+  ///
+  /// Returns a map from keys computed by [key] to the last value for which [key]
+  /// returns that key.
+  Map<K, T> lastBy<K>(K Function(T) key) => functions.lastBy(this, key);
+
   /// Groups elements by [keyOf] then folds the elements in each group.
   ///
   /// A key is found for each element using [keyOf].
diff --git a/test/extensions_test.dart b/test/extensions_test.dart
index 1ccebde..ce4b2e9 100644
--- a/test/extensions_test.dart
+++ b/test/extensions_test.dart
@@ -520,6 +520,25 @@
           expect(iterable([1, 3, 5]).singleOrNull, null);
         });
       });
+      group('.lastBy', () {
+        test('empty', () {
+          expect(iterable([]).lastBy((dynamic _) {}), {});
+        });
+        test('single', () {
+          expect(iterable([1]).lastBy(toString), {
+            '1': 1,
+          });
+        });
+        test('multiple', () {
+          expect(
+            iterable([1, 2, 3, 4, 5]).lastBy((x) => x.isEven),
+            {
+              false: 5,
+              true: 4,
+            },
+          );
+        });
+      });
       group('.groupFoldBy', () {
         test('empty', () {
           expect(iterable([]).groupFoldBy(unreachable, unreachable), {});
diff --git a/test/functions_test.dart b/test/functions_test.dart
index 97bb4ab..9d41625 100644
--- a/test/functions_test.dart
+++ b/test/functions_test.dart
@@ -79,6 +79,26 @@
     });
   });
 
+  group('lastBy()', () {
+    test('returns an empty map for an empty iterable', () {
+      expect(
+        lastBy([], (_) => fail("Must not be called for empty input")),
+        isEmpty,
+      );
+    });
+
+    test("keeps the latest element for the function's return value", () {
+      expect(
+          lastBy(['foo', 'bar', 'baz', 'bop', 'qux'],
+              (String string) => string[1]),
+          equals({
+            'o': 'bop',
+            'a': 'baz',
+            'u': 'qux',
+          }));
+    });
+  });
+
   group('groupBy()', () {
     test('returns an empty map for an empty iterable', () {
       expect(groupBy([], expectAsync1((dynamic _) {}, count: 0)), isEmpty);