@rowsncolumns/spreadsheet-state 29.1.4 → 29.1.7

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 (225) hide show
  1. package/dist/cjs/api/circular-reference.d.ts +31 -0
  2. package/dist/cjs/api/circular-reference.d.ts.map +1 -0
  3. package/dist/cjs/api/circular-reference.js +85 -0
  4. package/dist/cjs/api/copy-paste.d.ts +6 -0
  5. package/dist/cjs/api/copy-paste.d.ts.map +1 -1
  6. package/dist/cjs/api/copy-paste.js +24 -15
  7. package/dist/cjs/api/datatype.d.ts +2 -0
  8. package/dist/cjs/api/datatype.d.ts.map +1 -1
  9. package/dist/cjs/api/datatype.js +61 -35
  10. package/dist/cjs/api/date.d.ts.map +1 -1
  11. package/dist/cjs/api/date.js +23 -14
  12. package/dist/cjs/api/decimals.d.ts +10 -0
  13. package/dist/cjs/api/decimals.d.ts.map +1 -1
  14. package/dist/cjs/api/decimals.js +34 -11
  15. package/dist/cjs/api/dependency.d.ts +19 -1
  16. package/dist/cjs/api/dependency.d.ts.map +1 -1
  17. package/dist/cjs/api/dependency.js +30 -14
  18. package/dist/cjs/api/filter-condition.d.ts +58 -0
  19. package/dist/cjs/api/filter-condition.d.ts.map +1 -0
  20. package/dist/cjs/api/filter-condition.js +157 -0
  21. package/dist/cjs/api/formula.d.ts.map +1 -1
  22. package/dist/cjs/api/formula.js +9 -7
  23. package/dist/cjs/api/image-embed.d.ts +41 -0
  24. package/dist/cjs/api/image-embed.d.ts.map +1 -0
  25. package/dist/cjs/api/image-embed.js +55 -0
  26. package/dist/cjs/api/immer-utils.d.ts.map +1 -1
  27. package/dist/cjs/api/immer-utils.js +43 -15
  28. package/dist/cjs/api/named-range-structural.d.ts +29 -3
  29. package/dist/cjs/api/named-range-structural.d.ts.map +1 -1
  30. package/dist/cjs/api/named-range-structural.js +36 -11
  31. package/dist/cjs/api/parse-html.d.ts.map +1 -1
  32. package/dist/cjs/api/parse-html.js +11 -5
  33. package/dist/cjs/api/structural-formula-update.d.ts +20 -1
  34. package/dist/cjs/api/structural-formula-update.d.ts.map +1 -1
  35. package/dist/cjs/api/structural-formula-update.js +264 -90
  36. package/dist/cjs/api/structural-guards.d.ts +11 -0
  37. package/dist/cjs/api/structural-guards.d.ts.map +1 -1
  38. package/dist/cjs/api/structural-guards.js +45 -1
  39. package/dist/cjs/api/structural-table-refs.d.ts +15 -2
  40. package/dist/cjs/api/structural-table-refs.d.ts.map +1 -1
  41. package/dist/cjs/api/structural-table-refs.js +79 -35
  42. package/dist/cjs/commands.d.ts +17 -3
  43. package/dist/cjs/commands.d.ts.map +1 -1
  44. package/dist/cjs/components/editors/conditional-format-rule-editor.d.ts.map +1 -1
  45. package/dist/cjs/components/editors/conditional-format-rule-editor.js +19 -7
  46. package/dist/cjs/constants.d.ts +4 -1
  47. package/dist/cjs/constants.d.ts.map +1 -1
  48. package/dist/cjs/constants.js +4 -0
  49. package/dist/cjs/helpers/sheet-title.d.ts +2 -1
  50. package/dist/cjs/helpers/sheet-title.d.ts.map +1 -1
  51. package/dist/cjs/helpers/sheet-title.js +3 -1
  52. package/dist/cjs/hooks/use-can-edit-cellrange.d.ts +1 -1
  53. package/dist/cjs/hooks/use-can-edit-cellrange.d.ts.map +1 -1
  54. package/dist/cjs/hooks/use-can-edit-cellrange.js +1 -1
  55. package/dist/cjs/hooks/use-on-calculation-success.d.ts +5 -1
  56. package/dist/cjs/hooks/use-on-calculation-success.d.ts.map +1 -1
  57. package/dist/cjs/hooks/use-on-calculation-success.js +20 -3
  58. package/dist/cjs/hooks/use-on-copy.d.ts.map +1 -1
  59. package/dist/cjs/hooks/use-on-copy.js +23 -25
  60. package/dist/cjs/hooks/use-on-create-sheet.d.ts.map +1 -1
  61. package/dist/cjs/hooks/use-on-create-sheet.js +11 -1
  62. package/dist/cjs/hooks/use-on-delete.d.ts.map +1 -1
  63. package/dist/cjs/hooks/use-on-delete.js +15 -3
  64. package/dist/cjs/hooks/use-on-drag-n-drop.d.ts +5 -0
  65. package/dist/cjs/hooks/use-on-drag-n-drop.d.ts.map +1 -1
  66. package/dist/cjs/hooks/use-on-drag-n-drop.js +6 -5
  67. package/dist/cjs/hooks/use-on-filter-table.d.ts.map +1 -1
  68. package/dist/cjs/hooks/use-on-filter-table.js +6 -1
  69. package/dist/cjs/hooks/use-on-find-replace.d.ts.map +1 -1
  70. package/dist/cjs/hooks/use-on-find-replace.js +111 -23
  71. package/dist/cjs/hooks/use-on-insert-image.d.ts.map +1 -1
  72. package/dist/cjs/hooks/use-on-insert-image.js +20 -33
  73. package/dist/cjs/hooks/use-on-paste.d.ts +2 -1
  74. package/dist/cjs/hooks/use-on-paste.d.ts.map +1 -1
  75. package/dist/cjs/hooks/use-on-paste.js +53 -24
  76. package/dist/cjs/hooks/use-on-rename-sheet.d.ts +3 -1
  77. package/dist/cjs/hooks/use-on-rename-sheet.d.ts.map +1 -1
  78. package/dist/cjs/hooks/use-on-rename-sheet.js +19 -9
  79. package/dist/cjs/hooks/use-on-sort-column.d.ts +7 -1
  80. package/dist/cjs/hooks/use-on-sort-column.d.ts.map +1 -1
  81. package/dist/cjs/hooks/use-on-sort-column.js +25 -8
  82. package/dist/cjs/hooks/use-on-sort-range.d.ts +16 -2
  83. package/dist/cjs/hooks/use-on-sort-range.d.ts.map +1 -1
  84. package/dist/cjs/hooks/use-on-sort-range.js +72 -40
  85. package/dist/cjs/hooks/use-on-sort-table.d.ts +7 -2
  86. package/dist/cjs/hooks/use-on-sort-table.d.ts.map +1 -1
  87. package/dist/cjs/hooks/use-on-sort-table.js +25 -4
  88. package/dist/cjs/hooks/use-spreadsheet-state.d.ts +5 -3
  89. package/dist/cjs/hooks/use-spreadsheet-state.d.ts.map +1 -1
  90. package/dist/cjs/hooks/use-spreadsheet-state.js +7 -0
  91. package/dist/cjs/hooks/utils/sort-rows.d.ts +15 -0
  92. package/dist/cjs/hooks/utils/sort-rows.d.ts.map +1 -1
  93. package/dist/cjs/hooks/utils/sort-rows.js +16 -0
  94. package/dist/cjs/index.d.ts +3 -0
  95. package/dist/cjs/index.d.ts.map +1 -1
  96. package/dist/cjs/index.js +3 -0
  97. package/dist/cjs/interface/delete-column.d.ts.map +1 -1
  98. package/dist/cjs/interface/delete-column.js +254 -101
  99. package/dist/cjs/interface/delete-row.d.ts.map +1 -1
  100. package/dist/cjs/interface/delete-row.js +233 -81
  101. package/dist/cjs/interface/duplicate-sheet.d.ts.map +1 -1
  102. package/dist/cjs/interface/duplicate-sheet.js +4 -9
  103. package/dist/cjs/interface/insert-column.d.ts.map +1 -1
  104. package/dist/cjs/interface/insert-column.js +1 -1
  105. package/dist/cjs/interface/insert-row.d.ts.map +1 -1
  106. package/dist/cjs/interface/insert-row.js +1 -1
  107. package/dist/cjs/interface/new-sheet.d.ts.map +1 -1
  108. package/dist/cjs/interface/new-sheet.js +6 -13
  109. package/dist/cjs/sheet-cell.d.ts +9 -0
  110. package/dist/cjs/sheet-cell.d.ts.map +1 -1
  111. package/dist/cjs/sheet-cell.js +46 -9
  112. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  113. package/dist/esm/api/circular-reference.d.ts +31 -0
  114. package/dist/esm/api/circular-reference.d.ts.map +1 -0
  115. package/dist/esm/api/circular-reference.js +55 -0
  116. package/dist/esm/api/copy-paste.d.ts +6 -0
  117. package/dist/esm/api/copy-paste.d.ts.map +1 -1
  118. package/dist/esm/api/copy-paste.js +26 -14
  119. package/dist/esm/api/datatype.d.ts +2 -0
  120. package/dist/esm/api/datatype.d.ts.map +1 -1
  121. package/dist/esm/api/datatype.js +61 -35
  122. package/dist/esm/api/date.d.ts.map +1 -1
  123. package/dist/esm/api/date.js +23 -14
  124. package/dist/esm/api/decimals.d.ts +10 -0
  125. package/dist/esm/api/decimals.d.ts.map +1 -1
  126. package/dist/esm/api/decimals.js +31 -10
  127. package/dist/esm/api/dependency.d.ts +19 -1
  128. package/dist/esm/api/dependency.d.ts.map +1 -1
  129. package/dist/esm/api/dependency.js +15 -10
  130. package/dist/esm/api/filter-condition.d.ts +58 -0
  131. package/dist/esm/api/filter-condition.d.ts.map +1 -0
  132. package/dist/esm/api/filter-condition.js +146 -0
  133. package/dist/esm/api/formula.d.ts.map +1 -1
  134. package/dist/esm/api/formula.js +9 -7
  135. package/dist/esm/api/image-embed.d.ts +41 -0
  136. package/dist/esm/api/image-embed.d.ts.map +1 -0
  137. package/dist/esm/api/image-embed.js +52 -0
  138. package/dist/esm/api/immer-utils.d.ts.map +1 -1
  139. package/dist/esm/api/immer-utils.js +43 -14
  140. package/dist/esm/api/named-range-structural.d.ts +29 -3
  141. package/dist/esm/api/named-range-structural.d.ts.map +1 -1
  142. package/dist/esm/api/named-range-structural.js +35 -11
  143. package/dist/esm/api/parse-html.d.ts.map +1 -1
  144. package/dist/esm/api/parse-html.js +14 -5
  145. package/dist/esm/api/structural-formula-update.d.ts +20 -1
  146. package/dist/esm/api/structural-formula-update.d.ts.map +1 -1
  147. package/dist/esm/api/structural-formula-update.js +151 -55
  148. package/dist/esm/api/structural-guards.d.ts +11 -0
  149. package/dist/esm/api/structural-guards.d.ts.map +1 -1
  150. package/dist/esm/api/structural-guards.js +31 -0
  151. package/dist/esm/api/structural-table-refs.d.ts +15 -2
  152. package/dist/esm/api/structural-table-refs.d.ts.map +1 -1
  153. package/dist/esm/api/structural-table-refs.js +49 -18
  154. package/dist/esm/commands.d.ts +17 -3
  155. package/dist/esm/commands.d.ts.map +1 -1
  156. package/dist/esm/components/editors/conditional-format-rule-editor.d.ts.map +1 -1
  157. package/dist/esm/components/editors/conditional-format-rule-editor.js +18 -7
  158. package/dist/esm/constants.d.ts +4 -1
  159. package/dist/esm/constants.d.ts.map +1 -1
  160. package/dist/esm/constants.js +4 -0
  161. package/dist/esm/helpers/sheet-title.d.ts +2 -1
  162. package/dist/esm/helpers/sheet-title.d.ts.map +1 -1
  163. package/dist/esm/helpers/sheet-title.js +3 -1
  164. package/dist/esm/hooks/use-can-edit-cellrange.d.ts +1 -1
  165. package/dist/esm/hooks/use-can-edit-cellrange.d.ts.map +1 -1
  166. package/dist/esm/hooks/use-can-edit-cellrange.js +1 -1
  167. package/dist/esm/hooks/use-on-calculation-success.d.ts +5 -1
  168. package/dist/esm/hooks/use-on-calculation-success.d.ts.map +1 -1
  169. package/dist/esm/hooks/use-on-calculation-success.js +19 -3
  170. package/dist/esm/hooks/use-on-copy.d.ts.map +1 -1
  171. package/dist/esm/hooks/use-on-copy.js +17 -17
  172. package/dist/esm/hooks/use-on-create-sheet.d.ts.map +1 -1
  173. package/dist/esm/hooks/use-on-create-sheet.js +11 -1
  174. package/dist/esm/hooks/use-on-delete.d.ts.map +1 -1
  175. package/dist/esm/hooks/use-on-delete.js +15 -3
  176. package/dist/esm/hooks/use-on-drag-n-drop.d.ts +5 -0
  177. package/dist/esm/hooks/use-on-drag-n-drop.d.ts.map +1 -1
  178. package/dist/esm/hooks/use-on-drag-n-drop.js +7 -4
  179. package/dist/esm/hooks/use-on-filter-table.d.ts.map +1 -1
  180. package/dist/esm/hooks/use-on-filter-table.js +13 -2
  181. package/dist/esm/hooks/use-on-find-replace.d.ts.map +1 -1
  182. package/dist/esm/hooks/use-on-find-replace.js +111 -23
  183. package/dist/esm/hooks/use-on-insert-image.d.ts.map +1 -1
  184. package/dist/esm/hooks/use-on-insert-image.js +15 -25
  185. package/dist/esm/hooks/use-on-paste.d.ts +2 -1
  186. package/dist/esm/hooks/use-on-paste.d.ts.map +1 -1
  187. package/dist/esm/hooks/use-on-paste.js +46 -15
  188. package/dist/esm/hooks/use-on-rename-sheet.d.ts +3 -1
  189. package/dist/esm/hooks/use-on-rename-sheet.d.ts.map +1 -1
  190. package/dist/esm/hooks/use-on-rename-sheet.js +19 -9
  191. package/dist/esm/hooks/use-on-sort-column.d.ts +7 -1
  192. package/dist/esm/hooks/use-on-sort-column.d.ts.map +1 -1
  193. package/dist/esm/hooks/use-on-sort-column.js +18 -1
  194. package/dist/esm/hooks/use-on-sort-range.d.ts +16 -2
  195. package/dist/esm/hooks/use-on-sort-range.d.ts.map +1 -1
  196. package/dist/esm/hooks/use-on-sort-range.js +30 -3
  197. package/dist/esm/hooks/use-on-sort-table.d.ts +7 -2
  198. package/dist/esm/hooks/use-on-sort-table.d.ts.map +1 -1
  199. package/dist/esm/hooks/use-on-sort-table.js +25 -4
  200. package/dist/esm/hooks/use-spreadsheet-state.d.ts +5 -3
  201. package/dist/esm/hooks/use-spreadsheet-state.d.ts.map +1 -1
  202. package/dist/esm/hooks/use-spreadsheet-state.js +7 -0
  203. package/dist/esm/hooks/utils/sort-rows.d.ts +15 -0
  204. package/dist/esm/hooks/utils/sort-rows.d.ts.map +1 -1
  205. package/dist/esm/hooks/utils/sort-rows.js +15 -0
  206. package/dist/esm/index.d.ts +3 -0
  207. package/dist/esm/index.d.ts.map +1 -1
  208. package/dist/esm/index.js +3 -0
  209. package/dist/esm/interface/delete-column.d.ts.map +1 -1
  210. package/dist/esm/interface/delete-column.js +111 -80
  211. package/dist/esm/interface/delete-row.d.ts.map +1 -1
  212. package/dist/esm/interface/delete-row.js +91 -59
  213. package/dist/esm/interface/duplicate-sheet.d.ts.map +1 -1
  214. package/dist/esm/interface/duplicate-sheet.js +4 -9
  215. package/dist/esm/interface/insert-column.d.ts.map +1 -1
  216. package/dist/esm/interface/insert-column.js +1 -1
  217. package/dist/esm/interface/insert-row.d.ts.map +1 -1
  218. package/dist/esm/interface/insert-row.js +1 -1
  219. package/dist/esm/interface/new-sheet.d.ts.map +1 -1
  220. package/dist/esm/interface/new-sheet.js +6 -13
  221. package/dist/esm/sheet-cell.d.ts +9 -0
  222. package/dist/esm/sheet-cell.d.ts.map +1 -1
  223. package/dist/esm/sheet-cell.js +46 -7
  224. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  225. package/package.json +9 -9
