Fork me on GitHub
微软白板Excel xls列号数字转字母

Excel xls列号数字转字母

https://blog.csdn.net/lf124/article/details/53432817?utm_source=itdadao&utm_medium=referral

最近遇到导出的xls中 列是动态生成的,且单元格中需要用到公式,而xls公式不是用数字列号而是用列字母来表示的,这时需要把数字的列号转成该列对应的字母。因为是按月导出 一个月最多31天,所以刚开始采用的办法是定义一个包含1到31列字母的数组。后来想想这样总不是个办法 万一列数更多 且是不确定的呢。于是研究了下 怎么把xls数字列号转成对应的字母。

先来看xlsx中的字母规律。在xls中,1到26列是A~Z,从第27列开始 是2个以上的字母组合 AA AB ... AZ 然后到BA BB ... BZ 直到 ZA ZB ... ZZ 这时两个字母的组合完了 接下来到3个字母 AAA AAB ... AAZ 然后到ABA ABB ... ABZ 一直到AZZ 然后是BAA BAB ... BZZ最后到ZAA ... ZZZ,接下来又是4个字母的组合如此循环...  。

于是发现了规律: AAAZ是在AZ前面加了A,BABZ是在AZ前面加了B,... ZAZZ是在AZ前面加了Z,这时两个字母组合完毕;到3个字母 AAAAAZ是在AAAZ前面加了A,ABAABZ是在BABZ前面加了A,... AZAAZZ是在ZAZZ前面加了A,这时AAZZ遍历完了一次(这里的AAZZ是前面两个字母组合里出现过的);接下来到下一轮遍历 在前面加B,BAABAZ是在AAAZ前面加了B,... BZABZZ是在ZAZZ前面加了B,这时AA~ZZ又遍历完了一次;再进行下一轮遍历 一直到在所有两个字母的组合前都加过A~Z,这时3个字母的组合就全部组合完毕了。接下来到4个字母的组合 跟前面的2个、3个字母的组合类似,都是在上一个组合的基础上分别在前面加上A~Z。如下图

字母组合是从第27列开始,每次的组合都是在上一个组合的基础上分别在前面加上AZ。比如计算n=3个字母的组合AAAZZZ(上图红色竖线箭头部分):2个字母组合所在的范围是AA~ZZ(上图红色的“currentLen”部分),当前数组的位置(比如AAB)i与currentLen进行取余运算(i%currentLen)得到的结果就是要与2个字母组合的哪个种组合进行字符串拼接(该值还要与“lastLen”(上图红色lastLen)相加才能定位到该位置对应的字母),比如当前是第一轮遍历 则是A与AB进行拼接成AAB,这也就是上面举例的当前数组位置对应的字母(AAB)。假设我们用变量letterIdx表示第几轮,每上个组合遍历完一次letterIdx都要+1,表示下一个要拼接在前面的字母。因为拼接在前面的字母是A~Z 所以每次取该轮字母时都要跟26取余(letterIdx%26),当letterIdx%26=0时 说明A~Z都与上个组合拼接过了 也就是当前n个字母组合所有情况都组合过了,再进行下一轮n+1个(4个)字母的组合(上图黑色竖线箭头部分),这时currentLen变成了上一个组合的长度(上图黑色的currentLen),而lastLen变成了“上个组合的currentLen+lastLen”(上图黑色的lastLen部分),接下来的循环遍历跟上个组合一样进行。

上代码:(代码里的注释请结合上文来看,注释里说到的“组合的情形”是指该组合的某一种组合,如BK、AH、XI都是2个字母组合的一种情形。因为实在也不知道应该用哪个词来表达)

