I want to group data by dates which has initial format of yyyy-MM-dd'T'HH:mm:ss.SSSZ and display as E, dd MMM. I have managed to group them by the dates but failed to sort the dates in a descending order. How do I sort the date components after grouping in Dictionary?
JSON Response
{
"list": [
{
"userId": "test1",
"transactionTime": "2019-06-20T14:01:00.253+08:00"
},
{
"userId": "test2",
"transactionTime": "2019-06-16T14:02:00.253+08:00"
},
{
"userId": "test3",
"transactionTime": "2019-06-12T14:01:00.253+08:00"
},
{
"userId": "tes4",
"transactionTime": "2019-06-17T14:02:00.253+08:00"
},
]
}
Grouping
func convertToDateObj() -> Date {
let dateFormatter = DateFormatter()
// Convert from initial date string to date object
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
let dateObj = dateFormatter.date(from: self)!
return dateObj
}
// Group list by date
let groupedList = Dictionary(grouping: rawTransactionsList, by: { list -> DateComponents in
let dateObj = list.transactionTime.convertToDateObj()
let date = Calendar.current.dateComponents([.day, .month], from: (dateObj))
return date
})
// QUESTION
// How do I sort the keys?
// groupList.keys.sorted(...)?
// Populate my list with grouped list
groupedList.keys.forEach { key in
print("Group keys \(key)")
let values = groupedList[key]
groupedtransactionsList.append(values ?? [])
}
You should probably also group by the year, otherwise the same day in different years will be in the same group:
let date = Calendar.current.dateComponents([.day, .month, .year], from: (dateObj))
One way to sort the keys is to convert the date components back into Dates, using Calendar.current.date(from:):
let sortedList = groupedList.sorted {
Calendar.current.date(from: $0.key) ?? Date.distantFuture <
Calendar.current.date(from: $1.key) ?? Date.distantFuture
}
sortedList.forEach { key, values in
print("Group keys \(key)")
groupedtransactionsList.append(values ?? [])
}
My suggestion is to use a date string with format yyyy-MM-dd as dictionary key rather than date components. This string can be sorted.
let groupedList = Dictionary(grouping: rawTransactionsList, by: { list -> String in
let dateObj = list.transactionTime.convertToDateObj()
let comps = Calendar.current.dateComponents([.day, .month, .year], from: dateObj)
return String(format: "%ld-%.2ld-%.2ld", comps.year!, comps.month!, comps.day!)
})
groupedtransactionsList = groupedList.keys.sorted(by: >).map { groupedList[$0]! }
Related
I have an array of objects that contain Date property. I want to distribute them in UITableView sections according to their month by month order. How I can do that?
My model:
class Birthday: Object {
#objc dynamic var name: String = ""
#objc dynamic var date: Date = Date()
#objc dynamic var dayLeft: Int = 0
#objc dynamic var userImageData: Data?
}
First I use the next code with DateFormatter:
var grouped: [String: [Birthday]] = [:]
grouped = Dictionary(grouping: birthdaysList) { item -> String in
let calendar = Calendar.current
let components = calendar.dateComponents([.year, .month], from: item.date)
let date = calendar.date(from: components) ?? Date(timeIntervalSince1970: 0)
return date.monthAsString()
}
extension Date {
func monthAsString() -> String {
let df = DateFormatter()
df.setLocalizedDateFormatFromTemplate("MMM")
return df.string(from: self)
}
}
Then I use:
struct Section {
let month: String
let birthdays: [Birthday]
}
var sections: [Section] = []
let keys = Array(grouped.keys)
sections = keys.map({Section(month: $0, birthdays: grouped[$0]!)})
But I need to sort month names in order. January, february etc. How this can be done?
I try to return tuple from my code, but get error
Declared closure result '(String, Int)' is incompatible with contextual type 'String'
grouped = Dictionary(grouping: birthdaysList) { item -> (String, Int) in
let calendar = Calendar.current
let components = calendar.dateComponents([.year, .month], from: item.date)
let date = calendar.date(from: components) ?? Date(timeIntervalSince1970: 0)
return (date.monthAsString(), components.month!)
}
Create a wrapper struct
struct Section {
let month : String
let birthdays : [Birthday]
}
then map the grouped dictionary to an array of Section
let keys = Array(grouped.keys)
let sections = keys.map({Section(month: $0, birthdays: grouped[$0]!)})
sections represent the sections, birthdays the rows, month the section header titles.
Edit:
To be able to sort the months by their ordinal number return the number in front of the month name
grouped = Dictionary(grouping: birthdaysList) { item -> String in
let calendar = Calendar.current
let components = calendar.dateComponents([.year, .month], from: item.date)
let date = calendar.date(from: components) ?? Date(timeIntervalSince1970: 0)
return "\(components.month!)_" + date.monthAsString()
}
And modify the mapping code
let keys = grouped.keys.sorted{$0.localizedStandardCompare($1) == .orderedAscending}
let sections = keys.map({Section(month: $0.components(separatedBy:"_").last!, birthdays: grouped[$0]!)})
I am working on a scheduling app. I want all the dates of the given months I am not able to group dates by months that is what I tried but I want a different expected result
extension Date {
static func dates(from fromDate: Date, to toDate: Date) -> [Date] {
var dates: [Date] = []
var date = fromDate
while date <= toDate {
dates.append(date)
guard let newDate = Calendar.current.date(byAdding: .day, value: 1, to: date) else { break }
date = newDate
}
return dates
}
var month: Int {
return Calendar.current.component(.month, from: self)
}
}
let fromDate = Calendar.current.date(byAdding: .day, value: 30, to: Date())
let datesBetweenArray = Date.dates(from: Date(), to: fromDate!)
var sortedDatesByMonth: [[Date]] = []
let filterDatesByMonth = { month in datesBetweenArray.filter { $0.month == month } }
(1...12).forEach { sortedDatesByMonth.append(filterDatesByMonth($0)) }
The result is in this format [[], [], [], [], [], [], [2019-07-31
03:51:19 +0000],……., [], [], [], []]
This kinda result I want expecting
struct ScheduleDates {
var month: String
var dates: [Date]
init(month: String, dates: [Date]) {
self.month = month
self.dates = dates
}
}
var sections = [ScheduleDates]()
If you want to group your dates by month you can create a Dictionary like this:
Dictionary(grouping: datesBetweenArray, by: { $0.month })
This results in the following output of the format [Int: [Date]]
The key of the dictionary will be your month.
Now you can initialize your scheduleDates struct by looping through this dictionary in this way:
var sections = Dictionary(grouping: datesBetweenArray,
by: ({$0.month}))
.map { tuple in
ScheduleDates(month: String(tuple.0), dates: tuple.1)
}
Here's the code for a Playground
I think your structs should probably be Int values for the months, as when you go to populate something like a tableview, it'll be a PITA to re-order months if you've got them as Strings.
struct ScheduleDates {
var month: String
var dates: [Date]
}
Anyway, here's the extension I wrote based on what you provided. I frankly think you should return a dictionary with an Int as the key and an array of Dates as the value, but here's what you wanted...
I used Dictionary(grouping:by:) to construct a dictionary from an array of dates.
extension Date {
static func dateDictionary(from arrayOfDates: [Date]) -> [String: [Date]] {
// declare a dictionary
return Dictionary(grouping: arrayOfDates) { date -> String in
// get the month as an int
let monthAsInt = Calendar.current.dateComponents([.month], from: date).month
// convert the int to a string...i think you probably want to return an int value and do the month conversion in your tableview or collection view
let monthName = DateFormatter().monthSymbols[(monthAsInt ?? 0) - 1]
// return the month string
return monthName
}
}
}
Here's a utility method I wrote to generate data while I figured out how to do it. If you're going to be in production, don't force unwrap stuff as I did here.
// Utility method to generate dates
func createDate(month: Int, day: Int, year: Int) -> Date? {
var components = DateComponents()
components.month = month
components.day = day
components.year = year
return Calendar.current.date(from: components)!
}
Below is how I generated an array of sample dates to experiment.
// generate array of sample dates
let dateArray: [Date] = {
let months = Array(1...12)
let days = Array(1...31)
let years = [2019]
var dateArray: [Date] = []
while dateArray.count < 100 {
let randomMonth = months.randomElement()
let randomDay = days.randomElement()
let randomYear = years.randomElement()
if let month = randomMonth,
let day = randomDay,
let year = randomYear,
let date = createDate(month: month,
day: day,
year: year) {
dateArray.append(date)
}
}
return dateArray
}()
let monthDictionary = Date.dateDictionary(from: dateArray)
var arrayOfStructs: [ScheduleDates] = []
monthDictionary.keys.forEach { key in
let scheduleDate = ScheduleDates(month: key,
dates: monthDictionary[key] ?? [])
arrayOfStruct.append(scheduleDate)
}
print(arrayOfStructs)
You can use my code. Which i write to adapt your case.
let fromDate = Calendar.current.date(byAdding: .day, value: 30, to: Date())
let datesBetweenArray = Date.dates(from: Date(), to: fromDate!)
if datesBetweenArray.count <= 1 {
print(datesBetweenArray)
}
var sortedDatesByMonth: [[Date]] = []
var tempMonth = datesBetweenArray[0].month
var dates: [Date] = []
for i in 0..<datesBetweenArray.count {
if tempMonth == datesBetweenArray[i].month {
dates.append(datesBetweenArray[i])
if i == datesBetweenArray.count - 1 {
sortedDatesByMonth.append(dates)
}
} else {
sortedDatesByMonth.append(dates)
tempMonth = datesBetweenArray[i].month
dates.removeAll()
dates.append(datesBetweenArray[i])
}
}
print(sortedDatesByMonth.count)
print(sortedDatesByMonth)
I have Date() properties. startingAt and endingAt. And an array of Date(), which are alreadyRegistred. I have to create an array of strings with dates between startingAt and endingAt. StartingAt and endingAt are included and the last requirement is to exclude alreadyRegistred dates.
Do you have some elegant idea, how to do it? Thanks for help!
Edit: Maximum number of dates in final array will be about 7 days.
Dont forget that a Date is basically just a timestamp, and that you can have access to the addingTimeInterval(_:) method.
Knowing that, is very easy to do some calculation between two dates.
I do not have the whole knowledge about your required business logic, but here is a naive implementation that generates Dates between two dates. I'm sure you can run it in a playground and explore a little bit.
import UIKit
func intervalDates(from startingDate:Date, to endDate:Date, with interval:TimeInterval) -> [Date] {
guard interval > 0 else { return [] }
var dates:[Date] = []
var currentDate = startingDate
while currentDate <= endDate {
currentDate = currentDate.addingTimeInterval(interval)
dates.append(currentDate)
}
return dates
}
let startingDate = Date() // now
let endDate = Date(timeIntervalSinceNow: 3600 * 24 * 7) // one week from now
let intervalBetweenDates:TimeInterval = 3600 * 3// three hours
let dates:[Date] = intervalDates(from: startingDate, to: endDate, with: intervalBetweenDates)
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .long
dateFormatter.timeStyle = .long
let dateStrings = dates.map{dateFormatter.string(from: $0)}
print("NOW : \(startingDate)")
for (index, string) in dateStrings.enumerated() {
print("\(index) : \(string)")
}
print("END DATE : \(endDate)")
Try this and see:
// Start & End date string
let startingAt = "01/01/2018"
let endingAt = "08/03/2018"
// Sample date formatter
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd/MM/yyyy"
// start and end date object from string dates
var startDate = dateFormatter.date(from: startingAt) ?? Date()
let endDate = dateFormatter.date(from: endingAt) ?? Date()
// String date array, to be excluded
let alreadyRegistred = ["01/01/2018", "15/01/2018", "10/02/2018", "20/02/2018", "05/03/2018"]
// Actual operational logic
var dateRange: [String] = []
while startDate <= endDate {
let stringDate = dateFormatter.string(from: startDate)
startDate = Calendar.current.date(byAdding: .day, value: 1, to: startDate) ?? Date()
if (alreadyRegistred.contains(stringDate)) {
continue
} else {
dateRange.append(stringDate)
}
}
print("Resulting Array - \(dateRange)")
Here is result:
Resulting Array - ["02/01/2018", "03/01/2018", "04/01/2018", "05/01/2018", "06/01/2018", "07/01/2018", "08/01/2018", "09/01/2018", "10/01/2018", "11/01/2018", "12/01/2018", "13/01/2018", "14/01/2018", "16/01/2018", "17/01/2018", "18/01/2018", "19/01/2018", "20/01/2018", "21/01/2018", "22/01/2018", "23/01/2018", "24/01/2018", "25/01/2018", "26/01/2018", "27/01/2018", "28/01/2018", "29/01/2018", "30/01/2018", "31/01/2018", "01/02/2018", "02/02/2018", "03/02/2018", "04/02/2018", "05/02/2018", "06/02/2018", "07/02/2018", "08/02/2018", "09/02/2018", "11/02/2018", "12/02/2018", "13/02/2018", "14/02/2018", "15/02/2018", "16/02/2018", "17/02/2018", "18/02/2018", "19/02/2018", "21/02/2018", "22/02/2018", "23/02/2018", "24/02/2018", "25/02/2018", "26/02/2018", "27/02/2018", "28/02/2018", "01/03/2018", "02/03/2018", "03/03/2018", "04/03/2018", "06/03/2018", "07/03/2018", "08/03/2018"]
let startDate = Date()
let endDate = Date().addingTimeInterval(24*60*60*10) // i did this to get the end date for now
var stringdateArray = [String]()
if let days = getNumberofDays(date1: startDate, date2: endDate) {
for i in 0...days-1 {
let date = startDate.addingTimeInterval(Double(i)*24*3600)
let stringDate = getStringDate(fromDate: date, havingFormat: "yyyy-MM-dd")
if !(alreadyRegisteredArray.contains(stringDate)) { // checking if already registered
stringdateArray.append(stringDate)
}
}
}
and our helper method
let dateFormatter = DateFormatter()
func getStringDate(fromDate: Date,havingFormat: String) -> String {
dateFormatter.dateFormat = havingFormat
dateFormatter.amSymbol = "AM"
dateFormatter.pmSymbol = "PM"
let date = dateFormatter.string(from: fromDate)
return date
}
func getNumberofDays(date1: Date, date2: Date) -> Int? {
let calendar = NSCalendar.current
let date1 = calendar.startOfDay(for: date1)
let date2 = calendar.startOfDay(for: date2)
let components = calendar.dateComponents([.day], from: date1, to: date2)
return components.day
}
I am trying to make my date display such as "March 2nd, 2018 10:00pm". I tried "MM-dd-yyyy" and "yyyy-MM-dd hh:mm:ss" but it seems like none of these combinations are getting the date I desire.The function to pick the date is sendRequest and it is using a UIDatePicker.
func getCurrentDateTimeFromTimeStamp(timestamp:String)->String{
let date = NSDate(timeIntervalSince1970:Double(timestamp)!)
let formatter = DateFormatter()
formatter.dateFormat = "MMMM d, yyyy HH:mm a"
return formatter.string(from: date as Date)
}
let dateCellVar = request.timestamp
let dateString = dateCellVar.description
dateCell.textLabel?.text = self.getCurrentDateTimeFromTimeStamp(timestamp: dateString)
class Request {
var timestamp:Double
init(dict: [String: Any]) {
self.timestamp = dict["timestamp"] as? Double ?? 0.0
}
func sendRequest(){
print("HEY:")
guard let user = currentUser else { return }
print("USER: \(user.firstLastName)")
if let da = dateField.text{
print(da)
}
print(timeField.text)
print(locationField.text)
print(messageTextView.text)
guard let pickedDate = pickedDate else { return print("pickedDate") }
guard let date = dateField.text else { return print("pickedDate") }
guard let time = timeField.text else { return print("time") }
guard let location = locationField.text else { return print("location")}
let db = Database.database().reference()
let ref = db.child("requests").childByAutoId()
let data = [
"sender": user.uid,
"recipient": recipientUser.uid,
"name": user.firstLastName,
"photoURL": user.photoURL,
"location": location,
"date": date,
"time": time,
"pickedTimestamp": pickedDate.timeIntervalSince1970,
"message": messageTextView.text ?? "",
"status": "PENDING",
"timestamp": [".sv": "timestamp"]
] as [String:Any]
print("HEYO")
ref.setValue(data) { error, ref in
if error == nil {
print("Success")
} else {
print("Failed")
}
}
}
Based on your result, your timestamp is in milliseconds, not seconds. You need to divide by 1000.
You also have the wrong dateFormat. You want to use hh, not HH for the hour. H is for 24-hour time which makes no sense when using a for AM/PM. You should also avoid using dateFormat. Use dateStyle and timeStyle. Let the formatter give you a date formatted best for the user's locale.
Your code also does a lot of needless conversion. You get your timestamp as a Double and store it as a Double. But then your function to convert the timestamp you expect your number of seconds as a String which you then convert back to a Double. Avoid the needless use of a String.
To get the ordinal day, you can use Calendar to extract the day from your Date.
let date = Date()
let calendar = Calendar.current
let dateComponents = calendar.component(.day, from: date)
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .ordinal
let day = numberFormatter.string(from: dateComponents as NSNumber)
From there, you'd just code your date format with a DateFormatter and drop in the ordinal date you extracted above, like so:
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MMMM '\(String(describing: day!)),' yyyy h:mm a"
let dateString = "\(dateFormatter.string(from: date))"
print(dateString)
Additionally, the issue with your year probably stems from the number you're feeding it. timeIntervalSince1970 is expecting whole seconds, so make sure you're feeding it whole seconds.
init(timeIntervalSince1970: TimeInterval) Returns a date object
initialized relative to 00:00:00 UTC on 1 January 1970 by a given
number of seconds.
Adapted from this answer. Additionally, you may find this site helpful for formatting dates.
Here is the problem:
My API returns a list of monthly transactions, I have to grab that result, divide it into sections to populate a tableView where each section is a month with its respective transactions.
Here is the Code
var sectionTest = Dictionary<String, Array<TableSections>>()
var sortedSections: [String] = []
for entry in entries {
if entry.type != "S" {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy/MM/dd"
if let date = dateFormatter.date(from: entry.date) {
dateFormatter.dateFormat = "MMMM"
dateFormatter.locale = NSLocale(localeIdentifier: "pt_BR") as Locale!
let sectionMonth = dateFormatter.string(from: date)
if self.sectionTest.index(forKey: sectionMonth) == nil {
self.sectionTest[sectionMonth] = [TableSections(month: sectionMonth, description: entry.description, value: entry.value, date: entry.date, type: entry.type)]
} else {
self.sectionTest[sectionMonth]?.append(TableSections(month: sectionMonth, description: entry.description, value: entry.value, date: entry.date, type: entry.type))
}
self.sortedSections = self.sectionTest.keys.sorted(by: >)
Here is where I'm stuck
self.sortedSections returns an array sorted by alphabetical order, in this case if I request for the last 60 days of transactions it will return an array with:
print(sortedSections)
["november", "january", "december"]