Struct inside struct Equatable count duplicate item Swift - arrays

The answer I found is with one level of struct. In my code I want to find out how many same dates is in inside of the struct of the struct. I never use equatable before so I'm not sure how to implement it with struct inside struct.
I'm using Charts pod and the array chartMetricItemSets contains different readings with multiples days. ex: ["name": sugar, [["date": "Oct 10", "value": 120], ["date": "Oct 10", "value": 121], ["date": "Oct 11", "value": 120], ["date": "Oct 13", "value": 120], ["date": "Oct 13", "value": 121]]. The xAxisDates will be ["Oct 10", "Oct 11", "Oct 13"]
the x is the date and the y is for the same date sums
Goal: chartDataEntries should append [x:0(10/10), y:241], [x:1(10/11), y:120], [x:2(10/13), y: 241]
Thank you!
struct ChartData {
let name: String
let value: [ChartValue]
}
struct ChartValue {
let date: Date
let value: Double
}
var chartMetricItemSets = [[ChartData]]()
var xAxisDates = [String]() //date array
func getChartDataSets() {
for (index, chartHealthItemSet) in chartMetricItemSets.enumerated() {
let dataSourceSelected = dataSourcesSelected[index]
var chartDataEntries = [ChartDataEntry]() //array display on graph
var chartYaxis: Double = 0
var datePoint: Int = 0
var countChartYaxis = 0
var duplicateDateCount = 0
for (index, item) in chartHealthItemSet[0].value.enumerated() {
for (dateIndex, date) in xAxisDates.enumerated() {
if item.date.convertDateToString(dateFormat: .monthDay) == date {
chartYaxis += item.value
datePoint = dateIndex
chartYaxis += 1
break
}
//missing counting duplicateDateCount
if duplicateDateCount == countChartYaxis {
chartDataEntries.append(ChartDataEntry(x: Double(datePoint), y: chartYaxis.roundUpNDecimal(n: 2)))
}
}
print("chartYaxis: \(chartYaxis)")
print("chartDataEntries: \(chartDataEntries)")
}
chartDataEntries.sort(by: { $0.x < $1.x })
let set = LineChartDataSet(entries: chartDataEntries,
label: HEALTH_TABLE.allCases[dataSourceSelected].info.name)
set.mode = .linear // cubic line effect (smoother)
set.colors = [HEALTH_TABLE.allCases[dataSourceSelected].info.chartColor]
set.setCircleColor(.black)
set.lineWidth = 1.5
set.circleRadius = 3
set.drawCircleHoleEnabled = false
set.drawCirclesEnabled = true
set.drawHorizontalHighlightIndicatorEnabled = true
lineChartData?.addDataSet(set)
}
chartView.xAxis.valueFormatter = IndexAxisValueFormatter(values: xAxisDates)
chartView.xAxis.granularity = 1
chartView.xAxis.axisMinLabels = 4
chartView.xAxis.axisMaxLabels = 6
DispatchQueue.main.async {
self.chartView.data = self.lineChartData
}
}

