Swift and Spreadsheet-Style Sorting: filter() and sort() working together


The last few posts have made reference to spreadsheets quite a bit but up until now assumptions have been made that all values in a column would be of a single type. But what if they weren't? After all, they are not usually required to be in spreadsheets.

Fields

If you followed my posts about Aldwych then you might guess the starting point: enums with associated values.
enum Field:Comparable {
    case Number(Double)
    case Str(String)
    func num() -> Double? {
        switch self {
        case .Number(let num):
            return num
        default:
            return nil
        }
    }
    func str() -> String? {
        switch self {
        case .Str(let str):
            return str
        default:
            return nil
        }
    }
    
}
This is similar to the way in which I treated JSON values in Aldwych, but here I've restricted Field types to just two. The most important thing, however, if we are going to be able to sort columns is that the Fields are comparable. In order for this to be so, we don't just need to adopt the Comparable protocol, we also need to overload two functions == and <
func ==(x: Field, y: Field) -> Bool {
    if let n1 = x.num(),
        n2 = y.num() {
            return n1 == n2
    }
    else if let s1 = x.str(),
        s2 = y.str() {
            return s1 == s2
    }
    return false
}

func <(x: Field, y: Field) -> Bool {
    if let n1 = x.num(),
        n2 = y.num() {
            return n1 < n2
    }
    else if let s1 = x.str(),
        s2 = y.str() {
            return s1 < s2
    }
    return false

}

Columns

The Field code is now finished, and next to consider are the Columns, since each Field is going to be collected into a column:
struct Column {
    var fields:[Field]
    init(fields:[Field]) {
            self.fields = fields
    }
 
}
The Column type is straightforward and doesn't adopt any protocols. What it needs however is a way of sorting the fields within it.
struct Column {
    var fields:[Field]
    init(fields:[Field]) {
            self.fields = fields
    }
 
    mutating func sort() {
        let sortedStrings = sorted(filter(fields){$0.str() != nil})
        let sortedNumbers = sorted(filter(fields){$0.num() != nil})
        fields = [sortedStrings,sortedNumbers].flatMap{$0}
        
    }
}
You'll notice we can't do a generic sort, because comparing strings to numbers would just make a mess of things. So the types are separated out first before the comparison is made. And as with the challenge faced yesterday when sorting columns we need a way to not only sort a single column but also all the other columns in the Table while keeping rows together.

In order to achieve this, the secondarySorted() function used in that post will be adapted here:
func secondarySorted(arr1:Column, var arr2:Column) -> Column {
    let rows = arr1.fields.reverse().map{(object1:$0, object2:arr2.fields.removeLast())}
    var filteredStrings = rows.filter{$0.object1.str() != nil}
    var filteredNumbers = rows.filter{$0.object1.num() != nil}
    filteredStrings.sort({$0.object1 < $1.object1})
    filteredNumbers.sort({$0.object1 < $1.object1})
    let sortedRows = [filteredStrings,filteredNumbers]
    return Column(fields:sortedRows.flatMap{$0.map{$0.object2}})
}

Tables

This function will be privately contained within a Table type that will ensure that all columns are sorted at once and not allowed to separately fall out of sync.
struct Table {
    var columns:[Column]
    init (columns:[Column]) {
        self.columns = columns
    }
    
    private func secondarySorted(arr1:Column, var arr2:Column) -> Column {
        let rows = arr1.fields.reverse().map{(object1:$0, object2:arr2.fields.removeLast())}
        var filteredStrings = rows.filter{$0.object1.str() != nil}
        var filteredNumbers = rows.filter{$0.object1.num() != nil}
        filteredStrings.sort({$0.object1 < $1.object1})
        filteredNumbers.sort({$0.object1 < $1.object1})
        let sortedRows = [filteredStrings,filteredNumbers]
        return Column(fields:sortedRows.flatMap{$0.map{$0.object2}})
    }
    
    mutating func sortColumnsByColumn(ind:Int) {
        let sortingColumn = columns[ind]
        columns = columns.map{self.secondarySorted(sortingColumn,arr2: $0)}
    }

}
And now everything is set up to sort a Table full of columns. So let's do that now:
let fieldArray1 = [Field.Number(8), Field.Str("pear"), Field.Str("apple"), Field.Str("shoes"), Field.Number(10)]

let fieldArray2 = [Field.Number(8), Field.Number(12), Field.Str("apple"), Field.Str("pair"), Field.Number(10)]

let column1 = Column(fields: fieldArray2)
let column2 = Column(fields: fieldArray1)
var tab = Table(columns: [column1,column2])
tab.sortColumnsByColumn(0)
// now all columns are sorted

Conclusion

From here the number of types permitted in a Field can be fleshed out, but aside from that we're mostly there on sorting rows in spreadsheet tables. Download the full Gist to add to a Playground.


Endorse on Coderwall

Comments