Reducing object array with fallback values - javascript

Consider the following object:
var content = [
{
id: 'tab1',
langCode: 'en',
title: 'rocket'
},
{
id: 'tab1',
langCode: 'da',
title: 'raket'
},
{
id: 'tab2',
langCode: 'en',
title: 'factory'
},
{
id: 'tab3',
langCode: 'es',
title: 'boligrafo'
},
];
I'd like to reduce this array, to a new array with the following restriction:
No duplicate IDs
Values from the local language should take preference
In case there are no local translation, fall back to English
All other translations should be discarded (even if they have a unique ID)
That means, the output from the data above would be as follows, if the local language were Danish:
[
{
id: 'tab1',
langCode: 'da',
title: 'raket'
},
{
id: 'tab2',
langCode: 'en',
title: 'factory'
},
];
My goal is to keep the code as short and readable as possible, and I have ES6 and Lodash at my full disposal. This is what I have so far:
const LOCAL_LANG = 'da'; // Hard coded for the sake of the example
let localArr = content.filter(item => item.langCode === LOCAL_LANG);
if(LOCAL_LANG !== 'en') {
let enArr = content.filter(item => item.langCode === 'en');
for (let i = 0; i < enArr.length; i++) {
if (!_.find(localArr, { 'id': enArr[i].id})) {
localArr.push(enArr[i]);
}
}
}
This does the trick, but it creates two duplicate arrays, and then merges them back together in what I feel is a clumpsy way. I would like to see a more elegant solution – preferably one, where I don't have to pass over the arrays so many times.
An alternative (and perhaps slightly cleaner) solution would be to reduce the array on langCode === 'da' || langCode === 'en' on first pass, and then remove duplicates ... but still I'm just feeling I'm missing the most obvious solution.
Thanks!

I would reduce everything into an object keyed by ID to enable easy lookups without those extra _.find calls:
const targetLang = 'da';
const fallbackLang = 'en';
const itemsByKey = content.reduce((allItems, item) => {
if (item.langCode === targetLang
|| (!(item.id in allItems) && item.langCode === fallbackLang))
{
return Object.assign(allItems, { [item.id]: item });
} else {
return allItems;
}
}, {});
This solution will require only one pass over the original array. If you then need this lookup object converted back into an array, you'd need a second pass:
var normalizedArray = Object.keys(itemsByKey).map(key => itemsByKey[key]);

You could use a tree and filter the wanted items.
var content = [{ id: 'tab1', langCode: 'en', title: 'rocket' }, { id: 'tab1', langCode: 'da', title: 'raket' }, { id: 'tab2', langCode: 'es', title: 'boligrafo' }, { id: 'tab2', langCode: 'en', title: 'pen' }],
object = Object.create(null);
content.forEach(function (a) {
object[a.id] = object[a.id] || {}
object[a.id][a.langCode] = a;
});
var result = Object.keys(object).map(k => object[k][Object.keys(object[k]).filter(l => l !== 'en')[0] || 'en']);
console.log(result);

I would do as follows.
var content = [
{
id: 'tab1',
langCode: 'en',
title: 'rocket'
},
{
id: 'tab1',
langCode: 'da',
title: 'raket'
},
{
id: 'tab2',
langCode: 'es',
title: 'boligrafo'
},
{
id: 'tab2',
langCode: 'en',
title: 'pen'
}
],
result = content.sort((a,b) => a.langCode === "en" ? 1 : -1)
.reduce((p,c) => p.findIndex(o => o.id === c.id) === -1 ? p.concat(c) : p,[]);
console.log(result);

Related

How to transform only 2 properties but keep remaining same as is in a nested object structure?

