maintain key format when copying values from one object to another - javascript

The following code demonstrates an issue that has surfaced while writing an app using the Vue framework for the front end. The issue is really a JS one though.
Here is the data object needed by the Vue component:
let data = { accountId: '', prospectId: '', address: '', city: '', state: '' }
and here is an object containing a row of data from the database:
const retrieved = {
"ProspectID": "4",
"AccountID": "1003",
"Address": "E2828 Highway 14",
"City": "Madison",
"State": "WI",
"Created": "2021-02-27 11:49:33.523",
"Updated": "2021-02-27 11:49:33.523"
}
It is necessary to copy some of the values from retrieved into data. The current way of doing the copy is the following:
data.accountId = retrieved.AccountID;
data.prospectId = retrieved.ProspectID;
data.address = retrieved.Address;
data.city = retrieved.City;
data.state = retrieved.State;
console.log('data', data);
The result of the above code is the desired outcome and it looks like this:
I'm looking for a more efficient way to do the copying because it's tedious when there are many key/value pairs involved.
I've tried this:
data = { ...data, ...retrieved };
console.log('data', data);
which results in this
which basically unions all the key/value pairs together. Not the desired outcome.
It is critical that the key names in data keep their exact names and no extra key/value pairs get added to data. How can this be achieved?

Since the capitalization is different, spread won't work. You'll have to iterate over an array mapping the properties on the different objects to each other:
const propsToCopy = {
// data // retrieved
accountId: 'AccountID',
prospectId: 'ProspectID',
// ...
};
for (const [dataProp, retrievedProp] of Object.entries(propsToCopy)) {
data[dataProp] = retrieved[retrievedProp];
}
That said, having slightly different property names for the same data like this seems very strange, since it makes the code a lot more convoluted than it needs to be and greatly increases the risk of typo-based problems, when a property is capitalized but doesn't need to be, or vice-versa. Consider if you can use just a single property name format instead, if at all possible; then the propsToCopy could be reduced to an array:
const propsToCopy = ['accountId', 'prospectId', /* ... */ ];

You can use a Proxy in order to intercept all settings of values. This allows only setting known values and ignoring anything else.
To make the setting of the property preserve the case, we can just lookup the original key case-insensitively and use the original one.
Finally, using Object.assign() will call all the setters on the target which means that the proxy can intercept these calls:
const eqCaseInsensitive = a => b =>
a.toLowerCase() === b.toLowerCase();
const handler = {
set(target, prop, value, receiver) {
const key = Object.keys(target)
.find(eqCaseInsensitive(prop));
if (key !== undefined) {
return Reflect.set(target, key, value, receiver);
}
return true;
}
}
let data = { accountId: '', prospectId: '', address: '', city: '', state: '' }
const retrieved = {
"ProspectID": "4",
"AccountID": "1003",
"Address": "E2828 Highway 14",
"City": "Madison",
"State": "WI",
"Created": "2021-02-27 11:49:33.523",
"Updated": "2021-02-27 11:49:33.523"
}
Object.assign(new Proxy(data, handler), retrieved);
console.log(data);
This can further be converted to a helper function that is analogous to Object.assign() and allow for as many sources as you wish. To save some processing time, there is no need to do a full search for each property assignment - a simple lookup map can be precomputed that holds lowercase property names as keys and normal case property names as values:
const assignOnlyKnownProps = (target, ...sources) => {
const known = new Map(
Object.keys(target)
.map(key => [key.toLowerCase(), key])
);
const handler = {
set(target, prop, value, receiver) {
const lookup = prop.toLowerCase();
if (known.has(lookup)) {
Reflect.set(target, known.get(lookup), value, receiver);
}
return true;
}
}
return Object.assign(new Proxy(target, handler), ...sources);
}
let data = { accountId: '', prospectId: '', address: '', city: '', state: '' }
const retrieved = {
"ProspectID": "4",
"AccountID": "1003",
"Address": "E2828 Highway 14",
"City": "Madison",
"State": "WI",
"Created": "2021-02-27 11:49:33.523",
"Updated": "2021-02-27 11:49:33.523"
}
assignOnlyKnownProps(data, retrieved);
console.log(data);

