Reputation: 509
I'm struggling a bit with implementing a variant groupBy
that would allow grouping by multiple properties in a point-free style. (I'm using typescript & ramda).
I want to group some elements say of type A
by properties returned from function getProperties :: A a -> [b]
. In imperative paradigm the implementation could look like that:
const getProperties = (item: Item): Array<keyof Item> => [item.x, item.y.z];
const groupByMany = (items: Item[]): Dictionary<Item[]> => {
let groupped: Dictionary<Item[]> = {};
for (let item of items) {
for (let key of getProperties(item)) {
if (groupped[key]) {
groupped[key].push(item);
} else {
groupped[key] = [item];
}
}
}
}
Example:
const items = [
{ i: 1, x: 'A', y: { z: 'B' } },
{ i: 2, x: 'A' },
{ i: 3, x: 'B', y: { z: 'B' } },
];
const expectedOutput = {
A: [ { i: 1, ... }, { i: 2, ... }],
B: [ { i: 1, ... }, { i: 3, ... }],
};
Upvotes: 2
Views: 160
Reputation: 50797
I couldn't tell from the question whether you wanted something that made it easy for you to code point-free or if for some reason you were looking for an actual point-free implementation. If it's the latter, then I'm afraid this will be no help. But it's a fairly simple
const groupByMany = (fn) => (xs) =>
xs .reduce
( (a, x) => [...new Set ( fn(x) )] . reduce
( (a, k) => k ? {...a, [k]: [... (a [k] || []), x ] } : a
, a
)
, {}
)
// const getProperties = (item) => [path(['x'], item), path(['y', 'z'], item)]
const getProperties = juxt ( [path (['x']), path (['y', 'z']) ] )
const items = [{ i: 1, x: 'A', y: { z: 'B' } }, { i: 2, x: 'A'}, { i: 3, x: 'B', y: { z: 'B' } }]
console .log
( groupByMany (getProperties) (items)
)
<script src="https://bundle.run/[email protected]"></script></script>
<script>const { juxt, path } = ramda </script>
Running the keys through [... new Set ( fn(x) ) ]
is just a quick way to eliminate duplicates from the array returned by fn (x)
. The rest of the function should be pretty clear.
Upvotes: 0
Reputation: 135237
I'll get you started -
const reduce = (f, init, xs) =>
xs .reduce (f, init)
const upsert = (m, k, v) =>
m .has (k)
? m .get (k) .push (v)
: m .set (k, [ v ])
const groupByMany = (f, xs) =>
reduce
( (m, x) =>
( f (x) .forEach (k => k && upsert (m, k, x))
, m
)
, new Map
, xs
)
const items =
[ { i: 1, x: 'A', y: { z: 'B' } }
, { i: 2, x: 'A' }
, { i: 3, x: 'B', y: { z: 'B' } }
]
const result =
groupByMany
( item => [ item.x, item.y && item.y.z ]
, items
)
console.log(Object.fromEntries(result.entries()))
Notice how the last item has a B
for .x
and .y.z
so it get's inserted into the B
group twice. We change upsert
so it will not insert a duplicate value -
const upsert = (m, k, v) =>
m .has (k)
? m .get (k) .includes (v)
? m
: m .get (k) .push (v)
: m .set (k, [ v ])
Expand the snippet below to see the final result in your own browser -
const reduce = (f, init, xs) =>
xs .reduce (f, init)
const upsert = (m, k, v) =>
m .has (k)
? m .get (k) .includes (v)
? m
: m .get (k) .push (v)
: m .set (k, [ v ])
const groupByMany = (f, xs) =>
reduce
( (m, x) =>
( f (x) .forEach (k => k && upsert (m, k, x))
, m
)
, new Map
, xs
)
const items =
[ { i: 1, x: 'A', y: { z: 'B' } }
, { i: 2, x: 'A' }
, { i: 3, x: 'B', y: { z: 'B' } }
]
const result =
groupByMany
( item => [ item.x, item.y && item.y.z ]
, items
)
console.log(Object.fromEntries(result.entries()))
A note on SO's peculiar output: SO will not display the same object twice, instead it will give an object a reference, and print that reference where the duplicate object would appear. For example /**id:3**/
in the program's output -
{
"A": [
{
/**id:3**/
"i": 1,
"x": "A",
"y": {
"z": "B"
}
},
{
"i": 2,
"x": "A"
}
],
"B": [
/**ref:3**/,
{
"i": 3,
"x": "B",
"y": {
"z": "B"
}
}
]
}
Which matches your expected output -
const expectedOutput = {
A: [ { i: 1, ... }, { i: 2, ... }],
B: [ { i: 1, ... }, { i: 3, ... }],
};
It's not point-free like you asked for, but I only said I'd get you started ...
Upvotes: 3