I have an array of CKRecords. Each record has startTime and a Name, among other values. What I would like to do is sort the records first by unique startTime and then within each startTime sort by unique Name.
The end result would be an array that looks like this (I think): records = [Date: [Name: [CKRecord]]]
Here is what I have right now:
func buildIndex(records: [CKRecord]) -> [[CKRecord]] {
var dates = [NSDate]()
var result = [[CKRecord]]()
for record in records {
var date = record.objectForKey("startTime") as! NSDate
if !contains(dates, date) {
dates.append(date)
}
}
for date in dates {
var recordForDate = [CKRecord]()
for (index, exercise) in enumerate(exercises) {
let created = exercise.objectForKey("startTime") as! NSDate
if date == created {
let record = exercises[index] as CKRecord
recordForDate.append(record)
}
}
result.append(recordForDate)
}
return result
}
let records = self.buildIndex(data)
Why not use sorted? Like this.
// A simplified version of your `CKRecord` just for demonstration
struct Record {
let time: NSDate
let name: String
}
let records = [
Record(time: NSDate(timeIntervalSince1970: 1), name: "a"),
Record(time: NSDate(timeIntervalSince1970: 2), name: "b"),
Record(time: NSDate(timeIntervalSince1970: 1), name: "c"),
Record(time: NSDate(timeIntervalSince1970: 3), name: "d"),
Record(time: NSDate(timeIntervalSince1970: 3), name: "e"),
Record(time: NSDate(timeIntervalSince1970: 2), name: "f"),
]
func buildIndex(records: [Record]) -> [[Record]] {
var g = [NSDate: [Record]]()
for e in records {
if (g[e.time] == nil) {
g[e.time] = []
}
g[e.time]!.append(e) // grouping by `time`
}
return sorted(g.keys) { (a: NSDate, b: NSDate) in
a.compare(b) == .OrderedAscending // sorting the outer array by 'time'
}
// sorting the inner arrays by `name`
.map { sorted(g[$0]!) { $0.name < $1.name } }
}
println(buildIndex(records))
First of all, you're not really trying to sort an array here, you're trying to order a dictionary, which isn't built to be iterated over sequentially. In fact even if you do sort the array first and then build the dictionary like this:
var sortedRecords = [NSDate: [String: CKRecord]]()
records.sort { return $0.date.timeIntervalSinceDate($1.date) < 0 }
for record in records {
if sortedRecords[record.date] != nil {
sortedRecords[record.date] = [String: CKRecord]()
}
sortedRecords[record.date]![record.name] = record
}
The order isn't guaranteed when you iterate over it in the future. That said, a dictionary is essentially a look up table, and elements can be accessed in O(log n) time. What you'll really want to do is either drop the dictionary is favor of an array of [CKRecord] and then sort like this:
records.sort { $0.date.timeIntervalSinceDate($1.date) == 0 ? $0.name < $1.name : $0.date.timeIntervalSinceDate($1.date) < 0 }
Or, depending on what your end goal is, iterate across a range of dates, plucking the entries from the dictionary as you go.
You could execute the CloudKit query and make sure that you get the array returned in the correct sort order like this:
query.sortDescriptors = [NSSortDescriptor(key: "startTime", ascending: true), NSSortDescriptor(key: "Name", ascending: true)]
And then if you go to the detail view, you could use the filter for getting the records for that day like this:
var details = records.filter { (%0.objectForKey("startTime") As! NSDate) == selectedDate }
Related
I have a custom object class as below which consists of 3 variables:
class DateObj {
var startDate: Date?
var endDate: Date?
var updatedEndDate: Date?
}
Below are the objects I created for it:
let obj1 = DateObj.init(startDate: 1/13/2022 7:00am, endDate: 1/13/2022 6:30pm, updatedEndDate: nil)
let obj2 = DateObj.init(startDate: 1/13/2022 10:30am, endDate: 1/14/2022 10:30am, updatedEndDate: 1/13/2022 10:30pm)
let obj3 = DateObj.init(startDate: 1/13/2022 11:30am, endDate: 1/14/2022 11:30am, updatedEndDate: 1/13/2022 7:30pm)
let obj4 = DateObj.init(startDate: 1/13/2022 1:30pm, endDate: 1/13/2022 5:30pm, updatedEndDate: nil)
Doesn't matter what the start date is, I want to compare values of endTime with updatedEndTime and want to sort such that the event that ends first (end time could be in endDate or updatedEndDate) should be first in the array and the event that ends last should be last in the array.
Note: updatedEndTime will always be less than endTime since the event could have ended earlier than expected time.
var inputDates = [obj1, obj2, obj3, obj4]
var expectedOutputDates = [obj4, obj1, obj3, obj2] // Expected output after sort
Code that I tried:
inputDates.sorted { (lhs, rhs) in
if let lhsUpdated = lhs.updatedEndDate, let rhsUpdated = rhs.updatedEndDate {
return lhsUpdated < rhsUpdated
} else if let lhsUpdated = lhs.updatedEndDate, let rhsEndTime = rhs.endDate {
return lhsUpdated < rhsEndTime
} else if let lhsEndTime = lhs.endDate, let rhsUpdated = rhs.updatedEndDate {
return lhsEndTime < rhsUpdated
} else if let lhsEndTime = lhs.endDate, let rhsEndTime = rhs.endDate {
return lhsEndTime < rhsEndTime
}
return false
}
My code is not giving me the expected output. Could someone help and let me know how to compare values from 2 different attributes for sorting an array?
Thanks!
I have an array of Objects with multiple properties. I need to group it by those properties into sections. I've already wrote al algorithm which does that. However, I'd like to have a more succinct and reusable one, so that I can group items in a different manner.
Given an array of objects:
#objcMembers class Object: NSObject {
let name: UUID = UUID()
let value1: Int = Int(arc4random_uniform(6) + 1)
let value2: Int = Int(arc4random_uniform(6) + 1)
let value3: Int = Int(arc4random_uniform(6) + 1)
static func == (lhs: Object, rhs: Object) -> Bool {
lhs.name == rhs.name
}
}
[
Object1 {4, 4, 1},
Object2 {1, 3, 2},
...
Object99 {3, 4, 2},
]
... and given two data structure, Group and Section:
struct Group {
let title: String?
let sections: [Section]
}
struct Section {
let title: String?
let items: [Object]
}
I need to get the following result:
Value1: 1 // Group
Value2: 1 - Value3: 1 // Section
Object1
Object2
Object3
Value2: 1 - Value3: 2 // Section
Object1
Value2: 2 - Value3: 1 // Section
Object1
Object2
Object3
Value1: 2 // Group
Value2: 1 - Value3: 5 // Section
Object1
Value2: 4 - Value3: 1 // Section
Object1
Value2: 4 - Value3: 2 // Section
Object1
Object2
Object3
So, that the objects are grouped into sections by their Value3 and Value2 and sorted in ascending order.
Then, these sections are grouped into groups by their Value1 and, again, sorted in ascending order.
My current algorithm implemented in a basic imperative approach and I'm sure has a lot of points to be improved.
I've already tried to use Swift's Dictionary.init(grouping:by:) initialiser and then Dictionary.mapValues method to group entries further. However, Swift's dictionaries are not ordered, so I have to do a deep sort again.
Currently, my algorithm looks like this:
// Sort the array
let value1BasedDescriptors = [
NSSortDescriptor(keyPath: \Object.value1, ascending: true),
NSSortDescriptor(keyPath: \Object.value2, ascending: true),
NSSortDescriptor(keyPath: \Object.value3, ascending: true),
]
let sorted = (Array(objects) as NSArray).sortedArray(using: value1BasedDescriptors) as! [Object]
// Keep the previous object to find when one of the properties change
var previousObject: Object?
// Keep the group to be filled with sections
var currentGroup = [Section]()
// Keep the section to be filled with objects
var currentSection = [Object]()
// All the groups to be returned by the function
var groups = [Group]()
// Iterate over each object
for object in sorted {
// If it's a first time in a loop, set a previous object and skip
if previousObject == nil {
previousObject = object
// Append to the current section
currentSection.append(object)
continue
}
// If one of the value3 or value2 is different from the previously visited object -> Create a new section with the appropriate title
if object.value3 != previousObject?.value3 || object.value2 != previousObject?.value2 {
let section = Section(title: "Value2: \(previousObject?.value2) - Value3: \(previousObject?.value3)", items: currentSection)
// Add it to current group
currentGroup.append(section)
// Empty the section
currentSection.removeAll()
}
// If Value1 is different, group all the objects into group
if object.value1 != previousObject?.value1 {
let group = Group(title: "Value1: \(previousObject?.value1)", sections: currentGroup)
groups.append(group)
currentGroup.removeAll()
}
// Always add a visited object to a current section
currentSection.append(object)
// And mark as previous
previousObject = object
}
// since the last group & section won't be added in a loop, we have to add them manually
let section = Section(title: "Value2: \(previousObject?.value2) - Value3: \(previousObject?.value3)", items: currentSection)
currentGroup.append(section)
let group = Group(title: "Value1: \(previousObject?.value1)", sections: currentGroup)
groups.append(group)
debugPrint(groups)
It does exactly what I need to achieve, however, here are the limitations:
What if I want to group the objects in the following order: Value2 -> Value1 -> Value3 ? Or any other order? Then I'll have to write the same algorithm, but changing the properties
If I have to write the same algorithm multiple times, how can I make it shorter, e.g. utilising Functional or OOP methods?
Full code listing (copy-paste to Playground or the AppDelegate.swift file):
struct Group {
let title: String?
let sections: [Section]
}
struct Section {
let title: String?
let items: [Object]
}
#objcMembers class Object: NSObject {
let name: UUID = UUID()
let value1: Int = Int(arc4random_uniform(6) + 1)
let value2: Int = Int(arc4random_uniform(6) + 1)
let value3: Int = Int(arc4random_uniform(6) + 1)
static func == (lhs: Object, rhs: Object) -> Bool {
lhs.name == rhs.name
}
}
// Create a lot of objects
var objects = Set<Object>()
for i in 0...100 {
objects.insert(Object())
}
// Sort the array
let value1BasedDescriptors = [
NSSortDescriptor(keyPath: \Object.value1, ascending: true),
NSSortDescriptor(keyPath: \Object.value2, ascending: true),
NSSortDescriptor(keyPath: \Object.value3, ascending: true),
]
let sorted = (Array(objects) as NSArray).sortedArray(using: value1BasedDescriptors) as! [Object]
// Keep the previous object to find when one of the properties change
var previousObject: Object?
// Keep the group to be filled with sections
var currentGroup = [Section]()
// Keep the section to be filled with objects
var currentSection = [Object]()
// All the groups to be returned by the function
var groups = [Group]()
// Iterate over each object
for object in sorted {
// If it's a first time in a loop, set a previous object and skip
if previousObject == nil {
previousObject = object
// Append to the current section
currentSection.append(object)
continue
}
// If one of the value3 or value2 is different from the previously visited object -> Create a new section with the appropriate title
if object.value3 != previousObject?.value3 || object.value2 != previousObject?.value2 {
let section = Section(title: "Value2: \(previousObject?.value2) - Value3: \(previousObject?.value3)", items: currentSection)
// Add it to current group
currentGroup.append(section)
// Empty the section
currentSection.removeAll()
}
// If Value1 is different, group all the objects into group
if object.value1 != previousObject?.value1 {
let group = Group(title: "Value1: \(previousObject?.value1)", sections: currentGroup)
groups.append(group)
currentGroup.removeAll()
}
// Always add a visited object to a current section
currentSection.append(object)
// And mark as previous
previousObject = object
}
// since the last group & section won't be added in a loop, we have to add them manually
let section = Section(title: "Value2: \(previousObject?.value2) - Value3: \(previousObject?.value3)", items: currentSection)
currentGroup.append(section)
let group = Group(title: "Value1: \(previousObject?.value1)", sections: currentGroup)
groups.append(group)
debugPrint(groups)
Here's how I would do this. I would use Dictionary(_:groupingBy:) to produce a groups, and then take that dictionary as the input to a mapping process, transforming the key:value pairs into Group objects. The mapping itself involves another process, calling Dictionary(_:groupingBy:) to group by value2, mapping those key:value pairs into Section objects.
To add the customization you're looking for, you can replace this nesting of these repeating Dictionary(_:groupingBy:), map and sorted calls can be replaces with recursion, by taking an array of keypaths (which represent the values by which you want the various layers grouped by)
import Foundation
struct Object: Equatable {
// let name: UUID = UUID()
let value1 = Int.random(in: 1...6)
let value2 = Int.random(in: 1...6)
let value3 = Int.random(in: 1...6)
static func == (lhs: Object, rhs: Object) -> Bool {
return (lhs.value1, lhs.value2, lhs.value3) == (rhs.value1, rhs.value2, rhs.value3)
}
}
extension Object: Comparable {
static func < (lhs: Object, rhs: Object) -> Bool {
return (lhs.value1, lhs.value2, lhs.value3) < (rhs.value1, rhs.value2, rhs.value3)
}
}
struct Group: CustomDebugStringConvertible {
let title: String
let sections: [Section]
var debugDescription: String {
let sectionText = self.sections
.map { "\t" + $0.debugDescription }
.joined(separator: "\n")
return "Group: \(self.title)\n\(sectionText)"
}
}
struct Section: CustomDebugStringConvertible {
let title: String
let items: [Object]
var debugDescription: String {
let itemText = self.items
.map { "\t\t" + String(describing: $0) }
.joined(separator: "\n")
return "Section: \(self.title)\n\(itemText)"
}
}
let input = (0...100).map { _ in Object() }.sorted()
let groups = Dictionary(grouping: input, by: { $0.value1 })
.map { (arg: (key: Int, rawSections: [Object])) -> Group in
let (key, rawSections) = arg
let sections = Dictionary(grouping: rawSections, by: { $0.value2 })
.map { key, objects in
Section(title: String(key), items: objects.sorted { $0.value3 < $1.value3 })
}
.sorted { $0.title < $1.title }
return Group(title: String(key), sections: sections)
}
.sorted(by: { $0.title < $1.title })
for group in groups {
debugPrint(group)
}
I'm fetching some objects from core data. One of the properties is a name identifier.
The names can be either text or a number, so the property is a String type.
What I'd like to be able to do is sort it so that the text objects are first, then the numbers in numerical order.
Currently its putting the numbers first, and the numbers are in the wrong order, ie. 300, 301, 3011, 304, 3041, Blanc, White
let sortDescriptor = NSSortDescriptor(key: "number", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
Naive version:
let fetchedResults = ["300", "301", "3011", "304", "3041", "Blanc", "White"]
var words = [String]()
var numbers = [String]()
for value in fetchedResults {
if let number = Int(value) {
numbers.append(value)
} else {
words.append(value)
}
}
let result = words + numbers
print(result)
Prints:
["Blanc", "White", "300", "301", "3011", "304", "3041"]
Try this maybe:
var a: [Int] = []
var b: [String] = []
if let value = self[key] as? String {
if let valueAsInt = Int(value) {
a.append(valueAsInt)
} else {
b.append(value)
}
}
I have this code in my viewController
var myArray :Array<Data> = Array<Data>()
for i in 0..<mov.count {
myArray.append(Data(...))
}
class Data {
var value :CGFloat
var name :String=""
init({...})
}
My input of Data is as:
10.5 apple
20.0 lemon
15.2 apple
45
Once I loop through, I would like return a new array as:
sum(value) group by name
delete last row because no have name
ordered by value
Expected result based on input:
25.7 apple
20.0 lemon
and nothing else
I wrote many rows of code and it is too confused to post it. I'd find easier way, anyone has a idea about this?
First of all Data is reserved in Swift 3, the example uses a struct named Item.
struct Item {
let value : Float
let name : String
}
Create the data array with your given values
let dataArray = [Item(value:10.5, name:"apple"),
Item(value:20.0, name:"lemon"),
Item(value:15.2, name:"apple"),
Item(value:45, name:"")]
and an array for the result:
var resultArray = [Item]()
Now filter all names which are not empty and make a Set - each name occurs one once in the set:
let allKeys = Set<String>(dataArray.filter({!$0.name.isEmpty}).map{$0.name})
Iterate thru the keys, filter all items in dataArray with the same name, sum up the values and create a new Item with the total value:
for key in allKeys {
let sum = dataArray.filter({$0.name == key}).map({$0.value}).reduce(0, +)
resultArray.append(Item(value:sum, name:key))
}
Finally sort the result array by value desscending:
resultArray.sorted(by: {$0.value < $1.value})
---
Edit:
Introduced in Swift 4 there is a more efficient API to group arrays by a predicate, Dictionary(grouping:by:
var grouped = Dictionary(grouping: dataArray, by:{$0.name})
grouped.removeValue(forKey: "") // remove the items with the empty name
resultArray = grouped.keys.map { (key) -> Item in
let value = grouped[key]!
return Item(value: value.map{$0.value}.reduce(0.0, +), name: key)
}.sorted{$0.value < $1.value}
print(resultArray)
First of all, you should not name your class Data, since that's the name of a Foundation class. I've used a struct called MyData instead:
struct MyData {
let value: CGFloat
let name: String
}
let myArray: [MyData] = [MyData(value: 10.5, name: "apple"),
MyData(value: 20.0, name: "lemon"),
MyData(value: 15.2, name: "apple"),
MyData(value: 45, name: "")]
You can use a dictionary to add up the values associated with each name:
var myDictionary = [String: CGFloat]()
for dataItem in myArray {
if dataItem.name.isEmpty {
// ignore entries with empty names
continue
} else if let currentValue = myDictionary[dataItem.name] {
// we have seen this name before, add to its value
myDictionary[dataItem.name] = currentValue + dataItem.value
} else {
// we haven't seen this name, add it to the dictionary
myDictionary[dataItem.name] = dataItem.value
}
}
Then you can convert the dictionary back into an array of MyData objects, sort them and print them:
// turn the dictionary back into an array
var resultArray = myDictionary.map { MyData(value: $1, name: $0) }
// sort the array by value
resultArray.sort { $0.value < $1.value }
// print the sorted array
for dataItem in resultArray {
print("\(dataItem.value) \(dataItem.name)")
}
First change your data class, make string an optional and it becomes a bit easier to handle. So now if there is no name, it's nil. You can keep it as "" if you need to though with some slight changes below.:
class Thing {
let name: String?
let value: Double
init(name: String?, value: Double){
self.name = name
self.value = value
}
static func + (lhs: Thing, rhs: Thing) -> Thing? {
if rhs.name != lhs.name {
return nil
} else {
return Thing(name: lhs.name, value: lhs.value + rhs.value)
}
}
}
I gave it an operator so they can be added easily. It returns an optional so be careful when using it.
Then lets make a handy extension for arrays full of Things:
extension Array where Element: Thing {
func grouped() -> [Thing] {
var things = [String: Thing]()
for i in self {
if let name = i.name {
things[name] = (things[name] ?? Thing(name: name, value: 0)) + i
}
}
return things.map{$0.1}.sorted{$0.value > $1.value}
}
}
Give it a quick test:
let t1 = Thing(name: "a", value: 1)
let t2 = Thing(name: "b", value: 2)
let t3 = Thing(name: "a", value: 1)
let t4 = Thing(name: "c", value: 3)
let t5 = Thing(name: "b", value: 2)
let t6 = Thing(name: nil, value: 10)
let bb = [t1,t2,t3,t4,t5,t6]
let c = bb.grouped()
// ("b",4), ("c",3) , ("a",2)
Edit: added an example with nil for name, which is filtered out by the if let in the grouped() function
In my Application I create an array of "NSDate" in order to send local notifications.
The values saved are "UUID" and "deadline" and they are saved using let gameDictionary = NSUserDefaults.standardUserDefaults().dictionaryForKey(GAME_INFO) ?? [:]
The result is somenting similar to this:
[{
UUID = "546C5E4D-CFEE-42F3-9010-9936753D17D85";
deadline = "2015-12-25 15:44:26 +0000";
}, {
UUID = "7C030614-C93C-4EB9-AD0A-93096848FDC7A";
deadline = "2015-12-25 15:43:15 +0000";
}]
What I am trying to achieve is to compare the "deadline" values with the current date and if the deadline is before than current date the values need to be removed from the array.
func compareDeadline() {
let gameDictionary = NSUserDefaults.standardUserDefaults().dictionaryForKey(GAME_INFO) ?? [:]
var items = Array(gameDictionary.values)
for i in 0..<items.count {
let dateNotification = items[i]["deadline"]!! as! NSDate
print(dateNotification)
var isOverdue: Bool {
return (NSDate().compare(dateNotification) == NSComparisonResult.OrderedDescending) // deadline is earlier than current date
}
print(isOverdue)
if (isOverdue == true){
items.removeAtIndex(i)
}
}
}
When I try to remove the values from the array I get Fatal Error: Array index out of range
Any Idea How can I solve this?
You should use the .filter method on the array to remove anything that you don't want in that array. The result is a new array with just the filtered results.
.filter requires you to set the filter criteria in a closure that you send into it
Here is a good article on how to use it
You can use filter method of swift array
For example to filter even numbers in array:
func isEven(number: Int) -> Bool {
return number % 2 == 0
}
evens = Array(1...10).filter(isEven)
println(evens)
There are a few problems. The reason you are getting an error is because you cannot remove elements while iterating inside for-in block. You can filter the items with the following code:
func compareDeadline() {
let gameDictionary = NSUserDefaults.standardUserDefaults().dictionaryForKey(GAME_INFO) ?? [:]
let items = Array(gameDictionary.values)
let currentDate = NSDate()
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ssZ"
let filteredItems = items.flatMap({
guard let stringDeadline = $0["deadline"] as? String, let deadline = dateFormatter.dateFromString(stringDeadline) else {
return nil
}
return deadline
}).filter({
currentDate.compare($0) == .OrderedDescending
})
}