1.0.2 • Published 3 years ago

declarative-tree v1.0.2

Weekly downloads
-
License
MIT
Repository
github
Last release
3 years ago

Table of contents

Installation

npm install declarative-tree

Description

Generalized way to convert a string to tree, and vice versa. Useful for creating declarative tree tests.

Code coverage

The testing code coverage is around 90%.

Examples

The following examples deal with converting an AVL tree string representation to tree data structure and vice versa. Here is the code that defines the AVL tree:

class AvlTreeBaseNode {
    left: null | AvlTreeNode;
    right: null | AvlTreeNode;
    id: number;

    constructor(_: AvlTreeBaseNode) {
        const { id, left, right } = _;
        this.id = id;
        this.left = left;
        this.right = right;
    }
}

export class AvlTreeRootNode extends AvlTreeBaseNode {
    parent: null;

    constructor(_: AvlTreeRootNode) {
        const { id, left, right } = _;
        super({
            id,
            left,
            right,
        });
        this.parent = null;
    }
}

export class AvlTreeNode extends AvlTreeBaseNode {
    parent: AvlTreeNode | AvlTreeRootNode;

    constructor(_: AvlTreeNode) {
        const { parent, right, left, id } = _;
        super({
            id,
            left,
            right,
        });
        this.parent = parent;
    }
}

export class AvlTree {
    root: AvlTreeRootNode;

    constructor(_: { root: AvlTreeRootNode }) {
        const { root } = _;
        this.root = root;
    }
}

/**
 * @description
 * The tree looks like this:
 * ```
 * 20
 * |_ 25
 * |_ 10
 *    |_ 5
 * ```
 */
export const mockAvlTree: AvlTree = (() => {
    const root: AvlTreeRootNode = new AvlTreeRootNode({
        id: 20,
        left: null, //lazy setted
        parent: null,
        right: null, //lazy setted
    });
    const tree: AvlTree = new AvlTree({
        root,
    });
    const left: AvlTreeNode = new AvlTreeNode({
        id: 10,
        left: null, //lazy setted
        parent: root,
        right: null,
    });
    const leftLeft: AvlTreeNode = new AvlTreeNode({
        id: 5,
        left: null,
        parent: left,
        right: null,
    });
    const right: AvlTreeNode = new AvlTreeNode({
        id: 25,
        left: null,
        parent: root,
        right: null,
    });

    root.left = left;
    root.right = right;

    left.left = leftLeft;

    return tree;
})();

tree to string

import { treeToStringHorizontalFactory } from "..";
import { AvlTree, AvlTreeNode, AvlTreeRootNode, mockAvlTree } from "./mockTree";

describe(treeToStringHorizontalFactory.name, () => {
    it(`
        returns a function that converts a tree to string, according to the 
        instructions the factory has been provided
    `, () => {
        const avlTreeToString = treeToStringHorizontalFactory<
            AvlTreeNode | AvlTreeRootNode,
            AvlTree
        >({
            getChildren: ({ treeNode }) => {
                const children: AvlTreeNode[] = [];
                const { left, right } = treeNode;
                if (left !== null) children.push(left);
                if (right !== null) children.push(right);
                return children.reverse();
            },
            getRoot: ({ tree }) => tree.root,
            nodeToString: ({ treeNodeMetadata }) =>
                String(treeNodeMetadata.treeNode.id),
        });

        //prettier-ignore
        expect(avlTreeToString({ tree: mockAvlTree })).toBe(
            `20\n`+
            `|_ 25\n`+
            `|_ 10\n`+
            `   |_ 5`
        );
    });
});

string to tree horizontal

import { stringToTreeHorizontalFactory } from "..";
import { AvlTree, AvlTreeNode, AvlTreeRootNode, mockAvlTree } from "./mockTree";

