Generating an array of keyed arrays: More efficient way? - ios

I want to sort an array of performers so that they are grouped by the first character of their first name. So for example 'A' in the following output, is for a collection of performers who's first name starts with 'A'.
[
"A"[Performer,Performer,Performer,Performer]
"B"[Performer,Performer,Performer]
"C"[Performer,Performer,Performer]
"D"[Performer,Performer,Performer]
"F"[Performer,Performer,Performer]
"M"[Performer,Performer,Performer]
... etc
]
I have achieved it but I'm hoping there is a more trivial way to do it. The following is how I achieved it.
class Performer {
let firstName: String
let lastName: String
let dateOfBirth: NSDate
init(firstName: String, lastName: String, dateOfBirth: NSDate) {
self.firstName = firstName
self.lastName = lastName
self.dateOfBirth = dateOfBirth
}
}
private var keyedPerformers: [[String: [Performer]]]?
init() {
super.init()
let performers = generatePerformers()
let sortedPerformers = performers.sort { $0.0.firstName < $0.1.firstName }
keyedPerformers = generateKeyedPerformers(sortedPerformers)
}
//'sortedPerformers' param must be sorted alphabetically by first name
private func generateKeyedPerformers(sortedPerformers: [Performer]) -> [[String: [Performer]]] {
var collectionKeyedPerformers = [[String: [Performer]]]()
var keyedPerformers = [String: [Performer]]()
var lastLetter: String?
var collection = [Performer]()
for performer in sortedPerformers {
let letter = String(performer.firstName.characters.first!)
if lastLetter == nil {
lastLetter = letter
}
if letter == lastLetter {
collection.append(performer)
} else {
keyedPerformers[lastLetter!] = collection
collectionKeyedPerformers.append(keyedPerformers)
keyedPerformers = [String: [Performer]]()
collection = [Performer]()
}
lastLetter = letter
}
return collectionKeyedPerformers.sort { $0.0.keys.first! < $0.1.keys.first! }
}

