Common tools (ct, also @vyacheslav97/ct)
Common tools (hereinafter referred to as ct) - a common JavaScript data structures and associated processing procedures package (package repository).
Tip: questions bother your head? Feel free to contact me
Current version renewals (1.0.2) (see Prerequesites a bit below):
- clear method for Graph and Tree template classes (removes all nodes from a graph or node instance)
- Input nodes validation within Graph and Tree constructors (now they are added via addNode method with all its checks)
- isLeaf boolean flag (nodeData property member) for Tree nodes, and related logic of its auto handling during adding and removing nodes (now each newly added node has isLeaf === true, if node receives some child node, its isLeaf then demolished)
Next scheduled major updates:
- Table class template
- Default pathfinder template based on BFS algorithm
- Default leveling step function based on DFS algorithm
Prerequisites
tsconfig.json moduleResolution has to be set to node
Module guts
Universal iterable converter (UIC)
UIC module provides a user with two functions: transformToIterable and its wrapper, createIterableTransformer. UIC module is represented by uic.ts file of original project.
transformToIterable is a core procedure - it defines the way an input iterable is converted to an output one.
createIterableTransformer wraps transformToIterable to create an iterable convertor with encapsulated target iterable constructor.
transformToIterable algorithm essentials:
- Accepts 3 arguments: source iterable object, target iterable constructor and source iterable value converter (the last is optional)
- Sometimes source iterable can be consumed by target iterable constructor as it is, then source iterable value converter may be omitted
- Otherwise, a proxy iterable is created, that wraps source iterable values in source iterable value converter. Then this proxy iterable is passed to a target iterable constructor
Usage notes
UIC members can be imported as follows:
import {createIterableTransformer, transformToIterable} from '@vyacheslav97/ct';
createIterableTransformer is highly recommended for usage instead of transformToIterable to avoid redundant target iterable constructor referencing.
For example, lets inspect a createIterableTransformer application from uicsds.ts:
export function createArrayToMap<ArrayItemType, K, V>(
arrayItemTransformer: SingleToPair<ArrayItemType, K, V>,
) {
return createIterableTransformer<ArrayItemType[], [K, V], Map<K, V>>(Map, arrayItemTransformer);
}
createArrayToMap creates an Array to Map transformer. All such transformer needs to be utilized is an array item transform function, which converts an array item to key-value pair for Map constructor.
Here goes an createArrayToMap application example:
const shiftedArrayItemToMapItemTransformer: SingleToPair<number, number, number>
= (number): [number, number] => [number, number + 1];
const arrayToMapShifted = createArrayToMap<number, number, number>(shiftedArrayItemToMapItemTransformer);
arrayToMapShifted([1, 2, 3])
Remember, you are able to create your own custom iterable transformers!
Universal iterable converter (standard data structures, UICSDS)
UICSDS module consists of createIterableTransformer applications to built-in JavaScript iterable datastructures. uicsds.ts represents UICSDS module content.
UICSDS module members can be imported as follows:
import {createArrayToMap, createMapToSet} from '@vyacheslav97/ct';
Usage notes
It's pretty easy to use:
- first, just import caught your eye transformer creator
- then specify source iterable transformation function
- finally create your iterable converter
Example:
import {
SingleToPair,
SingleToSingle,
PairToSingle
} from "@vyacheslav97/ct";
const keyValuePairConcatenatedTransformer: PairToSingle<number, number, string>
= (pair) => pair.join('');
const keyValuePairConcatenated = createMapToString(keyValuePairConcatenatedTransformer);
keyValuepairConcatenated(new Map([[1, 1], [2, 2]]));
Graph abstract class
Graph abstract class implements directed graph abstraction with basic graph content manipulations: adding nodes, deleting nodes.
Graph node is represented via following generic interface:
interface GraphNodeInterface<NodeData = void> {
nodeId: string,
incomingBoundaries: Map<string, string>,
outgoingBoundaries: Map<string, string>,
nodeData?: NodeData,
}
Graph class itself implements next interface:
interface GraphInterface<NodeData = void> {
nodes: Map<string, GraphNodeInterface<NodeData>>,
getNode: (nodeId: string) => GraphNodeInterface<NodeData> | undefined,
getNodeData: (nodeId: string) => NodeData | undefined,
hasNode: (nodeId: string) => boolean,
addNode: (node: GraphNodeInterface<NodeData>) => void,
removeNode: (nodeId: string) => GraphNodeInterface<NodeData> | undefined,
flatten: (startNodeId: string) => Iterable<GraphNodeInterface<NodeData>>,
buildPath: (startNodeId: string, endNodeId: string) => GraphNodeInterface<NodeData>[] | undefined,
}
Some methods are not covered above:
- setFlattenProcedure - initializes flatten method via user defined leveling step function
- setPathFinderProcedure - initializes buildPath method via user defined pathfinder
- buildFlattenProcedure, buildPathFinder - are used for two previous method, it is highly recommended to avoid calling these methods
- clear - removes all nodes from a Graph instance (so nodes Map becomes empty)
Usage notes
Import Graph class:
import {Graph} from '@vyacheslav97/ct';
Initialization
Graph can be initialized via its constructor if proper set of arguments is passed.
Graph constructor expects following arguments:
constructor(
sourceIterable?: Iterable<[string, GraphNodeInterface<NodeData>]>,
config?: GraphConfig<NodeData>,
)
Where GraphConfig:
interface GraphConfig<NodeData = void> {
levelingStepFunction?: LevelingStepFunction<NodeData>,
pathFinder?: PathBuilder<NodeData>,
}
type LevelingStepFunction<NodeData = void> = (
currentNode: GraphNodeInterface<NodeData>,
sourceGraph: GraphInterface<NodeData>,
) => GraphNodeInterface<NodeData> | null;
type PathBuilder<NodeData = void> = (
startNode: GraphNodeInterface<NodeData>,
endNode: GraphNodeInterface<NodeData>,
sourceGraph: GraphInterface<NodeData>,
) => GraphNodeInterface<NodeData>[];
Graph constructor utilizes addNode method under the hood: all nodes from sourceIterable are added keeping their input order. Particularly, if some input node references to inexistent node or has some other wrong data, error will be thrown
BE ADVISED!!! Graph constructor, as well as Tree constructor, as well as both classes addNode methods mutate their arguments.
If no constructor arguments are specified, empty graph instance is created.
Soon graph instance was created, nodes can be added to it via addMethod:
addNode(nodeToAdd: GraphNodeInterface<NodeData>)
addNode adds a node to graph instance and all specified within that node edges.
Any node of graph instance can be retrieved using getNode method:
getNode(nodeId: string)
To retrieve nodeData you can use method getNodeData, which expects target node id as an argument.
To check whether graph possesses a node, use hasNode method (expects node id as an argument).
To remove some specific node, use removeNode method:
removeNode(nodeId: string)
If graph is needed to be flattened, use flatten method. It returns an iterable, which can be used to convert a graph instance into array.
To find path between two specific nodes of given graph, use buildPath method.
flatten and builtPath have own peculiarities: both can be overridden, but only flatten has default behavior - it returns nodes field values iterable if no custom flatten procedure is specified.
To override flatten procedure, use setFlattenProcedure method.
Example:
const levelingStepFunction: LevelingStepFunction<unknown> = (
currentNode: GraphNodeInterface<unknown>,
graph: GraphInterface<unknown>,
) => {
const {
outgoingBoundaries,
} = currentNode;
const firstOutgoingBoundary = [...outgoingBoundaries]?.[0]?.[0];
return graph.getNode(firstOutgoingBoundary) || null;
};
const graph = new Graph();
graph.setFlattenProcedure(levelingStepFunction);
To initialize or override buildPath method, call setPathFinderProcedure method.
Example:
const pathFinder: PathBuilder<unknown> = (
startNode: GraphNodeInterface<unknown>,
endNode: GraphNodeInterface<unknown>,
graph: GraphInterface<unknown>,
) => {
let currentNode: GraphNodeInterface<unknown> = startNode;
const result: GraphNodeInterface<unknown>[] = [];
do {
result.push(currentNode);
currentNode = graph.getNode([...currentNode.outgoingBoundaries]?.[0]?.[0])!;
} while(result[result.length - 1]?.nodeId !== endNode.nodeId)
return result;
};
const graph = new Graph();
graph.setPathFinderProcedure(pathFinder);
buildPath call with no initialization triggers an error.
Tree abstract class
Import Tree class:
import {Tree} from '@vyacheslav97/ct';
Tree abstract class extends Graph abstract class. This extension brings in following mutations:
-
addNode:
addNode(nodeToAdd: GraphNodeInterface<NodeData>)
-
removeNode:
removeNode(nodeId: string, deleteSubTree = true)
-
rootNodeId added - it contains tree root node id
-
nodeData field becomes mandatory (but remains extensible): it has to contain parentNodeId field for each tree node except root one
-
clear method calls Graph class clear method and sets rootNodeId = '' (empty string)
-
Tree constructor adds nodes using its addNode method
-
Each leaf node now has isLeaf nodeData property, set to true. It is handled automatically, so no needs to mess with it manually.
Mutations history template class
MutationsHistory template class import:
import {MutationsHistory} from '@vyacheslav97/ct';
MutationsHistory implements changes history abstract mechanism. It is described via following interface:
interface MutationsHistoryInterface<HistoryUnitType> {
lastSavedChangeIndex: number,
commitedMutations: HistoryUnitType[],
canceledMutations: HistoryUnitType[],
getCurrentMutation: () => HistoryUnitType | undefined,
addMutation: (mutation: HistoryUnitType) => void,
cancelLastMutation: () => void,
restoreCanceledMutation: () => void,
saveCurrentMutation: () => void,
backToSavedMutation: () => void,
saveAndClear: () => void,
clearHistory: () => void,
}
MutationshHistory API
Data:
- commitedMutations stack - stack of committed changes
- canceledMutations stack - stack of canceled changes
- lastSavedChangeIndex - index of last saved change
Methods:
- getCurrentMutation - returns the most recent committed change
- addMutation - adds a fresh mutation to commitedMutations stack. Devastates filled canceledMutations stack
- cancelLastMutation - removes the most recent mutation from commitedMutations stack and places it in canceledMutations stack
- restoreCanceledMutation - if there are canceled mutations, then removes fresh mutation from canceledMutations stack and moves it to commitedMutations stack
- saveCurrentMutation - updates lastSavedChangeIndex with commitedMutations.length - 1
- backToSavedMutation - cancels or restores all mutations up to the last saved
- saveAndClear - saves the most recent commited mutation and demolishes the rest (canceled inclusive)
- clearHistory - clears history
A MutationsHistory instance can be created using predefined committed history list via MutationsHistory class constructor:
constructor(sourceIterable?: Iterable<HistoryUnitType>) {
this.commitedMutations = sourceIterable ? [...sourceIterable]: [];
this.canceledMutations = [];
this.lastSavedChangeIndex = this.commitedMutations.length - 1;
}
MutationsHistory is able to store whatever you plan it to: from changed objects itself to actions that mutate certain data structure. But a whole template, that rules history rules, remains the same.
Binary search procedure template
Binary search template procedure import:
import {
binarySearch,
buildBinarySearch,
} from '@vyacheslav97/ct';
NOTE: it is highly recommended to prefer buildBinarySearch over binarySearch.
binarySearch implements well-known binary search algorithm for array of arbitrary content. It receives:
-
sourceList - list of data for binary search (list cotains data of the same arbitrary type)
-
target - binary search target
-
comparator - function that compares array elements and has a result described via following interface:
interface ComparatorResult {
equal?: boolean,
less?: boolean,
greater?: boolean,
}
binarySearch returns target index, if target does exist on given array or undefined otherwise.
buildBinarySearch creates binarySearch instance with a given comparator:
const numbersComparator = (x: number, y: number): ComparatorResult => ({
less: x < y,
greater: x > y,
equal: x === y,
});
const numericalBinarySearch = buildBinarySearch(numbersComparator);
numericalBinarySearch([], 5);
numericalBinarySearch([1, 2, 3, 4, 5], 2);
Duplicates search procedure template
Duplicates search procedure template import:
import {
duplicatesSearcher,
buildDuplicatesSearcher,
} from '@vyacheslav97/ct';
duplicatesSearcher function looks for duplicates within a given array of data. It accepts following arguments:
-
source – duplications source iterable
-
valueExtractor – optional function, derives a primitive according which two values are considered as duplicates
-
Returns Map, which keys are valueExtractor values, values are lists of DuplicateData:
interface DuplicateData<T> {
duplicateIndex: number;
duplicateValue: T;
}
Note: duplicatesSearcher returns empty Map only if source iterable is empty, otherwise it has maximum size equal to source iterable values amount.
buildDuplicatesSearcher builds a duplicatesSearcher instance with a given valueExtractor:
interface SimpleObjectData {
value: string,
}
const valueExtractor = (data: SimpleObjectData) => data.value;
const numericalDuplicatesSearcher = buildDuplicatesSearcher<number>();
numericalDuplicatesSearcher([1, 2, 3, 4, 5]);
export const objectDuplicatesSearcher = buildDuplicatesSearcher(valueExtractor);
objectDuplicatesSearcher([
{
value: 'a',
},
{
value: 'a',
},
{
value: 'c',
},
]);
isNumber
isNumber validator import:
import isNumber from '@vyacheslav97/ct';
isNumber represents a simple method to check whether its argument actually number or not. It based on two consequent test:
- Ensures, that typeof input argument is number
- Examines result of Number.isNaN call on input argument
Decimal integer RegExp
Regular expressions for decimal integers testing import:
import {
integerNumberRegex,
unsignedIntegerNumberRegex,
negativeIntegerNumberRegex,
} from '@vyacheslav97/ct';
All expressions are aimed to detangle whether a whole given string is an integer or not.
Particularly:
- integerNumberRegex checks, whether an input string has signed or unsigned integer number: string optionally starts with a single + or -, followed by digits sequence, which starts with any digits except 0, or single usingned 0
- unsignedIntegerNumberRegex almost copies integerNumberRegex, but start signs are not allowed
- negativeIntegerNumberRegex almost copies integerNumberRegex, but start sign must be -
Decimal float RegExp
Regular expressions for decimal floats testing import:
import {
floatNumberRegex,
unsignedFloatNumberRegex,
negativeFloatingPointNumberRegex,
} from '@vyacheslav97/ct';
All expressions are aimed to detangle whether a whole given string is a float number or not.
Particularly:
- floatNumberRegex checks, whether an input string has signed or unsigned float number: string optionally starts with a single + or -, followed by digits sequence, which first digit can't be 0, or single 0 with no sign. All this stuff is accompanied with optional single dot and that dot has optional digits sequence. So, following numbers are correct: 0, 0., 0.241253, -1.24, 3, -5, +12 .
- unsignedFloatNumberRegex resembles floatNumberRegex, but denies starting sign, positive or negative
- negativeFloatingPointNumberRegex mimics floatNumberRegex, but requires - as a starting sign
Exponential format number RegExp
Regular expression for number of exponential format testing import:
import {
exponentialNumberFormatRegex,
} from '@vyacheslav97/ct';
This expression is aimed to detangle whether an entire given string is a number of exponential format or not.
exponentialNumberFormatRegex unites three checks:
- e or E separator between right part and left part of number
- right part must be a decimal integer number with optional - sign
- left part must be a decimal float number with optional sign