@@ -1,6 +1,6 @@
1
- import { getDefaultDateFormat, getDefaultDateTimeFormat, isAFormula, isNil, ssfFormat, ssfFormatColor, toDate, toSerial, } from "@rowsncolumns/utils";
1
+ import { getDefaultDateFormat, getDefaultDateTimeFormat, isAFormula, isNil, ssfFormat, ssfFormatColor, toDate, toSerial, toSerialFromParts, } from "@rowsncolumns/utils";
2
2
  import { detectDateFormatPattern, detectDateTimeFormatPattern, isValidDate, isValidDateTime, toTimeSerial, validateDate, validateDateTime, } from "./date";
3
- import { detectDecimalPattern } from "./decimals";
3
+ import { changeDecimals, detectDecimalPattern, getDecimalPlaces, hasThousandsGrouping, pattern_number_thousands, } from "./decimals";
4
4
  import { buildGroupRegex, getGroupingInfo, getLocaleDigitMap, getLocaleSeparators, getPercentSignInfo, getSignInfo, } from "./intl";
5
5
  export const pattern_percent = "0%";
6
6
  export const pattern_percent_decimal = "0.00%";
@@ -11,6 +11,11 @@ export const pattern_percent_decimal = "0.00%";
11
11
  // Excel renders the workbook identically to one it produced itself.
