    001package org.cpsolver.studentsct.report;
    002
    003import java.util.ArrayList;
    004import java.util.Collections;
    005import java.util.List;
    006import java.util.regex.Matcher;
    007import java.util.regex.Pattern;
    008
    009import org.cpsolver.coursett.Constants;
    010import org.cpsolver.coursett.model.RoomLocation;
    011import org.cpsolver.ifs.assignment.Assignment;
    012import org.cpsolver.ifs.util.CSVFile;
    013import org.cpsolver.ifs.util.DataProperties;
    014import org.cpsolver.ifs.util.Query;
    015import org.cpsolver.studentsct.StudentSectioningModel;
    016import org.cpsolver.studentsct.model.Config;
    017import org.cpsolver.studentsct.model.Course;
    018import org.cpsolver.studentsct.model.CourseRequest;
    019import org.cpsolver.studentsct.model.Enrollment;
    020import org.cpsolver.studentsct.model.Instructor;
    021import org.cpsolver.studentsct.model.Request;
    022import org.cpsolver.studentsct.model.Request.RequestPriority;
    023import org.cpsolver.studentsct.model.Section;
    024import org.cpsolver.studentsct.model.Student;
    025import org.cpsolver.studentsct.model.Student.BackToBackPreference;
    026import org.cpsolver.studentsct.model.Student.ModalityPreference;
    027import org.cpsolver.studentsct.reservation.UniversalOverride;
    028
    029/**
    030 * Abstract student sectioning report. Adds filtering capabilities using the
    031 * filter parameter. It also checks the lastlike and real parameters (whether to
    032 * include projected and real students respectively) and passes on the useAmPm parameter
    033 * as {@link #isUseAmPm()}. The filter replicates most of the capabilities available
    034 * in UniTime on the Batch Student Solver Dashboard page.
    035 *
    036 * <br>
    037 * <br>
    038 *
    039 * @author  Tomáš Müller
    040 * @version StudentSct 1.3 (Student Sectioning)<br>
    041 *          Copyright (C) 2007 - 2014 Tomáš Müller<br>
    042 *          <a href="mailto:muller@unitime.org">muller@unitime.org</a><br>
    043 *          <a href="http://muller.unitime.org">http://muller.unitime.org</a><br>
    044 * <br>
    045 *          This library is free software; you can redistribute it and/or modify
    046 *          it under the terms of the GNU Lesser General Public License as
    047 *          published by the Free Software Foundation; either version 3 of the
    048 *          License, or (at your option) any later version. <br>
    049 * <br>
    050 *          This library is distributed in the hope that it will be useful, but
    051 *          WITHOUT ANY WARRANTY; without even the implied warranty of
    052 *          MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
    053 *          Lesser General Public License for more details. <br>
    054 * <br>
    055 *          You should have received a copy of the GNU Lesser General Public
    056 *          License along with this library; if not see
    057 *          <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>.
    058 */
    059public abstract class AbstractStudentSectioningReport implements StudentSectioningReport, StudentSectioningReport.Filter {
    060    private StudentSectioningModel iModel = null;
    061    private Query iFilter = null;
    062    private String iUser = null;
    063    private Assignment<Request, Enrollment> iAssignment;
    064    private boolean iIncludeLastLike = false;
    065    private boolean iIncludeReal = true;
    066    private boolean iUseAmPm = true;
    067
    068    public AbstractStudentSectioningReport(StudentSectioningModel model) {
    069        iModel = model;
    070    }
    071
    072    /**
    073     * Returns the student sectioning model
    074     */
    075    public StudentSectioningModel getModel() {
    076        return iModel;
    077    }
    078
    079    @Override
    080    public CSVFile create(Assignment<Request, Enrollment> assignment, DataProperties properties) {
    081        String filter = properties.getProperty("filter");
    082        if (filter != null && !filter.isEmpty())
    083            iFilter = new Query(filter);
    084        iUser = properties.getProperty("user");
    085        iIncludeLastLike = properties.getPropertyBoolean("lastlike", false);
    086        iIncludeReal = properties.getPropertyBoolean("real", true);
    087        iUseAmPm = properties.getPropertyBoolean("useAmPm", true);
    088        iAssignment = assignment;
    089        return createTable(assignment, properties);
    090    }
    091
    092    @Override
    093    public boolean matches(Request r, Enrollment e) {
    094        if (iFilter == null)
    095            return true;
    096        if (r.getStudent().isDummy() && !iIncludeLastLike) return false;
    097        if (!r.getStudent().isDummy() && !iIncludeReal) return false;
    098        return iFilter.match(new RequestMatcher(r, e, iAssignment, iUser));
    099    }
    100
    101    @Override
    102    public boolean matches(Request r) {
    103        if (iFilter == null)
    104            return true;
    105        if (r.getStudent().isDummy() && !iIncludeLastLike) return false;
    106        if (!r.getStudent().isDummy() && !iIncludeReal) return false;
    107        return iFilter.match(new RequestMatcher(r, iAssignment.getValue(r), iAssignment, iUser));
    108    }
    109
    110    @Override
    111    public boolean matches(Course c) {
    112        if (iFilter == null) return true;
    113        return iFilter.match(new CourseMatcher(c));
    114    }
    115
    116    @Override
    117    public boolean matches(Student student) {
    118        for (Request r: student.getRequests())
    119            if (matches(r)) return true;
    120        return false;
    121    }
    122
    123    /**
    124     * Time display settings
    125     */
    126    public boolean isUseAmPm() { return iUseAmPm; }
    127
    128    public abstract CSVFile createTable(Assignment<Request, Enrollment> assignment, DataProperties properties);
    129
    130    public static class RequestMatcher extends UniversalOverride.StudentMatcher {
    131        private Request iRequest;
    132        private Enrollment iEnrollment;
    133        private String iUser;
    134        private Assignment<Request, Enrollment> iAssignment;
    135
    136        public RequestMatcher(Request request, Enrollment enrollment, Assignment<Request, Enrollment> assignment, String user) {
    137            super(request.getStudent(), enrollment == null ? null : enrollment.getCourse());
    138            iRequest = request;
    139            iEnrollment = enrollment;
    140            iAssignment = assignment;
    141            iUser = user;
    142        }
    143
    144        public boolean isAssigned() {
    145            return iEnrollment != null;
    146        }
    147
    148        public Enrollment enrollment() {
    149            return iEnrollment;
    150        }
    151
    152        public Request request() {
    153            return iRequest;
    154        }
    155
    156        public CourseRequest cr() {
    157            return iRequest instanceof CourseRequest ? (CourseRequest) iRequest : null;
    158        }
    159
    160        public Course course() {
    161            if (enrollment() != null)
    162                return enrollment().getCourse();
    163            else if (request() instanceof CourseRequest)
    164                return ((CourseRequest) request()).getCourses().get(0);
    165            else
    166                return null;
    167        }
    168
    169        @Override
    170        public boolean match(String attr, String term) {
    171            if (super.match(attr, term))
    172                return true;
    173
    174            if ("assignment".equals(attr)) {
    175                if (eq("Assigned", term)) {
    176                    return isAssigned();
    177                } else if (eq("Reserved", term)) {
    178                    return isAssigned() && enrollment().getReservation() != null;
    179                } else if (eq("Not Assigned", term)) {
    180                    return !isAssigned() && !request().isAlternative();
    181                } else if (eq("Wait-Listed", term)) {
    182                    if (enrollment() == null)
    183                        return cr() != null && cr().isWaitlist();
    184                    else
    185                        return enrollment().isWaitlisted();
    186                } else if (eq("Critical", term)) {
    187                    return request().getRequestPriority() == RequestPriority.Critical;
    188                } else if (eq("Assigned Critical", term)) {
    189                    return request().getRequestPriority() == RequestPriority.Critical && isAssigned();
    190                } else if (eq("Not Assigned Critical", term)) {
    191                    return request().getRequestPriority() == RequestPriority.Critical && !isAssigned();
    192                } else if (eq("Vital", term)) {
    193                    return request().getRequestPriority() == RequestPriority.Vital;
    194                } else if (eq("Assigned Vital", term)) {
    195                    return request().getRequestPriority() == RequestPriority.Vital && isAssigned();
    196                } else if (eq("Not Assigned Vital", term)) {
    197                    return request().getRequestPriority() == RequestPriority.Vital && !isAssigned();
    198                } else if (eq("Visiting F2F", term)) {
    199                    return request().getRequestPriority() == RequestPriority.VisitingF2F;
    200                } else if (eq("Assigned Visiting F2F", term)) {
    201                    return request().getRequestPriority() == RequestPriority.VisitingF2F && isAssigned();
    202                } else if (eq("Not Assigned Visiting F2F", term)) {
    203                    return request().getRequestPriority() == RequestPriority.VisitingF2F && !isAssigned();
    204                } else if (eq("LC", term)) {
    205                    return request().getRequestPriority() == RequestPriority.LC;
    206                } else if (eq("Assigned LC", term)) {
    207                    return request().getRequestPriority() == RequestPriority.LC && isAssigned();
    208                } else if (eq("Not Assigned LC", term)) {
    209                    return request().getRequestPriority() == RequestPriority.LC && !isAssigned();
    210                } else if (eq("Important", term)) {
    211                    return request().getRequestPriority() == RequestPriority.Important;
    212                } else if (eq("Assigned Important", term)) {
    213                    return request().getRequestPriority() == RequestPriority.Important && isAssigned();
    214                } else if (eq("Not Assigned Important", term)) {
    215                    return request().getRequestPriority() == RequestPriority.Important && !isAssigned();
    216                } else if (eq("No-Subs", term) || eq("No-Substitutes", term)) {
    217                    return cr() != null && cr().isWaitlist();
    218                } else if (eq("Assigned No-Subs", term) || eq("Assigned  No-Substitutes", term)) {
    219                    return cr() != null && cr().isWaitlist() && isAssigned();
    220                } else if (eq("Not Assigned No-Subs", term) || eq("Not Assigned No-Substitutes", term)) {
    221                    return cr() != null && cr().isWaitlist() && !isAssigned();
    222                }
    223            }
    224
    225            if ("assigned".equals(attr) || "scheduled".equals(attr)) {
    226                if (eq("true", term) || eq("1", term))
    227                    return isAssigned();
    228                else
    229                    return !isAssigned();
    230            }
    231
    232            if ("waitlisted".equals(attr) || "waitlist".equals(attr)) {
    233                if (eq("true", term) || eq("1", term))
    234                    return !isAssigned() && cr() != null && cr().isWaitlist();
    235                else
    236                    return isAssigned() && cr() != null && cr().isWaitlist();
    237            }
    238
    239            if ("no-substitutes".equals(attr) || "no-subs".equals(attr)) {
    240                if (eq("true", term) || eq("1", term))
    241                    return !isAssigned() && cr() != null && cr().isWaitlist();
    242                else
    243                    return isAssigned() && cr() != null && cr().isWaitlist();
    244            }
    245
    246            if ("reservation".equals(attr) || "reserved".equals(attr)) {
    247                if (eq("true", term) || eq("1", term))
    248                    return isAssigned() && enrollment().getReservation() != null;
    249                else
    250                    return isAssigned() && enrollment().getReservation() == null;
    251            }
    252
    253            if ("mode".equals(attr)) {
    254                if (eq("My Students", term)) {
    255                    if (iUser == null)
    256                        return false;
    257                    for (Instructor a : student().getAdvisors())
    258                        if (eq(a.getExternalId(), iUser))
    259                            return true;
    260                    return false;
    261                }
    262                return true;
    263            }
    264
    265            if ("status".equals(attr)) {
    266                if ("default".equalsIgnoreCase(term) || "Not Set".equalsIgnoreCase(term))
    267                    return student().getStatus() == null;
    268                return like(student().getStatus(), term);
    269            }
    270
    271            if ("credit".equals(attr)) {
    272                float min = 0, max = Float.MAX_VALUE;
    273                Credit prefix = Credit.eq;
    274                String number = term;
    275                if (number.startsWith("<=")) {
    276                    prefix = Credit.le;
    277                    number = number.substring(2);
    278                } else if (number.startsWith(">=")) {
    279                    prefix = Credit.ge;
    280                    number = number.substring(2);
    281                } else if (number.startsWith("<")) {
    282                    prefix = Credit.lt;
    283                    number = number.substring(1);
    284                } else if (number.startsWith(">")) {
    285                    prefix = Credit.gt;
    286                    number = number.substring(1);
    287                } else if (number.startsWith("=")) {
    288                    prefix = Credit.eq;
    289                    number = number.substring(1);
    290                }
    291                String im = null;
    292                try {
    293                    float a = Float.parseFloat(number);
    294                    switch (prefix) {
    295                        case eq:
    296                            min = max = a;
    297                            break; // = a
    298                        case le:
    299                            max = a;
    300                            break; // <= a
    301                        case ge:
    302                            min = a;
    303                            break; // >= a
    304                        case lt:
    305                            max = a - 0.0001f;
    306                            break; // < a
    307                        case gt:
    308                            min = a + 0.0001f;
    309                            break; // > a
    310                    }
    311                } catch (NumberFormatException e) {
    312                    Matcher m = Pattern.compile("([0-9]+\\.?[0-9]*)([^0-9\\.].*)").matcher(number);
    313                    if (m.matches()) {
    314                        float a = Float.parseFloat(m.group(1));
    315                        im = m.group(2).trim();
    316                        switch (prefix) {
    317                            case eq:
    318                                min = max = a;
    319                                break; // = a
    320                            case le:
    321                                max = a;
    322                                break; // <= a
    323                            case ge:
    324                                min = a;
    325                                break; // >= a
    326                            case lt:
    327                                max = a - 0.0001f;
    328                                break; // < a
    329                            case gt:
    330                                min = a + 0.0001f;
    331                                break; // > a
    332                        }
    333                    }
    334                }
    335                if (term.contains("..")) {
    336                    try {
    337                        String a = term.substring(0, term.indexOf(".."));
    338                        String b = term.substring(term.indexOf("..") + 2);
    339                        min = Float.parseFloat(a);
    340                        max = Float.parseFloat(b);
    341                    } catch (NumberFormatException e) {
    342                        Matcher m = Pattern.compile("([0-9]+\\.?[0-9]*)\\.\\.([0-9]+\\.?[0-9]*)([^0-9].*)")
    343                                .matcher(term);
    344                        if (m.matches()) {
    345                            min = Float.parseFloat(m.group(1));
    346                            max = Float.parseFloat(m.group(2));
    347                            im = m.group(3).trim();
    348                        }
    349                    }
    350                }
    351                float credit = 0;
    352                for (Request r : student().getRequests()) {
    353                    if (r instanceof CourseRequest) {
    354                        CourseRequest cr = (CourseRequest) r;
    355                        Enrollment e = iAssignment.getValue(cr);
    356                        if (e == null)
    357                            continue;
    358                        Config g = e.getConfig();
    359                        if (g != null) {
    360                            if ("!".equals(im) && g.getInstructionalMethodReference() != null)
    361                                continue;
    362                            if (im != null && !"!".equals(im)
    363                                    && !im.equalsIgnoreCase(g.getInstructionalMethodReference()))
    364                                continue;
    365                            if (g.hasCreditValue())
    366                                credit += g.getCreditValue();
    367                            else if (e.getCourse().hasCreditValue())
    368                                credit += e.getCourse().getCreditValue();
    369                        }
    370                    }
    371                }
    372                return min <= credit && credit <= max;
    373            }
    374
    375            if ("rc".equals(attr) || "requested-credit".equals(attr)) {
    376                float min = 0, max = Float.MAX_VALUE;
    377                Credit prefix = Credit.eq;
    378                String number = term;
    379                if (number.startsWith("<=")) {
    380                    prefix = Credit.le;
    381                    number = number.substring(2);
    382                } else if (number.startsWith(">=")) {
    383                    prefix = Credit.ge;
    384                    number = number.substring(2);
    385                } else if (number.startsWith("<")) {
    386                    prefix = Credit.lt;
    387                    number = number.substring(1);
    388                } else if (number.startsWith(">")) {
    389                    prefix = Credit.gt;
    390                    number = number.substring(1);
    391                } else if (number.startsWith("=")) {
    392                    prefix = Credit.eq;
    393                    number = number.substring(1);
    394                }
    395                try {
    396                    float a = Float.parseFloat(number);
    397                    switch (prefix) {
    398                        case eq:
    399                            min = max = a;
    400                            break; // = a
    401                        case le:
    402                            max = a;
    403                            break; // <= a
    404                        case ge:
    405                            min = a;
    406                            break; // >= a
    407                        case lt:
    408                            max = a - 0.0001f;
    409                            break; // < a
    410                        case gt:
    411                            min = a + 0.0001f;
    412                            break; // > a
    413                    }
    414                } catch (NumberFormatException e) {
    415                }
    416                if (term.contains("..")) {
    417                    try {
    418                        String a = term.substring(0, term.indexOf(".."));
    419                        String b = term.substring(term.indexOf("..") + 2);
    420                        min = Float.parseFloat(a);
    421                        max = Float.parseFloat(b);
    422                    } catch (NumberFormatException e) {
    423                    }
    424                }
    425                if (min == 0 && max == Float.MAX_VALUE)
    426                    return true;
    427                float studentMinTot = 0f, studentMaxTot = 0f;
    428                int nrCoursesTot = 0;
    429                List<Float> minsTot = new ArrayList<Float>();
    430                List<Float> maxsTot = new ArrayList<Float>();
    431                for (Request r : student().getRequests()) {
    432                    if (r instanceof CourseRequest) {
    433                        CourseRequest cr = (CourseRequest) r;
    434                        Float minTot = null, maxTot = null;
    435                        for (Course c : cr.getCourses()) {
    436                            if (c.hasCreditValue()) {
    437                                if (minTot == null || minTot > c.getCreditValue())
    438                                    minTot = c.getCreditValue();
    439                                if (maxTot == null || maxTot < c.getCreditValue())
    440                                    maxTot = c.getCreditValue();
    441                            }
    442                        }
    443                        if (cr.isWaitlist()) {
    444                            if (minTot != null) {
    445                                studentMinTot += minTot;
    446                                studentMaxTot += maxTot;
    447                            }
    448                        } else {
    449                            if (minTot != null) {
    450                                minsTot.add(minTot);
    451                                maxsTot.add(maxTot);
    452                                if (!r.isAlternative())
    453                                    nrCoursesTot++;
    454                            }
    455                        }
    456                    }
    457                }
    458                Collections.sort(minsTot);
    459                Collections.sort(maxsTot);
    460                for (int i = 0; i < nrCoursesTot; i++) {
    461                    studentMinTot += minsTot.get(i);
    462                    studentMaxTot += maxsTot.get(maxsTot.size() - i - 1);
    463                }
    464                return min <= studentMaxTot && studentMinTot <= max;
    465            }
    466
    467            if ("fc".equals(attr) || "first-choice-credit".equals(attr)) {
    468                float min = 0, max = Float.MAX_VALUE;
    469                Credit prefix = Credit.eq;
    470                String number = term;
    471                if (number.startsWith("<=")) {
    472                    prefix = Credit.le;
    473                    number = number.substring(2);
    474                } else if (number.startsWith(">=")) {
    475                    prefix = Credit.ge;
    476                    number = number.substring(2);
    477                } else if (number.startsWith("<")) {
    478                    prefix = Credit.lt;
    479                    number = number.substring(1);
    480                } else if (number.startsWith(">")) {
    481                    prefix = Credit.gt;
    482                    number = number.substring(1);
    483                } else if (number.startsWith("=")) {
    484                    prefix = Credit.eq;
    485                    number = number.substring(1);
    486                }
    487                try {
    488                    float a = Float.parseFloat(number);
    489                    switch (prefix) {
    490                        case eq:
    491                            min = max = a;
    492                            break; // = a
    493                        case le:
    494                            max = a;
    495                            break; // <= a
    496                        case ge:
    497                            min = a;
    498                            break; // >= a
    499                        case lt:
    500                            max = a - 0.0001f;
    501                            break; // < a
    502                        case gt:
    503                            min = a + 0.0001f;
    504                            break; // > a
    505                    }
    506                } catch (NumberFormatException e) {
    507                }
    508                if (term.contains("..")) {
    509                    try {
    510                        String a = term.substring(0, term.indexOf(".."));
    511                        String b = term.substring(term.indexOf("..") + 2);
    512                        min = Float.parseFloat(a);
    513                        max = Float.parseFloat(b);
    514                    } catch (NumberFormatException e) {
    515                    }
    516                }
    517                if (min == 0 && max == Float.MAX_VALUE)
    518                    return true;
    519                float credit = 0f;
    520                for (Request r : student().getRequests()) {
    521                    if (r instanceof CourseRequest) {
    522                        CourseRequest cr = (CourseRequest) r;
    523                        for (Course c : cr.getCourses()) {
    524                            if (c != null && c.hasCreditValue()) {
    525                                credit += c.getCreditValue();
    526                                break;
    527                            }
    528                        }
    529                    }
    530                }
    531                return min <= credit && credit <= max;
    532            }
    533
    534            if ("rp".equals(attr)) {
    535                if ("subst".equalsIgnoreCase(term))
    536                    return request().isAlternative();
    537                int min = 0, max = Integer.MAX_VALUE;
    538                Credit prefix = Credit.eq;
    539                String number = term;
    540                if (number.startsWith("<=")) {
    541                    prefix = Credit.le;
    542                    number = number.substring(2);
    543                } else if (number.startsWith(">=")) {
    544                    prefix = Credit.ge;
    545                    number = number.substring(2);
    546                } else if (number.startsWith("<")) {
    547                    prefix = Credit.lt;
    548                    number = number.substring(1);
    549                } else if (number.startsWith(">")) {
    550                    prefix = Credit.gt;
    551                    number = number.substring(1);
    552                } else if (number.startsWith("=")) {
    553                    prefix = Credit.eq;
    554                    number = number.substring(1);
    555                }
    556                try {
    557                    int a = Integer.parseInt(number);
    558                    switch (prefix) {
    559                        case eq:
    560                            min = max = a;
    561                            break; // = a
    562                        case le:
    563                            max = a;
    564                            break; // <= a
    565                        case ge:
    566                            min = a;
    567                            break; // >= a
    568                        case lt:
    569                            max = a - 1;
    570                            break; // < a
    571                        case gt:
    572                            min = a + 1;
    573                            break; // > a
    574                    }
    575                } catch (NumberFormatException e) {
    576                }
    577                if (term.contains("..")) {
    578                    try {
    579                        String a = term.substring(0, term.indexOf(".."));
    580                        String b = term.substring(term.indexOf("..") + 2);
    581                        min = Integer.parseInt(a);
    582                        max = Integer.parseInt(b);
    583                    } catch (NumberFormatException e) {
    584                    }
    585                }
    586                if (min == 0 && max == Integer.MAX_VALUE)
    587                    return true;
    588                return !request().isAlternative() && min <= request().getPriority() + 1
    589                        && request().getPriority() + 1 <= max;
    590            }
    591
    592            if ("choice".equals(attr) || "ch".equals(attr)) {
    593                if (cr() == null)
    594                    return false;
    595                int min = 0, max = Integer.MAX_VALUE;
    596                Credit prefix = Credit.eq;
    597                String number = term;
    598                if (number.startsWith("<=")) {
    599                    prefix = Credit.le;
    600                    number = number.substring(2);
    601                } else if (number.startsWith(">=")) {
    602                    prefix = Credit.ge;
    603                    number = number.substring(2);
    604                } else if (number.startsWith("<")) {
    605                    prefix = Credit.lt;
    606                    number = number.substring(1);
    607                } else if (number.startsWith(">")) {
    608                    prefix = Credit.gt;
    609                    number = number.substring(1);
    610                } else if (number.startsWith("=")) {
    611                    prefix = Credit.eq;
    612                    number = number.substring(1);
    613                }
    614                try {
    615                    int a = Integer.parseInt(number);
    616                    switch (prefix) {
    617                        case eq:
    618                            min = max = a;
    619                            break; // = a
    620                        case le:
    621                            max = a;
    622                            break; // <= a
    623                        case ge:
    624                            min = a;
    625                            break; // >= a
    626                        case lt:
    627                            max = a - 1;
    628                            break; // < a
    629                        case gt:
    630                            min = a + 1;
    631                            break; // > a
    632                    }
    633                } catch (NumberFormatException e) {
    634                }
    635                if (term.contains("..")) {
    636                    try {
    637                        String a = term.substring(0, term.indexOf(".."));
    638                        String b = term.substring(term.indexOf("..") + 2);
    639                        min = Integer.parseInt(a);
    640                        max = Integer.parseInt(b);
    641                    } catch (NumberFormatException e) {
    642                    }
    643                }
    644                if (min == 0 && max == Integer.MAX_VALUE)
    645                    return true;
    646                if (enrollment() != null) {
    647                    int choice = 1;
    648                    for (Course course : cr().getCourses()) {
    649                        if (course.equals(enrollment().getCourse())) {
    650                            return min <= choice && choice <= max;
    651                        }
    652                        choice++;
    653                    }
    654                    return false;
    655                } else if (!request().isAlternative()) {
    656                    int choice = cr().getCourses().size();
    657                    return min <= choice && choice <= max;
    658                } else {
    659                    return false;
    660                }
    661            }
    662
    663            if ("btb".equals(attr)) {
    664                if ("prefer".equalsIgnoreCase(term) || "preferred".equalsIgnoreCase(term))
    665                    return student().getBackToBackPreference() == BackToBackPreference.BTB_PREFERRED;
    666                else if ("disc".equalsIgnoreCase(term) || "discouraged".equalsIgnoreCase(term))
    667                    return student().getBackToBackPreference() == BackToBackPreference.BTB_DISCOURAGED;
    668                else
    669                    return student().getBackToBackPreference() == BackToBackPreference.NO_PREFERENCE;
    670            }
    671
    672            if ("online".equals(attr)) {
    673                if ("prefer".equalsIgnoreCase(term) || "preferred".equalsIgnoreCase(term))
    674                    return student().getModalityPreference() == ModalityPreference.ONLINE_PREFERRED;
    675                else if ("require".equalsIgnoreCase(term) || "required".equalsIgnoreCase(term))
    676                    return student().getModalityPreference() == ModalityPreference.ONLINE_REQUIRED;
    677                else if ("disc".equalsIgnoreCase(term) || "discouraged".equalsIgnoreCase(term))
    678                    return student().getModalityPreference() == ModalityPreference.ONILNE_DISCOURAGED;
    679                else if ("no".equalsIgnoreCase(term) || "no-preference".equalsIgnoreCase(term))
    680                    return student().getModalityPreference() == ModalityPreference.NO_PREFERENCE;
    681            }
    682
    683            if ("online".equals(attr) || "face-to-face".equals(attr) || "f2f".equals(attr) || "no-time".equals(attr)
    684                    || "has-time".equals(attr)) {
    685                int min = 0, max = Integer.MAX_VALUE;
    686                Credit prefix = Credit.eq;
    687                String number = term;
    688                if (number.startsWith("<=")) {
    689                    prefix = Credit.le;
    690                    number = number.substring(2);
    691                } else if (number.startsWith(">=")) {
    692                    prefix = Credit.ge;
    693                    number = number.substring(2);
    694                } else if (number.startsWith("<")) {
    695                    prefix = Credit.lt;
    696                    number = number.substring(1);
    697                } else if (number.startsWith(">")) {
    698                    prefix = Credit.gt;
    699                    number = number.substring(1);
    700                } else if (number.startsWith("=")) {
    701                    prefix = Credit.eq;
    702                    number = number.substring(1);
    703                }
    704                boolean perc = false;
    705                if (number.endsWith("%")) {
    706                    perc = true;
    707                    number = number.substring(0, number.length() - 1).trim();
    708                }
    709                try {
    710                    int a = Integer.parseInt(number);
    711                    switch (prefix) {
    712                        case eq:
    713                            min = max = a;
    714                            break; // = a
    715                        case le:
    716                            max = a;
    717                            break; // <= a
    718                        case ge:
    719                            min = a;
    720                            break; // >= a
    721                        case lt:
    722                            max = a - 1;
    723                            break; // < a
    724                        case gt:
    725                            min = a + 1;
    726                            break; // > a
    727                    }
    728                } catch (NumberFormatException e) {
    729                }
    730                if (term.contains("..")) {
    731                    try {
    732                        String a = term.substring(0, term.indexOf(".."));
    733                        String b = term.substring(term.indexOf("..") + 2);
    734                        min = Integer.parseInt(a);
    735                        max = Integer.parseInt(b);
    736                    } catch (NumberFormatException e) {
    737                    }
    738                }
    739                if (min == 0 && max == Integer.MAX_VALUE)
    740                    return true;
    741                int match = 0, total = 0;
    742                for (Request r : student().getRequests()) {
    743                    if (r instanceof CourseRequest) {
    744                        CourseRequest cr = (CourseRequest) r;
    745                        Enrollment e = iAssignment.getValue(cr);
    746                        if (e == null)
    747                            continue;
    748                        for (Section section : enrollment().getSections()) {
    749                            if ("online".equals(attr) && section.isOnline())
    750                                match++;
    751                            else if (("face-to-face".equals(attr) || "f2f".equals(attr)) && !section.isOnline())
    752                                match++;
    753                            else if ("no-time".equals(attr)
    754                                    && (section.getTime() == null || section.getTime().getDayCode() == 0))
    755                                match++;
    756                            else if ("has-time".equals(attr) && section.getTime() != null
    757                                    && section.getTime().getDayCode() != 0)
    758                                match++;
    759                            total++;
    760                        }
    761                    }
    762                }
    763                if (total == 0)
    764                    return false;
    765                if (perc) {
    766                    double percentage = 100.0 * match / total;
    767                    return min <= percentage && percentage <= max;
    768                } else {
    769                    return min <= match && match <= max;
    770                }
    771            }
    772
    773            if ("overlap".equals(attr)) {
    774                int min = 0, max = Integer.MAX_VALUE;
    775                Credit prefix = Credit.eq;
    776                String number = term;
    777                if (number.startsWith("<=")) {
    778                    prefix = Credit.le;
    779                    number = number.substring(2);
    780                } else if (number.startsWith(">=")) {
    781                    prefix = Credit.ge;
    782                    number = number.substring(2);
    783                } else if (number.startsWith("<")) {
    784                    prefix = Credit.lt;
    785                    number = number.substring(1);
    786                } else if (number.startsWith(">")) {
    787                    prefix = Credit.gt;
    788                    number = number.substring(1);
    789                } else if (number.startsWith("=")) {
    790                    prefix = Credit.eq;
    791                    number = number.substring(1);
    792                }
    793                try {
    794                    int a = Integer.parseInt(number);
    795                    switch (prefix) {
    796                        case eq:
    797                            min = max = a;
    798                            break; // = a
    799                        case le:
    800                            max = a;
    801                            break; // <= a
    802                        case ge:
    803                            min = a;
    804                            break; // >= a
    805                        case lt:
    806                            max = a - 1;
    807                            break; // < a
    808                        case gt:
    809                            min = a + 1;
    810                            break; // > a
    811                    }
    812                } catch (NumberFormatException e) {
    813                }
    814                if (term.contains("..")) {
    815                    try {
    816                        String a = term.substring(0, term.indexOf(".."));
    817                        String b = term.substring(term.indexOf("..") + 2);
    818                        min = Integer.parseInt(a);
    819                        max = Integer.parseInt(b);
    820                    } catch (NumberFormatException e) {
    821                    }
    822                }
    823                int share = 0;
    824                for (Request r : student().getRequests()) {
    825                    if (r instanceof CourseRequest) {
    826                        CourseRequest cr = (CourseRequest) r;
    827                        Enrollment e = iAssignment.getValue(cr);
    828                        if (e == null)
    829                            continue;
    830                        for (Section section : e.getSections()) {
    831                            if (section.getTime() == null)
    832                                continue;
    833                            for (Request q : student().getRequests()) {
    834                                if (q.equals(request()))
    835                                    continue;
    836                                Enrollment otherEnrollment = iAssignment.getValue(q);
    837                                if (otherEnrollment != null && otherEnrollment.getCourse() != null) {
    838                                    for (Section otherSection : otherEnrollment.getSections()) {
    839                                        if (otherSection.getTime() != null
    840                                                && otherSection.getTime().hasIntersection(section.getTime())) {
    841                                            share += 5 * section.getTime().nrSharedHours(otherSection.getTime())
    842                                                    * section.getTime().nrSharedDays(otherSection.getTime());
    843                                        }
    844                                    }
    845                                }
    846                            }
    847                        }
    848                    }
    849                }
    850                return min <= share && share <= max;
    851            }
    852
    853            if ("prefer".equals(attr)) {
    854                if (cr() == null)
    855                    return false;
    856                if (eq("Any Preference", term))
    857                    return !cr().getSelectedChoices().isEmpty() || !cr().getRequiredChoices().isEmpty();
    858                if (eq("Met Preference", term) || eq("Unmet Preference", term)) {
    859                    if (enrollment() == null) {
    860                        if (eq("Unmet Preference", term))
    861                            return !cr().getSelectedChoices().isEmpty() || !cr().getRequiredChoices().isEmpty();
    862                        return false;
    863                    }
    864                    if (eq("Met Preference", term))
    865                        return enrollment().isSelected();
    866                    else
    867                        return !enrollment().isSelected();
    868                }
    869                return false;
    870            }
    871
    872            if ("require".equals(attr)) {
    873                if (cr() == null)
    874                    return false;
    875                if (eq("Any Requirement", term)) {
    876                    return !cr().getRequiredChoices().isEmpty();
    877                }
    878                if (eq("Met Requirement", term)) {
    879                    return enrollment() != null && enrollment().isRequired();
    880                }
    881                if (eq("Unmet Requirement", term)) {
    882                    return enrollment() != null && !enrollment().isRequired();
    883                }
    884                return false;
    885            }
    886
    887            if ("im".equals(attr)) {
    888                if (cr() == null)
    889                    return false;
    890                if (enrollment() == null) {
    891                    for (Course course : cr().getCourses()) {
    892                        for (Config config : course.getOffering().getConfigs()) {
    893                            if (term.equals(config.getInstructionalMethodReference()))
    894                                return true;
    895                        }
    896                        break;
    897                    }
    898                    return false;
    899                } else {
    900                    Config config = enrollment().getConfig();
    901                    if (config == null)
    902                        return false;
    903                    return term.equals(config.getInstructionalMethodReference());
    904                }
    905            }
    906
    907            if (enrollment() != null && enrollment().getCourse() != null) {
    908                for (Section section : enrollment().getSections()) {
    909                    if (attr == null || attr.equals("crn") || attr.equals("id") || attr.equals("externalId")
    910                            || attr.equals("exid") || attr.equals("name")) {
    911                        if (section.getName(enrollment().getCourse().getId()) != null && section
    912                                .getName(enrollment().getCourse().getId()).toLowerCase().startsWith(term.toLowerCase()))
    913                            return true;
    914                    }
    915                    if (attr == null || attr.equals("day")) {
    916                        if (section.getTime() == null && term.equalsIgnoreCase("none"))
    917                            return true;
    918                        if (section.getTime() != null) {
    919                            int day = parseDay(term);
    920                            if (day > 0 && (section.getTime().getDayCode() & day) == day)
    921                                return true;
    922                        }
    923                    }
    924                    if (attr == null || attr.equals("time")) {
    925                        if (section.getTime() == null && term.equalsIgnoreCase("none"))
    926                            return true;
    927                        if (section.getTime() != null) {
    928                            int start = parseStart(term);
    929                            if (start >= 0 && section.getTime().getStartSlot() == start)
    930                                return true;
    931                        }
    932                    }
    933                    if (attr != null && attr.equals("before")) {
    934                        if (section.getTime() != null) {
    935                            int end = parseStart(term);
    936                            if (end >= 0 && section.getTime().getStartSlot() + section.getTime().getLength()
    937                                    - section.getTime().getBreakTime() / 5 <= end)
    938                                return true;
    939                        }
    940                    }
    941                    if (attr != null && attr.equals("after")) {
    942                        if (section.getTime() != null) {
    943                            int start = parseStart(term);
    944                            if (start >= 0 && section.getTime().getStartSlot() >= start)
    945                                return true;
    946                        }
    947                    }
    948                    if (attr == null || attr.equals("room")) {
    949                        if ((section.getRooms() == null || section.getRooms().isEmpty())
    950                                && term.equalsIgnoreCase("none"))
    951                            return true;
    952                        if (section.getRooms() != null) {
    953                            for (RoomLocation r : section.getRooms()) {
    954                                if (has(r.getName(), term))
    955                                    return true;
    956                            }
    957                        }
    958                    }
    959                    if (attr == null || attr.equals("instr") || attr.equals("instructor")) {
    960                        if (attr != null && section.getInstructors().isEmpty() && term.equalsIgnoreCase("none"))
    961                            return true;
    962                        for (Instructor instuctor : section.getInstructors()) {
    963                            if (has(instuctor.getName(), term) || eq(instuctor.getExternalId(), term))
    964                                return true;
    965                            if (instuctor.getEmail() != null) {
    966                                String email = instuctor.getEmail();
    967                                if (email.indexOf('@') >= 0)
    968                                    email = email.substring(0, email.indexOf('@'));
    969                                if (eq(email, term))
    970                                    return true;
    971                            }
    972                        }
    973                    }
    974                }
    975            }
    976
    977            if (attr == null || "name".equals(attr) || "course".equals(attr)) {
    978                return course() != null && (course().getSubjectArea().equalsIgnoreCase(term) || course().getCourseNumber().equalsIgnoreCase(term) || (course().getSubjectArea() + " " + course().getCourseNumber()).equalsIgnoreCase(term));
    979            }
    980            if ("title".equals(attr)) {
    981                return course() != null && course().getTitle().toLowerCase().contains(term.toLowerCase());
    982            }
    983            if ("subject".equals(attr)) {
    984                return course() != null && course().getSubjectArea().equalsIgnoreCase(term);
    985            }
    986            if ("number".equals(attr)) {
    987                return course() != null && course().getCourseNumber().equalsIgnoreCase(term);
    988            }
    989
    990            return false;
    991        }
    992
    993        private boolean eq(String name, String term) {
    994            if (name == null)
    995                return false;
    996            return name.equalsIgnoreCase(term);
    997        }
    998
    999        private boolean has(String name, String term) {
    1000            if (name == null)
    1001                return false;
    1002            if (eq(name, term))
    1003                return true;
    1004            for (String t : name.split(" |,"))
    1005                if (t.equalsIgnoreCase(term))
    1006                    return true;
    1007            return false;
    1008        }
    1009
    1010        private boolean like(String name, String term) {
    1011            if (name == null)
    1012                return false;
    1013            if (term.indexOf('%') >= 0) {
    1014                return name.matches("(?i)" + term.replaceAll("%", ".*"));
    1015            } else {
    1016                return name.equalsIgnoreCase(term);
    1017            }
    1018        }
    1019
    1020        public static enum Credit {
    1021            eq, lt, gt, le, ge
    1022        }
    1023
    1024        public static String DAY_NAMES_CHARS[] = new String[] { "M", "T", "W", "R", "F", "S", "X" };
    1025
    1026        private int parseDay(String token) {
    1027            int days = 0;
    1028            boolean found = false;
    1029            do {
    1030                found = false;
    1031                for (int i = 0; i < Constants.DAY_NAMES_SHORT.length; i++) {
    1032                    if (token.toLowerCase().startsWith(Constants.DAY_NAMES_SHORT[i].toLowerCase())) {
    1033                        days |= Constants.DAY_CODES[i];
    1034                        token = token.substring(Constants.DAY_NAMES_SHORT[i].length());
    1035                        while (token.startsWith(" "))
    1036                            token = token.substring(1);
    1037                        found = true;
    1038                    }
    1039                }
    1040                for (int i = 0; i < DAY_NAMES_CHARS.length; i++) {
    1041                    if (token.toLowerCase().startsWith(DAY_NAMES_CHARS[i].toLowerCase())) {
    1042                        days |= Constants.DAY_CODES[i];
    1043                        token = token.substring(DAY_NAMES_CHARS[i].length());
    1044                        while (token.startsWith(" "))
    1045                            token = token.substring(1);
    1046                        found = true;
    1047                    }
    1048                }
    1049            } while (found);
    1050            return (token.isEmpty() ? days : 0);
    1051        }
    1052
    1053        private int parseStart(String token) {
    1054            int startHour = 0, startMin = 0;
    1055            String number = "";
    1056            while (!token.isEmpty() && token.charAt(0) >= '0' && token.charAt(0) <= '9') {
    1057                number += token.substring(0, 1);
    1058                token = token.substring(1);
    1059            }
    1060            if (number.isEmpty())
    1061                return -1;
    1062            if (number.length() > 2) {
    1063                startHour = Integer.parseInt(number) / 100;
    1064                startMin = Integer.parseInt(number) % 100;
    1065            } else {
    1066                startHour = Integer.parseInt(number);
    1067            }
    1068            while (token.startsWith(" "))
    1069                token = token.substring(1);
    1070            if (token.startsWith(":")) {
    1071                token = token.substring(1);
    1072                while (token.startsWith(" "))
    1073                    token = token.substring(1);
    1074                number = "";
    1075                while (!token.isEmpty() && token.charAt(0) >= '0' && token.charAt(0) <= '9') {
    1076                    number += token.substring(0, 1);
    1077                    token = token.substring(1);
    1078                }
    1079                if (number.isEmpty())
    1080                    return -1;
    1081                startMin = Integer.parseInt(number);
    1082            }
    1083            while (token.startsWith(" "))
    1084                token = token.substring(1);
    1085            boolean hasAmOrPm = false;
    1086            if (token.toLowerCase().startsWith("am")) {
    1087                token = token.substring(2);
    1088                hasAmOrPm = true;
    1089            }
    1090            if (token.toLowerCase().startsWith("a")) {
    1091                token = token.substring(1);
    1092                hasAmOrPm = true;
    1093            }
    1094            if (token.toLowerCase().startsWith("pm")) {
    1095                token = token.substring(2);
    1096                hasAmOrPm = true;
    1097                if (startHour < 12)
    1098                    startHour += 12;
    1099            }
    1100            if (token.toLowerCase().startsWith("p")) {
    1101                token = token.substring(1);
    1102                hasAmOrPm = true;
    1103                if (startHour < 12)
    1104                    startHour += 12;
    1105            }
    1106            if (startHour < 7 && !hasAmOrPm)
    1107                startHour += 12;
    1108            if (startMin % 5 != 0)
    1109                startMin = 5 * ((startMin + 2) / 5);
    1110            if (startHour == 7 && startMin == 0 && !hasAmOrPm)
    1111                startHour += 12;
    1112            return (60 * startHour + startMin) / 5;
    1113        }
    1114    }
    1115
    1116    public static class CourseMatcher implements Query.TermMatcher {
    1117        private Course iCourse;
    1118
    1119        public CourseMatcher(Course course) {
    1120                iCourse = course;
    1121        }
    1122
    1123        public Course course() { return iCourse; }
    1124
    1125        @Override
    1126        public boolean match(String attr, String term) {
    1127            if (attr == null || "name".equals(attr) || "course".equals(attr)) {
    1128                return course() != null && (course().getSubjectArea().equalsIgnoreCase(term) || course().getCourseNumber().equalsIgnoreCase(term) || (course().getSubjectArea() + " " + course().getCourseNumber()).equalsIgnoreCase(term));
    1129            }
    1130            if ("title".equals(attr)) {
    1131                return course() != null && course().getTitle().toLowerCase().contains(term.toLowerCase());
    1132            }
    1133            if ("subject".equals(attr)) {
    1134                return course() != null && course().getSubjectArea().equalsIgnoreCase(term);
    1135            }
    1136            if ("number".equals(attr)) {
    1137                return course() != null && course().getCourseNumber().equalsIgnoreCase(term);
    1138            }
    1139            return true;
    1140        }
    1141    }
    1142}