You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

528 lines
12 KiB

4 years ago
  1. //
  2. // list
  3. // ┌──────┐
  4. // ┌──────────────┼─head │
  5. // │ │ tail─┼──────────────┐
  6. // │ └──────┘ │
  7. // ▼ ▼
  8. // item item item item
  9. // ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
  10. // null ◀──┼─prev │◀───┼─prev │◀───┼─prev │◀───┼─prev │
  11. // │ next─┼───▶│ next─┼───▶│ next─┼───▶│ next─┼──▶ null
  12. // ├──────┤ ├──────┤ ├──────┤ ├──────┤
  13. // │ data │ │ data │ │ data │ │ data │
  14. // └──────┘ └──────┘ └──────┘ └──────┘
  15. //
  16. function createItem(data) {
  17. return {
  18. prev: null,
  19. next: null,
  20. data: data
  21. };
  22. }
  23. function allocateCursor(node, prev, next) {
  24. var cursor;
  25. if (cursors !== null) {
  26. cursor = cursors;
  27. cursors = cursors.cursor;
  28. cursor.prev = prev;
  29. cursor.next = next;
  30. cursor.cursor = node.cursor;
  31. } else {
  32. cursor = {
  33. prev: prev,
  34. next: next,
  35. cursor: node.cursor
  36. };
  37. }
  38. node.cursor = cursor;
  39. return cursor;
  40. }
  41. function releaseCursor(node) {
  42. var cursor = node.cursor;
  43. node.cursor = cursor.cursor;
  44. cursor.prev = null;
  45. cursor.next = null;
  46. cursor.cursor = cursors;
  47. cursors = cursor;
  48. }
  49. var cursors = null;
  50. var List = function() {
  51. this.cursor = null;
  52. this.head = null;
  53. this.tail = null;
  54. };
  55. List.createItem = createItem;
  56. List.prototype.createItem = createItem;
  57. List.prototype.updateCursors = function(prevOld, prevNew, nextOld, nextNew) {
  58. var cursor = this.cursor;
  59. while (cursor !== null) {
  60. if (cursor.prev === prevOld) {
  61. cursor.prev = prevNew;
  62. }
  63. if (cursor.next === nextOld) {
  64. cursor.next = nextNew;
  65. }
  66. cursor = cursor.cursor;
  67. }
  68. };
  69. List.prototype.getSize = function() {
  70. var size = 0;
  71. var cursor = this.head;
  72. while (cursor) {
  73. size++;
  74. cursor = cursor.next;
  75. }
  76. return size;
  77. };
  78. List.prototype.fromArray = function(array) {
  79. var cursor = null;
  80. this.head = null;
  81. for (var i = 0; i < array.length; i++) {
  82. var item = createItem(array[i]);
  83. if (cursor !== null) {
  84. cursor.next = item;
  85. } else {
  86. this.head = item;
  87. }
  88. item.prev = cursor;
  89. cursor = item;
  90. }
  91. this.tail = cursor;
  92. return this;
  93. };
  94. List.prototype.toArray = function() {
  95. var cursor = this.head;
  96. var result = [];
  97. while (cursor) {
  98. result.push(cursor.data);
  99. cursor = cursor.next;
  100. }
  101. return result;
  102. };
  103. List.prototype.toJSON = List.prototype.toArray;
  104. List.prototype.isEmpty = function() {
  105. return this.head === null;
  106. };
  107. List.prototype.first = function() {
  108. return this.head && this.head.data;
  109. };
  110. List.prototype.last = function() {
  111. return this.tail && this.tail.data;
  112. };
  113. List.prototype.each = function(fn, context) {
  114. var item;
  115. if (context === undefined) {
  116. context = this;
  117. }
  118. // push cursor
  119. var cursor = allocateCursor(this, null, this.head);
  120. while (cursor.next !== null) {
  121. item = cursor.next;
  122. cursor.next = item.next;
  123. fn.call(context, item.data, item, this);
  124. }
  125. // pop cursor
  126. releaseCursor(this);
  127. };
  128. List.prototype.forEach = List.prototype.each;
  129. List.prototype.eachRight = function(fn, context) {
  130. var item;
  131. if (context === undefined) {
  132. context = this;
  133. }
  134. // push cursor
  135. var cursor = allocateCursor(this, this.tail, null);
  136. while (cursor.prev !== null) {
  137. item = cursor.prev;
  138. cursor.prev = item.prev;
  139. fn.call(context, item.data, item, this);
  140. }
  141. // pop cursor
  142. releaseCursor(this);
  143. };
  144. List.prototype.forEachRight = List.prototype.eachRight;
  145. List.prototype.nextUntil = function(start, fn, context) {
  146. if (start === null) {
  147. return;
  148. }
  149. var item;
  150. if (context === undefined) {
  151. context = this;
  152. }
  153. // push cursor
  154. var cursor = allocateCursor(this, null, start);
  155. while (cursor.next !== null) {
  156. item = cursor.next;
  157. cursor.next = item.next;
  158. if (fn.call(context, item.data, item, this)) {
  159. break;
  160. }
  161. }
  162. // pop cursor
  163. releaseCursor(this);
  164. };
  165. List.prototype.prevUntil = function(start, fn, context) {
  166. if (start === null) {
  167. return;
  168. }
  169. var item;
  170. if (context === undefined) {
  171. context = this;
  172. }
  173. // push cursor
  174. var cursor = allocateCursor(this, start, null);
  175. while (cursor.prev !== null) {
  176. item = cursor.prev;
  177. cursor.prev = item.prev;
  178. if (fn.call(context, item.data, item, this)) {
  179. break;
  180. }
  181. }
  182. // pop cursor
  183. releaseCursor(this);
  184. };
  185. List.prototype.some = function(fn, context) {
  186. var cursor = this.head;
  187. if (context === undefined) {
  188. context = this;
  189. }
  190. while (cursor !== null) {
  191. if (fn.call(context, cursor.data, cursor, this)) {
  192. return true;
  193. }
  194. cursor = cursor.next;
  195. }
  196. return false;
  197. };
  198. List.prototype.map = function(fn, context) {
  199. var result = new List();
  200. var cursor = this.head;
  201. if (context === undefined) {
  202. context = this;
  203. }
  204. while (cursor !== null) {
  205. result.appendData(fn.call(context, cursor.data, cursor, this));
  206. cursor = cursor.next;
  207. }
  208. return result;
  209. };
  210. List.prototype.filter = function(fn, context) {
  211. var result = new List();
  212. var cursor = this.head;
  213. if (context === undefined) {
  214. context = this;
  215. }
  216. while (cursor !== null) {
  217. if (fn.call(context, cursor.data, cursor, this)) {
  218. result.appendData(cursor.data);
  219. }
  220. cursor = cursor.next;
  221. }
  222. return result;
  223. };
  224. List.prototype.clear = function() {
  225. this.head = null;
  226. this.tail = null;
  227. };
  228. List.prototype.copy = function() {
  229. var result = new List();
  230. var cursor = this.head;
  231. while (cursor !== null) {
  232. result.insert(createItem(cursor.data));
  233. cursor = cursor.next;
  234. }
  235. return result;
  236. };
  237. List.prototype.prepend = function(item) {
  238. // head
  239. // ^
  240. // item
  241. this.updateCursors(null, item, this.head, item);
  242. // insert to the beginning of the list
  243. if (this.head !== null) {
  244. // new item <- first item
  245. this.head.prev = item;
  246. // new item -> first item
  247. item.next = this.head;
  248. } else {
  249. // if list has no head, then it also has no tail
  250. // in this case tail points to the new item
  251. this.tail = item;
  252. }
  253. // head always points to new item
  254. this.head = item;
  255. return this;
  256. };
  257. List.prototype.prependData = function(data) {
  258. return this.prepend(createItem(data));
  259. };
  260. List.prototype.append = function(item) {
  261. return this.insert(item);
  262. };
  263. List.prototype.appendData = function(data) {
  264. return this.insert(createItem(data));
  265. };
  266. List.prototype.insert = function(item, before) {
  267. if (before !== undefined && before !== null) {
  268. // prev before
  269. // ^
  270. // item
  271. this.updateCursors(before.prev, item, before, item);
  272. if (before.prev === null) {
  273. // insert to the beginning of list
  274. if (this.head !== before) {
  275. throw new Error('before doesn\'t belong to list');
  276. }
  277. // since head points to before therefore list doesn't empty
  278. // no need to check tail
  279. this.head = item;
  280. before.prev = item;
  281. item.next = before;
  282. this.updateCursors(null, item);
  283. } else {
  284. // insert between two items
  285. before.prev.next = item;
  286. item.prev = before.prev;
  287. before.prev = item;
  288. item.next = before;
  289. }
  290. } else {
  291. // tail
  292. // ^
  293. // item
  294. this.updateCursors(this.tail, item, null, item);
  295. // insert to the ending of the list
  296. if (this.tail !== null) {
  297. // last item -> new item
  298. this.tail.next = item;
  299. // last item <- new item
  300. item.prev = this.tail;
  301. } else {
  302. // if list has no tail, then it also has no head
  303. // in this case head points to new item
  304. this.head = item;
  305. }
  306. // tail always points to new item
  307. this.tail = item;
  308. }
  309. return this;
  310. };
  311. List.prototype.insertData = function(data, before) {
  312. return this.insert(createItem(data), before);
  313. };
  314. List.prototype.remove = function(item) {
  315. // item
  316. // ^
  317. // prev next
  318. this.updateCursors(item, item.prev, item, item.next);
  319. if (item.prev !== null) {
  320. item.prev.next = item.next;
  321. } else {
  322. if (this.head !== item) {
  323. throw new Error('item doesn\'t belong to list');
  324. }
  325. this.head = item.next;
  326. }
  327. if (item.next !== null) {
  328. item.next.prev = item.prev;
  329. } else {
  330. if (this.tail !== item) {
  331. throw new Error('item doesn\'t belong to list');
  332. }
  333. this.tail = item.prev;
  334. }
  335. item.prev = null;
  336. item.next = null;
  337. return item;
  338. };
  339. List.prototype.push = function(data) {
  340. this.insert(createItem(data));
  341. };
  342. List.prototype.pop = function() {
  343. if (this.tail !== null) {
  344. return this.remove(this.tail);
  345. }
  346. };
  347. List.prototype.unshift = function(data) {
  348. this.prepend(createItem(data));
  349. };
  350. List.prototype.shift = function() {
  351. if (this.head !== null) {
  352. return this.remove(this.head);
  353. }
  354. };
  355. List.prototype.prependList = function(list) {
  356. return this.insertList(list, this.head);
  357. };
  358. List.prototype.appendList = function(list) {
  359. return this.insertList(list);
  360. };
  361. List.prototype.insertList = function(list, before) {
  362. // ignore empty lists
  363. if (list.head === null) {
  364. return this;
  365. }
  366. if (before !== undefined && before !== null) {
  367. this.updateCursors(before.prev, list.tail, before, list.head);
  368. // insert in the middle of dist list
  369. if (before.prev !== null) {
  370. // before.prev <-> list.head
  371. before.prev.next = list.head;
  372. list.head.prev = before.prev;
  373. } else {
  374. this.head = list.head;
  375. }
  376. before.prev = list.tail;
  377. list.tail.next = before;
  378. } else {
  379. this.updateCursors(this.tail, list.tail, null, list.head);
  380. // insert to end of the list
  381. if (this.tail !== null) {
  382. // if destination list has a tail, then it also has a head,
  383. // but head doesn't change
  384. // dest tail -> source head
  385. this.tail.next = list.head;
  386. // dest tail <- source head
  387. list.head.prev = this.tail;
  388. } else {
  389. // if list has no a tail, then it also has no a head
  390. // in this case points head to new item
  391. this.head = list.head;
  392. }
  393. // tail always start point to new item
  394. this.tail = list.tail;
  395. }
  396. list.head = null;
  397. list.tail = null;
  398. return this;
  399. };
  400. List.prototype.replace = function(oldItem, newItemOrList) {
  401. if ('head' in newItemOrList) {
  402. this.insertList(newItemOrList, oldItem);
  403. } else {
  404. this.insert(newItemOrList, oldItem);
  405. }
  406. this.remove(oldItem);
  407. };
  408. module.exports = List;