Add a CanonicalizedMap class to pkg/collection.

BUG=18708
R=lrn@google.com

Review URL: https://codereview.chromium.org//350183010

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/collection@37908 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6c1c554..1ed9863 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 0.9.4
+
+* Add a `CanonicalizedMap` class that canonicalizes its keys to provide a custom
+  equality relation.
+
 ## 0.9.3+1
 
 * Fix all analyzer hints.
diff --git a/lib/collection.dart b/lib/collection.dart
index d640071..d24dd39 100644
--- a/lib/collection.dart
+++ b/lib/collection.dart
@@ -21,4 +21,5 @@
 export "equality.dart";
 export "iterable_zip.dart";
 export "priority_queue.dart";
+export "src/canonicalized_map.dart";
 export "wrappers.dart";
diff --git a/lib/src/canonicalized_map.dart b/lib/src/canonicalized_map.dart
new file mode 100644
index 0000000..f1cd859
--- /dev/null
+++ b/lib/src/canonicalized_map.dart
@@ -0,0 +1,115 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library dart.pkg.collection.canonicalized_map;
+
+import 'dart:collection';
+
+import 'utils.dart';
+
+/**
+ * A map whose keys are converted to canonical values of type `C`.
+ *
+ * This is useful for using case-insensitive String keys, for example. It's more
+ * efficient than a [LinkedHashMap] with a custom equality operator because it
+ * only canonicalizes each key once, rather than doing so for each comparison.
+ *
+ * By default, `null` is allowed as a key. It can be forbidden via the
+ * `isValidKey` parameter.
+ */
+class CanonicalizedMap<C, K, V> implements Map<K, V> {
+  final Function _canonicalize;
+
+  final Function _isValidKeyFn;
+
+  final _base = new Map<C, Pair<K, V>>();
+
+  /**
+   * Creates an empty canonicalized map.
+   *
+   * The [canonicalize] function should return the canonical value for the given
+   * key. Keys with the same canonical value are considered equivalent.
+   *
+   * The [isValidKey] function is called before calling [canonicalize] for
+   * methods that take arbitrary objects. It can be used to filter out keys that
+   * can't be canonicalized.
+   */
+  CanonicalizedMap(C canonicalize(K key), {bool isValidKey(K key)})
+      : _canonicalize = canonicalize,
+        _isValidKeyFn = isValidKey;
+
+  /**
+   * Creates a canonicalized map that is initialized with the key/value pairs of
+   * [other].
+   *
+   * The [canonicalize] function should return the canonical value for the given
+   * key. Keys with the same canonical value are considered equivalent.
+   *
+   * The [isValidKey] function is called before calling [canonicalize] for
+   * methods that take arbitrary objects. It can be used to filter out keys that
+   * can't be canonicalized.
+   */
+  CanonicalizedMap.from(Map<K, V> other, C canonicalize(K key),
+          {bool isValidKey(K key)})
+      : _canonicalize = canonicalize,
+        _isValidKeyFn = isValidKey {
+    addAll(other);
+  }
+
+  V operator [](Object key) {
+    if (!_isValidKey(key)) return null;
+    var pair = _base[_canonicalize(key)];
+    return pair == null ? null : pair.last;
+  }
+
+  void operator []=(K key, V value) {
+    _base[_canonicalize(key)] = new Pair(key, value);
+  }
+
+  void addAll(Map<K, V> other) {
+    other.forEach((key, value) => this[key] = value);
+  }
+
+  void clear() {
+    _base.clear();
+  }
+
+  bool containsKey(Object key) {
+    if (!_isValidKey(key)) return false;
+    return _base.containsKey(_canonicalize(key));
+  }
+
+  bool containsValue(Object value) =>
+      _base.values.any((pair) => pair.last == value);
+
+  void forEach(void f(K key, V value)) {
+    _base.forEach((key, pair) => f(pair.first, pair.last));
+  }
+
+  bool get isEmpty => _base.isEmpty;
+
+  bool get isNotEmpty => _base.isNotEmpty;
+
+  Iterable<K> get keys => _base.values.map((pair) => pair.first);
+
+  int get length => _base.length;
+
+  V putIfAbsent(K key, V ifAbsent()) {
+    return _base.putIfAbsent(_canonicalize(key),
+        () => new Pair(key, ifAbsent())).last;
+  }
+
+  V remove(Object key) {
+    if (!_isValidKey(key)) return null;
+    var pair = _base.remove(_canonicalize(key));
+    return pair == null ? null : pair.last;
+  }
+
+  Iterable<V> get values => _base.values.map((pair) => pair.last);
+
+  String toString() => Maps.mapToString(this);
+
+  bool _isValidKey(Object key) => (key == null || key is K) &&
+      (_isValidKeyFn == null || _isValidKeyFn(key));
+}
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
new file mode 100644
index 0000000..c9c7537
--- /dev/null
+++ b/lib/src/utils.dart
@@ -0,0 +1,13 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library dart.pkg.collection.utils;
+
+/// A pair of values.
+class Pair<E, F> {
+  E first;
+  F last;
+
+  Pair(this.first, this.last);
+}
diff --git a/lib/wrappers.dart b/lib/wrappers.dart
index 2034174..509a966 100644
--- a/lib/wrappers.dart
+++ b/lib/wrappers.dart
@@ -16,6 +16,7 @@
 
 export "dart:collection" show UnmodifiableListView;
 
