Comparing partial objects in ramda.js - javascript

There is an equals function in Ramdajs which is totally awesome, it will provide the following:
// (1) true
R.equals({ id: 3}, { id: 3})
// (2) true
R.equals({ id: 3, name: 'freddy'}, { id: 3, name: 'freddy'})
// (3) false
R.equals({ id: 3, name: 'freddy'}, { id: 3, name: 'freddy', additional: 'item'});
How would I go about enhancing this function, or in some other way produce a true result for number 3
I would like to ignore all the properties of the rValue not present in the lValue, but faithfully compare the rest. I would prefer the recursive nature of equals remain intact - if that's possible.
I made a simple fiddle that shows the results above.

There's a constraint on equals in order to play nicely with the Fantasy Land spec that requires the symmetry of equals(a, b) === equals(b, a) to hold, so to satisfy your case we'll need to get the objects into some equivalent shape for comparison.
We can achieve this by creating a new version of the second object that has had all properties removed that don't exist in the first object.
const intersectObj = (a, b) => pick(keys(a), b)
// or if you prefer the point-free edition
const intersectObj_ = useWith(pick, [keys, identity])
const a = { id: 3, name: 'freddy' },
b = { id: 3, name: 'freddy', additional: 'item'}
intersectObj(a, b) // {"id": 3, "name": "freddy"}
Using this, we can now compare both objects according to the properties that exist in the first object a.
const partialEq = (a, b) => equals(a, intersectObj(a, b))
// again, if you prefer it point-free
const partialEq_ = converge(equals, [identity, intersectObj])
partialEq({ id: 3, person: { name: 'freddy' } },
{ id: 3, person: { name: 'freddy' }, additional: 'item'})
//=> true
partialEq({ id: 3, person: { name: 'freddy' } },
{ id: 3, person: { age: 15 }, additional: 'item'})
//=> false

Use whereEq
From the docs: "Takes a spec object and a test object; returns true if the test satisfies the spec, false otherwise."
whereEq({ id: 3, name: 'freddy' }, { id: 3, name: 'freddy', additional: 'item' })
The other way around is to develop your own version. It boils down to:
if (is object):
check all keys - recursive
otherwise:
compare using `equals`
This is recursive point-free version that handles deep objects, arrays and non-object values.
const { equals, identity, ifElse, is, mapObjIndexed, useWith, where } = R
const partialEquals = ifElse(
is(Object),
useWith(where, [
mapObjIndexed(x => partialEquals(x)),
identity,
]),
equals,
)
console.log(partialEquals({ id: 3 }, { id: 3 }))
console.log(partialEquals({ id: 3, name: 'freddy' }, { id: 3, name: 'freddy' }))
console.log(partialEquals({ id: 3, name: 'freddy' }, { id: 3, name: 'freddy', additional: 'item' }))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js"></script>

I haven't used Ramda.js before so if there's something wrong in my answer please be free to point out.
I learned the source code of Ramda.js
In src/equals.js, is where the function you use is defined.
var _curry2 = require('./internal/_curry2');
var _equals = require('./internal/_equals');
module.exports = _curry2(function equals(a, b) {
return _equals(a, b, [], []);
});
So it simply put the function equals (internally, called _equals) into the "curry".
So let's check out the internal _equals function, it did check the length in the line 84~86:
if (keysA.length !== keys(b).length) {
return false;
}
Just comment these lines it will be true as you wish.
You can 1) just comment these 3 lines in the distributed version of Ramda, or 2) you can add your own partialEquals function to it then re-build and create your version of Ramda (which is more recommended, from my point of view). If you need any help about that, don't hesitate to discuss with me. :)

This can also be accomplished by whereEq
R.findIndex(R.whereEq({id:3}))([{id:9}{id:8}{id:3}{id:7}])

Related

Given two array create another one with only different element