public final class Columns {

private Columns() {}

private static String[] sources = new String[]{
	"A","B","C","D","E","F","G","H",
	"I","J","K","L","M","N","O","P",
	"Q","R","S","T","U","V","W","X","Y","Z"
};

/**
 * (256 for *.xls, 16384 for *.xlsx)
 * @param columnNum 列的个数,至少要为1
 * @throws IllegalArgumentException 如果 columnNum 超出该范围 [1,16384]
 * @return 返回[1,columnNum]共columnNum个对应xls列字母的数组
 */
public static String[] getColumnLabels(int columnNum) {
	if(columnNum<1||columnNum>16384)
		throw new IllegalArgumentException();
	String[] columns = new String[columnNum];
	if(columnNum<27){	//小于27列 不用组合
		System.arraycopy(sources, 0, columns, 0, columnNum);
		return columns;
	}		
	System.arraycopy(sources, 0, columns, 0, 26);	//前26列不需要进行组合

	//因为基于数组是从0开始,每到新一轮letterIdx 会递增,所以第一轮 在递增前是-1
	int letterIdx = -1;
	int currentLen = 26;//第一轮组合(2个字母的组合)是分别与A-Z进行拼接 所以是26
	int remainder;
	int lastLen = 0;	//用于定位上文提到的i%currentLen实际在数组中的位置		
	int totalLen = 26;	//totalLen=currentLen+lastLen
	int currentLoopIdx = 0; //用来记录当前组合所有情形的个数

	for(int i=26;i<columnNum;i++){ //第27列(对应数组的第26个位置)开始组合

//currentLen是上个组合所有情形的个数,与它取余找到要与上个组合的哪种情形进行拼接
		remainder = currentLoopIdx%currentLen;

		if(remainder==0){
			letterIdx++; //完成一次上个组合的遍历,转到下个字母进行拼接
			int j = letterIdx%26;

		//A-Z 26个子母都与上个组合所有情形都进行过拼接了,需要进行下个组合的拼接
			if(j==0&&letterIdx!=0){ 
				lastLen = totalLen; //下个组合的lastLen是上个组合的totalLen

			/**
 		     * 下个组合的currentLen是上个组合的所有组合情形的个数
 		     * (等于上个组合的currentLen*26),26也就是拼接在前面的A-Z的个数
 		     */			 
				currentLen = 26*currentLen;

				totalLen = currentLen+lastLen; //为下一轮的开始做准备
				currentLoopIdx = 0; //到下一轮了 因此需要重置
			}
		}
		/**
 	     * sources[letterIdx%26]是该轮要拼接在前面的字母
 	     * columns[remainder+lastLen]是上个组合被拼接的情形
 	     */		
		columns[i] = sources[letterIdx%26]+columns[remainder+lastLen];
		currentLoopIdx++;
	}
	return columns;
}

}
测试:
public static void main(String[] args) {
String[] columns = getColumnLabels(37 );
System.out.println("1到37列:"+Arrays.toString(columns));
System.out.println();
long start = System.nanoTime();
columns = getColumnLabels(256);
System.out.println("创建"+columns.length+"列用时(纳秒):"
+(System.nanoTime()-start));
System.out.println("xls第"+columns.length+"列:"
+columns[columns.length-1]);
System.out.println();
start = System.nanoTime();
columns = getColumnLabels(16384);
System.out.println("创建"+columns.length+"列用时(纳秒):"
+(System.nanoTime()-start));
System.out.println("xlsx第"+columns.length+"列:"
+columns[columns.length-1]);
}
打印:
1到37列:[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T,
U, V, W, X, Y, Z, AA, AB, AC, AD, AE, AF, AG, AH, AI, AJ, AK]

创建256列用时(纳秒):192833
xls第256列:IV

创建16384列用时(纳秒):9574147
xlsx第16384列:XFD
------------------------------------------------------分隔线-----------------------------------------------------------------

