How to sort 1 array in Swift / Xcode and reorder multiple other arrays by the same keys changes - ios

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}

Related

Swift Printing an Array in a table form String and also adding special characters

Lets assume i have this string
var stringCSV = "Beth,Charles,Danielle,Adam,Eric\n17945,10091,10088,3907,10132\n2,12,13,48,11";
And i converted it into a 2D Array
[["Beth", "Charles", "Danielle", "Adam", "Eric"], ["17945", "10091", "10088", "3907", "10132"], ["2", "12", "13", "48", "11"]]
Below is the code i used to convert the String into a 2D Array and sort it.
struct Person {
let name: String
let id: String
let age: String
}
var csvFormatted = [[String]]()
stringCSV.enumerateLines { (line, _) in
var res = line.split(separator: ",",omittingEmptySubsequences: false).map{
String($0)
}
for i in 0 ..< res.count {
res[i] = res[i]
}
csvFormatted.append(res)
}
let properties = zip(csvFormatted[1],csvFormatted[2])
let namesAndProperties = zip(csvFormatted[0],properties)
let structArray = namesAndProperties.map { (name, properties) in
return Person(name: name, id: properties.0, age: properties.1)
}
let sortedArray = structArray.sorted {
return $0.name < $1.name
}
for i in sortedArray {
print(i.name, i.id, i.age)
}
i get the below output
Adam 3907 48
Beth 17945 2
Charles 10091 12
Danielle 10088 13
Eric 10132 11
But I wont to understand and know how i can print the sorted array back to string, just like it was before i splitted it into an array and sorted it and including the special characters like "\n" and ",".
and achieve something the below
Adam,Beth,Charles,Danielle,Eric\n
3907,17945,10091,10088,10132\n
48,2,12,13,11
Use map for each property and join it to create a comma separated row. Then using the results in an array join again with a new line character.
let csv = [array.map(\.name).joined(separator: ","),
array.map(\.id).joined(separator: ","),
array.map(\.age).joined(separator: ",")]
.joined(separator: "\n")

Using data in an array to insert elements of an array into another in Swift

Hey so could do with some more experienced eyes looking at this issue I'm having.
I have two arrays of playable cards and non playable cards I need to have them as one array to pass into the game when it loads each level but the location of where the cards are added is important so I've made an array of the locations I want them inserted.
Here is my Card model and Level model with an example data
struct Card {
var content: String
var id: Int
var isFaceUp = false
var isMatched = false
}
struct Level {
let levelNumber: String
let numberOfNonCards: Int
let numberOfPlayableCards: Int
let locationsToPlacePlayableCards: [Int]
let winningScore: Int
}
static let levels = [
Level(levelNumber: "one", numberOfNonCards: 20, numberOfPlayableCards: 10, locationsToPlacePlayableCards: [3, 4, 6, 7, 9, 12, 20, 21, 22, 23], winningScore: 5) ]
}
and below is me trying to get this to work.
private static func createCardGame(level: Level) -> [Card] {
var cards = [Card]()
var playableCards = [Card]()
for indexPath in 0..<level.numberOfNonCards {
cards.append(Card(content: catCards[0], id: indexPath*3))
}
for indexPath in 0..<level.numberOfPlayableCards {
playableCards.append(Card(content: catCards[indexPath], id: indexPath*2))
playableCards.append(Card(content: catCards[indexPath], id: indexPath*2 + 5))
}
playableCards.shuffle()
var playingCardLocations = level.locationsToPlacePlayableCards
for indexPath in 0..<playingCardLocations.count {
cards.insert(contentsOf: playableCards[indexPath], at: playingCardLocations[indexPath])
}
return cards
}
This isn't working as I have to make Card conform to Collection which seems to then require it conforms to Sequence and then IteratorProtocol. So wondering what's the best way to implement those protocols? Because can't find much about them? Or is there a better way of doing this in your experience? Would really appreciate your help and input.
insert(contentsOf:at:) expects to be passed a collection of elements to be inserted but it's only being passed a single card.
There's another version of insert that takes a single element. The only change is removing contentsOf.
cards.insert(playableCards[indexPath], at: playingCardLocations[indexPath])
https://developer.apple.com/documentation/swift/array/3126951-insert
Another option is to put the card into an array before inserting it, but it's unnecessary when there's a method that takes a single value.
cards.insert(contentsOf: [playableCards[indexPath]], at: playingCardLocations[indexPath])

