1
\$\begingroup\$

This Java code project was submitted for a job opportunity but was marked down for showing "bad habits that could be difficult to unlearn" but I am mystified by what this means. Any takers please. How could it be approached differently or improved.

I have removed any reference to the company involved, but the challenge was framed this way:

Problem description

An employee survey has been conducted, and you've been asked to figure out useful insights from it. You are given the data as a flat CSV file for all employees across the company, containing at least the following columns:

divisionId, teamId, managerId, employeeId, firstName, lastName, birthdate

For example, one record (row) of the CSV file looks like the following:

1,7,3,24,Jon,Snow,1986-12-26

Objective

Based on the structure above, write a piece of code which takes the CSV above as input, and creates a JSON object that looks like the following:

{
 "divisions": {
    "#divisionId": {
        "teams": {
            "#teamId": {
                "managers": {
                    "#managerId": {
                        "employees": {
                            "#employeeId": {
                                "id": "#employeeId",
                                "firstName": "Jon",
                                "lastName": "Snow",
                                "birthdate": "1986-12-26"
                            }
                        }
                    }
                }
            }
        }
    }
 }
}

NOTE: You can find the data set as data.csv in the /data directory.

Questions

  1. What is the big-O runtime complexity of the algorithm you’ve just written?
  2. Can you write the code such that all IDs are in ascending order in the JSON output?
  3. Can you create this such that the list of employees is sorted by their full name? (bonus points if you create a mechanism for arbitrary sort order, e.g., variable/multiple sort fields, ascending and/or descending order)
  4. [BONUS] Can you calculate the average age across the company, divisions, teams, and managers?

NOTE: for each of the extra questions, you can create different command-line arguments that changes the mode of the application. However, this is only a suggestion, and are you free to take any alternative approach you may wish.

Requirements

Unless explicitly requested otherwise, we expect the following to be used:

  • Java 8
  • Gradle as the build system
  • Any necessary libraries (e.g., Jackson for JSON)

Code:

CSVData.Java

package org.challenge.csv;

import java.util.Arrays;
import java.util.List;

import com.fasterxml.jackson.annotation.JsonIgnore;

/*
 * Base class for CSV data
 *   
 */
public class CSVData {

    private transient final String[] fieldsInCSVHeader;

    protected CSVData(String[] fieldsInCSVHeader) {
        this.fieldsInCSVHeader = fieldsInCSVHeader;
    }

    @JsonIgnore
    public List<String> getHeaderFields() {
        return Arrays.asList(fieldsInCSVHeader);
    }

    public enum SortDirection {
        ASCENDING,
        DESCENDING
    }
}

CSVParser.java

package org.challenge.csv;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.List;
import java.util.Objects;

import static java.util.stream.Collectors.*;

import org.challenge.csv.survey.SurveyCSVParser;
import org.challenge.csv.survey.SurveyCSVParser.SurveyCSVData;

/**
 * Base class for CSV parsers
 *
 */
public class CSVParser {

    private final File csvFile;
    private final short minimumFieldsPerLine;
    private final String seperatorOfFields;

    private List<String> linesOfCSVFile;

    protected CSVParser(File csvFile, short minimumFieldsPerLine, String seperatorOfFields) {
        this.csvFile = csvFile;
        this.minimumFieldsPerLine = minimumFieldsPerLine;
        this.seperatorOfFields = seperatorOfFields;
    }

    public static Parser createSurveyParser(File csvFile, SurveyCSVData.Employee.SortOrder order, CSVData.SortDirection direction) {
        Objects.requireNonNull(csvFile);
        return new SurveyCSVParser(csvFile, order, direction);
    }
    public static Parser createSurveyParser(File csvFile) {
        return new SurveyCSVParser(csvFile, SurveyCSVData.Employee.SortOrder.ORIGINAL, CSVData.SortDirection.ASCENDING);
    }

    protected boolean fileExists() {
        return csvFile.exists() && csvFile.canRead();
    }
    protected boolean fileIsCorrectlyFormatted() {
        readFile();     
        return linesOfCSVFile.size() > 0 && linesOfCSVFile.get(0).split(seperatorOfFields).length >= minimumFieldsPerLine;
    }
    protected List<String> fileLines() {
        readFile();
        return linesOfCSVFile.stream().skip(1).collect(toList());
    }

    private synchronized void readFile()
    {
        try {
            if (null == linesOfCSVFile) {
                if (true == fileExists())
                    linesOfCSVFile = Files.readAllLines(csvFile.toPath());      // NOTE - BufferedReader may be preferred for very large files, can then process line by line or in chunks...
            }
        }
        catch (IOException e) {
            // NOTE - Retry in a limited loop, ...
            throw new RuntimeException("FAILED to read file content");
        }
    }
}

Parser.java

package org.challenge.csv;

import java.util.Optional;

/*
 * Interface defining CSV parser functions
 * 
 */
public interface Parser {
    /**
     * Parse CSV file into an object structure
     * @return CSV data object
     */
    Optional<CSVData> parse();
}

JSONWriter.java

package org.challenge.json;

import org.challenge.csv.CSVData;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;


/**
 * Class for writing JSON data from object graph via Jackson libraries
 *
 */
public final class JSONWriter {

    private final CSVData csvData;

    public JSONWriter(CSVData csvData) {
        this.csvData = csvData;
    }

    public String write() throws JsonProcessingException {

        ObjectMapper objectToJsonMapper = new ObjectMapper();
        String jsonStringRepresentation = objectToJsonMapper.writeValueAsString(csvData);       
        return jsonStringRepresentation;
    }
}

SurveyCSVParser.java

package org.challenge.csv.survey;

import java.io.File;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.StringTokenizer;
import java.util.TreeMap;

import org.challenge.csv.CSVData;
import org.challenge.csv.CSVParser;
import org.challenge.csv.Parser;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;

/**
 * Class for parsing CSV data related to employee survey
 *
 */
public final class SurveyCSVParser extends CSVParser implements Parser {

    private static final short MIN_TOKENS_PER_LINE = 7;
    private static final String SEPERATOR_OF_TOKENS = ",";

    private final SurveyCSVData.Employee.SortOrder sortOrderOfDataOrEmployees;
    private final CSVData.SortDirection sortDirectionOfEmployees;

    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-M-d");

    public SurveyCSVParser(File csvFile, SurveyCSVData.Employee.SortOrder sortOrderOfDataOrEmployees, CSVData.SortDirection sortDirectionOfEmployees) {
        super(csvFile, MIN_TOKENS_PER_LINE, SEPERATOR_OF_TOKENS);
        this.sortOrderOfDataOrEmployees = sortOrderOfDataOrEmployees;
        this.sortDirectionOfEmployees = sortDirectionOfEmployees;
    }

    @Override
    public Optional<CSVData> parse() {
        SurveyCSVData csvDataParsed = null;

        if (fileExists() && fileIsCorrectlyFormatted()) {
            List<String> linesOfCSV = fileLines();
            SurveyCSVData csvData = new SurveyCSVData(sortOrderOfDataOrEmployees);
            try {
                if (SurveyCSVData.Employee.SortOrder.ORIGINAL != sortOrderOfDataOrEmployees)
                    linesOfCSV.parallelStream().forEach(l -> processLineOfCSV(l, csvData, sortOrderOfDataOrEmployees, sortDirectionOfEmployees));       
                else
                    linesOfCSV.stream().forEach(l -> processLineOfCSV(l, csvData, sortOrderOfDataOrEmployees, sortDirectionOfEmployees));   
                csvDataParsed = csvData;
            }
            catch (Exception e) {                       
                throw new RuntimeException("FAILED to parse CSV file");     // NOTE - Should a "bad" line prevent the remainder of the parse?
            }
        }

        return Optional.ofNullable(csvDataParsed);
    }

    private static void processLineOfCSV(String line, SurveyCSVData data, SurveyCSVData.Employee.SortOrder sortOrderOfDataOrEmployees, CSVData.SortDirection sortDirectionOfEmployees)
    {
        StringTokenizer tokenizer = new StringTokenizer(line, SEPERATOR_OF_TOKENS);
        short indexOfTokenFound = 0;
        String divisionId = null, teamId = null, managerId = null, employeeId = null, lastName = null, firstName = null, birthdate = null;      
        while (tokenizer.hasMoreTokens() && indexOfTokenFound < MIN_TOKENS_PER_LINE) {

            String token = tokenizer.nextToken();
            switch (indexOfTokenFound) {
                case 0:
                    divisionId = token;
                    break;
                case 1:
                    teamId = token;
                    break;
                case 2:
                    managerId = token;
                    break;
                case 3:
                    employeeId = token;
                    break;
                case 4:
                    firstName = token;
                    break;
                case 5:
                    lastName = token;
                    break;
                case MIN_TOKENS_PER_LINE-1:
                    birthdate = token;
                    break;
                default:
                    assert false;
            }
            indexOfTokenFound++;
        }

        if (indexOfTokenFound >= MIN_TOKENS_PER_LINE)
            buildSurveyData(divisionId, teamId, managerId, employeeId, firstName, lastName, birthdate, data, sortOrderOfDataOrEmployees, sortDirectionOfEmployees);
    }

