@cj-tech-master/excelts 2.0.1-canary.20251228093548.379d895 → 3.0.0-canary.20251228183403.d3eb98d

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.
@@ -114,7 +114,7 @@ function makePivotTable(worksheet, model) {
114
114
  validate(worksheet, model, source);
115
115
  const { rows, values } = model;
116
116
  const columns = model.columns ?? [];
117
- const cacheFields = makeCacheFields(source, [...rows, ...columns]);
117
+ const cacheFields = makeCacheFields(source, [...rows, ...columns], values);
118
118
  const nameToIndex = cacheFields.reduce((result, cacheField, index) => {
119
119
  result[cacheField.name] = index;
120
120
  return result;
@@ -171,13 +171,15 @@ function validate(_worksheet, model, source) {
171
171
  throw new Error("It is currently not possible to have multiple values when columns are specified. Please either supply an empty array for columns or a single value.");
172
172
  }
173
173
  }
174
- function makeCacheFields(source, fieldNamesWithSharedItems) {
174
+ function makeCacheFields(source, fieldNamesWithSharedItems, valueFieldNames) {
175
175
  // Cache fields are used in pivot tables to reference source data.
176
176
  // Fields in fieldNamesWithSharedItems get their unique values extracted as sharedItems.
177
- // Other fields (typically numeric) have sharedItems = null.
177
+ // Fields in valueFieldNames (but not in fieldNamesWithSharedItems) get min/max calculated.
178
+ // Other fields are unused and get empty sharedItems.
178
179
  const names = source.getRow(1).values;
179
180
  // Use Set for O(1) lookup instead of object
180
181
  const sharedItemsFields = new Set(fieldNamesWithSharedItems);
182
+ const valueFields = new Set(valueFieldNames);
181
183
  const aggregate = (columnIndex) => {
182
184
  const columnValues = source.getColumn(columnIndex).values;
183
185
  // Build unique values set directly, skipping header (index 0,1) and null/undefined
@@ -215,10 +217,11 @@ function makeCacheFields(source, fieldNamesWithSharedItems) {
215
217
  for (const columnIndex of (0, utils_1.range)(1, names.length)) {
216
218
  const name = names[columnIndex];
217
219
  if (sharedItemsFields.has(name)) {
220
+ // Field used for rows/columns - extract unique values as sharedItems
218
221
  result.push({ name, sharedItems: aggregate(columnIndex) });
219
222
  }
220
- else {
221
- // Numeric field - calculate min/max
223
+ else if (valueFields.has(name)) {
224
+ // Field used only for values (aggregation) - calculate min/max
222
225
  const minMax = getMinMax(columnIndex);
223
226
  result.push({
224
227
  name,
@@ -227,6 +230,10 @@ function makeCacheFields(source, fieldNamesWithSharedItems) {
227
230
  maxValue: minMax?.maxValue
228
231
  });
229
232
  }
233
+ else {
234
+ // Unused field - just empty sharedItems (like Excel does)
235
+ result.push({ name, sharedItems: null });
236
+ }
230
237
  }
231
238
  return result;
232
239
  }
@@ -47,28 +47,24 @@ class CacheFieldXform extends base_xform_1.BaseXform {
47
47
  break;
48
48
  case "sharedItems":
49
49
  this.inSharedItems = true;
50
- // Check if this is a numeric field (no string items)
51
- if (attributes.containsNumber === "1" || attributes.containsInteger === "1") {
52
- if (this.model) {
53
- this.model.containsNumber = attributes.containsNumber === "1";
54
- this.model.containsInteger = attributes.containsInteger === "1";
55
- if (attributes.minValue !== undefined) {
56
- this.model.minValue = parseFloat(attributes.minValue);
57
- }
58
- if (attributes.maxValue !== undefined) {
59
- this.model.maxValue = parseFloat(attributes.maxValue);
60
- }
61
- // Numeric fields have sharedItems = null
62
- this.model.sharedItems = null;
50
+ // Store numeric field metadata
51
+ if (this.model) {
52
+ this.model.containsNumber = attributes.containsNumber === "1";
53
+ this.model.containsInteger = attributes.containsInteger === "1";
54
+ if (attributes.minValue !== undefined) {
55
+ this.model.minValue = parseFloat(attributes.minValue);
63
56
  }
64
- }
65
- else {
66
- // String field - initialize sharedItems array if count > 0
67
- if (this.model) {
68
- const count = parseInt(attributes.count || "0", 10);
69
- if (count > 0) {
70
- this.model.sharedItems = [];
71
- }
57
+ if (attributes.maxValue !== undefined) {
58
+ this.model.maxValue = parseFloat(attributes.maxValue);
59
+ }
60
+ // Initialize sharedItems array if count > 0 (for both string and numeric fields)
61
+ const count = parseInt(attributes.count || "0", 10);
62
+ if (count > 0) {
63
+ this.model.sharedItems = [];
64
+ }
65
+ else {
66
+ // No count means no individual items (pure numeric field)
67
+ this.model.sharedItems = null;
72
68
  }
73
69
  }
74
70
  break;
@@ -13,7 +13,7 @@ class CacheField {
13
13
  //
14
14
  // or
15
15
  //
16
- // integer type
16
+ // integer type (no sharedItems)
17
17
  //
18
18
  // {
19
19
  // 'name': 'D',
@@ -21,6 +21,15 @@ class CacheField {
21
21
  // 'minValue': 5,
22
22
  // 'maxValue': 45
23
23
  // }
24
+ //
25
+ // or
26
+ //
27
+ // numeric type with shared items (used as both row/column and value field)
28
+ //
29
+ // {
30
+ // 'name': 'C',
31
+ // 'sharedItems': [5, 24, 35, 45]
32
+ // }
24
33
  this.name = name;
25
34
  this.sharedItems = sharedItems;
26
35
  this.minValue = minValue;
@@ -31,17 +40,37 @@ class CacheField {
31
40
  // Shared Items: http://www.datypic.com/sc/ooxml/e-ssml_sharedItems-1.html
32
41
  // Escape XML special characters in name attribute
33
42
  const escapedName = (0, utils_1.xmlEncode)(this.name);
34
- // integer types
43
+ // No shared items - field not used for rows/columns
35
44
  if (this.sharedItems === null) {
36
- // Build minValue/maxValue attributes if available
37
- const minMaxAttrs = this.minValue !== undefined && this.maxValue !== undefined
38
- ? ` minValue="${this.minValue}" maxValue="${this.maxValue}"`
39
- : "";
45
+ // If no minValue/maxValue, this is an unused field - use empty sharedItems like Excel does
46
+ if (this.minValue === undefined || this.maxValue === undefined) {
47
+ return `<cacheField name="${escapedName}" numFmtId="0">
48
+ <sharedItems />
49
+ </cacheField>`;
50
+ }
51
+ // Numeric field used only for values (not rows/columns) - include min/max
40
52
  return `<cacheField name="${escapedName}" numFmtId="0">
41
- <sharedItems containsSemiMixedTypes="0" containsString="0" containsNumber="1" containsInteger="1"${minMaxAttrs} />
53
+ <sharedItems containsSemiMixedTypes="0" containsString="0" containsNumber="1" containsInteger="1" minValue="${this.minValue}" maxValue="${this.maxValue}" />
54
+ </cacheField>`;
55
+ }
56
+ // Shared items exist - check if all values are numeric
57
+ // Note: empty array returns true for every(), so check length first
58
+ const allNumeric = this.sharedItems.length > 0 &&
59
+ this.sharedItems.every(item => typeof item === "number" && Number.isFinite(item));
60
+ const allInteger = allNumeric && this.sharedItems.every(item => Number.isInteger(item));
61
+ if (allNumeric) {
62
+ // Numeric shared items - used when field is both a row/column field AND a value field
63
+ // This allows Excel to both group by unique values AND perform aggregation
64
+ const minValue = Math.min(...this.sharedItems);
65
+ const maxValue = Math.max(...this.sharedItems);
66
+ const integerAttr = allInteger ? ' containsInteger="1"' : "";
67
+ return `<cacheField name="${escapedName}" numFmtId="0">
68
+ <sharedItems containsSemiMixedTypes="0" containsString="0" containsNumber="1"${integerAttr} minValue="${minValue}" maxValue="${maxValue}" count="${this.sharedItems.length}">
69
+ ${this.sharedItems.map(item => `<n v="${item}" />`).join("")}
70
+ </sharedItems>
42
71
  </cacheField>`;
43
72
  }
44
- // string types - escape XML special characters in each shared item value
73
+ // String shared items - escape XML special characters in each value
45
74
  return `<cacheField name="${escapedName}" numFmtId="0">
46
75
  <sharedItems count="${this.sharedItems.length}">
47
76
  ${this.sharedItems.map(item => `<s v="${(0, utils_1.xmlEncode)(String(item))}" />`).join("")}
@@ -119,7 +119,7 @@ class PivotCacheRecordsXform extends base_xform_1.BaseXform {
119
119
  }
120
120
  return `<s v="${(0, utils_1.xmlEncode)(String(value))}" />`;
121
121
  }
122
- // shared items
122
+ // shared items - use indexOf for value lookup (works for both string and numeric)
123
123
  const sharedItemsIndex = sharedItems.indexOf(value);
124
124
  if (sharedItemsIndex < 0) {
125
125
  throw new Error(`${JSON.stringify(value)} not in sharedItems ${JSON.stringify(sharedItems)}`);
@@ -585,25 +585,24 @@ function renderPivotFields(pivotTable) {
585
585
  const valueSet = new Set(pivotTable.values);
586
586
  return pivotTable.cacheFields
587
587
  .map((cacheField, fieldIndex) => {
588
- const fieldType = rowSet.has(fieldIndex)
589
- ? "row"
590
- : colSet.has(fieldIndex)
591
- ? "column"
592
- : valueSet.has(fieldIndex)
593
- ? "value"
594
- : null;
595
- return renderPivotField(fieldType, cacheField.sharedItems);
588
+ const isRow = rowSet.has(fieldIndex);
589
+ const isCol = colSet.has(fieldIndex);
590
+ const isValue = valueSet.has(fieldIndex);
591
+ return renderPivotField(isRow, isCol, isValue, cacheField.sharedItems);
596
592
  })
597
593
  .join("");
598
594
  }
599
- function renderPivotField(fieldType, sharedItems) {
600
- // fieldType: 'row', 'column', 'value', null
601
- // Note: defaultSubtotal="0" should only be on value fields and non-axis fields,
602
- // NOT on row/column axis fields (Excel will auto-calculate subtotals for them)
603
- if (fieldType === "row" || fieldType === "column") {
604
- const axis = fieldType === "row" ? "axisRow" : "axisCol";
595
+ function renderPivotField(isRow, isCol, isValue, sharedItems) {
596
+ // A field can be both a row/column field AND a value field (issue #15)
597
+ // In this case, it needs both axis attribute AND dataField="1"
598
+ if (isRow || isCol) {
599
+ const axis = isRow ? "axisRow" : "axisCol";
605
600
  // Row and column fields should NOT have defaultSubtotal="0"
606
- const axisAttributes = 'compact="0" outline="0" showAll="0"';
601
+ let axisAttributes = 'compact="0" outline="0" showAll="0"';
602
+ // If also a value field, add dataField="1"
603
+ if (isValue) {
604
+ axisAttributes = `dataField="1" ${axisAttributes}`;
605
+ }
607
606
  // items = one for each shared item + one default item
608
607
  const itemsXml = [
609
608
  ...sharedItems.map((_item, index) => `<item x="${index}" />`),
@@ -621,7 +620,7 @@ function renderPivotField(fieldType, sharedItems) {
621
620
  const defaultAttributes = 'compact="0" outline="0" showAll="0" defaultSubtotal="0"';
622
621
  return `
623
622
  <pivotField
624
- ${fieldType === "value" ? 'dataField="1"' : ""}
623
+ ${isValue ? 'dataField="1"' : ""}
625
624
  ${defaultAttributes}
626
625
  />
627
626
  `;
@@ -111,7 +111,7 @@ function makePivotTable(worksheet, model) {
111
111
  validate(worksheet, model, source);
112
112
  const { rows, values } = model;
113
113
  const columns = model.columns ?? [];
114
- const cacheFields = makeCacheFields(source, [...rows, ...columns]);
114
+ const cacheFields = makeCacheFields(source, [...rows, ...columns], values);
115
115
  const nameToIndex = cacheFields.reduce((result, cacheField, index) => {
116
116
  result[cacheField.name] = index;
117
117
  return result;
@@ -168,13 +168,15 @@ function validate(_worksheet, model, source) {
168
168
  throw new Error("It is currently not possible to have multiple values when columns are specified. Please either supply an empty array for columns or a single value.");
169
169
  }
170
170
  }
171
- function makeCacheFields(source, fieldNamesWithSharedItems) {
171
+ function makeCacheFields(source, fieldNamesWithSharedItems, valueFieldNames) {
172
172
  // Cache fields are used in pivot tables to reference source data.
173
173
  // Fields in fieldNamesWithSharedItems get their unique values extracted as sharedItems.
174
- // Other fields (typically numeric) have sharedItems = null.
174
+ // Fields in valueFieldNames (but not in fieldNamesWithSharedItems) get min/max calculated.
175
+ // Other fields are unused and get empty sharedItems.
175
176
  const names = source.getRow(1).values;
176
177
  // Use Set for O(1) lookup instead of object
177
178
  const sharedItemsFields = new Set(fieldNamesWithSharedItems);
179
+ const valueFields = new Set(valueFieldNames);
178
180
  const aggregate = (columnIndex) => {
179
181
  const columnValues = source.getColumn(columnIndex).values;
180
182
  // Build unique values set directly, skipping header (index 0,1) and null/undefined
@@ -212,10 +214,11 @@ function makeCacheFields(source, fieldNamesWithSharedItems) {
212
214
  for (const columnIndex of range(1, names.length)) {
213
215
  const name = names[columnIndex];
214
216
  if (sharedItemsFields.has(name)) {
217
+ // Field used for rows/columns - extract unique values as sharedItems
215
218
  result.push({ name, sharedItems: aggregate(columnIndex) });
216
219
  }
217
- else {
218
- // Numeric field - calculate min/max
220
+ else if (valueFields.has(name)) {
221
+ // Field used only for values (aggregation) - calculate min/max
219
222
  const minMax = getMinMax(columnIndex);
220
223
  result.push({
221
224
  name,
@@ -224,6 +227,10 @@ function makeCacheFields(source, fieldNamesWithSharedItems) {
224
227
  maxValue: minMax?.maxValue
225
228
  });
226
229
  }
230
+ else {
231
+ // Unused field - just empty sharedItems (like Excel does)
232
+ result.push({ name, sharedItems: null });
233
+ }
227
234
  }
228
235
  return result;
229
236
  }
@@ -44,28 +44,24 @@ class CacheFieldXform extends BaseXform {
44
44
  break;
45
45
  case "sharedItems":
46
46
  this.inSharedItems = true;
47
- // Check if this is a numeric field (no string items)
48
- if (attributes.containsNumber === "1" || attributes.containsInteger === "1") {
49
- if (this.model) {
50
- this.model.containsNumber = attributes.containsNumber === "1";
51
- this.model.containsInteger = attributes.containsInteger === "1";
52
- if (attributes.minValue !== undefined) {
53
- this.model.minValue = parseFloat(attributes.minValue);
54
- }
55
- if (attributes.maxValue !== undefined) {
56
- this.model.maxValue = parseFloat(attributes.maxValue);
57
- }
58
- // Numeric fields have sharedItems = null
59
- this.model.sharedItems = null;
47
+ // Store numeric field metadata
48
+ if (this.model) {
49
+ this.model.containsNumber = attributes.containsNumber === "1";
50
+ this.model.containsInteger = attributes.containsInteger === "1";
51
+ if (attributes.minValue !== undefined) {
52
+ this.model.minValue = parseFloat(attributes.minValue);
60
53
  }
61
- }
62
- else {
63
- // String field - initialize sharedItems array if count > 0
64
- if (this.model) {
65
- const count = parseInt(attributes.count || "0", 10);
66
- if (count > 0) {
67
- this.model.sharedItems = [];
68
- }
54
+ if (attributes.maxValue !== undefined) {
55
+ this.model.maxValue = parseFloat(attributes.maxValue);
56
+ }
57
+ // Initialize sharedItems array if count > 0 (for both string and numeric fields)
58
+ const count = parseInt(attributes.count || "0", 10);
59
+ if (count > 0) {
60
+ this.model.sharedItems = [];
61
+ }
62
+ else {
63
+ // No count means no individual items (pure numeric field)
64
+ this.model.sharedItems = null;
69
65
  }
70
66
  }
71
67
  break;
@@ -10,7 +10,7 @@ class CacheField {
10
10
  //
11
11
  // or
12
12
  //
13
- // integer type
13
+ // integer type (no sharedItems)
14
14
  //
15
15
  // {
16
16
  // 'name': 'D',
@@ -18,6 +18,15 @@ class CacheField {
18
18
  // 'minValue': 5,
19
19
  // 'maxValue': 45
20
20
  // }
21
+ //
22
+ // or
23
+ //
24
+ // numeric type with shared items (used as both row/column and value field)
25
+ //
26
+ // {
27
+ // 'name': 'C',
28
+ // 'sharedItems': [5, 24, 35, 45]
29
+ // }
21
30
  this.name = name;
22
31
  this.sharedItems = sharedItems;
23
32
  this.minValue = minValue;
@@ -28,17 +37,37 @@ class CacheField {
28
37
  // Shared Items: http://www.datypic.com/sc/ooxml/e-ssml_sharedItems-1.html
29
38
  // Escape XML special characters in name attribute
30
39
  const escapedName = xmlEncode(this.name);
31
- // integer types
40
+ // No shared items - field not used for rows/columns
32
41
  if (this.sharedItems === null) {
33
- // Build minValue/maxValue attributes if available
34
- const minMaxAttrs = this.minValue !== undefined && this.maxValue !== undefined
35
- ? ` minValue="${this.minValue}" maxValue="${this.maxValue}"`
36
- : "";
42
+ // If no minValue/maxValue, this is an unused field - use empty sharedItems like Excel does
43
+ if (this.minValue === undefined || this.maxValue === undefined) {
44
+ return `<cacheField name="${escapedName}" numFmtId="0">
45
+ <sharedItems />
46
+ </cacheField>`;
47
+ }
48
+ // Numeric field used only for values (not rows/columns) - include min/max
37
49
  return `<cacheField name="${escapedName}" numFmtId="0">
38
- <sharedItems containsSemiMixedTypes="0" containsString="0" containsNumber="1" containsInteger="1"${minMaxAttrs} />
50
+ <sharedItems containsSemiMixedTypes="0" containsString="0" containsNumber="1" containsInteger="1" minValue="${this.minValue}" maxValue="${this.maxValue}" />
51
+ </cacheField>`;
52
+ }
53
+ // Shared items exist - check if all values are numeric
54
+ // Note: empty array returns true for every(), so check length first
55
+ const allNumeric = this.sharedItems.length > 0 &&
56
+ this.sharedItems.every(item => typeof item === "number" && Number.isFinite(item));
57
+ const allInteger = allNumeric && this.sharedItems.every(item => Number.isInteger(item));
58
+ if (allNumeric) {
59
+ // Numeric shared items - used when field is both a row/column field AND a value field
60
+ // This allows Excel to both group by unique values AND perform aggregation
61
+ const minValue = Math.min(...this.sharedItems);
62
+ const maxValue = Math.max(...this.sharedItems);
63
+ const integerAttr = allInteger ? ' containsInteger="1"' : "";
64
+ return `<cacheField name="${escapedName}" numFmtId="0">
65
+ <sharedItems containsSemiMixedTypes="0" containsString="0" containsNumber="1"${integerAttr} minValue="${minValue}" maxValue="${maxValue}" count="${this.sharedItems.length}">
66
+ ${this.sharedItems.map(item => `<n v="${item}" />`).join("")}
67
+ </sharedItems>
39
68
  </cacheField>`;
40
69
  }
41
- // string types - escape XML special characters in each shared item value
70
+ // String shared items - escape XML special characters in each value
42
71
  return `<cacheField name="${escapedName}" numFmtId="0">
43
72
  <sharedItems count="${this.sharedItems.length}">
44
73
  ${this.sharedItems.map(item => `<s v="${xmlEncode(String(item))}" />`).join("")}
@@ -116,7 +116,7 @@ class PivotCacheRecordsXform extends BaseXform {
116
116
  }
117
117
  return `<s v="${xmlEncode(String(value))}" />`;
118
118
  }
119
- // shared items
119
+ // shared items - use indexOf for value lookup (works for both string and numeric)
120
120
  const sharedItemsIndex = sharedItems.indexOf(value);
121
121
  if (sharedItemsIndex < 0) {
122
122
  throw new Error(`${JSON.stringify(value)} not in sharedItems ${JSON.stringify(sharedItems)}`);
@@ -581,25 +581,24 @@ function renderPivotFields(pivotTable) {
581
581
  const valueSet = new Set(pivotTable.values);
582
582
  return pivotTable.cacheFields
583
583
  .map((cacheField, fieldIndex) => {
584
- const fieldType = rowSet.has(fieldIndex)
585
- ? "row"
586
- : colSet.has(fieldIndex)
587
- ? "column"
588
- : valueSet.has(fieldIndex)
589
- ? "value"
590
- : null;
591
- return renderPivotField(fieldType, cacheField.sharedItems);
584
+ const isRow = rowSet.has(fieldIndex);
585
+ const isCol = colSet.has(fieldIndex);
586
+ const isValue = valueSet.has(fieldIndex);
587
+ return renderPivotField(isRow, isCol, isValue, cacheField.sharedItems);
592
588
  })
593
589
  .join("");
594
590
  }
595
- function renderPivotField(fieldType, sharedItems) {
596
- // fieldType: 'row', 'column', 'value', null
597
- // Note: defaultSubtotal="0" should only be on value fields and non-axis fields,
598
- // NOT on row/column axis fields (Excel will auto-calculate subtotals for them)
599
- if (fieldType === "row" || fieldType === "column") {
600
- const axis = fieldType === "row" ? "axisRow" : "axisCol";
591
+ function renderPivotField(isRow, isCol, isValue, sharedItems) {
592
+ // A field can be both a row/column field AND a value field (issue #15)
593
+ // In this case, it needs both axis attribute AND dataField="1"
594
+ if (isRow || isCol) {
595
+ const axis = isRow ? "axisRow" : "axisCol";
601
596
  // Row and column fields should NOT have defaultSubtotal="0"
602
- const axisAttributes = 'compact="0" outline="0" showAll="0"';
597
+ let axisAttributes = 'compact="0" outline="0" showAll="0"';
598
+ // If also a value field, add dataField="1"
599
+ if (isValue) {
600
+ axisAttributes = `dataField="1" ${axisAttributes}`;
601
+ }
603
602
  // items = one for each shared item + one default item
604
603
  const itemsXml = [
605
604
  ...sharedItems.map((_item, index) => `<item x="${index}" />`),
@@ -617,7 +616,7 @@ function renderPivotField(fieldType, sharedItems) {
617
616
  const defaultAttributes = 'compact="0" outline="0" showAll="0" defaultSubtotal="0"';
618
617
  return `
619
618
  <pivotField
620
- ${fieldType === "value" ? 'dataField="1"' : ""}
619
+ ${isValue ? 'dataField="1"' : ""}
621
620
  ${defaultAttributes}
622
621
  />
623
622
  `;
@@ -4,7 +4,7 @@ import { BaseXform } from "../base-xform";
4
4
  */
5
5
  interface CacheFieldModel {
6
6
  name: string;
7
- sharedItems: string[] | null;
7
+ sharedItems: any[] | null;
8
8
  containsNumber?: boolean;
9
9
  containsInteger?: boolean;
10
10
  minValue?: number;
@@ -1,6 +1,6 @@
1
1
  interface CacheFieldConfig {
2
2
  name: string;
3
- sharedItems: string[] | null;
3
+ sharedItems: any[] | null;
4
4
  minValue?: number;
5
5
  maxValue?: number;
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cj-tech-master/excelts",
3
- "version": "2.0.1-canary.20251228093548.379d895",
3
+ "version": "3.0.0-canary.20251228183403.d3eb98d",
4
4
  "description": "TypeScript Excel Workbook Manager - Read and Write xlsx and csv Files.",
5
5
  "type": "module",
6
6
  "publishConfig": {