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.
Files changed (37) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/README.md +2 -3
  3. package/README_CN.md +0 -1
  4. package/benchmark/report.html +118 -40
  5. package/benchmark/report.json +726 -726
  6. package/dist/cjs/index.cjs +79 -21
  7. package/dist/cjs/index.cjs.map +1 -1
  8. package/dist/cjs-legacy/index.cjs +79 -21
  9. package/dist/cjs-legacy/index.cjs.map +1 -1
  10. package/dist/esm/index.mjs +79 -21
  11. package/dist/esm/index.mjs.map +1 -1
  12. package/dist/esm-legacy/index.mjs +79 -21
  13. package/dist/esm-legacy/index.mjs.map +1 -1
  14. package/dist/types/data-structures/binary-tree/tree-map.d.ts +17 -6
  15. package/dist/types/data-structures/binary-tree/tree-multi-map.d.ts +13 -5
  16. package/dist/types/data-structures/binary-tree/tree-multi-set.d.ts +12 -5
  17. package/dist/types/data-structures/binary-tree/tree-set.d.ts +15 -4
  18. package/dist/types/types/data-structures/binary-tree/tree-map.d.ts +6 -1
  19. package/dist/types/types/data-structures/binary-tree/tree-multi-set.d.ts +6 -1
  20. package/dist/types/types/data-structures/binary-tree/tree-set.d.ts +6 -1
  21. package/dist/umd/data-structure-typed.js +79 -21
  22. package/dist/umd/data-structure-typed.js.map +1 -1
  23. package/dist/umd/data-structure-typed.min.js +4 -4
  24. package/dist/umd/data-structure-typed.min.js.map +1 -1
  25. package/package.json +6 -2
  26. package/src/data-structures/binary-tree/tree-map.ts +35 -13
  27. package/src/data-structures/binary-tree/tree-multi-map.ts +41 -20
  28. package/src/data-structures/binary-tree/tree-multi-set.ts +17 -6
  29. package/src/data-structures/binary-tree/tree-set.ts +19 -6
  30. package/src/types/data-structures/binary-tree/tree-map.ts +7 -1
  31. package/src/types/data-structures/binary-tree/tree-multi-set.ts +7 -1
  32. package/src/types/data-structures/binary-tree/tree-set.ts +7 -1
  33. package/test/performance/reportor-enhanced.mjs +256 -100
  34. package/test/unit/data-structures/binary-tree/tree-map.test.ts +46 -0
  35. package/test/unit/data-structures/binary-tree/tree-multi-map.rfc.test.ts +47 -0
  36. package/test/unit/data-structures/binary-tree/tree-multi-set.test.ts +49 -0
  37. 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 side-by-side comparison layout
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
- const testGroups = new Map();
621
- for (const test of javascript) {
622
- if (!testGroups.has(test.testName)) {
623
- testGroups.set(test.testName, { testName: test.testName, js: null, native: null });
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
- for (const test of native) {
628
- if (!testGroups.has(test.testName)) {
629
- testGroups.set(test.testName, { testName: test.testName, js: null, native: null });
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: 1200px;
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: 20px;
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
- .lang-js {
721
- color: #f39c12;
722
- }
723
- .lang-cpp {
724
- color: #e74c3c;
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: 20px;
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 15px;
739
- text-align: left;
812
+ padding: 10px 12px;
813
+ text-align: right;
740
814
  font-weight: 600;
741
- font-size: 13px;
815
+ }
816
+ th:first-child {
817
+ text-align: left;
742
818
  }
743
819
  td {
744
- padding: 12px 15px;
820
+ padding: 10px 12px;
745
821
  border-bottom: 1px solid #ecf0f1;
746
- font-size: 13px;
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">data-structure-typed Tests</div>
765
- <div class="summary-value">${javascript.length}</div>
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">C++ Tests</div>
769
- <div class="summary-value">${native.length}</div>
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">Total Tests</div>
773
- <div class="summary-value">${javascript.length + native.length}</div>
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
- // Group by test name and generate side-by-side comparisons
779
- for (const [testName, group] of testGroups) {
780
- html += `<div class="test-section">`;
781
- html += `<div class="test-name">${testName}</div>`;
782
- html += `<div class="comparison">`;
783
-
784
- // JavaScript table (left side)
785
- if (group.js) {
786
- html += `<div class="comparison-column">`;
787
- html += `<div class="comparison-header lang-js">data-structure-typed</div>`;
788
- html += `<table>`;
789
- html += `<thead><tr><th>Test Case</th><th>Avg (ms)</th><th>Min (ms)</th><th>Max (ms)</th><th>Stability</th></tr></thead>`;
790
- html += `<tbody>`;
791
- for (const benchmark of group.js.benchmarks) {
792
- html += `<tr>`;
793
- html += `<td>${formatNumberAbbr(benchmark['Test Case'])}</td>`;
794
- html += `<td class="metric">${benchmark['Latency Avg (ms)']}</td>`;
795
- html += `<td>${benchmark['Min (ms)']}</td>`;
796
- html += `<td>${benchmark['Max (ms)']}</td>`;
797
- html += `<td>${benchmark['Stability']}</td>`;
798
- html += `</tr>`;
799
- }
800
- html += `</tbody></table>`;
801
- html += `</div>`;
802
- }
803
-
804
- // C++ table (right side)
805
- if (group.native) {
806
- html += `<div class="comparison-column">`;
807
- html += `<div class="comparison-header lang-cpp">C++</div>`;
808
- html += `<table>`;
809
- html += `<thead><tr><th>Test Case</th><th>Avg (ms)</th><th>Min (ms)</th><th>Max (ms)</th><th>Stability</th></tr></thead>`;
810
- html += `<tbody>`;
811
- for (const benchmark of group.native.benchmarks) {
812
- html += `<tr>`;
813
- html += `<td>${formatNumberAbbr(benchmark['Test Case'])}</td>`;
814
- html += `<td class="metric">${benchmark['Latency Avg (ms)']}</td>`;
815
- html += `<td>${benchmark['Min (ms)']}</td>`;
816
- html += `<td>${benchmark['Max (ms)']}</td>`;
817
- html += `<td>${benchmark['Stability']}</td>`;
818
- html += `</tr>`;
819
- }
820
- html += `</tbody></table>`;
821
- html += `</div>`;
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 += `</div></div>`;
975
+ html += `</tbody></table>`;
976
+ html += `</div>`;
825
977
  }
826
978
 
827
979
  html += `</div></body></html>`;
828
980
 
829
- const htmlPath = path.join(reportDistPath, 'report.html');
830
- fs.writeFileSync(htmlPath, html, 'utf-8');
831
- console.log(`${GREEN}✓ HTML report written to: ${htmlPath}${END}`);
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(` └─ 📊 Comparison Layout: Left JS | Right C++`);
858
- console.log(` └─ 🎨 Summary Stats + Test Sections`);
859
- console.log(` └─ 📋 2 Tables Per Data Structure`);
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
  });