+export "src/canonicalized_map.dart";
 part "src/unmodifiable_wrappers.dart";
 
 /**
diff --git a/pubspec.yaml b/pubspec.yaml
index 80b0972..3d48447 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: collection
-version: 0.9.3+1
+version: 0.9.4
 author: Dart Team <misc@dartlang.org>
 description: Collections and utilities functions and classes related to collections.
 homepage: http://www.dartlang.org
diff --git a/test/canonicalized_map_test.dart b/test/canonicalized_map_test.dart
new file mode 100644
index 0000000..1793777
--- /dev/null
+++ b/test/canonicalized_map_test.dart
@@ -0,0 +1,166 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import "package:collection/collection.dart";
+import "package:unittest/unittest.dart";
+
+void main() {
+  group("with an empty canonicalized map", () {
+    var map;
+    setUp(() {
+      map = new CanonicalizedMap<int, String, String>(int.parse,
+          isValidKey: new RegExp(r"^\d+$").hasMatch);
+    });
+
+    test("canonicalizes keys on set and get", () {
+      map["1"] = "value";
+      expect(map["01"], equals("value"));
+    });
+
+    test("get returns null for uncanonicalizable key", () {
+      expect(map["foo"], isNull);
+    });
+
+    test("canonicalizes keys for addAll", () {
+      map.addAll({
+        "1": "value 1",
+        "2": "value 2",
+        "3": "value 3"
+      });
+      expect(map["01"], equals("value 1"));
+      expect(map["02"], equals("value 2"));
+      expect(map["03"], equals("value 3"));
+    });
+
+    test("uses the final value for addAll collisions", () {
+      map.addAll({
+        "1": "value 1",
+        "01": "value 2",
+        "001": "value 3"
+      });
+      expect(map.length, equals(1));
+      expect(map["0001"], equals("value 3"));
+    });
+
+    test("clear clears the map", () {
+      map.addAll({
+        "1": "value 1",
+        "2": "value 2",
+        "3": "value 3"
+      });
+      expect(map, isNot(isEmpty));
+      map.clear();
+      expect(map, isEmpty);
+    });
+
+    test("canonicalizes keys for containsKey", () {
+      map["1"] = "value";
+      expect(map.containsKey("01"), isTrue);
+      expect(map.containsKey("2"), isFalse);
+    });
+
+    test("containsKey returns false for uncanonicalizable key", () {
+      expect(map.containsKey("foo"), isFalse);
+    });
+
+    test("canonicalizes keys for putIfAbsent", () {
+      map["1"] = "value";
+      expect(map.putIfAbsent("01", () => throw "shouldn't run"),
+             equals("value"));
+      expect(map.putIfAbsent("2", () => "new value"), equals("new value"));
+    });
+
+    test("canonicalizes keys for remove", () {
+      map["1"] = "value";
+      expect(map.remove("2"), isNull);
+      expect(map.remove("01"), equals("value"));
+      expect(map, isEmpty);
+    });
+
+    test("remove returns null for uncanonicalizable key", () {
+      expect(map.remove("foo"), isNull);
+    });
+
+    test("containsValue returns whether a value is in the map", () {
+      map["1"] = "value";
+      expect(map.containsValue("value"), isTrue);
+      expect(map.containsValue("not value"), isFalse);
+    });
+
+    test("isEmpty returns whether the map is empty", () {
+      expect(map.isEmpty, isTrue);
+      map["1"] = "value";
+      expect(map.isEmpty, isFalse);
+      map.remove("01");
+      expect(map.isEmpty, isTrue);
+    });
+
+    test("isNotEmpty returns whether the map isn't empty", () {
+      expect(map.isNotEmpty, isFalse);
+      map["1"] = "value";
+      expect(map.isNotEmpty, isTrue);
+      map.remove("01");
+      expect(map.isNotEmpty, isFalse);
+    });
+
+    test("length returns the number of pairs in the map", () {
+      expect(map.length, equals(0));
+      map["1"] = "value 1";
+      expect(map.length, equals(1));
+      map["01"] = "value 01";
+      expect(map.length, equals(1));
+      map["02"] = "value 02";
+      expect(map.length, equals(2));
+    });
+
+    test("uses original keys for keys", () {
+      map["001"] = "value 1";
+      map["02"] = "value 2";
+      expect(map.keys, equals(["001", "02"]));
+    });
+
+    test("uses original keys for forEach", () {
+      map["001"] = "value 1";
+      map["02"] = "value 2";
+
+      var keys = [];
+      map.forEach((key, value) => keys.add(key));
+      expect(keys, equals(["001", "02"]));
+    });
+
+    test("values returns all values in the map", () {
+      map.addAll({
+        "1": "value 1",
+        "01": "value 01",
+        "2": "value 2",
+        "03": "value 03"
+      });
+
+      expect(map.values, equals(["value 01", "value 2", "value 03"]));
+    });
+  });
+
+  group("CanonicalizedMap.from", () {
+    test("canonicalizes its keys", () {
+      var map = new CanonicalizedMap.from({
+        "1": "value 1",
+        "2": "value 2",
+        "3": "value 3"
+      }, int.parse);
+      expect(map["01"], equals("value 1"));
+      expect(map["02"], equals("value 2"));
+      expect(map["03"], equals("value 3"));
+    });
+
+    test("uses the final value for collisions", () {
+      var map = new CanonicalizedMap.from({
+        "1": "value 1",
+        "01": "value 2",
+        "001": "value 3"
+      }, int.parse);
+      expect(map.length, equals(1));
+      expect(map["0001"], equals("value 3"));
+    });
+  });
+}