    private static synchronized void buildSurveyData(String divisionId, String teamId, String managerId, String employeeId, String firstName, String lastName, String birthdate, SurveyCSVData data, SurveyCSVData.Employee.SortOrder sortOrderOfDataOrEmployees, CSVData.SortDirection direction) 
    {
        Objects.requireNonNull(divisionId);
        Objects.requireNonNull(teamId);
        Objects.requireNonNull(managerId);
        Objects.requireNonNull(employeeId);
        Objects.requireNonNull(firstName);
        Objects.requireNonNull(lastName);
        Objects.requireNonNull(birthdate);

        Integer divisionIdBox = Integer.parseInt(divisionId);
        Integer teamIdBox = Integer.parseInt(teamId);
        Integer managerIdBox = Integer.parseInt(managerId);
        Integer employeeIdBox = Integer.parseInt(employeeId);

        if (false == data.divisions.containsKey(divisionIdBox))
             data.divisions.put(divisionIdBox, new SurveyCSVData.Division(divisionIdBox, sortOrderOfDataOrEmployees));
        SurveyCSVData.Division division = data.divisions.get(divisionIdBox);

        if (false == division.teams.containsKey(teamIdBox))
            division.teams.put(teamIdBox, division.createTeam(teamIdBox));
        SurveyCSVData.Team team = division.teams.get(teamIdBox);

        if (false == team.managers.containsKey(managerIdBox))
            team.managers.put(managerIdBox, team.createManager(managerIdBox, direction));
        SurveyCSVData.Manager manager = team.managers.get(managerIdBox);

        if (false == manager.employees.containsKey(employeeIdBox)) 
            manager.employees.put(employeeIdBox, manager.createEmployee(employeeIdBox, firstName, lastName, birthdate));        // NOTE - Duplicates will not be added more than once
    }

    /**
     * 
     * Class representing survey data
     *
     */
    public final static class SurveyCSVData extends CSVData {

        private static final short VERSION = 1;                 // NOTE - Good idea to apply version to data structures

        private Map<Integer, Division> divisions;

        public SurveyCSVData(Employee.SortOrder sortOrderOfDataOrEmployees) {
            super(new String[] {"divisionId", "teamId", "managerId", "employeeId", "lastName", "firstName", "birthdate"});
            if (Employee.SortOrder.ORIGINAL == sortOrderOfDataOrEmployees)
                divisions = new LinkedHashMap <>();
            else
                divisions = new TreeMap<>();
        }

        public void addDivision(Integer id, Division division) {
            Objects.requireNonNull(id); Objects.requireNonNull(division);
            divisions.put(id, division);
        }

        public Map<Integer, Division>  getDivisions() {
            return Collections.unmodifiableMap(divisions);  
        }

        /**
         * Class representing division in survey data
         */
        public final static class Division {

            private Map<Integer, Team> teams;

            private transient final Integer id;
            private final Employee.SortOrder sortOrderOfDataOrEmployees;

            public Division(Integer id, Employee.SortOrder sortOrderOfDataOrEmployees) {
                this.id = id;
                this.sortOrderOfDataOrEmployees = sortOrderOfDataOrEmployees;
                if (Employee.SortOrder.ORIGINAL == sortOrderOfDataOrEmployees)
                    teams = new LinkedHashMap <>();
                else
                    teams = new TreeMap<>();
            }

            @JsonIgnore
            public Integer getId() {
                return id; 
            }   

            public void addTeam(Integer id, Team team) {
                Objects.requireNonNull(id); Objects.requireNonNull(team);
                teams.put(id, team);
            }
            public Team createTeam(Integer id) {
                return new Team(id, sortOrderOfDataOrEmployees);
            }

            public Map<Integer, Team> getTeams() {
                return Collections.unmodifiableMap(teams);  
            }       
        }

        /**
         * Class representing team in survey data
         */
        public final static class Team {

            private Map<Integer, Manager> managers;

            private transient final Integer id;
            private final Employee.SortOrder sortOrderOfDataOrEmployees;

            public Team(Integer id, Employee.SortOrder sortOrderOfDataOrEmployees) {
                this.id = id;
                this.sortOrderOfDataOrEmployees = sortOrderOfDataOrEmployees;
                if (Employee.SortOrder.ORIGINAL == sortOrderOfDataOrEmployees)
                    managers = new LinkedHashMap <>();
                else
                    managers = new TreeMap<>();
            }

            @JsonIgnore
            public Integer getId() {
                return id; 
            }

            public void addManager(Integer id, Manager manager) {
                Objects.requireNonNull(id); Objects.requireNonNull(manager);
                managers.put(id, manager);
            }           
            public Manager createManager(Integer id, CSVData.SortDirection sortDirectionOfEmployees) {
                return new Manager(id, sortOrderOfDataOrEmployees, sortDirectionOfEmployees);
            }

            public Map<Integer, Manager> getManagers() {
                return Collections.unmodifiableMap(managers);   
            }           
        }

        /**
         * Class representing manager in survey data
         */
        public final static class Manager {

            private final Employee.SortOrder sortOrderOfDataOrEmployees;
            private final CSVData.SortDirection sortDirectionOfEmployees;

            private transient Map<Integer, Employee> employees;

            private transient final Integer id;

            public Manager(Integer id, Employee.SortOrder sortOrderOfDataOrEmployees, CSVData.SortDirection sortDirectionOfEmployees) {
                this.id = id;
                this.sortOrderOfDataOrEmployees = sortOrderOfDataOrEmployees;
                this.sortDirectionOfEmployees = sortDirectionOfEmployees;
                if (Employee.SortOrder.ORIGINAL == sortOrderOfDataOrEmployees)
                    employees = new LinkedHashMap <>();
                else
                    employees = new TreeMap<>();
            }

            @JsonIgnore
            public Integer getId() {
                return id; 
            }   

            public void addEmployee(Integer id, Employee employee) {
                Objects.requireNonNull(id); Objects.requireNonNull(employee);
                employees.put(id, employee);
            }
            public Employee createEmployee(Integer id, String firstName, String lastName, String birthdate) {
                return new Employee(id, firstName, lastName, birthdate);
            }

            public Map<Integer, Employee> getEmployees() {
                return Collections.unmodifiableMap(employees);  
            }

            @JsonProperty("employees")
            public Map<Integer, Employee> getOrderedEmployees() {

                Map<Integer, Employee> orderedMapOfEmployees;
                if ((Employee.SortOrder.ID == sortOrderOfDataOrEmployees && CSVData.SortDirection.ASCENDING == sortDirectionOfEmployees) || Employee.SortOrder.ORIGINAL == sortOrderOfDataOrEmployees)
                    orderedMapOfEmployees = employees;
                else {
                    Comparator<Integer> valueComparator = (k1, k2) -> {
                          Employee e1 = employees.get(k1);
                          Employee e2 = employees.get(k2);
                          int compare = 0;
                          if(null != e1 && null != e2) {
                              switch (sortOrderOfDataOrEmployees) {
                                case ID:
                                    compare = Integer.valueOf(e1.id).compareTo(Integer.valueOf(e2.id)); 
                                    break;      
                                case LASTNAME:
                                    compare = e1.lastName.compareTo(e2.lastName);       
                                    break;                          
                                case FIRSTNAME:
                                    compare = e1.firstName.compareTo(e2.firstName);     
                                    break;  
                                case BIRTHDATE:
                                    compare = e1.birthdate.compareTo(e2.birthdate);     
                                    break;
                                default:
                                    assert false;
                                    break;
                              }
                              if (CSVData.SortDirection.DESCENDING == sortDirectionOfEmployees)
                                  compare = -compare;
                          }
                          else
                              throw new NullPointerException("Comparator does not support null values");
                          return compare;
                    };

                    Map<Integer, Employee> sortedMapOfEmployees = new TreeMap<>(valueComparator);
                    sortedMapOfEmployees.putAll(employees);
                    orderedMapOfEmployees = sortedMapOfEmployees;
                }
                return orderedMapOfEmployees;
            }

            @Override                   
            public String toString() {
                return Objects.toString(employees); 
            }
        }

        /**
         * Class representing employee in survey data
         */
        public final static class Employee {

            private final int id;
            private final String firstName;
            private final String lastName;          
            private final String birthdate;

            private transient final LocalDate birthdateDateType;

            public Employee(int id, String firstName, String lastName, String birthdate) {
                this.id = id;
                this.firstName = firstName;
                this.lastName = lastName;
                this.birthdate = birthdate;
                this.birthdateDateType = LocalDate.parse(birthdate, FORMATTER);         // NOTE - Formatter is not thread safe
            }

