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