Is there a more pythonic way of converting excel-style columns to numbers (starting with 1)?
Working code up to two letters:
I made this one-liner:
colNameToNum = lambda cn: sum([((ord(cn[-1-pos]) - 64) * 26 ** pos) for pos in range(len(cn))])
It works by iterating through the letters in reverse order and multiplying by 1, 26, 26 * 26 etc, then summing the list. This method would be compatible with longer strings of letters, too.
I call it with:
print(colNameToNum("AA")) # 27
or
print(colNameToNum("XFD")) # the highest column allowed, I believe. Result = 16384
Use:
LETTERS = list(string.ascii_uppercase)
def column_number(column_id):
return sum([(LETTERS.index(j)+1)*(26**i) for i,j in enumerate(column_id[::-1])])
There are several parts to this one-liner, so here's the explanation:
column_id[::-1]
: reverses the string, e.g. converts 'AZ'
to 'ZA'
, there's a good reason to do so, which we will see in a bit.
enumerate()
: produces a iterable, e.g. (0, 'Z'), (1, 'A')
With some observation:
A -> 1 = (26**0)*1 # ** is the exponential operator
B -> 2 = (26**0)*2
Z -> 26 = (26**0)*26
AA -> 27 = (26**0)*1 + (26**1)*1
AB -> 28 = (26**0)*2 + (26**1)*1
AZ -> 52 = (26**0)*26 + (26**1)*1 # recall that we have (0, 'Z'), (1, 'A')
Reversing the column_id
and enumerate()
allows us to use the index as the exponent for 26. The rest is now trivial.
LETTERS.index(j)
: gives us the index of the letter in LETTERS
sum()
: takes a list of numbers and returns the total.
excel_col_num = lambda a: 0 if a == '' else 1 + ord(a[-1]) - ord('A') + 26 * excel_col_num(a[:-1])
excel_col_name = lambda n: '' if n <= 0 else excel_col_name((n - 1) // 26) + chr((n - 1) % 26 + ord('A'))
def excel_column_name(n):
"""Number to Excel-style column name, e.g., 1 = A, 26 = Z, 27 = AA, 703 = AAA."""
name = ''
while n > 0:
n, r = divmod (n - 1, 26)
name = chr(r + ord('A')) + name
return name
def excel_column_number(name):
"""Excel-style column name to number, e.g., A = 1, Z = 26, AA = 27, AAA = 703."""
n = 0
for c in name:
n = n * 26 + 1 + ord(c) - ord('A')
return n
def test (name, number):
for n in [0, 1, 2, 3, 24, 25, 26, 27, 702, 703, 704, 2708874, 1110829947]:
a = name(n)
n2 = number(a)
a2 = name(n2)
print ("%10d %-9s %s" % (n, a, "ok" if a == a2 and n == n2 else "error %d %s" % (n2, a2)))
test (excel_column_name, excel_column_number)
test (excel_col_name, excel_col_num)
0 ok
1 A ok
2 B ok
3 C ok
24 X ok
25 Y ok
26 Z ok
27 AA ok
702 ZZ ok
703 AAA ok
704 AAB ok
2708874 EXCEL ok
1110829947 COLUMNS ok
Here is one way to do it. It is a variation on code in the XlsxWriter module:
def col_to_num(col_str):
""" Convert base26 column string to number. """
expn = 0
col_num = 0
for char in reversed(col_str):
col_num += (ord(char) - ord('A') + 1) * (26 ** expn)
expn += 1
return col_num
>>> col_to_num('A')
1
>>> col_to_num('AB')
28
>>> col_to_num('ABA')
729
>>> col_to_num('AAB')
704
Here is a recursive solution:
def column_string_to_num(s):
n = ord(s[-1]) - 64
if s[:-1]:
return 26 * (column_string_to_num(s[:-1])) + n
else:
return n
column_string_to_num("AB")
#output: 28
The inverse can also be defined recursively, in a similar way:
def column_num_to_string(n):
n, rem = divmod(n - 1, 26)
next_char = chr(65 + rem)
if n:
return column_string(n) + next_char
else:
return next_char
column_num_to_string(28)
#output: 'AB'