I have two array:
for example:
arraySelectedItems = [{id: 1, name: "item1"}, {id: 2, name: "item2"}]
arraySavedItems = [{id: 1, name: "item1"}, {id: 2, name: "item2"}]
now I need to check if there is some item in arraySavedItems that is not present in arraySelectedItems, and in this case I'll go to populate another array called arrayDeletedItems.
If the two arrays have the same items I don't need to populate the arrayDeletedItems.
So I have tried with this code:
arraySavedItems.filter((itemSaved) => !arraySelectedItems.find((itemSel) => {
if (itemSaved.id !== itemSel.id) {
arrayDeletedItems.push(itemSaved)
}
}
))
So with this data:
arraySelectedItems = [{id: 1, name: "item1"}, {id: 2, name: "item2"}]
arraySavedItems = [{id: 1, name: "item1"}, {id: 2, name: "item2"}]
I'll expect that arrayDeletedItems will be:
arrayDeletedItems = []
Instead whit this data for example:
arraySelectedItems = [{id: 1, name: "item1"}]
arraySavedItems = [{id: 1, name: "item1"}, {id: 2, name: "item2"}]
I'll expect that arrayDeletedItems will be:
arrayDeletedItems = [{id: 2, name: "item2"}]
With my code I receive and arrayDeletedItems that has the all values:
arrayDeletedItems = [{id: 1, name: "item1"}, {id: 2, name: "item2"}]
Consider this generic function:
function difference(a, b, keyFn) {
let keys = new Set(a.map(keyFn))
return b.filter(obj => !keys.has(keyFn(obj)))
}
//
selectedItems = [{id: 1, name: "item1"}, {id:4}]
savedItems = [{id: 1, name: "item1"}, {id: 2, name: "item2"}, {id:3}, {id:4}]
result = difference(selectedItems, savedItems, obj => obj.id)
console.log(result)
You can use the .includes() method on an array to check whether a value is contained in it (see the documentation for more information).
Now we can just filter the array of saved items to find only ones that aren't contained by the selected items array.
arrayDeletedItems = arraySavedItems.filter((itemSaved) =>
!arraySelectedItems.includes(itemSaved)
)
As #owenizedd points out in the comments, this only works for primitive data types where a shallow equality check is sufficient. A more robust approach can be used with the .reduce() method and a custom equality check. For example, lodash's isEqual() does a deep comparison for equality. You would have to import the module for this. Unfortunately there is no native deep equality check in JavaScript currently (workarounds like JSON.stringify() to then compare the string representations have various downsides).
arrayDeletedItems = arraySavedItems.filter((itemSaved) =>
!arraySelectedItems.reduce((previous, current) =>
previous || _.isEqual(current, itemSaved)
)
)
Note that passing previous as the first argument to the 'or' operator (||) means we can benefit from lazy evaluation - once a hit has been found, the second half of the statement does not need to be evaluated any more.
To solve this problem, since we have id we can utilize it.
You need a key that is unique. so id commonly known will have unique value.
So my approach, find items that is not exist in B array but in A array, and find items that exist in B but not in A array.
This approach not be the fastest, but the findDiff is reusable.
const a = [....];
const b = [....];
const findDiff = (source, target) => {
return source.filter((sourceItem, index) => {
const isInTarget = target.findIndex(targetItem => targetItem.id === sourceItem.id)
return isInTarget === -1
})
}
const difference = findDiff(a,b).concat(findDiff(b,a)); //result

transform an array of objects with map( )

