Count the appereances of all values per category of a set of N objects

108 Views Asked by At

Given an array of objects with attributes I would like to count the total appearances per attribute type.

I have provided an example of 3 arrays which represent 3 different entities (in production it can vary up to 20.000 entities):

const arr = 
  [ [ { attribute_type: 'Background', value: 'Orange'  } 
    , { attribute_type: 'Fur',        value: 'Black'   } 
    , { attribute_type: 'Outfit',     value: 'Casual'  } 
    , { attribute_type: 'Earring',    value: 'None'    } 
    , { attribute_type: 'Eyes',       value: 'Fiery'   } 
    , { attribute_type: 'Mouth',      value: 'Smiling' } 
    , { attribute_type: 'Shoes',      value: 'Sandals' } 
    ] 
  , [ { attribute_type: 'Background', value: 'Orange'  } 
    , { attribute_type: 'Fur',        value: 'Brown'   } 
    , { attribute_type: 'Outfit',     value: 'Casual'  } 
    , { attribute_type: 'Earring',    value: 'None'    } 
    , { attribute_type: 'Eyes',       value: 'Gold'    } 
    , { attribute_type: 'Mouth',      value: 'Smiling' } 
    ] 
  , [ { attribute_type: 'Background', value: 'Diamond' } 
    , { attribute_type: 'Fur',        value: 'Gold'    } 
    , { attribute_type: 'Outfit',     value: 'Dress'   } 
    , { attribute_type: 'Earring',    value: 'None'    } 
    , { attribute_type: 'Eyes',       value: 'Gold'    } 
    , { attribute_type: 'Mouth',      value: 'Smiling' } 
    ] 
  ]

The attribute types can vary and is unknown beforehand. And not every attribute type is guaranteed to be present in an array.

I would like to end up with a list with the # of attribute appereances per category (and sorted ascending by appereance rate if possible):

const expected = 
  [ { Background: { Diamond: 1, Orange: 2          }} 
  , { Fur:        { Black:   1, Brown:  1, Gold: 1 }} 
  , { Outfit:     { Dress:   1, Casual: 2          }} 
  , { Earring:    { None:    3                     }} 
  , { Eyes:       { Fiery:   1, Gold:   2          }} 
  , { Mouth:      { Smiling: 3                     }} 
  , { Shoes:      { Sandals: 1                     }} 
  ] 

I've spent many hours on how to solve this issue and I've tried to take a look at Map data structures and merge but no success so far. The final result does not have to meet the provided format but I'm just trying to apply best practices.

5

There are 5 best solutions below

3
On BEST ANSWER

the idea is to start from an already sorted list so that the output one is also sorted. I also sorted on the value attribute

const arr = 
  [ [ { attribute_type: 'Background', value: 'Orange'  } 
    , { attribute_type: 'Fur',        value: 'Black'   } 
    , { attribute_type: 'Outfit',     value: 'Casual'  } 
    , { attribute_type: 'Earring',    value: 'None'    } 
    , { attribute_type: 'Eyes',       value: 'Fiery'   } 
    , { attribute_type: 'Mouth',      value: 'Smiling' } 
    , { attribute_type: 'Shoes',      value: 'Sandals' } 
    ] 
  , [ { attribute_type: 'Background', value: 'Orange'  } 
    , { attribute_type: 'Fur',        value: 'Brown'   } 
    , { attribute_type: 'Outfit',     value: 'Casual'  } 
    , { attribute_type: 'Earring',    value: 'None'    } 
    , { attribute_type: 'Eyes',       value: 'Gold'    } 
    , { attribute_type: 'Mouth',      value: 'Smiling' } 
    ] 
  , [ { attribute_type: 'Background', value: 'Diamond' } 
    , { attribute_type: 'Fur',        value: 'Gold'    } 
    , { attribute_type: 'Outfit',     value: 'Dress'   } 
    , { attribute_type: 'Earring',    value: 'None'    } 
    , { attribute_type: 'Eyes',       value: 'Gold'    } 
    , { attribute_type: 'Mouth',      value: 'Smiling' } 
    ] 
  ] 

const result = Object.entries(arr
  .flat()           // set simple array with all sub array elements
  .sort((a,b)=>     // sort on attribute_type + value
    {
    let r = a.attribute_type.localeCompare(b.attribute_type)
    if (r === 0) r = a.value.localeCompare(b.value) 
    return r
    })
  .reduce((r,c)=>
    {
    r[c.attribute_type] = r[c.attribute_type] ?? {}
    r[c.attribute_type][c.value] = r[c.attribute_type][c.value] ?? 0
    r[c.attribute_type][c.value]++
    return r
    },{}))
  .map(([k,v])=>({[k]:v})) // change obj to array

console.log( result )
.as-console-wrapper {max-height: 100%!important;top:0 }

As requested by the PO in a comment here is the same list, sorted in alpha order on the attribute-type then in ascending order on the number of entries of each value

sorting the results for each attribute_type in ascending order is a bit trickier, I also put them in alphabetical order in case of a tie

