| /******************************************************************************* | |
| * You may amend and distribute as you like, but don't remove this header! | |
| * | |
| * EPPlus provides server-side generation of Excel 2007/2010 spreadsheets. | |
| * See http://www.codeplex.com/EPPlus for details. | |
| * | |
| * Copyright (C) 2011 Jan Källman | |
| * | |
| * This library is free software; you can redistribute it and/or | |
| * modify it under the terms of the GNU Lesser General Public | |
| * License as published by the Free Software Foundation; either | |
| * version 2.1 of the License, or (at your option) any later version. | |
| * This library is distributed in the hope that it will be useful, | |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | |
| * See the GNU Lesser General Public License for more details. | |
| * | |
| * The GNU Lesser General Public License can be viewed at http://www.opensource.org/licenses/lgpl-license.php | |
| * If you unfamiliar with this license or have questions about it, here is an http://www.gnu.org/licenses/gpl-faq.html | |
| * | |
| * All code and executables are provided "as is" with no warranty either express or implied. | |
| * The author accepts no liability for any damage or loss of business that this product may cause. | |
| * | |
| * Code change notes: | |
| * | |
| * Author Change Date | |
| * ****************************************************************************** | |
| * Jan Källman Initial Release 2011-11-02 | |
| * Jan Källman Total rewrite 2010-03-01 | |
| * Jan Källman License changed GPL-->LGPL 2011-12-27 | |
| *******************************************************************************/ | |
| using System; | |
| using System.Collections; | |
| using System.Collections.Generic; | |
| using System.Collections.Immutable; | |
| using System.ComponentModel; | |
| using System.Globalization; | |
| using System.IO; | |
| using System.Security; | |
| using System.Text; | |
| using System.Text.RegularExpressions; | |
| using System.Xml; | |
| namespace AppsheetEpplus; | |
| /// <summary> | |
| /// Worksheet hidden enumeration | |
| /// </summary> | |
| public enum eWorkSheetHidden { | |
| /// <summary> | |
| /// The worksheet is visible | |
| /// </summary> | |
| Visible, | |
| /// <summary> | |
| /// The worksheet is hidden but can be shown by the user via the user interface | |
| /// </summary> | |
| Hidden, | |
| /// <summary> | |
| /// The worksheet is hidden and cannot be shown by the user via the user interface | |
| /// </summary> | |
| VeryHidden, | |
| } | |
| [Flags] | |
| internal enum CellFlags { | |
| //Merged = 0x1, | |
| RichText = 0x2, | |
| SharedFormula = 0x4, | |
| ArrayFormula = 0x8, | |
| } | |
| /// <summary> | |
| /// Represents an Excel Chartsheet and provides access to its properties and methods | |
| /// </summary> | |
| public class ExcelChartsheet : ExcelWorksheet { | |
| public ExcelChartsheet( | |
| XmlNamespaceManager ns, | |
| ExcelPackage pck, | |
| ExcelWorkbook workbook, | |
| Uri worksheetUri, | |
| string name, | |
| int sheetId, | |
| int positionId, | |
| eWorkSheetHidden hidden) | |
| : base(ns, pck, workbook, worksheetUri, name, sheetId, positionId, hidden) {} | |
| } | |
| /// <summary> | |
| /// Represents an Excel worksheet and provides access to its properties and methods | |
| /// </summary> | |
| public class ExcelWorksheet : XmlHelper, IEqualityComparer<ExcelWorksheet> { | |
| internal class Formulas { | |
| public Formulas(ISourceCodeTokenizer tokenizer) { | |
| _tokenizer = tokenizer; | |
| } | |
| public static string RemoveDummyFunction(string formula) { | |
| const string dummyFunctionConcatenate = "\"&\""; | |
| const string dummyFunctionPrefix = "IFERROR(__xludf.DUMMYFUNCTION(\""; | |
| const string dummyFunctionSuffix = "\"),"; | |
| if (string.IsNullOrEmpty(formula)) { | |
| return formula; | |
| } | |
| // Look for Prefix | |
| if (!formula.StartsWith(dummyFunctionPrefix)) { | |
| return formula; | |
| } | |
| // Look for Suffix | |
| int index = formula.LastIndexOf(dummyFunctionSuffix); | |
| if (index < 0) { | |
| return formula; | |
| } | |
| // Trim Suffix | |
| formula = formula.Substring(0, index); | |
| // Trim Prefix | |
| formula = formula.Replace(dummyFunctionPrefix, ""); | |
| // Remove string concatentations from long formulas. | |
| // Google break the quoted string into 254 character segments which are concatenated. | |
| if (formula.Length >= 254) { | |
| formula = formula.Replace(dummyFunctionConcatenate, ""); | |
| } | |
| // Replace doubled quotes with single quote | |
| formula = formula.Replace("\"\"", "\""); | |
| // Return formula | |
| return formula; | |
| } | |
| private readonly ISourceCodeTokenizer _tokenizer; | |
| internal int Index { get; set; } | |
| internal string Address { get; set; } | |
| internal bool IsArray { get; set; } | |
| public string Formula { get; set; } | |
| public int StartRow { get; set; } | |
| public int StartCol { get; set; } | |
| private IEnumerable<Token> Tokens { get; set; } | |
| internal string GetFormula(int row, int column, string worksheet) { | |
| if ((StartRow == row && StartCol == column) || IsArray) { | |
| return RemoveDummyFunction(Formula); | |
| } | |
| if (Tokens == null) { | |
| Tokens = _tokenizer.Tokenize(RemoveDummyFunction(Formula), worksheet); | |
| } | |
| string f = ""; | |
| foreach (var token in Tokens) { | |
| if (token.TokenType == TokenType.ExcelAddress) { | |
| var a = new ExcelFormulaAddress(token.Value); | |
| f += a.GetOffset(row - StartRow, column - StartCol); | |
| } else { | |
| f += token.Value; | |
| } | |
| } | |
| return f; | |
| } | |
| } | |
| /// <summary> | |
| /// Collection containing merged cell addresses | |
| /// </summary> | |
| public class MergeCellsCollection : IEnumerable<string> { | |
| internal MergeCellsCollection() {} | |
| internal readonly CellStore<int> _cells = new(); | |
| private readonly List<string> _list = []; | |
| internal List<string> List => _list; | |
| public string this[int row, int column] { | |
| get { | |
| int ix = -1; | |
| if (_cells.Exists(row, column, ref ix) | |
| && ix >= 0 | |
| && ix | |
| < List.Count) //Fixes issue 15075 | |
| { | |
| return List[ix]; | |
| } | |
| return null; | |
| } | |
| } | |
| public string this[int index] => _list[index]; | |
| internal void Add(ExcelAddressBase address, bool doValidate) { | |
| //Validate | |
| if (doValidate && Validate(address) == false) { | |
| throw (new ArgumentException("Can't merge and already merged range")); | |
| } | |
| var ix = _list.Count; | |
| _list.Add(address.Address); | |
| SetIndex(address, ix); | |
| } | |
| private bool Validate(ExcelAddressBase address) { | |
| int ix = 0; | |
| if (_cells.Exists(address._fromRow, address._fromCol, ref ix)) { | |
| if (ix >= 0 && ix < _list.Count && _list[ix] != null && address.Address == _list[ix]) { | |
| return true; | |
| } | |
| return false; | |
| } | |
| var cse = new CellsStoreEnumerator<int>( | |
| _cells, | |
| address._fromRow, | |
| address._fromCol, | |
| address._toRow, | |
| address._toCol); | |
| //cells | |
| while (cse.Next()) { | |
| return false; | |
| } | |
| //Entire column | |
| cse = new(_cells, 0, address._fromCol, 0, address._toCol); | |
| while (cse.Next()) { | |
| return false; | |
| } | |
| //Entire row | |
| cse = new(_cells, address._fromRow, 0, address._toRow, 0); | |
| while (cse.Next()) { | |
| return false; | |
| } | |
| return true; | |
| } | |
| internal void SetIndex(ExcelAddressBase address, int ix) { | |
| if (address._fromRow == 1 | |
| && address._toRow | |
| == ExcelPackage.MaxRows) //Entire row | |
| { | |
| for (int col = address._fromCol; col <= address._toCol; col++) { | |
| _cells.SetValue(0, col, ix); | |
| } | |
| } else if (address._fromCol == 1 | |
| && address._toCol | |
| == ExcelPackage.MaxColumns) //Entire row | |
| { | |
| for (int row = address._fromRow; row <= address._toRow; row++) { | |
| _cells.SetValue(row, 0, ix); | |
| } | |
| } else { | |
| for (int col = address._fromCol; col <= address._toCol; col++) { | |
| for (int row = address._fromRow; row <= address._toRow; row++) { | |
| _cells.SetValue(row, col, ix); | |
| } | |
| } | |
| } | |
| } | |
| public int Count => _list.Count; | |
| internal void Remove(string item) { | |
| _list.Remove(item); | |
| } | |
| public IEnumerator<string> GetEnumerator() { | |
| return _list.GetEnumerator(); | |
| } | |
| IEnumerator IEnumerable.GetEnumerator() { | |
| return _list.GetEnumerator(); | |
| } | |
| internal void Clear(ExcelAddressBase destination) { | |
| var cse = new CellsStoreEnumerator<int>( | |
| _cells, | |
| destination._fromRow, | |
| destination._fromCol, | |
| destination._toRow, | |
| destination._toCol); | |
| var used = new HashSet<int>(); | |
| while (cse.Next()) { | |
| var v = cse.Value; | |
| if (!used.Contains(v) && _list[v] != null) { | |
| var adr = new ExcelAddressBase(_list[v]); | |
| if (!(destination.Collide(adr) == ExcelAddressBase.eAddressCollition.Inside | |
| || destination.Collide(adr) == ExcelAddressBase.eAddressCollition.Equal)) { | |
| throw (new InvalidOperationException( | |
| string.Format( | |
| "Can't delete merged cells. A range is partly merged with the deleted range. {0}", | |
| adr._address))); | |
| } | |
| used.Add(v); | |
| } | |
| } | |
| _cells.Clear( | |
| destination._fromRow, | |
| destination._fromCol, | |
| destination._toRow - destination._fromRow + 1, | |
| destination._toCol - destination._fromCol + 1); | |
| foreach (var i in used) { | |
| _list[i] = null; | |
| } | |
| } | |
| } | |
| internal readonly CellStore<object> _values = new(); | |
| internal readonly CellStore<string> _types = new(); | |
| internal readonly CellStore<int> _styles = new(); | |
| internal readonly CellStore<object> _formulas = new(); | |
| internal readonly FlagCellStore _flags = new(); | |
| internal CellStore<List<Token>> _formulaTokens; | |
| internal readonly CellStore<Uri> _hyperLinks = new(); | |
| internal readonly CellStore<ExcelComment> _commentsStore = new(); | |
| internal readonly Dictionary<int, Formulas> _sharedFormulas = new(); | |
| internal readonly ExcelPackage _package; | |
| private readonly ExcelWorkbook _workbook; | |
| private ExcelWorksheetView _sheetView; | |
| internal static ImmutableArray<string> WorksheetSchemaNodeOrder = [ | |
| "sheetPr", | |
| "tabColor", | |
| "outlinePr", | |
| "pageSetUpPr", | |
| "dimension", | |
| "sheetViews", | |
| "sheetFormatPr", | |
| "cols", | |
| "sheetData", | |
| "sheetProtection", | |
| "protectedRanges", | |
| "scenarios", | |
| "autoFilter", | |
| "sortState", | |
| "dataConsolidate", | |
| "customSheetViews", | |
| "customSheetViews", | |
| "mergeCells", | |
| "phoneticPr", | |
| "conditionalFormatting", | |
| "dataValidations", | |
| "hyperlinks", | |
| "printOptions", | |
| "pageMargins", | |
| "pageSetup", | |
| "headerFooter", | |
| "linePrint", | |
| "rowBreaks", | |
| "colBreaks", | |
| "customProperties", | |
| "cellWatches", | |
| "ignoredErrors", | |
| "smartTags", | |
| "drawing", | |
| "legacyDrawing", | |
| "legacyDrawingHF", | |
| "picture", | |
| "oleObjects", | |
| "activeXControls", | |
| "webPublishItems", | |
| "tableParts", | |
| "extLst", | |
| ]; | |
| protected override ImmutableArray<string> SchemaNodeOrder => WorksheetSchemaNodeOrder; | |
| internal ExcelWorksheet( | |
| XmlNamespaceManager ns, | |
| ExcelPackage excelPackage, | |
| ExcelWorkbook workbook, | |
| Uri worksheetUri, | |
| string name, | |
| int sheetId, | |
| int positionId, | |
| eWorkSheetHidden hidden) | |
| : base(ns, null) { | |
| _workbook = workbook; | |
| _package = excelPackage; | |
| _names = new(Workbook, this); | |
| Hidden = hidden; | |
| Name = name; | |
| PositionID = positionId; | |
| SheetID = sheetId; | |
| Part = _package.Package.GetPart(worksheetUri); | |
| Part.SaveHandler = SaveHandler; | |
| // First Columns, rows, cells, mergecells, hyperlinks and pagebreakes are loaded from an | |
| // XmlTextReader to optimize speed. | |
| using var stream = Part.GetStream(); | |
| var xr = new XmlTextReader(stream); | |
| xr.ProhibitDtd = true; | |
| xr.WhitespaceHandling = WhitespaceHandling.None; | |
| LoadColumns(xr); //columnXml | |
| long start = stream.Position; | |
| LoadCells(xr); | |
| var nextElementLength = GetAttributeLength(xr); | |
| long end = stream.Position - nextElementLength; | |
| LoadMergeCells(xr); | |
| LoadHyperLinks(xr); | |
| LoadRowPageBreakes(xr); | |
| LoadColPageBreakes(xr); | |
| // Then the rest of the XML is extracted and loaded into the WorksheetXml document. | |
| stream.Seek(0, SeekOrigin.Begin); | |
| var xml = GetWorkSheetXml(stream, start, end, out var encoding); | |
| //first char is invalid sometimes?? | |
| if (xml[0] != '<') { | |
| LoadXmlSafe(WorksheetXml, xml.Substring(1, xml.Length - 1), encoding); | |
| } else { | |
| LoadXmlSafe(WorksheetXml, xml, encoding); | |
| } | |
| ClearNodes(); | |
| TopNode = WorksheetXml.DocumentElement; | |
| } | |
| /// <summary> | |
| /// The Zip.ZipPackagePart for the worksheet within the package | |
| /// </summary> | |
| internal ZipPackagePart Part { get; } | |
| /// <summary> | |
| /// The unique identifier for the worksheet. | |
| /// </summary> | |
| internal int SheetID { get; } | |
| /// <summary> | |
| /// The position of the worksheet. | |
| /// </summary> | |
| internal int PositionID { get; } | |
| /// <summary> | |
| /// Address for autofilter | |
| /// <seealso cref="ExcelRangeBase.AutoFilter" /> | |
| /// </summary> | |
| public ExcelAddressBase AutoFilterAddress { | |
| get { | |
| CheckSheetType(); | |
| string address = GetXmlNodeString("d:autoFilter/@ref"); | |
| if (address == "") { | |
| return null; | |
| } | |
| return new(address); | |
| } | |
| internal set { | |
| CheckSheetType(); | |
| SetXmlNodeString("d:autoFilter/@ref", value.Address); | |
| } | |
| } | |
| internal void CheckSheetType() { | |
| if (this is ExcelChartsheet) { | |
| throw (new NotSupportedException( | |
| "This property or method is not supported for a Chartsheet")); | |
| } | |
| } | |
| /// <summary> | |
| /// Returns a ExcelWorksheetView object that allows you to set the view state properties of the worksheet | |
| /// </summary> | |
| public ExcelWorksheetView View { | |
| get { | |
| if (_sheetView == null) { | |
| XmlNode node = TopNode.SelectSingleNode("d:sheetViews/d:sheetView", NameSpaceManager); | |
| if (node == null) { | |
| CreateNode("d:sheetViews/d:sheetView"); //this one shouls always exist. but check anyway | |
| node = TopNode.SelectSingleNode("d:sheetViews/d:sheetView", NameSpaceManager); | |
| } | |
| _sheetView = new(NameSpaceManager, node, this); | |
| } | |
| return (_sheetView); | |
| } | |
| } | |
| /// <summary> | |
| /// The worksheet's display name as it appears on the tab | |
| /// </summary> | |
| public string Name { get; } | |
| private readonly ExcelNamedRangeCollection _names; | |
| /// <summary> | |
| /// Provides access to named ranges | |
| /// </summary> | |
| public ExcelNamedRangeCollection Names { | |
| get { | |
| CheckSheetType(); | |
| return _names; | |
| } | |
| } | |
| /// <summary> | |
| /// Indicates if the worksheet is hidden in the workbook | |
| /// </summary> | |
| public eWorkSheetHidden Hidden { | |
| get { | |
| string state = _workbook.GetXmlNodeString( | |
| string.Format("d:sheets/d:sheet[@sheetId={0}]/@state", SheetID)); | |
| if (state == "hidden") { | |
| return eWorkSheetHidden.Hidden; | |
| } | |
| if (state == "veryHidden") { | |
| return eWorkSheetHidden.VeryHidden; | |
| } | |
| return eWorkSheetHidden.Visible; | |
| } | |
| set { | |
| if (value == eWorkSheetHidden.Visible) { | |
| _workbook.DeleteNode(string.Format("d:sheets/d:sheet[@sheetId={0}]/@state", SheetID)); | |
| } else { | |
| string v; | |
| v = value.ToString(); | |
| v = v.Substring(0, 1).ToLower(CultureInfo.InvariantCulture) + v.Substring(1); | |
| _workbook.SetXmlNodeString( | |
| string.Format("d:sheets/d:sheet[@sheetId={0}]/@state", SheetID), | |
| v); | |
| } | |
| } | |
| } | |
| private double _defaultRowHeight = double.NaN; | |
| /// <summary> | |
| /// Get/set the default height of all rows in the worksheet | |
| /// </summary> | |
| public double DefaultRowHeight { | |
| get { | |
| CheckSheetType(); | |
| if (double.IsNaN(_defaultRowHeight)) { | |
| _defaultRowHeight = GetXmlNodeDouble("d:sheetFormatPr/@defaultRowHeight"); | |
| if (double.IsNaN(_defaultRowHeight)) { | |
| _defaultRowHeight = 15; // Excel default height | |
| } | |
| } | |
| return _defaultRowHeight; | |
| } | |
| set { | |
| CheckSheetType(); | |
| _defaultRowHeight = value; | |
| SetXmlNodeString( | |
| "d:sheetFormatPr/@defaultRowHeight", | |
| value.ToString(CultureInfo.InvariantCulture)); | |
| SetXmlNodeBool("d:sheetFormatPr/@customHeight", value != 15); | |
| if (double.IsNaN(GetXmlNodeDouble("d:sheetFormatPr/@defaultColWidth"))) { | |
| DefaultColWidth = 9.140625; | |
| } | |
| } | |
| } | |
| /// <summary> | |
| /// Get/set the default width of all rows in the worksheet | |
| /// </summary> | |
| public double DefaultColWidth { | |
| get { | |
| CheckSheetType(); | |
| double ret = GetXmlNodeDouble("d:sheetFormatPr/@defaultColWidth"); | |
| if (double.IsNaN(ret)) { | |
| ret = 9.140625; // Excel's default width | |
| } | |
| return ret; | |
| } | |
| set { | |
| CheckSheetType(); | |
| SetXmlNodeString( | |
| "d:sheetFormatPr/@defaultColWidth", | |
| value.ToString(CultureInfo.InvariantCulture)); | |
| if (double.IsNaN(GetXmlNodeDouble("d:sheetFormatPr/@defaultRowHeight"))) { | |
| DefaultRowHeight = 15; | |
| } | |
| } | |
| } | |
| /** <outlinePr applyStyles="1" summaryBelow="0" summaryRight="0" /> **/ | |
| private const string _outLineSummaryBelowPath = "d:sheetPr/d:outlinePr/@summaryBelow"; | |
| /// <summary> | |
| /// Summary rows below details | |
| /// </summary> | |
| public bool OutLineSummaryBelow { | |
| get { | |
| CheckSheetType(); | |
| return GetXmlNodeBool(_outLineSummaryBelowPath); | |
| } | |
| set { | |
| CheckSheetType(); | |
| SetXmlNodeString(_outLineSummaryBelowPath, value ? "1" : "0"); | |
| } | |
| } | |
| private const string _outLineSummaryRightPath = "d:sheetPr/d:outlinePr/@summaryRight"; | |
| /// <summary> | |
| /// Summary rows to right of details | |
| /// </summary> | |
| public bool OutLineSummaryRight { | |
| get { | |
| CheckSheetType(); | |
| return GetXmlNodeBool(_outLineSummaryRightPath); | |
| } | |
| set { | |
| CheckSheetType(); | |
| SetXmlNodeString(_outLineSummaryRightPath, value ? "1" : "0"); | |
| } | |
| } | |
| private const string _outLineApplyStylePath = "d:sheetPr/d:outlinePr/@applyStyles"; | |
| /// <summary> | |
| /// Automatic styles | |
| /// </summary> | |
| public bool OutLineApplyStyle { | |
| get { | |
| CheckSheetType(); | |
| return GetXmlNodeBool(_outLineApplyStylePath); | |
| } | |
| set { | |
| CheckSheetType(); | |
| SetXmlNodeString(_outLineApplyStylePath, value ? "1" : "0"); | |
| } | |
| } | |
| private const string _codeModuleNamePath = "d:sheetPr/@codeName"; | |
| internal string CodeModuleName { | |
| get => GetXmlNodeString(_codeModuleNamePath); | |
| set => SetXmlNodeString(_codeModuleNamePath, value); | |
| } | |
| /// <summary> | |
| /// The XML document holding the worksheet data. | |
| /// All column, row, cell, pagebreak, merged cell and hyperlink-data are loaded into memory and removed from the document when loading the document. | |
| /// </summary> | |
| internal XmlDocument WorksheetXml { get; } = new(); | |
| internal ExcelVmlDrawingCommentCollection _vmlDrawings; | |
| /// <summary> | |
| /// Vml drawings. underlaying object for comments | |
| /// </summary> | |
| internal ExcelVmlDrawingCommentCollection VmlDrawingsComments { | |
| get { | |
| if (_vmlDrawings == null) { | |
| CreateVmlCollection(); | |
| } | |
| return _vmlDrawings; | |
| } | |
| } | |
| internal ExcelCommentCollection _comments; | |
| /// <summary> | |
| /// Collection of comments | |
| /// </summary> | |
| public ExcelCommentCollection Comments { | |
| get { | |
| CheckSheetType(); | |
| if (_comments == null) { | |
| CreateVmlCollection(); | |
| _comments = new(_package, this, NameSpaceManager); | |
| } | |
| return _comments; | |
| } | |
| } | |
| private void CreateVmlCollection() { | |
| var vmlNode = WorksheetXml.DocumentElement.SelectSingleNode( | |
| "d:legacyDrawing/@r:id", | |
| NameSpaceManager); | |
| if (vmlNode == null) { | |
| _vmlDrawings = new(_package, this, null); | |
| } else { | |
| if (Part.RelationshipExists(vmlNode.Value)) { | |
| var rel = Part.GetRelationship(vmlNode.Value); | |
| var vmlUri = UriHelper.ResolvePartUri(rel.SourceUri, rel.TargetUri); | |
| _vmlDrawings = new(_package, this, vmlUri); | |
| _vmlDrawings.RelId = rel.Id; | |
| } | |
| } | |
| } | |
| /// <summary> | |
| /// Get the lenth of the attributes | |
| /// Conditional formatting attributes can be extremly long som get length of the attributes to finetune position. | |
| /// </summary> | |
| /// <param name="xr"></param> | |
| /// <returns></returns> | |
| private int GetAttributeLength(XmlTextReader xr) { | |
| if (xr.NodeType != XmlNodeType.Element) { | |
| return 0; | |
| } | |
| var length = 0; | |
| for (int i = 0; i < xr.AttributeCount; i++) { | |
| var a = xr.GetAttribute(i); | |
| length += string.IsNullOrEmpty(a) ? 0 : a.Length; | |
| } | |
| return length; | |
| } | |
| private void LoadRowPageBreakes(XmlTextReader xr) { | |
| if (!ReadUntil(xr, "rowBreaks", "colBreaks")) { | |
| return; | |
| } | |
| while (xr.Read()) { | |
| if (xr.LocalName == "brk") { | |
| if (xr.NodeType == XmlNodeType.Element) { | |
| if (int.TryParse(xr.GetAttribute("id"), out var id)) { | |
| Row(id).PageBreak = true; | |
| } | |
| } | |
| } else { | |
| break; | |
| } | |
| } | |
| } | |
| private void LoadColPageBreakes(XmlTextReader xr) { | |
| if (!ReadUntil(xr, "colBreaks")) { | |
| return; | |
| } | |
| while (xr.Read()) { | |
| if (xr.LocalName == "brk") { | |
| if (xr.NodeType == XmlNodeType.Element) { | |
| if (int.TryParse(xr.GetAttribute("id"), out var id)) { | |
| Column(id).PageBreak = true; | |
| } | |
| } | |
| } else { | |
| break; | |
| } | |
| } | |
| } | |
| private void ClearNodes() { | |
| if (WorksheetXml.SelectSingleNode("//d:cols", NameSpaceManager) != null) { | |
| WorksheetXml.SelectSingleNode("//d:cols", NameSpaceManager).RemoveAll(); | |
| } | |
| if (WorksheetXml.SelectSingleNode("//d:mergeCells", NameSpaceManager) != null) { | |
| WorksheetXml.SelectSingleNode("//d:mergeCells", NameSpaceManager).RemoveAll(); | |
| } | |
| if (WorksheetXml.SelectSingleNode("//d:hyperlinks", NameSpaceManager) != null) { | |
| WorksheetXml.SelectSingleNode("//d:hyperlinks", NameSpaceManager).RemoveAll(); | |
| } | |
| if (WorksheetXml.SelectSingleNode("//d:rowBreaks", NameSpaceManager) != null) { | |
| WorksheetXml.SelectSingleNode("//d:rowBreaks", NameSpaceManager).RemoveAll(); | |
| } | |
| if (WorksheetXml.SelectSingleNode("//d:colBreaks", NameSpaceManager) != null) { | |
| WorksheetXml.SelectSingleNode("//d:colBreaks", NameSpaceManager).RemoveAll(); | |
| } | |
| } | |
| private const int _blocksize = 8192; | |
| private string GetWorkSheetXml(Stream stream, long start, long end, out Encoding encoding) { | |
| StreamReader sr = new StreamReader(stream); | |
| int length = 0; | |
| char[] block; | |
| int pos; | |
| StringBuilder sb = new StringBuilder(); | |
| Match startmMatch, | |
| endMatch; | |
| do { | |
| int size = stream.Length < _blocksize ? (int)stream.Length : _blocksize; | |
| block = new char[size]; | |
| pos = sr.ReadBlock(block, 0, size); | |
| sb.Append(block, 0, pos); | |
| length += size; | |
| } while (length < start + 20 && length < end); | |
| startmMatch = Regex.Match(sb.ToString(), string.Format("(<[^>]*{0}[^>]*>)", "sheetData")); | |
| if (!startmMatch.Success) //Not found | |
| { | |
| encoding = sr.CurrentEncoding; | |
| return sb.ToString(); | |
| } | |
| string s = sb.ToString(); | |
| string xml = s.Substring(0, startmMatch.Index); | |
| if (startmMatch.Value.EndsWith("/>")) { | |
| xml += s.Substring(startmMatch.Index, s.Length - startmMatch.Index); | |
| } else { | |
| if (sr.Peek() != -1) { | |
| /**** Fixes issue 14788. Fix by Philip Garrett ****/ | |
| long endSeekStart = end; | |
| while (endSeekStart >= 0) { | |
| endSeekStart = Math.Max(endSeekStart - _blocksize, 0); | |
| int size = (int)(end - endSeekStart); | |
| stream.Seek(endSeekStart, SeekOrigin.Begin); | |
| block = new char[size]; | |
| sr = new(stream); | |
| pos = sr.ReadBlock(block, 0, size); | |
| sb = new(); | |
| sb.Append(block, 0, pos); | |
| s = sb.ToString(); | |
| endMatch = Regex.Match(s, string.Format("(</[^>]*{0}[^>]*>)", "sheetData")); | |
| if (endMatch.Success) { | |
| break; | |
| } | |
| } | |
| } | |
| endMatch = Regex.Match(s, string.Format("(</[^>]*{0}[^>]*>)", "sheetData")); | |
| xml += | |
| "<sheetData/>" | |
| + s.Substring( | |
| endMatch.Index + endMatch.Length, | |
| s.Length - (endMatch.Index + endMatch.Length)); | |
| } | |
| if (sr.Peek() > -1) { | |
| xml += sr.ReadToEnd(); | |
| } | |
| encoding = sr.CurrentEncoding; | |
| return xml; | |
| } | |
| private void GetBlockPos(string xml, string tag, ref int start, ref int end) { | |
| Match startmMatch, | |
| endMatch; | |
| startmMatch = Regex.Match(xml.Substring(start), string.Format("(<[^>]*{0}[^>]*>)", tag)); //"<[a-zA-Z:]*" + tag + "[?]*>"); | |
| if (!startmMatch.Success) //Not found | |
| { | |
| start = -1; | |
| end = -1; | |
| return; | |
| } | |
| var startPos = startmMatch.Index + start; | |
| if (startmMatch.Value.Substring(startmMatch.Value.Length - 2, 1) == "/") { | |
| end = startPos + startmMatch.Length; | |
| } else { | |
| endMatch = Regex.Match(xml.Substring(start), string.Format("(</[^>]*{0}[^>]*>)", tag)); | |
| if (endMatch.Success) { | |
| end = endMatch.Index + endMatch.Length + start; | |
| } | |
| } | |
| start = startPos; | |
| } | |
| private bool ReadUntil(XmlTextReader xr, params string[] tagName) { | |
| if (xr.EOF) { | |
| return false; | |
| } | |
| while (!Array.Exists(tagName, tag => xr.LocalName.EndsWith(tag))) { | |
| xr.Read(); | |
| if (xr.EOF) { | |
| return false; | |
| } | |
| } | |
| return (xr.LocalName.EndsWith(tagName[0])); | |
| } | |
| private void LoadColumns( | |
| XmlTextReader xr) //(string xml) | |
| { | |
| var colList = new List<IRangeId>(); | |
| if (ReadUntil(xr, "cols", "sheetData")) { | |
| //if (xml != "") | |
| //{ | |
| //var xr=new XmlTextReader(new StringReader(xml)); | |
| while (xr.Read()) { | |
| if (xr.NodeType == XmlNodeType.Whitespace) { | |
| continue; | |
| } | |
| if (xr.LocalName != "col") { | |
| break; | |
| } | |
| if (xr.NodeType == XmlNodeType.Element) { | |
| int min = int.Parse(xr.GetAttribute("min")); | |
| ExcelColumn col = new ExcelColumn(this, min); | |
| col.ColumnMax = int.Parse(xr.GetAttribute("max")); | |
| col.Width = | |
| xr.GetAttribute("width") == null | |
| ? 0 | |
| : double.Parse(xr.GetAttribute("width"), CultureInfo.InvariantCulture); | |
| col.BestFit = | |
| xr.GetAttribute("bestFit") != null && xr.GetAttribute("bestFit") == "1" | |
| ? true | |
| : false; | |
| col.Collapsed = | |
| xr.GetAttribute("collapsed") != null && xr.GetAttribute("collapsed") == "1" | |
| ? true | |
| : false; | |
| col.Phonetic = | |
| xr.GetAttribute("phonetic") != null && xr.GetAttribute("phonetic") == "1" | |
| ? true | |
| : false; | |
| col.OutlineLevel = (short)(xr.GetAttribute("outlineLevel") == null | |
| ? 0 | |
| : int.Parse(xr.GetAttribute("outlineLevel"), CultureInfo.InvariantCulture)); | |
| col.Hidden = | |
| xr.GetAttribute("hidden") != null && xr.GetAttribute("hidden") == "1" ? true : false; | |
| _values.SetValue(0, min, col); | |
| if (!(xr.GetAttribute("style") == null | |
| || !int.TryParse(xr.GetAttribute("style"), out var style))) { | |
| _styles.SetValue(0, min, style); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| /// <summary> | |
| /// Load Hyperlinks | |
| /// </summary> | |
| /// <param name="xr">The reader</param> | |
| private void LoadHyperLinks(XmlTextReader xr) { | |
| if (!ReadUntil(xr, "hyperlinks", "rowBreaks", "colBreaks")) { | |
| return; | |
| } | |
| while (xr.Read()) { | |
| if (xr.LocalName == "hyperlink") { | |
| ExcelCellBase.GetRowColFromAddress( | |
| xr.GetAttribute("ref"), | |
| out var fromRow, | |
| out var fromCol, | |
| out int toRow, | |
| out var toCol); | |
| ExcelHyperLink hl = null; | |
| if (xr.GetAttribute("id", ExcelPackage._schemaRelationships) != null) { | |
| var rId = xr.GetAttribute("id", ExcelPackage._schemaRelationships); | |
| var uri = Part.GetRelationship(rId).TargetUri; | |
| // Get Location, if any. EPPlus Bug 15517 | |
| var location = xr.GetAttribute("location"); | |
| location = (string.IsNullOrEmpty(location)) ? "" : "#" + location; | |
| if (uri.IsAbsoluteUri) { | |
| try { | |
| hl = new(uri.AbsoluteUri + location); | |
| } catch { | |
| hl = new(uri.OriginalString + location, UriKind.Absolute); | |
| } | |
| } else { | |
| hl = new(uri.OriginalString + location, UriKind.Relative); | |
| } | |
| hl.RId = rId; | |
| Part.DeleteRelationship(rId); //Delete the relationship, it is recreated when we save the package. | |
| } else if (xr.GetAttribute("location") != null) { | |
| hl = new(xr.GetAttribute("location"), xr.GetAttribute("display")); | |
| hl.RowSpann = toRow - fromRow; | |
| hl.ColSpann = toCol - fromCol; | |
| } | |
| string tt = xr.GetAttribute("tooltip"); | |
| if (!string.IsNullOrEmpty(tt)) { | |
| hl.ToolTip = tt; | |
| } | |
| _hyperLinks.SetValue(fromRow, fromCol, hl); | |
| } else { | |
| break; | |
| } | |
| } | |
| } | |
| /// <summary> | |
| /// Load cells | |
| /// </summary> | |
| /// <param name="xr">The reader</param> | |
| private void LoadCells(XmlTextReader xr) { | |
| //var cellList=new List<IRangeID>(); | |
| //var rowList = new List<IRangeID>(); | |
| //var formulaList = new List<IRangeID>(); | |
| ReadUntil(xr, "sheetData", "mergeCells", "hyperlinks", "rowBreaks", "colBreaks"); | |
| ExcelAddressBase address = null; | |
| string type = ""; | |
| int style = 0; | |
| int row = 0; | |
| int col = 0; | |
| xr.Read(); | |
| while (!xr.EOF) { | |
| while (xr.NodeType == XmlNodeType.EndElement) { | |
| xr.Read(); | |
| } | |
| if (xr.LocalName == "row") { | |
| var r = xr.GetAttribute("r"); | |
| if (r == null) { | |
| row++; | |
| } else { | |
| row = Convert.ToInt32(r); | |
| } | |
| if (DoAddRow(xr)) { | |
| _values.SetValue(row, 0, AddRow(xr, row)); | |
| if (xr.GetAttribute("s") != null) { | |
| _styles.SetValue(row, 0, int.Parse(xr.GetAttribute("s"), CultureInfo.InvariantCulture)); | |
| } | |
| } | |
| xr.Read(); | |
| } else if (xr.LocalName == "c") { | |
| //if (cell != null) cellList.Add(cell); | |
| //cell = new ExcelCell(this, xr.GetAttribute("r")); | |
| var r = xr.GetAttribute("r"); | |
| if (r == null) { | |
| //Handle cells with no reference | |
| col++; | |
| address = new(row, col, row, col); | |
| } else { | |
| address = new(r); | |
| col = address._fromCol; | |
| } | |
| //Datetype | |
| if (xr.GetAttribute("t") != null) { | |
| type = xr.GetAttribute("t"); | |
| _types.SetValue(address._fromRow, address._fromCol, type); | |
| } else { | |
| type = ""; | |
| } | |
| //Style | |
| if (xr.GetAttribute("s") != null) { | |
| style = int.Parse(xr.GetAttribute("s")); | |
| _styles.SetValue(address._fromRow, address._fromCol, style); | |
| _values.SetValue(address._fromRow, address._fromCol, null); //TODO:Better Performance ?? | |
| } else { | |
| style = 0; | |
| } | |
| xr.Read(); | |
| } else if (xr.LocalName == "v") { | |
| SetValueFromXml(xr, type, style, address._fromRow, address._fromCol); | |
| xr.Read(); | |
| } else if (xr.LocalName == "f") { | |
| string t = xr.GetAttribute("t"); | |
| if (t == null) { | |
| _formulas.SetValue(address._fromRow, address._fromCol, xr.ReadElementContentAsString()); | |
| _values.SetValue(address._fromRow, address._fromCol, null); | |
| //formulaList.Add(cell); | |
| } else if (t == "shared") { | |
| string si = xr.GetAttribute("si"); | |
| if (si != null) { | |
| var sfIndex = int.Parse(si); | |
| _formulas.SetValue(address._fromRow, address._fromCol, sfIndex); | |
| _values.SetValue(address._fromRow, address._fromCol, null); | |
| string fAddress = xr.GetAttribute("ref"); | |
| string formula = ConvertUtil.ExcelDecodeString(xr.ReadElementContentAsString()); | |
| if (formula != "") { | |
| _sharedFormulas.Add( | |
| sfIndex, | |
| new(SourceCodeTokenizer.Default) { | |
| Index = sfIndex, | |
| Formula = formula, | |
| Address = fAddress, | |
| StartRow = address._fromRow, | |
| StartCol = address._fromCol, | |
| }); | |
| } | |
| } else { | |
| xr.Read(); //Something is wrong in the sheet, read next | |
| } | |
| } else if (t | |
| == "array") //TODO: Array functions are not support yet. Read the formula for the start cell only. | |
| { | |
| string aAddress = xr.GetAttribute("ref"); | |
| ExcelRange addressRange = new ExcelRange(this, aAddress); | |
| string formula = xr.ReadElementContentAsString(); | |
| bool isIndexMatchFormula = | |
| Regex.IsMatch(formula, @"INDEX\(", RegexOptions.IgnoreCase) | |
| && Regex.IsMatch(formula, @"MATCH\(", RegexOptions.IgnoreCase) | |
| && !aAddress.Contains(":"); | |
| if (isIndexMatchFormula) { | |
| addressRange.IsArrayFormula = false; | |
| for (int colIndex = addressRange.Start.Column; | |
| colIndex <= addressRange.End.Column; | |
| colIndex++) { | |
| for (int rowIndex = addressRange.Start.Row; | |
| rowIndex <= addressRange.End.Row; | |
| rowIndex++) { | |
| var afIndex = GetMaxShareFunctionIndex(true); | |
| _formulas.SetValue(rowIndex, colIndex, afIndex); | |
| _values.SetValue(rowIndex, colIndex, null); | |
| _sharedFormulas.Add( | |
| afIndex, | |
| new(SourceCodeTokenizer.Default) { | |
| Index = afIndex, | |
| Formula = formula, | |
| Address = aAddress, | |
| StartRow = address._fromRow, | |
| StartCol = address._fromCol, | |
| IsArray = false, | |
| }); | |
| } | |
| } | |
| } else { | |
| addressRange.IsArrayFormula = true; | |
| var afIndex = GetMaxShareFunctionIndex(true); | |
| for (int colIndex = addressRange.Start.Column; | |
| colIndex <= addressRange.End.Column; | |
| colIndex++) { | |
| for (int rowIndex = addressRange.Start.Row; | |
| rowIndex <= addressRange.End.Row; | |
| rowIndex++) { | |
| _formulas.SetValue(rowIndex, colIndex, afIndex); | |
| _values.SetValue(rowIndex, colIndex, null); | |
| } | |
| } | |
| _sharedFormulas.Add( | |
| afIndex, | |
| new(SourceCodeTokenizer.Default) { | |
| Index = afIndex, | |
| Formula = formula, | |
| Address = aAddress, | |
| StartRow = address._fromRow, | |
| StartCol = address._fromCol, | |
| IsArray = true, | |
| }); | |
| } | |
| } else // ??? some other type | |
| { | |
| xr.Read(); //Something is wrong in the sheet, read next | |
| } | |
| } else if (xr.LocalName | |
| == "is") //Inline string | |
| { | |
| xr.Read(); | |
| if (xr.LocalName == "t") { | |
| _values.SetValue( | |
| address._fromRow, | |
| address._fromCol, | |
| ConvertUtil.ExcelDecodeString(xr.ReadElementContentAsString())); | |
| //cell._value = xr.ReadInnerXml(); | |
| } else { | |
| _values.SetValue(address._fromRow, address._fromCol, xr.ReadOuterXml()); | |
| _types.SetValue(address._fromRow, address._fromCol, "rt"); | |
| _flags.SetFlagValue(address._fromRow, address._fromCol, true, CellFlags.RichText); | |
| //cell.IsRichText = true; | |
| } | |
| } else { | |
| break; | |
| } | |
| } | |
| } | |
| private bool DoAddRow(XmlTextReader xr) { | |
| var c = xr.GetAttribute("r") == null ? 0 : 1; | |
| if (xr.GetAttribute("spans") != null) { | |
| c++; | |
| } | |
| return xr.AttributeCount > c; | |
| } | |
| /// <summary> | |
| /// Load merged cells | |
| /// </summary> | |
| /// <param name="xr"></param> | |
| private void LoadMergeCells(XmlTextReader xr) { | |
| if (ReadUntil(xr, "mergeCells", "hyperlinks", "rowBreaks", "colBreaks") && !xr.EOF) { | |
| while (xr.Read()) { | |
| if (xr.LocalName != "mergeCell") { | |
| break; | |
| } | |
| if (xr.NodeType == XmlNodeType.Element) { | |
| string address = xr.GetAttribute("ref"); | |
| _mergedCells.Add(new ExcelAddress(address), false); | |
| } | |
| } | |
| } | |
| } | |
| /// <summary> | |
| /// Update merged cells | |
| /// </summary> | |
| /// <param name="sw">The writer</param> | |
| private void UpdateMergedCells(StreamWriter sw) { | |
| sw.Write("<mergeCells>"); | |
| foreach (string address in _mergedCells) { | |
| sw.Write("<mergeCell ref=\"{0}\" />", address); | |
| } | |
| sw.Write("</mergeCells>"); | |
| } | |
| /// <summary> | |
| /// Reads a row from the XML reader | |
| /// </summary> | |
| /// <param name="xr">The reader</param> | |
| /// <param name="row">The row number</param> | |
| /// <returns></returns> | |
| private RowInternal AddRow(XmlTextReader xr, int row) { | |
| return new() { | |
| Collapsed = | |
| (xr.GetAttribute("collapsed") != null && xr.GetAttribute("collapsed") == "1" | |
| ? true | |
| : false), | |
| OutlineLevel = | |
| (xr.GetAttribute("outlineLevel") == null | |
| ? (short)0 | |
| : short.Parse(xr.GetAttribute("outlineLevel"), CultureInfo.InvariantCulture)), | |
| Height = | |
| (xr.GetAttribute("ht") == null | |
| ? -1 | |
| : double.Parse(xr.GetAttribute("ht"), CultureInfo.InvariantCulture)), | |
| Hidden = | |
| (xr.GetAttribute("hidden") != null && xr.GetAttribute("hidden") == "1" ? true : false), | |
| Phonetic = xr.GetAttribute("ph") != null && xr.GetAttribute("ph") == "1" ? true : false, | |
| CustomHeight = | |
| xr.GetAttribute("customHeight") == null ? false : xr.GetAttribute("customHeight") == "1", | |
| }; | |
| } | |
| private static readonly DateTime _excelEpoch = new(1899, 12, 30); | |
| public static DateTime IncorrectDurationFromOaDate(double value) { | |
| // This behavior is wrong. Real OADate values have a discontinuity on 30 December 1899. | |
| // For real OADate values, the negative sign applies only to the integer portion of | |
| // the float, *not* to the decimal portion. For example, -0.5 and 0.5 both refer to the | |
| // same date, and -1.5 is actually 1899-12-29 12:00 (1 day before 1899-12-30 00:00 | |
| // plus 0.5 days), *not* 1899-12-28 12:00 (1.5 days before 1899-12-30 00:00). | |
| // | |
| // Unfortunately, AppSheet's duration-handling code gets this very wrong, and treats the | |
| // duration as the offset from 1899-12-30 00:00. This is correct for positive durations, | |
| // but it's wrong for negative durations. This code tries to fix the bug that exists in | |
| // AppSheet's duration-handling code here, and it succeeds in some cases and fails in | |
| // others. | |
| // | |
| // This code also breaks date/time handling for dates before 1899-12-30 00:00 in some | |
| // cases. Specifically, dates end up being offset by one day. | |
| // | |
| // Regardless, changing this behavior is risky, so this code simply replicates the | |
| // existing behavior for | |
| if (value >= 0) { | |
| return DateTime.FromOADate(value); | |
| } | |
| // This looks like a very complicated way to call TimeSpan.FromDays(value), but | |
| // TimeSpan.FromDays actually only guarantees millisecond precision, and critically | |
| // rounding is different on .NET Core, resulting in values like (e.g.) 3:15:00 being | |
| // incorrectly rounded. | |
| var offset = DateTime.FromOADate(-value) - _excelEpoch; | |
| return _excelEpoch - offset; | |
| } | |
| private void SetValueFromXml(XmlTextReader xr, string type, int styleId, int row, int col) { | |
| //XmlNode vnode = colNode.SelectSingleNode("d:v", NameSpaceManager); | |
| //if (vnode == null) return null; | |
| if (type == "s") { | |
| int ix = xr.ReadElementContentAsInt(); | |
| // Temporary debugging code to locate intermittent 'Index was out of range' exception. | |
| if (ix < 0) { | |
| throw new( | |
| string.Format( | |
| "ReadElementContentAsInt returned value '{0}' which is less than zero.", | |
| ix)); | |
| } | |
| if (ix >= _workbook._sharedStringsList.Count) { | |
| throw new( | |
| string.Format( | |
| "ReadElementContentAsInt returned index value '{0}' which is greater than _sharedStringsList count of {1}.", | |
| ix, | |
| _workbook._sharedStringsList.Count)); | |
| } | |
| _values.SetValue(row, col, _workbook._sharedStringsList[ix].Text); | |
| if (_workbook._sharedStringsList[ix].isRichText) { | |
| _flags.SetFlagValue(row, col, true, CellFlags.RichText); | |
| } | |
| } else if (type == "str") { | |
| _values.SetValue(row, col, ConvertUtil.ExcelDecodeString(xr.ReadElementContentAsString())); | |
| } else if (type == "b") { | |
| _values.SetValue(row, col, (xr.ReadElementContentAsString() != "0")); | |
| } else if (type == "e") { | |
| _values.SetValue(row, col, GetErrorType(xr.ReadElementContentAsString())); | |
| } else { | |
| string v = xr.ReadElementContentAsString(); | |
| var nf = Workbook.Styles.CellXfs[styleId].NumberFormatId; | |
| if ((nf >= 20 && nf <= 21) | |
| || (nf >= 45 | |
| && nf | |
| <= 47)) // Duration | |
| { | |
| if (double.TryParse(v, NumberStyles.Any, CultureInfo.InvariantCulture, out var res)) { | |
| if (Workbook.Date1904) { | |
| res += ExcelWorkbook._date1904Offset; | |
| } | |
| if (res >= -657435.0 && res < 2958465.9999999) { | |
| // Get the Duration value expressed as a DateTime. | |
| _values.SetValue(row, col, IncorrectDurationFromOaDate(res)); | |
| } else { | |
| // Cope with Google Sheets export of cells having a formula. | |
| // Rather than exporting the native value, they export the formatted value. | |
| _values.SetValue(row, col, v); | |
| } | |
| } else { | |
| // Cope with Google Sheets export of cells having a formula. | |
| // Rather than exporting the native value, they export the formatted value. | |
| _values.SetValue(row, col, v); | |
| } | |
| } else if ((nf >= 14 && nf <= 19) | |
| || (nf | |
| == 22)) // DateTime | |
| { | |
| if (double.TryParse(v, NumberStyles.Any, CultureInfo.InvariantCulture, out var res)) { | |
| if (Workbook.Date1904) { | |
| res += ExcelWorkbook._date1904Offset; | |
| } | |
| if (res >= -657435.0 && res < 2958465.9999999) { | |
| _values.SetValue(row, col, DateTime.FromOADate(res)); | |
| } else { | |
| // Cope with Google Sheets export of cells having a formula. | |
| // Rather than exporting the native value, they export the formatted value. | |
| _values.SetValue(row, col, v); | |
| } | |
| } else { | |
| // Cope with Google Sheets export of cells having a formula. | |
| // Rather than exporting the native value, they export the formatted value. | |
| _values.SetValue(row, col, v); | |
| } | |
| } else { | |
| if (double.TryParse(v, NumberStyles.Any, CultureInfo.InvariantCulture, out var d)) { | |
| _values.SetValue(row, col, d); | |
| } else { | |
| // Cope with Google Sheets export of cells having a formula. | |
| // Rather than exporting the native value, they export the formatted value. | |
| _values.SetValue(row, col, v); | |
| //_values.SetValue(row, col, double.NaN); | |
| } | |
| } | |
| } | |
| } | |
| private object GetErrorType(string v) { | |
| return ExcelErrorValue.Parse(v.ToUpper(CultureInfo.InvariantCulture)); | |
| //switch(v.ToUpper()) | |
| //{ | |
| // case "#DIV/0!": | |
| // return new ExcelErrorValue.cre(eErrorType.Div0); | |
| // case "#REF!": | |
| // return new ExcelErrorValue(eErrorType.Ref); | |
| // case "#N/A": | |
| // return new ExcelErrorValue(eErrorType.NA); | |
| // case "#NAME?": | |
| // return new ExcelErrorValue(eErrorType.Name); | |
| // case "#NULL!": | |
| // return new ExcelErrorValue(eErrorType.Null); | |
| // case "#NUM!": | |
| // return new ExcelErrorValue(eErrorType.Num); | |
| // default: | |
| // return new ExcelErrorValue(eErrorType.Value); | |
| //} | |
| } | |
| ///// <summary> | |
| ///// Provides access to an individual cell within the worksheet. | |
| ///// </summary> | |
| ///// <param name="row">The row number in the worksheet</param> | |
| ///// <param name="col">The column number in the worksheet</param> | |
| ///// <returns></returns> | |
| //internal ExcelCell Cell(int row, int col) | |
| //{ | |
| // return new ExcelCell(_values, row, col); | |
| //} | |
| /// <summary> | |
| /// Provides access to a range of cells | |
| /// </summary> | |
| public ExcelRange Cells { | |
| get { | |
| CheckSheetType(); | |
| return new(this, 1, 1, ExcelPackage.MaxRows, ExcelPackage.MaxColumns); | |
| } | |
| } | |
| /// <summary> | |
| /// Provides access to the selected range of cells | |
| /// </summary> | |
| public ExcelRange SelectedRange { | |
| get { | |
| CheckSheetType(); | |
| return new(this, View.SelectedRange); | |
| } | |
| } | |
| private readonly MergeCellsCollection _mergedCells = new(); | |
| /// <summary> | |
| /// Addresses to merged ranges | |
| /// </summary> | |
| public MergeCellsCollection MergedCells { | |
| get { | |
| CheckSheetType(); | |
| return _mergedCells; | |
| } | |
| } | |
| /// <summary> | |
| /// Provides access to an individual row within the worksheet so you can set its properties. | |
| /// </summary> | |
| /// <param name="row">The row number in the worksheet</param> | |
| /// <returns></returns> | |
| public ExcelRow Row(int row) { | |
| //ExcelRow r; | |
| //ulong id = ExcelRow.GetRowID(_sheetID, row); | |
| //TODO: Fixa. | |
| //var v = _values.GetValue(row, 0); | |
| //if (v!=null) | |
| //{ | |
| // var ri=(RowInternal)v; | |
| // r = new ExcelRow(this, row) | |
| //} | |
| //else | |
| //{ | |
| //r = new ExcelRow(this, row); | |
| //_values.SetValue(row, 0, r); | |
| //_rows.Add(r); | |
| //} | |
| CheckSheetType(); | |
| if (row < 1 || row > ExcelPackage.MaxRows) { | |
| throw (new ArgumentException("Row number out of bounds")); | |
| } | |
| return new(this, row); | |
| //return r; | |
| } | |
| /// <summary> | |
| /// Provides access to an individual column within the worksheet so you can set its properties. | |
| /// </summary> | |
| /// <param name="col">The column number in the worksheet</param> | |
| /// <returns></returns> | |
| public ExcelColumn Column(int col) { | |
| CheckSheetType(); | |
| if (col < 1 || col > ExcelPackage.MaxColumns) { | |
| throw (new ArgumentException("Column number out of bounds")); | |
| } | |
| var column = _values.GetValue(0, col) as ExcelColumn; | |
| if (column != null) { | |
| if (column.ColumnMin != column.ColumnMax) { | |
| int maxCol = column.ColumnMax; | |
| column.ColumnMax = col; | |
| ExcelColumn copy = CopyColumn(column, col + 1, maxCol); | |
| } | |
| } else { | |
| int r = 0, | |
| c = col; | |
| if (_values.PrevCell(ref r, ref c)) { | |
| column = _values.GetValue(0, c) as ExcelColumn; | |
| int maxCol = column.ColumnMax; | |
| if (maxCol >= col) { | |
| column.ColumnMax = col - 1; | |
| if (maxCol > col) { | |
| ExcelColumn newC = CopyColumn(column, col + 1, maxCol); | |
| } | |
| return CopyColumn(column, col, col); | |
| } | |
| } | |
| //foreach (ExcelColumn checkColumn in _columns) | |
| //{ | |
| // if (col > checkColumn.ColumnMin && col <= checkColumn.ColumnMax) | |
| // { | |
| // int maxCol = checkColumn.ColumnMax; | |
| // checkColumn.ColumnMax = col - 1; | |
| // if (maxCol > col) | |
| // { | |
| // ExcelColumn newC = CopyColumn(checkColumn, col + 1, maxCol); | |
| // } | |
| // return CopyColumn(checkColumn, col,col); | |
| // } | |
| //} | |
| column = new(this, col); | |
| _values.SetValue(0, col, column); | |
| //_columns.Add(column); | |
| } | |
| return column; | |
| } | |
| /// <summary> | |
| /// Returns the name of the worksheet | |
| /// </summary> | |
| /// <returns>The name of the worksheet</returns> | |
| public override string ToString() { | |
| return Name; | |
| } | |
| internal ExcelColumn CopyColumn(ExcelColumn c, int col, int maxCol) { | |
| ExcelColumn newC = new ExcelColumn(this, col); | |
| newC.ColumnMax = maxCol < ExcelPackage.MaxColumns ? maxCol : ExcelPackage.MaxColumns; | |
| if (c.StyleName != "") { | |
| newC.StyleName = c.StyleName; | |
| } else { | |
| newC.StyleID = c.StyleID; | |
| } | |
| newC.OutlineLevel = c.OutlineLevel; | |
| newC.Phonetic = c.Phonetic; | |
| newC.BestFit = c.BestFit; | |
| //_columns.Add(newC); | |
| _values.SetValue(0, col, newC); | |
| newC._width = c._width; | |
| newC._hidden = c._hidden; | |
| return newC; | |
| } | |
| /// <summary> | |
| /// Make the current worksheet active. | |
| /// </summary> | |
| public void Select() { | |
| View.TabSelected = true; | |
| //Select(Address, true); | |
| } | |
| /// <summary> | |
| /// Selects a range in the worksheet. The active cell is the topmost cell. | |
| /// Make the current worksheet active. | |
| /// </summary> | |
| /// <param name="address">An address range</param> | |
| public void Select(string address) { | |
| Select(address, true); | |
| } | |
| /// <summary> | |
| /// Selects a range in the worksheet. The actice cell is the topmost cell. | |
| /// </summary> | |
| /// <param name="address">A range of cells</param> | |
| /// <param name="selectSheet">Make the sheet active</param> | |
| public void Select(string address, bool selectSheet) { | |
| CheckSheetType(); | |
| int toCol, | |
| toRow; | |
| //Get rows and columns and validate as well | |
| ExcelCellBase.GetRowColFromAddress( | |
| address, | |
| out var fromRow, | |
| out var fromCol, | |
| out toRow, | |
| out toCol); | |
| if (selectSheet) { | |
| View.TabSelected = true; | |
| } | |
| View.SelectedRange = address; | |
| View.ActiveCell = ExcelCellBase.GetAddress(fromRow, fromCol); | |
| } | |
| /// <summary> | |
| /// Selects a range in the worksheet. The active cell is the topmost cell of the first address. | |
| /// Make the current worksheet active. | |
| /// </summary> | |
| /// <param name="address">An address range</param> | |
| public void Select(ExcelAddress address) { | |
| CheckSheetType(); | |
| Select(address, true); | |
| } | |
| /// <summary> | |
| /// Selects a range in the worksheet. The active cell is the topmost cell of the first address. | |
| /// </summary> | |
| /// <param name="address">A range of cells</param> | |
| /// <param name="selectSheet">Make the sheet active</param> | |
| public void Select(ExcelAddress address, bool selectSheet) { | |
| CheckSheetType(); | |
| if (selectSheet) { | |
| View.TabSelected = true; | |
| } | |
| string selAddress = | |
| ExcelCellBase.GetAddress(address.Start.Row, address.Start.Column) | |
| + ":" | |
| + ExcelCellBase.GetAddress(address.End.Row, address.End.Column); | |
| if (address.Addresses != null) { | |
| foreach (var a in address.Addresses) { | |
| selAddress += | |
| " " | |
| + ExcelCellBase.GetAddress(a.Start.Row, a.Start.Column) | |
| + ":" | |
| + ExcelCellBase.GetAddress(a.End.Row, a.End.Column); | |
| } | |
| } | |
| View.SelectedRange = selAddress; | |
| View.ActiveCell = ExcelCellBase.GetAddress(address.Start.Row, address.Start.Column); | |
| } | |
| /// <summary> | |
| /// Inserts a new row into the spreadsheet. Existing rows below the position are | |
| /// shifted down. All formula are updated to take account of the new row. | |
| /// </summary> | |
| /// <param name="rowFrom">The position of the new row</param> | |
| /// <param name="rows">Number of rows to insert</param> | |
| public void InsertRow(int rowFrom, int rows) { | |
| InsertRow(rowFrom, rows, 0); | |
| } | |
| /// <summary> | |
| /// Inserts a new row into the spreadsheet. Existing rows below the position are | |
| /// shifted down. All formula are updated to take account of the new row. | |
| /// </summary> | |
| /// <param name="rowFrom">The position of the new row</param> | |
| /// <param name="rows">Number of rows to insert.</param> | |
| /// <param name="copyStylesFromRow">Copy Styles from this row. Applied to all inserted rows</param> | |
| public void InsertRow(int rowFrom, int rows, int copyStylesFromRow) { | |
| CheckSheetType(); | |
| var d = Dimension; | |
| if (rowFrom < 1) { | |
| throw (new ArgumentOutOfRangeException("rowFrom can't be lesser that 1")); | |
| } | |
| //Check that cells aren't shifted outside the boundries | |
| if (d != null && d.End.Row > rowFrom && d.End.Row + rows > ExcelPackage.MaxRows) { | |
| throw (new ArgumentOutOfRangeException( | |
| "Can't insert. Rows will be shifted outside the boundries of the worksheet.")); | |
| } | |
| _values.Insert(rowFrom, 0, rows, 0); | |
| _formulas.Insert(rowFrom, 0, rows, 0); | |
| _styles.Insert(rowFrom, 0, rows, 0); | |
| _types.Insert(rowFrom, 0, rows, 0); | |
| _commentsStore.Insert(rowFrom, 0, rows, 0); | |
| _hyperLinks.Insert(rowFrom, 0, rows, 0); | |
| _flags.Insert(rowFrom, 0, rows, 0); | |
| foreach (var f in _sharedFormulas.Values) { | |
| if (f.StartRow >= rowFrom) { | |
| f.StartRow += rows; | |
| } | |
| var a = new ExcelAddressBase(f.Address); | |
| if (a._fromRow >= rowFrom) { | |
| a._fromRow += rows; | |
| a._toRow += rows; | |
| } else if (a._toRow >= rowFrom) { | |
| a._toRow += rows; | |
| } | |
| f.Address = ExcelCellBase.GetAddress(a._fromRow, a._fromCol, a._toRow, a._toCol); | |
| f.Formula = ExcelCellBase.UpdateFormulaReferences(f.Formula, rows, 0, rowFrom, 0); | |
| } | |
| var cse = new CellsStoreEnumerator<object>(_formulas); | |
| while (cse.Next()) { | |
| if (cse.Value is string) { | |
| cse.Value = ExcelCellBase.UpdateFormulaReferences( | |
| cse.Value.ToString(), | |
| rows, | |
| 0, | |
| rowFrom, | |
| 0); | |
| } | |
| } | |
| FixMergedCellsRow(rowFrom, rows, false); | |
| if (copyStylesFromRow > 0) { | |
| var cseS = new CellsStoreEnumerator<int>( | |
| _styles, | |
| copyStylesFromRow, | |
| 0, | |
| copyStylesFromRow, | |
| ExcelPackage.MaxColumns); //Fixes issue 15068 , 15090 | |
| while (cseS.Next()) { | |
| for (var r = 0; r < rows; r++) { | |
| _styles.SetValue(rowFrom + r, cseS.Column, cseS.Value); | |
| } | |
| } | |
| } | |
| foreach (var tbl in Tables) { | |
| tbl.Address = tbl.Address.AddRow(rowFrom, rows); | |
| } | |
| } | |
| /// <summary> | |
| /// Inserts a new column into the spreadsheet. Existing columns below the position are | |
| /// shifted down. All formula are updated to take account of the new column. | |
| /// </summary> | |
| /// <param name="columnFrom">The position of the new column</param> | |
| /// <param name="columns">Number of columns to insert</param> | |
| public void InsertColumn(int columnFrom, int columns) { | |
| InsertColumn(columnFrom, columns, 0); | |
| } | |
| ///<summary> | |
| /// Inserts a new column into the spreadsheet. Existing column to the left are | |
| /// shifted. All formula are updated to take account of the new column. | |
| /// </summary> | |
| /// <param name="columnFrom">The position of the new column</param> | |
| /// <param name="columns">Number of columns to insert.</param> | |
| /// <param name="copyStylesFromColumn">Copy Styles from this column. Applied to all inserted columns</param> | |
| public void InsertColumn(int columnFrom, int columns, int copyStylesFromColumn) { | |
| CheckSheetType(); | |
| var d = Dimension; | |
| if (columnFrom < 1) { | |
| throw (new ArgumentOutOfRangeException("columnFrom can't be lesser that 1")); | |
| } | |
| //Check that cells aren't shifted outside the boundries | |
| if (d != null | |
| && d.End.Column > columnFrom | |
| && d.End.Column + columns > ExcelPackage.MaxColumns) { | |
| throw (new ArgumentOutOfRangeException( | |
| "Can't insert. Columns will be shifted outside the boundries of the worksheet.")); | |
| } | |
| _values.Insert(0, columnFrom, 0, columns); | |
| _formulas.Insert(0, columnFrom, 0, columns); | |
| _styles.Insert(0, columnFrom, 0, columns); | |
| _types.Insert(0, columnFrom, 0, columns); | |
| _commentsStore.Insert(0, columnFrom, 0, columns); | |
| _hyperLinks.Insert(0, columnFrom, 0, columns); | |
| _flags.Insert(0, columnFrom, 0, columns); | |
| foreach (var f in _sharedFormulas.Values) { | |
| if (f.StartCol >= columnFrom) { | |
| f.StartCol += columns; | |
| } | |
| var a = new ExcelAddressBase(f.Address); | |
| if (a._fromCol >= columnFrom) { | |
| a._fromCol += columns; | |
| a._toCol += columns; | |
| } else if (a._toCol >= columnFrom) { | |
| a._toCol += columns; | |
| } | |
| f.Address = ExcelCellBase.GetAddress(a._fromRow, a._fromCol, a._toRow, a._toCol); | |
| f.Formula = ExcelCellBase.UpdateFormulaReferences(f.Formula, 0, columns, 0, columnFrom); | |
| } | |
| var cse = new CellsStoreEnumerator<object>(_formulas); | |
| while (cse.Next()) { | |
| if (cse.Value is string) { | |
| cse.Value = ExcelCellBase.UpdateFormulaReferences( | |
| cse.Value.ToString(), | |
| 0, | |
| columns, | |
| 0, | |
| columnFrom); | |
| } | |
| } | |
| FixMergedCellsColumn(columnFrom, columns, false); | |
| var csec = new CellsStoreEnumerator<object>(_values, 0, 1, 0, ExcelPackage.MaxColumns); | |
| var lst = new List<ExcelColumn>(); | |
| foreach (var col in csec) { | |
| if (col is ExcelColumn column) { | |
| lst.Add(column); | |
| } | |
| } | |
| for (int i = lst.Count - 1; i >= 0; i--) { | |
| var c = lst[i]; | |
| if (c._columnMin >= columnFrom) { | |
| if (c._columnMin + columns <= ExcelPackage.MaxColumns) { | |
| c._columnMin += columns; | |
| } else { | |
| c._columnMin = ExcelPackage.MaxColumns; | |
| } | |
| if (c._columnMax + columns <= ExcelPackage.MaxColumns) { | |
| c._columnMax += columns; | |
| } else { | |
| c._columnMax = ExcelPackage.MaxColumns; | |
| } | |
| } else if (c._columnMax >= columnFrom) { | |
| var cc = c._columnMax - columnFrom; | |
| c._columnMax = columnFrom - 1; | |
| CopyColumn(c, columnFrom + columns, columnFrom + columns + cc); | |
| } | |
| } | |
| if (copyStylesFromColumn > 0) { | |
| for (var c = 0; c < columns; c++) { | |
| var col = Column(columnFrom + c); | |
| col.StyleID = Column(copyStylesFromColumn).StyleID; | |
| } | |
| } | |
| //Adjust tables | |
| foreach (var tbl in Tables) { | |
| if (columnFrom > tbl.Address.Start.Column && columnFrom <= tbl.Address.End.Column) { | |
| InsertTableColumns(columnFrom, columns, tbl); | |
| } | |
| tbl.Address = tbl.Address.AddColumn(columnFrom, columns); | |
| } | |
| } | |
| private static void InsertTableColumns(int columnFrom, int columns, ExcelTable tbl) { | |
| var node = tbl.Columns[0].TopNode.ParentNode; | |
| var ix = columnFrom - tbl.Address.Start.Column - 1; | |
| var insPos = node.ChildNodes[ix]; | |
| ix += 2; | |
| for (int i = 0; i < columns; i++) { | |
| var name = tbl.Columns.GetUniqueName( | |
| string.Format("Column{0}", (ix++).ToString(CultureInfo.InvariantCulture))); | |
| XmlElement tableColumn = (XmlElement) | |
| tbl.TableXml.CreateNode(XmlNodeType.Element, "tableColumn", ExcelPackage._schemaMain); | |
| tableColumn.SetAttribute( | |
| "id", | |
| (tbl.Columns.Count + i + 1).ToString(CultureInfo.InvariantCulture)); | |
| tableColumn.SetAttribute("name", name); | |
| insPos = node.InsertAfter(tableColumn, insPos); | |
| } //Create tbl Column | |
| tbl._cols = new(tbl); | |
| } | |
| /// <summary> | |
| /// Adds a value to the row of merged cells to fix for inserts or deletes | |
| /// </summary> | |
| /// <param name="row"></param> | |
| /// <param name="rows"></param> | |
| /// <param name="delete"></param> | |
| private void FixMergedCellsRow(int row, int rows, bool delete) { | |
| if (delete) { | |
| _mergedCells._cells.Delete(row, 0, rows, 0); | |
| } else { | |
| _mergedCells._cells.Insert(row, 0, rows, 0); | |
| } | |
| List<int> removeIndex = []; | |
| for (int i = 0; i < _mergedCells.Count; i++) { | |
| if (!string.IsNullOrEmpty(_mergedCells[i])) { | |
| ExcelAddressBase addr = new(_mergedCells[i]), | |
| newAddr; | |
| if (delete) { | |
| newAddr = addr.DeleteRow(row, rows); | |
| if (newAddr == null) { | |
| removeIndex.Add(i); | |
| continue; | |
| } | |
| } else { | |
| newAddr = addr.AddRow(row, rows); | |
| if (newAddr.Address != addr.Address) { | |
| // _mergedCells._cells.Insert(row, 0, rows, 0); | |
| _mergedCells.SetIndex(newAddr, i); | |
| } | |
| } | |
| if (newAddr.Address != addr.Address) { | |
| _mergedCells.List[i] = newAddr._address; | |
| } | |
| } | |
| } | |
| for (int i = removeIndex.Count - 1; i >= 0; i--) { | |
| _mergedCells.List.RemoveAt(removeIndex[i]); | |
| } | |
| } | |
| /// <summary> | |
| /// Adds a value to the row of merged cells to fix for inserts or deletes | |
| /// </summary> | |
| /// <param name="column"></param> | |
| /// <param name="columns"></param> | |
| /// <param name="delete"></param> | |
| private void FixMergedCellsColumn(int column, int columns, bool delete) { | |
| if (delete) { | |
| _mergedCells._cells.Delete(0, column, 0, columns); | |
| } else { | |
| _mergedCells._cells.Insert(0, column, 0, columns); | |
| } | |
| List<int> removeIndex = []; | |
| for (int i = 0; i < _mergedCells.Count; i++) { | |
| if (!string.IsNullOrEmpty(_mergedCells[i])) { | |
| ExcelAddressBase addr = new(_mergedCells[i]), | |
| newAddr; | |
| if (delete) { | |
| newAddr = addr.DeleteColumn(column, columns); | |
| if (newAddr == null) { | |
| removeIndex.Add(i); | |
| continue; | |
| } | |
| } else { | |
| newAddr = addr.AddColumn(column, columns); | |
| if (newAddr.Address != addr.Address) { | |
| _mergedCells.SetIndex(newAddr, i); | |
| } | |
| } | |
| if (newAddr.Address != addr.Address) { | |
| _mergedCells.List[i] = newAddr._address; | |
| } | |
| } | |
| } | |
| for (int i = removeIndex.Count - 1; i >= 0; i--) { | |
| _mergedCells.List.RemoveAt(removeIndex[i]); | |
| } | |
| } | |
| /// <summary> | |
| /// Delete the specified row from the worksheet. | |
| /// </summary> | |
| /// <param name="row">A row to be deleted</param> | |
| public void DeleteRow(int row) { | |
| DeleteRow(row, 1); | |
| } | |
| /// <summary> | |
| /// Delete the specified row from the worksheet. | |
| /// </summary> | |
| /// <param name="rowFrom">The start row</param> | |
| /// <param name="rows">Number of rows to delete</param> | |
| public void DeleteRow(int rowFrom, int rows) { | |
| CheckSheetType(); | |
| if (rowFrom < 1 || rowFrom + rows > ExcelPackage.MaxRows) { | |
| throw (new ArgumentException( | |
| "Row out of range. Spans from 1 to " | |
| + ExcelPackage.MaxRows.ToString(CultureInfo.InvariantCulture))); | |
| } | |
| _values.Delete(rowFrom, 0, rows, ExcelPackage.MaxColumns); | |
| _types.Delete(rowFrom, 0, rows, ExcelPackage.MaxColumns); | |
| _formulas.Delete(rowFrom, 0, rows, ExcelPackage.MaxColumns); | |
| _styles.Delete(rowFrom, 0, rows, ExcelPackage.MaxColumns); | |
| _flags.Delete(rowFrom, 0, rows, ExcelPackage.MaxColumns); | |
| _commentsStore.Delete(rowFrom, 0, rows, ExcelPackage.MaxColumns); | |
| _hyperLinks.Delete(rowFrom, 0, rows, ExcelPackage.MaxColumns); | |
| AdjustFormulasRow(rowFrom, rows); | |
| FixMergedCellsRow(rowFrom, rows, true); | |
| foreach (var tbl in Tables) { | |
| tbl.Address = tbl.Address.DeleteRow(rowFrom, rows); | |
| } | |
| } | |
| /// <summary> | |
| /// Delete the specified column from the worksheet. | |
| /// </summary> | |
| /// <param name="column">The column to be deleted</param> | |
| public void DeleteColumn(int column) { | |
| DeleteColumn(column, 1); | |
| } | |
| /// <summary> | |
| /// Delete the specified column from the worksheet. | |
| /// </summary> | |
| /// <param name="columnFrom">The start column</param> | |
| /// <param name="columns">Number of columns to delete</param> | |
| public void DeleteColumn(int columnFrom, int columns) { | |
| if (columnFrom < 1 || columnFrom + columns > ExcelPackage.MaxColumns) { | |
| throw (new ArgumentException( | |
| "Column out of range. Spans from 1 to " | |
| + ExcelPackage.MaxColumns.ToString(CultureInfo.InvariantCulture))); | |
| } | |
| var col = _values.GetValue(0, columnFrom) as ExcelColumn; | |
| if (col == null) { | |
| var r = 0; | |
| var c = columnFrom; | |
| if (_values.PrevCell(ref r, ref c)) { | |
| col = _values.GetValue(0, c) as ExcelColumn; | |
| if (col._columnMax >= columnFrom) { | |
| col.ColumnMax = columnFrom - 1; | |
| } | |
| } | |
| } | |
| _values.Delete(0, columnFrom, ExcelPackage.MaxRows, columns); | |
| _types.Delete(0, columnFrom, ExcelPackage.MaxRows, columns); | |
| _formulas.Delete(0, columnFrom, ExcelPackage.MaxRows, columns); | |
| _styles.Delete(0, columnFrom, ExcelPackage.MaxRows, columns); | |
| _flags.Delete(0, columnFrom, ExcelPackage.MaxRows, columns); | |
| _commentsStore.Delete(0, columnFrom, ExcelPackage.MaxRows, columns); | |
| _hyperLinks.Delete(0, columnFrom, ExcelPackage.MaxRows, columns); | |
| AdjustFormulasColumn(columnFrom, columns); | |
| FixMergedCellsColumn(columnFrom, columns, true); | |
| var csec = new CellsStoreEnumerator<object>(_values, 0, columnFrom, 0, ExcelPackage.MaxColumns); | |
| foreach (var column in csec) { | |
| if (column is ExcelColumn excelColumn) { | |
| if (excelColumn._columnMin >= columnFrom) { | |
| excelColumn._columnMin -= columns; | |
| excelColumn._columnMax -= columns; | |
| } | |
| } | |
| } | |
| foreach (var tbl in Tables) { | |
| if (columnFrom >= tbl.Address.Start.Column && columnFrom <= tbl.Address.End.Column) { | |
| var node = tbl.Columns[0].TopNode.ParentNode; | |
| var ix = columnFrom - tbl.Address.Start.Column; | |
| for (int i = 0; i < columns; i++) { | |
| if (node.ChildNodes.Count > ix) { | |
| node.RemoveChild(node.ChildNodes[ix]); | |
| } | |
| } | |
| tbl._cols = new(tbl); | |
| } | |
| tbl.Address = tbl.Address.DeleteColumn(columnFrom, columns); | |
| } | |
| } | |
| internal void AdjustFormulasRow(int rowFrom, int rows) { | |
| var delSf = new List<int>(); | |
| foreach (var sf in _sharedFormulas.Values) { | |
| var a = new ExcelAddress(sf.Address).DeleteRow(rowFrom, rows); | |
| if (a == null) { | |
| delSf.Add(sf.Index); | |
| } else { | |
| sf.Address = a.Address; | |
| if (sf.StartRow > rowFrom) { | |
| var r = Math.Min(sf.StartRow - rowFrom, rows); | |
| sf.Formula = ExcelCellBase.UpdateFormulaReferences(sf.Formula, -r, 0, rowFrom, 0); | |
| sf.StartRow -= r; | |
| } | |
| } | |
| } | |
| foreach (var ix in delSf) { | |
| _sharedFormulas.Remove(ix); | |
| } | |
| var cse = new CellsStoreEnumerator<object>( | |
| _formulas, | |
| 1, | |
| 1, | |
| ExcelPackage.MaxRows, | |
| ExcelPackage.MaxColumns); | |
| while (cse.Next()) { | |
| if (cse.Value is string) { | |
| cse.Value = ExcelCellBase.UpdateFormulaReferences( | |
| cse.Value.ToString(), | |
| -rows, | |
| 0, | |
| rowFrom, | |
| 0); | |
| } | |
| } | |
| } | |
| internal void AdjustFormulasColumn(int columnFrom, int columns) { | |
| var delSf = new List<int>(); | |
| foreach (var sf in _sharedFormulas.Values) { | |
| var a = new ExcelAddress(sf.Address).DeleteColumn(columnFrom, columns); | |
| if (a == null) { | |
| delSf.Add(sf.Index); | |
| } else { | |
| sf.Address = a.Address; | |
| if (sf.StartCol > columnFrom) { | |
| var c = Math.Min(sf.StartCol - columnFrom, columns); | |
| sf.Formula = ExcelCellBase.UpdateFormulaReferences(sf.Formula, 0, -c, 0, 1); | |
| sf.StartCol -= c; | |
| } | |
| } | |
| } | |
| foreach (var ix in delSf) { | |
| _sharedFormulas.Remove(ix); | |
| } | |
| var cse = new CellsStoreEnumerator<object>( | |
| _formulas, | |
| 1, | |
| 1, | |
| ExcelPackage.MaxRows, | |
| ExcelPackage.MaxColumns); | |
| while (cse.Next()) { | |
| if (cse.Value is string) { | |
| cse.Value = ExcelCellBase.UpdateFormulaReferences( | |
| cse.Value.ToString(), | |
| 0, | |
| -columns, | |
| 0, | |
| columnFrom); | |
| } | |
| } | |
| } | |
| /// <summary> | |
| /// Deletes the specified row from the worksheet. | |
| /// </summary> | |
| /// <param name="rowFrom">The number of the start row to be deleted</param> | |
| /// <param name="rows">Number of rows to delete</param> | |
| /// <param name="shiftOtherRowsUp">Not used. Rows are always shifted</param> | |
| public void DeleteRow(int rowFrom, int rows, bool shiftOtherRowsUp) { | |
| DeleteRow(rowFrom, rows); | |
| } | |
| /// <summary> | |
| /// Get the cell value from thw worksheet | |
| /// </summary> | |
| /// <param name="row">The row number</param> | |
| /// <param name="column">The row number</param> | |
| /// <returns>The value</returns> | |
| public object GetValue(int row, int column) { | |
| CheckSheetType(); | |
| var v = _values.GetValue(row, column); | |
| if (v != null) { | |
| if (_flags.GetFlagValue(row, column, CellFlags.RichText)) { | |
| return Cells[row, column].RichText.Text; | |
| } | |
| return v; | |
| } | |
| return null; | |
| } | |
| /// <summary> | |
| /// Get a strongly typed cell value from the worksheet | |
| /// </summary> | |
| /// <typeparam name="T">The type</typeparam> | |
| /// <param name="row">The row number</param> | |
| /// <param name="column">The row number</param> | |
| /// <returns>The value. If the value can't be converted to the specified type, the default value will be returned</returns> | |
| public T GetValue<T>(int row, int column) { | |
| CheckSheetType(); | |
| //ulong cellID=ExcelCellBase.GetCellID(SheetID, Row, Column); | |
| var v = _values.GetValue(row, column); | |
| if (v == null) { | |
| return default(T); | |
| } | |
| //var cell=((ExcelCell)_cells[cellID]); | |
| if (_flags.GetFlagValue(row, column, CellFlags.RichText)) { | |
| return (T)(object)Cells[row, column].RichText.Text; | |
| } | |
| return GetTypedValue<T>(v); | |
| } | |
| //Thanks to Michael Tran for parts of this method | |
| internal T GetTypedValue<T>(object v) { | |
| if (v == null) { | |
| return default(T); | |
| } | |
| Type fromType = v.GetType(); | |
| Type toType = typeof(T); | |
| if (fromType == toType) { | |
| return (T)v; | |
| } | |
| var cnv = TypeDescriptor.GetConverter(fromType); | |
| if (toType | |
| == typeof(DateTime)) //Handle dates | |
| { | |
| if (fromType == typeof(TimeSpan)) { | |
| return ((T)(object)(new DateTime(((TimeSpan)v).Ticks))); | |
| } | |
| if (fromType == typeof(string)) { | |
| if (DateTime.TryParse(v.ToString(), out var dt)) { | |
| return (T)(object)(dt); | |
| } | |
| return default(T); | |
| } | |
| if (cnv.CanConvertTo(typeof(double))) { | |
| return (T)(object)(DateTime.FromOADate((double)cnv.ConvertTo(v, typeof(double)))); | |
| } | |
| return default(T); | |
| } | |
| if (toType | |
| == typeof(TimeSpan)) //Handle timespan | |
| { | |
| if (fromType == typeof(DateTime)) { | |
| return ((T)(object)(new TimeSpan(((DateTime)v).Ticks))); | |
| } | |
| if (fromType == typeof(string)) { | |
| if (TimeSpan.TryParse(v.ToString(), out var ts)) { | |
| return (T)(object)(ts); | |
| } | |
| return default(T); | |
| } | |
| if (cnv.CanConvertTo(typeof(double))) { | |
| return (T) | |
| (object)(new TimeSpan( | |
| DateTime.FromOADate((double)cnv.ConvertTo(v, typeof(double))).Ticks)); | |
| } | |
| try { | |
| // Issue 14682 -- "GetValue<decimal>() won't convert strings" | |
| // As suggested, after all special cases, all .NET to do it's | |
| // preferred conversion rather than simply returning the default | |
| return (T)Convert.ChangeType(v, typeof(T)); | |
| } catch (Exception) { | |
| // This was the previous behaviour -- no conversion is available. | |
| return default(T); | |
| } | |
| } | |
| if (cnv.CanConvertTo(toType)) { | |
| return (T)cnv.ConvertTo(v, typeof(T)); | |
| } | |
| if (toType.IsGenericType && toType.GetGenericTypeDefinition().Equals(typeof(Nullable<>))) { | |
| toType = Nullable.GetUnderlyingType(toType); | |
| if (cnv.CanConvertTo(toType)) { | |
| return (T)cnv.ConvertTo(v, typeof(T)); | |
| } | |
| } | |
| if (fromType == typeof(double) && toType == typeof(decimal)) { | |
| return (T)(object)Convert.ToDecimal(v); | |
| } | |
| if (fromType == typeof(decimal) && toType == typeof(double)) { | |
| return (T)(object)Convert.ToDouble(v); | |
| } | |
| return default(T); | |
| } | |
| /// <summary> | |
| /// Set the value of a cell | |
| /// </summary> | |
| /// <param name="row">The row number</param> | |
| /// <param name="column">The column number</param> | |
| /// <param name="value">The value</param> | |
| public void SetValue(int row, int column, object value) { | |
| CheckSheetType(); | |
| if (row < 1 || column < 1 || row > ExcelPackage.MaxRows && column > ExcelPackage.MaxColumns) { | |
| throw new ArgumentOutOfRangeException("Row or Column out of range"); | |
| } | |
| _values.SetValue(row, column, value); | |
| } | |
| /// <summary> | |
| /// Set the value of a cell | |
| /// </summary> | |
| /// <param name="address">The Excel address</param> | |
| /// <param name="value">The value</param> | |
| public void SetValue(string address, object value) { | |
| CheckSheetType(); | |
| ExcelCellBase.GetRowCol(address, out var row, out var col, true); | |
| if (row < 1 || col < 1 || row > ExcelPackage.MaxRows && col > ExcelPackage.MaxColumns) { | |
| throw new ArgumentOutOfRangeException("Address is invalid or out of range"); | |
| } | |
| _values.SetValue(row, col, value); | |
| } | |
| /// <summary> | |
| /// Get MergeCell Index No | |
| /// </summary> | |
| /// <param name="row"></param> | |
| /// <param name="column"></param> | |
| /// <returns></returns> | |
| public int GetMergeCellId(int row, int column) { | |
| for (int i = 0; i < _mergedCells.Count; i++) { | |
| if (!string.IsNullOrEmpty(_mergedCells[i])) { | |
| ExcelRange range = Cells[_mergedCells[i]]; | |
| if (range.Start.Row <= row && row <= range.End.Row) { | |
| if (range.Start.Column <= column && column <= range.End.Column) { | |
| return i + 1; | |
| } | |
| } | |
| } | |
| } | |
| return 0; | |
| } | |
| internal void Save() { | |
| DeletePrinterSettings(); | |
| if (!(this is ExcelChartsheet)) { | |
| var d = Dimension; | |
| if (d == null) { | |
| DeleteAllNode("d:dimension/@ref"); | |
| } else { | |
| SetXmlNodeString("d:dimension/@ref", d.Address); | |
| } | |
| SaveTables(); | |
| SavePivotTables(); | |
| } | |
| } | |
| internal void SaveHandler(StreamWriter streamWriter) { | |
| //Create the nodes if they do not exist. | |
| if (this is ExcelChartsheet) { | |
| streamWriter.Write(WorksheetXml.OuterXml); | |
| } else { | |
| CreateNode("d:cols"); | |
| CreateNode("d:sheetData"); | |
| CreateNode("d:mergeCells"); | |
| CreateNode("d:hyperlinks"); | |
| CreateNode("d:rowBreaks"); | |
| CreateNode("d:colBreaks"); | |
| //StreamWriter sw=new StreamWriter(Part.GetStream(FileMode.Create, FileAccess.Write)); | |
| var xml = WorksheetXml.OuterXml; | |
| int colStart = 0, | |
| colEnd = 0; | |
| GetBlockPos(xml, "cols", ref colStart, ref colEnd); | |
| streamWriter.Write(xml.Substring(0, colStart)); | |
| UpdateColumnData(streamWriter); | |
| int cellStart = colEnd, | |
| cellEnd = colEnd; | |
| GetBlockPos(xml, "sheetData", ref cellStart, ref cellEnd); | |
| streamWriter.Write(xml.Substring(colEnd, cellStart - colEnd)); | |
| UpdateRowCellData(streamWriter); | |
| int mergeStart = cellEnd, | |
| mergeEnd = cellEnd; | |
| GetBlockPos(xml, "mergeCells", ref mergeStart, ref mergeEnd); | |
| streamWriter.Write(xml.Substring(cellEnd, mergeStart - cellEnd)); | |
| CleanupMergedCells(_mergedCells); | |
| if (_mergedCells.Count > 0) { | |
| UpdateMergedCells(streamWriter); | |
| } | |
| int hyperStart = mergeEnd, | |
| hyperEnd = mergeEnd; | |
| GetBlockPos(xml, "hyperlinks", ref hyperStart, ref hyperEnd); | |
| streamWriter.Write(xml.Substring(mergeEnd, hyperStart - mergeEnd)); | |
| UpdateHyperLinks(streamWriter); | |
| int rowBreakStart = hyperEnd, | |
| rowBreakEnd = hyperEnd; | |
| GetBlockPos(xml, "rowBreaks", ref rowBreakStart, ref rowBreakEnd); | |
| streamWriter.Write(xml.Substring(hyperEnd, rowBreakStart - hyperEnd)); | |
| UpdateRowBreaks(streamWriter); | |
| int colBreakStart = rowBreakEnd, | |
| colBreakEnd = rowBreakEnd; | |
| GetBlockPos(xml, "colBreaks", ref colBreakStart, ref colBreakEnd); | |
| streamWriter.Write(xml.Substring(rowBreakEnd, colBreakStart - rowBreakEnd)); | |
| UpdateColBreaks(streamWriter); | |
| streamWriter.Write(xml.Substring(colBreakEnd, xml.Length - colBreakEnd)); | |
| } | |
| } | |
| /// <summary> | |
| /// Delete the printersettings relationship and part. | |
| /// </summary> | |
| private void DeletePrinterSettings() { | |
| //Delete the relationship from the pageSetup tag | |
| XmlAttribute attr = (XmlAttribute) | |
| WorksheetXml.SelectSingleNode("//d:pageSetup/@r:id", NameSpaceManager); | |
| if (attr != null) { | |
| string relId = attr.Value; | |
| //First delete the attribute from the XML | |
| attr.OwnerElement.Attributes.Remove(attr); | |
| if (Part.RelationshipExists(relId)) { | |
| var rel = Part.GetRelationship(relId); | |
| Uri printerSettingsUri = UriHelper.ResolvePartUri(rel.SourceUri, rel.TargetUri); | |
| Part.DeleteRelationship(rel.Id); | |
| //Delete the part from the package | |
| if (_package.Package.PartExists(printerSettingsUri)) { | |
| _package.Package.DeletePart(printerSettingsUri); | |
| } | |
| } | |
| } | |
| } | |
| /// <summary> | |
| /// Save all table data | |
| /// </summary> | |
| private void SaveTables() { | |
| foreach (var tbl in Tables) { | |
| if (tbl.ShowHeader || tbl.ShowTotal) { | |
| int colNum = tbl.Address._fromCol; | |
| var colVal = new HashSet<string>(); | |
| foreach (var col in tbl.Columns) { | |
| string n = col.Name.ToLower(CultureInfo.InvariantCulture); | |
| if (tbl.ShowHeader) { | |
| n = tbl.WorkSheet.GetValue<string>( | |
| tbl.Address._fromRow, | |
| tbl.Address._fromCol + col.Position); | |
| if (string.IsNullOrEmpty(n)) { | |
| n = col.Name.ToLower(CultureInfo.InvariantCulture); | |
| } else { | |
| col.Name = n; | |
| } | |
| } else { | |
| n = col.Name.ToLower(CultureInfo.InvariantCulture); | |
| } | |
| if (colVal.Contains(n)) { | |
| throw (new InvalidDataException( | |
| string.Format( | |
| "Table {0} Column {1} does not have a unique name.", | |
| tbl.Name, | |
| col.Name))); | |
| } | |
| colVal.Add(n); | |
| col.Name = ConvertUtil.ExcelEncodeString(col.Name); | |
| if (tbl.ShowHeader) { | |
| _values.SetValue(tbl.Address._fromRow, colNum, col.Name); | |
| } | |
| if (tbl.ShowTotal) { | |
| SetTableTotalFunction(tbl, col, colNum); | |
| } | |
| if (!string.IsNullOrEmpty(col.CalculatedColumnFormula)) { | |
| int fromRow = tbl.ShowHeader ? tbl.Address._fromRow + 1 : tbl.Address._fromRow; | |
| int toRow = tbl.ShowTotal ? tbl.Address._toRow - 1 : tbl.Address._toRow; | |
| for (int row = fromRow; row <= toRow; row++) { | |
| //Cell(row, colNum).Formula = col.CalculatedColumnFormula; | |
| SetFormula(row, colNum, col.CalculatedColumnFormula); | |
| } | |
| } | |
| colNum++; | |
| } | |
| } | |
| } | |
| } | |
| internal void SetTableTotalFunction(ExcelTable tbl, ExcelTableColumn col, int colNum = -1) { | |
| if (tbl.ShowTotal == false) { | |
| return; | |
| } | |
| if (colNum == -1) { | |
| for (int i = 0; i < tbl.Columns.Count; i++) { | |
| if (tbl.Columns[i].Name == col.Name) { | |
| colNum = tbl.Address._fromCol + i; | |
| } | |
| } | |
| } | |
| if (col.TotalsRowFunction == RowFunctions.Custom) { | |
| SetFormula(tbl.Address._toRow, colNum, col.TotalsRowFormula); | |
| } else if (col.TotalsRowFunction != RowFunctions.None) { | |
| switch (col.TotalsRowFunction) { | |
| case RowFunctions.Average: | |
| SetFormula(tbl.Address._toRow, colNum, GetTotalFunction(col, "101")); | |
| break; | |
| case RowFunctions.Count: | |
| SetFormula(tbl.Address._toRow, colNum, GetTotalFunction(col, "102")); | |
| break; | |
| case RowFunctions.CountNums: | |
| SetFormula(tbl.Address._toRow, colNum, GetTotalFunction(col, "103")); | |
| break; | |
| case RowFunctions.Max: | |
| SetFormula(tbl.Address._toRow, colNum, GetTotalFunction(col, "104")); | |
| break; | |
| case RowFunctions.Min: | |
| SetFormula(tbl.Address._toRow, colNum, GetTotalFunction(col, "105")); | |
| break; | |
| case RowFunctions.StdDev: | |
| SetFormula(tbl.Address._toRow, colNum, GetTotalFunction(col, "107")); | |
| break; | |
| case RowFunctions.Var: | |
| SetFormula(tbl.Address._toRow, colNum, GetTotalFunction(col, "110")); | |
| break; | |
| case RowFunctions.Sum: | |
| SetFormula(tbl.Address._toRow, colNum, GetTotalFunction(col, "109")); | |
| break; | |
| default: | |
| throw (new("Unknown RowFunction enum")); | |
| } | |
| } else { | |
| _values.SetValue(tbl.Address._toRow, colNum, col.TotalsRowLabel); | |
| } | |
| } | |
| internal void SetFormula(int row, int col, object value) { | |
| _formulas.SetValue(row, col, value); | |
| if (!_values.Exists(row, col)) { | |
| _values.SetValue(row, col, null); | |
| } | |
| } | |
| internal void SetStyle(int row, int col, int value) { | |
| _styles.SetValue(row, col, value); | |
| if (!_values.Exists(row, col)) { | |
| _values.SetValue(row, col, null); | |
| } | |
| } | |
| private void SavePivotTables() { | |
| foreach (var pt in PivotTables) { | |
| if (pt.DataFields.Count > 1) { | |
| XmlElement parentNode; | |
| if (pt.DataOnRows) { | |
| parentNode = | |
| pt.PivotTableXml.SelectSingleNode("//d:rowFields", pt.NameSpaceManager) as XmlElement; | |
| if (parentNode == null) { | |
| pt.CreateNode("d:rowFields"); | |
| parentNode = | |
| pt.PivotTableXml.SelectSingleNode("//d:rowFields", pt.NameSpaceManager) | |
| as XmlElement; | |
| } | |
| } else { | |
| parentNode = | |
| pt.PivotTableXml.SelectSingleNode("//d:colFields", pt.NameSpaceManager) as XmlElement; | |
| if (parentNode == null) { | |
| pt.CreateNode("d:colFields"); | |
| parentNode = | |
| pt.PivotTableXml.SelectSingleNode("//d:colFields", pt.NameSpaceManager) | |
| as XmlElement; | |
| } | |
| } | |
| if (parentNode.SelectSingleNode("d:field[@ x= \"-2\"]", pt.NameSpaceManager) == null) { | |
| XmlElement fieldNode = pt.PivotTableXml.CreateElement("field", ExcelPackage._schemaMain); | |
| fieldNode.SetAttribute("x", "-2"); | |
| parentNode.AppendChild(fieldNode); | |
| } | |
| } | |
| var ws = Workbook.Worksheets[pt.CacheDefinition.SourceRange.WorkSheet]; | |
| var t = ws.Tables.GetFromRange(pt.CacheDefinition.SourceRange); | |
| var fields = pt.CacheDefinition.CacheDefinitionXml.SelectNodes( | |
| "d:pivotCacheDefinition/d:cacheFields/d:cacheField", | |
| NameSpaceManager); | |
| int ix = 0; | |
| if (fields != null) { | |
| var flds = new HashSet<string>(); | |
| foreach (XmlElement node in fields) { | |
| if (ix >= pt.CacheDefinition.SourceRange.Columns) { | |
| break; | |
| } | |
| var fldName = node.GetAttribute("name"); //Fixes issue 15295 dup name error | |
| if (string.IsNullOrEmpty(fldName)) { | |
| fldName = | |
| (t == null | |
| ? pt.CacheDefinition.SourceRange.Offset(0, ix++, 1, 1).Value.ToString() | |
| : t.Columns[ix++].Name); | |
| } | |
| if (flds.Contains(fldName)) { | |
| fldName = GetNewName(flds, fldName); | |
| } | |
| flds.Add(fldName); | |
| node.SetAttribute("name", fldName); | |
| } | |
| foreach (var df in pt.DataFields) { | |
| if (string.IsNullOrEmpty(df.Name)) { | |
| string name; | |
| if (df.Function == DataFieldFunctions.None) { | |
| name = df.Field.Name; //Name must be set or Excel will crash on rename. | |
| } else { | |
| name = df.Function + " of " + df.Field.Name; //Name must be set or Excel will crash on rename. | |
| } | |
| //Make sure name is unique | |
| var newName = name; | |
| var i = 2; | |
| while (pt.DataFields.ExistsDfName(newName, df)) { | |
| newName = name + (i++).ToString(CultureInfo.InvariantCulture); | |
| } | |
| df.Name = newName; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| private string GetNewName(HashSet<string> flds, string fldName) { | |
| int ix = 2; | |
| while (flds.Contains(fldName + ix.ToString(CultureInfo.InvariantCulture))) { | |
| ix++; | |
| } | |
| return fldName + ix.ToString(CultureInfo.InvariantCulture); | |
| } | |
| private static string GetTotalFunction(ExcelTableColumn col, string functionNum) { | |
| return string.Format("SUBTOTAL({0},{1}[{2}])", functionNum, col._tbl.Name, col.Name); | |
| } | |
| private void CleanupMergedCells(MergeCellsCollection mergedCells) { | |
| int i = 0; | |
| while (i < mergedCells.List.Count) { | |
| if (mergedCells[i] == null) { | |
| mergedCells.List.RemoveAt(i); | |
| } else { | |
| i++; | |
| } | |
| } | |
| } | |
| private void UpdateColBreaks(StreamWriter sw) { | |
| StringBuilder breaks = new StringBuilder(); | |
| int count = 0; | |
| var cse = new CellsStoreEnumerator<object>(_values, 0, 0, 0, ExcelPackage.MaxColumns); | |
| while (cse.Next()) { | |
| var col = cse.Value as ExcelColumn; | |
| if (col != null && col.PageBreak) { | |
| breaks.AppendFormat("<brk id=\"{0}\" max=\"16383\" man=\"1\" />", cse.Column); | |
| count++; | |
| } | |
| } | |
| if (count > 0) { | |
| sw.Write("<colBreaks count=\"{0}\" manualBreakCount=\"{0}\">{1}</colBreaks>", count, breaks); | |
| } | |
| } | |
| private void UpdateRowBreaks(StreamWriter sw) { | |
| StringBuilder breaks = new StringBuilder(); | |
| int count = 0; | |
| var cse = new CellsStoreEnumerator<object>(_values, 0, 0, ExcelPackage.MaxRows, 0); | |
| //foreach(ExcelRow row in _rows) | |
| while (cse.Next()) { | |
| var row = cse.Value as RowInternal; | |
| if (row != null && row.PageBreak) { | |
| breaks.AppendFormat("<brk id=\"{0}\" max=\"1048575\" man=\"1\" />", cse.Row); | |
| count++; | |
| } | |
| } | |
| if (count > 0) { | |
| sw.Write("<rowBreaks count=\"{0}\" manualBreakCount=\"{0}\">{1}</rowBreaks>", count, breaks); | |
| } | |
| } | |
| /// <summary> | |
| /// Inserts the cols collection into the XML document | |
| /// </summary> | |
| private void UpdateColumnData(StreamWriter sw) { | |
| var cse = new CellsStoreEnumerator<object>(_values, 0, 1, 0, ExcelPackage.MaxColumns); | |
| bool first = true; | |
| while (cse.Next()) { | |
| if (first) { | |
| sw.Write("<cols>"); | |
| first = false; | |
| } | |
| var col = cse.Value as ExcelColumn; | |
| ExcelStyleCollection<ExcelXfs> cellXfs = _workbook.Styles.CellXfs; | |
| sw.Write("<col min=\"{0}\" max=\"{1}\"", col.ColumnMin, col.ColumnMax); | |
| if (col.Hidden) { | |
| //sbXml.Append(" width=\"0\" hidden=\"1\" customWidth=\"1\""); | |
| sw.Write(" hidden=\"1\""); | |
| } else if (col.BestFit) { | |
| sw.Write(" bestFit=\"1\""); | |
| } | |
| sw.Write( | |
| string.Format( | |
| CultureInfo.InvariantCulture, | |
| " width=\"{0}\" customWidth=\"1\"", | |
| col.Width)); | |
| if (col.OutlineLevel > 0) { | |
| sw.Write(" outlineLevel=\"{0}\" ", col.OutlineLevel); | |
| if (col.Collapsed) { | |
| if (col.Hidden) { | |
| sw.Write(" collapsed=\"1\""); | |
| } else { | |
| sw.Write(" collapsed=\"1\" hidden=\"1\""); //Always hidden | |
| } | |
| } | |
| } | |
| if (col.Phonetic) { | |
| sw.Write(" phonetic=\"1\""); | |
| } | |
| var styleId = col.StyleID >= 0 ? cellXfs[col.StyleID].newID : col.StyleID; | |
| if (styleId > 0) { | |
| sw.Write(" style=\"{0}\"", styleId); | |
| } | |
| sw.Write(" />"); | |
| } | |
| if (!first) { | |
| sw.Write("</cols>"); | |
| } | |
| } | |
| /// <summary> | |
| /// Insert row and cells into the XML document | |
| /// </summary> | |
| private void UpdateRowCellData(StreamWriter sw) { | |
| ExcelStyleCollection<ExcelXfs> cellXfs = _workbook.Styles.CellXfs; | |
| int row = -1; | |
| var ss = _workbook._sharedStrings; | |
| var styles = _workbook.Styles; | |
| var cache = new StringBuilder(); | |
| cache.Append("<sheetData>"); | |
| //Set a value for cells with style and no value set. | |
| var cseStyle = new CellsStoreEnumerator<int>( | |
| _styles, | |
| 0, | |
| 0, | |
| ExcelPackage.MaxRows, | |
| ExcelPackage.MaxColumns); | |
| foreach (var s in cseStyle) { | |
| if (!_values.Exists(cseStyle.Row, cseStyle.Column)) { | |
| _values.SetValue(cseStyle.Row, cseStyle.Column, null); | |
| } | |
| } | |
| var cse = new CellsStoreEnumerator<object>( | |
| _values, | |
| 1, | |
| 0, | |
| ExcelPackage.MaxRows, | |
| ExcelPackage.MaxColumns); | |
| //foreach (IRangeID r in _cells) | |
| while (cse.Next()) { | |
| if (cse.Column > 0) { | |
| int styleId = cellXfs[styles.GetStyleId(this, cse.Row, cse.Column)].newID; | |
| //Add the row element if it's a new row | |
| if (cse.Row != row) { | |
| WriteRow(cache, cellXfs, row, cse.Row); | |
| row = cse.Row; | |
| } | |
| object v = cse.Value; | |
| object formula = _formulas.GetValue(cse.Row, cse.Column); | |
| if (formula is int sfId) { | |
| var f = _sharedFormulas[sfId]; | |
| if (f.Address.IndexOf(':') > 0) { | |
| if (f.StartCol == cse.Column && f.StartRow == cse.Row) { | |
| if (f.IsArray) { | |
| cache.AppendFormat( | |
| "<c r=\"{0}\" s=\"{1}\"{5}><f ref=\"{2}\" t=\"array\">{3}</f>{4}</c>", | |
| cse.CellAddress, | |
| styleId < 0 ? 0 : styleId, | |
| f.Address, | |
| SecurityElement.Escape(f.Formula), | |
| GetFormulaValue(v), | |
| GetCellType(v, true)); | |
| } else { | |
| cache.AppendFormat( | |
| "<c r=\"{0}\" s=\"{1}\"{6}><f ref=\"{2}\" t=\"shared\" si=\"{3}\">{4}</f>{5}</c>", | |
| cse.CellAddress, | |
| styleId < 0 ? 0 : styleId, | |
| f.Address, | |
| sfId, | |
| SecurityElement.Escape(f.Formula), | |
| GetFormulaValue(v), | |
| GetCellType(v, true)); | |
| } | |
| } else if (f.IsArray) { | |
| cache.AppendFormat( | |
| "<c r=\"{0}\" s=\"{1}\" />", | |
| cse.CellAddress, | |
| styleId < 0 ? 0 : styleId); | |
| } else { | |
| cache.AppendFormat( | |
| "<c r=\"{0}\" s=\"{1}\"{4}><f t=\"shared\" si=\"{2}\" />{3}</c>", | |
| cse.CellAddress, | |
| styleId < 0 ? 0 : styleId, | |
| sfId, | |
| GetFormulaValue(v), | |
| GetCellType(v, true)); | |
| } | |
| } else { | |
| // We can also have a single cell array formula | |
| if (f.IsArray) { | |
| cache.AppendFormat( | |
| "<c r=\"{0}\" s=\"{1}\"{5}><f ref=\"{2}\" t=\"array\">{3}</f>{4}</c>", | |
| cse.CellAddress, | |
| styleId < 0 ? 0 : styleId, | |
| string.Format("{0}:{1}", f.Address, f.Address), | |
| SecurityElement.Escape(f.Formula), | |
| GetFormulaValue(v), | |
| GetCellType(v, true)); | |
| } else { | |
| cache.AppendFormat("<c r=\"{0}\" s=\"{1}\">", f.Address, styleId < 0 ? 0 : styleId); | |
| cache.AppendFormat( | |
| "<f>{0}</f>{1}</c>", | |
| SecurityElement.Escape(f.Formula), | |
| GetFormulaValue(v)); | |
| } | |
| } | |
| } else if (formula != null && formula.ToString() != "") { | |
| cache.AppendFormat( | |
| "<c r=\"{0}\" s=\"{1}\"{2}>", | |
| cse.CellAddress, | |
| styleId < 0 ? 0 : styleId, | |
| GetCellType(v, true)); | |
| cache.AppendFormat( | |
| "<f>{0}</f>{1}</c>", | |
| SecurityElement.Escape(formula.ToString()), | |
| GetFormulaValue(v)); | |
| } else { | |
| if (v == null && styleId > 0) { | |
| cache.AppendFormat( | |
| "<c r=\"{0}\" s=\"{1}\" />", | |
| cse.CellAddress, | |
| styleId < 0 ? 0 : styleId); | |
| } else if (v != null) { | |
| if ((v.GetType().IsPrimitive | |
| || v is double | |
| || v is decimal | |
| || v is DateTime | |
| || v is TimeSpan)) { | |
| //string sv = GetValueForXml(v); | |
| cache.AppendFormat( | |
| "<c r=\"{0}\" s=\"{1}\" {2}>", | |
| cse.CellAddress, | |
| styleId < 0 ? 0 : styleId, | |
| GetCellType(v)); | |
| cache.AppendFormat("{0}</c>", GetFormulaValue(v)); | |
| } else { | |
| int ix; | |
| if (!ss.ContainsKey(v.ToString())) { | |
| ix = ss.Count; | |
| ss.Add( | |
| v.ToString(), | |
| new() { | |
| isRichText = _flags.GetFlagValue(cse.Row, cse.Column, CellFlags.RichText), | |
| pos = ix, | |
| }); | |
| } else { | |
| ix = ss[v.ToString()].pos; | |
| } | |
| cache.AppendFormat( | |
| "<c r=\"{0}\" s=\"{1}\" t=\"s\">", | |
| cse.CellAddress, | |
| styleId < 0 ? 0 : styleId); | |
| cache.AppendFormat("<v>{0}</v></c>", ix); | |
| } | |
| } | |
| } | |
| } else //ExcelRow | |
| { | |
| WriteRow(cache, cellXfs, row, cse.Row); | |
| row = cse.Row; | |
| } | |
| if (cache.Length > 0x600000) { | |
| sw.Write(cache.ToString()); | |
| cache = new(); | |
| } | |
| } | |
| if (row != -1) { | |
| cache.Append("</row>"); | |
| } | |
| cache.Append("</sheetData>"); | |
| sw.Write(cache.ToString()); | |
| sw.Flush(); | |
| } | |
| private object GetFormulaValue(object v) { | |
| if (v != null && v.ToString() != "") { | |
| return "<v>" + SecurityElement.Escape(GetValueForXml(v)) + "</v>"; //Fixes issue 15071 | |
| } | |
| return ""; | |
| } | |
| private string GetCellType(object v, bool allowStr = false) { | |
| if (v is bool) { | |
| return " t=\"b\""; | |
| } | |
| if ((v is double d && double.IsInfinity(d)) || v is ExcelErrorValue) { | |
| return " t=\"e\""; | |
| } | |
| if (allowStr | |
| && v != null | |
| && !(v.GetType().IsPrimitive | |
| || v is double | |
| || v is decimal | |
| || v is DateTime | |
| || v is TimeSpan)) { | |
| return " t=\"str\""; | |
| } | |
| return ""; | |
| } | |
| private string GetValueForXml(object v) { | |
| string s; | |
| try { | |
| if (v is DateTime time) { | |
| double sdv = time.ToOADate(); | |
| if (Workbook.Date1904) { | |
| sdv -= ExcelWorkbook._date1904Offset; | |
| } | |
| s = sdv.ToString(CultureInfo.InvariantCulture); | |
| } else if (v is TimeSpan span) { | |
| s = new DateTime(span.Ticks).ToOADate().ToString(CultureInfo.InvariantCulture); | |
| ; | |
| } else if (v.GetType().IsPrimitive || v is double || v is decimal) { | |
| if (v is double d && double.IsNaN(d)) { | |
| s = ""; | |
| } else if (v is double d1 && double.IsInfinity(d1)) { | |
| s = "#NUM!"; | |
| } else { | |
| s = Convert | |
| .ToDouble(v, CultureInfo.InvariantCulture) | |
| .ToString("R15", CultureInfo.InvariantCulture); | |
| } | |
| } else { | |
| s = v.ToString(); | |
| } | |
| } catch { | |
| s = "0"; | |
| } | |
| return s; | |
| } | |
| private void WriteRow( | |
| StringBuilder cache, | |
| ExcelStyleCollection<ExcelXfs> cellXfs, | |
| int prevRow, | |
| int row) { | |
| if (prevRow != -1) { | |
| cache.Append("</row>"); | |
| } | |
| //ulong rowID = ExcelRow.GetRowID(SheetID, row); | |
| cache.AppendFormat("<row r=\"{0}\" ", row); | |
| RowInternal currRow = _values.GetValue(row, 0) as RowInternal; | |
| if (currRow != null) { | |
| if (currRow.Hidden) { | |
| cache.Append("ht=\"0\" hidden=\"1\" "); | |
| } else if (currRow.Height != DefaultRowHeight && currRow.Height >= 0) { | |
| cache.AppendFormat(CultureInfo.InvariantCulture, "ht=\"{0}\" ", currRow.Height); | |
| if (currRow.CustomHeight) { | |
| cache.Append("customHeight=\"1\" "); | |
| } | |
| } | |
| if (currRow.OutlineLevel > 0) { | |
| cache.AppendFormat("outlineLevel =\"{0}\" ", currRow.OutlineLevel); | |
| if (currRow.Collapsed) { | |
| if (currRow.Hidden) { | |
| cache.Append(" collapsed=\"1\" "); | |
| } else { | |
| cache.Append(" collapsed=\"1\" hidden=\"1\" "); //Always hidden | |
| } | |
| } | |
| } | |
| if (currRow.Phonetic) { | |
| cache.Append("ph=\"1\" "); | |
| } | |
| } | |
| var s = _styles.GetValue(row, 0); | |
| if (s > 0) { | |
| cache.AppendFormat("s=\"{0}\" customFormat=\"1\"", cellXfs[s].newID); | |
| } | |
| cache.Append(">"); | |
| } | |
| /// <summary> | |
| /// Update xml with hyperlinks | |
| /// </summary> | |
| /// <param name="sw">The stream</param> | |
| private void UpdateHyperLinks(StreamWriter sw) { | |
| Dictionary<string, string> hyps = new Dictionary<string, string>(); | |
| var cse = new CellsStoreEnumerator<Uri>(_hyperLinks); | |
| bool first = true; | |
| //foreach (ulong cell in _hyperLinks) | |
| while (cse.Next()) { | |
| if (first) { | |
| sw.Write("<hyperlinks>"); | |
| first = false; | |
| } | |
| //int row, col; | |
| var uri = _hyperLinks.GetValue(cse.Row, cse.Column); | |
| //ExcelCell cell = _cells[cellId] as ExcelCell; | |
| if (uri is ExcelHyperLink link && !string.IsNullOrEmpty(link.ReferenceAddress)) { | |
| sw.Write( | |
| "<hyperlink ref=\"{0}\" location=\"{1}\" {2}{3}/>", | |
| Cells[cse.Row, cse.Column, cse.Row + link.RowSpann, cse.Column + link.ColSpann].Address, | |
| ExcelCellBase.GetFullAddress( | |
| SecurityElement.Escape(Name), | |
| SecurityElement.Escape(link.ReferenceAddress)), | |
| string.IsNullOrEmpty(link.Display) | |
| ? "" | |
| : "display=\"" + SecurityElement.Escape(link.Display) + "\" ", | |
| string.IsNullOrEmpty(link.ToolTip) | |
| ? "" | |
| : "tooltip=\"" + SecurityElement.Escape(link.ToolTip) + "\" "); | |
| } else if (uri != null) { | |
| Uri hyp; | |
| if (uri is ExcelHyperLink hyperLink) { | |
| hyp = hyperLink.OriginalUri; | |
| } else { | |
| hyp = uri; | |
| } | |
| if (!hyps.ContainsKey(hyp.OriginalString)) { | |
| var relationship = Part.CreateRelationship( | |
| hyp, | |
| TargetMode.External, | |
| ExcelPackage._schemaHyperlink); | |
| if (uri is ExcelHyperLink hl) { | |
| sw.Write( | |
| "<hyperlink ref=\"{0}\" {2}{3}r:id=\"{1}\" />", | |
| ExcelCellBase.GetAddress(cse.Row, cse.Column), | |
| relationship.Id, | |
| string.IsNullOrEmpty(hl.Display) | |
| ? "" | |
| : "display=\"" + SecurityElement.Escape(hl.Display) + "\" ", | |
| string.IsNullOrEmpty(hl.ToolTip) | |
| ? "" | |
| : "tooltip=\"" + SecurityElement.Escape(hl.ToolTip) + "\" "); | |
| } else { | |
| sw.Write( | |
| "<hyperlink ref=\"{0}\" r:id=\"{1}\" />", | |
| ExcelCellBase.GetAddress(cse.Row, cse.Column), | |
| relationship.Id); | |
| } | |
| } | |
| } | |
| } | |
| if (!first) { | |
| sw.Write("</hyperlinks>"); | |
| } | |
| } | |
| /// <summary> | |
| /// Dimension address for the worksheet. | |
| /// Top left cell to Bottom right. | |
| /// If the worksheet has no cells, null is returned | |
| /// </summary> | |
| public ExcelAddressBase Dimension { | |
| get { | |
| CheckSheetType(); | |
| if (_values.GetDimension(out var fromRow, out var fromCol, out var toRow, out var toCol)) { | |
| var addr = new ExcelAddressBase(fromRow, fromCol, toRow, toCol); | |
| addr._ws = Name; | |
| return addr; | |
| } | |
| return null; | |
| } | |
| } | |
| private ExcelSheetProtection _protection; | |
| /// <summary> | |
| /// Access to sheet protection properties | |
| /// </summary> | |
| public ExcelSheetProtection Protection { | |
| get { | |
| if (_protection == null) { | |
| _protection = new(NameSpaceManager, TopNode); | |
| } | |
| return _protection; | |
| } | |
| } | |
| private ExcelProtectedRangeCollection _protectedRanges; | |
| public ExcelProtectedRangeCollection ProtectedRanges { | |
| get { | |
| if (_protectedRanges == null) { | |
| _protectedRanges = new(NameSpaceManager, TopNode); | |
| } | |
| return _protectedRanges; | |
| } | |
| } | |
| private ExcelTableCollection _tables; | |
| /// <summary> | |
| /// Tables defined in the worksheet. | |
| /// </summary> | |
| public ExcelTableCollection Tables { | |
| get { | |
| CheckSheetType(); | |
| if (Workbook._nextTableID == int.MinValue) { | |
| Workbook.ReadAllTables(); | |
| } | |
| if (_tables == null) { | |
| _tables = new(this); | |
| } | |
| return _tables; | |
| } | |
| } | |
| private ExcelPivotTableCollection _pivotTables; | |
| /// <summary> | |
| /// Pivottables defined in the worksheet. | |
| /// </summary> | |
| public ExcelPivotTableCollection PivotTables { | |
| get { | |
| CheckSheetType(); | |
| if (_pivotTables == null) { | |
| if (Workbook._nextPivotTableID == int.MinValue) { | |
| Workbook.ReadAllTables(); | |
| } | |
| _pivotTables = new(this); | |
| } | |
| return _pivotTables; | |
| } | |
| } | |
| private ExcelConditionalFormattingCollection _conditionalFormatting; | |
| /// <summary> | |
| /// ConditionalFormatting defined in the worksheet. Use the Add methods to create ConditionalFormatting and add them to the worksheet. Then | |
| /// set the properties on the instance returned. | |
| /// </summary> | |
| /// <seealso cref="ExcelConditionalFormattingCollection"/> | |
| public ExcelConditionalFormattingCollection ConditionalFormatting { | |
| get { | |
| CheckSheetType(); | |
| if (_conditionalFormatting == null) { | |
| _conditionalFormatting = new(this); | |
| } | |
| return _conditionalFormatting; | |
| } | |
| } | |
| private ExcelDataValidationCollection _dataValidation; | |
| /// <summary> | |
| /// DataValidation defined in the worksheet. Use the Add methods to create DataValidations and add them to the worksheet. Then | |
| /// set the properties on the instance returned. | |
| /// </summary> | |
| /// <seealso cref="ExcelDataValidationCollection"/> | |
| public ExcelDataValidationCollection DataValidations { | |
| get { | |
| CheckSheetType(); | |
| if (_dataValidation == null) { | |
| _dataValidation = new(this); | |
| } | |
| return _dataValidation; | |
| } | |
| } | |
| /// <summary> | |
| /// Returns the style ID given a style name. | |
| /// The style ID will be created if not found, but only if the style name exists! | |
| /// </summary> | |
| /// <param name="styleName"></param> | |
| /// <returns></returns> | |
| internal int GetStyleId(string styleName) { | |
| ExcelNamedStyleXml namedStyle = null; | |
| Workbook.Styles.NamedStyles.FindById(styleName, ref namedStyle); | |
| if (namedStyle.XfId == int.MinValue) { | |
| namedStyle.XfId = Workbook.Styles.CellXfs.FindIndexById(namedStyle.Style.Id); | |
| } | |
| return namedStyle.XfId; | |
| } | |
| /// <summary> | |
| /// The workbook object | |
| /// </summary> | |
| public ExcelWorkbook Workbook => _workbook; | |
| /// <summary> | |
| /// Get the next ID from a shared formula or an Array formula | |
| /// Sharedforumlas will have an id from 0-x. Array formula ids start from 0x4000001-. | |
| /// </summary> | |
| /// <param name="isArray">If the formula is an array formula</param> | |
| /// <returns></returns> | |
| internal int GetMaxShareFunctionIndex(bool isArray) { | |
| int i = _sharedFormulas.Count + 1; | |
| if (isArray) { | |
| i |= 0x40000000; | |
| } | |
| while (_sharedFormulas.ContainsKey(i)) { | |
| i++; | |
| } | |
| return i; | |
| } | |
| internal void UpdateCellsWithDate1904Setting() { | |
| var cse = new CellsStoreEnumerator<object>(_values); | |
| var offset = Workbook.Date1904 ? -ExcelWorkbook._date1904Offset : ExcelWorkbook._date1904Offset; | |
| while (cse.MoveNext()) { | |
| if (cse.Value is DateTime time) { | |
| try { | |
| double sdv = time.ToOADate(); | |
| sdv += offset; | |
| cse.Value = DateTime.FromOADate(sdv); | |
| } catch {} | |
| } | |
| } | |
| } | |
| public string GetFormula(int row, int col) { | |
| var v = _formulas.GetValue(row, col); | |
| if (v is int i) { | |
| return _sharedFormulas[i].GetFormula(row, col, Name); | |
| } | |
| if (v != null) { | |
| return v.ToString(); | |
| } | |
| return ""; | |
| } | |
| public string GetFormulaR1C1(int row, int col) { | |
| var v = _formulas.GetValue(row, col); | |
| if (v is int i) { | |
| var sf = _sharedFormulas[i]; | |
| return ExcelCellBase.TranslateToR1C1( | |
| Formulas.RemoveDummyFunction(sf.Formula), | |
| sf.StartRow, | |
| sf.StartCol); | |
| } | |
| if (v != null) { | |
| return ExcelCellBase.TranslateToR1C1(Formulas.RemoveDummyFunction(v.ToString()), row, col); | |
| } | |
| return ""; | |
| } | |
| public string GetFormulaR1C1_V1(int row, int col) { | |
| var v = _formulas.GetValue(row, col); | |
| if (v is int i) { | |
| var sf = _sharedFormulas[i]; | |
| return ExcelCellBase.TranslateToR1C1_V1( | |
| Formulas.RemoveDummyFunction(sf.Formula), | |
| sf.StartRow, | |
| sf.StartCol); | |
| } | |
| if (v != null) { | |
| return ExcelCellBase.TranslateToR1C1_V1(Formulas.RemoveDummyFunction(v.ToString()), row, col); | |
| } | |
| return ""; | |
| } | |
| public bool IsArrayFormula(int row, int col) => | |
| _flags.GetFlagValue(row, col, CellFlags.ArrayFormula); | |
| public string GetArrayFormulaAddress(int row, int col) { | |
| var v = _formulas.GetValue(row, col); | |
| if ((v is int i) && (_sharedFormulas[i].IsArray)) { | |
| return _sharedFormulas[i].Address; | |
| } | |
| return ""; | |
| } | |
| public int GetStyleId(int row, int col) { | |
| int styleId = 0; | |
| if (!_styles.Exists(row, col, ref styleId) && !_styles.Exists(row, 0, ref styleId)) { | |
| styleId = _styles.GetValue(0, col); | |
| } | |
| return styleId; | |
| } | |
| /// <summary> | |
| /// Get the ExcelColumn for column (span ColumnMin and ColumnMax) | |
| /// </summary> | |
| /// <param name="column"></param> | |
| /// <returns></returns> | |
| internal ExcelColumn GetColumn(int column) { | |
| var c = _values.GetValue(0, column) as ExcelColumn; | |
| if (c == null) { | |
| int row = 0, | |
| col = column; | |
| if (_values.PrevCell(ref row, ref col)) { | |
| c = _values.GetValue(0, col) as ExcelColumn; | |
| if (c != null && c.ColumnMax >= column) { | |
| return c; | |
| } | |
| return null; | |
| } | |
| } | |
| return c; | |
| } | |
| public bool Equals(ExcelWorksheet x, ExcelWorksheet y) { | |
| return x.Name == y.Name | |
| && x.SheetID == y.SheetID | |
| && x.WorksheetXml.OuterXml == y.WorksheetXml.OuterXml; | |
| } | |
| public int GetHashCode(ExcelWorksheet obj) { | |
| return obj.WorksheetXml.OuterXml.GetHashCode(); | |
| } | |
| } |