Allow users to format values in a Matrix struct

I have a Matrix structure as shown below for working with two-dimensional numerical data. The underlying numeric values of the matrix are stored as a flat array.

struct Matrix<T> {

    let rows: Int
    let columns: Int
    var values: [T]

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

I extend the Matrix as shown here to print the values as a grid of numbers. If the matrix is only one row, then it is printed as a row vector; otherwise, the the max integer and fraction width (length) for each column is determined based on the values in the column. The integer and fraction lengths are determined by the NumberString protocol for Int, Float, and Double values. The integer and fraction lengths are used to pad the numbers so they are properly aligned in each column. Parentheses are added to each row for a prettier mathmatical display of the matrix.

extension Matrix: CustomStringConvertible where T: NumberString {

    var description: String {
        var desc = ""

        // Matrix is only one row so print as a row vector
        if self.rows == 1 {
            desc += "( "
            desc += self.values.map { "\($0)" }.joined(separator: "  ")
            desc += " )"
            return desc
        }

        // Max integer width and fraction width for each matrix column
        var maxIntWidth = Array(repeating: 0, count: self.columns)
        var maxFracWidth = Array(repeating: 0, count: self.columns)

        for i in 0..<self.rows {
            for j in 0..<self.columns {
                maxIntWidth[j] = max(maxIntWidth[j], self[i, j].integerLength)
                maxFracWidth[j] = max(maxFracWidth[j], self[i, j].fractionLength)
            }
        }

        // Matrix values as strings with padding
        for i in 0..<self.rows {
            switch i {
            case 0:
                desc += "⎛ "
            case self.rows - 1:
                desc += "⎝ "
            default:
                desc += "⎜ "
            }

            for j in 0..<self.columns {
                let leftPad = String(repeating: " ", count: maxIntWidth[j] - self[i, j].integerLength)
                let rightPad = String(repeating: " ", count: maxFracWidth[j] - self[i, j].fractionLength)
                let valDesc = leftPad + self[i, j].numberDescription + rightPad

                if j != self.columns - 1 {
                    desc += valDesc + "  "
                } else {
                    desc += valDesc
                }
            }

            switch i {
            case 0:
                desc += " ⎞\n"
            case self.rows - 1:
                desc += " ⎠"
            default:
                desc += " ⎟\n"
            }
        }

        return desc
    }
}
import Foundation

protocol NumberString {
    var numberDescription: String { get }
    var integerLength: Int { get }
    var fractionLength: Int { get }
}

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

    var integerLength: Int {
        String(self).count
    }

    var fractionLength: Int {
        0
    }
}

extension Float: NumberString {
    var numberDescription: String {
        self.formatted(.number.precision(.fractionLength(1...4)))
    }

    var integerLength: Int {
        self.numberDescription.split(separator: ".")[0].count
    }

    var fractionLength: Int {
        self.numberDescription.split(separator: ".")[1].count
    }
}

extension Double: NumberString {
    var numberDescription: String {
        self.formatted(.number.precision(.fractionLength(1...6)))
    }

    var integerLength: Int {
        self.numberDescription.split(separator: ".")[0].count
    }

    var fractionLength: Int {
        self.numberDescription.split(separator: ".")[1].count
    }
}

Here is an example of printing a matrix of integers:

func runExample3() {
    let array2D = [2, 1, 892,
                   4, 5, 3]

    let matrix = Matrix(rows: 2, columns: 3, values: array2D)
    print("\nExample 3")
    print(matrix)
}

runExample3()
Example 3
⎛ 2  1  892 ⎞
⎝ 4  5    3 ⎠

Here is an example of printing a matrix of doubles:

func runExample5() {
    let array2D: [Float] = [2.5, 1, 8.235,
                            0.409, 23.5, 3,
                            19, 0.02, 1,
                            201, 9, 1902.3]

    let matrix = Matrix(rows: 4, columns: 3, values: array2D)
    print("\nExample 5")
    print(matrix)
}

