-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
create aggregateObjects and flattenObject helpers (#29)
* create aggregateObjects and flattenObject helpers * update package version * nit * fix pre-commit * add commments
- Loading branch information
1 parent
db7e059
commit 84d82fb
Showing
7 changed files
with
374 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
// eslint-disable-next-line eslint-comments/disable-enable-pair | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
|
||
/** | ||
* | ||
* Aggregates multiple objects into a single object by combining values of matching keys. | ||
* For each key present in any of the input objects, creates a comma-separated string | ||
* of values from all objects. | ||
* @param param - the objects to aggregate and the aggregation method | ||
* @returns a single object containing all unique keys with aggregated values | ||
* @example | ||
* const obj1 = { name: 'John', age: 30 }; | ||
* const obj2 = { name: 'Jane', city: 'NY' }; | ||
* const obj3 = { name: 'Bob', age: 25 }; | ||
* | ||
* // Without wrap | ||
* aggregateObjects({ objs: [obj1, obj2, obj3] }) | ||
* // Returns: { name: 'John,Jane,Bob', age: '30,,25', city: ',NY,' } | ||
* | ||
* // With wrap | ||
* aggregateObjects({ objs: [obj1, obj2, obj3], wrap: true }) | ||
* // Returns: { name: '[John],[Jane],[Bob]', age: '[30],[],[25]', city: '[],[NY],[]' } | ||
*/ | ||
export const aggregateObjects = ({ | ||
objs, | ||
wrap = false, | ||
}: { | ||
/** the objects to aggregate in a single one */ | ||
objs: any[]; | ||
/** whether to wrap the concatenated values in a [] */ | ||
wrap?: boolean; | ||
}): any => { | ||
const allKeys = Array.from( | ||
new Set( | ||
objs.flatMap((a) => (a && typeof a === 'object' ? Object.keys(a) : [])), | ||
), | ||
); | ||
|
||
// Reduce into a single object, where each key contains concatenated values from all input objects | ||
return allKeys.reduce( | ||
(acc, key) => { | ||
const values = objs | ||
.map((o) => (wrap ? `[${o?.[key] ?? ''}]` : o?.[key] ?? '')) | ||
.join(','); | ||
acc[key] = values; | ||
return acc; | ||
}, | ||
{} as Record<string, any>, | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
// eslint-disable-next-line eslint-comments/disable-enable-pair | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
|
||
import { aggregateObjects } from './aggregateObjects'; | ||
|
||
/** | ||
* | ||
* Flattens a nested object into a single-level object with concatenated key names. Useful | ||
* for converting objects to CSV. | ||
* @param param - The information about the object to flatten | ||
* @returns A flattened object where nested keys are joined with underscores | ||
* @example | ||
* const nested = { | ||
* user: { | ||
* name: 'John', | ||
* address: { | ||
* city: 'NY', | ||
* zip: 10001 | ||
* }, | ||
* hobbies: ['reading', 'gaming'] | ||
* } | ||
* }; | ||
* | ||
* flattenObject(nested) | ||
* // Returns: { | ||
* // user_name: 'John', | ||
* // user_address_city: 'NY', | ||
* // user_address_zip: 10001, | ||
* // user_hobbies: 'reading,gaming' | ||
* // } | ||
*/ | ||
export const flattenObject = ({ | ||
obj, | ||
prefix = '', | ||
}: { | ||
/** the object to flatten */ | ||
obj: any; | ||
/** The prefix to prepend to keys (used in recursion) */ | ||
prefix?: string; | ||
}): any => | ||
!obj | ||
? {} | ||
: Object.keys(obj ?? []).reduce( | ||
(acc, key) => { | ||
const newKey = prefix ? `${prefix}_${key}` : key; | ||
const entry = obj[key]; | ||
|
||
// Handle arrays of objects | ||
if ( | ||
Array.isArray(entry) && | ||
entry.length > 0 && | ||
entry.some((item) => typeof item === 'object' && item !== null) | ||
) { | ||
// Flatten each object in the array | ||
const objEntries = entry.filter( | ||
(item) => typeof item === 'object' && item !== null, | ||
); | ||
const flattenedObjects = objEntries.map((item) => | ||
flattenObject({ obj: item }), | ||
); | ||
// Aggregate the flattened objects | ||
const aggregated = aggregateObjects({ objs: flattenedObjects }); | ||
// Add prefix to all keys | ||
Object.entries(aggregated).forEach(([k, v]) => { | ||
acc[`${newKey}_${k}`] = v; | ||
}); | ||
} | ||
// Handle regular objects | ||
else if ( | ||
typeof entry === 'object' && | ||
entry !== null && | ||
!Array.isArray(entry) | ||
) { | ||
Object.assign(acc, flattenObject({ obj: entry, prefix: newKey })); | ||
} | ||
// Handle primitive arrays and other values | ||
else { | ||
acc[newKey] = Array.isArray(entry) | ||
? entry | ||
.map((e) => { | ||
if (typeof e === 'string') { | ||
return e.replaceAll(',', ''); | ||
} | ||
return e ?? ''; | ||
}) | ||
.join(',') | ||
: typeof entry === 'string' | ||
? entry.replaceAll(',', '') | ||
: entry ?? ''; | ||
} | ||
return acc; | ||
}, | ||
{} as Record<string, any>, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import { expect } from 'chai'; | ||
import { aggregateObjects } from '../aggregateObjects'; | ||
|
||
describe.only('aggregateObjects', () => { | ||
it('should return empty object for empty input array', () => { | ||
const result = aggregateObjects({ objs: [] }); | ||
expect(result).to.deep.equal({}); | ||
}); | ||
|
||
it('should aggregate objects with same keys', () => { | ||
const objs = [ | ||
{ name: 'John', age: 30 }, | ||
{ name: 'Jane', age: 25 }, | ||
{ name: 'Bob', age: 35 }, | ||
]; | ||
const result = aggregateObjects({ objs }); | ||
expect(result).to.deep.equal({ | ||
name: 'John,Jane,Bob', | ||
age: '30,25,35', | ||
}); | ||
}); | ||
|
||
it('should handle missing properties with missing keys', () => { | ||
const objs = [ | ||
{ name: 'John', age: 30 }, | ||
{ name: 'Jane' }, | ||
{ name: 'Bob', age: 35 }, | ||
]; | ||
const result = aggregateObjects({ objs }); | ||
expect(result).to.deep.equal({ | ||
name: 'John,Jane,Bob', | ||
age: '30,,35', | ||
}); | ||
}); | ||
|
||
it('should handle null and undefined values', () => { | ||
const objs = [ | ||
{ name: 'John', age: null }, | ||
{ name: undefined, hobby: 'reading' }, | ||
{ name: 'Bob', hobby: null }, | ||
]; | ||
const result = aggregateObjects({ objs }); | ||
expect(result).to.deep.equal({ | ||
name: 'John,,Bob', | ||
age: ',,', | ||
hobby: ',reading,', | ||
}); | ||
}); | ||
|
||
it('should wrap values in brackets when wrap option is true', () => { | ||
const objs = [ | ||
{ name: 'John', age: 30 }, | ||
{ name: 'Jane' }, | ||
{ name: 'Bob', age: 35 }, | ||
]; | ||
const result = aggregateObjects({ objs, wrap: true }); | ||
expect(result).to.deep.equal({ | ||
name: '[John],[Jane],[Bob]', | ||
age: '[30],[],[35]', | ||
}); | ||
}); | ||
|
||
it('should handle objects with different keys', () => { | ||
const objs = [ | ||
{ name: 'John', age: 30 }, | ||
{ city: 'NY', country: 'USA' }, | ||
{ name: 'Bob', country: 'UK' }, | ||
]; | ||
const result = aggregateObjects({ objs }); | ||
expect(result).to.deep.equal({ | ||
name: 'John,,Bob', | ||
age: '30,,', | ||
city: ',NY,', | ||
country: ',USA,UK', | ||
}); | ||
}); | ||
|
||
it('should handle empty objects', () => { | ||
const objs = [{ name: 'John' }, {}, { name: 'Bob' }]; | ||
const result = aggregateObjects({ objs }); | ||
expect(result).to.deep.equal({ | ||
name: 'John,,Bob', | ||
}); | ||
}); | ||
|
||
it('should handle array with null or undefined objects', () => { | ||
const objs = [{ name: 'John' }, null, undefined, { name: 'Bob' }]; | ||
const result = aggregateObjects({ objs }); | ||
expect(result).to.deep.equal({ | ||
name: 'John,,,Bob', | ||
}); | ||
}); | ||
|
||
it('should handle numeric and boolean values', () => { | ||
const objs = [ | ||
{ count: 1, active: true }, | ||
{ count: 2, active: false }, | ||
{ count: 3, active: true }, | ||
]; | ||
const result = aggregateObjects({ objs }); | ||
expect(result).to.deep.equal({ | ||
count: '1,2,3', | ||
active: 'true,false,true', | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import { expect } from 'chai'; | ||
import { flattenObject } from '../flattenObject'; | ||
|
||
describe('flattenObject', () => { | ||
it('should return empty object for null input', () => { | ||
const result = flattenObject({ obj: null }); | ||
expect(result).to.deep.equal({}); | ||
}); | ||
|
||
it('should return empty object for undefined input', () => { | ||
let obj; | ||
const result = flattenObject({ obj }); | ||
expect(result).to.deep.equal({}); | ||
}); | ||
|
||
it('should flatten list of objects with some entries missing properties', () => { | ||
// create a list of users with one of them missiging the age property | ||
const obj = { | ||
users: [ | ||
{ | ||
name: 'Bob', | ||
}, | ||
{ | ||
name: 'Alice', | ||
age: 18, | ||
}, | ||
], | ||
}; | ||
const result = flattenObject({ obj }); | ||
// the flattened object should include the missing age property | ||
expect(result).to.deep.equal({ | ||
users_name: 'Bob,Alice', | ||
users_age: ',18', | ||
}); | ||
}); | ||
|
||
it('should ignore primitive and null types within list containing objects', () => { | ||
// create a list of a mix of objects, strings, and null | ||
const obj = { | ||
users: [ | ||
'[email protected]', | ||
null, | ||
{ | ||
name: 'Bob', | ||
}, | ||
{ | ||
name: 'Alice', | ||
age: 18, | ||
}, | ||
], | ||
}; | ||
const result = flattenObject({ obj }); | ||
// the flattened object should not include the strings | ||
expect(result).to.deep.equal({ | ||
users_name: 'Bob,Alice', | ||
users_age: ',18', | ||
}); | ||
}); | ||
|
||
it('should flatten an object with null entries', () => { | ||
const obj = { | ||
user: { | ||
siblings: null, | ||
}, | ||
}; | ||
const result = flattenObject({ obj }); | ||
expect(result).to.deep.equal({ | ||
user_siblings: '', | ||
}); | ||
}); | ||
|
||
it('should flatten an object with empty entries', () => { | ||
const obj = { | ||
user: { | ||
siblings: [], | ||
}, | ||
}; | ||
const result = flattenObject({ obj }); | ||
expect(result).to.deep.equal({ | ||
user_siblings: '', | ||
}); | ||
}); | ||
|
||
it('should flatten a deep nested object', () => { | ||
const obj = { | ||
user: { | ||
name: 'John', | ||
address: { | ||
city: 'NY', | ||
zip: 10001, | ||
}, | ||
hobbies: ['reading', 'gaming'], | ||
parents: [ | ||
{ | ||
name: 'Alice', | ||
biological: true, | ||
age: 52, | ||
}, | ||
{ | ||
name: 'Bob', | ||
biological: false, | ||
}, | ||
], | ||
siblings: [], | ||
grandParents: null, | ||
}, | ||
}; | ||
const result = flattenObject({ obj }); | ||
expect(result).to.deep.equal({ | ||
user_name: 'John', | ||
user_address_city: 'NY', | ||
user_address_zip: 10001, | ||
user_hobbies: 'reading,gaming', | ||
user_parents_name: 'Alice,Bob', | ||
user_parents_biological: 'true,false', | ||
user_parents_age: '52,', | ||
user_siblings: '', | ||
user_grandParents: '', | ||
}); | ||
}); | ||
}); |