describe(stringToTreeHorizontalFactory.name, () => {
    it(`
        returns a function that converts a string to tree, according to the 
        instructions the factory has been provided
    `, () => {
        const stringToAVLTree = stringToTreeHorizontalFactory<
            number,
            AvlTreeNode | AvlTreeRootNode,
            AvlTree
        >({
            strategy: (parameters) => {
                const root: AvlTreeRootNode = new AvlTreeRootNode({
                    id: parameters.treeNodeMetadataInLevelOrder[0]
                        .placeholderValue,
                    left: null,
                    parent: null,
                    right: null,
                });
                const avlTree: AvlTree = new AvlTree({ root });

                /**
                 * @description
                 * The main idea here is to iterate the tree node metadata, and
                 * use the information they provide to create the tree nodes.
                 * When you create a tree node, set it as the `treeNode`
                 * property of the tree node metadata it corresponds. The sole
                 * reason for that is that maybe you will need to access that
                 * node from other nodes.
                 */
                parameters.treeNodeMetadataInLevelOrder.forEach(
                    (treeNodeMetadata, i) => {
                        if (i === 0) {
                            treeNodeMetadata.treeNode = root;
                            return;
                        }
                        if (i !== 0) {
                            const parentTreeNodeMetadata =
                                treeNodeMetadata.parent;
                            if (parentTreeNodeMetadata === null) {
                                throw Error(
                                    "only the parent tree node metadata of" +
                                        "the root node can be null"
                                );
                            }
                            const parent = parentTreeNodeMetadata.treeNode;

                            const currentNode: AvlTreeNode = new AvlTreeNode({
                                id: treeNodeMetadata.placeholderValue,
                                left: null,
                                right: null,
                                parent,
                            });

                            treeNodeMetadata.treeNode = currentNode;

                            /**
                             * @description
                             * Here I connect the parent node with its child
                             * node. The only way to understand whether the
                             * child node is a left or right child, is through
                             * the brach that connects them.
                             * You are free to use the character that you see
                             * fit for branches. In this example I choose to use
                             * `⏝` for left branch and `⏜` for right branch.
                             * The branch character that you use has to be used
                             * in the tag function that is returned by the
                             * factory. Take a look at the test assertion a few
                             * lines bellow where the tag function is used.
                             */
                            if (treeNodeMetadata.branchTop === "⏝") {
                                parent.left = currentNode;
                                return;
                            }
                            if (treeNodeMetadata.branchTop === "⏜") {
                                parent.right = currentNode;
                                return;
                            }
                            throw Error(
                                `encountered branch that is not "⏝" or "⏜"`
                            );
                        }
                        throw Error("case not accounted for");
                    }
                );

                return avlTree;
            },
        });

        expect(stringToAVLTree`
            ${20}
            |⏜ ${25}
            |⏝ ${10}
               |⏝ ${5}
        `).toEqual(mockAvlTree);
    });
});

string to tree vertical

import { stringToTreeVerticalFactory } from "..";
import { AvlTree, AvlTreeNode, AvlTreeRootNode, mockAvlTree } from "./mockTree";

