NanoXLSX.Core 3.0.0-rc.3
Loading...
Searching...
No Matches
DataUtils.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 © 2025
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 System;
9using System.Collections.Generic;
10using System.Globalization;
11using System.Linq;
12using FormatException = NanoXLSX.Exceptions.FormatException;
13
14namespace NanoXLSX.Utils
15{
19 public static class DataUtils
20
21 {
22 #region constants
26#pragma warning disable CA1805 // Suppress: Do not initialize unnecessarily (to make clear that this is the minimum value)
27 public static readonly double MinOADateValue = 0d;
28#pragma warning restore CA1805
32 public static readonly double MaxOADateValue = 2958465.999988426d;
36 public static readonly DateTime FirstAllowedExcelDate = new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Unspecified);
40 public static readonly DateTime LastAllowedExcelDate = new DateTime(9999, 12, 31, 23, 59, 59, DateTimeKind.Unspecified);
41
47 public static readonly DateTime FirstValidExcelDate = new DateTime(1900, 3, 1, 0, 0, 0, DateTimeKind.Unspecified);
48
55 public static readonly CultureInfo InvariantCulture = CultureInfo.InvariantCulture;
56
57 private const float COLUMN_WIDTH_ROUNDING_MODIFIER = 256f;
58 private const float SPLIT_WIDTH_MULTIPLIER = 12f;
59 private const float SPLIT_WIDTH_OFFSET = 0.5f;
60 private const float SPLIT_WIDTH_POINT_MULTIPLIER = 3f / 4f;
61 private const float SPLIT_POINT_DIVIDER = 20f;
62 private const float SPLIT_WIDTH_POINT_OFFSET = 390f;
63 private const float SPLIT_HEIGHT_POINT_OFFSET = 300f;
64 private const float ROW_HEIGHT_POINT_MULTIPLIER = 1f / 3f + 1f;
65 private static readonly DateTime ROOT_DATE = new DateTime(1899, 12, 30, 0, 0, 0, DateTimeKind.Unspecified);
66 private static readonly double ROOT_MILLIS = (double)new DateTime(1899, 12, 30, 0, 0, 0, DateTimeKind.Unspecified).Ticks / TimeSpan.TicksPerMillisecond;
67
68 #endregion
69
70 #region enums
89 #endregion
90
105 public static string GetOADateTimeString(DateTime date)
106 {
107 double d = GetOADateTime(date);
108 return d.ToString("G", InvariantCulture);
109 }
110
126 public static double GetOADateTime(DateTime date, bool skipCheck = false)
127 {
128 if (!skipCheck && (date < FirstAllowedExcelDate || date > LastAllowedExcelDate))
129 {
130 throw new FormatException("The date is not in a valid range for Excel. Dates before 1900-01-01 or after 9999-12-31 are not allowed.");
131 }
132 DateTime dateValue = date;
133 if (date < FirstValidExcelDate)
134 {
135 dateValue = date.AddDays(-1); // Fix of the leap-year-1900-error
136 }
137 double currentMillis = (double)dateValue.Ticks / TimeSpan.TicksPerMillisecond;
138 return ((dateValue.Second + (dateValue.Minute * 60) + (dateValue.Hour * 3600)) / 86400d) + Math.Floor((currentMillis - ROOT_MILLIS) / 86400000d);
139 }
140
147 public static string GetOATimeString(TimeSpan time)
148 {
149 double d = GetOATime(time);
150 return d.ToString("G", InvariantCulture);
151 }
152
159 public static double GetOATime(TimeSpan time)
160 {
161 int seconds = time.Seconds + time.Minutes * 60 + time.Hours * 3600;
162 return time.Days + (double)seconds / 86400d;
163 }
164
175 public static DateTime GetDateFromOA(double oaDate)
176 {
177 if (oaDate < 60)
178 {
179 oaDate++;
180 }
181 return ROOT_DATE.AddSeconds(oaDate * 86400d);
182 }
183
201 public static float GetInternalColumnWidth(float columnWidth, float maxDigitWidth = 7f, float textPadding = 5f)
202 {
203 if (columnWidth < Worksheet.MinColumnWidth || columnWidth > Worksheet.MaxColumnWidth)
204 {
205 throw new FormatException("The column width " + columnWidth + " is not valid. The valid range is between " + Worksheet.MinColumnWidth + " and " + Worksheet.MaxColumnWidth);
206 }
207 if (columnWidth <= 0f || maxDigitWidth <= 0f)
208 {
209 return 0f;
210 }
211 else if (columnWidth <= 1f)
212 {
213 return (float)Math.Floor((columnWidth * (maxDigitWidth + textPadding)) / maxDigitWidth * COLUMN_WIDTH_ROUNDING_MODIFIER) / COLUMN_WIDTH_ROUNDING_MODIFIER;
214 }
215 else
216 {
217 return (float)Math.Floor((columnWidth * maxDigitWidth + textPadding) / maxDigitWidth * COLUMN_WIDTH_ROUNDING_MODIFIER) / COLUMN_WIDTH_ROUNDING_MODIFIER;
218 }
219 }
220
230 public static float GetInternalRowHeight(float rowHeight)
231 {
232 if (rowHeight < Worksheet.MinRowHeight || rowHeight > Worksheet.MaxRowHeight)
233 {
234 throw new FormatException("The row height " + rowHeight + " is not valid. The valid range is between " + Worksheet.MinRowHeight + " and " + Worksheet.MaxRowHeight);
235 }
236 if (rowHeight == 0f)
237 {
238 return 0f;
239 }
240 double heightInPixel = Math.Round(rowHeight * ROW_HEIGHT_POINT_MULTIPLIER);
241 return (float)heightInPixel / ROW_HEIGHT_POINT_MULTIPLIER;
242 }
243
259 public static float GetInternalPaneSplitWidth(float width, float maxDigitWidth = 7f, float textPadding = 5f)
260 {
261 float pixels;
262 // TODO: Check the <1 part again. Leads always to 390
263 if (width <= 1f)
264 {
265 width = 0;
266 pixels = (float)Math.Floor(width / SPLIT_WIDTH_MULTIPLIER + SPLIT_WIDTH_OFFSET);
267 }
268 else
269 {
270 pixels = (float)Math.Floor(width * maxDigitWidth + SPLIT_WIDTH_OFFSET) + textPadding;
271 }
272 float points = pixels * SPLIT_WIDTH_POINT_MULTIPLIER;
273 return points * SPLIT_POINT_DIVIDER + SPLIT_WIDTH_POINT_OFFSET;
274 }
275
286 public static float GetInternalPaneSplitHeight(float height)
287 {
288 if (height < 0)
289 {
290 height = 0f;
291 }
292 return (float)Math.Floor(SPLIT_POINT_DIVIDER * height + SPLIT_HEIGHT_POINT_OFFSET);
293 }
294
302 public static float GetPaneSplitHeight(float internalHeight)
303 {
304 if (internalHeight < 300f)
305 {
306 return 0;
307 }
308 else
309 {
310 return (internalHeight - SPLIT_HEIGHT_POINT_OFFSET) / SPLIT_POINT_DIVIDER;
311 }
312 }
313
323 public static float GetPaneSplitWidth(float internalWidth, float maxDigitWidth = 7f, float textPadding = 5f)
324 {
325 float points = (internalWidth - SPLIT_WIDTH_POINT_OFFSET) / SPLIT_POINT_DIVIDER;
326 if (points < 0.001f)
327 {
328 return 0;
329 }
330 else
331 {
332 float width = points / SPLIT_WIDTH_POINT_MULTIPLIER;
333 return (width - textPadding - SPLIT_WIDTH_OFFSET) / maxDigitWidth;
334 }
335 }
336
345 public static IReadOnlyList<Range> MergeRange(List<Range> givenRanges, Range newRange, RangeMergeStrategy strategy = RangeMergeStrategy.MergeColumns)
346 {
347 List<Range> result = new List<Range>();
348 List<Range> mergedCandidates = new List<Range> { newRange };
349 // Step 1: Find intersecting ranges and remove them from existingRanges
350 foreach (Range range in givenRanges)
351 {
352 if (IsMergeCandidate(newRange, range, strategy))
353 {
354 mergedCandidates.Add(range);
355 }
356 else
357 {
358 result.Add(range);
359 }
360 }
361 // Step 2: Slice intersecting/adjacent ranges into uniform rectangular pieces.
362 List<Range> slicedRanges = SliceRanges(mergedCandidates);
363 // Step 3: Merge adjacent rectangles where possible.
364 if (strategy == RangeMergeStrategy.MergeColumns)
365 {
366 result.AddRange(MergeAdjacentRanges(slicedRanges, RangeMergeStrategy.MergeColumns));
367 result = MergeAdjacentRanges(result, RangeMergeStrategy.MergeRows);
368 }
369 else if (strategy == RangeMergeStrategy.MergeRows)
370 {
371 result.AddRange(MergeAdjacentRanges(slicedRanges, RangeMergeStrategy.MergeRows));
372 result = MergeAdjacentRanges(result, RangeMergeStrategy.MergeColumns);
373 }
374 else
375 {
376 result.AddRange(slicedRanges);
377 }
378 return result;
379 }
380
389 private static bool IsMergeCandidate(Range a, Range b, RangeMergeStrategy strategy)
390 {
391 // First, if they overlap, they are candidates.
392 if (a.Overlaps(b))
393 {
394 return true;
395 }
396
397 // Otherwise, check for adjacency according to the strategy.
398 if (strategy == RangeMergeStrategy.MergeColumns)
399 {
400 // Vertical merging: require same columns.
403 (a.EndAddress.Row + 1 == b.StartAddress.Row || b.EndAddress.Row + 1 == a.StartAddress.Row))
404 {
405 return true;
406 }
407 }
408 else if (strategy == RangeMergeStrategy.MergeRows)
409 {
410 // Horizontal merging: require same rows.
411 if (a.StartAddress.Row == b.StartAddress.Row &&
412 a.EndAddress.Row == b.EndAddress.Row &&
414 {
415 return true;
416 }
417 }
418 return false;
419 }
420
429 public static IReadOnlyList<Range> SubtractRange(List<Range> givenRanges, Range rangeToRemove, RangeMergeStrategy strategy = RangeMergeStrategy.MergeColumns)
430 {
431 List<Range> result = new List<Range>();
432 // Process each existing range.
433 foreach (Range range in givenRanges)
434 {
435 if (!range.Overlaps(rangeToRemove))
436 {
437 // No overlap: keep the range unchanged.
438 result.Add(range);
439 }
440 else
441 {
442 // Overlapping range: subtract the removal area.
443 List<Range> subtractedPieces = SubtractRect(range, rangeToRemove);
444 result.AddRange(subtractedPieces);
445 }
446 }
447 // Slice all ranges before merge
448 List<Range> slicedRanges = SliceRanges(result);
449 // Merge adjacent pieces if requested.
450 if (strategy == RangeMergeStrategy.MergeColumns)
451 {
452 result = MergeAdjacentRanges(slicedRanges, RangeMergeStrategy.MergeColumns);
453 result = MergeAdjacentRanges(result, RangeMergeStrategy.MergeRows);
454 }
455 else if (strategy == RangeMergeStrategy.MergeRows)
456 {
457 result = MergeAdjacentRanges(slicedRanges, RangeMergeStrategy.MergeRows);
458 result = MergeAdjacentRanges(result, RangeMergeStrategy.MergeColumns);
459 }
460 else
461 {
462 result = slicedRanges;
463 }
464 return result;
465 }
466
472 private static List<Range> SliceRanges(List<Range> ranges)
473 {
474 HashSet<int> uniqueCols = new HashSet<int>();
475 HashSet<int> uniqueRows = new HashSet<int>();
476
477 // Collect all column and row boundaries
478 foreach (Range range in ranges)
479 {
480 uniqueCols.Add(range.StartAddress.Column);
481 uniqueCols.Add(range.EndAddress.Column + 1); // To handle gaps properly
482 uniqueRows.Add(range.StartAddress.Row);
483 uniqueRows.Add(range.EndAddress.Row + 1);
484 }
485
486 // Convert to sorted lists for iteration
487 List<int> sortedCols = uniqueCols.OrderBy(c => c).ToList();
488 List<int> sortedRows = uniqueRows.OrderBy(r => r).ToList();
489
490 List<Range> slicedRanges = new List<Range>();
491
492 // Step through the row and column boundaries to create the smallest sub-rectangles
493 for (int r = 0; r < sortedRows.Count - 1; r++)
494 {
495 for (int c = 0; c < sortedCols.Count - 1; c++)
496 {
497 Range subRange = new Range(
498 sortedCols[c],
499 sortedRows[r],
500 sortedCols[c + 1] - 1,
501 sortedRows[r + 1] - 1
502 );
503
504 // Only keep the sub-range if it was originally covered
505 if (ranges.Exists(range => range.Contains(subRange)))
506 {
507 slicedRanges.Add(subRange);
508 }
509 }
510 }
511 return slicedRanges;
512 }
513
519 private static List<Range> SubtractRect(Range original, Range toRemove)
520 {
521 List<Range> pieces = new List<Range>();
522 // Original boundaries:
523 int orig_left = original.StartAddress.Column;
524 int orig_top = original.StartAddress.Row;
525 int orig_right = original.EndAddress.Column;
526 int orig_bottom = original.EndAddress.Row;
527 // Removal boundaries:
528 int rem_left = toRemove.StartAddress.Column;
529 int rem_top = toRemove.StartAddress.Row;
530 int rem_right = toRemove.EndAddress.Column;
531 int rem_bottom = toRemove.EndAddress.Row;
532 // Compute intersection boundaries.
533 int isct_left = Math.Max(orig_left, rem_left);
534 int isct_top = Math.Max(orig_top, rem_top);
535 int isct_right = Math.Min(orig_right, rem_right);
536 int isct_bottom = Math.Min(orig_bottom, rem_bottom);
537
538 // Slice the original rectangle into up to four pieces.
539 // Top piece: if any rows exist above the intersection.
540 if (orig_top < isct_top)
541 {
542 pieces.Add(new Range(orig_left, orig_top, orig_right, isct_top - 1));
543 }
544 // Bottom piece: if any rows exist below the intersection.
545 if (isct_bottom < orig_bottom)
546 {
547 pieces.Add(new Range(orig_left, isct_bottom + 1, orig_right, orig_bottom));
548 }
549 // Left piece: if any columns exist to the left of the intersection within the vertical boundaries of the intersection.
550 if (orig_left < isct_left)
551 {
552 pieces.Add(new Range(orig_left, isct_top, isct_left - 1, isct_bottom));
553 }
554 // Right piece: if any columns exist to the right of the intersection within the vertical boundaries of the intersection.
555 if (isct_right < orig_right)
556 {
557 pieces.Add(new Range(isct_right + 1, isct_top, orig_right, isct_bottom));
558 }
559 return pieces;
560 }
561
569 private static List<Range> MergeAdjacentRanges(List<Range> ranges, RangeMergeStrategy strategy)
570 {
571 if (ranges.Count == 0)
572 {
573 return new List<Range>();
574 }
575 List<Range> mergedRanges = new List<Range>();
576
577 if (strategy == RangeMergeStrategy.MergeColumns)
578 {
579 // Vertical merging: Ranges must have identical column boundaries.
580 // Group by StartAddress.Column and EndAddress.Column.
581 var groups = ranges.GroupBy(r => new
582 {
583 StartCol = r.StartAddress.Column,
584 EndCol = r.EndAddress.Column
585 });
586 foreach (var group in groups)
587 {
588 // Order by row (ascending)
589 List<Range> sorted = group.OrderBy(r => r.StartAddress.Row).ToList();
590 Range current = sorted[0];
591 for (int i = 1; i < sorted.Count; i++)
592 {
593 Range next = sorted[i];
594 // Check if the current range is contiguous with or overlapping the next.
595 // (That is, if current.EndAddress.Row + 1 is >= next.StartAddress.Row.)
596 if (current.EndAddress.Row + 1 >= next.StartAddress.Row)
597 {
598 // They share the same columns.
599 // Create a new range from current.StartAddress.Row to the maximum of the two EndAddress.Row values.
600 int newStartRow = current.StartAddress.Row;
601 int newEndRow = Math.Max(current.EndAddress.Row, next.EndAddress.Row);
602 current = new Range(
603 current.StartAddress.Column, newStartRow,
604 current.EndAddress.Column, newEndRow);
605 }
606 else
607 {
608 mergedRanges.Add(current);
609 current = next;
610 }
611 }
612 mergedRanges.Add(current);
613 }
614 }
615 else if (strategy == RangeMergeStrategy.MergeRows)
616 {
617 // Horizontal merging: Ranges must have identical row boundaries.
618 // Group by StartAddress.Row and EndAddress.Row.
619 var groups = ranges.GroupBy(r => new
620 {
621 StartRow = r.StartAddress.Row,
622 EndRow = r.EndAddress.Row
623 });
624 foreach (var group in groups)
625 {
626 var sorted = group.OrderBy(r => r.StartAddress.Column).ToList();
627 Range current = sorted[0];
628 for (int i = 1; i < sorted.Count; i++)
629 {
630 Range next = sorted[i];
631 // Check if current.EndAddress.Column + 1 is >= next.StartAddress.Column.
632 if (current.EndAddress.Column + 1 >= next.StartAddress.Column)
633 {
634 int newStartCol = current.StartAddress.Column;
635 int newEndCol = Math.Max(current.EndAddress.Column, next.EndAddress.Column);
636 current = new Range(
637 newStartCol, current.StartAddress.Row,
638 newEndCol, current.EndAddress.Row);
639 }
640 else
641 {
642 mergedRanges.Add(current);
643 current = next;
644 }
645 }
646 mergedRanges.Add(current);
647 }
648 }
649 return mergedRanges;
650 }
651
652 }
653}
Class for exceptions regarding format error incidents.
General data utils class with static methods.
Definition DataUtils.cs:21
static readonly double MaxOADateValue
Maximum valid OAdate value (9999-12-31).
Definition DataUtils.cs:32
static float GetInternalPaneSplitWidth(float width, float maxDigitWidth=7f, float textPadding=5f)
Calculates the internal width of a split pane in a worksheet. This width is used only in the XML docu...
Definition DataUtils.cs:259
static readonly CultureInfo InvariantCulture
Constant for number conversions. The invariant culture (represents mostly the US numbering scheme) en...
Definition DataUtils.cs:55
static float GetPaneSplitHeight(float internalHeight)
Calculates the height of a split pane in a worksheet, based on the internal value (calculated by GetI...
Definition DataUtils.cs:302
static double GetOADateTime(DateTime date, bool skipCheck=false)
Method to convert a date or date and time into the internal Excel time format (OAdate).
Definition DataUtils.cs:126
static IReadOnlyList< Range > MergeRange(List< Range > givenRanges, Range newRange, RangeMergeStrategy strategy=RangeMergeStrategy.MergeColumns)
Merges a range with a list of given ranges. If there is no intersection between the list and the new ...
Definition DataUtils.cs:345
RangeMergeStrategy
Strategy how ranges should be merged.
Definition DataUtils.cs:75
@ NoMerge
No merge should be performed.
Definition DataUtils.cs:79
@ MergeColumns
Ranges of the same columns should be merged.
Definition DataUtils.cs:83
@ MergeRows
Ranges of the same row should be merged.
Definition DataUtils.cs:87
static readonly DateTime LastAllowedExcelDate
Last date that can be displayed by Excel. Real values after this date cannot be processed.
Definition DataUtils.cs:40
static float GetInternalRowHeight(float rowHeight)
Calculates the internal height of a row. This height is used only in the XML documents of worksheets ...
Definition DataUtils.cs:230
static readonly DateTime FirstAllowedExcelDate
First date that can be displayed by Excel. Real values before this date cannot be processed.
Definition DataUtils.cs:36
static string GetOATimeString(TimeSpan time)
Method to convert a time into the internal Excel time format (OAdate without days).
Definition DataUtils.cs:147
static readonly DateTime FirstValidExcelDate
All dates before this date are shifted in Excel by -1.0, since Excel assumes wrongly that the year 19...
Definition DataUtils.cs:47
static string GetOADateTimeString(DateTime date)
Method to convert a date or date and time into the internal Excel time format (OAdate).
Definition DataUtils.cs:105
static IReadOnlyList< Range > SubtractRange(List< Range > givenRanges, Range rangeToRemove, RangeMergeStrategy strategy=RangeMergeStrategy.MergeColumns)
Subtracts a range form a list of given ranges. If the range to be removed does not intersect any of t...
Definition DataUtils.cs:429
static float GetInternalPaneSplitHeight(float height)
Calculates the internal height of a split pane in a worksheet. This height is used only in the XML do...
Definition DataUtils.cs:286
static readonly double MinOADateValue
Minimum valid OAdate value (1900-01-01). However, Excel displays this value as 1900-01-00 (day zero).
Definition DataUtils.cs:27
static float GetInternalColumnWidth(float columnWidth, float maxDigitWidth=7f, float textPadding=5f)
Calculates the internal width of a column in characters. This width is used only in the XML documents...
Definition DataUtils.cs:201
static double GetOATime(TimeSpan time)
Method to convert a time into the internal Excel time format (OAdate without days).
Definition DataUtils.cs:159
static float GetPaneSplitWidth(float internalWidth, float maxDigitWidth=7f, float textPadding=5f)
Calculates the width of a split pane in a worksheet, based on the internal value (calculated by GetIn...
Definition DataUtils.cs:323
static DateTime GetDateFromOA(double oaDate)
Method to calculate a common Date from the OA date (OLE automation) format OA Date format starts at ...
Definition DataUtils.cs:175
Class representing a worksheet of a workbook.
Definition Worksheet.cs:26
static readonly float MinColumnWidth
Minimum column width as constant.
Definition Worksheet.cs:59
static readonly float MaxColumnWidth
Maximum column width as constant.
Definition Worksheet.cs:70
static readonly float MinRowHeight
Minimum row height as constant.
Definition Worksheet.cs:65
static readonly float MaxRowHeight
Maximum row height as constant.
Definition Worksheet.cs:84
int Row
Row number (zero based).
Definition Address.cs:28
int Column
Column number (zero based).
Definition Address.cs:24
Struct representing a cell range with a start and end address.
Definition Range.cs:16
bool Contains(Range other)
Gets whether another range is completely enclosed by this range.
Definition Range.cs:87
Address StartAddress
Start address of the range.
Definition Range.cs:27
Address EndAddress
End address of the range.
Definition Range.cs:23
bool Overlaps(Range other)
Determines whether the passed range overlaps with this range.
Definition Range.cs:113