I have an array of objects of type Thing:
class Thing: NSObject {
var data: String
var type: String
var created: NSDate
}
These things have an NSDate property called created. My aim is to write a function that reads the created property of every thing in the array and returns the thing that has the most recent date. The function looks like this:
public func getLastSwipe(list: Array<Thing>) -> Thing {
return someThing
}
Another approach is using Swift's .max, like this:
dates.max(by: <)
The following is my old answer. The above is updated in feb 2023.
let mostRecentDate = dates.max(by: {
$0.timeIntervalSinceReferenceDate < $1.timeIntervalSinceReferenceDate
})
This is the most performant solution I've found.
Returns the sequence’s most recent date if the sequence is not empty; otherwise, nil.
You could use reduce if you wanted. This will find the object with the highest timestamp.
var mostRecent = list.reduce(list[0], { $0.created.timeIntervalSince1970 > $1.created.timeIntervalSince1970 ? $0 : $1 } )
If your dates are not all in the past, you'll have to also compare against the current date to determine a cutoff. If your dates are all in the future, you'll want to switch the > to < to find the next future date (lowest timestamp).
You can sort the array, then find the first/last element. For example...
let objects: [Thing] = ... //Set the array
let mostResent = array.sorted { (firstThing, secondThing) -> Bool in
firstThing.created.timeIntervalSince1970 > secondThing.created.timeIntervalSince1970
}.first
This will return the most resent Thing as an Optional (because there is no guarantee that the array is not empty. If you know that the array is not empty, then you can end that line with .first!
Related
I think I'm introducing some logic error and I might be missing something here.
Please consider the following code:
// Model
class MyModel: NSObject {
let month: Int
let destiny: String
init(month: Int, destiny: String) {
self.month = month
self.destiny = destiny
}
}
var datasource: [MyModel] = []
var dict: [Int : [MyModel]] = [:]
func fillDatasource() {
for _ in 0...20 {
let month = Int.random(in: 1...12)
let destiny = "Any"
let model = MyModel(month: month, destiny: destiny)
datasource.append(model)
}
}
func fillDict() {
datasource.forEach {
let month = $0.month
dict[month]?.append($0)
}
print(dict) // always empty
}
fillDatasource()
fillDict()
Inside my fillDict function the array is always nil.
I think this is because the key doesn't exist , so the value cannot be appended to that specific key.
My question is: if the key doesn't exist, calling the append function would insert the key as well?
Am I missing something here?
Thanks.
Your assumption is incorrect and there is no reason to think that this would insert a new array.
It might seem intuitive for this case but it may be very wrong for some cases. How about something like this:
garages[myName]?.parkCar(myCar)
Should this construct a new garage for my car? I think not. But even if so; what if default constructor is unavailable and this is actually defined as a protocol:
protocol Garage {
func parkCar(_ car: Car)
}
var garages[String: Garage]
there is no way for Swift to fill in this object automatically.
Technically there would be a possible solution for this work that Swift would automatically construct an object for you in dictionary if this object had a default constructor and possibly the object type is a struct or a final class... But this would most likely only introduce more confusion than it would solve.
The most straight forward solution to your example is what #Sh_Khan wrote (but later deleted) which is:
if dict[month] == nil {
dict[month] = [$0]
}
else {
dict[month]?.append($0)
}
Probably some more feasible approach would be
dict[month] = (dict[month] ?? []) + [$0]
but as described in a comment there is already a method that does exactly that for you:
dict[month, default: []].append($0)
I hope we can agree that this is a more general approach and it fixes all cases. For instance
garages[myName, default: PublicGarage(parkingSpot: myName)].parkCar(myCar)
You can update your fillDict method to the following:
func fillDict() {
datasource.forEach {
let month = $0.month
if dict.keys.contains(month) {
dict[month]?.append($0)
} else {
dict[month] = [$0]
}
}
print(dict)
}
Explanation:
We need to check if the month key already exits in the dictionary, than append in it's array else we are assigning a new array against a month in the dictionary
Dic is empty because dic[month] is nil, the value has never been altered.
To group array by a property of the array elem, I'd use the following:
dic = Dictionary(grouping: datasource) { (model) -> Int in
return model.month
}
Let's say that I am fetching messages (objects) from a database. They do not arrive at the same time, a message always arrives alone (one fetch -> one message; a function gets called for every message) and at any time.
Every message contains a time stamp that represents the date when the message was created. So I can call message.date and I will get the date when the message was created.
The messages do not arrive in any order; it can be that the created last message is at the third/fourth/... position.
I'd like to group these messages by day in order to present them in a UITableView.
Each section represents a day. Eeach section header includes the day and every cell includes the time (kind of like in WhatsApp Messenger).
I know how to create custom header views, insert sections, rows etc.
The problem is that I don't know how or as what data type to sort the messages in order to create the TableView easily and resource-saving and efficient (in terms of storage requirement and clarity).
It would be easy if I had a two-dimensional array, but I am not clever enough to think up an efficient approach to sort (or rather group) the messages.
Thanks a lot for help!
The time stamp is a date, so sort by date (array) and group by day (dictionary).
Sorting an array that includes date information by date is a one-liner in Swift.
Grouping a sorted array that includes date information into a dictionary keyed by day is also a one-liner in Swift.
So that's two lines of code, and you didn't have to change anything.
However, a dictionary has no order, so I would then suggest taking a third step where you transform the dictionary into an array of some custom struct reflecting the section-row structure of your table. The correct data source for any sectioned table view is going to have a structure like this:
struct Row {
// row properties
}
struct Section {
var rowData : [Row]
// section properties
}
var model : [Section]!
So after you've made your dictionary as a way of grouping, you just map it onto an array of Section and maintain that going forward.
Of course if you have no data to start with and the data arrives one item at a time, then you can omit the two bulleted steps above. Just start with the structured model and keep slotting each item into the right spot as it arrives.
EDIT: You expressed interest (in a comment) on how to insert an element into the right place in an already sorted array, so here's an example (see https://stackoverflow.com/a/26679191/341994):
extension Array {
func insertionIndex(of elem: Element, by f: (Element, Element) -> Bool) -> Int {
var lo = 0
var hi = self.count - 1
while lo <= hi {
let mid = (lo + hi)/2
if f(self[mid], elem) {
lo = mid + 1
} else if f(elem, self[mid]) {
hi = mid - 1
} else {
return mid // found at position mid
}
}
return lo // not found, would be inserted at position lo
}
mutating func insertSorted(_ elem:Element, by f: (Element, Element) -> Bool) {
self.insert(elem, at:self.insertionIndex(of:elem, by:f))
}
}
Here's a test; of course your ordering function won't be as simple as < but that's really the only difference:
var arr = [Int]()
arr.insertSorted(1, by:<)
arr.insertSorted(10, by:<)
arr.insertSorted(9, by:<)
arr.insertSorted(3, by:<)
arr.insertSorted(5, by:<)
arr.insertSorted(7, by:<)
arr.insertSorted(6, by:<)
// [1, 3, 5, 6, 7, 9, 10]
It is very easy you can grouped it.
for example messages contain these following:
struct message {
let senderName:String
let mess:String
let reciever:String
let time:Date
}
and you have some messages:
var messages = [message]()
messages.append(message(senderName: "snow", mess: "Hello", reciever: "Dani", time: Date(timeIntervalSince1970: 1533078663)))
messages.append(message(senderName: "john", mess: "Hello", reciever: "Dani", time: Date(timeIntervalSince1970: 1533078606)))
messages.append(message(senderName: "alix", mess: "Hello", reciever: "Dani", time: Date(timeIntervalSince1970: 1533078633)))
you can grouped it easily by using this:
let groupedMessage = Dictionary(grouping: messages) { (mess) -> Date in
return mess.time
}
My DB looks like this:
shows{
show1{
name: //Showname
start: //Timestamp start
end: //Timestamp end
rating: //Showrating INT
}
show2{
...
}
}
How can i query the shows, which are running now (start < now && end > now), ordered by the rating?
Is this even possible with this Database Structure or do i have to change it?
You should name shows' children nodes by their UID, not "show1", "show2", etc. Then you would query your database for the shows ordered by their rating, and use a conditional to test whether each result is within the desired time frame. I haven't actually tested this code, but something like this should work:
ref?.child("shows").child(getUid()).queryOrdered(byChild: "rating").observeEventType(.Value, withBlock: { snapshot in
for child in snapshot.children as? [String: AnyObject] {
// filter results
if (child["start"] <= currentTime && child["end"] >> currentTime ) {
// results
resultsArray.append(child)
}
}
However, I recommend reading about denormalizing data in Firebase first:
https://firebase.google.com/docs/database/ios/structure-data
https://stackoverflow.com/a/16651115/3502608
And read the docs over querying after you understand denormalization:
https://firebase.google.com/docs/database/ios/lists-of-data
First of all if you are using timestamps and you want to manipulate them in your front end or perform any algorithmic procedure over the timestamp (i.e > or <) then use NSDate not FIRServerValue.timestamp().
To query your show that are having the end : before the current timestamp try using this:-
let currentTimeStamp = Int(NSDate.timeIntervalSinceReferenceDate*1000)
FIRDatabase.database().reference().child("shows").queryOrdered(byChild: "end").queryStarting(atValue: currentTimeStamp).observeSingleEvent(of: .value, with: {(Snapshot) in
print(Snapshot)
})
This will give you all the shows who are running now. Also for this to work you have to store the value of start and end in similar fashion i.e Int(NSDate.timeIntervalSinceReferenceDate*1000)
To order them according to your show rating , you can only retrieve the values and store them in a struct.
struct show_Struct {
var name : String!
var rating : Int! //If it is int or float if it is of type float.
...
}
Before calling the reloadData() function on any of your tableView or collectionView, just call
let showFeed = [show_Struct]()
..
self.showFeed.sort(by: {$0.rating > $1.rating})
self.tableView.reloadData()
I've got an array that I'm trying to store timestamps into.
I'm testing stuff out with something like:
#IBAction func buttonPressed(sender:UIButton!) {
let post = ["MinutesLeft" : (FIRServerValue.timestamp())]
DataService.ds.REF_POSTS.childByAutoId().setValue(post)
}
Then I'm trying to call it back and fill an array with some of those timestamps:
DataService.ds.REF_POSTS.queryOrderedByChild("MinutesLeft").queryStartingAtValue(Int(cutoff)).observeEventType(.ChildAdded, withBlock: { (snapshot:FIRDataSnapshot) in
let post = snapshot.value!["MinutesLeft"]
print("POST \(post)" )
self.postsArray.append(post)
print(self.postsArray)
} else {
print("Didn't work")
}
})
Then I'm running a timer that's meant to clear out certain posts:
func removeExpiredItems() {
let cutoff = UInt64(1000 * floor(NSDate().timeIntervalSince1970) - 10*60*1000)
while (self.postsArray.count > 0 && self.postsArray[0] < cutoff) {
// remove first item, because it expired
self.postsArray.removeAtIndex(0)
print(postsArray.count)
}
}
My issues are :
I don't know what kind of array to have here. It's telling me that a timestamp is an AnyObject, but when I go to make an array of anyobjects I get a straight up Xcode internal error crash...so I guess that's a no-go.
I can translate it all to strings and just store it like that, but the issue comes then when I'm trying to compare the times vs my cutoff in my removeExpiredItems func.
I was trying to do something like:
Take the timestamps, change them to a string , then down when I'm going to make the comparison change the String to an Int but I get something like "This kind of conversion will always fail".
Any ideas here?
let post = snapshot.value!["MinutesLeft"]
The type of post will be AnyObject, (Optional<AnyObject>) in this case.
You have store it as the data type you want. For example in this case you can store it as NSTimeInterval
let post = snapshot.value!["MinutesLeft"] as! NSTimeInterval
And store it in an Array of NSTimeInterval as
var postsArray :[NSTimeInterval] = []
I'm using childByAutoId() to generate my children. Each child looks like:
{
user_id: 1
}
I'd like to get the last 10 most recently added, sorted by time DESC. What's the easiest way to do this?
The answer is that you need to use a bit of reverse logic, and also store a timestamp key:value pair within each node as a negative value. I omitted the user_id: 1 to keep the answer cleaner.
Here's the Firebase structure
"test" : {
"-KFUR91fso4dEKnm3RIF" : {
"timestamp" : -1.46081635550362E12
},
"-KFUR9YH5QSCTRWEzZLr" : {
"timestamp" : -1.460816357590991E12
},
"-KFURA4H60DbQ1MbrFC1" : {
"timestamp" : -1.460816359767055E12
},
"-KFURAh15i-sWD47RFka" : {
"timestamp" : -1.460816362311195E12
},
"-KFURBHuE7Z5ZvkY9mlS" : {
"timestamp" : -1.460816364735218E12
}
}
and here's how that's written out to Firebase; I just used a IBAction for a button to write out a few nodes:
let testRef = self.myRootRef.childByAppendingPath("test")
let keyRef = testRef.childByAutoId()
let nodeRef = keyRef.childByAppendingPath("timestamp")
let t1 = Timestamp
nodeRef.setValue( 0 - t1) //note the negative value
and the code to read it in
let ref = self.myRootRef.childByAppendingPath("test")
ref.queryOrderedByChild("timestamp").queryLimitedToFirst(3).observeEventType(.ChildAdded, withBlock: { snapshot in
print("The key: \(snapshot.key)") //the key
})
and I declared a little function to return the current Timestamp
var Timestamp: NSTimeInterval {
return NSDate().timeIntervalSince1970 * 1000
}
and the output
The key: -KFURBHuE7Z5ZvkY9mlS
The key: -KFURAh15i-sWD47RFka
The key: -KFURA4H60DbQ1MbrFC1
As you can see, they are in reverse order.
Things to note:
Writing out your timestamp as negative values
When reading in use .queryLimitedToFirst instead of last.
On that note, you can also just read the data as usual and add it to an Array then then sort the array descending. That puts more effort on the client and if you have 10,000 nodes may not be a good solution.
I'm assuming your data actually looks like this:
someDataSet: {
longUID-1: {
timeCreated: 9999999999, // (seconds since the javascript epoch)
user_id: 1
},
longUID-2: {
timeCreated: 1111111111,
user_id: 2
},
longUID-3: {
timeCreated: 3141592653,
user_id: 3
}
}
You could automate that by calling Firebase.push({user_id: ###, timeCreated: ###}) multiple times in a for loop or any other method. Maybe you're adding news stories to a webpage, but you only want your user to see the most current stories--- IDK. But the answer to your question is to use Firebase's ref.orderByChild() and ref.limitToLast().
var ref = new Firebase("<YOUR-FIREBASE-URL>.firebaseio.com/someDataSet");
//the "/someDataSet" comes from the arbitrary name that I used up above
var sortedRef = ref.orderByChild('timeCreated');
//sort them by timeCreated, ascending
sortedRef.limitToLast(2).on("child_added", function(snapshot){
var data = snapshot.val();
console.log(data);
/* do something else with the data */
});
//The console would look like this
// Object {timeCreated: 9999999999, user_id: 1}
// Object {timeCreated: 3141592653, user_id: 3}
This happened because the program took the child with the greatest timeCreated value first and then the second greatest (value) second...
Also note, the longUID means nothing when you sort them by child and neither do the other values (user_id in this case)
Here is the documentation for:
Firebase .push() method (Sorry, I'm not allowed to post this link- I dont have enough reputation)
Firebase .orderByChild method
And also, Firebase .limitToLast method
The code: ref.queryOrderedByKey().queryLimitedToLast(10) can be used for getting the most recent 10 data. However, this is an ascending order by default.
Alternatively, you can order your data via
ref.orderByChild("id").on("child_added", function(snapshot) {
console.log(snapshot.key());
});
This also presents an ascending order by default. To change it into descending order is little bit tricky. What I would suggest it to multiply ids by -1 as shown below and then sort them.
var ref= new Firebase("your data");
ref.once("value", function(allDataSnapshot) {
allDataSnapshot.forEach(function(dataSnapshot) {
var updatedkey = -1 * dataSnapshot.key();
ref.update({ element: { id: updatedkey}});
});
});
This two SO page might be useful for you also, please check:
How to delete all but most recent X children in a Firebase node?
firebaseArray descending order?