XTRUST.ORG
XTRUST.ORG

Reputation: 3392

Advanced javascript array grouping by count

I have an array of arrays in javascript. And want to perform some kind of calculations.

My array looks like on the picture below:

Array

Where: - First value is a code. - Second value is a size. - Third value of each array is a count.

What I'm trying to do, is to find all groups, where all counts are 0.

For example:

What I have tried:

var a    = [];
$.each(arr[21].splits, function(idx, val) {
    console.log(val);   
    var current_code = arr[21].splits[idx][0];

    a[current_code] = [];
    a[current_code].push(arr[21].splits[idx]);
});  

But can't find the right solution.. Thanks!

The data set: {"code":"sjm","splits":[["FOG","L","0"],["FOG","XL","-1"],["FOG","XXXL","2"],["FOG","S","7"],["FOG","M","0"],["FOG","XXL","6"],["BLK","LT","30"],["BLK","XLT","23"],["BLK","XXXLT","0"],["BLK","L","102"],["BLK","XL","302"],["BLK","XXXL","64"],["BLK","S","25"],["BLK","XXLT","0"],["BLK","M","485"],["BLK","XXL","159"],["BGE","L","106"],["BGE","XL","41"],["BGE","XXXL","15"],["BGE","S","4"],["BGE","M","39"],["BGE","XXL","0"],["RED","L","36"],["RED","XL","41"],["RED","XXXL","8"],["RED","S","5"],["RED","M","19"],["RED","XXL","2"],["NVY","L","0"],["NVY","XL","0"],["NVY","XXXL","0"],["NVY","S","28"],["NVY","M","0"],["NVY","XXL","0"]]}

P.S On the screen above - no color codes, where all the values in group are 0. So, nothing to output.

Upvotes: 2

Views: 194

Answers (3)

Icepickle
Icepickle

Reputation: 12796

I am going to guess (since you talk about grouping), that you would first reduce and then filter

You could reduce to get your arrays grouped by the first index, you could go at it like

function groupBy( arr, t ) {
  return arr.reduce( (agg, c) => {
    const key = t(c);
    if (!agg[key]) {
      agg[key] = [];
    }
    agg[key].push( c );
    return agg;
  }, {} );
}

which you could then use as

const result = groupBy( arr[21], item => item[0] );

given you back an object like

{ "NVY": [["NVY", "S", "28"], ...] }

And then you can find the ones that have a total of 0 "count"

function find( obj, predicate ) {
  return Object.keys( obj ).filter( key => predicate( obj[key] ) );
}

which you could then use like

const groups = find( result, items => !items.some( i => i[2] !== "0" ) );

