Silver & Gold Challenge: Ch 19

No set up for this, let’s just dive into the challenge!

Silver Challenge

The key for this challenge is computing the widths of the columns before they are used to determine padding in the columns themselves. To accomplish this, I created a separate function to compute the widths and return the [Int] array needed to determine padding within the printing loops. There is probably a more concise way to do this, but I had neither the time nor the patience to do it. essentially all it does is go through the labels, compute the widths, and then go through the rows, and override the widths if any of the items within the row are larger than the width provided by the label.

func computeWidths(for dataSource: TabularDataSource) -> [Int] {
	var columnWidths = [Int]()

	for i in 0 ..< dataSource.numberOfColumns {
		let columnLabel = dataSource.label(forColumn: i)
		columnWidths.append(columnLabel.characters.count)
	}
	
	for i in 0 ..< dataSource.numberOfRows {
		for j in 0 ..< dataSource.numberOfColumns {
			let item = dataSource.itemFor(row: i, column: j)
			if columnWidths[j] < item.characters.count {
				columnWidths[j] = item.characters.count
			}
		}
	}
	
	return columnWidths
}

Now all you have to do is assign the variable in the printTable(_:) function, and use that value to assign padding within the labels AND the items within the rows. There is a bit of refactoring here, but it’s very similar to the code done in the chapter.

func printTable(_ dataSource: TabularDataSource & CustomStringConvertible) {
	print("Table: \(dataSource.description)")
	var firstRow = "|"
	var columnWidths = computeWidths(for: dataSource)
	
	// Heading Labels
	for i in 0 ..< dataSource.numberOfColumns {
		let columnLabel = dataSource.label(forColumn: i)
		let paddingNeeded = columnWidths[i] - columnLabel.characters.count
		let padding = repeatElement(" ", count: paddingNeeded).joined()
		let columnHeader = " \(padding)\(columnLabel) |"
		firstRow += columnHeader
	}
	print(firstRow)
	
	// Row data
	for i in 0 ..< dataSource.numberOfRows {
		// Start the output string
		var out = "|"
		
		// Append each item in this row to the string
		for j in 0 ..< dataSource.numberOfColumns {
			let item = dataSource.itemFor(row: i, column: j)
			var paddingNeeded = columnWidths[j] - item.characters.count
			if paddingNeeded < 0 { paddingNeeded = 0 }
			let padding = repeatElement(" ", count: paddingNeeded).joined(separator: "")
			out += " \(padding)\(item) |"
		}
		
		// Done - print the string
		print(out)
	}
}

Gold Challenge

This challenge was actually easier to complete than the Silver challenge, IMO. The BookCollection I modeled almost identically to the Department struct, implementing the same protocols with just minor property details that were changed. Although the book said that it needed to conform to TabularDataSource, it also needed to conform to CustomStringConvertible in order to be printed (since the printTable(_:) function conforms to both). I also created a Book struct, similar to the Person struct, to hold the same kind of information.

struct Book {
    let title: String
    let author: String
    let averageRating: Double
}

struct BookCollection: TabularDataSource, CustomStringConvertible {
    let name: String
    var books = [Book]()
    var numberOfRows: Int { return books.count }
    var numberOfColumns: Int { return 3 }
    var description: String { return "Book Collection (\(name))" }

    init(name: String) {
        self.name = name
    }

    mutating func add(_ book: Book) {
        books.append(book)
    }

    func label(forColumn column: Int) -> String {
        switch column {
            case 0: return "Title"
            case 1: return "Author"
            case 2: return "Average Rating"
            default: fatalError("Invalid Column!")
        }
    }

    func itemFor(row: Int, column: Int) -> String {
        let book = books[row]
        switch column {
            case 0: return book.title
            case 1: return book.author
            case 2: return String(book.averageRating)
            default: fatalError("Invalid column!")
        }
    }
}

Testing:

