chore: Implement unit tests as example code in the README.md and source code comments.

This commit is contained in:
Revone 2024-11-19 23:20:25 +13:00
parent 476395ef72
commit 0ff5ddc410
7 changed files with 474 additions and 33 deletions

View file

@ -8,7 +8,7 @@ All notable changes to this project will be documented in this file.
- [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
- [`auto-changelog`](https://github.com/CookPete/auto-changelog)
## [v1.53.1](https://github.com/zrwusa/data-structure-typed/compare/v1.51.5...main) (upcoming)
## [v1.53.2](https://github.com/zrwusa/data-structure-typed/compare/v1.51.5...main) (upcoming)
### Changes

View file

@ -798,6 +798,12 @@ Array.from(dijkstraResult?.seen ?? []).map(vertex => vertex.key) // ['A', 'B', '
```
[//]: # (No deletion!!! Start of Example Replace Section)
[//]: # (No deletion!!! End of Example Replace Section)
## API docs & Examples
[API Docs](https://data-structure-typed-docs.vercel.app)

52
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "data-structure-typed",
"version": "1.53.1",
"version": "1.53.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "data-structure-typed",
"version": "1.53.1",
"version": "1.53.2",
"license": "MIT",
"devDependencies": {
"@eslint/compat": "^1.2.2",
@ -19,11 +19,11 @@
"@typescript-eslint/eslint-plugin": "^8.12.1",
"@typescript-eslint/parser": "^8.12.1",
"auto-changelog": "^2.5.0",
"avl-tree-typed": "^1.53.0",
"avl-tree-typed": "^1.53.1",
"benchmark": "^2.1.4",
"binary-tree-typed": "^1.53.0",
"bst-typed": "^1.53.0",
"data-structure-typed": "^1.53.0",
"binary-tree-typed": "^1.53.1",
"bst-typed": "^1.53.1",
"data-structure-typed": "^1.53.1",
"dependency-cruiser": "^16.5.0",
"doctoc": "^2.2.1",
"eslint": "^9.13.0",
@ -32,7 +32,7 @@
"eslint-import-resolver-typescript": "^3.6.3",
"eslint-plugin-import": "^2.31.0",
"fast-glob": "^3.3.2",
"heap-typed": "^1.53.0",
"heap-typed": "^1.53.1",
"istanbul-badges-readme": "^1.9.0",
"jest": "^29.7.0",
"js-sdsl": "^4.4.2",
@ -3437,13 +3437,13 @@
}
},
"node_modules/avl-tree-typed": {
"version": "1.53.0",
"resolved": "https://registry.npmjs.org/avl-tree-typed/-/avl-tree-typed-1.53.0.tgz",
"integrity": "sha512-SVk6zEvzrB2GhHVFOhVO2yaB7p9iLGy2aml0j7UW1rWp+VACw4SZQhKuldl2qC7ECb9AOIbajD7Y9ott8Qfy8Q==",
"version": "1.53.1",
"resolved": "https://registry.npmjs.org/avl-tree-typed/-/avl-tree-typed-1.53.1.tgz",
"integrity": "sha512-4k54zM7lNtz37FSgNstoGlRSqN9kO1y98Lz7+8RXAoOtYe1McIUyJYhuukOFJi6XshEQDFHaxz/zklZJFqLq/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"data-structure-typed": "^1.53.0"
"data-structure-typed": "^1.53.1"
}
},
"node_modules/babel-jest": {
@ -3602,13 +3602,13 @@
}
},
"node_modules/binary-tree-typed": {
"version": "1.53.0",
"resolved": "https://registry.npmjs.org/binary-tree-typed/-/binary-tree-typed-1.53.0.tgz",
"integrity": "sha512-5EmlwbR4Kaphual+wF2rJwpZFhhK2u6zOFiA+yagWDbH6tzBCojT5o2DN+1tmW1Pj6bGJxz7PHcDBIdBkQruMw==",
"version": "1.53.1",
"resolved": "https://registry.npmjs.org/binary-tree-typed/-/binary-tree-typed-1.53.1.tgz",
"integrity": "sha512-Ym/VfNG8iRhBUxxlCFArQSbf46m39voLVXdKbLzP/G3Sb3N7xwSwMTRC2Evx9lEWnhMmg4WHXsqZByun2xIljw==",
"dev": true,
"license": "MIT",
"dependencies": {
"data-structure-typed": "^1.53.0"
"data-structure-typed": "^1.53.1"
}
},
"node_modules/brace-expansion": {
@ -3691,13 +3691,13 @@
}
},
"node_modules/bst-typed": {
"version": "1.53.0",
"resolved": "https://registry.npmjs.org/bst-typed/-/bst-typed-1.53.0.tgz",
"integrity": "sha512-f1f+RlbIA6J2reFO/DwLRLaZVBmMNNyKCi5eWd+r5ojV9bDC/IZ34nzID0zSJUwVqtAtIpaK9x63XAo42+5SoQ==",
"version": "1.53.1",
"resolved": "https://registry.npmjs.org/bst-typed/-/bst-typed-1.53.1.tgz",
"integrity": "sha512-pSAIStgqneF4kTbbJJU7tcTMUBvchUXoID9fpKmcpsLHzG0jgfmMnGYZBZlRQXYJkUAsiFHeDaDMbK6W2muBQA==",
"dev": true,
"license": "MIT",
"dependencies": {
"data-structure-typed": "^1.53.0"
"data-structure-typed": "^1.53.1"
}
},
"node_modules/buffer-from": {
@ -4069,9 +4069,9 @@
}
},
"node_modules/data-structure-typed": {
"version": "1.53.0",
"resolved": "https://registry.npmjs.org/data-structure-typed/-/data-structure-typed-1.53.0.tgz",
"integrity": "sha512-bukacSwbWyypNTdQCt51rwNcqM1hacHG6mMJ2RDBqsz8jJ5N4oMOgQhDyA1GkvMvdq83mcj9ilCFZ6lDcwShqA==",
"version": "1.53.1",
"resolved": "https://registry.npmjs.org/data-structure-typed/-/data-structure-typed-1.53.1.tgz",
"integrity": "sha512-D3KDDnf6zY7vW45ht/U467UCVx1Zs6E3uVlGe4HSa+qvMHtPZMZahU5X8HbQVfzHZ1wg7NBKW6OtQdy0YzbuSQ==",
"dev": true,
"license": "MIT"
},
@ -5946,13 +5946,13 @@
}
},
"node_modules/heap-typed": {
"version": "1.53.0",
"resolved": "https://registry.npmjs.org/heap-typed/-/heap-typed-1.53.0.tgz",
"integrity": "sha512-MtbVdNGb2mvLrv67j0ajaMDr/u0HpR0tMaCj1MUrmXKK+IhVqniq7I8ADUlDrEPpKXwES8RBa0v7lA5umKR9hg==",
"version": "1.53.1",
"resolved": "https://registry.npmjs.org/heap-typed/-/heap-typed-1.53.1.tgz",
"integrity": "sha512-4/fHFXV3YLC0wvi+H3IXTRF4t2CzU1AcaB8PZvU/vR5V7lTPHNAb9IUGj9bC1f/7tmz/3a38r1Mx0xJVPIhzaw==",
"dev": true,
"license": "MIT",
"dependencies": {
"data-structure-typed": "^1.53.0"
"data-structure-typed": "^1.53.1"
}
},
"node_modules/html-escaper": {

View file

@ -69,11 +69,11 @@
"@typescript-eslint/eslint-plugin": "^8.12.1",
"@typescript-eslint/parser": "^8.12.1",
"auto-changelog": "^2.5.0",
"avl-tree-typed": "^1.53.0",
"avl-tree-typed": "^1.53.1",
"benchmark": "^2.1.4",
"binary-tree-typed": "^1.53.0",
"bst-typed": "^1.53.0",
"data-structure-typed": "^1.53.0",
"binary-tree-typed": "^1.53.1",
"bst-typed": "^1.53.1",
"data-structure-typed": "^1.53.1",
"dependency-cruiser": "^16.5.0",
"doctoc": "^2.2.1",
"eslint": "^9.13.0",
@ -82,7 +82,7 @@
"eslint-import-resolver-typescript": "^3.6.3",
"eslint-plugin-import": "^2.31.0",
"fast-glob": "^3.3.2",
"heap-typed": "^1.53.0",
"heap-typed": "^1.53.1",
"istanbul-badges-readme": "^1.9.0",
"jest": "^29.7.0",
"js-sdsl": "^4.4.2",

View file

@ -457,3 +457,178 @@ describe('FibonacciHeap Stress Test', () => {
);
});
});
describe('classic use', () => {
it('heap sort', () => {
function heapSort(arr: number[]): number[] {
const heap = new Heap<number>(arr, { comparator: (a, b) => a - b });
const sorted: number[] = [];
while (!heap.isEmpty()) {
sorted.push(heap.poll()!); // Poll minimum element
}
return sorted;
}
const array = [5, 3, 8, 4, 1, 2];
expect(heapSort(array)).toEqual([1, 2, 3, 4, 5, 8]);
});
it('top k', () => {
function topKElements(arr: number[], k: number): number[] {
const heap = new Heap<number>([], { comparator: (a, b) => b - a }); // Max heap
arr.forEach(num => {
heap.add(num);
if (heap.size > k) heap.poll(); // Keep the heap size at K
});
return heap.toArray();
}
const numbers = [10, 30, 20, 5, 15, 25];
console.log('Top K:', topKElements(numbers, 3)); // [15, 10, 5]
expect(topKElements(numbers, 3)).toEqual([15, 10, 5]);
});
it('merge sorted sequences', () => {
function mergeSortedSequences(sequences: number[][]): number[] {
const heap = new Heap<{ value: number; seqIndex: number; itemIndex: number }>([], {
comparator: (a, b) => a.value - b.value // Min heap
});
// Initialize heap
sequences.forEach((seq, seqIndex) => {
if (seq.length) {
heap.add({ value: seq[0], seqIndex, itemIndex: 0 });
}
});
const merged: number[] = [];
while (!heap.isEmpty()) {
const { value, seqIndex, itemIndex } = heap.poll()!;
merged.push(value);
if (itemIndex + 1 < sequences[seqIndex].length) {
heap.add({
value: sequences[seqIndex][itemIndex + 1],
seqIndex,
itemIndex: itemIndex + 1
});
}
}
return merged;
}
const sequences = [
[1, 4, 7],
[2, 5, 8],
[3, 6, 9]
];
console.log('Merged Sequences:', mergeSortedSequences(sequences)); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
expect(mergeSortedSequences(sequences)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
});
it('@example median finder', () => {
class MedianFinder {
private low: MaxHeap<number>; // Max heap, stores the smaller half
private high: MinHeap<number>; // Min heap, stores the larger half
constructor() {
this.low = new MaxHeap<number>([]);
this.high = new MinHeap<number>([]);
}
addNum(num: number): void {
if (this.low.isEmpty() || num <= this.low.peek()!) this.low.add(num);
else this.high.add(num);
// Balance heaps
if (this.low.size > this.high.size + 1) this.high.add(this.low.poll()!);
else if (this.high.size > this.low.size) this.low.add(this.high.poll()!);
}
findMedian(): number {
return this.low.peek()!;
}
}
const medianFinder = new MedianFinder();
medianFinder.addNum(10);
expect(medianFinder.findMedian()).toBe(10);
medianFinder.addNum(20);
expect(medianFinder.findMedian()).toBe(10);
medianFinder.addNum(30);
expect(medianFinder.findMedian()).toBe(20);
medianFinder.addNum(40);
expect(medianFinder.findMedian()).toBe(20);
medianFinder.addNum(50);
expect(medianFinder.findMedian()).toBe(30);
});
it('schedule tasks', () => {
type Task = [string, number];
function scheduleTasks(tasks: Task[], machines: number): Map<number, Task[]> {
const machineHeap = new Heap<{ id: number; load: number }>([], { comparator: (a, b) => a.load - b.load }); // Min heap
const allocation = new Map<number, Task[]>();
// Initialize the load on each machine
for (let i = 0; i < machines; i++) {
machineHeap.add({ id: i, load: 0 });
allocation.set(i, []);
}
// Assign tasks
tasks.forEach(([task, load]) => {
const machine = machineHeap.poll()!;
allocation.get(machine.id)!.push([task, load]);
machine.load += load;
machineHeap.add(machine); // The machine after updating the load is re-entered into the heap
});
return allocation;
}
const tasks: Task[] = [
['Task1', 3],
['Task2', 1],
['Task3', 2],
['Task4', 5],
['Task5', 4]
];
const expectedMap = new Map<number, Task[]>();
expectedMap.set(0, [
['Task1', 3],
['Task4', 5]
]);
expectedMap.set(1, [
['Task2', 1],
['Task3', 2],
['Task5', 4]
]);
expect(scheduleTasks(tasks, 2)).toEqual(expectedMap);
});
it('@example Use Heap for load balancing', () => {
function loadBalance(requests: number[], servers: number): number[] {
const serverHeap = new Heap<{ id: number; load: number }>([], { comparator: (a, b) => a.load - b.load }); // min heap
const serverLoads = new Array(servers).fill(0);
for (let i = 0; i < servers; i++) {
serverHeap.add({ id: i, load: 0 });
}
requests.forEach(req => {
const server = serverHeap.poll()!;
serverLoads[server.id] += req;
server.load += req;
serverHeap.add(server); // The server after updating the load is re-entered into the heap
});
return serverLoads;
}
const requests = [5, 2, 8, 3, 7];
const serversLoads = loadBalance(requests, 3);
expect(serversLoads).toEqual([12, 8, 5]);
});
});

View file

@ -1 +1,54 @@
export {};
/**
* Convert any string to CamelCase format
*/
export function toCamelCase(str: string): string {
return str
.toLowerCase()
.replace(/[^a-zA-Z0-9]+(.)/g, (_, chr) => chr.toUpperCase());
}
/**
* Convert any string to SnakeCase format
*/
export function toSnakeCase(str: string): string {
return str
.replace(/([a-z])([A-Z])/g, '$1_$2') // Add underline between lowercase and uppercase letters
.toLowerCase() // Convert to lowercase
.replace(/[^a-z0-9]+/g, '_'); // Replace non-alphanumeric characters with underscores
}
/**
* Convert any string to PascalCase format (first letter capitalized)
*/
export function toPascalCase(str: string): string {
return str
.replace(/([a-z])([A-Z])/g, '$1 $2') // Add space between lowercase and uppercase letters
.replace(/[^a-zA-Z0-9]+/g, ' ') // Replace non-alphanumeric characters with spaces
.split(' ') // Separate strings by spaces
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) // The first letter is capitalized, the rest are lowercase
.join(''); // Combine into a string
}
/**
* Convert CamelCase or SnakeCase string to string format with specified separator
*/
export function toSeparatedCase(str: string, separator: string = '_'): string {
return str
.replace(/([a-z0-9])([A-Z])/g, '$1' + separator + '$2')
.replace(/[_\s]+/g, separator)
.toLowerCase();
}
/**
* Convert the string to all uppercase and delimit it using the specified delimiter
*/
export function toUpperSeparatedCase(
str: string,
separator: string = '_',
): string {
return str
.toUpperCase() // Convert all letters to uppercase
.replace(/([a-z0-9])([A-Z])/g, '$1' + separator + '$2') // Add separator between lowercase letters and uppercase letters
.replace(/[^A-Z0-9]+/g, separator) // Replace non-alphanumeric characters with separators
.replace(new RegExp(`^${separator}|${separator}$`, 'g'), ''); // Remove the starting and ending separators
}

207
testToExample.ts Normal file
View file

@ -0,0 +1,207 @@
import fs from 'fs';
import path from 'path';
import * as ts from 'typescript';
import { toPascalCase } from './test/utils';
const isReplaceMD = false;
const START_MARKER = '[//]: # (No deletion!!! Start of Example Replace Section)';
const END_MARKER = '[//]: # (No deletion!!! End of Example Replace Section)';
/**
* Recursively retrieve all `.ts` files in a directory.
*/
function getAllTestFiles(dir: string): string[] {
const entries = fs.readdirSync(dir, { withFileTypes: true });
const files = entries
.filter(file => !file.isDirectory() && file.name.endsWith('.ts'))
.map(file => path.join(dir, file.name));
const directories = entries.filter(entry => entry.isDirectory());
for (const directory of directories) {
files.push(...getAllTestFiles(path.join(dir, directory.name)));
}
return files;
}
/**
* Extract test cases with `@example` from TypeScript files using AST.
*/
function extractExamplesFromFile(filePath: string): { name: string; body: string }[] {
const fileContent = fs.readFileSync(filePath, 'utf-8');
const sourceFile = ts.createSourceFile(filePath, fileContent, ts.ScriptTarget.Latest, true);
const examples: { name: string; body: string }[] = [];
function visit(node: ts.Node) {
if (
ts.isCallExpression(node) && // Ensure it's a function call
node.arguments.length >= 2 && // At least two arguments
ts.isStringLiteral(node.arguments[0]) && // First argument is a string
node.arguments[0].text.startsWith('@example') && // Matches @example
ts.isArrowFunction(node.arguments[1]) // Second argument is an arrow function
) {
const exampleName = node.arguments[0].text.replace('@example ', '').trim();
const bodyNode = node.arguments[1].body;
let exampleBody: string;
if (ts.isBlock(bodyNode)) {
// If it's a block, remove outer {}
exampleBody = bodyNode.statements
.map(stmt => stmt.getFullText(sourceFile))
.join('')
.trim();
} else {
// If it's a single expression, use it directly
exampleBody = bodyNode.getFullText(sourceFile).trim();
}
const transformedBody = exampleBody
.replace(
/expect\((.*?)\)\.(toEqual|toBe|toStrictEqual|toHaveLength|toMatchObject)\((.*?)\);/g,
(match, actual, method, expected) => {
return `console.log(${actual}); // ${expected.trim()}`;
}
)
.trim();
examples.push({ name: exampleName, body: transformedBody });
}
ts.forEachChild(node, visit);
}
visit(sourceFile);
return examples;
}
/**
* Add examples to the corresponding class in the source file.
*/
function addExamplesToSourceFile(
sourceFilePath: string,
className: string,
examples: { name: string; body: string }[]
): void {
if (!fs.existsSync(sourceFilePath)) {
console.warn(`Source file not found: ${sourceFilePath}`);
return;
}
const sourceContent = fs.readFileSync(sourceFilePath, 'utf-8');
const sourceFile = ts.createSourceFile(sourceFilePath, sourceContent, ts.ScriptTarget.Latest, true);
let updatedContent = sourceContent;
const classNode = sourceFile.statements.find(
stmt => ts.isClassDeclaration(stmt) && stmt.name?.text === className
) as ts.ClassDeclaration | undefined;
if (classNode) {
const classStart = classNode.getStart(sourceFile);
const classEnd = classNode.getEnd();
const classText = classNode.getFullText(sourceFile);
// 提取注释内容
const existingCommentMatch = classText.match(/\/\*\*([\s\S]*?)\*\//);
if (!existingCommentMatch) {
console.warn(`No existing comment found for class: ${className}`);
return;
}
const existingCommentInner = existingCommentMatch[1]; // 提取注释内容(不包括`/**`和`*/`
// 替换 @example 部分
const exampleSection = examples
.map(
example =>
`@example \n * \/\/ ${example.name} \n${example.body
.split('\n')
.map(line => ` * ${line}`)
.join('\n')}\n * \n`
)
.join('\n');
let newComment = '';
if (existingCommentInner.includes('@example')) {
newComment = existingCommentInner.replace(/@example[\s\S]*?(?=\*\/|$)/g, exampleSection);
} else {
newComment = existingCommentInner + `\n * ${exampleSection}`;
}
// 替换原始内容
updatedContent =
sourceContent.slice(0, classStart - existingCommentInner.length - 1) +
newComment +
classText.slice(existingCommentMatch[0].length).trim() +
sourceContent.slice(classEnd);
}
fs.writeFileSync(sourceFilePath, updatedContent, 'utf-8');
console.log(`Updated examples in ${sourceFilePath}`);
}
/**
* Process all test files and update README.md and source files.
*/
function updateExamples(testDir: string, readmePath: string, sourceBaseDir: string): void {
const testFiles = getAllTestFiles(testDir);
let allExamples: string[] = [];
for (const file of testFiles) {
const examples = extractExamplesFromFile(file);
if (examples.length === 0) {
console.log(`No @example found in test file: ${file}`);
continue;
}
const relativePath = path.relative(testDir, file);
const sourceFilePath = path.resolve(sourceBaseDir, relativePath.replace('.test.ts', '.ts'));
const className = path.basename(sourceFilePath, '.ts');
addExamplesToSourceFile(sourceFilePath, toPascalCase(className), examples);
allExamples = allExamples.concat(
examples.map(example => `### ${example.name}\n\`\`\`typescript\n${example.body}\n\`\`\``)
);
}
if (isReplaceMD && allExamples.length > 0) {
replaceExamplesInReadme(readmePath, allExamples);
}
}
/**
* Replace content between markers in README.md.
*/
function replaceExamplesInReadme(readmePath: string, newExamples: string[]): void {
const readmeContent = fs.readFileSync(readmePath, 'utf-8');
const startIdx = readmeContent.indexOf(START_MARKER);
const endIdx = readmeContent.indexOf(END_MARKER);
if (startIdx === -1 || endIdx === -1) {
throw new Error(`Markers not found in ${readmePath}`);
}
const before = readmeContent.slice(0, startIdx + START_MARKER.length);
const after = readmeContent.slice(endIdx);
const updatedContent = `${before}\n\n${newExamples.join('\n\n')}\n\n${after}`;
fs.writeFileSync(readmePath, updatedContent, 'utf-8');
console.log(`README.md updated with new examples.`);
}
// Run the script
const testDir = path.resolve(__dirname, 'test/unit');
const readmePath = path.resolve(__dirname, 'README.md');
const sourceBaseDir = path.resolve(__dirname, 'src');
updateExamples(testDir, readmePath, sourceBaseDir);