Skip to content

Commit

Permalink
Merge pull request #598 from tuanchauict/shape
Browse files Browse the repository at this point in the history
Port Shape to TypeScript
  • Loading branch information
tuanchauict authored Sep 10, 2024
2 parents 2798be7 + aa5552e commit 46d3a5a
Show file tree
Hide file tree
Showing 11 changed files with 1,121 additions and 0 deletions.
3 changes: 3 additions & 0 deletions monosketch-svelte/src/lib/libs/todo.ts
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 monosketch-svelte/src/lib/mono/shape/collection/identifier.ts
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 monosketch-svelte/src/lib/mono/shape/collection/quick-list.test.ts
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 monosketch-svelte/src/lib/mono/shape/collection/quick-list.ts
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;
}
}
}
Loading

0 comments on commit 46d3a5a

Please sign in to comment.