I can't understand how the map () method works because all the examples are with numbers and to understand I need an example with something more specific.
so I made this
I have an array of objects:
let people = [
{
id: 1,
name: 'jhon',
last_name: 'wilson'
},
{
id: 2,
name: 'maria',
last_name: 'anyway'
},
{
id: 3,
name: 'lastOne',
last_name: 'example'
}
];
I want to understand how with people.map(); i can change the idk, name?? of the 2nd element.
this is how i think map() work:
people.map(() => {
people[1].name = prompt()
// At this point i don't know how continue
})
I'm studying on my own, so I will be very grateful to you :)
The .map() function will go through the entire array, and on each step of that process it will take the current item that we are looking at and will pass it as a parameter into the function. You can then do whatever you want to that item, and whatever you return from your function will replace what is in that position in the array.
Say for example, with the array you gave in your question, we wanted to remove the name and last_name properties, and combine them into a full_name property. We can do the following:
let people = [
{
id: 1,
name: 'jhon',
last_name: 'wilson'
},
{
id: 2,
name: 'maria',
last_name: 'anyway'
},
id: 3,
name: 'lastOne',
last_name: 'example'
}
];
people = people.map((person) => {
return {
id: person.id,
full_name: `${person.name} ${person.last_name}`
}
});
After this code runs, our people array would look like this:
[
{
id: 1,
full_name: 'jhon wilson'
},
{
id: 2,
full_name: 'maria anyway'
},
id: 3,
name: 'lastOne example'
}
];
You can think of it as doing something very similar to this:
function transformPerson(person) {
return {
id: person.id,
full_name: `${person.name} ${person.last_name}`
}
}
let newPeople = [];
for (let i = 0; i < people.length; i++) {
newPeople[i] = transformPerson(people[i])
}
people = newPeople;
Array.map() takes in a function as a parameter, passes each item of the array into the function, and returns an array of the result.
For example, if I wanted to multiply each of the items in the array by 2:
const x = [1, 2, 3, 4, 5]
const y = x.map(v => v * 2) // result: [2, 4, 6, 8, 10]
Note: Array.map does not affect the original array; it creates a new array of the results.
You could change your code to
let people = [{id:1,name:'john',last_name:'wilson'},{id:2,name:'maria',last_name:'anyway'},{id:3,name:'lastOne',last_name:'example'}];
people = people.map((p,i) =>({...p,name: i===1?prompt("New name"):p.name}))
console.log(people);
This will prompt the user only for a new name when i===1. The expression will create a new array that will be stored under the variable name people again. If you wanted people to remain unchanged you could assign the return value of the people.map()-call to a different variable (or constant).

How to use JavaScript spread... syntax to change the one field of the object, that belongs to the array and is accessed by name-value pair?

Here is the code (it fails to compile at the sentence that builds the state2, i.e. at the second spread):
let line_id = 6;
let state = {
invoice: {
id: 1015,
description: 'web order',
},
lines: [
{id: 5, description: 'phone', color: 'black'},
{id: 6, description: 'tablet', color: 'blue'},
{id: 7, description: 'computer', color: 'gray'},
]
};
//this alert and this access pattern works, so, I would like to use
//.find... to access element in spread... structure as well
//alert(state['lines'].find(line=>line['id']==line_id)['description']);
let state2 = {
...state,
['lines']: { ...state['lines'],
find(line=>line['id']==line_id): { ...state['lines'].find(line=>line['id']==line_id),
['description']: 'TV',
},
},
};
alert(state2['lines'].find(line=>line['id']==line_id)['description']);
I have state structure, I access lines array, I access the specific line by name-value pair id=6 and I would like to change the value of the field description. This effort is the continuation of https://stackoverflow.com/a/64116308/1375882 in which I am trying to create the general procedure, that use the spread... syntax and the access-by-name strategy for updating the complex object/array tree. In fact - this complex tree is the state of the Redux reducer and that update happend in the action that process the valueSetter function of the AgGrid. But - this is generally the interesting exercise by itself to better understand spread... and JavaScript and JSON structure in JavaScript.
So - the only question is: how to write line
find(line=>line['id']==line_id): { ...state['lines'].find(line=>line['id']==line_id),
so that the code compiles? How can I access the certain element of the array by name-value pair in this setting:
Note, that I am trying to build general code:
find(line=>line[keyFieldName]==keyFieldValue): { ...state['lines'].find(line=>line[keyFieldName]==keyFieldValue),
that uses arbitrary field names and field values - so that such handler can update the any field of the any record of arbitrary 2D AgGrid in React/Redux setting.
The desired result of my code: 1) it should compile; 2) the second alert should return 'TV'.
If I understood correctly what you want to achieve, this should work:
let line_id = 6;
let state = {
invoice: {
id: 1015,
description: 'web order',
},
lines: [{
id: 5,
description: 'phone',
color: 'black'
},
{
id: 6,
description: 'tablet',
color: 'blue'
},
{
id: 7,
description: 'computer',
color: 'gray'
},
]
};
const stateKeyId = 'lines';
const itemKeyId = 'id';
const itemAttr = 'description'
let state2 = {
...state,
[stateKeyId]: state[stateKeyId].map(item => {
if (item[itemKeyId] == line_id) {
return ({
...item,
[itemAttr]: 'TV'
});
}
return item
})
}
console.log(state2);
find(line=>line['id']==line_id) should become [find(line=>line['id']==line_id)], since just like the string it must be between square brackets for js to work properly.
Also, if you are using find from lodash, it will return the object, therefore if you need to use the id as key you can do something like:
[get(find(line => line['id'] === line_id]), 'id')]: whatever
a few observations though:
always please always use === over == in js
avoid snake_case, use camelCase with js, since it's standard
your code is not actually handling missing items correclty, if you need to do so split it in multiple lines since it would be more comprehensible
You can use the map method from arrays to return different elements based on the original one.
Here's how you could use it:
line_id = 6;
state = {
invoice: {
id: 1015,
description: 'web order',
},
lines: [
{id: 5, description: 'phone', color: 'black'},
{id: 6, description: 'tablet', color: 'blue'},
{id: 7, description: 'computer', color: 'gray'},
]
};
state2 = {
...state,
lines: state.lines.map(line => {
if (line.id === line_id)
return { ...line, description: 'YT' }
return { ...line }
})
};
alert(state2['lines'].find(line=>line['id']==line_id)['description']);

