Different logic if both or only one key present in object? - javascript

I have this kind of schema:
const schema = {
actions: {
ident: {
action: (v) => v,
path: 'some.path.key',
},
mul: {
action: (v) => v * 2,
path: 'some.other.path.key',
},
},
};
And a helper function, that takes object with keys present in schema actions, e.g:
const obj = {
ident: 1,
mul: 2,
}
const res = helper(schema, obj);
/* res */
{
some: {
path: {
key: 1,
},
other: {
path: {
key: 4,
}
}
},
}
And construct a new object with a function applied to the value.
Sometimes i need a behavior when both keys present in source object, e.g:
const schema2 = {
actions: {
ident: {
action: (v) => v,
path: 'some.path.key',
},
mul: {
action: (v) => v * 2,
path: 'some.other.path.key',
},
'mul:ident': {
action: (v1, v2) => v1/v2,
path: 'key',
}
},
};
In case like this, i need the result object to be:
const obj = {
ident: 1,
mul: 2,
}
const res = helper(schema, obj);
/* res */
{
key: 2 // 2/1 == 2
}
How can i implement such conditional logic in a good way?

I'd traverse your actions backwards, then delete the keys from the input, and skip the action if the keys are missing:
const input = { ...obj };
const output = {};
// you might want to sort the keys in the desired order first (e.g. by the number of parameters)
for(const [key, { action, path }] of Object.entries(schema.actions).reverse()) {
const keys = key.split(".");
// If one ov the values is missing, another action already consumed it
if(keys.some(key => !(key in input))
continue;
// consume all keys into values
const values = keys.map(key => {
const value = input[key];
delete input[key];
});
// TODO: assign path to output correctly
output[path] = action(...values);
}

Related

How to walk through the object/array in javascript by layers

I am converting the nested data object to the arrays, for the UI Library to show the relationship between the data.
Original
// assume that all object key is unique
{
"top":{
"test":{
"hello":"123"
},
"test2":{
"bye":"123"
"other":{
...
...
...
}
}
}
}
Preferred Result
[
{
id:"top",
parent: null,
},
{
id:"test",
parent: "top",
},
{
id:"hello",
parent: "test",
},
{
id:"test2",
parent: "top",
},
]
To do this, I write the code like this:
const test = []
const iterate = (obj, parent = null) => {
Object.keys(obj).forEach(key => {
const id = typeof obj[key] === 'object' ? key : obj[key]
const loopObj = {
id,
parent
}
test.push(loopObj)
if (typeof obj[key] === 'object') {
iterate(obj[key], id)
}
})
}
iterate(data)
console.log(test) // Done!!
It works.
However, I miss one important things, the library need the layers from the original data, to determine the type/ what function to do.
// The key name maybe duplicated in different layer
{
"top":{ // Layer 1
"test":{ // Layer 2
"hello":"123", // Layer 3
"test":"123" // Layer 3
// Maybe many many layers...
}
}
}
[
{
id:"top",
display:"0-top",
parent: null,
layer: 0
},
{
id: "1-top-test", // To prevent duplicated id, `${layer}-${parentDisplay}-${display}`
display:"test",
parent: "0-top",
parentDisplay: "top",
layer: 1
},
{
id: "3-test-test", // To prevent duplicated id,`${layer}-${parentDisplay}-${display}`
display:"test",
parent: "2-top-test",
parentDisplay: "test",
layer: 3
}
]
Editing the display or id format is very simple, just edit the function and add the field, but I don't know how to get the layer easily.
I tried to add the let count = 0 outside and do count++ when iterate function called.But I realized that it hit when the object detected, no by layers.
The original data may be very big,
So I think editing the original data structure or searching the parent id in the test[] every loop may be not a good solution.
Is there any solution to do this?
Just add the current depth as an argument that gets passed down on every recursive call (as well as the parent name).
const input = {
"top":{
"test":{
"hello":"123"
},
"test2":{
"bye":"123",
"other":{
}
}
}
};
const iterate = (obj, result = [], layer = 0, parentId = null, parentDisplay = '') => {
Object.entries(obj).forEach(([key, value]) => {
const id = `${layer}-${key}`;
result.push({
id,
display: key,
parentId,
parentDisplay,
layer,
});
if (typeof value === 'object') {
iterate(value, result, layer + 1, id, key);
}
});
return result;
}
console.log(iterate(input));
That said, your desired approach can still produce duplicate entries, if there exist two objects at the same level, with different grandparent objects, but whose parent objects use the same key, eg:
const input = {
"top1":{
"test":{
"hello":"123"
},
},
"top2": {
"test": {
"hello":"123"
}
}
};
const input = {
"top1":{
"test":{
"hello":"123"
},
},
"top2": {
"test": {
"hello":"123"
}
}
};
const iterate = (obj, result = [], layer = 0, parentId = null, parentDisplay = '') => {
Object.entries(obj).forEach(([key, value]) => {
const id = `${layer}-${key}`;
result.push({
id,
display: key,
parentId,
parentDisplay,
layer,
});
if (typeof value === 'object') {
iterate(value, result, layer + 1, id, key);
}
});
return result;
}
console.log(iterate(input));
If that's a problem, consider passing down the entire accessor string needed to access the property - eg top1.test.hello and top2.test.hello, which is guaranteed to be unique.
const input = {
"top1":{
"test":{
"hello":"123"
},
},
"top2": {
"test": {
"hello":"123"
}
}
};
const iterate = (obj, result = [], parentAccessor = '') => {
Object.entries(obj).forEach(([key, value]) => {
const accessor = `${parentAccessor}${parentAccessor ? '.' : ''}${key}`;
result.push({
id: key,
accessor,
});
if (typeof value === 'object') {
iterate(value, result, accessor);
}
});
return result;
}
console.log(iterate(input));

How to update an object value in array of objects when the keys are same

I have an Array of objects and one object
const filterArray = [{bestTimeToVisit: 'Before 10am'}, {bestDayToVisit: Monday}]
This values are setting in a reducer and the payload will be like
{bestTimeToVisit: 'After 10am'}
or
{bestDayToVisit: Tuesday}.
So what I need is when I get a payload {bestTimeToVisit: 'After 10am'} and if bestTimeToVisit not in filterList array, then add this value to the filterList array.
And if bestTimeToVisit already in the array with different value, then replace the value of that object with same key
if(filterArray.hasOwnProperty("bestTimeToVisit")) {
filterArray["bestTimeToVisit"] = payload["bestTimeToVisit"];
} else {
filterArray.push({"bestTimeToVisit": payload["bestTimeToVisit"]});
}
I convert the object array into a regular object and then back into an object array. makes things less complicated. I'm making the assumption each object coming back only has one key/value and that order doesnt matter.
const objectArraytoObject = (arr) =>
arr.reduce((acc, item) => {
const key = [Object.keys(item)[0]];
return { ...acc, [key]: item[key] };
}, {});
const newValues = [{ someKey: 'something' }, { bestDayToVisit: 'Tuesday' }];
const filterArray = [
{ bestTimeToVisit: 'Before 10am' },
{ bestDayToVisit: 'Monday' },
];
const newValuesObj = objectArraytoObject(newValues);
const filterObj = objectArraytoObject(filterArray);
const combined = { ...filterObj, ...newValuesObj };
const combinedToArray = Object.keys(combined).map((key) => ({
[key]: combined[key],
}));
console.log(combinedToArray);
Need to iterate over the array and find objects that satisfy for modification or addition if none are found.
function checkReduced(filterrray,valueToCheck="After 10am"){
let isNotFound =true;
for(let timeItem of filterrray) {
if(timeItem.bestTimeToVisit && timeItem.bestTimeToVisit !== valueToCheck) {
timeItem.bestTimeToVisit=valueToCheck;
isNotFound=false;
break;
}
}
if(isNotFound){filterrray.push({bestTimeToVisit:valueToCheck})}
}
const filterArray = [{bestDayToVisit: "Monday"}];
checkReduced(filterArray,"After 9am");//calling the function
const updateOrAdd = (arr, newItem) => {
// get the new item key
const newItemKey = Object.keys(newItem)[0];
// get the object have the same key
const find = arr.find(item => Object.keys(item).includes(newItemKey));
if(find) { // the find object is a reference type
find[newItemKey] = newItem[newItemKey]; // update the value
} else {
arr.push(newItem); // push new item if there is no object have the same key
}
return arr;
}
// tests
updateOrAdd([{ a: 1 }], { b: 2 }) // => [{ a: 1 }, { b: 2 }]
updateOrAdd([{ a: 1 }], { a: 2 }) // => [{ a: 2 }]

Dynamically spread setState doesn't work on ternary operator spread

I have a nested state like this:
this.state = {
fields: {
subject: '',
from: {
name: '',
},
},
};
In an onChange function I'm handling updates to these nested values.
I'm trying to build a dynamically spread setState() for deep nests using dot notation.
For instance, with the array: const tree = ['fields','subject'] I'm able to update the subject state value with:
this.setState(prevState => ({
[tree[0]]: {
...prevState[tree[0]],
...(tree[2] ? {
...prevState[tree[1]],
[tree[2]]: value
}
: { [tree[1]]: value })
},
}));
Since the ternary operator is ending on { [tree[1]]: value }
But when my tree array is: const tree = ['fields','from','name'] the state value for fields.from.name is not changing, where it should be resolving to the first part of the ternary operator:
{
...prevState[tree[1]],
[tree[2]]: value
}
Am I missing something?
I've grown to prefer using libraries for these sorts of functions when it otherwise feels like I'm reinventing the wheel. lodash provides a set function (which also supports string paths):
_.set(object, path, value)
var object = { 'a': [{ 'b': { 'c': 3 } }] };
_.set(object, 'a[0].b.c', 4);
console.log(object.a[0].b.c);
// => 4
_.set(object, ['x', '0', 'y', 'z'], 5);
console.log(object.x[0].y.z);
// => 5
You'd also want to use _.cloneDeep(value) because _.set mutates the object.
this.state = {
fields: {
subject: '',
from: { name: '' },
},
};
const tree = ['fields', 'from', 'name']; // or 'fields.from.name'
this.setState(prevState => {
const prevState_ = _.cloneDeep(prevState);
return _.set(prevState_, tree, value);
});
You'll need a loop. For instance:
function update(prevState, tree, value) {
const newState = {};
let obj = newState;
for (let i = 0; i < tree.length; ++i) {
const name = tree[i];
if (i === tree.length - 1) {
obj[name] = value;
} else {
obj = obj[name] = {...prevState[name]};
}
}
return newState;
}
Live Example:
this.state = {
fields: {
subject: '',
from: {
name: '',
},
},
};
function update(prevState, tree, value) {
const newState = {};
let obj = newState;
let prev = prevState;
for (let i = 0; i < tree.length; ++i) {
const name = tree[i];
if (i === tree.length - 1) {
obj[name] = value;
} else {
const nextPrev = prev[name];
obj = obj[name] = {...nextPrev};
prev = nextPrev;
}
}
return newState;
}
const tree = ['fields','from','name']
const value = "updated";
console.log(update(this.state, tree, value));
I'm sure that can be shoehorned into a call to Array#reduce (because any array operation can be), but it wouldn't buy you anything.

How can an array be grouped by two properties?

Ex:
const arr = [{
group: 1,
question: {
templateId: 100
}
}, {
group: 2,
question: {
templateId: 200
}
}, {
group: 1,
question: {
templateId: 100
}
}, {
group: 1,
question: {
templateId: 300
}
}];
Expected Result: const result = groupBy(arr, 'group', 'question.templateId');
const result = [
[{
group: 1,
question: {
templateId: 100
}
}, {
group: 1,
question: {
templateId: 100
}
}],
[{
group: 1,
question: {
templateId: 300
}
}],
[{
group: 2,
question: {
templateId: 200
}
}]
];
So far: I am able to group the result by a single property using Array.prototype.reduce().
function groupBy(arr, key) {
return [...arr.reduce((accumulator, currentValue) => {
const propVal = currentValue[key],
group = accumulator.get(propVal) || [];
group.push(currentValue);
return accumulator.set(propVal, group);
}, new Map()).values()];
}
const arr = [{
group: 1,
question: {
templateId: 100
}
}, {
group: 2,
question: {
templateId: 200
}
}, {
group: 1,
question: {
templateId: 100
}
}, {
group: 1,
question: {
templateId: 300
}
}];
const result = groupBy(arr, 'group');
console.log(result);
I would recommend to pass a callback function instead of a property name, this allows you to do the two-level-access easily:
function groupBy(arr, key) {
return Array.from(arr.reduce((accumulator, currentValue) => {
const propVal = key(currentValue),
// ^^^^ ^
group = accumulator.get(propVal) || [];
group.push(currentValue);
return accumulator.set(propVal, group);
}, new Map()).values());
}
Now you can do groupBy(arr, o => o.group) and groupBy(arr, o => o.question.templateId).
All you need to do for getting to your expected result is group by the first property and then group each result by the second property:
function concatMap(arr, fn) {
return [].concat(...arr.map(fn));
}
const result = concatMap(groupBy(arr, o => o.group), res =>
groupBy(res, o => o.question.templateId)
);
#Bergi's answer is really practical but I'll show you how building a multi-value "key" can be possible using JavaScript primitives – don't take this to mean Bergi's answer is bad in anyway; in fact, it's actually a lot better because of it's practicality. If anything, this answer exists to show you how much work is saved by using an approach like his.
I'm going to go over the code bit-by-bit and then I'll have a complete runnable demo at the end.
compound data equality
Comparing compound data in JavaScript is a little tricky, so we're gonna need to figure out a way around this first:
console.log([1,2] === [1,2]) // false
I want to cover a solution for the multi-value key because our entire answer will be based upon it - here I'm calling it a CollationKey. Our key holds some value and defines its own equality function which is used for comparing keys
const CollationKey = eq => x => ({
x,
eq: ({x: y}) => eq(x, y)
})
const myKey = CollationKey (([x1, x2], [y1, y2]) =>
x1 === y1 && x2 === y2)
const k1 = myKey([1, 2])
const k2 = myKey([1, 2])
console.log(k1.eq(k2)) // true
console.log(k2.eq(k1)) // true
const k3 = myKey([3, 4])
console.log(k1.eq(k3)) // false
wishful thinking
Now that we have a way to compare compound data, I want to make a custom reducing function that uses our multi-value key to group values. I'll call this function collateBy
// key = some function that makes our key
// reducer = some function that does our reducing
// xs = some input array
const collateBy = key => reducer => xs => {
// ...?
}
// our custom key;
// equality comparison of `group` and `question.templateId` properties
const myKey = CollationKey ((x, y) =>
x.group === y.group
&& x.question.templateId === y.question.templateId)
const result =
collateBy (myKey) // multi-value key
((group=[], x) => [...group, x]) // reducing function: (accumulator, elem)
(arr) // input array
So now that we know how we want collateBy to work, let's implement it
const collateBy = key => reducer => xs => {
return xs.reduce((acc, x) => {
const k = key(x)
return acc.set(k, reducer(acc.get(k), x))
}, Collation())
}
Collation data container
Ok, so we were being a little optimistic there too using Collation() as the starting value for the xs.reduce call. What should Collation be?
What we know:
someCollation.set accepts a CollationKey and some value, and returns a new Collation
someCollation.get accepts a CollationKey and returns some value
Well let's get to work!
const Collation = (pairs=[]) => ({
has (key) {
return pairs.some(([k, v]) => key.eq(k))
},
get (key) {
return (([k, v]=[]) => v)(
pairs.find(([k, v]) => k.eq(key))
)
},
set (key, value) {
return this.has(key)
? Collation(pairs.map(([k, v]) => k.eq(key) ? [key, value] : [k, v]))
: Collation([...pairs, [key, value]])
},
})
finishing up
So far our collateBy function returns a Collation data container which is internally implemented with an array of [key, value] pairs, but what we really want back (according to your question) is just an array of values
Let's modify collateBy in the slightest way that extracts the values – changes in bold
const collateBy = key => reducer => xs => {
return xs.reduce((acc, x) => {
let k = key(x)
return acc.set(k, reducer(acc.get(k), x))
}, Collation()).values()
}
So now we will add the values method to our Collation container
values () {
return pairs.map(([k, v]) => v)
}
runnable demo
That's everything, so let's see it all work now – I used JSON.stringify in the output so that the deeply nested objects would display all content
// data containers
const CollationKey = eq => x => ({
x,
eq: ({x: y}) => eq(x, y)
})
const Collation = (pairs=[]) => ({
has (key) {
return pairs.some(([k, v]) => key.eq(k))
},
get (key) {
return (([k, v]=[]) => v)(
pairs.find(([k, v]) => k.eq(key))
)
},
set (key, value) {
return this.has(key)
? Collation(pairs.map(([k, v]) => k.eq(key) ? [key, value] : [k, v]))
: Collation([...pairs, [key, value]])
},
values () {
return pairs.map(([k, v]) => v)
}
})
// collateBy
const collateBy = key => reducer => xs => {
return xs.reduce((acc, x) => {
const k = key(x)
return acc.set(k, reducer(acc.get(k), x))
}, Collation()).values()
}
// custom key used for your specific collation
const myKey =
CollationKey ((x, y) =>
x.group === y.group
&& x.question.templateId === y.question.templateId)
// your data
const arr = [ { group: 1, question: { templateId: 100 } }, { group: 2, question: { templateId: 200 } }, { group: 1, question: { templateId: 100 } }, { group: 1, question: { templateId: 300 } } ]
// your answer
const result =
collateBy (myKey) ((group=[], x) => [...group, x]) (arr)
console.log(result)
// [
// [
// {group:1,question:{templateId:100}},
// {group:1,question:{templateId:100}}
// ],
// [
// {group:2,question:{templateId:200}}
// ],
// [
// {group:1,question:{templateId:300}}
// ]
// ]
summary
We made a custom collation function which uses a multi-value key for grouping our collated values. This was done using nothing but JavaScript primitives and higher-order functions. We now have a way to iterate thru a data set and collate it in an arbitrary way using keys of any complexity.
If you have any questions about this, I'm happy to answer them ^_^
#Bergi's answer is great if you can hard-code the inputs.
If you want to use string inputs instead, you can use the sort() method, and walk the objects as needed.
This solution will handle any number of arguments:
function groupBy(arr) {
var arg = arguments;
return arr.sort((a, b) => {
var i, key, aval, bval;
for(i = 1 ; i < arguments.length ; i++) {
key = arguments[i].split('.');
aval = a[key[0]];
bval = b[key[0]];
key.shift();
while(key.length) { //walk the objects
aval = aval[key[0]];
bval = bval[key[0]];
key.shift();
};
if (aval < bval) return -1;
else if(aval > bval) return 1;
}
return 0;
});
}
const arr = [{
group: 1,
question: {
templateId: 100
}
}, {
group: 2,
question: {
templateId: 200
}
}, {
group: 1,
question: {
templateId: 100
}
}, {
group: 1,
question: {
templateId: 300
}
}];
const result = groupBy(arr, 'group', 'question.templateId');
console.log(result);

RxJS: Asynchronously mutate tree

I have a sequence of objects that I need to asynchronously modify by adding a property to each object:
[{ id: 1 }, { id: 2 }] => [{ id: 1, foo: 'bar' }, { id: 2, foo: 'bar' }]
The synchronous equivalent of this would be:
var xs = [{ id: 1 }, { id: 2 }];
// Warning: mutation!
xs.forEach(function (x) {
x.foo = 'bar';
});
var newXs = xs;
However, in my case I need to append the foo property asynchronously. I would like the end value to be a sequence of objects with the foo property added.
I came up with the following code to solve this problem. In this example I'm just adding a property to each object with a value of bar.
var xs = Rx.Observable.fromArray([{ id: 1 }, { id: 2 }]);
var propertyValues = xs
// Warning: mutation!
.flatMap(function (x) {
return Rx.Observable.return('bar');
});
var newXs =
.zip(propertyValues, function (x, propertyValue) {
// Append the property here
x.foo = propertyValue;
return x;
})
.toArray();
newXs.subscribe(function (y) { console.log(y); });
Is this the best way to solve my problem, or does Rx provide a better means for asynchronously mutating objects in a sequence? I'm looking for a cleaner solution because I have a deep tree that I need to mutate, and this code quickly becomes unweidly:
var xs = Rx.Observable.fromArray([{ id: 1, blocks: [ {} ] }, { id: 2, blocks: [ {} ] } ]);
var propertyValues = xs
// Warning: mutation!
.flatMap(function (x) {
return Rx.Observable.fromArray(x.blocks)
.flatMap(function (block) {
var blockS = Rx.Observable.return(block);
var propertyValues = blockS.flatMap(function (block) {
return Rx.Observable.return('bar');
});
return blockS.zip(propertyValues, function (block, propertyValue) {
block.foo = propertyValue;
return block;
});
})
.toArray();
});
xs
.zip(propertyValues, function (x, propertyValue) {
// Rewrite the property here
x.blocks = propertyValue;
return x;
})
.toArray()
.subscribe(function (newXs) { console.log(newXs); });
Perhaps I shouldn't be performing this mutation in the first place?
Is there a reason you need to create two separate Observables: one for the list you're updating and one for the resulting value?
If you simply perform a .map() over your original list, you should be able to asynchronously update the list and subscribe to the result:
// This is the function that generates the new property value
function getBlocks(x) { ... }
const updatedList$ = Rx.Observable.fromArray(originalList)
// What we're essentially doing here is scheduling work
// to be completed for each item
.map(x => Object.assign({}, x, { blocks: getBlocks(x)}))
.toArray();
// Finally we can wait for our updatedList$ observable to emit
// the modified list
updatedList$.subscribe(list => console.log(list));
To abstract this functionality, I created a helper function that will explicitly schedule work to occur for each item using setTimeout:
function asyncMap(xs, fn) {
return Rx.Observable.fromArray(xs)
.flatMap(x => {
return new Rx.Observable.create(observer => {
setTimeout(() => {
observer.onNext(fn(x));
observer.completed();
}, 0);
});
})
.toArray();
}
You can use this function to schedule work to be completed for each item:
function updateItem(x) {
return Object.assign({}, x, { blocks: getBlocks(x) }
}
var updatedList$ = asyncMap(originalList, updateItem);
updateList$.subscribe(newList => console.log(newList));

Categories