How to recursively print list of employees and their respective organizational hierarchy?

Viewed 46

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: [] } }

4 Answers

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.

Related