Wrote functions to convert collections and maps to strings and invoked

these functions from the toString methods of all built-in Collection
and Map implementations. Wrote smoke tests and basher tests.
Review URL: https://chromiumcodereview.appspot.com//9431015

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@4421 260f80e4-7a28-3924-810f-c04153c831b5
This commit is contained in:
jjb@google.com 2012-02-22 02:28:01 +00:00
parent 372a1e8602
commit a2f2769ec3
15 changed files with 531 additions and 78 deletions

View file

@ -0,0 +1,150 @@
// Copyright (c) 2012, 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.
/**
* The [Collections] class implements static methods useful when
* writing a class that implements [Collection] and the [iterator]
* method.
*/
class Collections {
static void forEach(Iterable<Object> iterable, void f(Object o)) {
for (final e in iterable) {
f(e);
}
}
static bool some(Iterable<Object> iterable, bool f(Object o)) {
for (final e in iterable) {
if (f(e)) return true;
}
return false;
}
static bool every(Iterable<Object> iterable, bool f(Object o)) {
for (final e in iterable) {
if (!f(e)) return false;
}
return true;
}
static List<Object> map(Iterable<Object> source,
List<Object> destination,
f(Object o)) {
for (final e in source) {
destination.add(f(e));
}
return destination;
}
static List<Object> filter(Iterable<Object> source,
List<Object> destination,
bool f(Object o)) {
for (final e in source) {
if (f(e)) destination.add(e);
}
return destination;
}
static bool isEmpty(Iterable<Object> iterable) {
return !iterable.iterator().hasNext();
}
// TODO(jjb): visiting list should be an identityHashSet when it exists
/**
* Returns a string representing the specified collection. If the
* collection is a [List], the returned string looks like this:
* [:'[element0, element1, ... elementN]':]. The value returned by its
* [toString] method is used to represent each element. If the specified
* collection is not a list, the returned string looks like this:
* [:{element0, element1, ... elementN}:]. In other words, the strings
* returned for lists are surrounded by square brackets, while the strings
* returned for other collections are surrounded by curly braces.
*
* If the specified collection contains a reference to itself, either
* directly or indirectly through other collections or maps, the contained
* reference is rendered as [:'[...]':] if it is a list, or [:'{...}':] if
* it is not. This prevents the infinite regress that would otherwise occur.
* So, for example, calling this method on a list whose sole element is a
* reference to itself would return [:'[[...]]':].
*
* A typical implementation of a collection's [toString] method will
* simply return the results of this method applied to the collection.
*/
static String collectionToString(Collection c) {
var result = new StringBuffer();
_emitCollection(c, result, new List());
return result.toString();
}
/**
* Appends a string representing the specified collection to the specified
* string buffer. The string is formatted as per [collectionToString].
* The [:visiting:] list contains references to all of the enclosing
* collections and maps (which are currently in the process of being
* emitted into [:result:]). The [:visiting:] parameter allows this method to
* generate a [:'[...]':] or [:'{...}':] where required. In other words,
* it allows this method and [_emitMap] to identify recursive collections
* and maps.
*/
static void _emitCollection(Collection c, StringBuffer result, List visiting) {
visiting.add(c);
bool isList = c is List;
result.add(isList ? '[' : '{');
bool first = true;
for (var e in c) {
if (!first) {
result.add(', ');
}
first = false;
_emitObject(e, result, visiting);
}
result.add(isList ? ']' : '}');
visiting.removeLast();
}
/**
* Appends a string representing the specified object to the specified
* string buffer. If the object is a [Collection] or [Map], it is formatted
* as per [collectionToString] or [mapToString]; otherwise, it is formatted
* by invoking its own [toString] method.
*
* The [:visiting:] list contains references to all of the enclosing
* collections and maps (which are currently in the process of being
* emitted into [:result:]). The [:visiting:] parameter allows this method
* to generate a [:'[...]':] or [:'{...}':] where required. In other words,
* it allows this method and [_emitCollection] to identify recursive maps
* and collections.
*/
static void _emitObject(Object o, StringBuffer result, List visiting) {
if (o is Collection) {
if (_containsRef(visiting, o)) {
result.add(o is List ? '[...]' : '{...}');
} else {
_emitCollection(o, result, visiting);
}
} else if (o is Map) {
if (_containsRef(visiting, o)) {
result.add('{...}');
} else {
Maps._emitMap(o, result, visiting);
}
} else { // o is neither a collection nor a map
result.add(o);
}
}
/**
* Returns true if the specified collection contains the specified object
* reference.
*/
static _containsRef(Collection c, Object ref) {
for (var e in c) {
if (e === ref) return true;
}
return false;
}
}

