Closest-match string-array sorting in Swift - arrays

Using Swift4, I would like to sort a string-array according to the closest match to a given searchTerm. Important is to me that if the searchTerm can be found as an exact-match, then the returnArray should show this searchTerm upfront !
Example: Given the Array = ["Hello world", "Hello Jamaica", "Hello", "Family", "Hel"]
And the searchTerm = "Hello", the algorithm should return:
["Hello", "Hello world", "Hello Jamaica", "Hel", "Family"].
Approach 1:
I tried to use FuzzyMatching - and it somehow worked (i.e. it did sort the inputArray according to a given searchTerm, however it did not put the exact-matches upfront ! i.e. With FuzzyMatching I achieved a good sorting according to substring-matches and syntactic sorting. But it did not bring me the exact-matches upfront in the returnArray).
Approach 2:
Then I tried my own algorithm - (see code below). But if there are several strings in the array that all start with my searchTerm (i.e. have searchTerm as a prefix), then somehow my algo does not a good job.
static func bestMatchFilterdStringArray(inputArray: [String], searchTerm: String) -> [String] {
let matchingTerms = inputArray
.filter { $0.range(of: searchTerm, options: .caseInsensitive) != nil }
.sorted { ($0.hasPrefix(searchTerm) ? 0 : 1) < ($1.hasPrefix(searchTerm) ? 0 : 1) }
return matchingTerms
}
How is a "Closest-match string-array sorting" done in Swift4? Especially bringing me exact-matches upfront in the returnArray? Any help appreciated!

You can use Levenshtein distance score to compare your search term with every string in the array, and the one with the highest score will be the first term in your result array etc. Your result will be an array of strings sorted in descending order of the score.
Following extension to string can be used to get Levenshtein distance score. In this algorithm, higher the value, better the equality.
extension String {
func levenshteinDistanceScore(to string: String, ignoreCase: Bool = true, trimWhiteSpacesAndNewLines: Bool = true) -> Double {
var firstString = self
var secondString = string
if ignoreCase {
firstString = firstString.lowercased()
secondString = secondString.lowercased()
}
if trimWhiteSpacesAndNewLines {
firstString = firstString.trimmingCharacters(in: .whitespacesAndNewlines)
secondString = secondString.trimmingCharacters(in: .whitespacesAndNewlines)
}
let empty = [Int](repeating:0, count: secondString.count)
var last = [Int](0...secondString.count)
for (i, tLett) in firstString.enumerated() {
var cur = [i + 1] + empty
for (j, sLett) in secondString.enumerated() {
cur[j + 1] = tLett == sLett ? last[j] : Swift.min(last[j], last[j + 1], cur[j])+1
}
last = cur
}
// maximum string length between the two
let lowestScore = max(firstString.count, secondString.count)
if let validDistance = last.last {
return 1 - (Double(validDistance) / Double(lowestScore))
}
return 0.0
}
}

Related

Need to sort array based on alphabetically and based on Upper and followed by lower case

