Array.reduce and recursion - javascript

I am trying to nest a recursive function in an Array.reduce call.
But the following does not work for the third instance only
const i1 = {'local': [['a','b']]};
const i2 = {'local': [['c','d']], 'recursive': []};
const i3 = {'local': [['c','d']], 'recursive': [{'local': [['e','f']]}]};
function reduce(current, result = []) {
if(current.hasOwnProperty('local'))
result.push(...current.local);
if(current.hasOwnProperty('recursive'))
result = current.recursive.reduce(reduce, result);
//'result =' should be optional, but yields a wrong answer anyway
return result;
}
console.log(reduce(i1));
console.log(reduce(i2));
console.log(reduce(i3));
So instead of calling reduce I tried the following loop
for(var i = 0; i < current.recursive.length; ++i)
result = reduce(current.recursive[i], result);
//'result = ' is optional for it is passed by reference
and it works.
Being new to JavaScript, I am certain of missing a key feature here, so could you explain ?
Output for the third instance should be
[ [ 'c', 'd' ], [ 'e', 'f' ] ]
but is
{ local: [ [ 'e', 'f' ] ] }
or
[ [ 'c', 'd' ] ]
when result = is removed.

The problem is the order of the parameters, the first argument to the reduce callback function is the accumulator not the current value. Try this:
const i1 = {'local': [['a','b']]};
const i2 = {'local': [['c','d']], 'recursive': []};
const i3 = {'local': [['c','d']], 'recursive': [{'local': [['e','f']]}]};
function reduce(result, current) {
if(current.hasOwnProperty('local'))
result.push(...current.local);
if(current.hasOwnProperty('recursive'))
result = current.recursive.reduce(reduce, result);
//'result =' should be optional, but yields a wrong answer anyway
return result;
}
console.log(reduce([], i1));
console.log(reduce([], i2));
console.log(reduce([], i3));

This only covers the cases when local and recursive only includes one element, like in your example.
const i1 = { local: [["a", "b"]] };
const i2 = { local: [["c", "d"]], recursive: [] };
const i3 = { local: [["c", "d"]], recursive: [{ local: [["e", "f"]] }] };
const i4 = {
local: [["c", "d"]],
recursive: [{ local: [["e", "f"]], recursive: [{ local: [["g", "h"]] }] }]
};
const solution = ({ local: [firstItem], recursive }) =>
[firstItem].concat(
recursive && recursive.length ? solution(recursive[0]) : []
);
console.log(solution(i1));
console.log(solution(i2));
console.log(solution(i3));
console.log(solution(i4));

I think what you want to implement is flatten - It's no coincidence that it's a simple wrapper for Array.prototype.flatMap -
const i1 =
{ local: [[1,2]] }
const i2 =
{ local: [[3,4]], recursive: [] }
const i3 =
{ local: [[3,4]], recursive: [{ local: [[5,6]] }] }
const i4 =
{ local: [[1,2]],
recursive: [
{ local: [[3,4]] },
{ local: [[5,6]] , recursive: [{ local: [[7,8]] }] }] }
const flatten = ({ local = [], recursive = [] }) =>
[ ...local, ...recursive.flatMap(flatten) ]
console.log(flatten(i1))
// [ [ 1, 2 ] ]
console.log(flatten(i2))
// [ [ 3, 4 ] ]
console.log(flatten(i3))
// [ [ 3, 4 ], [ 5, 6 ] ]
console.log(flatten(i4))
// [ [ 1, 2 ], [ 3, 4 ], [ 5, 6 ], [ 7, 8 ] ]
The spread arguments can be traded for Array.prototype.concat, if that is preferred -
const flatten = ({ local = [], recursive = [] }) =>
[ ...local, ...recursive.flatMap(flatten) ]
local.concat(recursive.flatMap(flatten))
Array.prototype.flatMap is a special kind of Array.prototype.reduce -
const flatten = ({ local = [], recursive = [] }) =>
local.concat(
recursive.reduce((r, x) => r.concat(flatten(x)), [])
)
And since Array.prototype.concat is a pure operation, we can simplify it a bit more -
const flatten = ({ local = [], recursive = [] }) =>
recursive.reduce((r, x) => r.concat(flatten(x)), local)
And finally we see it again using reduce and array spread arguments -
const flatten = ({ local = [], recursive = [] }) =>
recursive.reduce((r, x) => [...r, ...flatten(x)], local)
Each of these flatten produce the exact same outputs and the input arrays are not mutated in the process. Hopefully this gives you a little insight on how the helpful Array.prototype.flatMap works.