View file

@ -4,6 +4,7 @@
{
'sources': [
'collections.dart',
'dual_pivot_quicksort.dart',
'duration_implementation.dart',
'exceptions.dart',

View file

@ -1,4 +1,4 @@
// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file
// Copyright (c) 2012, 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.
@ -268,6 +268,10 @@ class HashMapImplementation<K extends Hashable, V> implements HashMap<K, V> {
}
return false;
}
String toString() {
return Maps.mapToString(this);
}
}
class HashSetImplementation<E extends Hashable> implements HashSet<E> {
@ -376,6 +380,10 @@ class HashSetImplementation<E extends Hashable> implements HashSet<E> {
return new HashSetIterator<E>(this);
}
String toString() {
return Collections.collectionToString(this);
}
// The map backing this set. The associations in this map are all
// of the form element -> element. If a value is not in the map,
// then it is not in the set.

View file

@ -1,4 +1,4 @@
// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file
// Copyright (c) 2012, 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.
@ -114,4 +114,8 @@ class LinkedHashMapImplementation<K extends Hashable, V>
_map.clear();
_list.clear();
}
String toString() {
return Maps.mapToString(this);
}
}

View file

@ -1,4 +1,4 @@
// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file
// Copyright (c) 2012, 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.
@ -59,4 +59,55 @@ class Maps {
static int length(Map map) => map.getKeys().length;
static bool isEmpty(Map map) => length(map) == 0;
/**
* Returns a string representing the specified map. The returned string
* looks like this: [:'{key0: value0, key1: value1, ... keyN: valueN}':].
* The value returned by its [toString] method is used to represent each
* key or value.
*
* If the map collection contains a reference to itself, either
* directly as a key or value, or indirectly through other collections
* or maps, the contained reference is rendered as [:'{...}':]. This
* prevents the infinite regress that would otherwise occur. So, for example,
* calling this method on a map whose sole entry maps the string key 'me'
* to a reference to the map would return [:'{me: {...}}':].
*
* A typical implementation of a map's [toString] method will
* simply return the results of this method applied to the collection.
*/
static String mapToString(Map m) {
var result = new StringBuffer();
_emitMap(m, result, new List());
return result.toString();
}
/**
* Appends a string representing the specified map to the specified
* string buffer. The string is formatted as per [mapToString].
* The [:visiting:] list contains references to all of the enclosing
* collections and maps (which are currently in the process of being
* emitted into [:result:]). The [:visiting:] parameter allows this method
* to generate a [:'[...]':] or [:'{...}':] where required. In other words,
* it allows this method and [_emitCollection] to identify recursive maps
* and collections.
*/
static void _emitMap(Map m, StringBuffer result, List visiting) {
visiting.add(m);
result.add('{');
bool first = true;
m.forEach((k, v) {
if (!first) {
result.add(', ');
}
first = false;
Collections._emitObject(k, result, visiting);
result.add(': ');
Collections._emitObject(v, result, visiting);
});
result.add('}');
visiting.removeLast();
}
}

View file

@ -1,4 +1,4 @@
// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file
// Copyright (c) 2012, 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.
@ -231,6 +231,10 @@ class DoubleLinkedQueue<E> implements Queue<E> {
_DoubleLinkedQueueIterator<E> iterator() {
return new _DoubleLinkedQueueIterator<E>(_sentinel);
}
String toString() {
return Collections.collectionToString(this);
}
}
class _DoubleLinkedQueueIterator<E> implements Iterator<E> {

View file

@ -1,4 +1,4 @@
// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file
// Copyright (c) 2012, 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.
@ -234,4 +234,8 @@ class SplayTree<K extends Comparable, V> implements Map<K, V> {
forEach((K k, V v) { list.add(v); });
return list;
}
String toString() {
return Maps.mapToString(this);
}
}

View file

