1

What do I mean with this? First let's look at some code I wrote:

  let names = ['James', 'james', 'bob', 'JaMeS', 'Bob'];
  let uNames = {};

  names.forEach(n => {
    let lower = n.toLowerCase();
    if (!uNames[lower]) {
      uNames[lower] = n;
    }
  });

  names = Object.values(uNames);
  console.log(names); // >>> (2) ["James", "bob"]

The goal here is to unique the given array case insensitive but keep one of the original inputs.

I was wondering if there is a more elegant/better performing solution to this problem than the one I came up with.

Just converting the whole array to lowercase before making it unique is not a solution, because I'd like the end result to consist only of values which were already in the input array. Which one (e.g. James or james or JaMeS) is not relevant.

Tobias Würth
  • 2,136
  • 3
  • 13
  • 48
  • 2
    Take a look at [`Array.prototype.map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) combined with [this answer using `Set`](https://stackoverflow.com/a/33121880/6361314). – Jeffrey Westerkamp Feb 11 '18 at 12:23

2 Answers2

6

I was wondering if there is a more elegant/better performing solution to this problem than the one I came up with.

Use a Map:

let names = ['James', 'james', 'bob', 'JaMeS', 'Bob'];
let uNames = new Map(names.map(s => [s.toLowerCase(), s]));

console.log([...uNames.values()]); 

The constructor of Map can take an array of pairs (nested arrays with 2 values: key and value). The Map will maintain a unique list of keys so while it is constructed previously stored values will get overwritten if the key is the same.

Once you have the Map, you can iterate over the values with .values().

With plain object

You could also use the Object.fromEntries method, which at the time of writing, is a stage 4 proposal (Draft ES2020) implemented in Chrome, Firefox, Opera and Safari:

let names = ['James', 'james', 'bob', 'JaMeS', 'Bob'];
let uNames = Object.fromEntries(names.map(s => [s.toLowerCase(), s]));

console.log(Object.values(uNames)); 

As you can see, the approach is quite similar.

First occurrence & Original order

The above will collect the last occurrence, in order of first occurrence.

In case you want to collect the first occurrence, you can just reverse the input first, and then continue as above. Then the output will have collected the first occurrence, in order of last occurrence.

In case you really need the first occurrences in order of first occurrence, you can use reduce as follows:

let names = ['James', 'james', 'bob', 'JaMeS', 'Bob'];
let uNames = names.map(s => s.toLowerCase()).reduce((map, s, i) => 
    map.get(s) ? map : map.set(s, names[i])
, new Map);

console.log([...uNames.values()]); 
trincot
  • 211,288
  • 25
  • 175
  • 211
  • what's the difference in using a `Map` over a object? – Ayush Gupta Feb 11 '18 at 12:29
  • 1
    The map, as I understand it, is to ensure that the output array contains actual input strings, instead of their lowercase variants. – Jeffrey Westerkamp Feb 11 '18 at 12:35
  • You can do the same with object properties but Maps have this useful constructor and are easy to iterate. In my view more elegant. But that is just an opinion. – trincot Feb 11 '18 at 12:44
  • Now `Object.fromEntries` also gives the possibility to construct an object from pairs. – trincot Mar 18 '19 at 19:23
  • Using above approach it's printing last occurence, If i want to print first obj in array then how is it possible – Soumya Gangamwar Aug 08 '19 at 07:15
  • @SoumyaGangamwar, you can just reverse the array before creating the Map: `names.reverse().map( .....` Or if you want to keep `names` unchanged, then: `[...names].reverse().map(......`. – trincot Aug 08 '19 at 07:25
  • No, I want exactly 1 st occurence,, If I put reverse() it's giving output ["bob", "James"], But I am expecting ['James','bob']; – Soumya Gangamwar Aug 08 '19 at 07:36
  • Yes, well then reverse the result also; `[...uNames.values()].reverse()`. – trincot Aug 08 '19 at 07:40
  • But here’s the caveat. [The `Object.values` doesn’t preserve the element order (the insertion order) if the original array has numeric strings.](https://stackoverflow.com/a/5525820/4510033) **Go with `Map` if you need to keep the order unchanged.** – Константин Ван Dec 23 '19 at 04:15
  • @trincot You can’t simply do `names.reverse()` and `[...uNames.values()].reverse()` to get the first occurrences _AND_ keep the order right. For `names = ["bOB", "james", "bob", "JaMeS", "Bob"]` you get `["james", "bOB"]`, not `["bOB", "james"]`, if you use _the `reverse()` and `reverse()` back_ method. – Константин Ван Dec 23 '19 at 04:25
  • @КонстантинВан, agreed. Added a version to my answer to get first occurrences in original order. – trincot Dec 23 '19 at 10:05
0

If you want to deduplicate a string array, with priority given to the first occurrences, keeping the insertion order, use this.

const a = ["ALPHA", "10", "BETA", "10", "alpha", "beta", "aLphA"];
const b = ["3", "1", "2", "2", "3", "1"];

function dedupe(string_array) {
 const entries = string_array
  .slice()
  .reverse()
  .map((string, index) => [
   string.toLowerCase(),
   {
    string,
    index: string_array.length - 1 - index
   }
  ]);
 const case_insensitively_deduped_string_array = Array
  .from((new Map(entries)).values())
  .sort((a, b) => (a.index - b.index))
  .map(item => item.string);
 return case_insensitively_deduped_string_array;
 // Takes the first occurrences, keeping the insertion order.
 // Doesn’t modify the input array.
}

console.log(a, dedupe(a));
console.log(b, dedupe(b));