Print table of numbers from a generic array

Below is a matrix struct for working with 2D numerical data.

struct Matrix<T> {
    let rows: Int
    let columns: Int
    var data: Array<T>

    subscript(row: Int, column: Int) -> T {
        get { return self.data[(row * self.columns) + column] }
        set { self.data[(row * self.columns) + column] = newValue }
    }
}

extension Matrix: CustomStringConvertible {

    var description: String {
        var desc = ""
        desc += "\(self.rows)x\(self.columns) \(type(of: self))\n"
        
        for i in 0..<self.rows {
            for j in 0..<self.columns {
                desc += "\(self[i, j])  "
            }
            desc += "\n"
        }

        return desc
    }
}

Here I print out a matrix of double-precision values:

let values: [Double] = [1, 2, 3, 4,
                        5, 6, 7, 8,
                        9, 10, 11, 12]

let mat = Matrix(rows: 3, columns: 4, data: values)

print(mat)
// 3x4 Matrix<Double>
// 1.0  2.0  3.0  4.0  
// 5.0  6.0  7.0  8.0  
// 9.0  10.0  11.0  12.0  

I would like the print output to be aligned by the decimal place like the following:

3x4 Matrix<Double>
1.0   2.0   3.0   4.0  
5.0   6.0   7.0   8.0  
9.0  10.0  11.0  12.0  

How can I align the numbers in each column by the decimal?

Also, if I have a matrix of integers, I would like to align the print output as shown below. So instead of aligning by a decimal place, the numbers are left padded.

3x4 Matrix<Int>
1   2   3   4  
5   6   7   8  
9  10  11  12  

The Ordo One Benchmark package uses a fork of the TextTable repo to print results. Maybe try there for some ideas to get started?

1 Like

I tried the following which gets me closer to what I want to achieve. But there's still some left padding in the first column and large decimal numbers aren't handled well.

extension Matrix: CustomStringConvertible where T: Comparable {

    var description: String {
        let maxValue = self.data.max()!
        let width = "\(maxValue)".components(separatedBy: ".")[0].count

        var desc = ""
        desc += "\(self.rows)x\(self.columns) \(type(of: self))\n"
        
        for i in 0..<self.rows {
            for j in 0..<self.columns {
                let newWidth = "\(self[i, j])".components(separatedBy: ".")[0].count
                let pad = String(repeating: " ", count: width + 2 - newWidth)
                let d = pad + "\(self[i, j])"
                if j == 0 {
                    desc += d.dropFirst(width)
                } else {
                    desc += d
                }
            }
            desc += "\n"
        }

        return desc
    }
}
3x4 Matrix<Int>
 1   2   3   4
 5   6   7   8
 9  10  11  12

3x4 Matrix<Double>
 1.0   2.0   3.0   4.0
 5.0   6.0   7.0   8.0
 9.0  10.0  11.0  12.0

Using a protocol I'm able to format each number based on its type. But I would like to get rid of the left padding in the first column.

protocol NumberString {
    var numberDescription: String { get }
}

extension Int: NumberString {
    var numberDescription: String {
        let d = String(format: "%4d", self)
        return d
    }
}

extension Double: NumberString {
    var numberDescription: String {
        let d = String(format: "%8.2f", self)
        return d
    }
}

extension Matrix: CustomStringConvertible where T: NumberString {

    var description: String {
        var desc = ""
        desc += "\(self.rows)x\(self.columns) \(type(of: self))\n"

        for i in 0..<self.rows {
            for j in 0..<self.columns {
                let d = self[i, j].numberDescription
                desc += d
            }
            desc += "\n"
        }

        return desc
    }
}
3x4 Matrix<Int>
   1   2   3   4
   5   6   7   8
   9  10  11  12

3x4 Matrix<Double>
    1.00    2.00    3.00    4.00
    5.00    6.00    7.00    8.00
    9.00   10.00   11.00   12.00

In the numberDescription implementation of Int, the %4d string format will always include minimum spacing, so the width of the string is always at least 4 characters.

