diff --git a/lib/collection.dart b/lib/collection.dart index 612508b..31394f5 100644 --- a/lib/collection.dart +++ b/lib/collection.dart @@ -10,6 +10,7 @@ export "src/equality_map.dart"; export "src/equality_set.dart"; export "src/functions.dart"; export "src/iterable_zip.dart"; +export "src/multi_map.dart"; export "src/priority_queue.dart"; export "src/queue_list.dart"; export "src/union_set.dart"; diff --git a/lib/src/multi_map.dart b/lib/src/multi_map.dart new file mode 100755 index 0000000..1173b3b --- /dev/null +++ b/lib/src/multi_map.dart @@ -0,0 +1,367 @@ +// Copyright (c) 2017, 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 "dart:collection"; +import 'dart:convert'; + + +/** + * An implementation of [Map]. That allows for either the insertion of a value or + * a list of values for a given key. Always returning either the value or the + * last value of the inserted list through the standard Map API. + * Additional method [multiple] allows access to the list of values for a given key. + * + * Default behavior is identical to [Map] with the exception that an unmodifiable + * Multimap cannot be constructed. + */ +class MultiMap implements Map { + + LinkedHashMap> _map; + + /** + * Default constructor see [Map] + */ + MultiMap({bool equals(K key1, K key2), + int hashCode(K key), + bool isValidKey(potentialKey)}) { + _map = new LinkedHashMap>(equals:equals,hashCode:hashCode,isValidKey:isValidKey); + } + + /** + * Factory constructing a Map from a parser generated Map literal. + * [elements] contains n key-value pairs. + * Todo: rwrozelle - How do we construct a multimap from a literal? + */ + /* + factory MultiMap._fromLiteral(List elements) { + var map = new MultiMap(); + var len = elements.length; + for (int i = 1; i < len; i += 2) { + map[elements[i - 1]] = elements[i]; + } + return map; + } + */ + + /** + * Unmodifiable constructor is not implemented due to class hierarchy mismatch. + * Todo: rwrozelle - Is it important enough to figure out an unmodifiable implmentation? + */ + /* + factory MultiMap.unmodifiable(Map other) { + } + */ + + /** + * Creates an identity-based map. See [Map] + */ + factory MultiMap.identity() { + return new MultiMap(equals:identical,hashCode:identityHashCode); + } + + /** + * Creates a MultiMap that contains all key value pairs of [other]. + */ + factory MultiMap.from(Map other) { + MultiMap result = new MultiMap(); + other.forEach((k, v) + { + result[k] = v; + }); + return result; + } + + /** + * Creates a MultiMap where the keys and values are computed from the + * [iterable]. See [Map] + */ + factory MultiMap.fromIterable(Iterable iterable, + {K key(element), V value(element)}) { + MultiMap map = new MultiMap(); + MultiMapHelper._fillMapWithMappedIterable(map, iterable, key, value); + return map; + } + + /** + * Creates a MultiMap associating the given [keys] to [values]. See [Map] + */ + factory MultiMap.fromIterables(Iterable keys, Iterable values) { + MultiMap map = new MultiMap(); + MultiMapHelper._fillMapWithIterables(map, keys, values); + return map; + } + + /** + * Returns true if this map contains the given [value]. + * + * Returns true if any of the values or last of list of values inserted in the + * map are equal to `value` according to the `==` operator. + */ + bool containsValue(Object value) { + for (K key in keys) { + if (this[key] == value) return true; + } + return false; + } + + /** + * Returns true if this map contains the given [key]. + * + * Returns true if any of the keys in the map are equal to `key` + * according to the equality used by the map. + */ + bool containsKey(Object key) => _map.containsKey(key); + + /** + * Returns the value or last of list of values for the given [key] or null if [key] + * is not in the map. See [Map] for what a return of 'null' means + */ + V operator [](Object key) { + Object listValue = _map[key]; + if (listValue == null || (listValue as List).isEmpty) return null; + return (listValue as List).last; + } + + /** + * Associates the [key] with the given [value]. + * + * The associated value (or list of values) + * is added or overwritten in the map. If the [value] provided is a List, + * the last value in this list is considered the "value" for standard Map API + */ + void operator []=(K key, Object value) { + if (value is List) { + _map[key] = value; + } + else if (value is V) { + //List listValue = _map[key]; + //if (listValue != null) listValue.add(value); + //else + _map[key] = [value]; + } + } + + /** + * Look up the value of [key], or add a new value if it isn't there. + * see [Map]. + */ + V putIfAbsent(key, ifAbsent()) { + if (containsKey(key)) return this[key]; + var value = ifAbsent(); + this[key] = value; + return value; + } + + /** + * Adds all key-value pairs of [other] to this map. + * + * If a key of [other] is already in this map, its value is inserted. + * + * The operation is equivalent to doing `this[key] = value` for each key + * and associated value in other. It iterates over [other], which must + * therefore not change during the iteration. + */ + void addAll(Map other) { + for (K key in other.keys) { + this[key] = other[key]; + } + } + + /** + * Removes [key] and its associated value(s), if present, from the map. + * + * Returns the value or last of list of values associated with `key` before it + * was removed. Returns `null` if `key` was not in the map. + * + * Note that values can be `null` and a returned `null` value doesn't + * always mean that the key was absent. + */ + V remove(Object key) { + List result = _map.remove(key); + if (result == null || result.isEmpty) return null; + return result.last; + } + + /** + * Removes all pairs from the map. + * + * After this, the map is empty. + */ + void clear() => _map.clear(); + + /** + * Applies [f] to each key-value pair of the map. + * + * Calling `f` must not add or remove keys from the map. + */ + void forEach(void f(K key, V value)) { + for (K key in keys) { + f(key, this[key]); + } + } + + /** + * Applies [f] to each key-value pair of the map. Value used by [f] is the + * list of values associated with the key + * + * Calling `f` must not add or remove keys from the map. + */ + void forEachMultiple(void f(K key, List value)) { + for (K key in keys) { + f(key, this.multiple(key)); + } + } + + /** + * The keys of [this]. + * + * The returned iterable has efficient `length` and `contains` operations, + * based on [length] and [containsKey] of the map. + * + * The order of iteration is defined by the individual `Map` implementation, + * but must be consistent between changes to the map. + * + * Modifying the map while iterating the keys + * may break the iteration. + */ + Iterable get keys => _map.keys; + + /** + * The values of [this]. This is either the value or last of a list of values + * associated with the key. + * + * The values are iterated in the order of their corresponding keys. + * This means that iterating [keys] and [values] in parallel will + * provided matching pairs of keys and values. + * + * The returned iterable has an efficient `length` method based on the + * [length] of the map. Its [Iterable.contains] method is based on + * `==` comparison. + * + * Modifying the map while iterating the + * values may break the iteration. + */ + Iterable get values { + Iterable result = new List(); + for (K key in this.keys) { + (result as List).add(this[key]); + } + return result; + } + + /** + * The number of key-value pairs in the map. + */ + int get length => _map.length; + + /** + * Returns true if there is no key-value pair in the map. + */ + bool get isEmpty => _map.isEmpty; + + /** + * Returns true if there is at least one key-value pair in the map. + */ + bool get isNotEmpty => _map.isNotEmpty; + + /** + * Returns the values of the key as a list + */ + List multiple(K key) { + return _map[key]; + } + + /** + * Returns the [query] split into a map according to the rules + * specified for FORM post in the [HTML 4.01 specification section + * 17.13.4](http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4 "HTML 4.01 section 17.13.4"). + * Each key and value in the returned map has been decoded. If the [query] + * is the empty string an empty map is returned. + * + * Keys in the query string that have no value are mapped to the + * empty string. + * + * Each query component will be decoded using [encoding]. The default encoding + * is UTF-8. + * This is a copy of dart-sdk core Uri.splitQueryString modified to return + * List when parameter is duplicated, for example: + * "foo=bar&baz=bang&fi=fo&fi=fum" returns {"foo": "bar", "baz": "bang", "fi":["fo","fum"]} + */ + static MultiMap splitQueryString(String query, {Encoding encoding: UTF8}) { + MultiMap mmap = new MultiMap(); + query.split("&").fold({}, (map, element) { + int index = element.indexOf("="); + if (index == -1) { + if (element != "") { + mmap[Uri.decodeQueryComponent(element, encoding: encoding)] = ""; + } + } else if (index != 0) { + var key = element.substring(0, index); + var value = element.substring(index + 1); + var decodedKey = Uri.decodeQueryComponent(key, encoding: encoding); + var existing = mmap.multiple(decodedKey); + if (existing != null) { + existing.add(Uri.decodeQueryComponent(value, encoding: encoding)); + mmap[decodedKey] = existing; + } + else { + mmap[decodedKey] = Uri.decodeQueryComponent(value, encoding: encoding); + } + } + }); + return mmap; + } +} + +/** + * Helper class which implements complex [Map] operations + * in term of basic ones ([Map.keys], [Map.operator []], + * [Map.operator []=] and [Map.remove].) Not all methods are + * necessary to implement each particular operation. + * This was copied from /dart/lib/collection/maps.dart because of privacy in + * original location. + */ +class MultiMapHelper { + + static _id(x) => x; + + /** + * Fills a map with key/value pairs computed from [iterable]. + * + * This method is used by Map classes in the named constructor fromIterable. + */ + static void _fillMapWithMappedIterable(Map map, Iterable iterable, + key(element), value(element)) { + if (key == null) key = _id; + if (value == null) value = _id; + + for (var element in iterable) { + map[key(element)] = value(element); + } + } + + /** + * Fills a map by associating the [keys] to [values]. + * + * This method is used by Map classes in the named constructor fromIterables. + */ + static void _fillMapWithIterables(Map map, Iterable keys, + Iterable values) { + Iterator keyIterator = keys.iterator; + Iterator valueIterator = values.iterator; + + bool hasNextKey = keyIterator.moveNext(); + bool hasNextValue = valueIterator.moveNext(); + + while (hasNextKey && hasNextValue) { + map[keyIterator.current] = valueIterator.current; + hasNextKey = keyIterator.moveNext(); + hasNextValue = valueIterator.moveNext(); + } + + if (hasNextKey || hasNextValue) { + throw new ArgumentError("Iterables do not have same length."); + } + } +} \ No newline at end of file diff --git a/test/multi_map_test.dart b/test/multi_map_test.dart new file mode 100644 index 0000000..6994e08 --- /dev/null +++ b/test/multi_map_test.dart @@ -0,0 +1,263 @@ +//import 'dart:collection'; +import 'package:test/test.dart'; +import '../lib/src/multi_map.dart'; + + +void main() { + test('default constructor', () { + MultiMap map = new MultiMap(); + expect(map is MultiMap, equals(true)); + expect(map is Map, equals(true)); + }); + + /** + * Todo: Only needed if MultiMap can be instantiated from literal. + */ + /* + test('literal constructor', () { + MultiMap map = {'foo':'bar'}; + expect(map['foo'],equals('bar')); + }); + */ + + test('identity constructor', () { + MultiMap map = new MultiMap.identity(); + expect(map is MultiMap, equals(true)); + }); + + test('from constructor', () { + Map other = {'foo': 'bar', 'fi':['fo','fum']}; + MultiMap map = new MultiMap.from(other); + expect(map is MultiMap, equals(true)); + expect(map['foo'],equals('bar')); + expect(map['fi'],equals('fum')); + expect(map.multiple('fi'),equals(['fo','fum'])); + }); + + test('fromIterable constructor', () { + List list = [1, 2, 3]; + Map map = new MultiMap.fromIterable(list, + key: (item) => item.toString(), + value: (item) => item * item); + + expect(map is MultiMap, equals(true)); + expect(map['1'],equals(1)); + expect(map['2'],equals(4)); + expect(map['3'],equals(9)); + }); + + test('fromIterables constructor', () { + Map other = {'foo': 'bar', 'fi':['fo','fum']}; + MultiMap map = new MultiMap.fromIterables(other.keys, other.values); + expect(map is MultiMap, equals(true)); + expect(map['foo'],equals('bar')); + expect(map['fi'],equals('fum')); + expect(map.multiple('fi'),equals(['fo','fum'])); + }); + + test('containsValue', () { + MultiMap map = new MultiMap(); + expect(map.containsValue('b'), equals(false)); + map['a'] = 'b'; + expect(map.containsValue('b'), equals(true)); + List cd = ['c', 'd']; + map['b'] = cd; + expect(map.containsValue('b'), equals(true)); + expect(map.containsValue('c'), equals(false)); + expect(map.containsValue('d'), equals(true)); + map['b'] = 'e'; + expect(map.containsValue('b'), equals(true)); + expect(map.containsValue('c'), equals(false)); + expect(map.containsValue('d'), equals(false)); + expect(map.containsValue('e'), equals(true)); + }); + + test('containsKey', () { + MultiMap map = new MultiMap(); + expect(map.containsKey('a'), equals(false)); + map['a'] = 'b'; + expect(map.containsKey('a'), equals(true)); + List cd = ['c', 'd']; + map['b'] = cd; + expect(map.containsKey('a'), equals(true)); + expect(map.containsKey('b'), equals(true)); + map['c'] = 'e'; + expect(map.containsKey('a'), equals(true)); + expect(map.containsKey('b'), equals(true)); + expect(map.containsKey('c'), equals(true)); + }); + + test('[]=', () { + MultiMap map = new MultiMap(); + expect(map['a'], equals(null)); + map['a'] = 'b'; + expect(map['a'], equals('b')); + List bc = ['b', 'c']; + map['a'] = bc; + expect(map['a'], equals('c')); + map['a'] = 'd'; + expect(map['a'], equals('d')); + }); + + test('putIfAbsent', () { + MultiMap map = new MultiMap(); + map.putIfAbsent(4, () { + map[5] = 5; + map[4] = -1; + return 4; + }); + expect(map[4], equals(4)); + map.putIfAbsent(4, (){return 25;}); + expect(map[4], equals(4)); + map.putIfAbsent(3, (){return 25;}); + expect(map[3], equals(25)); + }); + + test('addAll', () { + Map other = {'foo': 'bar', 'fi':['fo','fum']}; + MultiMap map = new MultiMap(); + map.addAll(other); + expect(map['foo'],equals('bar')); + expect(map['fi'],equals('fum')); + expect(map.multiple('fi'),equals(['fo','fum'])); + }); + + test('remove', () { + Map other = {'foo': 'bar', 'fi':['fo','fum']}; + MultiMap map = new MultiMap(); + map.addAll(other); + expect(map.remove('foo'),equals('bar')); + expect(map['foo'],equals(null)); + expect(map.containsKey('foo'),equals(false)); + + expect(map.remove('fi'),equals('fum')); + expect(map['fi'],equals(null)); + expect(map.containsKey('fi'),equals(false)); + expect(map.multiple('fi'),equals(null)); + }); + + test('clear', () { + Map other = {'foo': 'bar', 'fi':['fo','fum']}; + MultiMap map = new MultiMap(); + map.addAll(other); + map.clear(); + expect(map.length,equals(0)); + expect(map.keys,equals([])); + }); + + test('forEach', () { + Map other = {'foo': 'bar', 'fi':['fo','fum']}; + MultiMap map = new MultiMap(); + map.addAll(other); + + int i = 0; + map.forEach((String key, String value){ + if (key == 'foo') { + expect(i,equals(0)); + expect(value, equals('bar')); + } + if (key == 'fi') { + expect(i,equals(1)); + expect(value, equals('fum')); + } + i++; + }); + }); + + test('forEachMultiple', () { + Map other = {'foo': 'bar', 'fi':['fo','fum']}; + MultiMap map = new MultiMap(); + map.addAll(other); + + int i = 0; + map.forEachMultiple((String key, List value){ + if (key == 'foo') { + expect(i,equals(0)); + expect(value, equals(['bar'])); + } + if (key == 'fi') { + expect(i,equals(1)); + expect(value, equals(['fo','fum'])); + } + i++; + }); + }); + + test('get keys', () { + MultiMap map = new MultiMap(); + expect(map['a'], equals(null)); + map['a'] = 'b'; + List cd = ['c', 'd']; + map['b'] = cd; + Iterable keys = map.keys; + int i = 0; + for (String key in keys) { + if (i == 0) expect(key, equals('a')); + if (i == 1) expect(key, equals('b')); + i++; + } + }); + + test('get values', () { + MultiMap map = new MultiMap(); + expect(map['a'], equals(null)); + map['a'] = 'b'; + List cd = ['c', 'd']; + map['b'] = cd; + Iterable values = map.values; + int i = 0; + for (String value in values) { + if (i == 0) expect(value, equals('b')); + if (i == 1) expect(value, equals('d')); + i++; + } + }); + + test('length', () { + MultiMap map = new MultiMap(); + expect(map.length, equals(0)); + map['a'] = 'b'; + expect(map.length, equals(1)); + }); + + test('isEmpty', () { + MultiMap map = new MultiMap(); + expect(map.isEmpty, equals(true)); + map['a'] = 'b'; + expect(map.isEmpty, equals(false)); + }); + + test('isNotEmpty', () { + MultiMap map = new MultiMap(); + expect(map.isNotEmpty, equals(false)); + map['a'] = 'b'; + expect(map.isNotEmpty, equals(true)); + }); + + + test('multiple', () { + MultiMap map = new MultiMap(); + expect(map.multiple('a'), equals(null)); + map['a'] = 'b'; + expect(map.multiple('a'), equals(['b'])); + List cd = ['c', 'd']; + map['b'] = cd; + expect(map.multiple('b'), equals(['c', 'd'])); + map['b'] = 'e'; + expect(map.multiple('b'), equals(['e'])); + + List cdprime = ['c', 'd']; + map['b'] = cdprime; + expect(map.multiple('b'), equals(['c', 'd'])); + + }); +} \ No newline at end of file