@niicojs/excel 0.1.0

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sources":["../src/types.ts","../src/cell.ts","../src/range.ts","../src/worksheet.ts","../src/shared-strings.ts","../src/styles.ts","../src/pivot-cache.ts","../src/pivot-table.ts","../src/workbook.ts","../src/utils/address.ts"],"sourcesContent":["/**\n * Cell value types - what a cell can contain\n */\nexport type CellValue = number | string | boolean | Date | null | CellError;\n\n/**\n * Represents an Excel error value\n */\nexport interface CellError {\n error: ErrorType;\n}\n\nexport type ErrorType = '#NULL!' | '#DIV/0!' | '#VALUE!' | '#REF!' | '#NAME?' | '#NUM!' | '#N/A' | '#GETTING_DATA';\n\n/**\n * Discriminator for cell content type\n */\nexport type CellType = 'number' | 'string' | 'boolean' | 'date' | 'error' | 'empty';\n\n/**\n * Style definition for cells\n */\nexport interface CellStyle {\n bold?: boolean;\n italic?: boolean;\n underline?: boolean | 'single' | 'double';\n strike?: boolean;\n fontSize?: number;\n fontName?: string;\n fontColor?: string;\n fill?: string;\n border?: BorderStyle;\n alignment?: Alignment;\n numberFormat?: string;\n}\n\nexport interface BorderStyle {\n top?: BorderType;\n bottom?: BorderType;\n left?: BorderType;\n right?: BorderType;\n}\n\nexport type BorderType = 'thin' | 'medium' | 'thick' | 'double' | 'dotted' | 'dashed';\n\nexport interface Alignment {\n horizontal?: 'left' | 'center' | 'right' | 'justify';\n vertical?: 'top' | 'middle' | 'bottom';\n wrapText?: boolean;\n textRotation?: number;\n}\n\n/**\n * Cell address with 0-indexed row and column\n */\nexport interface CellAddress {\n row: number;\n col: number;\n}\n\n/**\n * Range address with start and end cells\n */\nexport interface RangeAddress {\n start: CellAddress;\n end: CellAddress;\n}\n\n/**\n * Internal cell data representation\n */\nexport interface CellData {\n /** Cell type: n=number, s=string (shared), str=inline string, b=boolean, e=error, d=date */\n t?: 'n' | 's' | 'str' | 'b' | 'e' | 'd';\n /** Raw value */\n v?: number | string | boolean;\n /** Formula (without leading =) */\n f?: string;\n /** Style index */\n s?: number;\n /** Formatted text (cached) */\n w?: string;\n /** Number format */\n z?: string;\n /** Array formula range */\n F?: string;\n /** Dynamic array formula flag */\n D?: boolean;\n /** Shared formula index */\n si?: number;\n}\n\n/**\n * Sheet definition from workbook.xml\n */\nexport interface SheetDefinition {\n name: string;\n sheetId: number;\n rId: string;\n}\n\n/**\n * Relationship definition\n */\nexport interface Relationship {\n id: string;\n type: string;\n target: string;\n}\n\n/**\n * Pivot table aggregation functions\n */\nexport type AggregationType = 'sum' | 'count' | 'average' | 'min' | 'max';\n\n/**\n * Configuration for a value field in a pivot table\n */\nexport interface PivotValueConfig {\n /** Source field name (column header) */\n field: string;\n /** Aggregation function */\n aggregation: AggregationType;\n /** Display name (e.g., \"Sum of Sales\") */\n name?: string;\n}\n\n/**\n * Configuration for creating a pivot table\n */\nexport interface PivotTableConfig {\n /** Name of the pivot table */\n name: string;\n /** Source data range with sheet name (e.g., \"Sheet1!A1:D100\") */\n source: string;\n /** Target cell where pivot table will be placed (e.g., \"Sheet2!A3\") */\n target: string;\n /** Refresh the pivot table data when the file is opened (default: true) */\n refreshOnLoad?: boolean;\n}\n\n/**\n * Internal representation of a pivot cache field\n */\nexport interface PivotCacheField {\n /** Field name (from header row) */\n name: string;\n /** Field index (0-based) */\n index: number;\n /** Whether this field contains numbers */\n isNumeric: boolean;\n /** Whether this field contains dates */\n isDate: boolean;\n /** Unique string values (for shared items) */\n sharedItems: string[];\n /** Min numeric value */\n minValue?: number;\n /** Max numeric value */\n maxValue?: number;\n}\n\n/**\n * Pivot field axis assignment\n */\nexport type PivotFieldAxis = 'row' | 'column' | 'filter' | 'value';\n","import type { CellValue, CellType, CellStyle, CellData, ErrorType } from './types';\nimport type { Worksheet } from './worksheet';\nimport { parseAddress, toAddress } from './utils/address';\n\n// Excel epoch: December 30, 1899 (accounting for the 1900 leap year bug)\nconst EXCEL_EPOCH = new Date(Date.UTC(1899, 11, 30));\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\n\n// Excel error types\nconst ERROR_TYPES: Set<string> = new Set([\n '#NULL!',\n '#DIV/0!',\n '#VALUE!',\n '#REF!',\n '#NAME?',\n '#NUM!',\n '#N/A',\n '#GETTING_DATA',\n]);\n\n/**\n * Represents a single cell in a worksheet\n */\nexport class Cell {\n private _row: number;\n private _col: number;\n private _data: CellData;\n private _worksheet: Worksheet;\n private _dirty = false;\n\n constructor(worksheet: Worksheet, row: number, col: number, data?: CellData) {\n this._worksheet = worksheet;\n this._row = row;\n this._col = col;\n this._data = data || {};\n }\n\n /**\n * Get the cell address (e.g., 'A1')\n */\n get address(): string {\n return toAddress(this._row, this._col);\n }\n\n /**\n * Get the 0-based row index\n */\n get row(): number {\n return this._row;\n }\n\n /**\n * Get the 0-based column index\n */\n get col(): number {\n return this._col;\n }\n\n /**\n * Get the cell type\n */\n get type(): CellType {\n const t = this._data.t;\n if (!t && this._data.v === undefined && !this._data.f) {\n return 'empty';\n }\n switch (t) {\n case 'n':\n return 'number';\n case 's':\n case 'str':\n return 'string';\n case 'b':\n return 'boolean';\n case 'e':\n return 'error';\n case 'd':\n return 'date';\n default:\n // If no type but has value, infer from value\n if (typeof this._data.v === 'number') return 'number';\n if (typeof this._data.v === 'string') return 'string';\n if (typeof this._data.v === 'boolean') return 'boolean';\n return 'empty';\n }\n }\n\n /**\n * Get the cell value\n */\n get value(): CellValue {\n const t = this._data.t;\n const v = this._data.v;\n\n if (v === undefined && !this._data.f) {\n return null;\n }\n\n switch (t) {\n case 'n':\n return typeof v === 'number' ? v : parseFloat(String(v));\n case 's':\n // Shared string reference\n if (typeof v === 'number') {\n return this._worksheet.workbook.sharedStrings.getString(v) ?? '';\n }\n return String(v);\n case 'str':\n // Inline string\n return String(v);\n case 'b':\n return v === 1 || v === '1' || v === true;\n case 'e':\n return { error: String(v) as ErrorType };\n case 'd':\n // ISO 8601 date string\n return new Date(String(v));\n default:\n // No type specified - try to infer\n if (typeof v === 'number') {\n // Check if this might be a date based on number format\n if (this._isDateFormat()) {\n return this._excelDateToJs(v);\n }\n return v;\n }\n if (typeof v === 'string') {\n if (ERROR_TYPES.has(v)) {\n return { error: v as ErrorType };\n }\n return v;\n }\n if (typeof v === 'boolean') return v;\n return null;\n }\n }\n\n /**\n * Set the cell value\n */\n set value(val: CellValue) {\n this._dirty = true;\n\n if (val === null || val === undefined) {\n this._data.v = undefined;\n this._data.t = undefined;\n this._data.f = undefined;\n return;\n }\n\n if (typeof val === 'number') {\n this._data.v = val;\n this._data.t = 'n';\n } else if (typeof val === 'string') {\n // Store as shared string\n const index = this._worksheet.workbook.sharedStrings.addString(val);\n this._data.v = index;\n this._data.t = 's';\n } else if (typeof val === 'boolean') {\n this._data.v = val ? 1 : 0;\n this._data.t = 'b';\n } else if (val instanceof Date) {\n // Store as ISO date string with 'd' type\n this._data.v = val.toISOString();\n this._data.t = 'd';\n } else if ('error' in val) {\n this._data.v = val.error;\n this._data.t = 'e';\n }\n\n // Clear formula when setting value directly\n this._data.f = undefined;\n }\n\n /**\n * Write a 2D array of values starting at this cell\n */\n set values(data: CellValue[][]) {\n for (let r = 0; r < data.length; r++) {\n const row = data[r];\n for (let c = 0; c < row.length; c++) {\n const cell = this._worksheet.cell(this._row + r, this._col + c);\n cell.value = row[c];\n }\n }\n }\n\n /**\n * Get the formula (without leading '=')\n */\n get formula(): string | undefined {\n return this._data.f;\n }\n\n /**\n * Set the formula (without leading '=')\n */\n set formula(f: string | undefined) {\n this._dirty = true;\n if (f === undefined) {\n this._data.f = undefined;\n } else {\n // Remove leading '=' if present\n this._data.f = f.startsWith('=') ? f.slice(1) : f;\n }\n }\n\n /**\n * Get the formatted text (as displayed in Excel)\n */\n get text(): string {\n if (this._data.w) {\n return this._data.w;\n }\n const val = this.value;\n if (val === null) return '';\n if (typeof val === 'object' && 'error' in val) return val.error;\n if (val instanceof Date) return val.toISOString().split('T')[0];\n return String(val);\n }\n\n /**\n * Get the style index\n */\n get styleIndex(): number | undefined {\n return this._data.s;\n }\n\n /**\n * Set the style index\n */\n set styleIndex(index: number | undefined) {\n this._dirty = true;\n this._data.s = index;\n }\n\n /**\n * Get the cell style\n */\n get style(): CellStyle {\n if (this._data.s === undefined) {\n return {};\n }\n return this._worksheet.workbook.styles.getStyle(this._data.s);\n }\n\n /**\n * Set the cell style (merges with existing)\n */\n set style(style: CellStyle) {\n this._dirty = true;\n const currentStyle = this.style;\n const merged = { ...currentStyle, ...style };\n this._data.s = this._worksheet.workbook.styles.createStyle(merged);\n }\n\n /**\n * Check if cell has been modified\n */\n get dirty(): boolean {\n return this._dirty;\n }\n\n /**\n * Get internal cell data\n */\n get data(): CellData {\n return this._data;\n }\n\n /**\n * Check if this cell has a date number format\n */\n private _isDateFormat(): boolean {\n // TODO: Check actual number format from styles\n // For now, return false - dates should be explicitly typed\n return false;\n }\n\n /**\n * Convert Excel serial date to JavaScript Date\n * Used when reading dates stored as numbers with date formats\n */\n _excelDateToJs(serial: number): Date {\n // Excel incorrectly considers 1900 a leap year\n // Dates after Feb 28, 1900 need adjustment\n const adjusted = serial > 60 ? serial - 1 : serial;\n const ms = Math.round((adjusted - 1) * MS_PER_DAY);\n return new Date(EXCEL_EPOCH.getTime() + ms);\n }\n\n /**\n * Convert JavaScript Date to Excel serial date\n * Used when writing dates as numbers for Excel compatibility\n */\n _jsDateToExcel(date: Date): number {\n const ms = date.getTime() - EXCEL_EPOCH.getTime();\n let serial = ms / MS_PER_DAY + 1;\n // Account for Excel's 1900 leap year bug\n if (serial > 60) {\n serial += 1;\n }\n return serial;\n }\n}\n\n/**\n * Parse a cell address or row/col to get row and col indices\n */\nexport const parseCellRef = (rowOrAddress: number | string, col?: number): { row: number; col: number } => {\n if (typeof rowOrAddress === 'string') {\n return parseAddress(rowOrAddress);\n }\n if (col === undefined) {\n throw new Error('Column must be provided when row is a number');\n }\n return { row: rowOrAddress, col };\n};\n","import type { CellValue, CellStyle, RangeAddress } from './types';\nimport type { Worksheet } from './worksheet';\nimport { toAddress, normalizeRange } from './utils/address';\n\n/**\n * Represents a range of cells in a worksheet\n */\nexport class Range {\n private _worksheet: Worksheet;\n private _range: RangeAddress;\n\n constructor(worksheet: Worksheet, range: RangeAddress) {\n this._worksheet = worksheet;\n this._range = normalizeRange(range);\n }\n\n /**\n * Get the range address as a string\n */\n get address(): string {\n const start = toAddress(this._range.start.row, this._range.start.col);\n const end = toAddress(this._range.end.row, this._range.end.col);\n if (start === end) return start;\n return `${start}:${end}`;\n }\n\n /**\n * Get the number of rows in the range\n */\n get rowCount(): number {\n return this._range.end.row - this._range.start.row + 1;\n }\n\n /**\n * Get the number of columns in the range\n */\n get colCount(): number {\n return this._range.end.col - this._range.start.col + 1;\n }\n\n /**\n * Get all values in the range as a 2D array\n */\n get values(): CellValue[][] {\n const result: CellValue[][] = [];\n for (let r = this._range.start.row; r <= this._range.end.row; r++) {\n const row: CellValue[] = [];\n for (let c = this._range.start.col; c <= this._range.end.col; c++) {\n const cell = this._worksheet.cell(r, c);\n row.push(cell.value);\n }\n result.push(row);\n }\n return result;\n }\n\n /**\n * Set values in the range from a 2D array\n */\n set values(data: CellValue[][]) {\n for (let r = 0; r < data.length && r < this.rowCount; r++) {\n const row = data[r];\n for (let c = 0; c < row.length && c < this.colCount; c++) {\n const cell = this._worksheet.cell(this._range.start.row + r, this._range.start.col + c);\n cell.value = row[c];\n }\n }\n }\n\n /**\n * Get all formulas in the range as a 2D array\n */\n get formulas(): (string | undefined)[][] {\n const result: (string | undefined)[][] = [];\n for (let r = this._range.start.row; r <= this._range.end.row; r++) {\n const row: (string | undefined)[] = [];\n for (let c = this._range.start.col; c <= this._range.end.col; c++) {\n const cell = this._worksheet.cell(r, c);\n row.push(cell.formula);\n }\n result.push(row);\n }\n return result;\n }\n\n /**\n * Set formulas in the range from a 2D array\n */\n set formulas(data: (string | undefined)[][]) {\n for (let r = 0; r < data.length && r < this.rowCount; r++) {\n const row = data[r];\n for (let c = 0; c < row.length && c < this.colCount; c++) {\n const cell = this._worksheet.cell(this._range.start.row + r, this._range.start.col + c);\n cell.formula = row[c];\n }\n }\n }\n\n /**\n * Get the style of the top-left cell\n */\n get style(): CellStyle {\n return this._worksheet.cell(this._range.start.row, this._range.start.col).style;\n }\n\n /**\n * Set style for all cells in the range\n */\n set style(style: CellStyle) {\n for (let r = this._range.start.row; r <= this._range.end.row; r++) {\n for (let c = this._range.start.col; c <= this._range.end.col; c++) {\n const cell = this._worksheet.cell(r, c);\n cell.style = style;\n }\n }\n }\n\n /**\n * Iterate over all cells in the range\n */\n *[Symbol.iterator]() {\n for (let r = this._range.start.row; r <= this._range.end.row; r++) {\n for (let c = this._range.start.col; c <= this._range.end.col; c++) {\n yield this._worksheet.cell(r, c);\n }\n }\n }\n\n /**\n * Iterate over cells row by row\n */\n *rows() {\n for (let r = this._range.start.row; r <= this._range.end.row; r++) {\n const row = [];\n for (let c = this._range.start.col; c <= this._range.end.col; c++) {\n row.push(this._worksheet.cell(r, c));\n }\n yield row;\n }\n }\n}\n","import type { CellData, RangeAddress } from './types';\nimport type { Workbook } from './workbook';\nimport { Cell, parseCellRef } from './cell';\nimport { Range } from './range';\nimport { parseRange, toAddress, parseAddress } from './utils/address';\nimport {\n parseXml,\n findElement,\n getChildren,\n getAttr,\n XmlNode,\n stringifyXml,\n createElement,\n createText,\n} from './utils/xml';\n\n/**\n * Represents a worksheet in a workbook\n */\nexport class Worksheet {\n private _name: string;\n private _workbook: Workbook;\n private _cells: Map<string, Cell> = new Map();\n private _xmlNodes: XmlNode[] | null = null;\n private _dirty = false;\n private _mergedCells: Set<string> = new Set();\n private _sheetData: XmlNode[] = [];\n\n constructor(workbook: Workbook, name: string) {\n this._workbook = workbook;\n this._name = name;\n }\n\n /**\n * Get the workbook this sheet belongs to\n */\n get workbook(): Workbook {\n return this._workbook;\n }\n\n /**\n * Get the sheet name\n */\n get name(): string {\n return this._name;\n }\n\n /**\n * Set the sheet name\n */\n set name(value: string) {\n this._name = value;\n this._dirty = true;\n }\n\n /**\n * Parse worksheet XML content\n */\n parse(xml: string): void {\n this._xmlNodes = parseXml(xml);\n const worksheet = findElement(this._xmlNodes, 'worksheet');\n if (!worksheet) return;\n\n const worksheetChildren = getChildren(worksheet, 'worksheet');\n\n // Parse sheet data (cells)\n const sheetData = findElement(worksheetChildren, 'sheetData');\n if (sheetData) {\n this._sheetData = getChildren(sheetData, 'sheetData');\n this._parseSheetData(this._sheetData);\n }\n\n // Parse merged cells\n const mergeCells = findElement(worksheetChildren, 'mergeCells');\n if (mergeCells) {\n const mergeChildren = getChildren(mergeCells, 'mergeCells');\n for (const mergeCell of mergeChildren) {\n if ('mergeCell' in mergeCell) {\n const ref = getAttr(mergeCell, 'ref');\n if (ref) {\n this._mergedCells.add(ref);\n }\n }\n }\n }\n }\n\n /**\n * Parse the sheetData element to extract cells\n */\n private _parseSheetData(rows: XmlNode[]): void {\n for (const rowNode of rows) {\n if (!('row' in rowNode)) continue;\n\n const rowChildren = getChildren(rowNode, 'row');\n for (const cellNode of rowChildren) {\n if (!('c' in cellNode)) continue;\n\n const ref = getAttr(cellNode, 'r');\n if (!ref) continue;\n\n const { row, col } = parseAddress(ref);\n const cellData = this._parseCellNode(cellNode);\n const cell = new Cell(this, row, col, cellData);\n this._cells.set(ref, cell);\n }\n }\n }\n\n /**\n * Parse a cell XML node to CellData\n */\n private _parseCellNode(node: XmlNode): CellData {\n const data: CellData = {};\n\n // Type attribute\n const t = getAttr(node, 't');\n if (t) {\n data.t = t as CellData['t'];\n }\n\n // Style attribute\n const s = getAttr(node, 's');\n if (s) {\n data.s = parseInt(s, 10);\n }\n\n const children = getChildren(node, 'c');\n\n // Value element\n const vNode = findElement(children, 'v');\n if (vNode) {\n const vChildren = getChildren(vNode, 'v');\n for (const child of vChildren) {\n if ('#text' in child) {\n const text = child['#text'] as string;\n // Parse based on type\n if (data.t === 's') {\n data.v = parseInt(text, 10); // Shared string index\n } else if (data.t === 'b') {\n data.v = text === '1' ? 1 : 0;\n } else if (data.t === 'e' || data.t === 'str') {\n data.v = text;\n } else {\n // Number or default\n data.v = parseFloat(text);\n }\n break;\n }\n }\n }\n\n // Formula element\n const fNode = findElement(children, 'f');\n if (fNode) {\n const fChildren = getChildren(fNode, 'f');\n for (const child of fChildren) {\n if ('#text' in child) {\n data.f = child['#text'] as string;\n break;\n }\n }\n\n // Check for shared formula\n const si = getAttr(fNode, 'si');\n if (si) {\n data.si = parseInt(si, 10);\n }\n\n // Check for array formula range\n const ref = getAttr(fNode, 'ref');\n if (ref) {\n data.F = ref;\n }\n }\n\n // Inline string (is element)\n const isNode = findElement(children, 'is');\n if (isNode) {\n data.t = 'str';\n const isChildren = getChildren(isNode, 'is');\n const tNode = findElement(isChildren, 't');\n if (tNode) {\n const tChildren = getChildren(tNode, 't');\n for (const child of tChildren) {\n if ('#text' in child) {\n data.v = child['#text'] as string;\n break;\n }\n }\n }\n }\n\n return data;\n }\n\n /**\n * Get a cell by address or row/col\n */\n cell(rowOrAddress: number | string, col?: number): Cell {\n const { row, col: c } = parseCellRef(rowOrAddress, col);\n const address = toAddress(row, c);\n\n let cell = this._cells.get(address);\n if (!cell) {\n cell = new Cell(this, row, c);\n this._cells.set(address, cell);\n }\n\n return cell;\n }\n\n /**\n * Get a range of cells\n */\n range(rangeStr: string): Range;\n range(startRow: number, startCol: number, endRow: number, endCol: number): Range;\n range(startRowOrRange: number | string, startCol?: number, endRow?: number, endCol?: number): Range {\n let rangeAddr: RangeAddress;\n\n if (typeof startRowOrRange === 'string') {\n rangeAddr = parseRange(startRowOrRange);\n } else {\n if (startCol === undefined || endRow === undefined || endCol === undefined) {\n throw new Error('All range parameters must be provided');\n }\n rangeAddr = {\n start: { row: startRowOrRange, col: startCol },\n end: { row: endRow, col: endCol },\n };\n }\n\n return new Range(this, rangeAddr);\n }\n\n /**\n * Merge cells in the given range\n */\n mergeCells(rangeOrStart: string, end?: string): void {\n let rangeStr: string;\n if (end) {\n rangeStr = `${rangeOrStart}:${end}`;\n } else {\n rangeStr = rangeOrStart;\n }\n this._mergedCells.add(rangeStr);\n this._dirty = true;\n }\n\n /**\n * Unmerge cells in the given range\n */\n unmergeCells(rangeStr: string): void {\n this._mergedCells.delete(rangeStr);\n this._dirty = true;\n }\n\n /**\n * Get all merged cell ranges\n */\n get mergedCells(): string[] {\n return Array.from(this._mergedCells);\n }\n\n /**\n * Check if the worksheet has been modified\n */\n get dirty(): boolean {\n if (this._dirty) return true;\n for (const cell of this._cells.values()) {\n if (cell.dirty) return true;\n }\n return false;\n }\n\n /**\n * Get all cells in the worksheet\n */\n get cells(): Map<string, Cell> {\n return this._cells;\n }\n\n /**\n * Generate XML for this worksheet\n */\n toXml(): string {\n // Build sheetData from cells\n const rowMap = new Map<number, Cell[]>();\n for (const cell of this._cells.values()) {\n const row = cell.row;\n if (!rowMap.has(row)) {\n rowMap.set(row, []);\n }\n rowMap.get(row)!.push(cell);\n }\n\n // Sort rows and cells\n const sortedRows = Array.from(rowMap.entries()).sort((a, b) => a[0] - b[0]);\n\n const rowNodes: XmlNode[] = [];\n for (const [rowIdx, cells] of sortedRows) {\n cells.sort((a, b) => a.col - b.col);\n\n const cellNodes: XmlNode[] = [];\n for (const cell of cells) {\n const cellNode = this._buildCellNode(cell);\n cellNodes.push(cellNode);\n }\n\n const rowNode = createElement('row', { r: String(rowIdx + 1) }, cellNodes);\n rowNodes.push(rowNode);\n }\n\n const sheetDataNode = createElement('sheetData', {}, rowNodes);\n\n // Build worksheet structure\n const worksheetChildren: XmlNode[] = [sheetDataNode];\n\n // Add merged cells if any\n if (this._mergedCells.size > 0) {\n const mergeCellNodes: XmlNode[] = [];\n for (const ref of this._mergedCells) {\n mergeCellNodes.push(createElement('mergeCell', { ref }, []));\n }\n const mergeCellsNode = createElement('mergeCells', { count: String(this._mergedCells.size) }, mergeCellNodes);\n worksheetChildren.push(mergeCellsNode);\n }\n\n const worksheetNode = createElement(\n 'worksheet',\n {\n xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',\n 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',\n },\n worksheetChildren,\n );\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([worksheetNode])}`;\n }\n\n /**\n * Build a cell XML node from a Cell object\n */\n private _buildCellNode(cell: Cell): XmlNode {\n const data = cell.data;\n const attrs: Record<string, string> = { r: cell.address };\n\n if (data.t && data.t !== 'n') {\n attrs.t = data.t;\n }\n if (data.s !== undefined) {\n attrs.s = String(data.s);\n }\n\n const children: XmlNode[] = [];\n\n // Formula\n if (data.f) {\n const fAttrs: Record<string, string> = {};\n if (data.F) fAttrs.ref = data.F;\n if (data.si !== undefined) fAttrs.si = String(data.si);\n children.push(createElement('f', fAttrs, [createText(data.f)]));\n }\n\n // Value\n if (data.v !== undefined) {\n children.push(createElement('v', {}, [createText(String(data.v))]));\n }\n\n return createElement('c', attrs, children);\n }\n}\n","import { parseXml, findElement, getChildren, XmlNode, stringifyXml, createElement, createText } from './utils/xml';\n\n/**\n * Manages the shared strings table from xl/sharedStrings.xml\n * Excel stores strings in a shared table to reduce file size\n */\nexport class SharedStrings {\n private strings: string[] = [];\n private stringToIndex: Map<string, number> = new Map();\n private _dirty = false;\n\n /**\n * Parse shared strings from XML content\n */\n static parse(xml: string): SharedStrings {\n const ss = new SharedStrings();\n const parsed = parseXml(xml);\n const sst = findElement(parsed, 'sst');\n if (!sst) return ss;\n\n const children = getChildren(sst, 'sst');\n for (const child of children) {\n if ('si' in child) {\n const siChildren = getChildren(child, 'si');\n const text = ss.extractText(siChildren);\n ss.strings.push(text);\n ss.stringToIndex.set(text, ss.strings.length - 1);\n }\n }\n\n return ss;\n }\n\n /**\n * Extract text from a string item (si element)\n * Handles both simple <t> elements and rich text <r> elements\n */\n private extractText(nodes: XmlNode[]): string {\n let text = '';\n for (const node of nodes) {\n if ('t' in node) {\n // Simple text: <t>value</t>\n const tChildren = getChildren(node, 't');\n for (const child of tChildren) {\n if ('#text' in child) {\n text += child['#text'] as string;\n }\n }\n } else if ('r' in node) {\n // Rich text: <r><t>value</t></r>\n const rChildren = getChildren(node, 'r');\n for (const rChild of rChildren) {\n if ('t' in rChild) {\n const tChildren = getChildren(rChild, 't');\n for (const child of tChildren) {\n if ('#text' in child) {\n text += child['#text'] as string;\n }\n }\n }\n }\n }\n }\n return text;\n }\n\n /**\n * Get a string by index\n */\n getString(index: number): string | undefined {\n return this.strings[index];\n }\n\n /**\n * Add a string and return its index\n * If the string already exists, returns the existing index\n */\n addString(str: string): number {\n const existing = this.stringToIndex.get(str);\n if (existing !== undefined) {\n return existing;\n }\n const index = this.strings.length;\n this.strings.push(str);\n this.stringToIndex.set(str, index);\n this._dirty = true;\n return index;\n }\n\n /**\n * Check if the shared strings table has been modified\n */\n get dirty(): boolean {\n return this._dirty;\n }\n\n /**\n * Get the count of strings\n */\n get count(): number {\n return this.strings.length;\n }\n\n /**\n * Generate XML for the shared strings table\n */\n toXml(): string {\n const siElements: XmlNode[] = [];\n for (const str of this.strings) {\n const tElement = createElement('t', str.startsWith(' ') || str.endsWith(' ') ? { 'xml:space': 'preserve' } : {}, [\n createText(str),\n ]);\n const siElement = createElement('si', {}, [tElement]);\n siElements.push(siElement);\n }\n\n const sst = createElement(\n 'sst',\n {\n xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',\n count: String(this.strings.length),\n uniqueCount: String(this.strings.length),\n },\n siElements,\n );\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([sst])}`;\n }\n}\n","import type { CellStyle, BorderType } from './types';\nimport { parseXml, findElement, getChildren, getAttr, XmlNode, stringifyXml, createElement } from './utils/xml';\n\n/**\n * Normalize a color to ARGB format (8 hex chars).\n * Accepts: \"#RGB\", \"#RRGGBB\", \"RGB\", \"RRGGBB\", \"AARRGGBB\", \"#AARRGGBB\"\n */\nconst normalizeColor = (color: string): string => {\n let c = color.replace(/^#/, '').toUpperCase();\n\n // Handle shorthand 3-char format (e.g., \"FFF\" -> \"FFFFFF\")\n if (c.length === 3) {\n c = c[0] + c[0] + c[1] + c[1] + c[2] + c[2];\n }\n\n // Add alpha channel if not present (6 chars -> 8 chars)\n if (c.length === 6) {\n c = 'FF' + c;\n }\n\n return c;\n};\n\n/**\n * Manages the styles (xl/styles.xml)\n */\nexport class Styles {\n private _numFmts: Map<number, string> = new Map();\n private _fonts: StyleFont[] = [];\n private _fills: StyleFill[] = [];\n private _borders: StyleBorder[] = [];\n private _cellXfs: CellXf[] = []; // Cell formats (combined style index)\n private _xmlNodes: XmlNode[] | null = null;\n private _dirty = false;\n\n // Cache for style deduplication\n private _styleCache: Map<string, number> = new Map();\n\n /**\n * Parse styles from XML content\n */\n static parse(xml: string): Styles {\n const styles = new Styles();\n styles._xmlNodes = parseXml(xml);\n\n const styleSheet = findElement(styles._xmlNodes, 'styleSheet');\n if (!styleSheet) return styles;\n\n const children = getChildren(styleSheet, 'styleSheet');\n\n // Parse number formats\n const numFmts = findElement(children, 'numFmts');\n if (numFmts) {\n for (const child of getChildren(numFmts, 'numFmts')) {\n if ('numFmt' in child) {\n const id = parseInt(getAttr(child, 'numFmtId') || '0', 10);\n const code = getAttr(child, 'formatCode') || '';\n styles._numFmts.set(id, code);\n }\n }\n }\n\n // Parse fonts\n const fonts = findElement(children, 'fonts');\n if (fonts) {\n for (const child of getChildren(fonts, 'fonts')) {\n if ('font' in child) {\n styles._fonts.push(styles._parseFont(child));\n }\n }\n }\n\n // Parse fills\n const fills = findElement(children, 'fills');\n if (fills) {\n for (const child of getChildren(fills, 'fills')) {\n if ('fill' in child) {\n styles._fills.push(styles._parseFill(child));\n }\n }\n }\n\n // Parse borders\n const borders = findElement(children, 'borders');\n if (borders) {\n for (const child of getChildren(borders, 'borders')) {\n if ('border' in child) {\n styles._borders.push(styles._parseBorder(child));\n }\n }\n }\n\n // Parse cellXfs (cell formats)\n const cellXfs = findElement(children, 'cellXfs');\n if (cellXfs) {\n for (const child of getChildren(cellXfs, 'cellXfs')) {\n if ('xf' in child) {\n styles._cellXfs.push(styles._parseCellXf(child));\n }\n }\n }\n\n return styles;\n }\n\n /**\n * Create an empty styles object with defaults\n */\n static createDefault(): Styles {\n const styles = new Styles();\n\n // Default font (Calibri 11)\n styles._fonts.push({\n bold: false,\n italic: false,\n underline: false,\n strike: false,\n size: 11,\n name: 'Calibri',\n color: undefined,\n });\n\n // Default fills (none and gray125 pattern are required)\n styles._fills.push({ type: 'none' });\n styles._fills.push({ type: 'gray125' });\n\n // Default border (none)\n styles._borders.push({});\n\n // Default cell format\n styles._cellXfs.push({\n fontId: 0,\n fillId: 0,\n borderId: 0,\n numFmtId: 0,\n });\n\n return styles;\n }\n\n private _parseFont(node: XmlNode): StyleFont {\n const font: StyleFont = {\n bold: false,\n italic: false,\n underline: false,\n strike: false,\n };\n\n const children = getChildren(node, 'font');\n for (const child of children) {\n if ('b' in child) font.bold = true;\n if ('i' in child) font.italic = true;\n if ('u' in child) font.underline = true;\n if ('strike' in child) font.strike = true;\n if ('sz' in child) font.size = parseFloat(getAttr(child, 'val') || '11');\n if ('name' in child) font.name = getAttr(child, 'val');\n if ('color' in child) {\n font.color = getAttr(child, 'rgb') || getAttr(child, 'theme');\n }\n }\n\n return font;\n }\n\n private _parseFill(node: XmlNode): StyleFill {\n const fill: StyleFill = { type: 'none' };\n const children = getChildren(node, 'fill');\n\n for (const child of children) {\n if ('patternFill' in child) {\n const pattern = getAttr(child, 'patternType');\n fill.type = pattern || 'none';\n\n const pfChildren = getChildren(child, 'patternFill');\n for (const pfChild of pfChildren) {\n if ('fgColor' in pfChild) {\n fill.fgColor = getAttr(pfChild, 'rgb') || getAttr(pfChild, 'theme');\n }\n if ('bgColor' in pfChild) {\n fill.bgColor = getAttr(pfChild, 'rgb') || getAttr(pfChild, 'theme');\n }\n }\n }\n }\n\n return fill;\n }\n\n private _parseBorder(node: XmlNode): StyleBorder {\n const border: StyleBorder = {};\n const children = getChildren(node, 'border');\n\n for (const child of children) {\n const style = getAttr(child, 'style') as BorderType | undefined;\n if ('left' in child && style) border.left = style;\n if ('right' in child && style) border.right = style;\n if ('top' in child && style) border.top = style;\n if ('bottom' in child && style) border.bottom = style;\n }\n\n return border;\n }\n\n private _parseCellXf(node: XmlNode): CellXf {\n return {\n fontId: parseInt(getAttr(node, 'fontId') || '0', 10),\n fillId: parseInt(getAttr(node, 'fillId') || '0', 10),\n borderId: parseInt(getAttr(node, 'borderId') || '0', 10),\n numFmtId: parseInt(getAttr(node, 'numFmtId') || '0', 10),\n alignment: this._parseAlignment(node),\n };\n }\n\n private _parseAlignment(node: XmlNode): AlignmentStyle | undefined {\n const children = getChildren(node, 'xf');\n const alignNode = findElement(children, 'alignment');\n if (!alignNode) return undefined;\n\n return {\n horizontal: getAttr(alignNode, 'horizontal') as AlignmentStyle['horizontal'],\n vertical: getAttr(alignNode, 'vertical') as AlignmentStyle['vertical'],\n wrapText: getAttr(alignNode, 'wrapText') === '1',\n textRotation: parseInt(getAttr(alignNode, 'textRotation') || '0', 10),\n };\n }\n\n /**\n * Get a style by index\n */\n getStyle(index: number): CellStyle {\n const xf = this._cellXfs[index];\n if (!xf) return {};\n\n const font = this._fonts[xf.fontId];\n const fill = this._fills[xf.fillId];\n const border = this._borders[xf.borderId];\n const numFmt = this._numFmts.get(xf.numFmtId);\n\n const style: CellStyle = {};\n\n if (font) {\n if (font.bold) style.bold = true;\n if (font.italic) style.italic = true;\n if (font.underline) style.underline = true;\n if (font.strike) style.strike = true;\n if (font.size) style.fontSize = font.size;\n if (font.name) style.fontName = font.name;\n if (font.color) style.fontColor = font.color;\n }\n\n if (fill && fill.fgColor) {\n style.fill = fill.fgColor;\n }\n\n if (border) {\n if (border.top || border.bottom || border.left || border.right) {\n style.border = {\n top: border.top,\n bottom: border.bottom,\n left: border.left,\n right: border.right,\n };\n }\n }\n\n if (numFmt) {\n style.numberFormat = numFmt;\n }\n\n if (xf.alignment) {\n style.alignment = {\n horizontal: xf.alignment.horizontal,\n vertical: xf.alignment.vertical,\n wrapText: xf.alignment.wrapText,\n textRotation: xf.alignment.textRotation,\n };\n }\n\n return style;\n }\n\n /**\n * Create a style and return its index\n * Uses caching to deduplicate identical styles\n */\n createStyle(style: CellStyle): number {\n const key = JSON.stringify(style);\n const cached = this._styleCache.get(key);\n if (cached !== undefined) {\n return cached;\n }\n\n this._dirty = true;\n\n // Create or find font\n const fontId = this._findOrCreateFont(style);\n\n // Create or find fill\n const fillId = this._findOrCreateFill(style);\n\n // Create or find border\n const borderId = this._findOrCreateBorder(style);\n\n // Create or find number format\n const numFmtId = style.numberFormat ? this._findOrCreateNumFmt(style.numberFormat) : 0;\n\n // Create cell format\n const xf: CellXf = {\n fontId,\n fillId,\n borderId,\n numFmtId,\n };\n\n if (style.alignment) {\n xf.alignment = {\n horizontal: style.alignment.horizontal,\n vertical: style.alignment.vertical,\n wrapText: style.alignment.wrapText,\n textRotation: style.alignment.textRotation,\n };\n }\n\n const index = this._cellXfs.length;\n this._cellXfs.push(xf);\n this._styleCache.set(key, index);\n\n return index;\n }\n\n private _findOrCreateFont(style: CellStyle): number {\n const font: StyleFont = {\n bold: style.bold || false,\n italic: style.italic || false,\n underline: style.underline === true || style.underline === 'single' || style.underline === 'double',\n strike: style.strike || false,\n size: style.fontSize,\n name: style.fontName,\n color: style.fontColor,\n };\n\n // Try to find existing font\n for (let i = 0; i < this._fonts.length; i++) {\n const f = this._fonts[i];\n if (\n f.bold === font.bold &&\n f.italic === font.italic &&\n f.underline === font.underline &&\n f.strike === font.strike &&\n f.size === font.size &&\n f.name === font.name &&\n f.color === font.color\n ) {\n return i;\n }\n }\n\n // Create new font\n this._fonts.push(font);\n return this._fonts.length - 1;\n }\n\n private _findOrCreateFill(style: CellStyle): number {\n if (!style.fill) return 0;\n\n // Try to find existing fill\n for (let i = 0; i < this._fills.length; i++) {\n const f = this._fills[i];\n if (f.fgColor === style.fill) {\n return i;\n }\n }\n\n // Create new fill\n this._fills.push({\n type: 'solid',\n fgColor: style.fill,\n });\n return this._fills.length - 1;\n }\n\n private _findOrCreateBorder(style: CellStyle): number {\n if (!style.border) return 0;\n\n const border: StyleBorder = {\n top: style.border.top,\n bottom: style.border.bottom,\n left: style.border.left,\n right: style.border.right,\n };\n\n // Try to find existing border\n for (let i = 0; i < this._borders.length; i++) {\n const b = this._borders[i];\n if (b.top === border.top && b.bottom === border.bottom && b.left === border.left && b.right === border.right) {\n return i;\n }\n }\n\n // Create new border\n this._borders.push(border);\n return this._borders.length - 1;\n }\n\n private _findOrCreateNumFmt(format: string): number {\n // Check if already exists\n for (const [id, code] of this._numFmts) {\n if (code === format) return id;\n }\n\n // Create new (custom formats start at 164)\n const id = Math.max(164, ...Array.from(this._numFmts.keys())) + 1;\n this._numFmts.set(id, format);\n return id;\n }\n\n /**\n * Check if styles have been modified\n */\n get dirty(): boolean {\n return this._dirty;\n }\n\n /**\n * Generate XML for styles\n */\n toXml(): string {\n const children: XmlNode[] = [];\n\n // Number formats\n if (this._numFmts.size > 0) {\n const numFmtNodes: XmlNode[] = [];\n for (const [id, code] of this._numFmts) {\n numFmtNodes.push(createElement('numFmt', { numFmtId: String(id), formatCode: code }, []));\n }\n children.push(createElement('numFmts', { count: String(numFmtNodes.length) }, numFmtNodes));\n }\n\n // Fonts\n const fontNodes: XmlNode[] = this._fonts.map((font) => this._buildFontNode(font));\n children.push(createElement('fonts', { count: String(fontNodes.length) }, fontNodes));\n\n // Fills\n const fillNodes: XmlNode[] = this._fills.map((fill) => this._buildFillNode(fill));\n children.push(createElement('fills', { count: String(fillNodes.length) }, fillNodes));\n\n // Borders\n const borderNodes: XmlNode[] = this._borders.map((border) => this._buildBorderNode(border));\n children.push(createElement('borders', { count: String(borderNodes.length) }, borderNodes));\n\n // Cell style xfs (required but we just add a default)\n children.push(\n createElement('cellStyleXfs', { count: '1' }, [\n createElement('xf', { numFmtId: '0', fontId: '0', fillId: '0', borderId: '0' }, []),\n ]),\n );\n\n // Cell xfs\n const xfNodes: XmlNode[] = this._cellXfs.map((xf) => this._buildXfNode(xf));\n children.push(createElement('cellXfs', { count: String(xfNodes.length) }, xfNodes));\n\n // Cell styles (required)\n children.push(\n createElement('cellStyles', { count: '1' }, [\n createElement('cellStyle', { name: 'Normal', xfId: '0', builtinId: '0' }, []),\n ]),\n );\n\n const styleSheet = createElement(\n 'styleSheet',\n { xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main' },\n children,\n );\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([styleSheet])}`;\n }\n\n private _buildFontNode(font: StyleFont): XmlNode {\n const children: XmlNode[] = [];\n if (font.bold) children.push(createElement('b', {}, []));\n if (font.italic) children.push(createElement('i', {}, []));\n if (font.underline) children.push(createElement('u', {}, []));\n if (font.strike) children.push(createElement('strike', {}, []));\n if (font.size) children.push(createElement('sz', { val: String(font.size) }, []));\n if (font.color) children.push(createElement('color', { rgb: normalizeColor(font.color) }, []));\n if (font.name) children.push(createElement('name', { val: font.name }, []));\n return createElement('font', {}, children);\n }\n\n private _buildFillNode(fill: StyleFill): XmlNode {\n const patternChildren: XmlNode[] = [];\n if (fill.fgColor) {\n const rgb = normalizeColor(fill.fgColor);\n patternChildren.push(createElement('fgColor', { rgb }, []));\n // For solid fills, bgColor is required (indexed 64 = system background)\n if (fill.type === 'solid') {\n patternChildren.push(createElement('bgColor', { indexed: '64' }, []));\n }\n }\n if (fill.bgColor && fill.type !== 'solid') {\n const rgb = normalizeColor(fill.bgColor);\n patternChildren.push(createElement('bgColor', { rgb }, []));\n }\n const patternFill = createElement('patternFill', { patternType: fill.type || 'none' }, patternChildren);\n return createElement('fill', {}, [patternFill]);\n }\n\n private _buildBorderNode(border: StyleBorder): XmlNode {\n const children: XmlNode[] = [];\n if (border.left) children.push(createElement('left', { style: border.left }, []));\n if (border.right) children.push(createElement('right', { style: border.right }, []));\n if (border.top) children.push(createElement('top', { style: border.top }, []));\n if (border.bottom) children.push(createElement('bottom', { style: border.bottom }, []));\n // Add empty elements if not present (required by Excel)\n if (!border.left) children.push(createElement('left', {}, []));\n if (!border.right) children.push(createElement('right', {}, []));\n if (!border.top) children.push(createElement('top', {}, []));\n if (!border.bottom) children.push(createElement('bottom', {}, []));\n children.push(createElement('diagonal', {}, []));\n return createElement('border', {}, children);\n }\n\n private _buildXfNode(xf: CellXf): XmlNode {\n const attrs: Record<string, string> = {\n numFmtId: String(xf.numFmtId),\n fontId: String(xf.fontId),\n fillId: String(xf.fillId),\n borderId: String(xf.borderId),\n };\n\n if (xf.fontId > 0) attrs.applyFont = '1';\n if (xf.fillId > 0) attrs.applyFill = '1';\n if (xf.borderId > 0) attrs.applyBorder = '1';\n if (xf.numFmtId > 0) attrs.applyNumberFormat = '1';\n\n const children: XmlNode[] = [];\n if (xf.alignment) {\n const alignAttrs: Record<string, string> = {};\n if (xf.alignment.horizontal) alignAttrs.horizontal = xf.alignment.horizontal;\n if (xf.alignment.vertical) alignAttrs.vertical = xf.alignment.vertical;\n if (xf.alignment.wrapText) alignAttrs.wrapText = '1';\n if (xf.alignment.textRotation) alignAttrs.textRotation = String(xf.alignment.textRotation);\n children.push(createElement('alignment', alignAttrs, []));\n attrs.applyAlignment = '1';\n }\n\n return createElement('xf', attrs, children);\n }\n}\n\n// Internal types for style components\ninterface StyleFont {\n bold: boolean;\n italic: boolean;\n underline: boolean;\n strike: boolean;\n size?: number;\n name?: string;\n color?: string;\n}\n\ninterface StyleFill {\n type: string;\n fgColor?: string;\n bgColor?: string;\n}\n\ninterface StyleBorder {\n top?: BorderType;\n bottom?: BorderType;\n left?: BorderType;\n right?: BorderType;\n}\n\ninterface CellXf {\n fontId: number;\n fillId: number;\n borderId: number;\n numFmtId: number;\n alignment?: AlignmentStyle;\n}\n\ninterface AlignmentStyle {\n horizontal?: 'left' | 'center' | 'right' | 'justify';\n vertical?: 'top' | 'middle' | 'bottom';\n wrapText?: boolean;\n textRotation?: number;\n}\n","import type { PivotCacheField, CellValue } from './types';\nimport { createElement, stringifyXml, XmlNode } from './utils/xml';\n\n/**\n * Manages the pivot cache (definition and records) for a pivot table.\n * The cache stores source data metadata and cached values.\n */\nexport class PivotCache {\n private _cacheId: number;\n private _sourceSheet: string;\n private _sourceRange: string;\n private _fields: PivotCacheField[] = [];\n private _records: CellValue[][] = [];\n private _recordCount = 0;\n private _refreshOnLoad = true; // Default to true\n\n constructor(cacheId: number, sourceSheet: string, sourceRange: string) {\n this._cacheId = cacheId;\n this._sourceSheet = sourceSheet;\n this._sourceRange = sourceRange;\n }\n\n /**\n * Get the cache ID\n */\n get cacheId(): number {\n return this._cacheId;\n }\n\n /**\n * Set refreshOnLoad option\n */\n set refreshOnLoad(value: boolean) {\n this._refreshOnLoad = value;\n }\n\n /**\n * Get refreshOnLoad option\n */\n get refreshOnLoad(): boolean {\n return this._refreshOnLoad;\n }\n\n /**\n * Get the source sheet name\n */\n get sourceSheet(): string {\n return this._sourceSheet;\n }\n\n /**\n * Get the source range\n */\n get sourceRange(): string {\n return this._sourceRange;\n }\n\n /**\n * Get the full source reference (Sheet!Range)\n */\n get sourceRef(): string {\n return `${this._sourceSheet}!${this._sourceRange}`;\n }\n\n /**\n * Get the fields in this cache\n */\n get fields(): PivotCacheField[] {\n return this._fields;\n }\n\n /**\n * Get the number of data records\n */\n get recordCount(): number {\n return this._recordCount;\n }\n\n /**\n * Build the cache from source data.\n * @param headers - Array of column header names\n * @param data - 2D array of data rows (excluding headers)\n */\n buildFromData(headers: string[], data: CellValue[][]): void {\n this._recordCount = data.length;\n\n // Initialize fields from headers\n this._fields = headers.map((name, index) => ({\n name,\n index,\n isNumeric: true,\n isDate: false,\n sharedItems: [],\n minValue: undefined,\n maxValue: undefined,\n }));\n\n // Analyze data to determine field types and collect unique values\n for (const row of data) {\n for (let colIdx = 0; colIdx < row.length && colIdx < this._fields.length; colIdx++) {\n const value = row[colIdx];\n const field = this._fields[colIdx];\n\n if (value === null || value === undefined) {\n continue;\n }\n\n if (typeof value === 'string') {\n field.isNumeric = false;\n if (!field.sharedItems.includes(value)) {\n field.sharedItems.push(value);\n }\n } else if (typeof value === 'number') {\n if (field.minValue === undefined || value < field.minValue) {\n field.minValue = value;\n }\n if (field.maxValue === undefined || value > field.maxValue) {\n field.maxValue = value;\n }\n } else if (value instanceof Date) {\n field.isDate = true;\n field.isNumeric = false;\n } else if (typeof value === 'boolean') {\n field.isNumeric = false;\n }\n }\n }\n\n // Store records\n this._records = data;\n }\n\n /**\n * Get field by name\n */\n getField(name: string): PivotCacheField | undefined {\n return this._fields.find((f) => f.name === name);\n }\n\n /**\n * Get field index by name\n */\n getFieldIndex(name: string): number {\n const field = this._fields.find((f) => f.name === name);\n return field ? field.index : -1;\n }\n\n /**\n * Generate the pivotCacheDefinition XML\n */\n toDefinitionXml(recordsRelId: string): string {\n const cacheFieldNodes: XmlNode[] = this._fields.map((field) => {\n const sharedItemsAttrs: Record<string, string> = {};\n const sharedItemChildren: XmlNode[] = [];\n\n if (field.sharedItems.length > 0) {\n // String field with shared items - Excel just uses count attribute\n sharedItemsAttrs.count = String(field.sharedItems.length);\n\n for (const item of field.sharedItems) {\n sharedItemChildren.push(createElement('s', { v: item }, []));\n }\n } else if (field.isNumeric) {\n // Numeric field - use \"0\"/\"1\" for boolean attributes as Excel expects\n sharedItemsAttrs.containsSemiMixedTypes = '0';\n sharedItemsAttrs.containsString = '0';\n sharedItemsAttrs.containsNumber = '1';\n // Check if all values are integers\n if (field.minValue !== undefined && field.maxValue !== undefined) {\n const isInteger = Number.isInteger(field.minValue) && Number.isInteger(field.maxValue);\n if (isInteger) {\n sharedItemsAttrs.containsInteger = '1';\n }\n sharedItemsAttrs.minValue = String(field.minValue);\n sharedItemsAttrs.maxValue = String(field.maxValue);\n }\n }\n\n const sharedItemsNode = createElement('sharedItems', sharedItemsAttrs, sharedItemChildren);\n return createElement('cacheField', { name: field.name, numFmtId: '0' }, [sharedItemsNode]);\n });\n\n const cacheFieldsNode = createElement('cacheFields', { count: String(this._fields.length) }, cacheFieldNodes);\n\n const worksheetSourceNode = createElement(\n 'worksheetSource',\n { ref: this._sourceRange, sheet: this._sourceSheet },\n [],\n );\n const cacheSourceNode = createElement('cacheSource', { type: 'worksheet' }, [worksheetSourceNode]);\n\n // Build attributes - refreshOnLoad should come early per OOXML schema\n const definitionAttrs: Record<string, string> = {\n xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',\n 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',\n 'r:id': recordsRelId,\n };\n\n // Add refreshOnLoad early in attributes (default is true)\n if (this._refreshOnLoad) {\n definitionAttrs.refreshOnLoad = '1';\n }\n\n // Continue with remaining attributes\n definitionAttrs.refreshedBy = 'User';\n definitionAttrs.refreshedVersion = '8';\n definitionAttrs.minRefreshableVersion = '3';\n definitionAttrs.createdVersion = '8';\n definitionAttrs.recordCount = String(this._recordCount);\n\n const definitionNode = createElement('pivotCacheDefinition', definitionAttrs, [cacheSourceNode, cacheFieldsNode]);\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([definitionNode])}`;\n }\n\n /**\n * Generate the pivotCacheRecords XML\n */\n toRecordsXml(): string {\n const recordNodes: XmlNode[] = [];\n\n for (const row of this._records) {\n const fieldNodes: XmlNode[] = [];\n\n for (let colIdx = 0; colIdx < this._fields.length; colIdx++) {\n const field = this._fields[colIdx];\n const value = colIdx < row.length ? row[colIdx] : null;\n\n if (value === null || value === undefined) {\n // Missing value\n fieldNodes.push(createElement('m', {}, []));\n } else if (typeof value === 'string') {\n // String value - use index into sharedItems\n const idx = field.sharedItems.indexOf(value);\n if (idx >= 0) {\n fieldNodes.push(createElement('x', { v: String(idx) }, []));\n } else {\n // Direct string value (shouldn't happen if cache is built correctly)\n fieldNodes.push(createElement('s', { v: value }, []));\n }\n } else if (typeof value === 'number') {\n fieldNodes.push(createElement('n', { v: String(value) }, []));\n } else if (typeof value === 'boolean') {\n fieldNodes.push(createElement('b', { v: value ? '1' : '0' }, []));\n } else if (value instanceof Date) {\n fieldNodes.push(createElement('d', { v: value.toISOString() }, []));\n } else {\n // Unknown type, treat as missing\n fieldNodes.push(createElement('m', {}, []));\n }\n }\n\n recordNodes.push(createElement('r', {}, fieldNodes));\n }\n\n const recordsNode = createElement(\n 'pivotCacheRecords',\n {\n xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',\n 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',\n count: String(this._recordCount),\n },\n recordNodes,\n );\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([recordsNode])}`;\n }\n}\n","import type { AggregationType, PivotFieldAxis } from './types';\nimport { PivotCache } from './pivot-cache';\nimport { createElement, stringifyXml, XmlNode } from './utils/xml';\n\n/**\n * Internal structure for tracking field assignments\n */\ninterface FieldAssignment {\n fieldName: string;\n fieldIndex: number;\n axis: PivotFieldAxis;\n aggregation?: AggregationType;\n displayName?: string;\n}\n\n/**\n * Represents an Excel pivot table with a fluent API for configuration.\n */\nexport class PivotTable {\n private _name: string;\n private _cache: PivotCache;\n private _targetSheet: string;\n private _targetCell: string;\n private _targetRow: number;\n private _targetCol: number;\n\n private _rowFields: FieldAssignment[] = [];\n private _columnFields: FieldAssignment[] = [];\n private _valueFields: FieldAssignment[] = [];\n private _filterFields: FieldAssignment[] = [];\n\n private _pivotTableIndex: number;\n\n constructor(\n name: string,\n cache: PivotCache,\n targetSheet: string,\n targetCell: string,\n targetRow: number,\n targetCol: number,\n pivotTableIndex: number,\n ) {\n this._name = name;\n this._cache = cache;\n this._targetSheet = targetSheet;\n this._targetCell = targetCell;\n this._targetRow = targetRow;\n this._targetCol = targetCol;\n this._pivotTableIndex = pivotTableIndex;\n }\n\n /**\n * Get the pivot table name\n */\n get name(): string {\n return this._name;\n }\n\n /**\n * Get the target sheet name\n */\n get targetSheet(): string {\n return this._targetSheet;\n }\n\n /**\n * Get the target cell address\n */\n get targetCell(): string {\n return this._targetCell;\n }\n\n /**\n * Get the pivot cache\n */\n get cache(): PivotCache {\n return this._cache;\n }\n\n /**\n * Get the pivot table index (for file naming)\n */\n get index(): number {\n return this._pivotTableIndex;\n }\n\n /**\n * Add a field to the row area\n * @param fieldName - Name of the source field (column header)\n */\n addRowField(fieldName: string): this {\n const fieldIndex = this._cache.getFieldIndex(fieldName);\n if (fieldIndex < 0) {\n throw new Error(`Field not found in source data: ${fieldName}`);\n }\n\n this._rowFields.push({\n fieldName,\n fieldIndex,\n axis: 'row',\n });\n\n return this;\n }\n\n /**\n * Add a field to the column area\n * @param fieldName - Name of the source field (column header)\n */\n addColumnField(fieldName: string): this {\n const fieldIndex = this._cache.getFieldIndex(fieldName);\n if (fieldIndex < 0) {\n throw new Error(`Field not found in source data: ${fieldName}`);\n }\n\n this._columnFields.push({\n fieldName,\n fieldIndex,\n axis: 'column',\n });\n\n return this;\n }\n\n /**\n * Add a field to the values area with aggregation\n * @param fieldName - Name of the source field (column header)\n * @param aggregation - Aggregation function (sum, count, average, min, max)\n * @param displayName - Optional display name (defaults to \"Sum of FieldName\")\n */\n addValueField(fieldName: string, aggregation: AggregationType = 'sum', displayName?: string): this {\n const fieldIndex = this._cache.getFieldIndex(fieldName);\n if (fieldIndex < 0) {\n throw new Error(`Field not found in source data: ${fieldName}`);\n }\n\n const defaultName = `${aggregation.charAt(0).toUpperCase() + aggregation.slice(1)} of ${fieldName}`;\n\n this._valueFields.push({\n fieldName,\n fieldIndex,\n axis: 'value',\n aggregation,\n displayName: displayName || defaultName,\n });\n\n return this;\n }\n\n /**\n * Add a field to the filter (page) area\n * @param fieldName - Name of the source field (column header)\n */\n addFilterField(fieldName: string): this {\n const fieldIndex = this._cache.getFieldIndex(fieldName);\n if (fieldIndex < 0) {\n throw new Error(`Field not found in source data: ${fieldName}`);\n }\n\n this._filterFields.push({\n fieldName,\n fieldIndex,\n axis: 'filter',\n });\n\n return this;\n }\n\n /**\n * Generate the pivotTableDefinition XML\n */\n toXml(): string {\n const children: XmlNode[] = [];\n\n // Calculate location (estimate based on fields)\n const locationRef = this._calculateLocationRef();\n\n // Calculate first data row/col offsets (1-based, relative to pivot table)\n // firstHeaderRow: row offset of column headers (usually 1)\n // firstDataRow: row offset where data starts (after filters and column headers)\n // firstDataCol: column offset where data starts (after row labels)\n const filterRowCount = this._filterFields.length > 0 ? this._filterFields.length + 1 : 0;\n const headerRows = this._columnFields.length > 0 ? 1 : 0;\n const firstDataRow = filterRowCount + headerRows + 1;\n const firstDataCol = this._rowFields.length > 0 ? this._rowFields.length : 1;\n\n const locationNode = createElement(\n 'location',\n {\n ref: locationRef,\n firstHeaderRow: String(filterRowCount + 1),\n firstDataRow: String(firstDataRow),\n firstDataCol: String(firstDataCol),\n },\n [],\n );\n children.push(locationNode);\n\n // Build pivotFields (one per source field)\n const pivotFieldNodes: XmlNode[] = [];\n for (const cacheField of this._cache.fields) {\n const fieldNode = this._buildPivotFieldNode(cacheField.index);\n pivotFieldNodes.push(fieldNode);\n }\n children.push(createElement('pivotFields', { count: String(pivotFieldNodes.length) }, pivotFieldNodes));\n\n // Row fields\n if (this._rowFields.length > 0) {\n const rowFieldNodes = this._rowFields.map((f) => createElement('field', { x: String(f.fieldIndex) }, []));\n children.push(createElement('rowFields', { count: String(rowFieldNodes.length) }, rowFieldNodes));\n\n // Row items\n const rowItemNodes = this._buildRowItems();\n children.push(createElement('rowItems', { count: String(rowItemNodes.length) }, rowItemNodes));\n }\n\n // Column fields\n if (this._columnFields.length > 0) {\n const colFieldNodes = this._columnFields.map((f) => createElement('field', { x: String(f.fieldIndex) }, []));\n // If we have multiple value fields, add -2 to indicate where \"Values\" header goes\n if (this._valueFields.length > 1) {\n colFieldNodes.push(createElement('field', { x: '-2' }, []));\n }\n children.push(createElement('colFields', { count: String(colFieldNodes.length) }, colFieldNodes));\n\n // Column items - need to account for multiple value fields\n const colItemNodes = this._buildColItems();\n children.push(createElement('colItems', { count: String(colItemNodes.length) }, colItemNodes));\n } else if (this._valueFields.length > 1) {\n // If no column fields but we have multiple values, need colFields with -2 (data field indicator)\n children.push(createElement('colFields', { count: '1' }, [createElement('field', { x: '-2' }, [])]));\n\n // Column items for each value field\n const colItemNodes: XmlNode[] = [];\n for (let i = 0; i < this._valueFields.length; i++) {\n colItemNodes.push(createElement('i', {}, [createElement('x', i === 0 ? {} : { v: String(i) }, [])]));\n }\n children.push(createElement('colItems', { count: String(colItemNodes.length) }, colItemNodes));\n } else if (this._valueFields.length === 1) {\n // Single value field - just add a single column item\n children.push(createElement('colItems', { count: '1' }, [createElement('i', {}, [])]));\n }\n\n // Page (filter) fields\n if (this._filterFields.length > 0) {\n const pageFieldNodes = this._filterFields.map((f) =>\n createElement('pageField', { fld: String(f.fieldIndex), hier: '-1' }, []),\n );\n children.push(createElement('pageFields', { count: String(pageFieldNodes.length) }, pageFieldNodes));\n }\n\n // Data fields (values)\n if (this._valueFields.length > 0) {\n const dataFieldNodes = this._valueFields.map((f) =>\n createElement(\n 'dataField',\n {\n name: f.displayName || f.fieldName,\n fld: String(f.fieldIndex),\n baseField: '0',\n baseItem: '0',\n subtotal: f.aggregation || 'sum',\n },\n [],\n ),\n );\n children.push(createElement('dataFields', { count: String(dataFieldNodes.length) }, dataFieldNodes));\n }\n\n // Pivot table style\n children.push(\n createElement(\n 'pivotTableStyleInfo',\n {\n name: 'PivotStyleMedium9',\n showRowHeaders: '1',\n showColHeaders: '1',\n showRowStripes: '0',\n showColStripes: '0',\n showLastColumn: '1',\n },\n [],\n ),\n );\n\n const pivotTableNode = createElement(\n 'pivotTableDefinition',\n {\n xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',\n 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',\n name: this._name,\n cacheId: String(this._cache.cacheId),\n applyNumberFormats: '0',\n applyBorderFormats: '0',\n applyFontFormats: '0',\n applyPatternFormats: '0',\n applyAlignmentFormats: '0',\n applyWidthHeightFormats: '1',\n dataCaption: 'Values',\n updatedVersion: '8',\n minRefreshableVersion: '3',\n useAutoFormatting: '1',\n rowGrandTotals: '1',\n colGrandTotals: '1',\n itemPrintTitles: '1',\n createdVersion: '8',\n indent: '0',\n outline: '1',\n outlineData: '1',\n multipleFieldFilters: '0',\n },\n children,\n );\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([pivotTableNode])}`;\n }\n\n /**\n * Build a pivotField node for a given field index\n */\n private _buildPivotFieldNode(fieldIndex: number): XmlNode {\n const attrs: Record<string, string> = {};\n const children: XmlNode[] = [];\n\n // Check if this field is assigned to an axis\n const rowField = this._rowFields.find((f) => f.fieldIndex === fieldIndex);\n const colField = this._columnFields.find((f) => f.fieldIndex === fieldIndex);\n const filterField = this._filterFields.find((f) => f.fieldIndex === fieldIndex);\n const valueField = this._valueFields.find((f) => f.fieldIndex === fieldIndex);\n\n if (rowField) {\n attrs.axis = 'axisRow';\n attrs.showAll = '0';\n // Add items for shared values\n const cacheField = this._cache.fields[fieldIndex];\n if (cacheField && cacheField.sharedItems.length > 0) {\n const itemNodes: XmlNode[] = [];\n for (let i = 0; i < cacheField.sharedItems.length; i++) {\n itemNodes.push(createElement('item', { x: String(i) }, []));\n }\n // Add default subtotal item\n itemNodes.push(createElement('item', { t: 'default' }, []));\n children.push(createElement('items', { count: String(itemNodes.length) }, itemNodes));\n }\n } else if (colField) {\n attrs.axis = 'axisCol';\n attrs.showAll = '0';\n const cacheField = this._cache.fields[fieldIndex];\n if (cacheField && cacheField.sharedItems.length > 0) {\n const itemNodes: XmlNode[] = [];\n for (let i = 0; i < cacheField.sharedItems.length; i++) {\n itemNodes.push(createElement('item', { x: String(i) }, []));\n }\n itemNodes.push(createElement('item', { t: 'default' }, []));\n children.push(createElement('items', { count: String(itemNodes.length) }, itemNodes));\n }\n } else if (filterField) {\n attrs.axis = 'axisPage';\n attrs.showAll = '0';\n const cacheField = this._cache.fields[fieldIndex];\n if (cacheField && cacheField.sharedItems.length > 0) {\n const itemNodes: XmlNode[] = [];\n for (let i = 0; i < cacheField.sharedItems.length; i++) {\n itemNodes.push(createElement('item', { x: String(i) }, []));\n }\n itemNodes.push(createElement('item', { t: 'default' }, []));\n children.push(createElement('items', { count: String(itemNodes.length) }, itemNodes));\n }\n } else if (valueField) {\n attrs.dataField = '1';\n attrs.showAll = '0';\n } else {\n attrs.showAll = '0';\n }\n\n return createElement('pivotField', attrs, children);\n }\n\n /**\n * Build row items based on unique values in row fields\n */\n private _buildRowItems(): XmlNode[] {\n const items: XmlNode[] = [];\n\n if (this._rowFields.length === 0) return items;\n\n // Get unique values from first row field\n const firstRowField = this._rowFields[0];\n const cacheField = this._cache.fields[firstRowField.fieldIndex];\n\n if (cacheField && cacheField.sharedItems.length > 0) {\n for (let i = 0; i < cacheField.sharedItems.length; i++) {\n items.push(createElement('i', {}, [createElement('x', i === 0 ? {} : { v: String(i) }, [])]));\n }\n }\n\n // Add grand total row\n items.push(createElement('i', { t: 'grand' }, [createElement('x', {}, [])]));\n\n return items;\n }\n\n /**\n * Build column items based on unique values in column fields\n */\n private _buildColItems(): XmlNode[] {\n const items: XmlNode[] = [];\n\n if (this._columnFields.length === 0) return items;\n\n // Get unique values from first column field\n const firstColField = this._columnFields[0];\n const cacheField = this._cache.fields[firstColField.fieldIndex];\n\n if (cacheField && cacheField.sharedItems.length > 0) {\n if (this._valueFields.length > 1) {\n // Multiple value fields - need nested items for each column value + value field combination\n for (let colIdx = 0; colIdx < cacheField.sharedItems.length; colIdx++) {\n for (let valIdx = 0; valIdx < this._valueFields.length; valIdx++) {\n const xNodes: XmlNode[] = [\n createElement('x', colIdx === 0 ? {} : { v: String(colIdx) }, []),\n createElement('x', valIdx === 0 ? {} : { v: String(valIdx) }, []),\n ];\n items.push(createElement('i', {}, xNodes));\n }\n }\n } else {\n // Single value field - simple column items\n for (let i = 0; i < cacheField.sharedItems.length; i++) {\n items.push(createElement('i', {}, [createElement('x', i === 0 ? {} : { v: String(i) }, [])]));\n }\n }\n }\n\n // Add grand total column(s)\n if (this._valueFields.length > 1) {\n // Grand total for each value field\n for (let valIdx = 0; valIdx < this._valueFields.length; valIdx++) {\n const xNodes: XmlNode[] = [\n createElement('x', {}, []),\n createElement('x', valIdx === 0 ? {} : { v: String(valIdx) }, []),\n ];\n items.push(createElement('i', { t: 'grand' }, xNodes));\n }\n } else {\n items.push(createElement('i', { t: 'grand' }, [createElement('x', {}, [])]));\n }\n\n return items;\n }\n\n /**\n * Calculate the location reference for the pivot table output\n */\n private _calculateLocationRef(): string {\n // Estimate output size based on fields\n const numRows = this._estimateRowCount();\n const numCols = this._estimateColCount();\n\n const startRow = this._targetRow;\n const startCol = this._targetCol;\n const endRow = startRow + numRows - 1;\n const endCol = startCol + numCols - 1;\n\n return `${this._colToLetter(startCol)}${startRow}:${this._colToLetter(endCol)}${endRow}`;\n }\n\n /**\n * Estimate number of rows in pivot table output\n */\n private _estimateRowCount(): number {\n let count = 1; // Header row\n\n // Add filter area rows\n count += this._filterFields.length;\n\n // Add row labels (unique values in row fields)\n if (this._rowFields.length > 0) {\n const firstRowField = this._rowFields[0];\n const cacheField = this._cache.fields[firstRowField.fieldIndex];\n count += (cacheField?.sharedItems.length || 1) + 1; // +1 for grand total\n } else {\n count += 1; // At least one data row\n }\n\n return Math.max(count, 3);\n }\n\n /**\n * Estimate number of columns in pivot table output\n */\n private _estimateColCount(): number {\n let count = 0;\n\n // Row label columns\n count += Math.max(this._rowFields.length, 1);\n\n // Column labels (unique values in column fields)\n if (this._columnFields.length > 0) {\n const firstColField = this._columnFields[0];\n const cacheField = this._cache.fields[firstColField.fieldIndex];\n count += (cacheField?.sharedItems.length || 1) + 1; // +1 for grand total\n } else {\n // Value columns\n count += Math.max(this._valueFields.length, 1);\n }\n\n return Math.max(count, 2);\n }\n\n /**\n * Convert 0-based column index to letter (A, B, ..., Z, AA, etc.)\n */\n private _colToLetter(col: number): string {\n let result = '';\n let n = col;\n while (n >= 0) {\n result = String.fromCharCode((n % 26) + 65) + result;\n n = Math.floor(n / 26) - 1;\n }\n return result;\n }\n}\n","import { readFile, writeFile } from 'fs/promises';\nimport type { SheetDefinition, Relationship, PivotTableConfig, CellValue } from './types';\nimport { Worksheet } from './worksheet';\nimport { SharedStrings } from './shared-strings';\nimport { Styles } from './styles';\nimport { PivotTable } from './pivot-table';\nimport { PivotCache } from './pivot-cache';\nimport { readZip, writeZip, readZipText, writeZipText, ZipFiles } from './utils/zip';\nimport { parseAddress, parseRange, toAddress } from './utils/address';\nimport { parseXml, findElement, getChildren, getAttr, XmlNode, stringifyXml, createElement } from './utils/xml';\n\n/**\n * Represents an Excel workbook (.xlsx file)\n */\nexport class Workbook {\n private _files: ZipFiles = new Map();\n private _sheets: Map<string, Worksheet> = new Map();\n private _sheetDefs: SheetDefinition[] = [];\n private _relationships: Relationship[] = [];\n private _sharedStrings: SharedStrings;\n private _styles: Styles;\n private _dirty = false;\n\n // Pivot table support\n private _pivotTables: PivotTable[] = [];\n private _pivotCaches: PivotCache[] = [];\n private _nextCacheId = 0;\n\n private constructor() {\n this._sharedStrings = new SharedStrings();\n this._styles = Styles.createDefault();\n }\n\n /**\n * Load a workbook from a file path\n */\n static async fromFile(path: string): Promise<Workbook> {\n const data = await readFile(path);\n return Workbook.fromBuffer(new Uint8Array(data));\n }\n\n /**\n * Load a workbook from a buffer\n */\n static async fromBuffer(data: Uint8Array): Promise<Workbook> {\n const workbook = new Workbook();\n workbook._files = await readZip(data);\n\n // Parse workbook.xml for sheet definitions\n const workbookXml = readZipText(workbook._files, 'xl/workbook.xml');\n if (workbookXml) {\n workbook._parseWorkbook(workbookXml);\n }\n\n // Parse relationships\n const relsXml = readZipText(workbook._files, 'xl/_rels/workbook.xml.rels');\n if (relsXml) {\n workbook._parseRelationships(relsXml);\n }\n\n // Parse shared strings\n const sharedStringsXml = readZipText(workbook._files, 'xl/sharedStrings.xml');\n if (sharedStringsXml) {\n workbook._sharedStrings = SharedStrings.parse(sharedStringsXml);\n }\n\n // Parse styles\n const stylesXml = readZipText(workbook._files, 'xl/styles.xml');\n if (stylesXml) {\n workbook._styles = Styles.parse(stylesXml);\n }\n\n return workbook;\n }\n\n /**\n * Create a new empty workbook\n */\n static create(): Workbook {\n const workbook = new Workbook();\n workbook._dirty = true;\n\n // Add default sheet\n workbook.addSheet('Sheet1');\n\n return workbook;\n }\n\n /**\n * Get sheet names\n */\n get sheetNames(): string[] {\n return this._sheetDefs.map((s) => s.name);\n }\n\n /**\n * Get number of sheets\n */\n get sheetCount(): number {\n return this._sheetDefs.length;\n }\n\n /**\n * Get shared strings table\n */\n get sharedStrings(): SharedStrings {\n return this._sharedStrings;\n }\n\n /**\n * Get styles\n */\n get styles(): Styles {\n return this._styles;\n }\n\n /**\n * Get a worksheet by name or index\n */\n sheet(nameOrIndex: string | number): Worksheet {\n let def: SheetDefinition | undefined;\n\n if (typeof nameOrIndex === 'number') {\n def = this._sheetDefs[nameOrIndex];\n } else {\n def = this._sheetDefs.find((s) => s.name === nameOrIndex);\n }\n\n if (!def) {\n throw new Error(`Sheet not found: ${nameOrIndex}`);\n }\n\n // Return cached worksheet if available\n if (this._sheets.has(def.name)) {\n return this._sheets.get(def.name)!;\n }\n\n // Load worksheet\n const worksheet = new Worksheet(this, def.name);\n\n // Find the relationship to get the file path\n const rel = this._relationships.find((r) => r.id === def.rId);\n if (rel) {\n const sheetPath = `xl/${rel.target}`;\n const sheetXml = readZipText(this._files, sheetPath);\n if (sheetXml) {\n worksheet.parse(sheetXml);\n }\n }\n\n this._sheets.set(def.name, worksheet);\n return worksheet;\n }\n\n /**\n * Add a new worksheet\n */\n addSheet(name: string, index?: number): Worksheet {\n // Check for duplicate name\n if (this._sheetDefs.some((s) => s.name === name)) {\n throw new Error(`Sheet already exists: ${name}`);\n }\n\n this._dirty = true;\n\n // Generate new sheet ID and relationship ID\n const sheetId = Math.max(0, ...this._sheetDefs.map((s) => s.sheetId)) + 1;\n const rId = `rId${Math.max(0, ...this._relationships.map((r) => parseInt(r.id.replace('rId', ''), 10) || 0)) + 1}`;\n\n const def: SheetDefinition = { name, sheetId, rId };\n\n // Add relationship\n this._relationships.push({\n id: rId,\n type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet',\n target: `worksheets/sheet${sheetId}.xml`,\n });\n\n // Insert at index or append\n if (index !== undefined && index >= 0 && index < this._sheetDefs.length) {\n this._sheetDefs.splice(index, 0, def);\n } else {\n this._sheetDefs.push(def);\n }\n\n // Create worksheet\n const worksheet = new Worksheet(this, name);\n this._sheets.set(name, worksheet);\n\n return worksheet;\n }\n\n /**\n * Delete a worksheet by name or index\n */\n deleteSheet(nameOrIndex: string | number): void {\n let index: number;\n\n if (typeof nameOrIndex === 'number') {\n index = nameOrIndex;\n } else {\n index = this._sheetDefs.findIndex((s) => s.name === nameOrIndex);\n }\n\n if (index < 0 || index >= this._sheetDefs.length) {\n throw new Error(`Sheet not found: ${nameOrIndex}`);\n }\n\n if (this._sheetDefs.length === 1) {\n throw new Error('Cannot delete the last sheet');\n }\n\n this._dirty = true;\n\n const def = this._sheetDefs[index];\n this._sheetDefs.splice(index, 1);\n this._sheets.delete(def.name);\n\n // Remove relationship\n const relIndex = this._relationships.findIndex((r) => r.id === def.rId);\n if (relIndex >= 0) {\n this._relationships.splice(relIndex, 1);\n }\n }\n\n /**\n * Rename a worksheet\n */\n renameSheet(oldName: string, newName: string): void {\n const def = this._sheetDefs.find((s) => s.name === oldName);\n if (!def) {\n throw new Error(`Sheet not found: ${oldName}`);\n }\n\n if (this._sheetDefs.some((s) => s.name === newName)) {\n throw new Error(`Sheet already exists: ${newName}`);\n }\n\n this._dirty = true;\n\n // Update cached worksheet\n const worksheet = this._sheets.get(oldName);\n if (worksheet) {\n worksheet.name = newName;\n this._sheets.delete(oldName);\n this._sheets.set(newName, worksheet);\n }\n\n def.name = newName;\n }\n\n /**\n * Copy a worksheet\n */\n copySheet(sourceName: string, newName: string): Worksheet {\n const source = this.sheet(sourceName);\n const copy = this.addSheet(newName);\n\n // Copy all cells\n for (const [address, cell] of source.cells) {\n const newCell = copy.cell(address);\n newCell.value = cell.value;\n if (cell.formula) {\n newCell.formula = cell.formula;\n }\n if (cell.styleIndex !== undefined) {\n newCell.styleIndex = cell.styleIndex;\n }\n }\n\n // Copy merged cells\n for (const mergedRange of source.mergedCells) {\n copy.mergeCells(mergedRange);\n }\n\n return copy;\n }\n\n /**\n * Create a pivot table from source data.\n *\n * @param config - Pivot table configuration\n * @returns PivotTable instance for fluent configuration\n *\n * @example\n * ```typescript\n * const pivot = wb.createPivotTable({\n * name: 'SalesPivot',\n * source: 'DataSheet!A1:D100',\n * target: 'PivotSheet!A3',\n * });\n *\n * pivot\n * .addRowField('Region')\n * .addColumnField('Product')\n * .addValueField('Sales', 'sum', 'Total Sales');\n * ```\n */\n createPivotTable(config: PivotTableConfig): PivotTable {\n this._dirty = true;\n\n // Parse source reference (Sheet!Range)\n const { sheetName: sourceSheet, range: sourceRange } = this._parseSheetRef(config.source);\n\n // Parse target reference\n const { sheetName: targetSheet, range: targetCell } = this._parseSheetRef(config.target);\n\n // Ensure target sheet exists\n if (!this._sheetDefs.some((s) => s.name === targetSheet)) {\n this.addSheet(targetSheet);\n }\n\n // Parse target cell address\n const targetAddr = parseAddress(targetCell);\n\n // Get source worksheet and extract data\n const sourceWs = this.sheet(sourceSheet);\n const { headers, data } = this._extractSourceData(sourceWs, sourceRange);\n\n // Create pivot cache\n const cacheId = this._nextCacheId++;\n const cache = new PivotCache(cacheId, sourceSheet, sourceRange);\n cache.buildFromData(headers, data);\n // refreshOnLoad defaults to true; only disable if explicitly set to false\n if (config.refreshOnLoad === false) {\n cache.refreshOnLoad = false;\n }\n this._pivotCaches.push(cache);\n\n // Create pivot table\n const pivotTableIndex = this._pivotTables.length + 1;\n const pivotTable = new PivotTable(\n config.name,\n cache,\n targetSheet,\n targetCell,\n targetAddr.row + 1, // Convert to 1-based\n targetAddr.col,\n pivotTableIndex,\n );\n this._pivotTables.push(pivotTable);\n\n return pivotTable;\n }\n\n /**\n * Parse a sheet reference like \"Sheet1!A1:D100\" into sheet name and range\n */\n private _parseSheetRef(ref: string): { sheetName: string; range: string } {\n const match = ref.match(/^(.+?)!(.+)$/);\n if (!match) {\n throw new Error(`Invalid reference format: ${ref}. Expected \"SheetName!Range\"`);\n }\n return { sheetName: match[1], range: match[2] };\n }\n\n /**\n * Extract headers and data from a source range\n */\n private _extractSourceData(sheet: Worksheet, rangeStr: string): { headers: string[]; data: CellValue[][] } {\n const range = parseRange(rangeStr);\n const headers: string[] = [];\n const data: CellValue[][] = [];\n\n // First row is headers\n for (let col = range.start.col; col <= range.end.col; col++) {\n const cell = sheet.cell(toAddress(range.start.row, col));\n headers.push(String(cell.value ?? `Column${col + 1}`));\n }\n\n // Remaining rows are data\n for (let row = range.start.row + 1; row <= range.end.row; row++) {\n const rowData: CellValue[] = [];\n for (let col = range.start.col; col <= range.end.col; col++) {\n const cell = sheet.cell(toAddress(row, col));\n rowData.push(cell.value);\n }\n data.push(rowData);\n }\n\n return { headers, data };\n }\n\n /**\n * Save the workbook to a file\n */\n async toFile(path: string): Promise<void> {\n const buffer = await this.toBuffer();\n await writeFile(path, buffer);\n }\n\n /**\n * Save the workbook to a buffer\n */\n async toBuffer(): Promise<Uint8Array> {\n // Update files map with modified content\n this._updateFiles();\n\n // Write ZIP\n return writeZip(this._files);\n }\n\n private _parseWorkbook(xml: string): void {\n const parsed = parseXml(xml);\n const workbook = findElement(parsed, 'workbook');\n if (!workbook) return;\n\n const children = getChildren(workbook, 'workbook');\n const sheets = findElement(children, 'sheets');\n if (!sheets) return;\n\n for (const child of getChildren(sheets, 'sheets')) {\n if ('sheet' in child) {\n const name = getAttr(child, 'name');\n const sheetId = getAttr(child, 'sheetId');\n const rId = getAttr(child, 'r:id');\n\n if (name && sheetId && rId) {\n this._sheetDefs.push({\n name,\n sheetId: parseInt(sheetId, 10),\n rId,\n });\n }\n }\n }\n }\n\n private _parseRelationships(xml: string): void {\n const parsed = parseXml(xml);\n const rels = findElement(parsed, 'Relationships');\n if (!rels) return;\n\n for (const child of getChildren(rels, 'Relationships')) {\n if ('Relationship' in child) {\n const id = getAttr(child, 'Id');\n const type = getAttr(child, 'Type');\n const target = getAttr(child, 'Target');\n\n if (id && type && target) {\n this._relationships.push({ id, type, target });\n }\n }\n }\n }\n\n private _updateFiles(): void {\n // Update workbook.xml\n this._updateWorkbookXml();\n\n // Update relationships\n this._updateRelationshipsXml();\n\n // Update content types\n this._updateContentTypes();\n\n // Update shared strings if modified\n if (this._sharedStrings.dirty || this._sharedStrings.count > 0) {\n writeZipText(this._files, 'xl/sharedStrings.xml', this._sharedStrings.toXml());\n }\n\n // Update styles if modified or if file doesn't exist yet\n if (this._styles.dirty || this._dirty || !this._files.has('xl/styles.xml')) {\n writeZipText(this._files, 'xl/styles.xml', this._styles.toXml());\n }\n\n // Update worksheets\n for (const [name, worksheet] of this._sheets) {\n if (worksheet.dirty || this._dirty) {\n const def = this._sheetDefs.find((s) => s.name === name);\n if (def) {\n const rel = this._relationships.find((r) => r.id === def.rId);\n if (rel) {\n const sheetPath = `xl/${rel.target}`;\n writeZipText(this._files, sheetPath, worksheet.toXml());\n }\n }\n }\n }\n\n // Update pivot tables\n if (this._pivotTables.length > 0) {\n this._updatePivotTableFiles();\n }\n }\n\n private _updateWorkbookXml(): void {\n const sheetNodes: XmlNode[] = this._sheetDefs.map((def) =>\n createElement('sheet', { name: def.name, sheetId: String(def.sheetId), 'r:id': def.rId }, []),\n );\n\n const sheetsNode = createElement('sheets', {}, sheetNodes);\n\n const children: XmlNode[] = [sheetsNode];\n\n // Add pivot caches if any\n if (this._pivotCaches.length > 0) {\n const pivotCacheNodes: XmlNode[] = this._pivotCaches.map((cache, idx) => {\n // Cache relationship ID is after sheets, sharedStrings, and styles\n const cacheRelId = `rId${this._relationships.length + 3 + idx}`;\n return createElement('pivotCache', { cacheId: String(cache.cacheId), 'r:id': cacheRelId }, []);\n });\n children.push(createElement('pivotCaches', {}, pivotCacheNodes));\n }\n\n const workbookNode = createElement(\n 'workbook',\n {\n xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',\n 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',\n },\n children,\n );\n\n const xml = `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([workbookNode])}`;\n writeZipText(this._files, 'xl/workbook.xml', xml);\n }\n\n private _updateRelationshipsXml(): void {\n const relNodes: XmlNode[] = this._relationships.map((rel) =>\n createElement('Relationship', { Id: rel.id, Type: rel.type, Target: rel.target }, []),\n );\n\n let nextRelId = this._relationships.length + 1;\n\n // Add shared strings relationship if needed\n if (this._sharedStrings.count > 0) {\n const hasSharedStrings = this._relationships.some(\n (r) => r.type === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings',\n );\n if (!hasSharedStrings) {\n relNodes.push(\n createElement(\n 'Relationship',\n {\n Id: `rId${nextRelId++}`,\n Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings',\n Target: 'sharedStrings.xml',\n },\n [],\n ),\n );\n }\n }\n\n // Add styles relationship if needed\n const hasStyles = this._relationships.some(\n (r) => r.type === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles',\n );\n if (!hasStyles) {\n relNodes.push(\n createElement(\n 'Relationship',\n {\n Id: `rId${nextRelId++}`,\n Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles',\n Target: 'styles.xml',\n },\n [],\n ),\n );\n }\n\n // Add pivot cache relationships\n for (let i = 0; i < this._pivotCaches.length; i++) {\n relNodes.push(\n createElement(\n 'Relationship',\n {\n Id: `rId${nextRelId++}`,\n Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition',\n Target: `pivotCache/pivotCacheDefinition${i + 1}.xml`,\n },\n [],\n ),\n );\n }\n\n const relsNode = createElement(\n 'Relationships',\n { xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships' },\n relNodes,\n );\n\n const xml = `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([relsNode])}`;\n writeZipText(this._files, 'xl/_rels/workbook.xml.rels', xml);\n }\n\n private _updateContentTypes(): void {\n const types: XmlNode[] = [\n createElement(\n 'Default',\n { Extension: 'rels', ContentType: 'application/vnd.openxmlformats-package.relationships+xml' },\n [],\n ),\n createElement('Default', { Extension: 'xml', ContentType: 'application/xml' }, []),\n createElement(\n 'Override',\n {\n PartName: '/xl/workbook.xml',\n ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml',\n },\n [],\n ),\n createElement(\n 'Override',\n {\n PartName: '/xl/styles.xml',\n ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml',\n },\n [],\n ),\n ];\n\n // Add shared strings if present\n if (this._sharedStrings.count > 0) {\n types.push(\n createElement(\n 'Override',\n {\n PartName: '/xl/sharedStrings.xml',\n ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml',\n },\n [],\n ),\n );\n }\n\n // Add worksheets\n for (const def of this._sheetDefs) {\n const rel = this._relationships.find((r) => r.id === def.rId);\n if (rel) {\n types.push(\n createElement(\n 'Override',\n {\n PartName: `/xl/${rel.target}`,\n ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml',\n },\n [],\n ),\n );\n }\n }\n\n // Add pivot cache definitions and records\n for (let i = 0; i < this._pivotCaches.length; i++) {\n types.push(\n createElement(\n 'Override',\n {\n PartName: `/xl/pivotCache/pivotCacheDefinition${i + 1}.xml`,\n ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml',\n },\n [],\n ),\n );\n types.push(\n createElement(\n 'Override',\n {\n PartName: `/xl/pivotCache/pivotCacheRecords${i + 1}.xml`,\n ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml',\n },\n [],\n ),\n );\n }\n\n // Add pivot tables\n for (let i = 0; i < this._pivotTables.length; i++) {\n types.push(\n createElement(\n 'Override',\n {\n PartName: `/xl/pivotTables/pivotTable${i + 1}.xml`,\n ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml',\n },\n [],\n ),\n );\n }\n\n const typesNode = createElement(\n 'Types',\n { xmlns: 'http://schemas.openxmlformats.org/package/2006/content-types' },\n types,\n );\n\n const xml = `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([typesNode])}`;\n writeZipText(this._files, '[Content_Types].xml', xml);\n\n // Also ensure _rels/.rels exists\n const rootRelsXml = readZipText(this._files, '_rels/.rels');\n if (!rootRelsXml) {\n const rootRels = createElement(\n 'Relationships',\n { xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships' },\n [\n createElement(\n 'Relationship',\n {\n Id: 'rId1',\n Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument',\n Target: 'xl/workbook.xml',\n },\n [],\n ),\n ],\n );\n writeZipText(\n this._files,\n '_rels/.rels',\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([rootRels])}`,\n );\n }\n }\n\n /**\n * Generate all pivot table related files\n */\n private _updatePivotTableFiles(): void {\n // Track which sheets have pivot tables for their .rels files\n const sheetPivotTables: Map<string, PivotTable[]> = new Map();\n\n for (const pivotTable of this._pivotTables) {\n const sheetName = pivotTable.targetSheet;\n if (!sheetPivotTables.has(sheetName)) {\n sheetPivotTables.set(sheetName, []);\n }\n sheetPivotTables.get(sheetName)!.push(pivotTable);\n }\n\n // Generate pivot cache files\n for (let i = 0; i < this._pivotCaches.length; i++) {\n const cache = this._pivotCaches[i];\n const cacheIdx = i + 1;\n\n // Pivot cache definition\n const definitionPath = `xl/pivotCache/pivotCacheDefinition${cacheIdx}.xml`;\n writeZipText(this._files, definitionPath, cache.toDefinitionXml('rId1'));\n\n // Pivot cache records\n const recordsPath = `xl/pivotCache/pivotCacheRecords${cacheIdx}.xml`;\n writeZipText(this._files, recordsPath, cache.toRecordsXml());\n\n // Pivot cache definition relationships (link to records)\n const cacheRelsPath = `xl/pivotCache/_rels/pivotCacheDefinition${cacheIdx}.xml.rels`;\n const cacheRels = createElement(\n 'Relationships',\n { xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships' },\n [\n createElement(\n 'Relationship',\n {\n Id: 'rId1',\n Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheRecords',\n Target: `pivotCacheRecords${cacheIdx}.xml`,\n },\n [],\n ),\n ],\n );\n writeZipText(\n this._files,\n cacheRelsPath,\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([cacheRels])}`,\n );\n }\n\n // Generate pivot table files\n for (let i = 0; i < this._pivotTables.length; i++) {\n const pivotTable = this._pivotTables[i];\n const ptIdx = i + 1;\n\n // Pivot table definition\n const ptPath = `xl/pivotTables/pivotTable${ptIdx}.xml`;\n writeZipText(this._files, ptPath, pivotTable.toXml());\n\n // Pivot table relationships (link to cache definition)\n const cacheIdx = this._pivotCaches.indexOf(pivotTable.cache) + 1;\n const ptRelsPath = `xl/pivotTables/_rels/pivotTable${ptIdx}.xml.rels`;\n const ptRels = createElement(\n 'Relationships',\n { xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships' },\n [\n createElement(\n 'Relationship',\n {\n Id: 'rId1',\n Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition',\n Target: `../pivotCache/pivotCacheDefinition${cacheIdx}.xml`,\n },\n [],\n ),\n ],\n );\n writeZipText(\n this._files,\n ptRelsPath,\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([ptRels])}`,\n );\n }\n\n // Generate worksheet relationships for pivot tables\n for (const [sheetName, pivotTables] of sheetPivotTables) {\n const def = this._sheetDefs.find((s) => s.name === sheetName);\n if (!def) continue;\n\n const rel = this._relationships.find((r) => r.id === def.rId);\n if (!rel) continue;\n\n // Extract sheet file name from target path\n const sheetFileName = rel.target.split('/').pop();\n const sheetRelsPath = `xl/worksheets/_rels/${sheetFileName}.rels`;\n\n const relNodes: XmlNode[] = [];\n for (let i = 0; i < pivotTables.length; i++) {\n const pt = pivotTables[i];\n relNodes.push(\n createElement(\n 'Relationship',\n {\n Id: `rId${i + 1}`,\n Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable',\n Target: `../pivotTables/pivotTable${pt.index}.xml`,\n },\n [],\n ),\n );\n }\n\n const sheetRels = createElement(\n 'Relationships',\n { xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships' },\n relNodes,\n );\n writeZipText(\n this._files,\n sheetRelsPath,\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([sheetRels])}`,\n );\n }\n }\n}\n","import type { CellAddress, RangeAddress } from '../types';\n\n/**\n * Converts a column index (0-based) to Excel column letters (A, B, ..., Z, AA, AB, ...)\n * @param col - 0-based column index\n * @returns Column letter(s)\n */\nexport const colToLetter = (col: number): string => {\n let result = '';\n let n = col;\n while (n >= 0) {\n result = String.fromCharCode((n % 26) + 65) + result;\n n = Math.floor(n / 26) - 1;\n }\n return result;\n};\n\n/**\n * Converts Excel column letters to a 0-based column index\n * @param letters - Column letter(s) like 'A', 'B', 'AA'\n * @returns 0-based column index\n */\nexport const letterToCol = (letters: string): number => {\n const upper = letters.toUpperCase();\n let col = 0;\n for (let i = 0; i < upper.length; i++) {\n col = col * 26 + (upper.charCodeAt(i) - 64);\n }\n return col - 1;\n};\n\n/**\n * Parses an Excel cell address (e.g., 'A1', '$B$2') to row/col indices\n * @param address - Cell address string\n * @returns CellAddress with 0-based row and col\n */\nexport const parseAddress = (address: string): CellAddress => {\n // Remove $ signs for absolute references\n const clean = address.replace(/\\$/g, '');\n const match = clean.match(/^([A-Z]+)(\\d+)$/i);\n if (!match) {\n throw new Error(`Invalid cell address: ${address}`);\n }\n const col = letterToCol(match[1].toUpperCase());\n const row = parseInt(match[2], 10) - 1; // Convert to 0-based\n return { row, col };\n};\n\n/**\n * Converts row/col indices to an Excel cell address\n * @param row - 0-based row index\n * @param col - 0-based column index\n * @returns Cell address string like 'A1'\n */\nexport const toAddress = (row: number, col: number): string => {\n return `${colToLetter(col)}${row + 1}`;\n};\n\n/**\n * Parses an Excel range (e.g., 'A1:B10') to start/end addresses\n * @param range - Range string\n * @returns RangeAddress with start and end\n */\nexport const parseRange = (range: string): RangeAddress => {\n const parts = range.split(':');\n if (parts.length === 1) {\n // Single cell range\n const addr = parseAddress(parts[0]);\n return { start: addr, end: addr };\n }\n if (parts.length !== 2) {\n throw new Error(`Invalid range: ${range}`);\n }\n return {\n start: parseAddress(parts[0]),\n end: parseAddress(parts[1]),\n };\n};\n\n/**\n * Converts a RangeAddress to a range string\n * @param range - RangeAddress object\n * @returns Range string like 'A1:B10'\n */\nexport const toRange = (range: RangeAddress): string => {\n const start = toAddress(range.start.row, range.start.col);\n const end = toAddress(range.end.row, range.end.col);\n if (start === end) {\n return start;\n }\n return `${start}:${end}`;\n};\n\n/**\n * Normalizes a range so start is always top-left and end is bottom-right\n */\nexport const normalizeRange = (range: RangeAddress): RangeAddress => {\n return {\n start: {\n row: Math.min(range.start.row, range.end.row),\n col: Math.min(range.start.col, range.end.col),\n },\n end: {\n row: Math.max(range.start.row, range.end.row),\n col: Math.max(range.start.col, range.end.col),\n },\n };\n};\n\n/**\n * Checks if an address is within a range\n */\nexport const isInRange = (addr: CellAddress, range: RangeAddress): boolean => {\n const norm = normalizeRange(range);\n return (\n addr.row >= norm.start.row && addr.row <= norm.end.row && addr.col >= norm.start.col && addr.col <= norm.end.col\n );\n};\n"],"names":["__cell.Cell"],"mappings":"AAAA;AACA;AACA;AACO,KAAA,SAAA,+BAAA,IAAA,UAAA,SAAA;AACP;AACA;AACA;AACO,UAAA,SAAA;AACP,WAAA,SAAA;AACA;AACO,KAAA,SAAA;AACP;AACA;AACA;AACO,KAAA,QAAA;AACP;AACA;AACA;AACO,UAAA,SAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAA,WAAA;AACA,gBAAA,SAAA;AACA;AACA;AACO,UAAA,WAAA;AACP,UAAA,UAAA;AACA,aAAA,UAAA;AACA,WAAA,UAAA;AACA,YAAA,UAAA;AACA;AACO,KAAA,UAAA;AACA,UAAA,SAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,UAAA,WAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACO,UAAA,YAAA;AACP,WAAA,WAAA;AACA,SAAA,WAAA;AACA;AACA;AACA;AACA;AACO,UAAA,QAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAiBA;AACA;AACA;AACO,KAAA,eAAA;AACP;AACA;AACA;AACO,UAAA,gBAAA;AACP;AACA;AACA;AACA,iBAAA,eAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,UAAA,gBAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,UAAA,eAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,KAAA,cAAA;;ACjJP;AACA;AACA;AACO,cAAA,IAAA;AACP;AACA;AACA;AACA;AACA;AACA,2BAAA,SAAA,mCAAA,QAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,gBAAA,QAAA;AACA;AACA;AACA;AACA,iBAAA,SAAA;AACA;AACA;AACA;AACA,mBAAA,SAAA;AACA;AACA;AACA;AACA,qBAAA,SAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAA,SAAA;AACA;AACA;AACA;AACA,qBAAA,SAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,gBAAA,QAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,oCAAA,IAAA;AACA;AACA;AACA;AACA;AACA,yBAAA,IAAA;AACA;;ACxFA;AACA;AACA;AACO,cAAA,KAAA;AACP;AACA;AACA,2BAAA,SAAA,SAAA,YAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,kBAAA,SAAA;AACA;AACA;AACA;AACA,qBAAA,SAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAA,SAAA;AACA;AACA;AACA;AACA,qBAAA,SAAA;AACA;AACA;AACA;AACA,KAAA,MAAA,CAAA,QAAA,KAAA,SAAA,CAAmCA,IAAgB;AACnD;AACA;AACA;AACA,YAAA,SAAA,CAAsBA,IAAgB;AACtC;;AClDA;AACA;AACA;AACO,cAAA,SAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA,0BAAA,QAAA;AACA;AACA;AACA;AACA,oBAAA,QAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,uDAAA,IAAA;AACA;AACA;AACA;AACA,6BAAA,KAAA;AACA,+EAAA,KAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAA,GAAA,SAAA,IAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC5EA;AACA;AACA;AACA;AACO,cAAA,aAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA,+BAAA,aAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACrCA;AACA;AACA;AACO,cAAA,MAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,+BAAA,MAAA;AACA;AACA;AACA;AACA,4BAAA,MAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAAA,SAAA;AACA;AACA;AACA;AACA;AACA,uBAAA,SAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AClDA;AACA;AACA;AACA;AACO,cAAA,UAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,kBAAA,eAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,2CAAA,SAAA;AACA;AACA;AACA;AACA,4BAAA,eAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AClEA;AACA;AACA;AACO,cAAA,UAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,qCAAA,UAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAA,UAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,mDAAA,eAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACvFA;AACA;AACA;AACO,cAAA,QAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,mCAAA,OAAA,CAAA,QAAA;AACA;AACA;AACA;AACA,4BAAA,UAAA,GAAA,OAAA,CAAA,QAAA;AACA;AACA;AACA;AACA,qBAAA,QAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,yBAAA,aAAA;AACA;AACA;AACA;AACA,kBAAA,MAAA;AACA;AACA;AACA;AACA,yCAAA,SAAA;AACA;AACA;AACA;AACA,4CAAA,SAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,oDAAA,SAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAAA,gBAAA,GAAA,UAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,0BAAA,OAAA;AACA;AACA;AACA;AACA,gBAAA,OAAA,CAAA,UAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACtGA;AACA;AACA;AACA;AACA;AACO,cAAA,YAAA,uBAAA,WAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACO,cAAA,SAAA;AACP;AACA;AACA;AACA;AACA;AACO,cAAA,UAAA,qBAAA,YAAA;AACP;AACA;AACA;AACA;AACA;AACO,cAAA,OAAA,UAAA,YAAA;;;;"}