First of all, since you have an (sorted) array of section index titles it's not necessary to use an array of dictionaries. The most efficient way is to retrieve the array of Performers from the dictionary by key.
Add a lazy instantiated variable initialLetter in the Performer class. I added also the description property to get a more descriptive string representation.
class Performer : CustomStringConvertible{
let firstName: String
let lastName: String
let dateOfBirth: NSDate
init(firstName: String, lastName: String, dateOfBirth: NSDate) {
self.firstName = firstName
self.lastName = lastName
self.dateOfBirth = dateOfBirth
}
lazy var initialLetter : String = {
return self.firstName.substringToIndex(self.firstName.startIndex.successor())
}()
var description : String {
return "\(firstName) \(lastName)"
}
To create the performers use an array rather than a simple string. The "missing" letters are already omitted.
private let createIndexTitles = ["A","B","C","D","E","F","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"]
The function to create the dummy instances pre-sorts the arrays of performers by initialLetter and then by lastName
private func generatePerformers() -> [Performer] {
var performers = [Performer]()
for i in 0...99 {
let x = i % createIndexTitles.count
let letter = createIndexTitles[x]
performers.append(Performer(firstName: "\(letter)", lastName: "Doe", dateOfBirth: NSDate()))
}
return performers.sort({ (p1, p2) -> Bool in
if p1.initialLetter < p2.initialLetter { return true }
return p1.lastName < p2.lastName
})
}
Now create an empty array of strings for the section title indexes
private var sectionIndexTitles = [String]()
The function to create the keyed performers follows a simple algorithm : If the key for initialLetter doesn't exist, create it. Then append the performer to the array. At the end assign the sorted keys of the dictionary to sectionIndexTitles, the elements of the keyed arrays are already sorted.
private func generateKeyedPerformers(sortedPerformers: [Performer]) -> [String: [Performer]] {
var keyedPerformers = [String: [Performer]]()
for performer in sortedPerformers {
let letter = performer.initialLetter
var keyedPerformer = keyedPerformers[letter]
if keyedPerformer == nil {
keyedPerformer = [Performer]()
}
keyedPerformer!.append(performer)
keyedPerformers[letter] = keyedPerformer!
}
sectionIndexTitles = Array(keyedPerformers.keys).sort { $0 < $1 }
return keyedPerformers
}
Now test it
let performers = generatePerformers()
let keyedPerformers = generateKeyedPerformers(performers)
print(sectionIndexTitles , keyedPerformers)
In the (presumably) table view use sectionIndexTitles as the section array and get the performer arrays by key respectively.

Related

Assign keys and values from [(key: String, value: [[String : Any]])] in UITableView

I have an array of dictionaries, [[String:AnyObject]], which is reduce+sorted as below successfully.
var arrUserList = [(key: String, value: [[String : Any]])]()
let result = self.arrJsonDict.reduce(into: [String: [[String:Any]]]()) { result, element in
let strName: String = (element as! NSDictionary).value(forKey: "name") as! String
if let firstLetter = strName.first {
let initial = String(describing: firstLetter).uppercased()
result[initial, default: [[String:Any]]() ].append(element as! [String : Any])
}}.sorted { return $0.key < $1.key }
self.arrUserList = result
Now I wanted to assign keys to table sections and values as table cell text from the array.
This is very cumbersome code.
You are highly encouraged to use a struct rather than a dictionary at least with a member name
struct Person {
let name : String
}
Declare and rename arrJsonDic (more descriptively) as
var people : [Person]()
and arrUserList as
var users = [String: [Person]]()
For the sections declare another array
var letters = [String]()
Group the array and populate letters simply with
users = Dictionary(grouping: people, by: { String($0.name.first!) })
letters = users.keys.sorted()
In the table view in numberOfSections return
return letters.count
and in numberOfRows return
let letter = letters[section]
return users[letter]!.count
In cellForRowAt assign a name to a label with
let letter = letters[indexPath.section]
let user = users[letter]![indexPath.row]
cell.nameLabel.text = user.name
------------------------------
To make it still swiftier declare a second struct Section
struct Section {
let index : String
let people : [Person]
}
delete
var letters = [String]()
and declare users
var users = [Section]()
The grouping is slightly different
let grouped = Dictionary(grouping: people, by: { String($0.name.first!) })
users = grouped.map({ Section(index: $0.0, people: $0.1) }).sorted{$0.index < $1.index}
The code in the three table view delegate methods are
return users.count
-
return users[section].people.count
-
let user = users[indexPath.section].people[indexPath.row]
cell.nameLabel.text = user.name

How to insert different type of element in array

I have a class that has a reference to another class. A student class has a variable that has a reference to module info. I want to insert the moduleId into an array but the struct is made up of different types of objects and is of type moduleRef. I would like to get the moduleId which is of type string. How can I grab this moduleId element form ModuleRef.
class Student {
var firstName: String
var lastName: String
var modulesRefs: [ModuleRef]
var fullName: String {
return self.firstName + " " + self.lastName
}
init(firstName: String, lastName: String, modulesRefs: [ModuleRef]) {
self.firstName = firstName
self.lastName = lastName
self.modulesRefs = modulesRefs
}
func addModuleId(_ moduleId: String) {
self.modulesRefs.insert(moduleId, at: 0)
}
func removeModuleId(_ moduleId: String) {
self.modulesRefs = self.modulesRefs.filter { $0.moduleId != moduleId }
}
}
class ModuleRef {
var moduleName: String
var moduleId: String
var moduleStartdate: Int
init(moduleName: String, moduleId: String, moduleStartdate: Int) {
self.moduleName = moduleName
self.moduleId = moduleId
self.moduleStartdate = moduleStartdate
}
}
Cannot convert value of type 'String' to expected argument type 'ModuleRef'
You can insert the values of Type ModuleRef into that array. So i may modify the functions as:
func addModuleId(_ moduleId: String) {
let module = ModuleRef(moduleName: "", moduleId: moduleId, moduleStartdate: 0)
self.modulesRefs.insert(module, at: 0)
}
func removeModuleId(_ moduleId: String) {
modulesRefs.removeAll(where: { $0.moduleId == moduleId })
}
NB: If you don't need moduleName & moduleStartdate all time, you can mark it as Optional.
In your code you have made class ModuleRef { ... } values compulsory and you are just passing a single value moduleId and you are adding it to an Array of ModuleRef which is totally wrong approach. So, based on what you have said here is the updated code of your code:
class Student {
var firstName: String
var lastName: String
var modulesRefs: [ModuleRef]
var fullName: String {
return self.firstName + " " + self.lastName
}
init(firstName: String, lastName: String, modulesRefs: [ModuleRef]) {
self.firstName = firstName
self.lastName = lastName
self.modulesRefs = modulesRefs
}
func addModuleId(_ moduleId: String) {
// This will be your updated intializer where you can pass any value you want and other will be taken as default value
let moduleRef = ModuleRef(moduleId: moduleId)
self.modulesRefs.insert(moduleRef, at: 0)
}
func removeModuleId(_ moduleId: String) {
self.modulesRefs = self.modulesRefs.filter { $0.moduleId != moduleId }
}
}
class ModuleRef {
var moduleName: String
var moduleId: String
var moduleStartdate: Int
/// Here you can provide all the default values which you don't want to pass
init(moduleName: String = "", moduleId: String = "", moduleStartdate: Int = 0) {
self.moduleName = moduleName
self.moduleId = moduleId
self.moduleStartdate = moduleStartdate
}
}
Besides, you can also pass the nil value for all making the variable as optional. Let me know, if you have any confusion!
You cannot insert a ModuleRef just by passing a String.
You need to create an instance for example adding the full name of the student and the UNIX timestamp of the current date
func addModuleId(_ moduleId: String) {
let moduleRef = ModuleRef(moduleName: self.fullName,
moduleId: moduleId,
moduleStartDate: Int(Date().timeIntervalSince1970))
self.modulesRefs.insert(moduleRef, at: 0)
}
And to remove an object this is more efficient, however the class must conform to Equatable
func removeModuleId(_ moduleId: String) {
guard let index = self.modulesRefs.firstIndex(where: {$0.moduleId == moduleId}) else { return }
self.modulesRefs.remove(at: index)
}
I'd even declare moduleStartDate as Date.
You need to updated your Array type to Any as follows:
var modulesRefs: [Any]
While access value from array,
if let moduleRef = modulesRefs[index] as? ModuleRef {
} else if let str = modulesRefs[index] as? String {
}
What you're trying to do, actually means that there's a flaw in your design.
If you need the functionality to call student.addModuleId(moduleId) that means, that Student should not hold an array of ModuleRef. (Or at least have a mechanism to fetch the whole ModuleRef by it's id, and then insert that ModuleRef inside the array)
That being said, After figuring out your design to which will it be: modules: [ModuleRef] or moduleIds: [String], there are still somethings that you can get use out of:
Equatable protocol. If you make ModuleRef to conform to Equatable protocol, and check its equality with id, then Swift will give you access to remove(element:_) method, and you no longer need the removeModuleId() method.
Or if all ModuleIds are unique, you can implement Hashable protocol as well, and use Set<ModuleRef> instead of an array to have O(1) remove and insert methods. (right now, remove function, takes O(n) time)
You need to make an array of Any type..
var modulesRefs: [Any] = []
You can add element in array like this...
modulesRefs.append("Test string")
let obj = ModuleRef(...)
modulesRefs.append(obj)
And when you access an element you need to user either if let or guard let to check its type with safety.
For eg.
for obj in modulesRefs {
if let strType = obj as? String {
print("\(obj) is string type")
} else if let moduleRefObj = obj as? ModuleRef {
print("\(obj) is ModuleRef type")
}

How to use multiple key for the same value in an Swift Dictionary?

How can I store user records into Swift Dictionary, but index them by two type of keys: userID and firstName? Is it possible?
If a value is removed both expression users[aUserID] and user[aFirstName] should return nil.
Dictionaries don't care about unique values, they only care about unique keys. So you can have as many identical values in a dictionary as you like, as long as their keys are all different.
The way I would approach your problem is by hiding all the necessary logic to keep the dictionary keys in sync in a wrapper class:
struct User {
let userId: Int
let firstName: String
// more user properties
}
class UserDict {
private var dict = [String: User]()
subscript(key: String) -> User? {
return dict[key]
}
subscript(key: Int) -> User? {
return dict["\(key)"]
}
func add(_ user: User) {
dict[user.firstName] = user
dict["\(user.userId)"] = user
}
func remove(forKey key: String) {
if let user = dict[key] {
dict.removeValue(forKey: user.firstName)
dict.removeValue(forKey: "\(user.userId)")
}
}
func remove(forKey key: Int) {
remove(forKey: "\(key)")
}
var count: Int {
return dict.count / 2
}
}
You can have multiple keys for the same value, consider this example:
class Value {
var name = "John"
}
var dictionary: [String: Value] = [:]
var value1 = Value()
dictionary["key1"] = value1
dictionary["key2"] = value1
value1.name = "Theresa"
dictionary["key1"]?.name // "Theresa"
dictionary["key2"]?.name // "Theresa"
For the second part of your question:
If value is removed both expression users[aUserID] and user[aFirstName] should return nil.
When you do this dict["key1"] = nil, the value for the key2 is still there and would be value1, so you could create a custom subscript that would handle that by finding identical values in the dictionary.

How to populate data from NSMutableArray into struct using Swift3

I'v created a struct and I want to populate it with my data.
My struct:
struct CrimeNameSection {
var firstChar: Character
var name: [String]
var detail: [String]
var time: [String]
init(firstLetter: Character, object1: [String], object2: [String], object3: [String]) {
firstChar = firstLetter // First letter of 'name'
name = object1
detail = object2
time = object3
}
The first value of my struct ('firstChar') should hold the first letter in 'name' to create an alphabetic sections in tableView, the rest ('name','detail','time') should hold the data from my database (three columns: name, detail, time).
My code:
var marrCrimesData : NSMutableArray! // Hold the database
func getSectionsFromData() -> [CrimeNameSection] {
guard marrCrimesData != nil else {
return []
}
var sectionDictionary = [CrimeNameSection]()
for crime in marrCrimesData {
let crime = crime as! CrimesInfo
let firstChar = CrimeNameSection(firstLetter: crime.name[crime.name.startIndex], object1: [crime.name], object2: [crime.detail], object3: [crime.time])
if var names = firstChar {
names.append(crime.name)
sectionDictionary[firstChar] = names
} else {
sectionDictionary[firstChar] = [crime.name]
}
}
let sections = sectionDictionary.map { (key, value) in
return CrimeNameSection(firstLetter: key, name: value)
}
let sortedSections = sections.sorted { $0.firstLetter < $1.firstLetter }
return sortedSections
}
I get errors all over the place, I need help with storing the data inside my struct and sort it alphabetically.
Thank you all
Consider
struct Crime {
let name: String
let detail: String
let time: String
}
let crimes = [
Crime(name: "Foo", detail: "detail 1", time: "9am"),
Crime(name: "Bar", detail: "detail 2", time: "10am"),
Crime(name: "Baz", detail: "detail 3", time: "11am"),
Crime(name: "Qux", detail: "detail 4", time: "12am")
]
One approach is to just build an dictionary indexed by the first character and then sort it:
var crimeIndex = [Character: [Crime]]()
for crime in crimes {
if let firstCharacter = crime.name.characters.first {
if crimeIndex[firstCharacter] == nil {
crimeIndex[firstCharacter] = [crime]
} else {
crimeIndex[firstCharacter]?.append(crime)
}
}
}
let sortedIndex = crimeIndex.sorted { $0.0 < $1.0 }
The advantage of the above is that we can use the dictionary to efficiently find the section. If you really want to use your custom "name section" structure, I'd first make it to use an array of Crime objects (having disjointed arrays of the properties of a Crime can be fragile, e.g. if you later decide to add sorting of the crimes). So it might look like:
struct CrimeNameSection {
let firstCharacter: Character
var crimes: [Crime]
}
And because we've lost some of the Dictionary efficiency for finding the index and have manually iterate through looking for the section, and I'll go ahead and do an insertion sort at the time, saving me from having to do a separate sort later:
var crimeSections = [CrimeNameSection]()
for crime in crimes {
if let firstCharacter = crime.name.characters.first {
var hasBeenAdded = false
for (index, crimeIndex) in crimeSections.enumerated() {
if firstCharacter == crimeIndex.firstCharacter { // if we found section, add to it
crimeSections[index].crimes.append(crime)
hasBeenAdded = true
break
}
if firstCharacter < crimeIndex.firstCharacter { // if we've passed where the section should have been, insert new section
crimeSections.insert(CrimeNameSection(firstCharacter: firstCharacter, crimes: [crime]), at: index)
hasBeenAdded = true
break
}
}
// if we've gotten to the end and still haven't found section, add new section to end
if !hasBeenAdded {
crimeSections.append(CrimeNameSection(firstCharacter: firstCharacter, crimes: [crime]))
}
}
}
First of all you could not instantiate an Array and map over it like a dictionary
var sectionDictionary = [CrimeNameSection]() // Here you are init an Array
For a dictionary you have also to specify the key, for instance if the key is a string:
var sectionDictionary = [String: CrimeNameSection]() // Dictionary init
But be aware that the key have to be unique so that the dict would work properly.
Another problem here is the constructor in your .map function because you have not created a constructor for your CrimeNameSection that only takes two parameters:
init(firstLetter: Character, object1: [String], object2: [String], object3: [String]) {
firstChar = firstLetter // First letter of 'name'
name = object1
detail = object2
time = object3
}
// Another constructor with 2 arguments
init(firstLetter: Character, object1: [String]) {
firstChar = firstLetter // First letter of 'name'
name = object1
detail = []()
time = []()
}
If you don't want to use another constructor then you have to provide default values to object2 and object3 in your initial constructor.

Create an Array of Object from an Array of Dictionaries with Swift

I'm receiving a JSON dictionary from a web service and I need to map the return values to existing values. Here's essentially what I'm trying to do:
class Contract {
var contractID: String?
var ebState: String?
var ibState: String?
var importerState: String?
var exportersBankRefNo: String?
var importersBankRefNo: String?
}
let contract1 = Contract()
contract1.contractID = "001"
let contract2 = Contract()
contract2.contractID = "002"
// This is the JSON return dictionary
let exportAppnStatusList: [[String: String]] = [["contractID":"001",
"ExporterBankRefNo":"ExporterBankRefNo001",
"ExporterBankState":"ACCEPTED",
"ImporterBankRefNo":"",
"ImporterBankState":"UNKNOWN",
"ImporterState":"UNKNOWN" ],
["contractID":"002",
"ExporterBankRefNo":"ExporterBankRefNo002",
"ExporterBankState":"ACCEPTED",
"ImporterBankRefNo":"ImporterBankRefNo002",
"ImporterBankState":"ACCEPTED",
"ImporterState":"UNKNOWN" ]]
I need to take the exportAppnStatusList and fill in the associated values in the existing contract1 and contract2, mapping by the contractID
This fills the contracts with available information, it ignores contracts where the id could not be found:
for contract in [contract1, contract2] {
if let contractDict = exportAppnStatusList.filter({$0["contractID"] == contract.contractID}).first {
contract.exportersBankRefNo = contractDict["ExporterBankRefNo"]
contract.ebState = contractDict["ExporterBankState"]
contract.importersBankRefNo = contractDict["ImporterBankRefNo"]
contract.ibState = contractDict["ImporterBankState"]
contract.importerState = contractDict["ImporterState"]
}
}
Why not generate the contract object by mapping over the array of dictionaries like this? You'll need to write a custom initializer that takes all these params
exportAppnStatusList.map { (dict:[Stirng:String]) -> Contract in
return Contract(contractID:dict["contractID"],
ebState:dict["ExporterBankState"],
ibState:dict["ImporterBankState"],
importerState:dict["ImporterState"],
exportersBankRefNo:dict["ExporterBankRefNo"],
importersBankRefNo:dict["ImporterBankRefNo"]
}
Try using this init (your class must inherit from NSObject):
init(jsonDict: [String: String]) {
super.init()
for (key, value) in jsonDict {
if class_respondsToSelector(Contract.self, NSSelectorFromString(key)) {
setValue(value, forKey: key)
}
}
}
Then you can do this:
exportAppnStatusList.forEach {
print(Contract(jsonDict: $0))
}

Resources