Reduce array list, to set of common paths - javascript

I am trying to reduce a list to a shorter list, of just common filesystem paths. Trying to find all the common grandparents, and put only those in the final list. Here is the goal: The goal is that we must eliminate all directories in the list for which there is a parent directory in the list.
A better way to phrase that might be:
The goal is that we must eliminate all paths in the list for which there is a parent directory of that path in the list.
Say I have this input and expected output:
const input = [
"/home/oleg/WebstormProjects/oresoftware/r2g",
"/home/oleg/WebstormProjects/oresoftware/sumanjs/suman",
"/home/oleg/WebstormProjects/oresoftware/sumanjs",
"/home/oleg/WebstormProjects/oresoftware/sumanjs/suman-types",
"/home/oleg/WebstormProjects/oresoftware/sumanjs/suman-watch"
];
const output = [
"/home/oleg/WebstormProjects/oresoftware/r2g",
"/home/oleg/WebstormProjects/oresoftware/sumanjs",
];
const getReducedList = function (input) {
return input
.sort((a, b) => (a.length - b.length))
.reduce((a, b) => {
// console.log('a:', a, 'b:', b);
const s = !a.some(v => {
return b.startsWith(v);
});
if (s) {
a.push(b);
}
return a;
}, []);
};
console.log(getReducedList(input));
that getReducedList function seems to work with our first test case, 5 is reduced to 2. But, if we add a second test case, here is where things get weird:
If I remove an item from the original list and change it to this list of 4:
const input = [
"/home/oleg/WebstormProjects/oresoftware/r2g",
"/home/oleg/WebstormProjects/oresoftware/sumanjs/suman",
"/home/oleg/WebstormProjects/oresoftware/sumanjs/suman-types",
"/home/oleg/WebstormProjects/oresoftware/sumanjs/suman-watch"
];
then I would expect to get this output (the same list of 4):
const output = [
"/home/oleg/WebstormProjects/oresoftware/r2g",
"/home/oleg/WebstormProjects/oresoftware/sumanjs/suman",
"/home/oleg/WebstormProjects/oresoftware/sumanjs/suman-types",
"/home/oleg/WebstormProjects/oresoftware/sumanjs/suman-watch"
];
the reason why I would expect/desire the same list of 4, is because no item in the list has a parent directory elsewhere in the list. But I actually get this output, a list of 2, which is incorrect:
const output = [
'/home/oleg/WebstormProjects/oresoftware/r2g',
'/home/oleg/WebstormProjects/oresoftware/sumanjs/suman'
];
does anyone know how I can fix this to get the expected result instead? An answer needs to satisfy both test cases.
To make it perfectly clear, if you add "/home/oleg" to the original list, then "/home/oleg" should be the only entry in the output.

I think you can do this with a very simple recursive function. You simply sort by length, then recursively pop the shortest, add it to the results, filter the array with that, recurse:
const input = [
"/home/oleg/WebstormProjects/oresoftware/r2g",
"/home/oleg/WebstormProjects/oresoftware/sumanjs/suman",
"/home/oleg/WebstormProjects/oresoftware/sumanjs",
"/home/oleg/WebstormProjects/oresoftware/sumanjs/suman-types",
"/home/oleg/WebstormProjects/oresoftware/sumanjs/suman-watch"
];
input.sort((a,b) => b.length - a.length)
function getPrefixes(list, res =[]) {
if (list.length < 1) return res
let next = list.pop()
res.push(next)
return getPrefixes(list.filter(u => !u.startsWith(next + '/')), res)
}
console.log(getPrefixes(input))

Looks like we only needed to change one line. Here is the original:
const getReducedList = function (input) {
return input
.sort((a, b) => (a.length - b.length))
.reduce((a, b) => {
const s = !a.some(v => {
return b.startsWith(v);
});
if (s) {
a.push(b);
}
return a;
}, []);
};
we need to change the one line to this instead:
return b.startsWith(v + '/');

You could simply use node's path module
const path = require('path');
const input = [
'/home/oleg/WebstormProjects/oresoftware/r2g',
'/home/oleg/WebstormProjects/oresoftware/sumanjs/suman',
'/home/oleg/WebstormProjects/oresoftware/sumanjs',
'/home/oleg/WebstormProjects/oresoftware/sumanjs/suman-types',
'/home/oleg/WebstormProjects/oresoftware/sumanjs/suman-watch'
];
const s = new Set();
const out = [];
input.forEach(e => {
let p = path.dirname(e);
if (! s.has(p)) {
s.add(p);
out.push(e);
}
});
console.log(out);
The forEach could obviously replaced by reduce if you want to...

