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
- What is the big-O runtime complexity of the algorithm you’ve just written?
- Can you write the code such that all IDs are in ascending order in the JSON output?
- 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)
- [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
- 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).
- Use ID command-line argument to obtain all data sorted by ID, the argument ORIGINAL (default) will use CSV data order.
- 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.
- 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.
1 Answer 1
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
: InCSVParser.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, yourCSVParser.readFile()
method should not be catchingIOException
at all, and certainly should not be rethrowing it as a degradedRuntimeException
, 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 anull
wrapped in anOptional
. That makes no sense: those conditions should trigger an exception instead of causing anull
to be returned. (What isOptional
good for? If you have a function that finds the minimum value of a list, then theOptional
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 itthrows IOException
— then you do away with thefileExists()
check, and get rid of thecatch (IOException e)
clause. You should define aclass MalformedCSVException extends Exception
, and throw it if you encounter a line of bad data during normal processing. Finally,SurveyCSVParser.parse()
should just return aCSVData
instead of anOptional<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
extendsCSVParser
. 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
orSurveyCSVParser
.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. ButInteger.parseInt(divisionId)
would naturally crash anyway ifdivisionId
is null, so why bother with an explicit check?synchronized
andtransient
: TheCSVParser.readFile()
andSurveyCSVParser.buildSurveyData()
methods aresynchronized
. 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 eventransient final
. Why? I suspect that you wrote those modifiers out of superstition.Looping:
SurveyCSVParser.processLineOfCSV()
has awhile
loop that contains aswitch
: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.
-
\$\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– Stuart2018年10月19日 08:37:24 +00:00Commented Oct 19, 2018 at 8:37