12
12
  export const pattern_currency = '"$"#,##0_);("$"#,##0)';
13
13
  export const pattern_currency_decimal = '"$"#,##0.00_);("$"#,##0.00)';
14
+ // Excel's built-in "Comma Style" / "Comma [0]" cell styles (numFmtId 43 / 41):
15
+ // accounting layout without a currency symbol — negatives in parentheses,
16
+ // zero as `-`, padded so the digits align in a column. Neither is red.
17
+ export const pattern_comma = '_(* #,##0.00_);_(* (#,##0.00);_(* "-"??_);_(@_)';
18
+ export const pattern_comma_rounded = '_(* #,##0_);_(* (#,##0);_(* "-"??_);_(@_)';
14
19
  export const date_pattern = "mm/dd/yyyy";
15
20
  export const time_pattern_base = "hh:mm:ss";
16
21
  export const time_pattern = `hh:mm:ss AM/PM`;
@@ -403,11 +408,12 @@ export const createNumberValue = (value, numFormatType, locale, use1904, lenient
403
408
  const isDate = numFormatType === "DATE";
404
409
  const isDateTime = numFormatType === "DATE_TIME";
405
410
  if (typeof value !== "boolean" && (isDate || isDateTime)) {
411
+ // A number is already a serial: applying the Date type only changes the
412
+ // format. Round-tripping it through `toDate`/`toSerial` moved Excel's
413
+ // phantom 1900-02-29 (60) onto the 28th (59) because no `Date` holds the
414
+ // 29th (rowsncolumns/spreadsheet#1026).
406
415
  if (typeof value === "number") {
407
- const dateValue = createDateValueFromNumber(value, use1904);
408
- if (!isNil(dateValue) && dateValue instanceof Date) {
409
- return toSerial(dateValue, use1904);
410
- }
416
+ return value;
411
417
  }
412
418
  const dateValue = isDate
413
419
  ? validateDate(value, locale)
@@ -417,7 +423,7 @@ export const createNumberValue = (value, numFormatType, locale, use1904, lenient
417
423
  if (isTimeOnly) {
418
424
  return toTimeSerial(hour ?? 0, minute ?? 0, second ?? 0);
419
425
  }
420
- return toSerial(new Date(year, month, day, hour, minute, second, 0), use1904);
426
+ return toSerialFromParts(year, month, day, hour, minute, second, use1904);
421
427
  }
422
428
  }
423
429
  return convertToNumber(value, locale, lenientGrouping);
@@ -463,6 +469,33 @@ export const detectNumberFormatType = (value, locale, lenientGrouping = true) =>
463
469
  return "DATE_TIME";
464
470
  }
465
471
  };
472
+ /**
473
+ * Currency base pattern for a typed literal: the detected symbol in the position it was typed
474
+ * (`$30` → `"$"#,##0`, `12,50 €` → `#,##0"€"`), defaulting to a `$` prefix.
475
+ */
476
+ const currencyBasePattern = (value) => {
477
+ const currencyInfo = extractCurrencySymbol(String(value));
478
+ const symbol = currencyInfo?.symbol || "$";
479
+ const position = currencyInfo?.position || "prefix";
480
+ return position === "suffix" ? `#,##0"${symbol}"` : `"${symbol}"#,##0`;
481
+ };
482
+ /**
483
+ * Currency / percent pattern whose decimal places are the ones the user TYPED
484
+ * (`$1,234.50` → `"$"#,##0.00`, `50.00%` → `0.00%`), not the parsed number's (`1234.5` → one
485
+ * place), so trailing zeros survive like in Excel. The base already carries the symbol and
486
+ * grouping, so only the decimals are appended. Ports Rust `detect_decimal_pattern_typed`; both
487
+ * `detectValueTypeAndPattern` (typed cell entry) and `detectNumberFormatPattern` go through it
488
+ * so the two never disagree on the same literal.
489
+ */
490
+ const detectTypedDecimalPattern = (value, numberValue, basePattern, locale) => {
491
+ // A JS number carries no typed digits; its own decimal expansion is the best we have (a
492
+ // percent's `numberValue` is already /100, so it must not be the source for a numeric input).
493
+ const typed = typeof value === "string" || typeof value === "number"
494
+ ? value
495
+ : numberValue ?? 0;
496
+ const decimalsLen = getDecimalPlaces(typed, locale);
497
+ return decimalsLen ? changeDecimals(basePattern, decimalsLen, true) : basePattern;
498
+ };
466
499
  /**
467
500
  * Detect pattern of a number format
468
501
  * @param type
@@ -470,16 +503,10 @@ export const detectNumberFormatType = (value, locale, lenientGrouping = true) =>
470
503
  */