            public int getId() {
                return id;
            }
            public String getFirstName() {
                return firstName;
            }
            public String getLastName() {
                return lastName;
            }
            public String getBirthdate() {
                return birthdate;
            }
            @JsonIgnore
            public LocalDate getBirthdateDateType() {
                return birthdateDateType;
            }

            @Override                   
            public String toString() {
                return "(id='" + id + "', firstName='" + firstName + "', lastName='" + lastName + "', birthdate='" + birthdate + "')";
            }

            public enum SortOrder {
                ORIGINAL,
                ID,
                LASTNAME,
                FIRSTNAME,
                BIRTHDATE
            }
        }       
    }
}

AgeCalculator.java

package org.challenge.analysis;

import java.time.Period;
import java.util.Optional;

/**
 * Interface for obtaining average age from survey data at different scopes
 *
 */
public interface AgeCalculator {

    /**
     * Calculate average age of employees within a specified scope
     * @param scope enum value
     * @param id of division, team or manager, can be not present for company scope
     * @return Period of time showing the average age
     * @exception  AgeCalculatorException if id is not present for non-company scope
     */
    Period getAverageAge(Scope scope, Optional<Integer> id) throws AgeCalculatorException;

    public enum Scope {
        COMPANY,
        DIVISION,           
        TEAM,
        MANAGER
    }

    /**
     * Exception class for age calculator
     */
    class AgeCalculatorException extends Exception {
        private static final long serialVersionUID = 1L;

        AgeCalculatorException(String message) {
            super(message);
        }
        AgeCalculatorException(String message, Exception inner) {
            super(message, inner);
        }
    }   
}

SurveyAnalyzer.java

package org.challenge.analysis;

import java.time.Duration;
import java.time.LocalDate;
import java.time.Period;
import java.util.Objects;
import java.util.Optional;

import org.challenge.csv.survey.SurveyCSVParser;

/**
 * Class implementing average age calculations on survey data  
 *
 */
public final class SurveyAnalyzer implements AgeCalculator {

    private final SurveyCSVParser.SurveyCSVData surveyData;

    public SurveyAnalyzer(SurveyCSVParser.SurveyCSVData surveyData) {
        this.surveyData = surveyData;
    }

    @Override
    public Period getAverageAge(Scope scope, Optional<Integer> id) throws AgeCalculator.AgeCalculatorException {

        if (AgeCalculator.Scope.COMPANY != scope && (null == id || false == id.isPresent()))
            throw new AgeCalculator.AgeCalculatorException("For non-COMPANY scope an identifier is required");

        long totalDaysAgeOfEmployeesInScope, totalEmployeesInScope;     
        switch (scope) {

            default:    
            //case COMPANY:
                totalDaysAgeOfEmployeesInScope = surveyData.getDivisions().values().parallelStream()
                        .flatMap(d -> d.getTeams().values().parallelStream())
                        .flatMap(t -> t.getManagers().values().parallelStream())
                        .flatMap(m -> m.getEmployees().values().parallelStream())
                        .mapToLong(e -> Duration.between(e.getBirthdateDateType().atStartOfDay(), LocalDate.now().atStartOfDay()).toDays()).sum();
                totalEmployeesInScope = surveyData.getDivisions().values().parallelStream()
                        .flatMap(d -> d.getTeams().values().parallelStream())
                        .flatMap(t -> t.getManagers().values().parallelStream())
                        .flatMap(m -> m.getEmployees().values().parallelStream())
                        .count();
                break;
            case DIVISION:
                totalDaysAgeOfEmployeesInScope = surveyData.getDivisions().values().parallelStream()
                        .filter(d -> Objects.equals(d.getId(), id.get()))
                        .flatMap(d -> d.getTeams().values().parallelStream())
                        .flatMap(t -> t.getManagers().values().parallelStream())
                        .flatMap(m -> m.getEmployees().values().parallelStream())
                        .mapToLong(e -> Duration.between(e.getBirthdateDateType().atStartOfDay(), LocalDate.now().atStartOfDay()).toDays()).sum();
                totalEmployeesInScope = surveyData.getDivisions().values().parallelStream()
                        .filter(d -> Objects.equals(d.getId(), id.get()))
                        .flatMap(d -> d.getTeams().values().parallelStream())
                        .flatMap(t -> t.getManagers().values().parallelStream())
                        .flatMap(m -> m.getEmployees().values().parallelStream())
                        .count();
                break;
            case TEAM:
                totalDaysAgeOfEmployeesInScope = surveyData.getDivisions().values().parallelStream()
                        .flatMap(d -> d.getTeams().values().parallelStream())
                        .filter(t -> Objects.equals(t.getId(), id.get()))
                        .flatMap(t -> t.getManagers().values().parallelStream())
                        .flatMap(m -> m.getEmployees().values().parallelStream())
                        .mapToLong(e -> Duration.between(e.getBirthdateDateType().atStartOfDay(), LocalDate.now().atStartOfDay()).toDays()).sum();
                totalEmployeesInScope = surveyData.getDivisions().values().parallelStream()
                        .flatMap(d -> d.getTeams().values().parallelStream())
                        .filter(t -> Objects.equals(t.getId(), id.get()))
                        .flatMap(t -> t.getManagers().values().parallelStream())
                        .flatMap(m -> m.getEmployees().values().parallelStream())
                        .count();
                break;
            case MANAGER:
                totalDaysAgeOfEmployeesInScope = surveyData.getDivisions().values().parallelStream()
                        .flatMap(d -> d.getTeams().values().parallelStream())
                        .flatMap(t -> t.getManagers().values().parallelStream())
                        .filter(m -> Objects.equals(m.getId(), id.get()))
                        .flatMap(m -> m.getEmployees().values().parallelStream())
                        .mapToLong(e -> Duration.between(e.getBirthdateDateType().atStartOfDay(), LocalDate.now().atStartOfDay()).toDays()).sum();
                totalEmployeesInScope = surveyData.getDivisions().values().parallelStream()
                        .flatMap(d -> d.getTeams().values().parallelStream())
                        .flatMap(t -> t.getManagers().values().parallelStream())
                        .filter(m -> Objects.equals(m.getId(), id.get()))
                        .flatMap(m -> m.getEmployees().values().parallelStream())
                        .count();
                break;
        }

        long averageAgeDays = 0;
        if (totalEmployeesInScope > 0)
            averageAgeDays = (long)Math.floor(totalDaysAgeOfEmployeesInScope / totalEmployeesInScope);      // NOTE - Some rounding down here to nearest day over all employees in scope
        Period averageAge = Period.between(LocalDate.now(), LocalDate.now().plusDays(averageAgeDays));
        return averageAge;
    }
}

Task1.java

package org.challenge;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Period;
import java.util.Objects;
import java.util.Optional;

import org.challenge.analysis.AgeCalculator;
import org.challenge.analysis.AgeCalculator.AgeCalculatorException;
import org.challenge.analysis.SurveyAnalyzer;
import org.challenge.csv.CSVData;
import org.challenge.csv.CSVParser;
import org.challenge.csv.Parser;
import org.challenge.csv.survey.SurveyCSVParser;
import org.challenge.json.JSONWriter;


/**
 * Main class with entry point
 *
 */
class Task1 {

