If we assume that the start and end ranges will follow the same alternating pattern, and limit the range of digits to 0-9
and A-Z
, we can think of each group of digits as a component in a multi-dimensonal coordinate. For example, 1A
would correspond to the two-dimensional coordinate (1,A)
(which is what Excel uses to label its two-dimensional grid of rows and columns); whereas AA1BB2
would be a four-dimensional coordinate (AA,1,BB,2)
.
Because each component is independent, to expand the range between two coordinates we just return all combinations of the expansion of each component. Below is a quick implementation I cooked up this afternoon. It works for an arbitrary number of alternations of normal and alphabetic numbers, and handles large alphabetic ranges (i.e. from AB
to CDE
, not just AB
to CD
).
Note: This is intended as a rough draft of an actual implementation (I'm taking off tomorrow, so it is even less polished than usual ;). All the usual caveats regarding error handling, robustness, (readability ;), etc, apply.
IEnumerable<string> ExpandRange( string start, string end ) {
// Split coordinates into component parts.
string[] startParts = GetRangeParts( start );
string[] endParts = GetRangeParts( end );
// Expand range between parts
// (i.e. 1->3 becomes 1,2,3; A->C becomes A,B,C).
int length = startParts.Length;
int[] lengths = new int[length];
string[][] expandedParts = new string[length][];
for( int i = 0; i < length; ++i ) {
expandedParts[i] = ExpandRangeParts( startParts[i], endParts[i] );
lengths[i] = expandedParts[i].Length;
}
// Return all combinations of expanded parts.
int[] indexes = new int[length];
do {
var sb = new StringBuilder( );
for( int i = 0; i < length; ++i ) {
int partIndex = indexes[i];
sb.Append( expandedParts[i][partIndex] );
}
yield return sb.ToString( );
} while( IncrementIndexes( indexes, lengths ) );
}
readonly Regex RangeRegex = new Regex( "([0-9]*)([A-Z]*)" );
string[] GetRangeParts( string range ) {
// Match all alternating digit-letter components of coordinate.
var matches = RangeRegex.Matches( range );
var parts =
from match in matches.Cast<Match>( )
from matchGroup in match.Groups.Cast<Group>( ).Skip( 1 )
let value = matchGroup.Value
where value.Length > 0
select value;
return parts.ToArray( );
}
string[] ExpandRangeParts( string startPart, string endPart ) {
int start, end;
Func<int, string> toString;
bool isNumeric = char.IsDigit( startPart, 0 );
if( isNumeric ) {
// Parse regular integers directly.
start = int.Parse( startPart );
end = int.Parse( endPart );
toString = ( i ) => i.ToString( );
}
else {
// Convert alphabetic numbers to integers for expansion,
// then convert back for display.
start = AlphaNumberToInt( startPart );
end = AlphaNumberToInt( endPart );
toString = IntToAlphaNumber;
}
int count = end - start + 1;
return Enumerable.Range( start, count )
.Select( toString )
.Where( s => s.Length > 0 )
.ToArray( );
}
bool IncrementIndexes( int[] indexes, int[] lengths ) {
// Increment indexes from right to left (i.e. Arabic numeral order).
bool carry = true;
for( int i = lengths.Length; carry && i > 0; --i ) {
int index = i - 1;
int incrementedValue = (indexes[index] + 1) % lengths[index];
indexes[index] = incrementedValue;
carry = (incrementedValue == 0);
}
return !carry;
}
// Alphabetic numbers are 1-based (i.e. A = 1, AA = 11, etc, mod base-26).
const char AlphaDigitZero = (char)('A' - 1);
const int AlphaNumberBase = 'Z' - AlphaDigitZero + 1;
int AlphaNumberToInt( string number ) {
int sum = 0;
int place = 1;
foreach( char c in number.Cast<char>( ).Reverse( ) ) {
int digit = c - AlphaDigitZero;
sum += digit * place;
place *= AlphaNumberBase;
}
return sum;
}
string IntToAlphaNumber( int number ) {
List<char> digits = new List<char>( );
while( number > 0 ) {
int digit = number % AlphaNumberBase;
if( digit == 0 ) // Compensate for 1-based alphabetic numbers.
return "";
char c = (char)(AlphaDigitZero + digit);
digits.Add( c );
number /= AlphaNumberBase;
}
digits.Reverse( );
return new string( digits.ToArray( ) );
}