@a3s-lab/office 0.37.4 → 0.37.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -309,9 +309,13 @@ and [CLI reference](./docs/latest/en/cli-reference.md).
309
309
 
310
310
  ## Current release
311
311
 
312
- Version `0.37.4` keeps the release surface focused on local, testable
312
+ Version `0.37.5` keeps the release surface focused on local, testable
313
313
  workflows:
314
314
 
315
+ - Spreadsheet XLSX import/export now retains the workbook's native 1900 or
316
+ 1904 date system, exact typed date serials, dynamic date filters, and
317
+ epoch-correct current-date authoring across controlled and collaborative
318
+ reopen flows.
315
319
  - Spreadsheet Custom Sort now recognizes native table and AutoFilter-owned
316
320
  ranges, keeps headers and totals structurally safe, and reapplies active
317
321
  filter visibility after one formula-safe, undoable sort.
@@ -4516,19 +4516,21 @@ function spreadsheetSortColumnLabel(column) {
4516
4516
  }
4517
4517
  ]).replace(/\d+$/, '');
4518
4518
  }
4519
- function reconcileSpreadsheetFiltersAfterFortune(sheets, sourceSheets, operations = []) {
4519
+ function reconcileSpreadsheetFiltersAfterFortune(sheets, sourceSheets, operations = [], context = {
4520
+ now: new Date()
4521
+ }) {
4520
4522
  const coordinates = spreadsheetCellOperationCoordinates(operations);
4521
4523
  return sheets.map((sheet, index)=>{
4522
4524
  const source = spreadsheet_filter_reconciliation_sourceSheet(sheet, sourceSheets, index);
4523
4525
  if (!source || !spreadsheetSheetHasCriteria(source) || !spreadsheetFilterReconciliationIsBounded(source) || !spreadsheetFilterRangesWereEdited(source, operations, coordinates)) return sheet;
4524
- const previousTableOwnedRows = spreadsheetTableFilterOwnedRows(source);
4526
+ const previousTableOwnedRows = spreadsheetTableFilterOwnedRows(source, context);
4525
4527
  const manuallyHiddenRows = workSpreadsheetAutoFilterManuallyHiddenRows(source);
4526
4528
  for (const row of previousTableOwnedRows)manuallyHiddenRows.delete(row);
4527
4529
  let next = sheet;
4528
- if (normalizedWorkSpreadsheetAutoFilterRange(sheet.filter_select) && workSpreadsheetAutoFilterCriteriaEntries(source).length) next = workSpreadsheetSheetWithReappliedAutoFilterCriteria(sheet, source, manuallyHiddenRows);
4530
+ if (normalizedWorkSpreadsheetAutoFilterRange(sheet.filter_select) && workSpreadsheetAutoFilterCriteriaEntries(source).length) next = workSpreadsheetSheetWithReappliedAutoFilterCriteria(sheet, source, manuallyHiddenRows, context);
4529
4531
  const hiddenRows = new Set(manuallyHiddenRows);
4530
4532
  for (const row of workSpreadsheetAutoFilterOwnedRows(next))hiddenRows.add(row);
4531
- for (const row of spreadsheetTableFilterOwnedRows(next))hiddenRows.add(row);
4533
+ for (const row of spreadsheetTableFilterOwnedRows(next, context))hiddenRows.add(row);
4532
4534
  const rowhidden = Object.fromEntries([
4533
4535
  ...hiddenRows
4534
4536
  ].sort(compareSpreadsheetRowKeys).map((row)=>[
@@ -4544,26 +4546,28 @@ function reconcileSpreadsheetFiltersAfterFortune(sheets, sourceSheets, operation
4544
4546
  };
4545
4547
  });
4546
4548
  }
4547
- function reconcileSpreadsheetFiltersAfterSort(sheet, source, range, sourceIndexes) {
4549
+ function reconcileSpreadsheetFiltersAfterSort(sheet, source, range, sourceIndexes, context = {
4550
+ now: new Date()
4551
+ }) {
4548
4552
  if (!spreadsheetFilterReconciliationIsBounded(source) || !spreadsheetSortSourceIndexesAreValid(range, sourceIndexes)) return null;
4549
4553
  const reconciled = reconcileSpreadsheetFiltersAfterFortune([
4550
4554
  sheet
4551
4555
  ], [
4552
4556
  source
4553
- ])[0];
4557
+ ], [], context)[0];
4554
4558
  if (!reconciled) return null;
4555
4559
  let filter = reconciled.filter;
4556
4560
  const autoFilter = normalizedWorkSpreadsheetAutoFilterRange(source.filter_select);
4557
4561
  if (autoFilter && autoFilter.row[0] === range.row[0] && autoFilter.row[1] === range.row[1] && autoFilter.column[0] === range.column[0] && autoFilter.column[1] === range.column[1]) filter = spreadsheetFilterWithRemappedOpaqueEntries(reconciled, source, range, sourceIndexes);
4558
4562
  const manuallyHiddenRows = workSpreadsheetAutoFilterManuallyHiddenRows(source);
4559
- for (const row of spreadsheetTableFilterOwnedRows(source))manuallyHiddenRows.delete(row);
4563
+ for (const row of spreadsheetTableFilterOwnedRows(source, context))manuallyHiddenRows.delete(row);
4560
4564
  const withFilter = {
4561
4565
  ...reconciled,
4562
4566
  filter
4563
4567
  };
4564
4568
  const hiddenRows = new Set(manuallyHiddenRows);
4565
4569
  for (const row of workSpreadsheetAutoFilterOwnedRows(withFilter))hiddenRows.add(row);
4566
- for (const row of spreadsheetTableFilterOwnedRows(withFilter))hiddenRows.add(row);
4570
+ for (const row of spreadsheetTableFilterOwnedRows(withFilter, context))hiddenRows.add(row);
4567
4571
  const rowhidden = Object.fromEntries([
4568
4572
  ...hiddenRows
4569
4573
  ].sort(compareSpreadsheetRowKeys).map((row)=>[
@@ -4645,13 +4649,13 @@ function spreadsheetFilterRanges(sheet) {
4645
4649
  }
4646
4650
  return ranges;
4647
4651
  }
4648
- function spreadsheetTableFilterOwnedRows(sheet) {
4652
+ function spreadsheetTableFilterOwnedRows(sheet, context) {
4649
4653
  const hidden = new Set();
4650
4654
  for (const table of sheet.tables ?? []){
4651
4655
  const range = spreadsheetTableFilterRange(table);
4652
4656
  if (range) for (const filter of table.filters){
4653
4657
  const column = range.column[0] + filter.column;
4654
- const rowhidden = workSpreadsheetFilterHiddenRows(sheet, range, column, filter.criteria);
4658
+ const rowhidden = workSpreadsheetFilterHiddenRows(sheet, range, column, filter.criteria, context);
4655
4659
  for (const row of Object.keys(rowhidden ?? {}))hidden.add(row);
4656
4660
  }
4657
4661
  }
@@ -5348,8 +5352,12 @@ function recordSpreadsheetProjectionMeasure(start, end, detail) {
5348
5352
  });
5349
5353
  } catch {}
5350
5354
  }
5351
- function spreadsheetSheetsFromFortune(sheets, sourceSheets, operations = []) {
5355
+ function spreadsheetSheetsFromFortune(sheets, sourceSheets, operations = [], dateSystem = '1900') {
5352
5356
  const startedAt = spreadsheetProjectionNow();
5357
+ const filterContext = {
5358
+ dateSystem,
5359
+ now: new Date()
5360
+ };
5353
5361
  const incremental = projectSpreadsheetSheetsFromFortuneOperations(sheets, sourceSheets, operations);
5354
5362
  if (incremental) {
5355
5363
  recordSpreadsheetControlledProjectionMeasure(startedAt, spreadsheetProjectionNow(), {
@@ -5357,7 +5365,7 @@ function spreadsheetSheetsFromFortune(sheets, sourceSheets, operations = []) {
5357
5365
  incremental: true,
5358
5366
  operationCount: operations.length
5359
5367
  });
5360
- return reconcileSpreadsheetFiltersAfterFortune(reconcileSpreadsheetTablesAfterFortune(incremental.sheets, sourceSheets, operations), sourceSheets, operations);
5368
+ return reconcileSpreadsheetFiltersAfterFortune(reconcileSpreadsheetTablesAfterFortune(incremental.sheets, sourceSheets, operations), sourceSheets, operations, filterContext);
5361
5369
  }
5362
5370
  const operationCoordinatesBySheet = spreadsheetCellOperationCoordinates(operations);
5363
5371
  const projected = sheets.map((sheet, index)=>{
@@ -5399,7 +5407,7 @@ function spreadsheetSheetsFromFortune(sheets, sourceSheets, operations = []) {
5399
5407
  incremental: false,
5400
5408
  operationCount: operations.length
5401
5409
  });
5402
- return reconcileSpreadsheetFiltersAfterFortune(reconcileSpreadsheetTablesAfterFortune(projected, sourceSheets, operations), sourceSheets, operations);
5410
+ return reconcileSpreadsheetFiltersAfterFortune(reconcileSpreadsheetTablesAfterFortune(projected, sourceSheets, operations), sourceSheets, operations, filterContext);
5403
5411
  }
5404
5412
  function recordSpreadsheetControlledProjectionMeasure(start, end, detail) {
5405
5413
  try {
@@ -5929,18 +5937,18 @@ function toggleSpreadsheetAutoFilter(content, sheetId, selection) {
5929
5937
  function spreadsheetAutoFilterCriteria(sheet, column) {
5930
5938
  return workSpreadsheetAutoFilterCriteria(sheet, column);
5931
5939
  }
5932
- function spreadsheetAutoFilterColumnIsNumeric(sheet, range, column) {
5933
- return spreadsheetAutoFilterColumnProfile(sheet, range, column).numeric;
5940
+ function spreadsheetAutoFilterColumnIsNumeric(sheet, range, column, dateSystem = '1900') {
5941
+ return spreadsheetAutoFilterColumnProfile(sheet, range, column, dateSystem).numeric;
5934
5942
  }
5935
- function spreadsheetAutoFilterColumnIsDate(sheet, range, column) {
5936
- return spreadsheetAutoFilterColumnProfile(sheet, range, column).date;
5943
+ function spreadsheetAutoFilterColumnIsDate(sheet, range, column, dateSystem = '1900') {
5944
+ return spreadsheetAutoFilterColumnProfile(sheet, range, column, dateSystem).date;
5937
5945
  }
5938
- function spreadsheetAutoFilterColumnProfile(sheet, range, column) {
5946
+ function spreadsheetAutoFilterColumnProfile(sheet, range, column, dateSystem) {
5939
5947
  if (!Number.isSafeInteger(column) || column < range.column[0] || column > range.column[1]) return {
5940
5948
  date: false,
5941
5949
  numeric: false
5942
5950
  };
5943
- const key = `${range.row[0]}:${range.row[1]}:${column}`;
5951
+ const key = `${dateSystem}:${range.row[0]}:${range.row[1]}:${column}`;
5944
5952
  const cached = spreadsheetAutoFilterColumnProfileCache.get(sheet)?.get(key);
5945
5953
  if (cached) return cached;
5946
5954
  const cellAt = spreadsheetSheetCellReader(sheet);
@@ -5949,7 +5957,7 @@ function spreadsheetAutoFilterColumnProfile(sheet, range, column) {
5949
5957
  const cell = cellAt(row, column);
5950
5958
  const value = cell?.v ?? cell?.m;
5951
5959
  if (null == value || '' === value) continue;
5952
- const valueKind = workSpreadsheetCellIsDate(cell) ? 'date' : 'number' == typeof value && Number.isFinite(value) ? 'numeric' : null;
5960
+ const valueKind = workSpreadsheetCellIsDate(cell, dateSystem) ? 'date' : 'number' == typeof value && Number.isFinite(value) ? 'numeric' : null;
5953
5961
  if (!valueKind || null !== columnKind && columnKind !== valueKind) {
5954
5962
  columnKind = null;
5955
5963
  break;
@@ -5969,7 +5977,10 @@ function applySpreadsheetAutoFilterCriteria(content, sheetId, column, criteria,
5969
5977
  const sheetIndex = content.sheets.findIndex((sheet)=>sheet.id === sheetId);
5970
5978
  const sheet = content.sheets[sheetIndex];
5971
5979
  if (!sheet) return null;
5972
- const nextSheet = workSpreadsheetSheetWithAutoFilterCriteria(sheet, column, criteria, context);
5980
+ const nextSheet = workSpreadsheetSheetWithAutoFilterCriteria(sheet, column, criteria, {
5981
+ dateSystem: content.dateSystem,
5982
+ now: context?.now ?? new Date()
5983
+ });
5973
5984
  if (!nextSheet) return null;
5974
5985
  const sheets = [
5975
5986
  ...content.sheets
@@ -8529,13 +8540,15 @@ function clampSpreadsheetDataValidationFocus(value, range) {
8529
8540
  const MILLISECONDS_PER_DAY = 86400000;
8530
8541
  const MINUTES_PER_DAY = 1440;
8531
8542
  const EXCEL_1900_EPOCH_UTC = Date.UTC(1899, 11, 30);
8532
- function spreadsheetDateTimeEntry(kind, now) {
8543
+ const EXCEL_1904_EPOCH_UTC = Date.UTC(1904, 0, 1);
8544
+ function spreadsheetDateTimeEntry(kind, now, dateSystem = '1900') {
8533
8545
  if (!Number.isFinite(now.getTime())) return null;
8534
8546
  if ('date' === kind) {
8535
8547
  const year = now.getFullYear();
8536
8548
  const month = now.getMonth();
8537
8549
  const day = now.getDate();
8538
- const value = (Date.UTC(year, month, day) - EXCEL_1900_EPOCH_UTC) / MILLISECONDS_PER_DAY;
8550
+ const epoch = '1904' === dateSystem ? EXCEL_1904_EPOCH_UTC : EXCEL_1900_EPOCH_UTC;
8551
+ const value = (Date.UTC(year, month, day) - epoch) / MILLISECONDS_PER_DAY;
8539
8552
  if (!Number.isFinite(value)) return null;
8540
8553
  return {
8541
8554
  format: {
@@ -8576,7 +8589,7 @@ function createSpreadsheetDateTimeExtension() {
8576
8589
  function insertCurrentSpreadsheetDateTime(context, kind) {
8577
8590
  if (!isSpreadsheetDateTimeKind(kind)) return false;
8578
8591
  const target = spreadsheetDateTimeTarget(context);
8579
- const entry = spreadsheetDateTimeEntry(kind, new Date());
8592
+ const entry = spreadsheetDateTimeEntry(kind, new Date(), context.content.dateSystem);
8580
8593
  if (!target || !entry || !context.workbook) return false;
8581
8594
  try {
8582
8595
  context.workbook.batchCallApis([
@@ -10369,7 +10382,10 @@ function applySpreadsheetSort(context, request) {
10369
10382
  if (!result.ok) return false;
10370
10383
  if (owned && spreadsheetSortRequiresFilterReconciliation(sheet, owned)) {
10371
10384
  const sorted = spreadsheetSheetWithSortedRange(sheet, validation.request.range, result.rows);
10372
- const reconciled = reconcileSpreadsheetFiltersAfterSort(sorted, sheet, validation.request.range, result.sourceIndexes);
10385
+ const reconciled = reconcileSpreadsheetFiltersAfterSort(sorted, sheet, validation.request.range, result.sourceIndexes, {
10386
+ dateSystem: context.content.dateSystem,
10387
+ now: new Date()
10388
+ });
10373
10389
  if (!reconciled) return false;
10374
10390
  context.onChange({
10375
10391
  ...context.content,
@@ -23509,7 +23525,7 @@ const SPREADSHEET_AUTO_FILTER_MENU_ITEMS = [
23509
23525
  '|',
23510
23526
  'filter-by-value'
23511
23527
  ];
23512
- function enhanceSpreadsheetAutoFilterSurface(container, sheet, invoker) {
23528
+ function enhanceSpreadsheetAutoFilterSurface(container, sheet, invoker, dateSystem = '1900') {
23513
23529
  const range = sheet?.filter_select;
23514
23530
  const startRow = range?.row?.[0];
23515
23531
  const startColumn = range?.column?.[0];
@@ -23556,14 +23572,14 @@ function enhanceSpreadsheetAutoFilterSurface(container, sheet, invoker) {
23556
23572
  action.setAttribute('aria-haspopup', 'dialog');
23557
23573
  }
23558
23574
  }
23559
- synchronizeSpreadsheetAutoFilterRankAction(menu, sheet, trigger);
23575
+ synchronizeSpreadsheetAutoFilterRankAction(menu, sheet, trigger, dateSystem);
23560
23576
  return menu;
23561
23577
  }
23562
- function synchronizeSpreadsheetAutoFilterRankAction(menu, sheet, trigger) {
23578
+ function synchronizeSpreadsheetAutoFilterRankAction(menu, sheet, trigger, dateSystem) {
23563
23579
  const existing = menu.querySelector('[data-a3s-auto-filter-rank]');
23564
23580
  const range = normalizedWorkSpreadsheetAutoFilterRange(sheet?.filter_select);
23565
23581
  const column = Number(trigger?.dataset.filterColumn);
23566
- const numeric = Boolean(sheet && range && Number.isSafeInteger(column) && spreadsheetAutoFilterColumnIsNumeric(sheet, range, column));
23582
+ const numeric = Boolean(sheet && range && Number.isSafeInteger(column) && spreadsheetAutoFilterColumnIsNumeric(sheet, range, column, dateSystem));
23567
23583
  if (!numeric) return void existing?.remove();
23568
23584
  if (existing) {
23569
23585
  existing.dataset.filterColumn = String(column);
@@ -24291,7 +24307,7 @@ function useSpreadsheetAutoFilter({ canvasRef, commandsRef, content, editable, m
24291
24307
  const column = spreadsheetAutoFilterHeaderColumn(activeSheet, currentSpreadsheetAutoFilterSelection(selectionRef.current, workbookRef.current));
24292
24308
  const startColumn = activeSheet?.filter_select?.column?.[0];
24293
24309
  if (!container || null === column || !Number.isFinite(startColumn)) return false;
24294
- enhanceSpreadsheetAutoFilterSurface(container, activeSheet, keyboardInvokerRef.current);
24310
+ enhanceSpreadsheetAutoFilterSurface(container, activeSheet, keyboardInvokerRef.current, contentRef.current.dateSystem);
24295
24311
  const triggers = [
24296
24312
  ...container.querySelectorAll('.luckysheet-filter-options')
24297
24313
  ];
@@ -24300,7 +24316,7 @@ function useSpreadsheetAutoFilter({ canvasRef, commandsRef, content, editable, m
24300
24316
  keyboardInvokerRef.current = trigger;
24301
24317
  restoreInvokerFocusRef.current = true;
24302
24318
  trigger.click();
24303
- enhanceSpreadsheetAutoFilterSurface(container, activeSheet, trigger);
24319
+ enhanceSpreadsheetAutoFilterSurface(container, activeSheet, trigger, contentRef.current.dateSystem);
24304
24320
  focusSpreadsheetAutoFilterMenu(container);
24305
24321
  return true;
24306
24322
  }, [
@@ -24328,11 +24344,11 @@ function useSpreadsheetAutoFilter({ canvasRef, commandsRef, content, editable, m
24328
24344
  column,
24329
24345
  columnLabel: spreadsheetAutoFilterColumnLabel(activeSheet, range.row[0], column),
24330
24346
  criteria,
24331
- date: spreadsheetAutoFilterColumnIsDate(activeSheet, range, column),
24347
+ date: spreadsheetAutoFilterColumnIsDate(activeSheet, range, column, contentRef.current.dateSystem),
24332
24348
  filterRange: range,
24333
24349
  hasActiveFilter: Object.hasOwn(activeSheet.filter ?? {}, key),
24334
24350
  invoker,
24335
- numeric: spreadsheetAutoFilterColumnIsNumeric(activeSheet, range, column),
24351
+ numeric: spreadsheetAutoFilterColumnIsNumeric(activeSheet, range, column, contentRef.current.dateSystem),
24336
24352
  sheetId: activeSheet.id ?? sheetIdRef.current,
24337
24353
  sheetName: activeSheet.name
24338
24354
  };
@@ -24354,7 +24370,7 @@ function useSpreadsheetAutoFilter({ canvasRef, commandsRef, content, editable, m
24354
24370
  if (!container || !editable) return;
24355
24371
  const enhance = ()=>{
24356
24372
  const activeSheet = contentRef.current.sheets.find((candidate)=>candidate.id === sheetIdRef.current);
24357
- const menu = enhanceSpreadsheetAutoFilterSurface(container, activeSheet, keyboardInvokerRef.current);
24373
+ const menu = enhanceSpreadsheetAutoFilterSurface(container, activeSheet, keyboardInvokerRef.current, contentRef.current.dateSystem);
24358
24374
  const menuOpen = Boolean(menu);
24359
24375
  if (menuWasOpenRef.current && !menuOpen && restoreInvokerFocusRef.current) {
24360
24376
  const invoker = keyboardInvokerRef.current;
@@ -24389,7 +24405,7 @@ function useSpreadsheetAutoFilter({ canvasRef, commandsRef, content, editable, m
24389
24405
  keyboardInvokerRef.current = trigger;
24390
24406
  restoreInvokerFocusRef.current = true;
24391
24407
  trigger.click();
24392
- enhanceSpreadsheetAutoFilterSurface(container, contentRef.current.sheets.find((candidate)=>candidate.id === sheetIdRef.current), trigger);
24408
+ enhanceSpreadsheetAutoFilterSurface(container, contentRef.current.sheets.find((candidate)=>candidate.id === sheetIdRef.current), trigger, contentRef.current.dateSystem);
24393
24409
  focusSpreadsheetAutoFilterMenu(container);
24394
24410
  return;
24395
24411
  }
@@ -30136,7 +30152,7 @@ function SpreadsheetEditorSurface({ autoFocus = true, content, collaborationHist
30136
30152
  if (ignoreChangeDuringExternalSync(matchesRenderedWorkbook)) return;
30137
30153
  const hasPendingResultPatches = calculation.hasPendingResultPatches();
30138
30154
  if (previewRef.current || matchesRenderedWorkbook && !hasPendingResultPatches) return;
30139
- const controlledSheets = spreadsheetSheetsFromFortune(sheets, contentRef.current.sheets, operations);
30155
+ const controlledSheets = spreadsheetSheetsFromFortune(sheets, contentRef.current.sheets, operations, contentRef.current.dateSystem);
30140
30156
  const withCharts = reconcileSpreadsheetChartPreviews(contentRef.current, controlledSheets);
30141
30157
  const next = reconcileSpreadsheetPivots(contentRef.current, withCharts.sheets);
30142
30158
  const isHistoryNoop = sameSpreadsheetHistoryContent(contentRef.current, next);
package/dist/5416.js CHANGED
@@ -591,6 +591,10 @@ function validateWorkOfficeSpreadsheetContent(content) {
591
591
  type: 'spreadsheet',
592
592
  sheets
593
593
  };
594
+ if (void 0 !== content.dateSystem) {
595
+ if ('1900' !== content.dateSystem && '1904' !== content.dateSystem) invalidWorkOfficeSpreadsheetInput("a Spreadsheet date system of '1900' or '1904'");
596
+ result.dateSystem = content.dateSystem;
597
+ }
594
598
  copyOptionalJsonField(content, result, 'calculation', 'calculation settings');
595
599
  if (void 0 !== content.namedRanges) result.namedRanges = validateIdRecords(content.namedRanges, 'named range');
596
600
  copyOptionalSheetSidecars(content, result, 'printAreas', 'print area');
@@ -1209,6 +1213,7 @@ function claimLabel(claim) {
1209
1213
  return `${claim.kind} in sheet '${claim.parentId}'`;
1210
1214
  }
1211
1215
  function assertWorkOfficeSpreadsheetPatchSafe(previous, next, shared) {
1216
+ assertCompatibleValue(previous.dateSystem, next.dateSystem, shared.dateSystem, 'date system');
1212
1217
  assertCompatibleValue(previous.calculation, next.calculation, shared.calculation, 'calculation settings');
1213
1218
  assertRecordCollection(previous.sheets, next.sheets, shared.sheets, (sheet)=>sheet.id, 'sheet', assertCompatibleSheet);
1214
1219
  assertRecordCollection(previous.namedRanges ?? [], next.namedRanges ?? [], shared.namedRanges ?? [], (range)=>range.id, 'named range');
@@ -1843,7 +1848,7 @@ function workOfficeSpreadsheetRoots(document, rootName) {
1843
1848
  }
1844
1849
  function initializeWorkOfficeSpreadsheetRoots(roots, content) {
1845
1850
  appendWorkOfficeSpreadsheetRecordClaims(roots.recordClaims, void 0, content);
1846
- patchSpreadsheetFlatJsonMap(roots.options, void 0, content.calculation, 'calculation settings');
1851
+ patchSpreadsheetFlatJsonMap(roots.options, void 0, spreadsheetWorkbookOptions(content), 'workbook options');
1847
1852
  patchSheets(roots, void 0, content);
1848
1853
  patchIdRecords(roots.namedRanges, roots.namedRangeOrder, [], content.namedRanges ?? [], 'named range');
1849
1854
  patchSheetSidecars(roots.printAreas, [], content.printAreas ?? [], 'print area');
@@ -1856,7 +1861,9 @@ function readWorkOfficeSpreadsheetRoots(roots) {
1856
1861
  type: 'spreadsheet',
1857
1862
  sheets: validatedSpreadsheetOrder(roots.sheetOrder, roots.sheets, 'sheet').map((id)=>readSpreadsheetSheetRecord(office_spreadsheet_collaboration_records_requiredSpreadsheetMap(roots.sheets, id, `sheet '${id}'`), id))
1858
1863
  };
1859
- const calculation = readSpreadsheetFlatJsonMap(roots.options, 'calculation settings');
1864
+ const options = readSpreadsheetFlatJsonMap(roots.options, 'workbook options');
1865
+ const { dateSystem, ...calculation } = options;
1866
+ if (void 0 !== dateSystem) result.dateSystem = dateSystem;
1860
1867
  if (Object.keys(calculation).length > 0) result.calculation = calculation;
1861
1868
  const namedRanges = readIdRecords(roots.namedRanges, roots.namedRangeOrder, 'named range');
1862
1869
  if (namedRanges.length > 0) result.namedRanges = namedRanges;
@@ -1872,7 +1879,7 @@ function patchWorkOfficeSpreadsheetRoots(roots, previous, next) {
1872
1879
  const shared = readWorkOfficeSpreadsheetRoots(roots);
1873
1880
  assertWorkOfficeSpreadsheetPatchSafe(previous, next, shared);
1874
1881
  appendWorkOfficeSpreadsheetRecordClaims(roots.recordClaims, previous, next);
1875
- patchSpreadsheetFlatJsonMap(roots.options, previous.calculation, next.calculation, 'calculation settings');
1882
+ patchSpreadsheetFlatJsonMap(roots.options, spreadsheetWorkbookOptions(previous), spreadsheetWorkbookOptions(next), 'workbook options');
1876
1883
  patchSheets(roots, previous, next);
1877
1884
  patchIdRecords(roots.namedRanges, roots.namedRangeOrder, previous.namedRanges ?? [], next.namedRanges ?? [], 'named range');
1878
1885
  patchSheetSidecars(roots.printAreas, previous.printAreas ?? [], next.printAreas ?? [], 'print area');
@@ -1880,6 +1887,13 @@ function patchWorkOfficeSpreadsheetRoots(roots, previous, next) {
1880
1887
  patchSheetSidecars(roots.pageBreaks, previous.pageBreaks ?? [], next.pageBreaks ?? [], 'page breaks');
1881
1888
  patchSheetSidecars(roots.pageSetups, previous.pageSetups ?? [], next.pageSetups ?? [], 'page setup');
1882
1889
  }
1890
+ function spreadsheetWorkbookOptions(content) {
1891
+ const options = {
1892
+ ...content.calculation
1893
+ };
1894
+ if (void 0 !== content.dateSystem) options.dateSystem = content.dateSystem;
1895
+ return Object.keys(options).length ? options : void 0;
1896
+ }
1883
1897
  function assertWorkOfficeSpreadsheetRootsEmpty(roots) {
1884
1898
  if (roots.options.size > 0 || roots.sheets.size > 0 || roots.sheetOrder.length > 0 || roots.namedRanges.size > 0 || roots.namedRangeOrder.length > 0 || roots.recordClaims.length > 0 || roots.printAreas.size > 0 || roots.printTitles.size > 0 || roots.pageBreaks.size > 0 || roots.pageSetups.size > 0) throw new WorkOfficeCollaborationError('office.collaboration.bootstrap_ambiguous', 'The Spreadsheet collaboration roots contain data without initialized metadata.');
1885
1899
  }
@@ -2212,9 +2226,8 @@ function workSpreadsheetFilterTextIsBounded(value) {
2212
2226
  return characters >= 1 && characters <= WORK_SPREADSHEET_FILTER_TEXT_MAX_CHARACTERS;
2213
2227
  }
2214
2228
  const MILLISECONDS_PER_DAY = 86400000;
2215
- const EXCEL_1900_EPOCH_UTC = Date.UTC(1899, 11, 30);
2216
2229
  const EXCEL_1900_CIVIL_EPOCH_UTC = Date.UTC(1899, 11, 31);
2217
- const EXCEL_1900_LOCAL_BASE = new Date(1899, 11, 30);
2230
+ const EXCEL_1904_EPOCH_UTC = Date.UTC(1904, 0, 1);
2218
2231
  const SERIAL_INTEGER_EPSILON = 1e-7;
2219
2232
  const CALENDAR_MONTH_FILTERS = [
2220
2233
  'month-1',
@@ -2239,18 +2252,18 @@ const CALENDAR_QUARTER_FILTERS = [
2239
2252
  function workSpreadsheetDynamicFilterMatcher(kind, cells, context) {
2240
2253
  if ('above-average' === kind || 'below-average' === kind) return averageMatcher(kind, cells);
2241
2254
  const month = CALENDAR_MONTH_FILTERS.indexOf(kind);
2242
- if (month >= 0) return (cell)=>spreadsheetDateParts(cell)?.month === month;
2255
+ if (month >= 0) return (cell)=>spreadsheetDateParts(cell, context.dateSystem)?.month === month;
2243
2256
  const quarter = CALENDAR_QUARTER_FILTERS.indexOf(kind);
2244
- if (quarter >= 0) return (cell)=>spreadsheetDateParts(cell)?.quarter === quarter;
2257
+ if (quarter >= 0) return (cell)=>spreadsheetDateParts(cell, context.dateSystem)?.quarter === quarter;
2245
2258
  const bounds = relativeDateBounds(kind, context.now);
2246
2259
  if (!bounds) return null;
2247
2260
  return (cell)=>{
2248
- const day = spreadsheetDateSerialDay(cell);
2261
+ const day = spreadsheetDateCivilDay(cell, context.dateSystem);
2249
2262
  return null !== day && day >= bounds.start && day < bounds.end;
2250
2263
  };
2251
2264
  }
2252
- function workSpreadsheetCellIsDate(cell) {
2253
- return null !== spreadsheetDateSerialDay(cell);
2265
+ function workSpreadsheetCellIsDate(cell, dateSystem = '1900') {
2266
+ return null !== spreadsheetDateCivilDay(cell, dateSystem);
2254
2267
  }
2255
2268
  function averageMatcher(kind, cells) {
2256
2269
  const average = finiteArithmeticMean(cells);
@@ -2284,7 +2297,7 @@ function relativeDateBounds(kind, now) {
2284
2297
  const year = now.getFullYear();
2285
2298
  if (!Number.isFinite(timestamp) || !Number.isSafeInteger(year) || year < 1900 || year > 9999) return null;
2286
2299
  const month = now.getMonth();
2287
- const today = excelSerialFromCivilDate(year, month, now.getDate());
2300
+ const today = civilDayFromDateParts(year, month, now.getDate());
2288
2301
  if ('yesterday' === kind) return dayBounds(today - 1);
2289
2302
  if ('today' === kind) return dayBounds(today);
2290
2303
  if ('tomorrow' === kind) return dayBounds(today + 1);
@@ -2292,65 +2305,64 @@ function relativeDateBounds(kind, now) {
2292
2305
  if ('last-week' === kind) return rangeBounds(weekStart - 7, weekStart);
2293
2306
  if ('this-week' === kind) return rangeBounds(weekStart, weekStart + 7);
2294
2307
  if ('next-week' === kind) return rangeBounds(weekStart + 7, weekStart + 14);
2295
- const monthStart = excelSerialFromCivilDate(year, month, 1);
2296
- if ('last-month' === kind) return rangeBounds(excelSerialFromCivilDate(year, month - 1, 1), monthStart);
2297
- if ('this-month' === kind) return rangeBounds(monthStart, excelSerialFromCivilDate(year, month + 1, 1));
2298
- if ('next-month' === kind) return rangeBounds(excelSerialFromCivilDate(year, month + 1, 1), excelSerialFromCivilDate(year, month + 2, 1));
2308
+ const monthStart = civilDayFromDateParts(year, month, 1);
2309
+ if ('last-month' === kind) return rangeBounds(civilDayFromDateParts(year, month - 1, 1), monthStart);
2310
+ if ('this-month' === kind) return rangeBounds(monthStart, civilDayFromDateParts(year, month + 1, 1));
2311
+ if ('next-month' === kind) return rangeBounds(civilDayFromDateParts(year, month + 1, 1), civilDayFromDateParts(year, month + 2, 1));
2299
2312
  const quarterStartMonth = 3 * Math.floor(month / 3);
2300
- const quarterStart = excelSerialFromCivilDate(year, quarterStartMonth, 1);
2301
- if ('last-quarter' === kind) return rangeBounds(excelSerialFromCivilDate(year, quarterStartMonth - 3, 1), quarterStart);
2302
- if ('this-quarter' === kind) return rangeBounds(quarterStart, excelSerialFromCivilDate(year, quarterStartMonth + 3, 1));
2303
- if ('next-quarter' === kind) return rangeBounds(excelSerialFromCivilDate(year, quarterStartMonth + 3, 1), excelSerialFromCivilDate(year, quarterStartMonth + 6, 1));
2304
- const yearStart = excelSerialFromCivilDate(year, 0, 1);
2305
- if ('last-year' === kind) return rangeBounds(excelSerialFromCivilDate(year - 1, 0, 1), yearStart);
2306
- if ('this-year' === kind) return rangeBounds(yearStart, excelSerialFromCivilDate(year + 1, 0, 1));
2307
- if ('next-year' === kind) return rangeBounds(excelSerialFromCivilDate(year + 1, 0, 1), excelSerialFromCivilDate(year + 2, 0, 1));
2313
+ const quarterStart = civilDayFromDateParts(year, quarterStartMonth, 1);
2314
+ if ('last-quarter' === kind) return rangeBounds(civilDayFromDateParts(year, quarterStartMonth - 3, 1), quarterStart);
2315
+ if ('this-quarter' === kind) return rangeBounds(quarterStart, civilDayFromDateParts(year, quarterStartMonth + 3, 1));
2316
+ if ('next-quarter' === kind) return rangeBounds(civilDayFromDateParts(year, quarterStartMonth + 3, 1), civilDayFromDateParts(year, quarterStartMonth + 6, 1));
2317
+ const yearStart = civilDayFromDateParts(year, 0, 1);
2318
+ if ('last-year' === kind) return rangeBounds(civilDayFromDateParts(year - 1, 0, 1), yearStart);
2319
+ if ('this-year' === kind) return rangeBounds(yearStart, civilDayFromDateParts(year + 1, 0, 1));
2320
+ if ('next-year' === kind) return rangeBounds(civilDayFromDateParts(year + 1, 0, 1), civilDayFromDateParts(year + 2, 0, 1));
2308
2321
  if ('year-to-date' === kind) return rangeBounds(yearStart, today + 1);
2309
2322
  return null;
2310
2323
  }
2311
- function spreadsheetDateParts(cell) {
2312
- const day = spreadsheetDateSerialDay(cell);
2324
+ function spreadsheetDateParts(cell, dateSystem = '1900') {
2325
+ const day = spreadsheetDateCivilDay(cell, dateSystem);
2313
2326
  if (null === day) return null;
2314
- const adjustedDay = day > 60 ? day - 1 : day;
2315
- const date = new Date(EXCEL_1900_CIVIL_EPOCH_UTC + adjustedDay * MILLISECONDS_PER_DAY);
2327
+ const date = new Date(day * MILLISECONDS_PER_DAY);
2316
2328
  const timestamp = date.getTime();
2317
2329
  const year = date.getUTCFullYear();
2318
- if (!Number.isFinite(timestamp) || year < 1900 || year > 9999) return null;
2330
+ if (!Number.isFinite(timestamp) || year > 9999) return null;
2319
2331
  const month = date.getUTCMonth();
2320
2332
  return {
2321
2333
  month,
2322
2334
  quarter: Math.floor(month / 3)
2323
2335
  };
2324
2336
  }
2325
- function spreadsheetDateSerialDay(cell) {
2326
- const serial = spreadsheetDateCellSerial(cell);
2327
- if (null === serial) return null;
2328
- const rounded = Math.round(serial);
2329
- const normalized = Math.abs(serial - rounded) <= SERIAL_INTEGER_EPSILON ? rounded : serial;
2330
- const day = Math.floor(normalized);
2331
- return day >= 1 && 60 !== day ? day : null;
2332
- }
2333
- function spreadsheetDateCellSerial(cell) {
2337
+ function spreadsheetDateCivilDay(cell, dateSystem = '1900') {
2334
2338
  const value = spreadsheetCellRawValue(cell);
2335
2339
  if (value instanceof Date) {
2336
2340
  if (!Number.isFinite(value.getTime())) return null;
2337
- const threshold = EXCEL_1900_LOCAL_BASE.getTime() + (value.getTimezoneOffset() - EXCEL_1900_LOCAL_BASE.getTimezoneOffset()) * 60000;
2338
- const serial = (value.getTime() - threshold) / MILLISECONDS_PER_DAY;
2339
- return Number.isFinite(serial) ? serial : null;
2341
+ const year = value.getFullYear();
2342
+ const minimumYear = '1904' === dateSystem ? 1904 : 1900;
2343
+ return year >= minimumYear && year <= 9999 ? civilDayFromDateParts(year, value.getMonth(), value.getDate()) : null;
2340
2344
  }
2341
- return 'number' == typeof value && Number.isFinite(value) && cell?.ct?.t === 'd' ? value : null;
2345
+ if ('number' != typeof value || !Number.isFinite(value) || cell?.ct?.t !== 'd') return null;
2346
+ const serial = value;
2347
+ const rounded = Math.round(serial);
2348
+ const normalized = Math.abs(serial - rounded) <= SERIAL_INTEGER_EPSILON ? rounded : serial;
2349
+ const day = Math.floor(normalized);
2350
+ if ('1904' === dateSystem) return day >= 0 ? (EXCEL_1904_EPOCH_UTC + day * MILLISECONDS_PER_DAY) / MILLISECONDS_PER_DAY : null;
2351
+ if (day < 1 || 60 === day) return null;
2352
+ const civilOffset = day > 60 ? day - 1 : day;
2353
+ return (EXCEL_1900_CIVIL_EPOCH_UTC + civilOffset * MILLISECONDS_PER_DAY) / MILLISECONDS_PER_DAY;
2342
2354
  }
2343
2355
  function spreadsheetNumericCellValue(cell) {
2344
- if (null !== spreadsheetDateCellSerial(cell)) return null;
2345
2356
  const value = spreadsheetCellRawValue(cell);
2357
+ if (value instanceof Date || cell?.ct?.t === 'd') return null;
2346
2358
  return 'number' == typeof value && Number.isFinite(value) ? value : null;
2347
2359
  }
2348
2360
  function spreadsheetCellRawValue(cell) {
2349
2361
  const candidate = cell;
2350
2362
  return candidate?.v ?? candidate?.m;
2351
2363
  }
2352
- function excelSerialFromCivilDate(year, month, day) {
2353
- return (Date.UTC(year, month, day) - EXCEL_1900_EPOCH_UTC) / MILLISECONDS_PER_DAY;
2364
+ function civilDayFromDateParts(year, month, day) {
2365
+ return Date.UTC(year, month, day) / MILLISECONDS_PER_DAY;
2354
2366
  }
2355
2367
  function dayBounds(day) {
2356
2368
  return {
package/dist/core.js CHANGED
@@ -3464,12 +3464,16 @@ async function createWorkSpreadsheetBlob(artifact) {
3464
3464
  const XLSX = await import("xlsx");
3465
3465
  const workbook = XLSX.utils.book_new();
3466
3466
  for (const sheet of content.sheets){
3467
- const worksheet = createSparseXlsxWorksheet(sheet, XLSX);
3467
+ const worksheet = createSparseXlsxWorksheet(sheet, XLSX, content.dateSystem);
3468
3468
  applySpreadsheetLayout(worksheet, sheet);
3469
3469
  XLSX.utils.book_append_sheet(workbook, worksheet, sheet.name.slice(0, 31) || '工作表');
3470
3470
  }
3471
3471
  workbook.Workbook = {
3472
3472
  ...workbook.Workbook,
3473
+ WBProps: {
3474
+ ...workbook.Workbook?.WBProps,
3475
+ date1904: '1904' === content.dateSystem
3476
+ },
3473
3477
  Sheets: content.sheets.map((sheet)=>({
3474
3478
  name: sheet.name.slice(0, 31) || '工作表',
3475
3479
  Hidden: sheet.hide ? 1 : 0
@@ -3492,7 +3496,7 @@ async function createWorkSpreadsheetBlob(artifact) {
3492
3496
  type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
3493
3497
  });
3494
3498
  }
3495
- function createSparseXlsxWorksheet(sheet, XLSX) {
3499
+ function createSparseXlsxWorksheet(sheet, XLSX, dateSystem = '1900') {
3496
3500
  const data = sheet.data ?? [];
3497
3501
  const worksheet = {
3498
3502
  '!a3sSparse': true
@@ -3511,7 +3515,7 @@ function createSparseXlsxWorksheet(sheet, XLSX) {
3511
3515
  const style = xlsxCellStyle(cell);
3512
3516
  const comment = exportXlsxCellComment(cell.ps);
3513
3517
  const hyperlink = sheet.hyperlink?.[`${rowIndex}_${columnIndex}`];
3514
- const target = xlsxCellObject(cell, rowIndex, columnIndex, sheet) ?? (style || comment || hyperlink ? {
3518
+ const target = xlsxCellObject(cell, rowIndex, columnIndex, sheet, dateSystem) ?? (style || comment || hyperlink ? {
3515
3519
  t: 's',
3516
3520
  v: ''
3517
3521
  } : null);
@@ -3538,14 +3542,30 @@ function createSparseXlsxWorksheet(sheet, XLSX) {
3538
3542
  });
3539
3543
  return worksheet;
3540
3544
  }
3541
- function xlsxCellObject(cell, row, column, sheet) {
3542
- if (cell.f) return createXlsxFormulaCell(cell, row, column, sheet);
3545
+ function xlsxCellObject(cell, row, column, sheet, dateSystem) {
3546
+ if (cell.f) {
3547
+ const formula = createXlsxFormulaCell(cell, row, column, sheet);
3548
+ if (formula.v instanceof Date && Number.isFinite(formula.v.getTime())) return {
3549
+ ...formula,
3550
+ t: 'n',
3551
+ v: spreadsheetDateSerial(formula.v, dateSystem)
3552
+ };
3553
+ if ('d' === formula.t && 'number' == typeof formula.v) return {
3554
+ ...formula,
3555
+ t: 'n'
3556
+ };
3557
+ return formula;
3558
+ }
3543
3559
  if (cell.ct?.t === 'e') return createXlsxErrorCell(cell);
3544
3560
  const value = xlsxRichTextCellText(cell) ?? cell.v ?? cell.m;
3545
3561
  if (null == value) return cell.ps ? {
3546
3562
  t: 's',
3547
3563
  v: ''
3548
3564
  } : null;
3565
+ if (value instanceof Date && Number.isFinite(value.getTime())) return {
3566
+ t: 'n',
3567
+ v: spreadsheetDateSerial(value, dateSystem)
3568
+ };
3549
3569
  if ('boolean' == typeof value) return {
3550
3570
  t: 'b',
3551
3571
  v: value
@@ -3559,6 +3579,15 @@ function xlsxCellObject(cell, row, column, sheet) {
3559
3579
  v: String(value)
3560
3580
  };
3561
3581
  }
3582
+ function spreadsheetDateSerial(value, dateSystem) {
3583
+ const year = value.getFullYear();
3584
+ const month = value.getMonth();
3585
+ const day = value.getDate();
3586
+ const time = (3600000 * value.getHours() + 60000 * value.getMinutes() + 1000 * value.getSeconds() + value.getMilliseconds()) / 86400000;
3587
+ if ('1904' === dateSystem) return (Date.UTC(year, month, day) - Date.UTC(1904, 0, 1)) / 86400000 + time;
3588
+ const civilDay = (Date.UTC(year, month, day) - Date.UTC(1899, 11, 31)) / 86400000;
3589
+ return civilDay + (civilDay >= 60 ? 1 : 0) + time;
3590
+ }
3562
3591
  function applySpreadsheetLayout(worksheet, sheet) {
3563
3592
  const merges = Object.values(sheet.config?.merge ?? {});
3564
3593
  if (merges.length) worksheet['!merges'] = merges.map((merge)=>({
@@ -4101,7 +4130,7 @@ async function importWorkSpreadsheetFile(file, extension, context) {
4101
4130
  const initialRequiresCellStyles = archive ? archive.has('xl/styles.xml') : true;
4102
4131
  let readOptions = {
4103
4132
  type: 'array',
4104
- cellDates: true,
4133
+ cellDates: false,
4105
4134
  cellFormula: true,
4106
4135
  cellHTML: false,
4107
4136
  cellStyles: initialRequiresCellStyles,
@@ -4185,6 +4214,11 @@ async function importWorkSpreadsheetFile(file, extension, context) {
4185
4214
  ]);
4186
4215
  const XLSX = usesPlainXlsxFastPath ? null : await loadSheetJs();
4187
4216
  const [sheetFeatures, formulaFeatures, pivotFeatures] = importedFeatures;
4217
+ const dateSystem = workbook.Workbook?.WBProps?.date1904 ? '1904' : '1900';
4218
+ const dynamicFilterContext = {
4219
+ dateSystem,
4220
+ now: new Date()
4221
+ };
4188
4222
  await context.controller.checkpoint('parsing', 0.2);
4189
4223
  await context.controller.checkpoint('parsing', 0.4);
4190
4224
  const conversionStartedAt = spreadsheetImportNow();
@@ -4301,7 +4335,7 @@ async function importWorkSpreadsheetFile(file, extension, context) {
4301
4335
  charts: features?.charts.length ? xlsxWorksheetChartsToSheet(features.charts, config) : void 0,
4302
4336
  tables: features?.tables.length ? features.tables : void 0,
4303
4337
  formulaMetadata
4304
- }, features?.autoFilterCriteria ?? []));
4338
+ }, features?.autoFilterCriteria ?? [], dynamicFilterContext));
4305
4339
  await context.controller.checkpoint('parsing', sheetProgressStart + sheetProgressSize);
4306
4340
  }
4307
4341
  const workbookMetadata = importXlsxDefinedNames(workbook, sheets);
@@ -4330,6 +4364,9 @@ async function importWorkSpreadsheetFile(file, extension, context) {
4330
4364
  artifact.content = applyImportedXlsxPivotTables({
4331
4365
  type: 'spreadsheet',
4332
4366
  sheets,
4367
+ ...'1904' === dateSystem ? {
4368
+ dateSystem
4369
+ } : {},
4333
4370
  calculation: formulaFeatures?.calculation,
4334
4371
  ...workbookMetadata,
4335
4372
  pageBreaks: pageBreaks.length ? pageBreaks : void 0,
@@ -4447,7 +4484,7 @@ function fortuneSheetConfig(worksheet) {
4447
4484
  }
4448
4485
  function fortuneCellFromXlsx(source, row, column, sheetId, hyperlink, comment, XLSX, directStyle) {
4449
4486
  const cell = {
4450
- ...fortuneCellStyle(source),
4487
+ ...fortuneCellStyle(source, XLSX),
4451
4488
  ...directStyle
4452
4489
  };
4453
4490
  const orientation = spreadsheetExplicitTextOrientationFromCell(cell);
@@ -4476,7 +4513,7 @@ function fortuneCellFromXlsx(source, row, column, sheetId, hyperlink, comment, X
4476
4513
  function spreadsheetCellKey(row, column) {
4477
4514
  return `${row}_${column}`;
4478
4515
  }
4479
- function fortuneCellStyle(source) {
4516
+ function fortuneCellStyle(source, XLSX) {
4480
4517
  const style = source.s;
4481
4518
  const font = style && 'object' == typeof style ? style.font : void 0;
4482
4519
  const fill = style && 'object' == typeof style ? style.fill : void 0;
@@ -4504,10 +4541,19 @@ function fortuneCellStyle(source) {
4504
4541
  if (orientationStyle) Object.assign(target, orientationStyle);
4505
4542
  if (source.z && 'General' !== source.z || 'e' === source.t) target.ct = {
4506
4543
  fa: source.z && 'General' !== source.z ? String(source.z) : void 0,
4507
- t: source.t
4544
+ t: xlsxCellIsDate(source, XLSX) ? 'd' : source.t
4508
4545
  };
4509
4546
  return target;
4510
4547
  }
4548
+ function xlsxCellIsDate(source, XLSX) {
4549
+ if ('d' === source.t) return true;
4550
+ if ('n' !== source.t || !source.z || 'General' === source.z) return false;
4551
+ try {
4552
+ return Boolean(XLSX.SSF.is_date(String(source.z)));
4553
+ } catch {
4554
+ return false;
4555
+ }
4556
+ }
4511
4557
  function fortuneCellDisplayText(source, XLSX) {
4512
4558
  if (void 0 !== source.w) return source.w;
4513
4559
  if (void 0 === source.v || null === source.v) return;
@@ -1,6 +1,6 @@
1
- import type { WorkSpreadsheetSheet } from '../work-types';
1
+ import type { WorkSpreadsheetDateSystem, WorkSpreadsheetSheet } from '../work-types';
2
2
  export declare const SPREADSHEET_AUTO_FILTER_MENU_ITEMS: string[];
3
- export declare function enhanceSpreadsheetAutoFilterSurface(container: HTMLElement, sheet: WorkSpreadsheetSheet | undefined, invoker: HTMLElement | null): HTMLElement | null;
3
+ export declare function enhanceSpreadsheetAutoFilterSurface(container: HTMLElement, sheet: WorkSpreadsheetSheet | undefined, invoker: HTMLElement | null, dateSystem?: WorkSpreadsheetDateSystem): HTMLElement | null;
4
4
  export declare function focusSpreadsheetAutoFilterMenu(container: HTMLElement): void;
5
5
  export declare function spreadsheetAutoFilterMenuFocusable(menu: HTMLElement): HTMLElement[];
6
6
  export declare function spreadsheetAutoFilterTrigger(target: EventTarget | null): HTMLElement | null;
@@ -1,5 +1,5 @@
1
1
  import type { Selection } from '@fortune-sheet/core';
2
- import type { WorkSpreadsheetFilterCriteria, WorkSpreadsheetContent, WorkSpreadsheetSheet } from '../work-types';
2
+ import type { WorkSpreadsheetDateSystem, WorkSpreadsheetFilterCriteria, WorkSpreadsheetContent, WorkSpreadsheetSheet } from '../work-types';
3
3
  import { type WorkSpreadsheetAutoFilterRange } from '../work-spreadsheet-auto-filter';
4
4
  import { type WorkSpreadsheetDynamicFilterContext } from '../work-spreadsheet-dynamic-filter';
5
5
  export type SpreadsheetAutoFilterRange = WorkSpreadsheetAutoFilterRange;
@@ -16,7 +16,7 @@ export declare function spreadsheetSelectionOrCurrentRegion(sheet: WorkSpreadshe
16
16
  export declare function spreadsheetAutoFilterHeaderColumn(sheet: WorkSpreadsheetSheet | undefined, selection: Selection): number | null;
17
17
  export declare function toggleSpreadsheetAutoFilter(content: WorkSpreadsheetContent, sheetId: string, selection: Selection): WorkSpreadsheetContent | null;
18
18
  export declare function spreadsheetAutoFilterCriteria(sheet: WorkSpreadsheetSheet | undefined, column: number): WorkSpreadsheetFilterCriteria | null;
19
- export declare function spreadsheetAutoFilterColumnIsNumeric(sheet: WorkSpreadsheetSheet, range: SpreadsheetAutoFilterRange, column: number): boolean;
20
- export declare function spreadsheetAutoFilterColumnIsDate(sheet: WorkSpreadsheetSheet, range: SpreadsheetAutoFilterRange, column: number): boolean;
19
+ export declare function spreadsheetAutoFilterColumnIsNumeric(sheet: WorkSpreadsheetSheet, range: SpreadsheetAutoFilterRange, column: number, dateSystem?: WorkSpreadsheetDateSystem): boolean;
20
+ export declare function spreadsheetAutoFilterColumnIsDate(sheet: WorkSpreadsheetSheet, range: SpreadsheetAutoFilterRange, column: number, dateSystem?: WorkSpreadsheetDateSystem): boolean;
21
21
  export declare function applySpreadsheetAutoFilterCriteria(content: WorkSpreadsheetContent, sheetId: string, column: number, criteria: WorkSpreadsheetFilterCriteria, context?: WorkSpreadsheetDynamicFilterContext): WorkSpreadsheetContent | null;
22
22
  export declare function clearSpreadsheetAutoFilterCriteria(content: WorkSpreadsheetContent, sheetId: string, column: number): WorkSpreadsheetContent | null;
@@ -1,4 +1,5 @@
1
1
  import type { Cell } from '@fortune-sheet/core';
2
+ import type { WorkSpreadsheetDateSystem } from '../work-types';
2
3
  import type { SpreadsheetCommandContext, SpreadsheetEditorCommands } from './spreadsheet-command-controller';
3
4
  import { type OfficeEditorExtension } from './office-editor-extension';
4
5
  export type SpreadsheetDateTimeKind = 'date' | 'time';
@@ -10,5 +11,5 @@ export interface SpreadsheetDateTimeEntry {
10
11
  formulaBarValue: string;
11
12
  value: number;
12
13
  }
13
- export declare function spreadsheetDateTimeEntry(kind: SpreadsheetDateTimeKind, now: Date): SpreadsheetDateTimeEntry | null;
14
+ export declare function spreadsheetDateTimeEntry(kind: SpreadsheetDateTimeKind, now: Date, dateSystem?: WorkSpreadsheetDateSystem): SpreadsheetDateTimeEntry | null;
14
15
  export declare function createSpreadsheetDateTimeExtension(): OfficeEditorExtension<SpreadsheetCommandContext, SpreadsheetEditorCommands>;
@@ -1,5 +1,5 @@
1
1
  import type { Cell, Op, Selection } from '@fortune-sheet/core';
2
- import type { WorkSpreadsheetContent } from '../work-types';
2
+ import type { WorkSpreadsheetContent, WorkSpreadsheetDateSystem } from '../work-types';
3
3
  import type { OfficeSelectOption } from './office-select';
4
4
  export interface SpreadsheetSelectionSummary {
5
5
  average: number | null;
@@ -21,7 +21,7 @@ export declare function spreadsheetFontSizeOptions(current: number | undefined):
21
21
  export declare function spreadsheetFontFamilyOptions(current: string | undefined): OfficeSelectOption[];
22
22
  export declare function spreadsheetSheetsWithFiniteSelections(sheets: WorkSpreadsheetContent['sheets']): WorkSpreadsheetContent['sheets'];
23
23
  export declare function spreadsheetSheetsForFortune(sheets: WorkSpreadsheetContent['sheets']): WorkSpreadsheetContent['sheets'];
24
- export declare function spreadsheetSheetsFromFortune(sheets: WorkSpreadsheetContent['sheets'], sourceSheets: WorkSpreadsheetContent['sheets'], operations?: readonly Op[]): WorkSpreadsheetContent['sheets'];
24
+ export declare function spreadsheetSheetsFromFortune(sheets: WorkSpreadsheetContent['sheets'], sourceSheets: WorkSpreadsheetContent['sheets'], operations?: readonly Op[], dateSystem?: WorkSpreadsheetDateSystem): WorkSpreadsheetContent['sheets'];
25
25
  export declare function finiteSpreadsheetSelection(selection: Selection | undefined): Selection;
26
26
  export declare function sameSpreadsheetWorkbookState(changed: WorkSpreadsheetContent['sheets'], rendered: WorkSpreadsheetContent['sheets']): boolean;
27
27
  /**
@@ -1,6 +1,7 @@
1
1
  import type { Op } from '@fortune-sheet/core';
2
+ import type { WorkSpreadsheetDynamicFilterContext } from '../work-spreadsheet-dynamic-filter';
2
3
  import type { WorkSpreadsheetContent, WorkSpreadsheetSheet } from '../work-types';
3
4
  import type { SpreadsheetCellRange } from './spreadsheet-cell-range';
4
- export declare function reconcileSpreadsheetFiltersAfterFortune(sheets: WorkSpreadsheetContent['sheets'], sourceSheets: WorkSpreadsheetContent['sheets'], operations?: readonly Op[]): WorkSpreadsheetContent['sheets'];
5
- export declare function reconcileSpreadsheetFiltersAfterSort(sheet: WorkSpreadsheetSheet, source: WorkSpreadsheetSheet, range: SpreadsheetCellRange, sourceIndexes: readonly number[]): WorkSpreadsheetSheet | null;
5
+ export declare function reconcileSpreadsheetFiltersAfterFortune(sheets: WorkSpreadsheetContent['sheets'], sourceSheets: WorkSpreadsheetContent['sheets'], operations?: readonly Op[], context?: WorkSpreadsheetDynamicFilterContext): WorkSpreadsheetContent['sheets'];
6
+ export declare function reconcileSpreadsheetFiltersAfterSort(sheet: WorkSpreadsheetSheet, source: WorkSpreadsheetSheet, range: SpreadsheetCellRange, sourceIndexes: readonly number[], context?: WorkSpreadsheetDynamicFilterContext): WorkSpreadsheetSheet | null;
6
7
  export declare function spreadsheetFilterReconciliationIsBounded(sheet: WorkSpreadsheetSheet): boolean;
@@ -1,7 +1,8 @@
1
1
  import type { Cell } from '@fortune-sheet/core';
2
- import type { WorkSpreadsheetDynamicFilter } from './work-types';
2
+ import type { WorkSpreadsheetDateSystem, WorkSpreadsheetDynamicFilter } from './work-types';
3
3
  export interface WorkSpreadsheetDynamicFilterContext {
4
+ dateSystem?: WorkSpreadsheetDateSystem;
4
5
  now: Date;
5
6
  }
6
7
  export declare function workSpreadsheetDynamicFilterMatcher(kind: WorkSpreadsheetDynamicFilter, cells: Iterable<Cell | null>, context: WorkSpreadsheetDynamicFilterContext): ((cell: Cell | null) => boolean) | null;
7
- export declare function workSpreadsheetCellIsDate(cell: Cell | null): boolean;
8
+ export declare function workSpreadsheetCellIsDate(cell: Cell | null, dateSystem?: WorkSpreadsheetDateSystem): boolean;
@@ -182,9 +182,12 @@ export interface WorkDocumentComment {
182
182
  resolved: boolean;
183
183
  replies?: WorkDocumentCommentReply[];
184
184
  }
185
+ export type WorkSpreadsheetDateSystem = '1900' | '1904';
185
186
  export interface WorkSpreadsheetContent {
186
187
  type: 'spreadsheet';
187
188
  sheets: WorkSpreadsheetSheet[];
189
+ /** Omitted values use the Excel-compatible 1900 date system. */
190
+ dateSystem?: WorkSpreadsheetDateSystem;
188
191
  calculation?: WorkSpreadsheetCalculationSettings;
189
192
  namedRanges?: WorkSpreadsheetNamedRange[];
190
193
  printAreas?: WorkSpreadsheetPrintArea[];
Binary file
@@ -1574,8 +1574,10 @@ number as a date. Relative weeks begin on Sunday; calendar month and quarter
1574
1574
  families classify independently of year. Column profiling is cached per
1575
1575
  immutable sheet and keeps typed dates out of numeric rank actions. The editor
1576
1576
  uses the current local clock, while model tests inject a deterministic clock.
1577
- The controlled model still normalizes dates to the 1900 system; source-level
1578
- 1904 retention remains an explicit import boundary.
1577
+ The controlled model carries one workbook-owned `dateSystem`; omission means
1578
+ 1900 and imported 1904 workbooks retain the explicit alternate epoch. Numeric
1579
+ date serials remain numeric through import, filtering, controlled remounts,
1580
+ collaboration, export, and reopen, including valid 1904 serial zero.
1579
1581
 
1580
1582
  The same closed `WorkSpreadsheetFilterCriteria` union now serves worksheet and
1581
1583
  table native OOXML. Package scan and import read only the worksheet root's
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a3s-lab/office",
3
- "version": "0.37.4",
3
+ "version": "0.37.5",
4
4
  "description": "Open-source collaborative browser editors for documents, Markdown, spreadsheets, presentations, and PDFs.",
5
5
  "keywords": [
6
6
  "office",
@@ -223,6 +223,8 @@
223
223
  "test:e2e:spreadsheet-diagonal-borders:check": "a3s-test check tests/e2e/spreadsheet-diagonal-borders.acl --json",
224
224
  "test:e2e:spreadsheet-date-time": "a3s-test run tests/e2e/spreadsheet-date-time.acl --json",
225
225
  "test:e2e:spreadsheet-date-time:check": "a3s-test check tests/e2e/spreadsheet-date-time.acl --json",
226
+ "test:e2e:spreadsheet-1904-date-system": "A3S_TEST_SUITE=tests/e2e/spreadsheet-1904-date-system.acl bash scripts/run-a3s-test-web-gate.sh",
227
+ "test:e2e:spreadsheet-1904-date-system:check": "a3s-test check tests/e2e/spreadsheet-1904-date-system.acl --json",
226
228
  "test:e2e:spreadsheet-copy-from-above": "A3S_TEST_SUITE=tests/e2e/spreadsheet-copy-from-above.acl bash scripts/run-a3s-test-web-gate.sh",
227
229
  "test:e2e:spreadsheet-copy-from-above:check": "a3s-test check tests/e2e/spreadsheet-copy-from-above.acl --json",
228
230
  "test:e2e:spreadsheet-font-dialog-shortcuts": "A3S_TEST_SUITE=tests/e2e/spreadsheet-font-dialog-shortcuts.acl bash scripts/run-a3s-test-web-gate.sh",