# Convert an ASCII art table to a UTF-8 table

When I write documentation, comments, etc. I love making ASCII tables. They usually end up looking pretty good, but I always feel that they could look even better - especially since UTF-8/Unicode includes the box drawing characters. However, these characters are very burdensome to use, requiring several key presses to insert. Your task? Write a program or a function that can automatically convert ASCII tables to the UTF-8/Unicode equivalent.

This challenge was sandboxed.

## Challenge

Write a program, that given an ASCII table as an input string, outputs the table redrawn with the Unicode/UTF-8 box drawing characters. Specifically, the characters that are a part of the table should be translated as follows:

(Unicode, 3 bytes each in UTF-8)
- to ─ (\u2500)
| to │ (\u2502)
= to ═ (\u2550)

and + to one of:
┌ (\u250C), ┐ (\u2510), └ (\u2514), ┘ (\u2518),
├ (\u251C), ┤ (\u2524), ┬ (\u252C), ┴ (\u2534),
┼ (\u253C)
or, if '=' on either side:
╒ (\u2552), ╕ (\u2555), ╘ (\u2558), ╛ (\u255D),
╞ (\u255E), ╡ (\u2561), ╤ (\u2564), ╧ (\u2567),
╪ (\u256A)


## Details

I/O:

• Default I/O is allowed
• You may take input in any reasonable format, including the table as a string, or a path to a file containing the table.
• You may output to a file and take the file name as an additional argument.
• However, you may not modify the input file. (It should be retained for ease of future editing)

Input:

• You may assume that every row of input has been padded to be the same length with .
• You may not assume that the first character after a newline is a part of the table borders (as it may be whitespace).
• Input is considered a valid table if all characters (that are a part of the table) -=| are connected to exactly two characters and + are connected to at least one character both horizontally and vertically.
• Your program may not produce any errors with valid inputs.
• If the input is not valid the behavior is undefined and you may produce any output.
• The input may contain any UTF-8 characters, including the box drawing characters.

Output:

• Any of the characters -=|+ that are not a part of the table must be left as-is.
• Similarly, any other characters must be left as-is.
• A single leading and/or trailing newline is allowed.

Other:

• Standard loopholes are forbidden, as per usual.
• If your preferred language has a built-in that solves this problem, you may not use it.
• This means programs, functions, subroutines or instructions that would be valid submissions for this challenge with no additions.
• Each of the characters needed in this challenge are three bytes long when they're encoded in UTF-8.

Connected characters:

A character is connected to another, if:

• It is | and is directly above or below + or |;
• It is - and is directly before or after + or -;
• It is = and is directly before or after + or =;
• It is + and is directly above or below | or +, or is directly before or after -, = or +.

A character is considered a part of the table, if it is connected to any character that is a part of the table. By definition, the first + in the input is a part of the table.

## Examples

Examples available here as a copy-pastable version.

 Input:                    Output:
+------------------+      ┌──────────────────┐
|   Hello+World!   |      │   Hello+World!   │
+==================+      ╞══════════════════╡
| This is+my first |  ->  │ This is+my first │
|+-+ code|golf  +-+|      │+-+ code|golf  +-+│
|+-+chall|enge! +-+|      │+-+chall|enge! +-+│
+------------------+      └──────────────────┘

+===+===+===+             ╒═══╤═══╤═══╕
| 1 | 2 | 3 |             │ 1 │ 2 │ 3 │
+---+===+===+===+         ┌───╪═══╪═══╪═══╡
| 1 | 1 | 2 | 3 |         │ 1 │ 1 │ 2 │ 3 │
+---+---+---+---+    ->   ├───┼───┼───┼───┤
| 2 | 2 | 4 | 6 |         │ 2 │ 2 │ 4 │ 6 │
+---+---+---+---+         ├───┼───┼───┼───┤
|-3 |-3 |-6 |-9 |         │-3 │-3 │-6 │-9 │
+===+---+---+---+         ╘═══╧───┴───┴───┘

+-----+         ->      <Undefined>

