Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 30 additions & 12 deletions src/lualib/Map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,46 +113,64 @@ export class Map<K extends AnyNotNil, V> {
}

public entries(): IterableIterator<[K, V]> {
const getFirstKey = () => this.firstKey;
const { items, nextKey } = this;
let key = this.firstKey;
let key: K | undefined;
let started = false;
return {
[Symbol.iterator](): IterableIterator<[K, V]> {
return this;
},
next(): IteratorResult<[K, V]> {
const result = { done: !key, value: [key, items.get(key!)] as [K, V] };
key = nextKey.get(key!);
return result;
if (!started) {
started = true;
key = getFirstKey();
} else {
key = nextKey.get(key!);
}
return { done: !key, value: [key!, items.get(key!)] as [K, V] };
},
};
}

public keys(): IterableIterator<K> {
const getFirstKey = () => this.firstKey;
const nextKey = this.nextKey;
let key = this.firstKey;
let key: K | undefined;
let started = false;
return {
[Symbol.iterator](): IterableIterator<K> {
return this;
},
next(): IteratorResult<K> {
const result = { done: !key, value: key };
key = nextKey.get(key!);
return result as IteratorResult<K>;
if (!started) {
started = true;
key = getFirstKey();
} else {
key = nextKey.get(key!);
}
return { done: !key, value: key! };
},
};
}

public values(): IterableIterator<V> {
const getFirstKey = () => this.firstKey;
const { items, nextKey } = this;
let key = this.firstKey;
let key: K | undefined;
let started = false;
return {
[Symbol.iterator](): IterableIterator<V> {
return this;
},
next(): IteratorResult<V> {
const result = { done: !key, value: items.get(key!) };
key = nextKey.get(key!);
return result;
if (!started) {
started = true;
key = getFirstKey();
} else {
key = nextKey.get(key!);
}
return { done: !key, value: items.get(key!) };
},
};
}
Expand Down
42 changes: 30 additions & 12 deletions src/lualib/Set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,46 +102,64 @@ export class Set<T extends AnyNotNil> {
}

public entries(): IterableIterator<[T, T]> {
const getFirstKey = () => this.firstKey;
const nextKey = this.nextKey;
let key: T = this.firstKey!;
let key: T | undefined;
let started = false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why add another bool instead of simply checking if key is undefined?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

key can also become undefined if the iterator is exhausted. if the iterator is then next'ed again, it will reset and get the first key

return {
[Symbol.iterator](): IterableIterator<[T, T]> {
return this;
},
next(): IteratorResult<[T, T]> {
const result = { done: !key, value: [key, key] as [T, T] };
key = nextKey.get(key);
return result;
if (!started) {
started = true;
key = getFirstKey();
} else {
key = nextKey.get(key!);
}
return { done: !key, value: [key!, key!] as [T, T] };
},
};
}

public keys(): IterableIterator<T> {
const getFirstKey = () => this.firstKey;
const nextKey = this.nextKey;
let key: T = this.firstKey!;
let key: T | undefined;
let started = false;
return {
[Symbol.iterator](): IterableIterator<T> {
return this;
},
next(): IteratorResult<T> {
const result = { done: !key, value: key };
key = nextKey.get(key);
return result;
if (!started) {
started = true;
key = getFirstKey();
} else {
key = nextKey.get(key!);
}
return { done: !key, value: key! };
},
};
}

public values(): IterableIterator<T> {
const getFirstKey = () => this.firstKey;
const nextKey = this.nextKey;
let key: T = this.firstKey!;
let key: T | undefined;
let started = false;
return {
[Symbol.iterator](): IterableIterator<T> {
return this;
},
next(): IteratorResult<T> {
const result = { done: !key, value: key };
key = nextKey.get(key);
return result;
if (!started) {
started = true;
key = getFirstKey();
} else {
key = nextKey.get(key!);
}
return { done: !key, value: key! };
},
};
}
Expand Down
38 changes: 38 additions & 0 deletions test/unit/builtins/map.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,44 @@ describe.each(iterationMethods)("map.%s() preserves insertion order", iterationM
});
});

describe.each(iterationMethods)("map.%s() handles mutation", iterationMethod => {
test("iterator persists after delete", () => {
util.testFunction`
const map = new Map<number, string>([[1, "a"], [2, "b"]]);
const iter = map.${iterationMethod}();
map.delete(1);
return iter.next().value;
`.expectToMatchJsResult();
});

test("iterator with delete and add between iterations", () => {
util.testFunction`
const map = new Map<number, string>([[1, "a"], [2, "b"], [3, "c"]]);
const iter = map.${iterationMethod}();
iter.next(); // 1
map.delete(2);
map.set(4, "d");
const results: IteratorResult<any>[] = [];
let r = iter.next();
while (!r.done) { results.push({ done: r.done, value: r.value }); r = iter.next(); }
return results;
`.expectToMatchJsResult();
});

test("iterator does not restart after exhaustion", () => {
util.testFunction`
const map = new Map<number, string>([[1, "a"], [2, "b"]]);
const iter = map.${iterationMethod}();
const results: boolean[] = [];
results.push(iter.next().done!);
results.push(iter.next().done!);
results.push(iter.next().done!); // should be done
results.push(iter.next().done!); // should still be done, not restart
return results;
`.expectToMatchJsResult();
});
});

describe("Map.groupBy", () => {
test("empty", () => {
util.testFunction`
Expand Down
42 changes: 42 additions & 0 deletions test/unit/builtins/set.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,48 @@ describe.each(iterationMethods)("set.%s() preserves insertion order", iterationM
});
});

describe.each(iterationMethods)("set.%s() handles mutation", iterationMethod => {
test("iterator persists after delete", () => {
util.testFunction`
const set1 = new Set<string | number>();
set1.add(42);
set1.add("forty two");

const iterator1 = set1.${iterationMethod}();
set1.delete(42);

return iterator1.next().value;
`.expectToMatchJsResult();
});

test("iterator with delete and add between iterations", () => {
util.testFunction`
const set = new Set([1, 2, 3]);
const iter = set.${iterationMethod}();
iter.next(); // 1
set.delete(2);
set.add(4);
const results: IteratorResult<any>[] = [];
let r = iter.next();
while (!r.done) { results.push({ done: r.done, value: r.value }); r = iter.next(); }
return results;
`.expectToMatchJsResult();
});

test("iterator does not restart after exhaustion", () => {
util.testFunction`
const set = new Set([1, 2]);
const iter = set.${iterationMethod}();
const results: boolean[] = [];
results.push(iter.next().done!);
results.push(iter.next().done!);
results.push(iter.next().done!); // should be done
results.push(iter.next().done!); // should still be done, not restart
return results;
`.expectToMatchJsResult();
});
});

test("instanceof Set without creating set", () => {
util.testFunction`
const myset = 3 as any;
Expand Down