If you can use sets from es6 then you can simply sort with respect to directory length and keep pushing to a set.

You could add a slash for look up and pop the last value if found in next element.
This proposal uses a sorted list by value, not by lenght of the string, because it needs a sorted list for compairing the next element.
function uniqueFolder(array) {
return array
.sort()
.reduce((a, b) => {
if (b.startsWith(a[a.length - 1] + '/')) {
a.pop();
}
a.push(b);
return a;
}, []);
}
function commonPath(array) {
return array
.sort()
.reduce((a, b) => {
if (!b.includes(a[a.length - 1])) {
a.push(b);
}
return a;
}, []);
}
const input = [ "/home/oleg/WebstormProjects/oresoftware/r2g", "/home/oleg/WebstormProjects/oresoftware/sumanjs/suman", "/home/oleg/WebstormProjects/oresoftware/sumanjs", "/home/oleg/WebstormProjects/oresoftware/sumanjs/suman-types", "/home/oleg/WebstormProjects/oresoftware/sumanjs/suman-watch"];
console.log(uniqueFolder(input));
console.log(commonPath(input));
.as-console-wrapper { max-height: 100% !important; top: 0; }

You need to avoid matches when the next value does not have its last forward slash following the previous value.
const input = [
"/home/oleg/WebstormProjects/oresoftware/r2g",
"/home/oleg/WebstormProjects/oresoftware/sumanjs/suman",
"/home/oleg/WebstormProjects/oresoftware/sumanjs",
"/home/oleg/WebstormProjects/oresoftware/sumanjs/suman-types",
"/home/oleg/WebstormProjects/oresoftware/sumanjs/suman-watch"
];
const getReducedList = (set => input => input.filter(b =>
!set.has(b.substr(0, b.lastIndexOf("/")))))(new Set(input));
console.log(getReducedList(input));
The logic is that it puts all paths in a set, and then checks for each path whether its parent is in the set. A path's parent is the path that has all characters up to, and excluding, the final /.
Removing the second entry from the input will not change the output. suman is treated the same way as suman-types and suman-watch.
Here is a more verbose, ES5 version (replacing the Set with a plain object):
const input = [
"/home/oleg/WebstormProjects/oresoftware/r2g",
"/home/oleg/WebstormProjects/oresoftware/sumanjs/suman",
"/home/oleg/WebstormProjects/oresoftware/sumanjs",
"/home/oleg/WebstormProjects/oresoftware/sumanjs/suman-types",
"/home/oleg/WebstormProjects/oresoftware/sumanjs/suman-watch"
];
function getReducedList(input) {
var hash = {};
input.forEach(function(path) {
hash[path] = true;
});
return input.filter(function (b) {
return !hash[b.substr(0, b.lastIndexOf("/"))];
}, []);
}
console.log(getReducedList(input));

It works without sort though has other drawbacks.
Edit: Had to add sort to work properly
const input = [
"/home/oleg/WebstormProjects/oresoftware/r2g",
"/home/oleg/WebstormProjects/oresoftware/sumanjs/suman",
"/home/oleg/WebstormProjects/oresoftware/sumanjs",
"/home/oleg/WebstormProjects/oresoftware/sumanjs/suman-types",
"/home/oleg/WebstormProjects/oresoftware/sumanjs/suman-watch"
];
const input2 = [
"/home/oleg/WebstormProjects/oresoftware",
"/home/oleg/WebstormProjects/oresoftware/r2g",
"/home/oleg/WebstormProjects/oresoftware/sumanjs/suman",
"/home/oleg/WebstormProjects/oresoftware/sumanjs/suman-types",
"/home/oleg/WebstormProjects/oresoftware/sumanjs/suman-watch"
];
function getParents(input) {
return input.sort((a, b) => a.length - b.length)
.reduce((acc, a) => {
const index = acc.findIndex(b => b.startsWith(a) || a.startsWith(b));
const slashDiff = index > 0 ? a.split('/').length === acc[index].split('/').length : false;
if (index === -1 || slashDiff) {
return [...acc, a];
}
acc.splice(index, 1, a.length < acc[index].length ? a : acc[index])
return acc;
}, []);
}
console.log(getParents(input));
console.log(getParents(input2));

