C (170 characters)
i,j;main(c,s){char**r=s,*p=*++r;for(;i<3;)j--?putchar(!p[-1]?p=*r,++i,j=0,10:
"##3#3133X=W.<X/`^_G0?:0@"[i*8+c/2]-33>>c%2*3+j&1?"|_"[j&1]:32):(j=3,c=*p++&31,
c-=c>6?10:1);}
This takes the input string as a command-line argument. Conversion to use stdin would be one more character:
i,j;main(c){char s[99],*p=s;for(gets(s+1);i<3;)j--?putchar(!*p?p=s,++i,j=0,10:
"##3#3133X=W.<X/`^_G0?:0@"[i*8+c/2]-33>>c%2*3+j&1?"|_"[j&1]:32):(j=3,c=*++p&31,
c-=c>6?10:1);}
The stdin version can accept up to 98 input characters. Of course, any more than floor(terminalWidth / 3)
will cause confusing line wrap.
The output for each character is treated like a 3x3 grid, where the cells in each row are the segments. A segment is either "on" or "off". If a segment is "on", either a '|'
or a '_'
is output, depending on position. If it's off, a space is output. The character array is an array of bits that determine whether each segment is on or off. More about that after the code:
i,j; /* Loop variables. As globals, they'll be initialized to zero. */
main(c,s){
/* The signature for main is
*
* main(int argc, char **argv)
*
* Rather than add more characters for properly declaring the parameters,
* I'm leaving them without type specifiers, allowing them to default to
* int. On almost all modern platforms, a pointer is the same size as
* an int, so we can get away with the next line, which assigns the int
* value s to the char** variable r.
*/
char**r=s,*p=*++r;
/* After coercing the int s to a char** r, offset it by 1 to get the
* value of argv[1], which is the command-line argument. (argv[0] would
* be the name of the executable.)
*/
for(;i<3;) /* loop until we're done with 3 lines */
j--?
/* j is our horizontal loop variable. If we haven't finished a
* character, then ... */
putchar( /* ...we will output something */
!p[-1]? /* if the previous char was a terminating null ... */
p=*r,++i,j=0,10
/* ... reset for the next row. We need to:
*
* - reinitialize p to the start of the input
* - increment our vertical loop variable, i
* - set j to zero, since we're finished with this
* "character" (real characters take 3 iterations of
* the j loop to finish, but we need to short-circuit
* for end-of-string, since we need to output only one
* character, the newline)
* - finally, send 10 to putchar to output the newline. */
:"##3#3133X=W.<X/`^_G0?:0@"[i*8+c/2]-33>>c%2*3+j&1?
/* If we haven't reached the terminating null, then
* check whether the current segment should be "on" or
* "off". This bit of voodoo is explained after the
* code. */
"|_"[j&1]:32
/* if the segment is on, output either '|' or '_',
* depending on position (value of j), otherwise,
* output a space (ASCII 32) */
)/* end of putchar call */
:(j=3,c=*p++&31,c-=c>6?10:1);
/* this is the else condition for j--? above. If j was zero,
* then we need to reset for the next character:
*
* - set j to 3, since there are three cells across in the grid
* - increment p to the next input character with p++
* - convert the next character to a value in the range 0–15.
* The characters we're interested in, 0–9, A–F, and a–f, are
* unique in the bottom four bits, except the upper- and
* lowercase letters, which is what we want. So after anding
* with 15, the digits will be in the range 16–25, and the
* letters will be in the range 1–6. So we subtract 10 if
* it's above 6, or 1 otherwise. Therefore, input letters
* 'A'–'F', or 'a'–'f' map to values of c between 0 and 5,
* and input numbers '0'–'9' map to values of c between
* 6 and 15. The fact that this is not the same as the
* characters' actual hex values is not important, and I've
* simply rearranged the data array to match this order.
*/
}
The character array describes the character grids. Each character in the array describes one horizontal row of the output grid for two input characters. Each cell in the grid is represented by one bit, where 1
means that segment is "on" (so output a '|'
or a '_'
, depending on position), and 0
means that segment is "off".
It takes three characters in the array to describe the entire grid for two input characters. The lowest three bits of each character in the array, bits 0-2, describe one row for the even input character of the two. The next three bits, bits 3-5, describe one row for the odd input character of the two. Bits 6 and 7 are unused. This arrangement, with an offset of +33, allows every character in the array to be printable, without escape codes or non-ASCII characters.
I toyed with several different encodings, including putting the bits for all 7 segments of an input character into one character in the array, but found this one to be the overall shortest. While this scheme requires 24 characters in the array to represent the segments of only 16 input characters, other encodings either required using non-ASCII characters (which unsurprisingly caused problems when I used this in my Morse Code golf answer), a lot of escape codes, and/or complex decoding code. The decoding code for this scheme is surprisingly simple, although it does take full advantage of C's operator precedence to avoid having to add any parentheses.
Let's break it into tiny steps to understand it.
"##3#3133X=W.<X/`^_G0?:0@"
This is the encoded array. Let's grab the appropriate character to decode.
[i*8
The first 8 characters describe the top row of segments, the next 8 describe the middle row of segments, and the last 8 describe the bottom row of segments.
+c/2]
Remember that, by this point, c contains a value from 0 to 15, which corresponds to an input of ABCDEF0123456789, and that the array encodes two input characters per encoded character. So the first character in the array, '#'
, holds the bits for the top row of 'A' and of 'B', the second character, also '#'
, encodes the top row of 'C' and 'D', and so on.
-33
The encoding results in several values that are under 32, which would require escape codes. This offset brings every encoded character into the range of printable, unescaped characters.
>>
The right shift operator has lower precedence than arithmetic operators, so this shift is done to the character after subtracting the offset.
c%2*3
c%2
evaluates to zero for even numbers, and to one for odd numbers, so we'll shift right by three for odd characters, to get at bits 3–5, and not shift at all for even characters, providing access to bits 0–2. While I'd prefer to use c&1
for even/odd check, and that is what I use everywhere else, the &
operator has too low precedence to use here without adding parentheses. The %
operator has just the right precedence.
+j
Shift by an additional j
bits to get at the correct bit for the current output position.
&1
The bitwise and operator has lower precedence than both the arithmetic operators and the shift operators, so this will test whether bit zero is set after shifting has brought the relevant bit into bit zero.
?
If bit zero is set ...
"|_"
... output one of these characters, chosen by ...
[j&1]
... whether our horizontal loop variable is even or odd.
:32
Otherwise (bit zero is not set), output 32 (space character).
I don't think I can trim this down much more, if any, and certainly not enough to beat hobbs's perl entry.