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