diff --git a/main/SS/Formula/FormulaParser.cs b/main/SS/Formula/FormulaParser.cs index 238354994..c56a2aeb2 100644 --- a/main/SS/Formula/FormulaParser.cs +++ b/main/SS/Formula/FormulaParser.cs @@ -1173,9 +1173,6 @@ private static AreaReference CreateAreaRef(SimpleRangePart part1, SimpleRangePar } return new AreaReference(part1.CellReference, part2.CellReference); } - private string CELL_REF_PATTERN = "(\\$?[A-Za-z]+)?(\\$?[0-9]+)?"; - - /** @@ -1213,11 +1210,9 @@ private SimpleRangePart ParseSimpleRangePart() { return null; } - String rep = _formulaString.Substring(_pointer - 1, ptr - _pointer + 1); + ReadOnlySpan rep = _formulaString.AsSpan(_pointer - 1, ptr - _pointer + 1); - Regex pattern = new Regex(CELL_REF_PATTERN); - - if (!pattern.IsMatch(rep)) + if (!CellReferenceParser.TryParseCellReference(rep, out _, out var column, out _, out var row)) { return null; } @@ -1231,19 +1226,14 @@ private SimpleRangePart ParseSimpleRangePart() } else if (hasLetters) { - if (!CellReference.IsColumnWithinRange(rep.Replace("$", ""), _ssVersion)) + if (!CellReference.IsColumnWithinRange(column, _ssVersion)) { return null; } } else if (hasDigits) { - int i; - try - { - i = Int32.Parse(rep.Replace("$", ""), CultureInfo.InvariantCulture); - } - catch + if (!CellReferenceParser.TryParsePositiveInt32Fast(row, out int i)) { return null; } @@ -1260,7 +1250,7 @@ private SimpleRangePart ParseSimpleRangePart() ResetPointer(ptr + 1); // stepping forward - return new SimpleRangePart(rep, hasLetters, hasDigits); + return new SimpleRangePart(rep.ToString(), hasLetters, hasDigits); } @@ -1544,7 +1534,9 @@ private void ResetPointer(int ptr) /** * @return true if the specified name is a valid cell reference */ - private bool IsValidCellReference(String str) + private bool IsValidCellReference(String str) => IsValidCellReference(str.AsSpan()); + + private bool IsValidCellReference(ReadOnlySpan str) { //check range bounds against grid max bool result = CellReference.ClassifyCellReference(str, _ssVersion) == NameType.Cell; @@ -1557,7 +1549,7 @@ private bool IsValidCellReference(String str) * (b) LOG10 + 1 * In (a) LOG10 is a name of a built-in function. In (b) LOG10 is a cell reference */ - bool isFunc = FunctionMetadataRegistry.GetFunctionByName(str.ToUpper()) != null; + bool isFunc = FunctionMetadataRegistry.GetFunctionByName(str.ToString().ToUpper()) != null; if (isFunc) { int savePointer = _pointer; diff --git a/main/SS/Formula/Function/FunctionMetadataRegistry.cs b/main/SS/Formula/Function/FunctionMetadataRegistry.cs index 10a193845..e5f90a3a0 100644 --- a/main/SS/Formula/Function/FunctionMetadataRegistry.cs +++ b/main/SS/Formula/Function/FunctionMetadataRegistry.cs @@ -93,10 +93,8 @@ public static short LookupIndexByName(String name) private FunctionMetadata GetFunctionByNameInternal(String name) { - if (_functionDataByName.ContainsKey(name)) - return _functionDataByName[name]; - else - return null; + _functionDataByName.TryGetValue(name, out var metadata); + return metadata; } diff --git a/main/SS/Util/CellAddress.cs b/main/SS/Util/CellAddress.cs index bff17ae80..86c1f92c9 100644 --- a/main/SS/Util/CellAddress.cs +++ b/main/SS/Util/CellAddress.cs @@ -78,11 +78,12 @@ public CellAddress(String address) } } - String sCol = address.Substring(0, loc).ToUpper(); - String sRow = address.Substring(loc); + ReadOnlySpan sCol = address.AsSpan(0, loc); + ReadOnlySpan sRow = address.AsSpan(loc); // FIXME: breaks if Address Contains a sheet name or dollar signs from an absolute CellReference - this._row = int.Parse(sRow) - 1; + CellReferenceParser.TryParsePositiveInt32Fast(sRow, out var rowNumber); + this._row = rowNumber - 1; this._col = CellReference.ConvertColStringToIndex(sCol); } diff --git a/main/SS/Util/CellRangeAddress.cs b/main/SS/Util/CellRangeAddress.cs index e06fb3e8a..c03cfc6ad 100644 --- a/main/SS/Util/CellRangeAddress.cs +++ b/main/SS/Util/CellRangeAddress.cs @@ -112,8 +112,8 @@ public static CellRangeAddress ValueOf(String reference) } else { - a = new CellReference(reference.Substring(0, sep)); - b = new CellReference(reference.Substring(sep + 1)); + a = new CellReference(reference.AsSpan(0, sep)); + b = new CellReference(reference.AsSpan(sep + 1)); } return new CellRangeAddress(a.Row, b.Row, a.Col, b.Col); } diff --git a/main/SS/Util/CellReference.cs b/main/SS/Util/CellReference.cs index 9bcacec4e..b43c91479 100644 --- a/main/SS/Util/CellReference.cs +++ b/main/SS/Util/CellReference.cs @@ -62,38 +62,6 @@ public class CellReference /** The character (') used to quote sheet names when they contain special characters */ private const char SPECIAL_NAME_DELIMITER = '\''; - /** - * Matches a run of one or more letters followed by a run of one or more digits. - * Both the letter and number groups are optional. - * The run of letters is group 1 and the run of digits is group 2. - * Each group may optionally be prefixed with a single '$'. - */ - //private const string CELL_REF_PATTERN = @"^\$?([A-Za-z]+)\$?([0-9]+)"; - //private static final Pattern CELL_REF_PATTERN = Pattern.compile("(\\$?[A-Z]+)?" + "(\\$?[0-9]+)?", Pattern.CASE_INSENSITIVE); - private static Regex CELL_REF_PATTERN = new Regex("(\\$?[A-Z]+)?" + "(\\$?[0-9]+)?", RegexOptions.IgnoreCase | RegexOptions.Compiled| RegexOptions.CultureInvariant); - - /** - * Matches references only where row and column are included. - * Matches a run of one or more letters followed by a run of one or more digits. - * If a reference does not match this pattern, it might match COLUMN_REF_PATTERN or ROW_REF_PATTERN - * References may optionally include a single '$' before each group, but these are excluded from the Matcher.group(int). - */ - //private static final Pattern STRICTLY_CELL_REF_PATTERN = Pattern.compile("\\$?([A-Z]+)" + "\\$?([0-9]+)", Pattern.CASE_INSENSITIVE); - private static Regex STRICTLY_CELL_REF_PATTERN = new Regex("^\\$?([A-Z]+)" + "\\$?([0-9]+)$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); - /** - * Matches a run of one or more letters. The run of letters is group 1. - * References may optionally include a single '$' before the group, but these are excluded from the Matcher.group(int). - */ - private static Regex COLUMN_REF_PATTERN = new Regex(@"^\$?([A-Za-z]+)$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); - /** - * Matches a run of one or more letters. The run of numbers is group 1. - * References may optionally include a single '$' before the group, but these are excluded from the Matcher.group(int). - */ - private static Regex ROW_REF_PATTERN = new Regex(@"^\$?([0-9]+)$"); - /** - * Named range names must start with a letter or underscore. Subsequent characters may include - * digits or dot. (They can even end in dot). - */ //private static final Pattern NAMED_RANGE_NAME_PATTERN = Pattern.compile("[_A-Z][_.A-Z0-9]*", Pattern.CASE_INSENSITIVE); private static Regex NAMED_RANGE_NAME_PATTERN = new Regex("^[_A-Za-z][_.A-Za-z0-9]*$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); //private static string BIFF8_LAST_COLUMN = "IV"; @@ -113,25 +81,20 @@ public class CellReference * Create an cell ref from a string representation. Sheet names containing special characters should be * delimited and escaped as per normal syntax rules for formulas. */ - public CellReference(String cellRef) + public CellReference(String cellRef) : this(cellRef.AsSpan()) + { + } + + public CellReference(ReadOnlySpan cellRef) { - if (cellRef.EndsWith("#REF!", StringComparison.InvariantCulture)) + if (cellRef.EndsWith("#REF!".AsSpan(), StringComparison.InvariantCulture)) { - throw new ArgumentException("Cell reference invalid: " + cellRef); + throw new ArgumentException("Cell reference invalid: " + cellRef.ToString()); } CellRefPartsInner parts = SeparateRefParts(cellRef); _sheetName = parts.sheetName;//parts[0]; - String colRef = parts.colRef;// parts[1]; - //if (colRef.Length < 1) - //{ - // throw new ArgumentException("Invalid Formula cell reference: '" + cellRef + "'"); - //} - _isColAbs = (colRef.Length > 0) && colRef[0] == '$'; - //_isColAbs = colRef[0] == '$'; - if (_isColAbs) - { - colRef = colRef.Substring(1); - } + ReadOnlySpan colRef = parts.colRef;// parts[1]; + _isColAbs = parts.columnPrefix == '$'; if (colRef.Length == 0) { _colIndex = -1; @@ -142,24 +105,16 @@ public CellReference(String cellRef) } - String rowRef = parts.rowRef;// parts[2]; - //if (rowRef.Length < 1) - //{ - // throw new ArgumentException("Invalid Formula cell reference: '" + cellRef + "'"); - //} - //_isRowAbs = rowRef[0] == '$'; - _isRowAbs = (rowRef.Length > 0) && rowRef[0] == '$'; - if (_isRowAbs) - { - rowRef = rowRef.Substring(1); - } + ReadOnlySpan rowRef = parts.rowRef;// parts[2]; + _isRowAbs = parts.rowPrefix == '$'; if (rowRef.Length == 0) { _rowIndex = -1; } else { - _rowIndex = int.Parse(rowRef, CultureInfo.InvariantCulture) - 1; // -1 to convert 1-based to zero-based + CellReferenceParser.TryParsePositiveInt32Fast(rowRef, out var rowRefNumber); + _rowIndex = rowRefNumber - 1; // -1 to convert 1-based to zero-based } } public CellReference(ICell cell):this(cell.RowIndex, cell.ColumnIndex, false, false) @@ -237,18 +192,19 @@ public String SheetName * 'IV' -> 255 * @return zero based column index */ - public static int ConvertColStringToIndex(String ref1) + public static int ConvertColStringToIndex(String refs) => ConvertColStringToIndex(refs.AsSpan()); + + public static int ConvertColStringToIndex(ReadOnlySpan refs) { int retval = 0; - char[] refs = ref1.ToUpper().ToCharArray(); for (int k = 0; k < refs.Length; k++) { - char thechar = refs[k]; + char thechar = char.ToUpperInvariant(refs[k]); if (thechar == ABSOLUTE_REFERENCE_MARKER) { if (k != 0) { - throw new ArgumentException("Bad col ref format '" + ref1 + "'"); + throw new ArgumentException("Bad col ref format '" + refs.ToString() + "'"); } continue; } @@ -258,11 +214,15 @@ public static int ConvertColStringToIndex(String ref1) } return retval - 1; } + public static bool IsPartAbsolute(String part) { return part[0] == ABSOLUTE_REFERENCE_MARKER; } - public static NameType ClassifyCellReference(String str, SpreadsheetVersion ssVersion) + + public static NameType ClassifyCellReference(String str, SpreadsheetVersion ssVersion) => ClassifyCellReference(str.AsSpan(), ssVersion); + + public static NameType ClassifyCellReference(ReadOnlySpan str, SpreadsheetVersion ssVersion) { int len = str.Length; if (len < 1) @@ -289,14 +249,12 @@ public static NameType ClassifyCellReference(String str, SpreadsheetVersion ssVe // no digits at end of str return ValidateNamedRangeName(str, ssVersion); } - Regex cellRefPatternMatcher = STRICTLY_CELL_REF_PATTERN; - if (!cellRefPatternMatcher.IsMatch(str)) + + if (!CellReferenceParser.TryParseStrictCellReference(str, out var lettersGroup, out var digitsGroup)) { return ValidateNamedRangeName(str, ssVersion); } - MatchCollection matches = cellRefPatternMatcher.Matches(str); - string lettersGroup = matches[0].Groups[1].Value; - string digitsGroup = matches[0].Groups[2].Value; + if (CellReferenceIsWithinRange(lettersGroup, digitsGroup, ssVersion)) { // valid cell reference @@ -314,28 +272,25 @@ public static NameType ClassifyCellReference(String str, SpreadsheetVersion ssVe } return NameType.NamedRange; } - private static NameType ValidateNamedRangeName(String str, SpreadsheetVersion ssVersion) - { - Regex colMatcher = COLUMN_REF_PATTERN; - if (colMatcher.IsMatch(str)) + private static NameType ValidateNamedRangeName(ReadOnlySpan str, SpreadsheetVersion ssVersion) + { + if (CellReferenceParser.TryParseColumnReference(str, out var colStr)) { - Group colStr = colMatcher.Matches(str)[0].Groups[1]; - if (IsColumnWithinRange(colStr.Value, ssVersion)) + if (IsColumnWithinRange(colStr, ssVersion)) { return NameType.Column; } } - Regex rowMatcher = ROW_REF_PATTERN; - if (rowMatcher.IsMatch(str)) + if (CellReferenceParser.TryParseRowReference(str, out var rowStr)) { - Group rowStr = rowMatcher.Matches(str)[0].Groups[1]; - if (IsRowWithinRange(rowStr.Value, ssVersion)) + if (IsRowWithinRange(rowStr, ssVersion)) { return NameType.Row; } } - if (!NAMED_RANGE_NAME_PATTERN.IsMatch(str)) + // TODO + if (!NAMED_RANGE_NAME_PATTERN.IsMatch(str.ToString())) { return NameType.BadCellOrNamedRange; } @@ -368,17 +323,21 @@ public static String ConvertNumToColString(int col) return colRef.ToString(); } - internal class CellRefPartsInner + internal readonly ref struct CellRefPartsInner { - public String sheetName; - public String rowRef; - public String colRef; + public readonly String sheetName; + public readonly char rowPrefix; + public readonly char columnPrefix; + public readonly ReadOnlySpan rowRef; + public readonly ReadOnlySpan colRef; - public CellRefPartsInner(String sheetName, String rowRef, String colRef) + public CellRefPartsInner(string sheetName, char rowPrefix, ReadOnlySpan rowRef, char columnPrefix, ReadOnlySpan colRef) { this.sheetName = sheetName; - this.rowRef = rowRef ?? ""; - this.colRef = colRef ?? ""; + this.rowPrefix = rowPrefix; + this.rowRef = rowRef; + this.columnPrefix = columnPrefix; + this.colRef = colRef; } } /** @@ -386,24 +345,21 @@ public CellRefPartsInner(String sheetName, String rowRef, String colRef) * is the sheet name. Only the first element may be null. The second element in is the column * name still in ALPHA-26 number format. The third element is the row. */ - private static CellRefPartsInner SeparateRefParts(String reference) + private static CellRefPartsInner SeparateRefParts(ReadOnlySpan reference) { int plingPos = reference.LastIndexOf(SHEET_NAME_DELIMITER); String sheetName = ParseSheetName(reference, plingPos); int start = plingPos + 1; - String cell = reference.Substring(plingPos + 1).ToUpper(CultureInfo.InvariantCulture); - Match matcher = CELL_REF_PATTERN.Match(cell); - if (!matcher.Success) - throw new ArgumentException("Invalid CellReference: " + reference); - String col = matcher.Groups[1].Value; - String row = matcher.Groups[2].Value; - - CellRefPartsInner cellRefParts = new CellRefPartsInner(sheetName, row, col); + String cell = reference.ToString().Substring(plingPos + 1).ToUpper(CultureInfo.InvariantCulture); + if (!CellReferenceParser.TryParseCellReference(cell.AsSpan(), out var columnPrefix, out var column, out var rowPrefix, out var row)) + throw new ArgumentException("Invalid CellReference: " + reference.ToString()); + + CellRefPartsInner cellRefParts = new CellRefPartsInner(sheetName, rowPrefix, row, columnPrefix, column); return cellRefParts; } - private static String ParseSheetName(String reference, int indexOfSheetNameDelimiter) + private static String ParseSheetName(ReadOnlySpan reference, int indexOfSheetNameDelimiter) { if (indexOfSheetNameDelimiter < 0) { @@ -416,17 +372,17 @@ private static String ParseSheetName(String reference, int indexOfSheetNameDelim // sheet names with spaces must be quoted if (reference.IndexOf(' ') == -1) { - return reference.Substring(0, indexOfSheetNameDelimiter); + return reference.Slice(0, indexOfSheetNameDelimiter).ToString(); } else { - throw new ArgumentException("Sheet names containing spaces must be quoted: (" + reference + ")"); + throw new ArgumentException("Sheet names containing spaces must be quoted: (" + reference.ToString() + ")"); } } int lastQuotePos = indexOfSheetNameDelimiter - 1; if (reference[lastQuotePos] != SPECIAL_NAME_DELIMITER) { - throw new ArgumentException("Mismatched quotes: (" + reference + ")"); + throw new ArgumentException("Mismatched quotes: (" + reference.ToString() + ")"); } // TODO - refactor cell reference parsing logic to one place. @@ -456,7 +412,7 @@ private static String ParseSheetName(String reference, int indexOfSheetNameDelim continue; } } - throw new ArgumentException("Bad sheet name quote escaping: (" + reference + ")"); + throw new ArgumentException("Bad sheet name quote escaping: (" + reference.ToString() + ")"); } return sb.ToString(); } @@ -577,6 +533,9 @@ public void AppendCellReference(StringBuilder sb) * @return true if the row and col parameters are within range of a BIFF8 spreadsheet. */ public static bool CellReferenceIsWithinRange(String colStr, String rowStr, SpreadsheetVersion ssVersion) + => CellReferenceIsWithinRange(colStr.AsSpan(), rowStr.AsSpan(), ssVersion); + + public static bool CellReferenceIsWithinRange(ReadOnlySpan colStr, ReadOnlySpan rowStr, SpreadsheetVersion ssVersion) { if (!IsColumnWithinRange(colStr, ssVersion)) { @@ -593,9 +552,14 @@ public static bool IsColumnWithnRange(String colStr, SpreadsheetVersion ssVersio { return IsColumnWithinRange(colStr, ssVersion); } + public static bool IsRowWithinRange(String rowStr, SpreadsheetVersion ssVersion) + => IsRowWithinRange(rowStr.AsSpan(), ssVersion); + + public static bool IsRowWithinRange(ReadOnlySpan rowStr, SpreadsheetVersion ssVersion) { - int rowNum = int.Parse(rowStr) - 1; + CellReferenceParser.TryParsePositiveInt32Fast(rowStr, out var rowNum); + rowNum -= 1; return 0 <= rowNum && rowNum <= ssVersion.LastRowIndex; } @@ -604,7 +568,11 @@ public static bool isRowWithnRange(String rowStr, SpreadsheetVersion ssVersion) { return IsRowWithinRange(rowStr, ssVersion); } + public static bool IsColumnWithinRange(String colStr, SpreadsheetVersion ssVersion) + => IsColumnWithinRange(colStr.AsSpan(), ssVersion); + + public static bool IsColumnWithinRange(ReadOnlySpan colStr, SpreadsheetVersion ssVersion) { String lastCol = ssVersion.LastColumnName; int lastColLength = lastCol.Length; @@ -618,7 +586,7 @@ public static bool IsColumnWithinRange(String colStr, SpreadsheetVersion ssVersi if (numberOfLetters == lastColLength) { //if (colStr.ToUpper().CompareTo(lastCol) > 0) - if (string.Compare(colStr.ToUpper(), lastCol, StringComparison.Ordinal) > 0) + if (colStr.CompareTo(lastCol.AsSpan(), StringComparison.OrdinalIgnoreCase) > 0) { return false; } diff --git a/main/SS/Util/CellReferenceParser.cs b/main/SS/Util/CellReferenceParser.cs new file mode 100644 index 000000000..16e08effa --- /dev/null +++ b/main/SS/Util/CellReferenceParser.cs @@ -0,0 +1,175 @@ +using System; + +namespace NPOI.SS.Util; + +internal static class CellReferenceParser +{ + // (\$?[A-Za-z]+)?(\$?[0-9]+)? + public static bool TryParseCellReference(ReadOnlySpan input, out char columnPrefix, out ReadOnlySpan column, out char rowPrefix, out ReadOnlySpan row) + { + return TryParse(input, out columnPrefix, out column, out rowPrefix, out row) + && columnPrefix is '$' or char.MinValue && rowPrefix is '$' or char.MinValue; + } + + // Matches references only where row and column are included. + // Matches a run of one or more letters followed by a run of one or more digits. + // If a reference does not match this pattern, it might match COLUMN_REF_PATTERN or ROW_REF_PATTERN + // References may optionally include a single '$' before each group, but these are excluded from the Matcher.group(int). + // ^\$?([A-Z]+)\$?([0-9]+)$ + public static bool TryParseStrictCellReference(ReadOnlySpan input, out ReadOnlySpan column, out ReadOnlySpan row) + { + return TryParse(input, out var columnPrefix, out column, out var rowPrefix, out row) + && columnPrefix is '$' or char.MinValue + && column.Length > 0 + && rowPrefix is '$' or char.MinValue + && row.Length > 0; + } + + + // Matches a run of one or more letters. The run of letters is group 1. + // References may optionally include a single '$' before the group, but these are excluded from the Matcher.group(int). + // ^\$?([A-Za-z]+)$ + public static bool TryParseColumnReference(ReadOnlySpan input, out ReadOnlySpan column) + { + return TryParse(input, out var columnPrefix, out column, out var rowPrefix, out var row) + && columnPrefix is '$' or char.MinValue + && column.Length > 0 + && rowPrefix is char.MinValue + && row.Length == 0; + } + + // Matches a run of one or more letters. The run of numbers is group 1. + // References may optionally include a single '$' before the group, but these are excluded from the Matcher.group(int). + // ^\$?([0-9]+)$ + public static bool TryParseRowReference(ReadOnlySpan input, out ReadOnlySpan row) + { + return TryParse(input, out var columnPrefix, out var cell, out var rowPrefix, out row) + && columnPrefix is '$' or char.MinValue + && cell.Length == 0 + && rowPrefix is '$' or char.MinValue + && row.Length > 0; + } + + /// + /// Generic parsing logic that extracts reference information. + /// + /// Input to parse. + /// Possible column prefix like '$', if none. + /// Column name string, empty if none. + /// Possible row prefix like '$', if none. + /// Row data, empty if none + /// + private static bool TryParse( + ReadOnlySpan input, + out char columnPrefix, + out ReadOnlySpan column, + out char rowPrefix, + out ReadOnlySpan row) + { + column = default; + columnPrefix = char.MinValue; + row = default; + rowPrefix = char.MinValue; + + if (input.Length == 0) + { + return false; + } + + // quick check for common case, alphabet + numbers, like A11 + var firstChar = input[0]; + if (input.Length > 1 + && char.IsLetter(firstChar) + && char.IsDigit(input[1]) + && TryParsePositiveInt32Fast(input.Slice(1), out _)) + { + column = input.Slice(0, 1); + row = input.Slice(1); + return true; + } + + int cellStartIndex = 0; + int cellEndIndex = input.Length - 1; + int rowStartIndex = input.Length; + + if (char.IsDigit(firstChar)) + { + // no cell + cellStartIndex = int.MaxValue; + rowStartIndex = 0; + } + else if (!char.IsLetter(firstChar)) + { + if (input.Length > 1 && char.IsDigit(input[1])) + { + // actually row starts now + rowStartIndex = 0; + cellStartIndex = input.Length; + } + else + { + columnPrefix = firstChar; + cellStartIndex = 1; + } + } + + for (int i = cellStartIndex; i < input.Length; ++i) + { + var c = input[i]; + cellEndIndex = i + 1; + if (!char.IsLetter(c)) + { + // end of cell information + rowStartIndex = i; + cellEndIndex--; + break; + } + } + + for (int i = rowStartIndex; i < input.Length; ++i) + { + var c = input[i]; + + if (!char.IsNumber(c) && i == rowStartIndex) + { + // first is allowed to be a prefix + rowPrefix = c; + rowStartIndex++; + continue; + } + + if (!char.IsDigit(input[i])) + { + return false; + } + } + + // seems ok + var cellStringLength = cellEndIndex - cellStartIndex; + if (cellStringLength > 0) + { + column = input.Slice(cellStartIndex, cellStringLength); + } + + row = input.Slice(rowStartIndex); + return true; + } + + public static bool TryParsePositiveInt32Fast(ReadOnlySpan s, out int result) + { + int value = 0; + foreach (var c in s) + { + if (!char.IsDigit(c)) + { + result = -1; + return false; + } + + value = 10 * value + (c - 48); + } + + result = value; + return true; + } +} \ No newline at end of file diff --git a/testcases/Directory.Build.props b/testcases/Directory.Build.props index 51c4db1f2..2262377e2 100644 --- a/testcases/Directory.Build.props +++ b/testcases/Directory.Build.props @@ -4,6 +4,7 @@ false false $(MSBuildProjectDirectory)\..\..\test.runsettings + latest \ No newline at end of file diff --git a/testcases/main/CellReferenceParserTest.cs b/testcases/main/CellReferenceParserTest.cs new file mode 100644 index 000000000..fb56bb138 --- /dev/null +++ b/testcases/main/CellReferenceParserTest.cs @@ -0,0 +1,102 @@ +using System; +using NPOI; +using NPOI.SS.Util; +using NUnit.Framework; + +namespace TestCases; + +public class CellReferenceParserTest +{ + [TestCase("A", char.MinValue, "A", char.MinValue, "")] + [TestCase("1", char.MinValue, "", char.MinValue, "1")] + [TestCase("$A$1", '$', "A", '$', "1")] + [TestCase("$AB$12", '$', "AB", '$', "12")] + [TestCase("$A$12", '$', "A", '$', "12")] + [TestCase("$AB$1", '$', "AB", '$', "1")] + [TestCase("A1", char.MinValue, "A", char.MinValue, "1")] + [TestCase("AB1", char.MinValue, "AB", char.MinValue, "1")] + [TestCase("$A123", '$', "A", char.MinValue, "123")] + [TestCase("$123", char.MinValue, "", '$', "123")] + public void TryParseCellReferenceShouldSucceedForValidInput(string input, char expectedColumnPrefix, string expectedColumn, char expectedRowPrefix, string expectedRow) + { + Assert.True(CellReferenceParser.TryParseCellReference(input.AsSpan(), out var columnPrefix, out var column, out var rowPrefix, out var row)); + Assert.AreEqual(expectedColumnPrefix, columnPrefix, "Column prefix mismatch"); + Assert.AreEqual(expectedColumn, column.ToString(), "Column mismatch"); + Assert.AreEqual(expectedRowPrefix, rowPrefix, "Row prefix mismatch"); + Assert.AreEqual(expectedRow, row.ToString(), "Row mismatch"); + } + + [TestCase("$1$1")] + [TestCase("1$1")] + [TestCase("1$")] + public void TryParseCellReferenceShouldFailForInvalidInput(string input) + { + Assert.False(CellReferenceParser.TryParseCellReference(input.AsSpan(), out _, out _, out _, out _)); + } + + [TestCase("$A$1", "A", "1")] + [TestCase("$AB$12", "AB", "12")] + [TestCase("$A$12", "A", "12")] + [TestCase("$AB$1", "AB", "1")] + [TestCase("A1", "A", "1")] + [TestCase("AB1", "AB", "1")] + [TestCase("$A123", "A", "123")] + public void TryParseStrictCellReferenceShouldSucceedForValidInput(string input, string expectedColumn, string expectedRow) + { + Assert.True(CellReferenceParser.TryParseStrictCellReference(input.AsSpan(), out var column, out var row)); + Assert.AreEqual(expectedColumn, column.ToString(), "Column mismatch"); + Assert.AreEqual(expectedRow, row.ToString(), "Row mismatch"); + } + + [TestCase("$1$1")] + [TestCase("1$1")] + [TestCase("1$")] + [TestCase("A")] + [TestCase("1")] + public void TryParseStrictCellReferenceShouldFailForInvalidInput(string input) + { + Assert.False(CellReferenceParser.TryParseStrictCellReference(input.AsSpan(), out _, out _)); + } + + [TestCase("A", "A")] + [TestCase("$A", "A")] + [TestCase("$ABC", "ABC")] + public void TryParseColumnReferenceShouldSucceedForValidInput(string input, string expectedColumn) + { + Assert.True(CellReferenceParser.TryParseColumnReference(input.AsSpan(), out var column)); + Assert.AreEqual(expectedColumn, column.ToString(), "Column mismatch"); + } + + [TestCase("1")] + [TestCase("$A$1")] + [TestCase("$AB$12")] + [TestCase("$AB$")] + [TestCase("$A$12")] + [TestCase("$AB$1")] + [TestCase("A1")] + [TestCase("AB1")] + [TestCase("$A123")] + public void TryParseColumnReferenceShouldFailForInvalidInput(string input) + { + Assert.False(CellReferenceParser.TryParseColumnReference(input.AsSpan(), out _)); + } + + + [TestCase("1", "1")] + [TestCase("123", "123")] + [TestCase("$123", "123")] + public void TryParseRowReferenceShouldSucceedForValidInput(string input, string expectedRow) + { + Assert.True(CellReferenceParser.TryParseRowReference(input.AsSpan(), out var row)); + Assert.AreEqual(expectedRow, row.ToString(), "Row mismatch"); + } + + [TestCase("$")] + [TestCase("A$")] + [TestCase("$A")] + [TestCase("A")] + public void TryParseRowReferenceShouldFailForInvalidInput(string input) + { + Assert.False(CellReferenceParser.TryParseRowReference(input.AsSpan(), out _)); + } +} \ No newline at end of file