Count and show duplicate data in JavaScript - javascript

I would like to count and show how much data are covered in each array.
At first, I filtered and set only the label which is not duplicated. And then I counted each as array. But it wouldn't work...
dataArray = ["a", "a", "bs", "bs", "bs", "bs", "vgvg"]
const [count, setCounts] = useState<any[]>([])
const [labels, setLabels] = useState<any[]>([])
useEffect(() => {
let b = dataArray.filter((x, i, self) => {
return self.indexOf(x) === i
})
setLabels(b)
console.log(label)
// ["a", "bs", "vgvg"]
}, [dataArray])
useEffect(() => {
let c = [] as any[]
dataArray.map((data) => {
c[data] = (c[data] || 0) + 1
})
setCounts(c)
console.log(count)
// {"a": 2, "bs": 4, "vgvg": 1}
}, [dataArray])
return (
{labels.map((label, idx) => (
<div>{label}: {counts[label]}</div>
))}
)
error
Element implicitly has an 'any' type because index expression is not of type 'number'.

You could probably use something like Array.prototype.reduce for this.
You could:
const TestComponent = ({ dataArray }) => {
// Memoise the counts so it doesn't need to be calculated on each render.
const counts = useMemo(() => {
// use array reduce to calculate counts.
return dataArray.reduce(
(state, item) => {
// if property does not yet exist add it and set to 0 count.
if (state[item] === undefined) {
state[item] = 0;
}
// increment count
state[item]++;
// return updated state
return state;
},
{} //initial state
);
}, [dataArray]);
return (
{
Object
// Use Object.keys to get an array of property names
.keys(counts)
// Map property names to desired view (label / count)
.map((key) => (
<div
key={key}
>
{key}: counts[key]
</div>
))
}
)
}
The example reduce function given a dataArray of:
const dataArray = ["a", "a", "bs", "bs", "bs", "bs", "vgvg"]
Would reduce to:
const result = {
a: 2,
bs: 4,
vgvg: 1
}

Related

How to copy objects with updates in nested objects in Typescript?

I have a React component that has some form fields in it:
<TextField
label="Description"
id="description"
value={company.companyDescription}
onChange={updateFormField("companyDescription")}
></TextField>
and a function that updates my company state whenever the values change:
const updateFormField = (property: string) => (event: ChangeEvent<HTMLInputElement>) => {
setCompany(prev => ({ ...prev, [property]: event.target.value }))
}
This means that whenever a form field changes I'd like to create a new copy (hence the spread operator) of the old object.
My problem is that company has nested properties in it, like company.location.address:
{
name: "some company",
location: {
address: "some street"
}
// ...
}
Is there a way to refactor the above function to update the nested properties? For example something like:
const updateFormField = (property: string[]) => (event: ChangeEvent<HTMLInputElement>) => {
setCompany(prev => ({ ...prev, [...property]: event.target.value }))
}
I don't think there's a particularly neat solution to this, however it should be possible to loop through selecting/adding the new path:
const updateFormField = (property: string[]) => (event: ChangeEvent<HTMLInputElement>) => {
setCompany(prev => {
// Take a copy of the state
const newObj = { ...prev }
// Set up a refernce to that object
let selected = newObj
// Loop through each property string in the array
for (let i = 0; i < property.length; i++) {
// If we're at the end of the properties, set the value
if (i === property.length - 1) {
selected[property[i]] = event.target.value
} else {
// If the property doesn't exist, or is a value we can't add a new property to, set it to a new object
if (typeof selected[property[i]] !== 'object') {
selected[property[i]] = {}
}
// Update our refernce to the currently selected property and repeat
selected = selected[property[i]]
}
}
// Return the object with each nested property added
return newObj
)}
}
Plain JS working example of the same method:
const test = (prev, property, value) => {
const newObj = { ...prev
}
let selected = newObj
for (let i = 0; i < property.length; i++) {
if (i === property.length - 1) {
selected[property[i]] = value
} else {
if (typeof selected[property[i]] !== 'object') {
selected[property[i]] = {}
}
selected = selected[property[i]]
}
}
return newObj
}
console.log(test(
{"a": "1"},
["b", "c", "d"],
100
))
console.log(test(
{"a": "1"},
["a", "b"],
100
))
console.log(test(
{"a": {"b": {"c": 1}}},
["a", "b", "c"],
100
))
Object.assign() and dynamically finding the inner reference should do it. I'm assuming the input type of string[] above indicates the nested path is an array like ['company', 'location', 'address']:
const updateFormField = (property: string[]) => (event: ChangeEvent<HTMLInputElement>) => {
setCompany(prev => {
const copy = Object.assign({}, prev);
let ref = copy;
for (let i = 0; i < property.length - 1; i++) {
ref = ref[property[i]];
}
ref[property[property.length - 1]] = event.target.value
return copy;
});
}

