| /******************************************************************************* |
| * 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; |
| using OfficeOpenXml.ConditionalFormatting; |
| using OfficeOpenXml.DataValidation; |
| using OfficeOpenXml.Drawing.Vml; |
| using OfficeOpenXml.FormulaParsing.LexicalAnalysis; |
| using OfficeOpenXml.Packaging; |
| using OfficeOpenXml.Style.XmlAccess; |
| using OfficeOpenXml.Table; |
| using OfficeOpenXml.Table.PivotTable; |
| using OfficeOpenXml.Utils; |
| |
| namespace OfficeOpenXml; |
| |
| /// <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 = new(); |
| |
| 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 = new List<int>(); |
| 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 = new List<int>(); |
| 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(); |
| } |
| } |