18
\$\begingroup\$

The goal is to create a fully compliant converter between the official Unicode encodings as given in the UTF FAQ. Given that this is centred on Unicode, I will accept the answer with the lowest byte count using the best possible of the involved encodings (which will probably be UTF-8, unless maybe you program it in APL). I apologize for the long post, but a lot of it is explaining the encodings which can also be accessed in the official specification (pdf, section 3.9 D90 - D92), or Wikipedia.

Specifications

If at any time your language of choice cannot exactly meet a requirement, substitute it with something that sticks the spirit of the rules given. Eg. not every language has built-in arrays, functions etc.

  • No using string libraries/functions, or encoding libraries/functions. The point of this code golf is to implement the converter using bit/byte manipulation. Using strings themselves in their capacity as a character or byte array is allowed though. Oh, and no OS calls which perform the conversion either.

  • The converter is a function which will take three parameters: a byte array representing the encoded input string, and the "input" and "output" encodings represented as numbers. Arbitrarily we will assign UTF-8, UTF-16, UTF-16BE, UTF-16LE, UTF-32, UTF-32BE, and UTF32LE numbers from 0 to 6 in that order. There is no need to check if the number is < 0 or > 6, we will assume these parameters are correct. The converter will return a valid byte array in the desired output encoding.

  • We will use the null character (U+0000) as a string terminator. Anything after this doesn't matter. We will assume that the input array has the null character somewhere so you do not need to do a bounds check.

  • As per the FAQ, if the input byte array is invalid for its declared encoding, we must signal an error. We will do this in one of the following ways: crash the program, throw an exception, return null or return an array whose first four bytes are all 0 (so that it can be recognized as U+0000 in every encoding).

The Encodings

The official specifications must be followed, but Wikipedia provides a good (and as far as I believe correct) explanation of the encodings, and I will summarize them here for completeness. Note that UTF-16 and UTF-32 have variants for endianness.

UTF-32, UTF-32LE, UTF-32BE

The simplest encoding, each code point is simply encoded in 4 bytes equal to its numeric value. LE/BE represents endianness (little endian/big endian).

UTF-16, UTF-16LE, UTF-16BE

Code points from U+0000 - U+FFFF are encoded in 2 bytes equal to its numeric value. Larger values are encoded using a pair of surrogates which are reserved values from U+D800 - U+DFFF. So to encode points greater than U+FFFF, the following algorithm can be used (shamelessly copied from Wikipedia):

  • 0x010000 is subtracted from the code point, leaving a 20 bit number in the range 0..0x0FFFFF.
  • The top ten bits (a number in the range 0..0x03FF) are added to 0xD800 to give the first code unit or lead surrogate, which will be in the range 0xD800..0xDBFF [...].
  • The low ten bits (also in the range 0..0x03FF) are added to 0xDC00 to give the second code unit or trail surrogate, which will be in the range 0xDC00..0xDFFF [...].

UTF-8

Code points from U+0000 - U+007F are encoded as 1 byte equal to its numeric value. From U+0080 - U+07FF they are encoded as 110xxxxx 10xxxxxx, U+0800 - U+FFFF is 1110xxxx 10xxxxxx 10xxxxxx, higher values are 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx. The x's are the bits from the numeric value of the code point.

BOM

The byte-order mark (BOM, U+FEFF) is used as the first code point to indicate endianness. Following the FAQ guidelines on BOMs, the BOM will be used as follows: For UTF-8, UTF-16 and UTF-32 it is optional. If the BOM is absent in UTF-16 or UTF-32, it is assumed to be big endian. The BOM must not appear in UTF-16LE, UTF-16BE, UTF-32LE and UTF-32BE.

Common Pitfalls Causing Invalid UTF

Various things may cause a byte sequence to be invalid UTF.

  • UTF-8 and UTF-32: Directly encoding surrogate code points ( U+D800 - U+DFFF ), or code points greater than U+10FFFF.
  • UTF-8: Many invalid byte sequences.
  • UTF-16: Unpaired, or improperly paired surrogates.
  • BOM: Must be used as specified in the encoding section. Note that when outputting UTF-16 or UTF-32 (no inherent endianness specified) you can pick, but with little endian, you must include the BOM.

Note that non-characters and unassigned code points (both distinct from surrogates) are to be treated like regular characters.