Create an Dynamic Array Object with key value pairs

I need to create an Dynamic key value pairs from the existing object
const balanceScheule = 1255;
const reqCost = [{Labour Cost: "1555"}, {Material Cost: "1575"}]; // key is dynamic and keeps on changing
const amfqtyCost = 1416;
Here the logic is to create an new array of object and subtract the amfqtyCost from reqCost
Logic i Have written
reqCost.forEach(element => {
const adjustedAmount = Object.entries(element).map((m) => {
let adjustedAmount = parseInt(m[1]) - amfqtyCost;
return adjustedAmount;
});
// console.log(...adjustedAmount)
});
this return 139 and 159 which is (1555 - 1416 = 139) and (1575 1416 = 159) respectively
Expected output :
[{Labour Cost: "139"}, {Material Cost: "159"}]
How to do i merge ?
You just need to return the updated object from within map function. Also for the outer iteration use map instead of forEach to return the final result
const balanceScheule = 1255;
const reqCost = [{
'Labour Cost': "1555",
}, {
'Material Cost': "1575",
}]; // key is dynamic and keeps on changing
const amfqtyCost = 1416;
const updatedData = reqCost.map(element => {
return Object.assign({}, ...Object.entries(element).map(([key, value]) => {
let adjustedAmount = parseInt(value) - amfqtyCost;
return {
[key]: String(adjustedAmount)
};
}));
});
console.log(updatedData);
You can do something like this:
const reqCost = [{
'Labour Cost': "1555"
}, {
'Material Cost': "1575"
}];
const amfqtyCost = 1416;
const adjustedCost = reqCost.map(cost => ({
[Object.keys(cost)[0]]: (parseInt(Object.values(cost)[0]) - amfqtyCost).toFixed(0)
}));
console.log(adjustedCost);
// OR, if you prefer to be a bit more verbose:
const adjustedCost2 = reqCost.map(cost => {
const [key, value] = Object.entries(cost)[0];
return {
[key]: (parseInt(value) - amfqtyCost).toFixed(0)
}
});
console.log(adjustedCost2);
You can reverse the Object.entries
{ key : value } => [ [ key, value ] ]
transformation by using Object.fromEntries
[ [ key, value ] ] => { key : value }
the code will look like this
reqCost.map((obj) =>
Object.fromEntries(
Object.entries(obj).map(([key, value]) => [
key,
parseInt(value) - amfqtyCost,
])
)
);

Sort and Filter an Object that has numbers in its property names