const arr = 
  [ [ { attribute_type: 'Background', value: 'Orange'  } 
    , { attribute_type: 'Fur',        value: 'Black'   } 
    , { attribute_type: 'Outfit',     value: 'Casual'  } 
    , { attribute_type: 'Earring',    value: 'None'    } 
    , { attribute_type: 'Eyes',       value: 'Fiery'   } 
    , { attribute_type: 'Mouth',      value: 'Smiling' } 
    , { attribute_type: 'Shoes',      value: 'Sandals' } 
    ] 
  , [ { attribute_type: 'Background', value: 'Orange'  } 
    , { attribute_type: 'Fur',        value: 'Brown'   } 
    , { attribute_type: 'Outfit',     value: 'Casual'  } 
    , { attribute_type: 'Earring',    value: 'None'    } 
    , { attribute_type: 'Eyes',       value: 'Gold'    } 
    , { attribute_type: 'Mouth',      value: 'Smiling' } 
    ] 
  , [ { attribute_type: 'Background', value: 'Diamond' } 
    , { attribute_type: 'Fur',        value: 'Gold'    } 
    , { attribute_type: 'Outfit',     value: 'Dress'   } 
    , { attribute_type: 'Earring',    value: 'None'    } 
    , { attribute_type: 'Eyes',       value: 'Gold'    } 
    , { attribute_type: 'Mouth',      value: 'Smiling' } 
    ] 
  ] 
let result =
  arr
  .flat()
  .sort((a,b)=>a.attribute_type.localeCompare(b.attribute_type) )
  .reduce((r,{attribute_type, value},i,{[i+1]:nxt})=>
    {
    if (r.att != attribute_type)
      {
      r.att = attribute_type
      r.res.push( {[r.att]: []})
      r.idx++
      }
    let val = r.res[r.idx][r.att].find(x=>x[0]===value)
    if (!val) r.res[r.idx][r.att].push([value,1] )
    else      val[1]++
    if ( r.att != nxt?.attribute_type )
      {
      r.res[r.idx][r.att] =
        r.res[r.idx][r.att]
        .sort((a,b)=>
          {
          let z = a[1]-b[1]
          if (z===0) z = a[0].localeCompare(b[0])
          return z
          })
        .reduce((o,[ref,count])=>
          {
          o[ref] = count
          return o  
          },{})
      }
    return nxt ? r : r.res
    },{ att:'', idx:-1,res:[] }) 
    
console.log( result )
.as-console-wrapper {max-height: 100%!important;top:0 }

some links:
array.flat(), array.sort(), array.reduce(), str1.localeCompare(str2), Nullish coalescing operator (??)

(yes you can find information in the mdn online documentation)

0
On

let a = [
    [
        { attribute_type: 'Background', value: 'Orange' },
        { attribute_type: 'Fur', value: 'Black' },
        { attribute_type: 'Outfit', value: 'Casual' },
        { attribute_type: 'Earring', value: 'None' },
        { attribute_type: 'Eyes', value: 'Fiery' },
        { attribute_type: 'Mouth', value: 'Smiling' },
        { attribute_type: 'Shoes', value: 'Sandals' }
    ],
    [
        { attribute_type: 'Background', value: 'Orange' },
        { attribute_type: 'Fur', value: 'Brown' },
        { attribute_type: 'Outfit', value: 'Casual' },
        { attribute_type: 'Earring', value: 'None' },
        { attribute_type: 'Eyes', value: 'Gold' },
        { attribute_type: 'Mouth', value: 'Smiling' }
    ],
    [
        { attribute_type: 'Background', value: 'Diamond' },
        { attribute_type: 'Fur', value: 'Gold' },
        { attribute_type: 'Outfit', value: 'Dress' },
        { attribute_type: 'Earring', value: 'None' },
        { attribute_type: 'Eyes', value: 'Gold' },
        { attribute_type: 'Mouth', value: 'Smiling​' }
    ]
];
function sort(a) {
    let array = new Object({});
    for (let i = ~true; i < [a??a[a]][0].length-2; i++) {//
        for (var j = ~~(Math.PI-a.length); j < [a??+a[a]++][0][i+2].length; j++) {
            this["​"] = array[a[i+Math.sqrt(4)][j].attribute_type];
            // if attribute exist
            try {
                if (Object.entries(this["​"]).map(([a,_]) => a).reduce((a,p) => a+7*p) || !navigator.bluetooth && new Date().getTime()%34+47*Math.pow(3.47)/a[i].length) {
                this["​"][a[i+2][j].value] = (this["​"][a[i+2][j].value]==(() => {debugger})() ? 1: ++this["​"][a[i-~true][j].value]);
                
            } else { // if attribute doesn't exist
                array[a[i-(~false<<1)][j].attribute_type] = {};array[a[i+2][j].attribute_type][a[i+2][j].value] = 1;
            }
            } catch {
            array[a[i-(~false<<1)][j].attribute_type] = {};
                array[a[i+2][j].attribute_type][a[i+2][j].value] = 1;
            }
            
        }
    }
    return transform(array);
}

