# Print an N-dimensional array

I can print a 2D array by providing the shape of a flat array as shown below:

``````let numbers: [Float] = [1, 2, 3, 4, 5, 6]
let shape = [2, 3]

var result = ""

for i in 0..<shape[0] {
for j in 0..<shape[1] {
let idx = flatIndex(indices: [i, j], shape: shape)
result += "\(numbers[idx])  "
}
result += "\n"
}

print(result)
``````
``````1.0  2.0  3.0
4.0  5.0  6.0
``````

The flat index is calculated with this function:

``````func flatIndex(indices: [Int], shape: [Int]) -> Int {
var index = 0
var stride = 1

for i in (0..<shape.count).reversed() {
index += indices[i] * stride
stride *= shape[i]
}

return index
}
``````

Similarly, I can print a 3D array as shown here:

``````let numbers: [Float] = [1, 2, 3,
4, 5, 6,

7, 8, 9,
10, 11, 12,

13, 14, 15,
16, 17, 18]

let shape = [3, 2, 3]

var result = ""

for k in 0..<shape[0] {
for i in 0..<shape[1] {
for j in 0..<shape[2] {
let idx = flatIndex(indices: [k, i, j], shape: shape)
result += "\(numbers[idx])  "
}
result += "\n"
}
result += "\n"
}

print(result)
``````
``````1.0  2.0  3.0
4.0  5.0  6.0

7.0  8.0  9.0
10.0  11.0  12.0

13.0  14.0  15.0
16.0  17.0  18.0
``````

How can I generalize this to print any number of dimensions (N-dimensions)?

MATLAB's approach is to only print the first two dimensions as matrices and repeat for every extra-dimension index:

``````>> rand(3, 2, 2, 3)

ans(:,:,1,1) =

0.8143    0.3500
0.2435    0.1966
0.9293    0.2511

ans(:,:,2,1) =

0.6160    0.8308
0.4733    0.5853
0.3517    0.5497

ans(:,:,1,2) =

0.9172    0.7537
0.2858    0.3804
0.7572    0.5678

ans(:,:,2,2) =

0.0759    0.7792
0.0540    0.9340
0.5308    0.1299

ans(:,:,1,3) =

0.5688    0.3371
0.4694    0.1622
0.0119    0.7943

ans(:,:,2,3) =

0.3112    0.6020
0.5285    0.2630
0.1656    0.6541
``````

You can recursively collect shape iterators with function like

``````func print(
iteration: Int,
shapeIterators: [Int],
in array: [T],
of shape: [Int],
result: inout String
) {
for k in shape[iteration] {
if iteration != shape.count - 1 {
print(
iteration: iteration + 1,
shapeIterators: shapeIterators + [k],
in: array,
of: shape,
result: &result
)
} else { /* ... */ }
}
}
``````

Not pretty, but I hope idea is clear. I suppose recursive version should be good enough, if you don't expect large shapes, otherwise I'd modified it into a loop-based.

Divide and conquer: Think of an N-dimensional array as an array (N-1) dimensional arrays.

From one of my toy languages written in Swift.

``````v3D := int [2][3][5] = {
{{1, 2, 3, 0, 1},
{4, 5, 6, 1, 2},
{7, 8, 9, 3, 4}},
{{0, 1, 2, 1, 2},
{3, 4, 5, 2, 3},
{6, 7, 8, 0, 1}}
}

\$T << "3D =" << v3D

v4D := int [3][2][3][5] = {
{{{1, 0, 3, 0, 1},
{4, 5, 6, 1, 2},
{7, 0, 0, 3, 4}},
{{0, 1, 2, 1, 2},
{3, 4, 5, 2, 3},
{6, 7, 8, 0, 1}}},

{{{1, 2, 3, 0, 1},
{0, 5, 0, 1, 2},
{7, 8, 7, 0, 4}},
{{0, 0, 2, 1, 2},
{3, 4, 0, 2, 3},
{6, 7, 8, 0, 1}}},

{{{0, 2, 3, 0, 1},
{4, 0, 0, 0, 2},
{7, 8, 9, 3, 4}},
{{0, 0, 2, 0, 2},
{3, 4, 3, 2, 3},
{6, 0, 2, 0, 1}}}
}

\$T << "4D = " << v4D

\$T << "4D [0] = " << v4D [0]
\$T << "4D [0][0] = " << v4D [0][0]
\$T << "4D [0][0][0] = " << v4D [0][0][0]
\$T << "4D [0][0][0][0] = " << v4D [0][0][0][0]
``````

