One use case of the Map object

ES6 introduced the Map object into JavaScript. From my experience, other features of ES6, like arrow functions or object destructuring are widely used but one needs to look hard to find Maps in codes. Let's have a look at one example where Map can be useful.

About Map

The Map object holds key-value pairs, just like “regular” objects. The good thing in Map is that as opposed to objects, Map can hold any values both as key and value. The creation of a Map can be done using the new keyword and we can write some key-value pairs in the parenthesis:

const animals = new Map([
  ['dog', 'Spike'],
  ['cat', 'Fluffy'],
  ['fish', 'Nemo']
]);

We can also use the Map.set(KEY, VALUE) syntax for adding new elements to the Map.

It’s easy to find the values belonging to each key:

console.log(animals.get('dog')); // 'Spike'

We can also extract all keys and all values with a little trick:

console.log(Array.from(animals.keys())); // [ 'dog', 'cat', 'fish' ]
console.log(Array.from(animals.values())); // [ 'Spike', 'Fluffy', 'Nemo' ]

This works because animals.keys() returns a Map iterable, so similarly to DOM node elements, we can create a ‘real’ array using the Array.from() - also ES6 - method.

Now that we have a bird’s view on Map, let’s take a situation where we will actually use it.

The problem

The local tourist office approaches your company to create an app that gives information on each restaurant in the area for visitors. One part of the app is to display ratings of the restaurants. The rates go from 1 to 5 and their meaning is the following: 5 - outstanding, 4 - good, 3 - average, 2 - room for improvement, 1 - not recommended.

The input is an array of objects which contains a bulk of information on each restaurant: name, location, style etc. One of these properties is ‘rating’, and the value of the property is a number from 1 to 5.

Your task is to complete a small part of the app, which collects how many outstanding, good etc. restaurants are in the area. If a rating cannot be found, then you still should display it with a value of 0.

Below comes a portion of the data:

const restaurants = [
  {
    name: 'Frankie',
    location: '23 Main St',
    rating: 5
  },
  {
    name: 'Tower',
    location: '5 Hay St',
    rating: 4
  },
  {
    name: 'We can cook!',
    location: '52 Gregory Rd',
    rating: 4
  },
  {
    name: 'Delicio Us',
    location: '441 Umbria St',
    rating: 5
  },
  {
    name: 'To the Palm',
    location: '19 Palm St',
    rating: 3
  },
  {
    name: 'You can sea us',
    location: '96 Beach Rd',
    rating: 4
  },
  {
    name: 'Artificio',
    location: '78 Joseph St',
    rating: 2
  }
];

The restaurants array contains much more objects and properties, but the array above will be sufficient for us by now.

Breaking down the problem

Let’s break down the problem to smaller ingredients.

What are the building blocks of the problem?

I would create the following blocks:

  • We have an input of an array of objects, and only need the rating property. We need to take it out from each objects.
  • We need to sum up each value.
  • Because we can't be sure if all five ratings appear in the input, we will need to default that rating to zero.
  • Finally we have to put out the answer as a form of strings, i.e. how many outstanding, good etc. ratings are, so we should not display number ratings.

How can the building blocks be represented best in JS?

We need to extract the relevant rating property from each object, so we can use the map method on the input array.

Then we have to add up the ratings of the same type. That is, collect how many 1s, 2s etc. are in the data. We can use the reduce method here. It’s also possible to handle the default values inside reduce, but we want to avoid repeating the rating keys here. We can create a function that returns the default values of 0 for each.

The last block is bit tricky. We first need to have a reference to the corresponding string value of each rating. We need to know somehow that outstanding is number 5, good is number 4 etc. We can use a Map object here, where the key-value pairs are the string and number versions of the rating. This way we can easily create references to the corresponding string once we have the number value.

Considerations

As always, we need to keep our code DRY, so avoid repetition as much as possible but the code should still be readable.

Also, it’s always a good idea to cover the edge cases.

