将数字转换为excel的列名

293 阅读13分钟

要点

数字(0,1,2...)转为excel的列名(A,B,C...)

/**
 * @param number num 0,1,2,3...
 * @return string A-Z,AA-ZZ,AAA-ZZZ....(excel的列名)
 */
const getColumnByNum = (num) => {
  let offset = 26
  const getCode = (num) => String.fromCharCode('A'.charCodeAt(0) + num)
  const digits = 26
  let length = 1//至少有一位字母
  while (num >= offset) {
    num = num - offset//累减 -(26+26^2+...+26^N)
    offset = offset * 26
    length++
  }
  let str = getCode(num % digits)
  while (num >= digits) {
    num = Math.floor(num / digits)
    str = getCode(num % digits) + str
  }
  str = 'A'.repeat(length - str.length) + str//往前补加A
  return str
};
/**
 * @param number end 0,1,2,3...
 * @param number start 0,1,2,3...
 * @return string[] [A,B,C,D...,XXX](excel的列名)
 */
const getColumns = (end,start)=>{
    let arr = start ? [getColumnByNum(start)]:['A']
    const getNextColumn = (column)=>{
        let str = ''
        let differ = 1
        for(let i=column.length-1;i>-1;i--){
            if(column[i]==='Z' && differ!==0){
                str ='A' + str 
            }else{
                str = String.fromCharCode(column[i].charCodeAt(0) + differ) + str
                differ = 0
            }
        }
        differ === 1 &&(str = 'A' + str )//说明column为Z+
        return str
    }
    for(let i = 0,len = end-(start || 0) ;i<len;i++){
        arr.push(getNextColumn(arr[i]))
    }
    return arr
}
//测试
((start, end) => {
  let arr = getColumns(end,start)
  for (let i = start; i < end; i++) {
    let str = getColumnByNum(i)
    let nextStr = getColumnByNum(i + 1)
    if(arr[i-start] !== str){
        throw new Error(`第 "${i}" 列 "${str}"、"${arr[i-start]}" 出错`)
    }
    if (/[^A-Z]/.test(str)) {
      throw new Error(`第 "${i}" 列 "${str}" 含有错误字符`)
    }
    if (str.length === nextStr.length) {
      //如果长度相等
      let differ = 1
      for (let j = str.length - 1; j > -1; j--) {
        //如果当前结尾为Z
        if (str.charAt(j) === 'Z' && nextStr.charAt(j) === 'A') {
        } else if (nextStr.charCodeAt(j) - str.charCodeAt(j) === differ) {
          differ = 0
        } else {
          throw new Error(`第 "${i}" 列 "${str}" 发生错误,与下一列 "${nextStr}" 未相邻`)
        }
      }
      if (differ !== 0) {
        throw new Error(`第 "${i}" 列 "${str}" 发生错误,与下一列 "${nextStr}" 未相邻`)
      }
    } else  //如果长度不相等,则str肯定是Z+,nextStr肯定是A+,且A+比Z+多一个
      if (!(/^[Z]+$/.test(str)
        && /^[A]+$/.test(nextStr)
        && nextStr.length - str.length === 1)) {
        throw new Error(`第 "${i}" 列 "${str}" 发生错误,与下一列 "${nextStr}" 未相邻`)
      }

  }
  console.log('测试通过')
  console.log(arr)
})(10000, 1000000);//此处改变测试范围

1 进制转换

在之前,用excel.js写过一个转换,本来打算自己生成列名(将数字(0,1,2...)转为excel的列名(A,B,C...))。后来试了两次居然都失败了,没办法,只好用它提供的方法了,并乖乖复习一下进制转换的内容。

1.1 10进制转换为其他进制

转换为其他进制表现为满“N”进一

const parseDecToDigits = (num: number, digits: number, codeArr: string[])
    : string => {
    if (digits === 10) {
        return String(num)
    }
    const getCode = (num: number): string => codeArr[num]
    let str: string = getCode(num % digits)
    while (num >= digits) {
        num = Math.floor(num / digits)
        str = getCode(num % digits) + str
    }
    return str
}

1.2 其他进制转为10进制

const parseToDec = (str: string, digits: number, codeArr: string[])
    :number => {
    if (digits === 10) {
        return Number(str) || 0
    }
    const getNum = (code: string): number => Number(codeArr.findIndex(c => c === code)) || 0
    let sum = str.split('').reduce(
        (preSum: number, code, index) => preSum
            + getNum(code)
            * Math.pow(digits, str.length - index - 1)
        , 0)
    return sum
}

1.3 测试

