proph3t
proph3t

Reputation: 965

How can I Map Object Array To Array With Identifier For Duplicate Values?

Working with an object array like:

const data = [
  {count: 400, value: "Car Wash Drops"},
  {count: 48, value: "Personal/Seeding"},
  {count: 48, value: "Personal/Seeding"},
];

I am wanting to map to an array with an additional identifier for duplicate values:

const expected = [
  ["Car Wash Drops", 400],
  ["Personal/Seeding (1)", 48],
  ["Personal/Seeding (2)", 48],
];

So far, I have a map function to map the values accordingly, but am unsure of how to proceed with appending the identifier only for duplicates.

data.map(d => [`${d.value}`, d.count]);

results in:

[
  ["Car Wash Drops", 400],
  ["Personal/Seeding", 48],
  ["Personal/Seeding", 48],
]

I have utilized the index as well with, but it adds the index on every value:

data.map((d, i) => [`${d.value} ${i}`, d.count]);

results in:

[
  ["Car Wash Drops (0)", 400],
  ["Personal/Seeding (1)", 48],
  ["Personal/Seeding (2)", 48],
]

Upvotes: 4

Views: 1674

Answers (4)

Humoyun Ahmad
Humoyun Ahmad

Reputation: 3081

The answer was already given, with all due respect to the author of the answer, I argue there might some improvements can be made on the answer, both syntactically and performance wise:

  1. reduce should be avoided if the source is very big, though I always prefer reduce if the source is relatively small (data.length<1000). Try to use POF (plain old for :) ) as it is the fastest option.

  2. The ES6 Map is a nice helper when we deal with key-value pairs, but I prefer POO (plain old object :) ) if it is possible and it will make our code more aesthetic.

I will provide my solution to the problem (it will run O(n) and used extra O(n) space for key-value pairs in the worst case, it does two-passes on the source so, the second pass is needed to put 1 where we missed when we first meet it on the first pass):

let data =  [
  {count: 400, value: "Car Wash Drops"},
  {count: 48, value: "Personal/Seeding"},
  {count: 48, value: "Personal/Seeding"},
  {count: 300, value: "Operators/Management"},
  {count: 48, value: "Personal/Seeding"}
];

const map = {};

for (let i=0;i<data.length;i+=1) {
  map[ data[i].value ] = map[ data[i].value ]+1 || 0;
  data[i].value = map[data[i].value]?data[i].value+` (${map[data[i].value]+1})`:data[i].value; 
}

for (let i=0;i<data.length;i+=1) {
  data[i].value = map[data[i].value]?data[i].value+` (1)`:data[i].value; 
}

console.log(data.map(o=>[o.value,o.count]));

Or more concise version using ES6 of operator [link to of]:

let data =  [
      {count: 400, value: "Car Wash Drops"},
      {count: 48, value: "Personal/Seeding"},
      {count: 48, value: "Personal/Seeding"},
      {count: 300, value: "Operators/Management"},
      {count: 48, value: "Personal/Seeding"}
    ];

    const map = {};

    for (let d of data) {
      map[d.value] = map[d.value]+1 || 0;
      d.value = map[d.value]?d.value+` (${map[d.value]+1})`:d.value; 
    }

    for (let d of data) {
      d.value = map[d.value]?d.value+` (1)`:d.value; 
    }

    console.log(data.map(o=>[o.value,o.count]));

Upvotes: 1

Shidersz
Shidersz

Reputation: 17190

Using your approach, you can use filter() inside the map to check how much elements on the original array have the same value of the current analyzed one, using this condition you can choice what to return as the new value:

const data = [
  {count: 400, value: "Car Wash Drops"},
  {count: 48, value: "Personal/Seeding"},
  {count: 48, value: "Personal/Seeding"},
];

let res = data.map((x, idx) =>
{
    if (data.filter(y => y.value === x.value).length > 1)
        return [`${x.value} (${idx})`, x.count];
    else
        return [`${x.value}`, x.count];
});

console.log(res);

The performance of the previous approach could be improved if we use some() instead of filter(), like this:

const data = [
  {count: 400, value: "Car Wash Drops"},
  {count: 48, value: "Personal/Seeding"},
  {count: 48, value: "Personal/Seeding"},
  {count: 300, value: "Operators/Management"},
  {count: 48, value: "Personal/Seeding"}
];

let res = data.map((x, idx) =>
{
    if (data.some((y, j) => y.value === x.value && idx !== j))
        return [`${x.value} (${idx})`, x.count];
    else
        return [`${x.value}`, x.count];
});

console.log(res);

And could be improved even more if we previously create a Map with the counter of times an element appears in the original array. Like this:

const data = [
  {count: 400, value: "Car Wash Drops"},
  {count: 48, value: "Personal/Seeding"},
  {count: 48, value: "Personal/Seeding"},
  {count: 300, value: "Operators/Management"},
  {count: 48, value: "Personal/Seeding"}
];

let counters = data.reduce((res, {value}) =>
{
    res.set(value, res.has(value) ? res.get(value) + 1 : 1);
    return res;
}, new Map());

let res = data.map((x, idx) =>
{
    return [
        `${x.value}` + (counters.get(x.value) > 1 ? `(${idx})` : ""),
        x.count
    ]; 
});

console.log(res);

Upvotes: 3

slider
slider

Reputation: 12990

You can maintain a mapping from values you've seen to how many of them you've seen. Then it will be easier to get the right number based on whether there's a duplicate or not:

const data = [
  {count: 400, value: "Car Wash Drops"},
  {count: 48, value: "Personal/Seeding"},
  {count: 48, value: "Personal/Seeding"},
];

let valueCounts = data.reduce((a, c) => {
  a[c.value] = a[c.value] || {current: 1, total: 0};
  a[c.value].total += 1;
  return a;
}, {});

const expected = data.map(({count, value}) => {
  if (valueCounts[value].total === 1) return [value, count];
  return [`${value} (${valueCounts[value].current++})`, count];
});

console.log(expected);

Upvotes: 0

Pointy
Pointy

Reputation: 413737

You can pass a second argument to .map() after the callback. That value will be used as the value of this when the callback is invoked. Thus you can pass through an object that you can use to accumulate a count of repeats:

data.map(function(d, i) {
  if (!(d.value in this))
    this[d.value] = 0;
  else
    this[d.value] += 1;

  return [
    d.value + (this[d.value] ? " (" + this[d.value] + ")" : ""),
    d.count
  ];
}, {});

By starting with an empty object, the callback can keep track of how many times it's seen each d.value string. When it sees a repeat, it can add the qualifier to the string.

Now, this doesn't do exactly what you asked, because it only looks at one value at a time. Thus the first time it sees "Personal/Seeding" it does not know it's a duplicate, so it's not modified with the qualifier. The second time through it is of course.

Doing what you asked, if this isn't good enough, would require making a complete pass through the array first to get a final count of duplicates for each string.

Upvotes: -1

Related Questions