You are given a list of strings containing data about an organization structure.
Input example:
const employeeData = [
'Alice,Heidi,Engineering Manager,Vancouver',
'Bob,Grace,Product Director,SF',
'Charlie,Bob,Product Manager,Tempe',
'David,Alice,Software Developer,Bangalore',
'Eve,Heidi,Principal Engineer,SF',
'Frank,Bob,Designer,SF',
'Grace,Grace,CEO,SF',
'Heidi,Grace,CTO,SF',
'Ivan,Grace,Operations Director,SF',
]
For example, 'Alice,Heidi,Engineering Manager,Vancouver' means that
Alice reports to Heidi, and Alice is an Engineering Manager located in Vancouver.
Please build a function to print out an org chart in the following format:
Grace [CEO, SF]
Bob [Product Director, SF]
Charlie [Product Manager, Tempe]
Frank [Designer, SF]
Heidi [CTO, SF]
Alice [Engineering Manager, Vancouver]
David [Software Developer, Bangalore]
Eve [Principal Engineer, SF]
Ivan [Operations Director, SF]
This is what I've written so far, but am having trouble coming up with the right logic to search through the object and print out the hierarchy. I know that I'll probably need to utilize recursion to iterate through the nested object, but I'm getting a bit tripped up on what the exact logic needs to look like.
function printOrgChart(employeeData) {
const results = {};
const formatted = employeeData.map((employee) => employee.split(','));
for (let i = 0; i < formatted.length; i++) {
let person = formatted[i][0];
let manager = formatted[i][1];
let role = formatted[i][2];
let location = formatted[i][3];
console.log(results);
if (results.hasOwnProperty(manager)) {
results[manager]['reports'].push(person);
} else {
results[manager] = {
details: [],
reports: [person],
};
}
if (results.hasOwnProperty(person)) {
results[person]['details'].push(role, location);
} else {
results[person] = {
details: [role, location],
reports: [],
};
}
}
console.log(results);
}
This is what I have so far:
{ Heidi: { details: [ 'CTO', 'SF' ], reports: [ 'Alice', 'Eve' ] },
Alice:
{ details: [ 'Engineering Manager', 'Vancouver' ],
reports: [ 'David' ] },
Grace:
{ details: [ 'CEO', 'SF' ],
reports: [ 'Bob', 'Grace', 'Heidi', 'Ivan' ] },
Bob:
{ details: [ 'Product Director', 'SF' ],
reports: [ 'Charlie', 'Frank' ] },
Charlie: { details: [ 'Product Manager', 'Tempe' ], reports: [] },
David: { details: [ 'Software Developer', 'Bangalore' ], reports: [] },
Eve: { details: [ 'Principal Engineer', 'SF' ], reports: [] },
Frank: { details: [ 'Designer', 'SF' ], reports: [] },
Ivan: { details: [ 'Operations Director', 'SF' ], reports: [] } }
Once you've constructed the results, you can identify the root name by iterating through the entries and finding the one who has their own name in their reports array. After that, it's simple to pass that onto a function that logs a person's details, with a specified indent, then iterates through that person's reports and does the same thing with an increased indent.
const rootName = Object.entries(results).find(([name, { reports }]) => reports.includes(name))[0];
display(results, rootName);
const display = (results, name, indent = 0) => {
const { details, reports } = results[name];
console.log(`${' '.repeat(indent)}${name} [${details[0]}, ${details[1]}]`);
for (const reportsToThis of reports) {
if (reportsToThis !== name) {
display(results, reportsToThis, indent + 3);
}
}
};
const employeeData = [
'Alice,Heidi,Engineering Manager,Vancouver',
'Bob,Grace,Product Director,SF',
'Charlie,Bob,Product Manager,Tempe',
'David,Alice,Software Developer,Bangalore',
'Eve,Heidi,Principal Engineer,SF',
'Frank,Bob,Designer,SF',
'Grace,Grace,CEO,SF',
'Heidi,Grace,CTO,SF',
'Ivan,Grace,Operations Director,SF',
]
function printOrgChart(employeeData) {
const results = {};
const formatted = employeeData.map((employee) => employee.split(','));
for (let i = 0; i < formatted.length; i++) {
let person = formatted[i][0];
let manager = formatted[i][1];
let role = formatted[i][2];
let location = formatted[i][3];
if (results.hasOwnProperty(manager)) {
results[manager]['reports'].push(person);
} else {
results[manager] = {
details: [],
reports: [person],
};
}
if (results.hasOwnProperty(person)) {
results[person]['details'].push(role, location);
} else {
results[person] = {
details: [role, location],
reports: [],
};
}
}
const rootName = Object.entries(results).find(([name, { reports }]) => reports.includes(name))[0];
display(results, rootName);
}
const display = (results, name, indent = 0) => {
const { details, reports } = results[name];
console.log(`${' '.repeat(indent)}${name} [${details[0]}, ${details[1]}]`);
for (const reportsToThis of reports) {
if (reportsToThis !== name) {
display(results, reportsToThis, indent + 3);
}
}
};
printOrgChart(employeeData)
Your approach creates a good data structure that maps managers to their reports, but the missing piece is code to walk the structure as a tree and print it formatted to the specification.
You can do this with recursion or a stack. A depth parameter is incremented for every recursive call, enabling you to compute the correct padding at each level.
const employeeData = [
'Alice,Heidi,Engineering Manager,Vancouver',
'Bob,Grace,Product Director,SF',
'Charlie,Bob,Product Manager,Tempe',
'David,Alice,Software Developer,Bangalore',
'Eve,Heidi,Principal Engineer,SF',
'Frank,Bob,Designer,SF',
'Grace,Grace,CEO,SF',
'Heidi,Grace,CTO,SF',
'Ivan,Grace,Operations Director,SF',
];
const reports = {};
let root;
for (const e of employeeData) {
const [name, mgr, pos, loc] = e.split(",");
if (!reports[mgr]) {
reports[mgr] = [];
}
if (name === mgr) {
root = {name, pos, loc};
}
else {
reports[mgr].push({name, pos, loc});
}
}
const print = ({name, pos, loc}, tree, depth=0) => {
const pad = " ".repeat(depth * 2);
console.log(`${pad}${name} [${pos}, ${loc}]`);
tree[name]?.forEach(e => print(e, tree, depth + 1));
};
print(root, reports);
Your data structure is correctly built. To identify the root of the tree, spot the employee that is its own manager and in that case don't push that (reflexive) relation into their reports array (so to avoid a circular reference).
For the output format you can use a recursive function that takes the expected indenting as extra argument.
In below solution I also rewrote your structure building code, so we have two functions:
makeTree: building the hierarchical data structure (no printing), which as a result returns the root node.
treeToString: returns the data as a string in the output format.
const employeeData = ['Alice,Heidi,Engineering Manager,Vancouver','Bob,Grace,Product Director,SF','Charlie,Bob,Product Manager,Tempe','David,Alice,Software Developer,Bangalore','Eve,Heidi,Principal Engineer,SF','Frank,Bob,Designer,SF','Grace,Grace,CEO,SF','Heidi,Grace,CTO,SF','Ivan,Grace,Operations Director,SF'];
function makeTree(employeeData) {
const employees = Object.fromEntries(
employeeData.map(csv => csv.split(","))
.map(([person, manager, ...details]) =>
[person, {person, manager, details, reports: []}]
)
);
let root;
for (const employee of Object.values(employees)) {
if (employee.manager == employee.person) root = employee;
else employees[employee.manager].reports.push(employee);
}
return root;
}
function treeToString({person, details, reports}, indent="") {
return `${indent}${person} [${details.join(", ")}]\n`
+ reports.map(member => treeToString(member, indent+" ")).join("");
}
console.log(treeToString(makeTree(employeeData)));
I would choose a slightly different intermediate representation. I would make the individual employee's nodes somewhat flatter, moving up the title and location fields to their node (removing details) but also make the whole thing deeper by nesting the employee's direct reports as whole nodes rather than just the string keys. It might look like this:
{
emp: "Grace",
title: "CEO",
loc: "SF",
reports: [
{
emp: "Bob",
title: "Product Director",
loc: "SF",
reports: [
{
emp: "Charlie",
title: "Product Manager",
loc: "Tempe",
reports: []
},
{
emp: "Frank",
title: "Designer",
loc: "SF",
reports: []
}
]
},
/* ... */
]
}
In order to do this, I would do a similar breakdown as the other answers suggest. In this case there are three functions:
const toTree = (
rows,
[emp, mgr, title, loc] = rows .find (([emp, mgr]) => emp == mgr),
reports = rows .filter ((e) => e[1] == emp && e[0] !== emp)
) => ({emp, title, loc, reports: reports .map ((r) => toTree (rows, r))})
const parseEmployees = (ss) =>
toTree (employeeData .map (s => s .split (',')))
const display = ({emp, loc, title, reports}, indent = '') =>
`${indent}${emp} [${title}, ${loc}]\n` +
reports .map (rep => display (rep, indent + ' ')).join ('')
const employeeData = ['Alice,Heidi,Engineering Manager,Vancouver', 'Bob,Grace,Product Director,SF', 'Charlie,Bob,Product Manager,Tempe', 'David,Alice,Software Developer,Bangalore', 'Eve,Heidi,Principal Engineer,SF', 'Frank,Bob,Designer,SF', 'Grace,Grace,CEO,SF', 'Heidi,Grace,CTO,SF', 'Ivan,Grace,Operations Director,SF']
console .log (display (parseEmployees (employeeData)))
.as-console-wrapper {max-height: 100% !important; top: 0}
display takes the structure I describe above and turns it into a useful output string. Notice the separation of concerns here; even this function doesn't call console.log, letting its caller do this.
parseEmployees splits the input string into separate arrays such as ['Alice', 'Heidi', 'Engineering Manager', 'Vancouver'], then calls toTree to turn this into the format we've described.
toTree is the recursive function, looking first for the root node by checking a self-manager situation, turning it into a useful object, and finding its direct reports with recursive calls.
The breakdown between the latter two functions could be altered in many interesting ways. But in the end, you have a structure that describes the whole organization.
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);
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)
}));
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/