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