var names = [ "Alpha","alpha1","Alpha1","alpha2", "beta","Beta1"]
var sortedNames = names.sorted { $0.localizedCaseInsensitiveCompare($1) == ComparisonResult.orderedAscending }
I tried to sort the above array but not able to get what i want. Need the array to be sorted like below, can anyone please help on this to implement.
Need Output like below-:
[ "Alpha","Alpha1", "alpha1","alpha2", "Beta1","beta"]
You could try NSString.CompareOptions.literal out -
Exact character-by-character equivalence.
let names = ["Alpha", "alpha1", "Alpha1", "alpha2", "beta", "Beta1"]
let sortedNames = names.sorted { $0.compare($1, options: [.literal]) == .orderedAscending }
This gives you -
▿ 6 elements
- 0 : "Alpha"
- 1 : "Alpha1"
- 2 : "Beta1"
- 3 : "alpha1"
- 4 : "alpha2"
- 5 : "beta"
As you can see, it will sort precisely based on char-by-char basis. However it will still keep all UPPERCASE ones at the start and lowercase ones in the end.
What you need is a custom implementation where you have the full control on comparison algorithm -
Code
extension Array where Element == String {
/// This is a very limited logic that compares values based on following two distinctions only -
///
/// 1. Letters - See docs for `Character.isLetter`
/// - "A" (U+0041 LATIN CAPITAL LETTER A)
/// - "é" (U+0065 LATIN SMALL LETTER E, U+0301 COMBINING ACUTE ACCENT)
/// - "ϴ" (U+03F4 GREEK CAPITAL THETA SYMBOL)
/// - "ڈ" (U+0688 ARABIC LETTER DDAL)
/// - "日" (U+65E5 CJK UNIFIED IDEOGRAPH-65E5)
/// - "ᚨ" (U+16A8 RUNIC LETTER ANSUZ A)
///
/// 2. Whole Numbers - See docs for `Character.isWholeNumber`
/// - "1" (U+0031 DIGIT ONE) => 1
/// - "५" (U+096B DEVANAGARI DIGIT FIVE) => 5
/// - "๙" (U+0E59 THAI DIGIT NINE) => 9
/// - "万" (U+4E07 CJK UNIFIED IDEOGRAPH-4E07) => 10_000
///
func sortedAlphabeticallyNumericallyCaseSensitive() -> [String] {
return self.sorted { s1, s2 in
for (i1, c1) in s1.enumerated() {
let i2 = s2.index(s2.startIndex, offsetBy: i1)
if i2 < s2.endIndex {
let c2: Character = s2[i2]
/// 1. Support for `Letters`
if c1.isLetter, let c1Int = c1.asciiValue,
c2.isLetter, let c2Int = c2.asciiValue {
if c1Int == c2Int {
continue
}
if (c1.isUppercase && c2.isUppercase) || (c1.isLowercase && c2.isLowercase) {
return c1Int < c2Int
}
else if c1.isUppercase {
return c1Int <= (c2.uppercased().first?.asciiValue ?? 0)
}
else {
return (c1.uppercased().first?.asciiValue ?? 0) < c2Int
}
}
/// 2. Support for Whole Numbers
else if c1.isWholeNumber, let c1NumberValue = c1.wholeNumberValue,
c2.isWholeNumber, let c2NumberValue = c2.wholeNumberValue {
if c1NumberValue == c2NumberValue {
continue
}
return c1NumberValue < c2NumberValue
}
/// 1x2
else if c1.isWholeNumber, c2.isLetter {
return true
}
else if c1.isLetter, c2.isWholeNumber {
return false
}
/// You are welcome to add any other criteria
}
else {
return s1.count < s2.count
}
}
return s1.count < s2.count
}
}
Usage
let names = ["alpha58", "Zebra", "Alpha", "alpha55", "alpha5", "alpha", "ALPHA", "alpha1", "Alpha1", "alpha2", "beta", "Beta1"]
let sortedNames = names.sortedAlphabeticallyNumericallyCaseSensitive()
(lldb) po sortedNames
▿ 12 elements
- 0 : "ALPHA"
- 1 : "Alpha"
- 2 : "Alpha1"
- 3 : "alpha"
- 4 : "alpha1"
- 5 : "alpha2"
- 6 : "alpha5"
- 7 : "alpha55"
- 8 : "alpha58"
- 9 : "Beta1"
- 10 : "beta"
- 11 : "Zebra"
Notes
It does not limit the comparison to just the first character. It keeps comparing two strings char-by-char until it has a conclusion on which one comes first.
This will most likely have performance issues while sorting large lists. it's best that this sorting happens on a background thread so UI doesn't freeze.
Update
More often than not, in the apps we are dealing with record types that contain these string values. For example a Person record.
Above implementation can be adapted to work with any record type like following -
struct Person {
let id: String
let name: String
init(id: String = UUID().uuidString, name: String) {
self.id = id
self.name = name
}
}
extension Array where Element == Person {
func sortedAlphabeticallyNumericallyCaseSensitive() -> [Element] {
return self.sorted { p1, p2 in
let s1 = p1.name
let s2 = p2.name
/// same as above implementation
}
}
}
I couldn't find a simple answer but this two-step solution seems to work
First make a dictionary grouped by the first character in upper case
let dictionary = Dictionary(grouping: names, by: { String($0.prefix(1)).uppercased()})
Then sort the values first and then the keys and use flatMap to create a new array from the sorted values
let sorted = dictionary
.mapValues { $0.sorted() }
.sorted(by: { $0.key < $1.key })
.flatMap({ $0.value})
Previous version of this answer removed
With a custom sort(), this should do the trick:
let sorted = names.sorted {
if $0.isUppercasePrefixed == $1.isUppercasePrefixed || $0.prefix(1).localizedCaseInsensitiveCompare($1.prefix(1)) != .orderedSame {
return $0.localizedCaseInsensitiveCompare($1) == .orderedSame
} else {
return $0.isUppercasePrefixed
}
}
print("Sorted: \(sorted)")
With the help of:
extension String {
var isUppercasePrefixed: Bool {
return first?.isUppercase ?? false
}
}
Output:
$> sorted: ["Alpha", "Alpha1", "alpha1", "alpha2", "Beta1", "beta"]
I've simplified the cases, but here's the logic:
If both starts with an uppercase
-> "normal compare" (ie: $0.localizedCaseInsensitiveCompare($1) == .orderedSame)
If both starts with a lowercase
-> "normal compare" (ie: $0.localizedCaseInsensitiveCompare($1) == .orderedSame)
If one start with a lower case and the other starts with a lowercase
-> if it's the same letter: prioritize the one with the uppercase (ie $0.isUppercasePrefixed)
-> if they both have different starting letters: -> "normal compare" (ie: $0.localizedCompare($1) == .orderedSame)
We see there there a multiple conditions where the result would be the same, and after factorization of them, we got the result in the code.

swift calling item from an array with a string [duplicate]

How to concatenate string in Swift?
In Objective-C we do like
NSString *string = #"Swift";
NSString *resultStr = [string stringByAppendingString:#" is a new Programming Language"];
or
NSString *resultStr=[NSString stringWithFormat:#"%# is a new Programming Language",string];
But I want to do this in Swift-language.
You can concatenate strings a number of ways:
let a = "Hello"
let b = "World"
let first = a + ", " + b
let second = "\(a), \(b)"
You could also do:
var c = "Hello"
c += ", World"
I'm sure there are more ways too.
Bit of description
let creates a constant. (sort of like an NSString). You can't change its value once you have set it. You can still add it to other things and create new variables though.
var creates a variable. (sort of like NSMutableString) so you can change the value of it. But this has been answered several times on Stack Overflow, (see difference between let and var).
Note
In reality let and var are very different from NSString and NSMutableString but it helps the analogy.
You can add a string in these ways:
str += ""
str = str + ""
str = str + str2
str = "" + ""
str = "\(variable)"
str = str + "\(variable)"
I think I named them all.
var language = "Swift"
var resultStr = "\(language) is a new programming language"
This will work too:
var string = "swift"
var resultStr = string + " is a new Programming Language"
\ this is being used to append one string to another string.
var first = "Hi"
var combineStr = "\(first) Start develop app for swift"
You can try this also:- + keyword.
var first = "Hi"
var combineStr = "+(first) Start develop app for swift"
Try this code.
let the_string = "Swift"
let resultString = "\(the_string) is a new Programming Language"
Very Simple:
let StringA = "Hello"
let StringB = "World"
let ResultString = "\(StringA)\(StringB)"
println("Concatenated result = \(ResultString)")
You can now use stringByAppendingString in Swift.
var string = "Swift"
var resultString = string.stringByAppendingString(" is new Programming Language")
Xcode didn't accept optional strings added with a normal string. I wrote this extensions to solve that problem:
extension String {
mutating func addString(str: String) {
self = self + str
}
}
Then you can call it like:
var str1: String?
var str1 = "hi"
var str2 = " my name is"
str1.addString(str2)
println(str1) //hi my name is
However you could now also do something like this:
var str1: String?
var str1 = "hi"
var str2 = " my name is"
str1! += str2
It is called as String Interpolation.
It is way of creating NEW string with CONSTANTS, VARIABLE, LITERALS and EXPRESSIONS.
for examples:
let price = 3
let staringValue = "The price of \(price) mangoes is equal to \(price*price) "
also
let string1 = "anil"
let string2 = "gupta"
let fullName = string1 + string2 // fullName is equal to "anilgupta"
or
let fullName = "\(string1)\(string2)" // fullName is equal to "anilgupta"
it also mean as concatenating string values.
Hope this helps you.
I just switched from Objective-C to Swift (4), and I find that I often use:
let allWords = String(format:"%# %# %#",message.body!, message.subject!, message.senderName!)
Swift 5
You can achieve it using appending API. This returns a new string made by appending a given string to the receiver.
API Details : here
Use:
var text = "Hello"
text = text.appending(" Namaste")
Result:
Hello
Hello Namaste
To print the combined string using
Println("\(string1)\(string2)")
or String3 stores the output of combination of 2 strings
let strin3 = "\(string1)\(string2)"
One can also use stringByAppendingFormat in Swift.
var finalString : NSString = NSString(string: "Hello")
finalString = finalString.stringByAppendingFormat("%#", " World")
print(finalString) //Output:- Hello World
finalString = finalString.stringByAppendingFormat("%#", " Of People")
print(finalString) //Output:- Hello World Of People
Concatenation refers to the combining of Strings in Swift. Strings may contain texts, integers, or even emojis! There are many ways to String Concatenation. Let me enumerate some:
Same String
Using +=
This is useful if we want to add to an already existing String. For this to work, our String should be mutable or can be modified, thus declaring it as a Variable. For instance:
var myClassmates = "John, Jane"
myClassmates += ", Mark" // add a new Classmate
// Result: "John, Jane, Mark"
Different Strings
If we want to combine different Strings together, for instance:
let oldClassmates = "John, Jane"
let newClassmate = "Mark"
We can use any of the following:
1) Using +
let myClassmates = oldClassmates + ", " + newClassmate
// Result: "John, Jane, Mark"
Notice that the each String may be a Variable or a Constant. Declare it as a Constant if you're only gonna change the value once.
2) String Interpolation
let myClassmates = "\(oldClassmates), \(newClassmate)"
// Result: "John, Jane, Mark"
3) Appending
let myClassmates = oldClassmates.appending(newClassmate)
// Result: "John, Jane, Mark"
Refer to Strings & Characters from the Swift Book for more.
Update: Tested on Swift 5.1
Swift 4.2
You can also use an extension:
extension Array where Element == String? {
func compactConcate(separator: String) -> String {
return self.compactMap {
if let unwrappedString = $0,
unwrappedString.isEmpty {
return nil
} else {
return $0
}
}
.joined(separator: separator)
}
}
Use:
label.text = [m.firstName, m.lastName].compactConcate(separator: " ")
Result:
"The Man"
"The"
"Man"
From: Matt Neuburg Book “iOS 13 Programming Fundamentals with Swift.” :
To combine (concatenate) two strings, the simplest approach is to use the + operator:
let s = "hello"
let s2 = " world"
let greeting = s + s2
This convenient notation is possible because the + operator is overloaded: it does one thing when the operands are numbers (numeric addition) and another when the operands are strings (concatenation).
The + operator comes with a += assignment shortcut; naturally, the variable on the left side must have been declared with var:
var s = "hello"
let s2 = " world"
s += s2
As an alternative to +=, you can call the append(_:) instance method:
var s = "hello"
let s2 = " world"
s.append(s2)
Another way of concatenating strings is with the joined(separator:) method. You start with an array of strings to be concatenated, and hand it the string that is to be inserted between all of them:
let s = "hello"
let s2 = "world"
let space = " "
let greeting = [s,s2].joined(separator:space)
Swift 5:
Array of strings into a Single string
let array = ["Ramana","Meharshi","Awareness","Oneness","Enlightnment","Nothing"]
let joined = array.joined(separator: ",")
Swift concatenate strings
Several words about performance
UI Testing Bundle on iPhone 7(real device), iOS 14, -Onone(debug, without optimizations)[About]
var result = ""
for i in 0...count {
<concat_operation>
}
Count = 5_000
//Append
result.append(String(i)) //0.007s 39.322kB
//Plus Equal
result += String(i) //0.006s 19.661kB
//Plus
result = result + String(i) //0.130s 36.045kB
//Interpolation
result = "\(result)\(i)" //0.164s 16.384kB
//NSString
result = NSString(format: "%#%i", result, i) //0.354s 108.142kB
//NSMutableString
result.append(String(i)) //0.008s 19.661kB
Disable next tests:
Plus up to 100_000 ~10s
interpolation up to 100_000 ~10s
NSString up to 10_000 -> memory issues
Count = 1_000_000
//Append
result.append(String(i)) //0.566s 5894.979kB
//Plus Equal
result += String(i) //0.570s 5894.979kB
//NSMutableString
result.append(String(i)) //0.751s 5891.694kB
*Note about Convert Int to String
Source code
import XCTest
class StringTests: XCTestCase {
let count = 1_000_000
let metrics: [XCTMetric] = [
XCTClockMetric(),
XCTMemoryMetric()
]
let measureOptions = XCTMeasureOptions.default
override func setUp() {
measureOptions.iterationCount = 5
}
func testAppend() {
var result = ""
measure(metrics: metrics, options: measureOptions) {
for i in 0...count {
result.append(String(i))
}
}
}
func testPlusEqual() {
var result = ""
measure(metrics: metrics, options: measureOptions) {
for i in 0...count {
result += String(i)
}
}
}
func testPlus() {
var result = ""
measure(metrics: metrics, options: measureOptions) {
for i in 0...count {
result = result + String(i)
}
}
}
func testInterpolation() {
var result = ""
measure(metrics: metrics, options: measureOptions) {
for i in 0...count {
result = "\(result)\(i)"
}
}
}
//Up to 10_000
func testNSString() {
var result: NSString = ""
measure(metrics: metrics, options: measureOptions) {
for i in 0...count {
result = NSString(format: "%#%i", result, i)
}
}
}
func testNSMutableString() {
let result = NSMutableString()
measure(metrics: metrics, options: measureOptions) {
for i in 0...count {
result.append(String(i))
}
}
}
}
You could use SwiftString (https://github.com/amayne/SwiftString) to do this.
"".join(["string1", "string2", "string3"]) // "string1string2string"
" ".join(["hello", "world"]) // "hello world"
DISCLAIMER: I wrote this extension
In Swift 5 apple has introduces Raw Strings using # symbols.
Example:
print(#"My name is "XXX" and I'm "28"."#)
let name = "XXX"
print(#"My name is \#(name)."#)
symbol # is necessary after \. A regular \(name) will be interpreted as characters in the string.

Swift Error Cannot convert value of type '[String.Element]' (aka 'Array<Character>') to expected argument type '[String]'

I am trying to create an array of letters from a given word by using the following Swift code (I have an array of words for allWords, but for simplicity I've just added an example word there for now):
let allWords = ["Leopards"]
var arrayOfLetters = Array(allWords[0])
let everyPossibleArrangementOfLetters = permute(list: arrayOfLetters)
func permute(list: [String], minStringLen: Int = 3) -> Set<String> {
func permute(fromList: [String], toList: [String], minStringLen: Int, set: inout Set<String>) {
if toList.count >= minStringLen {
set.insert(toList.joined(separator: ""))
}
if !fromList.isEmpty {
for (index, item) in fromList.enumerated() {
var newFrom = fromList
newFrom.remove(at: index)
permute(fromList: newFrom, toList: toList + [item], minStringLen: minStringLen, set: &set)
}
}
}
var set = Set<String>()
permute(fromList: list, toList:[], minStringLen: minStringLen, set: &set)
return set
}
I obtained this code from: Calculate all permutations of a string in Swift
But the following error is presented:
Cannot convert value of type '[String.Element]' (aka 'Array') to expected argument type '[String]'
I attempted the following, which works, but it takes over 10 seconds per word (depending on number of repeat letters) and I was hoping to find a better solution.
var arrayOfLetters: [String] = []
for letter in allWords[0] {
arrayOfLetters.append(String(letter))
}
let everyPossibleArrangementOfLetters = permute(list: arrayOfLetters)
I wasn't able to get the following solution to work, although I think is has promise I couldn't get past the productID name of items in the array whereas my array items aren't named...
Migration from swift 3 to swift 4 - Cannot convert String to expected String.Element
I'm also creating another array and checking each of those words to ensure their validity, and I run into the same error which I correct in the same way with array.append which is adding a lot more time in that location as well.
var everyPossibleArrangementOfLettersPartDeux: [String] = []
for word in everyPossibleArrangementOfLetters {
everyPossibleArrangementOfLettersPartDeux.append(word)
}
numberOfRealWords = possibleAnagrams(wordArray: everyPossibleArrangementOfLettersPartDeux)
func possibleAnagrams(wordArray: [String]) -> Int {
func isReal(word: String) -> Bool {
let checker = UITextChecker()
let range = NSMakeRange(0, word.utf16.count)
let misspelledRange = checker.rangeOfMisspelledWord(in: word, range: range, startingAt: 0, wrap: false, language: "en")
return misspelledRange.location == NSNotFound
}
var count = 0
for word in wordArray {
if isReal(word: word) {
count += 1
//print(word)
}
}
return count
}
I'm hoping the same replacement for array.append will work in both spots.
The problem is that Array(allWords[0]) produces [Character] and not the [String] that you need.
You can call map on a String (which is a collection of Characters and use String.init on each character to convert it to a String). The result of the map will be [String]:
var arrayOfLetters = allWords[0].map(String.init)
Notes:
When I tried this in a Playground, I was getting the mysterious message Fatal error: Only BidirectionalCollections can be advanced by a negative amount. This seems to be a Playground issue, because it works correctly in an app.
Just the word "Leopards" produces 109,536 permutations.
Another Approach
Another approach to the problem is to realize that permute doesn't have to work on [String]. It could use [Character] instead. Also, since you are always starting with a String, why not pass that string to the outer permute and let it create the [Character] for you.
Finally, since it is logical to think that you might just want anagrams of the original word, make minStringLen an optional with a value of nil and just use word.count if the value is not specified.
func permute(word: String, minStringLen: Int? = nil) -> Set<String> {
func permute(fromList: [Character], toList: [Character], minStringLen: Int, set: inout Set<String>) {
if toList.count >= minStringLen {
set.insert(String(toList))
}
if !fromList.isEmpty {
for (index, item) in fromList.enumerated() {
var newFrom = fromList
newFrom.remove(at: index)
permute(fromList: newFrom, toList: toList + [item], minStringLen: minStringLen, set: &set)
}
}
}
var set = Set<String>()
permute(fromList: Array(word), toList:[], minStringLen: minStringLen ?? word.count, set: &set)
return set
}
Examples:
print(permute(word: "foo", minStringLen: 1))
["of", "foo", "f", "fo", "o", "oof", "oo", "ofo"]
print(permute(word: "foo"))
["foo", "oof", "ofo"]
This line is returning a Character array, not a String one:
var arrayOfLetters = Array(allWords[0])
You can convert this to a String array like so:
var arrayOfLetters = Array(allWords[0]).map{String($0)}
You could alternatively write:
var arrayOfLetters = allWords[0].characters.map{String($0)}
If, it is optional character or string
usedLetters.append(currentWord.randomElement().map(String.init)!)
Here usedLetters is Array[String]
currentWord is Optional string

How to loop through array to find 4 identical values that are consecutive?

I have the following swift array:
var winSuitArray = [cardSuit1, cardSuit2, cardSuit3, cardSuit4, cardSuit5, cardSuit6, cardSuit7]
cardSuit1, cardSuit2 and so on, are variables that will equal strings like "clubs" or "hearts". What I want to do is loop through this array, and if the loop finds 4 identical objects whose indexes are consecutive, set the winSuitStatus bool to true.
For example, if the array looks like this:
["hearts", "clubs", "clubs", "clubs", "clubs", "diamonds", "spades"]
I want to loop through it like so:
for card in winSuitArray {
//find 4 identical and consecutive objects
// if the above requirements are met, let winSuitStatus = true
}
Is this possible to do?
To tell the truth, I'd probably do something similar to #KnightOfDragon's answer. There's nothing wrong with that approach. But this problem opens up some opportunities to build some much more reusable code at little cost, so it seems worth a little trouble to do that.
The basic problem is that you want to create a sliding window of a given size over the list, and then you want to know if any of the windows contain only a single value. So the first issue to to create these windows. We can do that very generally for all collections, and we can do it lazily so we don't have to compute all the windows (we might find our answer at the start of the list).
extension Collection {
func slidingWindow(length: Int) -> AnyRandomAccessCollection<SubSequence> {
guard length <= count else { return AnyRandomAccessCollection([]) }
let windows = sequence(first: (startIndex, index(startIndex, offsetBy: length)),
next: { (start, end) in
let nextStart = self.index(after: start)
let nextEnd = self.index(after: end)
guard nextEnd <= self.endIndex else { return nil }
return (nextStart, nextEnd)
})
return AnyRandomAccessCollection(
windows.lazy.map{ (start, end) in self[start..<end] }
)
}
}
The use of AnyRandomAccessCollection here is to just hide the lazy implementation detail. Otherwise we'd have to return a LazyMapSequence<UnfoldSequence<(Index, Index), ((Index, Index)?, Bool)>, SubSequence>, which would be kind of crazy.
Now are next question is whether all the elements in a window are equal. We can do that for any kind of Equatable sequence:
extension Sequence where Iterator.Element: Equatable {
func allEqual() -> Bool {
var g = makeIterator()
guard let f = g.next() else { return true }
return !contains { $0 != f }
}
}
And with those two pieces, we can just ask our question. In the windows of length 4, are there any runs that area all equal?
let didWin = suits.slidingWindow(length: 4).contains{ $0.allEqual() }
Or we could go a little different way, and create a SlidingWindowSequence that we could iterate over. The logic here is basically the same. This just wraps up the windowing into a specific type rather than a AnyRandomAccessCollection. This may be overkill for this problem, but it demonstrates another powerful pattern.
public struct SlidingWindowSequence<Base: Collection>: Sequence, IteratorProtocol {
let base: Base
let windowSize: Base.IndexDistance
private var windowStart: Base.Index
public init(_ base: Base, windowSize: Base.IndexDistance) {
self.base = base
self.windowSize = windowSize
self.windowStart = base.startIndex
}
public mutating func next() -> Base.SubSequence? {
if base.distance(from: windowStart, to: base.endIndex) < windowSize {
return nil
}
let window = base[windowStart..<base.index(windowStart, offsetBy: windowSize)]
windowStart = base.index(after: windowStart)
return window
}
}
let didWin = SlidingWindowSequence(suits, windowSize: 4).contains{ $0.allEqual() }
var suit = ""
var count = 1
for card in winSuitArray {
if(suit == card)
{
count++
}
else
{
count = 1
suit = card
}
if(count == 4)
{
//find 4 identical and consecutive objects
// if the above requirements are met, let winSuitStatus = true
}
}
You can use a counter variable to do this, initialized to 1.
for each value in array:
if value equals previous value
increment counter
else
counter = 1
if counter >= 4
set winCounter to true

Swift array loop once, write many

Consider the following silly, simple example:
let arr = ["hey", "ho"]
let doubled = arr.map {$0 + $0}
let capitalized = arr.map {$0.capitalizedString}
As you can see, I'm processing the same initial array in multiple ways in order to end up with multiple processed arrays.
Now imagine that arr is very long and that I have many such processes generating many final arrays. I don't like the above code because we are looping multiple times, once for each map call. I'd prefer to loop just once.
Now, obviously we could handle this by brute force, i.e. by starting with multiple mutable arrays and writing into all of them on each iteration:
let arr = ["hey", "ho"]
var doubled = [String]()
var capitalized = [String]()
for s in arr {
doubled.append(s + s)
capitalized.append(s.capitalizedString)
}
Fine. But now we don't get the joy of using map. So my question is: is there a better, Swiftier way? In a hazy way I imagine myself using map, or something like map, to generate something like a tuple and magically splitting that tuple out into all resulting arrays as we iterate, as if I could say something like this (pseudocode, don't try this at home):
let arr = ["hey", "ho"]
let (doubled, capitalized) = arr.map { /* ???? */ }
If I were designing my own language, I might even permit a kind of splatting by assignment into a pseudo-array of lvalues:
let arr = ["hey", "ho"]
let [doubled, capitalized] = arr.map { /* ???? */ }
No big deal if it can't be done, but it would be fun to be able to talk this way.
How about a function, multimap, that takes a collection of transformations, and applies each one, returning them as an array of arrays:
// yay protocol extensions
extension SequenceType {
// looks like T->U works OK as a constraint
func multimap
<U, C: CollectionType
where C.Generator.Element == Generator.Element->U>
(transformations: C) -> [[U]] {
return transformations.map {
self.map($0)
}
}
}
Then use it like this:
let arr = ["hey", "ho"]
let double: String->String = { $0 + $0 }
let uppercase: String->String = { $0.uppercaseString }
arr.multimap([double, uppercase])
// returns [["heyhey", "hoho"], ["HEY", "HO"]]
Or it might be quite nice in variadic form:
extension SequenceType {
func multimap<U>(transformations: (Generator.Element->U)...) -> [[U]] {
return self.multimap(transformations)
}
}
arr.multimap({ $0 + $0 }, { $0.uppercaseString })
Edit: if you want separate variables, I think the best you can do is a destructure function (which you have to declare n times for each n-tuple unfortunately):
// I don't think this can't be expressed as a protocol extension quite yet
func destructure<C: CollectionType>(source: C) -> (C.Generator.Element,C.Generator.Element) {
precondition(source.count == 2)
return (source[source.startIndex],source[source.startIndex.successor()])
}
// and, since it's a function, let's declare pipe forward
// to make it easier to call
infix operator |> { }
func |> <T,U>(lhs: T, rhs: T->U) -> U {
return rhs(lhs)
}
And then you can declare the variables like this:
let (doubled,uppercased)
= arr.multimap({ $0 + $0 }, { $0.uppercaseString }) |> destructure
Yes this is a teensy bit inefficient because you have to build the array then rip it apart – but that’s really not going to be material, since the arrays are copy-on-write and we’re talking about a small number of them in the outer array.
edit: an excuse to use the new guard statement:
func destructure<C: Sliceable where C.SubSlice.Generator.Element == C.Generator.Element>(source: C) -> (C.Generator.Element,C.Generator.Element) {
guard let one = source.first else { fatalError("empty source") }
guard let two = dropFirst(source).first else { fatalError("insufficient elements") }
return (one,two)
}
What is wrong with your suggestion of tuple?
let arr = ["hey", "ho"]
let mapped = arr.map {e in
return (e + e, e.capitalizedString)
}
How about this, we process 'capitalized' array while we map the 'doubled' array:
let arr = ["hey", "ho"]
var capitalized = [String]()
let doubled = arr.map {(var myString) -> String in
capitalized.append(myString.capitalizedString)
return myString + myString
}
//doubled ["heyhey", "hoho"]
//capitalized: ["Hey", "Ho"]

Resources