    /**
     * Main entry point
     * @param args
     */
    public static void main(String[] args) {

        try {
            // Path to supplied CSV data file
            Path csvFilePath = Paths.get("data", "data.csv");

            // Process command-line arguments for sort order and direction
            org.challenge.csv.survey.SurveyCSVParser.SurveyCSVData.Employee.SortOrder employeeSortOrder = processSortOrder(args);
            org.challenge.csv.CSVData.SortDirection employeeSortDirection = processSortAscendingDescending(args);

            // Create the parser
            Parser csvParser = CSVParser.createSurveyParser(csvFilePath.toFile(), employeeSortOrder, employeeSortDirection);

            long timeBeforeWorkMs = System.nanoTime();

            // Parse into object structure
            Optional<CSVData> csvDataObjectsOrNull = csvParser.parse();     
            if (true == csvDataObjectsOrNull.isPresent())
            {
                CSVData csvDataObjects = csvDataObjectsOrNull.get();
                Objects.requireNonNull(csvDataObjects, "FAILED to parse CSV");

                // Create the writer
                JSONWriter writerofJson = new JSONWriter(csvDataObjects);

                // Write out objects as JSON
                String jsonStringRepresentation = writerofJson.write();
                Objects.requireNonNull(jsonStringRepresentation, "FAILED to output JSON");

                System.out.println("Processed in " + (System.nanoTime() - timeBeforeWorkMs) + "ms");

                // Dump JSON to console
                System.out.println("JSON formatted survey data");
                System.out.println(jsonStringRepresentation);                           // NOTE - Verify and pretty print JSON output at https://jsonlint.com/

                // Check we have survey data
                if (true == csvDataObjects instanceof SurveyCSVParser.SurveyCSVData) {

                    SurveyCSVParser.SurveyCSVData surveyData = (SurveyCSVParser.SurveyCSVData)csvDataObjects;

                    // Dump some sample object data to console
                    SurveyCSVParser.SurveyCSVData.Manager sampleManager = surveyData.getDivisions().get(1).getTeams().get(5).getManagers().get(1);
                    System.out.println("Division 1, Team 5, Manager 1 has employees: " + sampleManager);

                    try {
                        // Create survey data analyzer
                        AgeCalculator averageAgeCalculator = new SurveyAnalyzer(surveyData);
                        Period averageAge;

                        // Calculate some sample average ages and dump to console
                        averageAge = averageAgeCalculator.getAverageAge(AgeCalculator.Scope.COMPANY, Optional.empty());
                        System.out.println("Average age of employees in company: " + formatPeriod(averageAge));

                        averageAge = averageAgeCalculator.getAverageAge(AgeCalculator.Scope.DIVISION, Optional.of(1));      // NOTE - Samples only, not added to command line arguments
                        System.out.println("Average age of employees in division 1: " + formatPeriod(averageAge));

                        averageAge = averageAgeCalculator.getAverageAge(AgeCalculator.Scope.TEAM, Optional.of(12));
                        System.out.println("Average age of employees in team 12: " + formatPeriod(averageAge));

                        averageAge = averageAgeCalculator.getAverageAge(AgeCalculator.Scope.MANAGER, Optional.of(2));
                        System.out.println("Average age of employees under manager 2: " + formatPeriod(averageAge));
                    }
                    catch (AgeCalculatorException e) {
                        System.out.println("AGE EXCEPTION: " + e.toString());
                    }
                }
                else {
                    System.out.println("UNEXPECTED CSV data type");
                }
            }
            else {
                System.out.println("FAILED to parse CSV data");
            }

            System.out.flush();
        }
        catch (Exception e) {
            System.out.println("EXCEPTION: " + e.toString());
        }
    }

    private static org.challenge.csv.survey.SurveyCSVParser.SurveyCSVData.Employee.SortOrder processSortOrder(String[] args) {

        org.challenge.csv.survey.SurveyCSVParser.SurveyCSVData.Employee.SortOrder sortOrder = org.challenge.csv.survey.SurveyCSVParser.SurveyCSVData.Employee.SortOrder.ORIGINAL;

        if (args.length > 0) {
            try {
                sortOrder = Enum.valueOf(org.challenge.csv.survey.SurveyCSVParser.SurveyCSVData.Employee.SortOrder.class, args[0]);             
            }
            catch (IllegalArgumentException e) {
                System.out.println("FAILED to process sort order, defaulting to ORIGINAL");
            }
        }
        System.out.println("Sort order is " + sortOrder.name());

        return sortOrder;
    }

    private static org.challenge.csv.CSVData.SortDirection processSortAscendingDescending(String[] args) {

        org.challenge.csv.CSVData.SortDirection sortDirection = org.challenge.csv.CSVData.SortDirection.ASCENDING;

        if (args.length > 1) {
            if (true == "DESC".equalsIgnoreCase(args[1]))
                sortDirection = org.challenge.csv.CSVData.SortDirection.DESCENDING;         
        }
        System.out.println("Sort direction is " + sortDirection.name());

        return sortDirection;       
    }

    private static String formatPeriod(Period period) {
        String formattedPeriod = String.format("%d years, %d months, %d days", period.getYears(), period.getMonths(), period.getDays());
        return formattedPeriod;
    }
}

Tests:

CSVParserTests.java

package org.challenge.csv;

import static org.junit.Assert.*;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;

import org.challenge.csv.survey.SurveyCSVParser.SurveyCSVData;

import org.junit.Test;

public class CSVParserTests {

    @Test
    public void testParse_givenCSV_success() {

        Path csvFilePath = Paths.get("data", "data.csv");

        Parser csvParser = CSVParser.createSurveyParser(csvFilePath.toFile());
        assertNotNull(csvParser);

        Optional<CSVData> csvData = csvParser.parse();  
        assertNotNull(csvData.orElse(null));
        assertTrue(csvData.get() instanceof SurveyCSVData);
        SurveyCSVData surveyData = (SurveyCSVData)csvData.get();
        assertNotNull(surveyData.getDivisions());
        assertNotNull(surveyData.getDivisions().get(1));
    }

    @Test
    public void testParse_emptyCSV_success() {

        Path csvFilePath = Paths.get("data", "empty.csv");

        Parser csvParser = CSVParser.createSurveyParser(csvFilePath.toFile());
        assertNotNull(csvParser);

        Optional<CSVData> csvData = csvParser.parse();  
        assertNotNull(csvData.orElse(null));
        assertTrue(csvData.get() instanceof SurveyCSVData);
        SurveyCSVData surveyData = (SurveyCSVData)csvData.get();
        assertNotNull(surveyData.getDivisions());
        assertEquals(0, surveyData.getDivisions().size());
    }

    @Test
    public void testParse_extraCSV_success() {

        Path csvFilePath = Paths.get("data", "extra.csv");

        Parser csvParser = CSVParser.createSurveyParser(csvFilePath.toFile());
        assertNotNull(csvParser);

        Optional<CSVData> csvData = csvParser.parse();  
        assertNotNull(csvData.orElse(null));
        assertTrue(csvData.get() instanceof SurveyCSVData);
        SurveyCSVData surveyData = (SurveyCSVData)csvData.get();
        assertNotNull(surveyData.getDivisions());
        assertNotNull(surveyData.getDivisions().get(1));
    }

    @Test
    public void testParse_badCSV_failure() {

        Path csvFilePath = Paths.get("data", "badformat.csv");

        Parser csvParser = CSVParser.createSurveyParser(csvFilePath.toFile());
        assertNotNull(csvParser);

        Optional<CSVData> csvData = csvParser.parse();  
        assertNull(csvData.orElse(null));
    }

    @Test
    public void testParse_nonExistantCSV_failure() {

        Path csvFilePath = Paths.get("data", "missing.csv");

        Parser csvParser = CSVParser.createSurveyParser(csvFilePath.toFile());
        assertNotNull(csvParser);

        Optional<CSVData> csvData = csvParser.parse();  
        assertNull(csvData.orElse(null));
    }
}

JSONWriterTests.java

package org.challenge.json;

import static org.junit.Assert.*;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

import org.challenge.csv.CSVData;
import org.challenge.csv.survey.SurveyCSVParser;

import org.junit.Test;

import com.fasterxml.jackson.core.JsonProcessingException;

public class JSONWriterTests {

    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-M-d");

    @Test
    public void testWrite_success() {

        SurveyCSVParser.SurveyCSVData surveyData = buildSampleData();

        JSONWriter writerOfJson = new JSONWriter(surveyData);

        try {
            String jsonString = writerOfJson.write();
            assertNotNull(jsonString);
            assertTrue(jsonString.length() > 0);
        }
        catch (JsonProcessingException e) {
            fail("JSON processing failed");
        }
    }

    private SurveyCSVParser.SurveyCSVData buildSampleData() {

        SurveyCSVParser.SurveyCSVData surveyData = new SurveyCSVParser.SurveyCSVData(SurveyCSVParser.SurveyCSVData.Employee.SortOrder.ORIGINAL);

        SurveyCSVParser.SurveyCSVData.Division division = new SurveyCSVParser.SurveyCSVData.Division(1, SurveyCSVParser.SurveyCSVData.Employee.SortOrder.ORIGINAL);
        SurveyCSVParser.SurveyCSVData.Team team = division.createTeam(1);
        SurveyCSVParser.SurveyCSVData.Manager manager = team.createManager(1, CSVData.SortDirection.ASCENDING);
        SurveyCSVParser.SurveyCSVData.Employee employee = manager.createEmployee(1, "Stuart", "Mackintosh", LocalDate.now().minusDays(1).format(FORMATTER));

        manager.addEmployee(1, employee);
        team.addManager(1, manager);
        division.addTeam(1, team);
        surveyData.addDivision(1, division);

        return surveyData;
    }
}

SurveyAnalyzerTests.java

package org.challenge.analysis;

import static org.junit.Assert.*;

import java.time.LocalDate;
import java.time.Period;
import java.time.format.DateTimeFormatter;
import java.util.Optional;

import org.challenge.csv.CSVData;
import org.challenge.csv.survey.SurveyCSVParser;

import org.junit.Test;

public class SurveyAnalyzerTests {

    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-M-d");

