NanoXLSX.Writer 3.2.1
Loading...
Searching...
No Matches
WorksheetWriter.cs
1/*
2 * NanoXLSX is a small .NET library to generate and read XLSX (Microsoft Excel 2007 or newer) files in an easy and native way
3 * Copyright Raphael Stoeckli © 2026
4 * This library is licensed under the MIT License.
5 * You find a copy of the license in project folder or on: http://opensource.org/licenses/MIT
6 */
7
8using NanoXLSX.Enums;
9using NanoXLSX.Interfaces;
10using NanoXLSX.Interfaces.Writer;
12using NanoXLSX.Registry;
13using NanoXLSX.Registry.Attributes;
14using NanoXLSX.Utils;
15using NanoXLSX.Utils.Xml;
16using System;
17using System.Collections.Generic;
18using System.Globalization;
19using System.Linq;
20using System.Text;
21using static NanoXLSX.Enums.Password;
22
24{
28 [NanoXlsxPlugIn(PlugInUUID = PlugInUUID.WorksheetWriter)]
29 internal class WorksheetWriter : IWorksheetWriter
30 {
31 private XmlElement worksheet;
32 private Worksheet currentWorksheet;
33 private IPasswordWriter passwordWriter;
34 private ISortedMap sharedStrings;
35 private ISharedStringWriter sharedStringWriter;
36
37 #region properties
41 public Workbook Workbook { get; set; }
42
46 public Worksheet CurrentWorksheet
47 {
48 get => currentWorksheet;
49 set
50 {
51 currentWorksheet = value;
52 IPassword passwordInstance = ((Worksheet)CurrentWorksheet).SheetProtectionPassword;
53 this.passwordWriter = PlugInLoader.GetPlugIn<IPasswordWriter>(PlugInUUID.PasswordWriter, new LegacyPasswordWriter());
54 this.passwordWriter.Init(PasswordType.WorksheetProtection, passwordInstance.PasswordHash);
55 }
56 }
57
61 public XmlElement XmlElement => worksheet;
62
63 #endregion
64 #region constructors
65
69 internal WorksheetWriter()
70 {
71 }
72
73 #endregion
74 #region methods
79 public void Init(IBaseWriter baseWriter)
80 {
81 this.Workbook = baseWriter.Workbook;
82 this.sharedStringWriter = baseWriter.SharedStringWriter;
83 this.sharedStrings = this.sharedStringWriter.SharedStrings;
84 }
85
89 public void Execute()
90 {
91 Worksheet ws = currentWorksheet;
92 ws.RecalculateAutoFilter();
93 ws.RecalculateColumns();
94 worksheet = XmlElement.CreateElement("worksheet");
95 worksheet.AddDefaultXmlNameSpace("http://schemas.openxmlformats.org/spreadsheetml/2006/main");
96 worksheet.AddNameSpaceAttribute("mc", "xmlns", "http://schemas.openxmlformats.org/markup-compatibility/2006");
97 worksheet.AddNameSpaceAttribute("x14ac", "xmlns", "http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac");
98 worksheet.AddAttribute("mc:Ignorable", "x14ac");
99 if (ws.GetLastCellAddress().HasValue && ws.GetFirstCellAddress().HasValue)
100 {
101 worksheet.AddChildElementWithAttribute("dimension", "ref", new Range(ws.GetFirstCellAddress().Value, ws.GetLastCellAddress().Value).ToString());
102 }
103 if (ws.SelectedCells.Count > 0 || ws.PaneSplitTopHeight != null || ws.PaneSplitLeftWidth != null || ws.PaneSplitAddress != null ||
104 ws.Hidden || ws.ZoomFactor != 100 || ws.ZoomFactors.Count > 1 || !ws.ShowGridLines || !ws.ShowRuler || !ws.ShowRowColumnHeaders || ws.ViewType != Worksheet.SheetViewType.Normal)
105 {
106 worksheet.AddChildElement(CreateSheetViewElement(ws));
107 }
108 XmlElement sheetFormatPr = worksheet.AddChildElement("sheetFormatPr");
109 if (!HasPaneSplitting(ws))
110 {
111 // TODO: Find the right calculation to compensate baseColWidth when using pane splitting
112 sheetFormatPr.AddAttribute("defaultColWidth", ParserUtils.ToString(ws.DefaultColumnWidth));
113 }
114 sheetFormatPr.AddAttribute("defaultRowHeight", ParserUtils.ToString(ws.DefaultRowHeight));
115 sheetFormatPr.AddAttribute("baseColWidth", ParserUtils.ToString(ws.DefaultColumnWidth));
116 sheetFormatPr.AddAttribute("dyDescent", "0.25", "x14ac");
117
118 worksheet.AddChildElement(CreateColsElement(ws));
119
120 XmlElement sheetData = worksheet.AddChildElement("sheetData");
121 sheetData.AddChildElements(CreateRowElements(ws));
122
123 worksheet.AddChildElement(CreateMergedCellsElement(ws));
124 worksheet.AddChildElement(CreateSheetProtectionElement(ws));
125 if (ws.AutoFilterRange != null)
126 {
127 worksheet.AddChildElementWithAttribute("autoFilter", "ref", ws.AutoFilterRange.Value.ToString());
128 }
129
130 WriterPlugInHandler.HandleInlineQueuePlugins(ref worksheet, Workbook, PlugInUUID.WorksheetInlineWriter, currentWorksheet.SheetID);
131 }
132
136 void IWorksheetWriter.ReleaseXmlElement()
137 {
138 this.worksheet = null;
139 }
140
146 private static XmlElement CreateMergedCellsElement(Worksheet worksheet)
147 {
148 if (worksheet.MergedCells.Count < 1)
149 {
150 return null;
151 }
152 XmlElement mergeCells = XmlElement.CreateElementWithAttribute("mergeCells", "count", ParserUtils.ToString(worksheet.MergedCells.Count));
153 foreach (KeyValuePair<string, Range> item in worksheet.MergedCells)
154 {
155 mergeCells.AddChildElementWithAttribute("mergeCell", "ref", item.Value.ToString());
156 }
157 return mergeCells;
158 }
159
165 private XmlElement CreateSheetViewElement(Worksheet worksheet)
166 {
167 XmlElement sheetViews = XmlElement.CreateElement("sheetViews");
168 XmlElement sheetView = sheetViews.AddChildElementWithAttribute("sheetView", "workbookViewId", "0");
169 if (Workbook.SelectedWorksheet == worksheet.SheetID - 1 && !worksheet.Hidden)
170 {
171 sheetView.AddAttribute("tabSelected", "1");
172 }
173 if (worksheet.ViewType != Worksheet.SheetViewType.Normal)
174 {
175 if (worksheet.ViewType == Worksheet.SheetViewType.PageLayout)
176 {
177 if (worksheet.ShowRuler)
178 {
179 sheetView.AddAttribute("showRuler", "1");
180 }
181 else
182 {
183 sheetView.AddAttribute("showRuler", "0");
184 }
185 sheetView.AddAttribute("view", "pageLayout");
186 }
187 else if (worksheet.ViewType == Worksheet.SheetViewType.PageBreakPreview)
188 {
189 sheetView.AddAttribute("view", "pageBreakPreview");
190 }
191 }
192 if (!worksheet.ShowGridLines)
193 {
194 sheetView.AddAttribute("showGridLines", "0");
195 }
196 if (!worksheet.ShowRowColumnHeaders)
197 {
198 sheetView.AddAttribute("showRowColHeaders", "0");
199 }
200 sheetView.AddAttribute("zoomScale", ParserUtils.ToString(worksheet.ZoomFactor));
201 foreach (KeyValuePair<Worksheet.SheetViewType, int> scaleFactor in worksheet.ZoomFactors)
202 {
203 if (scaleFactor.Key == worksheet.ViewType)
204 {
205 continue;
206 }
207 if (scaleFactor.Key == Worksheet.SheetViewType.Normal)
208 {
209 sheetView.AddAttribute("zoomScaleNormal", ParserUtils.ToString(scaleFactor.Value));
210 }
211 else if (scaleFactor.Key == Worksheet.SheetViewType.PageBreakPreview)
212 {
213 sheetView.AddAttribute("zoomScaleSheetLayoutView", ParserUtils.ToString(scaleFactor.Value));
214 }
215 else if (scaleFactor.Key == Worksheet.SheetViewType.PageLayout)
216 {
217 sheetView.AddAttribute("zoomScalePageLayoutView", ParserUtils.ToString(scaleFactor.Value));
218 }
219 }
220 sheetView.AddChildElements(CreatePaneElements(worksheet));
221 if (worksheet.SelectedCells.Count > 0)
222 {
223 XmlElement selection = sheetView.AddChildElement("selection");
224 selection.AddAttribute("activeCell", worksheet.SelectedCells[0].StartAddress.ToString());
225 StringBuilder sb = new StringBuilder(worksheet.SelectedCells.Count * 4);
226 for (int i = 0; i < worksheet.SelectedCells.Count; i++)
227 {
228 sb.Append(worksheet.SelectedCells[i].ToString());
229 if (i < worksheet.SelectedCells.Count - 1)
230 {
231 sb.Append(' ');
232 }
233 }
234 selection.AddAttribute("sqref", sb.ToString());
235 }
236 return sheetViews;
237 }
238
244 private static bool HasPaneSplitting(Worksheet worksheet)
245 {
246 if (worksheet.PaneSplitLeftWidth == null && worksheet.PaneSplitTopHeight == null && worksheet.PaneSplitAddress == null)
247 {
248 return false;
249 }
250 return true;
251 }
252
258 private XmlElement CreateSheetProtectionElement(Worksheet worksheet)
259 {
260 if (!worksheet.UseSheetProtection)
261 {
262 return null;
263 }
264 Dictionary<Worksheet.SheetProtectionValue, int> actualLockingValues = new Dictionary<Worksheet.SheetProtectionValue, int>();
265 if (!worksheet.SheetProtectionValues.Contains(Worksheet.SheetProtectionValue.Objects))
266 {
267 actualLockingValues.Add(Worksheet.SheetProtectionValue.Objects, 1);
268 }
269 if (!worksheet.SheetProtectionValues.Contains(Worksheet.SheetProtectionValue.Scenarios))
270 {
271 actualLockingValues.Add(Worksheet.SheetProtectionValue.Scenarios, 1);
272 }
273 bool allowSelectLocked = worksheet.SheetProtectionValues.Contains(Worksheet.SheetProtectionValue.SelectLockedCells);
274 bool allowSelectUnlocked = worksheet.SheetProtectionValues.Contains(Worksheet.SheetProtectionValue.SelectUnlockedCells);
275 if (allowSelectLocked && !allowSelectUnlocked)
276 {
277 // This shouldn't happen in Excel's UI, but handle it by allowing both
278 allowSelectUnlocked = true;
279 }
280 if (!allowSelectLocked)
281 {
282 actualLockingValues.Add(Worksheet.SheetProtectionValue.SelectLockedCells, 1);
283 }
284 if (!allowSelectUnlocked)
285 {
286 actualLockingValues.Add(Worksheet.SheetProtectionValue.SelectUnlockedCells, 1);
287 }
288 // Explicit permissions (set to 0 when allowed)
289 if (worksheet.SheetProtectionValues.Contains(Worksheet.SheetProtectionValue.FormatCells))
290 {
291 actualLockingValues.Add(Worksheet.SheetProtectionValue.FormatCells, 0);
292 }
293 if (worksheet.SheetProtectionValues.Contains(Worksheet.SheetProtectionValue.FormatColumns))
294 {
295 actualLockingValues.Add(Worksheet.SheetProtectionValue.FormatColumns, 0);
296 }
297 if (worksheet.SheetProtectionValues.Contains(Worksheet.SheetProtectionValue.FormatRows))
298 {
299 actualLockingValues.Add(Worksheet.SheetProtectionValue.FormatRows, 0);
300 }
301 if (worksheet.SheetProtectionValues.Contains(Worksheet.SheetProtectionValue.InsertColumns))
302 {
303 actualLockingValues.Add(Worksheet.SheetProtectionValue.InsertColumns, 0);
304 }
305 if (worksheet.SheetProtectionValues.Contains(Worksheet.SheetProtectionValue.InsertRows))
306 {
307 actualLockingValues.Add(Worksheet.SheetProtectionValue.InsertRows, 0);
308 }
309 if (worksheet.SheetProtectionValues.Contains(Worksheet.SheetProtectionValue.InsertHyperlinks))
310 {
311 actualLockingValues.Add(Worksheet.SheetProtectionValue.InsertHyperlinks, 0);
312 }
313 if (worksheet.SheetProtectionValues.Contains(Worksheet.SheetProtectionValue.DeleteColumns))
314 {
315 actualLockingValues.Add(Worksheet.SheetProtectionValue.DeleteColumns, 0);
316 }
317 if (worksheet.SheetProtectionValues.Contains(Worksheet.SheetProtectionValue.DeleteRows))
318 {
319 actualLockingValues.Add(Worksheet.SheetProtectionValue.DeleteRows, 0);
320 }
321 if (worksheet.SheetProtectionValues.Contains(Worksheet.SheetProtectionValue.Sort))
322 {
323 actualLockingValues.Add(Worksheet.SheetProtectionValue.Sort, 0);
324 }
325 if (worksheet.SheetProtectionValues.Contains(Worksheet.SheetProtectionValue.AutoFilter))
326 {
327 actualLockingValues.Add(Worksheet.SheetProtectionValue.AutoFilter, 0);
328 }
329 if (worksheet.SheetProtectionValues.Contains(Worksheet.SheetProtectionValue.PivotTables))
330 {
331 actualLockingValues.Add(Worksheet.SheetProtectionValue.PivotTables, 0);
332 }
333 XmlElement sheetProtection = XmlElement.CreateElement("sheetProtection");
334 string temp;
335 foreach (KeyValuePair<Worksheet.SheetProtectionValue, int> item in actualLockingValues)
336 {
337 temp = Worksheet.GetSheetProtectionName(item.Key); // Note! If the enum names differs from the OOXML definitions, this method will cause invalid OOXML entries
338 //temp = Enum.GetName(typeof(Worksheet.SheetProtectionValue), item.Key);
339 sheetProtection.AddAttribute(temp, ParserUtils.ToString(item.Value));
340 }
341 if (passwordWriter.PasswordIsSet())
342 {
343 sheetProtection.AddAttributes(passwordWriter.GetAttributes());
344 }
345 sheetProtection.AddAttribute("sheet", "1");
346 return sheetProtection;
347 }
348
354 private static List<DynamicRow> GetSortedSheetData(Worksheet worksheet)
355 {
356 List<Cell> temp = new List<Cell>(worksheet.CellValues);
357 temp.Sort();
358 DynamicRow row = new DynamicRow(); ;
359 Dictionary<int, DynamicRow> rows = new Dictionary<int, DynamicRow>();
360 int rowNumber;
361 if (temp.Count > 0)
362 {
363 rowNumber = temp[0].RowNumber;
364 row.RowNumber = rowNumber;
365 foreach (Cell cell in temp)
366 {
367 if (cell.RowNumber != rowNumber)
368 {
369 rows.Add(rowNumber, row);
370 row = new DynamicRow
371 {
372 RowNumber = cell.RowNumber
373 };
374 rowNumber = cell.RowNumber;
375 }
376 row.CellDefinitions.Add(cell);
377 }
378 if (row.CellDefinitions.Count > 0)
379 {
380 rows.Add(rowNumber, row);
381 }
382 }
383 foreach (KeyValuePair<int, float> rowHeight in worksheet.RowHeights)
384 {
385 if (!rows.ContainsKey(rowHeight.Key))
386 {
387 row = new DynamicRow
388 {
389 RowNumber = rowHeight.Key
390 };
391 rows.Add(rowHeight.Key, row);
392 }
393 }
394 foreach (KeyValuePair<int, bool> hiddenRow in worksheet.HiddenRows)
395 {
396 if (!rows.ContainsKey(hiddenRow.Key))
397 {
398 row = new DynamicRow
399 {
400 RowNumber = hiddenRow.Key
401 };
402 rows.Add(hiddenRow.Key, row);
403 }
404 }
405 List<DynamicRow> output = rows.Values.ToList();
406 output.Sort((r1, r2) => r1.RowNumber.CompareTo(r2.RowNumber)); // Lambda sort
407 return output;
408 }
409
415 private static List<XmlElement> CreatePaneElements(Worksheet worksheet)
416 {
417 if (!HasPaneSplitting(worksheet))
418 {
419 return null;
420 }
421 List<XmlElement> elements = new List<XmlElement>(2);
422 XmlElement pane = XmlElement.CreateElement("pane");
423 bool applyXSplit = false;
424 bool applyYSplit = false;
425 if (worksheet.PaneSplitAddress != null)
426 {
427 bool freeze = worksheet.FreezeSplitPanes != null && worksheet.FreezeSplitPanes.Value;
428 int xSplit = worksheet.PaneSplitAddress.Value.Column;
429 int ySplit = worksheet.PaneSplitAddress.Value.Row;
430 if (xSplit > 0)
431 {
432 if (freeze)
433 {
434 pane.AddAttribute("xSplit", ParserUtils.ToString(xSplit));
435 }
436 else
437 {
438 pane.AddAttribute("xSplit", ParserUtils.ToString(CalculatePaneWidth(worksheet, xSplit)));
439 }
440 applyXSplit = true;
441 }
442 if (ySplit > 0)
443 {
444 if (freeze)
445 {
446 pane.AddAttribute("ySplit", ParserUtils.ToString(ySplit));
447 }
448 else
449 {
450 pane.AddAttribute("ySplit", ParserUtils.ToString(CalculatePaneHeight(worksheet, ySplit)));
451 }
452 applyYSplit = true;
453 }
454 if (freeze && applyXSplit && applyYSplit)
455 {
456 pane.AddAttribute("state", "frozenSplit");
457 }
458 else if (freeze)
459 {
460 pane.AddAttribute("state", "frozen");
461 }
462 }
463 else
464 {
465 if (worksheet.PaneSplitLeftWidth != null)
466 {
467 pane.AddAttribute("xSplit", ParserUtils.ToString(DataUtils.GetInternalPaneSplitWidth(worksheet.PaneSplitLeftWidth.Value)));
468 applyXSplit = true;
469 }
470 if (worksheet.PaneSplitTopHeight != null)
471 {
472 pane.AddAttribute("ySplit", ParserUtils.ToString(DataUtils.GetInternalPaneSplitHeight(worksheet.PaneSplitTopHeight.Value)));
473 applyYSplit = true;
474 }
475 }
476 if ((applyXSplit || applyYSplit) && worksheet.ActivePane != null)
477 {
478 switch (worksheet.ActivePane.Value)
479 {
480 case Worksheet.WorksheetPane.BottomLeft:
481 pane.AddAttribute("activePane", "bottomLeft");
482 break;
483 case Worksheet.WorksheetPane.BottomRight:
484 pane.AddAttribute("activePane", "bottomRight");
485 break;
486 case Worksheet.WorksheetPane.TopLeft:
487 pane.AddAttribute("activePane", "topLeft");
488 break;
489 case Worksheet.WorksheetPane.TopRight:
490 pane.AddAttribute("activePane", "topRight");
491 break;
492 }
493 }
494 string topLeftCell = worksheet.PaneSplitTopLeftCell.Value.GetAddress();
495 pane.AddAttribute("topLeftCell", topLeftCell);
496 elements.Add(pane);
497 if (applyXSplit && !applyYSplit)
498 {
499 XmlElement selection = XmlElement.CreateElement("selection");
500 selection.AddAttribute("pane", "topRight");
501 selection.AddAttribute("activeCell", topLeftCell);
502 selection.AddAttribute("sqref", topLeftCell);
503 elements.Add(selection);
504 }
505 else if (applyYSplit && !applyXSplit)
506 {
507 XmlElement selection = XmlElement.CreateElement("selection");
508 selection.AddAttribute("pane", "bottomLeft");
509 selection.AddAttribute("activeCell", topLeftCell);
510 selection.AddAttribute("sqref", topLeftCell);
511 elements.Add(selection);
512 }
513 else if (applyYSplit && applyXSplit)
514 {
515 XmlElement selection = XmlElement.CreateElement("selection");
516 selection.AddAttribute("activeCell", topLeftCell);
517 selection.AddAttribute("sqref", topLeftCell);
518 elements.Add(selection);
519 }
520 return elements;
521 }
522
529 private static float CalculatePaneHeight(Worksheet worksheet, int numberOfRows)
530 {
531 float height = 0;
532 for (int i = 0; i < numberOfRows; i++)
533 {
534 if (worksheet.RowHeights.ContainsKey(i))
535 {
536 height += DataUtils.GetInternalRowHeight(worksheet.RowHeights[i]);
537 }
538 else
539 {
540 height += DataUtils.GetInternalRowHeight(Worksheet.DefaultWorksheetRowHeight);
541 }
542 }
543 return DataUtils.GetInternalPaneSplitHeight(height);
544 }
545
552 private static float CalculatePaneWidth(Worksheet worksheet, int numberOfColumns)
553 {
554 float width = 0;
555 for (int i = 0; i < numberOfColumns; i++)
556 {
557 if (worksheet.Columns.ContainsKey(i))
558 {
559 width += DataUtils.GetInternalColumnWidth(worksheet.Columns[i].Width);
560 }
561 else
562 {
563 width += DataUtils.GetInternalColumnWidth(Worksheet.DefaultWorksheetColumnWidth);
564 }
565 }
566 // Add padding of 75 per column
567 return DataUtils.GetInternalPaneSplitWidth(width) + ((numberOfColumns - 1) * 0f);
568 }
569
575 private static XmlElement CreateColsElement(Worksheet worksheet)
576 {
577 XmlElement cols = null;
578 if (worksheet.Columns.Count == 0)
579 {
580 return cols;
581 }
582 foreach (KeyValuePair<int, Column> column in worksheet.Columns)
583 {
584 if (Comparators.CompareDimensions(column.Value.Width, worksheet.DefaultColumnWidth) == 0 && !column.Value.IsHidden && column.Value.DefaultColumnStyle == null)
585 {
586 continue;
587 }
588 if (cols == null)
589 {
590 cols = XmlElement.CreateElement("cols");
591 }
592 XmlElement col = cols.AddChildElement("col");
593 col.AddAttribute("width", ParserUtils.ToString(DataUtils.GetInternalColumnWidth(column.Value.Width)));
594 string minMax = ParserUtils.ToString(column.Key + 1); // Add 1 for Address
595 col.AddAttribute("max", minMax);
596 col.AddAttribute("min", minMax);
597 col.AddAttribute("customWidth", "1");
598 if (worksheet.Columns.ContainsKey(column.Key) && worksheet.Columns[column.Key].IsHidden)
599 {
600 col.AddAttribute("hidden", "1");
601 }
602 if (column.Value.DefaultColumnStyle != null)
603 {
604 col.AddAttribute("style", ParserUtils.ToString(column.Value.DefaultColumnStyle.InternalID.Value));
605 }
606 }
607 return cols;
608 }
609
617 private XmlElement CreateRowElement(DynamicRow dynamicRow, Worksheet worksheet)
618 {
619 int rowNumber = dynamicRow.RowNumber;
620 XmlElement row = XmlElement.CreateElementWithAttribute("row", "r", ParserUtils.ToString(rowNumber + 1));
621 if (worksheet.RowHeights.ContainsKey(rowNumber) && Comparators.CompareDimensions(worksheet.RowHeights[rowNumber], worksheet.DefaultRowHeight) != 0)
622 {
623 row.AddAttribute("dyDescent", "0.25", "x14ac");
624 row.AddAttribute("customHeight", "1");
625 row.AddAttribute("ht", ParserUtils.ToString(DataUtils.GetInternalRowHeight(worksheet.RowHeights[rowNumber])));
626 }
627 if (worksheet.HiddenRows.ContainsKey(rowNumber) && worksheet.HiddenRows[rowNumber])
628 {
629 row.AddAttribute("hidden", "1");
630 }
631
632 foreach (Cell item in dynamicRow.CellDefinitions)
633 {
634 string cellValue = null;
635 XmlAttribute? cellStyle = null;
636 XmlAttribute? cellType = null;
637 XmlElement formulaElement = null;
638
639 if (item.CellStyle != null)
640 {
641 cellStyle = XmlAttribute.CreateAttribute("s", ParserUtils.ToString(item.CellStyle.InternalID.Value));
642 }
643 // Data types
644 if (item.DataType == Cell.CellType.Bool)
645 {
646 cellType = XmlAttribute.CreateAttribute("t", "b");
647 cellValue = (bool)item.Value ? "1" : "0";
648 }
649 // Number casting
650 else if (item.DataType == Cell.CellType.Number)
651 {
652 cellType = XmlAttribute.CreateAttribute("t", "n");
653 cellValue = CastNumber(item.Value); // Not a number should not be possible
654 }
655 // Date parsing (style should be on cell)
656 else if (item.DataType == Cell.CellType.Date)
657 {
658 DateTime date = (DateTime)item.Value;
659 cellValue = DataUtils.GetOADateTimeString(date);
660 }
661 // Time parsing (style should be on cell)
662 else if (item.DataType == Cell.CellType.Time)
663 {
664 TimeSpan time = (TimeSpan)item.Value;
665 cellValue = DataUtils.GetOATimeString(time);
666 }
667 else if (item.DataType == Cell.CellType.String)
668 {
669 cellType = XmlAttribute.CreateAttribute("t", "s");
670 if (item.Value is IFormattableText text)
671 {
672 cellValue = sharedStrings.Add(text, ParserUtils.ToString(sharedStrings.Count));
673 }
674 else
675 {
676 cellValue = sharedStrings.Add(new PlainText(item.Value.ToString()), ParserUtils.ToString(sharedStrings.Count));
677 }
678 this.sharedStringWriter.SharedStringsTotalCount++;
679 }
680 else if (item.DataType == Cell.CellType.Formula)
681 {
682 formulaElement = CreateFormulaElement(item, out cellValue, out cellType); // If null: formula omitted -> array referenced cell
683 }
684 else if (item.DataType == Cell.CellType.Error)
685 {
686 cellValue = ResolveErrorValue(item.Value);
687 cellType = XmlAttribute.CreateAttribute("t", "e");
688 }
689 // else Cell.CellType.Empty or Cell.CellType.Empty lead to empty cell (no "t" attribute & <v> element)
690
691 XmlElement c = row.AddChildElementWithAttribute("c", "r", item.CellAddress);
692 c.AddAttribute(cellType);
693 c.AddAttribute(cellStyle);
694 if (formulaElement != null)
695 {
696 c.AddChildElement(formulaElement);
697 }
698 if (!string.IsNullOrEmpty(cellValue))
699 {
700 c.AddChildElementWithValue("v", XmlUtils.SanitizeXmlValue(cellValue));
701 }
702
703 }
704 return row;
705 }
706
712 private static string ResolveErrorValue(object value)
713 {
714 if (value is Errors.FormulaError formulaError)
715 {
716 switch (formulaError)
717 {
718 case Errors.FormulaError.Null:
719 case Errors.FormulaError.DivisionByZero:
720 case Errors.FormulaError.Value:
721 case Errors.FormulaError.Reference:
722 case Errors.FormulaError.Name:
723 case Errors.FormulaError.Number:
724 case Errors.FormulaError.NotAvailable:
725 case Errors.FormulaError.GettingData:
726 return Errors.FormulaErrorToString(formulaError);
727 }
728 }
729 else if (value is string errorText && Errors.TryParseFormulaError(errorText, out Errors.FormulaError parsedError))
730 {
731 return Errors.FormulaErrorToString(parsedError);
732 }
733 return Errors.FormulaErrorToString(Errors.FormulaError.Name);
734 }
735
743 private static XmlElement CreateFormulaElement(Cell cell, out string cellValue, out XmlAttribute? cellType)
744 {
745 XmlElement formulaElement = null;
746 cellType = null;
747 XmlAttribute? formulaTypeAttribute = null;
748 XmlAttribute? formulaRangeAttribute = null;
749 string formulaExpression;
750 bool omitFormula = false;
751 switch (cell.Formula.Type)
752 {
753 case FormulaData.FormulaType.DataTable:
754 formulaTypeAttribute = new XmlAttribute("t", "dataTable");
755 break;
756 case FormulaData.FormulaType.Shared:
757 formulaTypeAttribute = new XmlAttribute("t", "shared");
758 break;
759 case FormulaData.FormulaType.Array:
760 formulaTypeAttribute = new XmlAttribute("t", "array");
761 break;
762 default:
763 formulaTypeAttribute = new XmlAttribute("t", "normal");
764 break;
765 }
766 if (cell.Formula.FormulaRange != null)
767 {
768 formulaRangeAttribute = new XmlAttribute("ref", cell.Formula.FormulaRange); // Assumed as validated
769 }
770 ResolveFormulaCachedValue(cell.Formula, out cellValue, out cellType);
771 if (cell.Formula.MasterCellAddress != null)
772 {
773 omitFormula = true; // Array referenced cells are only internal formulas
774 }
775 if (cell.Formula.DefinedNameReference != null)
776 {
777 if (cell.Formula.DefinedNameReference.Error != Errors.FormulaError.NoError)
778 {
779 cellType = XmlAttribute.CreateAttribute("t", "e"); // Mark cell type as error (propagated form defined name)
780 }
781 formulaExpression = XmlUtils.SanitizeXmlValue(cell.Formula.DefinedNameReference.Name); // reflects currently cell.Value
782 }
783 else
784 {
785 formulaExpression = XmlUtils.SanitizeXmlValue(cell.Formula.Expression); // reflects currently cell.Value
786 }
787 if (!omitFormula)
788 {
789 formulaElement = XmlElement.CreateElement("f");
790 formulaElement.AddAttribute(formulaTypeAttribute);
791 formulaElement.AddAttribute(formulaRangeAttribute);
792 formulaElement.InnerValue = formulaExpression;
793 }
794 return formulaElement;
795 }
796
803 private static void ResolveFormulaCachedValue(FormulaData formula, out string cellValue, out XmlAttribute? cellType)
804 {
805 cellValue = string.Empty;
806 cellType = null;
807 if (formula.CachedValue == null)
808 {
809 return;
810 }
811 Cell.CellType cachedValueType = formula.CachedValueType;
812 if (cachedValueType == Cell.CellType.Default)
813 {
814 cachedValueType = FormulaData.ResolveCachedValueType(formula.CachedValue);
815 }
816 switch (cachedValueType)
817 {
818 case Cell.CellType.Number:
819 cellValue = CastNumber(formula.CachedValue) ?? formula.CachedValue.ToString();
820 break;
821 case Cell.CellType.Date:
822 if (formula.CachedValue is DateTime dateTime)
823 {
824 cellValue = DataUtils.GetOADateTimeString(dateTime);
825 }
826 else
827 {
828 cellValue = formula.CachedValue.ToString();
829 if (!ParserUtils.TryParseDouble(cellValue, out _, NumberStyles.Float))
830 {
831 cellType = XmlAttribute.CreateAttribute("t", "d");
832 }
833 }
834 break;
835 case Cell.CellType.Time:
836 if (formula.CachedValue is TimeSpan timeSpan)
837 {
838 cellValue = DataUtils.GetOATimeString(timeSpan);
839 }
840 else
841 {
842 cellValue = formula.CachedValue.ToString();
843 }
844 break;
845 case Cell.CellType.Bool:
846 if (formula.CachedValue is bool booleanValue)
847 {
848 cellValue = booleanValue ? "1" : "0";
849 }
850 else
851 {
852 cellValue = formula.CachedValue.ToString();
853 if (ParserUtils.TryParseBool(cellValue, out booleanValue))
854 {
855 cellValue = booleanValue ? "1" : "0";
856 }
857 }
858 cellType = XmlAttribute.CreateAttribute("t", "b");
859 break;
860 case Cell.CellType.Error:
861 cellValue = formula.CachedValue is Errors.FormulaError formulaError
862 ? Errors.FormulaErrorToString(formulaError)
863 : formula.CachedValue.ToString();
864 cellType = XmlAttribute.CreateAttribute("t", "e");
865 break;
866 case Cell.CellType.String:
867 default:
868 cellValue = formula.CachedValue.ToString();
869 cellType = XmlAttribute.CreateAttribute("t", "str");
870 break;
871 }
872 }
873
879 private static string CastNumber(object item)
880 {
881 string cellValue = null;
882 Type t = item.GetType();
883
884 if (t == typeof(byte)) { cellValue = ParserUtils.ToString((byte)item); }
885 else if (t == typeof(sbyte)) { cellValue = ParserUtils.ToString((sbyte)item); }
886 else if (t == typeof(decimal)) { cellValue = ParserUtils.ToString((decimal)item); }
887 else if (t == typeof(double)) { cellValue = ParserUtils.ToString((double)item); }
888 else if (t == typeof(float)) { cellValue = ParserUtils.ToString((float)item); }
889 else if (t == typeof(int)) { cellValue = ParserUtils.ToString((int)item); }
890 else if (t == typeof(uint)) { cellValue = ParserUtils.ToString((uint)item); }
891 else if (t == typeof(long)) { cellValue = ParserUtils.ToString((long)item); }
892 else if (t == typeof(ulong)) { cellValue = ParserUtils.ToString((ulong)item); }
893 else if (t == typeof(short)) { cellValue = ParserUtils.ToString((short)item); }
894 else if (t == typeof(ushort)) { cellValue = ParserUtils.ToString((ushort)item); }
895 // Not a number = null
896 return cellValue;
897 }
898
899
900
906 private List<XmlElement> CreateRowElements(Worksheet worksheet)
907 {
908 List<DynamicRow> cellData = GetSortedSheetData(worksheet);
909 List<XmlElement> rows = new List<XmlElement>(cellData.Count);
910 foreach (DynamicRow row in cellData)
911 {
912 rows.Add(CreateRowElement(row, worksheet));
913 }
914 return rows;
915 }
916
917 #endregion
918 #region helperClasses
922 internal class DynamicRow
923 {
924 public int RowNumber { get; set; }
925
929 public List<Cell> CellDefinitions { get; }
930
934 public DynamicRow()
935 {
936 this.CellDefinitions = new List<Cell>();
937 }
938 }
939 #endregion
940
941 }
942}