6

This JavaScript function takes an array of numbers (in the range 0-255) and converts to a base64-encoded string, then breaks long lines if necessary:

function encode(data)
{
  var str = "";
  for (var i = 0; i < data.length; i++)
    str += String.fromCharCode(data[i]);

  return btoa(str).split(/(.{75})/).join("\n").replace(/\n+/g, "\n").trim();
}

Can you do the same thing in less code? Can you do it so it runs faster? Portability no object, use brand new language features if you want, but 's gotta be in JavaScript.

Cœur
  • 37,241
  • 25
  • 195
  • 267
zwol
  • 135,547
  • 38
  • 252
  • 361
  • Runs faster in what browser(s)? `btoa` is only supported by Gecko and WebKit browsers as far as I know. – Gabe Mar 20 '11 at 04:41
  • As it happens, this is a very small piece of a Firefox extension, but if you have a clever way to do it using some other browser's JS I'd be happy to see that, too. – zwol Mar 20 '11 at 17:13
  • ahem... http://codegolf.stackexchange.com/ – jessegavin Mar 20 '11 at 17:50
  • 2
    @jessegavin: codegolf.SE is just for entertainment; this is an actual, legitimate programming problem. – Gabe Mar 20 '11 at 17:57

3 Answers3

14

I have another entry:

function encode(data)
{
    var str = String.fromCharCode.apply(null,data);
    return btoa(str).replace(/.{76}(?=.)/g,'$&\n');
}

Minified, 88 characters:

function e(d){return btoa(String.fromCharCode.apply(d,d)).replace(/.{76}(?=.)/g,'$&\n')}

Or if you want trailing newlines, 85 characters:

function e(d){return btoa(String.fromCharCode.apply(d,d)).replace(/.{1,76}/g,'$&\n')}
Anomie
  • 92,546
  • 13
  • 126
  • 145
  • 1
    Oh, very nice! Although I'm not 100% sure about the use of `apply`. There's a hard limit of something like 2**19 arguments to any function (in Firefox, anyway) and I think it might have to do more than a little extra work unpacking the array into the argument area. I don't expect my arrays to be *that* long, though, and the shortness is nice. – zwol Mar 20 '11 at 19:06
  • I tested it just now (in Firebug), it worked up to about 12*2**20 numbers here. And that wasn't even a hard limit, it just reported that the script stack space quota was exhausted. – Anomie Mar 20 '11 at 19:43
  • How long does it take? Can you time our various methods with large byte arrays? – Gabe Mar 20 '11 at 20:22
  • @Gabe: Hah, just finished doing that with an array of 2**23 entries. I timed each 5 times in Firebug, threw out the lowest and highest, and took the mean of the 3 remaining. My first takes about 6 seconds. Your first takes about 5.8, your second about 5.3, your third takes about 2.7, and this one about 0.9 seconds. – Anomie Mar 20 '11 at 20:30
  • Nice! I figured that `map` or `apply` would be the best, which is why I upvoted this answer but not your other one. – Gabe Mar 20 '11 at 21:25
  • Very cool, but you're using two variables in the non-minified - data and d, when they should be one. – Levitikon Mar 29 '12 at 16:22
3

Works in Firefox 3.6.13:

function encode(data)
{
    var str = data.reduce(function(a,b){ return a+String.fromCharCode(b) },'');
    return btoa(str).replace(/.{76}(?=.)/g,'$&\n');
}
Anomie
  • 92,546
  • 13
  • 126
  • 145
  • Do you need that `trim` in there? – Gabe Mar 20 '11 at 17:20
  • @Gabe: I put it in to avoid the returned string having a trailing linebreak when the base64-encoded string is an exact multiple of the line length while lacking the linebreak otherwise. But when writing that in reply to you, I thought "Does Firefox's javascript regex engine support zero-width positive look-ahead?". And it does! Edited. – Anomie Mar 20 '11 at 17:28
  • Oh, is the object here minimum characters? This can be minified to 116 characters, or 113 if you want every output to have a trailing newline (change the regex to `/.{1,76}/g`). – Anomie Mar 20 '11 at 18:06
  • Minimum *characters* are not really the goal - I'll be running the entire thing through an auto-minifier in any case - but minimum *operations* is desirable. I like the use of `reduce` although I imagine it's not any faster than the for loop, since the big cost here is gonna be growing the string. – zwol Mar 20 '11 at 19:12
1

I don't have Firefox handy, so I can't try it out, but from a general string-handling perspective it looks like you have some room to improve. What you're doing is, for every byte, creating a new string one character longer than your previous one. This is an O(N^2) operation. There are a few ways to cut down N so that your algorithm runs in near-linear time:

  1. Build up strings to length 57 (this will yield a 76-char Base64 result), then perform a btoa on it and add the resulting string to your output

  2. Just like #1, only build an array of lines and call join on it to create the final output string.

  3. Use map to create an array of 1-character strings, then call join on it.

Here's some untested code for each method:

function encode(data)
{
  var output = "";
  var str = "";
  for (var i = 0; i < data.length; i++)
  {
    str += String.fromCharCode(data[i]);
    // the "&& i != data.length - 1" clause
    // keeps the extra \n off the end of the output
    // when the last line is exactly 76 characters
    if (str.length == 57 && i != data.length - 1)
    {
      output += btoa(str) + "\n";
      str = "";
    }
  }
  return output + btoa(str);
}

function encode(data)
{
  var output = [];
  var str = "";
  for (var i = 0; i < data.length; i++)
  {
    str += String.fromCharCode(data[i]);
    if (str.length == 57)
    {
      output[output.length] = btoa(str);
      str = "";
    }
  }
  if (str != "")
    output[output.length] = btoa(str);
  return output.join("\n");
}

function encode(data)
{
  var str = data.map(function (d) { return String.fromCharCode(d) }).join("");
  return btoa(str).replace(/.{76}(?=.)/g,'$&\n');
}

And here's the last one, minified (116 chars):

function e(b){return btoa(b.map(function(d){return
String.fromCharCode(d)}).join("")).replace(/.{76}(?=.)/g,'$&\n')}
Gabe
  • 84,912
  • 12
  • 139
  • 238
  • A closure is required, because map invokes its function with 3 args (array element, index, and the array object) and String.fromCharCode can take multiple code points as arguments to return a multiple-character string. Coincidentally, I remembered this just before you posted and realized I could take advantage of that fact. – Anomie Mar 20 '11 at 18:16
  • I like the idea of chunking 57 bytes at a time to feed to `btoa`. – zwol Mar 20 '11 at 19:13