后来有想到,如果是不需要(或者条件不允许)提前创建(这么大的)数组呢,这时需要通过列号直接获取该列对应的字母。于是又想 应该怎么转换。。。
先看看从字母转数字的,比如BGQCV 转成数字(虽然xlsx最后一列是XFD,这里只讨论数字与字母的互转):该列标有5个字母,说明前面已经有4个字母、3、2、1个字母的全组合了 才会到5个字母的组合,于是W1=264+263+262+261,BGQCV 的第一个字母是B,说明前面有Axxxx的全组合了,于是有该组合数T1=1(264)。再来看第二个字母是G,说明前面已经有BAxxx~BFxxx的全组合了,于是有该组合数T2=6*(263)。第三个字母是Q,说明前面已经有BGAxxBGPxx的全组合了,于是有该组合数T3=16*(26^2)。第四个字母是C,说明前面已有BGQAxBGQBx的全组合了,该组合数T4=2(26^1)。最后一个字母是V,说明前缀是BGQC的组合BGQCA~BGQCV共有T5=22个。
好了,BGQCV 所处的列数W=W1+T1+T2+T3+T4+T5=(264+263+262+261)+1(264)+6*(263)+16(262)+2*(261)+22 = 1048576。
String[] sources = new String[]{"A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"};
数组sources作为我们解决问题的字母来源(当然也可以不定义数组而是用ASCII码)。
现在知道了通过字母列标是如何求出其对应的第几列,我们把问题一般化:求第W列对应的列标。设第W列对应的字母有n个,根据上面的分析可知W1=26(n-1)+26(n-2)+26(n-3)+...+262+26^1。我们规定A对应1,B对应2 ... Z对应26。设第1个字母对应数字是num_1,第二个字母对应的数字是num_2,...,第n个字母对应数字num_n。根据上面的分析 于是有T1=(num_1-1)26(n-1),T2=(num_2-1)*26(n-2),...,Tn=(num_n-1)26^0。
整合起来,又因为26^0=1 于是有W=W1+T1+T2+...+Tn=(26(n-1)+26(n-2)+26(n-3)+...+262+261)+(num_1-1)*26(n-1)+(num_2-1)26(n-2)+...+(num_(n-1)-1)*261+(num_n-1)。
除了最右边的字母(也就是第n个字母)对应的位置的数字num_n-1除外,其它字母对应的位置的数字均是26的倍数。于是  第一次W对26取余W%26的结果所对应的字母(还记得吗 我们用1代表A,2代表B,... ,26代表Z)就是最右边的字母。好了 现在已经求出了最右边的字母,还剩n-1个未知字母,采用同样的办法可求出次右边的字母:W-(num_n-1)然后再除以26,得到的结果再减去1(因为W1中存在26^1,其除以26后结果就是1,为了保证除了次右边字母对应的位置的数字外,其余各字母对应的位置的数字均是26的倍数),把结果赋回给W,这时求次右边的字母就跟求最右边的字母类似了:
W-(num_n-1)=(26(n-1)+26(n-2)+26(n-3)+...+262+261)+(num_1-1)*26(n-1)+(num_2-1)
26(n-2)+...+(num_(n-1)-1)*261
然后两边除以26:(W-(num_n-1))/26=(26(n-2)+26(n-3)+26(n-4)+...+261+1)+(num_1-1)26(n-2)+(num_2-1)*26(n-3)+...+(num_(n-1)-1)
然后两边再减去1:(W-(num_n-1))/26-1=(26(n-2)+26(n-3)+26(n-4)+...+261)+(num_1-1)
26(n-2)+(num_2-1)*26(n-3)+...+(num_(n-1)-1)
然后把左边的(W-(num_n-1))/26-1看成整体的W,是不是跟最开始求最右边字母的很类似?
这样从右往左求出来的字符串 跟所要的结果恰好是相反的 所要需要反转。
上代码:
/**

  • 返回该列号对应的字母

  • @param columnNo (xls的)第几列(从1开始)
    */
    public static String getCorrespondingLabel(int columnNo){
    if(columnNo<1/||columnNo>16384/)
    throw new IllegalArgumentException();
    String[] sources = new String[]{"A","B","C","D","E","F","G","H","I","J","K","L","M"
    ,"N","O","P","Q","R","S","T","U","V","W","X","Y","Z"};
    StringBuilder sb = new StringBuilder(5);
    int remainder = columnNo%26; //求最右边的字母
    if(remainder==0){ //说明(num_n-1)=26,第26个字母是Z
    sb.append("Z");
    remainder = 26; //因为接下来W-(num_n-1)也就是columnNo-remainder,所以需要把remainder赋值回26
    }
    else{ //如果最右边字母不是Z的话,就去sources数组相应的位置取字母,remainder不用变
    sb.append(sources[remainder-1]);
    }
    columnNo = (columnNo-remainder)/26-1; //用来判断接下来是否还有其他字母

    //当 当前循环是求最后一个字母时(从右往左),(columnNo-remainder)/26就会是0,再减1也就是-1。
    //因此通过判断(columnNo-remainder)/26-1是否大于-1来判断结束
    while(columnNo>-1){
    remainder = columnNo%26;
    sb.append(sources[remainder]);
    columnNo = (columnNo-remainder)/26-1;
    }

    return sb.reverse().toString(); //因为是从右往左解析的 所以需要反转
    }

