JS:将布尔列表转换为紧凑字符串?

Posted

技术标签:

【中文标题】JS:将布尔列表转换为紧凑字符串?【英文标题】:JS: Convert a list of bools to a compact string? 【发布时间】:2020-05-12 09:07:27 【问题描述】:

我在 javascript 中有一个巨大的布尔值列表,并希望在 url 中将它们编码为参数,但不会占用太多空间。所以我在想,是否可以将布尔数组转换为位数组,然后将这些位转换为字符串?

例如,如果我的布尔值列表是:

[false, true, true, false, false, false, false, true]

那么它会是位

[0, 1, 1, 0, 0, 0, 0, 1]

这是字母 a 的二进制文件(至少根据 this)。

这样的事情可能吗?如果是这样,如何转换回来?

【问题讨论】:

什么是“巨大”,你有一些数量级吗?因为在某些时候,你可以使用 1 和 0 组成的字符串,没有空格或逗号,但是通过以某种方式压缩这个字符串仍然可以获得巨大的收益(以更多的编码和处理能力为代价) 另外,您确定需要它们作为 url 参数吗?您不能使用 POST 请求的正文参数的任何原因? 嗯,目前只有 200 多个布尔值。我目前将它们编码为 1 和 0 的列表,但 200 很长。 @Pac0 它应该是一个 url 参数,因为它意味着人们可以轻松地将其复制并发送给其他人。但公平的问题。 如果你选择二进制选项,请记住,你会得到很多不能出现在 URL 中的字符,所以你要么需要转义它们(可能是 3 倍长度)或使用 base64 编码(正好长 33%)。 【参考方案1】:

你可以使用地图:

console.log( [false, true, true].map(item => item ? 1 : 0).join("") );

但地图在 Internet Explorer 中无法正常工作。相反,我会使用一个简单的 for 循环:

var bools = [false, true, true];
for(var i = 0; i < bools.length; i++) bools[i] = bools[i] ? 1 : 0;
console.log(bools.join(""));

但是如果你可以使字符串比 0 和 1 更短,那将是非常酷的。如果您可以将多个具有相同值的连续布尔值压缩为一个字符会怎样?所以[true, true, true, true] 只是"4" 而不是"1111"?这就是我在创建此代码时采用并运行的想法:

var trueMultiples = ['1', '2', '3', '4', '5', '6', '7', '8', '9', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', 'A', 'S', 'D'];
var falseMultiples = ['0', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'z', 'm', 'n', 'b', 'p', 'x', 'c', 'v', 'F', 'G', 'H', 'J', 'K', 'L', 'Z', 'X', 'C', 'V', 'B', 'N', 'M'];

function encryptBools(bools) 
  var str = "",
    run = [];

  for (var i = 0; i < bools.length; i++) 
    if (run.length == 0 || run[run.length - 1] === bools[i]) 
      //stack up successive trues or successive falses as a "run"
      run.push(bools[i]);
     else 
      //when the run ends, convert it to a trueMultiples or falseMultiples character
      var encryptionSet = bools[i] ? falseMultiples : trueMultiples;
      while (run.length > encryptionSet.length) 
        //if it's too long to be a single character, use multiple characters
        str += encryptionSet[encryptionSet.length - 1];
        run = run.slice(0, run.length - encryptionSet.length);
      
      str += encryptionSet[run.length - 1];
      run = [bools[i]];
    
  

  if (bools.length > 0) 
    //for the last run, convert it to a trueMultiples or falseMultiples character
    var encryptionSet = run[run.length - 1] ? trueMultiples : falseMultiples;
    while (run.length > encryptionSet.length) 
      //if it's too long to be a single character, use multiple characters
      str += encryptionSet[encryptionSet.length - 1];
      run = run.slice(0, run.length - encryptionSet.length);
    
    str += encryptionSet[run.length - 1];
  

  return str;


function decryptBools(str) 
  var bools = [];

  for (var i = 0; i < str.length; i++) 
    if (trueMultiples.indexOf(str[i]) > -1) 
      for (var j = 0; j <= trueMultiples.indexOf(str[i]); j++) 
        bools.push(true);
      
     else if (falseMultiples.indexOf(str[i]) > -1) 
      for (var j = 0; j <= falseMultiples.indexOf(str[i]); j++) 
        bools.push(false);
      
    
  

  return bools;


var bools = [true, false, false, false, false, false, true, true, true, true, false];
console.log("ORIGINAL:" + JSON.stringify(bools));

var encryptedBools = encryptBools(bools);
console.log("ENCRYPTED: " + encryptedBools);

var decryptedBools = decryptBools(encryptedBools);
console.log("DECRYPTED: " + JSON.stringify(decryptedBools));

trueMultiplesfalseMultiples 是表示你有多少个连续布尔值的字符。比如"3"表示连续3次为真,"s"表示连续3次为假。

最佳情况下,您可以将 200 个布尔值减少到一个 7 个字符的长字符串。最坏的情况,200 个字符长。预计长度为 100.497 个字符。

我坚持使用基本的字母数字字符,但如果您愿意,可以随意添加“-”、“_”和“~”。他们是safe for urls。

更新

实际上,让我印象深刻的是,我们将布尔值转换为 0 和 1 的第一步给我们留下了如下所示的内容:

[1, 1, 0, 1]

在我看来,这与二进制数惊人地相似。如果我们将该数组连接在一起并获得1101,然后将其切换为十进制表示法以将其显示为13,该怎么办?或者更好的是,我们可以使用更高的基数,比如 36,让它读起来就像 d!能够像这样切换数字的基数是产生较小结果的绝佳方式!

现在,我知道你在想什么。如果开头有false 并且数字最终变成001 怎么办?领先的0 会迷路!!好吧,别担心。我们可以将我们的算法设置为在我们切换碱基之前总是在开头添加一个1。这样一来,所有0 将保持重要。

这里有一些限制。有了 200 多个布尔值,这些人为的数字将是巨大的。事实上,对于 JavaScript 来说太大了。我们需要将其分解为可管理的块,然后将这些块连接在一起以获得我们的结果。

旁注:我们可以投入更多的工作来表明有多少前导零,而不是强制前导 1 来改善我们的最佳情况,但我认为这实际上可能会损害我们的平均情况,所以我没有。强制前导 1 会强制我们所有的完整块始终为 11 字符长,这一事实使我们无需额外的分隔符。为什么要搞砸呢?

无论如何,这就是我最终得到的结果:

function compress(bools) 
  var sections = [], MAX_SAFE_SECTION = 52;
  for (var i = 0; i < bools.length; i++) 
    if (i % MAX_SAFE_SECTION == 0) sections.push([]);
    sections[Math.floor(i / MAX_SAFE_SECTION)][i % MAX_SAFE_SECTION] = bools[i] ? 1 : 0;
  
  for (var i = 0; i < sections.length; i++) sections[i] = parseInt("1" + sections[i].join(""), 2).toString(36);
  return sections.join("");


function expand(str) 
  var sections = [];
  while (str.length > 0) str = str.replace(sections[sections.length] = str.substring(0, 11), "");
  for (var i = 0; i < sections.length; i++) sections[i] = parseInt(sections[i], 36).toString(2).substring(1);
  var bools = sections.join("").split("");
  for (var i = 0; i < bools.length; i++) bools[i] = bools[i] == "1";
  return bools;



var bools = [true, false, false, false, false, false, true, true, true, true, false];
console.log("ORIGINAL:" + JSON.stringify(bools));

var compressedBools = compress(bools);
console.log("COMPRESSED: " + compressedBools);

var expandedBools = expand(compressedBools);
console.log("EXPANDED: " + JSON.stringify(expandedBools));

它需要一个包含 200 个布尔值的数组,并一致地将其缩减为 42 个字符的字符串。

这很好,但您可能会问自己,为什么我们只选择 36 基数?我们能走得更高吗?答案是我只选择了 36,因为它已经是 JavaScript 的 parseInt 函数中内置的最高数字。如果我们愿意添加自定义基本转换代码,我们可以走得更高。 here 提供了一个很好的答案,它提供了一个很好的基本转换函数,所以我将复制他们的函数并将其粘贴到这里来证明我的观点:

function convertBase(value, from_base, to_base) 
  var range = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_'.split('');
  var from_range = range.slice(0, from_base);
  var to_range = range.slice(0, to_base);

  var dec_value = value.split('').reverse().reduce(function(carry, digit, index) 
    if (from_range.indexOf(digit) === -1) throw new Error('Invalid digit `' + digit + '` for base ' + from_base + '.');
    return carry += from_range.indexOf(digit) * (Math.pow(from_base, index));
  , 0);

  var new_value = '';
  while (dec_value > 0) 
    new_value = to_range[dec_value % to_base] + new_value;
    dec_value = (dec_value - (dec_value % to_base)) / to_base;
  
  return new_value || '0';


function compress(bools) 
  var sections = [], MAX_SAFE_SECTION = 52;
  for (var i = 0; i < bools.length; i++) 
    if (i % MAX_SAFE_SECTION == 0) sections.push([]);
    sections[Math.floor(i / MAX_SAFE_SECTION)][i % MAX_SAFE_SECTION] = bools[i] ? 1 : 0;
  
  for (var i = 0; i < sections.length; i++) sections[i] = convertBase("1" + sections[i].join(""), 2, 62);
  return sections.join("");


function expand(str) 
  var sections = [];
  while (str.length > 0) str = str.replace(sections[sections.length] = str.substring(0, 9), "");
  for (var i = 0; i < sections.length; i++) sections[i] = convertBase(sections[i], 62, 2).substring(1);
  var bools = sections.join("").split("");
  for (var i = 0; i < bools.length; i++) bools[i] = bools[i] == "1";
  return bools;



var bools = [true, false, false, false, false, false, true, true, true, true, false];
console.log("ORIGINAL:" + JSON.stringify(bools));

var compressedBools = compress(bools);
console.log("COMPRESSED: " + compressedBools);

var expandedBools = expand(compressedBools);
console.log("EXPANDED: " + JSON.stringify(expandedBools));

使用这个自定义函数,我们可以安全地达到 base 62。这意味着我们可以获取一个包含 200 个布尔值的数组,并将其一致地缩减为 35 个字符的字符串。如果您的数组中没有大量的顺序重复,您可能希望使用此选项。这是我会选择的算法。

【讨论】:

很好的完整答案。作为旁注,这不是“加密”,而是“压缩” 是的,很公平。【参考方案2】:

var bools = [false, true, true, false, false, false, false, true]

var str = bools.map(Number).join('')              // array to string

var arr = str.split('').map(Number).map(Boolean)  // string to array

console.log( str )
console.log( arr )

【讨论】:

【参考方案3】:

您可以将布尔值转换为字节,然后将其编码为 base64:

function encode(booleans) 
    var bits = booleans.map(Number).join('');
    var bytes = Array.from(
        bits.matchAll(/[01]8/g)
    ).map(byte => parseInt(byte, 2));
    var characters = bytes.map(byte => String.fromCharCode(byte)).join('');

    return btoa(characters);

要解码,请将 base64 字符串转换回字节,然后一次取一位:

function decode(string) 
    var bytes = atob(string).split('').map(char => char.charCodeAt(0));
    var bits = [];

    for (var i = 0; i < bytes.length; i++) 
        var byte = bytes[i];
        var temp = [];
        for (var bit = 0; bit < 8; bit++) 
            temp.unshift(byte & 1)
            byte >>= 1;
        
        bits = bits.concat(temp)
    

    return bits.map(Boolean)

这仅在布尔列表的长度是 8 的倍数时才有效

【讨论】:

【参考方案4】:

由于您有数百个值,因此可以只创建一个包含 1 和 0 的字符串,这将适合 URL 而无需进一步压缩。

您可以简单地将布尔值映射到数字,方法是在数字前面使用+,然后将它们转换为字符串。

例如,在控制台中试试这个: :

    let a = [true, true, false, true];
    console.log(a.map(x => (+x).toString())); // Array(4) [ "1", "1", "0", "1" ]
    console.log(a.map(x => (+x).toString()).join("")); // "1101"

上面将是您的布尔数组的“序列化”。 (在这种情况下,这是恰当的术语)。

“反序列化”将是相反的步骤(将字符串拆分为字符,将单个字符转换为数字,然后转换为布尔值):

let s = "1101";
console.log(s.split("")); // Array(4) [ "1", "1", "0", "1" ]
console.log((s.split("")).map(x => +x)); // Array(4) [ 1, 1, 0, 1 ]
console.log((s.split("")).map(x => !!(+x))); // Array(4) [ true, true, false, true ]

(中间的步骤我留下了让你看推理,但只有每个sn-p的最后一行有用)

【讨论】:

【参考方案5】:

这些函数会将 200 个布尔值压缩为 40 个字符的 URL 安全字符串,并将它们展开回原始布尔值数组。它们应该适用于任何长度的布尔数组,每六个布尔值增长大约一个字符:

const compressBools = (bools) =>
  String (bools .length) + '~' + 
  btoa ( bools
    .map (b => b ? '1' : '0')
    .reduce (
      ([c, ...r], b, i) => (bools .length - i) % 8 == 0 ? [[b], c, ...r] : [[...c, b], ...r],   
      [[]]
    )
    .reverse ()
    .map (a => a .join (''))
    .map (s => parseInt(s, 2))
    .map (n => String.fromCharCode(n))
    .join ('')
  )
  .replace (/\+/g, '-')
  .replace (/\//g, '_')
  .replace (/\=/g, '.')

const expandBools = (s, [len, str] = s .split ('~')) => 
  atob (str
    .replace (/\./g, '=')
    .replace (/_/g, '/')
    .replace (/\-/g, '+')
  )
  .split ('')
  .map (c => c .charCodeAt (0))
  .map (s => Number (s) .toString (2) .padStart (8, '0'))
  .flatMap (a => a .split (''))
  .slice (-len)
  .map (c => c == '1')


const arr = Array.from(length: 200, _ => Math.random() < .5)

const compressed = compressBools (arr)
console .log (`Compressed String: "$compressed"`)

const expanded = expandBools(compressed)
console .log (`Output matches: $expanded.every((b, i) => b == arr[i])`)

每个中的三个正则表达式替换用于处理底层 base64 转换的 +/ 字符,以及它的 = 填充字符,将它们替换为 URL 安全的替代方案。您可以改为调用encode/decodeURIComponent,但这种方式会导致字符串更短。

压缩中丑陋的reduce 是将一长串 0 和 1 分成 8 个组,第一个可能更短。这为我们提供了字节,然后我们可以将其转换为字符。

请注意,输出字符串以要生成的布尔值计数开始。这是因为我们无法将数字中的一些前导零(将转换为初始 falses)与更短且没有此类前导零的数组区分开来。该数字与其余字符串用波浪号 (~) 分隔;如果您愿意,您可以轻松地将其替换为另一个字符,但很难找到 URL 安全的特殊字符。


如果我们喜欢这些,我们还可以玩一个小游戏,找到导致有趣字符串的布尔数组。例如:

const arr = [true, false, false, true, false, true, false, true, true, false, true, false, true, true, false, true, false, false, true, true, true, false, false, true, false, false, true, false, false, true, true, true, true, true, true, false, false, true, true, true, false, true, false, true, true, true, true, false, true, true, true, true, false, true, false, true, false, true, true, false, true, true, true, true, true, true, false, false, true, false, true, true, false, true, false, false, false, true, true, false, false, false, false, true, true, true, true, true, true, false, true, false, false, false, false, true, false, true, true, true, false, false, true, true, true, true, false, true, false, true, true, false, false, true, false, true, true, false, true, true, false, false, false, true, false, true, false, true, false, false, false, true, false, false, true, true, true, true, true, true, true, true, true, true, true, true, false, false, true, true, true, true, true, false, true, true, true, true, true, false, true, true, true, false, true, true, false, true, true, false, true, true, true, true, true, true, false, false, true, true, true, false, true, true, true, true, true, true, false, true, true]

console .log (compressBools (arr)) //~> "191~Stack_Overflow_Question_59923537"

【讨论】:

【参考方案6】:

我最近制作了一个简单的库来完成这项工作。 它将布尔数组视为二进制数据,并将其编码为 Base64。

它使用这两个常量。您可以通过更改characters 字符串(并可能调整bitsInChar)来使用不同的基础(Base64 用于 url / Base128 / ...)。

// Base64 character set
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

// How many bits does one character represent
const bitsInChar = 6; // = Math.floor( Math.log2(characters.length) );

编码功能

function compress(array) 

    // The output string
    let string = "";

    // Loop through the bool array (six bools at a time)
    for (let charIndex = 0; charIndex < array.length / bitsInChar; section++) 
        let number = 0;

        // Convert these six bools to a number (think of them as bits of the number) 
        for (let bit = 0; bit < bitsInChar; bit++)
            number = number * 2 + (array[charIndex*bitsInChar + bit] ? 1 : 0);

        // Convert the number to a Base64 character and add it to output
        string += characters.charAt(number);
    

    return string;

解码功能

function decompress(string) 

    // The output array
    const array = [];

    // Loop through the input string one character at a time
    for (let charIndex = 0; charIndex < string.length; charIndex++) 
        
        // Convert the Base64 char to a number 
        let number = characters.indexOf(string.charAt(charIndex));

        // Convert the number to six bools (think of them as bits of the number) 
        // And assign them to the right places in the array
        for (let bit = bitsInChar - 1; bit >= 0; bit--) 
            array[charIndex*bitsInChar + bit] = !!(number % 2)
            number = Math.floor(number / 2);
        
    

    return array;

有一个问题:经过一个压缩-解压缩循环后,新数组的长度将四舍五入到最接近的 6 的倍数(或一些不同的数字,取决于您选择的基数),并附加 @987654326 @s 被添加到数组的末尾。在我的情况下这无关紧要,但如果您需要完全相同的数组,您还必须存储原始长度。

【讨论】:

以上是关于JS:将布尔列表转换为紧凑字符串?的主要内容,如果未能解决你的问题,请参考以下文章

将 List<boolean> 转换为字符串

Python:二进制字符串的布尔列表

我们如何将字符串转换为布尔值? [复制]

JS-类型转换

js类型转换 之 转字符串及布尔类型

JS 类型转换