runExample5()
Example 5
⎛   2.5     1.0       8.235 ⎞
⎜   0.409  23.5       3.0   ⎟
⎜  19.0     0.02      1.0   ⎟
⎝ 201.0     9.0   1,902.3   ⎠

This works fine but the current method does not provide a way for the user to define the number formatting. The formatting is coded into the NumberString protocol for each value type. Ideally, I would like to provide a format style to the matrix which is then used for printing. Something like this where the values are printed using scientific notation:

let matrix = Matrix(rows: 4, columns: 3, values: array2D)
matrix.formatted(.number.notation(.scientific))

print(matrix)

Any suggestions on how this could be accomplished?

Here is a possible solution that uses the FormatStyle protocol to format the values in the matrix. This is applied with the formatted(_:) method shown below.

import Foundation

extension Matrix {

    func formatted<F: FormatStyle>(_ format: F) -> String where F.FormatInput == T, F.FormatOutput == String {
        var desc = ""

        // Matrix is only one row so print as a row vector
        if self.rows == 1 {
            desc += "( "
            desc += values.map { format.format($0) }.joined(separator: "  ")
            desc += " )"
            return desc
        }

        // Max integer width and fraction width for each matrix column
        var maxIntWidth = Array(repeating: 0, count: self.columns)
        var maxFracWidth = Array(repeating: 0, count: self.columns)

        for i in 0..<rows {
            for j in 0..<columns {
                let value = format.format(self[i, j])
                let intLength = value.split(separator: ".")[0].count
                var fracLength = 0
                if value.contains(".") { fracLength = value.split(separator: ".")[1].count }

                maxIntWidth[j] = max(maxIntWidth[j], intLength)
                maxFracWidth[j] = max(maxFracWidth[j], fracLength)
            }
        }

        for i in 0..<rows {
            switch i {
            case 0:
                desc += "⎛ "
            case rows - 1:
                desc += "⎝ "
            default:
                desc += "⎜ "
            }

            for j in 0..<columns {
                let value = format.format(self[i, j])
                let intLength = value.split(separator: ".")[0].count
                var fracLength = 0
                if value.contains(".") { fracLength = value.split(separator: ".")[1].count }

                let leftPad = String(repeating: " ", count: maxIntWidth[j] - intLength)
                let rightPad = String(repeating: " ", count: maxFracWidth[j] - fracLength)
                let valueDesc = leftPad + value + rightPad

                if j != columns - 1 {
                    desc += valueDesc + "  "
                } else {
                    desc += valueDesc
                }
            }

            switch i {
            case 0:
                desc += " ⎞\n"
            case rows - 1:
                desc += " ⎠"
            default:
                desc += " ⎟\n"
            }
        }

        return desc
    }
}

This allows the user to specify the number format using various numeric styles. Here the matrix is formatted using scientific notation and another is formatted with a fraction length from 1 to 4.

let array2D: [Float] = [2.5, 1, 8.235,
                        0.409, 23.5, 3,
                        19, 0.02, 1,
                        201, 9, 1902.3]

let matrix = Matrix(rows: 4, columns: 3, values: array2D)

let mat = matrix.formatted(.number.precision(.fractionLength(2)).notation(.scientific))
print(mat)

let matt = matrix.formatted(.number.precision(.fractionLength(1...4)))
print(matt)
⎛ 2.50E0   1.00E0   8.23E0 ⎞
⎜ 4.09E-1  2.35E1   3.00E0 ⎟
⎜ 1.90E1   2.00E-2  1.00E0 ⎟
⎝ 2.01E2   9.00E0   1.90E3 ⎠

⎛   2.5     1.0       8.235 ⎞
⎜   0.409  23.5       3.0   ⎟
⎜  19.0     0.02      1.0   ⎟
⎝ 201.0     9.0   1,902.3   ⎠

The only thing I don't like about this approach is that an extra step is needed before printing the matrix. You have to create the formatted string representation then print that string. It would be cleaner if I could do something like print(matrix, precision: 2) where I pass in the format style as part of the print statement.