Related

How create array containing all combinations of array 1-n array items?

We have the following task, to convert the array below called passengerFlights (object with passenger-id-keys and array of flights) to:
(1) an array with all possible combination of passenger-flights:
EXPECTED OUTPUT:
[
["aaa", "ddd", "eee"],
["aaa", "ddd", "fff"],
["bbb", "ddd", "eee"],
["bbb", "ddd", "fff"],
["ccc", "ddd", "eee"],
["ccc", "ddd", "fff"]
]
and (2) with the stipulation that there can be any number of passengers.
The following is an attempt to solve this first as a static example of three flights, although it's not clear the best way to (1) create the array with all possible combinations, and (2) how to solve the 2-n requirement, we assume recursion of some kind.
const passengerFlights = {
777: [
{
_id: "aaa"
},
{
_id: "bbb"
},
{
_id: "ccc"
}
],
888: [
{
_id: "ddd"
}
],
999: [
{
_id: "eee"
},
{
_id: "fff"
}
],
};
const getGroupedFlights = (passengerFlights) => {
let indexPointer = 0;
const indexCounters = [0, 0, 0];
const arr = [];
while (indexCounters[0] <= passengerFlights['777'].length - 1 || indexCounters[1] <= passengerFlights['888'].length - 1 || indexCounters[2] <= passengerFlights['999'].length - 1) {
arr.push([passengerFlights['777'][0]._id, passengerFlights['888'][0]._id, passengerFlights['999'][0]._id]);
if (indexCounters[2] < passengerFlights['999'].length) indexCounters[2]++;
if (indexCounters[2] >= passengerFlights['999'].length - 1 && indexCounters[1] < passengerFlights['888'].length) indexCounters[1]++;
if (indexCounters[1] >= passengerFlights['888'].length - 1 && indexCounters[0] < passengerFlights['777'].length) indexCounters[0]++;
console.log(indexCounters, passengerFlights['888'].length - 1);
}
return arr;
}
const groupedFlights = getGroupedFlights(passengerFlights);
console.log(groupedFlights);
It's just a simple recursive problem....
const
passengerFlights =
{ 777: [ { _id: 'aaa' }, { _id: 'bbb' }, { _id: 'ccc' } ]
, 888: [ { _id: 'ddd' } ]
, 999: [ { _id: 'eee' }, { _id: 'fff' } ]
}
, result = combinations( passengerFlights, '_id' )
;
console.log( showArr(result) )
function combinations( obj, KeyName )
{
let
result = []
, keys = Object.keys(obj) // [ "777", "888", "999" ]
, max = keys.length -1
;
f_recursif_combi(0)
return result
function f_recursif_combi( level, arr = [] )
{
obj[ keys[level] ] // like :passengerFlights['777']
.forEach( elm =>
{
let arr2 = [...arr, elm[KeyName] ]; // arr + elm['_id']
(level < max)
? f_recursif_combi(level +1, arr2 )
: result.push( arr2 )
})
}
}
// ************************************ just to present result...
function showArr(Arr)
{
const r = { '[["': `[ [ '`, '","': `', '`, '"],["': `' ]\n, [ '`, '"]]': `' ]\n]` }
return JSON
.stringify(result)
.replace(/\[\[\"|\"\,\"|\"\]\,\[\"|\"\]\]/g,(x)=>r[x])
}
.as-console-wrapper {max-height: 100%!important;top:0 }
I think it's the Cartesian product of flights sets. So this should help:
Cartesian product of multiple arrays in JavaScript
As another answer suggests, you can use a basic cartesian product function. Use Object.values(passengerFlights) to pass in the array of arrays.
function *product(arrs, p = []) {
if (arrs.length == 0)
yield p
else
for (const value of arrs[0])
yield *product(arrs.slice(1), [...p, value])
}
const passengerFlights =
{777: [{_id: "aaa"},{_id: "bbb"},{_id: "ccc"}],888: [{_id: "ddd"}],999: [{_id: "eee"},{_id: "fff"}]}
for (const p of product(Object.values(passengerFlights)))
console.log(JSON.stringify(p.map(obj => obj._id)))
I used JSON.stringify for easy visualization in the demo
["aaa","ddd","eee"]
["aaa","ddd","fff"]
["bbb","ddd","eee"]
["bbb","ddd","fff"]
["ccc","ddd","eee"]
["ccc","ddd","fff"]
But for your program you will probably prefer Array.from
console.log(
Array.from(
product(Object.values(passengerFlights)),
p => p.map(obj => obj._id)
)
)
[
["aaa","ddd","eee"],
["aaa","ddd","fff"],
["bbb","ddd","eee"],
["bbb","ddd","fff"],
["ccc","ddd","eee"],
["ccc","ddd","fff"]
]
Since the order of the expected result is not important, we can make the program more efficient.
function *product(arrs) {
if (arrs.length == 0)
yield []
else
for (const p of product(arrs.slice(1)))
for (const value of arrs[0])
yield [value, ...p]
}
const passengerFlights =
{777: [{_id: "aaa"},{_id: "bbb"},{_id: "ccc"}],888: [{_id: "ddd"}],999: [{_id: "eee"},{_id: "fff"}]}
for (const p of product(Object.values(passengerFlights)))
console.log(JSON.stringify(p.map(obj => obj._id)))
["aaa","ddd","eee"]
["bbb","ddd","eee"]
["ccc","ddd","eee"]
["aaa","ddd","fff"]
["bbb","ddd","fff"]
["ccc","ddd","fff"]

How to loop inside a map() loop?

The below script outputs
================================================
Log entry ID: 1
UUID: 2
Timestamp: 3
--------------------
Log entry ID: 4
UUID: 5
Timestamp: 6
which is what I want.
Right now description is hard coded, where I would like it to be built using arr instead.
My current thinking were to somehow generate the inner array in the map() function:
[
`Log entry ID: ${element['_id']}`,
`UUID: ${element['_source'].uuid}`,
`Timestamp: ${element['_source']['#timestamp']}\n`,
]
but because of the templating, this is not even a valid array with 3 elements, when looking at it on its own. So I am all out of ideas.
Question
Somehow I have to loop over the elements in arr before it is given to map(), I suppose.
Does anyone know how that could be done?
const dedent = require('dedent');
const arr = [
[ 'Log entry ID', '_id' ],
[ 'UUID', 'uuid' ],
[ 'Timestamp', '#timestamp' ],
]
;
const docs = [
{'_id': 1,'_source': {'uuid': 2,'#timestamp': 3}},
{'_id': 4,'_source': {'uuid': 5,'#timestamp': 6}},
];
const description = dedent`
================================================
${
docs.map((element) => [
`Log entry ID: ${element['_id']}`,
`UUID: ${element['_source'].uuid}`,
`Timestamp: ${element['_source']['#timestamp']}\n`,
].join('\n')).join('--------------------\n')
}
`;
console.log(description);
Update
I control arr so changing it to eg. is possible, or something else
const arr = [
[ 'Log entry ID', '_id' ],
[ 'UUID', {'_source': 'uuid'} ],
[ 'Timestamp', {'_source': '#timestamp'} ],
]
;
Since arr is in your control perhaps you can specify the path of the key itself.
const arr = [
['Log entry ID', '_id'],
['UUID', '_source.uuid'],
['Timestamp', '_source.#timestamp'],
['Description', '_source._desc']
];
const docs = [{
'_id': 1,
'_source': {
'uuid': 2,
'#timestamp': 3,
'_desc': 'test 1'
}
},
{
'_id': 4,
'_source': {
'uuid': 5,
'#timestamp': 6,
'_desc': 'test 2'
}
},
];
const getValue = (object, keys) => keys.split('.').reduce((o, k) => (o || {})[k], object);
console.log(docs.map((element) => arr.map((label) => {
return `${label[0]}: ${getValue(element, label[1])}`
}).join('\n')).join('\n--------------------\n'))
Assuming that the keys of a docs array element are all unique, one could traverse the object looking for a matching key.
function findVal(object, key) {
var value;
Object.keys(object).some(function(k) {
if (k === key) {
value = object[k];
return true;
}
if (object[k] && typeof object[k] === 'object') {
value = findVal(object[k], key);
return value !== undefined;
}
});
return value;
}
docs.map((element) =>
arr.map(([item, key]) =>
`${item}: ${findVal(element, key)}`)
)
The FindVal is taken from here.
you could create a function to get data from arr and put in description
/* Simple Hello World in Node.js */
const arr = [
[ 'Log entry ID', '_id' ],
[ 'UUID', 'uuid' ],
[ 'Timestamp', '#timestamp' ],
]
;
const docs = [
{'_id': 1,'_source': {'uuid': 2,'#timestamp': 3}},
{'_id': 4,'_source': {'uuid': 5,'#timestamp': 6}},
];
const findInArr=function(value){
var t = ""
arr.forEach(item=>{
if (item.includes(value)) {t = item[0]}
})
return t
}
const description = dedent`
================================================
${
docs.map((element) => [
`${findInArr('_id')}: ${element['_id']}`,
`${findInArr('UUID')}: ${element['_source'].uuid}`,
`${findInArr('Timestamp')}: ${element['_source']['#timestamp']}\n`,
].join('\n')).join('--------------------\n')
}`;
console.log(description);

Merge arrays from different objects with same key

I have the following code:
const blueData = {
"items": [
{
"id": 35,
"revision": 1,
"updatedAt": "2021-09-10T14:29:54.595012Z",
},
]
}
const redData = {}
const greenData = {
"items": [
{
"id": 36,
"revision": 1,
"updatedAt": "2021-09-10T14:31:07.164368Z",
}
]
}
let colorData = []
colorData = blueData.items ? [colorData, ...blueData.items] : colorData
colorData = redData.items ? [colorData, ...redData.items] : colorData
colorData = greenData.items ? [colorData, ...greenData.items] : colorData
I am guessing the spread operator is not the right approache here as I'm getting some extra arrays in my final colorData array. I simply want to build a single array of 'items' that contains all of the 'items' from the 3 objects.
Here's a link to that code in es6 console: https://es6console.com/ktkhc3j2/
Put your data into an array then use flatMap to unwrap each .items:
[greenData, redData, blueData].flatMap(d => d.items ?? [])
//=> [ {id: 36, revision: 1, updatedAt: '2021-09-10T14:31:07.164368Z'}
//=> , {id: 35, revision: 1, updatedAt: '2021-09-10T14:29:54.595012Z'}]
If you fancy you could abstract d => d.items ?? [] with a bit of curry (no pun intended ;)
const take = k => o => o[k] ?? [];
Which gives us:
[greenData, redData, blueData].flatMap(take('items'))
We can even go a step further if you ever need to repeat this process with different keys:
const concatBy = fn => xs => xs.flatMap(x => fn(x));
Now it almost feels like you're expressing your intent with words instead of code:
const takeItems = concatBy(take('items'));
takeItems([greenData, redData, blueData]);
//=> [ {id: 36, revision: 1, updatedAt: '2021-09-10T14:31:07.164368Z'}
//=> , {id: 35, revision: 1, updatedAt: '2021-09-
Let's build another function:
const takeFood = concatBy(take('food'));
takeFood([{food: ['🥑', '🥕']}, {food: ['🌽', '🥦']}]);
//=> ['🥑', '🥕', '🌽', '🥦']
Addendum
This is only meant as a potentially useful learning material. My advice is to use flatMap.
This:
[[1, 2], [3, 4]].flatMap(x => x)
//=> [1, 2, 3, 4]
Can also be expressed with reduce. Slightly more verbose but does what it says on the tin:
[[1, 2], [3, 4]].reduce((xs, x) => xs.concat(x), [])
//=> [1, 2, 3, 4]
So to put it simply you could also do:
[greenData, redData, blueData].reduce((xs, x) => xs.concat(x.items ?? []), [])
You can do this using the Logical OR operator which lets you provide a default value if the items field is missing.
const blueData = { items: [ { id: 35, revision: 1, updatedAt: '2021-09-10T14:29:54.595012Z', }, ], };
const redData = {};
const greenData = { items: [ { id: 36, revision: 1, updatedAt: '2021-09-10T14:31:07.164368Z', }, ], };
const colorData = [
...(blueData.items || []),
...(redData.items || []),
...(greenData.items || []),
];
console.log(colorData);
Maybe I'm a little old-fashioned but I'd use concat for that:
The concat() method is used to merge two or more arrays. This method does not change the existing arrays, but instead returns a new array.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/concat
const blueData = {
"items": [
{
"id": 35,
"revision": 1,
"updatedAt": "2021-09-10T14:29:54.595012Z",
},
]
}
const redData = {}
const greenData = {
"items": [
{
"id": 36,
"revision": 1,
"updatedAt": "2021-09-10T14:31:07.164368Z",
}
]
}
const colorData = [].concat(blueData.items,redData.items,greenData.items).filter(x => x)
console.log(colorData)
the last filter is for removing undefined values
Like this?
colorData = blueData.items ? [...colorData, ...blueData.items] : colorData
colorData = redData.items ? [...colorData, ...redData.items] : colorData
colorData = greenData.items ? [...colorData, ...greenData.items] : colorData
Output:
[{"id":35,"revision":1,"updatedAt":"2021-09-10T14:29:54.595012Z"},
{"id":36,"revision":1,"updatedAt":"2021-09-10T14:31:07.164368Z"}]
I think you need to add the spread operator also to the colorData array, because if not you are adding the colorData array itself, not its items.
If you want the simplest solution, you can iterate with a for-loop between all arrays. Create a temporary array that will store data found on each index. This is the fastest and the most flexible solution.
var x1 = {
"items": [
{ "testKey1": "testVal" }
]
};
var x2 = {
"items": [
{ "testKey2.0": "testVal2" },
{ "testKey2.1": "testVal2" },
{ "testKey2.2": "testVal2" },
]
};
var x3 = {
"items": [
{ "testKey3.0": "testVal3" },
{ "testKey3.1": "testVal3" }
]
};
function combineArrays(...arrays) {
var tempArray = [];
for (let index in arrays) {
let currentArray = arrays[index];
for (let innerArrayIndex in currentArray) {
tempArray.push(currentArray[innerArrayIndex]);
}
}
return tempArray;
}
var result = combineArrays(x1.items, x2.items, x3.items);
console.log(result);
The solutions using a spread operator do not take into consideration that all the objects will be cloned using a shallow copy. Have a look.

Lift a key’s values to top level whilst preserving rest of object using Ramda

I would like to build it into my compose function so that record’s values go to the top level of the object, whilst the rest of the keys stay in tact:
{
record: {
seasons: [
1
],
colors: [
2
]
},
tag_ids: [
2091
]
}
The result I am after:
{
seasons: [
1
],
colors: [
2
],
tag_ids: [
2091
]
}
Any of the keys may or may not exist.
I have always scratched my head with the ramda way to do it in a compose function. Currently I am looking at toPairs and doing some pretty long winded transforms with no luck.
This may be simpler in plain JS rather than Ramda:
const data = { record: { seasons: [1], colors: [2] }, tag_ids: [2091] }
const flattenRecord = ({record = {}, ...rest}) => ({...record, ...rest})
flattenRecord(data) //=> {"colors": [2], "seasons": [1], "tag_ids": [2091]}
If you would still like to utilise Ramda for the solution, consider looking into R.mergeLeft (or R.mergeRight) and R.omit.
You can use R.chain with R.merge and R.prop to flatten a key's content by merging it with the original object, and then you can omit the original key.
const { pipe, chain, merge, prop, omit } = R
const fn = key => pipe(
chain(merge, prop(key)), // flatten the key's content
omit([key]) // remove the key
)
const data = { record: { seasons: [1], colors: [2] }, tag_ids: [2091] }
const result = fn('record')(data)
console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.0/ramda.js"></script>
You can use the spread operator.
const startingWithAllProps = {
record: {
seasons: [
1
],
colors: [
2
]
},
tag_ids: [
2091
]
}
const startingWithoutRecord = {
tag_ids: [
2091
]
}
const startingWithoutTagIds = {
record: {
seasons: [
1
],
colors: [
2
]
}
}
const moveRecordUpOneLevel = (startingObject) => {
const temp = {
...startingObject.record,
tag_ids: startingObject.tag_ids
}
return JSON.parse(JSON.stringify(temp)) // To remove any undefined props
}
const afterTransformWithAllProps = moveRecordUpOneLevel(startingWithAllProps)
const afterTransformWithoutRecord = moveRecordUpOneLevel(startingWithoutRecord)
const afterTransformWithoutTagIds = moveRecordUpOneLevel(startingWithoutTagIds)
console.log('afterTransformWithAllProps', afterTransformWithAllProps)
console.log('afterTransformWithoutRecord', afterTransformWithoutRecord)
console.log('afterTransformWithoutTagIds', afterTransformWithoutTagIds)
This might help too!
const lift = key => R.converge(R.mergeRight, [
R.dissoc(key),
R.prop(key),
]);
const liftRecord = lift('record');
// ====
const data = {
record: {
seasons: [
1
],
colors: [
2
]
},
tag_ids: [
2091
]
};
console.log(
liftRecord(data),
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.0/ramda.js" integrity="sha256-buL0byPvI/XRDFscnSc/e0q+sLA65O9y+rbF+0O/4FE=" crossorigin="anonymous"></script>

Functional way to get previous element during map

I have an array which I map over. I need to compare the current element with the previous. I am detecting if the current element is the same as the previous element by comparing their ids and doing something different based on this condition. Is there any purely functional way to do it without doing index math?
items.map((item, index) => {
if(item.id === items[index - 1 > 0 ? index - 1 : 0].id) {
// do something
} else {
// do something else
}
})
The code works but I would like to avoid doing math on the index. Is there any way to do it?
The reduce() function provides a functional what you need:
items.reduce((previousValue, currentValue) => {
if(currentValue.id === previousValue.id) {
// do something
} else {
// do something else
}
});
Are you sure that you want a map? This sounds like an XY problem. If you want to map over adjacent elements of an array then you'd have to define your own function.
const mapAdjacent = (mapping, array) => {
const {length} = array, size = length - 1, result = new Array(size);
for (let i = 0; i < size; i++) result[i] = mapping(array[i], array[i + 1]);
return result;
};
const items = [1, 2, 3, 4, 5];
const result = mapAdjacent((x, y) => [x, y], items);
console.log(result); // [[1, 2], [2, 3], [3, 4], [4, 5]]
Note that this will throw a RangeError if you give it an empty array as input.
const mapAdjacent = (mapping, array) => {
const {length} = array, size = length - 1, result = new Array(size);
for (let i = 0; i < size; i++) result[i] = mapping(array[i], array[i + 1]);
return result;
};
const items = [];
const result = mapAdjacent((x, y) => [x, y], items); // RangeError: Invalid array length
console.log(result);
I think this is good behaviour because you shouldn't be giving mapAdjacent an empty array to begin with.
Here's a purely functional implementation of mapAdjacent which uses reduceRight. As an added bonus, it works for any iterable object.
const mapAdjacent = (mapping, [head, ...tail]) =>
tail.reduceRight((recur, item) => prev =>
[mapping(prev, item), ...recur(item)]
, _ => [])(head);
const items = "hello";
const result = mapAdjacent((x, y) => [x, y], items);
console.log(result); // [['h', 'e'], ['e', 'l'], ['l', 'l'], ['l', 'o']]
Unlike the iterative version, it returns an empty array instead of throwing an error if you give it an empty array as input.
const mapAdjacent = (mapping, [head, ...tail]) =>
tail.reduceRight((recur, item) => prev =>
[mapping(prev, item), ...recur(item)]
, _ => [])(head);
const items = "";
const result = mapAdjacent((x, y) => [x, y], items);
console.log(result); // []
Note that this is an unintended side effect of array destructuring with rest elements in JavaScript. The equivalent Haskell version does raise an exception.
mapAdjacent :: (a -> a -> b) -> [a] -> [b]
mapAdjacent f (x:xs) = foldr (\y g x -> f x y : g y) (const []) xs x
main :: IO ()
main = do
print $ mapAdjacent (,) "hello" -- [('h','e'),('e','l'),('l','l'),('l','o')]
print $ mapAdjacent (,) "" -- Exception: Non-exhaustive patterns in function mapAdjacent
However, returning an empty array might be desirable for this function. It's equivalent to adding the mapAdjacent f [] = [] case in Haskell.
Not a particularly fast implementation, but destructuring assignment makes it particularly elegant -
const None =
Symbol ()
const mapAdjacent = (f, [ a = None, b = None, ...more ] = []) =>
a === None || b === None
? []
: [ f (a, b), ...mapAdjacent (f, [ b, ...more ]) ]
const pair = (a, b) =>
[ a, b ]
console.log(mapAdjacent(pair, [ 1, 2, 3 ]))
// [ [ 1, 2 ], [ 2, 3 ] ]
console.log(mapAdjacent(pair, "hello"))
// [ [ h, e ], [ e, l ], [ l, l ], [ l, o ] ]
console.log(mapAdjacent(pair, [ 1 ]))
// []
console.log(mapAdjacent(pair, []))
// []
Or write it as a generator -
const mapAdjacent = function* (f, iter = [])
{ while (iter.length > 1)
{ yield f (...iter.slice(0,2))
iter = iter.slice(1)
}
}
const pair = (a, b) =>
[ a, b ]
console.log(Array.from(mapAdjacent(pair, [ 1, 2, 3 ])))
// [ [ 1, 2 ], [ 2, 3 ] ]
console.log(Array.from(mapAdjacent(pair, "hello")))
// [ [ h, e ], [ e, l ], [ l, l ], [ l, o ] ]
console.log(Array.from(mapAdjacent(pair, [ 1 ])))
// []
console.log(Array.from(mapAdjacent(pair, [])))
// []
As I mentioned in a comment, I would suggest using reduce. Here is an example:
const input = [
{id: 1, value: "Apple Turnover"},
{id: 1, value: "Apple Turnover"},
{id: 2, value: "Banana Bread"},
{id: 3, value: "Chocolate"},
{id: 3, value: "Chocolate"},
{id: 3, value: "Chocolate"},
{id: 1, value: "Apple"},
{id: 4, value: "Danish"},
];
// Desired output: Array of strings equal to values in the above array,
// but with a prefix string of "New: " or "Repeated: " depending on whether
// the id is repeated or not
const reducer = (accumulator, currentValue) => {
let previousValue, descriptions, isRepeatedFromPrevious;
if (accumulator) {
previousValue = accumulator.previousValue;
descriptions = accumulator.descriptions;
isRepeatedFromPrevious = previousValue.id === currentValue.id;
} else {
descriptions = [];
isRepeatedFromPrevious = false;
}
if (isRepeatedFromPrevious) {
// The following line is not purely functional and performs a mutation,
// but maybe we do not care because the mutated object did not exist
// before this reducer ran.
descriptions.push("Repeated: " + currentValue.value);
} else {
// Again, this line is mutative
descriptions.push("New: " + currentValue.value);
}
return { previousValue: currentValue, descriptions }
};
const output = input.reduce(reducer, null).descriptions;
document.getElementById('output').innerText = JSON.stringify(output);
<output id=output></output>

Categories

Resources