在JavaScript中将数字转换为64进制的最快方法是什么?

83

在JavaScript中,您可以使用以下方式将数字转换为特定进制的字符串表示形式:

(12345).toString(36) // "9ix"

你可以通过下面的代码将其转换回普通数字:

parseInt("9ix", 36) // 12345

最大的基数是36,可以使用字符0-9a-z作为数字(共36个)。

我的问题是:将一个数字转换为64进制表示法的最快方法是什么(例如,使用A-Z-_作为额外的28个数字)?


更新:有四个人发布了回复,称此问题重复,或者说我要寻找Base64。 我不是。

Base64”是一种编码二进制数据的简单ASCII字符集,以使其在网络等上安全传输(这样纯文本系统就不会使二进制数据失真)。

这不是我所问的。我正在询问将数字转换为64进制字符串表示形式的方法。 (JavaScript的toString(radix)自动为任何基数进行此操作,但我需要一个自定义函数来获得基数为64。)


更新2:这里是一些输入和输出示例...

0   → "0"
1   → "1"
9   → "9"
10  → "a"
35  → "z"
61  → "Z"
62  → "-"
63  → "_"
64  → "10"
65  → "11"
128 → "20"
etc.

可能是重复问题:如何使用JavaScript进行Base64编码? - Andy E
9
@Andy E:不是这样的,请看我的更新澄清。 - callum
@callum:由于数字是二进制数据,所以您正在询问的问题是如何将数字转换为base64(除了您使用不同的顺序和 -_ 而不是 +/)。 然而,链接的问题只能将字符串转换为base64,这对您没有帮助。 - Mooing Duck
我创建了另一个类似于这个问题,但具有未知/可变字符集的问题。其中包括@Reb.Cabin代码的微调,请参见http://stackoverflow.com/a/35700950/3232832。 - ryanm
使用现代的BigInt:现在,任意精度整数是JavaScript中的本机数据类型。不幸的是,base36限制仍然存在,但转换算法可以从中受益。 - Peter Krauss
显示剩余2条评论
15个回答

59
这是一个解决正数(不是字节数组)的草图,忽略小数部分,尚未经过充分测试 - 只是一个草图!
Base64 = {

    _Rixits :
//   0       8       16      24      32      40      48      56     63
//   v       v       v       v       v       v       v       v      v
    "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/",
    // You have the freedom, here, to choose the glyphs you want for 
    // representing your base-64 numbers. The ASCII encoding guys usually
    // choose a set of glyphs beginning with ABCD..., but, looking at
    // your update #2, I deduce that you want glyphs beginning with 
    // 0123..., which is a fine choice and aligns the first ten numbers
    // in base 64 with the first ten numbers in decimal.

    // This cannot handle negative numbers and only works on the 
    //     integer part, discarding the fractional part.
    // Doing better means deciding on whether you're just representing
    // the subset of javascript numbers of twos-complement 32-bit integers 
    // or going with base-64 representations for the bit pattern of the
    // underlying IEEE floating-point number, or representing the mantissae
    // and exponents separately, or some other possibility. For now, bail
    fromNumber : function(number) {
        if (isNaN(Number(number)) || number === null ||
            number === Number.POSITIVE_INFINITY)
            throw "The input is not valid";
        if (number < 0)
            throw "Can't represent negative numbers now";

        var rixit; // like 'digit', only in some non-decimal radix 
        var residual = Math.floor(number);
        var result = '';
        while (true) {
            rixit = residual % 64
            // console.log("rixit : " + rixit);
            // console.log("result before : " + result);
            result = this._Rixits.charAt(rixit) + result;
            // console.log("result after : " + result);
            // console.log("residual before : " + residual);
            residual = Math.floor(residual / 64);
            // console.log("residual after : " + residual);

            if (residual == 0)
                break;
            }
        return result;
    },

    toNumber : function(rixits) {
        var result = 0;
        // console.log("rixits : " + rixits);
        // console.log("rixits.split('') : " + rixits.split(''));
        rixits = rixits.split('');
        for (var e = 0; e < rixits.length; e++) {
            // console.log("_Rixits.indexOf(" + rixits[e] + ") : " + 
                // this._Rixits.indexOf(rixits[e]));
            // console.log("result before : " + result);
            result = (result * 64) + this._Rixits.indexOf(rixits[e]);
            // console.log("result after : " + result);
        }
        return result;
    }
}

更新:这里有一些(非常轻量级的)测试,供在NodeJs中运行并使用console.log的人使用。

function testBase64(x) {
    console.log("My number is " + x);
    var g = Base64.fromNumber(x);
    console.log("My base-64 representation is " + g);
    var h = Base64.toNumber(g);
    console.log("Returning from base-64, I get " + h);
    if (h !== Math.floor(x))
        throw "TEST FAILED";
}