\$\endgroup\$
3
  • \$\begingroup\$ "No using string libraries/functions, or encoding libraries/functions." What about real built-ins. In APL, ''⎕R''⍠'InEnc' 'UTF16BE' 'OutEnc' 'UTF8-BOM'. \$\endgroup\$
    – Adám
    Jan 14, 2016 at 16:17
  • 2
    \$\begingroup\$ @NBZ Those would not be allowed since the point of this challenge is to implement the behaviour that those provide. \$\endgroup\$
    – DPenner1
    Jan 14, 2016 at 17:09
  • \$\begingroup\$ Note to answerers: I had more or less abandoned this question, but given recent renewed interest, in the coming days, I will take some time to go through the answers. \$\endgroup\$
    – DPenner1
    Jan 14, 2016 at 17:10

4 Answers 4

5
\$\begingroup\$

Python - 1367 UTF-8 chars

Alright! This was an extremely difficult question because of the sheer amount of work it took to understand and implement all the specifications, but I think that I have a correct implementation.

O,P,Q,R=65536,128,b'\xff\xfe\x00\x00',63
def A(x,y):assert x;return y
def B(x):
    o,c=[],0
    for b in x:
        if c:c,v=c-1,A(127<b<192,v<<6)|(b-P)
        else:
            c,v=(b>127)+(b>223)+(b>239),b
            if b>127:v=A(191<b<248,b&(R>>c))
        o+=[v][c:]
    return o[o[0]in(65279,O-2):]
def C(k):
    def o(x,s=None):
        for a,b in zip(x[k::2],x[1-k::2]):
            d=a|(b<<8)
            if s!=None:yield(A(56319<d<57344,d-56320)|(s<<10))+O;s=None
            elif 55295<d<57344:s=A(s<1024,d-55296)
            else:yield d
    return o
def D(x):n=(2,3,1)[[Q[:2],Q[1::-1],x[:2]].index(x[:2])];return C(n&1)(x[n&2:])
E=lambda a,b,c,d:lambda x:[L|(l<<8)|(m<<16) for L,l,m in zip(x[a::4],x[b::4],x[c::4])]
def F(x):n,m=((1,4),(-1,4),(-1,0))[[Q,Q[::-1],x[:4]].index(x[:4])];return E(*range(4)[::n])(x[m:])
S=lambda x,s=0,a=255:(x>>s)&a
G=lambda e:(e,)if e<P else(192|S(e,6),P|(e&R))if e<2048 else(224|S(e,12),P|S(e,6,R),P|(e&R))if e<O else(240|S(e,18),P|S(e,12,R),P|S(e,6,R),P|(e&R))
H=lambda e:(S(e,8),S(e))if e<O else(216|S(e-O,18),S(e-O,10),220+S((e-O)&1023,8),S(e-O))
I=lambda e:(S(e),S(e,8))if e<O else(S(e-O,10),216|S(e-O,18),S(e-O),220+S((e-O)&1023,8))
J=lambda e:(S(e,24),S(e,16),S(e,8),S(e))
K=lambda e:(S(e),S(e,8),S(e,16),S(e,24))
convert=lambda d,i,o:bytes(sum(map(L[o],N(list(M[i](d)))),()))if d else d
L,M=[G,H,H,I,J,J,K],[B,D,C(1),C(0),F,E(3,2,1,0),E(0,1,2,3)]
N=lambda d:[A(-1<x<1114112 and x&~2047!=55296,x)for x in d]

convert is the function that takes the data 'bytes' object, the input ID, and the output ID. It seems to work - although python seems to have a slightly broken usage of BOMs when unspecified in the encoding, so using python's builtin encoding to test modes 1 and 4 won't work.

Fun fact: The size is also 55516 or 101010101012.

773 chars for decoding, 452 for encoding, 59 for verification and 83 for miscellaneous parts.