Recursive .reduce() to output an array of parents

I have an flat array of Folders like this one :
const foldersArray = [{id: "1", parentId: null, name: "folder1"}, {id: "2", parentId: null, name: "folder2"}, {id: "1.1", parentId: 1, name: "folder1.1"}, {id: "1.1.1", parentId: "1.1", name: "folder1.1.1"},{id: "2.1", parentId: 2, name: "folder2.1"}]
I want to output an array of all parents of a given folder to generate a Breadcrumb-like component of Folder path.
I have presently a code that does what I need but I'd like to write it better in a more "functional" way, using reduce recursively.
If I do :
getFolderParents(folder){
return this.foldersArray.reduce((all, item) => {
if (item.id === folder.parentId) {
all.push (item.name)
this.getFolderParents(item)
}
return all
}, [])
}
and I log the output, I can see it successfully finds the first Parent, then reexecute the code, and outputs the parent's parent... as my initial array is logically reset to [] at each step... Can't find a way around though...
You could do this with a Map so you avoid iterating over the array each time you need to retrieve the next parent. This way you get an O(n) instead of an O(n²) time complexity:
const foldersArray = [{id: "1", parentId: null, name: "folder1"}, {id: "2", parentId: null, name: "folder2"}, {id: "1.1", parentId: "1", name: "folder1.1"}, {id: "1.1.1", parentId: "1.1", name: "folder1.1.1"},{id: "2.1", parentId: "2", name: "folder2.1"}];
const folderMap = new Map(foldersArray.map( o => [o.id, o] ));
const getFolderParents = folder =>
(folder.parentId ? getFolderParents(folderMap.get(folder.parentId)) : [])
.concat(folder.name);
// Example call:
console.log(getFolderParents(foldersArray[4]));
Just a minor remark: your parentId data type is not consistent: it better be always a string, just like the data type of the id property. If not, you need to cast it in your code, but it is really better to have the data type right from the start. You'll notice that I have defined parentId as a string consistently: this is needed for the above code to work. Alternatively, cast it to string in the code with String(folder.parentId).
Secondly, the above code will pre-pend the parent folder name (like is done in file folder notations). If you need to append the parent name after the child, then swap the concat subject and argument:
[folder.name].concat(folder.parentId ? getFolderParents(folderMap.get(folder.parentId)) : []);
You can do what you're looking for with a rather ugly looking while loop. Gets the job done though. Each loop iteration filters, looking for an instance of a parent. If that doesn't exist, it stops and exits. If it does exist, it pushes that parent into the tree array, sets folder to its parent to move up a level, then moves on to the next iteration.
const foldersArray = [{
id: "1",
parentId: null,
name: "folder1"
}, {
id: "2",
parentId: null,
name: "folder2"
}, {
id: "1.1",
parentId: 1,
name: "folder1.1"
}, {
id: "1.1.1",
parentId: "1.1",
name: "folder1.1.1"
}, {
id: "2.1",
parentId: 2,
name: "folder2.1"
}]
function getParents(folder){
const tree = [], storeFolder = folder
let parentFolder
while((parentFolder = foldersArray.filter(t => t.id == folder.parentId)[0]) !== undefined){
tree.push(parentFolder)
folder = parentFolder
}
console.log({ originalFolder: storeFolder, parentTree: tree})
}
getParents(foldersArray[3])
You're thinking about it in a backwards way. You have a single folder as input and you wish to expand it to a breadcrumb list of many folders. This is actually the opposite of reduce which takes as input many values, and returns a single value.
Reduce is also known as fold, and the reverse of a fold is unfold. unfold accepts a looping function f and an init state. Our function is given loop controllers next which add value to the output and specifies the next state, and done which signals the end of the loop.
const unfold = (f, init) =>
f ( (value, nextState) => [ value, ...unfold (f, nextState) ]
, () => []
, init
)
const range = (m, n) =>
unfold
( (next, done, state) =>
state > n
? done ()
: next ( state // value to add to output
, state + 1 // next state
)
, m // initial state
)
console.log (range (3, 10))
// [ 3, 4, 5, 6, 7, 8, 9, 10 ]
Above, we start with an initial state of a number, m in this case. Just like the accumulator variable in reduce, you can specify any initial state to unfold. Below, we express your program using unfold. We add parent to make it easy to select a folder's parent
const parent = ({ parentId }) =>
data .find (f => f.id === String (parentId))
const breadcrumb = folder =>
unfold
( (next, done, f) =>
f == null
? done ()
: next ( f // add folder to output
, parent (f) // loop with parent folder
)
, folder // init state
)
breadcrumb (data[3])
// [ { id: '1.1.1', parentId: '1.1', name: 'folder1.1.1' }
// , { id: '1.1', parentId: 1, name: 'folder1.1' }
// , { id: '1', parentId: null, name: 'folder1' } ]
breadcrumb (data[4])
// [ { id: '2.1', parentId: 2, name: 'folder2.1' }
// , { id: '2', parentId: null, name: 'folder2' } ]
breadcrumb (data[0])
// [ { id: '1', parentId: null, name: 'folder1' } ]
You can verify the results of the program below
const data =
[ {id: "1", parentId: null, name: "folder1"}
, {id: "2", parentId: null, name: "folder2"}
, {id: "1.1", parentId: 1, name: "folder1.1"}
, {id: "1.1.1", parentId: "1.1", name: "folder1.1.1"}
, {id: "2.1", parentId: 2, name: "folder2.1"}
]
const unfold = (f, init) =>
f ( (value, state) => [ value, ...unfold (f, state) ]
, () => []
, init
)
const parent = ({ parentId }) =>
data .find (f => f.id === String (parentId))
const breadcrumb = folder =>
unfold
( (next, done, f) =>
f == null
? done ()
: next ( f // add folder to output
, parent (f) // loop with parent folder
)
, folder // init state
)
console.log (breadcrumb (data[3]))
// [ { id: '1.1.1', parentId: '1.1', name: 'folder1.1.1' }
// , { id: '1.1', parentId: 1, name: 'folder1.1' }
// , { id: '1', parentId: null, name: 'folder1' } ]
console.log (breadcrumb (data[4]))
// [ { id: '2.1', parentId: 2, name: 'folder2.1' }
// , { id: '2', parentId: null, name: 'folder2' } ]
console.log (breadcrumb (data[0]))
// [ { id: '1', parentId: null, name: 'folder1' } ]
If you trace the computation above, you see that find is called once per folder f added to the outupt in the unfolding process. This is an expensive operation, and if your data set is significantly large, could be a problem for you.
A better solution would be to create an additional representation of your data that has a structure better suited for this type of query. If all you do is create a Map of f.id -> f, you can decrease lookup time from linear to logarithmic.
unfold is really powerful and suited for a wide variety of problems. I have many other answers relying on it in various ways. There's even some dealing with asynchrony in there, too.
If you get stuck, don't hesitate to ask follow-up questions :D

