How to sort an array of string by similarity to specific key
Asked Answered
K

2

7

Given is the following:

var theArray: [String] = ["uncool", "chill", "nifty", "precooled", "dandy", "cool"]

I want to sort the array by how similar the words are to the key word.

var keyWord: String = "cool"

The wanted result would be:

print// ["cool", "uncool", "precooled", ...] and then it does not matter anymore. But the words that are the key or contain it should be the very first objects.

My closest tryout so far has been:

let _theArray = entries.sorted { element1, element2 in

     return element1.contains(keyWord) && !element2.contains(keyWord)
}

But that results in uncool being the first item, then precooled and the most related item cool even comes after nifty .

What am I missing?

Kitten answered 6/11, 2017 at 6:21 Comment(2)
your sort is in descending for details please check this sorted function link developer.apple.com/documentation/swift/array/2905744-sortedNumskull
Your code produces ["uncool", "precooled", "cool", "chill", "nifty", "dandy"], with all strings containing "cool" coming first in the array.Lonilonier
G
14

You can define your own similarity sorting method. Note that I have also added a hasPrefix priority over the ones which only contains the keyword which you can just remove if you don't want it:

var theArray = ["chill", "nifty", "precooled", "cooldaddy", "cool", "coolguy", "dandy", "uncool"]
let key = "cool"

let sorted = theArray.sorted {
    if $0 == key && $1 != key {
        return true
    }
    else if $0.hasPrefix(key) && !$1.hasPrefix(key)  {
        return true
    }
    else if !$0.hasPrefix(key) && $1.hasPrefix(key)  {
        return false
    }
    else if $0.hasPrefix(key) && $1.hasPrefix(key)
        && $0.count < $1.count  {
        return true
    }
    else if $0.contains(key) && !$1.contains(key) {
        return true
    }
    else if !$0.contains(key) && $1.contains(key) {
        return false
    }
    else if $0.contains(key) && $1.contains(key)
        && $0.count < $1.count {
        return true
    }
    return false
}

print(sorted)   // ["cool", "coolguy", "cooldaddy", "uncool", "precooled", "chill", "nifty", "dandy"]

You can also extend Sequence and create a sorted by key similarity method:

extension Sequence where Element: StringProtocol {
    func sorted<S>(by key: S) -> [Element] where S: StringProtocol {
        sorted {
            if $0 == key && $1 != key {
                return true
            }
            else if $0.hasPrefix(key) && !$1.hasPrefix(key)  {
                return true
            }
            else if !$0.hasPrefix(key) && $1.hasPrefix(key)  {
                return false
            }
            else if $0.hasPrefix(key) && $1.hasPrefix(key)
                && $0.count < $1.count  {
                return true
            }
            else if $0.contains(key) && !$1.contains(key) {
                return true
            }
            else if !$0.contains(key) && $1.contains(key) {
                return false
            }
            else if $0.contains(key) && $1.contains(key)
                && $0.count < $1.count {
                return true
            }
            return false
        }
    }
}

let sorted = theArray.sorted(by: key)  // "cool", "coolguy", "cooldaddy", "uncool", "precooled", "chill", "nifty", "dandy"]

And the mutating version as well:

extension MutableCollection where Element: StringProtocol, Self: RandomAccessCollection {
    mutating func sort<S>(by key: S) where S: StringProtocol {
        sort {
            if $0 == key && $1 != key {
                return true
            }
            else if $0.hasPrefix(key) && !$1.hasPrefix(key)  {
                return true
            }
            else if !$0.hasPrefix(key) && $1.hasPrefix(key)  {
                return false
            }
            else if $0.hasPrefix(key) && $1.hasPrefix(key)
                && $0.count < $1.count  {
                return true
            }
            else if $0.contains(key) && !$1.contains(key) {
                return true
            }
            else if !$0.contains(key) && $1.contains(key) {
                return false
            }
            else if $0.contains(key) && $1.contains(key)
                && $0.count < $1.count {
                return true
            }
            return false
        }
    }
}

Glossator answered 6/11, 2017 at 7:56 Comment(9)
By using .localizedCaseInsensitiveContains(key) eligible with CaseInsensitive pattern matching. :)Pyrope
@Pyrope The question is how to do a custom sort. The input provided by the OP it is all lowercase and has no mentions about it. So you don't even know if thats what OP needs.Glossator
@Pyrope Case Insensitive (not localized) sample dropbox.com/s/bhc5fr3x5q4bvgl/…Glossator
@LeoDabus one more trick - herePyrope
@Pyrope Case Insensitive (Localized) sample dropbox.com/s/eiml3zpxs45lp9t/…Glossator
@LeoDabus Good addition:)Pyrope
if the array is ["chill", "nifty", "precooled", "cooldaddy", "cool", "coolguy", "dandy", "uncool"], the result is not correct: ["cool", "uncool", "coolguy", "cooldaddy", "precooled", "chill", "nifty", "dandy"]Lifetime
@LeoDabus , can you please check this issue , when we put "uncool" at the end of the array ?Lifetime
@HassanTaleb yes there was a missing condition. Check my last edit.Glossator
H
3

First you need a measure of how similar two strings are. Here's a simple example:

extension String {
    func equalityScore(with string: String) -> Double {
        if self == string {
            return 2     // the greatest equality score this method can give
        } else if self.contains(string) {
            return 1 + 1 / Double(self.count - string.count)   // contains our term, so the score will be between 1 and 2, depending on number of letters.
        } else {
            // you could of course have other criteria, like string.contains(self)
            return 1 / Double(abs(self.count - string.count))
        }
    }
}

Once you have that, you can use it to sort the array:

var theArray: [String] = ["uncool", "chill", "nifty", "precooled", "dandy", "cool"]

var compareString = "cool"

theArray.sort { lhs, rhs in
    return lhs.equalityScore(with: compareString) > rhs.equalityScore(with: compareString)
}

Result: ["cool", "uncool", "precooled", "chill", "nifty", "dandy"]

Heterosexual answered 6/11, 2017 at 6:39 Comment(1)
if the array is : ["chill", "nifty", "precooled", "cooldaddy", "cool", "coolguy", "dandy", "uncool"] the result is incorrect: ["cool", "uncool", "coolguy", "precooled", "cooldaddy", "chill", "nifty", "dandy"]Lifetime

© 2022 - 2024 — McMap. All rights reserved.