如何将列号(例如127)转换为Excel列(例如AA)

558

如何在C#中将数字转换为Excel列名,而不使用自动化从Excel直接获取值。

Excel 2007的可能范围是1到16384,这是它支持的列数。转换后的值应该是Excel列名的形式,例如A、AA、AAA等。


2
不要忘记可用列数的限制。例如:* Excel 2003(v11)最多到IV,2^8或256列。* Excel 2007(v12)最多到XFD,2^14或16384列。 - Unsliced
1
可能是重复的问题:如何找到对应于给定整数的Excel列名? - Daniel Trebbien
这个问题被标记为C#和Excel。我将此问题标记为过时,因为我们生活在2016年,有EPPLUS。这是一个常用的C#库,可以在服务器上创建高级Excel电子表格。它是根据GNU图书馆通用公共许可证(LGPL)提供的。使用EPPlus,您可以轻松地获取列字符串。 - Tony_KiloPapaMikeGolf
请注意,行和列的限制更多取决于文件格式而不是 Excel 版本,并且对于每个工作簿可能会有所不同。如果将其保存为旧版或新版格式,即使是同一工作簿,它们也可能会发生变化。 - Slai
@Tony_KiloPapaMikeGolf 我认为这并不过时。事实上,EPPLUS已经更改了许可证,这可能不适合各种原因。另外,如果你所需的只是如此简单的东西,为什么要引入一个库呢?我正在使用OpenXML以Excel格式导出数据,只需要一些像这里提问的算法。为什么要混合使用库呢?需求因人而异。该问题并不过时,对于简单的用例仍然很相关。话虽如此,EPPLUS是一个非常酷的库。 ;) - MetalMikester
如果您需要用于Aspose Cells,它们有内置的帮助程序。请参考:https://docs.aspose.com/cells/net/names-and-indices/。 - Uwe Keim
60个回答

9

虽然已经有很多有效的答案1,但没有一个涉及到其背后的理论。

Excel列名是它们编号的双射基数26表示法。这与普通的基数26(没有前导零)非常不同,我真的建议阅读维基百科条目以了解其中的区别。例如,十进制值702(分解为26*26 + 26)在“普通”的基数26中由110表示(即1x26^2 + 1x26^1 + 0x26^0),而在双射基数26中由ZZ表示(即26x26^1 + 26x26^0)。

除了差异外,双射数码是一种位值记数法,因此我们可以使用迭代(或递归)算法执行转换,在每次迭代中找到下一个位置的数字(类似于普通基础转换算法)。
获取十进制数m的双射基-k表示中最后一位数字(索引为0)的通用公式为(f为向上取整函数减1):
m - (f(m / k) * k)

下一位数码(即索引为1的数字)是通过将相同的公式应用于f(m / k)的结果来找到的。 我们知道对于最后一位数字(即具有最高索引的数字),f(m / k)为0。
这构成了在十进制数的双射基础-k中找到每个连续数字的迭代的基础。 伪代码如下所示(digit()将十进制整数映射到其在双射基数中的表示 - 例如,digit(1)将返回A)。
fun conv(m)
    q = f(m / k)
    a = m - (q * k)
    if (q == 0)
        return digit(a)
    else
        return conv(q) + digit(a);

所以我们可以将此翻译为C#2,以获得一个通用的“转换为双射基-k”ToBijective()例程:
class BijectiveNumeration {
    private int baseK;
    private Func<int, char> getDigit;
    public BijectiveNumeration(int baseK, Func<int, char> getDigit) {
        this.baseK = baseK;
        this.getDigit = getDigit;
    }

    public string ToBijective(double decimalValue) {
        double q = f(decimalValue / baseK);
        double a = decimalValue - (q * baseK);
        return ((q > 0) ? ToBijective(q) : "") + getDigit((int)a);
    }

    private static double f(double i) {
        return (Math.Ceiling(i) - 1);
    }
}

现在转换为双射基数26(我们的“Excel列名”用例):
static void Main(string[] args)
{
    BijectiveNumeration bijBase26 = new BijectiveNumeration(
        26,
        (value) => Convert.ToChar('A' + (value - 1))
    );

    Console.WriteLine(bijBase26.ToBijective(1));     // prints "A"
    Console.WriteLine(bijBase26.ToBijective(26));    // prints "Z"
    Console.WriteLine(bijBase26.ToBijective(27));    // prints "AA"
    Console.WriteLine(bijBase26.ToBijective(702));   // prints "ZZ"
    Console.WriteLine(bijBase26.ToBijective(16384)); // prints "XFD"
}

Excel的最大列索引为16384 / XFD,但此代码将转换任何正数。

作为额外的奖励,我们现在可以轻松地转换为任何双射基。例如对于双射基10

static void Main(string[] args)
{
    BijectiveNumeration bijBase10 = new BijectiveNumeration(
        10,
        (value) => value < 10 ? Convert.ToChar('0'+value) : 'A'
    );

    Console.WriteLine(bijBase10.ToBijective(1));     // prints "1"
    Console.WriteLine(bijBase10.ToBijective(10));    // prints "A"
    Console.WriteLine(bijBase10.ToBijective(123));   // prints "123"
    Console.WriteLine(bijBase10.ToBijective(20));    // prints "1A"
    Console.WriteLine(bijBase10.ToBijective(100));   // prints "9A"
    Console.WriteLine(bijBase10.ToBijective(101));   // prints "A1"
    Console.WriteLine(bijBase10.ToBijective(2010));  // prints "19AA"
}

