Use pipe filter functions with arguments in RamdaJS - ramda.js

Say I got these two filters:
const getNamesStartingWith = (letter) => persons => {
return persons.filter(person => person.name.startsWith(letter);
}
const getPersonsStartingWithZipCode = (zipCode) => persons => {
return persons.filter(person => person.address.zipCode.startsWith(zipCode);
}
I want to make a generic pipe filter.
const pipeFilter = (funcArr, args) => arr => {
...
}
The funcArr is an array of the functions to run. The args is a double array where the index is the arguments of the functions.
So with my example functions it would be:
const pipeFilter = ([getNamesStartingWith, getPersonsStartingWithZipCode], [['J'], [4]]) => persons => {..}
The argument for getNamesStartingWith is J. The argument for getPersonsStartingWithZipCode is 4
If I would to it 'manually' I would to something like:
export const doPipeFiltering = (funcArr: any[], args: any[]) => (arr) => {
return funcArr.reduce((acc, func, index) => {
let filterdArr;
if (index === 0) {
filterdArr = func(...args[index])(arr);
} else {
filterdArr = func(...args[index])(acc);
}
acc = filterdArr;
return acc;
}, []);
};
Works. But I would like to do it in RamdaJs so I can use all neat functions there.
I don't find how can apply arguments for differet filter functions in Ramda. Is it possible?

You can definitely make this a fair bit cleaner using Ramda functions. Here's one approach:
const doPipeFiltering = curry ( (funcArr, args, arr) =>
reduce ( (acc, func) => func (acc), arr, zipWith (apply, funcArr, args) ))
const getNamesStartingWith = (letter) => (persons) => {
return persons.filter(person => person.name.startsWith(letter))
}
const getPersonsStartingWithZipCode = (zipCode) => persons => {
return persons.filter(person => person.address.zipCode.startsWith(zipCode))
}
const people = [
{name: 'Amanda', address: {zipCode: '86753'}},
{name: 'Aaron', address: {zipCode: '09867'}},
{name: 'Amelia', address: {zipCode: '85309'}},
{name: 'Bob', address: {zipCode: '67530'}}
]
console .log (
doPipeFiltering (
[getNamesStartingWith, getPersonsStartingWithZipCode],
[['A'], ['8']],
people
)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {curry, reduce, zipWith, apply} = R </script>
But I would suggest that this is not a very ergonomic API. First of all, if the filtering functions had an entirely generic interface, it might not be so bad, but now you have to supply two separate array parameters, whose values must be synchronized. That to me is a recipe for disaster.
Secondly, what we have to create for this is functions that filter a list. I would find it much cleaner if the code handled the filtering, and we only supplied a set of simple predicates.
So this is an alternative suggestion, for an API that I think is much cleaner:
const runFilters = useWith (filter, [allPass, identity] )
// or one of these
// const runFilter = curry((filters, xs) => filter(allPass(filters), xs))
// const runFilters = curry ((filters, xs) => reduce ((a, f) => filter (f, a), xs, filters))
const nameStartsWith = (letter) => (person) => person.name.startsWith (letter)
const zipStartsWith = (digits) => (person) => person.address.zipCode.startsWith (digits)
const myFilter = runFilters ([nameStartsWith ('A'), zipStartsWith ('8')])
const people = [
{name: 'Amanda', address: {zipCode: '86753'}},
{name: 'Aaron', address: {zipCode: '09867'}},
{name: 'Amelia', address: {zipCode: '85309'}},
{name: 'Bob', address: {zipCode: '67530'}}
]
console .log (
myFilter (people)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {useWith, filter, allPass, identity} = R </script>
Note that here the main function is simpler, the filter predicates are simpler, and the call is much more explicit. Especially improved is the readability of nameStartsWith ('A') and zipStartsWith ('8')

Related

Item filtering but keeping track of filtered out items

Let's say I have a list of items like below and I would like to apply a list of filters onto it with ramda.
const data = [
{id: 1, name: "Andreas"},
{id: 2, name: "Antonio"},
{id: 3, name: "Bernhard"},
{id: 4, name: "Carlos"}
]
No biggie: pipe(filter(predA), filter(predB), ...)(data)
The tricky part is I would like to define my filters with a key for tracking what items have been filtered out by which filter.
const filterBy = (key, pred) => subs => {
const [res, rej] = partition(pred, subs)
return [{[key]: rej.map(prop('id'))}, res]
}
This all screams monad chaining or a transducer, but I can't get my head around it how to put it all together.
Let's say I have a 2 predicates:
const isEven = filterBy('id', i => i % 2 === 0)
const startsWithA = filterBy('name', startsWith('A'))
I would like to get a result that looks like this tuple with a rejection map and a list of "accepted" items (isEven threw out 1 and 3 and startsWithA rejected 3 and 4):
[
{
id: [1, 3],
name: [3, 4]
},
[{id: 2, name: "Antonio"}]
]
Vanilla JS version
I'm bothered by using the field name to describe the predicate. What happens if we also have, say, const nameTooLong = ({name}) => name .length < 8. Then how could we distinguish the two predicates in the output? So I would prefer to use descriptive predicate names, for instance,
[
{isEven: [1, 3], startsWithA: [3, 4]},
[{id: 2, name: "Antonio"}]
]
So that's what I do in this code:
const process = (preds) => (xs) => {
const rej = Object .fromEntries (Object .entries (preds)
.map (([k, v]) => [k, xs .filter (x => !v (x)) .map (x => x .id)])
)
const excluded = Object .values (rej) .flat()
return [rej, data .filter (({id}) => !excluded .includes (id))]
}
const data = [{id: 1, name: "Andreas"}, {id: 2, name: "Antonio"}, {id: 3, name: "Bernhard"}, {id: 4, name: "Carlos"}]
console .log (process ({
isEven: ({id}) => id % 2 === 0,
startsWithA: ({name}) => name .startsWith ('A')
}) (data))
.as-console-wrapper {max-height: 100% !important; top: 0}
It would not be overly difficult to alter this to return something like your requested format.
Using Ramda
The question was tagged Ramda, and I wrote this initially using Ramda tools, with a version that looks like this:
const process = (preds) => (xs) => {
const rej = pipe (map (flip (reject) (xs)), map (pluck ('id'))) (preds)
const excluded = uniq (flatten (values (rej)))
return [rej, reject (pipe (prop ('id'), flip (includes) (excluded))) (data)]
}
And we could continue to hack away at this until we made it entirely point-free. I just don't see any reason for that.
I'm a founder of Ramda and a big fan, but I don't see this as any more readable than the vanilla version. There is one exception: Ramda's map working on a plain object is much nicer than the Object .entries -> map -> Object .fromEntries dance in the vanilla code. I might use that feature and leave the rest in vanilla, though.
Ok so after some fiddling I came up with this kind of solution. Implementing a new monad seemed unnecessary and overwriting fantasy-land/filter was also a bad idea, as my predicates are basically tagged.
This seems to have a good mix of readability and returns basically an extended array for further processing.
class Partition extends Array {
constructor(items, filtered = {}) {
super(...items)
this.filtered = filtered
}
filterWithKey = (key, pred) => {
const [ok, notOk] = partition(pred, this.slice())
const filtered = mergeDeepWith(concat, this.filtered, {[key]: notOk})
return new Partition(ok, filtered)
}
filter = pred => this.filterWithKey("", pred)
}
const res = new Partition([
{id: 1, name: "Andreas"},
{id: 2, name: "Antonio"},
{id: 3, name: "Bernhard"},
{id: 4, name: "Carlos"}
])
.filterWithKey('id', ({id}) => id % 2 === 0)
.filterWithKey('name', ({name}) => name.startsWith('A'))
const toIds = map(prop('id'))
const rejected = map(toIds, res.filtered)
const accepted = [...res]
console.log(rejected, accepted)

Pick fields from an object and return one field as an array

Using Ramda, I am trying to achieve something like this:
let data = {
'accountNumber' : '12345',
'holderName' : 'XYZ',
'id' : '12XX',
'type' : 'savings',
...rest
}
let newObj = R.pick(['accountNumber', 'id', 'type']) (data);
output -> newObj = {
'accountNumber' : '12345',
'id' : '12XX',
'type' : 'savings'
}
I want result like this:
newObj = {
'accountNumbers' : ['12345'],
'id' : '12XX',
'type' : 'savings'
}
I want accountNumber field to be put inside an array of accountNumbers.
I understand pick won't work in this case but I am not able to achieve it through Ramda. It can be done easily using JavaScript but is it possible to achieve via Ramda?
You can pick the properties, and create a new object with changed keys using R.applySpec():
const { applySpec, pipe, of, prop } = R
const fn = applySpec({
accountNumbers: pipe(prop('accountNumber'), of),
holderName: prop('holderName'),
id: prop('id')
})
const data = {
'accountNumber': '12345',
'holderName': 'XYZ',
'id': '12XX',
'type': 'savings',
}
const result = fn(data)
console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.28.0/ramda.min.js" integrity="sha512-t0vPcE8ynwIFovsylwUuLPIbdhDj6fav2prN9fEu/VYBupsmrmk9x43Hvnt+Mgn2h5YPSJOk7PMo9zIeGedD1A==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
Another option is to add accountNumbers using R.applySpec (based on this answer), and then pick the properties, including the updated one from the object:
const { pipe, applySpec, chain, mergeLeft, prop, of, pick } = R
const updateObject = pipe(applySpec, chain(mergeLeft))
const addAccountNumbers = updateObject({
accountNumbers: pipe(prop('accountNumber'), of)
})
const fn = pipe(addAccountNumbers, pick(['accountNumbers', 'holderName', 'id']))
const data = {
'accountNumber': '12345',
'holderName': 'XYZ',
'id': '12XX',
'type': 'savings',
}
const result = fn(data)
console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.28.0/ramda.min.js" integrity="sha512-t0vPcE8ynwIFovsylwUuLPIbdhDj6fav2prN9fEu/VYBupsmrmk9x43Hvnt+Mgn2h5YPSJOk7PMo9zIeGedD1A==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

Replace value of key using ramda.js

I have the following array of objects:
const originalArray = [
{name: 'name1', value: 10},
{name: 'name2', value: 20}
]
And the following object
names = {
name1: 'generic_name_1',
name2: 'generic_name_2'
}
I would like the first array to be transformed like this:
[
{name: 'generic_name_1', value: 10},
{name: 'generic_name_2', value: 20}
]
What I have tried so far:
const replaceName = (names, obj) => {
if(obj['name'] in names){
obj['name'] = names[obj['name']];
}
return obj;
}
const modifiedArray = R.map(replaceName(names), originalArray)
Is there a more ramda-ish way to do this?
Using native JS inside Ramda functions is not unramdaish. The only problem in your code is that you mutate the original object - obj['name'] = names[obj['name']];.
I would use R.when to check if the name exists in the names object, and if it does evolve the object to the new name. If it doesn't the original object would be returned.
const { flip, has, prop, map, when, pipe, evolve } = R
const hasProp = flip(has)
const getProp = flip(prop)
const fn = names => map(when(
pipe(prop('name'), hasProp(names)),
evolve({
name: getProp(names)
})
))
const originalArray = [{"name":"name1","value":10},{"name":"name2","value":20},{"name":"name3","value":30}]
const names = {"name1":"generic_name_1","name2":"generic_name_2"}
const result = fn(names)(originalArray)
console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.0/ramda.js"></script>
I wouldn't use any Ramda functions for this. I would simply avoid mutating the original, perhaps with code like this:
const transform = (names) => (arr) => arr .map (
({name, ... rest}) => ({name: names [name] || name, ... rest})
)
const originalArray = [{name: 'name1', value: 10},{name: 'name2', value: 20}]
const names = {name1: 'generic_name_1',name2: 'generic_name_2'}
console .log (
transform (names) (originalArray)
)

Ramdajs, group array with arguments

List to group:
const arr = [
{
"Global Id": "1231",
"TypeID": "FD1",
"Size": 160,
"Flöde": 55,
},
{
"Global Id": "5433",
"TypeID": "FD1",
"Size": 160,
"Flöde": 100,
},
{
"Global Id": "50433",
"TypeID": "FD1",
"Size": 120,
"Flöde": 100,
},
{
"Global Id": "452",
"TypeID": "FD2",
"Size": 120,
"Flöde": 100,
},
]
Input to function which specifies what keys to group:
const columns = [
{
"dataField": "TypeID",
"summarize": false,
},
{
"dataField": "Size",
"summarize": false,
},
{
"dataField": "Flöde",
"summarize": true,
},
]
Expected output:
const output = [
{
"TypeID": "FD1",
"Size": 160,
"Flöde": 155 // 55 + 100
"nrOfItems": 2
},
{
"TypeID": "FD1",
"Size": 120,
"Flöde": 100,
"nrOfItems": 1
},
{
"TypeID": "FD2",
"Size": 120,
"Flöde": 100,
"nrOfItems": 1
}
]
// nrOfItems adds up 4. 2 + 1 +1. The totalt nr of items.
Function:
const groupArr = (columns) => R.pipe(...);
The "summarize" property tells if the property should summarize or not.
The dataset is very large, +100k items. So I don't want to iterate more than necessary.
I've looked at the R.group but I'm not sure it can be applied here?
Maybe something with R.reduce? Store the group in the accumulator, summarize values and add to count if the group already exists? Need to find the group fast so maybe store the group as a key?
Or is it better to use vanilla javascript in this case?
Here's an answer in vanilla javascipt first, because I'm not super familiar with the Ramda API. I'm pretty sure the approach is the quite similar with Ramda.
The code has comments explaining every step. I'll try to follow up with a rewrite to Ramda.
const arr=[{"Global Id":"1231",TypeID:"FD1",Size:160,"Flöde":55},{"Global Id":"5433",TypeID:"FD1",Size:160,"Flöde":100},{"Global Id":"50433",TypeID:"FD1",Size:120,"Flöde":100},{"Global Id":"452",TypeID:"FD2",Size:120,"Flöde":100}],columns=[{dataField:"TypeID",summarize:!1},{dataField:"Size",summarize:!1},{dataField:"Flöde",summarize:!0}];
// The columns that don't summarize
// give us the keys we need to group on
const groupKeys = columns
.filter(c => c.summarize === false)
.map(g => g.dataField);
// We compose a hash function that create
// a hash out of all the items' properties
// that are in our groupKeys
const groupHash = groupKeys
.map(k => x => x[k])
.reduce(
(f, g) => x => `${f(x)}___${g(x)}`,
() => "GROUPKEY"
);
// The columns that summarize tell us which
// properties to sum for the items within the
// same group
const sumKeys = columns
.filter(c => c.summarize === true)
.map(c => c.dataField);
// Again, we compose in to a single function.
// This function concats two items, taking the
// "last" item with only applying the sum
// logic for keys in concatKeys
const concats = sumKeys
.reduce(
(f, k) => (a, b) => Object.assign(f(a, b), {
[k]: (a[k] || 0) + b[k]
}),
(a, b) => Object.assign({}, a, b)
)
// Now, we take our data and group by the groupHash
const groups = arr.reduce(
(groups, x) => {
const k = groupHash(x);
if (!groups[k]) groups[k] = [x];
else groups[k].push(x);
return groups;
},
{}
);
// These are the keys we want our final objects to have...
const allKeys = ["nrTotal"]
.concat(groupKeys)
.concat(sumKeys);
// ...baked in to a helper to remove other keys
const cleanKeys = obj => Object.assign(
...allKeys.map(k => ({ [k]: obj[k] }))
);
// With the items neatly grouped, we can reduce each
// group using the composed concatenator
const items = Object
.values(groups)
.flatMap(
xs => cleanKeys(
xs.reduce(concats, { nrTotal: xs.length })
),
);
console.log(items);
Here's an attempt at porting to Ramda, but I didn't get much further than replacing the vanilla js methods with the Ramda equivalents. Curious to see which cool utilities and functional concepts I missed! I'm sure somebody more knowledgable on the Ramda specifics will chime in!
const arr=[{"Global Id":"1231",TypeID:"FD1",Size:160,"Flöde":55},{"Global Id":"5433",TypeID:"FD1",Size:160,"Flöde":100},{"Global Id":"50433",TypeID:"FD1",Size:120,"Flöde":100},{"Global Id":"452",TypeID:"FD2",Size:120,"Flöde":100}],columns=[{dataField:"TypeID",summarize:!1},{dataField:"Size",summarize:!1},{dataField:"Flöde",summarize:!0}];
const [ sumCols, groupCols ] = R.partition(
R.prop("summarize"),
columns
);
const groupKeys = R.pluck("dataField", groupCols);
const sumKeys = R.pluck("dataField", sumCols);
const grouper = R.reduce(
(f, g) => x => `${f(x)}___${g(x)}`,
R.always("GROUPKEY"),
R.map(R.prop, groupKeys)
);
const reducer = R.reduce(
(f, k) => (a, b) => R.mergeRight(
f(a, b),
{ [k]: (a[k] || 0) + b[k] }
),
R.mergeRight,
sumKeys
);
const allowedKeys = new Set(
[ "nrTotal" ].concat(sumKeys).concat(groupKeys)
);
const cleanKeys = R.pipe(
R.toPairs,
R.filter(([k, v]) => allowedKeys.has(k)),
R.fromPairs
);
const items = R.flatten(
R.values(
R.map(
xs => cleanKeys(
R.reduce(
reducer,
{ nrTotal: xs.length },
xs
)
),
R.groupBy(grouper, arr)
)
)
);
console.log(items);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>
Here's my initial approach. Everything but summarize is a helper function which I suppose could be inlined if you really wanted. I find it cleaner with this separation.
const getKeys = (val) => pipe (
filter (propEq ('summarize', val) ),
pluck ('dataField')
)
const keyMaker = (columns, keys = getKeys (false) (columns)) => pipe (
pick (keys),
JSON .stringify
)
const makeReducer = (
columns,
toSum = getKeys (true) (columns),
toInclude = getKeys (false) (columns),
) => (a, b) => ({
...mergeAll (map (k => ({ [k]: b[k] }), toInclude ) ),
...mergeAll (map (k => ({ [k]: (a[k] || 0) + b[k] }), toSum ) ),
nrOfItems: (a .nrOfItems || 0) + 1
})
const summarize = (columns) => pipe (
groupBy (keyMaker (columns) ),
values,
map (reduce (makeReducer (columns), {} ))
)
const arr = [{"Flöde": 55, "Global Id": "1231", "Size": 160, "TypeID": "FD1"}, {"Flöde": 100, "Global Id": "5433", "Size": 160, "TypeID": "FD1"}, {"Flöde": 100, "Global Id": "50433", "Size": 120, "TypeID": "FD1"}, {"Flöde": 100, "Global Id": "452", "Size": 120, "TypeID": "FD2"}]
const columns = [{"dataField": "TypeID", "summarize": false}, {"dataField": "Size", "summarize": false}, {"dataField": "Flöde", "summarize": true}]
console .log (
summarize (columns) (arr)
)
<script src="https://bundle.run/ramda#0.26.1"></script><script>
const {pipe, filter, propEq, pluck, pick, mergeAll, map, groupBy, values, reduce} = ramda</script>
There is a lot of overlap with the solution from Joe, but also some real differences. His was already posted when I saw the question, but I wanted my own approach not to be influenced, so I didn't look until I wrote the above. Note the difference in our hash functions. Mine does JSON.stringify on values like {TypeID: "FD1", Size: 160} while Joe's creates "GROUPKEY___FD1___160". I think I like mine better for the simplicity. On the other hand, Joe's solution is definitely better than mine in handling nrOfItems. I updated it on each reduce iteration and have to use an || 0 to handle the initial case. Joe simply starts the fold with the already-known value. But overall, the solutions are quite similar.
You mention wanting to reduce the number of passes through the data. The way I write Ramda code tends not to help with this. This code iterates the whole list to group it into like items then iterates through each of those groups to fold down to individual values. (Also there is a perhaps a minor iteration in values.) These could certainly be changed to combine those two iterations. It might even make for shorter code. But to my mind, it would become harder to understand.
Update
I was curious about the single-pass approach, and found that I could use all the infrastructure I built for the multi-pass one, rewriting only the main function:
const summarize2 = (columns) => (
arr,
makeKey = keyMaker (columns),
reducer = makeReducer (columns)
) => values (reduce (
(a, item, key = makeKey (item) ) => assoc (key, reducer (key in a ? a[key]: {}, item), a),
{},
arr
))
console .log (
summarize2 (columns) (arr)
)
I wouldn't choose this over the original unless testing showed that this code was a bottleneck in my application. But it's not as much more complex as I thought it would be, and it does everything in one iteration (well, except for whatever values does.) Interestingly, it makes me change my mind a bit about the handling of nrOfItems. My helper code just worked in this version, and I never had to know the total size of the group. That wouldn't have happened if I used Joe's approach.

Ramda js maximum elements

I wonder how will be the best way to get max elements from array.
For example I have regions with temperaturs:
let regions = [{name: 'alabama', temp: 20}, {name: 'newyork', temp: 30}...];
It can be done with one line but I want to be performant.
I want to iterate over the array only once.
If more than 1 region has the same max temperature i want to get them all
Do you know a way to make it with more compact code than procedure code with temporary variables and so on.
If it can be done in "functional programming" way it will be very good.
This is sample procedure code:
regions = [{name:'asd', temp: 13},{name: 'fdg', temp: 30}, {name: 'asdsd', temp: 30}]
maxes = []
max = 0
for (let reg of regions) {
if (reg.temp > max) {
maxes = [reg];
max = reg.temp
} else if (reg.temp == max) {
maxes.push(reg)
} else {
maxes =[]
}
}
Another Ramda approach:
const {reduce, append} = R
const regions = [{name:'asd', temp: 13},{name: 'fdg', temp: 30}, {name: 'asdsd', temp: 30}]
const maxTemps = reduce(
(tops, curr) => curr.temp > tops[0].temp ? [curr] : curr.temp === tops[0].temp ? append(curr, tops) : tops,
[{temp: -Infinity}]
)
console.log(maxTemps(regions))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.js"></script>
This version only iterates the list once. But it's a bit ugly.
I would usually prefer the version from Ori Drori unless testing shows that the performance is a problem in my application. Even with the fix from my comment, I think that code is easier to understand than this one. (That wouldn't be true if there were only two cases. (< versus >= for instance.) But when there are three, this gets hard to read, however we might format it.
But if performance is really a major issue, then your original code is probably faster than this one too.
Use R.pipe to
Group the objects by temp's value,
Convert the object of groups to an array of pairs
Reduce the pairs to the one with the max key (the temp)
return the value from the pair
const { pipe, groupBy, prop, toPairs, reduce, maxBy, head, last } = R;
const regions = [
{name: 'california', temp: 30},
{name: 'alabama', temp: 20},
{name: 'newyork', temp: 30}
];
const result = pipe(
groupBy(prop('temp')),
toPairs,
reduce(maxBy(pipe(head, Number)), [-Infinity]),
last
)(regions);
console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.js"></script>
A different approach to this (albeit a little more verbose) is to create some helpers to generically take care of folding over a list of things to extract the list of maximums.
We can do this by defining a Semigroup wrapper class (could also be a plain function instead of a class).
const MaxManyBy = fn => class MaxMany {
constructor(values) {
this.values = values
}
concat(other) {
const otherValue = fn(other.values[0]),
thisValue = fn(this.values[0])
return otherValue > thisValue ? other
: otherValue < thisValue ? this
: new MaxMany(this.values.concat(other.values))
}
static of(x) {
return new MaxMany([x])
}
}
The main purpose of this class is to be able to combine two lists by comparing the values contained within, with the invariant that each list contains the same comparable values.
We now can introduce a new helper function which applies some function to each value of a list and then combines them all using concat.
const foldMap = (fn, [x, ...xs]) =>
xs.reduce((acc, next) => acc.concat(fn(next)), fn(x))
With these helpers, we can now create a function that pulls the maximum temperatures from your example.
const maxTemps = xs =>
foldMap(MaxManyBy(({temp}) => temp).of, xs).values
maxTemps([
{name: 'california', temp: 30},
{name: 'alabama', temp: 20},
{name: 'newyork', temp: 30}
])
//=> [{"name": "california", "temp": 30}, {"name": "newyork", "temp": 30}]
There is an assumption here that the list being passed to foldMap is non-empty. If there's a chance that you'll encounter an empty list then you will need to modify accordingly to return a default value of some kind (or wrap it in a Maybe type if no sane default exists).
See the complete snippet below.
const MaxManyBy = fn => class MaxMany {
constructor(values) {
this.values = values
}
concat(other) {
const otherValue = fn(other.values[0]),
thisValue = fn(this.values[0])
return otherValue > thisValue ? other
: otherValue < thisValue ? this
: new MaxMany(this.values.concat(other.values))
}
static of(x) {
return new MaxMany([x])
}
}
const foldMap = (fn, [x, ...xs]) =>
xs.reduce((acc, next) => acc.concat(fn(next)), fn(x))
const maxTemps = xs =>
foldMap(MaxManyBy(({temp}) => temp).of, xs).values
const regions = [
{name: 'california', temp: 30},
{name: 'alabama', temp: 20},
{name: 'newyork', temp: 30}
]
console.log(maxTemps(regions))