Solving the problem

Let’s create our Map first. This will contain each rating and their corresponding string value:

const RATINGS = new Map([
  [5, 'Outstanding'],
  [4, 'Good'],
  [3, 'Average'],
  [2, 'Room for improvement'],
  [1, 'Not recommended'],
  [NaN, 'No rating']
]);

The last key-value pair will serve some error handling.

Next we can create the function that returns the default values:

const defaultRatings = () => {
  const obj = {}; // step 1
  for (const [, str] of RATINGS) {
    obj[str] = 0;
  } // step 2
  return obj; // step 3
};

First, we take an empty object, and then fill it up with properties taken from RATINGS, and finally we return the object.

Let’s elaborate on step 2 as it might need some explanation. This is the step when we create the default object which has the string keys.

We loop over RATINGS using the for...of syntax. This is how Maps can be iterated and when we do so, we refer to the actual key-value pair with const [key, value].

However, we only need the string from each pair, which is the second value (that is the value from the key-value pair), so we ignore the key part (the numbers). We show this by simply entering a comma at the beginning of the array, hence the const [, str] notation (str refers to each corresponding strings, i.e. outstanding, good etc.).

All good so far, we can now move on to the actual logic that gets the ratings from the restaurants array and adds up the ratings for each category.

Let’s create a function for the main logic and pass it the restaurants array:

const collectRatings = restaurants => {

};

Although it’s likely that we will have data for each restaurant (hence the request for the app), we need to handle the case when it’s not for any reason otherwise our code (and maybe the app itself) can break. We can simply return a gentle warning that something is not 100% right inside collectRatings:

if (!restaurants) return 'Error in evaluating data.';
if (restaurants.length === 0) return 'No data are available for analysis.';

But we very probably can’t get away with that. Real data will be coming, so we need to handle them somehow.

Let’s scroll up and have a look at our breakdown of the problem. We need to get the ratings from the restaurants data and then need to collect the relevant information. We agreed that map and reduce methods can come handy here, so we can write something like this:

const collectRatings = (restaurants) => {
  if (!restaurants) return 'Error in evaluating data.';
  if (restaurants.length === 0) return 'No data are available for analysis.';

  return restaurants
    .map((restaurant) => {
      const ratingNumber = Number(restaurant.rating);
      return RATINGS.get(ratingNumber);
    }).reduce((ratingCollection, rating) => {
      ratingCollection[rating]++;
      return ratingCollection;
    }, defaultRatings());
};

Let’s have a look at the map method first. We apply it on the restaurants array, and do some logic on each restaurant. First, we check if the rating property of each restaurant object can be converted to a number using the Number object. In case we get something stupid like banana for a rating or no rating at all for a particular restaurant (undefined), the input cannot be converted to a number, and Number returns NaN, so the assigned string rating will be No rating. We then get the corresponding string rating from RATINGS using the get method available on Map.

After the iteration completes, an array of rating strings will be available for as a result of the map method (it returns an array). So it’s time to count how many of each rating we have in the array and sum them up in an object. So we use the reduce array method here.

The default object will be the returned value of the defaultRatings function, i.e. each rating string will be a key of the object with values of 0. From here, it’s easy: increase the value of the rating by one once it comes up.

Eventually we will get our collection of ratings when the collectRatings function is invoked with the restaurants array:

console.log(collectRatings(restaurants));
// returns
// {
//   Outstanding: 2,
//   Good: 3,
//   Average: 1,
//   'Room for improvement': 1,
//   'Not recommended': 0,
//   'No rating': 0
// }

We are done!

Conclusion

Maps are really useful in some cases when the use of objects might be cumbersome, like in the example above. Although it’s possible to solve this task using objects, Map provided here a nice and clean solution.

It’s worth thinking the problem further: What if we accidentally receive ratings above 5? How can it be handled? Can we return something else as default? Can you think of anything so that the solution can be improved?

Until next, enjoy coding!