testBase64(0);
try {
    testBase64(-1);
    }
catch (err) {
    console.log("caught >>>>>>  " + err);
    }
try {
    testBase64(undefined);
    }
catch (err) {
    console.log("caught >>>>>>  " + err);
    }
try {
    testBase64(null);
    }
catch (err) {
    console.log("caught >>>>>>  " + err);
    }
try {
    testBase64(Number.NaN);
    }
catch (err) {
    console.log("caught >>>>>>  " + err);
    }
try {
    testBase64(Number.POSITIVE_INFINITY);
    }
catch (err) {
    console.log("caught >>>>>>  " + err);
    }
try {
    testBase64(Number.NEGATIVE_INFINITY);
    }
catch (err) {
    console.log("caught >>>>>>  " + err);
    }

for(i=0; i<100; i++)
    testBase64(Math.random()*1e14);

1
作为额外的奖励,您的代码速度非常快。我之前一直在使用node.js的缓冲区编码/解码将整数转换为base-64数字并进行反向操作,而测试表明您的Base64.fromNumber()速度是它的两倍,而Base64.toNumber()速度则是它的十倍! - Paul d'Aoust
1
划掉之前的 -- Base64.fromNumber() 的速度是五倍,而 Base64.toNumber() 的速度是六倍。真快!很惊讶缓冲区不如此高效,因为在 node.js 中它们是用 C 写的。 - Paul d'Aoust
@distilledchaos 哦,我以为这是Node的最大优势——它可以将昂贵的东西快速传递给更接近底层的库。谢谢你的警告。 - Paul d'Aoust
1
@Pauld'Aoust 我应该澄清一下;在桥接Node的过程中可能会有很多开销,这主要是由于V8的内部机制。Node的强大之处在于其核心模块和极其高效的反应器模式,更不用说动态编译了。我每天都使用Node并且喜欢它,但我只是想让您对C++绑定有一个现实的期望。(看看node的markdown模块,最快的是用纯javascript编写的) - skeggse
1
值得注意的是,while(true) { ... if(residual == 0) break; } 可以被替换为 do { ... } while(residual > 0)do...while 的目的是在运行循环一次后再评估条件。 - Yoshiyahu
显示剩余4条评论

18

这里是32位整数版本,即介于-2147483648和2147483647(包括两端)之间的任意数字。

我修改了顶部答案中Reb Cabin的版本。这个版本应该会更快,因为它使用了位运算和查找表。

Base64 = (function () {
    var digitsStr = 
    //   0       8       16      24      32      40      48      56     63
    //   v       v       v       v       v       v       v       v      v
        "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+-";
    var digits = digitsStr.split('');
    var digitsMap = {};
    for (var i = 0; i < digits.length; i++) {
        digitsMap[digits[i]] = i;
    }
    return {
        fromInt: function(int32) {
            var result = '';
            while (true) {
                result = digits[int32 & 0x3f] + result;
                int32 >>>= 6;
                if (int32 === 0)
                    break;
            }
            return result;
        },
        toInt: function(digitsStr) {
            var result = 0;
            var digits = digitsStr.split('');
            for (var i = 0; i < digits.length; i++) {
                result = (result << 6) + digitsMap[digits[i]];
            }
            return result;
        }
    };
})();
例如,
Base64.fromInt(-2147483648); // gives "200000"
Base64.toInt("200000"); // gives -2147483648

1
仅仅是为了好玩,它比最佳答案快10倍以上:http://jsperf.com/base64-and-back - jahooma
1
错误,第二次运行时速度快了近6倍。仍然不错。 - jahooma
如果你在处理Radix64数字时非常快,而你的输入生成的输出不能适应JavaScript基本整数,那么你就不会遇到溢出问题......然后你的方法将返回一个无效的结果。 - Rafael Lima

12

我认为这个问题缺少一个简短的解决方案。

const digit="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_";
toB64=x=>x.toString(2).split(/(?=(?:.{6})+(?!.))/g).map(v=>digit[parseInt(v,2)]).join("")
fromB64=x=>x.split("").reduce((s,v)=>s*64+digit.indexOf(v),0)

适用于介于 0Number.MAX_SAFE_INTEGER 之间的所有整数。


3
如果您使用"-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"替代现有的字符,那么toB64函数的输出将按字典顺序排列。 - art
此外,在URL中使用_- +字母数字组合时无需转义:encodeURIComponent("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_") = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz" - BruceJo

6

对于所有的javascript安全整数范围(从-90071992547409919007199254740991),都有非常快速的实现:

const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';

// binary to string lookup table
const b2s = alphabet.split('');

// string to binary lookup table
// 123 == 'z'.charCodeAt(0) + 1
const s2b = new Array(123);
for (let i = 0; i < alphabet.length; i++) {
  s2b[alphabet.charCodeAt(i)] = i;
}

