2

I have an array of tuples of Int, CustomType, OtherCustomType. The array is sorted by the Int part of the tuples.

To add new elements at the correct position I wrote a binary search function to get the insertion point index.

The function returns a new tuple of Int, Bool, where Bool tells if the element is already present, and Int is either the index of the first occurrence of the new element, or the index of the first element which is larger than the new element.

The function is written genericly, and takes an array of a comparable type and a new element of the same type as arguments, so, obviously, I cannot simply pass my array of tuples.

A simple solution would be to re-organize my data so, instead of storing three values as tuples in one array, I could use three separate arrays, each of only one of the three values. I would then pass only the first array to the binary search function and then perform the desired action on all three arrays at the found index.

But is there a way to keep my data organized as tuples and pass only one element of each tuple to the function, like we are able to ignore parts of a tuple in comparisons like if tuple == (_ ,23, _) ?

Here is some sample code:

func findInsertPoint <T: Comparable> (forElement: T, inArray: [T]) -> (Int, Bool) {
    var low = 0
    var high = inArray.count

    if forElement > inArray[high-1] {
        return (high, false)
    }
    while low < high {
        let mid = (low+high)/2
        if inArray[mid] >= forElement {
            high = mid
        } else {
            low = mid+1
        }
    }
    return(low,(inArray[low] == forElement))
}

An array of Ints works perfectly fine:

// index         0 1 2 3 4 5  6  7  8  9  10 11 12 13 14 15
var testArray = [1,2,5,7,8,11,12,12,12,15,19,22,22,26,52,56]

findInsertPoint(forElement: x, inArray: testArray)

// x = 17 returns (10,false)
// x = 19 returns (10,true)

But my actual array looks something like this:

var testArray = [(4,"Bee",2.5),(5,"dog",1.0),(8,"dog",43.13)]

How do I pass an array of only the first part of each tuple, but without the expensive creation of an actual new array each function call?

A possibility would be to call the function:

findInsertPoint(forElement: 7 in Array: testArray.0)

which would be perfect, but I know this doesn't work.

Is there a Swift way to temporarily ignore members of a tuple or a struct for a function call that only accepts arrays of a single type?

If not, I know my two possibilities are:

  1. stick to my taylored binary search, not the generic one from the code above.
  2. divide the tuple into 3 separate arrays.
the Tin Man
  • 158,662
  • 42
  • 215
  • 303
MassMover
  • 529
  • 2
  • 17
  • The usual approach is to pass a custom comparison function, like e.g. here: https://stackoverflow.com/a/26679191/1187415, similarly to existing sort methods like https://developer.apple.com/documentation/swift/array/2296815-sorted. – Martin R Jul 06 '17 at 16:36
  • " I could use 3 separate arrays, each of only one of the 3 values." Most certainly don't do this. That way lies madness. – Alexander Jul 06 '17 at 16:37
  • Can you give us some compilable sample data to work this? – Alexander Jul 06 '17 at 16:38
  • https://stackoverflow.com/a/44832227/3141234 – Alexander Jul 06 '17 at 16:38
  • Storing data in tuples is very troublesome. I only use tuples for return values of methods. Create a class or a struct instead. – Sweeper Jul 06 '17 at 16:44
  • At the moment I have a custom binary search, but having in mind the DRY paradigm I thought it to be useful to have a general purpose search function at hand. The function itself works perfectly nice on an testArray of single elements. – MassMover Jul 06 '17 at 16:49
  • An array of structs or classes would not solve my initial problem, would it? – MassMover Jul 06 '17 at 16:51
  • Using the method from https://stackoverflow.com/a/26679191/1187415 it would be just `array.insertionIndexOf(elem: ..., isOrderedBefore: { $0.0 < $1.0 })` – Martin R Jul 06 '17 at 16:53

2 Answers2

6

This is the solution I found:

If you have an array of types which are collection types on their own, and you want to look only at a certain property of each member of the outer array, use the .map method of swift's collection type:

var testArray = [(4,"Bee",2.5),(5,"dog",1.0),(8,"dog",43.13)]
var onlyFirstProperty = testArray.map({$0.0}) // [4,5,8]

This way you'll get a new array which consists only of the first elements of each tuple. $0.0 is shorthand syntax for firstMember.firstProperty. In my code I can call my function like this:

findInsertPoint(forElement: 7 in Array: testArray.map({$0.0}))
MassMover
  • 529
  • 2
  • 17
  • Can somebody tell how expensive the .map method will be? When you use such a map as a function parameter, will swift make an actual new array or are the new array's members be references to the original array? Or will at least swift's copy-on-write behaviour for value types being applied? – MassMover Jul 09 '17 at 11:32
0

You could create a structure and implement the Comparable protocol in it like so:

struct Foo: Comparable {
    let a: Int
    let b: TypeB
    let c: TypeC

    // compare according to the integers
    static func ==(lhs: Foo, rhs: Foo) -> Bool {
        return lhs.a == rhs.a
    }

    static func <(lhs: Foo, rhs: Foo) -> Bool {
        return lhs.a < rhs.a
    }
}

With this structure, you could then call your custom generic sort function with the desired results as follows:

let foos = [Foo]()
let (position, exists) = customSort(foos)

Since your custom sort function uses the comparable protocol, it should work just fine with the struct.

mohak
  • 603
  • 1
  • 5
  • 11
  • I do not quite understand the last part of this code, but making it a comparable struct is a possible solution which I was yet thinking about. I have reason to believe though, that in the further development there are more possible situations other than sorting where I have to find the index of a given value. – MassMover Jul 06 '17 at 17:28
  • @MassMover By finding index of a given value, do you mean given an Int, find the tuple with the same int value? 'cause that can be easily achieved using `foos.filter({ $0.a == x})` where `x` is the int value you're interested in. – mohak Jul 06 '17 at 18:52
  • This would actually return an array of the matching element as the only member, instead of the index of the element. But the higher order functions .map, .filter. and .reduce, which had been unknown to me when asking my question, were indeed the right place to go for. – MassMover Jul 09 '17 at 11:46