@dou-ui/pivot 0.0.0-stage → 0.1.0-alpha.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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -2
  3. package/dist/assets/default-PXde2wzm.js.map +1 -0
  4. package/dist/core/format.js +116 -0
  5. package/dist/core/format.js.map +1 -0
  6. package/dist/core/labels.js +28 -0
  7. package/dist/core/labels.js.map +1 -0
  8. package/dist/core/layout.js +190 -0
  9. package/dist/core/layout.js.map +1 -0
  10. package/dist/core/source.js +46 -0
  11. package/dist/core/source.js.map +1 -0
  12. package/dist/core/table.js +154 -0
  13. package/dist/core/table.js.map +1 -0
  14. package/dist/engine/aggregate.js +51 -0
  15. package/dist/engine/aggregate.js.map +1 -0
  16. package/dist/engine/compute.js +735 -0
  17. package/dist/engine/compute.js.map +1 -0
  18. package/dist/engine/engine.js +46 -0
  19. package/dist/engine/engine.js.map +1 -0
  20. package/dist/engine/formula.js +225 -0
  21. package/dist/engine/formula.js.map +1 -0
  22. package/dist/engine/group.js +107 -0
  23. package/dist/engine/group.js.map +1 -0
  24. package/dist/engine/hash.js +108 -0
  25. package/dist/engine/hash.js.map +1 -0
  26. package/dist/engine/index.js +8 -0
  27. package/dist/engine/show.js +132 -0
  28. package/dist/engine/show.js.map +1 -0
  29. package/dist/engine/source.js +339 -0
  30. package/dist/engine/source.js.map +1 -0
  31. package/dist/engine/tree.js +226 -0
  32. package/dist/engine/tree.js.map +1 -0
  33. package/dist/fields/fields.js +6 -0
  34. package/dist/fields/fields.js.map +1 -0
  35. package/dist/fields/fields.vue_vue_type_script_setup_true_lang.js +881 -0
  36. package/dist/fields/fields.vue_vue_type_script_setup_true_lang.js.map +1 -0
  37. package/dist/fields/filter-list.js +6 -0
  38. package/dist/fields/filter-list.js.map +1 -0
  39. package/dist/fields/filter-list.vue_vue_type_script_setup_true_lang.js +184 -0
  40. package/dist/fields/filter-list.vue_vue_type_script_setup_true_lang.js.map +1 -0
  41. package/dist/fields/formula-dialog.js +6 -0
  42. package/dist/fields/formula-dialog.js.map +1 -0
  43. package/dist/fields/formula-dialog.vue_vue_type_script_setup_true_lang.js +165 -0
  44. package/dist/fields/formula-dialog.vue_vue_type_script_setup_true_lang.js.map +1 -0
  45. package/dist/fields/model.js +200 -0
  46. package/dist/fields/model.js.map +1 -0
  47. package/dist/fields/top-dialog.js +6 -0
  48. package/dist/fields/top-dialog.js.map +1 -0
  49. package/dist/fields/top-dialog.vue_vue_type_script_setup_true_lang.js +174 -0
  50. package/dist/fields/top-dialog.vue_vue_type_script_setup_true_lang.js.map +1 -0
  51. package/dist/index.js +16 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/locale.js +508 -0
  54. package/dist/locale.js.map +1 -0
  55. package/dist/pivot.js +6 -0
  56. package/dist/pivot.js.map +1 -0
  57. package/dist/pivot.vue_vue_type_script_setup_true_lang.js +1711 -0
  58. package/dist/pivot.vue_vue_type_script_setup_true_lang.js.map +1 -0
  59. package/dist/style.css +1 -0
  60. package/dist/style.d.ts +1 -0
  61. package/dist/style.js +13 -0
  62. package/dist/types/core/format.d.ts +26 -0
  63. package/dist/types/core/labels.d.ts +14 -0
  64. package/dist/types/core/layout.d.ts +72 -0
  65. package/dist/types/core/source.d.ts +7 -0
  66. package/dist/types/core/table.d.ts +73 -0
  67. package/dist/types/engine/aggregate.d.ts +35 -0
  68. package/dist/types/engine/compute.d.ts +173 -0
  69. package/dist/types/engine/engine.d.ts +29 -0
  70. package/dist/types/engine/formula.d.ts +44 -0
  71. package/dist/types/engine/group.d.ts +34 -0
  72. package/dist/types/engine/hash.d.ts +36 -0
  73. package/dist/types/engine/index.d.ts +15 -0
  74. package/dist/types/engine/show.d.ts +27 -0
  75. package/dist/types/engine/source.d.ts +108 -0
  76. package/dist/types/engine/tree.d.ts +87 -0
  77. package/dist/types/engine/types.d.ts +148 -0
  78. package/dist/types/fields/fields.vue.d.ts +26 -0
  79. package/dist/types/fields/filter-list.vue.d.ts +20 -0
  80. package/dist/types/fields/formula-dialog.vue.d.ts +30 -0
  81. package/dist/types/fields/model.d.ts +52 -0
  82. package/dist/types/fields/top-dialog.vue.d.ts +25 -0
  83. package/dist/types/index.d.ts +458 -0
  84. package/dist/types/locale.d.ts +184 -0
  85. package/dist/types/pivot.vue.d.ts +29 -0
  86. package/dist/types/types.d.ts +199 -0
  87. package/dist/types/worker/client.d.ts +19 -0
  88. package/dist/types/worker/default.d.ts +1 -0
  89. package/dist/types/worker/index.d.ts +5 -0
  90. package/dist/types/worker/protocol.d.ts +60 -0
  91. package/dist/types/worker/serve.d.ts +22 -0
  92. package/dist/worker/client.js +191 -0
  93. package/dist/worker/client.js.map +1 -0
  94. package/dist/worker/default.js +20 -0
  95. package/dist/worker/default.js.map +1 -0
  96. package/dist/worker/index.js +3 -0
  97. package/dist/worker/serve.js +101 -0
  98. package/dist/worker/serve.js.map +1 -0
  99. package/package.json +64 -4