Apologies if title is not clear.
I am using json2csv npm package to prepare csv from json object and this package allows us to add a hook to transform object before actual csv line is prepared.
I only need to manipulate two properties out of all. How can I do this effectively? My code feels too bloated.
const {
Parser: Json2csvParser,
transforms: { unwind },
} = require('json2csv');
const json2csvFields = [
{ value: 'root.filename', label: 'File Name' },
{ value: 'issue.root.priority', label: 'Priority' },
{ value: 'issue.root.url', label: 'URL' },
{ value: 'issue.root.startline', label: 'Start Line' },
{ value: 'issue.root.stopline', label: 'Stop Line' },
{ value: 'issue.root.startcolumn', label: 'Start Column' },
{ value: 'issue.root.stopcolumn', label: 'Stop Column' },
{ value: 'issue.root.issuename', label: 'Issue Name' },
{ value: 'issue.root.issuecategory', label: 'Issue Category' },
{ value: 'issue._', label: 'Issue Description' },
];
const sampleData = [
{
root: {
filename:
'/home/users/john-doe/workspace/foo-project/src/main/classes/foo.cls',
},
issue: {
root: {
priority: 1,
url: 'www.example.com',
startline: 100,
stopline: 105,
startcolumn: 20,
stopcolumn: 25,
issuename: 'blah',
issuecategory: 'Category A',
},
_: ' Fox ',
},
},
];
const json2csvOptions = {
fields: json2csvFields,
quote: '',
header: true,
transforms: [
(item) => ({
'root.filename': item.root.filename.replace(
'/home/users/john-doe/workspace/foo-project/src/main/classes/',
''
),
'issue._': `"${item.issue._.trim()}"`,
// Except for the above two, everything else doens't need any transformation.
'issue.root.priority': item.issue.root.priority,
'issue.root.url': item.issue.root.url,
'issue.root.startline': item.issue.root.startline,
'issue.root.stopline': item.issue.root.stopline,
'issue.root.startcolumn': item.issue.root.startcolumn,
'issue.root.stopcolumn': item.issue.root.stopcolumn,
'issue.root.issuename': item.issue.root.issuename,
'issue.root.issuecategory': item.issue.root.issuecategory,
}),
],
};
const json2csvParser = new Json2csvParser(json2csvOptions);
const csv = json2csvParser.parse(sampleData);
console.log(csv);
This prints below output:
File Name,Priority,URL,Start Line,Stop Line,Start Column,Stop Column,Issue Name,Issue Category,Issue Description
foo.cls,1,www.example.com,100,105,20,25,blah,Category A,"Fox"
EDIT: Updated code to a working example.
After listing the two properties with special treatment, use Object.fromEntries and Object.entries to transform all the issue.root properties to their flat structure with .s in the property names. Then that object can be spread into the returned object.
const transformsFn = ({ root, issue }) => ({
'root.filename': root.filename.replace(
'/home/users/john-doe/workspace/foo-project/src/main/classes/',
''
),
'issue._': `"${issue._.trim()}"`,
...Object.fromEntries(
Object.entries(issue.root).map(
([key, val]) => [`issue.root.${key}`, val]
)
),
});
const json2csvOptions = {
fields: json2csvFields,
quote: '',
header: true,
transforms: [transformsFn],
};

How to transfer field names to groups in a React json schema ui: groups nested object scenario