const parseDecToDigits = (num, digits, codeArr) => {
    if (digits === 10) {
        return String(num)
    }
    const getCode = (num) => codeArr[num]
    let str = getCode(num % digits)
    while (num >= digits) {
        num = Math.floor(num / digits)
        str = getCode(num % digits) + str
    }
    return str
}
const parseToDec = (str, digits, codeArr) => {
    if (digits === 10) {
        return Number(str) || 0
    }
    const getNum = (code) => Number(codeArr.findIndex(c => c === code))
    let sum = str.toString().split('').reduce(
        (preSum, code, index) => preSum
            + getNum(code)
            * Math.pow(digits, str.length - index - 1)
        , 0)
    return sum
}
//测试 将10进制转换为其他进制,再转换为10进制,相等即说明无问题
((minNum, maxNum, minDigits, maxDigits) => {
    minDigits < 2 && (minDigits = 2)
    for (let digit = minDigits; digit < maxDigits; digit++) {
        let codeArr = []
        for (let i = 0; i < digit; i++) {
          //判断是否大于9,大于9就用字母表示某位
            if (i < 10) {
                codeArr.push(String(i))
            } else {
                codeArr.push(String.fromCharCode('A'.charCodeAt(0) + i - 10))
            }
        }
        for (let cnum = minNum; cnum < maxNum; cnum++) {
            let str = parseDecToDigits(cnum, digit,codeArr)
            let num = parseToDec(str, digit,codeArr)
            if (cnum !== num) {
                throw new Error(` "${cnum}" 的 "${maxDigits}" 进制转换出错`)
            }
        }
    }
    console.log(`${minDigits}-${maxNum}${minDigits}进制-${maxDigits}进制相互转换,测试通过`)
})(0, 100000, 35, 64)

1.4 任意进制转换为任意进制

const parseDigit = //转换成10进制,再从10进制转换成其他进制
    (fromStr: string | number,
        fromCodes: string[],
        fromDigit: number,
        toDigit: number,
        toCodes: string[]) => parseDecToDigits(
            parseToDec(fromStr.toString(), fromDigit, fromCodes),
            toDigit,
            toCodes
        )

2 将数字(0,1,2...)转为excel的列名(A,B,C...)

2.1 错误版

一开始认为,A-Z 26个字母,转为26进制不就ok了,结果错了

const parseDecToDigits = (num, digits, codeArr) => {
    if (digits === 10) {
        return String(num)
    }
    const getCode = (num) => codeArr[num]
    let str = getCode(num % digits)
    while (num >= digits) {
        num = Math.floor(num / digits)
        str = getCode(num % digits) + str
    }
    return str
}
//形成闭包,将[A,B,C...Z]存起来
const getColumnByNum = (() => {
    let codeArr = []
    //将codeArr变为[A,B,C...Z]
    for (let i = 0; i < 26; i++) {
        codeArr.push(String.fromCharCode('A'.charCodeAt(0) + i))
    }
    return (num)=>parseDecToDigits(num, 26, codeArr)
})();//立即执行函数后加分号,不然易报错
//测试
((start, end) => {
    let arr = []
    for (let i = start; i < end; i++) {
        let str = getColumnByNum(i)
        let nextStr = getColumnByNum(i + 1)
        arr.push(str)
        if (/[^A-Z]/.test(str)) {
            throw new Error(`第 "${i}" 列 "${str}" 含有错误字符`)
        }
        if (str.length === nextStr.length) {
            //如果长度相等
            let differ = 1
            for (let j = str.length - 1; j > -1; j--) {
                //如果当前结尾为Z
                if (str.charAt(j) === 'Z' && nextStr.charAt(j) === 'A') {
                } else if (nextStr.charCodeAt(j) - str.charCodeAt(j) === differ) {
                    if (differ === 1) {
                        differ = 0
                    }
                } else {
                    throw new Error(`第 "${i}" 列 "${str}" 发生错误,与下一列 "${nextStr}" 未相邻`)
                }
            }
            if(differ !== 0){
                throw new Error(`第 "${i}" 列 "${str}" 发生错误,与下一列 "${nextStr}" 未相邻`)
            }
        } else  //如果长度不相等,则str肯定是Z+,nextStr肯定是A+,且长度... 
            if (!(/^[Z]+$/.test(str)
                && /^[A]+$/.test(nextStr)
                && nextStr.length - str.length === 1)) {
                throw new Error(`第 "${i}" 列 "${str}" 发生错误,与下一列 "${nextStr}" 未相邻`)
            }

    }
    console.log('测试通过')
    console.log(arr)
})(0, 1000000);//此处改变测试范围

结果,错了。

excel26error

2.2 正确版

仔细想了想,excel的列不是26进制,也不是27进制,当A位于左边第一位时,代表1,位于右边时,代表0