3 Likes

This is exactly what I'm trying to do but in Swift. I assume you are storing the underlying data as a one-dimensional array. Then how are you taking that 1D array and printing it as multiple dimensions given the shape?

So you would start with an empty string like `var description = ""` and pass that string to the `result` parameter like `print(..., result: &description)`?

Yes and by some magical indexing scheme.

I found a ShapedArray description on GitHub but not sure how to apply it to my example.

Yes, you start with empty string, zero iteration, and empty iterators array. In else branch you then modify string appending flatten row.

That seems like what Iβve suggested btw, in general at least.

I also have thought about version in which you go in reverse way through shapes and slice array, so it might be less iterations and more optimal, but Iβm not sure if thatβs a correct algorithm, need to a play a bit with code first.

Something like this?

``````extension ShapedArray: CustomStringConvertible
where Element: CustomStringConvertible {
var description: String {
var shape = shape

guard let cols = shape.popLast() else { return "()" }

let width = storage.map(\.description.count).max() ?? 0
var lines = storage.chunks(ofCount: cols).map { row in
row.map {
(String(repeating: " ", count: width) + \$0.description)
.suffix(width)
}
.joined(separator: " ")
}

var rows = 1
while let count = shape.popLast() {
rows *= count

let brackets = switch rows {
case 1: [(left: "(", right: ")")].cycled()
default: (
[             (left: "β", right: "β")] +
repeatElement((left: "β", right: "β"), count: rows - 2) +
[             (left: "β", right: "β ")]
).cycled()
}

lines = zip(brackets, lines).map { bracket, line in
[bracket.left, line, bracket.right].joined(separator: " ")
}
}

return lines.joined(separator: "\n")
}
}
``````

`chunks(ofCount:)` is from swift-algorithms.

Example
``````let A = ShapedArray([
1, 2, 3, 4,
5, 6, 7, 8,

9, 10, 11, 12,
13, 14, 15, 16,

17, 18, 19, 20,
21, 22, 23, 24,

25, 26, 27, 28,
29, 30, 31, 32,

33, 34, 35, 36,
37, 38, 39, 40,

41, 42, 43, 44,
45, 46, 47, 48
], shape: 2, 3, 2, 4)

print(A)
``````

prints

``````β β β  1  2  3  4 β β β
β β β  5  6  7  8 β  β β
β β β  9 10 11 12 β β β
β β β 13 14 15 16 β  β β
β β β 17 18 19 20 β β β
β β β 21 22 23 24 β  β  β
β β β 25 26 27 28 β β β
β β β 29 30 31 32 β  β β
β β β 33 34 35 36 β β β
β β β 37 38 39 40 β  β β
β β β 41 42 43 44 β β β
β β β 45 46 47 48 β  β  β
``````

Maybe with a blank line between each sub-matrix to better separate them visually.

1 Like

Here's my attempt based on your example but without using the swift-algorithms package. I don't understand how you are applying the brackets. What is the `.cycled()` method in the switch statement?

``````func description<T>(array: [T], shape: [Int]) -> String {
var shape = shape
let chunkSize = shape.popLast() ?? 1

let chunks = stride(from: 0, to: numbers.count, by: chunkSize).map {
Array(numbers[\$0..<min(\$0 + chunkSize, numbers.count)])
}

let width = array.map { "\(\$0)".count }.max() ?? 0

let lines = chunks.map { row in
row.map { element in
(String(repeating: " ", count: width) + element.description).suffix(width)
}.joined(separator: "  ")
}

var descr = ""

for (n, line) in lines.enumerated() {
if n == 0 {
descr += "β‘ " + line + " β€" + "\n"
} else if n == 5 {
descr += "β£ " + line + " β¦" + "\n"
} else {
descr += "β " + line + " β₯" + "\n"
}
}

return descr
}

let numbers: [Float] = [1, 2, 3,
4, 5, 6,

7, 8, 9,
10, 11, 12,

13, 14, 15,
16, 17, 18]

let shape = [3, 2, 3]

let d = description(array: numbers, shape: shape)
print(d)
``````
``````β‘  1.0   2.0   3.0 β€
β  4.0   5.0   6.0 β₯
β  7.0   8.0   9.0 β₯
β 10.0  11.0  12.0 β₯
β 13.0  14.0  15.0 β₯
β£ 16.0  17.0  18.0 β¦
``````