+-----+         ->      ┌─────┐
+-----+                 └─────┘

+-----------------+
|  Hello, World!  |
| This is invalid |   ->      <Undefined>
|      input      |
-----------------+

++++                      ┌┬┬┐
++++           ->         ├┼┼┤
++++                      └┴┴┘

+--+
++++           ->      <Undefined>
+--+


## Finally...

This is , so the least amount of bytes wins. Happy golfing!

• In the first example, why are the consecutive +-+ excerpts not considered to form a connected table? Oct 17, 2018 at 17:51
• If a maybe 16-bit function use a single byte to represent ╡, how's the byte count?
– l4m2
Oct 17, 2018 at 18:20
• @recursive If you mean the first Hello World table, the inner tables are not considered to form a table because the text inside the table must stay unchanged, and they are not considered a part of the outer table borders as they are not connected to them properly.
– user77406
Oct 17, 2018 at 19:19
• If you mean the first +----+ example, it would be because the direction of the corners would be ambiguous.
– user77406
Oct 17, 2018 at 19:20
• Oh, the "no tables within tables unless they connect to extend the outermost possible table" requirement makes this a lot tougher. Oct 17, 2018 at 19:22

# Python 3, 914898827823594587569540 469 bytes

Edit: significantly changed strategy, now making a bitfield of neighbors (similar to dead-possum's answer). I've left the earlier version below.

H='─│═-|=└╘++++┌╒├╞++┘╛++┴╧┐╕┤╡┬╤┼╪'
def n(l):
def j(r,c,t=0):O=(0,r-1,c),(1,r,c+1),(2,r+1,c),(3,r,c-1);v=f(r,c);b=t|any(f(Y,X)=='='for i,Y,X in O);l[r][c]={'+':H[b+2*sum((f(Y,X)in H)<<x for x,Y,X in O)],**dict(zip(H[3:6],H))}.get(v,v);[f(Y,X)!=';'and v in'+++-|='[i%2::2]and j(Y,X,v=='=')for i,Y,X in O]
for i,I in enumerate(l):
if'+'in I:f=lambda r,c:l[r][c]if len(l)>r>=0and 0<=c<len(l[r])else';';j(i,I.index('+'));break


Try it online!

Input is in the form of a list of lists of characters, which is modified in place. Recurses from the first + that it finds.

x=range
C='┌┐└┘','╒╕╘╛'
D='┬┤┴├','╤╡╧╞'
A='┼╪'
H,V,T='─│═'
J={'-':H,'|':V,'=':T}
K=C[1]+D[1]+A[1]+'='+T
E=('+|','+-=')*2
F=['+|'+V,'+-='+H+T]*2
O=(0,-1,0),(1,0,1),(2,1,0),(3,0,-1)
for i in x(4):
for j in{0,1,2,3}-{i}:F[i+2&3]+=D[0][j]+D[1][j]
h=C[0][i]+C[1][i];F[i&2]+=h;F[3-2*(i&1)]+=h
def n(l):
for i,I in enumerate(l):
if'+'in I:r=i;c=I.index('+');break
else:return l
def f(r,c):
try:assert c>=0 and r>=0;return l[r][c]
except:return'\0'
def j(r,c):
v=f(r,c)
l[r][c]=J.get(v,v)
if v=='+':
X=[f(r+Y,c+X)for i,Y,X in O];B=any(x in K for x in X);X=[X[x]in F[x]for x in x(4)];L=sum(X)
if L in(2,3,4):l[r][c]=D[B][X.index(False)]if L==3 else C[B][X[0]*2+X[3]]if L==2 else A[B]
for i,Y,X in O:
if v in E[i]and f(r+Y,c+X)in E[i]:j(r+Y,c+X)
j(r,c);return l


Try it online!

Here's the closest thing I have to an ungolfed version:

def tr(s):
t='┌┐└┘','╒╕╘╛'
t2='┬┤┴├','╤╡╧╞'
A = '┼','╪'
H,V,T = '─│═'
Th = ''.join(x[1]for x in (t,t2,A))+'='+T
ps = ['+|'+V, '+-='+H+T, '+|'+V, '+-='+H+T]
ps2 = ('+|', '+-=')*2
for i in range(4):
for j in {0,1,2,3}-{i}:
ps[(i+2)%4] += t2[0][j]+t2[1][j]
h=t[0][i] + t[1][i]
ps[i & 2] += h
ps[3 - 2 * (i & 1)] += h

l = [list(x) for x in s.split('\n')]
r = 0
for i,I in enumerate(l):
if'+'in I:
r=i;c=I.index('+')
break
def g(r,c): return l[r][c]
def G(r,c):
if r >= 0 and r < len(l) and c >= 0 and c < len(l[r]):
return g(r,c)
return '\0'
def process(r,c):
v = g(r,c)
if v == '-': l[r][c] = H
elif v == '|': l[r][c] = V
elif v == '=': l[r][c] = T
elif v == '+':
all=[G(r-1,c),G(r,c+1),G(r+1,c),G(r,c-1)]
bold=any(x in Th for x in all)
for i in range(4):all[i] = all[i] in ps[i]
N,E,S,W=all
tt=sum(all)
if tt == 3:
l[r][c]=t2[bold][all.index(False)]
elif tt == 2:
l[r][c]=t[bold][N*2+W]
elif tt == 4:
l[r][c]=A[bold]
else: return
for i,(dy,dx) in enumerate(((-1,0),(0,1),(1,0),(0,-1))):
if v in ps2[i] and G(r+dy,c+dx) in ps2[i]:
process(r+dy,c+dx)
process(r,c)
return l

• Minor improvements to save 9 bytes (down to 814) bit.ly/2NOu7HF Oct 19, 2018 at 17:55
• Few more for another 9 bytes (805 bytes now) bit.ly/2pYom0x Oct 19, 2018 at 18:06
• Down to 763: bit.ly/2OxErsJ Oct 19, 2018 at 18:37

# Python 3, 392 281 bytes

Golfed it quite a bit more and converted to a recursive solution instead of an iterative one:

def h(a):
def g(i):
k=-3;d=a[i]=='=';z[i]=''
for p,j,r in zip((1,2,4,8),(i+1,i+w,i-1,i-w),('+-=','+|')*2):
if 0<=j<len(a)and{a[i],a[j]}<={*r}:k+=p;d|=a[j]=='=';z[j]and g(j)
z[i]="┌╒!!─═┐╕┬╤@@└╘││├╞┘╛┴╧┤╡┼╪"[2*k+d]
w=a.find('\n')+1;z=[*a];g(a.find('+'))
return''.join(z)


Takes a string of equal length rows separated by newlines, and returns a string in the same format. May throw an exception on invalid input.

Previous solution:

def h(a):
i=a.find('+');q=[i];w=a.find('\n')+1;z=[*a]
while q:
i=q.pop();c=a[i];k=-5
for p,j in enumerate((i+1,i-1,i+w,i-w)):
r='++-|='[p>1::2]
if 0<=j<len(a)and a[i]in r and a[j]in r:
k+=1<<p;q+=[j][:z[j]<'─']
z[i]='│'if c>'='else'─═'[a[i]>'-']if c>'+'else"┌╒┐╕┬╤@@└╘┘╛┴╧##├╞┤╡┼╪$$"['='in a[abs(i-1):i+2]::2][k] return''.join(z)  Ungolfed version: def h(a): i = a.find('+') # find index of first '+'. It is first node q = [i] # in the queue of indexes to convert to unicode w = a.find('\n')+1 # width of the table z = [*a] # strings are immutable, so copy it to a list while q: # while the queue isn't empty i=q.pop() # get the next index to process c=a[i] # and the associated character k=-5 # 'k' is the index into the unicode string, U. The way they # are encoded, the first unicode value is at index 5. # directions E W S N for p,j in enumerate((i+1,i-1,i+w,i-w)): # j is the index of an adjacent cell # r='++-|='[p>1::2] is equivalent to: if p > 1: r = '+|' # compatible symbols for vertical connections else: r = '+-=' # compatible symbols for horizontal connections # if adjacent cell index is valid and the characters are compatible if 0 <= j < len(a) and a[i] in r and a[j] in r: k += 1<<p # update the unicode symbol index # q += [j][:z[j]<'─'] is equivalent to: if z[j] < '-': # if the adjacent cell hasn't been converted already q.append(j) # append it's index to the queue if c > '=': z[i] = '│' # replace a '|' with a '│' elif c > '+': z[i] = '─═'[a[i]>'-'] # replace a '-' or '=' with '─' or '═' respectively else: # it's a '+' U = "┌╒┐╕┬╤@@└╘┘╛┴╧##├╞┤╡┼╪$$"         # even indexes are single horizontal line,
# double horizontal lines are at odd indexes

z[i] = U['='in a[abs(i-1):i+2]::2][k]  # '='in a[abs(i-1):i+2] is true if there is an '=' to the left or right
# so this selects the odd chars from U
#  then [k] selects the correct char

return''.join(z)


# JavaScript, 311 307 bytes

X=>(O=[...X],P=(I,j=0,_=0)=>!P[I]&&(P[I]=1,['-─1','|│','=═1'].map(([a,b,n=X.indexOf('\n')+1])=>[-n,+n].map(n=>{for(i=I;X[i+=n]==a;)O[i]=b
if(X[i]=='+')j|=[1,2,4,8,I-i>1&&17,i-I>1&&18][_],P(i)
_++})),O[I]='┘└┴ ┐┌┬ ┤├┼     ╛╘╧ ╕╒╤ ╡╞╪'[j-5]),P(X.indexOf+),O.join)


f=
X=>(W=X.indexOf('\n')+1,O=[...X],P=(I,j=0,_=0)=>!P[I]&&(P[I]=1,['-─1','|│','=═1'].map(([a,b,n=W])=>[-n,+n].map(n=>{for(i=I;X[i+=n]==a;O[i]=b);if(X[i]=='+')j|=[1,2,4,8,I-i>1&&17,i-I>1&&18][_],P(i);_++})),O[I]='┘└┴ ┐┌┬ ┤├┼     ╛╘╧ ╕╒╤ ╡╞╪'[j-5]),P(X.indexOf+),O.join)

console.log(
f(
+------------------+
|   Hello+World!   |
+==================+
| This is+my first |
|+-+ code|golf  +-+|
|+-+chall|enge! +-+|
+------------------+
))

console.log(
f(
     +===+===+===+
| 1 | 2 | 3 |
+---+===+===+===+
| 1 | 1 | 2 | 3 |
+---+---+---+---+
| 2 | 2 | 4 | 6 |
+---+---+---+---+
|-3 |-3 |-6 |-9 |
+===+---+---+---+
))

console.log(
f(
+-----+
+-----+))

console.log(
f(
++++
++++
++++))

## Explanation

Starting at the first found + junction, the program attempts to find paths to other junctions in every direction, performing replacements as it goes. It stores the found directions and the "double-bordered" state in a bitmap, which then determines the appropriate junction character.

// Take an input string X
f = X => {
// Copy the input string into an array so characters can be overwritten and eventually output
O = [...X]

// Define a function that processes a junction ("+" symbol) at index I in the input string X:
P = I => {
// Make a bitmap to keep track of the direction coming out of the junction and double borders
// Bits from right to left: west, east, north, south, double border
// E.g. a double-bordered south/east junction corresponds to the binary number 11010 ("╒")
let j = 0

// A counter
let _ = 0

// Ensure this junction hasn't already been processed
if(!P[I]){
P[I] = 1,

// We'll walk away from the junction in each of the four directions, then west and east again to check for double borders
// i.e. walk along a, replace with b, move index i by n
// 1st pass: walk along "-", replace with "─", move index by 1
// 2nd pass: walk along "|", replace with "│", move index by the width of the input (plus 1 for the newline) to traverse vertically
// 3rd pass: walk along "=", replace with "═", move index by 1
['-─1','|│','=═1'].map(([a, b, n = X.indexOf('\n') + 1])=>
// We'll walk in the negative and positive directions for each pass
[-n,+n].map(n=>{
// Start the walk
i=I
// Keep walking (incrementing by n) as long as we're on a "path" character, "a"
while(i += n, X[i] == a)
// Replace the corresponding character in the output with "b"
O[i] = b

// Upon reaching another junction at index i:
if(X[i] == '+'){
// OR the bitmap according to the direction we walked
j |= [
// Pass 1: Horizontal
1, // west
2, // east

// Pass 2: Vertical
4, // north
8, // south

// Pass 3: Double Horizontal (only if we've walked more than 1 step)
I-i > 1 && 17, // west, double border
i-I > 1 && 18 // east, double border
][_]

// Process the junction we walked to
P(i)
}
_++
})
)

// Finally, replace the "+" with a proper junction character based on the bitmap value
O[I] = '     ┘└┴ ┐┌┬ ┤├┼     ╛╘╧ ╕╒╤ ╡╞╪'[j]
}
}

// Process the first junction to kick off the recursion
P(X.indexOf+)

// Return our modified character array as a joined string
return O.join
}

• Fixed - I must have looked at the character count and not the byte count. Oct 19, 2018 at 8:23

# Python 3, 599 bytes

I'm not really good at golfing in Python 3, but (to my shame) I couldn't get normal output of UTF-8 chars in Python 2. So here we are.

I guess the only interesting trick here is deciding of + tranformation.
I've encoded all possible variants with 4-bit adresses. Each bit of adress resembles connection to neightbour cell. So 0 - no connection and 1 - connection.
1111 is ┼
0011 is ┐
etc
Some configurations of connections are invalid and replaced with dummy values: '012┐45┌┬8┘0┤└┴├┼'

If any neightbour cell contains =, second list will be used with doubled lines.

['012┐45┌┬8┘0┤└┴├┼','012╕45╒╤8╛0╡╘╧╞╪']['='in r]


r=''.join([str(int(V(y,x)))+W(y,x)for y,x in[(Y-1,X),(Y,X+1),(Y+1,X),(Y,X-1)]])


r contains string lenght 8, where every two chars are 1/0 and actuals neightbour char.
For example: 1+0y1-1|.
This is used to choose list of substitutions as shown before. And then contracted to adress: int(r[0::2],2)

This lambda used to verify that cell coordinates are valid and cell's char is one of '+-|='

V=lambda y,x:~0<x<len(I[0])and~0<y<len(I)and I[y][x]in'+-|='


This lambda used to receive char from cell. Returns ' ' if coordinates are invalid. (definately can be golfed away)

W=lambda y,x:V(y,x)and I[y][x]or' '


Conditions for recursion. Might be golfable too.

if Z in'+-=':F(Y,X+1);F(Y,X-1)
if Z in'+|':F(Y-1,X);F(Y+1,X)


I=eval(input())
J=[i[:]for i in I]
V=lambda y,x:~0<x<len(I[0])and~0<y<len(I)and I[y][x]in'+-|='
W=lambda y,x:V(y,x)and I[y][x]or' '
def F(Y,X):
if V(Y,X)and I[Y][X]==J[Y][X]:
Z=I[Y][X]
if','>Z:
r=''.join([str(int(V(y,x)))+W(y,x)for y,x in[(Y-1,X),(Y,X+1),(Y+1,X),(Y,X-1)]])
J[Y][X]=['012┐45┌┬8┘0┤└┴├┼','012╕45╒╤8╛0╡╘╧╞╪']['='in r][int(r[0::2],2)]
else:J[Y][X]=dict(zip('|-=','│─═'))[Z]
if Z in'+-=':F(Y,X+1);F(Y,X-1)
if Z in'+|':F(Y-1,X);F(Y+1,X)
e=enumerate
F(*[(y,x)for y,r in e(I)for x,c in e(r)if'+'==c][0])
for r in J:print(''.join(r))


Try it online!