Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Cardinal Trees Support #21

Open
wants to merge 2 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dist/grove.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

181 changes: 181 additions & 0 deletions src/trie/CardinalTrie.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import CardinalTrieNode from "./CardinalTrieNode.js";

export default class CardinalTrie {
/**
* Constructor for the Cardinal Trie data structure
* @param {number} k The degree (k) of the cardinal tree
* @param {Array<string>} [alphabet] Optional alphabet for mapping characters to indices
*/
constructor(k = 2, alphabet = null) {
this.k = k;
this._root = new CardinalTrieNode(null, true, k);
this._lastIndex = 1;

if (alphabet) {
if (alphabet.length !== k)
throw new Error("Alphabet length must match the k");

this.alphabet = alphabet;
this.charToIndex = {};
this.indexToChar = {};
for (let i = 0; i < alphabet.length; i++) {
const char = alphabet[i];
this.charToIndex[char] = i;
this.indexToChar[i] = char;
}
} else {
this.alphabet = null;
this.charToIndex = null;
this.indexToChar = null;
}
}

/**
* Get the root node of the Cardinal Trie
* @returns {CardinalTrieNode}
*/
get root() {
return this._root;
}

/**
* Insert a word into the Cardinal Trie and map data onto it.
* If data is not provided, it is automatically generated as an increasing number.
* @param {string} word
* @param {Object} [data]
*/
insert(word, data) {
return this._insertWord(word, data, this._root, 0);
}

_insertWord(word, data, currentNode, wordIndex) {
if (wordIndex === word.length) {
currentNode.word = word;
currentNode.update(data || this._getNextIndex());
return true;
}

let index;
if (this.alphabet) {
const char = word.charAt(wordIndex);
if (!(char in this.charToIndex))
throw new Error("Invalid character in word: " + char);
index = this.charToIndex[char];
} else {
index = parseInt(word.charAt(wordIndex), 10);
if (isNaN(index) || index < 0 || index >= this.k)
throw new Error("Invalid character in word: " + word.charAt(wordIndex));
}

if (!currentNode.hasChild(index)) {
currentNode.addChild(index, new CardinalTrieNode({ key: index, node: currentNode }, false, this.k));
}

return this._insertWord(word, data, currentNode.children[index], wordIndex + 1);
}

/**
* Search for data indexed by the provided word in the Cardinal Trie.
* @param {string} word
* @returns {Object|null} Returns the data if found, otherwise null.
*/
search(word) {
const node = this._searchNode(word, this._root, 0);
return !node ? null : node.data;
}

_searchNode(word, currentNode, wordIndex) {
if (wordIndex === word.length) {
return currentNode.isEndOfWord ? currentNode : null;
}

let index;
if (this.alphabet) {
const char = word.charAt(wordIndex);
if (!(char in this.charToIndex))
return null;
index = this.charToIndex[char];
} else {
index = parseInt(word.charAt(wordIndex), 10);
if (isNaN(index) || index < 0 || index >= this.k)
return null;
}

return currentNode.hasChild(index) ? this._searchNode(word, currentNode.children[index], wordIndex + 1) : null;
}

/**
* Delete a word from the Cardinal Trie.
* @param {string} word
* @returns {boolean} True if the word was deleted, otherwise false.
*/
delete(word) {
const node = this._searchNode(word, this._root, 0);
if (!node)
return false;

if (node.hasChildren()) {
node.update(null);
return true;
}

this._deleteWord(node);
return true;
}

_deleteWord(currentNode) {
if (currentNode === this._root)
return;
const parent = currentNode.parent;

parent.node.deleteChild(parent.key);
if (parent.node.hasChildren())
return;

this._deleteWord(parent.node);
}

/**
* Update data associated with a word in the Cardinal Trie.
* @param {string} word
* @param {*} data
* @returns {boolean} True if the word was updated, otherwise false.
*/
update(word, data) {
const node = this._searchNode(word, this._root, 0);
if (!node)
return false;

node.update(data);
return true;
}

/**
* Get the node containing data for the given word.
* @param {string} word
* @returns {CardinalTrieNode|null}
*/
getDataNode(word) {
return this._searchNode(word, this._root, 0);
}

/**
* Get the path (nodes) representing the given word in the Cardinal Trie.
* @param {string} word
* @returns {Array<CardinalTrieNode|null>}
*/
getPath(word) {
const path = [];
path.push(this._root);

for (let i = 1; i <= word.length; i++) {
path.push(this._searchNode(word.substring(0, i), this._root, 0));
}

return path;
}

_getNextIndex() {
return this._lastIndex++;
}
}
104 changes: 104 additions & 0 deletions src/trie/CardinalTrieNode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
export default class CardinalTrieNode {
/**
* Constructor of a new node in the Cardinal Trie data structure
* @param {Object} parent Parent config object
* @param {number} parent.key Index for this node in its parent node
* @param {CardinalTrieNode} parent.node Reference to the parent node
* @param {boolean} [isRoot] Boolean flag indicating if this node is the root
* @param {number} k The degree (k) of the cardinal tree
*/
constructor(parent = { key: null, node: null }, isRoot = false, k = 2) {
if (!isRoot && (parent.key === null || typeof parent.key !== 'number'))
throw new Error("Parent key cannot be null or not type of number!");
if (!isRoot && (!parent.node || !(parent.node instanceof CardinalTrieNode)))
throw new Error("Parent node cannot be null or not instance of CardinalTrieNode");

this._parent = parent;
this.k = k;
this._children = new Array(k).fill(null);
this.data = null;
this.isEndOfWord = false;
this.word = null;
}

/**
* Get parent object consisting of the child index and parent node.
* @returns {{key: number, node: CardinalTrieNode}}
*/
get parent() {
return this._parent;
}

/**
* Get array of all node's children.
* @returns {Array<CardinalTrieNode|null>} Array of child nodes.
*/
get children() {
return this._children;
}

/**
* Update indexed data of the node.
* @param {*} data If data is set, the node is marked as the end of a word.
*/
update(data) {
this.isEndOfWord = !!data;
this.data = data;

if (!this.isEndOfWord)
this.word = null;
}

/**
* Remove parent object from this node.
*/
unlink() {
this._parent = { key: null, node: null };
}

/**
* Check if the node has any child nodes attached to it.
* @returns {boolean} True if it has any children, otherwise false.
*/
hasChildren() {
return this._children.some(child => child !== null);
}

/**
* Delete child at the given index.
* @param {number} index Child index
*/
deleteChild(index) {
if (index < 0 || index >= this.k || this._children[index] === null)
return;

this._children[index].update(null);
this._children[index].unlink();
this._children[index].word = null;
this._children[index] = null;
}

/**
* Add a child to the node at the specified index.
* @param {number} index Child index
* @param {CardinalTrieNode} node Child node to add
* @returns {CardinalTrieNode|null} Returns the old child if it existed, otherwise null.
*/
addChild(index, node) {
if (index < 0 || index >= this.k || !node)
return null;

const old = this._children[index];
this._children[index] = node;
return old;
}

/**
* Check if the node has a child at the specified index.
* @param {number} index Child index
* @returns {boolean} True if a child exists at the index, otherwise false.
*/
hasChild(index) {
return index >= 0 && index < this.k && this._children[index] !== null;
}
}
2 changes: 2 additions & 0 deletions src/trie/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export {default as Trie} from "./Trie";
export {default as TrieNode} from "./TrieNode.js";
export {default as CardinalTrie} from "./CardinalTrie.js";
export {default as CardinalTrieNode} from "./CardinalTrieNode.js";
34 changes: 34 additions & 0 deletions test-report.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<testExecutions version="1">
<file path="C:\Users\navi1\OneDrive\Počítač\grove\test\trie.spec.js">
<testCase name="Trie spec creating new instance of Trie" duration="4"/>
<testCase name="Trie spec inserting word CACAO" duration="43"/>
<testCase name="CardinalTrie spec should insert and search words" duration="2"/>
<testCase name="CardinalTrie spec should delete a word from the trie" duration="2"/>
<testCase name="CardinalTrie spec should update the data of a word" duration="1"/>
<testCase name="CardinalTrie spec should retrieve the path of a word" duration="1"/>
<testCase name="CardinalTrie spec should handle insertion and search with overlapping prefixes" duration="1"/>
<testCase name="CardinalTrie spec should throw an error for invalid characters in word" duration="57"/>
<testCase name="CardinalTrie spec should return null when searching in an empty trie" duration="1"/>
<testCase name="CardinalTrie spec inserting very long word to test recursion depth" duration="14"/>
<testCase name="CardinalTrie without alphabet should insert and search numeric words" duration="0"/>
<testCase name="CardinalTrie without alphabet should throw an error for invalid characters (non-digit)" duration="1"/>
<testCase name="CardinalTrie without alphabet should handle insertion and search with overlapping numeric prefixes" duration="1"/>
<testCase name="CardinalTrie without alphabet should return null when searching for non-existent numeric word" duration="1"/>
<testCase name="CardinalTrieNode spec should throw an error if parent key is null and not root" duration="1"/>
<testCase name="CardinalTrieNode spec should throw an error if parent node is null and not root" duration="1"/>
<testCase name="CardinalTrieNode spec should create a root node without a parent" duration="1"/>
<testCase name="CardinalTrieNode methods should check if node has a child at a specific index" duration="1"/>
<testCase name="CardinalTrieNode methods should add and delete child nodes correctly" duration="0"/>
<testCase name="CardinalTrieNode methods should verify if node has any children" duration="0"/>
<testCase name="CardinalTrieNode methods should unlink a child node from its parent" duration="1"/>
<testCase name="CardinalTrieNode methods should update node data correctly" duration="1"/>
<testCase name="CardinalTrie additional tests should handle insertion with provided data" duration="1"/>
<testCase name="CardinalTrie additional tests should update existing word with new data" duration="0"/>
<testCase name="CardinalTrie additional tests should delete a word and ensure it&apos;s removed" duration="0"/>
<testCase name="CardinalTrie additional tests should retrieve the correct path for a given word" duration="0"/>
<testCase name="CardinalTrie additional tests should return null when searching for a non-existent word" duration="0"/>
<testCase name="CardinalTrie additional tests should handle insertion and search with overlapping prefixes without alphabet" duration="0"/>
<testCase name="CardinalTrie additional tests should throw an error when inserting a word with invalid character without alphabet" duration="0"/>
</file>
</testExecutions>
Loading