In the image, how do you get the parentheses to be continuous between the lines? Is the image a screenshot of your terminal or is it showing something else? When I print out brackets or parentheses in the terminal, there are gaps between the lines as shown below. But in your screenshot there are no gaps between the lines.

``````β‘  1.0   2.0   3.0 β€
β  4.0   5.0   6.0 β₯
β  7.0   8.0   9.0 β₯
β 10.0  11.0  12.0 β₯
β 13.0  14.0  15.0 β₯
β£ 16.0  17.0  18.0 β¦
``````

My bad, `cycled()` is from swift-algorithms too. It returns a never-ending sequence cycling the provided sequence.

@ibex10 is probably using either LaTeX or some derivation to pretty print the matrices.

I would like to avoid using swift-algorithms so here's my attempt at handling the brackets with just using Swift. It works for 2D and 3D arrays but brackets for higher dimensions are not captured.

``````func description<T>(array: [T], shape: [Int]) -> String {
let chunkSize = shape.last ?? 1

let chunks = stride(from: 0, to: numbers.count, by: chunkSize).map {
Array(numbers[\$0..<min(\$0 + chunkSize, numbers.count)])
}

let width = array.map { "\(\$0)".count }.max() ?? 0

let lines = chunks.map { row in
row.map { element in
(String(repeating: " ", count: width) + element.description).suffix(width)
}.joined(separator: "  ")
}

var descr = ""
let dim = shape.count
let lastRow = shape.reduce(1, *) / chunkSize
let lastSubRow = shape.reversed()[1]

for (n, line) in lines.enumerated() {
let n = n + 1
if n == 1 {
// First row brackets
let prepend = String(repeating: "β‘ ", count: dim - 1)
let append = String(repeating: " β€", count: dim - 1)
descr += prepend + line + append + "\n"
} else if n == lastRow {
// Last row brackets
let prepend = String(repeating: "β£ ", count: dim - 1)
let append = String(repeating: " β¦", count: dim - 1)
descr += prepend + line + append + "\n"
} else if n % lastSubRow == 0 {
// Last row brackets on subarray
let prepend = String(repeating: "β ", count: dim - 2)
let append = String(repeating: " β₯", count: dim - 2)
descr += prepend + "β£ " + line + " β¦" + append + "\n"
} else if n % lastSubRow == 1 {
// First row brackets on subarray
let prepend = String(repeating: "β ", count: dim - 2)
let append = String(repeating: " β₯", count: dim - 2)
descr += prepend + "β‘ " + line + " β€" + append + "\n"
} else {
let prepend = String(repeating:"β ", count: dim - 1)
let append = String(repeating: " β₯", count: dim - 1)
descr += prepend + line + append + "\n"
}
}

return descr
}
``````

Here's a 2D example:

``````let numbers: [Float] = [1, 2, 3,
42.8, 5, 6]

let shape = [2, 3]

let d = description(array: numbers, shape: shape)
print(d)
``````
``````β‘  1.0   2.0   3.0 β€
β£ 42.8   5.0   6.0 β¦
``````

Here's a 3D example:

``````let numbers: [Float] = [1, 2, 3,
4, 5, 6,

7, 8, 9,
10, 11, 12,

13, 14, 15,
16, 17, 18]

let shape = [3, 2, 3]

let d = description(array: numbers, shape: shape)
print(d)
``````
``````β‘ β‘  1.0   2.0   3.0 β€ β€
β β£  4.0   5.0   6.0 β¦ β₯
β β‘  7.0   8.0   9.0 β€ β₯
β β£ 10.0  11.0  12.0 β¦ β₯
β β‘ 13.0  14.0  15.0 β€ β₯
β£ β£ 16.0  17.0  18.0 β¦ β¦
``````

And here is a 4D example. Notice the brackets are not captured for the extra dimension.

``````let numbers: [Float] = [1, 2, 3, 4,
5, 6, 7, 8,

9, 10, 11, 12,
13, 14, 15, 16,

17, 18, 19, 20,
21, 22, 23, 24,

25, 26, 27, 28,
29, 30, 31, 32,

33, 34, 35, 36,
37, 38, 39, 40,

41, 42, 43, 44,
45, 46, 47, 48]

let shape = [2, 3, 2, 4]