// number to base64
const ntob = (number) => {
  if (number < 0) return `-${ntob(-number)}`;

  let lo = number >>> 0;
  let hi = (number / 4294967296) >>> 0;

  let right = '';
  while (hi > 0) {
    right = b2s[0x3f & lo] + right;
    lo >>>= 6;
    lo |= (0x3f & hi) << 26;
    hi >>>= 6;
  }

  let left = '';
  do {
    left = b2s[0x3f & lo] + left;
    lo >>>= 6;
  } while (lo > 0);

  return left + right;
};

// base64 to number
const bton = (base64) => {
  let number = 0;
  const sign = base64.charAt(0) === '-' ? 1 : 0;

  for (let i = sign; i < base64.length; i++) {
    number = number * 64 + s2b[base64.charCodeAt(i)];
  }

  return sign ? -number : number;
};

npm: number-to-base64

性能比较: https://jsperf.com/number-to-base64-encoding


2
我认为这是最好的实现! - jahooma

3

以下是不同的观点:

function base64(value) {
  if (typeof(value) === 'number') {
    return base64.getChars(value, '');
  }

  if (typeof(value) === 'string') {
    if (value === '') { return NaN; }
    return value.split('').reverse().reduce(function(prev, cur, i) {
      return prev + base64.chars.indexOf(cur) * Math.pow(64, i);
    }, 0);
  }
}

base64.chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_";

base64.getChars = function(num, res) {
  var mod = num % 64,
      remaining = Math.floor(num / 64),
      chars = base64.chars.charAt(mod) + res;

  if (remaining <= 0) { return chars; }
  return base64.getChars(remaining, chars);
};

这个支持完整范围的安全整数,这是一个很大的优点。可以使用Math.abs(value)来防止负数影响输出。 - bryc

3
我正在寻找一个与之相同的问题的解决方案,但是针对ActionScript (AS3),很明显许多人混淆Base64编码'基于64进制的数字'(64进制)

绝大多数网站提供的解决方案都是针对'计算机加密'而不是数学。这些解决方案对我们需要的转换没有用处。

在进行此咨询之前,我知道toString(radix)和parseInt(radix)方法,我曾在两种颜色中使用过16进制数字(radix 16),以及其他功能。

然而,在AS3JS中都不存在将数字从64进制进行转换的方法。

在来到这个网站之前,我找到了:

  1. 在各种在线计算器中,基数64不是从头开始,而是从A开始。
    例如:convertix.comalfredo4570.net
  2. 基数64由以下有序字符集形成:AZ、az、0-9、+和/(我已定义一个常量:STR64)

为避免与加密方法混淆,所使用的方法基于众所周知的名称:

  • toString / to64String
  • parseInt / to64Parse

该代码是用AS3编写的,但非常清晰(与JS相似)。

注意:建议在小于1 * 1016的数字下使用

最后,包括一个操作示例和结果。

const STR64:Array = ('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/').split( '' );

// TRANSFORM NUMBERS BETWEEN radix 10 AND radix 64
/** Methods based on answers shared in:
* @url   https://dev59.com/82025IYBdhLWcg3wHR4Y
*/

// METHODS 
/** to64String: Method to transform a radix 10 number to radix 64 number (as string)
* @param input   Number for transform to radix 64 (as String)
* @param current String data (don't needed in request)
* @return String Number in radix 64 as String;
*
* @based http://stackoverflow.com/users/383780/monocle
* @based base64( Method for number to string - NOT string part )
*/
function to64String( input:Number, current:String = '' ):String
{
    if ( input < 0 && current.length == 0 ){
        input = input * - 1;
    }
    var modify:Number = input % 64;
    var remain:Number = Math.floor( input / 64 );
    var result:String = STR64[ modify ] + current;
    return ( remain <= 0 ) ? result : to64String( remain, result );
}

/** to64Parse: Method for transform a number in radix 64 (as string) in radix 10 number
* @param input   Number in radix 64 (as String) to transform in radix 10
* @return Number in radix 10
*
* @based http://stackoverflow.com/users/520997/reb-cabin
* @based Base64.toNumber( Method for string to number )
*/
function to64Parse ( input:String ):Number
{
    var result:Number = 0;
    var toProc:Array  = input.split( '' );
    var e:String;
    for ( e in toProc ){
        result = ( result * 64 ) + STR64.indexOf( toProc[ e ] );
    }
    return result;
}

// TEST
var i:int = 0;
var max:Number = 1000000000000;
var min:Number = 0;
for ( i == 0; i < 20; i++ ){
    var num:Number = ( Math.ceil( Math.random() * ( max - min + 1 ) ) + min );
    var s64:String = to64String( num );
    var ret:Number = to64Parse ( s64 );
    trace( i + '\t# ' + num + '\t' + s64 + '\t' + ret + '\t' + ( ret == num ) )
}

