Convert a number to Excel’s base 26

Question:

OK, I’m stuck on something seemingly simple. I am trying to convert a number to base 26 (ie. 3 = C, 27 = AA, ect.). I am guessing my problem has to do with not having a 0 in the model? Not sure. But if you run the code, you will see that numbers 52, 104 and especially numbers around 676 are really weird. Can anyone give me a hint as to what I am not seeing? I will appreciate it. (just in case to avoid wasting your time, @ is ascii char 64, A is ascii char 65)

def toBase26(x):
    x = int(x)
    if x == 0:
        return '0'
    if x < 0:
        negative = True
        x = abs(x)
    else:
        negative = False
    def digit_value (val):
        return str(chr(int(val)+64))
    digits = 1
    base26 = ""
    while 26**digits < x:
        digits += 1
    while digits != 0:
        remainder = x%(26**(digits-1))
        base26 += digit_value((x-remainder)/(26**(digits-1)))
        x = remainder
        digits -= 1
    if negative:
        return '-'+base26
    else:
        return base26

import io    
with io.open('numbers.txt','w') as f:
    for i in range(1000):
        f.write('{} is {}n'.format(i,toBase26(i)))

So, I found a temporary workaround by making a couple of changes to my function (the 2 if statements in the while loop). My columns are limited to 500 anyways, and the following change to the function seems to do the trick up to x = 676, so I am satisfied. However if any of you find a general solution for any x (may be my code may help), would be pretty cool!

def toBase26(x):
    x = int(x)
    if x == 0:
        return '0'
    if x < 0:
        negative = True
        x = abs(x)
    else:
        negative = False
    def digit_value (val):
        return str(chr(int(val)+64))
    digits = 1
    base26 = ""
    while 26**digits < x:
        digits += 1
    while digits != 0:
        remainder = x%(26**(digits-1))
        if remainder == 0:
            remainder += 26**(digits-1)
        if digits == 1:
            remainder -= 1
        base26 += digit_value((x-remainder)/(26**(digits-1)))
        x = remainder
        digits -= 1
    if negative:
        return '-'+base26
    else:
        return base26
Asked By: Yaroslav

||

Answers:

The problem when converting to Excel’s “base 26” is that for Excel, a number ZZ is actually 26 * 26**1 + 26 * 26**0 = 702 while normal base 26 number systems would make a 1 * 26**2 + 1 * 26**1 + 0 * 26**0 = 702 (BBA) out of that. So we cannot use the usual ways here to convert these numbers.

Instead, we have to roll our own divmod_excel function:

def divmod_excel(n):
    a, b = divmod(n, 26)
    if b == 0:
        return a - 1, b + 26
    return a, b

With that, we can create a to_excel function:

import string
def to_excel(num):
    chars = []
    while num > 0:
        num, d = divmod_excel(num)
        chars.append(string.ascii_uppercase[d - 1])
    return ''.join(reversed(chars))

For the other direction, this is a bit simpler

import string
from functools import reduce
def from_excel(chars):
    return reduce(lambda r, x: r * 26 + x + 1, map(string.ascii_uppercase.index, chars), 0)

This set of functions does the right thing:

>>> to_excel(26)
'Z'
>>> to_excel(27)
'AA'
>>> to_excel(702)
'ZZ'
>>> to_excel(703)
'AAA'
>>> from_excel('Z')
26
>>> from_excel('AA')
27
>>> from_excel('ZZ')
702
>>> from_excel('AAA')
703

And we can actually confirm that they work correctly opposite of each other by simply checking whether we can chain them to reproduce the original number:

for i in range(100000):
    if from_excel(to_excel(i)) != i:
        print(i)
# (prints nothing)
Answered By: poke

Sorry, I wrote this in Pascal and know no Python

function NumeralBase26Excel(numero: Integer): string;
var
  algarismo: Integer;
begin
  Result := '';
  numero := numero - 1;
  if numero >= 0 then
  begin
    algarismo := numero mod 26;
    if numero < 26 then
      Result := Chr(Ord('A') + algarismo)
    else
      Result := NumeralBase26Excel(numero div 26) + Chr(Ord('A') + algarismo);
  end;
end;

Simplest way, if you do not want to do it yourself:

from openpyxl.utils import get_column_letter

proper_excel_column_letter = get_column_letter(5)
# will equal "E"
Answered By: TheUltimateOptimist

You can do it in one line (with line continuations for easier reading). Written here in VBA:

Function sColumn(nColumn As Integer) As String

' Return Excel column letter for a given column number.

' 703 = 26^2 + 26^1 + 26^0
' 64 = Asc("A") - 1

sColumn = _
    IIf(nColumn < 703, "", Chr(Int((Int((nColumn - 1) / 26) - 1) / 26)       + 64)) & _
    IIf(nColumn <  27, "", Chr(   ((Int((nColumn - 1) / 26) - 1) Mod 26) + 1 + 64)) & _
                           Chr(   (     (nColumn - 1)            Mod 26) + 1 + 64)

End Function

Or you can do it in the the worksheet:

=if(<col num> < 703, "", char(floor((floor((<col num> - 1) / 26, 1) - 1) / 26, 1) + 64)) & 
 if(<col num> <  27, "", char(mod(   floor((<col num> - 1) / 26, 1) - 1, 26) + 1  + 64)) & 
                         char(mod(          <col num> - 1              , 26) + 1  + 64)

I’ve also posted the inverse operation done similarly.

Answered By: NewSites

Based on @TheUltimateOptimist’s answer, I looked in the openpyxl implementation and found the "actual" algorithm used by openpyxl==3.0.10:

Be warned; it only supports values between 1 & 18278 (inclusive).

def _get_column_letter(col_idx):
    """Convert a column number into a column letter (3 -> 'C')

    Right shift the column col_idx by 26 to find column letters in reverse
    order.  These numbers are 1-based, and can be converted to ASCII
    ordinals by adding 64.

    """
    # these indicies corrospond to A -> ZZZ and include all allowed
    # columns
    if not 1 <= col_idx <= 18278:
        raise ValueError("Invalid column index {0}".format(col_idx))
    letters = []
    while col_idx > 0:
        col_idx, remainder = divmod(col_idx, 26)
        # check for exact division and borrow if needed
        if remainder == 0:
            remainder = 26
            col_idx -= 1
        letters.append(chr(remainder+64))
    return ''.join(reversed(letters))
Answered By: Abhishek Divekar
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.