001package org.cpsolver.studentsct.model; 002 003import java.text.DecimalFormat; 004import java.util.HashSet; 005import java.util.Iterator; 006import java.util.Set; 007 008import org.cpsolver.ifs.assignment.Assignment; 009import org.cpsolver.ifs.model.Value; 010import org.cpsolver.ifs.util.ToolBox; 011import org.cpsolver.studentsct.StudentSectioningModel; 012import org.cpsolver.studentsct.extension.DistanceConflict; 013import org.cpsolver.studentsct.extension.StudentQuality; 014import org.cpsolver.studentsct.extension.TimeOverlapsCounter; 015import org.cpsolver.studentsct.reservation.Reservation; 016 017 018/** 019 * Representation of an enrollment of a student into a course. A student needs 020 * to be enrolled in a section of each subpart of a selected configuration. When 021 * parent-child relation is defined among sections, if a student is enrolled in 022 * a section that has a parent section defined, he/she has be enrolled in the 023 * parent section as well. Also, the selected sections cannot overlap in time. <br> 024 * <br> 025 * 026 * @author Tomáš Müller 027 * @version StudentSct 1.3 (Student Sectioning)<br> 028 * Copyright (C) 2007 - 2014 Tomáš Müller<br> 029 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br> 030 * <a href="http://muller.unitime.org">http://muller.unitime.org</a><br> 031 * <br> 032 * This library is free software; you can redistribute it and/or modify 033 * it under the terms of the GNU Lesser General Public License as 034 * published by the Free Software Foundation; either version 3 of the 035 * License, or (at your option) any later version. <br> 036 * <br> 037 * This library is distributed in the hope that it will be useful, but 038 * WITHOUT ANY WARRANTY; without even the implied warranty of 039 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 040 * Lesser General Public License for more details. <br> 041 * <br> 042 * You should have received a copy of the GNU Lesser General Public 043 * License along with this library; if not see 044 * <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>. 045 */ 046 047public class Enrollment extends Value<Request, Enrollment> { 048 private static DecimalFormat sDF = new DecimalFormat("0.000"); 049 private Request iRequest = null; 050 private Config iConfig = null; 051 private Course iCourse = null; 052 private Set<? extends SctAssignment> iAssignments = null; 053 private Double iCachedPenalty = null; 054 private int iPriority = 0; 055 private boolean iNoReservationPenalty = false; 056 private Reservation iReservation = null; 057 private Long iTimeStamp = null; 058 private String iApproval = null; 059 060 /** 061 * Constructor 062 * 063 * @param request 064 * course / free time request 065 * @param priority 066 * zero for the course, one for the first alternative, two for the second alternative 067 * @param noReservationPenalty 068 * when true +1 is added to priority (prefer enrollments with reservations) 069 * @param course 070 * selected course 071 * @param config 072 * selected configuration 073 * @param assignments 074 * valid list of sections 075 * @param reservation used reservation 076 */ 077 public Enrollment(Request request, int priority, boolean noReservationPenalty, Course course, Config config, Set<? extends SctAssignment> assignments, Reservation reservation) { 078 super(request); 079 iRequest = request; 080 iConfig = config; 081 iAssignments = assignments; 082 iPriority = priority; 083 iCourse = course; 084 iNoReservationPenalty = noReservationPenalty; 085 if (iConfig != null && iCourse == null) 086 for (Course c: ((CourseRequest)iRequest).getCourses()) { 087 if (c.getOffering().getConfigs().contains(iConfig)) { 088 iCourse = c; 089 break; 090 } 091 } 092 iReservation = reservation; 093 } 094 095 /** 096 * Constructor 097 * 098 * @param request 099 * course / free time request 100 * @param priority 101 * zero for the course, one for the first alternative, two for the second alternative 102 * @param course 103 * selected course 104 * @param config 105 * selected configuration 106 * @param assignments 107 * valid list of sections 108 * @param reservation used reservation 109 */ 110 public Enrollment(Request request, int priority, Course course, Config config, Set<? extends SctAssignment> assignments, Reservation reservation) { 111 this(request, priority, false, course, config, assignments, reservation); 112 } 113 114 /** 115 * Constructor 116 * 117 * @param request 118 * course / free time request 119 * @param priority 120 * zero for the course, one for the first alternative, two for the second alternative 121 * @param config 122 * selected configuration 123 * @param assignments 124 * valid list of sections 125 * @param assignment current assignment (to guess the reservation) 126 */ 127 public Enrollment(Request request, int priority, Config config, Set<? extends SctAssignment> assignments, Assignment<Request, Enrollment> assignment) { 128 this(request, priority, null, config, assignments, null); 129 if (assignments != null && assignment != null) 130 guessReservation(assignment, true); 131 } 132 133 /** 134 * Guess the reservation based on the enrollment 135 * @param assignment current assignment 136 * @param onlyAvailable use only reservation that have some space left in them 137 */ 138 public void guessReservation(Assignment<Request, Enrollment> assignment, boolean onlyAvailable) { 139 if (iCourse != null) { 140 Reservation best = null; 141 for (Reservation reservation: ((CourseRequest)iRequest).getReservations(iCourse)) { 142 if (reservation.isIncluded(this)) { 143 if (onlyAvailable && reservation.getContext(assignment).getReservedAvailableSpace(assignment, iConfig, iRequest) < iRequest.getWeight() && !reservation.canBatchAssignOverLimit()) 144 continue; 145 if (best == null || best.getPriority() > reservation.getPriority()) { 146 best = reservation; 147 } else if (best.getPriority() == reservation.getPriority() && 148 best.getContext(assignment).getReservedAvailableSpace(assignment, iConfig, iRequest) < reservation.getContext(assignment).getReservedAvailableSpace(assignment, iConfig, iRequest)) { 149 best = reservation; 150 } 151 } 152 } 153 iReservation = best; 154 } 155 } 156 157 /** Student 158 * @return student 159 **/ 160 public Student getStudent() { 161 return iRequest.getStudent(); 162 } 163 164 /** Request 165 * @return request 166 **/ 167 public Request getRequest() { 168 return iRequest; 169 } 170 171 /** True if the request is course request 172 * @return true if the request if course request 173 **/ 174 public boolean isCourseRequest() { 175 return iConfig != null; 176 } 177 178 /** Offering of the course request 179 * @return offering of the course request 180 **/ 181 public Offering getOffering() { 182 return (iConfig == null ? null : iConfig.getOffering()); 183 } 184 185 /** Config of the course request 186 * @return config of the course request 187 **/ 188 public Config getConfig() { 189 return iConfig; 190 } 191 192 /** Course of the course request 193 * @return course of the course request 194 **/ 195 public Course getCourse() { 196 return iCourse; 197 } 198 199 /** List of assignments (selected sections) 200 * @return assignments (selected sections) 201 **/ 202 @SuppressWarnings("unchecked") 203 public Set<SctAssignment> getAssignments() { 204 return (Set<SctAssignment>) iAssignments; 205 } 206 207 /** List of sections (only for course request) 208 * @return selected sections 209 **/ 210 @SuppressWarnings("unchecked") 211 public Set<Section> getSections() { 212 if (isCourseRequest()) 213 return (Set<Section>) iAssignments; 214 return new HashSet<Section>(); 215 } 216 217 /** True when this enrollment is overlapping with the given enrollment 218 * @param enrl other enrollment 219 * @return true if there is an overlap 220 **/ 221 public boolean isOverlapping(Enrollment enrl) { 222 if (enrl == null || isAllowOverlap() || enrl.isAllowOverlap()) 223 return false; 224 for (SctAssignment a : getAssignments()) { 225 if (a.isOverlapping(enrl.getAssignments())) 226 return true; 227 } 228 return false; 229 } 230 231 /** Percent of sections that are wait-listed 232 * @return percent of sections that are wait-listed 233 **/ 234 public double percentWaitlisted() { 235 if (!isCourseRequest()) 236 return 0.0; 237 CourseRequest courseRequest = (CourseRequest) getRequest(); 238 int nrWaitlisted = 0; 239 for (Section section : getSections()) { 240 if (courseRequest.isWaitlisted(section)) 241 nrWaitlisted++; 242 } 243 return ((double) nrWaitlisted) / getAssignments().size(); 244 } 245 246 /** Percent of sections that are selected 247 * @return percent of sections that are selected 248 **/ 249 public double percentSelected() { 250 if (!isCourseRequest()) 251 return 0.0; 252 CourseRequest courseRequest = (CourseRequest) getRequest(); 253 int nrSelected = 0; 254 for (Section section : getSections()) { 255 if (courseRequest.isSelected(section)) 256 nrSelected++; 257 } 258 return ((double) nrSelected) / getAssignments().size(); 259 } 260 261 /** Percent of sections that are selected 262 * @return percent of sections that are selected 263 **/ 264 public double percentSelectedSameSection() { 265 if (!isCourseRequest() || getStudent().isDummy()) return (getRequest().hasSelection() ? 1.0 : 0.0); 266 CourseRequest courseRequest = (CourseRequest) getRequest(); 267 int nrSelected = 0; 268 Set<Long> nrMatching = new HashSet<Long>(); 269 sections: for (Section section : getSections()) { 270 for (Choice choice: courseRequest.getSelectedChoices()) { 271 if (choice.getSubpartId() != null) nrMatching.add(choice.getSubpartId()); 272 if (choice.sameSection(section)) { 273 nrSelected ++; continue sections; 274 } 275 } 276 } 277 return (nrMatching.isEmpty() ? 1.0 : ((double) nrSelected) / nrMatching.size()); 278 } 279 280 /** Percent of sections that have the same configuration 281 * @return percent of sections that are selected 282 **/ 283 public double percentSelectedSameConfig() { 284 if (!isCourseRequest() || getStudent().isDummy() || getConfig() == null) return (getRequest().hasSelection() ? 1.0 : 0.0); 285 CourseRequest courseRequest = (CourseRequest) getRequest(); 286 boolean hasConfigSelection = false; 287 for (Choice choice: courseRequest.getSelectedChoices()) { 288 if (choice.getConfigId() != null) { 289 hasConfigSelection = true; 290 if (choice.getConfigId().equals(getConfig().getId())) return 1.0; 291 } 292 } 293 return (hasConfigSelection ? 0.0 : 1.0); 294 } 295 296 /** Percent of sections that are initial 297 * @return percent of sections that of the initial enrollment 298 **/ 299 public double percentInitial() { 300 if (!isCourseRequest()) 301 return 0.0; 302 if (getRequest().getInitialAssignment() == null) 303 return 0.0; 304 Enrollment inital = getRequest().getInitialAssignment(); 305 int nrInitial = 0; 306 for (Section section : getSections()) { 307 if (inital.getAssignments().contains(section)) 308 nrInitial++; 309 } 310 return ((double) nrInitial) / getAssignments().size(); 311 } 312 313 /** Percent of sections that have same time as the initial assignment 314 * @return percent of sections that have same time as the initial assignment 315 **/ 316 public double percentSameTime() { 317 if (!isCourseRequest()) 318 return 0.0; 319 Enrollment ie = getRequest().getInitialAssignment(); 320 if (ie != null) { 321 int nrInitial = 0; 322 sections: for (Section section : getSections()) { 323 for (Section initial: ie.getSections()) { 324 if (section.sameInstructionalType(initial) && section.sameTime(initial)) { 325 nrInitial ++; 326 continue sections; 327 } 328 } 329 } 330 return ((double) nrInitial) / getAssignments().size(); 331 } 332 Set<Choice> selected = ((CourseRequest)getRequest()).getSelectedChoices(); 333 if (!selected.isEmpty()) { 334 int nrInitial = 0; 335 sections: for (Section section : getSections()) { 336 for (Choice choice: selected) { 337 if (choice.sameOffering(section) && choice.sameInstructionalType(section) && choice.sameTime(section)) { 338 nrInitial ++; 339 continue sections; 340 } 341 342 } 343 } 344 return ((double) nrInitial) / getAssignments().size(); 345 } 346 return 0.0; 347 } 348 349 /** True if all the sections are wait-listed 350 * @return all the sections are wait-listed 351 **/ 352 public boolean isWaitlisted() { 353 if (!isCourseRequest()) 354 return false; 355 CourseRequest courseRequest = (CourseRequest) getRequest(); 356 for (Iterator<? extends SctAssignment> i = getAssignments().iterator(); i.hasNext();) { 357 Section section = (Section) i.next(); 358 if (!courseRequest.isWaitlisted(section)) 359 return false; 360 } 361 return true; 362 } 363 364 /** True if all the sections are selected 365 * @return all the sections are selected 366 **/ 367 public boolean isSelected() { 368 if (!isCourseRequest()) 369 return false; 370 CourseRequest courseRequest = (CourseRequest) getRequest(); 371 for (Section section : getSections()) { 372 if (!courseRequest.isSelected(section)) 373 return false; 374 } 375 return true; 376 } 377 378 public boolean isRequired() { 379 if (!isCourseRequest()) 380 return false; 381 CourseRequest courseRequest = (CourseRequest) getRequest(); 382 for (Section section : getSections()) { 383 if (!courseRequest.isRequired(section)) 384 return false; 385 } 386 return true; 387 } 388 389 /** 390 * Enrollment penalty -- sum of section penalties (see 391 * {@link Section#getPenalty()}) 392 * @return online penalty 393 */ 394 public double getPenalty() { 395 if (iCachedPenalty == null) { 396 double penalty = 0.0; 397 if (isCourseRequest()) { 398 for (Section section : getSections()) { 399 penalty += section.getPenalty(); 400 } 401 } 402 iCachedPenalty = Double.valueOf(penalty / getAssignments().size()); 403 } 404 return iCachedPenalty.doubleValue(); 405 } 406 407 /** Enrollment value */ 408 @Override 409 public double toDouble(Assignment<Request, Enrollment> assignment) { 410 return toDouble(assignment, true); 411 } 412 413 /** Enrollment value 414 * @param assignment current assignment 415 * @param precise if false, distance conflicts and time overlaps are ignored (i.e., much faster, but less precise computation) 416 * @return enrollment penalty 417 **/ 418 public double toDouble(Assignment<Request, Enrollment> assignment, boolean precise) { 419 if (precise) { 420 StudentSectioningModel model = (StudentSectioningModel)variable().getModel(); 421 if (model.getStudentQuality() != null) 422 return - getRequest().getWeight() * model.getStudentWeights().getWeight(assignment, this, studentQualityConflicts(assignment)); 423 else 424 return - getRequest().getWeight() * model.getStudentWeights().getWeight(assignment, this, distanceConflicts(assignment), timeOverlappingConflicts(assignment)); 425 } else { 426 Double value = (assignment == null ? null : variable().getContext(assignment).getLastWeight()); 427 if (value != null) return - value; 428 return - getRequest().getWeight() * ((StudentSectioningModel)variable().getModel()).getStudentWeights().getWeight(assignment, this); 429 } 430 } 431 432 /** Enrollment name */ 433 @Override 434 public String getName() { 435 if (getRequest() instanceof CourseRequest) { 436 Course course = null; 437 CourseRequest courseRequest = (CourseRequest) getRequest(); 438 for (Course c : courseRequest.getCourses()) { 439 if (c.getOffering().getConfigs().contains(getConfig())) { 440 course = c; 441 break; 442 } 443 } 444 String ret = (course == null ? getConfig() == null ? "" : getConfig().getName() : course.getName()); 445 for (Iterator<? extends SctAssignment> i = getAssignments().iterator(); i.hasNext();) { 446 Section assignment = (Section) i.next(); 447 ret += "\n " + assignment.getLongName(true) + (i.hasNext() ? "," : ""); 448 } 449 return ret; 450 } else if (getRequest() instanceof FreeTimeRequest) { 451 return "Free Time " + ((FreeTimeRequest) getRequest()).getTime().getLongName(true); 452 } else { 453 String ret = ""; 454 for (Iterator<? extends SctAssignment> i = getAssignments().iterator(); i.hasNext();) { 455 SctAssignment assignment = i.next(); 456 ret += assignment.toString() + (i.hasNext() ? "," : ""); 457 if (i.hasNext()) 458 ret += "\n "; 459 } 460 return ret; 461 } 462 } 463 464 public String toString(Assignment<Request, Enrollment> a) { 465 if (getAssignments().isEmpty()) return "not assigned"; 466 Set<DistanceConflict.Conflict> dc = distanceConflicts(a); 467 Set<TimeOverlapsCounter.Conflict> toc = timeOverlappingConflicts(a); 468 int share = 0; 469 if (toc != null) 470 for (TimeOverlapsCounter.Conflict c: toc) 471 share += c.getShare(); 472 String ret = toDouble(a) + "/" + sDF.format(getRequest().getBound()) 473 + (getPenalty() == 0.0 ? "" : "/" + sDF.format(getPenalty())) 474 + (dc == null || dc.isEmpty() ? "" : "/dc:" + dc.size()) 475 + (share <= 0 ? "" : "/toc:" + share); 476 if (getRequest() instanceof CourseRequest) { 477 double sameGroup = 0.0; int groupCount = 0; 478 for (RequestGroup g: ((CourseRequest)getRequest()).getRequestGroups()) { 479 if (g.getCourse().equals(getCourse())) { 480 sameGroup += g.getEnrollmentSpread(a, this, 1.0, 0.0); 481 groupCount ++; 482 } 483 } 484 if (groupCount > 0) 485 ret += "/g:" + sDF.format(sameGroup / groupCount); 486 } 487 if (getRequest() instanceof CourseRequest) { 488 ret += " "; 489 for (Iterator<? extends SctAssignment> i = getAssignments().iterator(); i.hasNext();) { 490 SctAssignment assignment = i.next(); 491 ret += assignment + (i.hasNext() ? ", " : ""); 492 } 493 } 494 if (getReservation() != null) ret = "(r) " + ret; 495 return ret; 496 } 497 498 @Override 499 public String toString() { 500 if (getAssignments().isEmpty()) return "not assigned"; 501 String ret = sDF.format(getRequest().getBound()) + (getPenalty() == 0.0 ? "" : "/" + sDF.format(getPenalty())); 502 if (getRequest() instanceof CourseRequest) { 503 ret += " "; 504 for (Iterator<? extends SctAssignment> i = getAssignments().iterator(); i.hasNext();) { 505 SctAssignment assignment = i.next(); 506 ret += assignment + (i.hasNext() ? ", " : ""); 507 } 508 } 509 if (getReservation() != null) ret = "(r) " + ret; 510 if (getRequest().isMPP()) ret += " [i" + sDF.format(100.0 * percentInitial()) + "/t" + sDF.format(100.0 * percentSameTime()) + "]"; 511 return ret; 512 } 513 514 @Override 515 public boolean equals(Object o) { 516 if (o == null || !(o instanceof Enrollment)) 517 return false; 518 Enrollment e = (Enrollment) o; 519 if (!ToolBox.equals(getCourse(), e.getCourse())) 520 return false; 521 if (!ToolBox.equals(getConfig(), e.getConfig())) 522 return false; 523 if (!ToolBox.equals(getRequest(), e.getRequest())) 524 return false; 525 if (!ToolBox.equals(getAssignments(), e.getAssignments())) 526 return false; 527 if (!ToolBox.equals(getReservation(), e.getReservation())) 528 return false; 529 return true; 530 } 531 532 /** Distance conflicts, in which this enrollment is involved. 533 * @param assignment current assignment 534 * @return distance conflicts 535 **/ 536 public Set<DistanceConflict.Conflict> distanceConflicts(Assignment<Request, Enrollment> assignment) { 537 if (!isCourseRequest()) 538 return null; 539 if (getRequest().getModel() instanceof StudentSectioningModel) { 540 DistanceConflict dc = ((StudentSectioningModel) getRequest().getModel()).getDistanceConflict(); 541 if (dc == null) return null; 542 return dc.allConflicts(assignment, this); 543 } else 544 return null; 545 } 546 547 /** Time overlapping conflicts, in which this enrollment is involved. 548 * @param assignment current assignment 549 * @return time overlapping conflicts 550 **/ 551 public Set<TimeOverlapsCounter.Conflict> timeOverlappingConflicts(Assignment<Request, Enrollment> assignment) { 552 if (getRequest().getModel() instanceof StudentSectioningModel) { 553 TimeOverlapsCounter toc = ((StudentSectioningModel) getRequest().getModel()).getTimeOverlaps(); 554 if (toc == null) 555 return null; 556 return toc.allConflicts(assignment, this); 557 } else 558 return null; 559 } 560 561 public Set<StudentQuality.Conflict> studentQualityConflicts(Assignment<Request, Enrollment> assignment) { 562 if (!isCourseRequest()) 563 return null; 564 if (getRequest().getModel() instanceof StudentSectioningModel) { 565 StudentQuality sq = ((StudentSectioningModel) getRequest().getModel()).getStudentQuality(); 566 if (sq == null) return null; 567 return sq.allConflicts(assignment, this); 568 } else 569 return null; 570 } 571 572 /** 573 * Return enrollment priority 574 * @return zero for the course, one for the first alternative, two for the second alternative 575 */ 576 public int getPriority() { 577 return iPriority + (iNoReservationPenalty ? 1 : 0); 578 } 579 580 /** 581 * Return enrollment priority, ignoring priority bump provided by reservations 582 * @return zero for the course, one for the first alternative, two for the second alternative 583 */ 584 public int getTruePriority() { 585 return iPriority; 586 } 587 588 /** 589 * Return adjusted enrollment priority, including priority bump provided by reservations 590 * (but ensuring that getting the course without a reservation is still better than getting an alternative) 591 * @return zero for the course, two for the first alternative, four for the second alternative; plus one when the no reservation penalty applies 592 */ 593 public int getAdjustedPriority() { 594 return 2 * iPriority + (iNoReservationPenalty ? 1 : 0); 595 } 596 597 /** 598 * Return total number of slots of all sections in the enrollment. 599 * @return number of slots used 600 */ 601 public int getNrSlots() { 602 int ret = 0; 603 for (SctAssignment a: getAssignments()) { 604 if (a.getTime() != null) ret += a.getTime().getLength() * a.getTime().getNrMeetings(); 605 } 606 return ret; 607 } 608 609 /** 610 * Return reservation used for this enrollment 611 * @return used reservation 612 */ 613 public Reservation getReservation() { return iReservation; } 614 615 /** 616 * Set reservation for this enrollment 617 * @param reservation used reservation 618 */ 619 public void setReservation(Reservation reservation) { iReservation = reservation; } 620 621 /** 622 * Time stamp of the enrollment 623 * @return enrollment time stamp 624 */ 625 public Long getTimeStamp() { 626 return iTimeStamp; 627 } 628 629 /** 630 * Time stamp of the enrollment 631 * @param timeStamp enrollment time stamp 632 */ 633 public void setTimeStamp(Long timeStamp) { 634 iTimeStamp = timeStamp; 635 } 636 637 /** 638 * Approval of the enrollment (only used by the online student sectioning) 639 * @return consent approval 640 */ 641 public String getApproval() { 642 return iApproval; 643 } 644 645 /** 646 * Approval of the enrollment (only used by the online student sectioning) 647 * @param approval consent approval 648 */ 649 public void setApproval(String approval) { 650 iApproval = approval; 651 } 652 653 /** 654 * True if this enrollment can overlap with other enrollments of the student. 655 * @return can overlap with other enrollments of the student 656 */ 657 public boolean isAllowOverlap() { 658 return (getReservation() != null && getReservation().isAllowOverlap()); 659 } 660 661 /** 662 * Enrollment limit, i.e., the number of students that would be able to get into the offering using this enrollment (if all the sections are empty) 663 * @return enrollment limit 664 */ 665 public int getLimit() { 666 if (!isCourseRequest()) return -1; // free time requests have no limit 667 Integer limit = null; 668 for (Section section: getSections()) 669 if (section.getLimit() >= 0) { 670 if (limit == null) 671 limit = section.getLimit(); 672 else 673 limit = Math.min(limit, section.getLimit()); 674 } 675 return (limit == null ? -1 : limit); 676 } 677 678 /** 679 * Credit of this enrollment (using either {@link Course#getCreditValue()} or {@link Subpart#getCreditValue()} when course credit is not present) 680 * @return credit of this enrollment 681 */ 682 public float getCredit() { 683 if (getCourse() == null) return 0f; 684 if (getAssignments().isEmpty()) return 0f; 685 if (getCourse().hasCreditValue()) return getCourse().getCreditValue(); 686 float subpartCredit = 0f; 687 for (Subpart subpart: getConfig().getSubparts()) { 688 if (subpart.hasCreditValue()) subpartCredit += subpart.getCreditValue(); 689 } 690 return subpartCredit; 691 } 692 693}