| using System.Collections.Frozen; |
| using System.Collections.Generic; |
| using System.Globalization; |
| |
| /// <summary> |
| /// A range address |
| /// </summary> |
| /// <remarks>Examples of addresses are "A1" "B1:C2" "A:A" "1:1" "A1:E2,G3:G5" </remarks> |
| internal static class ExcelAddressUtilities { |
| /// <summary> |
| /// Maximum number of columns in a worksheet (16384). |
| /// </summary> |
| private const int _maxColumns = 16384; |
| |
| /// <summary> |
| /// Maximum number of rows in a worksheet (1048576). |
| /// </summary> |
| private const int _maxRows = 1048576; |
| |
| private static readonly FrozenSet<char> _formulaChars = new HashSet<char> { |
| '(', |
| ')', |
| '+', |
| '-', |
| '*', |
| '/', |
| '.', |
| '=', |
| '^', |
| '&', |
| '%', |
| '\"', |
| }.ToFrozenSet(); |
| |
| public enum AddressType { |
| Invalid, |
| InternalAddress, |
| ExternalAddress, |
| InternalName, |
| ExternalName, |
| Formula, |
| } |
| |
| public static AddressType IsValid(string address) { |
| if (address == "#REF!") { |
| return AddressType.Invalid; |
| } |
| if (double.TryParse( |
| address, |
| NumberStyles.Any, |
| CultureInfo.InvariantCulture, |
| out _)) //A double, no valid address |
| { |
| return AddressType.Invalid; |
| } |
| if (IsFormula(address)) { |
| return AddressType.Formula; |
| } |
| if (SplitAddress(address, out var wb, out var intAddress)) { |
| if (intAddress.Contains( |
| '[')) //Table reference |
| { |
| return string.IsNullOrEmpty(wb) ? AddressType.InternalAddress : AddressType.ExternalAddress; |
| } |
| if (intAddress.Contains(',')) { |
| intAddress = intAddress[..intAddress.IndexOf(',')]; |
| } |
| if (IsAddress(intAddress)) { |
| return string.IsNullOrEmpty(wb) ? AddressType.InternalAddress : AddressType.ExternalAddress; |
| } |
| return string.IsNullOrEmpty(wb) ? AddressType.InternalName : AddressType.ExternalName; |
| } |
| return AddressType.Invalid; |
| } |
| |
| private static bool GetRowCol(string address, out int row, out int col, bool throwException) { |
| var colPart = true; |
| var colStartIx = 0; |
| var colLength = 0; |
| col = 0; |
| row = 0; |
| |
| if (address.EndsWith("#REF!")) { |
| row = 0; |
| col = 0; |
| return true; |
| } |
| |
| var sheetMarkerIndex = address.IndexOf('!'); |
| if (sheetMarkerIndex >= 0) { |
| colStartIx = sheetMarkerIndex + 1; |
| } |
| address = address.ToUpper(CultureInfo.InvariantCulture); |
| for (var i = colStartIx; i < address.Length; i++) { |
| var c = address[i]; |
| if (colPart && c >= 'A' && c <= 'Z' && colLength <= 3) { |
| col *= 26; |
| col += c - 64; |
| colLength++; |
| } else if (c >= '0' && c <= '9') { |
| row *= 10; |
| row += c - 48; |
| colPart = false; |
| } else if (c == '$') { |
| if (i == colStartIx) { |
| colStartIx++; |
| } else { |
| colPart = false; |
| } |
| } else { |
| row = 0; |
| col = 0; |
| if (throwException) { |
| throw new($"Invalid Address format {address}"); |
| } |
| return false; |
| } |
| } |
| return row != 0 || col != 0; |
| } |
| |
| private static bool IsAddress(string intAddress) { |
| if (string.IsNullOrEmpty(intAddress)) { |
| return false; |
| } |
| var cells = intAddress.Split(':'); |
| int toRow, |
| toCol; |
| |
| if (!GetRowCol(cells[0], out var fromRow, out var fromCol, false)) { |
| return false; |
| } |
| if (cells.Length > 1) { |
| if (!GetRowCol(cells[1], out toRow, out toCol, false)) { |
| return false; |
| } |
| } else { |
| toRow = fromRow; |
| toCol = fromCol; |
| } |
| return fromRow <= toRow |
| && fromCol <= toCol |
| && fromCol > -1 |
| && toCol <= _maxColumns |
| && fromRow > -1 |
| && toRow <= _maxRows; |
| } |
| |
| private static bool SplitAddress(string address, out string wb, out string intAddress) { |
| wb = ""; |
| intAddress = ""; |
| var text = ""; |
| var isText = false; |
| var brackPos = -1; |
| for (var i = 0; i < address.Length; i++) { |
| if (address[i] == '\'') { |
| isText = !isText; |
| if (i > 0 && address[i - 1] == '\'') { |
| text += "'"; |
| } |
| } else { |
| if (address[i] == '!' && !isText) { |
| if (text.Length > 0 && text[0] == '[') { |
| wb = text.Substring(1, text.IndexOf(']') - 1); |
| } |
| intAddress = address[(i + 1)..]; |
| return true; |
| } |
| if (address[i] == '[' && !isText) { |
| if (i |
| > 0) //Table reference return full address; |
| { |
| intAddress = address; |
| return true; |
| } |
| brackPos = i; |
| } else if (address[i] == ']' && !isText) { |
| if (brackPos > -1) { |
| wb = text; |
| text = ""; |
| } else { |
| return false; |
| } |
| } else { |
| text += address[i]; |
| } |
| } |
| } |
| intAddress = text; |
| return true; |
| } |
| |
| private static bool IsFormula(string address) { |
| var isText = false; |
| foreach (var c in address) { |
| if (c == '\'') { |
| isText = !isText; |
| } else { |
| if (isText == false && _formulaChars.Contains(c)) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| } |