    001package org.cpsolver.studentsct.report;
    002
    003import java.io.File;
    004import java.text.DecimalFormat;
    005import java.util.ArrayList;
    006import java.util.HashMap;
    007import java.util.HashSet;
    008import java.util.List;
    009import java.util.Map;
    010import java.util.Set;
    011
    012import org.cpsolver.coursett.Constants;
    013import org.cpsolver.coursett.model.Placement;
    014import org.cpsolver.coursett.model.RoomLocation;
    015import org.cpsolver.coursett.model.TimeLocation;
    016import org.cpsolver.ifs.assignment.Assignment;
    017import org.cpsolver.ifs.assignment.DefaultSingleAssignment;
    018import org.cpsolver.ifs.util.CSVFile;
    019import org.cpsolver.ifs.util.DataProperties;
    020import org.cpsolver.ifs.util.DistanceMetric;
    021import org.cpsolver.ifs.util.CSVFile.CSVField;
    022import org.cpsolver.studentsct.StudentSectioningModel;
    023import org.cpsolver.studentsct.StudentSectioningXMLLoader;
    024import org.cpsolver.studentsct.extension.DistanceConflict;
    025import org.cpsolver.studentsct.extension.StudentQuality;
    026import org.cpsolver.studentsct.extension.TimeOverlapsCounter;
    027import org.cpsolver.studentsct.extension.TimeOverlapsCounter.Conflict;
    028import org.cpsolver.studentsct.model.AreaClassificationMajor;
    029import org.cpsolver.studentsct.model.Choice;
    030import org.cpsolver.studentsct.model.Config;
    031import org.cpsolver.studentsct.model.Course;
    032import org.cpsolver.studentsct.model.CourseRequest;
    033import org.cpsolver.studentsct.model.Enrollment;
    034import org.cpsolver.studentsct.model.FreeTimeRequest;
    035import org.cpsolver.studentsct.model.Offering;
    036import org.cpsolver.studentsct.model.Request;
    037import org.cpsolver.studentsct.model.SctAssignment;
    038import org.cpsolver.studentsct.model.Section;
    039import org.cpsolver.studentsct.model.Student;
    040import org.cpsolver.studentsct.model.Subpart;
    041import org.cpsolver.studentsct.model.Unavailability;
    042import org.cpsolver.studentsct.model.Request.RequestPriority;
    043import org.cpsolver.studentsct.model.Student.StudentPriority;
    044
    045/**
    046 * This class computes solution statistics report.
    047 * <br>
    048 * <br>
    049 *
    050 * Usage: new SolutionStatsReport(model).create(assignment, config).save(aFile);
    051 *
    052 * <br>
    053 * <br>
    054 *
    055 * @author  Tomáš Müller
    056 * @version StudentSct 1.3 (Student Sectioning)<br>
    057 *          Copyright (C) 2013 - 2025 Tomáš Müller<br>
    058 *          <a href="mailto:muller@unitime.org">muller@unitime.org</a><br>
    059 *          <a href="http://muller.unitime.org">http://muller.unitime.org</a><br>
    060 * <br>
    061 *          This library is free software; you can redistribute it and/or modify
    062 *          it under the terms of the GNU Lesser General Public License as
    063 *          published by the Free Software Foundation; either version 3 of the
    064 *          License, or (at your option) any later version. <br>
    065 * <br>
    066 *          This library is distributed in the hope that it will be useful, but
    067 *          WITHOUT ANY WARRANTY; without even the implied warranty of
    068 *          MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
    069 *          Lesser General Public License for more details. <br>
    070 * <br>
    071 *          You should have received a copy of the GNU Lesser General Public
    072 *          License along with this library; if not see
    073 *          <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>.
    074 */
    075public class SolutionStatsReport extends AbstractStudentSectioningReport {
    076    protected static DecimalFormat sIntFormat = new DecimalFormat("#,##0");
    077    protected static DecimalFormat sPercentFormat = new DecimalFormat("0.00");
    078    protected static DecimalFormat sDoubleFormat = new DecimalFormat("0.00");
    079
    080    public SolutionStatsReport(StudentSectioningModel model) {
    081        super(model);
    082    }
    083
    084    public static interface StudentFilter {
    085        public boolean matches(Student student);
    086    }
    087
    088    public static class NotFilter implements StudentFilter {
    089        StudentFilter iFilter;
    090        public NotFilter(StudentFilter filter) {
    091            iFilter = filter;
    092        }
    093        @Override
    094        public boolean matches(Student student) {
    095            return !iFilter.matches(student);
    096        }
    097    }
    098
    099    public static class OrFilter implements StudentFilter {
    100        StudentFilter[] iFilters;
    101        public OrFilter(StudentFilter... filters) {
    102            iFilters = filters;
    103        }
    104        @Override
    105        public boolean matches(Student student) {
    106            for (StudentFilter filter: iFilters)
    107                if (filter.matches(student)) return true;
    108            return false;
    109        }
    110    }
    111
    112    public static class AndFilter implements StudentFilter {
    113        StudentFilter[] iFilters;
    114        public AndFilter(StudentFilter... filters) {
    115            iFilters = filters;
    116        }
    117        @Override
    118        public boolean matches(Student student) {
    119            for (StudentFilter filter: iFilters)
    120                if (!filter.matches(student)) return false;
    121            return true;
    122        }
    123    }
    124
    125    public static class GroupFilter implements StudentFilter {
    126        private String iGroup;
    127        public GroupFilter(String group) {
    128            iGroup = group;
    129        }
    130        @Override
    131        public boolean matches(Student student) {
    132            for (org.cpsolver.studentsct.model.StudentGroup g: student.getGroups())
    133                if (iGroup.equalsIgnoreCase(g.getReference())) return true;
    134            return false;
    135        }
    136    }
    137
    138    public static class PriorityFilter implements StudentFilter {
    139        private StudentPriority iPriority;
    140        public PriorityFilter(StudentPriority p) {
    141                iPriority = p;
    142        }
    143        @Override
    144        public boolean matches(Student student) {
    145            return student.getPriority() == iPriority;
    146        }
    147    }
    148
    149    public static class DummyFilter implements StudentFilter {
    150        public DummyFilter() {
    151        }
    152        @Override
    153        public boolean matches(Student student) {
    154            return student.isDummy();
    155        }
    156    }
    157
    158    public static class DummyOrNoRequestsFilter implements StudentFilter {
    159        public DummyOrNoRequestsFilter() {
    160        }
    161        @Override
    162        public boolean matches(Student student) {
    163            return student.isDummy() || student.getRequests().isEmpty();
    164        }
    165    }
    166
    167    public static class OnlineFilter implements StudentFilter {
    168        public OnlineFilter() {
    169        }
    170        @Override
    171        public boolean matches(Student student) {
    172                for (org.cpsolver.studentsct.model.StudentGroup aac: student.getGroups()) {
    173                if ("SCOVIDONL".equalsIgnoreCase(aac.getReference())) return true;
    174                if ("SCONTONL".equalsIgnoreCase(aac.getReference())) return true;
    175                if ("SCOVIDPMPE".equalsIgnoreCase(aac.getReference())) return true;
    176                }
    177                return false;
    178        }
    179    }
    180
    181    public static class AthletesFilter implements StudentFilter {
    182        public AthletesFilter() {
    183        }
    184        @Override
    185        public boolean matches(Student student) {
    186                for (org.cpsolver.studentsct.model.StudentGroup aac: student.getGroups()) {
    187                        if ("SPORT".equalsIgnoreCase(aac.getType())) return true;
    188                }
    189                return false;
    190        }
    191    }
    192
    193    public static class OnlineLateFilter extends OnlineFilter {
    194        public OnlineLateFilter() {
    195        }
    196        @Override
    197        public boolean matches(Student student) {
    198                if (!super.matches(student)) return false;
    199                boolean hasOL = false;
    200                boolean hasRS = false;
    201                for (Request r: student.getRequests()) {
    202                        if (r instanceof CourseRequest) {
    203                                CourseRequest cr = (CourseRequest)r;
    204                                for (Course c: cr.getCourses()) {
    205                                        if (c.getName().matches(".* [0-9]+I?OL(\\-[A-Za-z]+)?"))
    206                                                hasOL = true;
    207                                        else
    208                                                hasRS = true;
    209                                }
    210                        }
    211                }
    212                return hasRS && !hasOL;
    213        }
    214    }
    215
    216    public static class StarFilter implements StudentFilter {
    217        public StarFilter() {
    218        }
    219        @Override
    220        public boolean matches(Student student) {
    221                for (org.cpsolver.studentsct.model.StudentGroup aac: student.getGroups()) {
    222                if (aac.getReference() != null && aac.getReference().startsWith("STAR")) return true;
    223                if (aac.getReference() != null && aac.getReference().startsWith("VSTAR")) return true;
    224                if (aac.getReference() != null && aac.getReference().startsWith("NewStCRF")) return true;
    225                if (aac.getReference() != null && aac.getReference().startsWith("NewStOther")) return true;
    226                }
    227                return false;
    228        }
    229    }
    230
    231    private static StudentFilter FILTER_ALL = new AndFilter(new NotFilter(new DummyOrNoRequestsFilter()), new NotFilter(new OnlineLateFilter()));
    232    private static StudentFilter FILTER_ALL_RES = new AndFilter(new NotFilter(new DummyOrNoRequestsFilter()), new NotFilter(new OnlineFilter()));
    233
    234    public enum StudentGroup implements StudentFilter {
    235        ALL("All Students", FILTER_ALL),
    236
    237        DUMMY("Projected", new DummyFilter()),
    238        // ONLINE_LATE("Online-Late", new OnlineLateFilter()),
    239
    240        PRIORITY("Priority", new AndFilter(new PriorityFilter(StudentPriority.Priority), FILTER_ALL)),
    241        SENIOR("Seniors", new AndFilter(new PriorityFilter(StudentPriority.Senior), FILTER_ALL)),
    242        JUNIOR("Juniors", new AndFilter(new PriorityFilter(StudentPriority.Junior), FILTER_ALL)),
    243        SOPHOMORE("Sophomores", new AndFilter(new PriorityFilter(StudentPriority.Sophomore), FILTER_ALL)),
    244        FRESHMEN("Freshmen", new AndFilter(new PriorityFilter(StudentPriority.Freshmen), FILTER_ALL)),
    245        NORMAL("Non-priority", new AndFilter(new PriorityFilter(StudentPriority.Normal), FILTER_ALL)),
    246
    247        REBATCH("RE-BATCH", new AndFilter(new GroupFilter("RE-BATCH"), FILTER_ALL_RES)),
    248        ONLINE("Online", new AndFilter(new OnlineFilter(), FILTER_ALL)),
    249                GR_SCONTONL("SCONTONL", new AndFilter(new GroupFilter("SCONTONL"), FILTER_ALL)),
    250                GR_SCOVIDONL("SCOVIDONL", new AndFilter(new GroupFilter("SCOVIDONL"), FILTER_ALL)),
    251                GR_SCOVIDPMPE("SCOVIDPMPE", new AndFilter(new GroupFilter("SCOVIDPMPE"), FILTER_ALL)),
    252        PREREG("PREREG", new AndFilter(new GroupFilter("PREREG"), FILTER_ALL_RES, new NotFilter(new StarFilter()))),
    253        STAR("STAR", new AndFilter(new StarFilter(), FILTER_ALL_RES, new NotFilter(new GroupFilter("RE-BATCH")))),
    254                GR_STAR("On-campus STAR", new AndFilter(new GroupFilter("STAR"), FILTER_ALL_RES, new NotFilter(new GroupFilter("RE-BATCH")))),
    255                GR_VSTAR("Virtual STAR", new AndFilter(new GroupFilter("VSTAR"), FILTER_ALL_RES, new NotFilter(new GroupFilter("RE-BATCH")))),
    256        OTHER("Other", new AndFilter(FILTER_ALL_RES, new NotFilter(new GroupFilter("RE-BATCH")), new NotFilter(new GroupFilter("PREREG")), new NotFilter(new StarFilter()))),
    257
    258        ATHLETES("Athletes", new AndFilter(new AthletesFilter(), FILTER_ALL)),
    259        PRIORITY_ATHLETES("Priority\nAthletes", new AndFilter(new AthletesFilter(), new PriorityFilter(StudentPriority.Priority), FILTER_ALL)),
    260        OTHER_ATHLETES("Other\nAthletes", new AndFilter(new AthletesFilter(), new NotFilter(new PriorityFilter(StudentPriority.Priority)), FILTER_ALL)),
    261        ;
    262        String iName;
    263        StudentFilter iFilter;
    264        StudentGroup(String name, StudentFilter filter) {
    265            iName = name;
    266            iFilter = filter;
    267        }
    268        public String getName() { return iName; }
    269
    270        @Override
    271        public boolean matches(Student student) { return iFilter.matches(student); }
    272        public boolean matches(Student student, StudentSectioningReport.Filter filter) { return iFilter.matches(student) && filter.matches(student); }
    273    }
    274
    275    public static interface Statistic {
    276        public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter);
    277    }
    278
    279    public enum Statistics {
    280        NBR_STUDENTS(
    281                        "Number of Students",
    282                        "Number of students for which a schedule was computed",
    283                        new Statistic() {
    284            @Override
    285            public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) {
    286                int count = 0;
    287                for (Student student: model.getStudents()) {
    288                    if (!group.matches(student, filter)) continue;
    289                    count ++;
    290                }
    291                return new String[] {sIntFormat.format(count)};
    292            }
    293        }),
    294        COMPL_SCHEDULE(
    295                        new String[] {"Complete Schedule","- missing one course", "- missing two courses", "- missing three courses", "- missing four or more courses"},
    296                        new String[] {
    297                                "Percentage of students with a complete schedule (all requested courses assigned or reaching max credit)",
    298                                "Students that did not get a requested course",
    299                                "Students that did not get two requested courses"
    300                        },
    301                        new Statistic() {
    302            @Override
    303            public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) {
    304                int total = 0;
    305                int[] missing = new int[] {0, 0, 0, 0};
    306                int complete = 0;
    307                for (Student student: model.getStudents()) {
    308                    if (!group.matches(student, filter)) continue;
    309                    total ++;
    310                    int nrRequests = 0;
    311                    int nrAssignedRequests = 0;
    312                    for (Request r : student.getRequests()) {
    313                        if (!(r instanceof CourseRequest)) continue; // ignore free times
    314                        if (!filter.matches(r)) continue; // check the filter
    315                        if (!r.isAlternative()) nrRequests++;
    316                        if (r.isAssigned(assignment)) nrAssignedRequests++;
    317                    }
    318                    if (nrAssignedRequests < nrRequests) {
    319                        missing[Math.min(nrRequests - nrAssignedRequests, missing.length) - 1] ++;
    320                    }
    321                    if (student.isComplete(assignment)) complete ++;
    322                }
    323                return new String[] {
    324                        sPercentFormat.format(100.0 * complete / total) + "%",
    325                        sPercentFormat.format(100.0 * missing[0] / total) + "%",
    326                        sPercentFormat.format(100.0 * missing[1] / total) + "%",
    327                        sPercentFormat.format(100.0 * missing[2] / total) + "%",
    328                        sPercentFormat.format(100.0 * missing[3] / total) + "%"
    329                };
    330            }
    331        }),
    332        REQUESTED_COURSES(
    333                        new String[] {
    334                                        "Requested Courses", "- pre-enrolled", "- impossible",
    335                                        "Courses per Student", "Assigned Courses", "- 1st choice", "- 2nd choice", "- 3rd choice", "- 4th+ choice", "- substitute"},
    336                        new String[] {
    337                                        "Total number of requested courses by all students (not counting substitutes or alternatives)",
    338                                        "Percentage of requested courses that were already enrolled (solver was not allowed to change)",
    339                                        "Percentage of requested courses that have no possible enrollment (e.g., due to having all classes disabled)",
    340                                        "The average number of course requested per student",
    341                                        "Percentage of all course requests satisfied",
    342                                        "Out of the above, the percentage of cases where the 1st choice course was given",
    343                                        "2nd choice (1st alternative) course was given", "3rd choice course was given", "4th or later choice was given",
    344                                        "a substitute course was given instead",
    345                        },
    346                        new Statistic() {
    347            @Override
    348            public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) {
    349                int requests = 0, students = 0, assigned = 0;
    350                int fixed = 0, initial = 0;
    351                int noenrl = 0;
    352                int[] assignedChoice = new int[] {0, 0, 0, 0};
    353                int assignedSubst = 0;
    354                int assignedChoiceTotal = 0;
    355                for (Student student: model.getStudents()) {
    356                    if (!group.matches(student, filter)) continue;
    357                    students ++;
    358                    for (Request r : student.getRequests()) {
    359                        if (!(r instanceof CourseRequest)) continue; // ignore free times
    360                        if (!filter.matches(r)) continue; // check the filter
    361                        if (!r.isAlternative()) requests ++;
    362                        if (!r.isAlternative() && ((CourseRequest)r).isFixed()) fixed++;
    363                        if (!r.isAlternative() && ((CourseRequest)r).computeRandomEnrollments(assignment, 1).isEmpty()) noenrl ++;
    364                        Enrollment e = r.getAssignment(assignment);
    365                        if (r.getInitialAssignment() != null && r.getInitialAssignment().equals(e)) initial ++;
    366                        if (e != null) {
    367                                assigned ++;
    368                                if (r.isAlternative())
    369                                        assignedSubst ++;
    370                                else
    371                                        assignedChoice[Math.min(e.getTruePriority(), assignedChoice.length - 1)] ++;
    372                            assignedChoiceTotal ++;
    373                        }
    374                    }
    375                }
    376                if (fixed == 0 && initial > 0)
    377                        fixed = initial;
    378                if (requests == 0)
    379                        return new String[] {
    380                            sIntFormat.format(requests),
    381                            "",
    382                            "",
    383                            "",
    384                            "",
    385                            "",
    386                            "",
    387                            "",
    388                            "",
    389                            "",
    390                            };
    391                return new String[] {
    392                        sIntFormat.format(requests),
    393                        (fixed == 0 ? "" : sPercentFormat.format(100.0 * fixed / requests) + "%"),
    394                        (noenrl == 0 ? "" : sPercentFormat.format(100.0 * noenrl / requests) + "%"),
    395                        sDoubleFormat.format(((double)requests)/students),
    396                        sPercentFormat.format(100.0 * assigned / requests) + "%",
    397                        sPercentFormat.format(100.0 * assignedChoice[0] / assignedChoiceTotal) + "%",
    398                        sPercentFormat.format(100.0 * assignedChoice[1] / assignedChoiceTotal) + "%",
    399                        sPercentFormat.format(100.0 * assignedChoice[2] / assignedChoiceTotal) + "%",
    400                        sPercentFormat.format(100.0 * assignedChoice[3] / assignedChoiceTotal) + "%",
    401                        sPercentFormat.format(100.0 * assignedSubst / assignedChoiceTotal) + "%",
    402                        };
    403            }
    404        }),
    405        NOT_ASSIGNED_PRIORITY(
    406                        new String[] {"Not-assigned priority", "- 1st priority not assigned",
    407                "- 2nd priority not assigned", "- 3rd priority not assigned", "- 4th priority not assigned",
    408                "- 5th priority not assigned", "- 6th or later priority not assigned"},
    409                        new String[] {
    410                                        "The average priority of the course requests that were not satisfied",
    411                                        "Number of cases where a student did not get a 1st priority course",
    412                                        "Number of cases where a student did not get a 2nd priority course"
    413                        },
    414                        new Statistic() {
    415            @Override
    416            public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) {
    417                int[] notAssignedPriority = new int[] {0, 0, 0, 0, 0, 0};
    418                int notAssignedTotal = 0;
    419                int avgPriority = 0;
    420                for (Student student: model.getStudents()) {
    421                    if (!group.matches(student, filter)) continue;
    422                    for (Request r : student.getRequests()) {
    423                        if (!(r instanceof CourseRequest)) continue; // ignore free times
    424                        if (!filter.matches(r)) continue; // check the filter
    425                        Enrollment e = r.getAssignment(assignment);
    426                        if (e == null) {
    427                            if (!r.isAlternative()) {
    428                                notAssignedPriority[Math.min(r.getPriority(), notAssignedPriority.length - 1)] ++;
    429                                notAssignedTotal ++;
    430                                avgPriority += r.getPriority();
    431                            }
    432                        }
    433                    }
    434                }
    435                if (notAssignedTotal == 0)
    436                        return new String[] {
    437                            "",
    438                            "",
    439                            "",
    440                            "",
    441                            "",
    442                            "",
    443                            ""
    444                            };
    445                return new String[] {
    446                        sDoubleFormat.format(1.0 + ((double)avgPriority) / notAssignedTotal),
    447                        sIntFormat.format(notAssignedPriority[0]),
    448                        sIntFormat.format(notAssignedPriority[1]),
    449                        sIntFormat.format(notAssignedPriority[2]),
    450                        sIntFormat.format(notAssignedPriority[3]),
    451                        sIntFormat.format(notAssignedPriority[4]),
    452                        sIntFormat.format(notAssignedPriority[5])
    453                        };
    454            }
    455        }, true),
    456        ASSIGNED_COM(new String[] {"Assigned WC/OC", "Missing space in WC/OC"},
    457                        new String[] {
    458                                        "Number of students enrolled in a WC/OC course",
    459                                        "Number of unassigned course requests in written/oral communication courses"
    460                        },
    461                        new Statistic() {
    462            String[] sComCourses = new String[] {
    463                    "AMST 10100", "CLCS 23100", "CLCS 23700", "CLCS 33900",
    464                    "COM 11400", "COM 20400", "COM 21700", "EDCI 20500",
    465                    "EDPS 31500", "ENGL 10600", "ENGL 10800", "HONR 19903",
    466                    "PHIL 26000", "SCLA 10100", "SCLA 10200", "SPAN 33000",
    467                    "EDCI 49600", "EDCI 49800",  "EDPS 49800", "ENGL 30400",
    468                    "ENGL 38000", "HDFS 45000",
    469            };
    470            private boolean isComCourse(Course course) {
    471                for (String c: sComCourses) {
    472                    if (course.getName().startsWith(c)) return true;
    473                }
    474                return false;
    475            }
    476            @Override
    477            public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) {
    478
    479
    480                int assigned = 0, notAssigned = 0;
    481                for (Student student: model.getStudents()) {
    482                    if (!group.matches(student, filter)) continue;
    483                    for (Request r : student.getRequests()) {
    484                        if (!(r instanceof CourseRequest)) continue; // ignore free times
    485                        if (!filter.matches(r)) continue; // check the filter
    486                        CourseRequest cr = (CourseRequest)r;
    487                        Enrollment e = cr.getAssignment(assignment);
    488                        if (e != null && isComCourse(e.getCourse())) {
    489                                assigned ++;
    490                        } else if (e == null && isComCourse(cr.getCourses().get(0)) && student.canAssign(assignment, r) && !r.isAlternative()) {
    491                                notAssigned ++;
    492                        }
    493                    }
    494                }
    495                return new String[] { sIntFormat.format(assigned), sIntFormat.format(notAssigned) };
    496            }
    497        }, true),
    498        LC(new String[] {"LC courses", "Assigned LC courses"},
    499                        new String[] {
    500                                        "Number of course requests with a matching LC reservation"
    501                        },
    502                        new Statistic() {
    503            @Override
    504            public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) {
    505                int assigned = 0, total = 0;
    506                for (Student student: model.getStudents()) {
    507                    if (!group.matches(student, filter)) continue;
    508                    for (Request r : student.getRequests()) {
    509                        if (!(r instanceof CourseRequest)) continue; // ignore free times
    510                        if (!filter.matches(r)) continue; // check the filter
    511                        CourseRequest cr = (CourseRequest)r;
    512                        if (!cr.isAlternative() && cr.getRequestPriority() == RequestPriority.LC) {
    513                            total ++;
    514                            if (cr.isAssigned(assignment)) assigned ++;
    515                        }
    516                    }
    517                }
    518                if (total == 0) return new String[] { "N/A", ""};
    519                return new String[] { sIntFormat.format(total), sPercentFormat.format(100.0 * assigned / total) + "%" };
    520            }
    521        }),
    522        CRITICAL(new String[] {"Critical courses", "Assigned critical courses"},
    523                        new String[] {
    524                                        "Number of course requests marked as critical (~ course/group/placeholder critical in degree plan)"
    525                        },
    526                        new Statistic() {
    527            @Override
    528            public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) {
    529                int assigned = 0, total = 0;
    530                for (Student student: model.getStudents()) {
    531                    if (!group.matches(student, filter)) continue;
    532                    for (Request r : student.getRequests()) {
    533                        if (!(r instanceof CourseRequest)) continue; // ignore free times
    534                        if (!filter.matches(r)) continue; // check the filter
    535                        CourseRequest cr = (CourseRequest)r;
    536                        if (!cr.isAlternative() && cr.getRequestPriority() == RequestPriority.Critical) {
    537                            total ++;
    538                            if (cr.isAssigned(assignment)) assigned ++;
    539                        }
    540                    }
    541                }
    542                if (total == 0) return new String[] { "N/A", ""};
    543                return new String[] { sIntFormat.format(total), sPercentFormat.format(100.0 * assigned / total) + "%" };
    544            }
    545        }),
    546        VITAL(new String[] {"Vital courses", "Assigned vital courses"},
    547                        new String[] {
    548                                        "Number of course requests marked as vital by advisors"
    549                        },
    550                        new Statistic() {
    551            @Override
    552            public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) {
    553                int assigned = 0, total = 0;
    554                for (Student student: model.getStudents()) {
    555                    if (!group.matches(student, filter)) continue;
    556                    for (Request r : student.getRequests()) {
    557                        if (!(r instanceof CourseRequest)) continue; // ignore free times
    558                        if (!filter.matches(r)) continue; // check the filter
    559                        CourseRequest cr = (CourseRequest)r;
    560                        if (!cr.isAlternative() && cr.getRequestPriority() == RequestPriority.Vital) {
    561                            total ++;
    562                            if (cr.isAssigned(assignment)) assigned ++;
    563                        }
    564                    }
    565                }
    566                if (total == 0) return new String[] { "N/A", ""};
    567                return new String[] { sIntFormat.format(total), sPercentFormat.format(100.0 * assigned / total) + "%" };
    568            }
    569        }),
    570        IMPORTANT(new String[] {"Important courses", "Assigned important courses"},
    571                        new String[] {
    572                                        "Number of course requests marked as important (~ course/group/placeholder critical in the first choice major)"
    573                        },
    574                        new Statistic() {
    575            @Override
    576            public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) {
    577                int assigned = 0, total = 0;
    578                for (Student student: model.getStudents()) {
    579                    if (!group.matches(student, filter)) continue;
    580                    for (Request r : student.getRequests()) {
    581                        if (!(r instanceof CourseRequest)) continue; // ignore free times
    582                        if (!filter.matches(r)) continue; // check the filter
    583                        CourseRequest cr = (CourseRequest)r;
    584                        if (!cr.isAlternative() && cr.getRequestPriority() == RequestPriority.Important) {
    585                            total ++;
    586                            if (cr.isAssigned(assignment)) assigned ++;
    587                        }
    588                    }
    589                }
    590                if (total == 0) return new String[] { "N/A", ""};
    591                return new String[] { sIntFormat.format(total), sPercentFormat.format(100.0 * assigned / total) + "%" };
    592            }
    593        }, true),
    594        PREFERENCES(new String[] {"Course requests with preferences", "Satisfied preferences", "- instructional method", "- classes"},
    595                        new String[] {
    596                                        "Course requests with IM or section preferences",
    597                                        "Percentage of satisfied preferences (both class and IM)",
    598                                        "Percentage of cases when the preferred instructional method was given to the student",
    599                                        "Percentage of cases when the preferred class was given to the student"
    600                        },
    601                        new Statistic() {
    602            @Override
    603            public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) {
    604                int prefs = 0, configPrefs = 0, sectionPrefs = 0;
    605                double sectionPref = 0.0, configPref = 0.0;
    606                double satisfied = 0.0;
    607                for (Student student: model.getStudents()) {
    608                    if (!group.matches(student, filter)) continue;
    609                    for (Request r : student.getRequests()) {
    610                        if (!(r instanceof CourseRequest)) continue; // ignore free times
    611                        if (!filter.matches(r)) continue; // check the filter
    612                        CourseRequest cr = (CourseRequest)r;
    613                        Enrollment e = r.getAssignment(assignment);
    614                        if (e != null) {
    615                            if (r.hasSelection()) {
    616                                prefs ++;
    617                                satisfied += //0.3 * e.percentSelectedSameConfig() + 0.7 * e.percentSelectedSameSection();
    618                                                e.percentSelected();
    619                                for (Choice ch: cr.getSelectedChoices()) {
    620                                    if (ch.getConfigId() != null) {
    621                                        configPrefs ++;
    622                                        configPref += e.percentSelectedSameConfig();
    623                                        break;
    624                                    }
    625                                }
    626                                for (Choice ch: cr.getSelectedChoices()) {
    627                                    if (ch.getSectionId() != null) {
    628                                        sectionPrefs ++;
    629                                        sectionPref += e.percentSelectedSameSection();
    630                                        break;
    631                                    }
    632                                }
    633                            }
    634                        }
    635                    }
    636                }
    637                if (prefs == 0) return new String[] { "N/A", "", "", ""};
    638                return new String[] { sIntFormat.format(prefs), sPercentFormat.format(100.0 * satisfied / prefs) + "%",
    639                        sPercentFormat.format(100.0 * sectionPref / sectionPrefs) + "%",
    640                        sPercentFormat.format(100.0 * configPref / configPrefs) + "%"
    641                };
    642            }
    643        }, true),
    644        BALANCING(new String[] {"Unbalanced sections", "- average disbalance"},
    645                        new String[] {"Classes dis-balanced by 10% or more", "Average difference between target and actual enrollment in the section"},
    646                new Statistic() {
    647            @Override
    648            public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) {
    649                double disbWeight = 0;
    650                int disb10Sections = 0;
    651                int totalSections = 0;
    652                for (Offering offering: model.getOfferings()) {
    653                        if (offering.isDummy()) continue;
    654                    for (Config config: offering.getConfigs()) {
    655                        double enrl = 0;
    656                        for (Enrollment e: config.getEnrollments(assignment)) {
    657                            if (group.matches(e.getStudent()) && filter.matches(e.getRequest(), e)) enrl += e.getRequest().getWeight();
    658                        }
    659                        for (Subpart subpart: config.getSubparts()) {
    660                            if (subpart.getSections().size() <= 1) continue;
    661                            if (subpart.getLimit() > 0) {
    662                                // sections have limits -> desired size is section limit x (total enrollment / total limit)
    663                                double ratio = enrl / subpart.getLimit();
    664                                for (Section section: subpart.getSections()) {
    665                                    double sectEnrl = 0;
    666                                    for (Enrollment e: section.getEnrollments(assignment)) {
    667                                        if (group.matches(e.getStudent()) && filter.matches(e.getRequest(), e)) sectEnrl += e.getRequest().getWeight();
    668                                    }
    669                                    double desired = ratio * section.getLimit();
    670                                    disbWeight += Math.abs(sectEnrl - desired);
    671                                    if (Math.abs(desired - sectEnrl) >= Math.max(1.0, 0.1 * section.getLimit())) {
    672                                        disb10Sections++;
    673                                    }
    674                                    totalSections++;
    675                                }
    676                            } else {
    677                                // unlimited sections -> desired size is total enrollment / number of sections
    678                                for (Section section: subpart.getSections()) {
    679                                    double sectEnrl = 0;
    680                                    for (Enrollment e: section.getEnrollments(assignment)) {
    681                                        if (group.matches(e.getStudent()) && filter.matches(e.getRequest(), e)) sectEnrl += e.getRequest().getWeight();
    682                                    }
    683                                    double desired = enrl / subpart.getSections().size();
    684                                    disbWeight += Math.abs(sectEnrl - desired);
    685                                    if (Math.abs(desired - sectEnrl) >= Math.max(1.0, 0.1 * desired)) {
    686                                        disb10Sections++;
    687                                    }
    688                                    totalSections++;
    689                                }
    690                            }
    691                        }
    692                    }
    693                }
    694                return new String[] {
    695                                sPercentFormat.format(100.0 * disb10Sections / totalSections) + "%",
    696                                sDoubleFormat.format(disbWeight / totalSections)
    697                };
    698            }
    699        }, true),
    700        DISTANCE(new String[] {"Distance conflicts", "- students with distance conflicts", "- average distance in minutes",
    701                        "Distance conflicts (SD)", "- students with distance conflicts", "- average distance in minutes"
    702        }, new String[] {"Total number of distance conflicts",
    703                        "Total number of students with one or more distance conflicts",
    704                        "Average distance between two classes in minutes per conflict",
    705                        "Total number of distance conflicts (students needed short distances)",
    706                        "Total number of SD students with one or more distance conflicts",
    707                        "Average distance between two classes in minutes per conflict"},
    708                        new Statistic() {
    709
    710            protected int getDistanceInMinutes(StudentSectioningModel model, RoomLocation r1, RoomLocation r2) {
    711                if (r1.getId().compareTo(r2.getId()) > 0) return getDistanceInMinutes(model, r2, r1);
    712                if (r1.getId().equals(r2.getId()) || r1.getIgnoreTooFar() || r2.getIgnoreTooFar())
    713                    return 0;
    714                if (r1.getPosX() == null || r1.getPosY() == null || r2.getPosX() == null || r2.getPosY() == null)
    715                    return model.getDistanceMetric().getMaxTravelDistanceInMinutes();
    716                return  model.getDistanceMetric().getDistanceInMinutes(r1.getId(), r1.getPosX(), r1.getPosY(), r2.getId(), r2.getPosX(), r2.getPosY());
    717            }
    718
    719            protected int getDistanceInMinutes(StudentSectioningModel model, Placement p1, Placement p2) {
    720                if (p1.isMultiRoom()) {
    721                    if (p2.isMultiRoom()) {
    722                        int dist = 0;
    723                        for (RoomLocation r1 : p1.getRoomLocations()) {
    724                            for (RoomLocation r2 : p2.getRoomLocations()) {
    725                                dist = Math.max(dist, getDistanceInMinutes(model, r1, r2));
    726                            }
    727                        }
    728                        return dist;
    729                    } else {
    730                        if (p2.getRoomLocation() == null)
    731                            return 0;
    732                        int dist = 0;
    733                        for (RoomLocation r1 : p1.getRoomLocations()) {
    734                            dist = Math.max(dist, getDistanceInMinutes(model, r1, p2.getRoomLocation()));
    735                        }
    736                        return dist;
    737                    }
    738                } else if (p2.isMultiRoom()) {
    739                    if (p1.getRoomLocation() == null)
    740                        return 0;
    741                    int dist = 0;
    742                    for (RoomLocation r2 : p2.getRoomLocations()) {
    743                        dist = Math.max(dist, getDistanceInMinutes(model, p1.getRoomLocation(), r2));
    744                    }
    745                    return dist;
    746                } else {
    747                    if (p1.getRoomLocation() == null || p2.getRoomLocation() == null)
    748                        return 0;
    749                    return getDistanceInMinutes(model, p1.getRoomLocation(), p2.getRoomLocation());
    750                }
    751            }
    752
    753                public boolean inConflict(StudentSectioningModel model, Student student, Section s1, Section s2) {
    754                if (s1.getPlacement() == null || s2.getPlacement() == null)
    755                    return false;
    756                TimeLocation t1 = s1.getTime();
    757                TimeLocation t2 = s2.getTime();
    758                if (!t1.shareDays(t2) || !t1.shareWeeks(t2))
    759                    return false;
    760                int a1 = t1.getStartSlot(), a2 = t2.getStartSlot();
    761                if (student.isNeedShortDistances()) {
    762                    if (model.getDistanceMetric().doComputeDistanceConflictsBetweenNonBTBClasses()) {
    763                        if (a1 + t1.getNrSlotsPerMeeting() <= a2) {
    764                            int dist = getDistanceInMinutes(model, s1.getPlacement(), s2.getPlacement());
    765                            if (dist > Constants.SLOT_LENGTH_MIN * (a2 - a1 - t1.getLength()))
    766                                return true;
    767                        } else if (a2 + t2.getNrSlotsPerMeeting() <= a1) {
    768                            int dist = getDistanceInMinutes(model, s1.getPlacement(), s2.getPlacement());
    769                            if (dist > Constants.SLOT_LENGTH_MIN * (a1 - a2 - t2.getLength()))
    770                                return true;
    771                        }
    772                    } else {
    773                        if (a1 + t1.getNrSlotsPerMeeting() == a2) {
    774                            int dist = getDistanceInMinutes(model, s1.getPlacement(), s2.getPlacement());
    775                            if (dist > 0) return true;
    776                        } else if (a2 + t2.getNrSlotsPerMeeting() == a1) {
    777                            int dist = getDistanceInMinutes(model, s1.getPlacement(), s2.getPlacement());
    778                            if (dist > 0) return true;
    779                        }
    780                    }
    781                    return false;
    782                }
    783                if (model.getDistanceMetric().doComputeDistanceConflictsBetweenNonBTBClasses()) {
    784                    if (a1 + t1.getNrSlotsPerMeeting() <= a2) {
    785                        int dist = getDistanceInMinutes(model, s1.getPlacement(), s2.getPlacement());
    786                        if (dist > t1.getBreakTime() + Constants.SLOT_LENGTH_MIN * (a2 - a1 - t1.getLength()))
    787                            return true;
    788                    } else if (a2 + t2.getNrSlotsPerMeeting() <= a1) {
    789                        int dist = getDistanceInMinutes(model, s1.getPlacement(), s2.getPlacement());
    790                        if (dist > t2.getBreakTime() + Constants.SLOT_LENGTH_MIN * (a1 - a2 - t2.getLength()))
    791                            return true;
    792                    }
    793                } else {
    794                    if (a1 + t1.getNrSlotsPerMeeting() == a2) {
    795                        int dist = getDistanceInMinutes(model, s1.getPlacement(), s2.getPlacement());
    796                        if (dist > t1.getBreakTime())
    797                            return true;
    798                    } else if (a2 + t2.getNrSlotsPerMeeting() == a1) {
    799                        int dist = getDistanceInMinutes(model, s1.getPlacement(), s2.getPlacement());
    800                        if (dist > t2.getBreakTime())
    801                            return true;
    802                    }
    803                }
    804                return false;
    805            }
    806
    807                public Set<DistanceConflict.Conflict> conflicts(StudentSectioningModel model, Enrollment e1) {
    808                Set<DistanceConflict.Conflict> ret = new HashSet<DistanceConflict.Conflict>();
    809                if (!e1.isCourseRequest())
    810                    return ret;
    811                for (Section s1 : e1.getSections()) {
    812                    for (Section s2 : e1.getSections()) {
    813                        if (s1.getId() < s2.getId() && inConflict(model, e1.getStudent(), s1, s2))
    814                            ret.add(new DistanceConflict.Conflict(e1.getStudent(), e1, s1, e1, s2));
    815                    }
    816                }
    817                return ret;
    818            }
    819
    820                public Set<DistanceConflict.Conflict> conflicts(StudentSectioningModel model, Enrollment e1, Enrollment e2) {
    821                Set<DistanceConflict.Conflict> ret = new HashSet<DistanceConflict.Conflict>();
    822                if (!e1.isCourseRequest() || !e2.isCourseRequest() || !e1.getStudent().equals(e2.getStudent()))
    823                    return ret;
    824                for (Section s1 : e1.getSections()) {
    825                    for (Section s2 : e2.getSections()) {
    826                        if (inConflict(model, e1.getStudent(), s1, s2))
    827                            ret.add(new DistanceConflict.Conflict(e1.getStudent(), e1, s1, e2, s2));
    828                    }
    829                }
    830                return ret;
    831            }
    832
    833                public Set<DistanceConflict.Conflict> computeAllConflicts(StudentSectioningModel model, Assignment<Request, Enrollment> assignment) {
    834                Set<DistanceConflict.Conflict> ret = new HashSet<DistanceConflict.Conflict>();
    835                for (Request r1 : model.variables()) {
    836                    Enrollment e1 = assignment.getValue(r1);
    837                    if (e1 == null || !(r1 instanceof CourseRequest))
    838                        continue;
    839                    ret.addAll(conflicts(model, e1));
    840                    for (Request r2 : r1.getStudent().getRequests()) {
    841                        Enrollment e2 = assignment.getValue(r2);
    842                        if (e2 == null || r1.getId() >= r2.getId() || !(r2 instanceof CourseRequest))
    843                            continue;
    844                        ret.addAll(conflicts(model, e1, e2));
    845                    }
    846                }
    847                return ret;
    848            }
    849
    850                @Override
    851            public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) {
    852                        if (model.getDistanceMetric() == null)
    853                                return new String[] {"N/A", "", ""};
    854                        Set<DistanceConflict.Conflict> conflicts = computeAllConflicts(model, assignment);
    855                        Set<Student> students = new HashSet<Student>(), studentsSD = new HashSet<Student>();
    856                double distance = 0, distanceSD = 0;
    857                int total = 0, totalSD = 0;
    858                for (DistanceConflict.Conflict conflict: conflicts) {
    859                        if (group.matches(conflict.getStudent()) && filter.matches(conflict.getR1(), conflict.getE1())) {
    860                                if (conflict.getStudent().isNeedShortDistances()) {
    861                                        totalSD ++;
    862                                        studentsSD.add(conflict.getStudent());
    863                                        distanceSD += Placement.getDistanceInMinutes(model.getDistanceMetric(), conflict.getS1().getPlacement(), conflict.getS2().getPlacement());
    864                                } else {
    865                                        total ++;
    866                                        students.add(conflict.getStudent());
    867                                        distance += Placement.getDistanceInMinutes(model.getDistanceMetric(), conflict.getS1().getPlacement(), conflict.getS2().getPlacement());
    868                                }
    869                        }
    870                }
    871                        return new String[] {
    872                                        sIntFormat.format(total),
    873                                        sIntFormat.format(students.size()),
    874                                        (total == 0 ? "" : sDoubleFormat.format(distance / total)),
    875                                        sIntFormat.format(totalSD),
    876                                        sIntFormat.format(studentsSD.size()),
    877                                        (totalSD == 0 ? "" : sDoubleFormat.format(distanceSD / totalSD))
    878                        };
    879                }
    880        }, true),
    881        OVERLAP(new String[] {"Free time conflict", "- students in conflict", "- average minutes", "Course time conflict", "- students in conflict", "- average minutes", "Teaching conflicts", "- students in conflict", "- average minutes"},
    882                        new String[] {
    883                                        "Total number of free time conflicts",
    884                                        "Total number of students with a free time conflict",
    885                                        "For students with a free time conflict, the average number of overlapping minutes per student",
    886                                        "Total number of course time conflicts",
    887                                        "Total number of students with a course time conflict",
    888                                        "For students with a course time conflict, the average number of overlapping minutes per student",
    889                                        "Total number of teaching time conflicts",
    890                                        "Total number of students with a teaching conflict",
    891                                        "For students with a teaching time conflict, the average number of overlapping minutes per student"
    892                        },
    893                        new Statistic() {
    894
    895                public boolean inConflict(SctAssignment a1, SctAssignment a2) {
    896                if (a1.getTime() == null || a2.getTime() == null) return false;
    897                if (a1 instanceof Section && a2 instanceof Section && ((Section)a1).isToIgnoreStudentConflictsWith(a2.getId())) return false;
    898                return a1.getTime().hasIntersection(a2.getTime());
    899            }
    900
    901                public int share(SctAssignment a1, SctAssignment a2) {
    902                if (!inConflict(a1, a2)) return 0;
    903                return a1.getTime().nrSharedDays(a2.getTime()) * a1.getTime().nrSharedHours(a2.getTime());
    904            }
    905
    906                public Set<Conflict> conflicts(Enrollment e1, Enrollment e2) {
    907                Set<Conflict> ret = new HashSet<Conflict>();
    908                if (!e1.getStudent().equals(e2.getStudent())) return ret;
    909                if (e1.getRequest() instanceof FreeTimeRequest && e2.getRequest() instanceof FreeTimeRequest) return ret;
    910                for (SctAssignment s1 : e1.getAssignments()) {
    911                    for (SctAssignment s2 : e2.getAssignments()) {
    912                        if (inConflict(s1, s2))
    913                            ret.add(new Conflict(e1.getStudent(), share(s1, s2), e1, s1, e2, s2));
    914                    }
    915                }
    916                return ret;
    917            }
    918
    919                public Set<Conflict> computeAllConflicts(StudentSectioningModel model, Assignment<Request, Enrollment> assignment) {
    920                Set<Conflict> ret = new HashSet<Conflict>();
    921                for (Request r1 : model.variables()) {
    922                    Enrollment e1 = assignment.getValue(r1);
    923                    if (e1 == null || r1 instanceof FreeTimeRequest) continue;
    924                    for (Request r2 : r1.getStudent().getRequests()) {
    925                        Enrollment e2 = assignment.getValue(r2);
    926                        if (r2 instanceof FreeTimeRequest) {
    927                            FreeTimeRequest ft = (FreeTimeRequest)r2;
    928                            ret.addAll(conflicts(e1, ft.createEnrollment()));
    929                        } else if (e2 != null && r1.getId() < r2.getId()) {
    930                            ret.addAll(conflicts(e1, e2));
    931                        }
    932                    }
    933                    for (Unavailability unavailability: e1.getStudent().getUnavailabilities())
    934                        for (SctAssignment section: e1.getAssignments())
    935                            if (inConflict(section, unavailability))
    936                                ret.add(new Conflict(e1.getStudent(), share(section, unavailability), e1, section, unavailability.createEnrollment(), unavailability));
    937                }
    938                return ret;
    939            }
    940
    941            @Override
    942            public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) {
    943                Set<Student> timeFt = new HashSet<Student>();
    944                Set<Student> timeCourse = new HashSet<Student>();
    945                Set<Student> timeUnav = new HashSet<Student>();
    946                int ftMin = 0, courseMin = 0, unavMin = 0;
    947                int totFt = 0, totCourse = 0, totUn = 0;
    948                Set<TimeOverlapsCounter.Conflict> conf = computeAllConflicts(model, assignment);
    949                for (TimeOverlapsCounter.Conflict c: conf) {
    950                    if (group.matches(c.getStudent()) && filter.matches(c.getR1(), c.getE1())) {
    951                        if (c.getR1() instanceof CourseRequest && c.getR2() instanceof CourseRequest) {
    952                                totCourse ++;
    953                            courseMin += 5 * c.getShare();
    954                            timeCourse.add(c.getStudent());
    955                        } else if (c.getS2() instanceof Unavailability) {
    956                                totUn ++;
    957                                unavMin += 5 * c.getShare();
    958                                timeUnav.add(c.getStudent());
    959                        } else {
    960                                totFt ++;
    961                            ftMin += 5 * c.getShare();
    962                            timeFt.add(c.getStudent());
    963                        }
    964                    }
    965                }
    966                return new String[] {
    967                                sIntFormat.format(totFt),
    968                        sIntFormat.format(timeFt.size()),
    969                        (timeFt.isEmpty() ? "" : sDoubleFormat.format(((double)ftMin) / timeFt.size())),
    970                        sIntFormat.format(totCourse),
    971                        sIntFormat.format(timeCourse.size()),
    972                        (timeCourse.isEmpty() ? "" : sDoubleFormat.format(((double)courseMin) / timeCourse.size())),
    973                        sIntFormat.format(totUn),
    974                        sIntFormat.format(timeUnav.size()),
    975                        (timeUnav.isEmpty() ? "" : sDoubleFormat.format(((double)unavMin) / timeUnav.size()))
    976                };
    977            }
    978        }, true),
    979        CREDITS(new String[] { "Students requesting 12+ credits", "- 12+ credits assigned", "Students requesting 15+ credits", "- 15+ credits assigned" },
    980                        new String[] {
    981                                        "Total number of students requesting 12 or more credit hours",
    982                                        "Out of these, the percentage of students having 12 or more credits assigned",
    983                                        "Total number of students requesting 15 or more credit hours",
    984                                        "Out of these, the percentage of students having 15 or more credits assigned",
    985                        },
    986                        new Statistic() {
    987            @Override
    988            public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) {
    989                int total12 = 0, assigned12 = 0;
    990                int total15 = 0, assigned15 = 0;
    991                for (Student student: model.getStudents()) {
    992                    if (!group.matches(student, filter)) continue;
    993                    float credit = 0;
    994                    float assignedCredit = 0;
    995                    for (Request r : student.getRequests()) {
    996                        if (!(r instanceof CourseRequest)) continue; // ignore free times
    997                        if (!filter.matches(r)) continue; // check the filter
    998                        CourseRequest cr = (CourseRequest)r;
    999                        if (!cr.isAlternative()) {
    1000                            Course c = cr.getCourses().get(0);
    1001                            if (c.hasCreditValue())
    1002                                credit += c.getCreditValue();
    1003                            else
    1004                                credit += cr.getMinCredit();
    1005                        }
    1006                        Enrollment e = cr.getAssignment(assignment);
    1007                        if (e != null) {
    1008                            assignedCredit += e.getCredit();
    1009                        }
    1010                    }
    1011                    if (credit >= 12f) {
    1012                        total12 ++;
    1013                        if (assignedCredit >= 12f)
    1014                            assigned12 ++;
    1015                    }
    1016                    if (credit >= 15f) {
    1017                        total15 ++;
    1018                        if (assignedCredit >= 15f)
    1019                            assigned15 ++;
    1020                    }
    1021                }
    1022                return new String[] {
    1023                        sIntFormat.format(total12),
    1024                        (total12 == 0 ? "" : sPercentFormat.format(100.0 * assigned12 / total12) + "%"),
    1025                        sIntFormat.format(total15),
    1026                        (total15 == 0 ? "" : sPercentFormat.format(100.0 * assigned15 / total15) + "%"),
    1027                };
    1028            }
    1029        }, true),
    1030        F2F(new String[] {
    1031                                "Residential Students",
    1032                                "Arranged Hours Assignments", "- percentage of all assignments",
    1033                                "Online Assignments", "- percentage of all assignments",
    1034                                "Students with no face-to-face classes", "- percentage of all undergrad students",
    1035                                "Students with <50% classes face-to-face", "- percentage of all undergrad students"},
    1036                        new String[] {
    1037                                "Number of students that are NOT online-only (only residential students are counted in the following numbers)",
    1038                                "Number of class assignments that are Arranged Hours", "Percentage of all class assignments",
    1039                                "Number of class assignments that are Online (no time, time with no room, or time with ONLINE room)", "Percentage of all class assignments",
    1040                                "Total number of undergraduate students with no face-to-face classes.", "Percentage of all undergraduate students",
    1041                                "Total number of undergraduate students with less than half of their schedule face-to-face.", "Percentage of all undergraduate students",
    1042                        },
    1043                        new Statistic() {
    1044            @Override
    1045            public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) {
    1046                int arrClass = 0, onlineClass = 0, allClass = 0;
    1047                int residentialStudents = 0;
    1048                for (Student student: model.getStudents()) {
    1049                        if (!group.matches(student, filter)) continue;
    1050                        if (!FILTER_ALL_RES.matches(student)) { continue; }
    1051                        residentialStudents ++;
    1052                        for (Request r: student.getRequests()) {
    1053                                Enrollment e = r.getAssignment(assignment);
    1054                                if (e != null && e.isCourseRequest()) {
    1055                                        for (Section section: e.getSections()) {
    1056                                                if (section.isOnline()) onlineClass ++;
    1057                                                if (!section.hasTime()) arrClass ++;
    1058                                                allClass ++;
    1059                                        }
    1060                                }
    1061                        }
    1062                }
    1063                int online = 0;
    1064                int half = 0;
    1065                int total = 0;
    1066                for (Student student: model.getStudents()) {
    1067                    if (!group.matches(student, filter)) continue;
    1068                    if (!FILTER_ALL_RES.matches(student)) continue;
    1069                    boolean gr = false;
    1070                    for (AreaClassificationMajor acm: student.getAreaClassificationMajors()) {
    1071                        if (acm.getClassification().startsWith("G") || acm.getClassification().startsWith("P")) gr = true;
    1072                    }
    1073                    if (gr) continue;
    1074                    int sections = 0, onlineSections = 0;
    1075                    for (Request r : student.getRequests()) {
    1076                        if (!(r instanceof CourseRequest)) continue; // ignore free times
    1077                        if (!filter.matches(r)) continue; // check the filter
    1078                        Enrollment e = r.getAssignment(assignment);
    1079                        if (e != null)
    1080                            for (Section s: e.getSections()) {
    1081                                sections ++;
    1082                                if (s.isOnline()) onlineSections ++;
    1083                            }
    1084                    }
    1085                    if (sections > 0) {
    1086                        total ++;
    1087                        if (onlineSections == sections) online ++;
    1088                        if (onlineSections > 0.5 * sections) half++;
    1089                    }
    1090                }
    1091                return new String[] {
    1092                                sIntFormat.format(residentialStudents),
    1093                                (residentialStudents == 0 ? "" : sIntFormat.format(arrClass)),
    1094                                (residentialStudents == 0 ? "" : sPercentFormat.format(100.0 * arrClass / allClass) + "%"),
    1095                                (onlineClass == 0 ? "" : sIntFormat.format(onlineClass)),
    1096                                (onlineClass == 0 ? "" : sPercentFormat.format(100.0 * onlineClass / allClass) + "%"),
    1097                        (online == 0 ? "" : sIntFormat.format(online)),
    1098                        (online == 0 ? "" : sPercentFormat.format(100.0 * online / total) + "%"),
    1099                        (half == 0 ? "" : sIntFormat.format(half)),
    1100                        (half == 0 ? "" : sPercentFormat.format(100.0 * half / total) + "%")
    1101                };
    1102            }
    1103        }, true),
    1104        FULL_OFFERINGS(
    1105                        new String[] {
    1106                                        "Full Offerings", "- percentage of all requested offerings", "- percentage of all assignments",
    1107                                        "Offerings with ≤ 2% available", "- percentage of all requested offerings", "- percentage of all assignments",
    1108                                        "Offerings with ≤ 5% available", "- percentage of all requested offerings", "- percentage of all assignments",
    1109                                        "Offerings with ≤ 10% available", "- percentage of all requested offerings", "- percentage of all assignments",
    1110                                        "Full Sections", "- percentage of all sections", "- percentage of all assignments",
    1111                                        "Disabled Sections", "- percentage of all sections", "- percentage of all assignments",
    1112                                        "Sections with ≤ 2% available", "- percentage of all sections", "- percentage of all assignments",
    1113                                        "Sections with ≤ 5% available", "- percentage of all sections", "- percentage of all assignments",
    1114                                        "Sections with ≤ 10% available", "- percentage of all sections", "- percentage of all assignments",
    1115                                        },
    1116                        new String[] {
    1117                                        "Number of instructional offerings that are completely full (only counting courses that are requested by the students)",
    1118                                                "Percentage full offerings vs all requested offerings",
    1119                                                "Percentage of all course assignments that are for courses that are full",
    1120                                        "Number of instructional offerings that have 2% or less space available", "", "",
    1121                                        "Number of instructional offerings that have 5% or less space available", "", "",
    1122                                        "Number of instructional offerings that have 10% or less space available", "", "",
    1123                                        "Number of sections that have no space available (only counting sections from courses that are requested by the students)",
    1124                                                "Percentage full sections vs all sections of the requested courses",
    1125                                                "Percentage of all class assignments that are in sections that are full",
    1126                                        "Number of sections that are disabled",
    1127                                                "Percentage disabled sections vs all sections of the requested courses",
    1128                                                "Percentage of all class assignments that are in sections that are disabled",
    1129                                        "Number of sections that have 2% or less space available", "", "",
    1130                                        "Number of sections that have 5% or less space available", "", "",
    1131                                        "Number of sections that have 10% or less space available", "", "",
    1132                                        },
    1133        new Statistic() {
    1134
    1135                protected int getEnrollments(StudentGroup group, Section section, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) {
    1136                        int enrl = 0;
    1137                        for (Enrollment e: section.getEnrollments(assignment)) {
    1138                    if (group.matches(e.getStudent()) && filter.matches(e.getRequest(), e)) enrl ++;
    1139                }
    1140                        return enrl;
    1141                }
    1142
    1143                protected int getEnrollments(StudentGroup group, Config config, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) {
    1144                        int enrl = 0;
    1145                        for (Enrollment e: config.getEnrollments(assignment)) {
    1146                    if (group.matches(e.getStudent()) && filter.matches(e.getRequest(), e)) enrl ++;
    1147                }
    1148                        return enrl;
    1149                }
    1150
    1151                        @Override
    1152                        public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) {
    1153
    1154                        int nbrSections = 0, nbrFullSections = 0, nbrSections98 = 0, nbrSections95 = 0, nbrSections90 = 0, nbrSectionsDis = 0;
    1155                        int enrlSections = 0, enrlFullSections = 0, enrlSections98 = 0, enrlSections95 = 0, enrlSections90 = 0, enrlSectionsDis = 0;
    1156                        int nbrOfferings = 0, nbrFullOfferings = 0, nbrOfferings98 = 0, nbrOfferings95 = 0, nbrOfferings90 = 0;
    1157                        int enrlOfferings = 0, enrlOfferingsFull = 0, enrlOfferings98 = 0, enrlOfferings95 = 0, enrlOfferings90 = 0;
    1158                        for (Offering offering: model.getOfferings()) {
    1159                                if (offering.isDummy()) continue;
    1160                                int crs = 0;
    1161                                for (Course course: offering.getCourses()) {
    1162                                        for (CourseRequest cr: course.getRequests()) {
    1163                                                if (group.matches(cr.getStudent()) && filter.matches(cr)) crs++;
    1164                                        }
    1165                                }
    1166                                if (crs == 0) continue;
    1167                            int offeringLimit = 0, offeringEnrollment = 0, offeringMatchingEnrollment = 0;
    1168                            for (Config config: offering.getConfigs()) {
    1169                                int configLimit = config.getLimit();
    1170                                for (Subpart subpart: config.getSubparts()) {
    1171                                    int subpartLimit = 0;
    1172                                    for (Section section: subpart.getSections()) {
    1173                                        if (section.isCancelled()) continue;
    1174                                        int enrl = section.getEnrollments(assignment).size();
    1175                                        int matchingEnrl = getEnrollments(group, section, assignment, filter);
    1176                                        if (section.getLimit() < 0 || subpartLimit < 0)
    1177                                            subpartLimit = -1;
    1178                                        else
    1179                                            subpartLimit += (section.isEnabled() ? section.getLimit() : enrl);
    1180                                        nbrSections ++;
    1181                                        enrlSections += matchingEnrl;
    1182                                        if (section.getLimit() >= 0 && section.getLimit() <= enrl) {
    1183                                            nbrFullSections ++;
    1184                                            enrlFullSections += matchingEnrl;
    1185                                        }
    1186                                        if (!section.isEnabled()) { //&& (enrl > 0 || section.getLimit() >= 0)) {
    1187                                            nbrSectionsDis ++;
    1188                                            enrlSectionsDis += matchingEnrl;
    1189                                        }
    1190                                        if (section.getLimit() >= 0 && (section.getLimit() - enrl) <= Math.round(0.02 * section.getLimit())) {
    1191                                            nbrSections98 ++;
    1192                                            enrlSections98 += matchingEnrl;
    1193                                        }
    1194                                        if (section.getLimit() >= 0 && (section.getLimit() - enrl) <= Math.round(0.05 * section.getLimit())) {
    1195                                            nbrSections95 ++;
    1196                                            enrlSections95 += matchingEnrl;
    1197                                        }
    1198                                        if (section.getLimit() >= 0 && (section.getLimit() - enrl) <= Math.round(0.10 * section.getLimit())) {
    1199                                            nbrSections90 ++;
    1200                                            enrlSections90 += matchingEnrl;
    1201                                        }
    1202                                    }
    1203                                    if (configLimit < 0 || subpartLimit < 0)
    1204                                        configLimit = -1;
    1205                                    else
    1206                                        configLimit = Math.min(configLimit, subpartLimit);
    1207                                }
    1208                                if (offeringLimit < 0 || configLimit < 0)
    1209                                    offeringLimit = -1;
    1210                                else
    1211                                    offeringLimit += configLimit;
    1212                                offeringEnrollment += config.getEnrollments(assignment).size();
    1213                                offeringMatchingEnrollment += getEnrollments(group, config, assignment, filter);
    1214                            }
    1215                            nbrOfferings ++;
    1216                            enrlOfferings += offeringMatchingEnrollment;
    1217
    1218                            if (offeringLimit >=0 && offeringEnrollment >= offeringLimit) {
    1219                                nbrFullOfferings ++;
    1220                                enrlOfferingsFull += offeringMatchingEnrollment;
    1221                            }
    1222                            if (offeringLimit >= 0 && (offeringLimit - offeringEnrollment) <= Math.round(0.02 * offeringLimit)) {
    1223                                nbrOfferings98++;
    1224                                enrlOfferings98 += offeringMatchingEnrollment;
    1225                            }
    1226                            if (offeringLimit >= 0 && (offeringLimit - offeringEnrollment) <= Math.round(0.05 * offeringLimit)) {
    1227                                nbrOfferings95++;
    1228                                enrlOfferings95 += offeringMatchingEnrollment;
    1229                            }
    1230                            if (offeringLimit >= 0 && (offeringLimit - offeringEnrollment) <= Math.round(0.10 * offeringLimit)) {
    1231                                nbrOfferings90++;
    1232                                enrlOfferings90 += offeringMatchingEnrollment;
    1233                            }
    1234                        }
    1235                        return new String[] {
    1236                                        sIntFormat.format(nbrFullOfferings), sPercentFormat.format(100.0 * nbrFullOfferings / nbrOfferings) + "%", sPercentFormat.format(100.0 * enrlOfferingsFull / enrlOfferings) + "%",
    1237                                        sIntFormat.format(nbrOfferings98), sPercentFormat.format(100.0 * nbrOfferings98 / nbrOfferings) + "%", sPercentFormat.format(100.0 * enrlOfferings98 / enrlOfferings) + "%",
    1238                                        sIntFormat.format(nbrOfferings95), sPercentFormat.format(100.0 * nbrOfferings95 / nbrOfferings) + "%", sPercentFormat.format(100.0 * enrlOfferings95 / enrlOfferings) + "%",
    1239                                        sIntFormat.format(nbrOfferings90), sPercentFormat.format(100.0 * nbrOfferings90 / nbrOfferings) + "%", sPercentFormat.format(100.0 * enrlOfferings90 / enrlOfferings) + "%",
    1240                                        sIntFormat.format(nbrFullSections), sPercentFormat.format(100.0 * nbrFullSections / nbrSections) + "%", sPercentFormat.format(100.0 * enrlFullSections / enrlSections) + "%",
    1241                                        sIntFormat.format(nbrSectionsDis), sPercentFormat.format(100.0 * nbrSectionsDis / nbrSections) + "%", sPercentFormat.format(100.0 * enrlSectionsDis / enrlSections) + "%",
    1242                                        sIntFormat.format(nbrSections98), sPercentFormat.format(100.0 * nbrSections98 / nbrSections) + "%", sPercentFormat.format(100.0 * enrlSections98 / enrlSections) + "%",
    1243                                        sIntFormat.format(nbrSections95), sPercentFormat.format(100.0 * nbrSections95 / nbrSections) + "%", sPercentFormat.format(100.0 * enrlSections95 / enrlSections) + "%",
    1244                                        sIntFormat.format(nbrSections90), sPercentFormat.format(100.0 * nbrSections90 / nbrSections) + "%", sPercentFormat.format(100.0 * enrlSections90 / enrlSections) + "%",
    1245                        };
    1246                        }
    1247        }),
    1248        ;
    1249        String[] iNames;
    1250        String[] iNotes;
    1251        Statistic iStatistic;
    1252        boolean iNewLine = false;
    1253        Statistics(String[] names, String notes[], Statistic stat, boolean nl) {
    1254            iNames = names; iNotes = notes; iStatistic = stat; iNewLine = nl;
    1255        }
    1256        Statistics(String name, String note, Statistic stat, boolean nl) {
    1257            this(new String[] {name}, new String[] {note}, stat, nl);
    1258        }
    1259        Statistics(String[] names, String notes[], Statistic stat) {
    1260            this(names, notes, stat, false);
    1261        }
    1262        Statistics(String name, String note, Statistic stat) {
    1263            this(name, note, stat, false);
    1264        }
    1265        public String[] getNames() { return iNames; }
    1266        public String[] getNotes() { return iNotes; }
    1267        public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) {
    1268            return iStatistic.getValues(group, model, assignment, filter);
    1269        }
    1270        public boolean isNewLine() { return iNewLine; }
    1271    }
    1272
    1273    @Override
    1274    public CSVFile createTable(Assignment<Request, Enrollment> assignment, DataProperties properties) {
    1275        CSVFile csv = new CSVFile();
    1276        List<CSVField> header = new ArrayList<CSVField>();
    1277        List<StudentGroup> groups = new ArrayList<StudentGroup>();
    1278        header.add(new CSVField(""));
    1279        Map<Integer, StudentGroup> counts = new HashMap<Integer, StudentGroup>();
    1280
    1281        for (StudentGroup g: StudentGroup.values()) {
    1282            int nrStudents = 0;
    1283            for (Student student: getModel().getStudents()) {
    1284                if (g.matches(student, this)) nrStudents ++;
    1285            }
    1286            if (nrStudents > 0 && !counts.containsKey(nrStudents)) {
    1287                groups.add(g);
    1288                header.add(new CSVField(g.getName()));
    1289                counts.put(nrStudents, g);
    1290            }
    1291        }
    1292        header.add(new CSVField("Note"));
    1293        csv.setHeader(header);
    1294        for (Statistics stat: Statistics.values()) {
    1295            String[] names = stat.getNames();
    1296            List<List<CSVField>> table = new ArrayList<List<CSVField>>();
    1297            for (String name: names) {
    1298                List<CSVField> line = new ArrayList<CSVField>(); line.add(new CSVField(name));
    1299                table.add(line);
    1300            }
    1301            for (StudentGroup g: groups) {
    1302                String[] values = stat.getValues(g, getModel(), assignment, this);
    1303                for (int i = 0; i < values.length; i++) {
    1304                    table.get(i).add(new CSVField(values[i]));
    1305                }
    1306            }
    1307            String[] notes = stat.getNotes();
    1308            for (int i = 0; i < notes.length; i++) {
    1309                table.get(i).add(new CSVField(notes[i]));
    1310            }
    1311            for (List<CSVField> line: table) {
    1312                csv.addLine(line);
    1313            }
    1314            if (stat.isNewLine())
    1315                csv.addLine(new CSVField[] {new CSVField(" ")});
    1316        }
    1317        return csv;
    1318    }
    1319
    1320    public static void main(String[] args) {
    1321        try {
    1322            DataProperties cfg = new DataProperties();
    1323            cfg.setProperty("General.Input", args[0]);
    1324            cfg.setProperty("Distances.Ellipsoid", "WGS84");
    1325            cfg.setProperty("Distances.ShortDistanceAccommodationReference", "SD");
    1326            StudentSectioningModel model = new StudentSectioningModel(cfg);
    1327            model.setStudentQuality(new StudentQuality((DistanceMetric)null, cfg));
    1328            Assignment<Request, Enrollment> assignment = new DefaultSingleAssignment<Request, Enrollment>();
    1329            new StudentSectioningXMLLoader(model, assignment).load();
    1330            new SolutionStatsReport(model).create(assignment, cfg).save(new File(new File(args[0]).getParentFile(), "stats.csv"));
    1331        } catch (Exception e) {
    1332            e.printStackTrace();
    1333        }
    1334
    1335    }
    1336
    1337}