How to prevent lodash mapKeys from reordering my array?

I'm using lodash mapKeys to take my array of objects and convert it to a mapped object using the id property. That's simple enough, but the problem is that it's sorting the new object by id.
For example if I had three objects in my array:
let myArray = [
{
id: 3,
name: 'Number Three'
},
{
id: 1,
name: 'Number One'
},
{
id: 2,
name: 'Number Two'
}
];
Then I map the keys by id:
_.mapKeys(myArray, 'id')
It returns the following:
{
1: {
id: 1,
name: 'Number One'
},
2: {
id: 2,
name: 'Number Two'
},
3: {
id: 3,
name: 'Number Three'
}
}
My server returns the array in a specific order, so I would like the objects to remain the same, so that when I loop over the object properties, they are in the correct order.
Is that possible with this method? If not, is there a possible alternative to achieve the results?
Use a Map because each item has a custom key (like objects), but the order of insertion will be the order of iteration (like arrays):
const myArray = [
{
id: 3,
name: 'Number Three'
},
{
id: 1,
name: 'Number One'
},
{
id: 2,
name: 'Number Two'
}
];
const map = myArray.reduce((map, item) => map.set(item.id, item), new Map());
map.forEach((item) => console.log(item));
As pointed out in the comments, looping over an object doesn't guarantee order. If you want an ordered list, you need an array.
However, you could apply the iterator pattern. In this pattern, it's up to you to decide what “next” element is. So, you could have a set with the objects (in order to get them in constant time) and an array to store the order. To iterate, you'd use the iterator.
This code could be used as example.
Hope it helps.
let myArray = [{
id: 3,
name: 'Number Three'
}, {
id: 1,
name: 'Number One'
}, {
id: 2,
name: 'Number Two'
}];
let myIterator = ((arr) => {
let mySet = _.mapKeys(arr, 'id'),
index = 0,
myOrder = _.map(arr, _.property('id'));
return {
getObjById: (id) => mySet[id],
next: () => mySet[myOrder[index++]],
hasNext: () => index < myOrder.length
};
})(myArray);
// Access elements by id in constant time.
console.log(myIterator.getObjById(1));
// Preserve the order that you got from your server.
while (myIterator.hasNext()) {
console.log(myIterator.next());
}
<script src="https://cdn.jsdelivr.net/lodash/4.16.6/lodash.min.js"></script>
Like mentioned in the comments, the best would be to keep the object references both in an array to keep the order and in a hash to ease updating.
Backbone's collection (source) works like this. It keeps objects in an array (models), but automatically updates a hash (_byId) when adding and removing models (objects) or when a model's id changes.
Here's a simple implementation of the concept. You could make your own implementation or check for a collection lib.
// a little setup
var array = [];
var hash = {};
var addObject = function addObject(obj) {
hash[obj.id] = obj;
array.push(obj);
}
// Create/insert the objects once
addObject({ id: 3, name: 'Number Three' });
addObject({ id: 1, name: 'Number One' });
addObject({ id: 2, name: 'Number Two' });
// Easy access by id
console.log("by id with hash", hash['1']);
// updating is persistent with the object in the array
hash['1'].name += " test";
// keeps the original ordering
for (var i = 0; i < 3; ++i) {
console.log("iterating", i, array[i]);
}

Categories