@ -1,4 +1,4 @@
// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file
// Copyright (c) 2012, 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.
@ -31,7 +31,7 @@ class ObjectArray<E> implements List<E> {
void operator []=(int index, E value) native "ObjectArray_setIndexed";
String toString() {
return Arrays.asString(this);
return Collections.collectionToString(this);
}
int get length() native "ObjectArray_getLength";
@ -238,7 +238,7 @@ class ImmutableArray<E> implements List<E> {
}
String toString() {
return Arrays.asString(this);
return Collections.collectionToString(this);
}
int indexOf(E element, [int start = 0]) {

View file

@ -1,21 +1,9 @@
// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file
// Copyright (c) 2012, 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.
// TODO(ngeoffray): Rename to Lists.
class Arrays {
static String asString(List list) {
String result = "[";
int len = list.length;
for (int i = 0; i < len; i++) {
// TODO(4466785): Deal with recursion and formatting.
result += list[i].toString() + ", ";
}
result += "]";
return result;
}
static void copy(List src, int srcStart,
List dst, int dstStart, int count) {
if (srcStart === null) srcStart = 0;

View file

@ -141,6 +141,10 @@ class _ByteArrayBase {
return this[length - 1];
}
String toString() {
return Collections.collectionToString(this);
}
void setRange(int start, int length, List<int> from, [int startFrom = 0]) {
if (length < 0) {
throw new IllegalArgumentException("negative length $length");

View file

@ -1,52 +0,0 @@
// Copyright (c) 2011, 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.
/**
* The [Collections] class implements static methods useful when
* writing a class that implements [Collection] and the [iterator]
* method.
*/
class Collections {
static void forEach(Iterable<Object> iterable, void f(Object o)) {
for (final e in iterable) {
f(e);
}
}
static bool some(Iterable<Object> iterable, bool f(Object o)) {
for (final e in iterable) {
if (f(e)) return true;
}
return false;
}
static bool every(Iterable<Object> iterable, bool f(Object o)) {
for (final e in iterable) {
if (!f(e)) return false;
}
return true;
}
static List<Object> map(Iterable<Object> source,
List<Object> destination,
f(Object o)) {
for (final e in source) {
destination.add(f(e));
}
return destination;
}
static List<Object> filter(Iterable<Object> source,
List<Object> destination,
bool f(Object o)) {
for (final e in source) {
if (f(e)) destination.add(e);
}
return destination;
}
static bool isEmpty(Iterable<Object> iterable) {
return !iterable.iterator().hasNext();
}
}

View file

@ -1,4 +1,4 @@
// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file
// Copyright (c) 2012, 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.
@ -227,7 +227,7 @@ class GrowableObjectArray<T> implements List<T> {
}
String toString() {
return Arrays.asString(this);
return Collections.collectionToString(this);
}
Iterator<T> iterator() {

View file

@ -88,8 +88,7 @@ class ImmutableMap<K, V> implements Map<K, V> {
}
String toString() {
// TODO(srdjan): Extend text representation.
return "ImmutableMap";
return Maps.mapToString(this);
}
}

View file

@ -10,7 +10,6 @@
'array.dart',
'arrays.dart',
'bool.dart',
'collections.dart',
'date.cc',
'date.dart',
'double.cc',

View file

@ -0,0 +1,293 @@
// Copyright (c) 2012, 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.
/**
* Tests for the toString methods on collections (including maps).
*/
// TODO(jjb): seed random number generator when API allows it
final int NUM_TESTS = 3000;
final int MAX_COLLECTION_SIZE = 7;
main() {
smokeTest();
exactTest();
inexactTest();
}
/**
* Test a few simple examples.
*/
void smokeTest() {
// Non-const lists
Expect.equals([].toString(), '[]');
Expect.equals([1].toString(), '[1]');
Expect.equals(['Elvis'].toString(), '[Elvis]');
Expect.equals([1, 2].toString(), '[1, 2]');
Expect.equals(['I', 'II'].toString(), '[I, II]');
Expect.equals([[1, 2], [3, 4], [5, 6]].toString(), '[[1, 2], [3, 4], [5, 6]]');
// Const lists
Expect.equals((const[]).toString(), '[]');
Expect.equals((const[1]).toString(), '[1]');
Expect.equals((const['Elvis']).toString(), '[Elvis]');
Expect.equals((const[1, 2]).toString(), '[1, 2]');
Expect.equals((const['I', 'II']).toString(), '[I, II]');
Expect.equals((const[const[1, 2], const[3, 4], const[5, 6]]).toString(),
'[[1, 2], [3, 4], [5, 6]]');
// Non-const maps - Note that all keys are strings; the spec currently demands this
Expect.equals({}.toString(), '{}');
Expect.equals({'Elvis': 'King'}.toString(), '{Elvis: King}');
Expect.equals({'I': 1, 'II': 2}.toString(), '{I: 1, II: 2}');
Expect.equals({'X':{'I':1, 'II':2}, 'Y':{'III':3, 'IV':4}, 'Z':{'V':5, 'VI':6}}.toString(),
'{X: {I: 1, II: 2}, Y: {III: 3, IV: 4}, Z: {V: 5, VI: 6}}');
// Const maps
Expect.equals(const{}.toString(), '{}');
Expect.equals(const{'Elvis': 'King'}.toString(), '{Elvis: King}');
Expect.equals(const{'I': 1, 'II': 2}.toString(), '{I: 1, II: 2}');
Expect.equals(const{'X': const{'I': 1, 'II': 2}, 'Y': const{'III': 3, 'IV': 4},
'Z': const{'V': 5, 'VI': 6}}.toString(),
'{X: {I: 1, II: 2}, Y: {III: 3, IV: 4}, Z: {V: 5, VI: 6}}');
}
// SERIOUS "BASHER" TESTS
/**
* Generate a bunch of random collections (including Maps), and test that
* there string form is as expected. The collections include collections
* as elements, keys, and values, and include recursive references.
*
* This test restricts itself to collections with well-defined iteration
* orders (i.e., no HashSet, HashMap).
*/
void exactTest() {
for (int i = 0; i < NUM_TESTS; i++) {
// Choose a size from 0 to MAX_COLLECTION_SIZE, favoring larger sizes
int size = Math.sqrt(random(MAX_COLLECTION_SIZE * MAX_COLLECTION_SIZE)).toInt();
StringBuffer stringRep = new StringBuffer();
Object o = randomCollection(size, stringRep, exact:true);
Expect.equals(o.toString(), stringRep.toString());
}
}
/**
* Generate a bunch of random collections (including Maps), and test that
* there string form is as expected. The collections include collections
* as elements, keys, and values, and include recursive references.
*
* This test includes collections with ill-defined iteration orders (i.e.,
* HashSet, HashMap). As a consequence, it can't use equality tests on the
* string form. Instead, it performs equality tests on their "alphagrams."
* This might allow false positives, but it does give a fair amount of
* confidence.
*/
void inexactTest() {
for (int i = 0; i < NUM_TESTS; i++) {
// Choose a size from 0 to MAX_COLLECTION_SIZE, favoring larger sizes
int size = Math.sqrt(random(MAX_COLLECTION_SIZE * MAX_COLLECTION_SIZE)).toInt();
StringBuffer stringRep = new StringBuffer();
Object o = randomCollection(size, stringRep, exact:false);
Expect.equals(alphagram(o.toString()), alphagram(stringRep.toString()));
}
}
/**
* Return a random collection (or Map) of the specified size, placing its
* string representation into the given string buffer.
*
* If exact is true, the returned collections will not be, and will not contain
* a collection with ill-defined iteration order (i.e., a HashSet or HashMap).
*/
Object randomCollection(int size, StringBuffer stringRep, [bool exact]) {
return randomCollectionHelper(size, exact, stringRep, []);
}
/**
* Return a random collection (or map) of the specified size, placing its
* string representation into the given string buffer. The beingMade
* parameter is a list of collections currently under construction, i.e.,
* candidates for recursive references.
*
* If exact is true, the returned collections will not be, and will not contain
* a collection with ill-defined iteration order (i.e., a HashSet or HashMap).
*/
Object randomCollectionHelper(int size, bool exact, StringBuffer stringRep,
List beingMade) {
double interfaceFrac = Math.random();
if (exact) {
if (interfaceFrac < 1/3) {
return randomList(size, exact, stringRep, beingMade);
} else if (interfaceFrac < 2/3) {
return randomQueue(size, exact, stringRep, beingMade);
} else {
return randomMap(size, exact, stringRep, beingMade);
}
} else {
if (interfaceFrac < 1/4) {
return randomList(size, exact, stringRep, beingMade);
} else if (interfaceFrac < 2/4) {
return randomQueue(size, exact, stringRep, beingMade);
} else if (interfaceFrac < 3/4) {
return randomSet(size, exact, stringRep, beingMade);
} else {
return randomMap(size, exact, stringRep, beingMade);
}
}
}
/**
* Return a random List of the specified size, placing its string
* representation into the given string buffer. The beingMade
* parameter is a list of collections currently under construction, i.e.,
* candidates for recursive references.
*
* If exact is true, the returned collections will not be, and will not contain
* a collection with ill-defined iteration order (i.e., a HashSet or HashMap).
*/
List randomList(int size, bool exact, StringBuffer stringRep, List beingMade) {
return populateRandomCollection(size, exact, stringRep, beingMade, []);
}
/**
* Like randomList, but returns a queue.
*/
Queue randomQueue(int size, bool exact, StringBuffer stringRep, List beingMade){
return populateRandomCollection(size, exact, stringRep, beingMade, new Queue());
}
/**
* Like randomList, but returns a Set.
*/
Set randomSet(int size, bool exact, StringBuffer stringRep, List beingMade) {
// Until we have LinkedHashSet, method will only be called with exact==true
return populateRandomSet(size, exact, stringRep, beingMade, new Set());
}
/**
* Like randomList, but returns a map.
*/
Map randomMap(int size, bool exact, StringBuffer stringRep, List beingMade) {
if (exact) {
return populateRandomMap(size, exact, stringRep, beingMade,
new LinkedHashMap());
} else {
return populateRandomMap(size, exact, stringRep, beingMade,
randomBool() ? new Map() : new LinkedHashMap());
}
}
/**
* Populates the given empty collection with elements, emitting the string
* representation of the collection to stringRep. The beingMade parameter is
* a list of collections currently under construction, i.e., candidates for
* recursive references.
*
* If exact is true, the elements of the returned collections will not be,
* and will not contain a collection with ill-defined iteration order
* (i.e., a HashSet or HashMap).
*/
Collection populateRandomCollection(int size, bool exact,
StringBuffer stringRep, List beingMade, Collection coll) {
beingMade.add(coll);
stringRep.add(coll is List ? '[' : '{');
for (int i = 0; i < size; i++) {
if (i != 0) stringRep.add(', ');
coll.add(randomElement(random(size), exact, stringRep, beingMade));
}
stringRep.add(coll is List ? ']' : '}');
beingMade.removeLast();
return coll;
}
/** Like populateRandomCollection, but for sets (elements must be hashable) */
Set populateRandomSet(int size, bool exact, StringBuffer stringRep,
List beingMade, Set set) {
stringRep.add('{');
for (int i = 0; i < size; i++) {
if (i != 0) stringRep.add(', ');
set.add(i);
stringRep.add(i);
}
stringRep.add('}');
return set;
}
/** Like populateRandomCollection, but for maps. */
Map populateRandomMap(int size, bool exact, StringBuffer stringRep,
List beingMade, Map map) {
beingMade.add(map);
stringRep.add('{');
for (int i = 0; i < size; i++) {
if (i != 0) stringRep.add(', ');
int key = i; // Ensures no duplicates
stringRep.add(key);
stringRep.add(': ');
Object val = randomElement(random(size), exact, stringRep, beingMade);
map[key] = val;
}
stringRep.add('}');
beingMade.removeLast();
return map;
}
/**
* Generates a random element which can be an int, a collection, or a map,
* and emits it to StringRep. The beingMade parameter is a list of collections
* currently under construction, i.e., candidates for recursive references.
*
* If exact is true, the returned element will not be, and will not contain
* a collection with ill-defined iteration order (i.e., a HashSet or HashMap).
*/
void randomElement(int size, bool exact, StringBuffer stringRep,
List beingMade) {
Object result;
double elementTypeFrac = Math.random();
if (elementTypeFrac < 1/3) {
result = random(1000);
stringRep.add(result);
} else if (elementTypeFrac < 2/3) {
// Element Is a random (new) collection
result = randomCollectionHelper(size, exact, stringRep, beingMade);
} else {
// Element Is a random recursive ref
result = beingMade[random(beingMade.length)];
if (result is List)
stringRep.add('[...]');
else
stringRep.add('{...}');
}
return result;
}
/** Returns a random int on [0, max) */
int random(int max) {
return (Math.random() * max).toInt();
}
/** Returns a random boolean value. */
bool randomBool() {
return Math.random() < .5;
}
/** Returns the alphabetized characters in a string. */
String alphagram(String s) {
List<int> chars = s.charCodes();
chars.sort((int a, int b) => a - b);
return new String.fromCharCodes(chars);
}