Filter dictionary of array objects

I am trying to filter my dictionary according to user input in UISearchController. I have following model and array of objects.
struct People {
var name: String
var id: Int
}
let first = People(name: "Atalay", id: 1)
let second = People(name: "Ahmet", id: 2)
let third = People(name: "Mehmet", id: 3)
let fourth = People(name: "Yusuf", id: 4)
let peoples: [People] = [first, second, third, fourth, fifth]
I put them into a dictionary to create section indexed table view with following code.
var dict: [String: [People]] = Dictionary(grouping: peoples, by: { (people) -> String in
return String(people.name.prefix(1))
})
Above code gives me a dictionary with first letter of People names. Now, I would like to filter my array according to user input. However, I tried following code for filtering but it is not working as I expected.
let filteredDict = (dict.filter { $0.1.contains { $0.name.lowercased().contains("ata") } })
It returns all "A" letter section indexes like ["A": People(name: "Atalay", id: 1), People(name: "Ahmet", id: 2)]
How can I achieve filter also my array inside dictionary?
If I'm not mistaken, you want your final dictionary to have all the keys and only the filtered array of items as the values. If that is right, reduce is the tool for that:
let filtered = dict.reduce(into: [String: [People]]()) {
$0[$1.key] = $1.value.filter { $0.name.lowercased().contains("ata") }
}
I decided it was simplest to get this right by using an old fashioned for loop and filter each group separately
var filtered = [String: [People]]()
for (k, v) in dict {
let result = v.filter {$0.name.lowercased().contains("ata")}
if result.count > 0 {
filtered[k] = result
}
}
Note that if you want to keep all the groups in the result dictionary just skip the if result.count > 0 condition
How can I achieve filter also my array inside dictionary?
You should have an array first, you can use flatMap to group all the values in your filteredDict
let array = filteredDict.flatMap { $0.value }
Then you just filter the array as usually
let filteredArray = array.filter { $0.name.lowercased().contains("ata") }

Swift: create algorithm for grouping array items

