Skip to content

Commit 4b6cbda

Browse files
phodalcodex
andcommitted
fix(office): support xlsx date formula helpers
Co-authored-by: Codex (GPT 5.5) <codex@openai.com>
1 parent 4b7af9c commit 4b6cbda

3 files changed

Lines changed: 90 additions & 0 deletions

File tree

docs/issues/2026-05-01-office-document-viewer-wasm-reader.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ Routa 应能在 session canvas 或 artifact tab 中直接预览 Office 文档(
128128
- XLSX conditional formula evaluation now handles common helper functions used in production rules, including `ISERROR`, `ISNA`, `IF`, `IFERROR`, and `ABS`.
129129
- XLSX conditional formula evaluation now supports sparse-range `SUMIF`, `SUMIFS`, and `AVERAGEIF` for aggregate threshold rules without dense range materialization.
130130
- XLSX conditional formula evaluation now handles common text helpers used in production rules, including `SEARCH`, `FIND`, `LEFT`, `RIGHT`, `MID`, `LOWER`, `UPPER`, and `TRIM`.
131+
- XLSX conditional formula evaluation now handles date-part and parity helpers used in production rules, including `YEAR`, `MONTH`, `DAY`, `WEEKDAY`, `ISODD`, and `ISEVEN`.
131132

132133
## Codex 技术方案逆向分析
133134

src/app/debug/office-wasm-poc/__tests__/spreadsheet-conditional-visuals.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,72 @@ describe("spreadsheet conditional visuals", () => {
860860
expect(visuals.get("4:3")?.background).toBe("#DDD6FE");
861861
});
862862

863+
it("evaluates date-part and parity helpers in conditional format formulas", () => {
864+
const visuals = buildSpreadsheetConditionalVisuals({
865+
conditionalFormattings: [
866+
{
867+
ranges: ["A2:A5"],
868+
rules: [
869+
{
870+
fillColor: "BAE6FD",
871+
formulas: ["=AND(YEAR(A2)=2026,MONTH(A2)=5,DAY(A2)=4)"],
872+
type: "expression",
873+
},
874+
],
875+
},
876+
{
877+
ranges: ["B2:B5"],
878+
rules: [
879+
{
880+
fillColor: "FDE68A",
881+
formulas: ["=WEEKDAY(B2,2)>5"],
882+
type: "expression",
883+
},
884+
],
885+
},
886+
{
887+
ranges: ["C2:C5"],
888+
rules: [
889+
{
890+
fillColor: "DDD6FE",
891+
formulas: ["=ISODD(ROW())"],
892+
type: "expression",
893+
},
894+
],
895+
},
896+
{
897+
ranges: ["D2:D5"],
898+
rules: [
899+
{
900+
fillColor: "BBF7D0",
901+
formulas: ["=ISEVEN(D2)"],
902+
type: "expression",
903+
},
904+
],
905+
},
906+
],
907+
rows: [
908+
{ cells: [{ address: "A2", value: 46146 }, { address: "B2", value: 46144 }, { address: "C2", value: "row2" }, { address: "D2", value: 2 }], index: 2 },
909+
{ cells: [{ address: "A3", value: 46147 }, { address: "B3", value: 46145 }, { address: "C3", value: "row3" }, { address: "D3", value: 3 }], index: 3 },
910+
{ cells: [{ address: "A4", value: 46146 }, { address: "B4", value: 46146 }, { address: "C4", value: "row4" }, { address: "D4", value: 4 }], index: 4 },
911+
{ cells: [{ address: "A5", value: 45782 }, { address: "B5", value: 46150 }, { address: "C5", value: "row5" }, { address: "D5", value: 5 }], index: 5 },
912+
],
913+
});
914+
915+
expect(visuals.get("2:0")?.background).toBe("#BAE6FD");
916+
expect(visuals.get("3:0")).toBeUndefined();
917+
expect(visuals.get("4:0")?.background).toBe("#BAE6FD");
918+
expect(visuals.get("5:0")).toBeUndefined();
919+
expect(visuals.get("2:1")?.background).toBe("#FDE68A");
920+
expect(visuals.get("3:1")?.background).toBe("#FDE68A");
921+
expect(visuals.get("4:1")).toBeUndefined();
922+
expect(visuals.get("3:2")?.background).toBe("#DDD6FE");
923+
expect(visuals.get("5:2")?.background).toBe("#DDD6FE");
924+
expect(visuals.get("2:3")?.background).toBe("#BBF7D0");
925+
expect(visuals.get("3:3")).toBeUndefined();
926+
expect(visuals.get("4:3")?.background).toBe("#BBF7D0");
927+
});
928+
863929
it("resolves table structured references in formula conditional formats", () => {
864930
const visuals = buildSpreadsheetConditionalVisuals({
865931
conditionalFormattings: [

src/app/debug/office-wasm-poc/spreadsheet-conditional-formula.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,14 @@ function evaluateFormulaFunction(name: string, args: string[], context: Conditio
120120
if (normalizedName === "ISNA") {
121121
return asString(evaluateFormulaValue(args[0] ?? "", context)).trim().toUpperCase() === "#N/A";
122122
}
123+
if (normalizedName === "ISODD") {
124+
const value = Number(evaluateFormulaValue(args[0] ?? "", context));
125+
return Number.isFinite(value) && Math.abs(Math.floor(value)) % 2 === 1;
126+
}
127+
if (normalizedName === "ISEVEN") {
128+
const value = Number(evaluateFormulaValue(args[0] ?? "", context));
129+
return Number.isFinite(value) && Math.abs(Math.floor(value)) % 2 === 0;
130+
}
123131
if (normalizedName === "IF") {
124132
return valueToBoolean(evaluateFormulaExpression(args[0] ?? "", context))
125133
? evaluateFormulaExpression(args[1] ?? "", context)
@@ -226,6 +234,9 @@ function evaluateFormulaFunction(name: string, args: string[], context: Conditio
226234
}
227235
return Number.NaN;
228236
}
237+
if (normalizedName === "YEAR" || normalizedName === "MONTH" || normalizedName === "DAY" || normalizedName === "WEEKDAY") {
238+
return excelSerialDatePart(Number(evaluateFormulaValue(args[0] ?? "", context)), normalizedName, Number(evaluateFormulaValue(args[1] ?? "1", context)));
239+
}
229240

230241
return "";
231242
}
@@ -618,6 +629,18 @@ function excelSerialDay(year: number, month: number, day: number): number {
618629
return Math.floor((Date.UTC(year, month - 1, day) - EXCEL_SERIAL_EPOCH_UTC) / DAY_MS);
619630
}
620631

632+
function excelSerialDatePart(value: number, part: string, weekdayReturnType: number): number {
633+
if (!Number.isFinite(value)) return Number.NaN;
634+
const date = new Date(EXCEL_SERIAL_EPOCH_UTC + Math.floor(value) * DAY_MS);
635+
if (part === "YEAR") return date.getUTCFullYear();
636+
if (part === "MONTH") return date.getUTCMonth() + 1;
637+
if (part === "DAY") return date.getUTCDate();
638+
const weekday = date.getUTCDay();
639+
if (weekdayReturnType === 2) return weekday === 0 ? 7 : weekday;
640+
if (weekdayReturnType === 3) return weekday === 0 ? 6 : weekday - 1;
641+
return weekday + 1;
642+
}
643+
621644
function parseFunctionCall(value: string): { args: string[]; name: string } | null {
622645
const nameMatch = value.match(/^([A-Z][A-Z0-9.]*)\(/i);
623646
if (!nameMatch || !value.endsWith(")")) return null;

0 commit comments

Comments
 (0)