10

My input is an array like so:

[7, 7, 7, 7, 4, 4, 5, 5, 5, 1, 9, 2, 7, 7]

I want to group together the numbers and add them, but by neighbors, not by total in the array. So the output would be:

['7:4', '4:2', '5:3', 1, 9, 2, '7:2']

I've attempted a few different methods using reduce, and gotten close but using the built-in Javascript methods I end up counting ALL in the array, not by neighbors.

const firstArray = [7, 7, 7, 7, 4, 4, 5, 5, 5, 1, 9, 2, 7, 7];
const masterArray = [];

const unique = new Set (numberArray); // Set {7, 4, 5, 1, 9, 2, 7}
unique.forEach(u => {
  masterArray.push(numberArray.filter(e => e === u));
});

console.log(masterArray);

Set is obviously wrong to use here because that gets the unique values and counts them, but I want to do it by neighbor only. So then I think I should be using a reduce but I run into the same problem.

stonerose036
  • 241
  • 3
  • 14

8 Answers8

3

You'll need to keep track of the last number iterated over in a persistent variable, as well as the number of occurrences of the last number which gets incremented:

const arr = [7, 7, 7, 7, 4, 4, 5, 5, 5, 1, 9, 2, 7, 7];
let lastNum = arr[0];
let count = 0;
const results = [];
const doPush = () => {
  results.push(
    count === 1
      ? lastNum
      : `${lastNum}:${count}`
  );
};
for (const num of arr) {
  if (num !== lastNum) {
    doPush();
    lastNum = num;
    count = 1;
  } else count++;
}
doPush();

console.log(results);
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • This looks like I would solve it in C++, but is this also faster in javascript? also when there is a million numbers? (displaying total lack of experience in javascript) – Surt Dec 04 '20 at 12:33
  • I don't know. On one hand, I think higher-level languages like JS are fundamentally somewhat slower than lower-level languages *if* the lower-level languages are optimized for performance. On the other hand, JS compilers nowadays are *pretty good* at optimization, so the difference might not be that large. As always, for performance, run some tests yourself with realistic inputs to find out. – CertainPerformance Dec 04 '20 at 14:16
3

var test = [7, 7, 7, 7, 4, 4, 5, 5, 5, 1, 9, 2, 7, 7];

console.log(
  test.reduce((acc, element) => {
    if (acc.length && acc[acc.length - 1].key == element) {
      acc[acc.length - 1].count++;
    } else {
      acc.push({ key: element, count: 1 });
    }
    
    return acc;
  }, []).map(element => `${element.key}:${element.count}`)
);

So the logic first reduces the number to an array, tracking the last key and count. So long as the key is the same, the count is incremented. Once the key changes, a new object is pushed to start the next run. After the reduce is done, a map is performed to convert the key and counts into the desired strings.

Taplar
  • 24,788
  • 4
  • 22
  • 35
  • Perfect. I added a check in the .map part to leave single numbers alone: element => element.count !== 1 ? `${element.key}:${element.count}` : element.key) and it works perfectly. – stonerose036 Dec 03 '20 at 18:20
3

You could reduce the array by having a look to the last value a the index before the actual value.

const 
    array = [7, 7, 7, 7, 4, 4, 5, 5, 5, 1, 9, 2, 7, 7],
    result = array.reduce((r, value, i, { [i - 1]: last }) => {
        let count = 0;
        if (last === value) count = (+r.pop().toString().split(':')[1] || 0) + 1;
        return [...r, count ? `${value}:${count}`: value];
    }, []);

console.log(result);
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
2

Here is a solution that uses a recursive function to group the neighbors, and then Array.prototype.map() to format with a colon:

const arr = [7, 7, 7, 7, 4, 4, 5, 5, 5, 1, 9, 2, 7, 7],
  output = (function groupNeighbors([first, ...rest], output = [], last) {
    last === first ? output[output.length - 1].push(first) : output.push([first]); 
    return rest.length ? groupNeighbors(rest, output, first) : output;
  })(arr).map(({ [0]: num, length }) => length > 1 ? [num, length].join(':') : num);

console.log(output);

As it uses recursion, it is limited in terms of input array length, you can find the stack size limits per browser here: Browser Javascript Stack size limit

Variant with Array.prototype.reduce() (slightly shortest, no recursion, unlimited input length):

const arr = [7, 7, 7, 7, 4, 4, 5, 5, 5, 1, 9, 2, 7, 7],
  output = arr
    .reduce(
      (acc, cur, i, { [i - 1]: last }) =>
        (cur === last ? acc[acc.length - 1].push(cur) : acc.push([cur])) && acc,
      []
    )
    .map(({ [0]: num, length }) => length > 1 ? [num, length].join(':') : num);

console.log(output);
Guerric P
  • 30,447
  • 6
  • 48
  • 86
1

Yes, the proper choice here is reduce:

const countDuplicatesQuantity = (arr) => {
    const duplicatesObj = arr.reduce((duplicates, item) => {
        duplicates[item] = duplicates[item] ? duplicates[item] + 1 : 1;

        return duplicates;
    }, {});

    return Object.keys(duplicatesObj).map(
        (key) => `${key}:${duplicatesObj[key]}`,
    );
};
Oleksandr Sakun
  • 452
  • 4
  • 9
1

You wanted reduce, how about twice? :) (I don't know if I've done something stupid here.)

First reduce finds where the values change in the array, second uses that to build new array.

const firstArray = [7, 7, 7, 7, 4, 4, 5, 5, 5, 1, 9, 2, 7, 7];

const masterArray = firstArray
  .reduce((acc, v, i, all) => all[i + 1] !== v ? acc.concat(i) : acc, [])
  .reduce(
    (acc, v, i, all) => {
      const range = v - (i === 0 ? -1 : all[i - 1]);
      return acc.concat(range > 1 ? firstArray[v] + ':' + range : firstArray[v]);
    }, []
  );
  

console.log(masterArray);
Ben Stephens
  • 3,303
  • 1
  • 4
  • 8
0

Using array.reduce:

const firstArray = [7, 7, 7, 7, 4, 4, 5, 5, 5, 1, 9, 2, 7, 7];
var newArray = [];
var count = 0;
firstArray.reduce((acc, cur, index) => {
  if (acc == cur) {
    count++;
  }
  if (acc != cur || index+1 == firstArray.length) {
    newArray.push(acc + ":" + count);
    count=1
  }
  return cur;
})
console.log(newArray);
Lemondoge
  • 959
  • 4
  • 17
0

my way ... I felt that there could be "simpler"

const firstArray = [7, 7, 7, 7, 4, 4, 5, 5, 5, 1, 9, 2, 7, 7];
 
const result = firstArray.reduceRight((r,v)=>
  {
  let [n,c] = !r.length ? [-v,0] : isNaN(r[0]) ? r[0].split(':') : [r[0],1]
  if (n==v) r[0] = `${n}:${++c}`
  else      r.unshift(v)
  return r
  },[])
  
console.log( JSON.stringify(result) )
//  [ "7:4", "4:2", "5:3", 1, 9, 2, "7:2" ]
.as-console-wrapper { max-height: 100% !important; top: 0; }
Mister Jojo
  • 20,093
  • 6
  • 21
  • 40