Gold Challenge - trying to expand the Column Header widths

Is there a way to go back and reset column widths for each column so that everyone lines up? I thought my if statement alone could do the trick but I’m not getting the right output:

// func printTable(_ data: [[String]], withColumnLabels columnLabels: [String]) {
func printTable(_ dataSource: TabularDataSource & CustomStringConvertible) { // listing 19.18 - making printTable's argument conform to CustomStringConvertible
    
    print("Table: \(dataSource)")
    
    // create header row containing column headers
    var headerRow = "|"
    
    // also, keep track of column width
    var columnWidths = [Int]()

    
    // for columnLabel in columnLabels {
    for i in 0 ..< dataSource.numberOfColumns {
        let columnLabel = dataSource.label(forColumn: i)
        let columnHeader = " \(columnLabel) |"
        headerRow += columnHeader
        columnWidths.append(columnLabel.count)

        
    }

    print(headerRow)
    

    for i in 0 ..< dataSource.numberOfRows {
        // start the output string
        var out = "|"

        for j in 0 ..< dataSource.numberOfColumns {
            let item = dataSource.itemFor(row: i, column: j)

            // Gold challenge - thought this would be enough, but it's not adjusting my column headers
            if item.count > columnWidths[j] {
                columnWidths[j] = item.count
            }
        
            
            let paddingNeeded = columnWidths[j] - item.count // I took out the - item.count
            let padding = repeatElement(" ", count: paddingNeeded).joined(separator: "")
            out += " \(padding)\(item) |"
        }
        
        // done -- print it!        
        print(out)

    }
    
}

At one point, I also tried creating a separate array for headerWidths:

var headerWidths = [Int]()

But as soon as I tried it, my Playground gave me an Index Out of Range error after it printed out my header columns. Why did that happen?

You’re going to have to rework the code so that it doesn’t print anything until you’re done figuring out the column widths. As the code stands now, you’re printing each line as soon as it’s processed; when you find a value that’s too large to fit the current column width you can’t make the column wider for the entire table because you’ve already printed the lines above it.

There’s a couple ways you can do that. One is to make two passes through the data; the first pass you just figure out the column widths and then the second pass you print everything pretty much as in the original code but using the column widths from the first pass.

The second way would be to make a single pass through the data, saving the output to an array of strings that you can add extra padding to whenever you discover you need to make a column wider, then when you reach the end of the data you can print all the strings.

The first way is probably better. It’s easier to code, and in the worst case where each line in the table requires expanding the column width I think it would wind up being faster as well.

1 Like

I got this to work:

func printTable(_ dataSource: TabularDataSource & CustomStringConvertible) { 
    
    print("Table: \(dataSource)")
    
    // create header row containing column headers
    var headerRow = "|"
    
    // also, keep track of column width
    var columnWidths = [Int]()
    
    // for columnLabel in columnLabels {
    for i in 0 ..< dataSource.numberOfColumns {
        let columnLabel = dataSource.label(forColumn: i)
        columnWidths.append(columnLabel.count) // gold challenge: add the header's column widths to the array first
    }
    
    
    for i in 0 ..< dataSource.numberOfRows {
        for j in 0 ..< dataSource.numberOfColumns {
            let item = dataSource.itemFor(row: i, column: j)
            
            if item.count > columnWidths[j] {
                columnWidths[j] = item.count
            } // gold challenge...then re-size those column widths
        }
        
    
    }
    
    for i in 0 ..< dataSource.numberOfColumns {
        let columnLabel = dataSource.label(forColumn: i)
        
        let headerPaddingNeeded = columnWidths[i] - columnLabel.count
        let headerPadding = repeatElement(" ", count: headerPaddingNeeded).joined(separator: "")
        let columnHeader = " \(headerPadding)\(columnLabel) |"
        headerRow += columnHeader
        columnWidths.append(columnLabel.count)
        // gold challenge: added padding to the header rows based on content in later rows
        
        
    }
    
    print(headerRow)
    
    // for row in data {
    for i in 0 ..< dataSource.numberOfRows {
        // start the output string
        var out = "|"
        // append each item in this row to the string
        // for (j, item) in row.enumerated() {
        for j in 0 ..< dataSource.numberOfColumns {
            let item = dataSource.itemFor(row: i, column: j)

            if item.count > columnWidths[j] {
                columnWidths[j] = item.count
            }

            let paddingNeeded = columnWidths[j] - item.count
            let padding = repeatElement(" ", count: paddingNeeded).joined(separator: "")
            out += " \(padding)\(item) |"
        }
        
        // done -- print it!
        print(out)
    }
    
}

Looks similar to what I came up with. One final comment, you could get rid of this line in the loop that prints out the header row:

It doesn’t hurt anything, but it’s adding extra data to the columnWidths array that you don’t need.

FWIW, here’s my code:

// Add padding to a string to make it equal to a specified length,
// add a space before & after the string, and add a column separator at the end
func formatString (_ input: String, length: Int) -> String {
    precondition(length >= input.count, "\"\(input)\" is longer than \(length) characters")
    let paddingNeeded = length - input.count
    let padding = repeatElement(" ", count: paddingNeeded).joined(separator: "")
    return " \(padding)\(input) |"
}

func printTable(_ dataSource: TabularDataSource & CustomStringConvertible) {
    // figure out how wide each column needs to be to accomodate all values in that column
    var columnWidths = [Int]()
    for column in 0 ..< dataSource.numberOfColumns {
        var columnWidth = dataSource.label(forColumn: column).count  // initialize column width to header width
        for row in 0 ..< dataSource.numberOfRows {
            let itemWidth = dataSource.itemFor(row: row, column: column).count
            columnWidth = max(columnWidth, itemWidth)  // make the column wider if needed
        }
        columnWidths.append(columnWidth)
    }

    // print the header row
    var out = "|"
    for column in 0 ..< dataSource.numberOfColumns {
        out += formatString(dataSource.label(forColumn: column),
                            length: columnWidths[column])
    }
    print(out)

    // print the table one row at a time
    for row in 0 ..< dataSource.numberOfRows {
        out = "|"  // Reset the output string
        for column in 0 ..< dataSource.numberOfColumns {
            out += formatString(dataSource.itemFor(row: row, column: column),
                                length: columnWidths[column])
        }
        print(out)
    }
}

1 Like