Giving you a result of (in case NVY didn't have any items that where not "0")

["NVY", "..."]

Which you could then combine with your code property

So, if you combine all that, you would get something like

const set = {"code":"sjm","splits":[["FOG","L","0"],["FOG","XL","-1"],["FOG","XXXL","2"],["FOG","S","7"],["FOG","M","0"],["FOG","XXL","6"],["BLK","LT","30"],["BLK","XLT","23"],["BLK","XXXLT","0"],["BLK","L","102"],["BLK","XL","302"],["BLK","XXXL","64"],["BLK","S","25"],["BLK","XXLT","0"],["BLK","M","485"],["BLK","XXL","159"],["BGE","L","106"],["BGE","XL","41"],["BGE","XXXL","15"],["BGE","S","4"],["BGE","M","39"],["BGE","XXL","0"],["RED","L","36"],["RED","XL","41"],["RED","XXXL","8"],["RED","S","5"],["RED","M","19"],["RED","XXL","2"],["NVY","L","0"],["NVY","XL","0"],["NVY","XXXL","0"],["NVY","S","28"],["NVY","M","0"],["NVY","XXL","0"]]};

function groupBy( arr, t ) {
  return arr.reduce( (agg, c) => {
    const key = t(c);
    if (!agg[key]) {
      agg[key] = [];
    }
    agg[key].push( c );
    return agg;
  }, {} );
}

function find( obj, predicate ) {
  return Object.keys( obj ).filter( key => predicate( obj[key] ) );
}

const result = groupBy( set.splits, item => item[0] );

const groups = find( result, items => !items.some( i => i[2] !== "0" ) );

console.log( groups );

// and combined

console.log( groups.map( g => `${set.code}-${g}` ) );

// for verification
console.log( result );

Upvotes: 2

zfrisch
zfrisch

Reputation: 8660

Answer:

You can create an Inventory Constructor that allows for you to parse your data however you'd like and in as many ways as you'd like.

code:

function Inventory( from ) {
    return {
        data: from,
        getBy: function( type ) {
            if ( ![ "code", "size", "quantity" ].includes( type.toLowerCase() ) ) throw new Error( "Incorrect Inventory::getBy Type" );
            return function( value ) {
                return Inventory( 
                    from.filter( ( [ code, size, quantity ] ) => 
                        new Function( "code, size, quantity", `return ${type} === '${value.toUpperCase()}'` )( code, size, quantity ) 
                    ) 
                );
            }
        }
    }
}

Example:

let data={splits:[["FOG","L","0"],["FOG","M","0"],["FOG","XL","-1"],["FOG","XXXL","2"],["NVY","M","0"],["NVY","L","0"],["NVY","S","0"]]};


function Inventory(from) {
	return {
		data: from,
		getBy: function(type) {
			if (!["code", "size", "quantity"].includes(type.toLowerCase())) throw new Error("Incorrect Inventory::getBy Type");
			return function(value) {
				return Inventory(from.filter(([code, size, quantity]) => new Function("code, size, quantity", `return ${type} === '${value.toUpperCase()}'`)(code, size, quantity)));
			}
		}
	}
}

let Inv = Inventory(data.splits),
noQuantity = Inv.getBy("quantity")("0").data;

console.log("Items with 0 Quantity: ");
console.log(noQuantity);

Why's this answer so verbose?

On the surface this probably seems like overkill, and in fact it may be. I know there are other fine answers, but I really felt they were lacking in optimal use.

Why?

  • They don't allow nested searches
  • They don't provide maintainability and reusability

This means that though they may answer your question, they're mostly not adaptable, which is important to all code as needs mutate, evolve, and edge cases need to be dealt with.

My answer provides all of the above, with very little code, and the explanation below ( if you choose to read it ) should give you a thorough explanation of how it works.


Explanation: How does this work?

We'll start from the beginning and create a simple Constructor.

Let's call it Inventory because that seems to be what your data references.

function Inventory(from) {
   return { 

  }
}

The first thing we need to do is to store the data that we receive in the function. This can be done like so:

function Inventory(from) {
 return {
    data: from
  }
}

This means that when we call the constructor with our Inventory data:

let Inv = Inventory(data.splits);

Within Inv we now have:

{ 
  data: [ Inventory Data ]
}

Example:

let data={splits:[["FOG","L","0"],["FOG","M","0"],["FOG","XL","-1"],["FOG","XXXL","2"],["NVY","M","0"],["NVY","L","0"],["NVY","S","0"]]};


function Inventory(from) {
     return {
        data: from
      }
    }
    
let Inv = Inventory(data.splits);
console.log(Inv);


To manipulate this data in a way where we can dig into it and get our requested results, we can actually use a filter method on our array.

As an example we can adjust our constructor to look for specific quantities like this:

function Inventory(from) {
    return {
        data: from,
        getByQuantity: function(quantity) {
            return from.filter(([, , q]) => quantity === q);
        }
    }
}

Example:

let data={splits:[["FOG","L","0"],["FOG","M","0"],["FOG","XL","-1"],["FOG","XXXL","2"],["NVY","M","0"],["NVY","L","0"],["NVY","S","0"]]};

function Inventory(from) {
  return {
    data: from,
    getByQuantity: function(quantity) {
      return from.filter(([, , q]) => quantity === q);
    }
  }
}

let Inv = Inventory(data.splits),
noQty = Inv.getByQuantity("0");

console.log( noQty );


This is all well and good.

But what if we need to get all of the data with the code NVY? Or what if we need to get all of the data with the quantity 1 within the code FOG ?

Our current pattern requires a lot of boilerplate code to generate anything more than our specified quantity result!

How do we mitigate our code and bolster functionality?

Templating Pattern:

To be more useful we can utilize a Templating Pattern combined with our filter to provide us with the ability to get any result from our data and then continue to drill down until we've gotten exactly what we need.

We can do this by adjusting our constructor. I'll walk through the steps to make it easier to understand.


Step 1:

  • First we change our method to getBy to make it variable in use.
  • We provide a type parameter, that is checked to make sure it's valid

code:

    function Inventory(from) {
    return {
        data: from,
        getBy: function(type) {
            if (!["code", "size", "quantity"].includes(type.toLowerCase())) throw new Error("Incorrect Inventory::getBy Type");
        }
    }
}

Step 2:

  • We can then return a new function that takes a value
    • This value is what we'll check against our type while filtering

code:

function Inventory(from) {
    return {
        data: from,
        getBy: function(type) {
            if (!["code", "size", "quantity"].includes(type.toLowerCase())) throw new Error("Incorrect Inventory::getBy Type");

            return function(value) {

        }
    }
}

Step 3:

  • We can then create a Function that will Template our type and value into a conditional, and return a boolean (true/false) of whether or not it matches.
  • By passing this function into our filter we can search for anything

code:

function Inventory(from) {
  return {
    data: from,
    getBy: function(type) {
      if (!["code", "size", "quantity"].includes(type.toLowerCase())) throw new Error("Incorrect Inventory::getBy Type");
      return function(value) {
        return from.filter(([code, size, quantity]) =>
          new Function("code, size, quantity", `return ${type} === '${value.toUpperCase()}'`)(code, size, quantity)
        );

      }
    }
  }
}

Re-usability aside this is enough to provide a one-time search of your data based on any parameter, be that code, size, or quantity

Example:

    let data={splits:[["FOG","L","0"],["FOG","M","0"],["FOG","XL","-1"],["FOG","XXXL","2"],["NVY","M","0"],["NVY","L","0"],["NVY","S","0"]]};

function Inventory(from) {
  return {
    data: from,
    getBy: function(type) {
      if (!["code", "size", "quantity"].includes(type.toLowerCase())) throw new Error("Incorrect Inventory::getBy Type");
      return function(value) {
        return from.filter(([code, size, quantity]) =>
          new Function("code, size, quantity", `return ${type} === '${value.toUpperCase()}'`)(code, size, quantity)
        );

      }
    }
  }
}
  
  let Inv = Inventory(data.splits);

  console.log(" FOG Code: ");
  console.log( Inv.getBy("code")("FOG") );
  
  console.log(" Size Medium: ");
  console.log( Inv.getBy("size")("M") );


In practice as far as your question goes, you do get your result. BUT you'll notice that it's difficult to get all Medium sizes within NVY - though it is possible. It would look something like:

        let allMNVY = Inventory(
                    Inventory(data.splits)
                    .getBy("code")("NVY")
                  )
                  .getBy("size")("M");

This is not ideal.

How can we fix this?

This last step allows us to do multiple searches within our data to continuously drill down until we get the data we want.

This is done through Recursion.


Step 4:

To retain the ability to continue searching, we want to return an Inventory Constructor with the returned data as the parameter.

code:

function Inventory(from) {
    return {
        data: from,
        getBy: function(type) {
            if (!["code", "size", "quantity"].includes(type.toLowerCase())) throw new Error("Incorrect Inventory::getBy Type");

            return function(value) {
                return Inventory(from.filter(([code, size, quantity]) => new Function("code, size, quantity", `return ${type} === '${value.toUpperCase()}'`)(code, size, quantity)));
            }

        }
    }
}

This constructor then allows us to continue to dig through our data, in very few lines of code. The caveat is that when we finish our search, we must look at the data property.


Our previous example of all Medium sizes within code NVY:

    let allMNVY = Inventory(
                    Inventory(data.splits)
                    .getBy("code")("NVY")
                  )
                  .getBy("size")("M");

Our new example of all Medium sizes within code NVY:

    let allMNVY = Inventory(data.splits)
                  .getBy("code")("NVY")
                  .getBy("size")("M")
                  .data;

As you can see it's much more sensible because we know exactly what occurs in this example.

  • We turn data.splits into our inventory
  • We get all the items with code NVY
  • We get all the items with size M
  • We get the data instead of making another search.

Final Solution:

function Inventory(from) {
    return {
        data: from,
        getBy: function(type) {
            if (!["code", "size", "quantity"].includes(type.toLowerCase())) throw new Error("Incorrect Inventory::getBy Type");
            return function(value) {
                return Inventory(from.filter(([code, size, quantity]) => new Function("code, size, quantity", `return ${type} === '${value.toUpperCase()}'`)(code, size, quantity)));
            }
        }
    }
}

let noQty = Inventory(data.splits).getBy("quantity")("0");

Final Example:

let data={splits:[["FOG","L","0"],["FOG","M","0"],["FOG","XL","-1"],["FOG","XXXL","2"],["NVY","M","0"],["NVY","L","0"],["NVY","S","0"]]};


function Inventory(from) {
	return {
		data: from,
		getBy: function(type) {
			if (!["code", "size", "quantity"].includes(type.toLowerCase())) throw new Error("Incorrect Inventory::getBy Type");
			return function(value) {
				return Inventory(from.filter(([code, size, quantity]) => new Function("code, size, quantity", `return ${type} === '${value.toUpperCase()}'`)(code, size, quantity)));
			}
		}
	}
}

let Inv = Inventory(data.splits),
noQuantity = Inv.getBy("quantity")("0").data,
allFog = Inv.getBy("code")("FOG").data,
allNvyNoQuantity = Inv.getBy("code")("NVY").getBy("quantity")("0").data;

console.log("noQuantity : " + noQuantity);
console.log("allFog : " + allFog);
console.log("allNvyNoQuantity : " + allNvyNoQuantity);




Edit after thinking about this a while, I decided to add demo code for a comparison instead of exact matching

Note Because of the number of examples within this answer, StackOverflow doesn't seem to be rendering the results of the console.log in the following examples. You may need to open your own console to verify, but they do work!


Extending Functionality: Providing a filterBy Comparison

The last thing you may want to consider instead of explicit matching, is a comparison operation to filter fluid matches.

With our above code if we want to grab the following:

  • quantity is "0"
  • quantity is "1"

We would need to look twice at the data. Once for "0" and again for "1".

To adjust this we can provide the ability to make a comparison. This way we can simply say:

  • quantity is greater than 0

We do this by adjusting our constructor very slightly and adding a new method called filterBy:

    filterBy: function( comparison ) {
        return Inventory( from.filter( ( [ code, size, quantity ] ) => new Function( "code, size, quantity", `return ${comparison};`)( code, size, quantity ) ) );
    }

It is almost exactly the same as our prior Templating Function, except this method will take a string and use it to compare against our data set.

Example:

let data={splits:[["FOG","L","0"],["FOG","M","0"],["FOG","XL","-1"],["FOG","XXXL","2"],["NVY","M","0"],["NVY","L","0"],["NVY","S","0"]]};


function Inventory( from ) {
	return {
		data: from,
		getBy: function( type ) {
			if ( ![ "code", "size", "quantity" ].includes( type.toLowerCase() ) ) throw new Error( "Incorrect Inventory::getBy Type" );
			return function( value ) {
				return Inventory( from.filter( ( [ code, size, quantity ] ) => new Function( "code, size, quantity", `return ${type} === '${value.toUpperCase()}'` )( code, size, quantity ) ) );
			}
		},
		filterBy: function( comparison ) {
			return Inventory( from.filter( ( [ code, size, quantity ] ) => new Function( "code, size, quantity", `return ${comparison};`)( code, size, quantity ) ) );
		}
	}
}

let Inv = Inventory(data.splits),
negativeQuantity = Inv.filterBy("Number(quantity) < 0").data;
positiveQuantity = Inv.filterBy("Number(quantity) > 0").data;

console.log("Negative Quantity:");
console.log(negativeQuantity);

console.log("Positive Quantity:");
console.log(positiveQuantity);

Additionally it also has access to any and all of the types ( code, quantity, and size )

Note In our comparison string all of our types (code, quantity, and size) are strings, and all lowercase. That is why we use Number(quantity) and not just quantity in the above example.

A huge bonus of our Templating Pattern is that we can write any JavaScript comparison and it will work. Going back to our code NVY and size M example, we can now write:

let MNVY = Inv.filterBy("code == 'NVY' && size == 'M'").data;

Example:

let data={splits:[["FOG","L","0"],["FOG","M","0"],["FOG","XL","-1"],["FOG","XXXL","2"],["NVY","M","0"],["NVY","L","0"],["NVY","S","0"]]};


function Inventory( from ) {
	return {
		data: from,
		getBy: function( type ) {
			if ( ![ "code", "size", "quantity" ].includes( type.toLowerCase() ) ) throw new Error( "Incorrect Inventory::getBy Type" );
			return function( value ) {
				return Inventory( from.filter( ( [ code, size, quantity ] ) => new Function( "code, size, quantity", `return ${type} === '${value.toUpperCase()}'` )( code, size, quantity ) ) );
			}
		},
		filterBy: function( comparison ) {
			return Inventory( from.filter( ( [ code, size, quantity ] ) => new Function( "code, size, quantity", `return ${comparison};`)( code, size, quantity ) ) );
		}
	}
}

let Inv = Inventory(data.splits),
MNVY = Inv.filterBy("code == 'NVY' && size == 'M'").data;

console.log("NVY and M : " + MNVY);


Conclusion:

Though a bit lengthy, I hope this helps!

Happy coding!

Upvotes: 2

Shahzad
Shahzad

Reputation: 2063

Here is a reusable function which can take different counts to search for a given array with the structure that you have given:

function findByCount(array, code, countToFind) {
    var result = [];

    $.each(array, function (index, subArray) {
        var count = subArray[2];
        var groupCode = code + "-" + subArray[0];
        if (count == countToFind && result.indexOf(groupCode) === -1) {
            result.push(groupCode);
        }
    });

    return result;
}

console.log(findByCount(data.splits, data.code, 0));

Upvotes: 1

Related Questions