Creating a copy of retrieved and converting all the key names to lowercase, then populating the values for data works -
const r = Object.fromEntries(
Object.entries(retrieved).map(([k, v]) => [k.toLowerCase(), v])
);
// Object.keys(data).forEach(key => {
// data[key] = r[key.toLowerCase()];
// });
Object.keys(data).map(k => data[k] = r[k.toLowerCase()]);
console.log('r', r);
console.log('data', data);

Related

Retrieve multidimensional JSON data where index number is randomized/unknown

Been delivered some confusing JSON data with a problem I haven't seen before.
The JSON is formatted similar to this structure:
[
{
"title": "Event",
"start_date": "2022-08-20 15:00:00",
"end_date": "2022-08-20 16:00:00",
"branch": {
"85": "branchname"
},
"room": {
"156": "roomname"
},
"age_group": {
"5": "Youth",
"6": "Teen"
}
},
{
"title": "Event02",
"start_date": "2022-08-20 15:00:00",
"end_date": "2022-08-20 16:00:00",
"branch": {
"72": "branchname"
},
"room": {
"104": "roomname02"
},
"age_group": {
"5": "Youth",
"6": "Teen"
}
}
]
I'm trying to pull roomname out of the data, but it's nested in an object that has a random index number. If I manually put in the index number, I can retrieve the data, but the number changes every entry.
If I can figure out how to retrieve the number and store it in a variable, then use it again, or just somehow wildcard to just show any child of any key under the parent node "room" it would work perfect, but I don't know of a way to do this in javascript.
I'm limited to vanilla javascript, no external libraries or jquery.
here is the code that will output correctly if I manually enter the index numbers, but it only works for a single entry.
<script>
const url = 'example.json';
fetch(url)
.then((response) => {
return response.json();
})
.then((json) => {
json.map(function(event) {
console.log(`${event.start_date}`);
console.log(`${event.title}`);
console.log(`${event.room[156]}`);
return element;
});
}, 80);
</script>
EDIT: Forgot to point out, there is always only 1 entry in the "room" tag, but it's index is randomized, so if you just select the room tag it returns undefined or invalid. If I could wildcard the index so it just tries them all, or somehow retrieve the index number and store it in a variable, it would fix the issue.
I think this will work:
Here as you don't know the key so, instead of just guessing, you can use Object.values(JSONObjName) to get the list/array of values in that json.
Here I'm also using optional chaining (?.) to handle the case when the json has no key value pairs.
<script>
const url = 'example.json';
fetch(url)
.then((response) => {
return response.json();
})
.then((json) => {
json.map(function(event) {
const roomName = Object.values(event.room)?.[0];
console.log(`${event.start_date}`);
console.log(`${event.title}`);
console.log(`${roomName}`);
return {...event, room: roomName};
});
}, 80);
</script>
As long as you always want the first key you can fetch it like this
room = event.room[Object.keys(event.room)[0]]
if you want to get just roomname, you could do Object.values(room)[0]
or if you want the index and value you could go for Object.entries(room)[0]
arr?.map(({ room }) => {
for(let [key, value] of Object.entries(room)) {
console.log('Random Key : ',key)
console.log('Roomname : ', value)
console.log('Using random key : ',room[key])
}
})
By this way you can find the value of room against the random key.
Or you can try this if it is more relevant to you.
arr.map(({ room }) => {
for(let key of Object.keys(room)) {
console.log('Random Key : ',key)
console.log('Using random key : ',room[key])
}
})
Since you may want to do this for branch as well, here's an alternative solution which uses the object key as a computed property name (aka "dynamic key") to get the value.
And since, in this example it's done more than once, I've added that to a function that you can call in the destructuring assignment.
const data=[{title:"Event",start_date:"2022-08-20 15:00:00",end_date:"2022-08-20 16:00:00",branch:{85:"branchname"},room:{156:"roomname"},age_group:{5:"Youth",6:"Teen"}},{title:"Event02",start_date:"2022-08-20 15:00:00",end_date:"2022-08-20 16:00:00",branch:{72:"branchname02"},room:{104:"roomname02"},age_group:{5:"Youth",6:"Teen"}}];
// Get first key from an object
function getKey(obj) {
return Object.keys(obj)[0];
}
const out = data.map(obj => {
// Destructure the object and call `getKey` with the
// object to get its only key, and use that
// as a computed property to get its value, which
// we then relabel e.g. `roomName`
const {
branch: { [getKey(obj.branch)]: branchName },
room: { [getKey(obj.room)]: roomName },
...rest
} = obj;
// Now just return a new object with your new keys/values
return { ...rest, branchName, roomName };
});
console.log(out);
Additional documentation
Rest parameters
Spread syntax