In Swift 4 a useful API was introduced to group arrays to dictionaries by an arbitrary predicate.
You can group chartHealthItemSet[0]
let grouped = Dictionary(grouping: chartHealthItemSet[0], by: {$0.date.convertDateToString(dateFormat: .monthDay)}
The result is ["10/10":[ChartData], "10/11":[ChartData], ...]
Sort the keys and assign them to xAxisDates
xAxisDates = grouped.keys.sorted()
To get the sum of the values get the array from the grouped dictionary for the date key, map it to the values and sum them up
for (index, date) in xAxisDates.enumerated() {
let sum = grouped[date]!.map{$0.value}.reduce{0.0, +}
chartDataEntries.append(ChartDataEntry(x: Double(index), y: sum.roundUpNDecimal(n: 2)))
}

Related

Group days of the week with same values in Swift

I have a service request with schedules for a shop in an array called "hoursArray" with this format:
"hours": [
      "00:00-23:59",
      "00:00-23:59",
      "00:00-21:59",
      "00:00-21:59",
      "00:00-22:59",
      "00:00-22:59",
      "00:00-23:59"
    ]
I show this information in an horizontal stack view that contains two verticals stack views, one with labels for the seven days of the week and another one with seven labels for the schedule for that day, I fill this labels with this function:
func getSchedule(){
scheduleLabel1.text = hoursArray[0] as? String
scheduleLabel2.text = hoursArray[1] as? String
scheduleLabel3.text = hoursArray[2] as? String
scheduleLabel4.text = hoursArray[3] as? String
scheduleLabel5.text = hoursArray[4] as? String
scheduleLabel6.text = hoursArray[5] as? String
scheduleLabel7.text = hoursArray[6] as? String
dayLabel1.text = "Monday"
dayLabel2.text = "Tuesday"
dayLabel3.text = "Wednesday"
dayLabel4.text = "Thursday"
dayLabel5.text = "Friday"
dayLabel6.text = "Saturday"
dayLabel7.text = "Sunday"
}
What I need is to group correlative days to show it when I have a response of the request with days with the same value, for example if Monday and Tuesday have the same value I will show "Monday - Tuesday" in day1Label.text and its schedule in scheduleLabel1.text, how could I do that?
You can use following function to format
let hours = [
"00:00-23:59",
"00:00-23:59",
"00:00-21:59",
"00:00-21:59",
"00:00-22:59",
"00:00-22:59",
"00:00-23:59"
]
let days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
func format() -> [(String, String)] {
var temp: [(String, String)] = []
for i in 0..<hours.count {
temp.append((days[i], hours[i]))
}
var result: [(String, String)] = []
var index = 0
var dayStart: String?
var dayEnd: String?
var time: String?
while index < temp.count {
if dayStart == nil {
dayStart = temp[index].0
time = temp[index].1
}
if (index != temp.count - 1) && (temp[index].1 == temp[index+1].1) {
dayEnd = temp[index+1].0
index += 1
}
else {
if let start = dayStart, let tm = time {
if let end = dayEnd {
result.append(("\(start)-\(end)", tm))
dayEnd = nil
}
else {
result.append((start, tm))
}
dayStart = nil
time = nil
}
index += 1
}
}
return result
}
From this you will get formatted result and you can then pass these as an input to TableView or IBOutletCollection. Using static labels for these is not recommended as base on groups numbers of labels may change b/w 1-7. So it's better to use Tableview.
One way to do it is like this, first grouping the data by hour range:
let hourRanges = [
"00:00-23:59",
"00:00-23:59",
"00:00-21:59",
"00:00-21:59",
"00:00-22:59",
"00:00-22:59",
"00:00-23:59"
]
let weekDays = [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
]
struct WeekDaysHourRange {
let hourRange: String
var weekDays: [String]
}
var daysRanges: [WeekDaysHourRange] = []
// assuming both arrays have the same length, otherwise checks are needed
var previousWeekDayRange: WeekDaysHourRange?
for i in 0...6 {
let hourRange = hourRanges[i]
let weekDay = weekDays[i]
if previousWeekDayRange?.hourRange == hourRange {
daysRanges[daysRanges.count - 1].weekDays.append(weekDay)
} else {
let newWeekDayRange = WeekDaysHourRange(hourRange: hourRange, weekDays: [weekDay])
previousWeekDayRange = newWeekDayRange
daysRanges.append(newWeekDayRange)
}
}
To update the UI, I would create to arrays of UILabels, to allow populating the labels with a for loop easilly, like this:
let scheduleLabels: [UILabel] = [
scheduleLabel1,
scheduleLabel2,
scheduleLabel3,
scheduleLabel4,
scheduleLabel5,
scheduleLabel6,
scheduleLabel7
]
let dayLabels: [UILabel] = [
dayLabel1,
dayLabel2,
dayLabel3,
dayLabel4,
dayLabel5,
dayLabel6,
dayLabel7
]
daysRanges.enumerated().forEach { (i, weekDayHourRange) in
var daysString: String
if weekDayHourRange.weekDays.count == 1 {
daysString = weekDayHourRange.weekDays.first!
} else {
daysString = "\(weekDayHourRange.weekDays.first!) - \(weekDayHourRange.weekDays.last!)"
}
dayLabels[i].text = daysString
scheduleLabels[i].text = weekDayHourRange.hourRange
}
Following Joakim Danielson comment, and If you only need to create the view once and not updated it, you can create the views on the fly:
daysRanges.enumerated().forEach { (i, weekDayHourRange) in
var daysString: String
if weekDayHourRange.weekDays.count == 1 {
daysString = weekDayHourRange.weekDays.first!
} else {
daysString = "\(weekDayHourRange.weekDays.first!) - \(weekDayHourRange.weekDays.last!)"
}
let dayLabel = UILabel()
dayLabel.text = daysString
let scheduleLabel = UILabel()
scheduleLabel.text = weekDayHourRange.hourRange
dayLabelsStackView.addArrangedSubview(dayLabel)
scheduleLabelsStackView.addArrangedSubview(scheduleLabel)
}
This would output something like this:
"Monday - Tuesday - 00:00-23:59"
"Wednesday - Thursday - 00:00-21:59"
"Friday - Saturday - 00:00-22:59"
"Sunday - 00:00-23:59"
There are already some decent answers, but reading this it seemed like a functional approach would be a good match for handling the inputs. This approach uses zip to combine the two datasets into an array of tuples and then reduce to consolidate them, with a bit of pattern matching to mangle the strings into the required shape. Finally it iterates through the resulting array with map to add the consolidated items to the stackviews.
let days = ["Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday"
]
let hours = [
"00:00-23:59",
"00:00-23:59",
"00:00-23:59",
"00:00-21:59",
"00:00-22:59",
"00:00-22:59",
"00:00-23:59"
]
zip(days, hours)
.reduce([(String, String)]()){
guard let last = $0.last else {return [$1]}
if last.1 == $1.1 {
var days = last.0
if let lastDayRange = days.range(of: #"(?<=- )[A-Z,a-z]*day"#, options: .regularExpression) {
days.replaceSubrange(lastDayRange, with: $1.0)
} else {
days = days + " - \($1.0)"
}
return $0.dropLast() + [(days, $1.1)]
} else {
return $0 + [$1]
}
}
.map{
let dayLabel = UILabel()
let hoursLabel = UILabel()
dayLabel.text = $0.0
hoursLabel.text = $0.1
daysStack.addArrangedSubview(dayLabel) //or whatever the stackview is called
hoursStack.addArrangedSubview(hoursLabel)
}
The UI part is simplistic, and more mght be required to get the desired output, but it should be enough to get started.

Swift loop through array of dictionaries and create a new one

I'm struggling on getting a new array of dictionaries with conditioned 10 elements out of 80.
here is sample of students json:
{
"id": 111,
"name": "John",
"gender": "male",
"grade": 80,
"extraCredit": 20
},
{
"id": 112,
"name": "Jenny",
"gender": "female",
"grade": 85,
"extraCredit": 5
}
and my struct:
struct StudentData: Decodable {
var id: Int
var name: String
var grade: Int
var extraCredit: Int
}
I need to calculate each student's grade to get final grade and display top 10
here is my code to loop through:
var newStudentData = [Dictionary<String, Any>]()
for student in studentData {
let finalGrade = student.grade + (student.extraCredit * 0.5)
if finalGrade > 88 {
newStudentData.append(student) // error on cannot convert value type of ’StudentData’ to expected argument type ‘[String: Any]
}
}
what did I do wrong? or is there a better way?
newStudentData is a dictionary (why?), but you're appending instances of StudentData to it. This won't work.
That said, I would probably rewrite this like so:
struct StudentData: Decodable {
let id: Int
let name: String
let grade: Int
let extraCredit: Int
var finalGrade: Int {
return self.grade + self.extraCredit / 2
}
}
var students = [StudentData]()
// fill students array here
let beat88 = students.filter { $0.finalGrade > 88 }
let top10 = beat88.sorted { $0.finalGrade > $1.finalGrade }.prefix(10)

Creating a body for Alamofire POST request Swift 4

I need help with creating a custom body when sending POST request with Alamofire.
I'm sending to API products. There are two types of products. First type has just quantity, second one - different quantities(size_id) and quantities that match each size_id.
Final body should look like:
"factory_id": "1"
"order_products[0][product_id]": "1"
"order_products[0][size_id]": "2"
"order_products[0][quantity]": "10"
"order_products[1][product_id]": "1"
"order_products[1][size_id]": "3"
"order_products[1][quantity]": "10"
"order_products[1][product_id]": "2"
"order_products[1][size_id]": "2"
"order_products[1][quantity]": "10"
"order_products[2][product_id]": "3"
"order_products[2][quantity]": "10"
Here's what I achieved so far:
var createOrderBody = [String: Any]()
let productIds = ["1", "2", "3"]
var body = [String: Any]()
var quantity = ["1" : "10", "2": "10"]
var noIdQuantity = ["10"]
var finalBody = [String: Any]()
func formBody(products: String, factoryId: String, productId: String, size_id: String, quantity: String) -> [String: Any] {
createOrderBody["factory_id"] = factoryId
createOrderBody["order_products[\(products)][product_id]"] = productId
createOrderBody["order_products[\(products)][size_id]"] = size_id
createOrderBody["order_products[\(products)][quantity]"] = quantity
return createOrderBody
}
for (index, value) in productIds.enumerated() {
for (id, size) in quantity {
print(id)
print(size)
body = formBody(products: String(index), factoryId: "1", productId: String(value), size_id: id, quantity: size)
print("Body quantity - ", body)
}
}
And the result I have is:
"factory_id": "1",
"order_products[0][product_id]": "1"
"order_products[0][size_id]": "2",
"order_products[0][quantity]": "10",
"order_products[1][product_id]": "2",
"order_products[1][size_id]": "2",
"order_products[1][quantity]": "10",
"order_products[2][product_id]": "3",
"order_products[2][size_id]": "2",
"order_products[2][quantity]": "10",
As you can see, I have almost achieved desired result, but the problem is that it is adding only last element of quantity dictionary and omits other values. Also, I don't know how to add quantity to the product, that doesn't have size_id
Also, I know that it is not a good practice to place for in loop inside other for in loop but I'm new to development and this is the best idea that I have came up with.
Would be grateful for any help with this issue, as I've been battling with it almost for a week right now.
Many thanks and have a nice weekends!
After studying and searching on the Internet, here's a solution to the issue we have here. For reference, original post here - Original Post
Assuming that every product has to hold it's own quantity:
We can define a struct like this:
struct Product {
let id: String
let quantities: [(sizeId: String, quantity: Int)]?
let noIdQuantity: Int?
init(id: String, quantities: [(sizeId: String, quantity: Int)]) {
self.id = id
self.quantities = quantities
self.noIdQuantity = nil
}
init(id: String, quantity: Int) {
self.id = id
self.quantities = nil
self.noIdQuantity = quantity
}
}
With the struct above, we just need only one input variable and one output variable:
// Define input `product with id` as an Array of `Product`
let products = [
Product(id: "1", quantities: [("1", 10), ("2", 10)]),
Product(id: "2", quantities: [("1", 10), ("2", 10)]),
Product(id: "3", quantities: [("1", 15), ("2", 20)]),
Product(id: "4", quantity: 10),
]
// Output dictionary
var body = [String: Any]()
To make entries for a single Product into a Dictionary:
extension Product {
func formBody(_ index: inout Int, into body: inout [String: Any]) {
if let quantities = self.quantities {
for (sizeId, quantity) in quantities {
body["order_products[\(index)][product_id]"] = self.id
body["order_products[\(index)][size_id]"] = sizeId
body["order_products[\(index)][quantity]"] = quantity
index += 1
}
}
if let quantity = self.noIdQuantity {
body["order_products[\(index)][product_id]"] = self.id
body["order_products[\(index)][quantity]"] = quantity
index += 1
}
}
}
And use them as follows:
var index = 0
body["factory_id"] = "1"
for product in products {
product.formBody(&index, into: &body)
}
print("Body quantity - ", body)
body.sorted {$0.key < $1.key}.forEach{print("\($0.key)=\($0.value)")} //For showing readable result, not for Alammofire body
So, the final result would be:
factory_id=1
order_products[0][product_id]=1
order_products[0][quantity]=10
order_products[0][size_id]=1
order_products[1][product_id]=1
order_products[1][quantity]=10
order_products[1][size_id]=2
order_products[2][product_id]=2
order_products[2][quantity]=10
order_products[2][size_id]=1
order_products[3][product_id]=2
order_products[3][quantity]=10
order_products[3][size_id]=2
order_products[4][product_id]=3
order_products[4][quantity]=15
order_products[4][size_id]=1
order_products[5][product_id]=3
order_products[5][quantity]=20
order_products[5][size_id]=2
order_products[6][product_id]=4
order_products[6][quantity]=10
Hope this helps someone, who has same complex structure or similar.

How can I compare two date string in array of Objects using NSPredicate?

I want to compare dates of each object using NSPredicate. If the Object has same dateCreated It will return an array of object which has same dates.
In the below Array of dictionary 0 index has the different date as compared to another one how can I get data like that.
Ex:
{
"Data": [
{
"id": "c9967156ad8945fba8cc482cd8aad900",
"description": "Hi",
"dateCreated": "2018-03-20T06:15:11.000+0000",
},
{
"id": "343e70818044457b884f7ad1907803fa",
"description": "The only ",
"dateCreated": "2018-03-16T17:22:50.000+0000",
},
{
"id": "dd542edfaa364e40ae0ef0562b6831be",
"description": "The new ",
"dateCreated": "2018-03-16T17:10:36.000+0000",
},
{
"id": "090f43c83e5b42039f70b133d031e715",
"description": "The new version ",
"dateCreated": "2018-03-16T17:08:07.000+0000",
},
{
"id": "b2ddb8fa990843a28f0670d2b88e3d01",
"description": "Add to the test",
"dateCreated": "2018-03-16T17:08:07.000+0000",
}
]
}
#Edit1:
I am converting dateCreated String object to Date and then I am using NSPredicate for desired data. Currently, I am trying with NSPredicate
#Edit2
Currently, I am not using NSPredicate. I am iterating each element of the array and compare its date
if let dateCur = dateCreated.dateFromISO8601 {
if let datePrev = dateCreatedPrev.dateFromISO8601 {
let curLocal = ISO8601.getStringDate(dateCur). // dd/MM/yyyy
let prevLocal = ISO8601.getStringDate(datePrev). // dd/MM/yyyy
if (curLocal.compare(prevLocal) != .orderedSame {
//diffrent
}else {
//same
}
}
}
I am using an extension for achieving it
extension Date {
func getFormatter() -> DateFormatter {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .gregorian)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"
return formatter
}
var iso8601: String {
return Formatter.iso8601.string(from: self)
}
extension String {
var dateFromISO8601: Date? {
return Formatter.iso8601.date(from: self) // "Mar 22, 2017, 10:22 AM"
}
}
Can you please help me.
First of all there is no need to use NSPredicate in Swift (except the APIs which require NSPredicate).
The native filter method is more appropriate.
I recommend to decode the JSON
let jsonString = """
{
"Data": [
{"id": "c9967156ad8945fba8cc482cd8aad900", "description": "Hi", "dateCreated": "2018-03-20T06:15:11.000+0000"},
{"id": "343e70818044457b884f7ad1907803fa", "description": "The only ", "dateCreated": "2018-03-16T17:22:50.000+0000"},
{"id": "dd542edfaa364e40ae0ef0562b6831be", "description": "The new ", "dateCreated": "2018-03-16T17:10:36.000+0000"},
{"id": "090f43c83e5b42039f70b133d031e715", "description": "The new version ", "dateCreated": "2018-03-16T17:08:07.000+0000"},
{"id": "b2ddb8fa990843a28f0670d2b88e3d01", "description": "Add to the test", "dateCreated": "2018-03-16T17:08:07.000+0000"}
]
}
"""
into custom structs
struct Root : Decodable {
private enum CodingKeys : String, CodingKey { case items = "Data" }
let items : [Item]
}
struct Item : Decodable {
let id, description : String
let dateCreated : Date
}
The decoder uses a custom date formatter to decode the ISO8601 date properly
let data = Data(jsonString.utf8)
let decoder = JSONDecoder()
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ"
decoder.dateDecodingStrategy = .formatted(dateFormatter)
do {
let result = try decoder.decode(Root.self, from: data)
The array is in result.items. Now you can filter the array by a specific date. In this example the reference date is created hard-coded with DateComponents.
let components = DateComponents(timeZone: TimeZone(secondsFromGMT: 0), year:2018, month:03, day:16, hour:17, minute:8, second:7)
let date = Calendar.current.date(from: components)!
let filteredItems = result.items.filter { $0.dateCreated == date }
print(filteredItems)
} catch { print(error) }
If you want to find all matching records for a given date in the array use a loop
for item in result.items {
let filteredItems = result.items.filter { $0.dateCreated == item.dateCreated }
if filteredItems.count > 1 { print(filteredItems) }
}
You might look into Dictionary's initializer init(grouping:by:) which was introduced in Swift 4.
You should have your objects array. Then you can group them into [String:Object] where the key will be your Date in yyyy-MM-dd format as String.
Prerequisites: Your dateCreated property should be in Date format.
let groupedObjectBySameDate = Dictionary(grouping: objects) { (object) -> String in
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
return formatter.string(from: object.dateCreated)
}
An example:
As I don't know your data structure, I'm providing an easy example that can help you understand what's going on.
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ"
formatter.locale = Locale(identifier: "en_US_POSIX")
return formatter
}
let stringDates = ["2018-03-20T06:15:11.000+0000", "2018-03-16T17:22:50.000+0000", "2018-03-16T17:10:36.000+0000", "2018-03-16T17:08:07.000+0000", "2018-03-16T17:08:07.000+0000"]
let dates = stringDates.map { (each) -> Date in
return dateFormatter.date(from: each) ?? Date(timeIntervalSinceReferenceDate: 0)
}
let groupedObjectBySameDate = Dictionary(grouping: dates) { (date) -> String in
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
return formatter.string(from: date)
}
print(groupedObjectBySameDate)
// This will produce:
// [
// "2018-03-20": [2018-03-20 06:15:11 +0000],
// "2018-03-16": [2018-03-16 17:22:50 +0000, 2018-03-16 17:10:36 +0000, 2018-03-16 17:08:07 +0000, 2018-03-16 17:08:07 +0000]
// ]

Group elements of an array by some property

I have an array of objects with property date.
What I want is to create array of arrays where each array will contain objects with the same date.
I understand, that I need something like .filter to filter objects, and then .map to add every thing to array.
But how to tell .map that I want separate array for each group from filtered objects and that this array must be added to "global" array and how to tell .filter that I want objects with the same date ?
It might be late but new Xcode 9 sdk dictionary has new init method
init<S>(grouping values: S, by keyForValue: (S.Element) throws -> Key) rethrows where Value == [S.Element], S : Sequence
Documentation has simple example what this method does.
I just post this example below:
let students = ["Kofi", "Abena", "Efua", "Kweku", "Akosua"]
let studentsByLetter = Dictionary(grouping: students, by: { $0.first! })
Result will be:
["E": ["Efua"], "K": ["Kofi", "Kweku"], "A": ["Abena", "Akosua"]]
improving on oriyentel solution to allow ordered grouping on anything:
extension Sequence {
func group<GroupingType: Hashable>(by key: (Iterator.Element) -> GroupingType) -> [[Iterator.Element]] {
var groups: [GroupingType: [Iterator.Element]] = [:]
var groupsOrder: [GroupingType] = []
forEach { element in
let key = key(element)
if case nil = groups[key]?.append(element) {
groups[key] = [element]
groupsOrder.append(key)
}
}
return groupsOrder.map { groups[$0]! }
}
}
Then it will work on any tuple, struct or class and for any property:
let a = [(grouping: 10, content: "a"),
(grouping: 20, content: "b"),
(grouping: 10, content: "c")]
print(a.group { $0.grouping })
struct GroupInt {
var grouping: Int
var content: String
}
let b = [GroupInt(grouping: 10, content: "a"),
GroupInt(grouping: 20, content: "b"),
GroupInt(grouping: 10, content: "c")]
print(b.group { $0.grouping })
With Swift 5, you can group the elements of an array by one of their properties into a dictionary using Dictionary's init(grouping:by:) initializer. Once done, you can create an array of arrays from the dictionary using Dictionary's values property and Array init(_:) initializer.
The following Playground sample code shows how to group the elements of an array by one property into a new array of arrays:
import Foundation
struct Purchase: CustomStringConvertible {
let id: Int
let date: Date
var description: String {
return "Purchase #\(id) (\(date))"
}
}
let date1 = Calendar.current.date(from: DateComponents(year: 2010, month: 11, day: 22))!
let date2 = Calendar.current.date(from: DateComponents(year: 2015, month: 5, day: 1))!
let date3 = Calendar.current.date(from: DateComponents(year: 2012, month: 8, day: 15))!
let purchases = [
Purchase(id: 1, date: date1),
Purchase(id: 2, date: date1),
Purchase(id: 3, date: date2),
Purchase(id: 4, date: date3),
Purchase(id: 5, date: date3)
]
let groupingDictionary = Dictionary(grouping: purchases, by: { $0.date })
print(groupingDictionary)
/*
[
2012-08-14 22:00:00 +0000: [Purchase #4 (2012-08-14 22:00:00 +0000), Purchase #5 (2012-08-14 22:00:00 +0000)],
2010-11-21 23:00:00 +0000: [Purchase #1 (2010-11-21 23:00:00 +0000), Purchase #2 (2010-11-21 23:00:00 +0000)],
2015-04-30 22:00:00 +0000: [Purchase #3 (2015-04-30 22:00:00 +0000)]
]
*/
let groupingArray = Array(groupingDictionary.values)
print(groupingArray)
/*
[
[Purchase #3 (2015-04-30 22:00:00 +0000)],
[Purchase #4 (2012-08-14 22:00:00 +0000), Purchase #5 (2012-08-14 22:00:00 +0000)],
[Purchase #1 (2010-11-21 23:00:00 +0000), Purchase #2 (2010-11-21 23:00:00 +0000)]
]
*/
Abstracting one step, what you want is to group elements of an array by a certain property. You can let a map do the grouping for you like so:
protocol Groupable {
associatedtype GroupingType: Hashable
var grouping: GroupingType { get set }
}
extension Array where Element: Groupable {
typealias GroupingType = Element.GroupingType
func grouped() -> [[Element]] {
var groups = [GroupingType: [Element]]()
for element in self {
if let _ = groups[element.grouping] {
groups[element.grouping]!.append(element)
} else {
groups[element.grouping] = [element]
}
}
return Array<[Element]>(groups.values)
}
}
Note that this grouping is stable, that is groups appear in order of appearance, and inside the groups the individual elements appear in the same order as in the original array.
Usage Example
I'll give an example using integers; it should be clear how to use any (hashable) type for T, including Date.
struct GroupInt: Groupable {
typealias GroupingType = Int
var grouping: Int
var content: String
}
var a = [GroupInt(grouping: 1, content: "a"),
GroupInt(grouping: 2, content: "b") ,
GroupInt(grouping: 1, content: "c")]
print(a.grouped())
// > [[GroupInt(grouping: 2, content: "b")], [GroupInt(grouping: 1, content: "a"), GroupInt(grouping: 1, content: "c")]]
Rapheal's solution does work. However, I would propose altering the solution to support the claim that the grouping is in fact stable.
As it stands now, calling grouped() will return a grouped array but subsequent calls could return an array with groups in a different order, albeit the elements of each group will be in the expected order.
internal protocol Groupable {
associatedtype GroupingType : Hashable
var groupingKey : GroupingType? { get }
}
extension Array where Element : Groupable {
typealias GroupingType = Element.GroupingType
func grouped(nilsAsSingleGroup: Bool = false) -> [[Element]] {
var groups = [Int : [Element]]()
var groupsOrder = [Int]()
let nilGroupingKey = UUID().uuidString.hashValue
var nilGroup = [Element]()
for element in self {
// If it has a grouping key then use it. Otherwise, conditionally make one based on if nils get put in the same bucket or not
var groupingKey = element.groupingKey?.hashValue ?? UUID().uuidString.hashValue
if nilsAsSingleGroup, element.groupingKey == nil { groupingKey = nilGroupingKey }
// Group nils together
if nilsAsSingleGroup, element.groupingKey == nil {
nilGroup.append(element)
continue
}
// Place the element in the right bucket
if let _ = groups[groupingKey] {
groups[groupingKey]!.append(element)
} else {
// New key, track it
groups[groupingKey] = [element]
groupsOrder.append(groupingKey)
}
}
// Build our array of arrays from the dictionary of buckets
var grouped = groupsOrder.flatMap{ groups[$0] }
if nilsAsSingleGroup, !nilGroup.isEmpty { grouped.append(nilGroup) }
return grouped
}
}
Now that we track the order that we discover new groupings, we can return a grouped array more consistently than just relying on a Dictionary's unordered values property.
struct GroupableInt: Groupable {
typealias GroupingType = Int
var grouping: Int?
var content: String
}
var a = [GroupableInt(groupingKey: 1, value: "test1"),
GroupableInt(groupingKey: 2, value: "test2"),
GroupableInt(groupingKey: 2, value: "test3"),
GroupableInt(groupingKey: nil, value: "test4"),
GroupableInt(groupingKey: 3, value: "test5"),
GroupableInt(groupingKey: 3, value: "test6"),
GroupableInt(groupingKey: nil, value: "test7")]
print(a.grouped())
// > [[GroupableInt(groupingKey: 1, value: "test1")], [GroupableInt(groupingKey: 2, value: "test2"),GroupableInt(groupingKey: 2, value: "test3")], [GroupableInt(groupingKey: nil, value: "test4")],[GroupableInt(groupingKey: 3, value: "test5"),GroupableInt(groupingKey: 3, value: "test6")],[GroupableInt(groupingKey: nil, value: "test7")]]
print(a.grouped(nilsAsSingleGroup: true))
// > [[GroupableInt(groupingKey: 1, value: "test1")], [GroupableInt(groupingKey: 2, value: "test2"),GroupableInt(groupingKey: 2, value: "test3")], [GroupableInt(groupingKey: nil, value: "test4"),GroupableInt(groupingKey: nil, value: "test7")],[GroupableInt(groupingKey: 3, value: "test5"),GroupableInt(groupingKey: 3, value: "test6")]]
+1 to GolenKovkosty answer.
init<S>(grouping values: S, by keyForValue: (S.Element) throws -> Key) rethrows where Value == [S.Element], S : Sequence
More Examples:
enum Parity {
case even, odd
init(_ value: Int) {
self = value % 2 == 0 ? .even : .odd
}
}
let parity = Dictionary(grouping: 0 ..< 10 , by: Parity.init )
Equilvalent to
let parity2 = Dictionary(grouping: 0 ..< 10) { $0 % 2 }
In your case:
struct Person : CustomStringConvertible {
let dateOfBirth : Date
let name :String
var description: String {
return "\(name)"
}
}
extension Date {
init(dateString:String) {
let formatter = DateFormatter()
formatter.timeZone = NSTimeZone.default
formatter.dateFormat = "MM/dd/yyyy"
self = formatter.date(from: dateString)!
}
}
let people = [Person(dateOfBirth:Date(dateString:"01/01/2017"),name:"Foo"),
Person(dateOfBirth:Date(dateString:"01/01/2017"),name:"Bar"),
Person(dateOfBirth:Date(dateString:"02/01/2017"),name:"FooBar")]
let parityFields = Dictionary(grouping: people) {$0.dateOfBirth}
Output:
[2017-01-01: [Foo, Bar], 2017-02-01: [FooBar] ]
This is a clean way to perform group by:
let grouped = allRows.group(by: {$0.groupId}) // Dictionary with the key groupId
Assuming you have array of contacts like :
class ContactPerson {
var groupId:String?
var name:String?
var contactRecords:[PhoneBookEntry] = []
}
To achieve this, add this extension:
class Box<A> {
var value: A
init(_ val: A) {
self.value = val
}
}
public extension Sequence {
func group<U: Hashable>(by key: (Iterator.Element) -> U) -> [U: [Iterator.Element]] {
var categories: [U: Box<[Iterator.Element]>] = [:]
for element in self {
let key = key(element)
if case nil = categories[key]?.value.append(element) {
categories[key] = Box([element])
}
}
var result: [U: [Iterator.Element]] = Dictionary(minimumCapacity: categories.count)
for (key, val) in categories {
result[key] = val.value
}
return result
}
}

Resources