1 这个通用的答案最终可以归结为其他正确的具体答案,但是我发现在没有掌握双射计数法背后的正式理论之前,很难完全理解解决方案的逻辑。这也很好地证明了它的正确性。此外,还有几个类似的问题与这个问题相连,其中一些是与语言无关或更通用的。这就是为什么我认为添加这个答案是必要的,并且这个问题是放置它的好地方。

2 C# 免责声明:我在这里实现了一个 C# 的例子,因为这是所要求的,但我从未学习过也从未使用过这种语言。我已经验证它可以编译和运行,但如果需要,请根据最佳实践/一般约定来适应它。

3 此示例仅旨在正确和易于理解;如果性能至关重要,则可以进行优化(例如使用尾递归,但似乎在 C# 中需要 trampolining),并使其更安全(例如通过验证参数)。


这个答案提供了理论,值得更多点赞。 - Stuart Grassie

9

..并转换为php:

function GetExcelColumnName($columnNumber) {
    $columnName = '';
    while ($columnNumber > 0) {
        $modulo = ($columnNumber - 1) % 26;
        $columnName = chr(65 + $modulo) . $columnName;
        $columnNumber = (int)(($columnNumber - $modulo) / 26);
    }
    return $columnName;
}

使用ord('A')代替65。 - Mulli

9

这里提供一个简单的C#递归实现,仅两行代码。因为其他答案看起来比必要的复杂。

/// <summary>
/// Gets the column letter(s) corresponding to the given column number.
/// </summary>
/// <param name="column">The one-based column index. Must be greater than zero.</param>
/// <returns>The desired column letter, or an empty string if the column number was invalid.</returns>
public static string GetColumnLetter(int column) {
    if (column < 1) return String.Empty;
    return GetColumnLetter((column - 1) / 26) + (char)('A' + (column - 1) % 26);
}

6
我想分享我使用的静态类,用于在列索引和列标签之间进行互操作。我对“ColumnLabel”方法的答案进行了修改。
public static class Extensions
{
    public static string ColumnLabel(this int col)
    {
        var dividend = col;
        var columnLabel = string.Empty;
        int modulo;

        while (dividend > 0)
        {
            modulo = (dividend - 1) % 26;
            columnLabel = Convert.ToChar(65 + modulo).ToString() + columnLabel;
            dividend = (int)((dividend - modulo) / 26);
        } 

        return columnLabel;
    }
    public static int ColumnIndex(this string colLabel)
    {
        // "AD" (1 * 26^1) + (4 * 26^0) ...
        var colIndex = 0;
        for(int ind = 0, pow = colLabel.Count()-1; ind < colLabel.Count(); ++ind, --pow)
        {
            var cVal = Convert.ToInt32(colLabel[ind]) - 64; //col A is index 1
            colIndex += cVal * ((int)Math.Pow(26, pow));
        }
        return colIndex;
    }
}

使用方法如下...

30.ColumnLabel(); // "AD"
"AD".ColumnIndex(); // 30

4
在 Delphi (Pascal) 中:
function GetExcelColumnName(columnNumber: integer): string;
var
  dividend, modulo: integer;
begin
  Result := '';
  dividend := columnNumber;
  while dividend > 0 do begin
    modulo := (dividend - 1) mod 26;
    Result := Chr(65 + modulo) + Result;
    dividend := (dividend - modulo) div 26;
  end;
end;

4
在Perl中,对于输入的1(A),27(AA)等等。
sub excel_colname {
  my ($idx) = @_;       # one-based column number
  --$idx;               # zero-based column index
  my $name = "";
  while ($idx >= 0) {
    $name .= chr(ord("A") + ($idx % 26));
    $idx   = int($idx / 26) - 1;
  }
  return scalar reverse $name;
}

4

如果您只想用单元格公式而不是代码实现,这里提供一个公式:

IF(COLUMN()>=26,CHAR(ROUND(COLUMN()/26,1)+64)&CHAR(MOD(COLUMN(),26)+64),CHAR(COLUMN()+64))

4
private String getColumn(int c) {
    String s = "";
    do {
        s = (char)('A' + (c % 26)) + s;
        c /= 26;
    } while (c-- > 0);
    return s;
}

它并不完全是26进制,因为该系统中没有0。如果有的话,'Z'将被'B A'而不是'AA'所跟随。


4

虽然有点晚了,但这是我在C#中使用的代码:

private static readonly string _Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
public static int ColumnNameParse(string value)
{
    // assumes value.Length is [1,3]
    // assumes value is uppercase
    var digits = value.PadLeft(3).Select(x => _Alphabet.IndexOf(x));
    return digits.Aggregate(0, (current, index) => (current * 26) + (index + 1));
}

2
你做了与要求相反的事情,但是因为你的lambda技巧而加1分。 - nurettin
1
IndexOf 很慢,最好预先计算反向映射。 - Vlad

3

不知道是否有用,这是Graham的Powershell代码:

function ConvertTo-ExcelColumnID {
    param (
        [parameter(Position = 0,
            HelpMessage = "A 1-based index to convert to an excel column ID. e.g. 2 => 'B', 29 => 'AC'",
            Mandatory = $true)]
        [int]$index
    );

    [string]$result = '';
    if ($index -le 0 ) {
        return $result;
    }

    while ($index -gt 0) {
        [int]$modulo = ($index - 1) % 26;
        $character = [char]($modulo + [int][char]'A');
        $result = $character + $result;
        [int]$index = ($index - $modulo) / 26;
    }

    return $result;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接