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 });
  }
});