Skip to content

Excel Compatibility ​

SheetKit aims for Excel parity: a workbook that SheetKit writes or edits should open in Microsoft Excel without repair, and a calculation or structural edit should produce what Excel would produce for the same input. SheetKit 1.0 tightened several behaviors for security and correctness. Some of those changes break compatibility with earlier SheetKit releases. They are acceptable only where they match Excel, so this page records each changed behavior next to what Excel did when given the same input.

How this was verified ​

Each behavior was run twice on the same input: once through the public SheetKit API (Rust crate, current 1.0 branch), and once in Microsoft Excel 16.114 for Mac, driven through AppleScript on 2026-10-23. Formulas were entered into a scratch workbook, and the value or error Excel calculated was read back. Structural edits were made with Excel's own delete and insert commands on a file SheetKit generated, and Excel then saved the result. The saved XML was compared with SheetKit's output for the same edit. Table names and totals rows were set through Excel's list object model.

Status values:

  • MATCH: SheetKit and Excel produced the same result.
  • DIFFERS: the results differ. The Notes column says whether the difference is deliberate, a security limit, or a defect.
  • NOT VERIFIABLE: Excel could not be driven to the same input through AppleScript. The reason is given.
  • N/A: the behavior has no Excel-visible effect, or applies only to the Node.js API.

The AppleScript object model is not the Excel user interface. Where the two can disagree (table naming, for example), the table says which one was tested.

Node.js methods follow the Rust names in camelCase (remove_row is removeRow, add_table is addTable). Formula results are the same in both bindings.

Aggregate functions ​

Test data: A1 empty, A2 = TRUE, A3 = FALSE, A4 = text "3", A5 = 5. B1:B3 empty.

BehaviorSheetKitExcelStatusNotes
AVERAGE, SUM, MIN, MAX, COUNT over A1:A5calculate_all / evaluate_formula: 5, 5, 5, 5, 15, 5, 5, 5, 1MATCHBooleans, text, and empty cells inside a range argument are ignored.
AVERAGE(B1:B3) (all empty) and AVERAGE(A2:A4) (only booleans and text)#DIV/0!#DIV/0!MATCH
MIN, MAX, SUM over B1:B300MATCHSheetKit returns SUM of an empty range as negative zero (-0.0), which compares equal to 0.
Direct scalar arguments: AVERAGE(TRUE), SUM("3"), SUM(TRUE)1, 3, 11, 3, 1MATCHBooleans and numeric text typed directly as arguments are counted.
AVERAGE("x")#VALUE! from calculate_all#VALUE!MATCHevaluate_formula returns an Err ("cannot convert") instead of an error value.
COUNT(TRUE,"3","x"), COUNT("3"), COUNT(TRUE)2, 1, 12, 1, 1MATCHBooleans and numeric text typed directly as COUNT arguments are counted; through references only numbers are counted.

Date functions ​

BehaviorSheetKitExcelStatusNotes
DATE(10000,1,1), DATE(-1,1,1)#NUM!#NUM!MATCH
DATE(0,1,1), DATE(9999,12,31)1, 29584651 (1900-01-01), 2958465MATCH
DATE(2024,2147483647,1)#NUM!#NUM!MATCH
DATE(2024,1,day) with day = -32768, -5, 32766, 3276712523, 45286, 78057, 7805812523, 45286, 78057, 78058MATCH
DATE(2024,1,day) with day = 32768, 40000, 65536, -4000078059, 85291, 110827, 529178058 for every oneDIFFERSExcel returned serial 78058 (2113-09-17, the result for day 32767) for every day value tested outside -32768..32767. SheetKit applies the day offset arithmetically and does not reproduce this clamping.
DATE(2024,1,day) with day = 2958101, 2147483647, 4294967296, 1E10, -2147483648#NUM!78058DIFFERSSame Excel clamping as above. SheetKit returns #NUM! when the result falls outside 1900-01-01..9999-12-31.
EDATE(45000,2147483647), EDATE(45000,-600)#NUM!, 26738#NUM!, 26738MATCH
EDATE(0,0), EDATE(-1,0)0, #NUM!0, #NUM!MATCHSerial 0 is 1900-01-00; negative serials return #NUM!.
EOMONTH(45000,2147483647), EOMONTH(2958465,0), EOMONTH(2958465,1)#NUM!, 2958465, #NUM!#NUM!, 2958465, #NUM!MATCH
EOMONTH(1,-1), EOMONTH(31,-1)#NUM!, #NUM!#NUM!, #NUM!MATCHA result before 1900-01-01 is an error.
EOMONTH(0,0), EOMONTH(-1,0), EOMONTH(60,0)31, #NUM!, 5931, #NUM!, 59MATCH
WORKDAY(45000,1E17), WORKDAY(45000,-1E17), WORKDAY(1,1)#NUM!, #NUM!, 2#NUM!, #NUM!, 2MATCHSheetKit bounds the day count before iterating.
WORKDAY(0,1), WORKDAY(-1,1)2, #NUM!2, #NUM!MATCH

