-
-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #598 from tuanchauict/shape
Port Shape to TypeScript
- Loading branch information
Showing
11 changed files
with
1,121 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export const TODO = (reason: string = "Not implemented") => { | ||
throw new Error(reason); | ||
}; |
6 changes: 6 additions & 0 deletions
6
monosketch-svelte/src/lib/mono/shape/collection/identifier.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
/** | ||
* A type for an object with an id. | ||
*/ | ||
export type Identifier = { | ||
id: string; | ||
} |
120 changes: 120 additions & 0 deletions
120
monosketch-svelte/src/lib/mono/shape/collection/quick-list.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
import type { Identifier } from "$mono/shape/collection/identifier"; | ||
import { beforeEach, describe, expect, test } from "vitest"; | ||
import { QuickList, AddPosition, MoveActionType } from './quick-list'; | ||
|
||
describe('QuickList', () => { | ||
let target: QuickList<Item>; | ||
|
||
class Item implements Identifier { | ||
id: string; | ||
|
||
constructor(id: string) { | ||
this.id = id; | ||
} | ||
} | ||
|
||
const TestData = { | ||
ITEM_0: new Item('0'), | ||
ITEM_1: new Item('1'), | ||
ITEM_2: new Item('2'), | ||
ITEM_3: new Item('3'), | ||
get ITEMS() { | ||
return [this.ITEM_0, this.ITEM_1, this.ITEM_2, this.ITEM_3]; | ||
}, | ||
}; | ||
|
||
beforeEach(() => { | ||
target = new QuickList<Item>(); | ||
}); | ||
|
||
test('add', () => { | ||
expect(target.isEmpty()).toBeTruthy(); | ||
|
||
expect(target.add(TestData.ITEM_1)).toBeTruthy(); | ||
expect(target.add(TestData.ITEM_1)).toBeFalsy(); | ||
expect(target.size).toBe(1); | ||
expect(target.get('1')).toEqual(TestData.ITEM_1); | ||
|
||
target.add(TestData.ITEM_0, AddPosition.First); | ||
expect(target.get('0')).toEqual(TestData.ITEM_0); | ||
expect(target.get('1')).toEqual(TestData.ITEM_1); | ||
expect(Array.from(target)).toEqual([TestData.ITEM_0, TestData.ITEM_1]); | ||
|
||
target.add(TestData.ITEM_3); | ||
target.add(TestData.ITEM_2, AddPosition.After(TestData.ITEM_1)); | ||
expect(Array.from(target)).toEqual([TestData.ITEM_0, TestData.ITEM_1, TestData.ITEM_2, TestData.ITEM_3]); | ||
}); | ||
|
||
describe('addAll', () => { | ||
test('last', () => { | ||
target.add(TestData.ITEM_0); | ||
target.add(TestData.ITEM_1); | ||
target.addAll(TestData.ITEMS.slice(2)); | ||
expect(Array.from(target)).toEqual(TestData.ITEMS); | ||
}); | ||
|
||
test('first', () => { | ||
target.add(TestData.ITEM_2); | ||
target.add(TestData.ITEM_3); | ||
target.addAll(TestData.ITEMS.slice(0, 2), AddPosition.First); | ||
expect(Array.from(target)).toEqual(TestData.ITEMS); | ||
}); | ||
|
||
test('after', () => { | ||
target.add(TestData.ITEM_0); | ||
target.add(TestData.ITEM_3); | ||
target.addAll(TestData.ITEMS.slice(1, 3), AddPosition.After(TestData.ITEM_0)); | ||
expect(Array.from(target)).toEqual(TestData.ITEMS); | ||
}); | ||
}); | ||
|
||
test('remove', () => { | ||
target.add(TestData.ITEM_0); | ||
target.add(TestData.ITEM_1); | ||
|
||
expect(target.remove(TestData.ITEM_2)).toBeNull(); | ||
|
||
expect(target.remove(new Item('1'))).toEqual(TestData.ITEM_1); | ||
expect(target.size).toBe(1); | ||
expect(target.get('1')).toBeNull(); | ||
|
||
expect(target.remove(new Item('0'))).toEqual(TestData.ITEM_0); | ||
expect(target.isEmpty()).toBeTruthy(); | ||
}); | ||
|
||
test('removeAll', () => { | ||
target.addAll(TestData.ITEMS); | ||
target.removeAll(); | ||
expect(target.isEmpty()).toBeTruthy(); | ||
}); | ||
|
||
describe('move', () => { | ||
test('up', () => { | ||
target.addAll(TestData.ITEMS); | ||
|
||
expect(target.move(new Item('1'), MoveActionType.UP)).toBeTruthy(); | ||
expect(Array.from(target)).toEqual([TestData.ITEM_0, TestData.ITEM_2, TestData.ITEM_1, TestData.ITEM_3]); | ||
}); | ||
|
||
test('down', () => { | ||
target.addAll(TestData.ITEMS); | ||
|
||
expect(target.move(new Item('2'), MoveActionType.DOWN)).toBeTruthy(); | ||
expect(Array.from(target)).toEqual([TestData.ITEM_0, TestData.ITEM_2, TestData.ITEM_1, TestData.ITEM_3]); | ||
}); | ||
|
||
test('top', () => { | ||
target.addAll(TestData.ITEMS); | ||
|
||
expect(target.move(new Item('1'), MoveActionType.TOP)).toBeTruthy(); | ||
expect(Array.from(target)).toEqual([TestData.ITEM_0, TestData.ITEM_2, TestData.ITEM_3, TestData.ITEM_1]); | ||
}); | ||
|
||
test('bottom', () => { | ||
target.addAll(TestData.ITEMS); | ||
|
||
expect(target.move(new Item('2'), MoveActionType.BOTTOM)).toBeTruthy(); | ||
expect(Array.from(target)).toEqual([TestData.ITEM_2, TestData.ITEM_0, TestData.ITEM_1, TestData.ITEM_3]); | ||
}); | ||
}); | ||
}); |
215 changes: 215 additions & 0 deletions
215
monosketch-svelte/src/lib/mono/shape/collection/quick-list.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,215 @@ | ||
import type { Identifier } from "./identifier"; | ||
|
||
export enum MoveActionType { | ||
UP, | ||
DOWN, | ||
TOP, | ||
BOTTOM | ||
} | ||
|
||
export class AddPosition { | ||
static First = new AddPosition(null); | ||
static Last = new AddPosition(null); | ||
|
||
static After(identifier: Identifier): AddPosition { | ||
return new AddPosition(identifier); | ||
} | ||
|
||
private constructor(public identifier: Identifier | null) { | ||
} | ||
} | ||
|
||
/** | ||
* A collection which works similar to LinkedHashMap in Java by making accessing item fast while keeping | ||
* the order of items based on add-sequence. | ||
* This also supports move up/down/top/bottom of the list for an item as well as adding item into | ||
* the head or the tail or after a specific item. | ||
*/ | ||
export class QuickList<T extends Identifier> implements Iterable<T> { | ||
private linkedList: DoubleLinkedList<T> = new DoubleLinkedList<T>(); | ||
private map: Map<string, Node<T>> = new Map<string, Node<T>>(); | ||
|
||
get size(): number { | ||
return this.map.size; | ||
} | ||
|
||
contains(element: T): boolean { | ||
return this.map.has(element.id); | ||
} | ||
|
||
containsAll(elements: Iterable<T>): boolean { | ||
for (const element of elements) { | ||
if (!this.contains(element)) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
|
||
isEmpty(): boolean { | ||
return this.size === 0; | ||
} | ||
|
||
[Symbol.iterator](): Iterator<T> { | ||
return this.linkedList.iterator(); | ||
} | ||
|
||
add(element: T, position: AddPosition = AddPosition.Last): boolean { | ||
if (this.contains(element)) { | ||
return false; | ||
} | ||
|
||
const preNode = (() => { | ||
switch (position) { | ||
case AddPosition.Last: | ||
return this.linkedList.tail.pre; | ||
case AddPosition.First: | ||
return this.linkedList.head; | ||
default: // AddPosition after | ||
return this.map.get(position.identifier!.id); | ||
} | ||
})(); | ||
|
||
if (!preNode) { | ||
return false; | ||
} | ||
|
||
const node = this.linkedList.add(element, preNode); | ||
this.map.set(element.id, node); | ||
|
||
return true; | ||
} | ||
|
||
addAll(collection: Iterable<T>, position: AddPosition = AddPosition.Last) { | ||
let previous = position; | ||
for (const element of collection) { | ||
this.add(element, previous); | ||
previous = AddPosition.After(element); | ||
} | ||
} | ||
|
||
remove(identifier: Identifier): T | null { | ||
const node = this.map.get(identifier.id); | ||
if (!node) { | ||
return null; | ||
} | ||
this.map.delete(identifier.id); | ||
this.linkedList.remove(node); | ||
return node.value; | ||
} | ||
|
||
removeAll(): T[] { | ||
const result = Array.from(this); | ||
this.linkedList.clear(); | ||
this.map.clear(); | ||
return result; | ||
} | ||
|
||
get(id: string): T | null { | ||
return this.map.get(id)?.value || null; | ||
} | ||
|
||
move(identifier: Identifier, moveActionType: MoveActionType): boolean { | ||
if (this.size < 2) { | ||
return false; | ||
} | ||
|
||
const node = this.map.get(identifier.id); | ||
if (!node) { | ||
return false; | ||
} | ||
return this.linkedList.move(node, moveActionType); | ||
} | ||
} | ||
|
||
class Node<T> { | ||
constructor( | ||
public pre: Node<T> | null, | ||
public next: Node<T> | null, | ||
public value: T | null, | ||
) { | ||
} | ||
} | ||
|
||
class DoubleLinkedList<T> { | ||
head: Node<T> = new Node<T>(null, null, null); | ||
tail: Node<T> = new Node<T>(null, null, null); | ||
|
||
constructor() { | ||
this.head.next = this.tail; | ||
this.tail.pre = this.head; | ||
} | ||
|
||
add(value: T, previousNode: Node<T>): Node<T> { | ||
const afterNode = previousNode.next; | ||
const node = new Node<T>(previousNode, afterNode, value); | ||
previousNode.next = node; | ||
if (afterNode) { | ||
afterNode.pre = node; | ||
} | ||
return node; | ||
} | ||
|
||
remove(node: Node<T>) { | ||
const previousNode = node.pre; | ||
const afterNode = node.next; | ||
if (previousNode) { | ||
previousNode.next = afterNode; | ||
} | ||
if (afterNode) { | ||
afterNode.pre = previousNode; | ||
} | ||
node.next = null; | ||
node.pre = null; | ||
} | ||
|
||
clear(): void { | ||
this.head.next = this.tail; | ||
this.tail.pre = this.head; | ||
} | ||
|
||
move(node: Node<T>, moveActionType: MoveActionType): boolean { | ||
let newPreviousNode: Node<T> | null | undefined; | ||
switch (moveActionType) { | ||
case MoveActionType.UP: | ||
newPreviousNode = node.next !== this.tail ? node.next : null; | ||
break; | ||
case MoveActionType.TOP: | ||
newPreviousNode = node.next !== this.tail ? this.tail.pre : null; | ||
break; | ||
case MoveActionType.DOWN: | ||
newPreviousNode = node.pre !== this.head ? node.pre?.pre : null; | ||
break; | ||
case MoveActionType.BOTTOM: | ||
newPreviousNode = node.pre !== this.head ? this.head : null; | ||
break; | ||
default: | ||
return false; | ||
} | ||
|
||
if (!newPreviousNode) { | ||
return false; | ||
} | ||
|
||
this.remove(node); | ||
|
||
const newNextNode = newPreviousNode.next; | ||
newPreviousNode.next = node; | ||
if (newNextNode) { | ||
newNextNode.pre = node; | ||
} | ||
node.pre = newPreviousNode; | ||
node.next = newNextNode; | ||
return true; | ||
} | ||
|
||
* iterator(): IterableIterator<T> { | ||
let current = this.head.next; | ||
while (current && current !== this.tail) { | ||
if (current.value) { | ||
yield current.value; | ||
} | ||
current = current.next; | ||
} | ||
} | ||
} |
Oops, something went wrong.