How to transfer field names to groups in a React json schema ui: groups nested object scenario. For example: object1.field1
I want to group the modelMetada and legancyMetaData objects given in the schema by selecting them from the properties inside.
The reason I want this is to show it by grouping it as I want in the tab.
const newSchema = {
$schema: 'http://json-schema.org/draft-07/schema#',
title: 'Physical Model',
type: 'object',
properties: {
modelMetadata: {
type: 'object',
properties: {
name: {
type: 'string',
title: 'Model Name',
description: 'Name of the Model',
},
displayName: {
type: 'string',
title: 'Model Display Name',
description: 'Display Name of the Model',
},
},
},
legacyMetaData: {
type: 'object',
properties: {
id: {
type: 'string',
title: 'Legacy ID',
description: 'ID of the Model in the Legacy System',
},
name: {
type: 'string',
title: 'Legacy Name',
description: 'Legacy Name of the Model',
},
},
},
},
};
const groups = [
{
modelMetadata: ['modelMetadata'],
legacyMetaData: ['legacyMetaData'],
customize: ['modelMetadata.displayName','legacyMetaData.name'], // ????
},
];
The code block I grouped
const doGrouping = ({ properties, groups, formContext }):any => {
const mapped = groups.map((g) => {
if (typeof g === 'string') {
const found = properties.filter((p) => p.name === g);
if (found.length === 1) {
const el = found[0];
return el.content;
}
return EXTRANEOUS;
} else if (typeof g === 'object') {
const { templates } = formContext;
const GroupComponent = templates
? templates[g['ui:template']]
: DefaultTemplate;
// #ts-ignore
const tempProperties = Object.keys(g).reduce((acc, key) => {
const field = g[key];
if (key.startsWith('ui:')) return acc;
if (!Array.isArray(field)) return acc;
return [
...acc,
{
name: key,
children: doGrouping({
properties,
formContext,
groups: field,
}),
},
];
}, []);
return <GroupComponent properties={tempProperties} />;
}
throw new Error(`Invalid object type: ${typeof g} ${g}`);
});
const remainder = mapped.filter((m) => m === REST);
if (remainder.length > 0) {
throw new Error('Remainder fields not supported');
}
return mapped;
};

Convert Array into Object with filtering and prioritizatoin and minimum loops