    @Test
    public void test_CompanyScope_success() throws AgeCalculator.AgeCalculatorException {

        SurveyCSVParser.SurveyCSVData surveyData = buildSampleData();

        SurveyAnalyzer analysis = new SurveyAnalyzer(surveyData);
        Period period = analysis.getAverageAge(AgeCalculator.Scope.COMPANY, Optional.empty());
        assertNotNull(period);
        assertEquals(1, period.getDays());      
    }

    @Test
    public void test_NoEmployees_success() throws AgeCalculator.AgeCalculatorException {

        SurveyCSVParser.SurveyCSVData surveyData = buildEmptySampleData();

        SurveyAnalyzer analysis = new SurveyAnalyzer(surveyData);
        Period period = analysis.getAverageAge(AgeCalculator.Scope.COMPANY, Optional.empty());
        assertNotNull(period);
        assertTrue(period.equals(Period.ZERO));     
    }

    @Test
    public void test_DivisionScope_success() throws AgeCalculator.AgeCalculatorException {

        SurveyCSVParser.SurveyCSVData surveyData = buildSampleData();

        SurveyAnalyzer analysis = new SurveyAnalyzer(surveyData);
        Period period = analysis.getAverageAge(AgeCalculator.Scope.DIVISION, Optional.of(1));
        assertNotNull(period);
        assertEquals(1, period.getDays());      
    }

    @Test(expected=AgeCalculator.AgeCalculatorException.class)
    public void test_DivisionScope_failure() throws AgeCalculator.AgeCalculatorException {

        SurveyCSVParser.SurveyCSVData surveyData = buildEmptySampleData();

        SurveyAnalyzer analysis = new SurveyAnalyzer(surveyData);
        analysis.getAverageAge(AgeCalculator.Scope.DIVISION, null);     
    }

    private SurveyCSVParser.SurveyCSVData buildSampleData() {

        SurveyCSVParser.SurveyCSVData surveyData = new SurveyCSVParser.SurveyCSVData(SurveyCSVParser.SurveyCSVData.Employee.SortOrder.ORIGINAL);

        SurveyCSVParser.SurveyCSVData.Division division = new SurveyCSVParser.SurveyCSVData.Division(1, SurveyCSVParser.SurveyCSVData.Employee.SortOrder.ORIGINAL);
        SurveyCSVParser.SurveyCSVData.Team team = division.createTeam(1);
        SurveyCSVParser.SurveyCSVData.Manager manager = team.createManager(1, CSVData.SortDirection.ASCENDING);
        SurveyCSVParser.SurveyCSVData.Employee employee1 = manager.createEmployee(1, "Stuart", "Mackintosh", LocalDate.now().minusDays(1).format(FORMATTER));
        SurveyCSVParser.SurveyCSVData.Employee employee2 = manager.createEmployee(2, "Stuart L", "Mackintosh", LocalDate.now().minusDays(2).format(FORMATTER));

        manager.addEmployee(1, employee1);
        manager.addEmployee(2, employee2);
        team.addManager(1, manager);
        division.addTeam(1, team);
        surveyData.addDivision(1, division);

        return surveyData;
    }

    private SurveyCSVParser.SurveyCSVData buildEmptySampleData() {

        SurveyCSVParser.SurveyCSVData surveyData = new SurveyCSVParser.SurveyCSVData(SurveyCSVParser.SurveyCSVData.Employee.SortOrder.ORIGINAL);

        SurveyCSVParser.SurveyCSVData.Division division = new SurveyCSVParser.SurveyCSVData.Division(1, SurveyCSVParser.SurveyCSVData.Employee.SortOrder.ORIGINAL);
        SurveyCSVParser.SurveyCSVData.Team team = division.createTeam(1);
        SurveyCSVParser.SurveyCSVData.Manager manager = team.createManager(1, CSVData.SortDirection.ASCENDING);

        team.addManager(1, manager);
        division.addTeam(1, team);
        surveyData.addDivision(1, division);

        return surveyData;      
    }
}

Data.csv (excerpt):

