dataStructures.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. // Derived from
  2. // https://github.com/wooorm/linked-list/blob/d2390fe1cab9f780cfd34fa31c8fa8ede4ad674d/index.js
  3. export const TYPE_TEXT = 'text';
  4. // Creates a new `Iterator` for looping over the `List`.
  5. class Iterator {
  6. constructor(item) {
  7. this.item = item;
  8. }
  9. // Move the `Iterator` to the next item.
  10. next() {
  11. this.value = this.item;
  12. this.done = !this.item;
  13. this.item = this.item ? this.item.next : undefined;
  14. return this;
  15. }
  16. }
  17. // Creates a new `Item`:
  18. // An item is a bit like DOM node: It knows only about its "parent" (`list`),
  19. // the item before it (`prev`), and the item after it (`next`).
  20. export class LLNode {
  21. // Prepends the given item *before* the item operated on.
  22. prepend(item) {
  23. const list = this.list;
  24. if (!item || !item.append || !item.prepend || !item.detach) {
  25. throw new Error(
  26. 'An argument without append, prepend, or detach methods was given to `Item#prepend`.',
  27. );
  28. }
  29. // If self is detached, return false.
  30. if (!list) {
  31. return false;
  32. }
  33. if (this === item) {
  34. return false;
  35. }
  36. // Detach the prependee.
  37. const transient = this.list === item.list;
  38. item.detach(transient);
  39. // If self has a previous item...
  40. if (this.prev) {
  41. item.prev = this.prev;
  42. this.prev.next = item;
  43. }
  44. // Connect the prependee.
  45. item.next = this;
  46. item.list = list;
  47. // Set the previous item of self to the prependee.
  48. this.prev = item;
  49. // If self is the first item in the parent list, link the lists first item to
  50. // the prependee.
  51. if (this === list.head) {
  52. list.head = item;
  53. }
  54. // If the the parent list has no last item, link the lists last item to self.
  55. if (!list.tail) {
  56. list.tail = this;
  57. }
  58. list.length++;
  59. if (!transient) {
  60. item._addToIndex();
  61. }
  62. return item;
  63. }
  64. // Appends the given item *after* the item operated on.
  65. append(item) {
  66. const list = this.list;
  67. if (!item || !item.append || !item.prepend || !item.detach) {
  68. throw new Error(
  69. 'An argument without append, prepend, or detach methods was given to `Item#append`.',
  70. );
  71. }
  72. if (!list) {
  73. return false;
  74. }
  75. if (this === item) {
  76. return false;
  77. }
  78. // Detach the appendee.
  79. const transient = this.list === item.list;
  80. item.detach(transient);
  81. // If self has a next item...
  82. if (this.next) {
  83. item.next = this.next;
  84. this.next.prev = item;
  85. }
  86. // Connect the appendee.
  87. item.prev = this;
  88. item.list = list;
  89. // Set the next item of self to the appendee.
  90. this.next = item;
  91. // If the the parent list has no last item or if self is the parent lists last
  92. // item, link the lists last item to the appendee.
  93. if (this === list.tail || !list.tail) {
  94. list.tail = item;
  95. }
  96. list.length++;
  97. if (!transient) {
  98. item._addToIndex();
  99. }
  100. return item;
  101. }
  102. // Detaches the item operated on from its parent list.
  103. detach(transient) {
  104. const list = this.list;
  105. if (!list) {
  106. return this;
  107. }
  108. if (!transient) {
  109. this._removeFromIndex();
  110. }
  111. // If self is the last item in the parent list, link the lists last item to
  112. // the previous item.
  113. if (list.tail === this) {
  114. list.tail = this.prev;
  115. }
  116. // If self is the first item in the parent list, link the lists first item to
  117. // the next item.
  118. if (list.head === this) {
  119. list.head = this.next;
  120. }
  121. // If both the last and first items in the parent list are the same, remove
  122. // the link to the last item.
  123. if (list.tail === list.head) {
  124. list.tail = null;
  125. }
  126. // If a previous item exists, link its next item to selfs next item.
  127. if (this.prev) {
  128. this.prev.next = this.next;
  129. }
  130. // If a next item exists, link its previous item to selfs previous item.
  131. if (this.next) {
  132. this.next.prev = this.prev;
  133. }
  134. // Remove links from self to both the next and previous items, and to the
  135. // parent list.
  136. this.prev = this.next = this.list = null;
  137. list.length--;
  138. return this;
  139. }
  140. nextCyclic() {
  141. return this.next || this.list.head;
  142. }
  143. prevCyclic() {
  144. return this.prev || this.list.last();
  145. }
  146. _addToIndex() {
  147. const hash = this._hash();
  148. if (hash === undefined || hash === null) {
  149. return;
  150. }
  151. if (this.type === TYPE_TEXT) {
  152. this.list.bytes += this.text.length;
  153. }
  154. let entries = this.list.invertedIndex[hash];
  155. if (!entries) {
  156. entries = [];
  157. this.list.invertedIndex[hash] = entries;
  158. }
  159. entries.push(this.id);
  160. this.list.idsToItems[this.id] = this;
  161. }
  162. _removeFromIndex() {
  163. const hash = this._hash();
  164. if (hash === undefined || hash === null) {
  165. return;
  166. }
  167. if (this.type === TYPE_TEXT) {
  168. this.list.bytes -= this.text.length;
  169. }
  170. const entries = this.list.invertedIndex[hash];
  171. if (entries.length === 1) {
  172. delete this.list.invertedIndex[hash];
  173. } else {
  174. entries.splice(entries.indexOf(this.id), 1);
  175. }
  176. delete this.list.idsToItems[this.id];
  177. }
  178. _hash() {
  179. if (this.type === TYPE_TEXT) {
  180. return _hashText(this.text);
  181. } else {
  182. return null;
  183. }
  184. }
  185. }
  186. LLNode.prototype.next = LLNode.prototype.prev = LLNode.prototype.list = null;
  187. // Creates a new List: A linked list is a bit like an Array, but knows nothing
  188. // about how many items are in it, and knows only about its first (`head`) and
  189. // last (`tail`) items.
  190. // Each item (e.g. `head`, `tail`, &c.) knows which item comes before or after
  191. // it (its more like the implementation of the DOM in JavaScript).
  192. export class LinkedList {
  193. // Creates a new list from the arguments (each a list item) passed in.
  194. static of(...items) {
  195. return appendAll(new this(), items);
  196. }
  197. // Creates a new list from the given array-like object (each a list item) passed
  198. // in.
  199. static from(items) {
  200. return appendAll(new this(), items);
  201. }
  202. constructor(...items) {
  203. appendAll(this, items);
  204. this.idsToItems = {};
  205. this.invertedIndex = {};
  206. /** Note: this isn't an accurate count because of UTF encoding and other JS mumbo jumbo. */
  207. this.bytes = 0;
  208. }
  209. // Returns the list's items as an array.
  210. // This does *not* detach the items.
  211. toArray() {
  212. let item = this.head;
  213. const result = [];
  214. while (item) {
  215. result.push(item);
  216. item = item.next;
  217. }
  218. return result;
  219. }
  220. // Prepends the given item to the list.
  221. // `item` will be the new first item (`head`).
  222. prepend(item) {
  223. if (!item) {
  224. return false;
  225. }
  226. if (!item.append || !item.prepend || !item.detach) {
  227. throw new Error(
  228. 'An argument without append, prepend, or detach methods was given to `List#prepend`.',
  229. );
  230. }
  231. if (this.head) {
  232. return this.head.prepend(item);
  233. }
  234. item.detach();
  235. item.list = this;
  236. this.head = item;
  237. this.length++;
  238. item._addToIndex();
  239. return item;
  240. }
  241. // Appends the given item to the list.
  242. // `item` will be the new last item (`tail`) if the list had a first item, and
  243. // its first item (`head`) otherwise.
  244. append(item) {
  245. if (!item) {
  246. return false;
  247. }
  248. if (!item.append || !item.prepend || !item.detach) {
  249. throw new Error(
  250. 'An argument without append, prepend, or detach methods was given to `List#append`.',
  251. );
  252. }
  253. // If self has a last item, defer appending to the last items append method,
  254. // and return the result.
  255. if (this.tail) {
  256. return this.tail.append(item);
  257. }
  258. // If self has a first item, defer appending to the first items append method,
  259. // and return the result.
  260. if (this.head) {
  261. return this.head.append(item);
  262. }
  263. // ...otherwise, there is no `tail` or `head` item yet.
  264. item.detach();
  265. item.list = this;
  266. this.head = item;
  267. this.length++;
  268. item._addToIndex();
  269. return item;
  270. }
  271. last() {
  272. return this.tail || this.head;
  273. }
  274. findById(id) {
  275. return this.idsToItems[id];
  276. }
  277. findTextItem(text) {
  278. const entries = this.invertedIndex[_hashText(text)];
  279. if (!entries) {
  280. return null;
  281. }
  282. for (let i = entries.length - 1; i >= 0; i--) {
  283. const item = this.idsToItems[entries[i]];
  284. if (item.type === TYPE_TEXT && item.text === text) {
  285. return item;
  286. }
  287. }
  288. return null;
  289. }
  290. // Creates an iterator from the list.
  291. [Symbol.iterator]() {
  292. return new Iterator(this.head);
  293. }
  294. }
  295. LinkedList.prototype.length = 0;
  296. LinkedList.prototype.tail = LinkedList.prototype.head = null;
  297. // Creates a new list from the items passed in.
  298. export function appendAll(list, items) {
  299. let index;
  300. let item;
  301. let iterator;
  302. if (!items) {
  303. return list;
  304. }
  305. if (items[Symbol.iterator]) {
  306. iterator = items[Symbol.iterator]();
  307. item = {};
  308. while (!item.done) {
  309. item = iterator.next();
  310. list.append(item && item.value);
  311. }
  312. } else {
  313. index = -1;
  314. while (++index < items.length) {
  315. list.append(items[index]);
  316. }
  317. }
  318. return list;
  319. }
  320. function _hashText(text) {
  321. // The goal of this hash function is to be extremely fast while minimizing collisions. To do
  322. // this, we make an assumption about our data. If users copy text, the guess is that there is
  323. // a very low likelihood of collisions when the text is very long. For example, why would
  324. // someone copy two different pieces of text that are exactly 29047 characters long? However, for
  325. // smaller pieces of text, it's very easy to get length collisions. For example, I can copy "the"
  326. // and "123" to cause a collision. Thus, our hash function returns the string length for longer
  327. // strings while using an ok-ish hash for short strings.
  328. if (text.length > 500) {
  329. return text.length;
  330. }
  331. // Copied from https://stackoverflow.com/a/7616484/4548500
  332. let hash = 0;
  333. for (let i = 0; i < text.length; i++) {
  334. let chr = text.charCodeAt(i);
  335. hash = (hash << 5) - hash + chr;
  336. hash |= 0; // Convert to integer
  337. }
  338. return hash;
  339. }