array.groupBy in TypeScript - arrays

The basic array class has .map, .forEach, .filter, and .reduce, but .groupBy i noticably absent, preventing me from doing something like
const MyComponent = (props:any) => {
return (
<div>
{
props.tags
.groupBy((t)=>t.category_name)
.map((group)=>{
[...]
})
}
</div>
)
}
I ended up implementing something myself:
class Group<T> {
key:string;
members:T[] = [];
constructor(key:string) {
this.key = key;
}
}
function groupBy<T>(list:T[], func:(x:T)=>string): Group<T>[] {
let res:Group<T>[] = [];
let group:Group<T> = null;
list.forEach((o)=>{
let groupName = func(o);
if (group === null) {
group = new Group<T>(groupName);
}
if (groupName != group.key) {
res.push(group);
group = new Group<T>(groupName);
}
group.members.push(o)
});
if (group != null) {
res.push(group);
}
return res
}
So now I can do
const MyComponent = (props:any) => {
return (
<div>
{
groupBy(props.tags, (t)=>t.category_name)
.map((group)=>{
return (
<ul key={group.key}>
<li>{group.key}</li>
<ul>
{
group.members.map((tag)=>{
return <li key={tag.id}>{tag.name}</li>
})
}
</ul>
</ul>
)
})
}
</div>
)
}
Works pretty well, but it is too bad that I need to wrap the list rather than just being able to chain method calls.
Is there a better solution?

You can use the following code to group stuff using Typescript.
const groupBy = <T, K extends keyof any>(list: T[], getKey: (item: T) => K) =>
list.reduce((previous, currentItem) => {
const group = getKey(currentItem);
if (!previous[group]) previous[group] = [];
previous[group].push(currentItem);
return previous;
}, {} as Record<K, T[]>);
// A little bit simplified version
const groupBy = <T, K extends keyof any>(arr: T[], key: (i: T) => K) =>
arr.reduce((groups, item) => {
(groups[key(item)] ||= []).push(item);
return groups;
}, {} as Record<K, T[]>);
So, if you have the following structure and array:
type Person = {
name: string;
age: number;
};
const people: Person[] = [
{
name: "Kevin R",
age: 25,
},
{
name: "Susan S",
age: 18,
},
{
name: "Julia J",
age: 18,
},
{
name: "Sarah C",
age: 25,
},
];
You can invoke it like:
const results = groupBy(people, i => i.name);
Which in this case, will give you an object with string keys, and Person[] values.
There are a few key concepts here:
1- You can use function to get the key, this way you can use TS infer capabilities to avoid having to type the generic every time you use the function.
2- By using the K extends keyof any type constraint, you're telling TS that the key being used needs to be something that can be a key string | number | symbol, that way you can use the getKey function to convert Date objects into strings for example.
3- Finally, you will be getting an object with keys of the type of the key, and values of the of the array type.

