Given a list of string widths, a width of the minimum gap to place
    between them, and the maximum width of the output (such as a terminal
    width), calculate the number of columns and rows, and the width of each
    column, for the optimal layout.

def get_num_columns_and_rows(widths, gap_width, term_width):
    '''Given a list of string widths, a width of the minimum gap to place
    between them, and the maximum width of the output (such as a terminal
    width), calculate the number of columns and rows, and the width of each
    column, for the optimal layout.

    '''
    def calc_longest_width(widths, gap_width, ncols):
        longest = 0
        rows = [widths[s:s + ncols] for s in range(0, len(widths), ncols)]
        col_widths = rows[0] # Column widths start at the first row widths
        for r in rows:
            for ii, c in enumerate(r):
                if c > col_widths[ii]:
                    col_widths[ii] = c
            length = sum(col_widths) + gap_width * (ncols - 1)
            if length > longest:
                longest = length
        return longest, col_widths

    def calc_num_rows(num_items, cols):
        div, mod = divmod(num_items, cols)
        return div + (mod != 0)

    # Start with one row
    ncols = len(widths)
    # Calculate the width of the longest row as the longest set of item widths
    # ncols long and gap widths (gap_width * ncols - 1) that fits within the
    # terminal width.
    while ncols > 0:
        longest_width, col_widths = calc_longest_width(widths, gap_width, ncols)
        if longest_width < term_width:
            # This number of columns fits
            return calc_num_rows(len(widths), ncols), ncols, col_widths
        else:
            # This number of columns doesn't fit, so try one less
            ncols -= 1
    # If got here, it all has to go in one column
    return len(widths), 1, 0