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