Reduce an array of objects based on object field - arrays

I have a Country object and a City object
struct Country: {
let name: String
let countryCode: String
let cities: [City]
let population: Int
init(name: String, countryCode: String, cities: [City], population: Int) {
self.name = name
self.countryCode = countryCode
self.cities = cities
self.population = population
}
}
struct City {
let id: Int
let name: String
let latitude: Double
let longitude: Double
let countryCode: String
let population: Int
}
Incoming JSON data looks like this which decodes into [City] array
{
"cities":[
{
"id":1,
"name":"Paris",
"latitude":0,
"logitude":0,
"country_code":"FR",
"population":0
},
{
"id":2,
"name":"Nice",
"latitude":0,
"logitude":0,
"country_code":"FR",
"population":0
},
{
"id":3,
"name":"Berlin",
"latitude":0,
"logitude":0,
"country_code":"DE",
"population":0
},
{
"id":4,
"name":"Munich",
"latitude":0,
"logitude":0,
"country_code":"DE",
"population":0
},
{
"id":5,
"name":"Amsterdam",
"latitude":0,
"logitude":0,
"country_code":"NL",
"population":0
},
{
"id":6,
"name":"Leiden",
"latitude":0,
"logitude":0,
"country_code":"NL",
"population":0
}
]
}
How would I create [Country] array from [City] array efficiently? I've tried to use reduce:into: but not sure that's what I have to use.
I know I could go with an empty array and add/create Countries one by one then search if there is one already and add City to it. That creates awful looking code as for me. I feel like there is an elegant solution to this problem using map or reduce functions.
reduce:into: code I've tried so far
func transformArrayOf(_ cities: [City]) -> [Country] {
let empty: [Country] = []
return cities.reduce(into: empty) { countries, city in
let existing = countries.filter { $0.countryCode == city.countryCode }.first
countries[existing].cities.append(city)
}
}
EDIT:
The function only gets [City] array. So countries must be created only from that.
Dictionary(grouping:by:) with map(_:) works perfectly! Two lines instead on nested for loops and if statements :)
And Country name can be parsed from a country code

Use Dictionary(grouping:by:) and map(_:) combined to get the expected result.
let countries = Dictionary(grouping: cities, by: { $0.countryCode }).map { (countryCode, cities) -> Country in
return Country(name: "", countryCode: countryCode, countryName: "", cities: cities, population: cities.reduce(0) { $0 + $1.population })
}
Since the values for name and countryName are unknown, I've used empty String ("") for both.

This is what Dictionary(grouping:by:) is for:
let citiesByCountryCode = Dictionary(grouping: cities, by: \.countryCode)
But you'll need separate logic to create the countries, because they contain data that isn't derived from the cities like name, countryName (how are those different?), etc.

Related

Combine two arrays by id

