001package net.sf.cpsolver.studentsct.model; 002 003import java.text.DecimalFormat; 004import java.util.ArrayList; 005import java.util.HashMap; 006import java.util.HashSet; 007import java.util.List; 008import java.util.Map; 009import java.util.Set; 010 011import net.sf.cpsolver.coursett.model.Placement; 012import net.sf.cpsolver.coursett.model.RoomLocation; 013import net.sf.cpsolver.coursett.model.TimeLocation; 014import net.sf.cpsolver.studentsct.reservation.Reservation; 015 016/** 017 * Representation of a class. Each section contains id, name, scheduling 018 * subpart, time/room placement, and a limit. Optionally, parent-child relation 019 * between sections can be defined. <br> 020 * <br> 021 * Each student requesting a course needs to be enrolled in a class of each 022 * subpart of a selected configuration. In the case of parent-child relation 023 * between classes, if a student is enrolled in a section that has a parent 024 * section defined, he/she has to be enrolled in the parent section as well. If 025 * there is a parent-child relation between two sections, the same relation is 026 * defined on their subparts as well, i.e., if section A is a parent section B, 027 * subpart of section A isa parent of subpart of section B. <br> 028 * <br> 029 * 030 * @version StudentSct 1.2 (Student Sectioning)<br> 031 * Copyright (C) 2007 - 2010 Tomáš Müller<br> 032 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br> 033 * <a href="http://muller.unitime.org">http://muller.unitime.org</a><br> 034 * <br> 035 * This library is free software; you can redistribute it and/or modify 036 * it under the terms of the GNU Lesser General Public License as 037 * published by the Free Software Foundation; either version 3 of the 038 * License, or (at your option) any later version. <br> 039 * <br> 040 * This library is distributed in the hope that it will be useful, but 041 * WITHOUT ANY WARRANTY; without even the implied warranty of 042 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 043 * Lesser General Public License for more details. <br> 044 * <br> 045 * You should have received a copy of the GNU Lesser General Public 046 * License along with this library; if not see 047 * <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>. 048 */ 049public class Section implements Assignment, Comparable<Section> { 050 private static DecimalFormat sDF = new DecimalFormat("0.000"); 051 private long iId = -1; 052 private String iName = null; 053 private Map<Long, String> iNameByCourse = null; 054 private Subpart iSubpart = null; 055 private Section iParent = null; 056 private Placement iPlacement = null; 057 private int iLimit = 0; 058 private Set<Enrollment> iEnrollments = new HashSet<Enrollment>(); 059 private Choice iChoice = null; 060 private double iPenalty = 0.0; 061 private double iEnrollmentWeight = 0.0; 062 private double iMaxEnrollmentWeight = 0.0; 063 private double iMinEnrollmentWeight = 0.0; 064 private double iSpaceExpected = 0.0; 065 private double iSpaceHeld = 0.0; 066 private String iNote = null; 067 private Set<Long> iIgnoreConflictsWith = null; 068 069 /** 070 * Constructor 071 * 072 * @param id 073 * section unique id 074 * @param limit 075 * section limit, i.e., the maximal number of students that can 076 * be enrolled in this section at the same time 077 * @param name 078 * section name 079 * @param subpart 080 * subpart of this section 081 * @param placement 082 * time/room placement 083 * @param instructorIds 084 * instructor(s) id -- needed for {@link Section#getChoice()} 085 * @param instructorNames 086 * instructor(s) name -- needed for {@link Section#getChoice()} 087 * @param parent 088 * parent section -- if there is a parent section defined, a 089 * student that is enrolled in this section has to be enrolled in 090 * the parent section as well. Also, the same relation needs to 091 * be defined between subpart of this section and the subpart of 092 * the parent section 093 */ 094 public Section(long id, int limit, String name, Subpart subpart, Placement placement, String instructorIds, 095 String instructorNames, Section parent) { 096 iId = id; 097 iLimit = limit; 098 iName = name; 099 iSubpart = subpart; 100 iSubpart.getSections().add(this); 101 iPlacement = placement; 102 iParent = parent; 103 iChoice = new Choice(getSubpart().getConfig().getOffering(), getSubpart().getInstructionalType(), getTime(), 104 instructorIds, instructorNames); 105 } 106 107 /** Section id */ 108 @Override 109 public long getId() { 110 return iId; 111 } 112 113 /** 114 * Section limit. This is defines the maximal number of students that can be 115 * enrolled into this section at the same time. It is -1 in the case of an 116 * unlimited section 117 */ 118 public int getLimit() { 119 return iLimit; 120 } 121 122 /** Set section limit */ 123 public void setLimit(int limit) { 124 iLimit = limit; 125 } 126 127 /** Section name */ 128 public String getName() { 129 return iName; 130 } 131 132 /** Set section name */ 133 public void setName(String name) { 134 iName = name; 135 } 136 137 /** Scheduling subpart to which this section belongs */ 138 public Subpart getSubpart() { 139 return iSubpart; 140 } 141 142 /** 143 * Parent section of this section (can be null). If there is a parent 144 * section defined, a student that is enrolled in this section has to be 145 * enrolled in the parent section as well. Also, the same relation needs to 146 * be defined between subpart of this section and the subpart of the parent 147 * section. 148 */ 149 public Section getParent() { 150 return iParent; 151 } 152 153 /** 154 * Time/room placement of the section. This can be null, for arranged 155 * sections. 156 */ 157 public Placement getPlacement() { 158 return iPlacement; 159 } 160 161 /** 162 * Set time/room placement of the section. This can be null, for arranged 163 * sections. 164 */ 165 public void setPlacement(Placement placement) { 166 iPlacement = placement; 167 } 168 169 /** Time placement of the section. */ 170 @Override 171 public TimeLocation getTime() { 172 return (iPlacement == null ? null : iPlacement.getTimeLocation()); 173 } 174 175 /** Number of rooms in which the section meet. */ 176 @Override 177 public int getNrRooms() { 178 return (iPlacement == null ? 0 : iPlacement.getNrRooms()); 179 } 180 181 /** 182 * Room placement -- list of 183 * {@link net.sf.cpsolver.coursett.model.RoomLocation} 184 */ 185 @Override 186 public List<RoomLocation> getRooms() { 187 if (iPlacement == null) 188 return null; 189 if (iPlacement.getRoomLocations() == null && iPlacement.getRoomLocation() != null) { 190 List<RoomLocation> ret = new ArrayList<RoomLocation>(1); 191 ret.add(iPlacement.getRoomLocation()); 192 return ret; 193 } 194 return iPlacement.getRoomLocations(); 195 } 196 197 /** 198 * True, if this section overlaps with the given assignment in time and 199 * space 200 */ 201 @Override 202 public boolean isOverlapping(Assignment assignment) { 203 if (isAllowOverlap() || assignment.isAllowOverlap()) return false; 204 if (getTime() == null || assignment.getTime() == null) return false; 205 if (assignment instanceof Section && isToIgnoreStudentConflictsWith(assignment.getId())) return false; 206 return getTime().hasIntersection(assignment.getTime()); 207 } 208 209 /** 210 * True, if this section overlaps with one of the given set of assignments 211 * in time and space 212 */ 213 @Override 214 public boolean isOverlapping(Set<? extends Assignment> assignments) { 215 if (isAllowOverlap()) return false; 216 if (getTime() == null || assignments == null) 217 return false; 218 for (Assignment assignment : assignments) { 219 if (assignment.isAllowOverlap()) 220 continue; 221 if (assignment.getTime() == null) 222 continue; 223 if (assignment instanceof Section && isToIgnoreStudentConflictsWith(assignment.getId())) 224 continue; 225 if (getTime().hasIntersection(assignment.getTime())) 226 return true; 227 } 228 return false; 229 } 230 231 /** Called when an enrollment with this section is assigned to a request */ 232 @Override 233 public void assigned(Enrollment enrollment) { 234 if (iEnrollments.isEmpty()) { 235 iMinEnrollmentWeight = iMaxEnrollmentWeight = enrollment.getRequest().getWeight(); 236 } else { 237 iMaxEnrollmentWeight = Math.max(iMaxEnrollmentWeight, enrollment.getRequest().getWeight()); 238 iMinEnrollmentWeight = Math.min(iMinEnrollmentWeight, enrollment.getRequest().getWeight()); 239 } 240 iEnrollments.add(enrollment); 241 iEnrollmentWeight += enrollment.getRequest().getWeight(); 242 } 243 244 /** Called when an enrollment with this section is unassigned from a request */ 245 @Override 246 public void unassigned(Enrollment enrollment) { 247 iEnrollments.remove(enrollment); 248 iEnrollmentWeight -= enrollment.getRequest().getWeight(); 249 if (iEnrollments.isEmpty()) { 250 iMinEnrollmentWeight = iMaxEnrollmentWeight = 0; 251 } else if (iMinEnrollmentWeight != iMaxEnrollmentWeight) { 252 if (iMinEnrollmentWeight == enrollment.getRequest().getWeight()) { 253 double newMinEnrollmentWeight = Double.MAX_VALUE; 254 for (Enrollment e : iEnrollments) { 255 if (e.getRequest().getWeight() == iMinEnrollmentWeight) { 256 newMinEnrollmentWeight = iMinEnrollmentWeight; 257 break; 258 } else { 259 newMinEnrollmentWeight = Math.min(newMinEnrollmentWeight, e.getRequest().getWeight()); 260 } 261 } 262 iMinEnrollmentWeight = newMinEnrollmentWeight; 263 } 264 if (iMaxEnrollmentWeight == enrollment.getRequest().getWeight()) { 265 double newMaxEnrollmentWeight = Double.MIN_VALUE; 266 for (Enrollment e : iEnrollments) { 267 if (e.getRequest().getWeight() == iMaxEnrollmentWeight) { 268 newMaxEnrollmentWeight = iMaxEnrollmentWeight; 269 break; 270 } else { 271 newMaxEnrollmentWeight = Math.max(newMaxEnrollmentWeight, e.getRequest().getWeight()); 272 } 273 } 274 iMaxEnrollmentWeight = newMaxEnrollmentWeight; 275 } 276 } 277 } 278 279 /** Set of assigned enrollments */ 280 @Override 281 public Set<Enrollment> getEnrollments() { 282 return iEnrollments; 283 } 284 285 /** 286 * Enrollment weight -- weight of all requests which have an enrollment that 287 * contains this section, excluding the given one. See 288 * {@link Request#getWeight()}. 289 */ 290 public double getEnrollmentWeight(Request excludeRequest) { 291 double weight = iEnrollmentWeight; 292 if (excludeRequest != null && excludeRequest.getAssignment() != null 293 && iEnrollments.contains(excludeRequest.getAssignment())) 294 weight -= excludeRequest.getWeight(); 295 return weight; 296 } 297 298 /** 299 * Maximal weight of a single enrollment in the section 300 */ 301 public double getMaxEnrollmentWeight() { 302 return iMaxEnrollmentWeight; 303 } 304 305 /** 306 * Minimal weight of a single enrollment in the section 307 */ 308 public double getMinEnrollmentWeight() { 309 return iMinEnrollmentWeight; 310 } 311 312 /** Long name: subpart name + time long name + room names + instructor names */ 313 public String getLongName() { 314 return getSubpart().getName() + " " + getName() + " " + (getTime() == null ? "" : " " + getTime().getLongName()) 315 + (getNrRooms() == 0 ? "" : " " + getPlacement().getRoomName(",")) 316 + (getChoice().getInstructorNames() == null ? "" : " " + getChoice().getInstructorNames()); 317 } 318 319 @Override 320 public String toString() { 321 return getSubpart().getConfig().getOffering().getName() + " " + getSubpart().getName() + " " + getName() 322 + (getTime() == null ? "" : " " + getTime().getLongName()) 323 + (getNrRooms() == 0 ? "" : " " + getPlacement().getRoomName(",")) 324 + (getChoice().getInstructorNames() == null ? "" : " " + getChoice().getInstructorNames()) + " (L:" 325 + (getLimit() < 0 ? "unlimited" : "" + getLimit()) 326 + (getPenalty() == 0.0 ? "" : ",P:" + sDF.format(getPenalty())) + ")"; 327 } 328 329 /** A (student) choice representing this section. */ 330 public Choice getChoice() { 331 return iChoice; 332 } 333 334 /** 335 * Return penalty which is added to an enrollment that contains this 336 * section. 337 */ 338 public double getPenalty() { 339 return iPenalty; 340 } 341 342 /** Set penalty which is added to an enrollment that contains this section. */ 343 public void setPenalty(double penalty) { 344 iPenalty = penalty; 345 } 346 347 /** 348 * Compare two sections, prefer sections with lower penalty and more open 349 * space 350 */ 351 @Override 352 public int compareTo(Section s) { 353 int cmp = Double.compare(getPenalty(), s.getPenalty()); 354 if (cmp != 0) 355 return cmp; 356 cmp = Double.compare(getLimit() - getEnrollmentWeight(null), s.getLimit() - s.getEnrollmentWeight(null)); 357 if (cmp != 0) 358 return cmp; 359 return Double.compare(getId(), s.getId()); 360 } 361 362 /** 363 * Return the amount of space of this section that is held for incoming 364 * students. This attribute is computed during the batch sectioning (it is 365 * the overall weight of dummy students enrolled in this section) and it is 366 * being updated with each incomming student during the online sectioning. 367 */ 368 public double getSpaceHeld() { 369 return iSpaceHeld; 370 } 371 372 /** 373 * Set the amount of space of this section that is held for incoming 374 * students. See {@link Section#getSpaceHeld()} for more info. 375 */ 376 public void setSpaceHeld(double spaceHeld) { 377 iSpaceHeld = spaceHeld; 378 } 379 380 /** 381 * Return the amount of space of this section that is expected to be taken 382 * by incoming students. This attribute is computed during the batch 383 * sectioning (for each dummy student that can attend this section (without 384 * any conflict with other enrollments of that student), 1 / x where x is 385 * the number of such sections of this subpart is added to this value). 386 * Also, this value is being updated with each incomming student during the 387 * online sectioning. 388 */ 389 public double getSpaceExpected() { 390 return iSpaceExpected; 391 } 392 393 /** 394 * Set the amount of space of this section that is expected to be taken by 395 * incoming students. See {@link Section#getSpaceExpected()} for more info. 396 */ 397 public void setSpaceExpected(double spaceExpected) { 398 iSpaceExpected = spaceExpected; 399 } 400 401 /** 402 * Online sectioning penalty. 403 */ 404 public double getOnlineSectioningPenalty() { 405 if (getLimit() <= 0) 406 return 0.0; 407 408 double available = getLimit() - getEnrollmentWeight(null); 409 410 double penalty = (getSpaceExpected() - available) / getLimit(); 411 412 return Math.max(-1.0, Math.min(1.0, penalty)); 413 } 414 415 /** 416 * Return true if overlaps are allowed, but the number of overlapping slots should be minimized. 417 * This can be changed on the subpart, using {@link Subpart#setAllowOverlap(boolean)}. 418 **/ 419 @Override 420 public boolean isAllowOverlap() { 421 return iSubpart.isAllowOverlap(); 422 } 423 424 /** Sections first, then by {@link FreeTimeRequest#getId()} */ 425 @Override 426 public int compareById(Assignment a) { 427 if (a instanceof Section) { 428 return new Long(getId()).compareTo(((Section)a).getId()); 429 } else { 430 return -1; 431 } 432 } 433 434 /** 435 * Available space in the section that is not reserved by any section reservation 436 * @param excludeRequest excluding given request (if not null) 437 **/ 438 public double getUnreservedSpace(Request excludeRequest) { 439 // section is unlimited -> there is unreserved space unless there is an unlimited reservation too 440 // (in which case there is no unreserved space) 441 if (getLimit() < 0) { 442 // exclude reservations that are not directly set on this section 443 for (Reservation r: getSectionReservations()) { 444 // ignore expired reservations 445 if (r.isExpired()) continue; 446 // there is an unlimited reservation -> no unreserved space 447 if (r.getLimit() < 0) return 0.0; 448 } 449 return Double.MAX_VALUE; 450 } 451 452 double available = getLimit() - getEnrollmentWeight(excludeRequest); 453 // exclude reservations that are not directly set on this section 454 for (Reservation r: getSectionReservations()) { 455 // ignore expired reservations 456 if (r.isExpired()) continue; 457 // unlimited reservation -> all the space is reserved 458 if (r.getLimit() < 0.0) return 0.0; 459 // compute space that can be potentially taken by this reservation 460 double reserved = r.getReservedAvailableSpace(excludeRequest); 461 // deduct the space from available space 462 available -= Math.max(0.0, reserved); 463 } 464 465 return available; 466 } 467 468 /** 469 * Total space in the section that cannot be used by any section reservation 470 **/ 471 public double getTotalUnreservedSpace() { 472 if (iTotalUnreservedSpace == null) 473 iTotalUnreservedSpace = getTotalUnreservedSpaceNoCache(); 474 return iTotalUnreservedSpace; 475 } 476 private Double iTotalUnreservedSpace = null; 477 private double getTotalUnreservedSpaceNoCache() { 478 // section is unlimited -> there is unreserved space unless there is an unlimited reservation too 479 // (in which case there is no unreserved space) 480 if (getLimit() < 0) { 481 // exclude reservations that are not directly set on this section 482 for (Reservation r: getSectionReservations()) { 483 // ignore expired reservations 484 if (r.isExpired()) continue; 485 // there is an unlimited reservation -> no unreserved space 486 if (r.getLimit() < 0) return 0.0; 487 } 488 return Double.MAX_VALUE; 489 } 490 491 // we need to check all reservations linked with this section 492 double available = getLimit(), reserved = 0, exclusive = 0; 493 Set<Section> sections = new HashSet<Section>(); 494 reservations: for (Reservation r: getSectionReservations()) { 495 // ignore expired reservations 496 if (r.isExpired()) continue; 497 // unlimited reservation -> no unreserved space 498 if (r.getLimit() < 0) return 0.0; 499 for (Section s: r.getSections(getSubpart())) { 500 if (s.equals(this)) continue; 501 if (s.getLimit() < 0) continue reservations; 502 if (sections.add(s)) 503 available += s.getLimit(); 504 } 505 reserved += r.getLimit(); 506 if (r.getSections(getSubpart()).size() == 1) 507 exclusive += r.getLimit(); 508 } 509 510 return Math.min(available - reserved, getLimit() - exclusive); 511 } 512 513 514 /** 515 * Get reservations for this section 516 */ 517 public List<Reservation> getReservations() { 518 if (iReservations == null) { 519 iReservations = new ArrayList<Reservation>(); 520 for (Reservation r: getSubpart().getConfig().getOffering().getReservations()) { 521 if (r.getSections(getSubpart()) == null || r.getSections(getSubpart()).contains(this)) 522 iReservations.add(r); 523 } 524 } 525 return iReservations; 526 } 527 private List<Reservation> iReservations = null; 528 529 /** 530 * Get reservations that require this section 531 */ 532 public List<Reservation> getSectionReservations() { 533 if (iSectionReservations == null) { 534 iSectionReservations = new ArrayList<Reservation>(); 535 for (Reservation r: getSubpart().getSectionReservations()) { 536 if (r.getSections(getSubpart()).contains(this)) 537 iSectionReservations.add(r); 538 } 539 } 540 return iSectionReservations; 541 } 542 private List<Reservation> iSectionReservations = null; 543 544 /** 545 * Clear reservation information that was cached on this section 546 */ 547 public void clearReservationCache() { 548 iReservations = null; 549 iSectionReservations = null; 550 iTotalUnreservedSpace = null; 551 } 552 553 /** 554 * Return course-dependent section name 555 */ 556 public String getName(long courseId) { 557 if (iNameByCourse == null) return getName(); 558 String name = iNameByCourse.get(courseId); 559 return (name == null ? getName() : name); 560 } 561 562 /** 563 * Set course-dependent section name 564 */ 565 public void setName(long courseId, String name) { 566 if (iNameByCourse == null) iNameByCourse = new HashMap<Long, String>(); 567 iNameByCourse.put(courseId, name); 568 } 569 570 /** 571 * Return course-dependent section names 572 */ 573 public Map<Long, String> getNameByCourse() { return iNameByCourse; } 574 575 @Override 576 public boolean equals(Object o) { 577 if (o == null || !(o instanceof Section)) return false; 578 return getId() == ((Section)o).getId(); 579 } 580 581 @Override 582 public int hashCode() { 583 return (int) (iId ^ (iId >>> 32)); 584 } 585 586 /** 587 * Section note 588 */ 589 public String getNote() { return iNote; } 590 591 /** 592 * Section note 593 */ 594 public void setNote(String note) { iNote = note; } 595 596 /** 597 * Add section id of a section that student conflicts are to be ignored with 598 */ 599 public void addIgnoreConflictWith(long sectionId) { 600 if (iIgnoreConflictsWith == null) iIgnoreConflictsWith = new HashSet<Long>(); 601 iIgnoreConflictsWith.add(sectionId); 602 } 603 604 /** 605 * Returns true if student conflicts between this section and the given one are to be ignored 606 */ 607 public boolean isToIgnoreStudentConflictsWith(long sectionId) { 608 return iIgnoreConflictsWith != null && iIgnoreConflictsWith.contains(sectionId); 609 } 610 611 /** 612 * Returns a set of ids of sections that student conflicts are to be ignored with (between this section and the others) 613 */ 614 public Set<Long> getIgnoreConflictWithSectionIds() { 615 return iIgnoreConflictsWith; 616 } 617}