const input = [
"/home/oleg/WebstormProjects/oresoftware/r2g",
"/home/oleg/WebstormProjects/oresoftware/sumanjs/suman",
"/home/oleg/WebstormProjects/oresoftware/sumanjs",
"/home/oleg/WebstormProjects/oresoftware/sumanjs/suman-types",
"/home/oleg/WebstormProjects/oresoftware/sumanjs/suman-watch",
"/home/oleg/WebstormProjects/oresoftware/sumanjs/suman-types-blabla",
];
let ouput = input.sort().reduce((a, c)=> {
let d = true;
for (let j =0; j< a.length; j++){
if(c.startsWith(a[j]+'/')) {
d=false;
break;}
}
if (d) a.push(c);
return a;
},[]);
console.log(ouput);

Related

Return all possible combinations of array with optional strings

Lets say I have an array keys = ["the?", "orange", "van", "s?"], with '?' at the end of strings to represent that it is optional.
I want a function in javascript generateCombinations(keys) that returns the possible combinations such as :
[["orange","van"],["the","orange","van"],["orange","van","s"],["the","orange","van","s"]]
One possible way of removing '?' is to simply do a replace("?',"").
I have a feeling it might require a recursive function, which I am not yet quite strong in. Help is appreciated!
So far I've tried this:
function isOptionalKey(key) {
return key.endsWith('?');
}
function hasOptionalKey(keys) {
return keys.some(isOptionalKey);
}
function stripOptionalSyntax(key) {
return key.endsWith('?') ? key.slice(0, -1) : key;
}
function generateCombinations(keys) {
if (keys.length === 1) {
return keys;
}
const combinations = [];
const startKey = keys[0];
const restKeys = keys.slice(1);
if (hasOptionalKey(restKeys)) {
const restCombinations = isOptionalKey(startKey)
? generateCombinations(restKeys)
: restKeys;
if (isOptionalKey(startKey)) {
combinations.push(restCombinations);
}
combinations.push(
restCombinations.map((c) => [stripOptionalSyntax(startKey), ...c])
);
} else {
if (isOptionalKey(startKey)) {
combinations.push(restKeys);
}
combinations.push([stripOptionalSyntax(startKey), ...restKeys]);
}
return combinations;
}
You could take a recursive approach by using only the first item of the array and stop if the array is empty.
const
getCombinations = array => {
if (!array.length) return [[]];
const
sub = getCombinations(array.slice(1)),
optional = array[0].endsWith('?'),
raw = optional ? array[0].slice(0, -1) : array[0],
temp = sub.map(a => [raw, ...a]);
return optional
? [...temp, ...sub]
: temp;
};
keys = ["the?", "orange", "van", "s?"],
result = getCombinations(keys);
console.log(result.map(a => a.join(' ')));

How do i move through the values of keys in javascript?

How would i cycle move the key values in a js array,
for example:
{Name:"A", Task:"Task1"},
{Name:"B", Task:"Task2"},
{Name:"C", Task:"Task3"},
to
{Name:"A", Task:"Task3"},
{Name:"B", Task:"Task1"},
{Name:"C", Task:"Task2"},
to clarify further it should be a function that every time is run "shifts the task column" by one.
I have tried using methods such as:
ary.push(ary.shift());
however i don't know any way that i can specifically apply this to a specific key while not moving the others.
Map the array, and take the task from the previous item in the cycle using the modulo operation.
Unlike the % operator the result of using modulo with negative numbers would be a positive remainder. It's needed because in the first item (i is 0) i - 1 would be -1.
const modulo = (a, n) => ((a % n ) + n) % n
const fn = arr =>
arr.map((o, i) => ({
...o,
Task: arr[modulo((i - 1), arr.length)].Task
}))
const arr = [{"Name":"A","Task":"Task1"},{"Name":"B","Task":"Task2"},{"Name":"C","Task":"Task3"}]
const result = fn(arr)
console.log(result)
You are very close. You can decompose the array into two individual ones and shift one of them. See below function:
const arr = [
{ Name: "A", Task: "Task1" },
{ Name: "B", Task: "Task2" },
{ Name: "C", Task: "Task3" }
];
function cycle(arr) {
const names = arr.map(el => el.Name);
const tasks = arr.map(el => el.Task);
const el = tasks.pop();
tasks.unshift(el);
return names.map((letter, i) => {
return {
Name: letter,
Task: tasks[i]
};
});
}
console.log(cycle(arr));
Also, codepen for conveninence.
Approach using a Generator and your pop and shift concept
const data=[{Name:"A",Task:"Task1"},{Name:"B",Task:"Task2"},{Name:"C",Task:"Task3"}];
function* taskGen(data){
// create standalone array of the names that can be shifted
const tasks = data.map(({Task}) => Task);
while(true){
// move first to last position
tasks.push(tasks.shift());
// return new mapped array
yield data.map((o, i) => ({...o, Task: tasks[i] }))
}
}
const shifter = taskGen(data);
for (let i =0; i< 6; i++){
const next = shifter.next().value
console.log(JSON.stringify(next))
}
.as-console-wrapper {max-height: 100%!important;top:0;}

Check whether an Object contains a specific value in any of its array

How to find if an object has a value?
My Object looks like below: I have to loop through the object array and check if an array has "SPOUSE" in its value. if exist set a flag spouseExits = true and store the number (in this case (4 because [SPOUSE<NUMBER>] NUMBER is 4) in a variable 'spouseIndex'
This function needs to render in IE9 as well.
eligibilityMap = {
"CHIP": [
"CHILD5"
],
"APTC/CSR": [
"SELF1",
"CHILD2",
"CHILD3",
"SPOUSE4"
]
}
Code:
Object.keys(eligibilityMap).reduce(function (acc, key) {
const array1 = eligibilityMap[key];
//console.log('array1', array1);
array1.forEach(element => console.log(element.indexOf('SPOUSE')))
var spouseExist = array1.forEach(function (element) {
//console.log('ex', element.indexOf('SPOUSE') >= 0);
return element.indexOf('SPOUSE') >= 0;
});
//console.log('spouseExist', spouseExist);
return acc;
}, {});
SpouseIndex is undefined. What am I doing wrong?
Here's a simple approach, supports in all browsers including IE:
var spouseExists = false;
var spouseNumber;
for(var key in eligibilityMap)
{
for(var index in eligibilityMap[key])
{
if (eligibilityMap[key][index].indexOf("SPOUSE") > -1)
{
spouseExists = true;
spouseNumber = eligibilityMap[key][index].replace("SPOUSE", '');
break;
}
}
}
console.log(spouseExists, spouseNumber);
Solution 1: do 3 steps
You can use flatMap to get all sub-array into one.
use findIndex combined with startsWith to get exactly the index of SPOUSE
assign 2 variables based on the value of the found index.
const eligibilityMap = { "CHIP": [ "CHILD5" ], "APTC/CSR": ["SELF1","CHILD2","CHILD3","SPOUSE4"]};
let SpouseExits = false, SpouseIndex = 0;
const arrays = Object.values(eligibilityMap).flatMap(r => r);
const index = arrays.findIndex(str => str.startsWith("SPOUSE"));
if(index >= 0){
SpouseExits = true;
SpouseIndex = arrays[index].slice(-1);
}
console.log({SpouseExits, SpouseIndex});
Solution 2: This function renders in IE9 as well
const eligibilityMap = { "CHIP": [ "CHILD5" ], "APTC/CSR": ["SELF1","CHILD2","CHILD3","SPOUSE4"]};
let SpouseExits = false, SpouseIndex = 0;
for(const [key, value] of Object.entries(eligibilityMap))
{
const index = value.findIndex(function(str){ return str.startsWith("SPOUSE")});
if(index >= 0){
SpouseExits = true;
SpouseIndex = value[index].slice(-1);
}
}
console.log({SpouseExits, SpouseIndex});
Your condition element.indexOf('SPOUSE') >= 0 is not matching any value spouse on your array, because your array has no value named spouse SPOUSE it has SPOUSE4 though.
that's why it's returning undefined.
You may use regex instead of direct matching,
Object.keys(eligibilityMap).reduce(function (acc, key) {
const array1 = eligibilityMap[key];
//console.log('array1', array1);
// array1.forEach(element => console.log(element.indexOf('SPOUSE')))
// var spouseExist = -1;
var spouseExist = array1.filter(function (element,index) {
if(element.match(/SPOUSE/g)) return element; //fixes
});
//fixes
console.log('spouseExist',spouseExist.length>0)
if(spouseExist.length>0){
spouseExist.forEach(element => {
console.log('spouseIndex',element[element.length-1])
});
}
return acc;
}, {});
you can get the number from the spouse name directly, or you can access the index number of the matching spouse from inside the filter function using the value of index and do whatever you like.
Hope this matches your requirement.

How to flatten/reduce only the deepest or otherwise specicified level of an Nth-dimensional array

assuming an N-th dimensional array, eg.
const array = [
"1",
["2","2"],
[["3","3"],["3","3"]],
[
[
[["4","4"],"3"],
[["4","4"],"3"]
],
],
[["3","3"],["3","3"]],
["2","2"],
"1"
];
I can only find methods to flatten arrays from shallow-end of the index up, but I need to flatten/reduce or otherwise handle only the deepest (or any arbitrary) index level.
When running the deepest level I am looking for an array output something along the lines of
array = [
"1",
["2","2"],
[["3","3"],["3","3"]],
[
[
["4,4","3"],
["4,4","3"]
],
],
[["3","3"],["3","3"]],
["2","2"],
"1"
];
I cannot find a solution that isn't garbage... (over-complicated/incredibly messy)
Any help would be appreciated
You could create a function that will take the data and level that you want to flatten and will flatten only that level. Then to get the last level you can create another function.
const array = ["1",["2","2"],[["3","3"],["3","3"]],[[[["4","4"],"3"],[["4","4"],"3"]],[["",""],["",""]]],[["3","3"],["3","3"]],["2","2"],"1"];
function lastLvl(data, lvl = 0) {
return data.reduce((r, e) => {
if (lvl > r) r = lvl
if (Array.isArray(e)) {
const nlvl = lastLvl(e, lvl + 1);
if (nlvl > r) r = nlvl
}
return r
}, 0)
}
function flattenLvl(data, lvl, clvl = 1) {
return data.reduce((r, e) => {
if (Array.isArray(e)) {
const nested = flattenLvl(e, lvl, clvl + 1);
if (clvl == lvl) r.push(...nested);
else r.push(nested)
} else {
r.push(e)
}
return r;
}, [])
}
const lvl = lastLvl(array)
const result = flattenLvl(array, lvl)
console.log(result)
You can use a recursive function that gets the array and a desired level, and then stop recurring when the level is 1, at which point you just filter out the non-array values in that (sub)array:
function extractLevel(arr, level) {
return level <= 1
? arr.filter(val => !Array.isArray(val))
: arr.filter(Array.isArray)
.flatMap(arr => extractLevel(arr, level-1));
}
// demo
const array = ["1",["2","2"],[["3","3"],["3","3"]],[[[["4","4"],"3"],[["4","4"],"3"]],[["",""],["",""]]],[["3","3"],["3","3"]],["2","2"],"1"];
// extract the values (not arrays) at level 3:
console.log(extractLevel(array, 3));

How to make an average of nested array

I'm trying to make an average array of a bigger and dynamic array. Simpler looks like this:
const bigArr = [[[1,1,1], [2,2,2], [3,3,3]],[[3,3,3], [4,4,4], [7,7,7]]]
in the end, I'm expecting to get:
const averageArray = [[2,2,2], [3,3,3], [5,5,5]]
I know the best way is to triple loop over this array's, but I couldn't manage to get expected result.
averageArray[0][0] is an average of bigArr[0][0] and bigArr[1][0].
There are a few ways to do it (for loops, reduce, etc.) here I show an example with reduce:
const bigArr = [
[[1,1,1], [2,2,2], [3,3,3]],
[[3,3,3], [4,4,4], [7,7,7]],
//[[5,5,5], [6,6,6], [11,11,11]]
];
const averageArray = bigArr.reduce((aggArr, arr, i) => {
if (i == 0){
return arr.map( a => a );
}
else {
arr.forEach( (a, j) => {
a.forEach( (b, k) => {
aggArr[j][k] = ((aggArr[j][k] * i) + b) / (i + 1)
})
});
}
return aggArr;
}, []);
console.log(averageArray);
Output:
[[2,2,2], [3,3,3], [5,5,5]]
It would also work with a larger input like this:
const bigArr = [
[[1,1,1], [2,2,2], [3,3,3]],
[[3,3,3], [4,4,4], [7,7,7]],
[[5,5,5], [6,6,6], [11,11,11]]
];
We get this output:
[[3,3,3], [4,4,4], [7,7,7]]
One final example:
It would also work with a larger input with non identical sub-arrays like this (to illustrate how the averaging is occurring):
const bigArr = [
[[1,2,3], [1,2,3], [1,2,3]],
[[3,4,7], [3,4,7], [3,4,7]],
[[5,6,11], [5,6,11], [5,6,11]]
];
We get this output:
[[3,4,7], [3,4,7], [3,4,7]]

Categories