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.

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
}
}

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
}
}

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.

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.

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
}
}