Text functions ​

BehaviorSheetKitExcelStatusNotes
Negative counts: LEFT("abc",-1), RIGHT("abc",-1), MID("abc",1,-1), REPT("a",-1), REPLACE("abc",1,-1,"x")#VALUE!#VALUE!MATCH
Start below 1: MID("abc",0,1), REPLACE("abc",0,1,"x"), REPLACE("abc",-1,1,"x")#VALUE!#VALUE!MATCH
Huge counts: LEFT("abc",1E10), LEFT("abc",1E300), RIGHT("abc",2147483648)"abc""abc"MATCH
MID past the end: MID("abc",4,1), MID("abc",32768,1), MID("abc",2147483648,1)""""MATCH
MID("abc",1,2147483647)"abc""abc"MATCH
MID("abc",1,2147483648), MID("abc",1,1E10), MID("abc",1E10,1)#VALUE!#VALUE!MATCHA start above 2147483648 or a length above 2147483647 is rejected.
REPT result length: LEN(REPT("a",32767)), REPT("a",32768), REPT("ab",1E10)32767, #VALUE!, #VALUE!32767, #VALUE!, #VALUE!MATCH32767 characters is Excel's cell text limit.
REPLACE("abc",4,1,"x")"abcx""abcx"MATCH
REPLACE with start or length above 32767: start 32768, 2147483647, 1E10; length 2147483647, 2147483648, 1E10#VALUE!#VALUE!MATCH
LEN of an error argument: LEN(1/0), LEN(REPT("a",32768))#DIV/0!, #VALUE!#DIV/0!, #VALUE!MATCHText functions propagate an error argument.

Rounding ​

BehaviorSheetKitExcelStatusNotes
ROUND(1,309), ROUNDDOWN(1,-309), ROUND(123.456,-2)1, 0, 1001, 0, 100MATCH
ROUNDUP(123.456,-400)#NUM!#NUM!MATCH
ROUND(1.7E308,-308)#NUM!Formula cannot be enteredNOT VERIFIABLEExcel rejects the literal 1.7E308 (its largest numeric literal is about 9.99999999999999E+307), and AppleScript left the cell empty. SheetKit accepts the literal and returns #NUM! because the rounded result overflows.
ROUND(9E307,-307), ROUND(9.9E307,-307)9.000000000000004E+307, 1.0000000000000004E+3089E+307, 1E+308DIFFERSFloating-point error at extreme magnitudes. The relative error is about 4E-16.

TEXT number formats ​

BehaviorSheetKitExcelStatusNotes
TEXT(1.25,"# ?/8")"1 2/8""1 2/8"MATCH
TEXT(1,"0E+0"), TEXT(1,"ʼn0")"1E+0", "ʼn1""1E+0", "ʼn1"MATCH
TEXT(1,"ʼnE+é")"1E+0"#VALUE!DIFFERSSecurity limit. SheetKit always produces finite, bounded output for any format code and does not reject every code Excel considers invalid.
TEXT(42949672964294967296,"/?"), TEXT(1E15,"/?")"42949672964294970000", "1000000000000000 "#VALUE!, #VALUE!DIFFERSSecurity limit, as above.
TEXT(1.5,"?/18446744073709551615"), TEXT(1.5,"?/1000000000")"3/2", "3/2"#VALUE!, #VALUE!DIFFERSSecurity limit. SheetKit falls back to a bounded fraction search for denominators it cannot use.
TEXT(1.5,"?/99999")"149999/99999""3/9 "DIFFERSRaw Excel output recorded as returned. SheetKit applies the fixed denominator.
TEXT(12345678901234567890,"0")"12345678901234567168""12345678901234500000"DIFFERSExcel shows 15 significant digits; SheetKit prints the full double value.

Cross-sheet evaluation ​

Test data: Sheet2!A5 = 1, Sheet2!B5 = =A5+ROW()+COLUMN()*100, Sheet2!C5 = =SUM(A5:B5), Sheet1!A5 = 5.

BehaviorSheetKitExcelStatusNotes
=Sheet2!B5 on Sheet1206206MATCHUnqualified A5, ROW(), and COLUMN() resolve against Sheet2, where the formula lives.
=Sheet2!C5, =SUM(Sheet2!A5:B5) on Sheet1207, 207207, 207MATCH

Deleting rows ​

Rows 5 and 6 of Sheet1 were deleted (remove_row twice in SheetKit; delete entire row of A5:A6 in Excel). Formulas on Sheet1 and on a second sheet referenced Sheet1.

BehaviorSheetKitExcelStatusNotes
Single reference to a deleted cell: =A5, =Sheet1!A5=#REF!, =Sheet1!#REF!=#REF!, =Sheet1!#REF!MATCH
Range partly deleted: SUM(A4:A7), SUM(Sheet1!A6:A9)SUM(A4:A5), SUM(Sheet1!A5:A7)sameMATCHThe range shrinks.
Range fully deleted: SUM(A5:A6), SUM(Sheet1!5:5)SUM(#REF!), SUM(Sheet1!#REF!)sameMATCH
References below the band: Sheet1!A10, SUM(Sheet1!4:7)Sheet1!A8, SUM(Sheet1!4:5)sameMATCH
Whole-column reference: SUM(A:A)unchangedunchangedMATCH
Merged cells C5:D6, E4:E7, F8:G9removed, E4:E5, F6:G7sameMATCH
Data validations H5, H8, H3:H7 (identical rules)removed, H6, H3:H5one rule on H3:H6MATCHThe same cells are covered. Excel merges identical rules into one sqref when it saves.
Conditional formats I5:I6, I4:I8removed, I4:I6sameMATCH
Hyperlinks on J5, J8removed, J6sameMATCH
Comments on L5, L8removed, L6sameMATCH
Row page breaks below rows 5 and 8removed, below row 6sameMATCH

Inserting rows and columns ​

After the deletion above, one column was inserted before column A and one row before row 1 (insert_cols and insert_rows in SheetKit; insert on an entire column and an entire row in Excel).

BehaviorSheetKitExcelStatusNotes
Whole-column references: SUM(A:A), SUM(Sheet1!B:B), SUM(Sheet1!A:B)SUM(B:B), SUM(Sheet1!C:C), SUM(Sheet1!B:C)sameMATCH
Whole-row references: SUM(2:2), SUM(Sheet1!1:1), SUM(Sheet1!4:5)SUM(3:3), SUM(Sheet1!2:2), SUM(Sheet1!5:6)sameMATCHWhole-column and whole-row references shift but are not evaluated. See Known Limitations.
Merges, conditional formats, hyperlinks, comments, page breaksF5:F6, G7:H8; J5:J7; K7; M7; below row 7sameMATCHData validations cover the same cells; Excel again merges identical rules.

Tables ​

Table names were set with add_table in SheetKit. In Excel they were set by assigning name on a list object through AppleScript, saving the workbook, and reading the saved name and displayName. The Excel user interface's table name box was not tested.

BehaviorSheetKitExcelStatusNotes
Name that differs only in case from an existing table (sales, SALES when Sales exists)add_table returns an error ("table 'sales' already exists")Accepted; saved name="sales", displayName="sales_1"DIFFERSDeliberate. Excel's object model renames the display name; SheetKit rejects the name instead of renaming silently.
Name that is a cell reference (A1, XFD1048576, R1C1, R, C, RC, R1)Error ("must not be a cell reference")A1 saved as displayName="A1_"; R1C1 as "_R1C1"DIFFERSDeliberate, as above. XFE1 (beyond the last column) is accepted by both.
Name with a space (Bad Name)Error ("contains invalid character")Saved as displayName="Bad_Name"DIFFERSDeliberate, as above.
Name starting with a digit (1abc)Error ("must start with a letter, underscore, or backslash")Saved as displayName="_1abc"DIFFERSDeliberate, as above.
256-character name; 255-character nameError; acceptedError -10006 ("Can't set name"); acceptedMATCH
_ok, \back, a.bAcceptedAcceptedMATCH
Totals row presentTableConfig column with totals_row_function or totals_row_label writes totalsRowCount="1"; the table ref includes the totals row and the autoFilter ref excludes ittotalsRowCount="1", ref="A1:B4", autoFilter ref="A1:B3"MATCH
SUBTOTAL function numbers for sum, average, count, countNums, min, max, stdDev, var109, 101, 103, 102, 105, 104, 107, 110109, 101, 103, 102, 105, 104, 107, 110MATCH
Accepted totals function namesnone, sum, average, count, countNums, min, max, stdDev, var; custom is rejectedThe same names plus customDIFFERSNot supported. A custom totals formula needs totalsRowFormula, which TableConfig cannot set. See Known Limitations.
Totals cell formula textSUBTOTAL(109,[Qty])Displays =SUBTOTAL(109,[Qty]) in the cell; saves SUBTOTAL(109,Sales[Qty])MATCHExcel opens SheetKit's form without repair and calculates it.

Merged cells ​

BehaviorSheetKitExcelStatusNotes
Merging B2:C3 when A1:B2 is already mergedWorkbook::merge_cells and StreamWriter::add_merge_cell return a merge overlap errorAccepted; the merge becomes A1:C3DIFFERSDeliberate. SheetKit rejects overlapping merges instead of silently enlarging the existing merge.
Merging the same range again (E1:F2 twice)StreamWriter::add_merge_cell: no-op. Workbook::merge_cells: overlap errorNo-opMATCH (stream), DIFFERS (Workbook)The Workbook difference is deliberate. Calling merge_cells again on an existing merge is reported rather than ignored.

Comments and shared strings ​

BehaviorSheetKitExcelStatusNotes
Notes list on a sheet with one note and one threaded commentget_comments returns only the noteExcel comments count is 1 (the note)MATCHExcel does not list the [Threaded comment] legacy twin as a note.
Threaded comment content as shown in Excelget_threaded_comments returns the threadNot readableNOT VERIFIABLEExcel's AppleScript dictionary has no access to threaded comments.
Plain Hello in A1 and bold rich text Hello in A2Two shared string entriesA1 characters not bold, A2 characters boldMATCHThe two cells keep separate formatting.

Not Excel-visible ​

BehaviorSheetKitExcelStatusNotes
Deleting the hyperlink on one of two cells that share a relationshipdelete_cell_hyperlink removes the relationship only when no remaining hyperlink uses itn/aN/APackage bookkeeping.
New relationship idsThe lowest unused rIdN is used, so gaps are reusedn/aN/APackage bookkeeping.
Node.js integer arguments (rows, columns, counts, style ids, outline levels, numeric style and option fields)Non-finite, fractional, unsafe (beyond Number.MAX_SAFE_INTEGER), and out-of-range values throw "<name> must be an integer from <min> to <max>"n/aN/AAPI contract. Earlier releases let napi truncate, wrap, or turn NaN into 0.

Summary ​

Of the 74 rows above, 54 MATCH, 14 DIFFER, 1 is split (merging the same range again matches for StreamWriter and differs for Workbook), 2 are NOT VERIFIABLE, and 3 are N/A.

  • Security limits: the TEXT format code rows. SheetKit keeps formatting bounded instead of rejecting format codes Excel rejects.
  • Deliberate: table naming (reject instead of rename) and merges (reject overlaps instead of enlarging).
  • Not reproduced: Excel's clamping of out-of-range DATE day arguments, Excel's 15-digit display precision in TEXT, and floating-point error in ROUND near 1E308.

Released under the MIT / Apache-2.0 License.