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}
Related
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"]
I have an array of strings. I would like to display 3 unique items from this array randomly. Then every 5 seconds, one of the items gets replaced with another unique item (my idea here is adding an animation with a delay).
I can display the 3 strings, however sometimes they repeat, and the timer is not updating the factLabel label.
Here's my progress:
override func viewDidLoad() {
super.viewDidLoad()
updateUI()
}
func randomFact() -> String {
let arrayCount = model.cancunFacts.count
let randomIndex = Int(arc4random_uniform(UInt32(arrayCount)))
return model.cancunFacts[randomIndex]
}
// Display the facts
func updateUI() {
Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(randomFact), userInfo: nil, repeats: true)
factLabel.text = randomFact() + " " + randomFact() + " " + randomFact()
}
How do I get the text to always update randomly, without the 3 facts repeating?
Create an array of indexes. Remove a random index from the array, use it to index into your strings. When the array of indexes is empty, refill it.
Here is some sample code that will generate random, non-repeating strings:
var randomStrings = ["Traitor", "Lord Dampnut", "Cheeto-In-Chief",
"F***face Von Clownstick", "Short-Fingered Vulgarian",
"Drumpf", "Der Gropenführer", "Pumpkin in a suit"]
var indexes = [Int]()
func randomString() -> String {
if indexes.isEmpty {
indexes = Array(0...randomStrings.count-1)
}
let index = Int(arc4random_uniform(UInt32(indexes.count)))
let randomIndex = indexes.remove(at: index)
return randomStrings[randomIndex]
}
for i in 1...100 {
print (randomString())
}
(Note that it may still generate repeating strings in the case when the array of indexes is empty and it needs to be refilled. You'd need to add extra logic to prevent that case.)
Version 2:
Here is the same code, modified slightly to avoid repeats when the array of indexes is empty and needs to be refilled:
var randomStrings = ["tiny-fingered", "cheeto-faced", "ferret-wearing", "sh*tgibbon"]
var indexes = [Int]()
var lastIndex: Int?
func randomString() -> String {
if indexes.isEmpty {
indexes = Array(0...randomStrings.count-1)
}
var randomIndex: Int
repeat {
let index = Int(arc4random_uniform(UInt32(indexes.count)))
randomIndex = indexes.remove(at: index)
} while randomIndex == lastIndex
lastIndex = randomIndex
return randomStrings[randomIndex]
}
for i in 1...10000 {
print (randomString())
}
Even though it's using a repeat...while statement, the repeat condition will never fire twice in a row, because you'll never get a repeat except right after refilling the array of indexes.
With that code, if there is a repeat, the selected string will be skipped on that pass through the array. To avoid that, you'd need to adjust the code slightly to not remove a given index from the array until you verify that it is not a repeat.
Version 3:
Version 2, above, will skip an entry if it picks a repeat when it refills the array. I wrote a 3rd version of the code that refills the array, removes the last item it returned so that it can't repeat, and then adds it back to the array once it's picked a random item. This third version will always return every item in the source array before refilling it and will also never repeat an item. Thus it's truly random with no bias:
import UIKit
var randomStrings = ["Traitor", "Lord Dampnut", "Cheeto-In-Chief",
"F***face Von Clownstick", "Short-Fingered Vulgarian",
"Drumpf", "Der Gropenführer", "Pumpkin in a suit"]
var indexes = [Int]()
var lastIndex: Int?
var indexToPutBack: Int?
func randomString() -> String {
//If our array of indexes is empty, fill it.
if indexes.isEmpty {
indexes = Array(0...randomStrings.count-1)
print("") //Print a blank line each time we refill the array so you can see
//If we have returned an item previously, find and remove that index
//From the refilled array
if let lastIndex = lastIndex,
let indexToRemove = indexes.index(of: lastIndex) {
indexes.remove(at: indexToRemove)
indexToPutBack = indexToRemove //Remember the index we removed so we can put it back.
}
}
var randomIndex: Int
let index = Int(arc4random_uniform(UInt32(indexes.count)))
randomIndex = indexes.remove(at: index)
//If we refilled the array and removed an index to avoid repeats, put the removed index back in the array
if indexToPutBack != nil{
indexes.append(indexToPutBack!)
indexToPutBack = nil
}
lastIndex = randomIndex
return randomStrings[randomIndex]
}
for i in 1...30 {
print (randomString())
}
Sample output:
Short-Fingered Vulgarian
F***face Von Clownstick
Pumpkin in a suit
Drumpf
Lord Dampnut
Traitor
Der Gropenführer
Cheeto-In-Chief
Der Gropenführer
Drumpf
Lord Dampnut
Short-Fingered Vulgarian
Cheeto-In-Chief
Pumpkin in a suit
Traitor
F***face Von Clownstick
Short-Fingered Vulgarian
F***face Von Clownstick
Drumpf
Traitor
Cheeto-In-Chief
Lord Dampnut
Pumpkin in a suit
Der Gropenführer
Lord Dampnut
Short-Fingered Vulgarian
Pumpkin in a suit
Cheeto-In-Chief
Der Gropenführer
F***face Von Clownstick
Your timer is calling random fact, which simply returns a fact and doesn't do anything. You should probably have some third method called initializeTimer that does the Timer.scheduledtimer, which you should take out of your updateUI method. That timer should call updateUI. This would fix your labels updating. Also you would call initializeTimer in your viewDidLoad instead of updateUI. As far as preventing the repetition of facts, Duncan C's idea of having a separate array that you remove items from as you set new random facts then fill back up when it's empty seems like a good idea.
It may be easiest to maintain two arrays, usedStrings and unusedStrings, of random strings, like this:
var unusedStrings: [String] = ["king", "philip", "calls", "out", "for", "good", "soup"]
var usedStrings: [String] = []
func randomString() -> String {
if unusedStrings.isEmpty() {
unusedStrings = usedStrings
usedStrings = []
}
let randomIndex = Int(arc4random_uniform(unusedStrings.count))
let randomString = unusedStrings[randomIndex]
usedStrings.append(randomString)
return randomString
}
I have an array of Super Hero objects. I want to group the superheroes based on the name property into separated arrays and then count how many objects are in each individual separated array
Object:
class SuperHero{
var name: String?
var power: Bool?
}
Array of superheroes (there can be an infinite num of superheroes)
var superHeroes = [SuperHero]()
let superHero1 = SuperHero()
superHero1.name = "SuperMan"
superHero1.power = true
superHeroes.append(superHero1)
let superHero2 = SuperHero()
superHero2.name = "BatMan"
superHero2.power = true
superHeroes.append(superHero2)
let superHero3 = SuperHero()
superHero3.name = "BatMan"
superHero3.power = true
superHeroes.append(superHero3)
let superHero4 = SuperHero()
superHero4.name = "SuperMan"
superHero4.power = true
superHeroes.append(superHero4)
//etc...
Use name property to sort:
let sortedHeros = superHeroes.sort{$0.name < $1.name}
for hero in sortedHeros{
print(hero.name)
/*
prints
BatMan
BatMan
SuperMan
SuperMan
*/
}
How do I put the sorted array into separate arrays then print the count of each separated array?
//this is what I want
separatedArraysOfSuperHeroes = [[superHero2, superHero3], [superHero1, superHero4]]
//subscriprting isn't ideal because i'll never know the exact number of separated arrays
print(separatedArraysOfSuperHeroes[0].count)
print(separatedArraysOfSuperHeroes[1].count)
As per the comments the reason why I want sub arrays is because I want to use them to populate different tableview sections. For i.e. inside my tableview I would now have a 2 sections. The first section would have a header that says "Batman" with 2 Batman objects inside of it and the second section would have a header that says Superman with 2 Superman objects inside of it. The count property would show the number of super hero objects inside each section.
func getSeparatedArrayBasedOnName(superHeroes: [SuperHero]) -> [[SuperHero]] {
guard let superNames = NSOrderedSet.init(array: superHeroes.map { $0.name ?? "" }).array as? [String] else {
print("Something went wrong with conversion")
return [[SuperHero]]()
}
var filteredArray = [[SuperHero]]()
for superName in superNames {
let innerArray = superHeroes.filter({ return $0.name == superName })
filteredArray.append(innerArray)
}
for array in filteredArray {
for hero in array {
print(hero.name ?? "")
}
}
return filteredArray
}
Here is my code so far
var counter = 0
for i in 0...9 {
var val = NamePicker()
// array to find duplicates
var buttonValues = ["", "", "", "", "", "", "", "", "", ""] // array for button names
buttonValues.insert(val, at: counter)
print(buttonValues[counter])
counter += 1
}
This code is putting 10 string values into my array. What I would like to do is find a way to check each value in my array. for eample if my end result array is ["a","a","a","b","b","c","c","e","f","c"] I want to see if there is a triple of the same name(single and duplicates are fine). However if there is a triple I would like to change the 3rd value to another val from my NamePicker() function.
so with my array of
["a","a","a","b","b","c","c","e","f","c"]
there are 3 "a" and 3 "c", having two of the same is ok, i would like to change the 3rd to a new values and if the new value makes another triple it will change until there are no more triples.
so that array could possible have an end result of
["a","a","f","b","b","c","c","e","f","z"]
this is where the triples where changed.
Any help on how to do this efficiently?
Both options below asume that your NamePciker() function can generate at least 5 distinct values so there exists an array that satisfies your requirement.
Your requirement is better handled by not generating so many duplicates to begin with. If all you want is an array of names when each name cannot be repeated more than twice, try this:
var buttonValues = [String]()
var dict = [String: Int]()
while buttonValues.count < 10 {
let name = NamePicker()
let count = dict[name] ?? 0
guard count < 2 else { continue }
buttonValues.append(name)
dict[name] = count + 1
}
If you already have the array and want to correct it, do this:
var buttonValues = ["a","a","a","b","b","c","c","e","f","c"]
// Scan the array to tally how many times each name appears
var totalDict = [String: Int]()
buttonValues.forEach { totalDict[$0] = (totalDict[$0] ?? 0) + 1 }
// Now scan it again to update names that appear too many times
var runningDict = [String: Int]()
for (index, value) in buttonValues.enumerated() {
let count = runningDict[value] ?? 0
if count >= 2 {
while true {
let newValue = NamePicker()
let newTotal = (totalDict[newValue] ?? 0) + 1
if newTotal < 3 {
buttonValues[index] = newValue
totalDict[newValue] = newTotal
break
}
}
} else {
runningDict[value] = count + 1
}
}
Dictionary is the best way I think. Have the key be the character and the value be the count of that character. Your runtime will be O(n) since you only have to run through each input once. Here is an example:
let chars = ["a","a","a","b","b","c","c","e","f","c"]
var dict = [String: Int]()
for char in chars {
//If already in Dictionary, increase by one
if var count = dict[char] {
count += 1
dict[char] = count
} else {//else is not in the dictionary already, init with 1
dict[char] = 1
}
}
Output:
["b": 2, "e": 1, "a": 3, "f": 1, "c": 3]
Now I'm not sure how you want to replace the value that's the same character for a third time, but this is probably the best way to group the strings to determine which are over the limit.
Instead of inserting the wrong value and then checking if the values are correct, I would suggest to automatically create the correct array.
//array for button names
var buttonValues = Array<String>()
//tracks what value has been inserted how many times
var trackerDict = [String: Int]()
for i in 0...9 {
//we initialize a new variable that tells us if we found a valid value (if the value has not been inserted 2 times already)
var foundValidValue = false
while !foundValidValue{
var val = NamePicker()
//now we check if the value exists and if it is inserted less than 2 times
if let count = trackerDict[val] {
if count < 2 {
foundValidValue = true
}
}
//if we found the value, we can add it
if foundValidValue {
trackerDict[val] = (trackerDict[val] ?? 0) + 1
buttonValues.append(val)
}
//if we did not find it, we just run through the loop again
}
}
I added a dictionary because it is faster to keep track of the count in a dictionary than counting the number of occurrences in the array every time.
So, I am using the official Hacker News API and it is hosted on Firebase.
The problem I am having is that I want to get a subset of a list basically.
Something along the way of.
let topNewsRef = firebase.childByAppendingPath("topstories").queryLimitedToFirst(UInt(batchSize + offset)).queryLimitedToLast(UInt(batchSize))
[I know this does not work but I would like this effect]. Basically I want a subset of a set, specified by a range; from item 2 to item 15, for example].
Say I want 50 items from the 75th item, but the above is not working. So the question is; how do I achieve the same effect?
For example; given a list of 100 items in Firebase. I want all item from the 50th and 75th. There is no property that gives away the order of the items.
Here is my current solution;
let topNewsRef = firebase.childByAppendingPath("topstories").queryLimitedToFirst(UInt(batchSize + offset))
var handle: UInt?
handle = topNewsRef.observeEventType(.Value) { (snapshot: FDataSnapshot!) -> Void in
if let itemIDs = snapshot.value as? [Int] {
itemIDs.dropFirst(offset) // This drops all items id I already fetched ...
for itemID in itemIDs {
let itemRef = self.firebase.childByAppendingPath("item/\(itemID)")
var itemHandle: UInt?
itemHandle = itemRef.observeEventType(.Value, withBlock: { (snapshot: FDataSnapshot!) -> Void in
if let itemHandle = itemHandle {
itemRef.removeObserverWithHandle(itemHandle)
}
if let json = snapshot.value as? [String:AnyObject],
// Handle JSON ...
}
})
}
}
if let handle = handle {
topNewsRef.removeObserverWithHandle(handle)
}
} // offset += batchSize
... which is gettting all items from the start (offset) to the end (batchSize + offset), then I drop the first end of the list by the size of offset. Thus leaving on the list with a size of batchSize left.
Following our comments to your question,
You can use .queryOrderedByKey in combination with queryStartingAtValue:, queryEndingAtValue:
The data in /topstories is stored as an array. when ordering by key, the indices of the objects in the array are the keys. Then, all you need to do is cast the offset and batchSize as strings and pass them to queryStartingAtValue: & queryEndingAtValue: like so:
ref.queryOrderedByKey().queryStartingAtValue(String(offset)).queryEndingAtValue(String(offset+batchSize-1));
Don't forget to subtract 1 for the endingValue if you are incrementing offset by batchsize.
For the nth query in the series,
startingAtIndex = initialOffset + (batchSize * (n - 1))
endingAtIndex = initialOffset + (batchSize * n) - 1
For example, let batchSize=100 and the initial offset=0.
The first 100 items are keys 0 through 99. The key (index) of the first item in the array is 0.
If you query for endingAt(offset+batchSize), you will be ending at index 100, which is the 101st item in the array.
Thus, you will be getting 101 items -- one more than your batch size.
the second query will then be for items with indices 100-199 (starting at 0+batchsize and ending at 0+batchsize+batchsize-1), which is 100 items.
Here is an example PLNKR of the same concept implemented in JavaScript.
:)
Okay, so the problem was solved with the following query.
firebase.childByAppendingPath("topstories").queryOrderedByKey().queryStartingAtValue(String(offset)).queryEndingAtValue(String(offset + batchSize - 1))
The thing is that the Firebase documentation does not specify that the "AnyObject!" parameters can be used as indices on the query. They also forget to mention that the indices should be of type String.