# Layout Text in Columns

Write a program or function that given some text, a number of columns, and the column width, formats the text into columns. This is plain text .

# Rules

## Input

• The text will be a string of printable ASCII and may contain newlines and spaces (but not tabs).
• The number of columns will be a positive integer.
• The column width is an integer greater or equal to 2 that specifies how many characters per column.

For this challenge, a word will consist of any non-whitespace characters. The input consists of one string and two integers, which may be taken in any order.

## Output

Output is the text formatted into balanced columns with as many words on each line as will fit.

• If a word is too long to fit in a column, put the word on the next line if there is another word on the current line and the line could be padded to the column width with three spaces or less.
• Otherwise, hyphenate the word so that it fills the rest of the line. Hyphenate wherever the line ends; don't worry about hyphenating between syllables.
• Columns should be separated by a margin of four space characters.
• The columns should be balanced so that they all have an equal number of lines if possible.
• The leftmost columns should have an extra line if necessary.
• Any line breaks and multiple spaces should be preserved. Trailing spaces are optional after the last column.

# Examples

Text:

The number of columns will be a positive integer.  Columns should be separated by a margin of four space characters.

The columns should be balanced.  The column width is greater than two.


Columns: 3, width: 10

The number    separated     ns should
of columns    by a marg-    be balanc-
will be a     in of four    ed.  The
positive      space cha-    column wi-
integer.      racters.      dth is gr-
Columns                     eater than
should be     The colum-    two.


Text:

This line is hyphenated.
This line, on the other hand, is not.


Columns: 1, width: 20

This line is hyphen-
ated.
This line, on the
other hand, is not.


Text: Tiny columns. columns: 4, width: 2

T-    c-    u-    s.
i-    o-    m-
ny    l-    n-


Text: Two spaces.<space> columns: 5, width: 2

T-          p-    c-    s.
wo    s-    a-    e-


Text: <newline>A phrase columns: 2, width: 5

         rase
A ph-


Text: A short sentence. columns: 10, width: 5

A sh-    ort      sent-    ence.


Text: It's "no word" 1234567890 -+-+-+-+ (*&!) columns: 3, width: 6

It's      12345-    +-+
"no       67890     (*&!)
word"     -+-+--


This is ; standard rules apply.

• Possible dupe
– user45941
Nov 17, 2015 at 4:07
• @Mego The challenges are related, however, this one requires hyphenating certain words and balancing columns, so I think it is different enough. Nov 17, 2015 at 4:24
• Is s:'tiny', c:4, w:2 = t- i- n- y or t- i- ny?? Nov 20, 2015 at 20:00
• @TFeld My examples were wrong, it should be t- i- ny. Is everything right now, or do I need to fix it again? Nov 20, 2015 at 21:02
• Not sure, should tiny s be t- i- n- y_ s or t- i- ny s Nov 20, 2015 at 21:05

# Python 3, 336339 335 bytes

@Jerry Jeremiah's recent activity on this question inspired me to try it myself, and see if Python 3 couldn't manage to beat the old Python 2 answer, and, after some painful golfing, I managed to get this slightly shorter:

def F(c,w,s):
r,R,i,e=[""],[],0," "
for j in s.split(e):
while 1:
if(a:=len(r[i]))+len(j)<=w:r[i]+=j.replace("\n",e*(w-a))+e;break
elif a+min(3,w)>w:r[i]+=e*(w-a+1)
else:r[i]+=j[:w-a-1]+"- ";j=j[w-a-1:]
i+=1;r+=[""]
h=-(-len(r)//c);r+=[e]*c;exec(c*"R.append(r[:h]);del r[:h];");return"\n".join(map("   ".join,zip(*R)))


Try it online!

Takes three arguments, as columns, width, text. It does require that all newlines in the input have a leading and trailing space, despite my best efforts to fix that.

Edit +3 bytes: Thanks @jezza_99 for pointing out it was erroring out, turns out my golf of assigning w-a to b didn't work, because the assignment wasn't in code that ran every loop.

Edit -4 bytes: -3 from @Sylvester Kruin spotting that I could save bytes by defining a variable for " ", and another -1 by cleaning up the line that pads r so the zip() doesn't truncate the text if some of the columns in the final row aren't filled up.

## Ungolfed code and explanation

def F(c,w,s):
r,R,i=[""],[],0
for j in s.split(" "): ## Loop through each word in the input
while 1: ## While we havent finished adding this word
if (a:=len(r[i]))+len(j)<=w: ## If theres room for the full word
r[i]+=j.replace("\n"," "*(w-a))+" " ## Add it + a trailing space, and if the string is a newline, replace it with enough spaces to finish the line
break ## Break to next word
elif a+min(3,w)>w: # If this line can be padded with spaces
r[i]+=" "*(w-a+1) ## Pad with spaces
else: ## Need to hypenate
r[i]+=j[:w-a-1]+"- " ## Add as much word as can fit plus the hypen
j=j[w-a-1:] ## Trim the part of this word we added
r+=[""] ## Initialize that line to the empty string
h=-(-len(r)//c) ## Determine column height
r+=[""]*c ## Pad the last row with empty strings so the zip() truncate it if some of the columns are incomplete
for j in range(c): ## Convert the flat list into a list containing each column
R.append(r[:h])
del r[:h]
return "\n".join( ## Join each line with a newline
map("   ".join, ## Use map() to .join the columns of each line with 3 spaces
zip(*R))) ## Zip the lists to get a list of each line

• You are getting an error for the test case Tiny columns. columns: 4, width: 2 Mar 21 at 19:50
• Oh sh*t @jezza_99, It was working at some point, time to figure out what I changed that broke it Mar 21 at 19:52
• Hate it when that happens aye. Good luck with the bug hunt Mar 21 at 19:57
• @jezza_99 luckily found it quick, unfortunately the bug was a golf I made that turned out to not work Mar 21 at 19:58
• To reduce it by one byte, you can create a variable x like this: x=" ", and use x instead of " " in the rest of the code. Unfortunately, the link is too long to post in a comment :-\. Mar 21 at 20:12

# Python 2, 346 338 bytes

i,C,W=input()
r=[]
for l in [x or' 'for x in i.split('\n')]:
while l:
if' '==l[0]:l=l[1:]
w=l[:W];x=W;s=w.rfind(' ')+1
if max(W-3,0)<s:w=w[:s];x-=W-s
elif x<len(l)and' 'not in l[x:x+1]:w=w[:-1]+'-';x-=1
r+=[w];l=l[x:]
r=[s.ljust(W)for s in r+['']*(C-1)]
print'\n'.join('    '.join(s)for s in zip(*zip(*[iter(r)]*((len(r))/C))))


Input as 'string',C,W

• Thanks, fixed by changing to rstrip(). Nov 20, 2015 at 21:48
• Great! that saves me the strip() at the end. Nov 20, 2015 at 21:55

# C++ 386

Thanks to @ceilingcat for some very nice pieces of golfing - now even shorter

#import<bits/stdc++.h>
#define b t.push_back
#define r l.substr
using namespace std;main(int n,char**a){int i=0,p,c=atoi(a[1]),w=atoi(a[2]);vector<string>t;for(string l;getline(cin,l);)for(;p=l.find_last_of(" \n",w),l=~p&&p>w-4?b(r(0,p)),r(p+1):w/l.size()?b(l),"":(b(r(0,w-1)+"-"),r(w-1)),l[0];);for(p=~-t.size()/c+1;i<p;i+=puts(""))for(n=0;n<c;b(""))cout<<left<<setw(w+4)<<t[i+n++*p];}


Try it online!