且有一位数时,A-Z有26种组合,两位数时AA-ZZ有26*26种组合,即N位有26^N种组合

而10进制数,0-9有10种组合,10-99有100-10种组合,即N位有10^N-10^(N-1)中组合

也就是说,26进制表示方法,N位数值都是从26^N-1开始,到26^N结束

而excel的列,无论几位数,都是从0(即A*A(个数为位数))开始,到26^N(Z*Z)结束

即如下所示:

26进制:0,1,2...25,26...26*26-1,26*26...26*26*26-1...

excel列:0...25,0....26*26-1,0...26*26*26-1

excel的列高于一位时,比26进制多出来26^N-1+...+26个数值,即要减去这个数值

所以编程时,我们先确定excel的列起始位置重置为0,即累减每一个字母该减的数字,

并依此确定列名(A-Z+)的长度

//0-25,一位,26-26*26+25,两位,...上一位的结束数值+1,...上一位的结束数值+26^N
let offset = 26
let length = 0
while(num >= offset){
    num = num - offset
    offset = offset*26
    length++
}

然后,因为重置为0了,把A也是当作0来处理的,前面位数可能会少A,需要补加A

let str = parseDecToDigits(num, 26, codeArr)
str = 'A'.repeat(length+1-str.length) + str//追加A

最后,代码如下


/**
 * @param number num 0,1,2,3...
 * @return string A-Z,AA-ZZ,AAA-ZZZ....(excel的列名)
 */
const getColumnByNum = (() => {
    //形成闭包,将[A,B,C...Z]存起来
    let codeArr = []
    //将codeArr变为[A,B,C...Z]
    for (let i = 0; i < 26; i++) {
        codeArr.push(String.fromCharCode('A'.charCodeAt(0) + i))
    }
    const parseDecToDigits = (num, digits, codeArr) => {
        if (digits === 10) {
            return String(num)
        }
        const getCode = (num) => codeArr[num]
        let str = getCode(num % digits)
        while (num >= digits) {
            num = Math.floor(num / digits)
            str = getCode(num % digits) + str
        }
        return str
    }
    return (num) => {
        let offset = 26
        let i = 0
        while (num >= offset) {
            num = num - offset
            offset = offset * 26
            i++
        }
        let str = parseDecToDigits(num, 26, codeArr)
        str = 'A'.repeat(i + 1 - str.length) + str//追加A
        return str
    }
})();
//测试
((start, end) => {
    let arr = []
    for (let i = start; i < end; i++) {
        let str = getColumnByNum(i)
        let nextStr = getColumnByNum(i + 1)
        arr.push(str)
        if (/[^A-Z]/.test(str)) {
            throw new Error(`第 "${i}" 列 "${str}" 含有错误字符`)
        }
        if (str.length === nextStr.length) {
            //如果长度相等
            let differ = 1
            for (let j = str.length - 1; j > -1; j--) {
                //如果当前结尾为Z
                if (str.charAt(j) === 'Z' && nextStr.charAt(j) === 'A') {
                } else if (nextStr.charCodeAt(j) - str.charCodeAt(j) === differ) {
                    if (differ === 1) {
                        differ = 0
                    }
                } else {
                    throw new Error(`第 "${i}" 列 "${str}" 发生错误,与下一列 "${nextStr}" 未相邻`)
                }
            }
            if (differ !== 0) {
                throw new Error(`第 "${i}" 列 "${str}" 发生错误,与下一列 "${nextStr}" 未相邻`)
            }
        } else  //如果长度不相等,则str肯定是Z+,nextStr肯定是A+,且长度... 
            if (!(/^[Z]+$/.test(str)
                && /^[A]+$/.test(nextStr)
                && nextStr.length - str.length === 1)) {
                throw new Error(`第 "${i}" 列 "${str}" 发生错误,与下一列 "${nextStr}" 未相邻`)
            }

    }
    console.log('测试通过')
    console.log(arr)
})(0, 1000000);//此处改变测试范围

2.3 获取列名数组

const getColumnsByNum = (num)=>{
    let arr = ['A']
    const getNextColumn = (column)=>{
        let str = ''
        let differ = 1
        for(let i=column.length-1;i>-1;i--){
            if(column[i]==='Z' && differ !== 0){
                str ='A' + str 
            }else{
                str = String.fromCharCode(column[i].charCodeAt(0) + differ) + str
                differ = 0
            }
        }
        differ === 1 &&(str = 'A' + str )//说明column为Z+
        return str
    }
    for(let i=0;i<num;i++){
        arr.push(getNextColumn(arr[i]))
    }
    return arr
}