From 8d1b3494433b05d9fa635867b5d05cae53587f47 Mon Sep 17 00:00:00 2001 From: Victor Irzak Date: Wed, 14 Oct 2026 09:37:37 -0400 Subject: [PATCH] Avoid per-cell allocations in XmlReaderHelper and TryParseCellReference IsStartElement and GetAttribute allocated a LINQ closure on every call, and TryParseCellReference allocated a substring for every row number. --- .../Helpers/CellReferenceConverter.cs | 17 ++++++++++++++--- .../Utils/XmlReaderHelper.cs | 19 +++++++++++++++---- .../Issues/MiniExcelGithubIssuesTests.cs | 18 ++++++++++++++++++ 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/MiniExcel.Core/Helpers/CellReferenceConverter.cs b/src/MiniExcel.Core/Helpers/CellReferenceConverter.cs index 5508fc01..a18c960b 100644 --- a/src/MiniExcel.Core/Helpers/CellReferenceConverter.cs +++ b/src/MiniExcel.Core/Helpers/CellReferenceConverter.cs @@ -118,10 +118,21 @@ public static bool TryParseCellReference(string? value, out int column, out int return false; } - if (position == 0) + if (position == 0 || position == value.Length) return false; - return int.TryParse(value[position..], NumberStyles.None, CultureInfo.InvariantCulture, out row) - && row is > 0 and <= MaxRowNumber; + // Parsed by hand rather than with int.TryParse on a substring, which allocated for every cell + for (; position < value.Length; position++) + { + var digit = value[position] - '0'; + row = row * 10 + digit; + if (digit is < 0 or > 9 || row > MaxRowNumber) + { + row = 0; + return false; + } + } + + return row > 0; } } diff --git a/src/MiniExcel.OpenXml/Utils/XmlReaderHelper.cs b/src/MiniExcel.OpenXml/Utils/XmlReaderHelper.cs index dacd9e0b..688a4f16 100644 --- a/src/MiniExcel.OpenXml/Utils/XmlReaderHelper.cs +++ b/src/MiniExcel.OpenXml/Utils/XmlReaderHelper.cs @@ -67,16 +67,27 @@ public static async Task SkipToNextSiblingAsync(this XmlReader reader, Cancellat } } + // Called for every element and cell, so these loop instead of allocating a LINQ closure per call public static bool IsStartElement(this XmlReader reader, string name, params string[] nss) { - return nss.Any(s => reader.IsStartElement(name, s)); + foreach (var ns in nss) + { + if (reader.IsStartElement(name, ns)) + return true; + } + + return false; } public static string? GetAttribute(this XmlReader reader, string name, params string[] nss) { - return nss - .Select(ns => reader.GetAttribute(name, ns)) - .FirstOrDefault(at => at is not null); + foreach (var ns in nss) + { + if (reader.GetAttribute(name, ns) is { } attribute) + return attribute; + } + + return null; } // Copied and modified from ExcelDataReader - @MIT License diff --git a/tests/MiniExcel.OpenXml.Tests/Issues/MiniExcelGithubIssuesTests.cs b/tests/MiniExcel.OpenXml.Tests/Issues/MiniExcelGithubIssuesTests.cs index b5b1090a..862e821d 100644 --- a/tests/MiniExcel.OpenXml.Tests/Issues/MiniExcelGithubIssuesTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/Issues/MiniExcelGithubIssuesTests.cs @@ -35,6 +35,24 @@ public void CellReferenceConverterEnforcesWorksheetLimits() Assert.False(CellReferenceConverter.TryParseCellReference("A1048577", out _, out _)); } + [Theory] + [InlineData("B3", 2, 3)] + [InlineData("b03", 2, 3)] + [InlineData("A", 0, 0)] + [InlineData("A0", 0, 0)] + [InlineData("A1x", 0, 0)] + [InlineData("A-1", 0, 0)] + [InlineData("A99999999999", 0, 0)] + public void CellReferenceConverterParsesRowNumbers(string reference, int expectedColumn, int expectedRow) + { + var parsed = CellReferenceConverter.TryParseCellReference(reference, out var column, out var row); + + Assert.Equal(expectedRow > 0, parsed); + Assert.Equal(expectedRow, row); + if (parsed) + Assert.Equal(expectedColumn, column); + } + [Fact] public void QueryRangeAcceptsMaximumWorksheetCoordinates() {