Merging two nested JSON arrays - javascript

I'm working with a form built dynamically in JavaScript from a JSON schema that looks like this:
{
"questionSets": [
{
"questionSetId": "example-fields",
"questions": [
{
"questionId": "text",
"question": "Text Field",
"input": {
"type": "textInput",
"default": ""
},
},
{
"questionId": "textarea",
"question": "Text Area",
"input": {
"type": "textareaInput",
"default": ""
}
}
]
}
]
}
When the form is submitted it just returns the updated values that look like this:
{
text: "some entered text",
textarea: "some more entered text"
}
The keys of this resulting JSON array correspond with the questionId and the value with the default key in the first array.
What's the best way to go about merging these 2 arrays so the result is:
{
"questionSets": [
{
"questionSetId": "example-fields",
"questions": [
{
"questionId": "text",
"question": "Text Field",
"input": {
"type": "textInput",
"default": "some entered text"
},
},
{
"questionId": "textarea",
"question": "Text Area",
"input": {
"type": "textareaInput",
"default": "some more entered text"
}
}
]
}
]
}

That's a tricky one. The simplest way is using underscore. Let reply be your input object, and defaultInputs the object with the default inputs to be filled out in the JSON.
'use strict';
let _ = require('underscore');
module.exports = function (defaultInputs, reply) {
reply.questionSets = _.map(reply.questionSets, questionSet => {
questionSet.questions = _.map(questionSet.questions, question => {
question.input.default = _.find(defaultInputs,(item, key) => (
new RegExp(`${key}Input`).test(question.input.type) && item
) || false) || '';
return question;
});
return questionSet;
});
return reply;
};
A proper code solution (including the test) can be found here.
UPDATE (07/01/2018)
Now it's possible to achieve the same via the new Array prototype functions like map and filter
'use strict';
module.exports = function (defaultInputs, reply) {
reply.questionSets = reply.questionSets.map(questionSet => {
questionSet.questions = questionSet.questions.map(question => {
question.input.default = defaultInputs.filter((item, key) => (
new RegExp(`${key}Input`).test(question.input.type) && item
) || false) || '';
return question;
});
return questionSet;
});
return reply;
};

Related

How to create a new array by filtering only the necessary data?

I have an object which contains questions with different types multiple, single and text.
const data = {
questions: [
{
"question": "Question 1",
"value": [
"value_1.1.1",
"value_1.1.2"
],
"type": "multiple",
"options": [
{
"value": "value_1.1.1",
"label": "Value 1.1.1"
},
{
"textValue":"Additional text value",
"value": "value_1.1.2",
"label": "Value 1.1.2"
},
{
"value":"value_1.1.3",
"label":"Value 1.1.3",
}
]
},
{
"question": "Question 2",
"value": "value_2.1.1",
"type": "single",
"options": [
{
"value": "value_2.1.1",
"label": "Value 2.1.1"
},
{
"value": "value_2.1.2",
"label": "Value 2.1.2"
},
]
},
{
"question":"Question 3",
"textValue":"Test 12345",
"type":"text"
}
]
}
I want to create a new array with objects, which contains the question key and the value key.
The value key must contain the label of the selected options.
For a multiple type, the value can contain multiple labels.
The selected option can be retrieved from the parent value key, if it is equal to the value of the option, I want to use the label of this option as value.
In some cases an option also contains a textValue (see 1.1.3), then this must also be added to the value.
So the new array should look like this for the example above:
const newData = [
{
question: "Question 1",
value: ['Value 1.1.1', 'Value 1.1.2', 'Additional text value']
},
{
question: "Question 2",
value: ['Value 2.1.1']
},
{
question: "Question 3",
value: ['Test 12345']
}
];
Well basically loop over the questions, handling each by its own type. The tricky part is the type: multiple but it is managable.
Update: Fixed according to comment
const data = {questions:[{question:"Question 1",value:["value_1.1.1","value_1.1.2"],type:"multiple",options:[{value:"value_1.1.1",label:"Value 1.1.1"},{value:"value_1.1.2",label:"Value 1.1.2"},{textValue:"test12",value:"value_1.1.3",label:"Value 1.1.3"}]},{question:"Question 2",value:"value_2.1.1",type:"single",options:[{value:"value_2.1.1",label:"Value 2.1.1"},{value:"value_2.1.2",label:"Value 2.1.2"},]},{question:"Question 3",textValue:"Test 12345",type:"text"}]};
function get_option_label(options, value) {
return options.find(item => item.value == value).label || value
}
const newData = [];
data.questions.forEach(function(obj) {
var new_obj = {
question: obj.question
}
if (obj.type == "multiple") {
new_obj.value = JSON.parse(JSON.stringify(obj.value))
new_obj.value = new_obj.value.map(item => get_option_label(obj.options, item))
// add textValue for multiple question
// but only if its value is on parent
var objText = obj.options.find((item) => item.textValue);
if (objText && obj.value.indexOf(objText.value) >= 0) {
new_obj.value.push(objText.textValue)
}
}
if (obj.type == "single") {
new_obj.value = [get_option_label(obj.options, obj.value)]
}
if (obj.type == "text") {
new_obj.value = [obj.textValue]
}
newData.push(new_obj)
})
// now replacing all values with labels
console.log(newData);
.as-console-wrapper {
max-height: 100% !important;
top: 0
}