I am trying to filter out items that has the mode set to null, however, after adding this filter, all items disappear, even if they do not have the mode prop at null.
const filteredAndSortedBotConfigs = Object.keys(botConfigs)
.sort((a, b) => {
return parseInt(a, 10) - parseInt(b, 10);
})
.filter(this.filterConfigsByBot)
.filter(this.filterConfigsByStatus)
.filter((item) => item.mode === null);
Assuming that botConfig data is object with numbers as properties:
const botConfigs = {
2: { mode: null, botId: "10", status: "ACTIVE", },
1: { mode: "A", botId: "20", status: "ACTIVE", },
3: { mode: "C", botId: "15", status: "STOPPED", },
};
And you want to sort by (number) properties and then filter on properties of values. So, this is how your filters functions might look like:
filterConfigsByBot = (key) => {
return botConfigs[key].botId !== "0"; // assuming botConfigs is available in scope
};
filterConfigsByStatus = (key) => {
return botConfigs[key].status !== "STOPPED";
};
Also, remember to map keys to values of botConfigs at the end (if needed):
const filteredAndSortedBotConfigs = Object.keys(botConfigs)
.sort((a, b) => parseInt(a, 10) - parseInt(b, 10))
.filter(this.filterConfigsByBot)
.filter(this.filterConfigsByStatus)
.filter((key) => botConfigs[key].mode !== null) // to filter out items that has the `mode` set to `null`
.map((key) => botConfigs[key]);
PS: You can combine the three filters into a single filter callback.
Edit:
Simplified version using a reduce:
const filteredAndSortedBotConfigs = Object.keys(botConfigs)
.sort((a, b) => parseInt(a, 10) - parseInt(b, 10))
.reduce((acc, curr) => {
if (
botConfigs[curr].botId !== "0" &&
botConfigs[curr].status !== "STOPPED" &&
botConfigs[curr].mode !== null
) {
acc.push(botConfigs[curr]); // pushing values, not keys
}
return acc;
}, []);

.map array by its objects keys then .map each key for values