471
504
  export const detectNumberFormatPattern = (value, numberValue, type, locale, use1904) => {
472
505
  if (type === "PERCENT") {
473
- return detectDecimalPattern(value, pattern_percent, locale);
506
+ return detectTypedDecimalPattern(value, numberValue, pattern_percent, locale);
474
507
  }
475
508
  if (type === "CURRENCY") {
476
- // Extract the actual currency symbol and position from the input
477
- const currencyInfo = extractCurrencySymbol(String(value));
478
- const symbol = currencyInfo?.symbol || "$";
479
- const position = currencyInfo?.position || "prefix";
480
- // Create a pattern with the detected currency symbol in the correct position
481
- const baseCurrencyPattern = position === "suffix" ? `#,##0"${symbol}"` : `"${symbol}"#,##0`;
482
- return detectDecimalPattern(numberValue, baseCurrencyPattern, locale);
509
+ return detectTypedDecimalPattern(value, numberValue, currencyBasePattern(value), locale);
483
510
  }
484
511
  if (type === "DATE") {
485
512
  return detectDateFormatPattern(value, locale) ?? date_pattern;
@@ -488,17 +515,20 @@ export const detectNumberFormatPattern = (value, numberValue, type, locale, use1
488
515
  return detectDateTimeFormatPattern(value) ?? time_pattern;
489
516
  }
490
517
  if (type === "NUMBER") {
491
- // An integer zero with no explicit format stays "General" — typing `0`,
492
- // `00`, or a signed zero into a blank cell shouldn't manufacture a `#`
493
- // number pattern that renders the value as blank. This rule is
494
- // specific to the implicit NUMBER format; explicit CURRENCY / PERCENT
495
- // formats keep their own base pattern for zero (handled above), so `$0`
496
- // stays `"$"#,##0` rather than collapsing to a bare `0`.
497
- if (value === 0 ||
498
- (typeof value === "string" && /^[-+]?0+$/.test(value.trim()))) {
499
- return "General";
518
+ // Excel leaves a General cell General for a plain number — `5`, `5.25`,
519
+ // `1.50`, `0.0000123` all display through General (`1.5`, `1.23E-05`)
520
+ // and stamp nothing, so a later entry in the same cell is never rounded
521
+ // through an inferred `#` / `#.00`. Only a thousands-grouped literal
522
+ // (`1,000`, `1,234.56`) is a formatting cue: it stamps `#,##0` plus the
523
+ // typed decimal places, and that stamp then sticks like any other format
524
+ // (Excel shows `0` for a later `0.25`). This rule is specific to the
525
+ // implicit NUMBER format; explicit CURRENCY / PERCENT formats keep their
526
+ // own base pattern (handled above), so `$0` stays `"$"#,##0`.
527
+ if (typeof value === "string" &&
528
+ hasThousandsGrouping(value, locale, numberValue)) {
529
+ return detectDecimalPattern(value, pattern_number_thousands, locale);
500
530
  }
501
- return detectDecimalPattern(value, undefined, locale);
531
+ return "General";
502
532
  }
503
533
  return undefined;
504
534
  };
@@ -559,18 +589,12 @@ export const detectValueTypeAndPattern = (value, locale, lenientGrouping = true,
559
589
  }
560
590
  // Check currency
561
591
  if (isCurr) {
562
- // Extract the actual currency symbol and position from the input
563
- const currencyInfo = extractCurrencySymbol(String(value));
564
- const symbol = currencyInfo?.symbol || "$";
565
- const position = currencyInfo?.position || "prefix";
566
- // Create a pattern with the detected currency symbol in the correct position
567
- const baseCurrencyPattern = position === "suffix" ? `#,##0"${symbol}"` : `"${symbol}"#,##0`;
568
- const pattern = detectDecimalPattern(numberValue, baseCurrencyPattern, locale);
592
+ const pattern = detectTypedDecimalPattern(value, numberValue, currencyBasePattern(value), locale);
569
593
  return ["numberValue", "CURRENCY", numberValue, pattern];
570
594
  }
571
595
  // Numbers can be number, percent, date, datetime
572
596
  if (isPercentage(value, locale, lenientGrouping)) {
573
- const pattern = detectDecimalPattern(numberValue, pattern_percent, locale);
597
+ const pattern = detectTypedDecimalPattern(value, numberValue, pattern_percent, locale);
574
598
  const percentValue = convertToNumber(value, locale, lenientGrouping) / 100;
575
599
  return ["numberValue", "PERCENT", percentValue, pattern];
576
600
  }
@@ -594,14 +618,16 @@ export const detectValueTypeAndPattern = (value, locale, lenientGrouping = true,
594
618
  const { parts: { year, month, day, hour, minute, second }, isTimeOnly, } = dateTimeValue;
595
619
  const numberValue = isTimeOnly
596
620
  ? toTimeSerial(hour ?? 0, minute ?? 0, second ?? 0)
597
- : toSerial(new Date(year, month, day, hour, minute, second, 0), use1904);
621
+ : toSerialFromParts(year, month, day, hour, minute, second, use1904);
598
622
  return ["numberValue", "DATE_TIME", numberValue, dateTimeValue.format];
599
623
  }
600
624
  // Dates
601
625
  const dateValue = validateDate(value, locale);
602
626
  if (dateValue) {
603
627
  const { parts: { year, month, day, hour, minute, second }, } = dateValue;
604
- const numberValue = toSerial(new Date(year, month, day, hour, minute, second, 0), use1904);
628
+ // Parts, not a `Date`: Excel's phantom 1900-02-29 must land on serial 60
629
+ // rather than roll over to March 1 (rowsncolumns/spreadsheet#1026).
630
+ const numberValue = toSerialFromParts(year, month, day, hour, minute, second, use1904);
605
631
  return ["numberValue", "DATE", numberValue, dateValue.format];
606
632
  }
607
633
  return ["stringValue"];
@@ -1 +1 @@
1
- {"version":3,"file":"date.d.ts","sourceRoot":"","sources":["../../../api/date.ts"],"names":[],"mappings":"AAoCA,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,SAAS,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAYD,eAAO,MAAM,YAAY,GACvB,OAAO,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,EACnD,QAAQ,MAAM,GAAG,SAAS,KACzB,eAAe,GAAG,SAiCpB,CAAC;AAGF,eAAO,MAAM,gBAAgB,GAC3B,OAAO,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,KAClD,eAAe,GAAG,SAoEpB,CAAC;AA6CF;;GAEG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,EAAE,QAAQ,MAAM,EAAE,QAAQ,MAAM,WAGxE,CAAC;AAwBF,eAAO,MAAM,WAAW,GACtB,OAAO,GAAG,EACV,QAAQ,MAAM,GAAG,SAAS,KACzB,OAKF,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,OAAO,GAAG,KAAG,OAK5C,CAAC;AAGF,eAAO,MAAM,uBAAuB,GAClC,OAAO,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,EACnD,QAAQ,MAAM,GAAG,SAAS,KACzB,MAAM,GAAG,SAKX,CAAC;AAEF,eAAO,MAAM,2BAA2B,GACtC,OAAO,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,KAClD,MAAM,GAAG,SAKX,CAAC;AAYF,eAAO,MAAM,cAAc,cAA+B,CAAC;AAC3D,eAAO,MAAM,eAAe,cAAgC,CAAC;AAC7D,eAAO,MAAM,aAAa,cAA8B,CAAC"}
1
+ {"version":3,"file":"date.d.ts","sourceRoot":"","sources":["../../../api/date.ts"],"names":[],"mappings":"AAwCA,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,SAAS,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAYD,eAAO,MAAM,YAAY,GACvB,OAAO,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,EACnD,QAAQ,MAAM,GAAG,SAAS,KACzB,eAAe,GAAG,SAsCpB,CAAC;AAGF,eAAO,MAAM,gBAAgB,GAC3B,OAAO,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,KAClD,eAAe,GAAG,SAsEpB,CAAC;AA6CF;;GAEG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,EAAE,QAAQ,MAAM,EAAE,QAAQ,MAAM,WAGxE,CAAC;AAwBF,eAAO,MAAM,WAAW,GACtB,OAAO,GAAG,EACV,QAAQ,MAAM,GAAG,SAAS,KACzB,OAKF,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,OAAO,GAAG,KAAG,OAK5C,CAAC;AAGF,eAAO,MAAM,uBAAuB,GAClC,OAAO,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,EACnD,QAAQ,MAAM,GAAG,SAAS,KACzB,MAAM,GAAG,SAKX,CAAC;AAEF,eAAO,MAAM,2BAA2B,GACtC,OAAO,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,KAClD,MAAM,GAAG,SAKX,CAAC;AAYF,eAAO,MAAM,cAAc,cAA+B,CAAC;AAC3D,eAAO,MAAM,eAAe,cAAgC,CAAC;AAC7D,eAAO,MAAM,aAAa,cAA8B,CAAC"}
@@ -17,14 +17,17 @@ const TIME_FORMATS = {
17
17
  FULL: "hh:mm:ss",
18
18
  SHORT_WITH_MERIDIAN: "hh:mm AM/PM",
19
19
  SHORT: "hh:mm",
20
- // Excel "elapsed time" patterns. The square brackets around `h` tell the
21
- // formatter to display the full elapsed-hours count without wrapping at
22
- // 24 — `[h]:mm:ss` will render a 25.5-hour duration as `25:30:00`, not
23
- // `01:30:00`. Excel auto-applies these when the user types a value with
24
- // hours > 24.
25
- FULL_ELAPSED: "[h]:mm:ss",
26
- SHORT_ELAPSED: "[h]:mm",
20
+ // Excel "elapsed time" (Duration) pattern. The square brackets around `h`
21
+ // tell the formatter to display the full elapsed-hours count without
22
+ // wrapping at 24 — `[h]:mm:ss` renders a 25.5-hour duration as `25:30:00`,
23
+ // not `01:30:00`. Excel auto-applies it when the typed hours reach 24
24
+ // (`24:00` is one day, `25:00` a duration) and always shows the seconds, so
25
+ // a typed `25:00` displays `25:00:00` (rowsncolumns/spreadsheet#1027) —
26
+ // the same pattern the Number menu's "Duration" item applies.
27
+ ELAPSED: "[h]:mm:ss",
27
28
  };
29
+ // Typed hours at or past a full day are a Duration, never a clock time.
30
+ const ELAPSED_HOURS_MIN = 24;
28
31
  // Cap elapsed-time hours at Excel's documented limit (`9999:59:59`). Beyond
29
32
  // this, treat as text rather than producing absurdly large fractional
30
33
  // serials. JS `Number.isFinite` would let `Infinity` through, so we cap
@@ -60,12 +63,17 @@ export const validateDate = (value, locale) => {
60
63
  if (year < EXCEL_DATE_MIN_YEAR || year > EXCEL_DATE_MAX_YEAR) {
61
64
  return undefined;
62
65
  }
66
+ // Excel's phantom 1900-02-29 (serial 60): the parser hands back a stand-in
67
+ // `Date` on the 28th with `excelLeapDay` set, because no `Date` can hold the
68
+ // 29th. Report the day the user typed; `toSerialFromParts` maps it to 60
69
+ // (rowsncolumns/spreadsheet#1026).
70
+ const day = "excelLeapDay" in result && result.excelLeapDay ? 29 : dateValue.getDate();
63
71
  return {
64
72
  format: result.pattern,
65
73
  parts: {
66
74
  year,
67
75
  month: dateValue.getMonth(),
68
- day: dateValue.getDate(),
76
+ day,
69
77
  hour: dateValue.getHours(),
70
78
  minute: dateValue.getMinutes(),
71
79
  second: dateValue.getSeconds(),
@@ -94,10 +102,10 @@ export const validateDateTime = (value) => {
94
102
  const minute = parseInt(match[2], 10);
95
103
  const second = parseInt(match[3], 10);
96
104
  if (isValidTimeComponents(hour, minute, second)) {
97
- // Excel parity: hour > 24 is an elapsed-time entry (e.g. `25:30:00`,
98
- // `48:00:00`). Auto-apply `[h]:mm:ss` so the cell displays the full
99
- // hours count instead of wrapping to `01:30:00`.
100
- const format = hour > 24 ? TIME_FORMATS.FULL_ELAPSED : TIME_FORMATS.FULL;
105
+ // Excel parity: hours >= 24 are an elapsed-time entry (`24:00:00`,
106
+ // `25:30:00`, `48:00:00`). Auto-apply `[h]:mm:ss` so the cell displays
107
+ // the full hours count instead of wrapping to `01:30:00`.
108
+ const format = hour >= ELAPSED_HOURS_MIN ? TIME_FORMATS.ELAPSED : TIME_FORMATS.FULL;
101
109
  return createTimeResult(format, hour, minute, second);
102
110
  }
103
111
  }
@@ -115,8 +123,9 @@ export const validateDateTime = (value) => {
115
123
  const hour = parseInt(match[1], 10);
116
124
  const minute = parseInt(match[2], 10);
117
125
  if (isValidTimeComponents(hour, minute, 0)) {
118
- // Same elapsed-time auto-pattern logic as the full form above.
119
- const format = hour > 24 ? TIME_FORMATS.SHORT_ELAPSED : TIME_FORMATS.SHORT;
126
+ // Same elapsed-time rule as the full form: a duration typed without
127
+ // seconds still gets `[h]:mm:ss` (`25:00` → `25:00:00`, as Excel).
128
+ const format = hour >= ELAPSED_HOURS_MIN ? TIME_FORMATS.ELAPSED : TIME_FORMATS.SHORT;
120
129
  return createTimeResult(format, hour, minute, 0);
121
130
  }
122
131
  }
@@ -1,6 +1,16 @@
1
1
  export declare const pattern_number = "#";
2
2
  export declare const pattern_number_thousands = "#,##0";
3
3
  export declare const getDecimalPlaces: (value: string | number, locale?: string) => number;
4
+ /**
5
+ * Whether a typed literal groups its integer part with the locale's thousands
6
+ * separator (`1,000` / `1,234.56` in en-US, `1.234,56` in de-DE, `1 234` in
7
+ * fr-FR). In a comma-decimal locale a canonical dot-decimal string (`1.234`)
8
+ * is ambiguous — the programmatic decimal 1.234, or the grouped 1234 — so when
9
+ * the caller knows what the literal parsed to (`parsedValue`) that decides;
10
+ * without it the canonical reading wins, so the locale's `.` group separator
11
+ * is not mistaken for grouping. Rust `has_thousands_grouping`.
12
+ */
13
+ export declare const hasThousandsGrouping: (value: string | number, locale?: string, parsedValue?: number) => boolean;
4
14
  export declare const detectDecimalPattern: (value: string | number | boolean | Date | undefined, defaultPattern?: string, locale?: string) => string;
5
15
  export declare const changeDecimals: (pattern?: string | undefined, changeBy?: number, delta?: boolean) => string;
6
16
  export declare const getCurrentDecimalCountInPattern: (pattern?: string | undefined | null) => number;
@@ -1 +1 @@
1
- {"version":3,"file":"decimals.d.ts","sourceRoot":"","sources":["../../../api/decimals.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,cAAc,MAAM,CAAC;AAClC,eAAO,MAAM,wBAAwB,UAAU,CAAC;AAyDhD,eAAO,MAAM,gBAAgB,GAC3B,OAAO,MAAM,GAAG,MAAM,EACtB,SAAS,MAAM,KACd,MAsCF,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,OAAO,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,EACnD,uBAA+B,EAC/B,SAAS,MAAM,WAkChB,CAAC;AAEF,eAAO,MAAM,cAAc,GACzB,UAAS,MAAM,GAAG,SAAc,EAChC,WAAU,MAAU,EACpB,eAAY,WAuCb,CAAC;AAEF,eAAO,MAAM,+BAA+B,GAC1C,UAAS,MAAM,GAAG,SAAS,GAAG,IAAS,WAexC,CAAC;AAGF,eAAO,MAAM,oBAAoB,GAAI,UAAS,MAAM,GAAG,SAAc,WAwBpE,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,UAAS,MAAM,GAAG,SAAc,WAQvE,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAAI,UAAS,MAAM,GAAG,SAAc,YAIrE,CAAC"}
1
+ {"version":3,"file":"decimals.d.ts","sourceRoot":"","sources":["../../../api/decimals.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,cAAc,MAAM,CAAC;AAClC,eAAO,MAAM,wBAAwB,UAAU,CAAC;AAyDhD,eAAO,MAAM,gBAAgB,GAC3B,OAAO,MAAM,GAAG,MAAM,EACtB,SAAS,MAAM,KACd,MAsCF,CAAC;AAcF;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAC/B,OAAO,MAAM,GAAG,MAAM,EACtB,SAAS,MAAM,EACf,cAAc,MAAM,KACnB,OAeF,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,OAAO,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,EACnD,uBAA+B,EAC/B,SAAS,MAAM,WAsBhB,CAAC;AAEF,eAAO,MAAM,cAAc,GACzB,UAAS,MAAM,GAAG,SAAc,EAChC,WAAU,MAAU,EACpB,eAAY,WAuCb,CAAC;AAEF,eAAO,MAAM,+BAA+B,GAC1C,UAAS,MAAM,GAAG,SAAS,GAAG,IAAS,WAexC,CAAC;AAGF,eAAO,MAAM,oBAAoB,GAAI,UAAS,MAAM,GAAG,SAAc,WAwBpE,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,UAAS,MAAM,GAAG,SAAc,WAQvE,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAAI,UAAS,MAAM,GAAG,SAAc,YAIrE,CAAC"}
@@ -73,19 +73,40 @@ export const getDecimalPlaces = (value, locale) => {
73
73
  // Return the length of the decimal part
74
74
  return decimalPart.length;
75
75
  };
76
- export const detectDecimalPattern = (value, defaultPattern = pattern_number, locale) => {
77
- const str = typeof value === "number" ? normalizeFloatingPointString(value) : String(value);
76
+ const WHITESPACE_GROUP_REGEX = /[\s\u00A0\u202F]/;
77
+ /**
78
+ * Whether `integerPart` contains the locale's group separator. Locales that
79
+ * group with a space accept the whole whitespace family (regular, no-break,
80
+ * narrow no-break) — users type a plain space for fr-FR's `\u202F`.
81
+ */
82
+ const integerPartHasGroup = (integerPart, group) => group.trim() === ""
83
+ ? WHITESPACE_GROUP_REGEX.test(integerPart)
84
+ : integerPart.includes(group);
85
+ /**
86
+ * Whether a typed literal groups its integer part with the locale's thousands
87
+ * separator (`1,000` / `1,234.56` in en-US, `1.234,56` in de-DE, `1 234` in
88
+ * fr-FR). In a comma-decimal locale a canonical dot-decimal string (`1.234`)
89
+ * is ambiguous — the programmatic decimal 1.234, or the grouped 1234 — so when
90
+ * the caller knows what the literal parsed to (`parsedValue`) that decides;
91
+ * without it the canonical reading wins, so the locale's `.` group separator
92
+ * is not mistaken for grouping. Rust `has_thousands_grouping`.
93
+ */
94
+ export const hasThousandsGrouping = (value, locale, parsedValue) => {
95
+ const str = typeof value === "number" ? normalizeFloatingPointString(value) : value;
78
96
  const { decimal, group } = getLocaleSeparators(locale);
79
97
  const groupSeparator = group ?? ",";
80
98
  const decimalSeparator = decimal ?? ".";
81
- const usesCanonicalDotDecimal = decimalSeparator !== "." && CANONICAL_DOT_DECIMAL_REGEX.test(str);
82
- const effectiveDecimalSeparator = usesCanonicalDotDecimal
83
- ? "."
84
- : decimalSeparator;
85
- const integerPart = effectiveDecimalSeparator
86
- ? str.split(effectiveDecimalSeparator)[0]
87
- : str;
88
- const hasThousands = Boolean(groupSeparator) && integerPart.includes(groupSeparator);
99
+ if (decimalSeparator !== "." && CANONICAL_DOT_DECIMAL_REGEX.test(str)) {
100
+ return (parsedValue !== undefined &&
101
+ Number(str) !== parsedValue &&
102
+ integerPartHasGroup(str, groupSeparator));
103
+ }
104
+ const integerPart = str.split(decimalSeparator)[0];
105
+ return Boolean(groupSeparator) && integerPartHasGroup(integerPart, groupSeparator);
106
+ };
107
+ export const detectDecimalPattern = (value, defaultPattern = pattern_number, locale) => {
108
+ const str = typeof value === "number" ? normalizeFloatingPointString(value) : String(value);
109
+ const hasThousands = hasThousandsGrouping(str, locale);
89
110
  const decimalsLen = getDecimalPlaces(value, locale);
90
111
  // Use "0" pattern for numbers less than 1 to ensure leading zero
91
112
  const numValue = Number(str.replace(/[^0-9.-]/g, ""));
@@ -38,7 +38,17 @@ export declare const collectUniqueDependentsForRange: <T extends CellData>(param
38
38
  rowIndex: number;
39
39
  columnIndex: number;
40
40
  }>;
41
- export declare const moveFormulaAtPosition: <T extends CellData>({ draft, position, destinationSheetId, activeSheetName, enqueueCalculation, enqueueGraphOperation, recalcCells, movedKeys, rowDelta, columnDelta, sourceRange, deletedRange, destinationPosition, locale, rowCount, columnCount, cellXfsRegistry, sharedStringRegistry, }: {
41
+ /**
42
+ * One reference shift of a structural edit: the references inside `sourceRange` move by the
43
+ * deltas, those inside `deletedRange` break to `#REF!`.
44
+ */
45
+ export type FormulaMoveStep = {
46
+ rowDelta: number;
47
+ columnDelta: number;
48
+ sourceRange?: GridRange;
49
+ deletedRange?: GridRange;
50
+ };
51
+ export declare const moveFormulaAtPosition: <T extends CellData>({ draft, position, destinationSheetId, activeSheetName, enqueueCalculation, enqueueGraphOperation, recalcCells, movedKeys, rowDelta, columnDelta, sourceRange, deletedRange, steps, destinationPosition, locale, rowCount, columnCount, cellXfsRegistry, sharedStringRegistry, }: {
42
52
  draft: Draft<SheetData<T>>;
43
53
  position: {
44
54
  sheetId: number;
@@ -56,6 +66,14 @@ export declare const moveFormulaAtPosition: <T extends CellData>({ draft, positi
56
66
  sourceRange?: GridRange;
57
67
  /** Cells removed by a delete-cells shift — references into it become `#REF!` (SHF-09). */
58
68
  deletedRange?: GridRange;
69
+ /**
70
+ * Several shifts applied to the formula text in order, one per deleted band of a
71
+ * non-contiguous delete (Ctrl-selected rows 2 and 4): each step shifts the references inside
72
+ * its own `sourceRange` by its delta and breaks those inside its `deletedRange`. Replaces the
73
+ * single `rowDelta` / `columnDelta` / `sourceRange` / `deletedRange` shift when given; pass
74
+ * `destinationPosition` with it (the cell's own move is not any single step's delta).
75
+ */
76
+ steps?: FormulaMoveStep[];
59
77
  /**
60
78
  * Where the formula cell itself ends up. Defaults to `position + delta` (the cell moves with the
61
79
  * shift). A dependent OUTSIDE the shifted band keeps its place while its references into the
@@ -1 +1 @@
1
- {"version":3,"file":"dependency.d.ts","sourceRoot":"","sources":["../../../api/dependency.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dependency.d.ts","sourceRoot":"","sources":["../../../api/dependency.ts"],"names":[],"mappings":"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"}
@@ -205,7 +205,7 @@ export const collectUniqueDependentsForRange = (params) => {
205
205
  return dependentMap;
206
206
  };
207
207
  // Process a single formula move at a specific position with the specified deltas.
208
- export const moveFormulaAtPosition = ({ draft, position, destinationSheetId, activeSheetName, enqueueCalculation, enqueueGraphOperation, recalcCells, movedKeys, rowDelta = 0, columnDelta = 0, sourceRange, deletedRange, destinationPosition, locale, rowCount, columnCount, cellXfsRegistry, sharedStringRegistry, }) => {
208
+ export const moveFormulaAtPosition = ({ draft, position, destinationSheetId, activeSheetName, enqueueCalculation, enqueueGraphOperation, recalcCells, movedKeys, rowDelta = 0, columnDelta = 0, sourceRange, deletedRange, steps, destinationPosition, locale, rowCount, columnCount, cellXfsRegistry, sharedStringRegistry, }) => {
209
209
  const sheetCell = new SheetCell();
210
210
  const currentSheet = createSheetDraft(draft, position.sheetId, position.rowIndex);
211
211
  const cellData = currentSheet[position.rowIndex]?.values?.[position.columnIndex];
@@ -218,15 +218,20 @@ export const moveFormulaAtPosition = ({ draft, position, destinationSheetId, act
218
218
  const resolvedSheetName = activeSheetName ?? "";
219
219
  // The delta is applied to the references inside `sourceRange`; the formula cell's own
220
220
  // destination (for the graph re-key) may differ when it does not move itself.
221
- sheetCell.moveFormula({ rowIndex: position.rowIndex, columnIndex: position.columnIndex }, {
222
- rowIndex: position.rowIndex + rowDelta,
223
- columnIndex: position.columnIndex + columnDelta,
224
- }, resolvedSheetName, {
225
- exclusionRange: sourceRange,
226
- deletedRange,
227
- rowCount,
228
- columnCount,
229
- });
221
+ const moves = steps ?? [
222
+ { rowDelta, columnDelta, sourceRange, deletedRange },
223
+ ];
224
+ for (const move of moves) {
225
+ sheetCell.moveFormula({ rowIndex: position.rowIndex, columnIndex: position.columnIndex }, {
226
+ rowIndex: position.rowIndex + move.rowDelta,
227
+ columnIndex: position.columnIndex + move.columnDelta,
228
+ }, resolvedSheetName, {
229
+ exclusionRange: move.sourceRange,
230
+ deletedRange: move.deletedRange,
231
+ rowCount,
232
+ columnCount,
233
+ });
234
+ }
230
235
  if (previousFormula !== sheetCell.formula) {
231
236
  // Set cell data at old position (formula updated prior to structural move)
232
237
  currentSheet[position.rowIndex].values[position.columnIndex] =
@@ -0,0 +1,58 @@
1
+ import type { BooleanCondition, ConditionType } from "@rowsncolumns/spreadsheet";
2
+ export declare const isDateFilterCondition: (type: ConditionType | string | null | undefined) => boolean;
3
+ /**
4
+ * Whether a filter clause compares the cell's NUMBER — a Number Filter or a
5
+ * Date Filter comparison. Text (even numeric-looking text such as `'500`),
6
+ * booleans, errors and blanks never satisfy such a clause.
7
+ */
8
+ export declare const isNumericFilterCondition: (type: ConditionType | string | null | undefined) => boolean;
9
+ /** A cell value that takes part in a Number / Date Filter: a finite number. */
10
+ export declare const isNumericFilterCellValue: (value: unknown) => value is number;
11
+ /**
12
+ * How a Number / Date Filter criterion (`userEnteredValue`, the free text
13
+ * typed into the filter dialog) reads.
14
+ *
15
+ * - `empty`: nothing typed — the clause is inactive.
16
+ * - `number`: a typed literal — number, `50%`, currency, or a date / time in
17
+ * the document locale — as its numeric value (a date is its serial, exactly
18
+ * what the cell holds).
19
+ * - `formula`: `=…` or a cell reference; only the calculator can resolve it.
20
+ * - `text`: anything else; no number satisfies it.
21
+ */
22
+ export type FilterCriterion = {
23
+ kind: "empty";
24
+ } | {
25
+ kind: "number";
26
+ value: number;
27
+ } | {
28
+ kind: "formula";
29
+ } | {
30
+ kind: "text";
31
+ };
32
+ /**
33
+ * Classify a filter criterion the way Excel's Custom AutoFilter reads the typed
34
+ * box — as a cell entry in the workbook `locale` (`1/15/2024` is Jan 15 in
35
+ * en-US, `15.1.2024` in de-DE, `2024-02-01` anywhere, `6,5` is 6.5 in de-DE),
36
+ * so a Date Filter compares serials instead of quoting the text.
37
+ */
38
+ export declare const classifyFilterCriterion: (raw: string | undefined | null, locale?: string) => FilterCriterion;
39
+ /**
40
+ * Decide one Number Filter / Date Filter clause against a cell's typed value,
41
+ * the way Excel does — without the calculator:
42
+ *
43
+ * - Only NUMBERS take part. Text (including numeric-looking text such as
44
+ * `'500`), booleans, errors and blanks never satisfy `> 500` / `after
45
+ * 1/15/2024` and are hidden (`false`). The calculator's coercion would say
46
+ * text > number is TRUE and a blank is 0 — neither is Excel. "Does not
47
+ * equal" is the complement of "equals": text and blanks stay visible.
48
+ * - A typed date criterion is read in the document `locale` and compared as a
49
+ * SERIAL; Date Filters compare calendar days (`INT`), so `1/15/2024 14:00`
50
+ * equals `1/15/2024` and is not after it.
51
+ * - A `=…` / reference criterion is `undefined` — left to the calculator, which
52
+ * then compares a number (the cell-type gate has already run). An empty
53
+ * criterion is `undefined` too (an inactive clause).
54
+ *
55
+ * `undefined` for a condition type this evaluator does not own.
56
+ */
57
+ export declare const evaluateNumericFilterCondition: (condition: BooleanCondition | null | undefined, value: unknown, locale?: string) => boolean | undefined;
58
+ //# sourceMappingURL=filter-condition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter-condition.d.ts","sourceRoot":"","sources":["../../../api/filter-condition.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,aAAa,EACd,MAAM,2BAA2B,CAAC;AA4CnC,eAAO,MAAM,qBAAqB,GAChC,MAAM,aAAa,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,KAC9C,OACsE,CAAC;AAE1E;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,GACnC,MAAM,aAAa,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,KAC9C,OAKA,CAAC;AAEJ,+EAA+E;AAC/E,eAAO,MAAM,wBAAwB,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,MACd,CAAC;AAEtD;;;;;;;;;;GAUG;AACH,MAAM,MAAM,eAAe,GACvB;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAErB;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GAClC,KAAK,MAAM,GAAG,SAAS,GAAG,IAAI,EAC9B,SAAS,MAAM,KACd,eAcF,CAAC;AAKF;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,8BAA8B,GACzC,WAAW,gBAAgB,GAAG,IAAI,GAAG,SAAS,EAC9C,OAAO,OAAO,EACd,SAAS,MAAM,KACd,OAAO,GAAG,SA2DZ,CAAC"}
@@ -0,0 +1,146 @@
1
+ import { checkIfStringIsReference } from "@rowsncolumns/utils";
2
+ import { detectValueTypeAndPattern } from "./datatype";
3
+ /**
4
+ * Excel's Number Filters and Date Filters — the AutoFilter clauses that act on
5
+ * the cell's NUMBER (a date is its serial) and hide every non-numeric cell.
6
+ * Twin of `is_numeric_filter_kind` in the Rust engine (`rnc-store/support.rs`);
7
+ * the shared table `libs/common-types/fixtures/filter-evaluator-table.json`
8
+ * (`numeric`) keeps the two evaluators identical.
9
+ */
10
+ const NUMBER_FILTER_CONDITION_TYPES = new Set([
11
+ "NUMBER_EQ",
12
+ "NUMBER_NOT_EQ",
13
+ "NUMBER_GREATER",
14
+ "NUMBER_GREATER_THAN_EQ",
15
+ "NUMBER_LESS",
16
+ "NUMBER_LESS_THAN_EQ",
17
+ "NUMBER_BETWEEN",
18
+ "NUMBER_NOT_BETWEEN",
19
+ ]);
20
+ const DATE_FILTER_CONDITION_TYPES = new Set([
21
+ "DATE_EQ",
22
+ "DATE_NOT_EQ",
23
+ "DATE_BEFORE",
24
+ "DATE_AFTER",
25
+ "DATE_ON_OR_BEFORE",
26
+ "DATE_ON_OR_AFTER",
27
+ "DATE_BETWEEN",
28
+ "DATE_NOT_BETWEEN",
29
+ ]);
30
+ const TWO_OPERAND_FILTER_CONDITION_TYPES = new Set([
31
+ "NUMBER_BETWEEN",
32
+ "NUMBER_NOT_BETWEEN",
33
+ "DATE_BETWEEN",
34
+ "DATE_NOT_BETWEEN",
35
+ ]);
36
+ export const isDateFilterCondition = (type) => Boolean(type && DATE_FILTER_CONDITION_TYPES.has(type));
37
+ /**
38
+ * Whether a filter clause compares the cell's NUMBER — a Number Filter or a
39
+ * Date Filter comparison. Text (even numeric-looking text such as `'500`),
40
+ * booleans, errors and blanks never satisfy such a clause.
41
+ */
42
+ export const isNumericFilterCondition = (type) => Boolean(type &&
43
+ (NUMBER_FILTER_CONDITION_TYPES.has(type) ||
44
+ DATE_FILTER_CONDITION_TYPES.has(type)));
45
+ /** A cell value that takes part in a Number / Date Filter: a finite number. */
46
+ export const isNumericFilterCellValue = (value) => typeof value === "number" && Number.isFinite(value);
47
+ /**
48
+ * Classify a filter criterion the way Excel's Custom AutoFilter reads the typed
49
+ * box — as a cell entry in the workbook `locale` (`1/15/2024` is Jan 15 in
50
+ * en-US, `15.1.2024` in de-DE, `2024-02-01` anywhere, `6,5` is 6.5 in de-DE),
51
+ * so a Date Filter compares serials instead of quoting the text.
52
+ */
53
+ export const classifyFilterCriterion = (raw, locale) => {
54
+ const trimmed = raw?.trim() ?? "";
55
+ if (!trimmed)
56
+ return { kind: "empty" };
57
+ if (trimmed.startsWith("=") || checkIfStringIsReference(trimmed)) {
58
+ return { kind: "formula" };
59
+ }
60
+ const [formatValue, , numberValue] = detectValueTypeAndPattern(trimmed, locale ?? "en-US");
61
+ if (formatValue === "numberValue" && isNumericFilterCellValue(numberValue)) {
62
+ return { kind: "number", value: numberValue };
63
+ }
64
+ return { kind: "text" };
65
+ };
66
+ const numbersEqual = (a, b) => Math.abs(a - b) <= 1e-9 * Math.max(Math.abs(a), Math.abs(b), 1);
67
+ /**
68
+ * Decide one Number Filter / Date Filter clause against a cell's typed value,
69
+ * the way Excel does — without the calculator:
70
+ *
71
+ * - Only NUMBERS take part. Text (including numeric-looking text such as
72
+ * `'500`), booleans, errors and blanks never satisfy `> 500` / `after
73
+ * 1/15/2024` and are hidden (`false`). The calculator's coercion would say
74
+ * text > number is TRUE and a blank is 0 — neither is Excel. "Does not
75
+ * equal" is the complement of "equals": text and blanks stay visible.
76
+ * - A typed date criterion is read in the document `locale` and compared as a
77
+ * SERIAL; Date Filters compare calendar days (`INT`), so `1/15/2024 14:00`
78
+ * equals `1/15/2024` and is not after it.
79
+ * - A `=…` / reference criterion is `undefined` — left to the calculator, which
80
+ * then compares a number (the cell-type gate has already run). An empty
81
+ * criterion is `undefined` too (an inactive clause).
82
+ *
83
+ * `undefined` for a condition type this evaluator does not own.
84
+ */
85
+ export const evaluateNumericFilterCondition = (condition, value, locale) => {
86
+ const type = condition?.type;
87
+ if (!type || !isNumericFilterCondition(type))
88
+ return undefined;
89
+ const values = condition?.values ?? [];
90
+ if (!isNumericFilterCellValue(value)) {
91
+ // "Does not equal" is the complement of "equals" over the whole column: a
92
+ // text cell or a blank does not equal 500 and stays visible (a text cell
93
+ // whose display text IS the criterion hides).
94
+ if (type === "NUMBER_NOT_EQ" || type === "DATE_NOT_EQ") {
95
+ const shown = value === undefined || value === null ? "" : String(value);
96
+ return (shown.trim().toLowerCase() !==
97
+ (values[0]?.userEnteredValue ?? "").trim().toLowerCase());
98
+ }
99
+ return false;
100
+ }
101
+ const first = classifyFilterCriterion(values[0]?.userEnteredValue, locale);
102
+ const second = TWO_OPERAND_FILTER_CONDITION_TYPES.has(type)
103
+ ? classifyFilterCriterion(values[1]?.userEnteredValue, locale)
104
+ : { kind: "number", value: 0 };
105
+ if (first.kind === "empty" || second.kind === "empty")
106
+ return undefined;
107
+ if (first.kind === "formula" || second.kind === "formula")
108
+ return undefined;
109
+ if (first.kind !== "number" || second.kind !== "number")
110
+ return false;
111
+ // Date Filters compare calendar days; Number Filters the exact values.
112
+ const day = isDateFilterCondition(type)
113
+ ? (n) => Math.floor(n)
114
+ : (n) => n;
115
+ const a = day(value);
116
+ const lo = day(first.value);
117
+ const hi = day(second.value);
118
+ switch (type) {
119
+ case "NUMBER_EQ":
120
+ case "DATE_EQ":
121
+ return numbersEqual(a, lo);
122
+ case "NUMBER_NOT_EQ":
123
+ case "DATE_NOT_EQ":
124
+ return !numbersEqual(a, lo);
125
+ case "NUMBER_GREATER":
126
+ case "DATE_AFTER":
127
+ return a > lo;
128
+ case "NUMBER_GREATER_THAN_EQ":
129
+ case "DATE_ON_OR_AFTER":
130
+ return a >= lo;
131
+ case "NUMBER_LESS":
132
+ case "DATE_BEFORE":
133
+ return a < lo;
134
+ case "NUMBER_LESS_THAN_EQ":
135
+ case "DATE_ON_OR_BEFORE":
136
+ return a <= lo;
137
+ case "NUMBER_BETWEEN":
138
+ case "DATE_BETWEEN":
139
+ return a >= lo && a <= hi;
140
+ case "NUMBER_NOT_BETWEEN":
141
+ case "DATE_NOT_BETWEEN":
142
+ return a < lo || a > hi;
143
+ default:
144
+ return undefined;
145
+ }
146
+ };