describe(stringToTreeVerticalFactory.name, () => {
    it(`
        returns a function that converts a string to tree, according to the 
        instructions the factory has been provided
    `, () => {
        const stringToAvlTree = stringToTreeVerticalFactory<
            number,
            AvlTreeNode | AvlTreeRootNode,
            AvlTree
        >({
            strategy: (parameters) => {
                const root: AvlTreeRootNode = new AvlTreeRootNode({
                    id: parameters.treeNodeMetadataInLevelOrder[0]
                        .placeholderValue,
                    left: null,
                    parent: null,
                    right: null,
                });
                const avlTree: AvlTree = new AvlTree({ root });

                /**
                 * @description
                 * The main idea here is to iterate the tree node metadata, and
                 * use the information they provide to create the tree nodes.
                 * When you create a tree node, set it as the `treeNode`
                 * property of the tree node metadata it corresponds. The sole
                 * reason for that is that maybe you will need to access that
                 * node from other nodes.
                 */
                parameters.treeNodeMetadataInLevelOrder.forEach(
                    (treeNodeMetadata, i) => {
                        if (i === 0) {
                            treeNodeMetadata.treeNode = root;
                            return;
                        }
                        if (i !== 0) {
                            const parentTreeNodeMetadata =
                                treeNodeMetadata.parent;
                            if (parentTreeNodeMetadata === null) {
                                throw Error(
                                    "only the parent tree node metadata of " +
                                        "the root node can be null"
                                );
                            }
                            const parent = parentTreeNodeMetadata.treeNode;

                            const currentNode: AvlTreeNode = new AvlTreeNode({
                                id: treeNodeMetadata.placeholderValue,
                                left: null,
                                right: null,
                                parent,
                            });

                            treeNodeMetadata.treeNode = currentNode;

                            /**
                             * @description
                             * Here I connect the parent node with its child
                             * node. The only way to understand whether the
                             * child node is a left or right child, is through
                             * the brach that connects them.
                             * You are free to use the character that you see
                             * fit for branches. In this example I choose to use
                             * `(` for left branch and `)` for right branch. The
                             * branch character that you use has to be used in
                             * the tag function that is returned by the factory.
                             * Take a look at the test assertion a few lines
                             * bellow where the tag function is used.
                             */
                            if (treeNodeMetadata.branchTop === "(") {
                                parent.left = currentNode;
                                return;
                            }
                            if (treeNodeMetadata.branchTop === ")") {
                                parent.right = currentNode;
                                return;
                            }
                            throw Error(
                                `encountered branch that is not "(" or ")"`
                            );
                        }
                        throw Error("case not accounted for");
                    }
                );

                return avlTree;
            },
        });

        /**
         * @description
         * You have to add dots. They define where the children group ends. You
         * do no need to do that in the last row of the tree. For example in the
         * following tree the tree node with id `20` has `10` and `25` as
         * children group. The group ends at `25` and hence the dot in the
         * branch above `25`. The tree node with id `10` has children group that
         * consist only of the tree node with id `5` and hence the dot on its
         * branch. The tree node with id `25` has no children so it has a dot
         * with no branches.
         */
        expect(stringToAvlTree`
                   ${20}
                (        ).
              ${10}     ${25}
            (.            .
            ${5}
        `).toEqual(mockAvlTree);
    });
});

Documentation

/**
 * @description
 * Returns a tag function that converts template literals to tree data
 * structures, in accordance to the instructions you have provided to the
 * factory.
 */
export declare const stringToTreeHorizontalFactory: IStringToTreeFactory;
/**
 * @description
 * Returns a tag function that converts template literals to tree data
 * structures, in accordance to the instructions you have provided to the
 * factory.
 */
export declare const stringToTreeVerticalFactory: IStringToTreeFactory;
/**
 * @description
 * Returns a function that converts tree data structures to horizontal tree
 * string representations, in accordance to the instructions you have provided
 * to the factory.
 */
export declare const treeToStringHorizontalFactory: ITreeToStringHorizontalFactory;

Motivation

I just wanted to create declarative tree tests for any kind of tree. No packages in npm could do that, so I decided to create my own.

Contributing

I am open to suggestions/pull request to improve this program.

You will find the following commands useful:

  • Clones the github repository of this project:

    git clone https://github.com/lillallol/declarative-tree
  • Installs the node modules (nothing will work without them):

    npm install
  • Tests the code and generates code coverage:

    npm run test

    The generated code coverage is saved in ./coverage.

  • Lints the source folder using typescript and eslint:

    npm run lint
  • Builds the typescript code from the ./src folder to javascript code in ./dist:

    npm run build-ts
  • Injects in place the generated toc and imported files to README.md:

    npm run build-md
  • Checks the project for spelling mistakes:

    npm run spell-check

    Take a look at the related configuration ./cspell.json.

  • Checks ./src for dead typescript files:

    npm run dead-files

    Take a look at the related configuration ./unimportedrc.json.

  • Logs which node modules can be updated:

    npm run check-updates
  • Updates the node modules to their latest version (even if they introduce breaking changes):

    npm run update
  • Formats all the typescript files in the ./src folder:

    npm run format

Changelog

1.0.2

Bugs fixed

  • Placeholders groups were being over flattened. For example the following template literal:

    `
        ${""}
        |_ ${"p"}
           |_ ${"a"}
              |_ ${"t"}
                 |_ ${"h"}
                    |_ ${["prop1", -1, "=>", 1]}
    `

    would wrongly have as placeholders:

    ["","p","a","t","h",...["prop1", -1, "=>", 1]]

    instead of:

    ["","p","a","t","h",["prop1", -1, "=>", 1]]

1.0.1

  • Fixed some mistakes in the examples.
  • Minor internal changes.

1.0.0

  • Published the package.

License

MIT