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