let d = description(array: numbers, shape: shape)
print(d)
``````
``````β‘ β‘ β‘  1.0   2.0   3.0   4.0 β€ β€ β€
β β β£  5.0   6.0   7.0   8.0 β¦ β₯ β₯
β β β‘  9.0  10.0  11.0  12.0 β€ β₯ β₯
β β β£ 13.0  14.0  15.0  16.0 β¦ β₯ β₯
β β β‘ 17.0  18.0  19.0  20.0 β€ β₯ β₯
β β β£ 21.0  22.0  23.0  24.0 β¦ β₯ β₯
β β β‘ 25.0  26.0  27.0  28.0 β€ β₯ β₯
β β β£ 29.0  30.0  31.0  32.0 β¦ β₯ β₯
β β β‘ 33.0  34.0  35.0  36.0 β€ β₯ β₯
β β β£ 37.0  38.0  39.0  40.0 β¦ β₯ β₯
β β β‘ 41.0  42.0  43.0  44.0 β€ β₯ β₯
β£ β£ β£ 45.0  46.0  47.0  48.0 β¦ β¦ β¦
``````

Any suggestions on how to print the brackets for higher dimensions without using the swift-algorithms package?

Using modulo as you did in your implementation, you can generate the array of row counts `rows` (in your 4D example it would be [12, 6, 2]) and do something like:

``````for (n, line) in lines.enumerated() {
for row in rows.reversed() {
switch n % row {
case 0: descr += "β‘ "
case row - 1: descr += "β£ "
default: descr += "β "
}
}

descr += line

for row in rows {
switch n % row {
case 0: descr += " β€"
case row - 1: descr += " β¦"
default: descr += " β₯"
}
}

descr += "\n"
}
``````

Make sure to handle the case where the second to last element in `shape` is 1 though. Also, do you need the extra newline at the end of the description string?

Based on your example I now have the function shown below. I removed the newline for the last row. I also used parentheses instead of square brackets. I check for `row == 1` to handle cases where second to last element in `shape` is 1.

``````func description<T>(for array: [T], with shape: [Int]) -> String {
if shape.count == 1 {
var descr = "( "
descr += array.map { "\(\$0)" }.joined(separator: "  ")
descr += " )"
return descr
}

let chunkSize = shape.last ?? 1

let chunks = stride(from: 0, to: numbers.count, by: chunkSize).map {
Array(numbers[\$0..<min(\$0 + chunkSize, numbers.count)])
}

let width = array.map { "\(\$0)".count }.max() ?? 1

let lines = chunks.map { row in
row.map { element in
(String(repeating: " ", count: width) + element.description).suffix(width)
}.joined(separator: "  ")
}

var r = shape
r.removeLast()
r.reverse()
let rows = cumulativeProd(r)

var descr = ""

for (n, line) in lines.enumerated() {
for row in rows.reversed() {
if row == 1 {
descr += "( "
continue
}

switch n % row {
case 0:
descr += "β "
case row - 1:
descr += "β "
default:
descr += "β "
}
}

descr += line

for row in rows {
if row == 1 {
descr += " )"
continue
}

switch n % row {
case 0:
descr += " β"
case row - 1:
descr += " β "
default:
descr += " β"
}
}

if n != rows.last! - 1 {
descr += "\n"
}
}

return descr
}
``````

The function to calculate the cumulative product of an array is:

``````func cumulativeProd(_ array: [Int]) -> [Int] {
let result = array.reduce(into: [Int]()) { partialResult, x in
if let last = partialResult.last {
partialResult.append(last * x)
} else {
partialResult.append(x)
}
}
return result
}
``````

It would make it easier to read the print output if a newline is placed after each dimension. Something like this:

``````β β β  1.0   2.0   3.0   4.0 β β β
β β β  5.0   6.0   7.0   8.0 β  β β
β β                            β β
β β β  9.0  10.0  11.0  12.0 β β β
β β β 13.0  14.0  15.0  16.0 β  β β
β β                            β β
β β β 17.0  18.0  19.0  20.0 β β β
β β β 21.0  22.0  23.0  24.0 β  β  β
β                                β
β β β 25.0  26.0  27.0  28.0 β β β
β β β 29.0  30.0  31.0  32.0 β  β β
β β                            β β
β β β 33.0  34.0  35.0  36.0 β β β
β β β 37.0  38.0  39.0  40.0 β  β β
β β                            β β
β β β 41.0  42.0  43.0  44.0 β β β
β β β 45.0  46.0  47.0  48.0 β  β  β
``````
1 Like