divisionId,teamId,managerId,employeeId,firstName,lastName,birthdate
7,6,2,597,Terrill,Lindgren,1956-7-21
7,10,2,632,Cecile,Mante,1955-3-11
6,2,1,489,Audreanne,Labadie,1964-4-5
9,9,1,859,Vinnie,Mann,1974-11-20
7,7,1,607,Cecilia,Kunde,1997-7-18
2,9,2,134,Taryn,Bednar,1970-5-8
9,9,2,865,Helmer,Littel,1964-2-19
11,4,1,1071,Declan,Bailey,1972-8-7
5,8,1,476,Gladyce,Mills,1988-12-15
12,3,1,1157,Cyrus,Tillman,1980-2-19
7,12,2,651,Camryn,Ernser,1965-11-17
6,4,1,515,Kadin,Wehner,1989-11-5
7,4,1,570,Kirk,Rowe,1966-1-26
10,6,1,993,Ashlee,Wuckert,1956-1-9
13,2,2,1183,Anibal,Botsford,1972-7-25
7,6,2,598,Roscoe,Corkery,1954-5-16
10,7,1,1008,Branson,Hammes,1983-5-2
14,9,1,1308,Favian,Skiles,1981-9-13
14,10,1,1331,Kaelyn,Rosenbaum,1956-8-22
8,9,1,765,Shea,Osinski,1964-3-5
7,6,2,594,Helena,Lesch,1957-10-26
9,9,2,864,Orrin,Stiedemann,1951-10-19
5,8,1,475,Vivien,Kemmer,1981-7-20
5,3,1,432,Genoveva,Kassulke,1987-5-27
8,5,2,745,Verdie,Gerhold,1988-6-25
1,5,2,15,Stefan,Stokes,1978-11-24
8,4,1,731,Tyrel,McDermott,1992-12-9
2,10,1,139,Rickey,Hodkiewicz,1968-1-24
10,8,1,1017,Gianni,Morissette,1992-8-23
14,11,2,1342,Uriel,Halvorson,1965-9-9
7,10,2,630,Robert,Johnston,1955-10-27
14,12,2,1366,Rene,Carter,1988-9-23
2,1,1,51,Maggie,O'Kon,1998-5-22
2,11,1,156,Jerod,Walker,1971-8-3
13,2,1,1174,Margarette,Jacobi,1955-8-10
3,12,1,301,Cleo,Hudson,1989-1-28
2,13,1,183,Jaquan,Skiles,1957-9-10
9,13,1,904,Elfrieda,Langworth,1953-10-18
5,7,1,461,Torrey,Gislason,1998-1-23
2,7,1,102,Demond,Herman,1950-7-3
11,3,1,1057,Gracie,Rau,1957-6-9
14,7,1,1290,Ollie,Bogan,1951-10-4
7,8,1,610,Rahul,Spencer,1981-10-3
8,5,2,742,Bell,Orn,1963-8-19
8,6,1,752,Alisa,Corwin,1969-11-4
3,2,1,198,Angelina,Corwin,1969-2-2
3,5,2,243,Rigoberto,Runolfsson,1971-1-21
9,16,1,934,Elise,Hegmann,1964-2-5
9,17,1,935,Richmond,Cormier,1980-1-6
14,5,2,1273,Eino,O'Conner,1969-8-24
13,7,1,1214,Kathryn,Kub,1966-1-6
5,7,2,470,Golden,Reichert,1952-9-10
2,1,1,57,Dejah,Schaefer,1975-2-5
2,7,1,101,Ronny,McCullough,1994-6-2
10,6,2,1001,Nikki,Runolfsson,1961-10-5
8,4,1,730,Wayne,Ward,1997-4-9
7,3,1,569,Vivian,Muller,1969-7-31
5,6,1,450,Juvenal,Schmidt,1973-8-5
14,5,2,1274,Orrin,O'Keefe,1981-12-13
14,9,1,1313,Jazlyn,Walter,1992-4-13
1,4,1,7,Darwin,Collins,1975-4-11
9,13,1,901,Margarita,Spinka,1972-5-4
10,2,1,954,Stephen,Schmitt,1980-2-5
2,7,1,104,Westley,Swift,1989-10-19
3,14,1,321,Emelie,Simonis,1951-5-29
11,11,2,1126,Clement,Lemke,1990-10-20
7,14,2,680,Millie,Haag,1979-8-23
2,1,1,56,Marcelino,Will,1987-10-29
14,7,1,1288,Cheyanne,Labadie,1954-7-20
4,3,2,386,Owen,Turner,1986-2-7
3,1,1,193,Lynn,Huel,1963-8-17
14,11,2,1345,Evangeline,Becker,1995-10-9
6,6,1,535,Bridget,Rath,1959-9-12
3,10,1,277,Rodolfo,O'Kon,1951-3-30
12,2,1,1153,Vincent,Collins,1991-8-17
12,1,2,1145,Coralie,Olson,1956-9-14
9,16,1,933,Annabell,Wehner,1965-5-25
7,12,2,649,Davonte,Kohler,1970-6-30
2,12,1,170,Laury,Muller,1968-12-16
7,9,2,622,Norval,Gusikowski,1967-5-18
2,12,1,173,Jena,Conn,1999-5-23
11,3,1,1056,Kamren,Koch,1960-3-24
14,1,1,1242,Mckenna,Graham,1958-6-20
12,3,1,1155,Raegan,Doyle,1996-12-9
9,17,1,939,Lazaro,Swaniawski,1981-8-16
10,4,2,974,Astrid,Rath,1998-9-19
3,4,1,215,Scarlett,Watsica,1987-5-27
5,1,1,400,Jenifer,Stokes,1969-5-16
12,3,2,1164,Kobe,Wisozk,1958-3-24
8,11,1,788,Margret,Zemlak,1993-8-20
9,15,1,920,Sister,Braun,1960-11-9
5,1,1,402,Daniela,Pollich,1968-11-11
9,4,2,820,Cornell,Robel,1952-4-9
3,4,2,227,Marion,Flatley,1997-11-13
1,4,1,3,Arvel,Runolfsdottir,1954-9-5
6,3,1,498,Jazmyn,Hartmann,1986-9-4
5,3,1,433,Cloyd,Botsford,1995-12-23
9,16,1,932,Jerrell,Moore,1972-12-3
11,4,1,1072,Jefferey,Goldner,1984-5-3
14,5,1,1267,Nicolas,Davis,1988-2-5
14,7,1,1291,Demarco,Rolfson,1980-1-3
6,1,1,483,Katelin,Hintz,1955-3-2
14,11,1,1339,Jennings,Schowalter,1985-8-6
12,3,2,1159,Kamryn,Wyman,1998-8-17
3,1,1,187,Tremayne,Cummings,1998-1-14
3,13,1,316,D'angelo,Morar,1990-10-20
3,16,1,346,Marian,Mante,1955-2-27
7,13,1,664,Audreanne,Schoen,1987-9-16
14,2,1,1245,Rylan,Conroy,1951-6-8
5,3,1,429,Leola,Hansen,1997-5-6
10,6,1,991,Ahmad,Schinner,1966-2-11
6,6,1,536,Irma,Osinski,1988-11-29
13,2,1,1173,Sister,Heller,1984-9-13
10,8,1,1019,Margaret,Stokes,1960-8-8
14,11,2,1347,Noah,Brakus,1983-4-21
9,5,1,827,Christina,Feeney,1972-5-31
2,12,1,172,Elwyn,Upton,1971-11-9
2,10,1,143,Reva,Hand,1955-3-17
7,7,1,605,Evangeline,Schuster,1995-7-20
6,5,2,529,Albina,Koss,1981-2-12
7,16,1,698,Ronaldo,Rutherford,1983-11-22
9,12,2,899,Odessa,McClure,1958-6-5
3,4,2,219,Braulio,Gibson,1960-6-12
13,8,2,1232,Josiah,Reynolds,1963-7-30
3,5,1,229,Vidal,Schuppe,1963-2-12
2,1,1,50,Friedrich,Ortiz,1951-1-30
13,4,2,1199,Orin,Vandervort,1981-4-17
3,2,1,196,Chelsey,Boyer,1995-9-5
14,5,2,1276,Tracy,Leffler,1983-3-31
12,3,2,1161,Edythe,Sauer,1972-2-1
13,3,1,1186,Otilia,O'Reilly,1986-2-20
4,4,2,394,Taylor,Quitzon,1975-8-16
11,10,2,1112,Kayli,Mohr,1961-10-10
10,2,1,950,Tillman,Abshire,1972-11-11
13,3,2,1190,Nya,Klocko,1971-6-10
9,14,1,913,Pedro,D'Amore,1957-10-19
12,3,2,1165,Gunner,Hamill,1986-9-15
7,3,1,566,Bailey,Bayer,1965-2-7
3,15,1,339,Broderick,Hettinger,1998-6-18
14,10,1,1326,Carlos,Von,1978-10-21
3,16,2,349,Webster,Rodriguez,1987-7-2
3,16,1,348,Vallie,Wyman,1995-3-16
14,9,2,1318,Clifford,Leuschke,1959-8-30
7,8,1,612,Otto,Mante,1951-3-26
3,8,1,267,Kaleb,Rice,1963-12-25
9,17,1,942,Dave,Erdman,1968-5-23
14,4,1,1263,Lemuel,Osinski,1966-4-20
14,7,2,1292,Winnifred,Mraz,1964-11-29
11,9,1,1093,Ottilie,Gutmann,1990-11-2
9,3,1,797,Rocio,Fisher,1960-2-21
2,10,1,147,Tobin,Larkin,1987-12-27
11,3,1,1058,Brenden,Bechtelar,1981-8-2
13,8,1,1228,Irma,Bruen,1972-8-21
11,1,1,1043,Francisco,Hartmann,1967-6-4
4,3,1,377,Donato,Hyatt,1955-4-21
5,3,1,428,Alexanne,Parker,1965-1-9
11,1,1,1041,Eleanora,Littel,1985-4-19
7,14,1,674,Curt,Kshlerin,1996-12-2
11,10,2,1107,Yazmin,Williamson,1971-5-25
3,12,1,305,Bailee,Rodriguez,1965-5-14
11,2,1,1052,Mathew,McClure,1960-10-18
14,9,2,1317,Macie,Rath,1971-11-6
9,3,2,805,Joany,Sanford,1972-1-24
1,6,1,20,Manley,Bednar,1973-5-31
4,1,2,369,Twila,Stoltenberg,1981-8-8
1,3,2,1,Maeve,Corwin,1963-4-7
11,5,1,1075,Effie,Dooley,1997-6-28
14,4,1,1261,Whitney,Gibson,1982-8-16
2,8,1,117,Chris,Mann,1992-3-2
10,8,1,1014,Hans,Hauck,1953-10-29
10,5,1,979,Wilfredo,Kub,1993-1-9
5,6,1,446,Gabe,Walter,1954-9-23
3,2,1,197,Maxine,Oberbrunner,1996-8-8
9,12,1,888,Cecile,Adams,1963-7-23
2,11,1,153,Tyree,Lemke,1983-8-7
2,11,1,149,Mathew,Lehner,1967-12-3
1,5,1,11,Hipolito,Collins,1961-12-17
7,1,1,548,Assunta,Murazik,1993-5-27
1,7,1,31,Maye,Torphy,1956-12-11
8,9,1,767,Kamille,Kessler,1995-11-27
4,3,1,380,Derrick,Bergnaum,1996-4-20
11,9,2,1096,Hal,Price,1970-11-29
8,1,1,705,Jailyn,Predovic,1993-6-7
10,2,1,958,Jamaal,Buckridge,1997-4-12
14,1,1,1240,Dovie,Yundt,1995-8-17
8,8,1,760,Marisol,Beahan,1975-6-25
3,6,2,248,Jade,Haag,1950-10-21
13,8,1,1223,Gerry,Ziemann,1976-4-3
14,1,1,1237,Logan,Schneider,1977-5-31
7,16,1,700,Nikki,Daniel,1978-4-28
10,5,2,989,Art,Bernhard,1969-9-28
14,1,1,1234,Cassie,Aufderhar,1990-8-31
9,12,2,898,Kayden,Spinka,1986-1-9
11,3,2,1063,Faustino,Schamberger,1994-11-14
6,3,2,509,April,Williamson,1984-4-21
10,4,1,971,Cristina,DuBuque,1968-9-19
9,14,1,911,Roel,Flatley,1958-10-17
7,5,1,578,Magdalena,Cole,1986-2-23
8,3,1,718,Pat,Dach,1956-2-29
9,15,2,923,Kiana,Jenkins,1994-7-28
11,1,1,1042,Gene,West,1953-5-21
13,1,1,1170,Adelbert,Lockman,1991-10-11
1,4,1,4,Anthony,Armstrong,1957-4-9
1,7,2,35,Ramiro,Kohler,1973-9-6
14,5,2,1275,Reyes,Funk,1960-6-10
14,11,1,1334,Ellis,Roob,1951-9-18
2,9,2,138,Linnea,Blanda,1968-2-29
2,4,2,85,Elinor,Jakubowski,1999-4-29
9,13,2,909,Kristy,Orn,1963-12-9
9,12,1,893,Mandy,Howell,1985-11-9
14,6,1,1281,Marisa,Terry,1991-2-12
12,1,1,1133,Taryn,Predovic,1990-9-19
11,7,2,1083,Otto,Bergstrom,1955-6-8
14,11,2,1343,Howell,Moore,1956-9-27
11,3,2,1061,Sabina,Senger,1968-4-1
1,8,2,45,Gaston,Graham,1963-9-2
9,17,1,941,Brant,Halvorson,1970-12-28
3,11,2,292,Hallie,Schaefer,1974-8-6
9,6,1,832,Kara,Block,1974-2-5
4,4,1,390,Terrence,Effertz,1986-11-29
4,1,1,356,Maye,Bauch,1980-9-16
2,6,2,96,Brock,Rowe,1971-1-17
7,4,1,575,Jessie,Larkin,1977-6-24
14,4,1,1260,Carlie,Gerlach,1995-5-11
1,7,2,33,Kirsten,Reichel,1988-11-6
3,9,1,271,Roselyn,Jakubowski,1970-11-19
14,12,1,1360,Alene,Jacobi,1999-5-26
10,5,2,986,Newton,Volkman,1969-12-13
7,13,1,656,Soledad,Spencer,1993-12-31
3,5,1,230,Lucas,Emmerich,1977-9-29
14,8,2,1307,Lyla,Vandervort,1979-6-16
10,3,1,969,Vena,Conn,1985-3-6
9,11,1,877,Sally,Runolfsdottir,1960-10-6
11,11,1,1121,Caroline,Smitham,1979-8-4
9,1,1,792,Litzy,Tromp,1972-8-22
7,10,2,631,Reynold,Dare,1991-4-14
10,7,2,1011,Beverly,McLaughlin,1999-8-20
1,6,1,18,Jaylen,Cole,1975-10-6
2,13,1,182,Delia,Strosin,1968-11-29
8,9,1,764,Lavina,Koch,1993-9-16
2,4,2,87,Marvin,Lehner,1956-11-1
10,2,1,956,Prince,Schroeder,1979-5-9
2,9,1,129,Emmie,Auer,1969-6-19
8,5,2,743,Antoinette,Legros,1986-5-17
9,7,1,847,Murphy,Jenkins,1955-12-2
11,11,2,1124,Quinton,Romaguera,1973-12-28
12,3,2,1166,Martine,Stanton,1977-3-1
8,4,2,737,Nova,Sporer,1993-4-5
3,13,1,314,Gilberto,Kuhic,1970-4-17
10,1,2,948,Faye,Wisoky,1958-12-11
7,12,2,650,Aleen,O'Connell,1987-9-21
10,5,1,975,Sandrine,Hegmann,1980-6-27
4,3,1,379,Shanna,Mann,1977-7-4
1,5,2,14,Eugenia,Nicolas,1976-5-5
13,2,1,1175,Heaven,Lang,1962-12-29
7,6,1,593,Hans,Fahey,1964-3-30
3,14,2,330,Alexa,Muller,1964-12-20
7,9,2,616,Elwyn,Russel,1966-5-21
10,2,1,953,Adolphus,Koch,1975-7-25
9,10,1,870,Rose,Walker,1950-6-15
3,3,2,212,Jairo,Smith,1980-11-5
8,11,1,781,Haylee,Stiedemann,1980-12-8
2,8,1,118,Alysson,Wisoky,1982-10-2
13,7,1,1217,Jace,Monahan,1995-12-10
14,5,2,1278,Danny,Kautzer,1967-12-30
1,6,2,26,Madyson,Bednar,1972-5-27
11,1,2,1047,Annalise,Lind,1982-7-23
1,5,2,13,Orlo,Wuckert,1991-2-14
9,15,2,924,Nicole,Balistreri,1950-10-5
7,6,2,596,Abigayle,Bogisich,1977-7-4
8,2,1,713,Malcolm,Spencer,1982-11-24
10,1,2,949,Eugene,Barrows,1983-7-30
6,6,2,546,Edward,Crist,1996-4-30
9,8,1,856,Percival,Bogan,1968-2-16
2,7,2,109,Gaetano,Rosenbaum,1979-1-30
14,11,2,1349,Eulalia,Nader,1958-4-18
1,8,1,37,Remington,Ratke,1988-11-15
7,15,1,687,Vada,Hansen,1960-10-13
4,3,2,387,Rogers,Larkin,1988-7-20
5,2,1,419,Arne,Ernser,1971-1-24
4,2,1,372,Orin,Quitzon,1995-1-26
11,9,2,1099,Roscoe,Collier,1990-6-1
13,7,1,1218,Chet,Wyman,1953-7-13
2,3,2,67,Aubree,Marvin,1979-6-6
3,15,1,337,Clare,Runolfsson,1969-9-9
14,11,2,1348,Urban,Hamill,1963-2-12
9,7,2,848,Hulda,Kautzer,1971-7-29
5,2,2,424,Neha,Jenkins,1961-8-22
3,4,1,214,Shawna,Boyle,1991-12-11
10,7,2,1012,Ivory,Davis,1987-5-16
11,8,1,1085,Esteban,Powlowski,1954-12-25
8,9,2,773,Dannie,Bogisich,1949-11-2
8,9,1,768,Aylin,Sporer,1990-12-30
2,2,1,59,Tianna,Kilback,1995-11-19
13,8,1,1226,Verla,Lehner,1969-4-14
2,12,1,169,Tina,Becker,1966-8-5
12,1,2,1142,Cassie,Littel,1956-10-4
5,3,1,430,Naomi,Stiedemann,1953-11-24
10,5,2,987,Deonte,Larson,1980-3-5
2,6,2,95,Odie,Halvorson,1992-11-5
2,13,1,181,Yolanda,Leannon,1996-3-2
10,5,1,980,Rachel,West,1978-3-24
8,10,2,777,Gladys,Lakin,1978-8-23
12,3,1,1156,Eugene,Farrell,1975-2-22
7,13,2,668,Ryley,Berge,1989-11-3
14,10,1,1324,Martine,Becker,1961-4-15
6,1,1,484,Greyson,Welch,1956-3-7
3,3,1,205,Cade,Hessel,1990-2-5
13,3,1,1188,Federico,Bins,1976-6-29
1,4,1,2,Lauren,Keeling,1969-12-27
2,10,1,144,Brandt,Torp,1960-8-1
10,2,1,951,Joey,Abernathy,1973-3-20
3,3,2,209,Alayna,Orn,1988-7-28
4,1,1,359,Jensen,Beier,1956-12-9
3,5,2,238,Victor,Murray,1983-6-3
2,11,2,167,Lauriane,Hodkiewicz,1952-2-25
9,13,1,906,Mayra,Heidenreich,1969-9-8
14,4,1,1257,Ova,Torp,1955-12-26
11,10,2,1111,Verla,Oberbrunner,1959-7-9
3,14,2,327,Brianne,Schoen,1975-7-29
7,1,2,559,Agustin,Pouros,1997-3-25
10,6,2,999,Earline,Becker,1964-1-22
13,2,1,1177,Genevieve,Kutch,1984-3-23
3,7,1,261,Gussie,Emmerich,1971-2-12
9,4,1,818,Louisa,Strosin,1953-8-16
10,5,2,985,Okey,Fisher,1960-12-2
2,13,1,179,Aylin,Kshlerin,1967-10-21
10,3,1,966,Sonya,Hagenes,1977-5-13
14,11,1,1338,Murl,Boehm,1986-2-4
7,5,2,581,Jamie,Aufderhar,1990-12-28
2,12,1,168,Keegan,Collins,1978-8-24
7,13,1,657,Connor,Kessler,1981-7-1
11,8,2,1091,Lauren,Hoeger,1958-1-1
10,4,1,970,Martina,Greenholt,1978-10-20
5,8,1,472,Clinton,Maggio,1968-5-31
9,14,1,917,Esther,Nitzsche,1989-7-24
2,10,1,142,Rashad,Ortiz,1950-3-5
4,3,2,385,Jacky,Crona,1989-6-18
12,2,1,1149,Neoma,Schamberger,1981-3-23
2,7,1,105,Giovanni,Wuckert,1989-4-28
14,4,1,1256,Zackary,Jaskolski,1961-11-14
13,4,1,1197,Sally,Stokes,1975-3-29
5,1,1,404,Terrence,Purdy,1969-6-15
7,14,1,672,Margret,Bradtke,1984-1-24
7,6,2,602,Maureen,Stark,1994-12-16
9,16,1,931,Heather,Goyette,1951-7-10
9,11,1,881,Rosario,Kohler,1969-3-24
2,6,2,98,Gerry,Daugherty,1986-6-22
2,7,2,111,Destany,Jacobs,1981-7-31
9,15,1,922,Caitlyn,Nikolaus,1949-12-16
14,5,1,1265,Fanny,Sawayn,1956-6-30
7,10,1,624,Vincenzo,Kozey,1975-10-20
7,4,1,574,Ciara,Prosacco,1958-11-14
5,7,1,465,Chandler,Borer,1957-3-12
10,6,2,998,Kraig,Ortiz,1953-8-6
5,1,1,405,Waldo,Swaniawski,1951-7-30
7,10,2,635,Jeramy,Kiehn,1981-4-10
2,8,1,119,Dedrick,Yundt,1973-2-26
2,2,1,63,Eugene,Kuhlman,1960-8-11
9,9,2,863,Dedrick,Conn,1965-5-11
14,5,1,1270,Susie,Labadie,1957-10-27
9,3,1,802,Ora,Mohr,1956-1-17
10,5,1,981,Delphine,Lindgren,1980-11-22
3,17,1,352,Chanel,Dare,1980-5-19
2,4,1,78,Shany,Kessler,1983-10-12
2,11,2,162,Loyal,Mertz,1964-11-21
8,10,1,775,Hettie,Kris,1969-1-21
14,1,1,1238,Jon,Pagac,1973-1-16
8,4,1,728,Katrina,Kovacek,1962-5-29
3,12,1,304,Jaime,Barrows,1968-2-25
14,11,1,1337,Casey,Gibson,1988-10-15
12,1,2,1140,Melany,Blanda,1978-1-11
5,2,1,415,Nelda,Hartmann,1973-8-30
10,6,1,990,Nelle,Gislason,1988-2-15
3,12,1,306,Gunnar,Hartmann,1977-7-6
11,6,1,1080,Hoyt,Nikolaus,1987-4-21
...