you could add the function to the array prototype in your app (note some don't recomend this: Why is extending native objects a bad practice?):
Array.prototype.groupBy = function(/* params here */) {
let array = this;
let result;
/* do more stuff here*/
return result;
};
Then create an interface in typescript like this:
.d.ts version:
interface Array<T>
{
groupBy<T>(func:(x:T) => string): Group<T>[]
}
OR in a normal ts file:
declare global {
interface Array<T>
{
groupBy<T>(func:(x:T) => string): Group<T>[]
}
}
Then you can use:
props.tags.groupBy((t)=>t.category_name)
.map((group)=>{
[...]
})

A good option might be lodash.
npm install --save lodash
npm install --save-dev #types/lodash
Just import it import * as _ from 'lodash' and use.
Example
_.groupBy(..)
_.map(..)
_.filter(..)

Instead of groupby use reduce. Suppose product is your array
let group = product.reduce((r, a) => {
console.log("a", a);
console.log('r', r);
r[a.organization] = [...r[a.organization] || [], a];
return r;
}, {});
console.log("group", group);

During the TC39 meeting of December 2021, the proposal introducing the new Array.prototype.groupBy and Array.prototype.groupByToMap function has reached stage 3 in the specification process.
https://github.com/tc39/proposal-array-grouping
https://github.com/tc39/proposals/commit/b537605f01df50fd4901be5ce4aa0d02fe6e7193
Here's how both functions are supposed to look like according to the README linked above:
const array = [1, 2, 3, 4, 5];
// groupBy groups items by arbitrary key.
// In this case, we're grouping by even/odd keys
array.groupBy((num, index, array) => {
return num % 2 === 0 ? 'even': 'odd';
});
// => { odd: [1, 3, 5], even: [2, 4] }
// groupByToMap returns items in a Map, and is useful for grouping using
// an object key.
const odd = { odd: true };
const even = { even: true };
array.groupByToMap((num, index, array) => {
return num % 2 === 0 ? even: odd;
});
// => Map { {odd: true}: [1, 3, 5], {even: true}: [2, 4] }
While not a 100% guaranty that it will really end up in a future version of JavaScript in the form described above (there's always a chance that the proposal can be adjusted or dropped, notably for compatibility reasons), it's nevertheless a strong commitment to have this groupBy feature offered in the standard lib soon.
By ripple effect, it also means that these functions will be also available in TypeScript.

Related

React Nested Loop Query

I'm new to React and struggling with something that would be very simple with XML/XPath.
I have two array objects. For simplicity sake, I've removed most properties to demonstrate and just set everything as strings...
customerList: Customer[]
export class Customer {
id: string = "";
firstname: string = "";
}
and then a second array object:
orderList: Order[]
export class Order {
id: string = "";
customerid: string = "";
}
Assume that the order.customerid is nullable.
What I want to do is loop through each customer, and check if there are any customers without an order.
What I've tried:
{customers.map((customer) => {
return(
orders.map((order) => {
if(order.customerid == customer.id)
{
order.customerid == customer.id ? <p>customer has order</p> : <p>customer does not have order</p>
}
})
)
)}
I figured I'd set some sort of boolean flag to indicate whether there are any customers without orders, but just wanted to get something functional.
When you use map functions, a new array is created from what's returned in each loop.
i.e.:
const arr = [1, 2, 3, 4, 5];
const newArr = arr.map((i) => i*i);
console.log(newArr)
//Output: [1, 4, 9, 16, 25]
In your case, since you have not defined what to return, it will just return undefined and hence you will get a nested array of undefined values.
In order to implement the functionality you want, I'd do something like this
const newArr = customerList.map((customer) => {
return {
...customer,
customerOrdered: orders.find(order => order.customerid === customer.id)
}
})
It will create a new array of customers indicating whether the customer has placed an order or not
You not mentioned in the question but i assume that you want to render that the customer to screen that this customer don't have any order.
So you have to RETURN it. this is common mistake when people start using React.
But look like you using typescript and use it wrong way. You don't have to create a class. Create a type or interface instead.
There are many way to archive what you want to do, this just one of them that you won't need to create new array or change state format.
interface Customer {
id: string,
name: string,
}
interface Order {
id: string,
customerId: string
}
// Pretend that you pass customer and orders to this component
function Component({customers, orders}) {
return (
{customers.map((customer) => {
let haveOrder = false;
orders.forEach((order) => {
if(order.customerid == customer.id){
haveOrder = true;
}
});
if (haveOrder){
return(
<p>customer has order</p>
)
}else {
return(
<p>customer does not have order</p>
)
}
)}
)

Updating state inside nested array when mapping the parent

I'm trying to update state inside a nested array with a map function and spread operator, but i don't understand how to get the key in the key/value pair to select a nested object.. There is an arrow in the code to the problematic part.
export default class ControlPanel extends Component {
state = {
words:
[
{
word: "a",
id: 1,
column: 1,
synonymns: {
all:[],
selected:[],
noneFound: false
}
}
]
}
}
updateSynonymnState = (wordId, theSynonymns) => {
const { words } = this.state
const newWords = words.map(word => {
if(word.id == wordId){
return {...word, synonymns.all: theSynonymns} //<--- synonymns.all is throwing an error, but that is the key that i need.
} else {
return word
}
})
this.setState ({words: newWords})
}
I could map the synonymns from the start but then I would loose the Id of the word which i need to select the right word..
How can I set synonymns.all = theSynonymns inside that words.map function, or is there a different way i should be able to set a nested key/value pair when mapping the parent parameter?
Immutable logic with nested values can get very tricky to get right. There are plenty of libraries that focus on that as well. For example: Immer, immutability-helper, immutable-js, and many more.
If you don't want to use another library for your state transitions, then you have to do a bit more work. You need to spread out each of the pieces of state from main object to the part you are modifying.
if (word.id == wordId) {
return { ...word, synonyms: { ...word.synonyms, all: theSynonymns } }; //<--- synonymns.all is throwing an error, but that is the key that i need.
} else {
return word;
}
You have to spread synonymns inside of word as well. Here is an example syntax of doing this:
const words = [{
word: "a",
id: 1,
column: 1,
synonymns: {
all: [],
selected: [],
noneFound: false
}
}]
const theSynonymns = ['b'];
const newWords = words.map(word => {
return {
...word,
synonymns: {
...word.synonymns,
all: theSynonymns
}
}
})
console.log(newWords)

typescript how to find inside an array that is already in an array?

I want to find a value inside an array that is already inside an array.
To give an example of my array:
[
{
ConcessionId: 1,
ConcessionName: "Coyotes",
KnownAs: [
{
TeamId: 1,
Name: "Arizona Coyotes",
},
{
TeamId: 2,
Name: "Phoenix Coyotes",
}
]
},
{
ConcessionId: 2,
ConcessionName: "Devils",
KnownAs: [
{
TeamId: 3,
Name: "Colorado Rockies",
},
{
TeamId: 4,
Name: "New-Jersey Devils",
}
]
}
]
What I want is when Icall my function it returns me the team name.
For example, I the parameter value is 3, I want Colorado Rockies as a name:
public getInfo(_TeamID) {
const concession: ConcessionInfo[] = this.concessionList$.filter(function (x) {
x.KnownAs.filter( (y)=> {
y.TeamId= +_TeamID;
return y.Name;
})
})
}
I try so many different way with filter. But never get something good. Never works.
I can make a double .foreach , for each array. but I think a better method exist than making a double loop.
Thanks
Instead of using the filter method (which is in fact working similar as a for loop), you could do forEach on both arrays. For your current data structure, there is no other way around it.
getInfo = (_TeamID) => {
let teamName = '';
this.concessionList$.forEach(entry => {
entry.KnownAs.forEach(team => {
if(team.TeamId === _TeamID){
teamName = team.Name;
return; // break the loop.
}
})
});
return teamName;
}
Here is a working example
https://stackblitz.com/edit/double-for-lopp
EDIT
If you have a look at the polyfill implementation of filter from Mozilla https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter which is in equivalent to the native implementation of filter, you can see that it is looping through the whole array, the same way as a forEach loop. The difference is that the filter method will return a new array based on the boolean condition inside the callback function, while a forEach loop does not return anything.
Assuming myArray is contains the data you provided.
The following code will work if you're using Typescript 3.7 and above.
public getInfo(teamId: number): string | undefined {
const team = this.concessionList$
.map(concession => concession.KnownAs)
.reduce((a, b) => a.concat(b), [])
.find(team => team.TeamId === teamId)
return team ? team.Name : undefined
}
Usage:
this.getInfo(3) // Colorado Rockies
Ok how this work?
You have to understand what is find. For example:
const result = [{name: 'foo', age: 1}, {name: 'bar', age: 2}]
.find(people => people.name === 'foo')
console.log(result) // {name: 'foo', age: 1}

nested object, array combination

So I have a dilemma.
I have the next code
const loc = [
{ location_key: [32, 22, 11], autoassign: 1 },
{ location_key: [41, 42], autoassign: 1 }
];
const bulkConfigs = [
{
dataValues: {
config_key: 100,
}
},
{
dataValues: {
config_key: 200,
}
}
];
I need to create an object looking like this:
config_key: here get the config key from from bulkConfigs,
location_key: here get the location_key,
autoassign: 1
Also I need this object created
config_key: config_key,
location_key: '',
autoassign: 1,
as many times as they are locations for each config_key, what I mean is in this example from config_key: 200 we will have 2 objects like this one and for config_key: 100 we will have 3 objects like this. I suppose this can be done with reduce ... also bulkConfigs and loc can have more then just 2 objects, but the number will be always the same, like if they are 3 bulkConfigs there will be also 3 loc, but location_key might be different, one can have 7 location_key, other 4, and the last one just 1.
So in other words, the arrys are always the same length and they are always in the same order so they have the same index. Only the location_key can change, and I need the object created as many times as location_key exist.
I have tried a few things, but I don't know when it comes to this stuff .... I just can't do, that's what happens when you start with react and not java script :)
Ok so I managed to do this using lodash, here is my solution, I know it's nested like hell and probably this could be done way easier, but for a newbie is good enough. Feel free to come with more elegant solutions.
If you have a similar problem, here is the solution.
A code sandbox so you can play with:
https://codesandbox.io/s/epic-field-bdwyi?file=/src/index.js
import _ from "lodash";
const locs = [{ location_key: [32, 22, 11] }, { location_key: [41, 42] }];
const bulkConfigs = [
{
dataValues: {
config_key: 100
}
},
{
dataValues: {
config_key: 200
}
}
];
// map over the array of bulckConfigs and get indexes
const mergedArrays = _.map(bulkConfigs, (bulkConfig, i) => {
// create the object that we need
const objectNeed = {
// flatMap over the locs array to get flat values from objects in it
location_key: _.flatMap(locs, ({ location_key }, index) => {
// match the indexs of both arrays
if (index === i) {
// return the location_key values for each config
return location_key;
} else {
// compact to remove the undefinded values returned
return _.compact();
}
}),
config_key: bulkConfig.dataValues.config_key,
autoassign: 1
};
return objectNeed;
});
// now we just need to crate the same object as many locations and use flatMap to flatten the objects
const allObjects = _.flatMap(mergedArrays, mergedArray => {
const yy = _.map(mergedArray.location_key, location => {
const zz = {
location_key: location,
config_key: mergedArray.config_key,
autoassign: 1
};
return zz;
});
return yy;
});
console.log(allObjects);
And the more elegant version of it :)
const getConfigs = (locEl, index) => {
return _.map(locEl.location_key, (locationKey) => {
return {
location_key: locationKey,
config_key: bulkConfigs[index].dataValues.config_key,
autoassign: 1,
};
});
};
const configLocations = _.chain(locs)
.map(getConfigs)
.flatten()
.value();
console.log(configLocations);

What is the Best way to loop over an array in scala

I'm new to scala and I'm trying to refactor the below code.I want to eliminate "index" used in the below code and loop over the array to fetch data.
subgroupMetricIndividual.instances.foreach { instanceIndividual =>
val MetricContextListBuffer: ListBuffer[Context] = ListBuffer()
var index = 0
contextListBufferForSubGroup.foreach { contextIndividual =>
MetricContextListBuffer += Context(
entity = contextIndividual,
value = instanceIndividual(index).toString
)
index += 1
}
}
For instance, if the values of variables are as below:
contextListBufferForSubGroup = ("context1","context2")
subgroupMetricIndividual.instances = {{"Inst1","Inst2",1},{"Inst3","Inst4",2}}
Then Context should be something like:
{
entity: "context1",
value: "Inst1"
},
{
entity: "context2",
value: "Inst2"
},
{
entity: "context1",
value: "Inst3"
},
{
entity: "context2",
value: "Inst4"
}
Note:
instanceIndividual can have more elements than those in contextListBufferForSubGroup. We must ignore the last extra elements in instanceIndividual in this case
You can zip two lists into a list of tuples and then map over that.
e.g.
subgroupMetricIndividual.instances.foreach { instanceIndividual =>
val MetricContextListBuffer = contextListBufferForSubGroup.zip(instanceIndividual).map {
case (contextIndividual, instanceIndividualIndex) => Context(
entity = contextIndividual,
value = instanceIndividualIndex.toString
)
}
}
If Context can be called like a function i.e. Context(contextIndividual, instanceIndividualIndex.toString) then you can write this even shorter.
subgroupMetricIndividual.instances.foreach { instanceIndividual =>
val MetricContextListBuffer = contextListBufferForSubGroup
.zip(instanceIndividual.map(_.toString)).map(Context.tupled)
}
Without knowing your exact datatypes, I'm mocked up something which is probably close to what you want, and is slightly more functional using maps, and immutable collections
case class Context(entity:String, value:String)
val contextListBufferForSubGroup = List("context1","context2")
val subgroupMetricIndividualInstances = List(List("Inst1","Inst2",1),List("Inst3","Inst4",2))
val result: List[Context] = subgroupMetricIndividualInstances.map { instanceIndividual =>
contextListBufferForSubGroup.zip(instanceIndividual) map { case v: (String, String) =>
Context(
entity = v._1,
value = v._2
)
}
}.flatten

Resources