data-structure-typed
Version:
Standard data structure
506 lines (492 loc) • 15.6 kB
JavaScript
import * as path from 'path';
import * as fs from 'fs';
import fastGlob from 'fast-glob';
import { fileURLToPath } from 'url';
const isNumber = (value) => {
return typeof value === 'number';
};
const isString = (value) => {
return typeof value === 'string';
};
const isBoolean = (value) => {
return typeof value === 'boolean';
};
const isDate = (value) => {
return value instanceof Date;
};
const isNull = (value) => {
return value === null;
};
const isUndefined = (value) => {
return typeof value === 'undefined';
};
const isFunction = (value) => {
return typeof value === 'function';
};
const isObject = (value) => {
return typeof value === 'object';
};
const isArray = (value) => {
return Array.isArray(value);
};
const isEqual = (objA, objB) => {
if (objA === objB) {
return true;
}
if (typeof objA !== 'object' || typeof objB !== 'object' || objA === null || objB === null) {
return false;
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
for (const key of keysA) {
if (!keysB.includes(key)) {
return false;
}
if (!isEqual(objA[key], objB[key])) {
return false;
}
}
return true;
};
function toggleJS(options) {
if (options === null || options === void 0 ? void 0 : options.plainHtml) {
return '';
}
else {
return 'onclick="json-to-html.toggleVisibility(this);return false"';
}
}
function makeLabelDiv(options, level, keyName, datatype) {
if (typeof keyName === 'number') {
return `<div class='index'><span class='json-to-html-label'>${keyName} </span></div>`;
}
else if (typeof keyName === 'string') {
if (datatype === 'array') {
return `<div class='collapsible level${level}' ${toggleJS(options)}><span class='json-to-html-label'>${keyName}</span></div>`;
}
else if (datatype === 'object') {
return `<div class='attribute collapsible level${level}' ${toggleJS(options)}><span class='json-to-html-label'>${keyName}:</span></div>`;
}
else {
return `<div class='leaf level${level}'><span class='json-to-html-label'>${keyName}:</span></div>`;
}
}
else {
return '';
}
}
function getContentClass(keyName) {
if (typeof keyName === 'string') {
return 'content';
}
else {
return '';
}
}
function isPlainObject(val) {
let lastKey;
let lastOwnKey;
for (const key in val) {
if (val.hasOwnProperty(key)) {
lastOwnKey = key;
}
}
for (const key in val) {
lastKey = key;
}
return lastOwnKey === lastKey;
}
function isLeafValue(val) {
return (isNumber(val) ||
isString(val) ||
isBoolean(val) ||
isDate(val) ||
isNull(val) ||
isUndefined(val) ||
isNaN(val) ||
isFunction(val) ||
!isPlainObject(val));
}
function isLeafObject(obj) {
if (!isObject(obj)) {
return false;
}
for (const key in obj) {
const val = obj[key];
if (!isLeafValue(val)) {
return false;
}
}
return true;
}
function isTable(arr) {
if (!isArray(arr)) {
return false;
}
if (arr.length === 0 || !isObject(arr[0])) {
return false;
}
else {
let nonCompliant = arr.find(row => !isLeafObject(row));
if (nonCompliant) {
return false;
}
else {
const cols = Object.keys(arr[0]);
nonCompliant = arr.find((row) => !isEqual(cols, Object.keys(row)));
return !nonCompliant;
}
}
}
function drawTable(arr) {
function drawRow(headers, rowObj) {
return '<td>' + headers.map(header => rowObj[header]).join('</td><td>') + '</td>';
}
const cols = Object.keys(arr[0]);
const content = arr.map(rowObj => drawRow(cols, rowObj));
const headingHtml = '<tr><th>' + cols.join('</th><th>') + '</th></tr>';
const contentHtml = '<tr>' + content.join('</tr><tr>') + '</tr>';
return '<table style="display: table; width:100%; table-layout: fixed;">' + headingHtml + contentHtml + '</table>';
}
function _render(name, data, options, level, altRow) {
const contentClass = getContentClass(name);
if (isArray(data)) {
const title = makeLabelDiv(options, level, `${name}`, 'array');
let subs;
if (isTable(data)) {
subs = drawTable(data);
}
else {
subs =
"<div class='altRows'>" +
data
.map((val, idx) => _render(idx.toString(), val, options, level + 1, idx % 2))
.join("</div><div class='altRows'>") +
'</div>';
}
return `<div class="json-to-html-collapse clearfix ${altRow}">
${title}
<div class="${contentClass}">${subs}</div>
</div>`;
}
else if (isLeafValue(data)) {
const title = makeLabelDiv(options, level, name);
if (isFunction(data)) {
return `${title}<span class='json-to-html-value'> -function() can't _render-</span>`;
}
else if (!isPlainObject(data)) {
if (isFunction(data.toString)) {
return `${title}<span class='json-to-html-value'> ${data.toString()}</span>`;
}
else {
return `${title}<span class='json-to-html-value'> -instance object, can't render-</span>`;
}
}
else {
return `${title}<span class='json-to-html-value'> ${data}</span>`;
}
}
else {
const title = makeLabelDiv(options, level, name, 'object');
let count = 0;
const subs = '<div>' +
Object.entries(data)
.map(([key, val]) => _render(key, val, options, level + 1, count++ % 2))
.join('</div><div>') +
'</div>';
const inner = `<div class="json-to-html-expand clearfix ${altRow}">
${title}
<div class="${contentClass}">${subs}</div>
</div>`;
return `${level === 0 ? "<div id='json-to-html'>" : ''}
${inner}
${level === 0 ? '</div>' : ''}`;
}
}
export function render(name, json, options) {
return `${_render(name, json, options, 0, 0)}`;
}
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
function numberFix(num, decimalPlaces) {
if (num > 10000 || num < 0.001) {
const [mantissa, exponent] = num.toExponential().split('e');
const formattedMantissa = Number(mantissa).toFixed(decimalPlaces);
return `${formattedMantissa}e${exponent}`;
} else {
return num.toFixed(decimalPlaces);
}
}
const ConsoleColor = {
END: '\x1b[0m',
BOLD: '\x1b[1m',
DIM: '\x1b[2m',
ITALIC: '\x1b[3m',
UNDERLINE: '\x1b[4m',
INVERSE: '\x1b[7m',
STRIKETHROUGH: '\x1b[9m',
NO_BOLD: '\x1b[22m',
NO_ITALIC: '\x1b[23m',
NO_UNDERLINE: '\x1b[24m',
NO_INVERSE: '\x1b[27m',
NO_STRIKETHROUGH: '\x1b[29m',
BLACK: '\x1b[30m',
RED: '\x1b[31m',
GREEN: '\x1b[32m',
YELLOW: '\x1b[33m',
BLUE: '\x1b[34m',
MAGENTA: '\x1b[35m',
GRAY: '\x1b[90m',
CYAN: '\x1b[36m',
WHITE: '\x1b[37m',
BG_BLACK: '\x1b[40m',
BG_RED: '\x1b[41m',
BG_GREEN: '\x1b[42m',
BG_YELLOW: '\x1b[43m',
BG_BLUE: '\x1b[44m',
BG_MAGENTA: '\x1b[45m',
BG_CYAN: '\x1b[46m',
BG_WHITE: '\x1b[47m'
};
const args = process.argv.slice(2);
const { GREEN, BOLD, END, YELLOW, GRAY, CYAN, BG_YELLOW } = ConsoleColor;
const isOnlyOrdered = true;
const runOrder = [
'heap',
'avl-tree',
'red-black-tree',
'doubly-linked-list',
'directed-graph',
'queue',
'deque',
'hash-map',
'trie',
'stack'
// 'singly-linked-list',
// 'priority-queue',
// 'binary-tree-overall'
];
const getRelativePath = file => {
return path.relative(__dirname, file);
};
const coloredLabeled = (label, file) => {
const relativeFilePath = getRelativePath(file);
const directory = path.dirname(relativeFilePath);
const fileName = path.basename(relativeFilePath);
return `${BG_YELLOW} ${label} ${END} ${GRAY}${directory}/${END}${CYAN}${fileName}${END}`;
};
const parentDirectory = path.resolve(__dirname, '../..');
const reportDistPath = path.join(parentDirectory, 'benchmark');
const testDir = path.join(__dirname, 'data-structures');
let allFiles = fastGlob.sync(path.join(testDir, '**', '*.test.mjs'));
let testFiles;
let isIndividual = false;
if (args.length > 0) {
console.log(`arguments: ${args.join(' ')}`);
testFiles = allFiles.filter(file => args.every(word => file.includes(word)));
isIndividual = true;
console.log(
`${testFiles.map(file => coloredLabeled('Found', file)).join(`
`)}`
);
} else {
isIndividual = false;
testFiles = allFiles;
}
const report = {};
let completedCount = 0;
const performanceTests = [];
for (const file of testFiles) {
const testName = path.basename(file, '.test.mjs');
const testFunction = await import(file);
const { suite } = testFunction;
if (suite)
performanceTests.push({
testName,
suite,
file
});
}
const composeReport = () => {
if (!fs.existsSync(reportDistPath))
fs.mkdirSync(reportDistPath, {
recursive: true
});
const filePath = path.join(reportDistPath, 'report.json');
const htmlFilePath = path.join(reportDistPath, 'report.html');
fs.writeFileSync(filePath, JSON.stringify(report, null, 2));
let html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>performance of data-structure-typed</title>
<style>
*{
box-sizing: border-box;
}
#json-to-html {
padding: 0 10px 20px;
}
.json-to-html-label {
font-size: 2rem;
margin: 2rem 0 0 3px;
}
.content table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
margin-top: 10px;
font-size: 16px;
}
.content table th,
.content table td {
padding: 8px 12px;
text-align: left;
border: 1px solid #ddd;
}
.content table th {
background-color: #f2f2f2;
font-weight: bold;
}
.content table tr:nth-child(odd) {
background-color: #ffffff;
}
</style>
</head>
<body>
<div id="json-to-html">`;
let htmlTables = '';
for (const r in report) {
if (report.hasOwnProperty(r)) {
htmlTables += render(report[r].testName, report[r].benchmarks, {
plainHtml: true,
'<>': 'table',
html: [
{
'<>': 'tr',
html: [
{
'<>': 'td',
html: '${name}'
},
{
'<>': 'td',
html: '${periodMS}'
},
{
'<>': 'td',
html: '${mean}'
}
]
}
]
});
}
}
htmlTables += `
`;
html += htmlTables;
html += `</div>
</body>
</html>`;
if (!isIndividual)
replaceMarkdownContent(
'[//]: # (No deletion!!! Start of Replace Section)', // Start tag
'[//]: # (No deletion!!! End of Replace Section)', // end identifier
htmlTables // New content to be inserted
);
fs.writeFileSync(htmlFilePath, html);
console.log(`Performance ${BOLD}${GREEN}report${END} file generated in file://${BOLD}${GREEN}${htmlFilePath}${END}`);
};
function replaceMarkdownContent(startMarker, endMarker, newText) {
const filePath = path.join(parentDirectory, 'README.md'); // Path to README.md file
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) {
console.error(`Unable to read ${filePath}:`, err);
return;
}
// Find the start and end markers in the content
const startIndex = data.indexOf(startMarker);
const endIndex = data.indexOf(endMarker, startIndex + 1);
if (startIndex === -1 || endIndex === -1) {
console.error('Unable to find start or end marker');
return;
}
// Replace the old content with the new text
const updatedMarkdown = data.slice(0, startIndex + startMarker.length) + '\n' + newText + data.slice(endIndex);
// Try writing the modified content back to the file
fs.writeFile(filePath, updatedMarkdown, 'utf8', err => {
if (err) {
console.error(`Unable to write to ${filePath}:`, err);
} else {
console.log(`The content has been successfully replaced in file://${BOLD}${GREEN}${filePath}${END}`);
}
});
});
}
const sortedPerformanceTests = (
isOnlyOrdered ? [...performanceTests].filter(test => runOrder.includes(test.testName)) : [...performanceTests]
).sort((a, b) => {
const indexA = runOrder.indexOf(a.testName);
const indexB = runOrder.indexOf(b.testName);
// If both a and b are in the runOrder, sort them according to their indices in the runOrder.
if (indexA !== -1 && indexB !== -1) {
return indexA - indexB;
}
// If there is only 'a' in the runOrder, then place 'b' in front.
if (indexA !== -1) {
return 1;
}
// If only b is in the runOrder, then a should be placed before it.
if (indexB !== -1) {
return -1;
}
// If neither a nor b are in runOrder, keep their original runOrder
return 0;
});
console.log(
`${GREEN} Matched Suites (${performanceTests.length})${END}: ${performanceTests.map(test => test.testName)}`
);
console.log(
`${GREEN} Running Suites (${sortedPerformanceTests.length})${END}: ${sortedPerformanceTests.map(test => test.testName)}`
);
sortedPerformanceTests.forEach(item => {
const { suite, testName, file } = item;
console.log(coloredLabeled('Running', file));
if (suite) {
let runTime = 0;
suite
.on('complete', function () {
completedCount++;
report[testName] = {};
report[testName].benchmarks = this.map(benchmark => {
runTime += benchmark.times.elapsed;
return {
'test name': benchmark.name,
'time taken (ms)': numberFix(benchmark.times.period * 1000, 2),
// 'executions per sec': numberFix(benchmark.hz, 2),
// 'executed times': numberFix(benchmark.count, 0),
'sample mean (secs)': numberFix(benchmark.stats.mean, 2),
'sample deviation': numberFix(benchmark.stats.deviation, 2)
};
});
report[testName].testName = testName;
const isDone = completedCount === sortedPerformanceTests.length;
runTime = Number(runTime.toFixed(2));
const isTimeWarn = runTime > 120;
console.log(
// `Files: ${GREEN}${testFileCount}${END} `,
// `Suites: ${GREEN}${performanceTests.length}${END} `,
`Suites Progress: ${isDone ? GREEN : YELLOW}${completedCount}${END}/${isDone ? GREEN : YELLOW}${sortedPerformanceTests.length}${END}`,
`Time Costs: ${isTimeWarn ? YELLOW : GREEN}${runTime}s${END}`
);
if (isDone) {
composeReport();
}
})
.run({ async: false });
}
});