Counting multiple json inputs js

I get an input like this:
input 1:
{
"name": "Ben",
"description": "Ben",
"attributes": [
{
"type": "Background",
"value": "Default"
},
{
"type": "Hair-color",
"value": "Brown"
}
]
}
input 2
{
"name": "Ice",
"description": "Ice",
"attributes": [
{
"type": "Background",
"value": "Green"
},
{
"type": "Hair-color",
"value": "White"
}
]
}
input 3
{
"name": "Itay",
"description": "Itay",
"attributes": [
{
"type": "Background",
"value": "Default"
},
{
"type": "Hair-color",
"value": "Brown"
}
]
}
What I want to do is count the amount of each type of background and each type of hair-color appearing.
(These are sample examples and in reality there are more types and different values)
Let's say in these examples we have 2 objects that have a background as default then I want to have a count of that like so:
export interface TraitCount {
value: string,
count: number
}
export interface CountOfEachAttribute {
trait_type: string,
trait_count: traitCount[] | null,
total_variations: number
}
I want the most effective code because there are other aspects to the code, in addition it will run on 5-10k queries not just three, so needs
to run in good times too :D
(It's similar to my other question done with python but now I need it in js also)
Atm it's something like this:
(Apart of a much bigger code so keep that in mind)
setInitalCountOfAllAttribute( state, { payload }: PayloadAction<CountOfEachAttribute[] | null> ) {
if (payload === null) {
state.countOfAllAttribute = null;
} else {
state.countOfAllAttribute = payload;
}
},
setCountOfAllAttribute(state, { payload }: PayloadAction<Attribute>) {
if (state.countOfAllAttribute !== null) {
state.countOfAllAttribute.map(
(countOfEachAttribute: CountOfEachAttribute) => {
// Find the trait type
if (countOfEachAttribute.trait_type === payload.trait_type) {
// initiate the trait count array to store all the trait values and add first trait value
if (countOfEachAttribute.trait_count === null) {
const new_trait_count = { value: payload.value, count: 1 };
countOfEachAttribute.trait_count = [new_trait_count];
countOfEachAttribute.total_variations++;
}
// Trait array already existed.
else {
// Check if value already present or not
const checkValue = (obj: any) => obj.value === String(payload.value);
const isPresent = countOfEachAttribute.trait_count.some(checkValue)
const isPresent2 = countOfEachAttribute.trait_count.find((elem: any) => elem.value === String(payload.value))
// Value matched, increase its count by one
if (isPresent2) {
countOfEachAttribute.trait_count &&
countOfEachAttribute.trait_count.map((trait) => {
if (trait.value === payload.value) {
trait.count++;
}
});
}
// Value doesn't match, add a new entry and increase the count of variations by one
else {
const new_trait_count = { value: payload.value, count: 1 };
countOfEachAttribute.trait_count = [
...countOfEachAttribute.trait_count,
new_trait_count,
];
countOfEachAttribute.total_variations++;
}
}
}
}
);
}
},
You can merge all arrays and use Array.reduce.
const input1 = {
"name": "Ben",
"description": "Ben",
"attributes": [{
"type": "Background",
"value": "Default"
},
{
"type": "Hair-color",
"value": "Brown"
}
]
}
const input2 = {
"name": "Ice",
"description": "Ice",
"attributes": [{
"type": "Background",
"value": "Green"
},
{
"type": "Hair-color",
"value": "White"
}
]
}
const input3 = {
"name": "Itay",
"description": "Itay",
"attributes": [{
"type": "Background",
"value": "Default"
},
{
"type": "Hair-color",
"value": "Brown"
}
]
}
const mergedInput = [input1, input2, input3];
const result = mergedInput.reduce((acc, item) => {
item.attributes.forEach(attrItem => {
const existType = acc.find(e => e.trait_type == attrItem.type);
if (existType) {
var existAttr = existType.trait_count.find(e => e.value == attrItem.value);
if (existAttr) {
existAttr.count++;
} else {
existType.trait_count.push({
value: attrItem.value,
count: 1
});
existType.total_variations++;
}
} else {
acc.push({
trait_type: attrItem.type,
trait_count: [{
value: attrItem.value,
count: 1
}],
total_variations: 1
})
}
});
return acc;
}, []);
console.log(result);
I suggest instead of creating an array for trait_count to make it an object so you don't have to iterate over it whenever you are adding a new attribute. In the snippet below I'm using the value of the attribute as a sort of hash that allows the access to the given property without having to call the Array.prototype.find function
const input1 = {"name":"Ben","description":"Ben","attributes":[{"type":"Background","value":"Default"},{"type":"Hair-color","value":"Brown"}]};
const input2 = {"name":"Ice","description":"Ice","attributes":[{"type":"Background","value":"Green"},{"type":"Hair-color","value":"White"}]};
const input3 = {"name":"Itay","description":"Itay","attributes":[{"type":"Background","value":"Default"},{"type":"Hair-color","value":"Brown"}]};
function countAtributes(input, totalCounts={}) {
input.attributes.forEach((attribute) => {
if (!totalCounts[attribute.type])
totalCounts[attribute.type] = {trait_type: attribute.type, trait_count: {}, total_variations: 0};
if (!totalCounts[attribute.type].trait_count[attribute.value]) {
totalCounts[attribute.type].trait_count[attribute.value] = {value: attribute.value, count: 1};
totalCounts[attribute.type].total_variations+=1;
}
else totalCounts[attribute.type].trait_count[attribute.value].count +=1;
})
}
const totalCounts = {};
countAtributes(input1, totalCounts);
countAtributes(input2, totalCounts);
countAtributes(input3, totalCounts);
console.log(totalCounts);
It could be turned into the array afterwards with Object.values if necessary
I believe it is a much better approach to what you had before as you don't have to iterate over the tables of trait_counts. In theory it should significantly reduce the time taken. Iterating over the array and checking a condition each time is much slower than key lookup in Javascript object

javascript - validate nested json object

I have the following json schema.
const myJson = {
"type": "typeName"
"firstName": "Steven",
"lastName": "Smith",
"address": {
"primary": {
"city": "abc",
"street": {
"name": {
"subName": "someName"
}
}
}
}
}
And I want to loop over each of the properties for required validation on this json, I have the following code so far which works if the property in the json is not nested.
let errors = [];
const typeName = ['firstName', 'lastName'],
const typeAttr = Object.keys(myJson);
typeName.forEach(attr => {
if (!typeAttr.includes(attr)) {
errors.push(`Missing field: ${attr}`);
}
});
How can I add the nested json property like primary, city, street and validate the way I have done it.
Here's how you can check nested properties on json object
const myJson = {
type: "typeName",
firstName: "Steven",
lastName: "Smith",
address: {
primary: {
city: "abc",
street: {
name: {
subName: "someName",
},
},
},
},
};
let errors = [];
const typeName = ["firstName", "lastName", "address.primary", "address.primary.city"];
function checkNested(obj, level, ...rest) {
if (obj === undefined) return false;
if (rest.length == 0 && obj.hasOwnProperty(level)) return true;
return checkNested(obj[level], ...rest);
}
typeName.forEach((attr) => {
let props = attr.split(".");
if (!checkNested(myJson, ...props)) {
errors.push(`Missing field: ${attr}`);
}
});
console.log(errors);
For reference on how to check Nested properties you can check this answer.
Test for existence of nested JavaScript object key
I would do something like this. This method gives whether the data is having all the provided keys or not i.e., will return either true or false
let obj = {"type":"typeName","firstName":"Steven","lastName":"Smith","address":{"primary":{"city":"abc","street":{"name":{"subName":"someName"}}}}};
const typeName = ['firstName', 'lastName', 'address', 'address.primary', 'address.primary.city', 'address.primary.street'];
const validate = (data, types) => {
return types.every(type => {
// Split the keys using `.`
const keys = type.split('.');
// Check if the length is more than 1,
// if yes, then we need to check deeply
if (keys.length > 1) {
let datum = {
...data
};
// Check through all the keys found using split
for (let key of keys) {
// if any key is not found or falsy then return false
if (!datum[key]) {
return false;
}
datum = datum[key];
}
return true;
} else {
// if the length of the keys is not more than 1 then it means
// the key is at the top level and return the value as boolean
return !!data[type]
}
})
}
console.log(validate(obj, typeName));
console.log(validate(obj, ['firstName', 'lastName', 'address', 'address.primary', 'address.primary.zip']));
This below method will return the keys that were not present in the provided data
const validate = (data, types) => {
let errors = [];
types.forEach(type => {
const keys = type.split('.');
let datum = {
...data
};
// Loop through the keys
for (let [index, key] of keys.entries()) {
// Check if the key is not available in the data
// then push the corresponding key to the errors array
// and break the loop
if (!datum[key]) {
errors.push(keys.slice(0, index + 1).join('.'));
break;
}
datum = datum[key];
}
})
return errors;
}
const obj = {"type":"typeName","firstName":"Steven","lastName":"Smith","address":{"primary":{"city":"abc","street":{"name":{"subName":"someName"}}}}};
const typeName = ['firstName', 'lastName', 'address', 'address.primary', 'address.primary.city', 'address.primary.street'];
console.log(validate(obj, ['firstName', 'lastName']));
console.log(validate(obj, ['firstName', 'lastName', 'address', 'address.primary', 'address.primary.zip']));
console.log(validate(obj, [...typeName, 'test', 'address.primary.zip', 'address.test.zip']));
Use JSON Schema, a proposed IETF standard with tons of library implementations available for several languages for describing, generating, and validating JSON documents.
To require your JSON to have all the properties be present, you'll need a JSON Schema like below.
let typeNameSchema = {
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://example.com/typename.schema.json",
"title": "Type Name",
"description": "A person's name and address details",
"type": "object",
"required": [
"firstName",
"lastName",
"address"
],
"properties": {
"type": {
"type": "string"
},
"firstName": {
"type": "string"
},
"lastName": {
"type": "string"
},
"address": {
"type": "object",
"required": [
"primary"
],
"properties": {
"primary": {
"type": "object",
"required": [
"city",
"street"
],
"properties": {
"city": {
"type": "string"
},
"street": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "object",
"required": [
"subName"
],
"properties": {
"subName": {
"type": "string"
}
}
}
}
}
}
}
}
}
}
}
Then use any library to validate your JSON data against the above schema. jsonschema is one of the most popular JSON Schema validators for JavaScript. Here's how to validate your myJson data against the above typeNameSchema.
const Validator = require('jsonschema').Validator;
const validator = new Validator();
console.log(validator.validate(myJson, typeNameSchema)); // result
Validation results will be returned in a ValidatorResult object with the most important properties valid of type boolean and errors of type ValidationError[]. ValidationError object will have the property name and error message.

