data-structure-typed 2.4.0 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1 -1
- package/README.md +2 -3
- package/README_CN.md +0 -1
- package/benchmark/report.html +118 -40
- package/benchmark/report.json +726 -726
- package/dist/cjs/index.cjs +79 -21
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs-legacy/index.cjs +79 -21
- package/dist/cjs-legacy/index.cjs.map +1 -1
- package/dist/esm/index.mjs +79 -21
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm-legacy/index.mjs +79 -21
- package/dist/esm-legacy/index.mjs.map +1 -1
- package/dist/types/data-structures/binary-tree/tree-map.d.ts +17 -6
- package/dist/types/data-structures/binary-tree/tree-multi-map.d.ts +13 -5
- package/dist/types/data-structures/binary-tree/tree-multi-set.d.ts +12 -5
- package/dist/types/data-structures/binary-tree/tree-set.d.ts +15 -4
- package/dist/types/types/data-structures/binary-tree/tree-map.d.ts +6 -1
- package/dist/types/types/data-structures/binary-tree/tree-multi-set.d.ts +6 -1
- package/dist/types/types/data-structures/binary-tree/tree-set.d.ts +6 -1
- package/dist/umd/data-structure-typed.js +79 -21
- package/dist/umd/data-structure-typed.js.map +1 -1
- package/dist/umd/data-structure-typed.min.js +4 -4
- package/dist/umd/data-structure-typed.min.js.map +1 -1
- package/package.json +6 -2
- package/src/data-structures/binary-tree/tree-map.ts +35 -13
- package/src/data-structures/binary-tree/tree-multi-map.ts +41 -20
- package/src/data-structures/binary-tree/tree-multi-set.ts +17 -6
- package/src/data-structures/binary-tree/tree-set.ts +19 -6
- package/src/types/data-structures/binary-tree/tree-map.ts +7 -1
- package/src/types/data-structures/binary-tree/tree-multi-set.ts +7 -1
- package/src/types/data-structures/binary-tree/tree-set.ts +7 -1
- package/test/performance/reportor-enhanced.mjs +256 -100
- package/test/unit/data-structures/binary-tree/tree-map.test.ts +46 -0
- package/test/unit/data-structures/binary-tree/tree-multi-map.rfc.test.ts +47 -0
- package/test/unit/data-structures/binary-tree/tree-multi-set.test.ts +49 -0
- package/test/unit/data-structures/binary-tree/tree-set.test.ts +44 -0
|
@@ -612,40 +612,93 @@ function escapeRegex(str) {
|
|
|
612
612
|
}
|
|
613
613
|
|
|
614
614
|
/**
|
|
615
|
-
* Generate HTML report with
|
|
615
|
+
* Generate HTML report with unified comparison table (matching PERFORMANCE.md structure)
|
|
616
616
|
*/
|
|
617
617
|
function generateHtmlReport(report) {
|
|
618
618
|
const { javascript = [], native = [] } = report;
|
|
619
619
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
620
|
+
// Build C++ lookup map
|
|
621
|
+
const cppMap = new Map();
|
|
622
|
+
for (const nativeTest of native) {
|
|
623
|
+
const nativeTestName = nativeTest.testName;
|
|
624
|
+
for (const benchmark of nativeTest.benchmarks) {
|
|
625
|
+
const testCaseName = benchmark['Test Case'];
|
|
626
|
+
const cppValue = benchmark['Latency Avg (ms)'];
|
|
627
|
+
const normalizedCase = normalizeCaseName(testCaseName);
|
|
628
|
+
|
|
629
|
+
cppMap.set(`${nativeTestName}|${testCaseName}`, cppValue);
|
|
630
|
+
if (normalizedCase !== testCaseName) {
|
|
631
|
+
cppMap.set(`${nativeTestName}|${normalizedCase}`, cppValue);
|
|
632
|
+
}
|
|
633
|
+
cppMap.set(`${nativeTestName}|${formatNumberAbbr(testCaseName)}`, cppValue);
|
|
634
|
+
if (normalizedCase !== testCaseName) {
|
|
635
|
+
cppMap.set(`${nativeTestName}|${formatNumberAbbr(normalizedCase)}`, cppValue);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const ruleMappings = getNativeMappings(testCaseName, nativeTestName);
|
|
639
|
+
for (const mapping of ruleMappings) {
|
|
640
|
+
cppMap.set(`${nativeTestName}|${mapping}`, cppValue);
|
|
641
|
+
}
|
|
624
642
|
}
|
|
625
|
-
testGroups.get(test.testName).js = test;
|
|
626
643
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
644
|
+
|
|
645
|
+
// Group JS benchmarks by display name
|
|
646
|
+
const groups = new Map();
|
|
647
|
+
const testNameToDisplay = new Map();
|
|
648
|
+
|
|
649
|
+
for (const jsResult of javascript) {
|
|
650
|
+
const testName = jsResult.testName;
|
|
651
|
+
const displayName = testNameMap[testName] || testName;
|
|
652
|
+
testNameToDisplay.set(testName, displayName);
|
|
653
|
+
|
|
654
|
+
if (!groups.has(displayName)) {
|
|
655
|
+
groups.set(displayName, { testName, items: [] });
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
for (const benchmark of jsResult.benchmarks) {
|
|
659
|
+
groups.get(displayName).items.push({
|
|
660
|
+
testName: testName,
|
|
661
|
+
benchmark: benchmark
|
|
662
|
+
});
|
|
630
663
|
}
|
|
631
|
-
testGroups.get(test.testName).native = test;
|
|
632
664
|
}
|
|
633
665
|
|
|
666
|
+
// Sort groups by runner-config.json order
|
|
667
|
+
const orderConfig = loadOrderConfig();
|
|
668
|
+
const sortedDisplayNames = Array.from(groups.keys()).sort((a, b) => {
|
|
669
|
+
const getTestName = (displayName) => {
|
|
670
|
+
const group = groups.get(displayName);
|
|
671
|
+
return group?.testName || displayName.toLowerCase().replace(/\s+/g, '-');
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
const testNameA = getTestName(a);
|
|
675
|
+
const testNameB = getTestName(b);
|
|
676
|
+
|
|
677
|
+
const indexA = orderConfig.indexOf(testNameA);
|
|
678
|
+
const indexB = orderConfig.indexOf(testNameB);
|
|
679
|
+
|
|
680
|
+
if (indexA !== -1 && indexB !== -1) return indexA - indexB;
|
|
681
|
+
if (indexA !== -1) return -1;
|
|
682
|
+
if (indexB !== -1) return 1;
|
|
683
|
+
return a.localeCompare(b);
|
|
684
|
+
});
|
|
685
|
+
|
|
634
686
|
let html = `<!DOCTYPE html>
|
|
635
687
|
<html lang="en">
|
|
636
688
|
<head>
|
|
637
689
|
<meta charset="UTF-8">
|
|
638
690
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
639
|
-
<title>Performance Benchmark Report</title>
|
|
691
|
+
<title>Performance Benchmark Report - data-structure-typed</title>
|
|
640
692
|
<style>
|
|
641
693
|
body {
|
|
642
694
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
643
695
|
background: #f5f5f5;
|
|
644
696
|
color: #333;
|
|
645
697
|
padding: 20px;
|
|
698
|
+
line-height: 1.6;
|
|
646
699
|
}
|
|
647
700
|
.container {
|
|
648
|
-
max-width:
|
|
701
|
+
max-width: 1000px;
|
|
649
702
|
margin: 0 auto;
|
|
650
703
|
background: white;
|
|
651
704
|
border-radius: 8px;
|
|
@@ -661,8 +714,18 @@ function generateHtmlReport(report) {
|
|
|
661
714
|
.timestamp {
|
|
662
715
|
color: #7f8c8d;
|
|
663
716
|
font-size: 14px;
|
|
717
|
+
margin-bottom: 10px;
|
|
718
|
+
}
|
|
719
|
+
.back-link {
|
|
664
720
|
margin-bottom: 30px;
|
|
665
721
|
}
|
|
722
|
+
.back-link a {
|
|
723
|
+
color: #3498db;
|
|
724
|
+
text-decoration: none;
|
|
725
|
+
}
|
|
726
|
+
.back-link a:hover {
|
|
727
|
+
text-decoration: underline;
|
|
728
|
+
}
|
|
666
729
|
.summary {
|
|
667
730
|
background: #ecf0f1;
|
|
668
731
|
padding: 15px;
|
|
@@ -687,6 +750,33 @@ function generateHtmlReport(report) {
|
|
|
687
750
|
color: #2c3e50;
|
|
688
751
|
margin-top: 5px;
|
|
689
752
|
}
|
|
753
|
+
.toc {
|
|
754
|
+
background: #f9f9f9;
|
|
755
|
+
padding: 20px;
|
|
756
|
+
border-radius: 6px;
|
|
757
|
+
margin-bottom: 30px;
|
|
758
|
+
}
|
|
759
|
+
.toc h3 {
|
|
760
|
+
margin-top: 0;
|
|
761
|
+
color: #2c3e50;
|
|
762
|
+
}
|
|
763
|
+
.toc ul {
|
|
764
|
+
column-count: 3;
|
|
765
|
+
column-gap: 20px;
|
|
766
|
+
list-style: none;
|
|
767
|
+
padding: 0;
|
|
768
|
+
margin: 0;
|
|
769
|
+
}
|
|
770
|
+
.toc li {
|
|
771
|
+
margin-bottom: 8px;
|
|
772
|
+
}
|
|
773
|
+
.toc a {
|
|
774
|
+
color: #3498db;
|
|
775
|
+
text-decoration: none;
|
|
776
|
+
}
|
|
777
|
+
.toc a:hover {
|
|
778
|
+
text-decoration: underline;
|
|
779
|
+
}
|
|
690
780
|
.test-section {
|
|
691
781
|
margin-bottom: 40px;
|
|
692
782
|
padding: 20px;
|
|
@@ -698,59 +788,72 @@ function generateHtmlReport(report) {
|
|
|
698
788
|
font-size: 18px;
|
|
699
789
|
font-weight: 600;
|
|
700
790
|
color: #2c3e50;
|
|
701
|
-
margin-bottom:
|
|
702
|
-
text-transform: uppercase;
|
|
703
|
-
letter-spacing: 1px;
|
|
704
|
-
}
|
|
705
|
-
.comparison {
|
|
706
|
-
display: flex;
|
|
707
|
-
gap: 20px;
|
|
708
|
-
margin-bottom: 20px;
|
|
709
|
-
}
|
|
710
|
-
.comparison-column {
|
|
711
|
-
flex: 1;
|
|
712
|
-
}
|
|
713
|
-
.comparison-header {
|
|
714
|
-
font-weight: 600;
|
|
715
|
-
padding-bottom: 10px;
|
|
716
|
-
margin-bottom: 10px;
|
|
717
|
-
border-bottom: 2px solid #3498db;
|
|
718
|
-
font-size: 14px;
|
|
791
|
+
margin-bottom: 15px;
|
|
719
792
|
}
|
|
720
|
-
.
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
793
|
+
.note {
|
|
794
|
+
font-size: 13px;
|
|
795
|
+
color: #7f8c8d;
|
|
796
|
+
margin-bottom: 15px;
|
|
797
|
+
font-style: italic;
|
|
725
798
|
}
|
|
726
799
|
table {
|
|
727
800
|
width: 100%;
|
|
728
801
|
border-collapse: collapse;
|
|
729
|
-
margin-bottom:
|
|
802
|
+
margin-bottom: 10px;
|
|
730
803
|
background: white;
|
|
731
804
|
border-radius: 6px;
|
|
732
805
|
overflow: hidden;
|
|
733
806
|
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
|
807
|
+
font-size: 13px;
|
|
734
808
|
}
|
|
735
809
|
th {
|
|
736
810
|
background: #34495e;
|
|
737
811
|
color: white;
|
|
738
|
-
padding: 12px
|
|
739
|
-
text-align:
|
|
812
|
+
padding: 10px 12px;
|
|
813
|
+
text-align: right;
|
|
740
814
|
font-weight: 600;
|
|
741
|
-
|
|
815
|
+
}
|
|
816
|
+
th:first-child {
|
|
817
|
+
text-align: left;
|
|
742
818
|
}
|
|
743
819
|
td {
|
|
744
|
-
padding: 12px
|
|
820
|
+
padding: 10px 12px;
|
|
745
821
|
border-bottom: 1px solid #ecf0f1;
|
|
746
|
-
|
|
822
|
+
text-align: right;
|
|
823
|
+
}
|
|
824
|
+
td:first-child {
|
|
825
|
+
text-align: left;
|
|
826
|
+
font-weight: 500;
|
|
747
827
|
}
|
|
748
828
|
tr:hover {
|
|
749
829
|
background: #f5f5f5;
|
|
750
830
|
}
|
|
751
|
-
.metric {
|
|
752
|
-
font-weight: 500;
|
|
831
|
+
.metric-dst {
|
|
753
832
|
color: #27ae60;
|
|
833
|
+
font-weight: 600;
|
|
834
|
+
}
|
|
835
|
+
.metric-sdsl {
|
|
836
|
+
color: #8e44ad;
|
|
837
|
+
}
|
|
838
|
+
.metric-native {
|
|
839
|
+
color: #e67e22;
|
|
840
|
+
}
|
|
841
|
+
.metric-cpp {
|
|
842
|
+
color: #e74c3c;
|
|
843
|
+
}
|
|
844
|
+
.na {
|
|
845
|
+
color: #bdc3c7;
|
|
846
|
+
}
|
|
847
|
+
@media (max-width: 768px) {
|
|
848
|
+
.toc ul {
|
|
849
|
+
column-count: 1;
|
|
850
|
+
}
|
|
851
|
+
table {
|
|
852
|
+
font-size: 12px;
|
|
853
|
+
}
|
|
854
|
+
th, td {
|
|
855
|
+
padding: 8px 6px;
|
|
856
|
+
}
|
|
754
857
|
}
|
|
755
858
|
</style>
|
|
756
859
|
</head>
|
|
@@ -758,77 +861,130 @@ function generateHtmlReport(report) {
|
|
|
758
861
|
<div class="container">
|
|
759
862
|
<h1>📊 Performance Benchmark Report</h1>
|
|
760
863
|
<div class="timestamp">Generated: ${new Date().toLocaleString()}</div>
|
|
864
|
+
<div class="back-link">
|
|
865
|
+
<a href="https://github.com/zrwusa/data-structure-typed">← Back to Repository</a> |
|
|
866
|
+
<a href="https://github.com/zrwusa/data-structure-typed/blob/main/docs/PERFORMANCE.md">View Markdown Version</a>
|
|
867
|
+
</div>
|
|
761
868
|
|
|
762
869
|
<div class="summary">
|
|
763
870
|
<div class="summary-stat">
|
|
764
|
-
<div class="summary-label">
|
|
765
|
-
<div class="summary-value">${
|
|
871
|
+
<div class="summary-label">Data Structures</div>
|
|
872
|
+
<div class="summary-value">${sortedDisplayNames.length}</div>
|
|
766
873
|
</div>
|
|
767
874
|
<div class="summary-stat">
|
|
768
|
-
<div class="summary-label">
|
|
769
|
-
<div class="summary-value">${
|
|
875
|
+
<div class="summary-label">JS Tests</div>
|
|
876
|
+
<div class="summary-value">${javascript.reduce((sum, t) => sum + t.benchmarks.length, 0)}</div>
|
|
770
877
|
</div>
|
|
771
878
|
<div class="summary-stat">
|
|
772
|
-
<div class="summary-label">
|
|
773
|
-
<div class="summary-value">${
|
|
879
|
+
<div class="summary-label">C++ Tests</div>
|
|
880
|
+
<div class="summary-value">${native.reduce((sum, t) => sum + t.benchmarks.length, 0)}</div>
|
|
774
881
|
</div>
|
|
775
882
|
</div>
|
|
883
|
+
|
|
884
|
+
<div class="toc">
|
|
885
|
+
<h3>📋 Table of Contents</h3>
|
|
886
|
+
<ul>
|
|
887
|
+
${sortedDisplayNames.map(name => `<li><a href="#${name.toLowerCase().replace(/\s+/g, '-')}">${name}</a></li>`).join('\n ')}
|
|
888
|
+
</ul>
|
|
889
|
+
</div>
|
|
776
890
|
`;
|
|
777
891
|
|
|
778
|
-
//
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
892
|
+
// Helper: keep non-DST variants out of the main table.
|
|
893
|
+
const isVariantCase = (name) => {
|
|
894
|
+
if (!name) return false;
|
|
895
|
+
return (
|
|
896
|
+
name.includes('(js-sdsl)') ||
|
|
897
|
+
name.includes('(Node Mode)') ||
|
|
898
|
+
name.startsWith('Native JS ')
|
|
899
|
+
);
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
for (const displayName of sortedDisplayNames) {
|
|
903
|
+
const group = groups.get(displayName);
|
|
904
|
+
const items = group.items;
|
|
905
|
+
const suiteName = group.testName.replace(/-esm$/, '');
|
|
906
|
+
|
|
907
|
+
const anchor = displayName.toLowerCase().replace(/\s+/g, '-');
|
|
908
|
+
html += `<div class="test-section" id="${anchor}">`;
|
|
909
|
+
html += `<div class="test-name">${displayName}</div>`;
|
|
910
|
+
|
|
911
|
+
// Build lookup maps for this suite
|
|
912
|
+
const jsAvgByCase = new Map();
|
|
913
|
+
for (const item of items) {
|
|
914
|
+
const rawName = item.benchmark['Test Case'];
|
|
915
|
+
jsAvgByCase.set(rawName, item.benchmark['Latency Avg (ms)']);
|
|
916
|
+
jsAvgByCase.set(formatNumberAbbr(rawName), item.benchmark['Latency Avg (ms)']);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
const pickOpt = (k) => jsAvgByCase.get(k);
|
|
920
|
+
const pick = (k) => pickOpt(k) ?? '-';
|
|
921
|
+
const cppPick = (k) =>
|
|
922
|
+
cppMap.get(`${suiteName}|${k}`) ??
|
|
923
|
+
cppMap.get(`${suiteName}|${formatNumberAbbr(k)}`) ??
|
|
924
|
+
'-';
|
|
925
|
+
|
|
926
|
+
// Gather base cases (non-variant)
|
|
927
|
+
const baseCases = [];
|
|
928
|
+
for (const it of items) {
|
|
929
|
+
const raw = it.benchmark?.['Test Case'];
|
|
930
|
+
if (!raw) continue;
|
|
931
|
+
if (isVariantCase(raw)) continue;
|
|
932
|
+
if (!baseCases.includes(raw)) baseCases.push(raw);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const hasNodeMode = items.some(it => (it.benchmark?.['Test Case'] ?? '').includes('(Node Mode)'));
|
|
936
|
+
const isBinaryTreeSuite = ['red-black-tree', 'avl-tree', 'bst', 'binary-tree', 'tree-map', 'tree-set'].includes(suiteName);
|
|
937
|
+
const showNodeMode = isBinaryTreeSuite && hasNodeMode;
|
|
938
|
+
const hasCpp = native.length > 0;
|
|
939
|
+
|
|
940
|
+
html += `<p class="note">Comparison table: DST is data-structure-typed. Values in ms (lower is better). "-" = no equivalent test.</p>`;
|
|
941
|
+
|
|
942
|
+
// Build header
|
|
943
|
+
const headers = ['Test Case', 'DST (ms)'];
|
|
944
|
+
if (showNodeMode) headers.push('Node Mode (ms)');
|
|
945
|
+
headers.push('js-sdsl (ms)', 'Native (ms)');
|
|
946
|
+
if (hasCpp) headers.push('C++ (ms)');
|
|
947
|
+
|
|
948
|
+
html += `<table>`;
|
|
949
|
+
html += `<thead><tr>${headers.map(h => `<th>${h}</th>`).join('')}</tr></thead>`;
|
|
950
|
+
html += `<tbody>`;
|
|
951
|
+
|
|
952
|
+
for (const base of baseCases) {
|
|
953
|
+
const abbr = formatNumberAbbr(base);
|
|
954
|
+
const dst = pick(base);
|
|
955
|
+
const nodeMode = pick(`${base} (Node Mode)`);
|
|
956
|
+
const sdsl = pick(`${base} (js-sdsl)`);
|
|
957
|
+
const nativeMs = (
|
|
958
|
+
pickOpt(`Native JS ${base}`) ??
|
|
959
|
+
pickOpt(`Native JS Array ${base}`) ??
|
|
960
|
+
pickOpt(`Native JS Map ${base}`) ??
|
|
961
|
+
pickOpt(`Native JS Set ${base}`)
|
|
962
|
+
);
|
|
963
|
+
const cpp = cppPick(base);
|
|
964
|
+
|
|
965
|
+
html += `<tr>`;
|
|
966
|
+
html += `<td>${abbr}</td>`;
|
|
967
|
+
html += `<td class="${dst !== '-' ? 'metric-dst' : 'na'}">${dst}</td>`;
|
|
968
|
+
if (showNodeMode) html += `<td class="${nodeMode !== '-' ? 'metric-dst' : 'na'}">${nodeMode}</td>`;
|
|
969
|
+
html += `<td class="${sdsl !== '-' ? 'metric-sdsl' : 'na'}">${sdsl}</td>`;
|
|
970
|
+
html += `<td class="${nativeMs ? 'metric-native' : 'na'}">${nativeMs ?? '-'}</td>`;
|
|
971
|
+
if (hasCpp) html += `<td class="${cpp !== '-' ? 'metric-cpp' : 'na'}">${cpp}</td>`;
|
|
972
|
+
html += `</tr>`;
|
|
822
973
|
}
|
|
823
974
|
|
|
824
|
-
html += `</
|
|
975
|
+
html += `</tbody></table>`;
|
|
976
|
+
html += `</div>`;
|
|
825
977
|
}
|
|
826
978
|
|
|
827
979
|
html += `</div></body></html>`;
|
|
828
980
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
981
|
+
// Write to both locations: benchmark/ (legacy) and docs/ (for publishing)
|
|
982
|
+
const htmlPathBenchmark = path.join(reportDistPath, 'report.html');
|
|
983
|
+
const htmlPathDocs = path.join(docsPath, 'benchmark.html');
|
|
984
|
+
fs.writeFileSync(htmlPathBenchmark, html, 'utf-8');
|
|
985
|
+
fs.writeFileSync(htmlPathDocs, html, 'utf-8');
|
|
986
|
+
console.log(`${GREEN}✓ HTML report written to: ${htmlPathBenchmark}${END}`);
|
|
987
|
+
console.log(`${GREEN}✓ HTML report written to: ${htmlPathDocs} (for docs publishing)${END}`);
|
|
832
988
|
}
|
|
833
989
|
|
|
834
990
|
async function main() {
|
|
@@ -854,9 +1010,9 @@ async function main() {
|
|
|
854
1010
|
);
|
|
855
1011
|
console.log(`\n${CYAN}📁 Output files:${END}`);
|
|
856
1012
|
console.log(` ${GREEN}✓${END} HTML Report: benchmark/report.html`);
|
|
857
|
-
console.log(` └─ 📊
|
|
858
|
-
console.log(` └─ 🎨
|
|
859
|
-
console.log(` └─ 📋
|
|
1013
|
+
console.log(` └─ 📊 Unified comparison table (DST | js-sdsl | Native | C++)`);
|
|
1014
|
+
console.log(` └─ 🎨 Same structure as PERFORMANCE.md`);
|
|
1015
|
+
console.log(` └─ 📋 Table of Contents + Anchor Navigation`);
|
|
860
1016
|
console.log(` └─ ✨ Number Abbreviation: 10M, 100K, 1K`);
|
|
861
1017
|
console.log(`\n ${GREEN}✓${END} Markdown Tables: docs/PERFORMANCE.md`);
|
|
862
1018
|
console.log(` └─ Comparison tables with C++ Avg column`);
|
|
@@ -267,4 +267,50 @@ describe('TreeMap (RedBlackTree-backed, no node exposure)', () => {
|
|
|
267
267
|
expect(spy).toHaveBeenCalled();
|
|
268
268
|
spy.mockRestore();
|
|
269
269
|
});
|
|
270
|
+
|
|
271
|
+
test('toEntryFn: construct from raw objects', () => {
|
|
272
|
+
interface User {
|
|
273
|
+
id: number;
|
|
274
|
+
name: string;
|
|
275
|
+
age: number;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const users: User[] = [
|
|
279
|
+
{ id: 3, name: 'Charlie', age: 35 },
|
|
280
|
+
{ id: 1, name: 'Alice', age: 30 },
|
|
281
|
+
{ id: 2, name: 'Bob', age: 25 }
|
|
282
|
+
];
|
|
283
|
+
|
|
284
|
+
const m = new TreeMap<number, User, User>(users, {
|
|
285
|
+
toEntryFn: u => [u.id, u]
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
expect(m.size).toBe(3);
|
|
289
|
+
expect([...m.keys()]).toEqual([1, 2, 3]); // sorted by key
|
|
290
|
+
expect(m.get(1)?.name).toBe('Alice');
|
|
291
|
+
expect(m.get(2)?.name).toBe('Bob');
|
|
292
|
+
expect(m.get(3)?.name).toBe('Charlie');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('toEntryFn: with custom comparator', () => {
|
|
296
|
+
interface Product {
|
|
297
|
+
sku: string;
|
|
298
|
+
price: number;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const products: Product[] = [
|
|
302
|
+
{ sku: 'B001', price: 29.99 },
|
|
303
|
+
{ sku: 'A001', price: 19.99 },
|
|
304
|
+
{ sku: 'C001', price: 39.99 }
|
|
305
|
+
];
|
|
306
|
+
|
|
307
|
+
const m = new TreeMap<string, number, Product>(products, {
|
|
308
|
+
toEntryFn: p => [p.sku, p.price],
|
|
309
|
+
comparator: (a, b) => a.localeCompare(b)
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
expect(m.size).toBe(3);
|
|
313
|
+
expect([...m.keys()]).toEqual(['A001', 'B001', 'C001']);
|
|
314
|
+
expect(m.get('A001')).toBe(19.99);
|
|
315
|
+
});
|
|
270
316
|
});
|
|
@@ -96,4 +96,51 @@ describe('TreeMultiMap (RFC additions)', () => {
|
|
|
96
96
|
[2, 'x']
|
|
97
97
|
]);
|
|
98
98
|
});
|
|
99
|
+
|
|
100
|
+
it('toEntryFn: construct from raw objects', () => {
|
|
101
|
+
interface Player {
|
|
102
|
+
score: number;
|
|
103
|
+
items: string[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const players: Player[] = [
|
|
107
|
+
{ score: 200, items: ['sword', 'shield'] },
|
|
108
|
+
{ score: 100, items: ['bow'] },
|
|
109
|
+
{ score: 150, items: ['staff', 'wand', 'robe'] }
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
const mm = new TreeMultiMap<number, string>(players, {
|
|
113
|
+
toEntryFn: ((p: Player) => [p.score, p.items]) as (raw: unknown) => [number, string[]]
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(mm.size).toBe(3);
|
|
117
|
+
expect([...mm.keys()]).toEqual([100, 150, 200]); // sorted by key
|
|
118
|
+
expect(mm.get(100)).toEqual(['bow']);
|
|
119
|
+
expect(mm.get(150)).toEqual(['staff', 'wand', 'robe']);
|
|
120
|
+
expect(mm.get(200)).toEqual(['sword', 'shield']);
|
|
121
|
+
expect(mm.totalSize).toBe(6); // 1 + 3 + 2
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('toEntryFn: with single value converted to bucket', () => {
|
|
125
|
+
interface Event {
|
|
126
|
+
date: string;
|
|
127
|
+
title: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const events: Event[] = [
|
|
131
|
+
{ date: '2024-01-01', title: 'New Year' },
|
|
132
|
+
{ date: '2024-02-14', title: 'Valentine' },
|
|
133
|
+
{ date: '2024-01-01', title: 'Party' } // same date
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
// Note: toEntryFn returns [K, V | V[]], but TreeMultiMap normalizes to array
|
|
137
|
+
const mm = new TreeMultiMap<string, string>(events, {
|
|
138
|
+
toEntryFn: ((e: Event) => [e.date, [e.title]]) as (raw: unknown) => [string, string[]]
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(mm.size).toBe(2); // 2 distinct dates
|
|
142
|
+
// The second entry for '2024-01-01' overwrites the first bucket
|
|
143
|
+
expect(mm.get('2024-01-01')).toEqual(['Party']);
|
|
144
|
+
expect(mm.get('2024-02-14')).toEqual(['Valentine']);
|
|
145
|
+
});
|
|
99
146
|
});
|
|
@@ -542,4 +542,53 @@ describe('TreeMultiSet', () => {
|
|
|
542
542
|
expect(ms.count(1)).toBe(1);
|
|
543
543
|
});
|
|
544
544
|
});
|
|
545
|
+
|
|
546
|
+
describe('toElementFn', () => {
|
|
547
|
+
it('construct from raw objects', () => {
|
|
548
|
+
interface Score {
|
|
549
|
+
playerId: string;
|
|
550
|
+
points: number;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const scores: Score[] = [
|
|
554
|
+
{ playerId: 'p1', points: 100 },
|
|
555
|
+
{ playerId: 'p2', points: 200 },
|
|
556
|
+
{ playerId: 'p3', points: 100 }, // duplicate points
|
|
557
|
+
{ playerId: 'p4', points: 150 }
|
|
558
|
+
];
|
|
559
|
+
|
|
560
|
+
const ms = new TreeMultiSet<number, Score>(scores, {
|
|
561
|
+
toElementFn: s => s.points
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
expect(ms.size).toBe(4);
|
|
565
|
+
expect(ms.distinctSize).toBe(3);
|
|
566
|
+
expect(ms.count(100)).toBe(2);
|
|
567
|
+
expect(ms.count(150)).toBe(1);
|
|
568
|
+
expect(ms.count(200)).toBe(1);
|
|
569
|
+
expect([...ms.keysDistinct()]).toEqual([100, 150, 200]); // sorted
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it('toElementFn with custom comparator', () => {
|
|
573
|
+
interface Item {
|
|
574
|
+
priority: number;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const items: Item[] = [
|
|
578
|
+
{ priority: 3 },
|
|
579
|
+
{ priority: 1 },
|
|
580
|
+
{ priority: 2 },
|
|
581
|
+
{ priority: 1 }
|
|
582
|
+
];
|
|
583
|
+
|
|
584
|
+
const ms = new TreeMultiSet<number, Item>(items, {
|
|
585
|
+
toElementFn: item => item.priority,
|
|
586
|
+
comparator: (a, b) => b - a // descending
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
expect(ms.size).toBe(4);
|
|
590
|
+
expect([...ms.keysDistinct()]).toEqual([3, 2, 1]); // descending order
|
|
591
|
+
expect(ms.count(1)).toBe(2);
|
|
592
|
+
});
|
|
593
|
+
});
|
|
545
594
|
});
|
|
@@ -184,4 +184,48 @@ describe('TreeSet (RedBlackTree-backed, no node exposure)', () => {
|
|
|
184
184
|
expect(spy).toHaveBeenCalled();
|
|
185
185
|
spy.mockRestore();
|
|
186
186
|
});
|
|
187
|
+
|
|
188
|
+
test('toElementFn: construct from raw objects', () => {
|
|
189
|
+
interface User {
|
|
190
|
+
id: number;
|
|
191
|
+
name: string;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const users: User[] = [
|
|
195
|
+
{ id: 3, name: 'Charlie' },
|
|
196
|
+
{ id: 1, name: 'Alice' },
|
|
197
|
+
{ id: 2, name: 'Bob' }
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
const s = new TreeSet<number, User>(users, {
|
|
201
|
+
toElementFn: u => u.id
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(s.size).toBe(3);
|
|
205
|
+
expect([...s]).toEqual([1, 2, 3]); // sorted
|
|
206
|
+
expect(s.has(1)).toBe(true);
|
|
207
|
+
expect(s.has(2)).toBe(true);
|
|
208
|
+
expect(s.has(3)).toBe(true);
|
|
209
|
+
expect(s.has(4)).toBe(false);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('toElementFn: with duplicates (deduplication)', () => {
|
|
213
|
+
interface Item {
|
|
214
|
+
category: string;
|
|
215
|
+
name: string;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const items: Item[] = [
|
|
219
|
+
{ category: 'fruit', name: 'apple' },
|
|
220
|
+
{ category: 'vegetable', name: 'carrot' },
|
|
221
|
+
{ category: 'fruit', name: 'banana' } // duplicate category
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
const s = new TreeSet<string, Item>(items, {
|
|
225
|
+
toElementFn: item => item.category
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
expect(s.size).toBe(2); // deduplicated
|
|
229
|
+
expect([...s]).toEqual(['fruit', 'vegetable']);
|
|
230
|
+
});
|
|
187
231
|
});
|