diff --git a/algorithms/sorting/quicksort/quicksort.js b/algorithms/sorting/quicksort/quicksort.js index ba876dc..0a0e4bb 100644 --- a/algorithms/sorting/quicksort/quicksort.js +++ b/algorithms/sorting/quicksort/quicksort.js @@ -1,5 +1,5 @@ /* - * Insertion sort implementation in JavaScript + * Quick sort implementation in JavaScript * Copyright (c) 2012 Nicholas C. Zakas * * Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/src/data-structures/binary-heap/package.json b/src/data-structures/binary-heap/package.json index 4b5146f..73016e3 100644 --- a/src/data-structures/binary-heap/package.json +++ b/src/data-structures/binary-heap/package.json @@ -1,7 +1,7 @@ { "name": "@humanwhocodes/binary-heap", - "version": "2.0.0", - "description": "A doubly linked list implementation in JavaScript", + "version": "2.0.1", + "description": "A binary heap implementation in JavaScript", "main": "binary-heap.js", "scripts": { "test": "npx mocha ../../../tests/data-structures/binary-heap/binary-heap.js" diff --git a/src/data-structures/doubly-linked-list/README.md b/src/data-structures/doubly-linked-list/README.md index 415c4d2..da6a042 100644 --- a/src/data-structures/doubly-linked-list/README.md +++ b/src/data-structures/doubly-linked-list/README.md @@ -11,6 +11,8 @@ A JavaScript implementation of a doubly linked list. This class uses the convent 1. There is a `[Symbol.iterator]` method so each instance is iterable. 1. The `size` getter property instead of a `length` data property to indicate that the size of the list is dynamically counted rather than stored. 1. Defining a `values()` generator method. +1. Defining a `find()` method for searching the list to return data. +1. Defining a `findIndex()` method for searching the list to return an index. 1. Returning `undefined` from `get()` when no such index exists. Read the [blog post](https://humanwhocodes.com/blog/2019/02/computer-science-in-javascript-doubly-linked-lists/) about the design of this class. @@ -44,6 +46,10 @@ let count = list.size; // get the index of a value let index = list.indexOf("foo"); +// search for a value +let result = list.find(value => value.length > 3); +let foundIndex = list.findIndex(value => value.length > 3); + // convert to an array using iterators let array1 = [...list.values()]; let array2 = [...list]; diff --git a/src/data-structures/doubly-linked-list/doubly-linked-list.js b/src/data-structures/doubly-linked-list/doubly-linked-list.js index a5983e1..cad7fca 100644 --- a/src/data-structures/doubly-linked-list/doubly-linked-list.js +++ b/src/data-structures/doubly-linked-list/doubly-linked-list.js @@ -392,6 +392,94 @@ class DoublyLinkedList { */ return -1; } + + /** + * Returns the first item that matches a given function. + * @param {Function} matcher A function returning true when an item matches + * and false when an item doesn't match. + * @returns {*} The first item that returns true from the matcher, undefined + * if no items match. + */ + find(matcher) { + + /* + * The `current` variable is used to iterate over the list nodes. + * It starts out pointing to the head and is overwritten inside + * of the loop below. + */ + let current = this[head]; + + /* + * This loop checks each node in the list to see if it matches. + * If a match is found, it returns the data immediately, exiting the + * loop because there's no reason to keep searching. The search + * continues until there are no more nodes to search (when `current` is `null`). + */ + while (current !== null) { + if (matcher(current.data)) { + return current.data; + } + + // traverse to the next node in the list + current = current.next; + } + + /* + * If execution gets to this point, it means we reached the end of the + * list and didn't find `data`. Just return `undefined` as the + * "not found" value. + */ + return undefined; + } + + /** + * Returns the index of the first item that matches a given function. + * @param {Function} matcher A function returning true when an item matches + * and false when an item doesn't match. + * @returns {int} The index of the first item that matches a given function + * or -1 if there are no matching items. + */ + findIndex(matcher) { + + /* + * The `current` variable is used to iterate over the list nodes. + * It starts out pointing to the head and is overwritten inside + * of the loop below. + */ + let current = this[head]; + + /* + * The `index` variable is used to track how deep into the list we've + * gone. This is important because this is the value that is returned + * from this method. + */ + let index = 0; + + /* + * This loop checks each node in the list to see if it matches. + * If a match is found, it returns the index immediately, exiting the + * loop because there's no reason to keep searching. The search + * continues until there are no more nodes to search (when `current` is `null`). + */ + while (current !== null) { + if (matcher(current.data)) { + return index; + } + + // traverse to the next node in the list + current = current.next; + + // keep track of where we are + index++; + } + + /* + * If execution gets to this point, it means we reached the end of the + * list and didn't find `data`. Just return -1 as the + * "not found" value. + */ + return -1; + } /** * Removes the node from the given location in the list. diff --git a/src/data-structures/doubly-linked-list/package.json b/src/data-structures/doubly-linked-list/package.json index 866a9c3..ab7ddb9 100644 --- a/src/data-structures/doubly-linked-list/package.json +++ b/src/data-structures/doubly-linked-list/package.json @@ -1,6 +1,6 @@ { "name": "@humanwhocodes/doubly-linked-list", - "version": "2.0.1", + "version": "2.2.0", "description": "A doubly linked list implementation in JavaScript", "main": "doubly-linked-list.js", "scripts": { diff --git a/src/data-structures/hash-map/README.md b/src/data-structures/hash-map/README.md new file mode 100644 index 0000000..e250514 --- /dev/null +++ b/src/data-structures/hash-map/README.md @@ -0,0 +1,71 @@ +# JavaScript Hash Map Class + +by [Nicholas C. Zakas](https://humanwhocodes.com) + +If you find this useful, please consider supporting my work with a [donation](https://humanwhocodes.com/donate). + +## Overview + +A JavaScript implementation of a hash map where all keys must be strings. This class uses the conventions of built-in JavaScript collection objects, such as: + +1. There is a `[Symbol.iterator]` method so each instance is iterable. +1. The `size` getter property instead of a `length` data property to indicate that the size of the list is dynamically counted rather than stored. +1. Defining `entries()`, `keys()`, and `values()` generator methods. + +## Usage + +Use CommonJS to get access to the `HashMap` constructor: + +```js +const { HashMap } = require("@humanwhocodes/hash-map"); +``` + +Each instance of `HashMap` has the following properties and methods: + +```js +const map = new HashMap(); + +// add an item +map.set("foo", 1); + +// get the value of an item +let value = map.get("foo"); + +// get the number of items +let count = map.size; + +// does the key exist in the map? +let found = map.has("foo"); + +// remove a key +map.delete("foo"); + +// get all key-value pairs +let entries1 = [...map.entries()]; +let entries2 = [...map]; + +// get all keys +let keys = [...map.keys()]; + +// get all values +let values = [...map.values()]; + +// remove all items +map.clear(); +``` + +## Note on Code Style + +You may find the code style of this module to be overly verbose with a lot of comments. That is intentional, as the primary use of this module is intended to be for educational purposes. There are frequently more concise ways of implementing the details of this class, but the more concise ways are difficult for newcomers who are unfamiliar with linked lists as a concept or JavaScript as a whole. + +## Note on Usage + +This module is intended for educational purposes. For production purposes, you should use the native JavaScript `Map` class. + +## Issues and Pull Requests + +As this is part of series of tutorials I'm writing, only bug fixes will be accepted. No new functionality will be added to this module. + +## License + +MIT \ No newline at end of file diff --git a/src/data-structures/hash-map/hash-map.js b/src/data-structures/hash-map/hash-map.js new file mode 100644 index 0000000..dbdd2f4 --- /dev/null +++ b/src/data-structures/hash-map/hash-map.js @@ -0,0 +1,473 @@ + +/** + * @fileoverview Hash Map implementation in JavaScript + */ + +"use strict"; + +//----------------------------------------------------------------------------- +// Requirements +//----------------------------------------------------------------------------- + +const { DoublyLinkedList } = require("@humanwhocodes/doubly-linked-list"); + +//----------------------------------------------------------------------------- +// Private +//----------------------------------------------------------------------------- + +const ARRAY_SIZE = 16; + +/** + * Adds up all of the code points in a string to create a numeric hash. + * @param {string} key The text key to hash. + * @returns {int} A numeric hash of text. + * @private + */ +function hashCodePoints(key) { + + let result = 0; + + // Iterate over each character (not each byte) in the string + for (const character of key) { + + /* + * The `codePointAt()` method has support for multi-byte characters, + * so that's better than `charCodeAt()` for this purpose. + * + * `character` is a single-character string, so we only want the code + * point at position 0. + */ + result += character.codePointAt(0); + } + + return result; +} + +/** + * Determines the correct array index for the given hash code. + * @param {int} hashCode The hash code to compute an index for. + * @returns {int} An index between 0 and `ARRAY_SIZE - 1`. + */ +function getArrayIndexFromHashCode(hashCode) { + return hashCode % ARRAY_SIZE; +} + +/** + * Creates an array to use as the basis for a hash map. + * @returns {void} + * @private + */ +function createArray() { + + /* + * FYI: It's not necessary to use an instance of `Array` for the + * purpose of creating a hash map. You could just as easily use a + * regular object. This implementation uses an array strictly because + * it's the most direct equivalent to how other languages implement + * hash maps. + */ + + // Object.seal() ensures we don't accidentally add more items + return Object.seal( + + /* + * Creates a new array with a predefined length of 16. This doesn't + * actually create each of the properties in the array, so the + * `fill()` method is used to do so. This is necessary because a + * sealed array cannot have new properties defined, and since the + * array starts out without any numeric properties defined, it + * would prevent us from even assigning a value to array[0]. + */ + new Array(ARRAY_SIZE).fill(undefined) + ); +} + +/** + * Checks that a key is a non-empty string. + * @param {string} key The key to validate. + * @returns {void} + * @throws {TypeError} When the key is either not a string or is an empty + * string. + */ +function assertNonEmptyString(key) { + if (typeof key !== "string" || key.length === 0) { + throw new TypeError("Key must be a non-empty string."); + } +} + +//----------------------------------------------------------------------------- +// HashMap Class +//----------------------------------------------------------------------------- + +/* + * These symbols are used to represent properties that should not be part of + * the public interface. You could also use ES2019 private fields, but those + * are not yet widely available as of the time of my writing. + */ +const array = Symbol("array"); + +/** + * A binary heap implementation in JavaScript. + * @class HashMap + */ +class HashMap { + + /** + * Creates a new instance of HashMap + */ + constructor() { + + /** + * Array used to manage the hash map. The array is sealed to ensure + * no new items accidentally get added. + * @property array + * @type Array + * @private + */ + this[array] = createArray(); + } + + /** + * Adds a key-value pair into the hash map. + * @param {string} key The key to add. + * @param {*} value The value to add. + * @returns {void} + * @throws {TypeError} If the key isn't a non-empty string. + */ + set(key, value) { + + // first, ensure the key is valid + assertNonEmptyString(key); + + /* + * Next, calculate the hash code from the string key. The hash code + * is then used to determine the index into the array where the data + * should be stored. + */ + const hashCode = hashCodePoints(key); + const index = getArrayIndexFromHashCode(hashCode); + + /* + * Special case: If the calculated index in the array hasn't yet + * been initialized, then create a new linked list in that + * location before continuing. + * + * Note: It's not necessary to use a linked list. Because JavaScript + * has an excellent `Array` class, you could also just use an array + * here. This implementation sticks with a linked list because that's + * the most common implementation in other languages. + */ + if (this[array][index] === undefined) { + this[array][index] = new DoublyLinkedList(); + } + + /* + * Check to see if the exact key is already present + * in the hash map. If so, then just update the value. + */ + const result = this[array][index].find((value) => { + return value.key === key; + }); + + // If the key doesn't already exist, then add it + if (result === undefined) { + + /* + * Add everything we know about this key-value pair, including the key, + * the value, and the hash code. We will need all of these later. + */ + this[array][index].add({ + key, + value + }); + } else { + + // The key already exists in the hash map, so just update the value. + result.value = value; + } + + } + + /** + * Retrieves a key-value pair from the hash map. + * @param {string} key The key whose value should be retrieved. + * @returns {*} The value associated with the key or `undefined` if the + * key doesn't exist in the hash map. + * @throws {TypeError} If the key isn't a non-empty string. + */ + get(key) { + + // first, ensure the key is valid + assertNonEmptyString(key); + + /* + * Next, calculate the hash code from the string key. The hash code + * is then used to determine the index into the array where the data + * should be stored. + */ + const hashCode = hashCodePoints(key); + const index = getArrayIndexFromHashCode(hashCode); + + /* + * Special case: If the calculated index in the array hasn't yet + * been initialized, then the key doesn't exist. Return + * `undefined` to indicate the key doesn't exist. + */ + if (this[array][index] === undefined) { + return undefined; + } + + /* + * If we've made it to here, then there is a linked list in the + * array at this location, so try to find the key that matches. + */ + const result = this[array][index].find((value) => { + return value.key === key; + }); + + /* + * If an item with the given hash code and key was not found, then + * there is no value to return. Just return undefined. + */ + if (result === undefined) { + return undefined; + } + + /* + * If we've made it to here, it means that the hash code and key were + * found, so return the value. + */ + return result.value; + } + + /** + * Determines if a given key is present in the hash map. + * @param {string} key The key to check. + * @returns {boolean} True if the key exists in the hash map, false if not. + * @throws {TypeError} If the key isn't a non-empty string. + */ + has(key) { + + // first, ensure the key is valid + assertNonEmptyString(key); + + /* + * Next, calculate the hash code from the string key. The hash code + * is then used to determine the index into the array where the data + * should be stored. + */ + const hashCode = hashCodePoints(key); + const index = getArrayIndexFromHashCode(hashCode); + + /* + * Special case: If the calculated index in the array hasn't yet + * been initialized, then the key doesn't exist. Return + * `false` to indicate the key doesn't exist. + */ + if (this[array][index] === undefined) { + return false; + } + + /* + * If we've made it to here, then there is a linked list in the + * array at this location, so try to find the key that matches. + */ + const resultIndex = this[array][index].findIndex((value) => { + return value.key === key; + }); + + /* + * Any value greater than -1 indicates that the key was found in the + * hash map and therefore this method should return `true`. + */ + return resultIndex > -1; + } + + /** + * Deletes the given key from the hash map. + * @param {string} key The key to delete. + * @returns {boolean} True if the key exists and was deleted, false if the + * key didn't exist. + * @throws {TypeError} If the key isn't a non-empty string. + */ + delete(key) { + + // first, ensure the key is valid + assertNonEmptyString(key); + + /* + * Next, calculate the hash code from the string key. The hash code + * is then used to determine the index into the array where the data + * should be stored. + */ + const hashCode = hashCodePoints(key); + const index = getArrayIndexFromHashCode(hashCode); + + /* + * Special case: If the calculated index in the array hasn't yet + * been initialized, then the key doesn't exist. Return + * `false` to indicate the key doesn't exist. + */ + if (this[array][index] === undefined) { + return false; + } + + /* + * If we've made it to here, then there is a linked list in the + * array at this location, so try to find the key that matches. + */ + const resultIndex = this[array][index].findIndex((value) => { + return value.key === key; + }); + + /* + * Special case: If `resultIndex` is -1, meaning the value wasn't + * found, just return -1. + */ + if (resultIndex === -1) { + return -1; + } + + /* + * If we've made it to here, then `resultIndex` is greater than -1 + * and we need to remove the given key from the hash map. + */ + this[array][index].remove(resultIndex); + + /* + * Because we actually performed the removal, we need to return `true` + * to give feedback that this happened. + */ + return true; + + } + + /** + * Returns the number of key-value pairs in the hash map. + * @returns {int} The number of key-value pairs in the hash map. + */ + get size() { + + /* + * Because the `entries()` generator already implements the correct + * traversal algorithm, use that here instead of duplicating the + * algorithm. + * + * The first step is to create a new iterator by calling `entries()`. + */ + const iterator = this.entries(); + + /* + * The `count` variable stores the number of entries we see and is + * incremented inside of a loop later on. + */ + let count = 0; + + /* + * Get the first entry from the iterator. Each entry has two properties: + * `value`, containing the value from the hash map, and `done`, a + * a Boolean value that is `true` when there are no more entries. + */ + let entry = iterator.next(); + + // Continue the loop as long as there are more entries + while (!entry.done) { + + // Increment the count to reflect the last entry + count++; + + // Get the entry from the iterator and repeat the loop + entry = iterator.next(); + } + + /* + * Once the loop exits, the `count` variable contains the number of + * entries in the iterator so return that value. + */ + return count; + } + + /** + * Removes all values from the heap. + * @returns {void} + */ + clear() { + + /* + * The simplest way to clear all values is just to overwrite the array + * we started with. + */ + this[array] = createArray(); + } + + /** + * The default iterator for the class. + * @returns {Iterator} An iterator for the class. + */ + [Symbol.iterator]() { + return this.entries(); + } + + /** + * Create an iterator that returns each entry in the hash map. + * @returns {Iterator} An iterator on the hash map. + */ + *entries() { + + // For each item in the array + for (const list of this[array]) { + + // If there is no linked list then no need to go any further + if (list !== undefined) { + + // If there is a linked list then yield each key-value pair + for (const item of list) { + yield [item.key, item.value]; + } + } + } + } + + /** + * Create an iterator that returns each key in the hash map. + * @returns {Iterator} An iterator on the hash map keys. + */ + *keys() { + + /* + * Because this iterator just returns a subset of the information + * returned by the `entries()` iterator, we just use `entries()` + * and take the information we care about. + */ + for (const [key] of this.entries()) { + yield key; + } + } + + /** + * Create an iterator that returns each value in the hash map. + * @returns {Iterator} An iterator on the hash map value. + */ + *values() { + + /* + * Because this iterator just returns a subset of the information + * returned by the `entries()` iterator, we just use `entries()` + * and take the information we care about. + */ + for (const [,value] of this.entries()) { + yield value; + } + } + + /** + * Converts the heap into a string representation. + * @returns {String} A string representation of the heap. + */ + toString(){ + // TODO + return [...this[array]].toString(); + } +} + +exports.HashMap = HashMap; \ No newline at end of file diff --git a/src/data-structures/hash-map/package.json b/src/data-structures/hash-map/package.json new file mode 100644 index 0000000..557a5bd --- /dev/null +++ b/src/data-structures/hash-map/package.json @@ -0,0 +1,32 @@ +{ + "name": "@humanwhocodes/hash-map", + "version": "2.0.0", + "description": "A hash map implementation in JavaScript", + "main": "hash-map.js", + "scripts": { + "test": "npx mocha ../../../tests/data-structures/hash-map/hash-map.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/humanwhocodes/computer-science-in-javascript.git" + }, + "keywords": [ + "hash", + "hash table", + "hash map", + "data structure", + "iterable" + ], + "author": "Nicholas C. Zakas", + "license": "MIT", + "bugs": { + "url": "https://github.com/humanwhocodes/computer-science-in-javascript/issues" + }, + "homepage": "https://github.com/humanwhocodes/computer-science-in-javascript#readme", + "engines": { + "node": ">=8.0.0" + }, + "dependencies": { + "@humanwhocodes/doubly-linked-list": "^2.2.0" + } +} diff --git a/tests/data-structures/doubly-linked-list/doubly-linked-list.js b/tests/data-structures/doubly-linked-list/doubly-linked-list.js index 4367a3a..41ff746 100644 --- a/tests/data-structures/doubly-linked-list/doubly-linked-list.js +++ b/tests/data-structures/doubly-linked-list/doubly-linked-list.js @@ -336,6 +336,56 @@ describe("DoublyLinkedList", () => { }); + describe("find()", () => { + + it("should return undefined when the list is empty", () => { + assert.isUndefined(list.find(() => true)); + }); + + it("should return 1 when the matching value is found", () => { + list.add(1); + assert.strictEqual(list.find(value => (value > 0)), 1); + }); + + it("should return 2 when the matching value is second", () => { + list.add(1); + list.add(2); + assert.strictEqual(list.find(value => (value > 1)), 2); + }); + + it("should return undefined when the list doesn't contain a match", () => { + list.add(1); + list.add(2); + assert.isUndefined(list.find((value) => value > 2)); + }); + + }); + + describe("findIndex()", () => { + + it("should return -1 when the list is empty", () => { + assert.strictEqual(list.findIndex(() => true), -1); + }); + + it("should return 0 when the matching value is found in the first item", () => { + list.add(1); + assert.strictEqual(list.findIndex(value => (value > 0)), 0); + }); + + it("should return 1 when the matching value is second", () => { + list.add(1); + list.add(2); + assert.strictEqual(list.findIndex(value => (value > 1)), 1); + }); + + it("should return -1 when the list doesn't contain a match", () => { + list.add(1); + list.add(2); + assert.strictEqual(list.findIndex((value) => value > 2), -1); + }); + + }); + ["values", Symbol.iterator].forEach(method => { describe(String(method) + "()", () => { diff --git a/tests/data-structures/hash-map/hash-map.js b/tests/data-structures/hash-map/hash-map.js new file mode 100644 index 0000000..8645295 --- /dev/null +++ b/tests/data-structures/hash-map/hash-map.js @@ -0,0 +1,261 @@ +/** + * @fileoverview Hash Map tests + */ +/* global it, describe, beforeEach */ +"use strict"; + +//----------------------------------------------------------------------------- +// Requirements +//----------------------------------------------------------------------------- + +const assert = require("chai").assert; +const { HashMap } = require("../../../src/data-structures/hash-map/hash-map"); + +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +/** + * Check that the contents of the hash map match the values of the array. + * @param {HashMap} map The list to check + * @param {Array} values An array of values that should match. + * @throws {AssertionError} If the values in the list don't match the + * values in the array. + */ +function assertHashMapValues(map, values) { + const hashMapValues = [...map]; + assert.deepStrictEqual(hashMapValues, values); + +} + +//----------------------------------------------------------------------------- +// Tests +//----------------------------------------------------------------------------- + +describe("HashMap", () => { + + let map; + + beforeEach(() => { + map = new HashMap(); + }); + + describe("set()", () => { + + it("should store a key in the hash map", () => { + map.set("foo", 1); + assertHashMapValues(map, [["foo", 1]]); + }); + + it("should store two keys in the hash map", () => { + map.set("foo", 1); + map.set("bar", 2); + assertHashMapValues(map, [["foo", 1], ["bar", 2]]); + }); + + it("should store two keys with the same hash codes in the hash map", () => { + map.set("foo", 1); + map.set("oof", 2); + assertHashMapValues(map, [["foo", 1], ["oof", 2]]); + }); + + it("should overwrite a key in the hash map when called twice with the same key", () => { + map.set("foo", 1); + map.set("bar", 2); + map.set("foo", 3); + assertHashMapValues(map, [["foo", 3], ["bar", 2]]); + }); + + }); + + describe("get()", () => { + + it("should retrieve a key from the hash map", () => { + map.set("foo", 1); + assert.strictEqual(map.get("foo"), 1); + }); + + it("should retrieve two keys from the hash map", () => { + map.set("foo", 1); + map.set("bar", 2); + assert.strictEqual(map.get("foo"), 1); + assert.strictEqual(map.get("bar"), 2); + }); + + it("should retrieve two keys from the hash map when one key is overwritten", () => { + map.set("foo", 1); + map.set("bar", 2); + map.set("foo", 3); + assert.strictEqual(map.get("foo"), 3); + assert.strictEqual(map.get("bar"), 2); + }); + + }); + + describe("has()", () => { + + it("should return true when a key exists in the hash map", () => { + map.set("foo", 1); + assert.isTrue(map.has("foo")); + }); + + it("should false when a key doesn't exist in the hash map", () => { + map.set("foo", 1); + assert.isFalse(map.has("foox")); + }); + + it("should return true when multiple keys exist in the hash map", () => { + map.set("foo", 1); + map.set("bar", 2); + assert.isTrue(map.has("foo")); + assert.isTrue(map.has("bar")); + }); + + it("should return true when one key is overwritten", () => { + map.set("foo", 1); + map.set("bar", 2); + map.set("foo", 3); + assert.isTrue(map.has("foo")); + assert.isTrue(map.has("bar")); + }); + + }); + + describe("delete()", () => { + + it("should delete a key in the hash map", () => { + map.set("foo", 1); + map.delete("foo"); + assertHashMapValues(map, []); + }); + + it("should return true when a key is deleted from the hash map", () => { + map.set("foo", 1); + assert.isTrue(map.delete("foo")); + }); + + it("should return false when a key is not deleted from the hash map", () => { + map.set("foo", 1); + assert.isFalse(map.delete("f")); + }); + + it("should return false when the hash map is empty", () => { + assert.isFalse(map.delete("f")); + }); + + it("should delete two keys in the hash map", () => { + map.set("foo", 1); + map.set("bar", 2); + map.delete("foo"); + map.delete("bar"); + assertHashMapValues(map, []); + }); + + it("should delete one key when the hash map has two keys", () => { + map.set("foo", 1); + map.set("oof", 2); + map.delete("foo"); + assertHashMapValues(map, [["oof", 2]]); + }); + + }); + + describe("size", () => { + + it("should return the correct size when the hash map has no items", () => { + assert.strictEqual(map.size, 0); + }); + + it("should return the correct size when the hash map has one item", () => { + map.set("foo", 1); + assert.strictEqual(map.size, 1); + }); + + it("should return the correct size when the hash map has two items", () => { + map.set("bar", 2); + map.set("foo", 1); + assert.strictEqual(map.size, 2); + }); + + it("should return the correct size when the hash map has three items", () => { + map.set("bar", 2); + map.set("baz", 3); + map.set("foo", 1); + assert.strictEqual(map.size, 3); + }); + + it("should return the correct size when the hash map has four items", () => { + map.set("bar", 2); + map.set("baz", 3); + map.set("foo", 1); + map.set("bazoo", 0); + assert.strictEqual(map.size, 4); + }); + }); + + ["entries", Symbol.iterator].forEach(method => { + + describe(String(method) + "()", () => { + + it("should create empty array when there are no items", () => { + assert.deepStrictEqual([...map[method]()], []); + }); + + it("should iterate over list when there is one item", () => { + map.set("foo", 1); + + assert.deepStrictEqual([...map[method]()], [["foo", 1]]); + }); + + it("should iterate over list when there are multiple items", () => { + map.set("foo", 1); + map.set("bar", 2); + assert.deepStrictEqual([...map[method]()], [["foo", 1], ["bar", 2]]); + }); + + }); + + }); + + describe("keys()", () => { + + it("should create empty array when there are no items", () => { + assert.deepStrictEqual([...map.keys()], []); + }); + + it("should iterate over list when there is one item", () => { + map.set("foo", 1); + + assert.deepStrictEqual([...map.keys()], ["foo"]); + }); + + it("should iterate over list when there are multiple items", () => { + map.set("foo", 1); + map.set("bar", 2); + assert.deepStrictEqual([...map.keys()], ["foo", "bar"]); + }); + + }); + + describe("values()", () => { + + it("should create empty array when there are no items", () => { + assert.deepStrictEqual([...map.values()], []); + }); + + it("should iterate over list when there is one item", () => { + map.set("foo", 1); + + assert.deepStrictEqual([...map.values()], [1]); + }); + + it("should iterate over list when there are multiple items", () => { + map.set("foo", 1); + map.set("bar", 2); + assert.deepStrictEqual([...map.values()], [1, 2]); + }); + + }); + + +});