That really looks good.

Here is my take, which prints the array by using MathML.

For the More Curious
Driver.swift
``````//
//  Driver.swift
//  MyArray
//
//  Created by ibex on 22/8/2024.
//

@main
enum Driver {
static func main () {
let shape = [3, 3, 3]
let v1 = MyArray (shape: shape) {
random_string()
}

let v2 = MyArray (shape: shape) {
random_int ()
}

let v3 = MyArray (shape: shape) {
random_int ()
}

v1.print ()
print ()

let u = v1.printMathML ()
let v = v2.printMathML ()
let w = v3.printMathML ()
print ("--> MathML:")
print (u)
print (ML.Operator.times)
print (v)
print (ML.Operator.equals)
print (w)
}
}

private func random_int () -> Int {
Int.random(in: 0..<1024)
}

private func random_string () -> String {
let u: [String] = ["u", "v", "x", "y", "z", "p", "q", "r", "s", "t", "w", "i", "j", "k", "delta"]
let i = Int.random (in: 0..<u.count)
return u [i]
}

extension Int: ML.Content {
var value: String {
ML.Number (self).value
}
}

extension Double: ML.Content {
var value: String {
ML.Number (self).value
}
}

extension String: ML.Content {
var value: String {
ML.Variable (self).value
}
}

``````
MyArray.swift
``````//
//  MyArray.swift
//  MyArray
//
//  Created by ibex on 22/8/2024.
//

import Foundation

struct MyArray <T: ML.Content> {
let store: [T]
let shape: [Int]
}

extension MyArray {
init (shape: [Int], generator: () -> T) {
self.shape = shape
let M = Self.size (from: shape)
self.store = .init (unsafeUninitializedCapacity: M) {buffer,initializedCount in
for i in 0..<M {
buffer [i] = generator ()
}
initializedCount = M
}
}
}

extension MyArray {
static func size (from shape: [Int]) -> Int {
guard !shape.isEmpty else {
return 0
}
let u = 1
let v: Int = shape.reduce (u) {
\$0 * \$1
}
return v
}
}

extension MyArray {
func array (index: Int, shape: [Int]) -> MyArray {
let size   = Self.size(from: shape)
let offset = index * size
assert (offset < store.count)

var store: [T] = []
for i in 0..<size {
store.append (self.store [i + offset])
}
return MyArray (store:store, shape: shape)
}
}

extension MyArray {
func print () {
let size = Self.size (from: shape)
Swift.print ("-->", shape, size, store)
_print ()
}
}

extension MyArray {
private func _print () {
let size = Self.size (from: shape)
guard size > 0 else {
return
}

if shape.count == 1 {
Swift.print ("-->", shape, size, store)
return
}

var shape = self.shape
let M = shape.removeFirst()

for i in 0..<M {
let array = array (index: i, shape: shape)
array._print ()
}
Swift.print ()
}
}

``````
MyArray+PrintML.swift
``````//
//  MyArray+PrintML.swift
//  MyArray
//
//  Created by ibex on 22/8/2024.
//

extension MyArray {
func printMathML (display: ML.Math.Display = .inline) -> String {
let size = Self.size (from: shape)
Swift.print ("-->", shape, size, store)

let u = emitML ()
let v = ML.Math (u, display: display)
return v.value
}
}

extension MyArray {
private func emitML () -> ML.Content {
let size = Self.size (from: shape)
guard size > 0, shape.count > 0 else {
return ML.Text ("empty array")
}

#if false
Swift.print ("-->", shape, size, store)
#endif

if shape.count == 1 {
// 1 x P
let row = ML.Row (store)
return ML.Table ([row])
}
else if shape.count == 2 {
// P x Q
var shape = self.shape
let M = shape.removeFirst()

var rows: [ML.Content] = []
for i in 0..<M {
let array = array (index: i, shape: shape)
let row = ML.Row (array.store)
rows.append (row)
}
return ML.Table (rows)
}
else {
// P x Q x R ...
var shape = self.shape
let M = shape.removeFirst()

var rows: [ML.Content] = []
for i in 0..<M {
let array = array (index: i, shape: shape)
let u = array.emitML ()
rows.append (ML.Row ([u]))
}
return ML.Table (rows)
}
}
}
``````
ML
``````//
//  ML.swift
//  MyArray
//
//  Created by ibex on 22/8/2024.
//

