optimal algorithm grouping data in javascript - javascript

The following (simplified) json type of data defines a Contact:
{
id: number;
name: string;
phone: string;
email: string
}
There is the following group of data:
+---+----------+-------------+---------------------------+
|id | name | phone |email |
+---+----------+-------------+---------------------------+
|1 | John | 11111111 |aaaa#test.com |
|2 | Marc | 22222222 |bbbb#test.com |
|3 | Ron | 99999999 |aaaa#test.com |
|4 | Andrew | 55555555 |dddd#test.com |
|5 | Wim | 99999999 |gggg#test.com |
|6 | Marc | 33333333 |cccc#test.com |
|7 | Dan | 44444444 |cccc#test.com |
+---+----------+-------------+---------------------------+
Goal is to find the groups that belong together using javascript(optionally in lodash, but main idea is to get the algorithm clear) according to the following constraint: a contact belongs to a group when any of the following criteria are the same: name, phone or email. The results shows the id's grouped as arrays in arrays. a contact in a group of 1 is ignored.
In the example above, it means that contacts with ids 1,3,5 belong together since 1,3 share the same email and 3 and 5 share the same phonenumber. Likewise 2,6,7: 2 and 6 have the same name and 6 and 7 have the same email. 5 does not have anything in common.
The expected result therefore is:
[[1,3,5], [2,6,7]]
Background:
One solution that works is iterating over every item and check for the remainder of the list if name, email, or phone is the same. If so, group them and take them out of the list (in the example we compare 1 with all the items in the list and only 3 is found). problem is that next items also need to be checked again to these groups, because in this case 5 is not yet detected as part of the group. This makes the algorithm complex, while I suspect there is a simple way of solving this in linear time. There might also be a name for this class of problems?
`

Idea:
Start with 0 groups
Iterate your list of contacts
Check if there is a group with the contact name, or phone, or email. Merge all the members of those groups as the same group. Then add yourself to that group. If not, begin a new group with yourself and set the name, phone and email group as yourself.
Union find is an efficient structure to handle merging of disjoint sets. Code taken from here. As it uses path compression and union by rank, you can consider that the whole code is linear in the amount of contacts.
var data = [
{id:1,name:'John',phone:'11111111',email:'aaaa#test.com'},
{id:2,name:'Marc',phone:'99999999',email:'bbbb#test.com'},
{id:3,name:'Ron',phone:'99999999',email:'aaaa#test.com'},
{id:4,name:'Andrew',phone:'55555555',email:'dddd#test.com'},
{id:5,name:'Wim',phone:'99999999',email:'gggg#test.com'},
{id:6,name:'Marc',phone:'33333333',email:'cccc#test.com'},
{id:7,name:'Dan',phone:'44444444',email:'cccc#test.com'}
];
// UNION-FIND structure, with path comression and union by rank
var UNIONFIND = (function () {
function _find(n)
{
if(n.parent == n) return n;
n.parent = _find(n.parent);
return n.parent;
}
return {
makeset:function(id){
var newnode = {
parent: null,
id: id,
rank: 0
};
newnode.parent = newnode;
return newnode;
},
find: _find,
combine: function(n1, n2) {
var n1 = _find(n1);
var n2 = _find(n2);
if (n1 == n2) return;
if(n1.rank < n2.rank)
{
n2.parent = n2;
return n2;
}
else if(n2.rank < n1.rank)
{
n2.parent = n1;
return n1;
}
else
{
n2.parent = n1;
n1.rank += 1;
return n1;
}
}
};
})();
var groupHash = {name: {}, phone: {}, email: {}}
var groupNodes = []
data.forEach(function(contact){
var group = UNIONFIND.makeset(contact.id);
var groups = new Set();
["name", "phone", "email"].forEach(function(attr){
if (groupHash[attr].hasOwnProperty(contact[attr])) groups.add(groupHash[attr][contact[attr]])
});
groups = Array.from(groups);
groups.push(group);
groupNodes.push(group);
for(var i = 1; i < groups.length; i++) {
UNIONFIND.combine(groups[0], groups[i]);
}
["name", "phone", "email"].forEach(function(attr){
groupHash[attr][contact[attr]] = groups[0];
});
})
var contactsInGroup = {}
groupNodes.forEach(function(group){
var groupId = UNIONFIND.find(group).id;
if (contactsInGroup.hasOwnProperty(groupId) == false) {
contactsInGroup[groupId] = [];
}
contactsInGroup[groupId].push(group.id);
})
var result = Object.values(contactsInGroup).filter(function(list){
return list.length > 1
})
console.log(result)

Any answer that iterates over each of n entries, and then over a growing list of m groups to match against is going to have worst-time performance of O(n*m) (found when there are no two entries that match on any term).
Any answer that iterates over each entry, and then over groups, and uses arrays to test for matching values among q options will further have to pay a penalty of O(q) per match. In the worst case, with say all e-mails the same and all phones different, this will mean O(n*m).
I believe this answer is O(n), because assuming that the number of fields to match with is a constant (in this case, 3: name, phone and email), all operations in the main loop, which is run once per entry, are O(1).
There is an extra complication to fix the fact that, late in the process, we may find a bridge between two (or even 3) groups, as entries can match on different fields with entries from different groups. This may happen several times. To avoid having to rebuild groups during the main loop, we leave merging to the very end, where we first build a map of what-group-ends-up-where, and then finally move all entry IDs to their final group. This can all be done in O(m), with m the number of groups; with an extra O(n) when actually copying entry IDs to the merged groups: overall, we are still in O(n) territory.
The last line builds arrays of ids from the merged groups, and filters out any that does not have over 1 element.
const data = [
{id:1,name:'John',phone:'11111111',email:'aaaa#test.com'},
{id:2,name:'Marc',phone:'99999999',email:'bbbb#test.com'},
{id:3,name:'Ron',phone:'99999999',email:'aaaa#test.com'},
{id:4,name:'Andrew',phone:'55555555',email:'dddd#test.com'},
{id:5,name:'Wim',phone:'99999999',email:'gggg#test.com'},
{id:6,name:'Marc',phone:'33333333',email:'cccc#test.com'},
{id:7,name:'Dan',phone:'44444444',email:'cccc#test.com'}
];
const groups = function(inputs) {
let valuesToGroups = new Map(
['name', 'phone', 'email'].map(key => [key, new Map()]));
let groups = new Map();
let pendingMerges = [];
for (const entry of inputs) {
let group = undefined;
let found = [];
for (const [key, valueMap] of valuesToGroups) {
// look up value in values-index for current key
group = valueMap.get(entry[key]);
if (group !== undefined) {
found.push(group);
// not breaking allows groups to be merged
}
}
if (found.length === 0) {
// not found: create new group
group = groups.size;
groups.set(group, [entry.id]);
} else {
// found: add entry to chosen group
group = found[0];
groups.get(group).push(entry.id);
if (found.length > 1) {
pendingMerges.push(found);
}
}
// add entry's values to index, pointing to group
for (const [key, valueMap] of valuesToGroups) {
valueMap.set(entry[key], group);
}
}
// do pending merges; initially, all groups are stand-alone
let merges = new Map(Array.from(groups.keys()).map(k => [k, k]));
for (const merge of pendingMerges) {
// contents will go to the lowest-numbered group
const sorted = merge.map(groupId => merges.get(groupId)).sort();
sorted.forEach(groupId => merges.set(groupId, sorted[0]));
}
const cleanGroups = new Map();
groups.forEach((value, key) => {
const k = merges.get(key);
if ( ! cleanGroups.has(k)) {
cleanGroups.set(k, []);
}
value.forEach(id => cleanGroups.get(k).push(id))
})
// return only non-empty groups
return [... cleanGroups].filter(g => g[1].length>1).map(g => [... g[1]]);
}(data);
console.log(""+JSON.stringify(groups))
// output is [[1,2,3,5,6,7]]

Here is another suggestion of a route you could take. The idea is to use one Array.reduce to group by id and keep all the values (vls) and combined results (ids) in that accumulator object.
This way you can easily compare the name/phone/email using Array.some + Array.includes (which is what the getGroupId function does).
Once you have grouped and have the almost final result just prettify it by removing the groups with length of one and picking only the ids array of the rest:
var data = [ {id:1,name:'John',phone:'11111111',email:'aaaa#test.com'}, {id:2,name:'Marc',phone:'22222222',email:'bbbb#test.com'}, {id:3,name:'Ron',phone:'99999999',email:'aaaa#test.com'}, {id:4,name:'Andrew',phone:'55555555',email:'dddd#test.com'}, {id:5,name:'Wim',phone:'99999999',email:'gggg#test.com'}, {id:6,name:'Marc',phone:'33333333',email:'cccc#test.com'}, {id:7,name:'Dan',phone:'44444444',email:'cccc#test.com'} ];
const getGroupId = (obj, vals) => Object.entries(obj)
.find(([k,v]) => v.vls.some(x => vals.includes(x))) || []
const group = d => d.reduce((r, c) => {
let values = Object.values(c), groupID = getGroupId(r, values)[0]
if(!groupID)
r[c.id] = ({ vls: values, ids: [...r[c.id] || [], c.id] })
else {
r[groupID] = ({
vls: [...r[groupID].vls, ...values], ids: [...r[groupID].ids, c.id]
})
}
return r
}, {})
const prettify = grp => Object.values(grp).reduce((r,c) => {
if(c.ids.length > 1)
r.push(c.ids)
return r
}, [])
console.log(prettify(group(data)))
One thing to note is that we do not care about the number of properties since we do Object.values. So you can easily add another address or fax to that list and it would still work with zero code changes.
As per feedback here is another version which works slightly different:
var data = [ {id:1,name:'John',phone:'11111111',email:'aaaa#test.com'}, {id:2,name:'Marc',phone:'22222222',email:'bbbb#test.com'}, {id:3,name:'Ron',phone:'99999999',email:'aaaa#test.com'}, {id:4,name:'Andrew',phone:'55555555',email:'dddd#test.com'}, {id:5,name:'Wim',phone:'99999999',email:'gggg#test.com'}, {id:6,name:'Marc',phone:'33333333',email:'cccc#test.com'}, {id:7,name:'Dan',phone:'44444444',email:'cccc#test.com'} ];
var testData = [{ id: 1, name: 'John', phone: '1', email: 'a' }, { id: 2, name: 'Marc', phone: '2', email: 'b' }, { id: 3, name: 'Ron', phone: '1', email: 'b' }];
const getGroupId = (obj, vals) => Object.entries(obj)
.find(([k,v]) => v.vls.some(x => vals.includes(x))) || []
const group = d => d.reduce((r,c,i,a) => {
let values = Object.values(c), groupID = !i ? i : getGroupId(r, values)[0]
if (!groupID) {
let hits = a.filter(x =>
x.id != c.id && values.some(v => Object.values(x).includes(v)))
hits.forEach(h =>
r[c.id] = ({ vls: [...values, ...Object.values(h)], ids: [c.id, h.id] }))
}
else
r[groupID] = r[groupID].ids.includes(c.id) ? r[groupID] :
({ vls: [...r[groupID].vls, ...values], ids: [...r[groupID].ids, c.id] })
return r
}, {})
const prettify = grp => Object.values(grp).reduce((r, c) => {
if (c.ids.length > 1)
r.push(c.ids)
return r
}, [])
console.log(prettify(group(data))) // OP data
console.log(prettify(group(testData))) // Test data
The reason for this version is due to the testData provided by #Mark which has the 2nd element not matching the first but matching the 3rd which actually matches the 1st ... so they all should be hits.
To get to that once we find a match we look for matches of that same initial match and push in the same group so we can have the maximum amount of data to match on.
The result is that once we get the first group with the first element we then find and push the 3rd as well and from there it is much easier to match the 2nd. The logic is slightly more complex and I would imagine less performant.

One way to accomplish what you need, is to separate the contacts into groups.
Each group will contain a list of names, phones and emails.
Then iterate through contacts, and see if the current contact falls into any of the groups. If not, create a new group and set its names/phones/emails so that next contacts might fall into the same on.
var data = [
{id:1,name:'John',phone:'11111111',email:'aaaa#test.com'},
{id:2,name:'Marc',phone:'22222222',email:'bbbb#test.com'},
{id:3,name:'Ron',phone:'99999999',email:'aaaa#test.com'},
{id:4,name:'Andrew',phone:'55555555',email:'dddd#test.com'},
{id:5,name:'Wim',phone:'99999999',email:'gggg#test.com'},
{id:6,name:'Marc',phone:'33333333',email:'cccc#test.com'},
{id:7,name:'Dan',phone:'44444444',email:'cccc#test.com'}
];
var groups = [];
data.forEach(function(person){
var phone = person.phone;
var email = person.email;
var name = person.name;
var id = person.id;
var found = false;
groups.forEach(function(g){
if( g.names.indexOf(name) > -1
|| g.phones.indexOf(phone)>-1
|| g.emails.indexOf(email)>-1) {
found = true;
g.names.push(name);
g.phones.push(phone);
g.emails.push(email);
g.people.push(id);
}
});
if(!found) {
groups.push({names:[name],phones:[phone],emails:[email],people:[id]});
}
});
var output=[];
groups.forEach(function(g){
output.push(g.people);
});
console.log(output); //[ [1,3,5] , [2,6,7] , [4] ]

Related

How to exclude redundant patterns among a large array of glob string

I have been working on this algorithm for days, and no idea how to figure out the most suitable/easy/optimized solution.
Here I have a large array of string as the followings
[
*.*.complete
*.*.read
*.*.update
*.order.cancel
accounting.*.delete
accounting.*.update
accounting.*.void
accounting.account.*
admin.user.read
admin.user.update
admin.format.delete
...
]
// the array may be in random order
all the values are in some wildcard patterns (in fact, they are the permissions for my system)
what i want to do is to remove redundant patterns, for example: admin.json_api.read is redundant due to *.*.read
can someone give me any suggestion/approach?
The following approach takes different glob segment length's into account as well.
Thus in a first step the glob-array is reduced into one or more segment-length specific arrays of better inspectable glob-items.
Such an item features e.g. a regex specific pattern of its actual glob-value.
Within a final tasks each segment-length specific array gets sanitized separately into an array of non redundant glob-values.
The latter gets achieved by 1st sorting each array descending by each item's glob-value (which assures a sorting from the more to the less generic glob values) and 2nd by rejecting each item where its glob-value gets covered already by a more generic glob-value.
And the base of such a detection is the glob-value specific regex where the asterisk wild card translates into a regex pattern with the same meaning ... thus any glob value of '*.' equals a regex of /[^.]+\./ and any terminating '.*' equals a regex of /\.[^.]+/.
Since the sanitizing task is done via flatMap, the end result is a flat array again ...
function createGlobInspectionItem(glob) {
const segments = glob.split('.');
return {
value: glob,
pattern: glob
.replace((/\*\./g), '[^.]+.')
.replace((/\.\*$/), '.[^.]+')
.replace((/(?<!\^)\./g), '\\.'),
segmentCount: segments.length,
};
}
function collectGlobInspectionItems({ index, result }, glob) {
const globItem = createGlobInspectionItem(glob);
const groupKey = globItem.segmentCount;
let groupList = index[groupKey];
if (!groupList) {
groupList = index[groupKey] = [];
result.push(groupList);
}
groupList.push(globItem);
return { index, result };
}
function createSanitizedGlobList(globItemList) {
const result = [];
let globItem;
globItemList.sort(({ value: aValue }, { value: bValue }) =>
(aValue > bValue && -1) || (aValue < bValue && 1) || 0
);
while (globItem = globItemList.pop()) {
globItemList = globItemList.filter(({ value }) =>
!RegExp(globItem.pattern).test(value)
);
result.push(globItem);
}
return result.map(({ value }) => value);
}
const sampleData = [
// 3 segments
'*.*.complete',
'*.*.read',
'*.*.update',
'*.order.cancel',
'accounting.*.delete',
'accounting.*.update',
'accounting.*.void',
'accounting.account.user',
'accounting.account.*',
'accounting.account.admin',
'admin.user.read',
'admin.user.update',
'admin.format.delete',
// 2 segments
'*.read',
'*.update',
'user.read',
'user.update',
'format.delete',
'format.account',
];
console.log(
'... intermediata inspection result grouped by section length ...',
sampleData
.reduce(collectGlobInspectionItems, { index: {}, result: [] })
.result
);
console.log(
'... final sanitized and flattened glob array ...',
sampleData
.reduce(collectGlobInspectionItems, { index: {}, result: [] })
.result
.flatMap(createSanitizedGlobList)
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
General idea:
Each your pattern can be transformed into regex using:
new RegExp('^' + pattern
.replace(/[./]/g, '\\$&') // escape chars (list isn't full)
.replace(/\*/g, '(.*)') // replace asterisk with '(.*)' - any char(s)
+ '$') // match only full pattern
If one pattern match another one - you don't need both, because pattern with * include second one:
if (pattern1.include('*') && pattern1.test(pattern2)) {
// delete pattern2
}
Simple realization can be found below (still need to optimize a bit).
Full code:
// Your initial array
const patterns = [
'*.*.complete',
'*.*.read',
'*.*.update',
'*.order.cancel',
'accounting.*.delete',
'accounting.*.update',
'accounting.*.void',
'accounting.account.*',
'admin.user.read',
'admin.user.update',
'admin.format.delete',
]
// Build a new one with regexes
const withRegexes = patterns.map(pattern => {
// Create a regex if pattern contain asterisk
const regexp = pattern.includes('*') ? new RegExp('^' + pattern
.replace(/[./]/g, '\\$&')
.replace(/\*/g, '(.*)')
+ '$') : null;
return { pattern, regexp };
});
// Array of indexes of elements where it's pattern already matched by another pattern
let duplicateIndexes = [];
for (let i = 0; i < withRegexes.length - 1; i++) {
for (let j = i + 1; j < withRegexes.length; j++) {
if (withRegexes[i].regexp
&& withRegexes[i].regexp.test(withRegexes[j].pattern)) {
duplicateIndexes.push(j);
}
}
}
// Get unique indexes to delete in desc order
duplicateIndexes = [ ...new Set(duplicateIndexes) ].sort((a, b) => b - a);
// Clear up initial array
for (let index of duplicateIndexes) {
patterns.splice(index, 1);
}
// New one
console.log(patterns);

Sorting array of objects into an array of paired objects with Javascript

I have an array of objects and I want to be able to sort them by their "site" value into pairs. There can't be more that 2 objects in each child array so if there is 3 matches I get 1 child array with 2 objects and 1 child array with 1 object.
I have:
[{site:'A'}, {site:'A'}, {site:'B'}, {site:'B'}, {site:'B'}];
I want:
[[{site:'A'}, {site:'A'}],[{site:'B'}, {site:'B'}], [{site:'B'}]]
Whats the best way to do this? any help is appreciated.
This should work for you
function sortArray(arr){
arr.sort((a,b)=>a.site > b.site ? 1 : -1) // Sorting the array to have consecutive values
let chunks = [];
for(let i = 0;i<arr.length;i+=2){
if(arr[i]?.site == arr[i+1]?.site) chunks.push(arr.slice(i,i+2));
else {
chunks.push([arr[i]]);
i--;
}
}
return chunks;
}
let arr = [{site:'A'}, {site:'A'}, {site:'B'}, {site:'B'}, {site:'B'}];
console.log(sortArray(arr))
Using reduce ;) :
const a = [{
site: 'A'
}, {
site: 'A'
}, {
site: 'B'
}, {
site: 'B'
}, {
site: 'B'
}];
var r = a.reduce((ac, x) => ({
...ac,
[x.site]: [...(ac[x.site] || []), x]
}), {})
var r2 = Object.values(r).flatMap(x =>
x.reduce((ac, z, i) => {
if (i % 2) {
ac[i - 1].push(z)
return ac
}
return [...ac, [z]]
}, []))
console.log(r2)
PS: Since this is hard to read I'd suggest to use lodash (specifically groupBy and chunk methods)
It's kind of a 'groupBy' operation (as seen in underscore or lodash). Those produce an object keyed by the values being grouped. Consider writing it that way for general use. To get the shape the OP is looking for, strip out the values of that result...
function groupBy(array, key) {
return array.reduce((acc, el) => {
let value = el[key];
if (!acc[value]) acc[value] = [];
acc[value].push(el);
return acc;
}, {});
}
let array = [{site:'A'}, {site:'A'}, {site:'B'}, {site:'B'}, {site:'B'}];
let grouped = groupBy(array, 'site'); // produces { A: [{site:'A'} ...], B: ... }
let groups = Object.values(grouped)
console.log(groups)

Filter and include based on two or more strings in array

I have a query/string that I want to do a filter + include to check if that string is matching an item in another array. For example, I have this which works for a single string:
const filteredString = `${this.filter}`.toLowerCase();
this.filteredCampaigns = this.filteredCampaigns.concat(this.allCampaigns.
filter((item) => item.description?.toLowerCase().includes(filteredString) ||
item.type?.toLowerCase().includes(filteredString) ||
item.code?.toLowerCase().includes(filteredString)
));
Since I want to match multiple strings, I do a split() in my string to make it to an array and put the strings in separated elements:
const filteredString: string = `${this.filter}`.toLowerCase().split(' ');
Now thats an array, I make a for-of loop through my filter methods:
for (const val of filteredString) {
this.filteredCampaigns = this.filteredCampaigns?.concat(this.allCampaigns.
filter((item) => item.description?.toLowerCase().includes(val) ||
item.type?.toLowerCase().includes(val) ||
item.code?.toLowerCase().includes(val)
));
}
}
Problem with above is that it will filter out each element from the array independently from each other. So if my query/filteredString have two string elements, and one of the elements returns a match and the other does not, I will still get a result. But I want ALL of the items in the string to be 'connected' to each other in the filtering, and return all items that partially matches all of the strings in my array. How to do it?
Your filter string.
const filter = 'not that';
Space-separated filters.
const filteredStrings: string[] = `${filter}`.toLowerCase().split(' ');
Assuming your campaign data.
const allCampaigns = [{
description: 'This is ...',
type: 'TYPE1',
code: 'TD35'
},
{
description: 'That will be done...',
type: 'TYPE2',
code: 'TC33'
},
{
description: 'You shall not pass',
type: 'UUGG',
code: 'CD01'
}
];
Filtered campaigns
let filteredCampaigns: any[] = [];
Assign method:
filteredCampaigns = filteredCampaigns?.concat(allCampaigns.filter((item) => filteredStrings.filter(val => item.description?.toLowerCase().includes(val)).length ||
filteredStrings.filter(val => item.type?.toLowerCase().includes(val)).length ||
filteredStrings.filter(val => item.code?.toLowerCase().includes(val)).length
));
Demo
You could destructure item and check if the filter contains one of the value.
const
filter = this.filter.toLowerCase().split(/\s+/);
this.filteredCampaigns = this.allCampaigns.filter(({ description = '', type = '', code = '' }) =>
[description.toLowerCase(), type.toLowerCase(), code.toLowerCase()]
.some(word => filter.includes(word))
);
Rather than concatenating the matches to the array, I would push the matches of each string to it's own array firstly, then check if the amount of matches found is the same number of filteredStrings.
If all strings had matches, I reduce the filteredCampaigns array from 2D to 1D
else, I reset the filteredCampaigns to an empty array
const filteredString: string = `${this.filter}`.toLowerCase().split(' ');
this.filteredCampaigns = [];
for (const val of filteredStrings) {
this.filteredCampaigns.push(this.allCampaigns.filter(item => item.description?.toLowerCase().includes(val) || item.type?.toLowerCase().includes(val) || item.code?.toLowerCase().includes(val)))
}
if (this.filteredCampaigns.length == filteredStrings.length) this.filteredCampaigns.flat();
else this.filteredCampaigns = [];

Find all the possible combinations of three string arrays to validate a provided slug

I need to generate all the possible combinations of three string arrays (style, color, room type)... Given this 3 string arrays:
const stylesArray = ['modern'];
const colorsArray = ['blue', 'white'];
const typesArray = ['kitchen', 'living room', 'bedroom'];
The output array should contain the following values:
modern
blue
white
kitchen
living room
bedroom
modern-blue
modern-white
modern-kitchen
modern-living room
modern-bedroom
modern-blue-kitchen
modern-white-kitchen
...
And so on... the combinations are valid only in that given order (style, color, room type). And note that only one, or two of the elements of each array is also valid. I found some posts where you can combine all the three, but not with this last condition I just mentioned.
The goal here, is to be able to validate if the slug provided is a valid one based on those given values for each "category" (style, color, room type). But those values aren't hardcoded, those are dynamically pulled from an API (which doesn't matter for this purpose).
So, the one listed above are valid slugs, but this are not:
blue-modern
kitchen-blue
kitchen-modern-blue
Which is why I mentioned that the valid order is style -> color -> type (but not all the values are mandatory, it could be just one or two, and up to three, but always in that order)
Thanks in advance!
I would create an array of valid combinations like this:
const stylesArray = ['modern'];
const colorsArray = ['blue', 'white'];
const typesArray = ['kitchen', 'living room', 'bedroom'];
stylesArray.push(null);
colorsArray.push(null);
typesArray.push(null);
var combinations = [];
stylesArray.forEach(style => {
colorsArray.forEach(color => {
combinations.push([style, color]);
typesArray.forEach(type => {
combinations.push([style, color, type]);
});
});
});
var validSlugs = combinations.map(comb => comb.filter(el => el).join('-'));
console.log(validSlugs);
I'm basically creating an array of combinations, where each element is an array of the three values (including possible nulls), and then mapping it to strings.
You can create a tree and then traverse in BFS
const stylesArray = ["modern"];
const colorsArray = ["blue", "white"];
const typesArray = ["kitchen", "living room", "bedroom"];
const items = [
{
id: stylesArray[0],
children: colorsArray.map((id) => ({
id,
children: typesArray,
})),
},
];
const traverse = (items, prefix = "", slugs = new Set()) => {
if (Array.isArray(items)) {
items.forEach((x) => traverse(x, prefix, slugs));
} else if (typeof items === "object") {
prefix && slugs.add(items.id);
slugs.add(prefix + items.id);
items.children.forEach((ch) => {
traverse(ch, prefix + items.id + "-", slugs);
});
} else {
items = items.replace(/\s+/, "-");
prefix && slugs.add(items); //comment if u need induvidule kitchen, living room
slugs.add(prefix + items);
}
return Array.from(slugs);
};
console.log(traverse(items).sort((x, y) => x.length - y.length));

get the highest role from array

I currently have an array u._roles that is filled with id's from groups like "581199939464462340".
Secondly I have a dictionary where the key is a role id and the value is the name, like {key: "581199939464462340", value: "Member"}.
Now I want to get the name of the "highest" role of the array. Currently I have this ugly if statement but I think that can be done better, but how? (The array u._roles is read only).
u._roles could look like this:
["5812040340469414946", "581200340469415946", "581209222700597248"] so the "highest" would be "581200340469415946" (second if). (And then get the name of this id from the dictionary roleMap)
var Rank;
if (u._roles.includes("581199939464462340")) {
Rank = roleMap["581199939464462340"]; // highest
} else if (u._roles.includes("581200340469415946")) {
Rank = roleMap["581200340469415946"]; //
} else if (u._roles.includes("581214123334041620")) {
Rank = roleMap["581214123334041620"]; //
} else if (u._roles.includes("588976480017448988")) {
Rank = roleMap["588976480017448988"]; //
} else if (u._roles.includes("581203853635223574")) {
Rank = roleMap["581203853635223574"]; //
} else if (u._roles.includes("581209222700597248")) {
Rank = roleMap["581209222700597248"]; //
} else if (u._roles.includes("592436270031175681")) {
Rank = roleMap["592436270031175681"]; // lowest
} else {
Rank = "";
}
Highest don't mean the highest number. Its just an order I like to use.
I completely changed this answer with some new insight. Hopefully this is a bit more helpful.
const rolePriority = {a:1, b:2, c:3, d:4};
const u = {"_roles": ['b','c', 'a']};
const rankNumber = u._roles.reduce((highest, role) => {
const rank = rolePriority[role];
return rank < highest ? rank : highest;
}, Infinity);
const rank = rankNumber === Infinity ? "" : roleMap[rankNumber]
We just save the right order in an array, loop it and check whether the value is included in roles. If yes find will return it and we can put it into roleMap[findvalue].
let order = ["581199939464462340", "581200340469415946", "581214123334041620", "588976480017448988", "581203853635223574", "581209222700597248","592436270031175681"],
roles = ["5812040340469414946", "581200340469415946", "581209222700597248"];
let rank = order.find(v => roles.includes(v));
console.log(rank) // => 581200340469415946
Replace the rank line in your code with:
let rank = roleMap[order.find(v => roles.includes(v))]
If your order or amount of elements in orders is likely to change this will be messy, if not, you can just save them for comparison like this.
Firstly copy the array, then sort it by the role property and take the first element.
var data = [{
name: "Rank 1",
role: "5812040340469414946",
},
{
name: "Rank 2",
role: "581200340469415946"
},
{
name: "Rank 3",
role: "581209222700597248"
}
];
// Copy the source array to avoid mutating it using destructuring it
var copy = [...data];
// Sort by role property
var rank = copy.sort((a, b) => (+a.role) - (+b.role))[0].name;
console.log(rank);

Categories

Resources