3

I need to iterate over an array of strings, count the occurrences of each string, and return a object with the value and the number of occurrences.

I'm trying to use the array reduce function to achieve such thing, but, despite the attempts, I haven't been able to achieve that yet.

So I have the following:

["tony", "tony", "tony", "tony", "kassandra", "tony", "tony", "kassandra"]

I need the output:

[{ name: "tony", matches: 6 }, { name: "kassandra", matches: 2 }]

What should I use to obtain the above output?

I have tried the following:

const names = nameList.map((user) => user.name);

const reduce = names.reduce((p, c) => {
  if (p == c) {
    return { name: p, matches: `idk what put here yet` };
  }
  return c;
});
7
  • 1
    what have you tried so far? Commented Jun 29, 2023 at 15:04
  • 1
    Array.reduce is a good start. Let’s see your array.reduce code. Commented Jun 29, 2023 at 15:06
  • Added a typescript version Commented Jun 29, 2023 at 16:46
  • @tevemadar here a Typescript solution is asked, so it's different, not a duplicate. Plus specific solution with Array::reduce() was asked. Plus a different desired output format. Plus a lot of effort was invested by responders. So flagging it as a duplicate is a shallow reaction Commented Jun 29, 2023 at 17:00
  • you should add typescript tag to typescript related questions Commented Jun 29, 2023 at 17:13

2 Answers 2

5

Firstly, count the occurrences of each name in the array.
Secondly, convert it into an object with the desired format of name and their matches.

const names = ["tony", "tony", "tony", "tony", "kassandra", "tony", "tony", "kassandra"];
const counts = {};
//1.
names.forEach(name => {
  if (counts[name]) {
    counts[name] += 1;
  } else {
    counts[name] = 1;
  }
});
//2.
const result = Object.keys(counts).map(name => {
  return {
    name: name,
    matches: counts[name]
  };
});
console.log(result);

If you want to use .reduce() approach to counting occurrences of names:

const counts = names.reduce((acc, name) => {
    acc[name] = (acc[name] || 0) + 1;
    return acc;
  }, {});

Another approach to convert occurrences back into an object using Object.entries()

Object.entries(counts).map(([name, matches]) => ({ name, matches }));

Two approaches combined:

const names = ["tony", "tony", "tony", "tony", "kassandra", "tony", "tony", "kassandra"];
const counts = names.reduce((acc, name) => {
    acc[name] = (acc[name] || 0) + 1;
    return acc;
  }, {});
const result = Object.entries(counts).map(([name, matches]) => ({ name, matches }));
console.log(result);

Sign up to request clarification or add additional context in comments.

7 Comments

Downvoted because a) No reduce was used as asked. b) You could do this in one loop.
I've updated the answer with .reduce(), I used forEach just for simplicity?
Already changed my downvote :) Just wanted to note that I much aprove the simplicity but not without the best approach. Now it has both! Perfect! ;)
That could be done with 1 line, no need in 2 steps here, check my solution
Instead of the fancy acc[name] = (acc[name] || 0) + 1;, you could do it in two lines to make it more readable: if (!acc[name]) acc[name] = 0; acc[name] += 1;.
|
1

A pure Array::reduce() Typescript solution.

We provide an aggregate object (r in the callback) where we store 2 values: the result array arr with the desired output and a map map where we can easily find an existing array items to increment found matches:

A live typescript gist is here.

const names = ["tony", "tony", "tony", "tony", "kassandra", "tony", "tony", "kassandra"];

interface MatchItem {
  name: string, 
  matches: number
};

const result = names.reduce((r, name) => {
  const {arr, map} = r;
  const item = map.get(name);
  item ? item.matches++ : map.set(name, arr[arr.length] = { name, matches: 1 });
  return r;
}, { arr: [], map: new Map } as {arr: MatchItem[], map: Map<string, MatchItem>}).arr;

console.log(result);

JS version:

const names = ["tony", "tony", "tony", "tony", "kassandra", "tony", "tony", "kassandra"];

const result = names.reduce((r, name) => {
  const {arr, map} = r;
  const item = map.get(name);
  item ? item.matches++ : map.set(name, arr[arr.length] = { name, matches: 1 });
  return r;
}, { arr: [], map: new Map }).arr;

console.log(result);

And a benchmark:

enter image description here

<script benchmark data-count="1">

// randomly fill with names
const src = 'tony kassandra alex peter mike arnold tina maria stephen nicolas samuel sonya natali elena julia'.split(' ');

let i = 16000000;
const names = [];
while (i--) {
    names.push(src[Math.floor(Math.random() * src.length)]);
}

// @benchmark XMehdi01's solution
const counts = names.reduce((acc, name) => {
    acc[name] = (acc[name] || 0) + 1;
    return acc;
}, {});
Object.entries(counts).map(([name, matches]) => ({ name, matches }));

//@benchmark Alexander's solution
names.reduce((r, name) => {
    const {arr, map} = r;
    const item = map.get(name);
    item ? item.matches++ : map.set(name, arr[arr.length] = { name, matches: 1 });
    return r;
}, { arr: [], map: new Map }).arr;

</script>
<script src="https://cdn.jsdelivr.net/gh/silentmantra/benchmark/loader.js"></script>

7 Comments

Good one, but even though I prefer readable code over short code
@Wimanicesir thank you for the feedback. btw i wrongly read the desired output in the question so provided a wrong result. the desired result required a more intricated logic, but i was able to manage it with 1 Array::reduce() call
@Wimanicesir added a description. looks ok?
@XMehdi01 thanks for input, actually i produced a wrong output. otherwise the code looked ok imho. fixed and benchmarked our solution. besides readability we should take into account performance also imho
@AlexanderNenashev I agree with you!
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.