@@ -0,0 +1,190 @@
1
+ // src/core/layout.ts
2
+ /** The nodes of collapsed paths; paths that no longer match a group are skipped. */
3
+ function collapsedNodes(tree, paths) {
4
+ const nodes = /* @__PURE__ */ new Set();
5
+ for (const path of paths ?? []) {
6
+ const node = tree.find(path);
7
+ if (node > 0) nodes.add(node);
8
+ }
9
+ return nodes;
10
+ }
11
+ /**
12
+ * The rows of the table, top to bottom, without the grand total: each open
13
+ * group is followed by its children; a group's own row holds its total
14
+ * (or, with `totalsAfter`, a row after its children does).
15
+ */
16
+ function rowLines(tree, order, collapsed, options) {
17
+ const lines = [];
18
+ const push = (node, kind) => {
19
+ if (options.valuesHere) for (let v = 0; v < options.values; v++) lines.push({
20
+ node,
21
+ kind,
22
+ value: v
23
+ });
24
+ else lines.push({
25
+ node,
26
+ kind,
27
+ value: -1
28
+ });
29
+ };
30
+ if (tree.levels.length === 0) return lines;
31
+ const { childStart, depth, levels } = tree;
32
+ const visit = (node) => {
33
+ for (let at = childStart[node]; at < childStart[node + 1]; at++) {
34
+ const child = order[at];
35
+ const open = !collapsed.has(child) && tree.isGroup(child);
36
+ if (open && options.totalsAfter) {
37
+ visit(child);
38
+ if (levels[depth[child] - 1].subtotal) push(child, "subtotal");
39
+ continue;
40
+ }
41
+ push(child, "group");
42
+ if (open) visit(child);
43
+ }
44
+ };
45
+ visit(0);
46
+ return lines;
47
+ }
48
+ /** The grand total row(s), kept apart so they can stay at the bottom. */
49
+ function totalLines(tree, options) {
50
+ if (tree.levels.length > 0 && !options.total) return [];
51
+ if (options.valuesHere) return Array.from({ length: options.values }, (_, v) => ({
52
+ node: 0,
53
+ kind: "total",
54
+ value: v
55
+ }));
56
+ return [{
57
+ node: 0,
58
+ kind: "total",
59
+ value: -1
60
+ }];
61
+ }
62
+ /**
63
+ * The columns of the table, left to right: an open group's children and
64
+ * then, when its field has subtotals, a column with its total; a closed
65
+ * group is one column; the grand total comes last.
66
+ */
67
+ function columnLines(tree, collapsed, options) {
68
+ const lines = [];
69
+ const push = (node, kind) => {
70
+ if (options.valuesHere) for (let v = 0; v < options.values; v++) lines.push({
71
+ node,
72
+ kind,
73
+ value: v
74
+ });
75
+ else lines.push({
76
+ node,
77
+ kind,
78
+ value: -1
79
+ });
80
+ };
81
+ if (tree.levels.length === 0) {
82
+ push(0, "total");
83
+ return lines;
84
+ }
85
+ const { childStart, children, depth, levels } = tree;
86
+ const visit = (node) => {
87
+ for (let at = childStart[node]; at < childStart[node + 1]; at++) {
88
+ const child = children[at];
89
+ if (!collapsed.has(child) && tree.isGroup(child)) {
90
+ visit(child);
91
+ if (levels[depth[child] - 1].subtotal) push(child, "subtotal");
92
+ } else push(child, "group");
93
+ }
94
+ };
95
+ visit(0);
96
+ if (options.total) push(0, "total");
97
+ return lines;
98
+ }
99
+ /**
100
+ * The header cells over the columns, one list per header row. Each field
101
+ * of the columns is a row; a group spans its children; a closed group, a
102
+ * subtotal and the grand total reach down to the values row. The values
103
+ * row names each column's value; without it, the cells reach one row
104
+ * further down instead (the header keeps a row for the row fields).
105
+ */
106
+ function headerSpans(tree, lines, valueRow) {
107
+ const fields = tree.levels.length;
108
+ const rows = fields + (valueRow ? 1 : 0);
109
+ const spans = Array.from({ length: rows }, () => []);
110
+ const reach = valueRow ? fields : fields + 1;
111
+ const chain = new Int32Array(fields + 1);
112
+ lines.forEach((line, i) => {
113
+ const own = line.kind === "total" ? 0 : tree.depth[line.node];
114
+ for (let at = line.node, d = own; d >= 0; d--, at = tree.parent[at]) chain[d] = at;
115
+ for (let level = 0; level < fields; level++) {
116
+ let node;
117
+ let kind;
118
+ let height = 1;
119
+ if (line.kind === "total") {
120
+ if (level > 0) break;
121
+ node = 0;
122
+ kind = "total";
123
+ height = reach;
124
+ } else if (level < own - 1) {
125
+ node = chain[level + 1];
126
+ kind = "group";
127
+ } else if (level === own - 1) {
128
+ node = line.node;
129
+ kind = line.kind;
130
+ height = reach - own + 1;
131
+ } else break;
132
+ const list = spans[level];
133
+ const last = list[list.length - 1];
134
+ if (last && last.to === i && last.node === node && last.kind === kind) last.to = i + 1;
135
+ else list.push({
136
+ from: i,
137
+ to: i + 1,
138
+ level,
139
+ rows: height,
140
+ node,
141
+ kind,
142
+ value: -1
143
+ });
144
+ }
145
+ if (valueRow) spans[fields].push({
146
+ from: i,
147
+ to: i + 1,
148
+ level: fields,
149
+ rows: 1,
150
+ node: line.node,
151
+ kind: "value",
152
+ value: line.value
153
+ });
154
+ });
155
+ return spans;
156
+ }
157
+ /** The spans that reach into the columns `from` to `to`. */
158
+ function spansIn(list, from, to) {
159
+ let low = 0;
160
+ let high = list.length;
161
+ while (low < high) {
162
+ const mid = low + high >> 1;
163
+ if (list[mid].to <= from) low = mid + 1;
164
+ else high = mid;
165
+ }
166
+ const out = [];
167
+ for (let i = low; i < list.length && list[i].from < to; i++) out.push(list[i]);
168
+ return out;
169
+ }
170
+ /** Paths of every group at `depth` and deeper (to collapse a level), or of every group. */
171
+ function groupPaths(tree, depth = 1) {
172
+ const out = [];
173
+ for (let node = 1; node < tree.size; node++) if (tree.depth[node] >= depth && tree.isGroup(node)) out.push(tree.path(node));
174
+ return out;
175
+ }
176
+ /** Whether two paths are the same groups; dates match by time. */
177
+ function samePath(a, b) {
178
+ if (a.length !== b.length) return false;
179
+ for (let i = 0; i < a.length; i++) {
180
+ const x = a[i];
181
+ const y = b[i];
182
+ if (x === y) continue;
183
+ if (x instanceof Date && y instanceof Date && x.getTime() === y.getTime()) continue;
184
+ return false;
185
+ }
186
+ return true;
187
+ }
188
+ export { collapsedNodes, columnLines, groupPaths, headerSpans, rowLines, samePath, spansIn, totalLines };
189
+
190
+ //# sourceMappingURL=layout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout.js","names":[],"sources":["../../src/core/layout.ts"],"sourcesContent":["import type { PivotTree } from '../engine'\nimport type { PivotCollapsed, PivotPath } from '../engine/types'\n\n/**\n * One row, or one column, of the table: a group (or the grand total), and\n * which value it shows when the values run along this axis.\n */\nexport interface PivotLine {\n node: number\n /**\n * `group`: a group's own line (its cells hold its total); `subtotal`: the\n * column after an open column group that holds its total; `total`: the\n * grand total.\n */\n kind: 'group' | 'subtotal' | 'total'\n /** The value's place in `values`, or -1 when the values run along the other axis. */\n value: number\n}\n\n/** A header cell over the columns `from` up to (not including) `to`. */\nexport interface PivotHeaderSpan {\n from: number\n to: number\n /** The header row it starts in, and how many it covers. */\n level: number\n rows: number\n node: number\n kind: PivotLine['kind'] | 'value'\n value: number\n}\n\n/** The nodes of collapsed paths; paths that no longer match a group are skipped. */\nexport function collapsedNodes(tree: PivotTree, paths: readonly PivotPath[] | undefined) {\n const nodes = new Set<number>()\n for (const path of paths ?? []) {\n const node = tree.find(path)\n if (node > 0) nodes.add(node)\n }\n return nodes\n}\n\nexport interface PivotLinesOptions {\n /** How many values run along this axis (1 when they run along the other one). */\n values: number\n /** Whether the values run along this axis. */\n valuesHere: boolean\n /** Whether the grand total gets a line. */\n total: boolean\n /**\n * Rows only: an open group's total goes on a line of its own after its\n * children (the tabular layout), instead of on the group's own line.\n */\n totalsAfter?: boolean\n}\n\n/**\n * The rows of the table, top to bottom, without the grand total: each open\n * group is followed by its children; a group's own row holds its total\n * (or, with `totalsAfter`, a row after its children does).\n */\nexport function rowLines(\n tree: PivotTree,\n order: Int32Array,\n collapsed: ReadonlySet<number>,\n options: PivotLinesOptions,\n): PivotLine[] {\n const lines: PivotLine[] = []\n const push = (node: number, kind: PivotLine['kind']) => {\n if (options.valuesHere)\n for (let v = 0; v < options.values; v++) lines.push({ node, kind, value: v })\n else lines.push({ node, kind, value: -1 })\n }\n if (tree.levels.length === 0) return lines\n const { childStart, depth, levels } = tree\n const visit = (node: number) => {\n for (let at = childStart[node]!; at < childStart[node + 1]!; at++) {\n const child = order[at]!\n const open = !collapsed.has(child) && tree.isGroup(child)\n if (open && options.totalsAfter) {\n visit(child)\n if (levels[depth[child]! - 1]!.subtotal) push(child, 'subtotal')\n continue\n }\n push(child, 'group')\n if (open) visit(child)\n }\n }\n visit(0)\n return lines\n}\n\n/** The grand total row(s), kept apart so they can stay at the bottom. */\nexport function totalLines(tree: PivotTree, options: PivotLinesOptions): PivotLine[] {\n if (tree.levels.length > 0 && !options.total) return []\n if (options.valuesHere)\n return Array.from({ length: options.values }, (_, v) => ({ node: 0, kind: 'total', value: v }))\n return [{ node: 0, kind: 'total', value: -1 }]\n}\n\n/**\n * The columns of the table, left to right: an open group's children and\n * then, when its field has subtotals, a column with its total; a closed\n * group is one column; the grand total comes last.\n */\nexport function columnLines(\n tree: PivotTree,\n collapsed: ReadonlySet<number>,\n options: PivotLinesOptions,\n): PivotLine[] {\n const lines: PivotLine[] = []\n const push = (node: number, kind: PivotLine['kind']) => {\n if (options.valuesHere)\n for (let v = 0; v < options.values; v++) lines.push({ node, kind, value: v })\n else lines.push({ node, kind, value: -1 })\n }\n if (tree.levels.length === 0) {\n push(0, 'total')\n return lines\n }\n const { childStart, children, depth, levels } = tree\n const visit = (node: number) => {\n for (let at = childStart[node]!; at < childStart[node + 1]!; at++) {\n const child = children[at]!\n if (!collapsed.has(child) && tree.isGroup(child)) {\n visit(child)\n if (levels[depth[child]! - 1]!.subtotal) push(child, 'subtotal')\n } else push(child, 'group')\n }\n }\n visit(0)\n if (options.total) push(0, 'total')\n return lines\n}\n\n/**\n * The header cells over the columns, one list per header row. Each field\n * of the columns is a row; a group spans its children; a closed group, a\n * subtotal and the grand total reach down to the values row. The values\n * row names each column's value; without it, the cells reach one row\n * further down instead (the header keeps a row for the row fields).\n */\nexport function headerSpans(\n tree: PivotTree,\n lines: readonly PivotLine[],\n valueRow: boolean,\n): PivotHeaderSpan[][] {\n const fields = tree.levels.length\n const rows = fields + (valueRow ? 1 : 0)\n const spans: PivotHeaderSpan[][] = Array.from({ length: rows }, () => [])\n const reach = valueRow ? fields : fields + 1\n const chain = new Int32Array(fields + 1)\n lines.forEach((line, i) => {\n const own = line.kind === 'total' ? 0 : tree.depth[line.node]!\n for (let at = line.node, d = own; d >= 0; d--, at = tree.parent[at]!) chain[d] = at\n for (let level = 0; level < fields; level++) {\n let node: number\n let kind: PivotHeaderSpan['kind']\n let height = 1\n if (line.kind === 'total') {\n if (level > 0) break\n node = 0\n kind = 'total'\n height = reach\n } else if (level < own - 1) {\n node = chain[level + 1]!\n kind = 'group'\n } else if (level === own - 1) {\n node = line.node\n kind = line.kind\n height = reach - own + 1\n } else break\n const list = spans[level]!\n const last = list[list.length - 1]\n if (last && last.to === i && last.node === node && last.kind === kind) last.to = i + 1\n else list.push({ from: i, to: i + 1, level, rows: height, node, kind, value: -1 })\n }\n if (valueRow) {\n spans[fields]!.push({\n from: i,\n to: i + 1,\n level: fields,\n rows: 1,\n node: line.node,\n kind: 'value',\n value: line.value,\n })\n }\n })\n return spans\n}\n\n/** The spans that reach into the columns `from` to `to`. */\nexport function spansIn(list: readonly PivotHeaderSpan[], from: number, to: number) {\n // spans are in order and do not overlap: find the first that ends after `from`\n let low = 0\n let high = list.length\n while (low < high) {\n const mid = (low + high) >> 1\n if (list[mid]!.to <= from) low = mid + 1\n else high = mid\n }\n const out: PivotHeaderSpan[] = []\n for (let i = low; i < list.length && list[i]!.from < to; i++) out.push(list[i]!)\n return out\n}\n\n/** Paths of every group at `depth` and deeper (to collapse a level), or of every group. */\nexport function groupPaths(tree: PivotTree, depth = 1): PivotPath[] {\n const out: PivotPath[] = []\n for (let node = 1; node < tree.size; node++) {\n if (tree.depth[node]! >= depth && tree.isGroup(node)) out.push(tree.path(node))\n }\n return out\n}\n\n/** Whether two paths are the same groups; dates match by time. */\nexport function samePath(a: PivotPath, b: PivotPath) {\n if (a.length !== b.length) return false\n for (let i = 0; i < a.length; i++) {\n const x = a[i]\n const y = b[i]\n if (x === y) continue\n if (x instanceof Date && y instanceof Date && x.getTime() === y.getTime()) continue\n return false\n }\n return true\n}\n\nexport type PivotAxis = keyof PivotCollapsed\n"],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ import { PivotSource } from "../engine/source.js";
2
+ import { useLocale } from "dou-ui";
3
+ import { computed, markRaw, toRaw, toValue } from "vue";
4
+ // src/core/source.ts
5
+ /**
6
+ * Sources by records, shared by every component on the page: a table and
7
+ * its field panel given the same array read each field only once.
8
+ */
9
+ var sources = /* @__PURE__ */ new WeakMap();
10
+ /** Whether two field lists describe the fields the same way (inline arrays are new each render). */
11
+ function sameFields(a, b) {
12
+ if (a === b) return true;
13
+ if (!a || !b || a.length !== b.length) return false;
14
+ return a.every((f, i) => {
15
+ const g = b[i];
16
+ return f.name === g.name && f.label === g.label && f.type === g.type && f.value === g.value && f.format === g.format && f.order === g.order;
17
+ });
18
+ }
19
+ /** The records as a `PivotSource`, read again only when the array, the fields or the locale change. */
20
+ function usePivotSource(data, fields) {
21
+ const { locale } = useLocale();
22
+ return computed(() => {
23
+ const rows = toRaw(toValue(data));
24
+ const given = toValue(fields);
25
+ const list = given && toRaw(given);
26
+ const name = locale.value.name;
27
+ let entries = sources.get(rows);
28
+ if (!entries) sources.set(rows, entries = []);
29
+ const hit = entries.find((e) => e.locale === name && sameFields(e.fields, list));
30
+ if (hit) return hit.source;
31
+ const source = markRaw(new PivotSource(rows, {
32
+ fields: list,
33
+ locale: name
34
+ }));
35
+ entries.push({
36
+ fields: list,
37
+ locale: name,
38
+ source
39
+ });
40
+ if (entries.length > 4) entries.shift();
41
+ return source;
42
+ });
43
+ }
44
+ export { sameFields, usePivotSource };
45
+
46
+ //# sourceMappingURL=source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source.js","names":[],"sources":["../../src/core/source.ts"],"sourcesContent":["import { useLocale } from 'dou-ui'\nimport { computed, markRaw, toRaw, toValue, type MaybeRefOrGetter } from 'vue'\nimport { PivotSource, type PivotData } from '../engine'\nimport type { PivotField } from '../engine/types'\n\ninterface Entry {\n fields: readonly PivotField<never>[] | undefined\n locale: string\n source: PivotSource<never>\n}\n\n/**\n * Sources by records, shared by every component on the page: a table and\n * its field panel given the same array read each field only once.\n */\nconst sources = new WeakMap<object, Entry[]>()\n\n/** Whether two field lists describe the fields the same way (inline arrays are new each render). */\nexport function sameFields(a?: readonly PivotField<never>[], b?: readonly PivotField<never>[]) {\n if (a === b) return true\n if (!a || !b || a.length !== b.length) return false\n return a.every((f, i) => {\n const g = b[i]!\n return (\n f.name === g.name &&\n f.label === g.label &&\n f.type === g.type &&\n f.value === g.value &&\n f.format === g.format &&\n f.order === g.order\n )\n })\n}\n\n/** The records as a `PivotSource`, read again only when the array, the fields or the locale change. */\nexport function usePivotSource<Row>(\n data: MaybeRefOrGetter<PivotData<Row>>,\n fields: MaybeRefOrGetter<readonly PivotField<Row>[] | undefined>,\n) {\n const { locale } = useLocale()\n return computed<PivotSource<Row>>(() => {\n const rows = toRaw(toValue(data)) as PivotData<Row>\n const given = toValue(fields)\n const list = (given && toRaw(given)) as readonly PivotField<never>[] | undefined\n const name = locale.value.name\n let entries = sources.get(rows as object)\n if (!entries) sources.set(rows as object, (entries = []))\n const hit = entries.find((e) => e.locale === name && sameFields(e.fields, list))\n if (hit) return hit.source as unknown as PivotSource<Row>\n const source = markRaw(\n new PivotSource<Row>(rows, { fields: list as readonly PivotField<Row>[], locale: name }),\n )\n // a few versions at most: the newest fields replace the oldest\n entries.push({ fields: list, locale: name, source: source as unknown as PivotSource<never> })\n if (entries.length > 4) entries.shift()\n return source\n })\n}\n"],"mappings":";;;;;;;;AAeA,IAAM,0BAAU,IAAI,QAAyB;;AAG7C,SAAgB,WAAW,GAAkC,GAAkC;CAC7F,IAAI,MAAM,GAAG,OAAO;CACpB,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,WAAW,EAAE,QAAQ,OAAO;CAC9C,OAAO,EAAE,OAAO,GAAG,MAAM;EACvB,MAAM,IAAI,EAAE;EACZ,OACE,EAAE,SAAS,EAAE,QACb,EAAE,UAAU,EAAE,SACd,EAAE,SAAS,EAAE,QACb,EAAE,UAAU,EAAE,SACd,EAAE,WAAW,EAAE,UACf,EAAE,UAAU,EAAE;CAElB,CAAC;AACH;;AAGA,SAAgB,eACd,MACA,QACA;CACA,MAAM,EAAE,WAAW,UAAU;CAC7B,OAAO,eAAiC;EACtC,MAAM,OAAO,MAAM,QAAQ,IAAI,CAAC;EAChC,MAAM,QAAQ,QAAQ,MAAM;EAC5B,MAAM,OAAQ,SAAS,MAAM,KAAK;EAClC,MAAM,OAAO,OAAO,MAAM;EAC1B,IAAI,UAAU,QAAQ,IAAI,IAAc;EACxC,IAAI,CAAC,SAAS,QAAQ,IAAI,MAAiB,UAAU,CAAC,CAAE;EACxD,MAAM,MAAM,QAAQ,MAAM,MAAM,EAAE,WAAW,QAAQ,WAAW,EAAE,QAAQ,IAAI,CAAC;EAC/E,IAAI,KAAK,OAAO,IAAI;EACpB,MAAM,SAAS,QACb,IAAI,YAAiB,MAAM;GAAE,QAAQ;GAAoC,QAAQ;EAAK,CAAC,CACzF;EAEA,QAAQ,KAAK;GAAE,QAAQ;GAAM,QAAQ;GAAc;EAAwC,CAAC;EAC5F,IAAI,QAAQ,SAAS,GAAG,QAAQ,MAAM;EACtC,OAAO;CACT,CAAC;AACH"}
@@ -0,0 +1,154 @@
1
+ // src/core/table.ts
2
+ /** A number without the float noise of adding up (`1592.0200000000002` is `1592.02`). */
3
+ var tidyNumber = (n) => Number.isFinite(n) ? Number(n.toPrecision(15)) : n;
4
+ /** What a spreadsheet program would start a formula with. */
5
+ var FORMULA_START = /^[=+\-@\t\r]/;
6
+ /** A cell's text in CSV or tab-separated text, with a number as its value or its text. */
7
+ function plainText(cell, text) {
8
+ if (!cell) return "";
9
+ if (cell.kind === "value") {
10
+ const v = cell.value;
11
+ return text ? cell.text : v === v ? String(tidyNumber(v)) : "";
12
+ }
13
+ return cell.text;
14
+ }
15
+ /** The grid as CSV. Labels that a spreadsheet would read as a formula get a `'` in front. */
16
+ function gridToCSV(grid, options = {}) {
17
+ const escape = options.escapeFormulas !== false;
18
+ const field = (cell) => {
19
+ let s = plainText(cell, !!options.text);
20
+ if (escape && cell && cell.kind !== "value" && FORMULA_START.test(s)) s = `'${s}`;
21
+ return /[",\r\n]/.test(s) ? `"${s.replaceAll("\"", "\"\"")}"` : s;
22
+ };
23
+ const lines = grid.cells.map((row) => row.map(field).join(","));
24
+ return (options.bom === false ? "" : "") + lines.join("\r\n");
25
+ }
26
+ /** The grid as tab-separated text, with the numbers as they show, for the clipboard. */
27
+ function tsvField(cell) {
28
+ const s = cell?.text ?? "";
29
+ return /[\t\n\r"]/.test(s) ? `"${s.replaceAll("\"", "\"\"")}"` : s;
30
+ }
31
+ function gridToTSV(grid) {
32
+ return grid.cells.map((row) => row.map(tsvField).join(" ")).join("\n");
33
+ }
34
+ var escapeHTML = (text) => text.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
35
+ /** The styles the sheet cells refer to. */
36
+ var SHEET_STYLES = {
37
+ "pivot-head": { bold: true },
38
+ "pivot-total": { bold: true }
39
+ };
40
+ /**
41
+ * A format whose decimals are optional (`#,##0.##`), for a number that
42
+ * needs none of them: without them, as Excel would show `226,895.` for
43
+ * `226895`.
44
+ */
45
+ function fitFormat(format, value) {
46
+ const match = /\.(0*)#+/.exec(format);
47
+ if (!match) return format;
48
+ const scaled = (format.includes("%") ? value * 100 : value) * 10 ** match[1].length;
49
+ if (Math.abs(scaled - Math.round(scaled)) > 1e-9) return format;
50
+ return format.replace(/\.(0*)#+/g, (_, zeros) => zeros ? `.${zeros}` : "");
51
+ }
52
+ function sheetCell(cell, formats) {
53
+ if (!cell) return null;
54
+ if (cell.kind === "value") {
55
+ const v = tidyNumber(cell.value);
56
+ if (v !== v) return null;
57
+ const given = formats[cell.valueIndex];
58
+ const format = given && fitFormat(given, v);
59
+ if (!format && !cell.total) return v;
60
+ return {
61
+ value: v,
62
+ ...format ? { format } : {},
63
+ ...cell.total ? { style: "pivot-total" } : {}
64
+ };
65
+ }
66
+ if (cell.text === "") return null;
67
+ const text = cell.text.startsWith("=") ? `'${cell.text}` : cell.text;
68
+ const style = cell.kind === "head" ? "pivot-head" : cell.total ? "pivot-total" : void 0;
69
+ return style ? {
70
+ value: text,
71
+ style
72
+ } : text;
73
+ }
74
+ /** The cells as `DouSheet` takes them, row by row. */
75
+ function gridToSheetCells(grid) {
76
+ return grid.cells.map((row) => row.map((cell) => sheetCell(cell, grid.formats)));
77
+ }
78
+ /** `A`, `B`, … `Z`, `AA`: a column's letters. */
79
+ function columnName(col) {
80
+ let name = "";
81
+ for (let n = col + 1; n > 0; n = Math.floor((n - 1) / 26)) name = String.fromCharCode(65 + (n - 1) % 26) + name;
82
+ return name;
83
+ }
84
+ /** The merged header cells, as ranges such as `'B1:D1'`. */
85
+ function gridMerges(grid) {
86
+ const out = [];
87
+ grid.cells.forEach((row, r) => row.forEach((cell, c) => {
88
+ const cols = cell?.colSpan ?? 1;
89
+ const rows = cell?.rowSpan ?? 1;
90
+ if (cols > 1 || rows > 1) out.push(`${columnName(c)}${r + 1}:${columnName(c + cols - 1)}${r + rows}`);
91
+ }));
92
+ return out;
93
+ }
94
+ /** The mark `DouSheet` puts on a table it copies, holding its cells. */
95
+ var SHEET_MARK = "data-dou-sheet";
96
+ /**
97
+ * The grid as an HTML table, for the clipboard and for printing: headers in
98
+ * `<thead>` (repeated on each printed page), spans kept. With `sheet`, the
99
+ * table also carries the cells for `DouSheet`, so pasting there keeps the
100
+ * numbers and their formats.
101
+ */
102
+ function gridToHTML(grid, { sheet = false } = {}) {
103
+ const row = (cells, head) => {
104
+ let out = "";
105
+ cells.forEach((cell, c) => {
106
+ if (!cell) return;
107
+ const tag = cell.kind === "value" ? "td" : "th";
108
+ const span = (cell.colSpan && cell.colSpan > 1 ? ` colspan="${cell.colSpan}"` : "") + (cell.rowSpan && cell.rowSpan > 1 ? ` rowspan="${cell.rowSpan}"` : "");
109
+ const cls = [
110
+ cell.kind === "value" ? "is-value" : cell.kind === "label" ? "is-label" : "is-head",
111
+ cell.total && "is-total",
112
+ !head && c === grid.headCols - 1 && "is-edge",
113
+ head && c >= grid.headCols && "is-end"
114
+ ].filter(Boolean).join(" ");
115
+ const scope = tag === "th" && !head ? " scope=\"row\"" : "";
116
+ const align = cell.kind === "value" ? " style=\"text-align:right\"" : "";
117
+ out += `<${tag}${span}${scope} class="${cls}"${align}>${escapeHTML(cell.text)}</${tag}>`;
118
+ });
119
+ return `<tr>${out}</tr>`;
120
+ };
121
+ const head = grid.cells.slice(0, grid.headRows).map((r) => row(r, true));
122
+ const body = grid.cells.slice(grid.headRows).map((r) => row(r, false));
123
+ return `<table${sheet ? ` ${SHEET_MARK}="${escapeHTML(JSON.stringify({
124
+ cells: gridToSheetCells(grid),
125
+ styles: SHEET_STYLES
126
+ }))}"` : ""} style="border-collapse:collapse">` + (head.length ? `<thead>${head.join("")}</thead>` : "") + `<tbody>${body.join("")}</tbody></table>`;
127
+ }
128
+ /** A sheet number format that writes numbers like these `Intl.NumberFormat` options. */
129
+ function sheetFormat(options, locale) {
130
+ const style = options.style ?? "decimal";
131
+ if (style === "unit") return void 0;
132
+ const currency = style === "currency";
133
+ const percent = style === "percent";
134
+ const ownMax = options.maximumFractionDigits;
135
+ const min = options.minimumFractionDigits ?? Math.min(currency ? 2 : 0, ownMax ?? Infinity);
136
+ const max = Math.max(min, ownMax ?? (currency ? 2 : percent ? 0 : 3));
137
+ let body = options.useGrouping !== false && options.useGrouping !== "false" ? "#,##0" : "0";
138
+ if (max > 0) body += `.${"0".repeat(min)}${"#".repeat(max - min)}`;
139
+ if (percent) body += "%";
140
+ if (currency) {
141
+ let symbol = "";
142
+ try {
143
+ symbol = new Intl.NumberFormat(locale, options).formatToParts(1).find((p) => p.type === "currency")?.value ?? "";
144
+ } catch {
145
+ return;
146
+ }
147
+ body = `"${symbol.replaceAll("\"", "")}"${body}`;
148
+ }
149
+ if (options.signDisplay === "exceptZero" || options.signDisplay === "always") return `+${body};-${body};${body}`;
150
+ return body;
151
+ }
152
+ export { SHEET_STYLES, gridMerges, gridToCSV, gridToHTML, gridToSheetCells, gridToTSV, sheetFormat, tidyNumber };
153
+
154
+ //# sourceMappingURL=table.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"table.js","names":[],"sources":["../../src/core/table.ts"],"sourcesContent":["import type { PivotSheetCell } from '../types'\n\n/*\n * The table as plain cells, for copying, CSV, a sheet and printing. Each\n * row field has a column of its own, whatever the layout on screen; the\n * column headers keep their groups as spans.\n */\n\n/** A cell of the table as it is copied or exported. */\nexport interface PivotGridCell {\n /** What the cell shows. */\n text: string\n /** `head`: a column header or the corner; `label`: a row's group; `value`: a number. */\n kind: 'head' | 'label' | 'value'\n /** For `value`: the number as shown (a share, when the value has `show`); `NaN` when empty. */\n value?: number\n /** For `value`: the value's place in `config.values`. */\n valueIndex?: number\n /** A subtotal or a total: a line, a column or both. */\n total?: boolean\n /** A header over several columns or rows; the cells it covers are `null`. */\n colSpan?: number\n rowSpan?: number\n}\n\n/** The table, or part of it, row by row. */\nexport interface PivotGrid {\n cells: (PivotGridCell | null)[][]\n /** How many rows at the top are column headers, and how many columns at the left are row headers. */\n headRows: number\n headCols: number\n /** Each value's number format for a sheet (`#,##0.00`, `0.0%`), by its place in `config.values`. */\n formats: (string | undefined)[]\n /** The width of each column on screen, in px. */\n widths: number[]\n}\n\n/** A number without the float noise of adding up (`1592.0200000000002` is `1592.02`). */\nexport const tidyNumber = (n: number) => (Number.isFinite(n) ? Number(n.toPrecision(15)) : n)\n\n/** What a spreadsheet program would start a formula with. */\nconst FORMULA_START = /^[=+\\-@\\t\\r]/\n\n/** A cell's text in CSV or tab-separated text, with a number as its value or its text. */\nfunction plainText(cell: PivotGridCell | null, text: boolean) {\n if (!cell) return ''\n if (cell.kind === 'value') {\n const v = cell.value!\n return text ? cell.text : v === v ? String(tidyNumber(v)) : ''\n }\n return cell.text\n}\n\nexport interface PivotCSVOptions {\n /**\n * Write the numbers as the table shows them (`¥1,280.00`, `21.3%`)\n * instead of values a spreadsheet can add up (`1280`, `0.213`).\n * Defaults to `false`.\n */\n text?: boolean\n /** Start with a byte order mark, so spreadsheets read the file as UTF-8. Defaults to `true`. */\n bom?: boolean\n /**\n * Put a `'` before labels a spreadsheet program would run as a formula\n * (starting with `=`, `+`, `-`, `@`, a tab or a carriage return). Defaults to\n * `true`; `false` writes them as they are.\n */\n escapeFormulas?: boolean\n}\n\n/** The grid as CSV. Labels that a spreadsheet would read as a formula get a `'` in front. */\nexport function gridToCSV(grid: PivotGrid, options: PivotCSVOptions = {}) {\n const escape = options.escapeFormulas !== false\n const field = (cell: PivotGridCell | null) => {\n let s = plainText(cell, !!options.text)\n if (escape && cell && cell.kind !== 'value' && FORMULA_START.test(s)) s = `'${s}`\n return /[\",\\r\\n]/.test(s) ? `\"${s.replaceAll('\"', '\"\"')}\"` : s\n }\n const lines = grid.cells.map((row) => row.map(field).join(','))\n return (options.bom === false ? '' : '') + lines.join('\\r\\n')\n}\n\n/** The grid as tab-separated text, with the numbers as they show, for the clipboard. */\nfunction tsvField(cell: PivotGridCell | null) {\n const s = cell?.text ?? ''\n return /[\\t\\n\\r\"]/.test(s) ? `\"${s.replaceAll('\"', '\"\"')}\"` : s\n}\n\nexport function gridToTSV(grid: PivotGrid) {\n return grid.cells.map((row) => row.map(tsvField).join('\\t')).join('\\n')\n}\n\nconst escapeHTML = (text: string) => text.replace(/[&<>\"']/g, (c) => `&#${c.charCodeAt(0)};`)\n\n/** The styles the sheet cells refer to. */\nexport const SHEET_STYLES = {\n 'pivot-head': { bold: true },\n 'pivot-total': { bold: true },\n}\n\n/**\n * A format whose decimals are optional (`#,##0.##`), for a number that\n * needs none of them: without them, as Excel would show `226,895.` for\n * `226895`.\n */\nfunction fitFormat(format: string, value: number) {\n const optional = /\\.(0*)#+/\n const match = optional.exec(format)\n if (!match) return format\n // a percentage shows the value a hundred times over\n const shown = format.includes('%') ? value * 100 : value\n const scaled = shown * 10 ** match[1]!.length\n if (Math.abs(scaled - Math.round(scaled)) > 1e-9) return format\n return format.replace(/\\.(0*)#+/g, (_, zeros: string) => (zeros ? `.${zeros}` : ''))\n}\n\nfunction sheetCell(cell: PivotGridCell | null, formats: PivotGrid['formats']): PivotSheetCell {\n if (!cell) return null\n if (cell.kind === 'value') {\n const v = tidyNumber(cell.value!)\n if (v !== v) return null\n const given = formats[cell.valueIndex!]\n const format = given && fitFormat(given, v)\n if (!format && !cell.total) return v\n return {\n value: v,\n ...(format ? { format } : {}),\n ...(cell.total ? { style: 'pivot-total' } : {}),\n }\n }\n if (cell.text === '') return null\n // text a sheet would read as a formula\n const text = cell.text.startsWith('=') ? `'${cell.text}` : cell.text\n const style = cell.kind === 'head' ? 'pivot-head' : cell.total ? 'pivot-total' : undefined\n return style ? { value: text, style } : text\n}\n\n/** The cells as `DouSheet` takes them, row by row. */\nexport function gridToSheetCells(grid: PivotGrid): PivotSheetCell[][] {\n return grid.cells.map((row) => row.map((cell) => sheetCell(cell, grid.formats)))\n}\n\n/** `A`, `B`, … `Z`, `AA`: a column's letters. */\nfunction columnName(col: number) {\n let name = ''\n for (let n = col + 1; n > 0; n = Math.floor((n - 1) / 26))\n name = String.fromCharCode(65 + ((n - 1) % 26)) + name\n return name\n}\n\n/** The merged header cells, as ranges such as `'B1:D1'`. */\nexport function gridMerges(grid: PivotGrid): string[] {\n const out: string[] = []\n grid.cells.forEach((row, r) =>\n row.forEach((cell, c) => {\n const cols = cell?.colSpan ?? 1\n const rows = cell?.rowSpan ?? 1\n if (cols > 1 || rows > 1)\n out.push(`${columnName(c)}${r + 1}:${columnName(c + cols - 1)}${r + rows}`)\n }),\n )\n return out\n}\n\n/** The mark `DouSheet` puts on a table it copies, holding its cells. */\nconst SHEET_MARK = 'data-dou-sheet'\n\n/**\n * The grid as an HTML table, for the clipboard and for printing: headers in\n * `<thead>` (repeated on each printed page), spans kept. With `sheet`, the\n * table also carries the cells for `DouSheet`, so pasting there keeps the\n * numbers and their formats.\n */\nexport function gridToHTML(grid: PivotGrid, { sheet = false }: { sheet?: boolean } = {}) {\n const row = (cells: (PivotGridCell | null)[], head: boolean) => {\n let out = ''\n cells.forEach((cell, c) => {\n if (!cell) return\n const tag = cell.kind === 'value' ? 'td' : 'th'\n const span =\n (cell.colSpan && cell.colSpan > 1 ? ` colspan=\"${cell.colSpan}\"` : '') +\n (cell.rowSpan && cell.rowSpan > 1 ? ` rowspan=\"${cell.rowSpan}\"` : '')\n const cls = [\n cell.kind === 'value' ? 'is-value' : cell.kind === 'label' ? 'is-label' : 'is-head',\n cell.total && 'is-total',\n !head && c === grid.headCols - 1 && 'is-edge',\n head && c >= grid.headCols && 'is-end',\n ]\n .filter(Boolean)\n .join(' ')\n const scope = tag === 'th' && !head ? ' scope=\"row\"' : ''\n const align = cell.kind === 'value' ? ' style=\"text-align:right\"' : ''\n out += `<${tag}${span}${scope} class=\"${cls}\"${align}>${escapeHTML(cell.text)}</${tag}>`\n })\n return `<tr>${out}</tr>`\n }\n const head = grid.cells.slice(0, grid.headRows).map((r) => row(r, true))\n const body = grid.cells.slice(grid.headRows).map((r) => row(r, false))\n const mark = sheet\n ? ` ${SHEET_MARK}=\"${escapeHTML(JSON.stringify({ cells: gridToSheetCells(grid), styles: SHEET_STYLES }))}\"`\n : ''\n return (\n `<table${mark} style=\"border-collapse:collapse\">` +\n (head.length ? `<thead>${head.join('')}</thead>` : '') +\n `<tbody>${body.join('')}</tbody></table>`\n )\n}\n\n/** A sheet number format that writes numbers like these `Intl.NumberFormat` options. */\nexport function sheetFormat(options: Intl.NumberFormatOptions, locale: string): string | undefined {\n const style = options.style ?? 'decimal'\n if (style === 'unit') return undefined\n const currency = style === 'currency'\n const percent = style === 'percent'\n // like Intl: a maximum below the default minimum lowers the minimum\n const ownMax = options.maximumFractionDigits\n const min = options.minimumFractionDigits ?? Math.min(currency ? 2 : 0, ownMax ?? Infinity)\n const max = Math.max(min, ownMax ?? (currency ? 2 : percent ? 0 : 3))\n const grouping = options.useGrouping !== false && (options.useGrouping as unknown) !== 'false'\n let body = grouping ? '#,##0' : '0'\n if (max > 0) body += `.${'0'.repeat(min)}${'#'.repeat(max - min)}`\n if (percent) body += '%'\n if (currency) {\n let symbol = ''\n try {\n symbol =\n new Intl.NumberFormat(locale, options).formatToParts(1).find((p) => p.type === 'currency')\n ?.value ?? ''\n } catch {\n return undefined\n }\n body = `\"${symbol.replaceAll('\"', '')}\"${body}`\n }\n // the sign shown on positive numbers too\n if (options.signDisplay === 'exceptZero' || options.signDisplay === 'always')\n return `+${body};-${body};${body}`\n return body\n}\n"],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ // src/engine/aggregate.ts
2
+ /** Declares a way to sum up a field, for `aggregators` and the config's `aggregate`. */
3
+ function defineAggregator(aggregator) {
4
+ return {
5
+ input: "number",
6
+ ...aggregator
7
+ };
8
+ }
9
+ var FAST_AGGREGATES = /* @__PURE__ */ new Set([
10
+ "sum",
11
+ "count",
12
+ "avg",
13
+ "min",
14
+ "max"
15
+ ]);
16
+ /** Counts each value once; the state holds group numbers, so it never compares values. */
17
+ var distinctAggregator = {
18
+ name: "distinct",
19
+ input: "value",
20
+ integer: true,
21
+ init: () => /* @__PURE__ */ new Set(),
22
+ add: (state, code) => state.add(code),
23
+ merge: (into, from) => {
24
+ for (const code of from) into.add(code);
25
+ return into;
26
+ },
27
+ result: (state) => state.size
28
+ };
29
+ var medianAggregator = {
30
+ name: "median",
31
+ input: "number",
32
+ init: () => [],
33
+ add: (state, value) => (state.push(value), state),
34
+ merge: (into, from) => {
35
+ for (let i = 0; i < from.length; i++) into.push(from[i]);
36
+ return into;
37
+ },
38
+ result: (state) => {
39
+ if (state.length === 0) return null;
40
+ const sorted = Float64Array.from(state).sort();
41
+ const mid = sorted.length >> 1;
42
+ return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
43
+ }
44
+ };
45
+ var BUILTIN_AGGREGATORS = {
46
+ distinct: distinctAggregator,
47
+ median: medianAggregator
48
+ };
49
+ export { BUILTIN_AGGREGATORS, FAST_AGGREGATES, defineAggregator, distinctAggregator, medianAggregator };
50
+
51
+ //# sourceMappingURL=aggregate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aggregate.js","names":[],"sources":["../../src/engine/aggregate.ts"],"sourcesContent":["/**\n * A way to sum up a field. A state is built per group of records: `add`\n * takes each record's value, `merge` joins two groups' states (subtotals and\n * totals are merged from the groups inside them, never re-read from the\n * records), and `result` turns a state into the number shown.\n */\n// oxlint-disable-next-line typescript/no-explicit-any\nexport interface PivotAggregator<State = any> {\n name: string\n /** The header word, as in \"金额 · 求和\"; the name when left out. */\n label?: string\n /**\n * What `add` gets: `'number'` the field as a number (records without one\n * are skipped), `'value'` the field's value as it is (empty ones skipped).\n */\n input: 'number' | 'value'\n init: () => State\n add: (state: State, value: never) => State\n merge: (into: State, from: State) => State\n /** `null` or `NaN` for an empty cell. */\n result: (state: State) => number | null\n /** Whether a value is an integer count (written without decimals); `false` by default. */\n integer?: boolean\n}\n\n/** Declares a way to sum up a field, for `aggregators` and the config's `aggregate`. */\nexport function defineAggregator<State, Input extends 'number' | 'value' = 'number'>(\n aggregator: Omit<PivotAggregator<State>, 'add' | 'input'> & {\n input?: Input\n add: (state: State, value: Input extends 'number' ? number : unknown) => State\n },\n): PivotAggregator<State> {\n return { input: 'number', ...aggregator } as PivotAggregator<State>\n}\n\n/** The built-in ways with a typed-array fast path: one number (or two, for `avg`) per group. */\nexport type PivotFastAggregate = 'sum' | 'count' | 'avg' | 'min' | 'max'\n\nexport const FAST_AGGREGATES: ReadonlySet<string> = new Set(['sum', 'count', 'avg', 'min', 'max'])\n\n/** Counts each value once; the state holds group numbers, so it never compares values. */\nexport const distinctAggregator: PivotAggregator<Set<number>> = {\n name: 'distinct',\n input: 'value',\n integer: true,\n init: () => new Set(),\n add: (state, code: never) => state.add(code),\n merge: (into, from) => {\n for (const code of from) into.add(code)\n return into\n },\n result: (state) => state.size,\n}\n\nexport const medianAggregator: PivotAggregator<number[]> = {\n name: 'median',\n input: 'number',\n init: () => [],\n add: (state, value: never) => (state.push(value), state),\n merge: (into, from) => {\n for (let i = 0; i < from.length; i++) into.push(from[i]!)\n return into\n },\n result: (state) => {\n if (state.length === 0) return null\n // oxlint-disable-next-line unicorn/no-array-sort -- sorts its own copy\n const sorted = Float64Array.from(state).sort()\n const mid = sorted.length >> 1\n return sorted.length % 2 ? sorted[mid]! : (sorted[mid - 1]! + sorted[mid]!) / 2\n },\n}\n\nexport const BUILTIN_AGGREGATORS: Readonly<Record<string, PivotAggregator>> = {\n distinct: distinctAggregator as PivotAggregator,\n median: medianAggregator as PivotAggregator,\n}\n"],"mappings":";;AA0BA,SAAgB,iBACd,YAIwB;CACxB,OAAO;EAAE,OAAO;EAAU,GAAG;CAAW;AAC1C;AAKA,IAAa,kCAAuC,IAAI,IAAI;CAAC;CAAO;CAAS;CAAO;CAAO;AAAK,CAAC;;AAGjG,IAAa,qBAAmD;CAC9D,MAAM;CACN,OAAO;CACP,SAAS;CACT,4BAAY,IAAI,IAAI;CACpB,MAAM,OAAO,SAAgB,MAAM,IAAI,IAAI;CAC3C,QAAQ,MAAM,SAAS;EACrB,KAAK,MAAM,QAAQ,MAAM,KAAK,IAAI,IAAI;EACtC,OAAO;CACT;CACA,SAAS,UAAU,MAAM;AAC3B;AAEA,IAAa,mBAA8C;CACzD,MAAM;CACN,OAAO;CACP,YAAY,CAAC;CACb,MAAM,OAAO,WAAkB,MAAM,KAAK,KAAK,GAAG;CAClD,QAAQ,MAAM,SAAS;EACrB,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK,KAAK,KAAK,KAAK,EAAG;EACxD,OAAO;CACT;CACA,SAAS,UAAU;EACjB,IAAI,MAAM,WAAW,GAAG,OAAO;EAE/B,MAAM,SAAS,aAAa,KAAK,KAAK,CAAC,CAAC,KAAK;EAC7C,MAAM,MAAM,OAAO,UAAU;EAC7B,OAAO,OAAO,SAAS,IAAI,OAAO,QAAS,OAAO,MAAM,KAAM,OAAO,QAAS;CAChF;AACF;AAEA,IAAa,sBAAiE;CAC5E,UAAU;CACV,QAAQ;AACV"}