Resetting filters before adding new ones

I am working on a homework assignment that requires me to use D3.
I am building an interactive table that users should be able to filter. I have two semi-working versions.
Version One: It works fine on the first search, but subsequent searches will filter the prior results. (For instance, if I first filter by location, then by date, rather than getting a completely new set of results, I get a list that uses both filters. If I enter a new location in my second search, I get no results, as each row has only one location.)
Version Two: Does new search, but applies only one filter at a time (whichever was last).
My original code is posted below.
I tried using tbody.html(""); to reset the table; that didn't work.
I tried switching the name of my filtered results to tableData2. (That did give me a fresh start for each click of the button but took away my ability to search for multiple criteria.)
I also tried earlier to put all of the filters into the final line defining tableData (here):
tableData = tableData.filter(row => row.shape==inputShape)
but that didn't work.
button.on("click", function() {
d3.event.preventDefault()
//Add date filter
let dateElement = d3.select("#datetime");
let inputDate = dateElement.property("value");
if (inputDate){
tableData = tableData.filter(row => row.datetime==inputDate)}; //add if filter to avoid nulls
// Add city filter
let cityElement = d3.select("#city");
let inputCity = cityElement.property("value");
if(inputCity){
tableData = tableData.filter(row => row.city==inputCity)};
// Add state filter
let stateElement = d3.select("#state");
let inputState = stateElement.property("value");
if (inputState) {
tableData = tableData.filter(row => row.state==inputState)};
// Add country filter
let countryElement = d3.select("#country");
let inputCountry = countryElement.property("value");
if (inputCountry){
tableData = tableData.filter(row => row.country == inputCountry)};
// Add shape filter
let shapeElement = d3.select("#shape");
let inputShape = shapeElement.property("value");
if (inputShape){
tableData = tableData.filter(row => row.shape==inputShape)};
buildTable(tableData);
});
The expected result is that if I run a search on a single element, I would get all results that have a near-match for that element. If I ran a search on two elements, I would get all results that had near-matches for both elements.
The desired result is that I get either a table that can be filtered by only one element at a time (but which will reset each time I click the filter button), or I get a table that will apply multiple filters, but only one at a time (and without being asked to).
The reason your code didn't work is because you are modifying original tableData while filtering, and using the modified tableData on subsequent filter button clicks.
You can avoid doing that by using a new array filteredTableData throughout your function.
var filteredTableData = tableData;
Declare above variable at start of your function, and replace tableData with filteredTableData everywhere in rest of your function.
No need to implement below changes, above fix will work. But you can follow below approach if you want to apply all property filters on each array element at once, or to keep the code more concise.
You can use Array.filter() to filter your data.
button.on("click", function () {
d3.event.preventDefault();
// Construct a filters object.
var filtersObj = {
"datetime": d3.select("#datetime").property("value"),
"city": d3.select("#city").property("value"),
"state": d3.select("#state").property("value"),
"country": d3.select("#country").property("value"),
"shape": d3.select("#shape").property("value")
};
// Remove properties which are undefined.
for (var key in filtersObj) {
if (!filtersObj[key]) {
delete filtersObj[key];
}
}
var filters = Object.entries(filtersObj);
// Filter on properties which are defined.
const filteredTableData = tableData.filter(item =>
filters.every(([key, value]) => item[key] == value)
);
buildTable(filteredTableData);
}
You can also filter without using Object.entries() and Array.every().
// Filter on properties which are defined.
const filteredTableData = tableData.filter(item => {
for (var key in filtersObj) {
if (item[key] != filtersObj[key]) {
return false;
}
}
return true;
});
Live Example:
var tableData = [
{ "city": "Chicago", "state": "Illinois", "country": "USA" },
{ "city": "Dallas", "state": "Texas", "country": "USA" },
{ "city": "Los Angeles", "state": "California", "country": "USA" },
{ "city": "San Francisco", "state": "California", "country": "USA" }
];
function filterData(inputDate, inputCity, inputState, inputCountry, inputShape) {
// Construct a filters object.
var filtersObj = {
"datetime": inputDate,
"city": inputCity,
"state": inputState,
"country": inputCountry,
"shape": inputShape
};
// Remove properties which are undefined.
for (var key in filtersObj) {
if (!filtersObj[key]) {
delete filtersObj[key];
}
}
var filters = Object.entries(filtersObj);
// Filter on properties which are defined.
const filteredTableData = tableData.filter(item =>
filters.every(([key, value]) => item[key] == value)
);
console.log(JSON.stringify(filteredTableData));
}
filterData(null, "Chicago", null, null, null);
filterData(null, null, "California", null, null);
filterData(null, null, "Texas", "USA", null);

In Typed JS (Typescript/FlowType) how do you handle normalized objects?

Working on a project. I'm starting with flow type because it's easier to implement piecemeal but eventually I plan to convert from flow to Typescript when we move from "proof of concept" into "prototype". However, a solution to this problem in either should work in both flow or TS.
I'm writing a backend API which makes queries to a database.
Now, my query to the DB gives me this:
type OneMeeting = {
location: string
participants: Array<string>
}
const RawDataFromDB: Array<OneMeeting> = await getDataFromDB();
Here's the problem:
I want to consolidate that data, so that if all participants are identical, the zip codes are combined.
So, I want this:
type Meeting = {
locations: Array<string>
participants: Array<string>
}
const RawDataFromDB: Array<OneMeeting> = [
{
location: "Trump Tower",
participants: ["Kushner", "Trump Jr.", "Manifort", "Veselnitskaya"]
},
{
location: "Mar A Lago",
participants: ["Kushner", "Trump Jr.", "Manifort", "Veselnitskaya"]
},
{
location: "Mar A Lago",
participants: ["Trump Sr.", "Abramovich"]
}
]
const WhatIWantAtTheEnd: Array<Meeting> = [
{
locations: ["Trump Tower", "Mar A Lago"],
participants: ["Kushner", "Trump Jr.", "Manifort", "Veselnitskaya"]
},
{
locations: ["Mar A Lago"],
participants: ["Trump Sr.", "Abramovich"]
}
]
Now, the way I had been converting from Raw Data to What I want was basically to sort() the participants in each meeting, create an object where the key is the JSON.stringified version of the participants array, and push the location values. So there's an intermediate step where, instead of an array of meetings, there's an intermediate Object with an unknown number of keys, where the names of those keys cannot be determined in advance.
And for the life of me, I can't figure out how to type out that intermediate Object so that it doesn't throw a type error, without making it an "any" - which will then throw errors if I try to .sort() on an "any" value.
So, typescripterinos, how would you approach this?
-- Edit, this is how I normally would do the conversion from A->B.
const getWhatIWant = (rawData: OneMeeting[]): Meeting[] => {
// what is the type I should use for normalized ?
let normalized: Object = rawData.reduce((pv: Object, curr: OneMeeting) => {
let key = curr.participants.sort().join(",")
if(!pv[key]){
pv[key] = {locations: [curr.location], participants: curr.participants}
} else {
pv[key].locations.push(curr.location)
}
return pv;
}, {})
return Object.values(normalized);
}
From what I understand of the algorithm you describe, the intermediate object type you are looking for, should have an indexer from string to Meeting defined:
let map: { [participants: string]: Meeting } = {};
for (let m of RawDataFromDB) {
let key = m.participants.sort().join(',');
let existingMeeting = map[key];
if (!existingMeeting) {
map[key] = {
locations: [m.location],
participants: m.participants
}
} else {
existingMeeting.locations.push(m.location);
}
}
Or using reduce as you do in your sample, you just need to specify the indexable type as a generic parameter (the parameter representing the result type) to reduce
const getWhatIWant = (rawData: OneMeeting[]): Meeting[] => {
// what is the type I should use for normalized ?
let normalized = rawData.reduce<{ [key: string]: Meeting }>((pv, curr) => {
let key = curr.participants.sort().join(",")
if (!pv[key]) {
pv[key] = { locations: [curr.location], participants: curr.participants }
} else {
pv[key].locations.push(curr.location)
}
return pv;
}, {})
return Object.values(normalized);
}
Not sure if it's what you really want, but here is a one liner code:
// Assuming that type Meeting = { location: string[]; participants: string[]; }
const WhatIWantAtTheEnd: Meeting[] = RawDataFromDB
.map(meeting => meeting.participants.sort((a, b) => a.localeCompare(b))) // Create a participant sorted list
.filter((value, index, array) => index === array.findIndex(compValue => value.every(participant => compValue.includes(participant)))) // strip duplicates
.map(sortedParticipants => ({
participants: sortedParticipants,
location: RawDataFromDB.filter(raw => raw.participants.every(participant => sortedParticipants.includes(participant))).map(e => e.location)
}));

Object.assign and ... spread operator fail silent, don't add elements

I have problems with Object.assign and ... spread operator. I need to process values (object with name and value tha are objects).
Example my values object:
{
id: "12",
name: "Hotel MESSI",
email: "myemail#aol.com",
phone: "+001060666661",
otherfields: "{
country: 'ZW',
city: 'Zurick'
}"
}
otherfields comes from graphql , so it's string, i must convert to object.
With my process I look for this result:
{
id: "12",
name: "Hotel MESSI",
email: "myemail#aol.com",
phone: "+001060666661",
country: 'ZW',
city: 'Zurick'
}
The code have more code that I paste here, there is a lot of controls for values and conversion but mainly, the idea is reassing values,
With these two case assign to the same variable is not working:
Case 1, with object.assign
processValues = (values)=>
let newValues = {...values}; //
for (const fieldName in Tables[table].fields) {
let value = values[fieldName];
value = JSON.parse(value);
newValues = { ...newValues, ...value};
console.error('after mix',newValues);
Case 2, with object.assign
processValues = (values)=>
let newValues = Object.assign({}, values}; //
for (const fieldName in Tables[table].fields) {
let value = values[fieldName];
value = JSON.parse(value);
newValues = Object.assign( newValues, value};
console.error('after mix',newValues);
How it's works, when I use a new variable, by example:
newValues2 = Object.assign( newValues, value};
but my idea is not use another variable because , i need to get values and set values for the original variable 'newValues' , if I use another variable the code would be more cumbersome.
I'm using in a project with create-react-app. I don't know if it's a problem with babel, because Object.assign and spread operator are not inmmutable; or yes ?
INFO:
Tables[table].fields is a object with definition por my table structure, there therea lot of rules, but basically i need to know why object and ... does not work
The use of JSON.stringify will not help, as this will produce a JSON string, which will have an entirely different behaviour when spreading it (you get the individual characters of that string).
Here is how you can achieve the result with "otherfields" as the special field (you can add other fields in the array I have used):
const processValues = values =>
Object.assign({}, ...Object.entries(values).map( ([key, val]) =>
["otherfields"].includes(key) ? val : { [key]: val }
));
// Example:
const values = {
id: "12",
name: "Hotel MESSI",
email: "myemail#aol.com",
phone: "+001060666661",
otherfields: {
country: 'ZW',
city: 'Zurick'
}
};
const result = processValues(values);
console.log(result);
The first argument to assign is the target. So it's going to get changed. You can simply pass an empty object for your target if you don't want any of the sources to change.
When you are using first argument as {} then no value will change.
For more please refer it.
https://wecodetheweb.com/2016/02/12/immutable-javascript-using-es6-and-beyond/

Combine multiple observable arrays into new object array

I have 3 observable arrays like below.
persons = [
{
"firstName":"john",
"lastName":"public",
"locationID":"1",
"departmentID":"100"
},
{
"firstName":"sam",
"lastName":"smith",
"locationID":"2",
"departmentID":"101"
}
]
departments = [{"departmentID": "100",
"name": "development"
},
{"departmentID": "101",
"name": "sales"
}]
locations = [{"locationID": "1", "name": "chicago"},
{"locationID":"2", "name": "ny"}]
I am trying to combine these 3 into below result ,
result = [
{
"firstName":"john",
"lastName":"public",
"location":"development",
"department":"sales"
},
{
"firstName":"sam",
"lastName":"smith",
"location":"ny",
"department":"sales"
}
]
To get the desired result, I have used map function on persons observable to give new object array.
this.store<Person>('persons')
.map(function(person){
let p = new personDetail()
p.firstName = person.firstName,
p.lastName = person.lastName
return p;
})
PersonDetail object has firstName, lastName, location and department properties. How do I do a lookup into departments observable and get a matching row for departmentID to get the department name ?
I am new to rxjs library, please let me know if there is a better way to attain the desired result.
Since you'll very likely want to fetch lists of departments and locations from a remote service (make another HTTP request) I'd do it right away with Observables as well.
Observable.from(persons)
.mergeMap(person => {
let department$ = Observable.from(departments)
.filter(department => department.departmentID == person.departmentID);
let location$ = Observable.from(locations)
.filter(location => location.locationID == person.locationID);
return Observable.forkJoin(department$, location$, (department, location) => {
return {
'firstName': person.firstName,
'lastName': person.lastName,
'location': location.name,
'department': department.name,
};
});
})
.toArray()
.subscribe(result => console.log(result));
This prints to console:
[ { firstName: 'john',
lastName: 'public',
location: 'chicago',
department: 'development' },
{ firstName: 'sam',
lastName: 'smith',
location: 'ny',
department: 'sales' } ]
There're two Observables department$ and location$ that are filtered with filter() operator to get the only item with matching ID. Then forkJoin() operator waits until both of them are complete. Operator mergeMap() then reemits the value returned from forkJoin(). At the end with toArray() we collect all items into a single array.
Instead of Observable.from(...) you can have whatever service you'll need (eg. http.get(...)).
See live demo: https://jsbin.com/nenekup/4/edit?js,console
Similar questions: Merge subarrays using Observables and Subscribing to a nested Observable
i think the RxJS .zip operator may be your friend here.
As far as I understand, .zip emits like this ...
.zip(
Observable.from[array1].switchMap( // map to http response here ),
Observable.from[array2].switchMap( // map to http response here ),
Observable.from[array3].switchMap( // map to http response here )
).map((valueFromArray1, valueFromArray2, valueFromArray3) {
// Create your object here
})
Something like that! Hopefully, puts you on the right track.
.zip should emit the first time when all 3 have emitted (and it will emit a second time when all three stream have emitted twice, etc) - In your scenario I expect all three stream emit once only, which makes it a simple case for .zip
It's not clear what exactly your observables are emitting, so I considered two options.
let persons = [
{
"firstName":"john",
"lastName":"public",
"locationID":"1",
"departmentID":"100"
},
{
"firstName":"sam",
"lastName":"smith",
"locationID":"2",
"departmentID":"101"
}
];
let departments = [
{"departmentID": "100", "name": "development"},
{"departmentID": "101", "name": "sales"}
];
let locations = [
{"locationID": "1", "name": "chicago"},
{"locationID": "2", "name": "ny"}
];
// Option 1: first observable emits persons one by one,
// locations and departments are emitted as whole arrays.
let o1: any = Observable.from(persons);
let o2: any = Observable.of(departments);
let o3: any = Observable.of(locations);
o1.withLatestFrom(o2, o3, (p, d, l) => {
// here it is probably better to convert array to some kind of map or dictionary,
// but I'm only showing Rxjs concept of doing such things.
let location = l.find(c => c.locationID === p.locationID);
let department = d.find(c => c.departmentID === p.departmentID);
return {
firstName: p.firstName,
lastName: p.lastName,
location: location ? location.name : "",
department: department ? department.name : ""
};
}).subscribe((f) => {
console.log(f);
});
// Option 2: all observables emit elements one by one.
// In this case we need to convert departments and locations to arrays.
o1 = Observable.from(persons);
o2 = Observable.from(departments);
o3 = Observable.from(locations);
o1.withLatestFrom(o2.toArray(), o3.toArray(), (p, d, l) => {
// this part of code is exactly the same as in previous case.
let location = l.find(c => c.locationID === p.locationID);
let department = d.find(c => c.departmentID === p.departmentID);
return {
firstName: p.firstName,
lastName: p.lastName,
location: location ? location.name : "",
department: department ? department.name : ""
};
}).subscribe((f) => {
console.log(f);
});

Categories