package nl.andrewlalis.util; import nl.andrewlalis.model.Student; import nl.andrewlalis.model.StudentTeam; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVRecord; import java.io.FileReader; import java.io.IOException; import java.util.*; import java.util.logging.Logger; public class TeamGenerator { private static final Logger logger = Logger.getLogger(TeamGenerator.class.getName()); static { logger.setParent(Logger.getGlobal()); } /** * Creates a list of teams by reading a CSV file of a certain format. The format for each column is as follows: * 1. Timestamp - The date and time the record was entered. * 2. Username - The email address. * 3. Name - The student's name. * 4. Student Number * 5. Github Username * 6. I have chosen a partner. (Yes / No) If yes: * 7. Your Partner's Student Number * @param filename The CSV file to load from. * @param teamSize The preferred teamsize used in creating teams. * @return A list of teams. * @throws IOException If the file is unable to be read. * @throws IllegalArgumentException If an invalid teamsize is given. */ public static List generateFromCSV(String filename, int teamSize) throws IOException, IllegalArgumentException { logger.fine("Generating teams of size " + teamSize); if (teamSize < 1) { logger.severe("Invalid team size."); throw new IllegalArgumentException("StudentTeam size must be greater than or equal to 1. Got " + teamSize); } logger.fine("Parsing CSV file."); Iterable records = CSVFormat.DEFAULT.withFirstRecordAsHeader().parse(new FileReader(filename)); logger.fine("Reading all records into map."); Map studentMap; try { studentMap = readAllStudents(records, teamSize); } catch (ArrayIndexOutOfBoundsException e) { logger.severe("StudentTeam size does not match column count in records."); throw new IllegalArgumentException("StudentTeam size does not match column count in records."); } logger.fine("Generating all valid teams from student map."); return generateAllValidTeams(studentMap, teamSize); } /** * Generates all teams, given a mapping of all students to their student numbers. It will first try to generate * teams from students' preferences, and then take all students who are not in a team, and merge them into as many * teams as possible, and grouping all remainder single students into one final team. * * The algorithm works as follows: * For each student, try to create a team from their preferred partner numbers. * Check if the team is valid by confirming that all their partners have the same preferred partners. * If that's true, add this team, and remove all students in it from the list of singles. * If it's not true, then the students will not be removed from the list of singles, and a warning is given. * After all students with preferred partners are placed in teams, the single students are merged, and their teams * are added afterwards. * * @param studentMap A mapping for each student to their student number. * @param teamSize The preferred maximum size for a team. * @return A list of teams, most of which are of teamSize size. */ private static List generateAllValidTeams(Map studentMap, int teamSize) { List singleStudents = new ArrayList<>(studentMap.values()); List studentTeams = new ArrayList<>(); int teamCount = 1; // For each student, try to make a team from its preferred partners. for (Map.Entry e : studentMap.entrySet()) { StudentTeam newTeam = e.getValue().getPreferredTeam(studentMap); logger.finest("Checking if student's preferred team is valid:\n" + newTeam); // Check if the team is of a valid size, and is not a duplicate. // Note that at this stage, singles are treated as studentTeams of 1, and thus not valid for any teamSize > 1. if (newTeam.isValid(teamSize)) { // We know that the team is valid on its own, so now we check if it has members identical to any team already created. boolean matchFound = false; for (StudentTeam team : studentTeams) { if (newTeam.hasSameMembers(team)) { matchFound = true; break; } } if (!matchFound) { // Once we know this team is completely valid, we remove all the students in it from the list of singles. newTeam.setId(teamCount++); singleStudents.removeAll(Arrays.asList(newTeam.getStudents())); studentTeams.add(newTeam); logger.fine("Created team:\n" + newTeam); } } } studentTeams.addAll(mergeSingleStudents(singleStudents, teamSize, teamCount)); return studentTeams; } /** * Given a list of single students, this method generates as many teams as possible. that are as close to the team * size as possible. * @param singleStudents A list of students who have no preferred partners. * @param teamSize The preferred team size. * @param teamIndex The current number used in assigning an id to the team. * @return A list of teams comprising of single students. */ private static List mergeSingleStudents(List singleStudents, int teamSize, int teamIndex) { List studentTeams = new ArrayList<>(); while (!singleStudents.isEmpty()) { StudentTeam t = new StudentTeam(); t.setId(teamIndex++); logger.fine("Creating new team of single students:\n" + t); while (t.memberCount() < teamSize && !singleStudents.isEmpty()) { Student s = singleStudents.remove(0); logger.finest("Single student: " + s); t.addMember(s); } studentTeams.add(t); logger.fine("Created team:\n" + t); } return studentTeams; } /** * Reads all the rows from the CSV file, and returns a map of students, using student number as the index. * @param records The records in the CSV file. * @param teamSize The preferred size of teams, or rather, the expected number of partners. * @return A map of all students in the file. * @throws ArrayIndexOutOfBoundsException if the teamSize does not work with the columns in the record. */ private static Map readAllStudents(Iterable records, int teamSize) throws ArrayIndexOutOfBoundsException { Map studentMap = new HashMap<>(); for (CSVRecord record : records) { logger.finest("Read record: " + record); List preferredIds = new ArrayList<>(); if (record.get(5).equals("Yes")) { int columnOffset = 6; for (int i = 0; i < teamSize-1; i++) { preferredIds.add(Integer.parseInt(record.get(columnOffset + i))); } } Student s = new Student(Integer.parseInt(record.get(3)), record.get(2), record.get(1), record.get(4), preferredIds); studentMap.put(s.getNumber(), s); } logger.fine("Read " + studentMap.size() + " students from records."); return studentMap; } }