if I have an array of objects like:
[{"name":"chair","type":"metal"},{"name":"chair","type":"wood"},{"name":"table","type":"plastic"},...]
how can I map it so it returns:
<h3>chair</h3>
metal
wood
<h3>table</h3>
plastic
ecc.
what I tried is:
return (
<>
{Object.values(myarray.reduce( (c, e) => {
if (!c[e.name]) c[e.name] = e;
return c;
}, {})).map((title, index) => (
<div key={index}>
<h3>{title}</h3>
{myarray.filter(one => one.name === title)
.map((item, i) => (
<div key={i}>{item.type}</div>
))}
</div>
))}
</>
)
but it throws
Error: Objects are not valid as a React child (found: object with keys
{name, type}). If you meant to render a collection of children, use an
array instead.
You can reduce data array to an object, then use the object to render.
// reducer
let data = [
{ name: "chair", type: "metal" },
{ name: "chair", type: "wood" },
{ name: "table", type: "plastic" }
];
let data2 = data.reduce((xuu, val) => {
xuu[val.name] = xuu[val.name] || [];
xuu[val.name].push(val.type);
return xuu;
}, {});
// render
{Object.keys(data2).map((key, index1) => {
return (
<div key={index1}>
<h3>{key}</h3>
{data2[key].map((val, index2) => {
return <div key={index2}>{val}</div>;
})}
</div>
);
})}
You can play around with my sandbox
Here's simple logic with grouping
var data= [{"name":"chair","type":"metal"},{"name":"chair","type":"wood"},{"name":"table","type":"plastic"}]
var groupBy = (xs, f) => {
return xs.reduce((r, v, i, a, k = f(v)) => ((r[k] || (r[k] = [])).push(v), r), {});
};
var groupData = groupBy(data, (d) => d.name);
$.each(groupData,function(key,val){
$('#pnlData').append("<h3>"+key+"</h3>")
$.each(val,function(skey,sval){
$('#pnlData').append("<span>"+sval.type+"</span> <br/>")
})
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="pnlData"> </div>
Reduce is the function you need:
const things = [{name: 'chair', type: 'metal'}, ...]
things.reduce((a, _) => {
a[_.name] = a[_.name] || [] // make array for this thing
a[_.name].indexOf(_.type) < 0 && a[_.name].push(_.type) // add type if not already present
return a
}, {})
You can use reduce function to grouping all entries by name. Then you can use Object.keys or Object.values to get entry name or types.
array.reduce((reducer, current) => {
if (!reducer[current.name]) {
reducer[current.name] = [];
}
reducer[current.name].push(current.type);
return reducer;
}, {})
// result : { chair: ["wood", "metal"], table: ["plastic"] }
If possible to change the data structure/array objects. I think it would be easier if it looks something like this. Your new Array:
const myArray = [
{
"name":"chair",
"types":["metal", "wood"]
},
{
"name":"table",
"types":["plastic"]
}
]
//in your ui code would be
myArray.map(item => {
return <div>
<h3>item.name</h3>
{item.types.map(type => <div>{type}</div>)}
</div>
})
Here is a sample code
const a = [{"name":"chair","type":"metal"},{"name":"chair","type":"wood"},{"name":"table","type":"plastic"}];
const b = {};
a.map(row => {
b[row.name] = b[row.name] || []
b[row.name].push(row.type)
})
console.log(b) // you can check results here...
// result: {"chair":["metal","wood"],"table":["plastic"]}

Javascript: variable is not undefined but while returning, being returned undefined

In the snippet below, before return(res) , I log it, and it's not undefined
but somehow, it's being returned as undefined.
What am I doing wrong?
filterData = (inputData, searchedKey) => {
inputData.forEach((data, index) => {
if(data){
if(data.hasOwnProperty(searchedKey)){
const res = data[searchedKey]
console.log(res) /// logs the results
return(res) /// returns undefined
}
var dataToProcess = [];
var fieldKeys = Object.keys(data)
fieldKeys = fieldKeys.filter((field, index) => !field.includes("#"))
fieldKeys.forEach((key, index) => {
dataToProcess.push(data[key]);
})
this.filterData(dataToProcess, searchedKey)
}
})
}
console.log(this.filterData([{"#name": "foo", "#type": "bar"}], "#type"))
Some issues:
forEach does not return anything else than undefined, so returning a value in its callback function does not do anything useful.
Your function does not return a value
The return value of the recursive call is not used.
if (data) is not good enough to make sure data is an object. For instance, it would also be true for a non-zero number. Use Object(data) === data instead.
Since there could be multiple matches (in a nested input object), your function should return an array, which is also what someone would expect when seeing your function name. Also the standard array filter method returns an array. So it would be in line with that.
Here is how you could make it work:
var filterData = (inputData, searchedKey) => {
inputData = inputData.filter( data => Object(data) === data );
return !inputData.length ? [] :
inputData.filter( data => data.hasOwnProperty(searchedKey) )
.map( data => data[searchedKey] )
// Add the results from recursion:
.concat(filterData([].concat(...
inputData.map( data =>
Object.keys(data)
.filter( key => !key.startsWith("#") )
.map( key => data[key] )
)
), searchedKey));
};
var data = [{
"#name": "foo",
"#title": "mr",
"deeper": [{
"#nope": "bad",
"deepest": [{
"nothing_here": null,
"#type": "deeper bar",
}]
}, {
"#type": "bar",
}]
}];
console.log(filterData(data, "#type"));
If you need only the first match, then use this variant:
var filterData = (inputData, searchedKey) => {
inputData = inputData.filter( data => Object(data) === data );
var res = inputData.find( data => data.hasOwnProperty(searchedKey) );
return res !== undefined ? res[searchedKey] :
// Try in nested objects:
filterData([].concat(...
inputData.map( data =>
Object.keys(data)
.filter( key => !key.startsWith("#") )
.map( key => data[key] )
)
), searchedKey);
};
var data = [{
"#name": "foo",
"#title": "mr",
"deeper": [{
"#nope": "bad",
"deepest": [{
"nothing_here": null,
"#type": "deeper bar",
}]
}, {
"#type": "bar",
}]
}];
console.log(filterData(data, "#type"));
Is this what you want to achieve?
filterData = (inputData, searchedKey) => {
return inputData.map((data, index) => {
if(data){
if(data.hasOwnProperty(searchedKey)){
const res = data[searchedKey]
console.log(res) /// logs the results
return(res) /// returns undefined
}
var dataToProcess = [];
var fieldKeys = Object.keys(data)
fieldKeys = fieldKeys.filter((field, index) => !field.includes("#"))
fieldKeys.forEach((key, index) => {
dataToProcess.push(data[key]);
})
this.filterData(dataToProcess, searchedKey)
}
})
}
console.log(this.filterData([{"#name": "foo", "#type": "bar"}], "#type"))
Use Array#map(), it's pretty useful in many cases.

Categories

Resources