Just extend String.StringInterpolation:

extension String.StringInterpolation {
    
    mutating func appendInterpolation(_ matrix: Matrix, precision: Int) {
        appendInterpolation(/* Matrix string value */)
    }
}

let matrix = Matrix(...)
print("\(matrix, precision: 2)")

Or, I don't necessarily see anything against introducing a global func print(_ matrix: Matrix, precision: Int, ...) since it doesn't conflict with stdlib print /shrug

NumPy has a numpy.set_printoptions function that determines how floating point numbers are displayed in arrays. This applies globally to all NumPy objects so you define the format once and all arrays are printed with that format. How would I accomplish this in Swift for my matrix example? Would I define a global PrintOptions struct as a singleton which would be used for all formatting operations?

You could define a MatrixFormatStyle struct that is generic over another FormatStyle for formatting individual values. With such an API, the call site could look like matrix.formatted(.matrix(.number.precision(.fractionLength(2))))

struct MatrixFormatStyle<Base: FormatStyle> where ValueFormat.FormatOutput == String {
    let style: Base
    init(_ base: Base) { self.style = style }

    func format(_ value: Matrix<Base.FormatInput>) -> String { ... }
}

extension FormatStyle {
    static func matrix<Base: FormatStyle>(_ base: Base) -> Self where Base.FormatOutput == String, Self == MatrixFormatStyle<Base> {
        MatrixFormatStyle(base)
    }
}

The code I shared above allows me to do:

matrix.formatted(.number.precision(.fractionLength(2)))

So I don't understand why I would want to do the more verbose approach that you suggest:

matrix.formatted(.matrix(.number.precision(.fractionLength(2))))

Stateful I/O formatting is one of those patterns that's great for toy examples, but almost never what you want in reality, because it makes building composable software pieces a giant pain (you have to keep specifying the stateful I/O anyway, because otherwise some other component will stomp on it and ruin your output, but you're using an API that assumes you don't have to do that, and so is not optimized to make it nice).

Generally what you'd do with a formatter pattern is create the formatter once, and then use it repeatedly, so your example actually becomes matrix.formatted(myFormat) at the point of use with either spelling.

3 Likes

Agreed, defining the format style and then using it at multiple places to print matrices (see below) is basically the same approach as having a global print option as in NumPy. Either way I am defining a format in one location then using it at other locations.

let arrayA = [2.5, 1, 8.235097,
              0.45, 23.58, 3]

let arrayB = [2.5, 1, 8.235,
              0.409, 23.5, 3,
              19, 0.02, 1,
              201, 9, 1902.3]

let matrixA = Matrix(rows: 2, columns: 3, values: arrayA)
let matrixB = Matrix(rows: 4, columns: 3, values: arrayB)

let style = FloatingPointFormatStyle<Double>().precision(.fractionLength(2))

print(matrixA.formatted(style))
print(matrixB.formatted(style))

The advantage to creating a full implementation of FormatStyle is that you can then use it in other places that accept a format style, like SwiftUI’s localized string literals. It also keeps the convention that formatted(_:) takes a format style as a parameter that uses self as its input and not some other type.

Here is what I currently have to format the underlying values in the matrix.

import Foundation

struct Matrix<T> {

    let rows: Int
    let columns: Int
    var values: [T]

    subscript(row: Int, column: Int) -> T {
        get { values[row * columns + column] }
        set { values[row * columns + column] = newValue }
    }
}
extension Matrix {