var bookCollection = BookCollection(name: "Harry Potter Series")
bookCollection.add(Book(title: "Harry Potter and the Sorcerer's Stone", author: "J. K. Rowling", averageRating: 4.5))
bookCollection.add(Book(title: "Harry Potter and the Chamber of Secrets", author: "J. K. Rowling", averageRating: 4.2))
bookCollection.add(Book(title: "Harry Potter and the Prisoner of Azkaban", author: "J. K. Rowling", averageRating: 4.7))
bookCollection.add(Book(title: "Harry Potter and the Goblet of Fire", author: "J. K. Rowling", averageRating: 5.0))
bookCollection.add(Book(title: "Harry Potter and the Order of the Pheonix", author: "J. K. Rowling", averageRating: 3.5))
bookCollection.add(Book(title: "Harry Potter and the Half Blood Prince", author: "J. K. Rowling", averageRating: 4.75))
bookCollection.add(Book(title: "Harry Potter and the Deathly Hollows", author: "J. K. Rowling", averageRating: 4.95))
printTable(bookCollection)

Results (ignore the syntax-highlighting):

Table: Book Collection (Harry Potter Series)
| Title                                     | Author        | Average Rating |
| Harry Potter and the Sorcerers  Stone     | J. K. Rowling | 4.5            |
| Harry Potter and the Chamber of Secrets   | J. K. Rowling | 4.5            |
| Harry Potter and the Prisoner of Azkaban  | J. K. Rowling | 4.5            |
| Harry Potter and the Goblet of Fire       | J. K. Rowling | 4.5            |
| Harry Potter and the Order of the Pheonix | J. K. Rowling | 4.5            |
| Harry Potter and the Half Blood Prince    | J. K. Rowling | 4.5            |
| Harry Potter and the Deathly Hollows      | J. K. Rowling | 4.5            |

Feel free to share your own solutions!

For the silver challenge, I didn’t create a separate function, but accomplished the same goal within the printTable function:

func printTable(_ dataSource: TabularDataSource & CustomStringConvertible) {
  
  print("Table: \(dataSource.description)")
  
  // Create first row containing column headers
  var firstRow = "| "
  // Also keep track of the widht of each column
  var columnWidths = [Int]()
  var sizeOfItem: Int = 0
  var sizeOfOtherItem: Int = 0
  
  for i in 0 ..< dataSource.numberOfColumns {
    let columnLabel = dataSource.label(forColumn: i)
    sizeOfItem = columnLabel.characters.count
    
    for j in 0 ..< dataSource.numberOfRows {
      let item = dataSource.itemFor(row: j, column: i)
      sizeOfOtherItem = item.characters.count
      sizeOfItem = sizeOfItem > sizeOfOtherItem ? sizeOfItem : sizeOfOtherItem
    }
    columnWidths.append(sizeOfItem)
    let paddingNeeded = columnWidths[i] - columnLabel.characters.count
    let padding = repeatElement(" ", count: paddingNeeded).joined(separator: "")
    let columnHeader = " \(padding)\(columnLabel) |"
    firstRow += columnHeader
  }
  print(firstRow)
  
  for i in 0 ..< dataSource.numberOfRows {
    // Start the outpout string
    var out = "| "
  
    // Append each item in this row to the string
    for j in 0 ..< dataSource.numberOfColumns {
      let item = dataSource.itemFor(row: i, column: j)
      let paddingNeeded = columnWidths[j] - item.characters.count
      let padding = repeatElement(" ", count: paddingNeeded).joined(separator: "")
      out += " \(padding)\(item) |"
    }
  
    // Done! Print it.
    print(out)

This was a challenge for me for some reason…but it seems to work. I think it could be way more efficient, though…maybe I’ll work on that next.

The gold challenge was easier for me as well - my solution is practically exactly the same as manintacos. Thanks for posting.

In the post from @macintacos for the Silver Challenge, the code appears to right justify the data items with leading spaces, the same as in the book. But in the test output for the Gold Challenge, the data is left justified with trailing spaces!

To get the Gold Challenge output provided, I believe two lines of code from the Silver Challenge need to be changed.

fund printTable(...
...
        let columnHeader = " \(padding)\(columnLabel) |"
...
            out += " \(padding)\(item) |"
...

becomes

...
func printTable...
...
        let columnHeader = " \(columnLabel)\(padding) |"
...
            out += " \(item)\(padding) |"
...

Then I believe the code will produce the output provided in the Gold Challenge test cases. (Unless of course I am missing something.)

But @macintacos, I like your coding style and and I appreciate your posts, keep up the good work!

Well done!

If you want a more concise version of computeWidths, reverse the nesting of the two for loops so that columns is the outer loop. Then you can merge the first & second loops together:

func computeWidths(for dataSource: TabularDataSource) -> [Int] {
    var columnWidths = [Int]()

    for j in 0 ..< dataSource.numberOfColumns {
	    let columnLabel = dataSource.label(forColumn: j)
	    columnWidths.append(columnLabel.characters.count)
        for i in 0 ..< dataSource.numberOfRows {
	    	let item = dataSource.itemFor(row: i, column: j)
		    if columnWidths[j] < item.characters.count {
		    	columnWidths[j] = item.characters.count
		    }
	    }
    }

	return columnWidths
}

Silver challenge. I’m embarrassed to say how long this took. I tried to just write out the code inline, but the number of curly braces needed was driving me crazy. So, like many others, I created a function. Mine creates an array of the largest items in each column. It then checks that number against the label length to see the longest label or item in the column. The largest value is written to the array and returned:

func wideColumns(forDataSource data: TabularDataSource) -> [Int] {
    // create an array to hold the largest sizes to return
    var greatestWidths = [Int]()
    
    //find the largest Person array items in each row and write the largest to the array
    for i in 0 ..< data.numberOfColumns {
        var tempHolder = [Int]()
        for j in 0 ..< data.numberOfRows {
            let item = data.itemFor(row: j, column: i)
            let itemSize = item.characters.count
            tempHolder.append(itemSize)
            }
//  I found this array function to just find the largest item in the column.
        if let biggest = tempHolder.max() {
        greatestWidths.append(biggest)
            }
        }
    // Now check the largest items against the largest labels
    // and write the largest label or Person item to the array.
    for i in 0 ..< data.numberOfColumns {
        var columnLabel = data.label(forColumn: i)
        let columnLabelWidth = columnLabel.characters.count
        for _ in 0 ..< data.numberOfRows {
            if columnLabelWidth > greatestWidths[i] {
                greatestWidths[i] = columnLabelWidth
            }
        }
    }
    return greatestWidths
}

Once that was removed from the main printTable function, the remainder was pretty easy to write.

func printTable(_ dataSource: TabularDataSource & CustomStringConvertible) { // 1 open
    print("Table: \(dataSource.description)")
    
    //Create first row containing column headers
    //Here, we just print the initial bar on the left side
    var firstRow = "|"
    
    //Get the size of the largest items in each row
    let largestItems = dataSource.wideColumns(forDataSource: dataSource)

    //Also keep track of the width of each column
    var columnWidths = [Int]()
    
    //Build the column headings row and count the width
    for i in 0 ..< dataSource.numberOfColumns {
        var columnLabel = dataSource.label(forColumn: i)
        var space = 0
        var columnHeader = " \(columnLabel) |"
        let columnLabelWidth = columnLabel.characters.count
        if columnLabelWidth < largestItems[i] {
                space = largestItems[i] - columnLabelWidth
            }
        let padding = repeatElement(" ", count: space).joined(separator: "")
        columnHeader = "\(padding)\(columnHeader)"
        firstRow += columnHeader
 //I found the following line the best way for me to count and then store the width of the column heading text
            let counter = columnHeader.characters.count - 3
            columnWidths.append(counter)
        }
        print(firstRow)
    
    //Build the properly spaced rows of data and print
    for i in 0 ..< dataSource.numberOfRows {
        //Start the output string
        var out = "|"
        
        //Append each item in this row to the string
        for j in 0 ..< dataSource.numberOfColumns {
            let item = dataSource.itemFor(row: i, column: j)
            let paddingNeeded = columnWidths[j] - item.characters.count
            let padding = repeatElement(" ", count: paddingNeeded).joined(separator: "")
            out += " \(padding)\(item) |"
        }
       print(out)
    }
}

I realize that I write more verbose than most, but it’s the only way I can keep up with all that is going on in the code.

Gold was easy after doing the hard version of the Silver Challenge. The print function just adapts to the new Book Collection structure easily.

Figured I’d post what I got here, as this challenge took me some considerable time and I was very proud to finish it. I had a really hard time translating what I wanted to do in my head into usable code, but I’m pretty happy with the result. Although, I’m sure there’s a more concise way to do it. With that said, I’d love some suggestions and feedback.

Within the printTable function, I created two arrays of the largest column counts in both sections (title and body), then compared the two to form an array containing the largest widths for the table as a whole. I then used that to set column widths and determine padding needed for each item. The gold challenge seemed pretty easy after getting through the silver.

SILVER:

func printTable(_ dataSource: TabularDataSource & CustomStringConvertible) {
print("Table: \(dataSource.description)")

// Create an array containing one value for each column, and record the count of the longest table body value in that column
var bodyColumnWidths = [Int](repeatElement(0, count: dataSource.numberOfColumns))
for i in 0 ..< dataSource.numberOfRows {
    for j in 0 ..< dataSource.numberOfColumns {
        let columnListingForRow = dataSource.itemFor(row: i, column: j)
        if columnListingForRow.count > bodyColumnWidths[j] {
            bodyColumnWidths[j] = columnListingForRow.count
        }
    }
}

// Create an array containing one value for each title, and record the count of each
var tableColumnWidths = [Int](repeatElement(0, count: dataSource.numberOfColumns))
for i in 0 ..< dataSource.numberOfColumns {
    let columnListingForTitleRow = dataSource.label(forColumn: i)
    if columnListingForTitleRow.count > tableColumnWidths[i] { // This is probably unnecessary for the title row
        tableColumnWidths[i] = columnListingForTitleRow.count
    }
}

// Create a final array of column widths, to be used throughout the table
var finalColumnWidths = [Int](repeatElement(0, count: dataSource.numberOfColumns))
for (index, value) in tableColumnWidths.enumerated() {
    if value > bodyColumnWidths[index] {
        finalColumnWidths[index] = value
    } else {
        finalColumnWidths[index] = bodyColumnWidths[index]
    }
}

// Create a variable to determine how much padding, if any is needed for each table element
var additionalColumnPaddingNeeded = 0

// Create first row containing column headers
var firstRow = "|"

for i in 0 ..< dataSource.numberOfColumns {
    let columnLabel = dataSource.label(forColumn: i)
    additionalColumnPaddingNeeded = (finalColumnWidths[i] - columnLabel.count)
    let additionalPadding = repeatElement(" ", count: additionalColumnPaddingNeeded).joined(separator: "")
    let columnHeader = " \(additionalPadding)\(columnLabel) |"
    firstRow += columnHeader
}
print(firstRow)

// Create the body portion of the table
for i in 0 ..< dataSource.numberOfRows {
    // Start the output string
    var out = "|"
    // Append each item in this row to the string
    for j in 0 ..< dataSource.numberOfColumns {
        let item = dataSource.itemFor(row: i, column: j)
        additionalColumnPaddingNeeded = (finalColumnWidths[j] - item.count)
        let additionalPadding = repeatElement(" ", count: additionalColumnPaddingNeeded).joined(separator: "")
        let bodyOutput = " \(additionalPadding)\(item) |"
        out += bodyOutput
    }

    // Done - print it!
    print(out)
}

}

GOLD:

struct Book {
    let title: String
    let author: String
    let avgAmazonRating: Double
    let isbn: String
}

struct BookCollection: TabularDataSource, CustomStringConvertible {
    let title: String
    var books = [Book]()
    
    var description: String {
        return "\(title)"
    }
    
    init (title: String) {
        self.title = title
    }
    
    mutating func add(_ book: Book) {
        books.append(book)
    }
    
    var numberOfRows: Int {
        return books.count
    }
    
    var numberOfColumns: Int {
        return 4
    }
    
    func label(forColumn column: Int) -> String {
        switch column {
        case 0: return "Book Title"
        case 1: return "Author(s)"
        case 2: return "Average Rating"
        case 3: return "ISBN"
        default: fatalError("Invalid column!")
        }
    }
    
    func itemFor(row: Int, column: Int) -> String {
        let book = books[row]
        switch column {
        case 0: return book.title
        case 1: return book.author
        case 2: return String(book.avgAmazonRating)
        case 3: return book.isbn
        default: fatalError("Invalid column!")
        }
    }
}




var department = Department(name: "Engineering")
department.add(Person(name: "Joe", age: 30, yearsOfExperience: 6))
department.add(Person(name: "Karen", age: 40, yearsOfExperience: 18))
department.add(Person(name: "Fred", age: 50, yearsOfExperience: 20))
department.add(Person(name: "Testy Mc Testerson", age: 4057, yearsOfExperience: 50001))

printTable(department)
print("")
print("")



var bookCollection = BookCollection(title: "My Favorite Books")
bookCollection.add(Book(title: "Swift Programming: The Big Nerd Ranch Guide", author: "Matthew Mathias and John Gallagher", avgAmazonRating: 4.5, isbn: "978-0134610610"))
bookCollection.add(Book(title: "iOS Programming: The Big Nerd Ranch Guide", author: "Christian Keur", avgAmazonRating: 4.5, isbn: "978-0134682334"))
bookCollection.add(Book(title: "what if? Serious Scientific Answers to Absure Hypothetical Questions", author: "Randall Munroe", avgAmazonRating: 4.5, isbn: "978-0544272996"))

printTable(bookCollection)
print("")
print("")

SILVER Challenge.

I took a slightly different approach. Instead of building the function that calculates column width directly in the PrintTable function, I’ve added a new function to the TablularDataSource protocol (maxColumnWidth) and implemented the function in the Department structure.

Amended Protocol
protocol TabularDataSource {

    func maxWidth(forColumn column: Int) -> Int
}

maxWidth Implementation in Department.
I acknowledge the code is redundant, could be refactored if I knew a way to inspect the Person structure and iterate over each attribute.

struct Department: TabularDataSource {
    ...
    
    func maxWidth(forColumn column:Int) -> Int {
        var columnContent=[Int]()
        switch column {
        case 0:
            for person in people {
                columnContent.append(person.name.count)
            }
            columnContent.append(label(forColumn: 0).count)
            if let maxSize = columnContent.max(by: {$1>$0}) {
                return maxSize
            } else {return 0}
        case 1:
            for person in people {
                columnContent.append(String(person.age).count)
            }
            columnContent.append(label(forColumn: 1).count)
            if let maxSize = columnContent.max(by: {$1>$0}) {
                return maxSize
            } else {return 0}
        case 2:
            for person in people {
                columnContent.append(String(person.yearsOfExperience).count)
            }
            columnContent.append(label(forColumn: 2).count)
            if let maxSize = columnContent.max(by: {$1>$0}) {
                return maxSize
            } else {return 0}
        default: fatalError("Invalid Column")
        }
        return 0
    }
}

Amended PrintTable()
The code for the PrintTable on the other hand is very straightforward. firstRow =+... and out =+... could be further refactored.

func printTable(_ dataSource: TabularDataSource) {
    var firstRow = "|"
    for i in 0..<dataSource.numberOfColumns {
        let columnLabel = dataSource.label(forColumn: i)
        firstRow += String.init(repeating: " ", count: max(0, dataSource.maxWidth(forColumn: i) - columnLabel.count)) + "\(columnLabel)|"
    }
    print(firstRow)
    for i in 0..<dataSource.numberOfRows {
        var out = "|"
        for j in 0..<dataSource.numberOfColumns {
            let cell = dataSource.itemFor(row: i, column: j)
            out += String.init(repeating: " ",  count: max(0, dataSource.maxWidth(forColumn: j) - cell.count)) + "\(cell)|"
        }
        print(out)
    }
}

I think this approach is more logical: the presentation code (PrintTable) is not responsible for calculating the size of the columns, just as it is not responsible for calculating the number of columns or rows. Any thoughts?

My solution to the silver challenge:

protocol TabularDataSource {
var numberOfRows: Int { get }
var numberOfColumns: Int { get }

func label(forColumn column: Int) -> String
func itemFor(row: Int, column: Int) -> String
func itemsFor(column: Int) -> [String]

}

func printTable(_ dataSource: TabularDataSource & CustomStringConvertible) {
    print("Table: \(dataSource.description)")
    var firstRow = "|"
    var columnsWidths = [Int]()
    for i in 0 ..< dataSource.numberOfColumns {
        var headerPadding = ""
        let columnLabel = dataSource.label(forColumn: i)
        let items = dataSource.itemsFor(column: i)
        if let longestItem = items.max(by: {$1.count > $0.count}) {
            if longestItem.count > columnLabel.count {
                headerPadding = repeatElement(" ", count: longestItem.count - columnLabel.count).joined(separator: "")
            }
        }
        let columnHeader = " \(headerPadding)\(columnLabel) |"
        firstRow += columnHeader
        columnsWidths.append((columnLabel.count + headerPadding.count))
    }
    print(firstRow)

    for i in 0 ..< dataSource.numberOfRows {
        var out = "|"
        for j in 0 ..< dataSource.numberOfColumns {
            let item = dataSource.itemFor(row: i, column: j)
            let paddingNeeded = columnsWidths[j] - item.count
            let padding = repeatElement(" ", count: paddingNeeded).joined(separator: "")
            out += " \(padding)\(item) |"
        }
        print(out)
    }
}

struct Person {
    let name: String
    let age: Int
    let yearsOfExperience: Int
}

struct Department: TabularDataSource, CustomStringConvertible {
    let name: String
    var people = [Person]()

    init(name: String) {
        self.name = name
    }

    mutating func add(_ person:Person) {
        people.append(person)
    }

    var numberOfRows: Int {
    
        return people.count
    }

    var numberOfColumns: Int {
    
        return 3
    }

    func label(forColumn column: Int) -> String {
        switch column {
        case 0:
            return "Employee Name"
        case 1:
            return "Age"
        case 2:
            return "Years of Experience"
        default:
            fatalError("Invalid column!")
        }
    }

    func itemFor(row: Int, column: Int) -> String {
        let person = people[row]
        switch column {
        case 0:
            return person.name
        case 1:
            return String(person.age)
        case 2:
            return String(person.yearsOfExperience)
        default:
            fatalError("Invalid column!")
        }
    }

   func itemsFor(column: Int) -> [String] {
        var items = [String]()
        for person in people {
            switch column {
            case 0:
                items.append(person.name)
            case 1:
                items.append(String(person.age))
            case 2:
                items.append(String(person.yearsOfExperience))
            default:
                fatalError("Invalid column")
            }
        }
    
        return items;
   }

    var description: String {
    
        return "Department (\(name))"
    }

}

var dept = Department(name: "Engeenering")
dept.add(Person(name: "Joe", age: 3000, yearsOfExperience: 6))
dept.add(Person(name: "Karen", age: 40, yearsOfExperience: 18))
dept.add(Person(name: "Fred", age: 50, yearsOfExperience: 20))

printTable(dept)

My solution to the gold challenge:

struct Book {
    let title: String
    let author: String
    let ratings: [Int]

    var averageRating: Double? {
        get{
            if ratings.count > 0 {
        
                return (Double(ratings.reduce(0, +)) / Double(ratings.count))
            } else {
        
                return nil
            }
        }
    }
}

struct BookCollection:TabularDataSource, CustomStringConvertible {
    let name: String
    var books = [Book]()

    init(name:String) {
        self.name = name
    }

    mutating func add(_ book:Book) {
        books.append(book)
    }

    var numberOfRows: Int {
    
        return books.count
    }

    var numberOfColumns: Int {
        
        return 3
    }

    func label(forColumn column: Int) -> String {
        switch column {
        case 0:
        
            return "Book Title"
        case 1:
        
            return "Author"
        case 2:
        
            return "Average rating"
        default:
            fatalError("Invalid column")
        }
    }

    func itemFor(row: Int, column: Int) -> String {
        let book = books[row]
        switch column {
        case 0:
        
            return book.title
        case 1:
        
            return book.author
        case 2:
            if let averageRating = book.averageRating {
            
                return String(averageRating)
            } else {
            
                return "No ratings"
            }
        default:
        
            fatalError("Invalid column")
        }
    }
    func itemsFor(column: Int) -> [String] {
       var items = [String]()
        for book in books {
            switch column {
            case 0:
                items.append(book.title)
            case 1:
                items.append(book.author)
            case 2:
                if let averageRating = book.averageRating {
                    items.append(String(averageRating))
                } else {
                    items.append("No ratings")
                }
            default:
                fatalError("Invalid column")
             }
        }
    
        return items
    }
    var description: String {
    
        return "Bookstore: \(name)"
    }
}

I didn’t understand it very well, I felt very depressed, though I spent a lot of time