Add object to end of array react

I have a bit of a problem with building an object in my Next.JS application. And it's most likely because of my lacking JavaScript and React competences.
I have a form that is rendered from an api call. It returns an array of objects like so:
[
{
"formId": "uuid",
"description": "Description of form",
"questions": [
{
"questionId": "uuid",
"text": "question 1?",
"alternatives": [
{
"text": "No",
"ratingValue": 1
},
{
"text": "Yes",
"ratingValue": 5
}
]
}
]
}]
The object will contain multiple forms and each form can contain multiple questions.
I pass the value of each question into a function called pushAnswer in my component like this:
<Form.Check
type="radio"
name={question.questionId}
value={alternative.ratingValue}
onChange={event => {
pushAnswer(form.formId, question.questionId, event.target.value)
}} />
I have state variable const [formBody, setFormBody] = useState({form:[]})
And the pushAnswer function looks like this:
const pushAnswer = (formId, questionId, answerValue) => {
const currentForm = formBody;
let answeredForm = currentForm.forms.find(form => form.formId === formId);
// if the form ID doesn't exist, add it
if (!answeredForm) {
answeredForm = {
formId,
answers: []
}
}
// check if the questionId exists
let answeredQuestion = answeredForm.answers.find(answer => answer.questionId === questionId)
if (!answeredQuestion) {
answeredQuestion = {
questionId,
rating: answerValue
}
}
answeredQuestion.rating = answerValue;
setFormBody(oldForm => ({
...oldForm,
forms: [
{
formId: answeredForm.formId,
questions: [
{
questionId: answeredQuestion.questionId,
rating: answeredQuestion.rating
}
]
}
]
}))
}
I want to produce an answer like this:
{
"forms": [
{
"formId": "2b945644-a9c3-473e-afac-1236bc1575ce",
"questions": [
{
"questionId": "289a9e8a-a607-464a-8939-48223819f413",
"rating": "1"
},
{
"questionId": "another uuid",
"rating": "5"
}
]
},
{
"formId": "another uuid",
"questions": [
{
"questionId": "another uuid",
"rating": "5"
}
]
}
]
}
The first question gets added to formBody, but when I try to get the values of another question I get an error at let answeredQuestion = answeredForm.answers.find(answer => answer.questionId === questionId) that says Cannot read property 'find' of undefined.
Can anyone advice how I can solve this?
You are not saving the correct value in the state, it should be
const [formBody, setFormBody] = useState({forms:[]})
setFormBody(oldForm => ({forms: [
...oldForm.forms,
[
{
formId: answeredForm.formId,
questions: [
{
questionId: answeredQuestion.questionId,
rating: answeredQuestion.rating
}
]
}
]
]}))