// TEST RESULT
/*
0   # 808936734685  LxYYv/d 808936734685    true
1   # 931332556532  NjXvwb0 931332556532    true
2   # 336368837395  E5RJSMT 336368837395    true
3   # 862123347820  Mi6jk9s 862123347820    true
4   # 174279278611  CiT2sAT 174279278611    true
5   # 279361353722  EELO/f6 279361353722    true
6   # 435602995568  GVr9jlw 435602995568    true
7   # 547163526063  H9lfNOv 547163526063    true
8   # 188017380425  CvGtYxJ 188017380425    true
9   # 720098771622  KepO0Km 720098771622    true
10  # 408089106903  F8EAZnX 408089106903    true
11  # 293941423763  ERwRi6T 293941423763    true
12  # 383302396164  Fk+mmkE 383302396164    true
13  # 695998940618  KIMxQXK 695998940618    true
14  # 584515331314  IgX1CTy 584515331314    true
15  # 528965753970  Hso0Nxy 528965753970    true
16  # 5324317143    E9WqHX  5324317143      true
17  # 772389841267  LPWBalz 772389841267    true
18  # 954212692102  N4rgjCG 954212692102    true
19  # 867031893694  MnfIMa+ 867031893694    true
*/

2
我编写了一个npm模块来处理这种操作,power-radix,它可以帮助你。你可以在用户定义的字符编码下将任何进制的数字转换为任何进制。
例如:
var base = ['Q', 'W', 'E', 'R', 'T', 'Y', 'I', 'O', 'U'];
new PowerRadix([1, 0], 10).toArray(base); // ['W', 'Q'] 
new PowerRadix('10', 10).toArray(base);   // ['W', 'Q'] 
new PowerRadix(10, 10).toArray(base);     // ['W', 'Q'] 

new PowerRadix([1, 0], 10).toString(base); // "WQ" 
new PowerRadix('10', 10).toString(base);   // "WQ" 
new PowerRadix(10, 10).toString(base);     // "WQ"

该模块还支持自定义源基数编码。
new PowerRadix('ba', ['a', 'b']); // base 2 source radix, uses 'a' = 0 & 'b' = 1 character set.
new PowerRadix('ba', ['a', 'b']).toString(10); // returns "2"

3
如果您的模块没有依赖项('bigi'),并且像此页面上的其他示例一样“开箱即用”,那么对我来说这将更有帮助。 - Mac

2
以下实现将正数、负数和非整数转换为任意进制。类似地,可以轻松地实现回到十进制的转换:

function toAnyBase(num, base) {
  if (!Number.isInteger(base) || base < 2) throw new RangeError("toAnyBase() base argument must be an integer >= 2");
  if (!Number.isFinite(num)) return num.toString();
  if (num < 0) return "-" + toAnyBase(-num, base);
  
  const digits = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ#_",
        inv_base = 1 / base;
  
  var result = "",
      residual;
  
  // Integer part:
  residual = Math.trunc(num);
  do {
    result = digits.charAt(residual % base) + result;
    residual = Math.trunc(residual * inv_base); 
  } while (residual != 0);
  
  // Fractional part:
  residual = num % 1;
  if (residual != 0) {
    result += ".";
    var max = 1000;
    do {
      residual *= base;
      result += digits.charAt(Math.trunc(residual));
      residual %= 1;
    } while (residual != 0 && --max != 0);
  }

  return result;
}

console.log(toAnyBase(  64, 64)); // "10"
console.log(toAnyBase(-1.5, 64)); // "-1.w"


2

我也在寻找同样的解决方案,我认为我已经用基本的Javascript尽可能简洁地概括了这个人想要的内容。它适用于任何正整数,而endex可以是你想要的任意长度和任意基数,只要所有字符都是唯一的。

var endex = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_";
function encode(intcode){
    if(intcode < endex.length){
        return endex[intcode];
    }else{
        return encode(Math.floor(intcode/endex.length)) + endex[intcode%endex.length];
    }
}
function decode(charcode){
    if(charcode.length < 2){
        return endex.indexOf(charcode);
    }else{
        return (decode(charcode.slice(0, -1)) * endex.length) + endex.indexOf(charcode.slice(-1));
    }
}

2
如果您正在使用NodeJS,可以使用以下代码:
var base64 = Buffer.from([i>>24,i>>16,i>>8,i]).toString('base64').substr(0,6);

你可以通过以下方式检查此解决方案:Buffer.from(Buffer.from([i>>24,i>>16,i>>8,i]).toString('base64'), 'base64').readInt32BE(0) - Claudio Bertozzi

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