    func formatted<F: FormatStyle>(_ format: F) -> String where F.FormatInput == T, F.FormatOutput == String {
        var desc = ""

        // Matrix is only one row so print as a row vector
        if self.rows == 1 {
            desc += "( "
            desc += values.map { format.format($0) }.joined(separator: "  ")
            desc += " )"
            return desc
        }

        // Max integer width and fraction width for each matrix column
        var maxIntWidth = Array(repeating: 0, count: self.columns)
        var maxFracWidth = Array(repeating: 0, count: self.columns)

        for i in 0..<rows {
            for j in 0..<columns {
                let value = format.format(self[i, j])
                var intLength = value.count
                var fracLength = 0

                if value.contains(".") {
                    intLength = value.split(separator: ".")[0].count
                    fracLength = value.split(separator: ".")[1].count
                }

                maxIntWidth[j] = max(maxIntWidth[j], intLength)
                maxFracWidth[j] = max(maxFracWidth[j], fracLength)
            }
        }

        // Matrix values as strings with padding
        for i in 0..<rows {
            switch i {
            case 0:
                desc += "⎛ "
            case rows - 1:
                desc += "⎝ "
            default:
                desc += "⎜ "
            }

            for j in 0..<columns {
                let value = format.format(self[i, j])
                var intLength = value.count
                var fracLength = 0

                if value.contains(".") {
                    intLength = value.split(separator: ".")[0].count
                    fracLength = value.split(separator: ".")[1].count
                }

                let leftPad = String(repeating: " ", count: maxIntWidth[j] - intLength)
                let rightPad = String(repeating: " ", count: maxFracWidth[j] - fracLength)
                let valueDesc = leftPad + value + rightPad

                if j != columns - 1 {
                    desc += valueDesc + "  "
                } else {
                    desc += valueDesc
                }
            }

            switch i {
            case 0:
                desc += " ⎞\n"
            case rows - 1:
                desc += " ⎠"
            default:
                desc += " ⎟\n"
            }
        }

        return desc
    }
}

This mostly works except the formatter is ignoring the matrix type and appears to be using the literal type for the underlying values. For example, if I print the matrix shown below, which has a type of Matrix<Double>, using the string description of each value I get the results shown in the output text below. Notice the -2, 1, 18 are correctly printed as -2.0, 1.0, and 18.0 because the matrix itself has a type of double.

let arrayB = [-2, 1, 18,
              4.5,  0, -300,
              18, 901, 7.01]

let matrixB = Matrix(rows: 3, columns: 3, values: arrayB)

print(matrixB)
⎛ -2.0    1.0    18.0  ⎞
⎜  4.5    0.0  -300.0  ⎟
⎝ 18.0  901.0     7.01 ⎠ 

But if I format the values using .number and print the matrix, then I get what is shown below. You can see that the -2, 1, 18 are printed as integers instead of doubles even though the matrix itself is Matrix<Double>. How can I tell the format style to infer the type based on the matrix type and not the underlying literal value type?

print(matrixB.formatted(.number))
⎛ -2     1    18   ⎞
⎜  4.5    0  -300   ⎟
⎝ 18   901     7.01 ⎠ 

I think you can mostly get the best of two worlds by doing this:

extension Matrix {
  struct MatrixFormatStyle<F: FormatStyle> {
    let elementFormat: F

    func format(_ matrix: Matrix<T>) -> String {
      // move your implementation here
    }

    func locale(_ locale: Locale) -> Matrix<T>.MatrixFormatStyle<F> {
      .init(elementFormat: elementFormat.locale(locale))
    }
  }
}

extension Matrix {
    func formatted<F: FormatStyle>(_ format: F) -> String where F.FormatInput == T, F.FormatOutput == String {
        MatrixFormatStyle(elementFormatter: format).format(self)
    }
}

Then you get the nice short syntax, but still expresses your formatting code as its own FormatStyle so other API that's generic over elements and formatters, can work with your matrices as well.

You should probably also try to make your code agnostic on decimal separator. I don't think there is a generic way of extracting the locale (and thus locale.decimalSeparator from any given FormatStyle, but it should be possible to try this to cover most cases:

let decimalSeparator = switch format {
  case let f as FloatingPointFormatStyle<T>: 
    f.locale.decimalSeparator
  case let f as Decimal.FormatStyle: 
    f.locale.decimalSeparator
  case let f as Measurement.FormatStyle: 
    f.locale.decimalSeparator
  default:
    "."
}