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;
});
Alexander Nenashev
  • 8,775
  • 2
  • 6
  • 17
  • 1
    what have you tried so far? – XMehdi01 Jun 29 '23 at 15:04
  • 1
    Array.reduce is a good start. Let’s see your array.reduce code. – James Jun 29 '23 at 15:06
  • Added a typescript version – Alexander Nenashev Jun 29 '23 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 – Alexander Nenashev Jun 29 '23 at 17:00
  • you should add `typescript` tag to typescript related questions – Alexander Nenashev Jun 29 '23 at 17:13
  • @AlexanderNenashev (1) The OP is not _specifically_ asking for TypeScript: types are not directly involved, so this is actually a valid JavaScript question as well. (2) Solutions were/are not required to use `Array::reduce()`; the OP simply stated that they had _tried_ that approach. Please do not read extra meaning into statements such as that. – Darryl Noakes Jun 29 '23 at 17:47
  • (3) I don't think "effort invested" is applicable. If it's a duplicate, it's a duplicate. If it were a definite duplicate, it wouldn't matter _how_ many people wrote answers if it's been well-answered before. As an aside, in certain cases, depending on the relative states of the questions, the _older_ one could be marked as a duplicate of the _newer_ one. – Darryl Noakes Jun 29 '23 at 17:47

3 Answers3

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);
XMehdi01
  • 5,538
  • 2
  • 10
  • 34
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>
Alexander Nenashev
  • 8,775
  • 2
  • 6
  • 17
  • Good one, but even though I prefer *readable code* over *short code* – XMehdi01 Jun 29 '23 at 15:22
  • I have to admit @XMehdi01 is correct on readability. However I feel like this could be more readable. A solution would be to split up the code as 1) a one liner 2) the one liner split up on multiple lines with clear comments on each line. – Wimanicesir Jun 29 '23 at 15:27
  • Off topic: I feel like you improved a lot this last month, but aside from your good solutions, always try to explain as much as possible! You can always directly submit your solution and edit a more detailed answer later :) – Wimanicesir Jun 29 '23 at 15:28
  • 1
    @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 – Alexander Nenashev Jun 29 '23 at 15:38
  • 1
    @Wimanicesir added a description. looks ok? – Alexander Nenashev Jun 29 '23 at 15:40
  • 1
    @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 – Alexander Nenashev Jun 29 '23 at 15:48
  • 1
    @AlexanderNenashev I agree with you! – XMehdi01 Jun 29 '23 at 15:50
1

To achieve the desired output, you can use the reduce function along with an object to store the counts of each name. Here's an example of how you can implement it:

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

const countMatches = nameList.reduce((acc, curr) => {
  if (acc[curr]) {
    acc[curr]++;
  } else {
    acc[curr] = 1;
  }
  return acc;
}, {});

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

console.log(result);

Output:


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

In the code above, the reduce function is used to iterate over the nameList array. The accumulator (acc) is an object that keeps track of the count for each name. If the current name (curr) already exists as a property in the accumulator, the count is incremented by 1. Otherwise, a new property is created with the name as the key and an initial count of 1.

After the reduce operation, we use Object.entries to convert the object back into an array of key-value pairs. Then, we use map to transform each key-value pair into an object with name and matches properties. Finally, the result is stored in the result variable.

Note: In your attempted code, you were comparing p and c using the equality operator (==) which would only compare the values, not the count of occurrences. Additionally, you didn't handle the case where the name is not equal to the previous one.

  • 1
    The implementation is pretty much the same as @XMehdi01's, but I give you an upvote for the explanation. – Darryl Noakes Jun 29 '23 at 16:19
  • @DarrylNoakes Then you should be thanking ChatGPT instead, not the user who didn't write it. – tchrist Jun 29 '23 at 19:40
  • I'm not sure. It could be; the final note doesn't _seem_ like ChatGPT (especially what I would consider a missing comma after "the equality operator (`==`)".), but could have been added by the user themselves onto the end of whatever ChatGPT generated. However, the opening doesn't restate the problem as is typical. I would also like to note that the two explanation paragraphs are pretty much how I would write it myself, so we can't go by those two much, at least in isolation. My overall opinion is that it isn't, or at least is only partially. – Darryl Noakes Jun 29 '23 at 20:30