Reduce an array of objects based on object field

Viewed 88

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

2 Answers

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