Given an array
const array = [{
typeName: 'welcome', orientation: 'landscape', languageId: 1, value: 'Welcome'
}, {
typeName: 'welcome', orientation: 'landscape', languageId: 2, value: 'Bonjour'
}, {
typeName: 'welcome', orientation: 'portrait', languageId: 2, value: 'Bonjour bonjour'
}]
My ultimate goal is to get this:
{
welcome: {
landscape: ['Welcome'],
portrait: ['Bonjour bonjour']
}
}
To do that, I need to convert into object like {typeName: {orientation: value[]}}, like this:
// This is NOT what I want, it's just an intermediate form -- keep reading
{
welcome: {
landscape: ['Welcome', 'Bonjour'],
portrait: ['Bonjour bonjour']
}
}
But including prioritization: if languageId=1 present on array, then ignore rest values for specific typeName, orientation..In the sample above should be only ['Welcome'] since it's languageId=1, so 'Bonjour' can be ignored, though if languageId=1 is missing, then any value can be added (welcome.portrait).
With convering I haven't faced with any problems..Doing it thought .reduce() method
array.reduce((prev, current) => ({
...prev,
[current.typeName]: {
...prev[current.typeName],
[current.orientation]: [
...(((prev[current.typeName] || {})[current.orientation]) || []),
current.value
]
}
}), {});
but prioritization I can do only with filtering that also does loop inside it..No problem so far, but if array will be pretty huge - performance will suffer
const array = [{
typeName: 'welcome', orientation: 'landscape', languageId: 1, value: 'Welcome'
}, {
typeName: 'welcome', orientation: 'landscape', languageId: 2, value: 'Bonjour'
}, {
typeName: 'welcome', orientation: 'portrait', languageId: 2, value: 'Bonjour bonjour'
}]
const result = array
.filter((item) => {
return item.languageId === 1 ||
!array.some((innerItem) => ( //Inner loop that I want to avoid
innerItem.typeName === item.typeName &&
innerItem.orientation === item.orientation &&
innerItem.languageId === 1
))
})
.reduce((prev, current) => ({
...prev,
[current.typeName]: {
...prev[current.typeName],
[current.orientation]: [
...(((prev[current.typeName] || {})[current.orientation]) || []),
current.value
]
}
}), {});
console.log(result)
So the question is what's the best approach to avoid inner loop?
You could take a Set for prio items and prevent adding more items to the array.
const
array = [{ typeName: 'welcome', orientation: 'landscape', languageId: 1, value: 'Welcome' }, { typeName: 'welcome', orientation: 'landscape', languageId: 2, value: 'Bonjour' }, { typeName: 'welcome', orientation: 'portrait', languageId: 2, value: 'Bonjour bonjour' }],
hasPrio = new Set,
result = array.reduce((r, { typeName, orientation, languageId, value }) => {
var key = `${typeName}|${orientation}`;
if (!r[typeName]) r[typeName] = {};
if (languageId === 1) {
r[typeName][orientation] = [value];
hasPrio.add(key);
} else if (!hasPrio.has(key)) {
if (!r[typeName][orientation]) r[typeName][orientation] = [];
r[typeName][orientation].push(value);
}
return r;
}, {});
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
You can use a Set to keep track of whether you've seen a languageId === 1 for a particular typeName and orientation. Set is required to have sublinear performance:
Set objects must be implemented using either hash tables or other mechanisms that, on average, provide access times that are sublinear on the number of elements in the collection.
So a Set will outperform an inner loop. (You could also use an object if your keys are strings, which they are in my example below. If you do, create it with Object.create(null) so it doesn't inherit from Object.prototype.)
Then just a single straightforward loop rather than reduce:
const array = [{
typeName: 'welcome', orientation: 'landscape', languageId: 1, value: 'Welcome'
}, { typeName: 'welcome', orientation: 'landscape', languageId: 1, value: 'Bon dia'
}, {
typeName: 'welcome', orientation: 'landscape', languageId: 2, value: 'Bonjour'
}, {
typeName: 'welcome', orientation: 'portrait', languageId: 2, value: 'Bonjour bonjour'
}]
const sawPriority = new Set();
const result = {};
for (const {typeName, orientation, languageId, value} of array) {
const key = `${typeName}-${orientation}`;
const isPriority = languageId === 1;
let entry = result[typeName];
if (!entry) {
// No entry yet, easy peasy
entry = result[typeName] = {[orientation]: [value]};
if (isPriority) {
sawPriority.add(key);
}
} else {
const hasPriority = sawPriority.has(key);
if (hasPriority === isPriority) {
// Either the first for a new orientation, or a subsequent entry that
// matches the priority
const inner = entry[orientation];
if (inner) {
// Subsequent, add
inner.push(value);
} else {
// First for orientation, create
entry[orientation] = [value];
}
} else if (isPriority) {
// It's a new priority entry, overwrite
entry[orientation] = [value];
sawPriority.add(key);
}
}
}
console.log(result)
(In a comment, you mentioned that if there are multiple languageId === 1 entries, they should be built up in an array, so I've included a second one in the above to show that working.)
In the above, I'm comparing two boolean values like this:
if (hasPriority === isPriority) {
That works because I know that they're both actually booleans, not just truthy or falsy values, because isPriority is the result of a === comparison (guaranteed to be a boolean) and hasPriority is the result of calling has on Set (guaranteed to be a boolean).
If there were any question about whether one of them might be just a truthy or falsy value rather than definitely true or false, I'd've used ! on them to ensure they were booleans:
if (!hasPriority === !isPriority) {
See live demo here
const array = [{
typeName: 'welcome', orientation: 'landscape', languageId: 1, value: 'Welcome'
}, {
typeName: 'welcome', orientation: 'landscape', languageId: 2, value: 'Bonjour'
}, {
typeName: 'welcome', orientation: 'portrait', languageId: 2, value: 'Bonjour bonjour'
}];
let obj = array.reduce( (acc, {orientation, value})=>{
acc[orientation] = (acc[orientation] || []).concat(value);
return acc;
}, {});
output = {};
output[array[0].typeName] = obj;
console.log(output);

JavaScript Recursive Search On An Array Of Objects

I have an array of objects that have deeply nested children and sometimes children within children. I am attempting to handle this recursively, but I am getting stuck.
The goal of the function is to return a single data object that matches the id.
My Data looks like this:
data: [
{
id: 'RAKUFNUBNY00UBZ40950',
name: 'Grade 1 Cover',
activityId: 'RAKUFNUBNY00UBZ40950',
nodeType: 'activity',
suppressed: false,
hidden: false
},
{
children: [
{
id: 'SLWDYEQHTZAFA3ALH195',
name: 'Build Background Video',
activityId: 'SLWDYEQHTZAFA3ALH195',
nodeType: 'activity',
suppressed: false,
hidden: false,
assetReference: {
referenceId: 'UWFHA5A1E0EGKCM0W899',
assetType: 'image'
}
},
{
children: [
{
id: 'HQUCD2SSRKMYC2PJM636',
name: 'Eat or Be Eaten Splash Card',
activityId: 'HQUCD2SSRKMYC2PJM636',
nodeType: 'activity',
suppressed: false,
hidden: true
},
{
children: [
{
id: 'ZDTWEZFL13L8516VY480',
name: 'Interactive Work Text: Eat or Be Eaten',
activityId: 'ZDTWEZFL13L8516VY480',
nodeType: 'activity',
suppressed: false,
hidden: true,
defaultLaunchMode: 'modal'
}
],
My attempt at solving this is like this:
findNode(id, currentNode) {
console.log('id', id);
console.log('findNode', currentNode);
var i, currentChild, result, counter;
counter = 0;
console.log('first conditional statement', currentNode);
if (id && currentNode.id === id) {
return currentNode[0];
} else {
counter++;
// Use a for loop instead of forEach to avoid nested functions
// Otherwise "return" will not work properly
console.log('counter', counter);
console.log('currentNode', currentNode[counter]);
console.log('currentNode Children', currentNode.children);
for (i = counter; i < currentNode.children.length; i += 1) {
console.log(currentNode[i].children[i]);
currentChild = currentNode[i].children[i];
// Search in the current child
result = this.findNode(id, currentChild);
// Return the result if the node has been found
if (result !== false) {
return result;
}
}
// The node has not been found and we have no more options
return false;
}
}
The code above fails because I having an extremely difficult time keeping track of a counter to loop through everything.
I also added a sample picture of my data output to give you a better example of how my data is structured. Any help will be greatly appreciated.
You shouldn't need a counter to locate a single node with a matching id. Try this simpler approach:
function findNode (id, array) {
for (const node of array) {
if (node.id === id) return node;
if (node.children) {
const child = findNode(id, node.children);
if (child) return child;
}
}
}
It will return undefined if there is no match.
To avoid the need for manual iteration, you might consider using an array method like reduce instead - return the accumulator if it's truthy (that is, an object was found already), or return the object being iterated over if the ID matches, or recursively iterate over the object's children to find a match.
const data=[{id:'RAKUFNUBNY00UBZ40950',name:'Grade 1 Cover',activityId:'RAKUFNUBNY00UBZ40950',nodeType:'activity',suppressed:!1,hidden:!1},{children:[{id:'SLWDYEQHTZAFA3ALH195',name:'Build Background Video',activityId:'SLWDYEQHTZAFA3ALH195',nodeType:'activity',suppressed:!1,hidden:!1,assetReference:{referenceId:'UWFHA5A1E0EGKCM0W899',assetType:'image'}},{children:[{id:'HQUCD2SSRKMYC2PJM636',name:'Eat or Be Eaten Splash Card',activityId:'HQUCD2SSRKMYC2PJM636',nodeType:'activity',suppressed:!1,hidden:!0},{children:[{id:'ZDTWEZFL13L8516VY480',name:'Interactive Work Text: Eat or Be Eaten',activityId:'ZDTWEZFL13L8516VY480',nodeType:'activity',suppressed:!1,hidden:!0,defaultLaunchMode:'modal'}],}],}],}]
function findId(id, arr) {
return arr.reduce((a, item) => {
if (a) return a;
if (item.id === id) return item;
if (item.children) return findId(id, item.children);
}, null);
}
console.log(findId('HQUCD2SSRKMYC2PJM636', data));
If your ids are unique and finding an object by id is a common task, you might want to consider creating a lookup object to improve performance. Creating the lookup object is an O(n) task; afterwards, looking up an object by id is O(1).
const data = [ { id: 'RAKUFNUBNY00UBZ40950', name: 'Grade 1 Cover', activityId: 'RAKUFNUBNY00UBZ40950', nodeType: 'activity', suppressed: false, hidden: false }, { children: [ { id: 'SLWDYEQHTZAFA3ALH195', name: 'Build Background Video', activityId: 'SLWDYEQHTZAFA3ALH195', nodeType: 'activity', suppressed: false, hidden: false, assetReference: { referenceId: 'UWFHA5A1E0EGKCM0W899', assetType: 'image' } }, { children: [ { id: 'HQUCD2SSRKMYC2PJM636', name: 'Eat or Be Eaten Splash Card', activityId: 'HQUCD2SSRKMYC2PJM636', nodeType: 'activity', suppressed: false, hidden: true }, { children: [ { id: 'ZDTWEZFL13L8516VY480', name: 'Interactive Work Text: Eat or Be Eaten', activityId: 'ZDTWEZFL13L8516VY480', nodeType: 'activity', suppressed: false, hidden: true, defaultLaunchMode: 'modal' } ] } ] } ] } ];
const lookup = {};
const registerIds = a => {
a.forEach(o => {
if ('id' in o) {
lookup[o.id] = o;
} else if ('children' in o) {
registerIds(o.children)
}
});
}
registerIds(data);
console.log(lookup)
Sorry for my two cents, just want to add a universal method that includes nested arrays
const cars = [{
id: 1,
name: 'toyota',
subs: [{
id: 43,
name: 'supra'
}, {
id: 44,
name: 'prius'
}]
}, {
id: 2,
name: 'Jeep',
subs: [{
id: 30,
name: 'wranger'
}, {
id: 31,
name: 'sahara'
}]
}]
function searchObjectArray(arr, key, value) {
let result = [];
arr.forEach((obj) => {
if (obj[key] === value) {
result.push(obj);
} else if (obj.subs) {
result = result.concat(searchObjectArray(obj.subs, key, value));
}
});
console.log(result)
return result;
}
searchObjectArray(cars, 'id', '31')
searchObjectArray(cars, 'name', 'Jeep')
I hope this helps someone

In a key value pair how to print out a value that isn't null JavaScript

So in an example like this I'm trying to print out the names that don't have null in the 'information'
let files = [
{
name: 'untitled',
information: null
},
{
name: 'folder'
information: 'has storage'
},
{
name: 'new folder',
information: 'has 42 items'
},
The code that I've been trying to use is this one but it doesn't work when I'm trying to print out the names of the folders that don't have null
let info = files.filter((a) => {
if (a.information !== null )
return a
});
console.log(info)
When i put console.log(info.length) to see if it's actually taking in, how many of the items don't have the null in it. It does count the items but when I try to see if I can print out their names it only prints undefined
is there another way to do this?
filter return an array , if you want to print the name you again need to iterate over the array
let files = [{
name: 'untitled',
information: null
},
{
name: 'folder',
information: 'has storage'
},
{
name: 'new folder',
information: 'has 42 items'
}
]
let info = files.filter((a) => {
return a.information !== null;
}).forEach(function(item) {
console.log(item.name)
})
If you want only the name of the folders. Use map to extract just that, as seen below:
let files = [{
name: 'untitled',
information: null
},
{
name: 'folder',
information: 'has storage'
},
{
name: 'new folder',
information: 'has 42 items'
}
]
let info = files.filter((a) => {
return a.information !== null;
}).map((item)=> {
// Get only the names of the folder
return item.name;
});
console.log(info);
Try the following code if it helps.
let files = [
{
name: 'untitled',
information: null
},
{
name: 'folder'
information: 'has storage'
},
{
name: 'new folder',
information: 'has 42 items'
};
var getFilesWithInfo = function () {
var array = [];
for(var i=0; i<files.length;i++){
if(files[i].information){
array.push(files[i]
}
return array;
}
}
console.log(getFilesWithInfo());
You can do this in two simple steps with filter and map.
const files = [{
name: 'untitled',
information: null
},
{
name: 'folder',
information: 'has storage'
},
{
name: 'new folder',
information: 'has 42 items'
}
];
const fileNames = files
.filter(f => f.information !== null)
.map(f => f.name);
console.log(fileNames);

Categories