\$\endgroup\$
8
  • \$\begingroup\$ @TrangOul: Generally, trivial edits (like language tagging) are frowned upon. \$\endgroup\$
    – Zach Gates
    Jan 12, 2016 at 15:13
  • 4
  • \$\begingroup\$ That question/answer doesn't show a community consensus. Across the network, trivial edits like these are frowned upon. Neither <1000 nor >1000 rep users should make these edits unless they clearly improve the content or format. It's best to hold off on editing things like language tags, single word corrections/changes, etc. @cat \$\endgroup\$
    – Zach Gates
    Jan 12, 2016 at 19:48
  • \$\begingroup\$ I think the size is no longer 0x555 :-(. But you might get closer using the standard Python-golf tip of using one space for indents. \$\endgroup\$ Jan 13, 2016 at 11:58
  • \$\begingroup\$ @TobySpeight it's 0x557 right now, you're right. And actually, I used tabs, which had to be converted to spaces for posting, but still count as one character. I'll go see if I can shave a couple of characters some other way when I get a chance. \$\endgroup\$
    – Cel Skeggs
    Jan 13, 2016 at 16:04
5
\$\begingroup\$

Python 3, 1138 bytes (UTF-8)

So it turns out that 14 hours of international travel is a fantastic opportunity to finish off a golfing challenge...

The conversion function is C(). This calls u(), v(), and w() to decode, and U(), V(), and W() to encode, UTF-8, -16 and -32, respectively. None of the encoders will output a BOM, but all of the decoders will correctly handle one. Error conditions result in an exception (usually a ZeroDivisionError, courtesy of the "die-suddenly" function E()).

from struct import*
l=len
j=''.join
b=lambda c:[*bin(c)[2:]]
P,Q,i,o,z,Z='HI10><'
B=65279
O,F,H,L,X=1024,65536,55296,56320,57344
E=lambda:1/0
R=lambda y,e,c,n:unpack(([[z,Z][y[:n]==pack(Z+c,B)],e][l(e)])+c*(l(y)//n),y)
S=lambda d,e:B!=d[0]and d or e and E()or d[1:]
def u(y,d=(),p=0):
 while p<l(y):
  q=b(y[p])
  if l(q)>7:
   x=q.index(o);C=1<x<5and q[x+1:]or E();X=x+p;X>l(y)>E();p+=1
   while p<X:q=b(y[p]);C=l(q)>7and(i,o==q[:2])and(*C,*q[2:])or E();p+=1
   d=*d,int(j(C),2)
  else:d=*d,y[p];p+=1
 return S(d,0)
def T(p):
 q=b(p);C=()
 while l(q)not in(7,11,16,21):q=o,*q
 while l(q)>6:C=int(i+o+j(q[-6:]),2),*C;q=q[:-6]
 return bytes(p<128and[p]or[int(i*(7-l(q))+o+j(q),2),*C])
U=lambda c:b''.join(map(T,c))
def v(y,e=''):
 c=R(y,e,P,2);d=[];n=0
 while n<l(c)-1:h,a=c[n:n+2];D=[(h,a),(F+(h-H)*O+a-L,)][H<=h<L<=a<X];M=3-l(D);n+=M;d+=D[:M]
 if n<l(c):d=*d,c[n]
 return S(d,e)
V=lambda c,e=z:W(sum(map(lambda p:([H+(p-F)//O,L+(p-F)%O],[p])[p<F],c),[]),e,P)
w=lambda y,e='':S(R(y,e,Q,4),e)
W=lambda c,e=z,C=Q:pack(e+C*l(c),*c)
K=(u,U),(v,V),(v,V,z),(v,V,Z),(w,W),(w,W,z),(w,W,Z)
def C(y,f,t):f,_,*a=K[f];_,t,*b=K[t];return t(f(y,*a),*b)
\$\endgroup\$
3
\$\begingroup\$

C++, (UTF-8) 971 bytes

#include<cstdint>
using u=uint8_t;using U=uint32_t;U i,o,x,b,m;U R(u*&p){x=*p++;if(!i){m=0;while(128>>m&x)++m;if(m>1)for(x&=127>>m;--m;)x=x<<6|((*p&192)-128?~0:*p++&63);return m?x=~0:x;}else if(i<3){x<<=8;x+=*p++;}else if(i<4){x+=*p++<<8;}else if(i<6){x<<=24;x+=*p++<<16;x+=*p++<<8;x+=*p++;}else{x+=*p++<<8;x+=*p++<<16;x+=*p++<<24;}return x;}U r(u*&p){U x0=R(p);if(i&&i<4&&x>>10==54)x=R(p)>>10==55?(x0<<10)+x-56613888:~0;if(!b++){if(x==65279)if(!i||i%3==1)r(p);else x=~0;else if(x==65534&&i==1)i=3,r(p);else if(x==4294836224&&i==4)i=6,r(p);}return x>1114111||x>>11==27?x=~0:x;}void w(U x,u*&p){if(!o){if(x<128)*p++=x;else{for(m=0;~63<<m&x;m+=6);for(*p++=~127>>m/6|x>>m;m;)*p++=128|x>>(m-=6)&63;}}else if(o<4&&x>65535)x-=65536,w(55296|x>>10,p),w(56320|x&1023,p);else if(o<3)*p++=x>>8,*p++=x;else if(o<4)*p++=x,*p++=x>>8;else if(o<6)*p++=x>>24,*p++=x>>16,*p++=x>>8,*p++=x;else*p++=x,*p++=x>>8,*p++=x>>16,*p++=x>>24;}int t(u*&p,u*&q){for(b=0,x=1;U(x+x);)w(r(p),q);return x;}

The readable program below can be condensed to the above form by filtering it through the following Perl command:

perl -p0 -e 's!//.*!!g;s/\s+/ /g;s/ \B|\B //g;s/0x[\da-f]+/hex($&)/ige;s/#include<[^<>]+>/\n$&\n/g;s/^\n+//mg'

The above command

  • removes comments
  • removes unnecessary whitespace
  • converts hexadecimal literals to decimal
  • reinstates newlines around #include lines

Readable code

#include <cstdint>
using u = uint8_t;
using U = uint32_t;

U   i,                          // input encoding
    o,                          // output encoding
    x,                          // last read value
    b,                          // char count(BOM only valid when b==0)
    m;                          // temporary variable for measuring UTF-8

//   Encodings:
// 0 UTF-8
// 1 UTF-16
// 2 UTF-16BE
// 3 UTF-16LE
// 4 UTF-32
// 5 UTF-32BE
// 6 UTF-32LE

// Read a character or UTF-16 surrogate
U R(u*& p) {
    x = *p++;
    if (!i) { // UTF-8
        m=0; while (128>>m&x) ++m; // how many bytes?
        if (m>1) for (x&=127>>m; --m; ) x = x<<6 | ((*p&192)-128?~0:*p++&63);
        return m ? x=~0 : x;
    } else if (i<3) { // UTF-16, UTF-16BE
        x<<=8; x+=*p++;
    } else if (i<4) { // UTF-16LE
        x+=*p++<<8;
    } else if (i<6) { // UTF-32, UTF-32BE
        x<<=24; x+=*p++<<16; x+=*p++<<8; x+=*p++;
    } else { // UTF-32LE
        x+=*p++<<8; x+=*p++<<16; x+=*p++<<24;
    }
    return x;
}

// Read a character, combining surrogates, processing BOM, and checking range
U r(u*& p) {
    U x0 = R(p);
    if (i && i<4 && x>>10==54)
        x = R(p)>>10==55 ? (x0<<10)+x-56613888: ~0; // 56613888 == 0xd800<<10 + 0xdc00 - 0x10000
    if (!b++) {                 // first char - is it BOM?
        if (x==0xFEFF)
            if (!i || i%3==1)
                r(p); // BOM in UTF-8 or UTF-16 or UTF-32 - ignore, and read next char
            else
                x = ~0; // not allowed in these modes
        else if (x==0xFFFE && i==1)
            i=3,r(p); // reversed BOM in UTF-16 - change to little-endian, and read next char
        else if (x==0xFFFE0000 && i==4)
            i=6,r(p); // reversed BOM in UTF-32 - change to little-endian, and read next char
    }
    return x>0x10ffff || x>>11==27 ? x=~0 : x;
}


// Write character(assumed in-range)
void w(U x, u*& p) {
    if (!o) { // UTF-8
        if (x<128) *p++=x;        // ASCII
        else {
            for (m=0; ~63<<m&x; m+=6); // how many bits?
            for (*p++=~127>>m/6|x>>m; m; ) *p++ = 128|x>>(m-=6)&63;
        }
    } else if (o<4 && x>65535)  // UTF-16 surrogate
        x-=65536, w(0xD800|x>>10,p), w(0xDC00|x&0x3FF,p);
    else if (o<3)  // UTF-16, UTF-16BE
        *p++=x>>8, *p++=x;
    else if (o<4)  // UTF-16LE
        *p++=x, *p++=x>>8;
    else if (o<6)  // UTF-32, UTF-32BE
        *p++=x>>24, *p++=x>>16, *p++=x>>8, *p++=x;
    else  // UTF-32LE
        *p++=x, *p++=x>>8, *p++=x>>16, *p++=x>>24;
}

// Transcode
int t(u*& p, u*& q)                  // input, output
{
    for (b=0,x=1;U(x+x);)    // exit condition is true only for x==-x, i.e. 0 and ~0
        w(r(p),q);
    return x;
}

The function to be called is t(), with input and output encodings passed in the global variables i and o respectively, and p pointing at the bytes of input, which must be null-terminated. q points to the output buffer, which will be overwritten, and must be big enough for the result - there is no attempt to avoid buffer overrun.

I hope the code comments are sufficiently explanatory - ask below if one of them is too cryptic (but do make an effort first!).

I compiled a substantial test suite whilst developing this answer; I include it below for the benefit of other entrants, and to document my interpretation of requirements:

Test functions

#include <vector>
#include <iostream>

std::ostream& operator<<(std::ostream& out, const std::vector<u>& v)
{
    out << "{ ";
    for (int i: v) out << i << " ";
    out << "}";
    return out;
}

int test_read(int encoding, std::vector<u> input, U expected)
{
    b = 0;
    i = encoding;
    auto d = input.data();
    U actual = r(d);
    if (actual == expected) return 0;
    std::cerr << std::hex << "Decoding " << encoding << "; " << input << " gave " << actual
              << " instead of " << expected << std::endl;
    return 1;
}

int test_write(int encoding, U input, std::vector<u> expected)
{
    o = encoding;
    u buf[20], *p = buf;
    w(input, p);
    std::vector<u> actual(buf,p);
    if (expected == actual) return 0;
    std::cerr << std::hex << "Encoding " << encoding << "; " << input << " gave " << actual
              << " instead of " << expected << std::endl;
    return 1;
}

int test_transcode(int ienc, std::vector<u> input, int oenc, std::vector<u> expected)
{
    b = 0;
    i = ienc; o = oenc;
    u buf[200], *p = buf, *d = input.data();
    int result = t(d, p);
    std::vector<u> actual(buf,p);
    if (result ? expected.empty() : expected == actual) return 0;
    std::cerr << std::hex << "Encoding " << ienc << " to " << oenc << "; " << input << " gave " << actual
              << " instead of " << expected << std::endl;
    return 1;
}

Test suite

static const U FAIL = ~0;
int main() {
    int e = 0;                        // error count
    // UTF-8
    e += test_read(0, { 128 }, FAIL); // unexpected continuation
    e += test_read(0, { 128, 1 }, FAIL);
    e += test_read(0, { 128, 128 }, FAIL);
    e += test_read(0, { 192, 192 }, FAIL); // start without continuation
    e += test_read(0, { 192, 0 }, FAIL);
    e += test_read(0, { 224, 0 }, FAIL);
    e += test_read(0, { 224, 192 }, FAIL);
    e += test_read(0, { 0xf4, 0x90, 128, 128 }, FAIL); // Unicode maximum+1

    e += test_read(0, { 127 }, 127);
    e += test_read(0, { 192, 129 }, 1); // We accept overlong UTF-8
    e += test_read(0, { 0xc2, 128 }, 128);
    e += test_read(0, { 224, 128, 129 }, 1);
    e += test_read(0, { 0xef, 128, 128 }, 0xF000);
    e += test_read(0, { 0xef, 191, 191 }, 0xFFFF);
    e += test_read(0, { 0xf4, 128, 128, 128 }, 0x100000);
    e += test_read(0, { 0xf4, 0x8f, 191, 191 }, 0x10FFFF); // Unicode maximum

    e += test_read(0, { 0xEF, 0xBB, 0xBF, 127 }, 127); // byte-order mark

    e += test_write(0, 0, { 0 });
    e += test_write(0, 127, { 127 });
    e += test_write(0, 128, { 0xc2, 128 });
    e += test_write(0, 255, { 0xc3, 191 });
    e += test_write(0, 0xFFFF, { 0xef, 191, 191 });
    e += test_write(0, 0x10FFFF, { 0xf4, 0x8f, 191, 191 });

    // UTF-16
    e += test_read(1, { 0, 1 }, 1);
    e += test_read(1, { 0xd8, 0, 0xdc, 1 }, 0x10001);
    e += test_read(1, { 0xdb, 0xff, 0xdf, 0xff }, 0x10ffff);

    e += test_read(1, { 0xd8, 0, 0xd8, 1 }, FAIL); // mismatched surrogate
    e += test_read(1, { 0xd8, 0, 0, 1 }, FAIL); // mismatched surrogate
    e += test_read(1, { 0xdc, 0 }, FAIL);

    e += test_write(1, 1, { 0, 1 });
    e += test_write(1, 256, { 1, 0 });
    e += test_write(1, 0xffff, { 255, 255 });
    e += test_write(1, 0x10001, { 0xd8, 0, 0xdc, 1 });
    e += test_write(1, 0x10ffff, { 0xdb, 0xff, 0xdf, 0xff });

    // UTF-16LE
    e += test_write(3, 1, { 1, 0 });
    e += test_write(3, 256, { 0, 1 });
    e += test_write(3, 0x10001, { 0, 0xd8, 1, 0xdc });
    e += test_write(3, 0x10fffe, { 0xff, 0xdb, 0xfe, 0xdf });

    // UTF-16 byte-order mark
    e += test_read(1, { 0xFE, 0xFF, 0x0, 1 }, 1); // byte-order mark
    e += test_read(1, { 0xFF, 0xFE, 1, 0x0 }, 1); // reversed byte-order mark
    // disallowed byte-order marks
    e += test_read(2, { 0xFE, 0xFF }, FAIL);
    e += test_read(3, { 0xFF, 0xFE }, FAIL);
    // reversed byte-order mark is an unassigned character - to be treated like regular character, according to question
    e += test_read(2, { 0xFF, 0xFE }, 0xfffe);
    e += test_read(3, { 0xFE, 0xFF }, 0xfffe);

    // UTF-32
    e += test_read(4, { 0, 0, 0, 1 }, 1);
    e += test_read(4, { 1, 0, 0, 0 }, FAIL);
    e += test_write(4, 1, { 0, 0, 0, 1 });
    e += test_write(4, 0x10203, { 0, 1, 2, 3 });

    // UTF-32LE
    e += test_read(6, { 0, 0, 0, 1 }, FAIL);
    e += test_read(6, { 1, 0, 0, 0 }, 1);

    // UTF-32 byte-order mark
    e += test_read(4, { 0, 0, 0xFE, 0xFF,  0, 0, 0, 1 }, 1); // byte-order mark
    e += test_read(4, { 0xFF, 0xFE, 0, 0,  1, 0, 0, 0 }, 1); // reversed byte-order mark
    // disallowed byte-order marks
    e += test_read(5, { 0, 0, 0xFE, 0xFF }, FAIL);
    e += test_read(5, { 0xFF, 0xFE, 0, 0 }, FAIL);
    e += test_read(6, { 0, 0, 0xFE, 0xFF }, FAIL);
    e += test_read(6, { 0xFF, 0xFE, 0, 0 }, FAIL);

    e += test_transcode(1, { 1, 2, 0xFE, 0xFF, 0, 0 }, // That's not a BOM; it's a zwnj when not the first char
                        1, { 1, 2, 0xFE, 0xFF, 0, 0 });
    e += test_transcode(1, { 0xFF, 0xFE, 1, 2, 0, 0 }, // reversed byte-order mark implies little-endian
                        1, { 2, 1, 0, 0 });
    e += test_transcode(4, { 0xFF, 0xFE, 0, 0, 1, 2, 0, 0, 0, 0 }, // reversed BOM means little-endian
                        4, { 0, 0, 2, 1, 0, 0, 0, 0 });
    e += test_transcode(1, { 0xdb, 0xff, 0xdf, 0xff, 0, 0 }, // U+10ffff UTF-16 to UTF-8
                        0, { 0xf4, 0x8f, 191, 191, 0 });

    return e;
}
\$\endgroup\$
2
  • \$\begingroup\$ Dang.. C++ beat Python. \$\endgroup\$
    – TickTock
    Apr 22, 2016 at 20:07
  • \$\begingroup\$ @TickTock : while it's technically not "transcoding" per se, my awk entry below does fully encode UTF-8 and UTF-32 BE+LE, all without needing anything fancy like lambda functions…. on top of not having a single hard-coded number \$\endgroup\$ Nov 10, 2022 at 19:42
2
\$\begingroup\$

This ENTIRELY self-contained and POSIX-compliant awk function of my own making, that is

  • fully-encapsulated with zero external dependencies

  • can now encode any single Unicode code-point, in both UTF-8 and UTF-32 (BE+LE), plus any arbitrary byte, regardless of locale-setting (as long as it's not something like EBCDIC). The only part missing is UTF-16.

  • is proven working on mawk-1/2, gawk (in any mode other than -t), even nawk (sans "\0" NUL byte), and

  • doesn't need loops, recursion, pre-made lookups in any shape or form, or caching/memoization

Every single threshold, range cutoff, scaling factor, and necessary offset (like xC0 xE0 xF0 etc), both for the auto-detection part and the encoding part, are being dynamically generated on the fly, while requiring only 1 additional temp variable on top of the 2 needed for user inputs anyway.

My own benchmarking of mawk-2 running it puts it within just 1.7x speed ratio against the built-in encoder in gawk ( 1.0x ), which is a decent showing for a user-level function benchmarked against compiled C-code binaries.

  • __ is code-point integer input, with auto-detection for out-of-range values such as x < 0, 1114111 < x, plus the UTF-16 surrogate ranges, which would simply print out 1 single arbitrary byte that is its modulo-equivalent.

  • _ is optional - enter -32 for UTF32-LE or 32 for UTF32-BE;

    any other non-empty input, e.g. "1" or "/" or "\t" etc, forces it to use the awk-built-in sprintf("%c", x) single-character encoder, taking the input as-is, and printing out 1 character that will be locale-setting specific

    (this feature is most useful when validating against gawk in unicode mode)

.

jot 1114112 0 | mawk 'BEGIN {  }

function chrUC8(__,_,___) {
    return \
    ((___=_)*_)>=(_+=_+=_^=_<_)^_ \
    ? ( (+___<-___)==(___=(--_+_)^_*(_^=++_))^!_ \
        ? sprintf("%c%c%c%c", (__=int(__) % (_*_*_*_))%_+___,
            int(__/_)%_+___, (__=int(__/_/_))%_+___, int(__/_)+___)\
        : sprintf("%c%c%c%c",  int((__%=_*_*_*_)/_/_/_)+___,
            int(__/_/_)%_+___, int(__/_)%_+___,__%_+___)\
                                 ) \
                                 :  +___  ||
          (((__=+__)+__)<_^_ && -__<=+__) ||
      ((___=(--_+_)^_*++_^_)<=__ && __<(_^_*(_+_)+___)) ||
                                  +__<-__ ||
                              ((_+_)^_*(_*_+_^_))<=__\
        ? sprintf("%c",+___<_?__: (__%(_^=_)+_)%_+___)\
    : (__+__)<(_+_)^_                                  \
        ? sprintf("%c%c", (___+=(_*=_*_)+_) +_+ int(__/_), __%_+___) \
    : __<_^(_+_)                                                    \
        ? sprintf("%c%c%c", _*(_+_) + (___+=(_*=_*_)+_) +_+ int(__/_/_),
                                     int(__/_)%_ + ___, __%_ + ___) \
        : sprintf("%c%c%c%c", _*-_+ (___+=(_*=_*_)+_) +_+_+ int(__/_/_/_),
                int(__/_/_)%_ +___, int(__/_)%_ + ___,__%_+___)  
}'

The squeezed form is roughly 789 bytes, give or take.

How can this form save space at all ?

e.g. one can write 6 ASCII-bytes of 262144 or 2^18 or even 64^3 to scale 3 continuation bytes in the supplementary planes…

…… or save space via 8^6 or 4^9.

because truth be told, the number 4, and combinations of it, is all you need to encode everything about Unicode :

4
16
256
 #
128
 2048
   65536
      1114112
           ##
           64
           4096
             262144
                 ###
                  128
                   192
                    224
                     240
                      #####
                         1048576
                              2048
                                55296
                                   1024
                                     56320
                                        57344
      # gawk profile,
     created Thu Nov 10 14:26:19 2022

     gawk 'BEGIN {
     1     OFS="\f\b\b"
     1     print "",
     _=4,
     _*_,
     _^_,
     "#",
     _*(_+_)*_,
     _^_*(_+_),
     _^(_+_),
     (_+_)^_*(_^_+_*_),
     "##",
     _*_*_,
     (_+_)^_,
     _^(_+_)*_,
     "###",
     _*(_+_)*_,
     _^_-_*_*_,
     _^_-(_+_)*_,
     _^_-_*_,
     "#####",
     _*_^(_+_)*_,
     _^_*(_+_),
     _^_*(_^_-_-(_+_)*_-_),
     _^_*_,
     _^_*(_^_-(_+_)*_-_),
     (_^_-_*_-_*_)*_^_
}' 
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.