How to retrieve an object in an array by one of its property?

In Javascript, how to retrieve an object in an array by one of its property ?
Hi all,
let's assume that we have the below :
"Attributes":[
{
"Name":"Brief",
"Value":"This skirt was fabriced from ...."
},
{
"Name":"Details",
"Value":"Measurements and Pictures are real"
},
{
"Name":"SKUNumber",
"Value":"12345678"
}
]
What I need to do is to get the value of "Value" based on "Name"..
For example :
console.log(Attributes.Brief) ==> "This skirt was fabriced from ...."
So I need a function to help doing that
Note that I don't want to use the index of the object, because its order may changed.
Thank you
Well, it's always better to show what you have attempted rather than just asking..
You can use Array.find to achieve this
let Attributes = [
{
"Name":"Brief",
"Value":"This skirt was fabriced from ...."
},
{
"Name":"Details",
"Value":"Measurements and Pictures are real"
},
{
"Name":"SKUNumber",
"Value":"12345678"
}
]
function getValueByName(name) {
return Attributes.find(d => d.Name.toLowerCase() == name.toLowerCase()).Value
}
console.log(getValueByName('Brief'))
console.log(getValueByName('details'))
console.log(getValueByName('SKUNumber'))
One option you have is to use Array.prototype.filter:
const d = [{
"Name": "Brief",
"Value": "This skirt was fabriced from ...."
},
{
"Name": "Details",
"Value": "Measurements and Pictures are real"
},
{
"Name": "SKUNumber",
"Value": "12345678"
}
]
console.log(d.filter(x=>x.Name==="Brief")[0].Value)
You can also make it more generic:
const d = [{
"Name": "Brief",
"Value": "This skirt was fabriced from ...."
},
{
"Name": "Details",
"Value": "Measurements and Pictures are real"
},
{
"Name": "SKUNumber",
"Value": "12345678"
}
]
const getValOfXfromArrByValOfY = (arr, x, y, val) => arr.find(z => z[y] === val)[x]
console.log(getValOfXfromArrByValOfY(d, 'Value', 'Name', 'SKUNumber'))
You could use a Proxy with a getter for the key, which returns a find of the object with the value.
var object = { attributes: [{ Name: "Brief", Value: "This skirt was fabriced from ...." }, { Name: "Details", Value: "Measurements and Pictures are real" }, { Name: "SKUNumber", Value: "12345678" }] },
attributes = new Proxy(
object.attributes,
{ get: (array, prop) => (array.find(({ Name }) => Name === prop) || {}).Value }
);
console.log(attributes.Brief);
console.log(attributes.SKUNumber);
You can use javascript find function see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find see bellow sample code:
var Attributes =[
{
"Name":"Brief",
"Value":"This skirt was fabriced from ...."
},
{
"Name":"Details",
"Value":"Measurements and Pictures are real"
},
{
"Name":"SKUNumber",
"Value":"12345678"
}
]
var found = Attributes.find(function(element) {
return element.Name == "Details";
});
console.log(found.Value); //output : Measurements and Pictures are real

Categories