Add EqualityBy (#40)

Closes #38
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2b05ddc..7cca956 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 1.13.0
+
+* Add `EqualityBy`
+
 ## 1.12.0
 
 * Add `CaseInsensitiveEquality`.
diff --git a/lib/src/equality.dart b/lib/src/equality.dart
index a901297..3b10176 100644
--- a/lib/src/equality.dart
+++ b/lib/src/equality.dart
@@ -30,6 +30,48 @@
   bool isValidKey(Object o);
 }
 
+typedef F _GetKey<E, F>(E object);
+
+/// Equality of objects based on derived values.
+///
+/// For example, given the class:
+/// ```dart
+/// abstract class Employee {
+///   int get employmentId;
+/// }
+/// ```
+///
+/// The following [Equality] considers employees with the same IDs to be equal:
+/// ```dart
+/// new EqualityBy((Employee e) => e.employmentId);
+/// ```
+///
+/// It's also possible to pass an additional equality instance that should be
+/// used to compare the value itself.
+class EqualityBy<E, F> implements Equality<E> {
+  // Returns a derived value F from an object E.
+  final _GetKey<E, F> _getKey;
+
+  // Determines equality between two values of F.
+  final Equality<F> _inner;
+
+  EqualityBy(F getKey(E object), [Equality<F> inner = const DefaultEquality()])
+      : _getKey = getKey,
+        _inner = inner;
+
+  bool equals(E e1, E e2) => _inner.equals(_getKey(e1), _getKey(e2));
+
+  int hash(E e) => _inner.hash(_getKey(e));
+
+  bool isValidKey(Object o) {
+    if (o is E) {
+      final value = _getKey(o);
+      return value is F && _inner.isValidKey(value);
+    }
+    return false;
+  }
+}
+
 /// Equality of objects that compares only the natural equality of the objects.
 ///
 /// This equality uses the objects' own [Object.==] and [Object.hashCode] for
diff --git a/pubspec.yaml b/pubspec.yaml
index ad3f111..587f9f0 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: collection
-version: 1.12.0
+version: 1.13.0
 author: Dart Team <misc@dartlang.org>
 description: Collections and utilities functions and classes related to collections.
 homepage: https://www.github.com/dart-lang/collection
diff --git a/test/equality_test.dart b/test/equality_test.dart
index 7d9246d..5352346 100644
--- a/test/equality_test.dart
+++ b/test/equality_test.dart
@@ -175,6 +175,52 @@
     expect(equality.hash("foo"), isNot(equals(equality.hash("bar"))));
     expect(equality.hash("fÕÕ"), isNot(equals(equality.hash("fõõ"))));
   });
+
+  group("EqualityBy should use a derived value for ", () {
+    var firstEquality = new EqualityBy<List<String>, String>(
+        (e) => e.first);
+    var firstInsensitiveEquality = new EqualityBy<List<String>, String>(
+        (e) => e.first, const CaseInsensitiveEquality());
+    var firstObjectEquality = new EqualityBy<List<Object>, Object>(
+        (e) => e.first, const IterableEquality());
+
+    test("equality", () {
+      expect(
+          firstEquality.equals(
+              ["foo", "foo"], ["foo", "bar"]),
+          isTrue);
+      expect(
+          firstEquality.equals(
+              ["foo", "foo"], ["bar", "bar"]),
+          isFalse);
+    });
+
+    test("equality with an inner equality", () {
+      expect(firstInsensitiveEquality.equals(["fOo"], ["FoO"]), isTrue);
+      expect(firstInsensitiveEquality.equals(["foo"], ["ffõõ"]), isFalse);
+    });
+
+    test("hash", () {
+      expect(firstEquality.hash(["foo", "bar"]), "foo".hashCode);
+    });
+
+    test("hash with an inner equality", () {
+      expect(
+          firstInsensitiveEquality.hash(["fOo"]),
+          const CaseInsensitiveEquality().hash("foo"));
+    });
+
+    test("isValidKey", () {
+      expect(firstEquality.isValidKey(["foo"]), isTrue);
+      expect(firstEquality.isValidKey("foo"), isFalse);
+      expect(firstEquality.isValidKey([1]), isFalse);
+    });
+
+    test('isValidKey with an inner equality', () {
+      expect(firstObjectEquality.isValidKey([[]]), isTrue);
+      expect(firstObjectEquality.isValidKey([{}]), isFalse);
+    });
+  });
 }
 
 /// Wrapper objects for an `id` value.