I have an array of arrays.
[
[3, 5],
[5, 1],
[9, 8],
[5, 3],
]
First two arrays have similar value "5". That means that they should be in one group. That means that all their values should be items in that group. So, first group will be 3, 5, 1.
As a result I should have all groups which has similar items:
[[3, 5, 1]]
The last array [5, 3] is not in the output because those items are already included. So, basically the output is array of Sets.
What is the best way to make this kind of algorithm?
The first that is coming to my mind is iteration, but that is very bad solution.
for example this set:
[[1,2], [2,3], [3,4], [5,6], [6,5], [12,11], [8,9],]
should give a results
[[1,2,3,4],[5,6]]
One way of doing this would be using a dictionary. You could accumulate all groups under their elements. Then each element that has more than one group is the one you are looking for. Take a look into this example:
func findClusterValues(inGroups groups: [[Int]]) -> [Int] {
var map: [Int: [[Int]]] = [Int: [[Int]]]()
groups.forEach { group in
group.forEach { element in
var items: [[Int]] = map[element] ?? [[Int]]()
items.append(group)
map[element] = items
}
}
var set: Set<Int> = Set<Int>()
map.forEach { key, value in
if value.count > 1 {
value.forEach { array in
array.forEach { item in
set.insert(item)
}
}
}
}
return Array<Int>(set)
}
This could be highly improved with some flat-maps to say the least. Also probably sets could already have been used inside the map dictionary instead of arrays of arrays. But this should be enough for demonstration for now.
So the actual numbers are used as keys here. And every key will include all groups that include element with the same value. When all is distributed we check if any of the keys has more than 1 groups and we extract all numbers from all of those groups and insert them into a Set. Set will automatically remove our duplicates for us and we can then return it back as an array (group).
A new algorithm as requested in comments needs a bit more work:
typealias Group = [Int]
class Cluster {
private(set) var uniqueElements: Set<Int> = Set<Int>()
private(set) var groups: [Group] = [Group]()
init() {}
convenience init(group: Group) {
self.init()
insertGroup(group)
}
func insertGroup(_ group: Group) {
groups.append(group)
group.forEach { uniqueElements.insert($0) }
}
func merge(cluster: Cluster?) {
guard let cluster = cluster, cluster !== self else { return }
cluster.groups.forEach { insertGroup($0) }
}
func deplete() {
groups = [Group]()
uniqueElements = Set<Int>()
}
}
func findClusterValues(inGroups groups: [Group]) -> [Group] {
var map: [Int: Cluster] = [Int: Cluster]()
groups.forEach { group in
let newCluster = Cluster(group: group)
newCluster.uniqueElements.forEach { value in
newCluster.merge(cluster: map[value])
}
newCluster.uniqueElements.forEach { value in
map[value] = newCluster
}
}
return map.compactMap { key, cluster in
guard cluster.groups.count > 1 else { return nil }
let group = Group(cluster.uniqueElements)
cluster.deplete()
return group
}
}
Now clusters are introduced which populate a part of dictionary that their elements correspond to. When a new cluster collides with existing one it basically consumes it and increases in size.
At the end the clusters that have only 1 group are removed. Those removed are then depleted (all groups removed) so that duplicates are avoided.

Create Dictionary<String, [SomeStruct]> from [SomeStruct] source-array

var sourceEntries: [Entry] = [entry1, ..., entry14]
var myDict: Dictionary<String, [Entry]> = [:]
for entry in sourceEntries {
if var array = myDict[entry.attribute1] { theArray.append(entry) }
else { myDict[entry.attribute1] = [entry] }
}
I am intending to create a Dictionary, which matches all the objects of the struct "Eintrag" with the same attribute from the source-Array "alleEinträge" to a String containing the value of the shared attribute. For some reason my final Dictionary just matches Arrays of one element to the Strings, although some Arrays ought to contain up to four elements.
The problem is that the array is passed by value (i.e. "copied"), so the array you are writing to when you say array.append is not the array that is "inside" the dictionary. You have to write back into the dictionary explicitly if you want to change what's in it.
Try it in a simple situation:
var dict = ["entry":[0,1,2]]
// your code
if var array = dict["entry"] { array.append(4) }
// so what happened?
println(dict) // [entry: [0, 1, 2]]
As you can see, the "4" never got into the dictionary.
You have to write back into the dictionary explicitly:
if var array = dict["entry"] { array.append(4); dict["entry"] = array }
FURTHER THOUGHTS: You got me thinking about whether there might be a more elegant way to do what you're trying to do. I'm not sure whether you will think this is "more elegant", but perhaps it has some appeal.
I will start by setting up a struct (like your Entry) with a name attribute:
struct Thing : Printable {
var name : String
var age : Int
var description : String {
return "{\(self.name), \(self.age)}"
}
}
Now I will create an array like your sourceEntries array, where some of the structs share the same name (like your shared attribute attribute1):
let t1 = Thing(name: "Jack", age: 40)
let t2 = Thing(name: "Jill", age: 38)
let t3 = Thing(name: "Jill", age: 37)
let arr = [t1,t2,t3]
And of course I will prepare the empty dictionary, like your myDict, which I call d:
var d = [String : [Thing]]()
Now I will create the dictionary! The idea is to use map and filter together to do all the work of creating key-value pairs, and then we just build the dictionary from those pairs:
let pairs : [(String, [Thing])] = arr.map {
t in (t.name, arr.filter{$0.name == t.name})
}
for pair in pairs { d[pair.0] = pair.1 }

Resources