I would like to take an array of [String] and split it up into a given number of groups.
I have tried using this extension
extension Array {
func chunked(into size: Int) -> [[Element]] {
return stride(from: 0, to:count, by: size).map {
Array(self[$0 ..< Swift.min($0 + size, count)])
}
}
}
to split the array into a given number of elements per subarray, which for that function it works.
But to split it into a desired number of subarrays, I tried dividing the array.count by the desired number of teams, which works but only in certain circumstances.
If there are any extra elements, it puts them into an extra subarray at the end, and the number needs to come out even if I want this to work perfectly, which is the minority of the time.
So I guess this array.chunked function is not the solution in any way.
Maybe there is a way to do it with a for loop by taking an array.randomElement(), adding that to a variable (which would be a team) and then removing that element from the original array, and iterating over it until the original array is empty. And end up with an array of subarrays which would be the teams, or just separate variables which would be the teams. It could be any of those options.
Any ideas on how to do this?
Think about how you deal cards.
If you have 7 players, you start with one player and go around, giving one card at a time to each player. At the end, you may run out of cards before giving everybody the same number of cards. Some people may have n cards, and some may have n-1. That's the best that you can do.
You could implement the same thing with a source array and your destination arrays. Remove one element at a time from the source array, and "round-robbin" add it to one of the destination arrays until the source array is exhausted.
That code might look like this:
func splitArray<T>(array: [T], subArrayCount: Int) -> [[T]] {
// Create an empty array of arrays
var result = [[T]]()
// Create the empty inner string arrays
for _ in 1...subArrayCount {
let innerArray = [T]()
result.append(innerArray)
}
for (index, element) in array.enumerated() {
result[index % subArrayCount].append(element)
}
return result
}
And to test it:
let string = "Now is the time for all good programmers to babble incoherently. The rain in spain falls mainly on the plain. Fourscore and seven years ago our forefathers brought forth to this continent a new nation conceived in liberty and dedicated to the cause that all men are created equal."
let array = string.split(separator: " ")
.map { String($0) }
let subArrays: [[String]] = splitArray(array: array, subArrayCount: 5)
for (index, array) in subArrays.enumerated() {
let countString = String(format: "%2d", array.count)
print ("Array[\(index)]. Count = \(countString). Contents = \(array)")
}
The output of that test is:
Array[0]. Count = 10. Contents = ["Now", "all", "incoherently.", "falls", "Fourscore", "our", "this", "conceived", "to", "men"]
Array[1]. Count = 10. Contents = ["is", "good", "The", "mainly", "and", "forefathers", "continent", "in", "the", "are"]
Array[2]. Count = 10. Contents = ["the", "programmers", "rain", "on", "seven", "brought", "a", "liberty", "cause", "created"]
Array[3]. Count = 10. Contents = ["time", "to", "in", "the", "years", "forth", "new", "and", "that", "equal."]
Array[4]. Count = 9. Contents = ["for", "babble", "spain", "plain.", "ago", "to", "nation", "dedicated", "all"]
Related
Based on straight SQL searches in a previous app, I am adding CoreData searching to a new app. These searches are in a custom dictionary db that the app contains; this function does the work:
public func wordMatcher (pad: Int, word: Array<String>, substitutes : Set<String> ) {
let context = CoreDataManager.shared.persistentContainer.viewContext
var query: Array<String>
var foundPositions : Set<Int> = []
var searchTerms : Array<String> = []
if word.count >= 4 {
for i in 0..<word.count {
for letter in substitutes {
query = word
query[i] = letter
searchTerms.append(query.joined())
let rq: NSFetchRequest<Word> = Word.fetchRequest()
rq.predicate = NSPredicate(format: "name LIKE %#", query.joined())
rq.fetchLimit = 1
do {
if try context.fetch(rq).count != 0 {
foundPositions.insert(i)
break
}
} catch {
}
}
// do aggregated searchTerms search here instead of individual searches?
}
}
}
The NSFetchRequest focuses on one permutation at a time. But I'm accumulating the search string fragments in the array searchTerms because I don't know if it would be more efficient to construct a single query connected with ORs, and I also don't know how to do that in CoreData.
The focus is on the positions in the original term word: I need to indicate if any given location has at least one of the substitutes as a valid fit. So to implement the aggregate searchTerms approach, a FetchRequest would have to happen for each location in the base term.
A second complication is the one referred to in the title of the question. I am using LIKE because the search term in the FetchRequest could be a substring in a longer word. However, the maximum number of letters is 11, and pad is the starting point of the original term in that field of 11 spaces.
So if pad is 3, then I would need to allow for 0..<pad preceding characters. And because there may be trailing characters, I would also want results with 0..<(11 - (pad + word.count)) alphabetic characters after the last letter in the search term.
Regex seems like one way to do this, but I haven't found a clear example of how to do this in this case, and especially with the multiple search terms (if that's the way to go). The limits of SQLite in the previous version forced constructing multiple queries with increasing numbers of "_" underscores to indicate the padding characters; that tended to really explode the number of queries.
BTW, substitutes is limited to an absolute maximum of 9 values, and in practice is usually below 5, so things are a little more manageable.
I would like to get a grip on this, and so if anyone can provide direction or examples that can make this a reasonably efficient function, the help is appreciated greatly.
EDIT:
I've realized that I need a result for each position in the target string, with cases where the leading and trailing spaces also may need to contain a substitute as well.
So I'm moving to this:
public func wordMatcher (pad: Int, word: Array<String>, substitutes : Set<String> ) {
let context = CoreDataManager.shared.persistentContainer.viewContext
var pad_ = pad
var query: Array<String>
var foundPositions : Set<Int> = []
let rq: NSFetchRequest<Word> = Word.fetchRequest()
rq.fetchLimit = 1
let subs = "[\(substitutes.joined())]"
// if word.count >= 4 { // because those locations will be blocked off anyway otherwise
let start = pad > 0 ? -1 : 0
let finish = 11 - (pad + word.count) > 0 ? word.count + 1 : word.count
for i in start..<finish {
query = word
var _pad = 11 - (pad + word.count)
if i == -1 {
query = Array(arrayLiteral: subs) + query
pad_ -= 1
} else if i > word.count {
query.append(subs)
_pad -= 1
} else {
pad_ = pad
query[i] = subs
}
let endPad = _pad > 0 ? "{0,\(_pad)}" : ""
let predMatch = ".\(query.joined())\(endPad)"
print(predMatch)
rq.predicate = NSPredicate(format:"position <= %# AND word MATCHES %#", pad_, predMatch)
do {
if try context.fetch(rq).count != 0 {
foundPositions.insert(i)
}
} catch {
}
// }
}
lFreq = foundPositions
}
This relies on a regex substitution, inserted into the original target string. What I'll have to find out is if this is fast enough at the edge cases, but it may not be critical even in the worst case.
predMatch will end up looking something like "ab[xyx]d{0,3}", and I think I can get rid of the position section by changing it to be "{0,2}ab[xyx]d{0,3}". But I guess I'm going to have to try to find out.
For example I need to get all english "text" values of "ex" scope from JSON example
What I do:
let result = json["def"].arrayValue.map({ $0["tr"].arrayValue.map { $0["ex"] } })
but at as a result I got a double massive and if I intend to get all "text" then I will get a triple array. Guess should be another more elegant approach to this task. Is anyone can show a really good solution?
If your expression gives 3 arrays of Strings you could add .reduce([], +) at the end to join the 3 arrays into
one.
EDIT:
I was typing from memory, and said the wrong thing. You would use reduce, not joined.
let result = json["def"]
.arrayValue
.map({ $0["tr"].arrayValue.map { $0["ex"] } })
.reduce([], +)
That should give you what you want.
EDIT #2:
The reduce function operates on sequences (like arrays). It takes an "initial result" value that seeds the process, and then a closure that operates on 2 elements, and returns a result. (a "binary" closure)
The + operator is actually a binary closure in Swift. It takes 2 elements and returns a single result, so you can simply pass in + instead of a closure. For arrays, + returns the result of combining the arrays.
So when you use reduce() on an array of arrays, and [] as the initial result, it combines [] with the first array, then the result of each + operator with the next entry in the array.
Take this simplified code for example:
//Start with an array of arrays of strings
let arrays = [
["string1", "string2", "string3"],
["string4", "string5", "string6"],
["string7", "string8", "string9"]
]
//First loop through the outer arrays and log their contents
for (index, object) in arrays.enumerated() {
print("array[\(index)] = \(object)")
}
//Now combine the outer arrays into a single array
let combined = arrays.reduce([], + )
//Now print the entries in the combined array
print("\n---- combined arrays ----")
for (index, object) in combined.enumerated() {
print("array[\(index)] = \(object)")
}
That produces the following output:
array[0] = ["string1", "string2", "string3"]
array[1] = ["string4", "string5", "string6"]
array[2] = ["string7", "string8", "string9"]
---- combined arrays ----
array[0] = string1
array[1] = string2
array[2] = string3
array[3] = string4
array[4] = string5
array[5] = string6
array[6] = string7
array[7] = string8
array[8] = string9
I have written an algorithm to create a section index for a tableview.
Unfortunately I have a bug when the list contains only one item the result is empty.
Do you have an elegant solution for that?
var sections : [(index: Int, length :Int, title: String)] = Array()
func createSectionIndices(participants: List<Participant>){
sections.removeAll()
var index = 0;
let array = participants.sort({$0.lastName < $1.lastName})
for i in 0.stride(to: array.count, by: 1){
let commonPrefix = array[i].lastName.commonPrefixWithString(array[index].lastName, options: .CaseInsensitiveSearch)
if (commonPrefix.isEmpty) {
let string = array[index].lastName.uppercaseString;
let firstCharacter = string[string.startIndex]
let title = "\(firstCharacter)"
let newSection = (index: index, length: i - index, title: title)
sections.append(newSection)
index = i;
}
}
print("sectionCount: \(sections.count)")
}
Here's a one line solution to build the sections list:
var participants:[(firstName:String, lastName:String)] =
[
("John", "Smith"),
("Paul", "smith"),
("Jane", "Doe"),
("John", "denver"),
("Leo", "Twain"),
("Jude", "law")
]
// case insensitive sort (will keep "denver" and "Doe" together)
participants = participants.sort({$0.lastName.uppercaseString < $1.lastName.uppercaseString})
// The process:
// - get first letter of each name (in uppercase)
// - combine with indices (enumerate)
// - only keep first occurrence of each letter (with corresponding indice)
// - build section tuples using index, letter and number of participants with name begining with letter
let sections = participants
.map({String($0.lastName.uppercaseString.characters.first!)})
.enumerate()
.filter({ $0 == 0 || !participants[$0 - 1].lastName.uppercaseString.hasPrefix($1) })
.map({ (start,letter) in return
(
index: start,
length: participants.filter({$0.lastName.uppercaseString.hasPrefix(letter)}).count,
title: letter
)
})
// sections will contain:
// (index:0, length:2, title:"D")
// (index:2, length:1, title:"L")
// (index:3, length:2, title:"S")
// (index:5, length:1, title:"T")
You may already have a lot of existing code based on the sections being stored in an array of tuples but, if not, I would suggest you approach this a little differently and build your sections array with the letter AND the participant data.
let sections = participants
.map({ String($0.lastName.uppercaseString.characters.first!) })
.reduce( Array<String>(), combine: { $0.contains($1) ? $0 : $0 + [$1] })
.map({ (letter) in return
(
title: letter,
participants: participants.filter({$0.lastName.uppercaseString.hasPrefix(letter)})
)
})
This would allow you to respond to the number of sections with sections.count but will also make it easier to manipulate index paths and data within each section:
number of participants in a section : sections[index].participants.count
participant at index path : sections[indexPath.section].participants[indexPath.row]
This is just syntactic candy but if you have a lot of references to the participants list, it will make the code more readable.
Also, if your participants are objects rather than tuples or structs, you can even update the data in you main participant list without having to rebuild the sections (unless a last name is changed).
[EDIT] fixed errors in last tuple syntax
[EDIT2] Swift 4 ...
The dictionaries in Swift 4 provide a much easier way to manage this kind of thing.
for the original reference structure:
let sections = [Character:[Int]](grouping:participants.indices)
{participants[$0].lastName.uppercased().first!}
.map{(index:$1.reduce(participants.count,min), length:$1.count, title:String($0))}
.sorted{$0.title<$1.title}
.
and for a section structure that contains its own sub-lists of participants (my recommendation):
let sectionData = [Character:[Participant]](grouping:participants)
{$0.lastName.uppercased().first!}
.map{(title:$0, participants:$1)}
.sorted{$0.title<$1.title}
Sorry for the complex wording of the question. My main experience is with PHP and it has a command called array_multisort. The syntax is below:
bool array_multisort ( array &$array1 [, mixed $array1_sort_order = SORT_ASC [, mixed $array1_sort_flags = SORT_REGULAR [, mixed $... ]]] )
It lets you sort 1 array and the reorder multiple other arrays based on the key changes in the original.
Is there an equivalent command in Swift / Xcode 7.2?
I have currently have a set of arrays:
FirstName
Age
City
Country
Active
Active is an array of time in seconds that a user has been active within my app. I would like to order that descending or ascending and the other arrays to change to remain consistent.
You could create an array of indexes in sorted order and use it as a mapping:
var names = [ "Paul", "John", "David" ]
var ages = [ 35, 42, 27 ]
let newOrder = names.enumerate().sort({$0.1<$1.1}).map({$0.0})
names = newOrder.map({names[$0]})
ages = newOrder.map({ages[$0]})
[EDIT] Here's an improvement on the technique :
It's the same approach but does the sorting and assignment in one step.
(can be reassigned to original arrays or to separate ones)
(firstNames,ages,cities,countries,actives) =
{(
$0.map{firstNames[$0]},
$0.map{ages[$0]},
$0.map{cities[$0]},
$0.map{countries[$0]},
$0.map{actives[$0]}
)}
(firstNames.enumerated().sorted{$0.1<$1.1}.map{$0.0})
[EDIT2] and an Array extension to make it even easier to use if you are sorting in place:
extension Array where Element:Comparable
{
func ordering(by order:(Element,Element)->Bool) -> [Int]
{ return self.enumerated().sorted{order($0.1,$1.1)}.map{$0.0} }
}
extension Array
{
func reorder<T>(_ otherArray:inout [T]) -> [Element]
{
otherArray = self.map{otherArray[$0 as! Int]}
return self
}
}
firstNames.ordering(by: <)
.reorder(&firstNames)
.reorder(&ages)
.reorder(&cities)
.reorder(&countries)
.reorder(&actives)
combining the previous two:
extension Array
{
func reordered<T>(_ otherArray:[T]) -> [T]
{
return self.map{otherArray[$0 as! Int]}
}
}
(firstNames,ages,cities,countries,actives) =
{(
$0.reordered(firstNames),
$0.reordered(ages),
$0.reordered(cities),
$0.reordered(countries),
$0.reordered(actives)
)}
(firstNames.ordering(by:<))
I would go with #AntonBronnikov suggestion, and put all your properties into an struct, making an Array of that particular struct and then sorting it.
This data is clearly related and it's a cleaner approach.
Edit this is valid for 2 arrays:
Adding to #AlainT answer, but using zip:
var names = [ "Paul", "John", "David" ]
var ages = [ 35, 42, 27 ]
let sortedTuple = zip(names, ages).sort { $0.0.0 < $0.1.0 }
Something more generic:
names.enumerate().sort({$0.1<$1.1}).map({ (name: $0.1, age: ages[$0.0]) })
I believe AlainT:s solution is to prefer, but to extend the variety of options, below follows a solution mimicking what a zip5 method could let us achive (in case we could use zip for zipping together 5 sequences instead of its limit of 2):
/* example arrays */
var firstName: [String] = ["David", "Paul", "Lisa"]
var age: [Int] = [17, 27, 22]
var city: [String] = ["London", "Rome", "New York"]
var country: [String] = ["England", "Italy", "USA"]
var active: [Int] = [906, 299, 5060]
/* create an array of 5-tuples to hold the members of the arrays above.
This is an approach somewhat mimicking a 5-tuple zip version. */
var quinTupleArr : [(String, Int, String, String, Int)] = []
for i in 0..<firstName.count {
quinTupleArr.append((firstName[i], age[i], city[i], country[i], active[i]))
}
/* sort w.r.t. 'active' tuple member */
quinTupleArr.sort { $0.4 < $1.4 }
/* map back to original arrays */
firstName = quinTupleArr.map {$0.0}
age = quinTupleArr.map {$0.1}
city = quinTupleArr.map {$0.2}
country = quinTupleArr.map {$0.3}
active = quinTupleArr.map {$0.4}
Say I have two arrays:
let arrayOne = ["Hi", "Hi", "Hello", "Not Hey", "Howdy", "Hi"]
let arrayTwo = ["Hi", "Hello", "Hey", "Not Howdy", "Hi", "Hi"]
and I have this for loop that gets the percent similarity of the doubles:
var matches = 0
for (index, item) in enumerate(arrayOne) {
if item == arrayTwo[index] {
matches++
}
}
However, what if those arrays are longer and I want data points of those similarities instead of one single calculation. What kind of a function could I write where it takes the first 5 elements off the array, returns their similarity with the for loop, and moves on to the next 5? ie. it would be a function that takes two string arrays and returns an array of doubles. I am sure this is a simple question but I do not know how to approach taking 5 array elements at a time and then returning an array of doubles (like data points the the string array similarities).
I’m not clear quite what you’re asking, however, you might find playing around with zip, map, and reduce helpful.
For example, you could rewrite your original loop like this (assuming Swift 2.0, you’d have to rearrange slightly for 1.2):
zip(arrayOne, arrayTwo).reduce(0) { $0 + ($1.0 == $1.1 ? 1 : 0) }
// returns 4
zip creates a new sequence of the pairs of elements at each corresponding position.
reduce takes a starting value, then keeps a running value by applying a function to the current value and the next value in the sequence – bearing in mind this is a sequence of pair elements, you want to add 1 when they are the same, 0 when they aren’t. This then gives you a count of the positions where both match.
If instead you wanted an array, with true representing a match at that point, and false if different, you could use map:
zip(arrayOne, arrayTwo).map(==)
// returns [true, false, false, false, false, true]
If on the other hand you wanted a list of the differences in string length between the two strings at each position, you could change it to:
zip(arrayOne, arrayTwo).map { (a,b) in
a.characters.count - b.characters.count
}
// returns [0, -3, 2, -2, 3, 0]
As some have suggested, Set might help, e.g.:
let commonElements = Set(arrayOne).intersect(arrayTwo)
// returns strings present in both e.g. {"Hi", "Hello”}
This is a good approach if you are OK treating your data as a set i.e. order doesn’t matter and duplicates can be ignored. If order and dupes do matter, you probably have to stick with arrays.
Take a look at this. I think it does what you are asking. By introducing a count variable to keep track of the number of items you have processed, you will know when to update your counts array:
var matches = 0
let arrayOne = ["Hi", "Hi", "Hello", "Not Hey", "Howdy", "Hi", "a", "b", "c"]
let arrayTwo = ["Hi", "Hello", "Hey", "Not Howdy", "Hi", "Hi", "a", "B", "C"]
var count = 0
var counts:[Int] = []
for (index, item) in enumerate(arrayOne) {
if item == arrayTwo[index] {
matches++
}
// Have we done 5? If so, time to update counts array
if ++count == 5 {
counts.append(matches)
count = 0
matches = 0
}
}
// If we didn't have 5, just append the matches for the remaining items
if count > 0 {
counts.append(matches)
}
println(counts) // prints "[1, 2]"
Right - so I think I understand what you're looking for: you want to have a matches function like the one you've written that works for chunks of a certain number of elements. First off, you're going to need a chunk function. There's a good discussion of them here, but they're for arrays, and you're going to want to zip your two arrays together here, so you'll need one for SequenceType. This works:
public extension SequenceType {
/// Returns an array of arrays of n non-overlapping elements of self
/// - Parameter n: The size of the chunk
/// ```swift
/// [1, 2, 3, 4, 5].chunk(2)
///
/// [[1, 2], [3, 4], [5]]
/// ```
func chunk(_ n: Int) -> [[Generator.Element]] {
var g = self.generate()
var ret: [[Generator.Element]] = [[]]
while let next = g.next() {
if ret.last!.count < n {
ret[ret.endIndex.predecessor()].append(next)
} else {
ret.append([next])
}
}
return ret
}
}
Then, you need a function that counts the matches in two arrays. You could inline it with a closure, or you could define it separately, it doesn't make much of a difference.
func matchesEvery<
S0 : SequenceType,
S1 : SequenceType,
T : Equatable where
S0.Generator.Element == T,
S1.Generator.Element == T
>(_ n: Int, s0: S0, s1: S1) -> [Int] {
return zip(s0, s1)
.chunk(5)
.map { $0.reduce(0) { $1.0 == $1.1 ? $0 + 1 : $0 } }
}
That will return:
let arrayOne = ["Hi", "Hi", "Hello", "Not Hey", "Howdy", "Hi"]
let arrayTwo = ["Hi", "Hello", "Hey", "Not Howdy", "Hi", "Hi"]
matchesEvery(5, s0: arrayOne, s1: arrayTwo) // [1, 1]
Since in the first five there's one match, and in the last there is one as well.