UNPKG

xlsx-populate

Version:

Excel XLSX parser/generator written in JavaScript with Node.js and browser support, jQuery/d3-style method chaining, and a focus on keeping existing workbook features and styles in tact.

866 lines (791 loc) 33.1 kB
"use strict"; const _ = require("lodash"); const Cell = require("./Cell"); const Row = require("./Row"); const Column = require("./Column"); const Range = require("./Range"); const Relationships = require("./Relationships"); const debug = require("./debug")("Sheet"); const xmlq = require("./xmlq"); const regexify = require("./regexify"); const addressConverter = require("./addressConverter"); const ArgHandler = require("./ArgHandler"); const colorIndexes = require("./colorIndexes"); // Order of the nodes as defined by the spec. const nodeOrder = [ "sheetPr", "dimension", "sheetViews", "sheetFormatPr", "cols", "sheetData", "sheetCalcPr", "sheetProtection", "protectedRanges", "scenarios", "autoFilter", "sortState", "dataConsolidate", "customSheetViews", "mergeCells", "phoneticPr", "conditionalFormatting", "dataValidations", "hyperlinks", "printOptions", "pageMargins", "pageSetup", "headerFooter", "rowBreaks", "colBreaks", "customProperties", "cellWatches", "ignoredErrors", "smartTags", "drawing", "drawingHF", "picture", "oleObjects", "controls", "webPublishItems", "tableParts", "extLst" ]; /** * A worksheet. */ class Sheet { // /** // * Creates a new instance of Sheet. // * @param {Workbook} workbook - The parent workbook. // * @param {{}} idNode - The sheet ID node (from the parent workbook). // * @param {{}} node - The sheet node. // * @param {{}} [relationshipsNode] - The optional sheet relationships node. // */ constructor(workbook, idNode, node, relationshipsNode) { debug("constructor(...)"); this._init(workbook, idNode, node, relationshipsNode); } /** * Gets a value indicating whether the sheet is the active sheet in the workbook. * @returns {boolean} True if active, false otherwise. *//** * Make the sheet the active sheet in the workkbok. * @param {boolean} active - Must be set to `true`. Deactivating directly is not supported. To deactivate, you should activate a different sheet instead. * @returns {Sheet} The sheet. */ active() { return new ArgHandler('Sheet.active') .case(() => { return this.workbook().activeSheet() === this; }) .case('boolean', active => { if (!active) throw new Error("Deactivating sheet directly not supported. Activate a different sheet instead."); this.workbook().activeSheet(this); return this; }) .handle(arguments); } /** * Get the active cell in the sheet. * @returns {Cell} The active cell. *//** * Set the active cell in the workbook. * @param {string|Cell} cell - The cell or address of cell to activate. * @returns {Sheet} The sheet. *//** * Set the active cell in the workbook by row and column. * @param {number} rowNumber - The row number of the cell. * @param {string|number} columnNameOrNumber - The column name or number of the cell. * @returns {Sheet} The sheet. */ activeCell() { debug("activeCell(%o)", arguments); const sheetViewNode = this._getOrCreateSheetViewNode(); let selectionNode = xmlq.findChild(sheetViewNode, "selection"); return new ArgHandler('Sheet.activeCell') .case(() => { const cellAddress = selectionNode ? selectionNode.attributes.activeCell : "A1"; return this.cell(cellAddress); }) .case(['number', '*'], (rowNumber, columnNameOrNumber) => { const cell = this.cell(rowNumber, columnNameOrNumber); return this.activeCell(cell); }) .case('*', cell => { if (!selectionNode) { selectionNode = { name: "selection", attributes: {}, children: [] }; xmlq.appendChild(sheetViewNode, selectionNode); } if (!(cell instanceof Cell)) cell = this.cell(cell); selectionNode.attributes.activeCell = selectionNode.attributes.sqref = cell.address(); return this; }) .handle(arguments); } /** * Gets the cell with the given address. * @param {string} address - The address of the cell. * @returns {Cell} The cell. *//** * Gets the cell with the given row and column numbers. * @param {number} rowNumber - The row number of the cell. * @param {string|number} columnNameOrNumber - The column name or number of the cell. * @returns {Cell} The cell. */ cell() { debug("cell(%o)", arguments); return new ArgHandler('Sheet.cell') .case('string', address => { const ref = addressConverter.fromAddress(address); if (ref.type !== 'cell') throw new Error('Sheet.cell: Invalid address.'); return this.row(ref.rowNumber).cell(ref.columnNumber); }) .case(['number', '*'], (rowNumber, columnNameOrNumber) => { return this.row(rowNumber).cell(columnNameOrNumber); }) .handle(arguments); } /** * Gets a column in the sheet. * @param {string|number} columnNameOrNumber - The name or number of the column. * @returns {Column} The column. */ column(columnNameOrNumber) { debug("column(%o)", arguments); const columnNumber = typeof columnNameOrNumber === "string" ? addressConverter.columnNameToNumber(columnNameOrNumber) : columnNameOrNumber; // If we're already created a column for this column number, return it. if (this._columns[columnNumber]) return this._columns[columnNumber]; // We need to create a new column, which requires a backing col node. There may already exist a node whose min/max cover our column. // First, see if there is an existing col node. const existingColNode = this._colNodes[columnNumber]; let colNode; if (existingColNode) { // If the existing node covered earlier columns than the new one, we need to have a col node to cover the min up to our new node. if (existingColNode.attributes.min < columnNumber) { // Clone the node and set the max to the column before our new col. const beforeColNode = _.cloneDeep(existingColNode); beforeColNode.attributes.max = columnNumber - 1; // Update the col nodes cache. for (let i = beforeColNode.attributes.min; i <= beforeColNode.attributes.max; i++) { this._colNodes[i] = beforeColNode; } } // Make a clone for the new column. Set the min/max to the column number and cache it. colNode = _.cloneDeep(existingColNode); colNode.attributes.min = columnNumber; colNode.attributes.max = columnNumber; this._colNodes[columnNumber] = colNode; // If the max of the existing node is greater than the nre one, create a col node for that too. if (existingColNode.attributes.max > columnNumber) { const afterColNode = _.cloneDeep(existingColNode); afterColNode.attributes.min = columnNumber + 1; for (let i = afterColNode.attributes.min; i <= afterColNode.attributes.max; i++) { this._colNodes[i] = afterColNode; } } } else { // The was no existing node so create a new one. colNode = { name: 'col', attributes: { min: columnNumber, max: columnNumber }, children: [] }; this._colNodes[columnNumber] = colNode; } // Create the new column and cache it. const column = new Column(this, colNode); this._columns[columnNumber] = column; return column; } /** * Gets a defined name scoped to the sheet. * @param {string} name - The defined name. * @returns {undefined|string|Cell|Range|Row|Column} What the defined name refers to or undefined if not found. Will return the string formula if not a Row, Column, Cell, or Range. *//** * Set a defined name scoped to the sheet. * @param {string} name - The defined name. * @param {string|Cell|Range|Row|Column} refersTo - What the name refers to. * @returns {Workbook} The workbook. */ definedName() { return new ArgHandler("Workbook.definedName") .case('string', name => { return this.workbook().scopedDefinedName(this, name); }) .case(['string', '*'], (name, refersTo) => { this.workbook().scopedDefinedName(this, name, refersTo); return this; }) .handle(arguments); } /** * Find the given pattern in the sheet and optionally replace it. * @param {string|RegExp} pattern - The pattern to look for. Providing a string will result in a case-insensitive substring search. Use a RegExp for more sophisticated searches. * @param {string|function} [replacement] - The text to replace or a String.replace callback function. If pattern is a string, all occurrences of the pattern in each cell will be replaced. * @returns {Array.<Cell>} The matching cells. */ find(pattern, replacement) { debug("find(%o)", arguments); pattern = regexify(pattern); let matches = []; this._rows.forEach(row => { if (!row) return; matches = matches.concat(row.find(pattern, replacement)); }); return matches; } /** * Gets a value indicating if the sheet is hidden or not. * @returns {boolean|string} True if hidden, false if visible, and 'very' if very hidden. *//** * Set whether the sheet is hidden or not. * @param {boolean|string} hidden - True to hide, false to show, and 'very' to make very hidden. * @returns {Sheet} The sheet. */ hidden() { debug("hidden(%o)", arguments); return new ArgHandler('Sheet.hidden') .case(() => { if (this._idNode.attributes.state === 'hidden') return true; if (this._idNode.attributes.state === 'veryHidden') return "very"; return false; }) .case('*', hidden => { if (hidden) { const visibleSheets = _.filter(this.workbook().sheets(), sheet => !sheet.hidden()); if (visibleSheets.length === 1 && visibleSheets[0] === this) { throw new Error("This sheet may not be hidden as a workbook must contain at least one visible sheet."); } // If activate, activate the first other visible sheet. if (this.active()) { const activeIndex = visibleSheets[0] === this ? 1 : 0; visibleSheets[activeIndex].active(true); } } if (hidden === 'very') this._idNode.attributes.state = 'veryHidden'; else if (hidden) this._idNode.attributes.state = 'hidden'; else delete this._idNode.attributes.state; return this; }) .handle(arguments); } /** * Move the sheet. * @param {number|string|Sheet} [indexOrBeforeSheet] The index to move the sheet to or the sheet (or name of sheet) to move this sheet before. Omit this argument to move to the end of the workbook. * @returns {Sheet} The sheet. */ move(indexOrBeforeSheet) { this.workbook().moveSheet(this, indexOrBeforeSheet); return this; } /** * Get the name of the sheet. * @returns {string} The sheet name. */ name() { debug("name(%o)", arguments); return this._idNode.attributes.name; } /** * Gets a range from the given range address. * @param {string} address - The range address (e.g. 'A1:B3'). * @returns {Range} The range. *//** * Gets a range from the given cells or cell addresses. * @param {string|Cell} startCell - The starting cell or cell address (e.g. 'A1'). * @param {string|Cell} endCell - The ending cell or cell address (e.g. 'B3'). * @returns {Range} The range. *//** * Gets a range from the given row numbers and column names or numbers. * @param {number} startRowNumber - The starting cell row number. * @param {string|number} startColumnNameOrNumber - The starting cell column name or number. * @param {number} endRowNumber - The ending cell row number. * @param {string|number} endColumnNameOrNumber - The ending cell column name or number. * @returns {Range} The range. */ range() { debug("range(%o)", arguments); return new ArgHandler('Sheet.range') .case('string', address => { const ref = addressConverter.fromAddress(address); if (ref.type !== 'range') throw new Error('Sheet.range: Invalid address'); return this.range(ref.startRowNumber, ref.startColumnNumber, ref.endRowNumber, ref.endColumnNumber); }) .case(['*', '*'], (startCell, endCell) => { if (typeof startCell === "string") startCell = this.cell(startCell); if (typeof endCell === "string") endCell = this.cell(endCell); return new Range(startCell, endCell); }) .case(['number', '*', 'number', '*'], (startRowNumber, startColumnNameOrNumber, endRowNumber, endColumnNameOrNumber) => { return this.range(this.cell(startRowNumber, startColumnNameOrNumber), this.cell(endRowNumber, endColumnNameOrNumber)); }) .handle(arguments); } /** * Gets the row with the given number. * @param {number} rowNumber - The row number. * @returns {Row} The row with the given number. */ row(rowNumber) { debug("row(%o)", arguments); if (this._rows[rowNumber]) return this._rows[rowNumber]; const rowNode = { name: 'row', attributes: { r: rowNumber }, children: [] }; const row = new Row(this, rowNode); this._rows[rowNumber] = row; return row; } /** * Get the tab color. (See style [Color](#color).) * @returns {undefined|Color} The color or undefined if not set. *//** * Sets the tab color. (See style [Color](#color).) * @returns {Color|string|number} color - Color of the tab. If string, will set an RGB color. If number, will set a theme color. */ tabColor() { debug("tabColor(%o)", arguments); return new ArgHandler("Sheet.tabColor") .case(() => { const tabColorNode = xmlq.findChild(this._sheetPrNode, "tabColor"); if (!tabColorNode) return; const color = {}; if (tabColorNode.attributes.hasOwnProperty('rgb')) color.rgb = tabColorNode.attributes.rgb; else if (tabColorNode.attributes.hasOwnProperty('theme')) color.theme = tabColorNode.attributes.theme; else if (tabColorNode.attributes.hasOwnProperty('indexed')) color.rgb = colorIndexes[tabColorNode.attributes.indexed]; if (tabColorNode.attributes.hasOwnProperty('tint')) color.tint = tabColorNode.attributes.tint; return color; }) .case("string", rgb => this.tabColor({ rgb })) .case("integer", theme => this.tabColor({ theme })) .case("nil", () => { xmlq.removeChild(this._sheetPrNode, "tabColor"); return this; }) .case("object", color => { const tabColorNode = xmlq.appendChildIfNotFound(this._sheetPrNode, "tabColor"); xmlq.setAttributes(tabColorNode, { rgb: color.rgb && color.rgb.toUpperCase(), indexed: null, theme: color.theme, tint: color.tint }); return this; }) .handle(arguments); } /** * Gets a value indicating whether this sheet is selected. * @returns {boolean} True if selected, false if not. */ /** * Sets whether this sheet is selected. * @param {boolean} selected - True to select, false to deselected. * @returns {Sheet} The sheet. */ tabSelected() { debug("tabSelected(%o)", arguments); const sheetViewNode = this._getOrCreateSheetViewNode(); return new ArgHandler('Sheet.selected') .case(() => { return sheetViewNode.attributes.tabSelected === 1; }) .case('boolean', selected => { if (selected) sheetViewNode.attributes.tabSelected = 1; else delete sheetViewNode.attributes.tabSelected; return this; }) .handle(arguments); } /** * Get the range of cells in the sheet that have contained a value or style at any point. Useful for extracting the entire sheet contents. * @returns {Range|undefined} The used range or undefined if no cells in the sheet are used. */ usedRange() { debug("usedRange(%o)", arguments); const minRowNumber = _.findIndex(this._rows); const maxRowNumber = this._rows.length - 1; let minColumnNumber = 0; let maxColumnNumber = 0; for (let i = 0; i < this._rows.length; i++) { const row = this._rows[i]; if (!row) continue; const minUsedColumnNumber = row.minUsedColumnNumber(); const maxUsedColumnNumber = row.maxUsedColumnNumber(); if (minUsedColumnNumber > 0 && (!minColumnNumber || minUsedColumnNumber < minColumnNumber)) minColumnNumber = minUsedColumnNumber; if (maxUsedColumnNumber > 0 && (!maxColumnNumber || maxUsedColumnNumber > maxColumnNumber)) maxColumnNumber = maxUsedColumnNumber; } // Return undefined if nothing in the sheet is used. if (minRowNumber <= 0 || minColumnNumber <= 0 || maxRowNumber <= 0 || maxColumnNumber <= 0) return; return this.range(minRowNumber, minColumnNumber, maxRowNumber, maxColumnNumber); } /** * Gets the parent workbook. * @returns {Workbook} The parent workbook. */ workbook() { debug("workbook(%o)", arguments); return this._workbook; } /** * Clear cells that are using a given shared formula ID. * @param {number} sharedFormulaId - The shared formula ID. * @returns {undefined} * @ignore */ clearCellsUsingSharedFormula(sharedFormulaId) { debug("clearCellsUsingSharedFormula(%o)", arguments); this._rows.forEach(row => { if (!row) return; row.clearCellsUsingSharedFormula(sharedFormulaId); }); } /** * Get an existing column style ID. * @param {number} columnNumber - The column number. * @returns {undefined|number} The style ID. * @ignore */ existingColumnStyleId(columnNumber) { debug("existingColumnStyleId(%o)", arguments); const colNode = this._colNodes[columnNumber]; return colNode && colNode.attributes.style; } /** * Get the hyperlink attached to the cell with the given address. * @param {string} address - The address of the hyperlinked cell. * @returns {string|undefined} The hyperlink or undefined if not set. * @ignore *//** * Set the hyperlink attached to the cell with the given address. * @param {string} address - The address to of the hyperlinked cell. * @param {boolean} hyperlink - The hyperlink to set or undefined to clear. * @returns {Sheet} The sheet. * @ignore */ hyperlink() { debug("hyperlink(%o)", arguments); return new ArgHandler('Sheet.hyperlink') .case('string', address => { const hyperlinkNode = this._hyperlinks[address]; if (!hyperlinkNode) return; const relationship = this._relationships.findById(hyperlinkNode.attributes['r:id']); return relationship && relationship.attributes.Target; }) .case(['string', 'nil'], address => { delete this._hyperlinks[address]; return this; }) .case(['string', 'string'], (address, hyperlink) => { const relationship = this._relationships.add("hyperlink", hyperlink, "External"); this._hyperlinks[address] = { name: 'hyperlink', attributes: { ref: address, 'r:id': relationship.attributes.Id }, children: [] }; return this; }) .handle(arguments); } /** * Increment and return the max shared formula ID. * @returns {number} The new max shared formula ID. * @ignore */ incrementMaxSharedFormulaId() { debug("incrementMaxSharedFormulaId(%o)", arguments); return ++this._maxSharedFormulaId; } /** * Get a value indicating whether the cells in the given address are merged. * @param {string} address - The address to check. * @returns {boolean} True if merged, false if not merged. * @ignore *//** * Merge/unmerge cells by adding/removing a mergeCell entry. * @param {string} address - The address to merge. * @param {boolean} merged - True to merge, false to unmerge. * @returns {Sheet} The sheet. * @ignore */ merged() { debug("merged(%o)", arguments); return new ArgHandler('Sheet.merge') .case('string', address => { return this._mergeCells.hasOwnProperty(address); }) .case(['string', '*'], (address, merge) => { if (merge) { this._mergeCells[address] = { name: 'mergeCell', attributes: { ref: address }, children: [] }; } else { delete this._mergeCells[address]; } return this; }) .handle(arguments); } /** * Convert the sheet to an object. * @returns {{}} The object form. * @ignore */ toObject() { debug("toObject(%o)", arguments); // Shallow clone the node so we don't have to remove these children later if they don't belong. const node = _.clone(this._node); node.children = node.children.slice(); // Rows must be in order. this._sheetDataNode.children = []; this._rows.forEach(row => { if (row) this._sheetDataNode.children.push(row.toObject()); }); // Add the columns if needed. this._colsNode.children = _.filter(this._colNodes, (colNode, i) => { // Columns should only be present if they have attributes other than min/max. return colNode && i === colNode.attributes.min && Object.keys(colNode.attributes).length > 2; }); if (this._colsNode.children.length) { xmlq.insertInOrder(node, this._colsNode, nodeOrder); } // Add the hyperlinks if needed. this._hyperlinksNode.children = _.values(this._hyperlinks); if (this._hyperlinksNode.children.length) { xmlq.insertInOrder(node, this._hyperlinksNode, nodeOrder); } // Add the merge cells if needed. this._mergeCellsNode.children = _.values(this._mergeCells); if (this._mergeCellsNode.children.length) { xmlq.insertInOrder(node, this._mergeCellsNode, nodeOrder); } return { id: this._idNode, sheet: node, relationships: this._relationships.toObject() }; } /** * Update the max shared formula ID to the given value if greater than current. * @param {number} sharedFormulaId - The new shared formula ID. * @returns {undefined} * @ignore */ updateMaxSharedFormulaId(sharedFormulaId) { debug("updateMaxSharedFormulaId(%o)", arguments); if (sharedFormulaId > this._maxSharedFormulaId) { this._maxSharedFormulaId = sharedFormulaId; } } /** * Get the sheet view node if it exists or create it if it doesn't. * @returns {{}} The sheet view node. * @private */ _getOrCreateSheetViewNode() { let sheetViewsNode = xmlq.findChild(this._node, "sheetViews"); if (!sheetViewsNode) { sheetViewsNode = { name: "sheetViews", attributes: {}, children: [{ name: "sheetView", attributes: { workbookViewId: 0 }, children: [] }] }; xmlq.insertInOrder(this._node, sheetViewsNode, nodeOrder); } return xmlq.findChild(sheetViewsNode, "sheetView"); } /** * Initializes the sheet. * @param {Workbook} workbook - The parent workbook. * @param {{}} idNode - The sheet ID node (from the parent workbook). * @param {{}} node - The sheet node. * @param {{}} [relationshipsNode] - The optional sheet relationships node. * @returns {undefined} * @private */ _init(workbook, idNode, node, relationshipsNode) { debug("_init(%o)", arguments); if (!node) { node = { name: "worksheet", attributes: { xmlns: "http://schemas.openxmlformats.org/spreadsheetml/2006/main", 'xmlns:r': "http://schemas.openxmlformats.org/officeDocument/2006/relationships", 'xmlns:mc': "http://schemas.openxmlformats.org/markup-compatibility/2006", 'mc:Ignorable': "x14ac", 'xmlns:x14ac': "http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac" }, children: [{ name: "sheetData", attributes: {}, children: [] }] }; } this._workbook = workbook; this._idNode = idNode; this._node = node; this._maxSharedFormulaId = -1; this._mergeCells = {}; this._hyperlinks = {}; // Create the relationships. this._relationships = new Relationships(relationshipsNode); // Create the rows. this._rows = []; this._sheetDataNode = xmlq.findChild(this._node, "sheetData"); this._sheetDataNode.children.forEach(rowNode => { const row = new Row(this, rowNode); this._rows[row.rowNumber()] = row; }); // Create the columns node. this._columns = []; this._colsNode = xmlq.findChild(this._node, "cols"); if (this._colsNode) { xmlq.removeChild(this._node, this._colNodes); } else { this._colsNode = { name: 'cols', attributes: {}, children: [] }; } // Cache the col nodes. this._colNodes = []; _.forEach(this._colsNode.children, colNode => { const min = colNode.attributes.min; const max = colNode.attributes.max; for (let i = min; i <= max; i++) { this._colNodes[i] = colNode; } }); // Create the sheet properties node. this._sheetPrNode = xmlq.findChild(this._node, "sheetPr"); if (!this._sheetPrNode) { this._sheetPrNode = { name: 'sheetPr', attributes: {}, children: [] }; xmlq.insertInOrder(this._node, this._sheetPrNode, nodeOrder); } // Create the merge cells. this._mergeCellsNode = xmlq.findChild(this._node, "mergeCells"); if (this._mergeCellsNode) { xmlq.removeChild(this._node, this._mergeCellsNode); } else { this._mergeCellsNode = { name: 'mergeCells', attributes: {}, children: [] }; } const mergeCellNodes = this._mergeCellsNode.children; this._mergeCellsNode.children = []; mergeCellNodes.forEach(mergeCellNode => { this._mergeCells[mergeCellNode.attributes.ref] = mergeCellNode; }); // Create the hyperlinks. this._hyperlinksNode = xmlq.findChild(this._node, "hyperlinks"); if (this._hyperlinksNode) { xmlq.removeChild(this._node, this._hyperlinksNode); } else { this._hyperlinksNode = { name: 'hyperlinks', attributes: {}, children: [] }; } const hyperlinkNodes = this._hyperlinksNode.children; this._hyperlinksNode.children = []; hyperlinkNodes.forEach(hyperlinkNode => { this._hyperlinks[hyperlinkNode.attributes.ref] = hyperlinkNode; }); } } module.exports = Sheet; /* xl/worksheets/sheetN.xml <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="x14ac" xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac"> <dimension ref="A1:I8"/> <sheetViews> <sheetView tabSelected="1" workbookViewId="0"> <selection activeCell="A9" sqref="A9"/> </sheetView> </sheetViews> <sheetFormatPr defaultRowHeight="15" x14ac:dyDescent="0.25"/> <sheetData> <row r="1" spans="1:9" x14ac:dyDescent="0.25"> <c r="A1" t="s"> <v>0</v> </c> </row> <row r="2" spans="1:9" x14ac:dyDescent="0.25"> <c r="A2" t="str"> <f>A1</f><v>Foo</v> </c><c r="B2"> <f t="shared" ref="B2:I2" si="0">B1</f><v>0</v> </c><c r="C2"> <f t="shared" si="0"/> <v>0</v> </c><c r="D2"> <f t="shared" si="0"/> <v>0</v> </c><c r="E2"> <f t="shared" si="0"/> <v>0</v> </c><c r="F2"> <f t="shared" si="0"/> <v>0</v> </c><c r="G2"> <f t="shared" si="0"/> <v>0</v> </c><c r="H2"> <f t="shared" si="0"/> <v>0</v> </c><c r="I2"> <f t="shared" si="0"/> <v>0</v> </c> </row> <row r="3" spans="1:9" x14ac:dyDescent="0.25"> <c r="A3" t="s"> <v>1</v> </c><c r="B3" t="s"> <v>1</v> </c><c r="C3" t="s"> <v>1</v> </c><c r="D3" t="s"> <v>1</v> </c><c r="E3" t="s"> <v>1</v> </c><c r="F3" t="s"> <v>1</v> </c><c r="G3" t="s"> <v>1</v> </c><c r="H3" t="s"> <v>1</v> </c><c r="I3" t="s"> <v>1</v> </c> </row> <row r="4" spans="1:9" x14ac:dyDescent="0.25"> <c r="A4"> <v>1</v> </c><c r="B4"> <v>2</v> </c><c r="C4"> <v>3</v> </c><c r="D4"> <v>4</v> </c><c r="E4"> <v>5</v> </c><c r="F4"> <v>6</v> </c><c r="G4"> <v>7</v> </c><c r="H4"> <v>8</v> </c><c r="I4"> <v>9</v> </c> </row> <row r="6" spans="1:9" x14ac:dyDescent="0.25"> <c r="A6" s="1" t="s"> <v>2</v> </c><c r="B6" s="1"/> <c r="C6" s="1"/> </row> <row r="7" spans="1:9" x14ac:dyDescent="0.25"> <c r="A7" t="s"> <v>0</v> </c> </row> <row r="8" spans="1:9" x14ac:dyDescent="0.25"> <c r="A8" t="s"> <v>3</v> </c> </row> </sheetData> <mergeCells count="1"> <mergeCell ref="A6:C6"/> </mergeCells> <pageMargins left="0.7" right="0.7" top="0.75" bottom="0.75" header="0.3" footer="0.3"/> <pageSetup orientation="portrait" verticalDpi="300" r:id="rId1"/> </worksheet> */