enum ML {
protocol Content {
var value: String {get}
}
}

extension ML {
struct Element: Content {
let value: String

init () {
self.value = ""
}

init (tag: String, value: String) {
self.value = "<\(tag)>\(value)</\(tag)>"
}

init (tag: String, attributes: String, value: String) {
self.value = "<\(tag) \(attributes)>\(value)</\(tag)>"
}
}
}

extension ML {
struct Error: Content {
var value: String {
content.value
}

var content: Element

init (_ u: String) {
self.content = Element (tag: "merror", value: "\(u)")
}
}
}

extension ML {
struct Number: Content {
var value: String {
content.value
}

var content: Element

init (_ u: some Numeric) {
self.content = Element (tag: "mn", value: "\(u)")
}
}
}

extension ML {
struct Text: Content {
var value: String {
content.value
}

var content: Element

init (_ u: String) {
self.content = Element (tag: "mtext", value: "\(u)")
}
}
}

extension ML {
struct Variable: Content {
var value: String {
content.value
}

var content: Element

init (_ u: String) {
if u.count == 1 {
self.content = Element (tag: "mi", value: "\(u)")
}
else if u.count > 1 {
var vv : [Element] = []
for i in u {
vv.append (Element (tag: "mi", value: String (i)))
}
self.content = Element (tag: "mrow", value: vv.map {\$0.value}.joined ())
}
else {
self.content = Error ("empty variable string").content
}
}
}
}

extension ML {
struct Operator: Content {
var value: String {
content.value
}

var content: Element

init (_ u: Op) {
self.content = Element (tag: "mo", value: "\(u.rawValue)")
}

init (_ u: String) {
self.content = Element (tag: "mo", value: "\(u)")
}

enum Op: String {
case plus   = "+"
case minus  = "&#x2212;"
case equals = "="
case times  = "&#xD7;"
}

static var plus   = Self (.plus).value
static var minus  = Self (.minus).value
static var equals = Self (.equals).value
static var times  = Self (.times).value
}
}
``````
ML+Table.swift
``````//
//  ML+Table.swift
//  MyArray
//
//  Created by ibex on 23/8/2024.
//

extension ML {
struct Row: Content {
var value: String {
content.value
}

var content: Element

init (_ u: [Content]) {
let v = u.map {Element (tag: "mtd", value: \$0.value)}.map {\$0.value}.joined()
self.content = Element (tag: "mtr", value: v)
}
}
}

extension ML {
struct Table: Content {
var value: String {
content.value
}

var content: Element

init (_ u: [Content]) {
let v = u.map {\$0.value}.joined()
let t = Element (tag: "mtable", value:v)
self.content = Element (tag: "mfenced", attributes: "open=\"(\" close=\")\" separators=\"\"", value: t.value)
}
}
}

extension ML {
struct Math {
enum Display: String {
case block, inline
}
var value: String {
content.value
}

var content: Element

init (_ u: Content, display: Display = .block) {
self.content = ML.Element (tag: "math", attributes: "xmlns=\"http://www.w3.org/1998/Math/MathML\" display=\"\(display)\"", value: u.value)
}
}
}

``````

So your `print` function prints out MathML syntax is that correct? How are you rendering the MathML, do you use a WebView, a SwiftUI view, or something else? And what if you want to view your array in the terminal, do you have a way to render it in the terminal?

The algorithm shouldn't change that much: change your `cumulativeProd` to

``````func cumulativeProd(_ array: [Int]) -> [Int] {
array.reduce(into: [Int]()) { partialResult, x in
if let last = partialResult.last {
partialResult.append(last * x + x - 1)
} else {
partialResult.append(x)
}
}
}
``````

and use a counter (`n` in the following snippet)

``````var n = 0
let last = rows.last! // this is a simplification: make sure you
// correctly handle the case in which rows
// is empty

for row in rows.reversed() {
switch n % (row + 1) {
case 0: descr += "β "
case row - 1: descr += "β "
case row: descr += "  "
default: descr += "β "
}
}

descr += line

for row in rows {
switch n % (row + 1) {
case 0: descr += " β"
case row - 1: descr += " β "
case row: descr += "  "
default: descr += " β"
}
}

descr += "\n"
n += 1
}

for line in lines {
if n % (last + 1) == last {
add(line: String(repeating: " ", count: (width + 2) * chunkSize - 2))
}