My instructions/answers:

Eclipse Photon project for Challenge

Requirements:

  • Java 8
  • Gradle 4.3
  • Jackson 2.4
  • JUnit 4.12

Build with Gradle:

Gradlew.bat build

Execute main in /src/main/java/org/challenge/Task1.java, possible arguments to control the data sort order and employee sort order are:

[ORIGINAL/ID/LASTNAME/FIRSTNAME/BIRTHDATE] [ASC/DESC]

e.g.

java ... org.challenge.Test1 LASTNAME DESC
java ... org.challenge.Test1 ID
java ... org.challenge.Test1

Run unit tests under /src/test/java/*:

Gradlew.bat test

For Javadoc:

Gradlew.bat javadoc

Questions

  1. Time complexity is linear or O(n) without sorting, i.e. ORIGINAL, each line of the CSV file is processed once, map insertions and gets are O(1) for unsorted hash maps or O(log n) for sorted maps. Space complexity is O(n).
  2. Use ID command-line argument to obtain all data sorted by ID, the argument ORIGINAL (default) will use CSV data order.
  3. Use ID, LASTNAME, FIRSTNAME or BIRTHDATE arguments to set sort order of employees, optionally with ASC or DESC to set sort direction, the default is ascending.
  4. The program will output some calculated average ages for the company and one sample division, team and manager at the end. I didn't add command-line arguments for this.

Assumptions

Employee birthdate is always in yyyy-M-D format. The CSV file can be read entirely into available heap. A badly formatted CSV row will end the parsing operation. File reading will not be retried. Integer data type can contain all IDs. Long data type can contain the total age in days of all employees during the scope of the average age calculation. Average age of all employees in scope is rounded down to the nearest day.

\$\endgroup\$
0

1 Answer 1

4
\$\begingroup\$

There are indeed a few weird/bad practices in your code.

  • Error handling: Broadly speaking, there are two categories of things that can go wrong when running this program: I/O errors and malformed input. You handle both of those poorly.

    • IOException: In CSVParser.fileExists(), you verify in advance that the file exists and is readable. First of all, the method does more than its name suggests. However, you shouldn't be performing those tests at all. Most importantly, your CSVParser.readFile() method should not be catching IOException at all, and certainly should not be rethrowing it as a degraded RuntimeException, which contains less information about what went wrong.

      There are many conditions that could lead to an IOException, besides the file not existing or your process lacking the permission to read it. The disk could spontaneously fail. The file might be on a network share, and there might be a network timeout. Don't try to screen for all the possible failure conditions. Even if you did check in advance, you might still encounter an error when you actually work with the file (if, for example, someone deletes the file at just the right moment).

    • Optional<CSVData>: Under certain conditions (if the CSV file does not exist, or you lack read permission, or the data is malformed), SurveyCSVParser.parse() returns a null wrapped in an Optional. That makes no sense: those conditions should trigger an exception instead of causing a null to be returned. (What is Optional good for? If you have a function that finds the minimum value of a list, then the Optional forces the caller to prepare for the possibility that there is no well defined minimum, if the list is empty. There's no result, but it's not really an error.)

      Under other conditions, it throws a RuntimeException instead. It's not clear why that would happen.

    A more reasonable design would be to have your methods throw the right kind of exception for each error condition. CSVParser.readFile() should simply declare that it throws IOException — then you do away with the fileExists() check, and get rid of the catch (IOException e) clause. You should define a class MalformedCSVException extends Exception, and throw it if you encounter a line of bad data during normal processing. Finally, SurveyCSVParser.parse() should just return a CSVData instead of an Optional<CSVData>.

  • Object-oriented design: Your CSVParser isn't actually a CSV parser. It's just a utility that opens a file for reading, splits it into lines, and counts commas per line.

    Your SurveyCSVParser extends CSVParser. I don't see any reason to use inheritance there. The survey-specific code simply needs to use a CSV parser; it doesn't need to be a CSV parser.

    Furthermore, there is a circular dependency here. A generic CSV parser shouldn't know anything about what it's to be used for, and therefore shouldn't contain any mentions of SurveyCSVData or SurveyCSVParser.

  • Generalization: I'm not convinced that each level of the tree needs to have its own class (SurveyCSVParser.SurveyCSVData.Division, SurveyCSVParser.SurveyCSVData.Team, etc.). All you need is a generalized container that knows its type (e.g. "division"), its ID, and can contain a sortable list of nested members.

    In fact, I'd like to see a smarter approach to sorting that involves writing less code. If you think of the CSV file as a database table, you should be able to do write a generalized Comparator that lets you specify, for example, that you want to sort by column 1 ascending, then column 3 descending, then column 2 ascending.

  • Null handling: In CSVParser.buildSurveyData(), you validate that the parameters are non-null. But Integer.parseInt(divisionId) would naturally crash anyway if divisionId is null, so why bother with an explicit check?

  • synchronized and transient: The CSVParser.readFile() and SurveyCSVParser.buildSurveyData() methods are synchronized. I'm not sure why. It doesn't look like the code is thread-safe in general, so it looks like you threw those in for no reason.

    Similarly, some of your instance variables are declared transient, or even transient final. Why? I suspect that you wrote those modifiers out of superstition.

  • Looping: SurveyCSVParser.processLineOfCSV() has a while loop that contains a switch:

    String divisionId = null, teamId = null, managerId = null, employeeId = null, lastName = null, firstName = null, birthdate = null;
    while (tokenizer.hasMoreTokens() && indexOfTokenFound < MIN_TOKENS_PER_LINE) {
    
        String token = tokenizer.nextToken();
        switch (indexOfTokenFound) {
            case 0:
                divisionId = token;
                break;
            case 1:
                teamId = token;
                break;
            case 2:
                managerId = token;
                break;
            case 3:
                employeeId = token;
                break;
            case 4:
                firstName = token;
                break;
            case 5:
                lastName = token;
                break;
            case MIN_TOKENS_PER_LINE-1:
                birthdate = token;
                break;
            default:
                assert false;
        }
        indexOfTokenFound++;
    }
    if (indexOfTokenFound >= MIN_TOKENS_PER_LINE)
        buildSurveyData(divisionId, teamId, managerId, employeeId, firstName, lastName, birthdate, data, sortOrderOfDataOrEmployees, sortDirectionOfEmployees);
    

    Why not just write…

    try {
        String divisionId = tokenizer.nextToken();
        String teamId = tokenizer.nextToken();
        String managerId = tokenizer.nextToken();
        String firstName = tokenizer.nextToken();
        String lastName = tokenizer.nextToken();
        String birthdate = tokenizer.nextToken();
        if (tokenizer.hasMoreTokens()) {
            throw new MalformedCSVException("Extra field in CSV");
        }
        buildSurveyData(divisionId, teamId, managerId, employeeId, firstName, lastName, birthdate, data, sortOrderOfDataOrEmployees, sortDirectionOfEmployees);
    } catch (NoSuchElementException missingField) {
        throw new MalformedCSVException("Missing field in CSV");
    }
    

    Note that no extra verification is necessary, and no magic numbers (MIN_TOKENS_PER_LINE) are necessary. The processing just happens naturally, and you throw an exception as you encounter an error.

\$\endgroup\$
1
  • \$\begingroup\$ Thanks for your time in reviewing, I take all the points you make especially on exception handling and OOP (prefer containment over inheritance). I made the build function synchronised because I had a parallel stream and a formatter being used (not thread safe?). I considered a single type for divisions, teams, etc. but didn't know how to get the correct JSON serialization from such a class without fixed JsonProperty annotation. You are right on the token handling too I think but I didn't want to throw on more tokens being present because of the way the question was phrased. \$\endgroup\$
    – Stuart
    Oct 19, 2018 at 8:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.