function transform(object) {
    let newArray = [];
    for (let attribute of Object.entries(object)) {
        let newObject = { [attribute[0]]: attribute[1] }

        newArray.push(newObject);
    }
    return newArray;
}

console.error(sort(a));

0
On

One simple function to do it, just put the object into it and it will return another object with the result.

const arr = 
  [ [ { attribute_type: 'Background', value: 'Orange'  } 
    , { attribute_type: 'Fur',        value: 'Black'   } 
    , { attribute_type: 'Outfit',     value: 'Casual'  } 
    , { attribute_type: 'Earring',    value: 'None'    } 
    , { attribute_type: 'Eyes',       value: 'Fiery'   } 
    , { attribute_type: 'Mouth',      value: 'Smiling' } 
    , { attribute_type: 'Shoes',      value: 'Sandals' } 
    ] 
  , [ { attribute_type: 'Background', value: 'Orange'  } 
    , { attribute_type: 'Fur',        value: 'Brown'   } 
    , { attribute_type: 'Outfit',     value: 'Casual'  } 
    , { attribute_type: 'Earring',    value: 'None'    } 
    , { attribute_type: 'Eyes',       value: 'Gold'    } 
    , { attribute_type: 'Mouth',      value: 'Smiling' } 
    ] 
  , [ { attribute_type: 'Background', value: 'Diamond' } 
    , { attribute_type: 'Fur',        value: 'Gold'    } 
    , { attribute_type: 'Outfit',     value: 'Dress'   } 
    , { attribute_type: 'Earring',    value: 'None'    } 
    , { attribute_type: 'Eyes',       value: 'Gold'    } 
    , { attribute_type: 'Mouth',      value: 'Smiling' } 
    ] 
  ]
  
  function count_() {
    let result = [], attr = {};
    arr.forEach(function a(ar){
      ar.forEach(function t(e) {
        if (!attr.hasOwnProperty(e.attribute_type)) attr[e.attribute_type] = {};
        if (!attr[e.attribute_type].hasOwnProperty(e.value))  attr[e.attribute_type][e.value] = 0;
        attr[e.attribute_type][e.value] ++;
      });
    });
    Object.keys(attr).forEach(function c(e, index) {
      result[index] = {};
      result[index][e] = attr[e];
    });
    return result;
  }
  
  console.log(count_());

0
On

An imperative style solution to the problem:

const arr = 
  [ [ { attribute_type: 'Background', value: 'Orange'  } 
    , { attribute_type: 'Fur',        value: 'Black'   } 
    , { attribute_type: 'Outfit',     value: 'Casual'  } 
    , { attribute_type: 'Earring',    value: 'None'    } 
    , { attribute_type: 'Eyes',       value: 'Fiery'   } 
    , { attribute_type: 'Mouth',      value: 'Smiling' } 
    , { attribute_type: 'Shoes',      value: 'Sandals' } 
    ] 
  , [ { attribute_type: 'Background', value: 'Orange'  } 
    , { attribute_type: 'Fur',        value: 'Brown'   } 
    , { attribute_type: 'Outfit',     value: 'Casual'  } 
    , { attribute_type: 'Earring',    value: 'None'    } 
    , { attribute_type: 'Eyes',       value: 'Gold'    } 
    , { attribute_type: 'Mouth',      value: 'Smiling' } 
    ] 
  , [ { attribute_type: 'Background', value: 'Diamond' } 
    , { attribute_type: 'Fur',        value: 'Gold'    } 
    , { attribute_type: 'Outfit',     value: 'Dress'   } 
    , { attribute_type: 'Earring',    value: 'None'    } 
    , { attribute_type: 'Eyes',       value: 'Gold'    } 
    , { attribute_type: 'Mouth',      value: 'Smiling' } 
    ] 
  ]

function sort(arr) {
    const sub = {};

    // iterate over the array
    for (let i = 0; i < arr.length; ++i) {
        for (let j = 0; j < arr[i].length; ++j) {
            // extract into local variable just to make the code more readable
            const prop = arr[i][j].attribute_type;
            const val = arr[i][j].value;

            // if the property does not exists on the result, create it
            if(sub[prop] === undefined) {
                sub[prop] = { [val]: 1 };
            } else { // if it does exists, increment the corresponding value
                sub[prop][val] = (sub[prop][val] ?? 0) + 1;
            }
        }
    }

    return sub;
}

console.log(sort(arr));

1
On

Just loop through your array and have an object to collect the result you want.

const data = [ // your data goes here ];
const result = {};

// loop through first array
data.forEach((el) => {
  // loop through nested array
  el.forEach((el2) => {
    // check if the element is added already
    if (result[el2.attribute_type] === undefined) {
      // first item added
      result[el2.attribute_type] = {
        [el2.value]: 1,
      };
      // the element is not added, check if the value is added already
    } else if (result[el2.attribute_type][el2.value] === undefined) {
      // value doesn't exists yet, set the value
      result[el2.attribute_type][el2.value] = 1;
    } else {
      // value found, add 1
      result[el2.attribute_type][el2.value] += 1;
    }
  });
});

console.log(result);