I have two arrays and I need to combine them by an id. This is how it looks like:
Every single Station has its own sensors. One, two, three or even none. We can find them thanks to ids. Station has an id property, and Sensors are owners of stationId properties. Unfortunately I can't to download them together in one array, so I have to use two arrays. I want to display them in the list view, so I have to create array of objects where the data of both lists form one list. I have created combined array, but there every Stations has only one Sensor and this my problem.
I can't make array where Stations are owners more than one Sensor. In the code below you can see, that there is a problem with accessing elements. It always takes the first index, but I don't know how to get another ones with same id. I'm stuck
Station:
[{
"id": 14,
"stationName": "Działoszyn",
"gegrLat": "50.972167",
"gegrLon": "14.941319",
"city": {
"id": 192,
"name": "Działoszyn",
"commune": {
"communeName": "Bogatynia",
"districtName": "zgorzelecki",
"provinceName": "DOLNOŚLĄSKIE"
}
},
"addressStreet": null
}]
Sensors:
[{
"id": 92,
"stationId": 14,
"param": {
"paramName": "pył zawieszony PM10",
"paramFormula": "PM10",
"paramCode": "PM10",
"idParam": 3
}
},
{
"id": 88,
"stationId": 14,
"param": {
"paramName": "dwutlenek azotu",
"paramFormula": "NO2",
"paramCode": "NO2",
"idParam": 6
}
}]
Edited code:
func setAddItemList(stations: [Station], sensors: [Sensor]) {
var stationItems = [StationItem]()
var sensorItems = [SensorItem]()
sensors.forEach { sensor in
guard let index = stations.firstIndex(where: { $0.id == sensor.stationId}) else {
print("Failed to find a station for sensor \(sensor.id)")
return
}
let sensorItem = SensorItem(
id: sensor.id,
stationId: sensor.stationId,
param: ParamItem(
paramName: sensor.param?.paramName ?? "",
paramFormula: sensor.param?.paramFormula ?? "",
paramCode: sensor.param?.paramCode ?? "",
idParam: sensor.param?.idParam ?? 0))
sensorItems.append(sensorItem)
if sensorItems.count == sensors.count {
let stationItem = stations.map { station in
StationItem(
id: station.id,
stationId: sensor.stationId,
cityName: station.city?.name ?? "",
addressStreet: station.addressStreet!,
sensor: stationItems[index].sensors?.append(sensorItem) ?? []
)
}
stationItems.append(contentsOf: stationItems)
}
}
I also tested Dictionaries, but I had the same problem.
First map Station to StationItem
var stationItems = stations.map { station in
StationItem(
id: station.id,
stationId: sensor.stationId,
cityName: station.city?.name ?? "",
addressStreet: station.addressStreet!,
sensor: [])
}
Then apply my solution from your previous question but instead of appending a Sensor object you first map it to a SensorItem object as you are doing in your code above

How can I count the item of property as an array inside a class?

How can I count the teamMember in team A?
class BasketBallTeam {
var teamName: String
var teamMember: [String]
init(teamName: String, teamMember: [String]) {
self.teamName = teamName
self.teamMember = teamMember
}
}
let highSchoolTeam: [BasketBallTeam] = [
BasketBallTeam(teamName: "A", teamMember: [
"John", "Sam", "Tom", "Ricky", "George"
]),
BasketBallTeam(teamName: "B", teamMember: [
"Jason", "Michael", "Daniel", "Henry", "David", "Luke"
]),
]
Just count it.
As highSchoolTeam is an array you have to get the first item
let numberOfMembers = highSchoolTeam.first?.teamMember.count
Please name teamMember in plural form teamMembers (or even members) to indicate the array
If you're doing this a lot, it would be best to create a dictionary that lets you easily access teams by their names:
struct BasketBallTeam {
var name: String
var members: [String]
init(name: String, members: [String]) {
self.name = name
self.members = members
}
}
let highSchoolTeams: [BasketBallTeam] = [
BasketBallTeam(name: "A", members: [
"John", "Sam", "Tom", "Ricky", "George"
]),
BasketBallTeam(name: "B", members: [
"Jason", "Michael", "Daniel", "Henry", "David", "Luke"
]),
]
let teamsByName = Dictionary(uniqueKeysWithValues:
highSchoolTeams.lazy.map { team in (key: team.name, value: team) })
Which makes looking up any team by name fast and easy:
let teamA = teamsByName["A"]
let teamASize = teamA?.members.count
print(teamASize as Any)
If you only need to do this once, you can use first(where:):
let teamA = highSchoolTeams.first(where: { $0.name == "A"})
You can get the count for each team as follows:
for team in highSchoolTeam {
print(team.teamMember.count)
}
You can simply use,
first(_:) on highSchoolTeam to get the team where teamName == "A".
count on teamMember to get the total count of teamMembers from the BasketBallTeam object obtained in step-1.
It goes like,
let teamAMemberCount = highSchoolTeam.first{ $0.teamName == "A" }?.teamMember.count
print(teamAMemberCount) //Output: 5

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 simplify an if..else statement when parsing an array

I have a function that filters an array of objects and reduces to one String. The array has this format:
[Person(destination: “city”, surname: [“sur”, “name”]]
This is the function that filters the persons to find the surname by a certain name.
And I have three states of name: "john", "paul", "james". I want to verify if each name exists and to do something with his surname. How can I do this without using if...else. I don't like how it looks with all this if's.
enum Destination: String, RawRepresentable {
case city
case rural
case both
}
func findPerson(person: persons, type: Destination) -> String? {
let surname = persons.filter{ $0.destination == type.rawValue}.reduce("") { id, element in
return element.details.joined(separator: " ")
}
return surname
}
func findPersons(person: persons) {
// Also I want to verify if is not null the string that i receive
if let city = self.findPerson(person: person, type: .city) {
self.handleCity(type: city)
}
if let rural = self.findPerson(person: person, type: .rural) {
self.handleRural(type: rural)
}
if let both = self.findPerson(person: person, type: .both) {
self.handleBoth(type: both)
}
}
enum Destination: String, RawRepresentable {
case city
case rural
case both
static let all : [Destination] = [
.city,
.rural,
.both
]
func findPersons(person: persons) {
// Also I want to verify if is not null the string that i receive
for destination in Destination.all{
if let findPerson(person, type: destination){
self.handleCity(type: destination.rawValue)
}
}
}

Swift - Grouping elements according to their equal features

In Swift 4, I have:
let customers = [Customer(name: "John", country: "US", profession: "Engineer"), Customer(name: "Mary", country: "UK", profession: "Nurse"), Customer(name: "Diana", country: "US", profession: "Engineer"), Customer(name: "Paul", country: "US", profession: "Plumber"), Customer(name: "Sam", country: "UK", profession: "Nurse")]
I would like to have for example a function that could filter the elements in customers, so that each time the names and professions of at least 2 elements in it are equal, they are added to an array automatically created by this function :
var customers1 = [Customer(name: "John", country: "US", profession: "Engineer"), Customer(name: "Diana", country: "US", profession: "Engineer")]
var customers2 = [Customer(name: "Mary", country: "UK", profession: "Nurse"), Customer(name: "Sam", country: "UK", profession: "Nurse")]
I searched without success, however I picked some solutions that perhaps could be adapted to this case:
extension Array where Element: Comparable {
func containsSameElements(as other: [Element]) -> Bool {
return self[1] == other[1] && self[2] == other[2]
}
}
or
func ==<Element : Equatable> (lhs: [[Element]], rhs: [[Element]]) -> Bool {
return lhs.elementsEqual(rhs, by: ==)
}
or
elementsEqual()/contains() with a loop.
or
a combination of flatMap()/reduce()/filter().
Thank you.
Based on your feedback and clarification, I would do something like this.
struct CountryAndProfession: Hashable, CustomStringConvertible {
let country: String
let profession: String
var description: String {
return "CountryAndProfession{country: \(country), profession: \(profession)}"
}
var hashValue: Int {
return "\(country)__\(profession)".hashValue
}
static func ==(left: CountryAndProfession, right: CountryAndProfession) -> Bool {
return left.country == right.country && left.profession == right.profession
}
}
// The Customer Type you apparently are dealing with. (May need a custom init depending on your use case
struct Customer: CustomStringConvertible {
let name: String
let countryAndProfession: CountryAndProfession
var description: String {
return "Customer{name: \(name), countryAndProfession: \(countryAndProfession)}"
}
// returns a dictionary with the passed customer's CountryAndProfession as the key, and the matching
static func makeDictionaryWithCountryAndProfession(from customers: [Customer]) -> [CountryAndProfession : [Customer]] {
var customersArrayDictionary: [CountryAndProfession : [Customer]] = [:]
customers.forEach { (customer) in
if customersArrayDictionary.keys.contains(customer.countryAndProfession) {
customersArrayDictionary[customer.countryAndProfession]?.append(customer)
}
else {
customersArrayDictionary[customer.countryAndProfession] = [customer]
}
}
return customersArrayDictionary
}
static func getArraysBasedOnCountries(from customerArray: [Customer]) -> [[Customer]] {
return Array(makeDictionaryWithCountryAndProfession(from: customerArray).values)
}
}
let arrayOfArrays = [["John", "A", "A" ], ["Mary", "A", "B" ], ["Diana", "A", "A" ], ["Paul", "B", "B" ], ["Sam", "A", "B" ]]
//If you're dealing with non-predictable data, you should probably have some Optionality
let allCustomers = arrayOfArrays.map{ Customer(name: $0[0], countryAndProfession: CountryAndProfession(country: $0[1], profession: $0[2])) }
let splitCustomers = Customer.getArraysBasedOnCountries(from: allCustomers)
//contains [[John, Diana], [Mary, Sam], [Paul]]
I'm still not quite sure what you want your final result to look like (something that is always helpful to put in the question), but you should be able to get the result you're looking for using makeDictionaryWithCountryAndProfession combined with the specific CountryAndProfession you're looking for or using .filter
This is how I would recommend doing what you're trying to do:
struct Customer {
let name: String
let country: String
let profession: String
func countryMatches(with otherCustomer: Customer) -> Bool {
return country == otherCustomer.country
}
func professionMatches(with otherCustomer: Customer) -> Bool {
return profession == otherCustomer.profession
}
func countryAndProfessionMatch(with otherCustomer: Customer) -> Bool {
return countryMatches(with: otherCustomer) && professionMatches(with: otherCustomer)
}
static func getAllCustomersWithProfessionsAndCountriesMatching(with customer: Customer, from allCustomers: [Customer]) -> [Customer] {
return allCustomers.filter { customer.countryAndProfessionMatch(with: $0) }
}
}
let arrayOfArrays = [["John", "A", "A" ], ["Mary", "A", "B" ], ["Diana", "A", "A" ], ["Paul", "B", "B" ], ["Sam", "A", "B" ]]
//If you're dealing with non-predictable data, you should probably have some Optionality
let allCustomers = arrayOfArrays.map{ Customer(name: $0[0], country: $0[1], profession: $0[2]) }
let allCustomersMatchingJohnProperties = Customer.getAllCustomersWithProfessionsAndCountriesMatching(with: allCustomers[0], from: allCustomers)
// contains John and Diane
let allCustomersMatchingMaryProperties = Customer.getAllCustomersWithProfessionsAndCountriesMatching(with: allCustomers[1], from: allCustomers)
// contains Mary and Sam
I believe this does what you're looking to do, but with a more structured/maintainable approach.
getAllCustomersWithProfessionsAndCountriesMatching is almost definitely way too long, but left it that way to be clear for the answer. I would advice renaming it to fit your use case.

Resources