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("LC", term)) {
198                    return request().getRequestPriority() == RequestPriority.LC;
199                } else if (eq("Assigned LC", term)) {
200                    return request().getRequestPriority() == RequestPriority.LC && isAssigned();
201                } else if (eq("Not Assigned LC", term)) {
202                    return request().getRequestPriority() == RequestPriority.LC && !isAssigned();
203                } else if (eq("Important", term)) {
204                    return request().getRequestPriority() == RequestPriority.Important;
205                } else if (eq("Assigned Important", term)) {
206                    return request().getRequestPriority() == RequestPriority.Important && isAssigned();
207                } else if (eq("Not Assigned Important", term)) {
208                    return request().getRequestPriority() == RequestPriority.Important && !isAssigned();
209                } else if (eq("No-Subs", term) || eq("No-Substitutes", term)) {
210                    return cr() != null && cr().isWaitlist();
211                } else if (eq("Assigned No-Subs", term) || eq("Assigned  No-Substitutes", term)) {
212                    return cr() != null && cr().isWaitlist() && isAssigned();
213                } else if (eq("Not Assigned No-Subs", term) || eq("Not Assigned No-Substitutes", term)) {
214                    return cr() != null && cr().isWaitlist() && !isAssigned();
215                }
216            }
217
218            if ("assigned".equals(attr) || "scheduled".equals(attr)) {
219                if (eq("true", term) || eq("1", term))
220                    return isAssigned();
221                else
222                    return !isAssigned();
223            }
224
225            if ("waitlisted".equals(attr) || "waitlist".equals(attr)) {
226                if (eq("true", term) || eq("1", term))
227                    return !isAssigned() && cr() != null && cr().isWaitlist();
228                else
229                    return isAssigned() && cr() != null && cr().isWaitlist();
230            }
231
232            if ("no-substitutes".equals(attr) || "no-subs".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 ("reservation".equals(attr) || "reserved".equals(attr)) {
240                if (eq("true", term) || eq("1", term))
241                    return isAssigned() && enrollment().getReservation() != null;
242                else
243                    return isAssigned() && enrollment().getReservation() == null;
244            }
245
246            if ("mode".equals(attr)) {
247                if (eq("My Students", term)) {
248                    if (iUser == null)
249                        return false;
250                    for (Instructor a : student().getAdvisors())
251                        if (eq(a.getExternalId(), iUser))
252                            return true;
253                    return false;
254                }
255                return true;
256            }
257
258            if ("status".equals(attr)) {
259                if ("default".equalsIgnoreCase(term) || "Not Set".equalsIgnoreCase(term))
260                    return student().getStatus() == null;
261                return like(student().getStatus(), term);
262            }
263
264            if ("credit".equals(attr)) {
265                float min = 0, max = Float.MAX_VALUE;
266                Credit prefix = Credit.eq;
267                String number = term;
268                if (number.startsWith("<=")) {
269                    prefix = Credit.le;
270                    number = number.substring(2);
271                } else if (number.startsWith(">=")) {
272                    prefix = Credit.ge;
273                    number = number.substring(2);
274                } else if (number.startsWith("<")) {
275                    prefix = Credit.lt;
276                    number = number.substring(1);
277                } else if (number.startsWith(">")) {
278                    prefix = Credit.gt;
279                    number = number.substring(1);
280                } else if (number.startsWith("=")) {
281                    prefix = Credit.eq;
282                    number = number.substring(1);
283                }
284                String im = null;
285                try {
286                    float a = Float.parseFloat(number);
287                    switch (prefix) {
288                        case eq:
289                            min = max = a;
290                            break; // = a
291                        case le:
292                            max = a;
293                            break; // <= a
294                        case ge:
295                            min = a;
296                            break; // >= a
297                        case lt:
298                            max = a - 1;
299                            break; // < a
300                        case gt:
301                            min = a + 1;
302                            break; // > a
303                    }
304                } catch (NumberFormatException e) {
305                    Matcher m = Pattern.compile("([0-9]+\\.?[0-9]*)([^0-9\\.].*)").matcher(number);
306                    if (m.matches()) {
307                        float a = Float.parseFloat(m.group(1));
308                        im = m.group(2).trim();
309                        switch (prefix) {
310                            case eq:
311                                min = max = a;
312                                break; // = a
313                            case le:
314                                max = a;
315                                break; // <= a
316                            case ge:
317                                min = a;
318                                break; // >= a
319                            case lt:
320                                max = a - 1;
321                                break; // < a
322                            case gt:
323                                min = a + 1;
324                                break; // > a
325                        }
326                    }
327                }
328                if (term.contains("..")) {
329                    try {
330                        String a = term.substring(0, term.indexOf('.'));
331                        String b = term.substring(term.indexOf("..") + 2);
332                        min = Float.parseFloat(a);
333                        max = Float.parseFloat(b);
334                    } catch (NumberFormatException e) {
335                        Matcher m = Pattern.compile("([0-9]+\\.?[0-9]*)\\.\\.([0-9]+\\.?[0-9]*)([^0-9].*)")
336                                .matcher(term);
337                        if (m.matches()) {
338                            min = Float.parseFloat(m.group(1));
339                            max = Float.parseFloat(m.group(2));
340                            im = m.group(3).trim();
341                        }
342                    }
343                }
344                float credit = 0;
345                for (Request r : student().getRequests()) {
346                    if (r instanceof CourseRequest) {
347                        CourseRequest cr = (CourseRequest) r;
348                        Enrollment e = iAssignment.getValue(cr);
349                        if (e == null)
350                            continue;
351                        Config g = e.getConfig();
352                        if (g != null) {
353                            if ("!".equals(im) && g.getInstructionalMethodReference() != null)
354                                continue;
355                            if (im != null && !"!".equals(im)
356                                    && !im.equalsIgnoreCase(g.getInstructionalMethodReference()))
357                                continue;
358                            if (g.hasCreditValue())
359                                credit += g.getCreditValue();
360                            else if (e.getCourse().hasCreditValue())
361                                credit += e.getCourse().getCreditValue();
362                        }
363                    }
364                }
365                return min <= credit && credit <= max;
366            }
367
368            if ("rc".equals(attr) || "requested-credit".equals(attr)) {
369                int min = 0, max = Integer.MAX_VALUE;
370                Credit prefix = Credit.eq;
371                String number = term;
372                if (number.startsWith("<=")) {
373                    prefix = Credit.le;
374                    number = number.substring(2);
375                } else if (number.startsWith(">=")) {
376                    prefix = Credit.ge;
377                    number = number.substring(2);
378                } else if (number.startsWith("<")) {
379                    prefix = Credit.lt;
380                    number = number.substring(1);
381                } else if (number.startsWith(">")) {
382                    prefix = Credit.gt;
383                    number = number.substring(1);
384                } else if (number.startsWith("=")) {
385                    prefix = Credit.eq;
386                    number = number.substring(1);
387                }
388                try {
389                    int a = Integer.parseInt(number);
390                    switch (prefix) {
391                        case eq:
392                            min = max = a;
393                            break; // = a
394                        case le:
395                            max = a;
396                            break; // <= a
397                        case ge:
398                            min = a;
399                            break; // >= a
400                        case lt:
401                            max = a - 1;
402                            break; // < a
403                        case gt:
404                            min = a + 1;
405                            break; // > a
406                    }
407                } catch (NumberFormatException e) {
408                }
409                if (term.contains("..")) {
410                    try {
411                        String a = term.substring(0, term.indexOf('.'));
412                        String b = term.substring(term.indexOf("..") + 2);
413                        min = Integer.parseInt(a);
414                        max = Integer.parseInt(b);
415                    } catch (NumberFormatException e) {
416                    }
417                }
418                if (min == 0 && max == Integer.MAX_VALUE)
419                    return true;
420                float studentMinTot = 0f, studentMaxTot = 0f;
421                int nrCoursesTot = 0;
422                List<Float> minsTot = new ArrayList<Float>();
423                List<Float> maxsTot = new ArrayList<Float>();
424                for (Request r : student().getRequests()) {
425                    if (r instanceof CourseRequest) {
426                        CourseRequest cr = (CourseRequest) r;
427                        Float minTot = null, maxTot = null;
428                        for (Course c : cr.getCourses()) {
429                            if (c.hasCreditValue()) {
430                                if (minTot == null || minTot > c.getCreditValue())
431                                    minTot = c.getCreditValue();
432                                if (maxTot == null || maxTot < c.getCreditValue())
433                                    maxTot = c.getCreditValue();
434                            }
435                        }
436                        if (cr.isWaitlist()) {
437                            if (minTot != null) {
438                                studentMinTot += minTot;
439                                studentMaxTot += maxTot;
440                            }
441                        } else {
442                            if (minTot != null) {
443                                minsTot.add(minTot);
444                                maxsTot.add(maxTot);
445                                if (!r.isAlternative())
446                                    nrCoursesTot++;
447                            }
448                        }
449                    }
450                }
451                Collections.sort(minsTot);
452                Collections.sort(maxsTot);
453                for (int i = 0; i < nrCoursesTot; i++) {
454                    studentMinTot += minsTot.get(i);
455                    studentMaxTot += maxsTot.get(maxsTot.size() - i - 1);
456                }
457                return min <= studentMaxTot && studentMinTot <= max;
458            }
459
460            if ("fc".equals(attr) || "first-choice-credit".equals(attr)) {
461                int min = 0, max = Integer.MAX_VALUE;
462                Credit prefix = Credit.eq;
463                String number = term;
464                if (number.startsWith("<=")) {
465                    prefix = Credit.le;
466                    number = number.substring(2);
467                } else if (number.startsWith(">=")) {
468                    prefix = Credit.ge;
469                    number = number.substring(2);
470                } else if (number.startsWith("<")) {
471                    prefix = Credit.lt;
472                    number = number.substring(1);
473                } else if (number.startsWith(">")) {
474                    prefix = Credit.gt;
475                    number = number.substring(1);
476                } else if (number.startsWith("=")) {
477                    prefix = Credit.eq;
478                    number = number.substring(1);
479                }
480                try {
481                    int a = Integer.parseInt(number);
482                    switch (prefix) {
483                        case eq:
484                            min = max = a;
485                            break; // = a
486                        case le:
487                            max = a;
488                            break; // <= a
489                        case ge:
490                            min = a;
491                            break; // >= a
492                        case lt:
493                            max = a - 1;
494                            break; // < a
495                        case gt:
496                            min = a + 1;
497                            break; // > a
498                    }
499                } catch (NumberFormatException e) {
500                }
501                if (term.contains("..")) {
502                    try {
503                        String a = term.substring(0, term.indexOf('.'));
504                        String b = term.substring(term.indexOf("..") + 2);
505                        min = Integer.parseInt(a);
506                        max = Integer.parseInt(b);
507                    } catch (NumberFormatException e) {
508                    }
509                }
510                if (min == 0 && max == Integer.MAX_VALUE)
511                    return true;
512                float credit = 0f;
513                for (Request r : student().getRequests()) {
514                    if (r instanceof CourseRequest) {
515                        CourseRequest cr = (CourseRequest) r;
516                        for (Course c : cr.getCourses()) {
517                            if (c != null && c.hasCreditValue()) {
518                                credit += c.getCreditValue();
519                                break;
520                            }
521                        }
522                    }
523                }
524                return min <= credit && credit <= max;
525            }
526
527            if ("rp".equals(attr)) {
528                if ("subst".equalsIgnoreCase(term))
529                    return request().isAlternative();
530                int min = 0, max = Integer.MAX_VALUE;
531                Credit prefix = Credit.eq;
532                String number = term;
533                if (number.startsWith("<=")) {
534                    prefix = Credit.le;
535                    number = number.substring(2);
536                } else if (number.startsWith(">=")) {
537                    prefix = Credit.ge;
538                    number = number.substring(2);
539                } else if (number.startsWith("<")) {
540                    prefix = Credit.lt;
541                    number = number.substring(1);
542                } else if (number.startsWith(">")) {
543                    prefix = Credit.gt;
544                    number = number.substring(1);
545                } else if (number.startsWith("=")) {
546                    prefix = Credit.eq;
547                    number = number.substring(1);
548                }
549                try {
550                    int a = Integer.parseInt(number);
551                    switch (prefix) {
552                        case eq:
553                            min = max = a;
554                            break; // = a
555                        case le:
556                            max = a;
557                            break; // <= a
558                        case ge:
559                            min = a;
560                            break; // >= a
561                        case lt:
562                            max = a - 1;
563                            break; // < a
564                        case gt:
565                            min = a + 1;
566                            break; // > a
567                    }
568                } catch (NumberFormatException e) {
569                }
570                if (term.contains("..")) {
571                    try {
572                        String a = term.substring(0, term.indexOf('.'));
573                        String b = term.substring(term.indexOf("..") + 2);
574                        min = Integer.parseInt(a);
575                        max = Integer.parseInt(b);
576                    } catch (NumberFormatException e) {
577                    }
578                }
579                if (min == 0 && max == Integer.MAX_VALUE)
580                    return true;
581                return !request().isAlternative() && min <= request().getPriority() + 1
582                        && request().getPriority() + 1 <= max;
583            }
584
585            if ("choice".equals(attr) || "ch".equals(attr)) {
586                if (cr() == null)
587                    return false;
588                int min = 0, max = Integer.MAX_VALUE;
589                Credit prefix = Credit.eq;
590                String number = term;
591                if (number.startsWith("<=")) {
592                    prefix = Credit.le;
593                    number = number.substring(2);
594                } else if (number.startsWith(">=")) {
595                    prefix = Credit.ge;
596                    number = number.substring(2);
597                } else if (number.startsWith("<")) {
598                    prefix = Credit.lt;
599                    number = number.substring(1);
600                } else if (number.startsWith(">")) {
601                    prefix = Credit.gt;
602                    number = number.substring(1);
603                } else if (number.startsWith("=")) {
604                    prefix = Credit.eq;
605                    number = number.substring(1);
606                }
607                try {
608                    int a = Integer.parseInt(number);
609                    switch (prefix) {
610                        case eq:
611                            min = max = a;
612                            break; // = a
613                        case le:
614                            max = a;
615                            break; // <= a
616                        case ge:
617                            min = a;
618                            break; // >= a
619                        case lt:
620                            max = a - 1;
621                            break; // < a
622                        case gt:
623                            min = a + 1;
624                            break; // > a
625                    }
626                } catch (NumberFormatException e) {
627                }
628                if (term.contains("..")) {
629                    try {
630                        String a = term.substring(0, term.indexOf('.'));
631                        String b = term.substring(term.indexOf("..") + 2);
632                        min = Integer.parseInt(a);
633                        max = Integer.parseInt(b);
634                    } catch (NumberFormatException e) {
635                    }
636                }
637                if (min == 0 && max == Integer.MAX_VALUE)
638                    return true;
639                if (enrollment() != null) {
640                    int choice = 1;
641                    for (Course course : cr().getCourses()) {
642                        if (course.equals(enrollment().getCourse())) {
643                            return min <= choice && choice <= max;
644                        }
645                        choice++;
646                    }
647                    return false;
648                } else if (!request().isAlternative()) {
649                    int choice = cr().getCourses().size();
650                    return min <= choice && choice <= max;
651                } else {
652                    return false;
653                }
654            }
655
656            if ("btb".equals(attr)) {
657                if ("prefer".equalsIgnoreCase(term) || "preferred".equalsIgnoreCase(term))
658                    return student().getBackToBackPreference() == BackToBackPreference.BTB_PREFERRED;
659                else if ("disc".equalsIgnoreCase(term) || "discouraged".equalsIgnoreCase(term))
660                    return student().getBackToBackPreference() == BackToBackPreference.BTB_DISCOURAGED;
661                else
662                    return student().getBackToBackPreference() == BackToBackPreference.NO_PREFERENCE;
663            }
664
665            if ("online".equals(attr)) {
666                if ("prefer".equalsIgnoreCase(term) || "preferred".equalsIgnoreCase(term))
667                    return student().getModalityPreference() == ModalityPreference.ONLINE_PREFERRED;
668                else if ("require".equalsIgnoreCase(term) || "required".equalsIgnoreCase(term))
669                    return student().getModalityPreference() == ModalityPreference.ONLINE_REQUIRED;
670                else if ("disc".equalsIgnoreCase(term) || "discouraged".equalsIgnoreCase(term))
671                    return student().getModalityPreference() == ModalityPreference.ONILNE_DISCOURAGED;
672                else if ("no".equalsIgnoreCase(term) || "no-preference".equalsIgnoreCase(term))
673                    return student().getModalityPreference() == ModalityPreference.NO_PREFERENCE;
674            }
675
676            if ("online".equals(attr) || "face-to-face".equals(attr) || "f2f".equals(attr) || "no-time".equals(attr)
677                    || "has-time".equals(attr)) {
678                int min = 0, max = Integer.MAX_VALUE;
679                Credit prefix = Credit.eq;
680                String number = term;
681                if (number.startsWith("<=")) {
682                    prefix = Credit.le;
683                    number = number.substring(2);
684                } else if (number.startsWith(">=")) {
685                    prefix = Credit.ge;
686                    number = number.substring(2);
687                } else if (number.startsWith("<")) {
688                    prefix = Credit.lt;
689                    number = number.substring(1);
690                } else if (number.startsWith(">")) {
691                    prefix = Credit.gt;
692                    number = number.substring(1);
693                } else if (number.startsWith("=")) {
694                    prefix = Credit.eq;
695                    number = number.substring(1);
696                }
697                boolean perc = false;
698                if (number.endsWith("%")) {
699                    perc = true;
700                    number = number.substring(0, number.length() - 1).trim();
701                }
702                try {
703                    int a = Integer.parseInt(number);
704                    switch (prefix) {
705                        case eq:
706                            min = max = a;
707                            break; // = a
708                        case le:
709                            max = a;
710                            break; // <= a
711                        case ge:
712                            min = a;
713                            break; // >= a
714                        case lt:
715                            max = a - 1;
716                            break; // < a
717                        case gt:
718                            min = a + 1;
719                            break; // > a
720                    }
721                } catch (NumberFormatException e) {
722                }
723                if (term.contains("..")) {
724                    try {
725                        String a = term.substring(0, term.indexOf('.'));
726                        String b = term.substring(term.indexOf("..") + 2);
727                        min = Integer.parseInt(a);
728                        max = Integer.parseInt(b);
729                    } catch (NumberFormatException e) {
730                    }
731                }
732                if (min == 0 && max == Integer.MAX_VALUE)
733                    return true;
734                int match = 0, total = 0;
735                for (Request r : student().getRequests()) {
736                    if (r instanceof CourseRequest) {
737                        CourseRequest cr = (CourseRequest) r;
738                        Enrollment e = iAssignment.getValue(cr);
739                        if (e == null)
740                            continue;
741                        for (Section section : enrollment().getSections()) {
742                            if ("online".equals(attr) && section.isOnline())
743                                match++;
744                            else if (("face-to-face".equals(attr) || "f2f".equals(attr)) && !section.isOnline())
745                                match++;
746                            else if ("no-time".equals(attr)
747                                    && (section.getTime() == null || section.getTime().getDayCode() == 0))
748                                match++;
749                            else if ("has-time".equals(attr) && section.getTime() != null
750                                    && section.getTime().getDayCode() != 0)
751                                match++;
752                            total++;
753                        }
754                    }
755                }
756                if (total == 0)
757                    return false;
758                if (perc) {
759                    double percentage = 100.0 * match / total;
760                    return min <= percentage && percentage <= max;
761                } else {
762                    return min <= match && match <= max;
763                }
764            }
765
766            if ("overlap".equals(attr)) {
767                int min = 0, max = Integer.MAX_VALUE;
768                Credit prefix = Credit.eq;
769                String number = term;
770                if (number.startsWith("<=")) {
771                    prefix = Credit.le;
772                    number = number.substring(2);
773                } else if (number.startsWith(">=")) {
774                    prefix = Credit.ge;
775                    number = number.substring(2);
776                } else if (number.startsWith("<")) {
777                    prefix = Credit.lt;
778                    number = number.substring(1);
779                } else if (number.startsWith(">")) {
780                    prefix = Credit.gt;
781                    number = number.substring(1);
782                } else if (number.startsWith("=")) {
783                    prefix = Credit.eq;
784                    number = number.substring(1);
785                }
786                try {
787                    int a = Integer.parseInt(number);
788                    switch (prefix) {
789                        case eq:
790                            min = max = a;
791                            break; // = a
792                        case le:
793                            max = a;
794                            break; // <= a
795                        case ge:
796                            min = a;
797                            break; // >= a
798                        case lt:
799                            max = a - 1;
800                            break; // < a
801                        case gt:
802                            min = a + 1;
803                            break; // > a
804                    }
805                } catch (NumberFormatException e) {
806                }
807                if (term.contains("..")) {
808                    try {
809                        String a = term.substring(0, term.indexOf('.'));
810                        String b = term.substring(term.indexOf("..") + 2);
811                        min = Integer.parseInt(a);
812                        max = Integer.parseInt(b);
813                    } catch (NumberFormatException e) {
814                    }
815                }
816                int share = 0;
817                for (Request r : student().getRequests()) {
818                    if (r instanceof CourseRequest) {
819                        CourseRequest cr = (CourseRequest) r;
820                        Enrollment e = iAssignment.getValue(cr);
821                        if (e == null)
822                            continue;
823                        for (Section section : e.getSections()) {
824                            if (section.getTime() == null)
825                                continue;
826                            for (Request q : student().getRequests()) {
827                                if (q.equals(request()))
828                                    continue;
829                                Enrollment otherEnrollment = iAssignment.getValue(q);
830                                if (otherEnrollment != null && otherEnrollment.getCourse() != null) {
831                                    for (Section otherSection : otherEnrollment.getSections()) {
832                                        if (otherSection.getTime() != null
833                                                && otherSection.getTime().hasIntersection(section.getTime())) {
834                                            share += 5 * section.getTime().nrSharedHours(otherSection.getTime())
835                                                    * section.getTime().nrSharedDays(otherSection.getTime());
836                                        }
837                                    }
838                                }
839                            }
840                        }
841                    }
842                }
843                return min <= share && share <= max;
844            }
845
846            if ("prefer".equals(attr)) {
847                if (cr() == null)
848                    return false;
849                if (eq("Any Preference", term))
850                    return !cr().getSelectedChoices().isEmpty() || !cr().getRequiredChoices().isEmpty();
851                if (eq("Met Preference", term) || eq("Unmet Preference", term)) {
852                    if (enrollment() == null) {
853                        if (eq("Unmet Preference", term))
854                            return !cr().getSelectedChoices().isEmpty() || !cr().getRequiredChoices().isEmpty();
855                        return false;
856                    }
857                    if (eq("Met Preference", term))
858                        return enrollment().isSelected();
859                    else
860                        return !enrollment().isSelected();
861                }
862                return false;
863            }
864
865            if ("require".equals(attr)) {
866                if (cr() == null)
867                    return false;
868                if (eq("Any Requirement", term)) {
869                    return !cr().getRequiredChoices().isEmpty();
870                }
871                if (eq("Met Requirement", term)) {
872                    return enrollment() != null && enrollment().isRequired();
873                }
874                if (eq("Unmet Requirement", term)) {
875                    return enrollment() != null && !enrollment().isRequired();
876                }
877                return false;
878            }
879
880            if ("im".equals(attr)) {
881                if (cr() == null)
882                    return false;
883                if (enrollment() == null) {
884                    for (Course course : cr().getCourses()) {
885                        for (Config config : course.getOffering().getConfigs()) {
886                            if (term.equals(config.getInstructionalMethodReference()))
887                                return true;
888                        }
889                        break;
890                    }
891                    return false;
892                } else {
893                    Config config = enrollment().getConfig();
894                    if (config == null)
895                        return false;
896                    return term.equals(config.getInstructionalMethodReference());
897                }
898            }
899
900            if (enrollment() != null && enrollment().getCourse() != null) {
901                for (Section section : enrollment().getSections()) {
902                    if (attr == null || attr.equals("crn") || attr.equals("id") || attr.equals("externalId")
903                            || attr.equals("exid") || attr.equals("name")) {
904                        if (section.getName(enrollment().getCourse().getId()) != null && section
905                                .getName(enrollment().getCourse().getId()).toLowerCase().startsWith(term.toLowerCase()))
906                            return true;
907                    }
908                    if (attr == null || attr.equals("day")) {
909                        if (section.getTime() == null && term.equalsIgnoreCase("none"))
910                            return true;
911                        if (section.getTime() != null) {
912                            int day = parseDay(term);
913                            if (day > 0 && (section.getTime().getDayCode() & day) == day)
914                                return true;
915                        }
916                    }
917                    if (attr == null || attr.equals("time")) {
918                        if (section.getTime() == null && term.equalsIgnoreCase("none"))
919                            return true;
920                        if (section.getTime() != null) {
921                            int start = parseStart(term);
922                            if (start >= 0 && section.getTime().getStartSlot() == start)
923                                return true;
924                        }
925                    }
926                    if (attr != null && attr.equals("before")) {
927                        if (section.getTime() != null) {
928                            int end = parseStart(term);
929                            if (end >= 0 && section.getTime().getStartSlot() + section.getTime().getLength()
930                                    - section.getTime().getBreakTime() / 5 <= end)
931                                return true;
932                        }
933                    }
934                    if (attr != null && attr.equals("after")) {
935                        if (section.getTime() != null) {
936                            int start = parseStart(term);
937                            if (start >= 0 && section.getTime().getStartSlot() >= start)
938                                return true;
939                        }
940                    }
941                    if (attr == null || attr.equals("room")) {
942                        if ((section.getRooms() == null || section.getRooms().isEmpty())
943                                && term.equalsIgnoreCase("none"))
944                            return true;
945                        if (section.getRooms() != null) {
946                            for (RoomLocation r : section.getRooms()) {
947                                if (has(r.getName(), term))
948                                    return true;
949                            }
950                        }
951                    }
952                    if (attr == null || attr.equals("instr") || attr.equals("instructor")) {
953                        if (attr != null && section.getInstructors().isEmpty() && term.equalsIgnoreCase("none"))
954                            return true;
955                        for (Instructor instuctor : section.getInstructors()) {
956                            if (has(instuctor.getName(), term) || eq(instuctor.getExternalId(), term))
957                                return true;
958                            if (instuctor.getEmail() != null) {
959                                String email = instuctor.getEmail();
960                                if (email.indexOf('@') >= 0)
961                                    email = email.substring(0, email.indexOf('@'));
962                                if (eq(email, term))
963                                    return true;
964                            }
965                        }
966                    }
967                }
968            }
969            
970            if (attr == null || "name".equals(attr) || "course".equals(attr)) {
971                return course() != null && (course().getSubjectArea().equalsIgnoreCase(term) || course().getCourseNumber().equalsIgnoreCase(term) || (course().getSubjectArea() + " " + course().getCourseNumber()).equalsIgnoreCase(term));
972            }
973            if ("title".equals(attr)) {
974                return course() != null && course().getTitle().toLowerCase().contains(term.toLowerCase());
975            }
976            if ("subject".equals(attr)) {
977                return course() != null && course().getSubjectArea().equalsIgnoreCase(term);
978            }
979            if ("number".equals(attr)) {
980                return course() != null && course().getCourseNumber().equalsIgnoreCase(term);
981            }
982
983            return false;
984        }
985
986        private boolean eq(String name, String term) {
987            if (name == null)
988                return false;
989            return name.equalsIgnoreCase(term);
990        }
991
992        private boolean has(String name, String term) {
993            if (name == null)
994                return false;
995            if (eq(name, term))
996                return true;
997            for (String t : name.split(" |,"))
998                if (t.equalsIgnoreCase(term))
999                    return true;
1000            return false;
1001        }
1002
1003        private boolean like(String name, String term) {
1004            if (name == null)
1005                return false;
1006            if (term.indexOf('%') >= 0) {
1007                return name.matches("(?i)" + term.replaceAll("%", ".*"));
1008            } else {
1009                return name.equalsIgnoreCase(term);
1010            }
1011        }
1012
1013        public static enum Credit {
1014            eq, lt, gt, le, ge
1015        }
1016
1017        public static String DAY_NAMES_CHARS[] = new String[] { "M", "T", "W", "R", "F", "S", "X" };
1018
1019        private int parseDay(String token) {
1020            int days = 0;
1021            boolean found = false;
1022            do {
1023                found = false;
1024                for (int i = 0; i < Constants.DAY_NAMES_SHORT.length; i++) {
1025                    if (token.toLowerCase().startsWith(Constants.DAY_NAMES_SHORT[i].toLowerCase())) {
1026                        days |= Constants.DAY_CODES[i];
1027                        token = token.substring(Constants.DAY_NAMES_SHORT[i].length());
1028                        while (token.startsWith(" "))
1029                            token = token.substring(1);
1030                        found = true;
1031                    }
1032                }
1033                for (int i = 0; i < DAY_NAMES_CHARS.length; i++) {
1034                    if (token.toLowerCase().startsWith(DAY_NAMES_CHARS[i].toLowerCase())) {
1035                        days |= Constants.DAY_CODES[i];
1036                        token = token.substring(DAY_NAMES_CHARS[i].length());
1037                        while (token.startsWith(" "))
1038                            token = token.substring(1);
1039                        found = true;
1040                    }
1041                }
1042            } while (found);
1043            return (token.isEmpty() ? days : 0);
1044        }
1045
1046        private int parseStart(String token) {
1047            int startHour = 0, startMin = 0;
1048            String number = "";
1049            while (!token.isEmpty() && token.charAt(0) >= '0' && token.charAt(0) <= '9') {
1050                number += token.substring(0, 1);
1051                token = token.substring(1);
1052            }
1053            if (number.isEmpty())
1054                return -1;
1055            if (number.length() > 2) {
1056                startHour = Integer.parseInt(number) / 100;
1057                startMin = Integer.parseInt(number) % 100;
1058            } else {
1059                startHour = Integer.parseInt(number);
1060            }
1061            while (token.startsWith(" "))
1062                token = token.substring(1);
1063            if (token.startsWith(":")) {
1064                token = token.substring(1);
1065                while (token.startsWith(" "))
1066                    token = token.substring(1);
1067                number = "";
1068                while (!token.isEmpty() && token.charAt(0) >= '0' && token.charAt(0) <= '9') {
1069                    number += token.substring(0, 1);
1070                    token = token.substring(1);
1071                }
1072                if (number.isEmpty())
1073                    return -1;
1074                startMin = Integer.parseInt(number);
1075            }
1076            while (token.startsWith(" "))
1077                token = token.substring(1);
1078            boolean hasAmOrPm = false;
1079            if (token.toLowerCase().startsWith("am")) {
1080                token = token.substring(2);
1081                hasAmOrPm = true;
1082            }
1083            if (token.toLowerCase().startsWith("a")) {
1084                token = token.substring(1);
1085                hasAmOrPm = true;
1086            }
1087            if (token.toLowerCase().startsWith("pm")) {
1088                token = token.substring(2);
1089                hasAmOrPm = true;
1090                if (startHour < 12)
1091                    startHour += 12;
1092            }
1093            if (token.toLowerCase().startsWith("p")) {
1094                token = token.substring(1);
1095                hasAmOrPm = true;
1096                if (startHour < 12)
1097                    startHour += 12;
1098            }
1099            if (startHour < 7 && !hasAmOrPm)
1100                startHour += 12;
1101            if (startMin % 5 != 0)
1102                startMin = 5 * ((startMin + 2) / 5);
1103            if (startHour == 7 && startMin == 0 && !hasAmOrPm)
1104                startHour += 12;
1105            return (60 * startHour + startMin) / 5;
1106        }
1107    }
1108    
1109    public static class CourseMatcher implements Query.TermMatcher {
1110        private Course iCourse;
1111        
1112        public CourseMatcher(Course course) {
1113                iCourse = course;
1114        }
1115
1116        public Course course() { return iCourse; }
1117        
1118        @Override
1119        public boolean match(String attr, String term) {
1120            if (attr == null || "name".equals(attr) || "course".equals(attr)) {
1121                return course() != null && (course().getSubjectArea().equalsIgnoreCase(term) || course().getCourseNumber().equalsIgnoreCase(term) || (course().getSubjectArea() + " " + course().getCourseNumber()).equalsIgnoreCase(term));
1122            }
1123            if ("title".equals(attr)) {
1124                return course() != null && course().getTitle().toLowerCase().contains(term.toLowerCase());
1125            }
1126            if ("subject".equals(attr)) {
1127                return course() != null && course().getSubjectArea().equalsIgnoreCase(term);
1128            }
1129            if ("number".equals(attr)) {
1130                return course() != null && course().getCourseNumber().equalsIgnoreCase(term);
1131            }
1132            return true;
1133        }
1134    }
1135}