测试:
public static void main(String[] args) {
String label = getCorrespondingLabel(37 );
System.out.println("第37列:"+label);
System.out.println();
long start = System.nanoTime();
label = getCorrespondingLabel(256);
System.out.println("查找第256列对应字母 用时(纳秒):"
+(System.nanoTime()-start));
System.out.println("xls第256列:"+label);
System.out.println();

label = getCorrespondingLabel(16384);

System.out.println("xlsx第16384列:"+label);

}
打印:
第37列:AK

查找第256列对应字母 用时(纳秒):7776
xls第256列:IV

xlsx第16384列:XFD
这时又想到,求n列列标 用方法1好一点呢 还是用方法2循环n次好呢。。。
方法1求数组的 对于26+26*26=702列以下,可以将求组合部分抽出来,减少不必要的求余运算multiple%26,因为这时multiple不会超过26。方法2也可以改成从0开始的 毕竟poi列数是从0开始的。不预定义字母数组的话 也可以用ASCII码来转换。
-----------------------------------------------分隔线-----------------------------------------------------
最终代码:

/**
*

  • Excel列号转字母工具类

*/
public final class Columns {

private Columns() {
}

private static String[] sources = new String[] { "A", "B", "C", "D", "E",
		"F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R",
		"S", "T", "U", "V", "W", "X", "Y", "Z" };

/**
 * (256 for *.xls, 16384 for *.xlsx)
 * 
 * @param columnNum
 *            列的个数,从1开始
 * @throws IllegalArgumentException
 *             如果 columnNum 超出该范围 [1,16384]
 * @return 返回[1,columnNum]共columnNum个对应xls列字母的数组
 */
public static String[] getColumnLabels(int columnNum) {
	if (columnNum < 1 || columnNum > 16384)
		throw new IllegalArgumentException();
	String[] columns = new String[columnNum];
	if (columnNum < 27) {
		System.arraycopy(sources, 0, columns, 0, columnNum);
		return columns;
	}
	int multiple = -1;
	int remainder;
	System.arraycopy(sources, 0, columns, 0, 26);
	int currentLoopIdx = 0;
	if (columnNum < 703) {
		for (int i = 26; i < columnNum; i++) {
			remainder = currentLoopIdx % 26;
			if (remainder == 0) {
				multiple++;
			}
			columns[i] = sources[multiple] + columns[remainder];
			currentLoopIdx++;
		}
	} else {
		int currentLen = 26;
		int totalLen = 26;
		int lastLen = 0;
		for (int i = 26; i < columnNum; i++) {
			remainder = currentLoopIdx % currentLen;
			if (remainder == 0) {
				multiple++;
				int j = multiple % 26;
				if (j == 0 && multiple != 0) {
					lastLen = totalLen;
					currentLen = 26 * currentLen;
					totalLen = currentLen + lastLen;
					currentLoopIdx = 0;
				}
			}
			columns[i] = sources[multiple % 26]
					+ columns[remainder + lastLen];
			currentLoopIdx++;
		}
	}

	return columns;
}

/**
 * 返回该列号对应的字母
 * 
 * @param columnNo
 *            (xls的)第几列(从1开始)
 */
private static String getCorrespondingLabel(int columnNo) {
	if (columnNo < 1/** ||columnNo>16384 **/
	)
		throw new IllegalArgumentException();

	StringBuilder sb = new StringBuilder(5);
	int remainder = columnNo % 26;
	if (remainder == 0) {
		sb.append("Z");
		remainder = 26;
	} else {
		sb.append(sources[remainder - 1]);
	}

	while ((columnNo = (columnNo - remainder) / 26 - 1) > -1) {
		remainder = columnNo % 26;
		sb.append(sources[remainder]);
	}

	return sb.reverse().toString();
}

/**
 * 列号转字母
 * 
 * @param columnIndex
 *            poi里xls的列号(从0开始)
 * @throws IllegalArgumentException
 *             if columnIndex less than 0
 * @return 该列对应的字母
 */
public static String getIndexLabel(int columnIndex) {
	return getCorrespondingLabel(columnIndex + 1);
}

}

微软白板

posted on 2018-10-16 11:52  HackerVirus  阅读(1542)  评论(0编辑  收藏  举报