If you want custom alignment, I would suggest using %d so that numberDescription values only include the digits with no whitespace padding. (You don't even need String(format:), then, you could just do "\\(self)".)

Then before you print the columns, you can loop all values, work out the maximum space of the column, and prepend (maxColumnWidth - itemDescription.count) spaces for every other row in the column, which will give you right alignment. For the first column, to get left alignment as desired, do the same but append spaces instead of prepending.

I would like each column to be the same size except for the first column. So instead of this:

3x4 Matrix<Int>
   1   2   3   4
  50   6   7   8
   0  10  11  12

I want it to be printed as:

3x4 Matrix<Int>
 1   2   3   4
50   6   7   8
 0  10  11  12

Basically, the width of the first column should be the width of the largest number string. So in this example, for the first column I want something like String(format: "%2d", self) and for the other columns it would be String(format: "%4d", self).

Sure, but what I'm saying is you don't want to involve the internals of numberDescription at all with the spacing. numberDescription produces the formatted number, so Int(1).numberDescription yields simply "1". Add the spacing as a separate step when you make the Matrix description.

Collect all the numberDescriptions of a column in a temporary array, get their maximum width, and then you know how many whitespace characters to add before or after each item's description, to achieve the alignment you want for the column it is in.

Here's a working code snippet to illustrate what I mean:


func descriptionWithAlignedColumns(_ matrix: [[Int]]) -> String {
	let formattedMatrix = matrix.map { row in
		row.map {
			String($0) // this would be your $0.numberDescription call
		}
	}
	
	let numberOfRows = matrix.count
	let numberOfColumns = matrix[0].count
	
	func maxWidth(forColumn col: Int) -> Int {
		var width: Int = 0
		
		for i in 0..<numberOfRows {
			width = max(width, formattedMatrix[i][col].count)
		}
		
		return width
	}
	
	var formatted = ""
	
	for y in 0..<numberOfRows {
		for x in 0..<numberOfColumns {
			let maxColumnWidth = maxWidth(forColumn: x)
			
			let itemDescription = formattedMatrix[y][x]
			let itemDescriptionWidth = itemDescription.count
			
                        let whitespacePadding =  String(repeating: " ", count: maxColumnWidth - itemDescriptionWidth)
			let itemDescriptionWithPadding: String
			
			if x == 0 {
				itemDescriptionWithPadding = itemDescription + whitespacePadding
			} else {
				itemDescriptionWithPadding = whitespacePadding + itemDescription
			}
			
			formatted += itemDescriptionWithPadding
			
			formatted += "  "
		}
		
		formatted += "\n"
	}
	
	return formatted
}

let numbers = [
    [42555555, 13, 150, 2400], 
    [2, 530, 1500, 400],
    [6, 3, 100, 925],
    [8, 9, 24, 100],
]
		
let s = descriptionWithAlignedColumns(numbers)
print(s)

This will print out the table of numbers with the first column left-aligned and all subsequent columns right-aligned.

I wrote this very quickly in like two minutes, but hopefully you can see how to adapt this to your Matrix type as appropriate. Note there are also some obvious performance optimisations you might want to do, which may matter if you have a bigger table of data in reality, like caching the max width calculation for each column.

This would give:

42555555   13   150  2400  
2         530  1500   400  
6           3   100   925  
8           9    24   100  

But what I'm trying to accomplish would be like this:

42555555   13   150  2400  
       2  530  1500   400  
       6    3   100   925  
       8    9    24   100  

All the columns should be right-aligned and the width of the first column should be determined from the largest number in that column. I got close to such an effect in my example that used the Comparable protocol but there was still a small left pad on the first column.

If the second formatting is what you want, then, remove the if x == 0 branch and everything will be right aligned.

Yep, it works when I remove the x == 0 check. And if make the function generic with descriptionWithAlignedColumns<T>(_ matrix: [[T]]) then it works with Float and Double values too. Thank you very much for helping me with this.

@bzamayo Here's what I ended up using for the Matrix struct.

protocol NumberString {
    var numberDescription: String { get }
}

extension Int: NumberString {
    var numberDescription: String {
        "\(self)"
    }
}

extension Double: NumberString {
    var numberDescription: String {
        String(format: "%.2f", self)
    }
}

extension Matrix: CustomStringConvertible where T: NumberString, T: Comparable {

    var description: String {
        var desc = ""

        // Matrix size and type
        desc += "\(self.rows)x\(self.columns) \(type(of: self))\n"

        // Max column width for each matrix column
        var colVals = [T]()
        var maxColWidth = [Int]()

        for j in 0..<self.columns {
            for i in 0..<self.rows {
                colVals.append(self[i, j])
            }
            maxColWidth.append(colVals.max()!.numberDescription.count)
            colVals = []
        }
        
        // Matrix values as strings with padding
        for i in 0..<self.rows {
            for j in 0..<self.columns {
                let colWidth = maxColWidth[j]
                let valWidth = self[i, j].numberDescription.count
                let pad = String(repeating: " ", count: colWidth - valWidth)
                let valDesc = pad + self[i, j].numberDescription + "  "
                desc += valDesc
            }
            desc += "\n"
        }

        return desc
    }
}
1 Like

Good work!

To finish it off, pretty print more than one vector side by side. :slight_smile:

[42555555   13   150  2400] [0 0 0] [ 13   150  2400]
[       2  530  1500   400] [1 0 0] [530  1500   400]
[       6    3   100   925] [0 1 0] [  3   100   925] 
[       8    9    24   100] [0 0 1] [  9    24   100]