001package org.cpsolver.studentsct.model; 002 003import java.util.Collections; 004import java.util.HashSet; 005import java.util.Set; 006import java.util.regex.Matcher; 007import java.util.regex.Pattern; 008 009import org.cpsolver.ifs.assignment.Assignment; 010import org.cpsolver.ifs.assignment.context.AbstractClassWithContext; 011import org.cpsolver.ifs.assignment.context.AssignmentConstraintContext; 012import org.cpsolver.ifs.assignment.context.CanInheritContext; 013import org.cpsolver.ifs.model.Model; 014 015 016/** 017 * Representation of a course offering. A course offering contains id, subject 018 * area, course number and an instructional offering. <br> 019 * <br> 020 * Each instructional offering (see {@link Offering}) is offered under one or 021 * more course offerings. 022 * 023 * <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 */ 046public class Course extends AbstractClassWithContext<Request, Enrollment, Course.CourseContext> implements CanInheritContext<Request, Enrollment, Course.CourseContext> { 047 private long iId = -1; 048 private String iSubjectArea = null; 049 private String iCourseNumber = null; 050 private Offering iOffering = null; 051 private int iLimit = 0, iProjected = 0; 052 private Set<CourseRequest> iRequests = Collections.synchronizedSet(new HashSet<CourseRequest>()); 053 private String iNote = null; 054 private Set<RequestGroup> iRequestGroups = new HashSet<RequestGroup>(); 055 private String iCredit = null; 056 private Float iCreditValue = null; 057 private String iTitle = null; 058 private String iType = null; 059 private Course iParent = null; 060 private Set<Course> iChildren = null; 061 062 /** 063 * Constructor 064 * 065 * @param id 066 * course offering unique id 067 * @param subjectArea 068 * subject area (e.g., MA, CS, ENGL) 069 * @param courseNumber 070 * course number under the given subject area 071 * @param offering 072 * instructional offering which is offered under this course 073 * offering 074 */ 075 public Course(long id, String subjectArea, String courseNumber, Offering offering) { 076 iId = id; 077 iSubjectArea = subjectArea; 078 iCourseNumber = courseNumber; 079 iOffering = offering; 080 iOffering.getCourses().add(this); 081 } 082 083 /** 084 * Constructor 085 * 086 * @param id 087 * course offering unique id 088 * @param subjectArea 089 * subject area (e.g., MA, CS, ENGL) 090 * @param courseNumber 091 * course number under the given subject area 092 * @param offering 093 * instructional offering which is offered under this course 094 * offering 095 * @param limit 096 * course offering limit (-1 for unlimited) 097 * @param projected 098 * projected demand 099 */ 100 public Course(long id, String subjectArea, String courseNumber, Offering offering, int limit, int projected) { 101 iId = id; 102 iSubjectArea = subjectArea; 103 iCourseNumber = courseNumber; 104 iOffering = offering; 105 iOffering.getCourses().add(this); 106 iLimit = limit; 107 iProjected = projected; 108 } 109 110 /** Course offering unique id 111 * @return coure offering unqiue id 112 **/ 113 public long getId() { 114 return iId; 115 } 116 117 /** Subject area 118 * @return subject area abbreviation 119 **/ 120 public String getSubjectArea() { 121 return iSubjectArea; 122 } 123 124 /** Course number 125 * @return course number 126 **/ 127 public String getCourseNumber() { 128 return iCourseNumber; 129 } 130 131 /** Course offering name: subject area + course number 132 * @return course name 133 **/ 134 public String getName() { 135 return iSubjectArea + " " + iCourseNumber; 136 } 137 138 @Override 139 public String toString() { 140 return getName(); 141 } 142 143 /** Instructional offering which is offered under this course offering. 144 * @return instructional offering 145 **/ 146 public Offering getOffering() { 147 return iOffering; 148 } 149 150 /** Course offering limit 151 * @return course offering limit, -1 if unlimited 152 **/ 153 public int getLimit() { 154 return iLimit; 155 } 156 157 /** Set course offering limit 158 * @param limit course offering limit, -1 if unlimited 159 **/ 160 public void setLimit(int limit) { 161 iLimit = limit; 162 } 163 164 /** Course offering projected number of students 165 * @return course projection 166 **/ 167 public int getProjected() { 168 return iProjected; 169 } 170 171 /** Called when an enrollment with this course is assigned to a request 172 * @param assignment current assignment 173 * @param enrollment assigned enrollment 174 **/ 175 public void assigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) { 176 getContext(assignment).assigned(assignment, enrollment); 177 } 178 179 /** Called when an enrollment with this course is unassigned from a request 180 * @param assignment current assignment 181 * @param enrollment unassigned enrollment 182 */ 183 public void unassigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) { 184 getContext(assignment).unassigned(assignment, enrollment); 185 } 186 187 /** Set of course requests requesting this course 188 * @return request for this course 189 **/ 190 public Set<CourseRequest> getRequests() { 191 return iRequests; 192 } 193 194 /** 195 * Course note 196 * @return course note 197 */ 198 public String getNote() { return iNote; } 199 200 /** 201 * Course note 202 * @param note course note 203 */ 204 public void setNote(String note) { iNote = note; } 205 206 @Override 207 public boolean equals(Object o) { 208 if (o == null || !(o instanceof Course)) return false; 209 return getId() == ((Course)o).getId(); 210 } 211 212 @Override 213 public int hashCode() { 214 return (int) (iId ^ (iId >>> 32)); 215 } 216 217 @Override 218 public Model<Request, Enrollment> getModel() { 219 return getOffering().getModel(); 220 } 221 222 /** 223 * Enrollment weight -- weight of all requests that are enrolled into this course, 224 * excluding the given one. See 225 * {@link Request#getWeight()}. 226 * @param assignment current assignment 227 * @param excludeRequest request to exclude 228 * @return enrollment weight 229 */ 230 public double getEnrollmentWeight(Assignment<Request, Enrollment> assignment, Request excludeRequest) { 231 return getContext(assignment).getEnrollmentWeight(assignment, excludeRequest); 232 } 233 234 /** Set of assigned enrollments 235 * @param assignment current assignment 236 * @return assigned enrollments for this course offering 237 **/ 238 public Set<Enrollment> getEnrollments(Assignment<Request, Enrollment> assignment) { 239 return getContext(assignment).getEnrollments(); 240 } 241 242 /** 243 * Maximal weight of a single enrollment in the course 244 * @param assignment current assignment 245 * @return maximal enrollment weight 246 */ 247 public double getMaxEnrollmentWeight(Assignment<Request, Enrollment> assignment) { 248 return getContext(assignment).getMaxEnrollmentWeight(); 249 } 250 251 /** 252 * Minimal weight of a single enrollment in the course 253 * @param assignment current assignment 254 * @return minimal enrollment weight 255 */ 256 public double getMinEnrollmentWeight(Assignment<Request, Enrollment> assignment) { 257 return getContext(assignment).getMinEnrollmentWeight(); 258 } 259 260 /** 261 * Add request group of this course. This is automatically called 262 * by the constructor of the {@link RequestGroup}. 263 * @param group request group to be added 264 */ 265 public void addRequestGroup(RequestGroup group) { 266 iRequestGroups.add(group); 267 } 268 269 /** 270 * Remove request group from this course. 271 * @param group request group to be removed 272 */ 273 public void removeRequestGroup(RequestGroup group) { 274 iRequestGroups.remove(group); 275 } 276 277 /** 278 * Lists all the request groups of this course 279 * @return all request groups of this course 280 */ 281 public Set<RequestGroup> getRequestGroups() { 282 return iRequestGroups; 283 } 284 285 /** 286 * Set credit (Online Student Scheduling only) 287 * @param credit scheduling course credit 288 */ 289 public void setCredit(String credit) { 290 iCredit = credit; 291 if (iCreditValue == null && credit != null) { 292 int split = credit.indexOf('|'); 293 String abbv = null; 294 if (split >= 0) { 295 abbv = credit.substring(0, split); 296 } else { 297 abbv = credit; 298 } 299 Matcher m = Pattern.compile("(^| )(\\d+\\.?\\d*)([,-]?(\\d+\\.?\\d*))?($| )").matcher(abbv); 300 if (m.find()) 301 iCreditValue = Float.parseFloat(m.group(2)); 302 } 303 } 304 305 /** 306 * Get credit (Online Student Scheduling only) 307 * @return scheduling course credit 308 */ 309 public String getCredit() { return iCredit; } 310 311 /** 312 * True if this course has a credit value defined 313 * @return true if a credit value is set 314 */ 315 public boolean hasCreditValue() { return iCreditValue != null; } 316 317 /** 318 * Set course credit value (null if not set) 319 * @param creditValue course credit value 320 */ 321 public void setCreditValue(Float creditValue) { iCreditValue = creditValue; } 322 323 /** 324 * Get course credit value (null if not set) 325 * return course credit value 326 */ 327 public Float getCreditValue() { return iCreditValue; } 328 329 @Override 330 public CourseContext createAssignmentContext(Assignment<Request, Enrollment> assignment) { 331 return new CourseContext(assignment); 332 } 333 334 335 @Override 336 public CourseContext inheritAssignmentContext(Assignment<Request, Enrollment> assignment, CourseContext parentContext) { 337 return new CourseContext(parentContext); 338 } 339 340 public class CourseContext implements AssignmentConstraintContext<Request, Enrollment> { 341 private double iEnrollmentWeight = 0.0; 342 private Set<Enrollment> iEnrollments = null; 343 private double iMaxEnrollmentWeight = 0.0; 344 private double iMinEnrollmentWeight = 0.0; 345 private boolean iReadOnly = false; 346 347 public CourseContext(Assignment<Request, Enrollment> assignment) { 348 iEnrollments = new HashSet<Enrollment>(); 349 for (CourseRequest request: getRequests()) { 350 Enrollment enrollment = assignment.getValue(request); 351 if (enrollment != null && Course.this.equals(enrollment.getCourse())) 352 assigned(assignment, enrollment); 353 } 354 } 355 356 public CourseContext(CourseContext parent) { 357 iEnrollmentWeight = parent.iEnrollmentWeight; 358 iMinEnrollmentWeight = parent.iMinEnrollmentWeight; 359 iMaxEnrollmentWeight = parent.iMaxEnrollmentWeight; 360 iEnrollments = parent.iEnrollments; 361 iReadOnly = true; 362 } 363 364 @Override 365 public void assigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) { 366 if (iReadOnly) { 367 iEnrollments = new HashSet<Enrollment>(iEnrollments); 368 iReadOnly = false; 369 } 370 if (iEnrollments.isEmpty()) { 371 iMinEnrollmentWeight = iMaxEnrollmentWeight = enrollment.getRequest().getWeight(); 372 } else { 373 iMaxEnrollmentWeight = Math.max(iMaxEnrollmentWeight, enrollment.getRequest().getWeight()); 374 iMinEnrollmentWeight = Math.min(iMinEnrollmentWeight, enrollment.getRequest().getWeight()); 375 } 376 if (iEnrollments.add(enrollment) && (enrollment.getReservation() == null || !enrollment.getReservation().canBatchAssignOverLimit())) 377 iEnrollmentWeight += enrollment.getRequest().getWeight(); 378 } 379 380 @Override 381 public void unassigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) { 382 if (iReadOnly) { 383 iEnrollments = new HashSet<Enrollment>(iEnrollments); 384 iReadOnly = false; 385 } 386 if (iEnrollments.remove(enrollment) && (enrollment.getReservation() == null || !enrollment.getReservation().canBatchAssignOverLimit())) 387 iEnrollmentWeight -= enrollment.getRequest().getWeight(); 388 if (iEnrollments.isEmpty()) { 389 iMinEnrollmentWeight = iMaxEnrollmentWeight = 0; 390 } else if (iMinEnrollmentWeight != iMaxEnrollmentWeight) { 391 if (iMinEnrollmentWeight == enrollment.getRequest().getWeight()) { 392 double newMinEnrollmentWeight = Double.MAX_VALUE; 393 for (Enrollment e : iEnrollments) { 394 if (e.getRequest().getWeight() == iMinEnrollmentWeight) { 395 newMinEnrollmentWeight = iMinEnrollmentWeight; 396 break; 397 } else { 398 newMinEnrollmentWeight = Math.min(newMinEnrollmentWeight, e.getRequest().getWeight()); 399 } 400 } 401 iMinEnrollmentWeight = newMinEnrollmentWeight; 402 } 403 if (iMaxEnrollmentWeight == enrollment.getRequest().getWeight()) { 404 double newMaxEnrollmentWeight = Double.MIN_VALUE; 405 for (Enrollment e : iEnrollments) { 406 if (e.getRequest().getWeight() == iMaxEnrollmentWeight) { 407 newMaxEnrollmentWeight = iMaxEnrollmentWeight; 408 break; 409 } else { 410 newMaxEnrollmentWeight = Math.max(newMaxEnrollmentWeight, e.getRequest().getWeight()); 411 } 412 } 413 iMaxEnrollmentWeight = newMaxEnrollmentWeight; 414 } 415 } 416 } 417 418 /** 419 * Enrollment weight -- weight of all requests that are enrolled into this course, 420 * excluding the given one. See 421 * {@link Request#getWeight()}. 422 * @param assignment current assignment 423 * @param excludeRequest request to exclude 424 * @return enrollment weight 425 */ 426 public double getEnrollmentWeight(Assignment<Request, Enrollment> assignment, Request excludeRequest) { 427 double weight = iEnrollmentWeight; 428 if (excludeRequest != null) { 429 Enrollment enrollment = assignment.getValue(excludeRequest); 430 if (enrollment!= null && iEnrollments.contains(enrollment) && (enrollment.getReservation() == null || !enrollment.getReservation().canBatchAssignOverLimit())) 431 weight -= excludeRequest.getWeight(); 432 } 433 return weight; 434 } 435 436 /** Set of assigned enrollments 437 * @return assigned enrollments for this course offering 438 **/ 439 public Set<Enrollment> getEnrollments() { 440 return iEnrollments; 441 } 442 443 /** 444 * Maximal weight of a single enrollment in the course 445 * @return maximal enrollment weight 446 */ 447 public double getMaxEnrollmentWeight() { 448 return iMaxEnrollmentWeight; 449 } 450 451 /** 452 * Minimal weight of a single enrollment in the course 453 * @return minimal enrollment weight 454 */ 455 public double getMinEnrollmentWeight() { 456 return iMinEnrollmentWeight; 457 } 458 } 459 460 public double getOnlineBound() { 461 double bound = 1.0; 462 for (Config config: getOffering().getConfigs()) { 463 int online = config.getNrOnline(); 464 if (online == 0) return 0.0; 465 double factor = ((double)online) / config.getSubparts().size(); 466 if (factor < bound) bound = factor; 467 } 468 return bound; 469 } 470 471 public double getArrHrsBound() { 472 double bound = 1.0; 473 for (Config config: getOffering().getConfigs()) { 474 int arrHrs = config.getNrArrHours(); 475 if (arrHrs == 0) return 0.0; 476 double factor = ((double)arrHrs) / config.getSubparts().size(); 477 if (factor < bound) bound = factor; 478 } 479 return bound; 480 } 481 482 public double getPastBound() { 483 double bound = 1.0; 484 for (Config config: getOffering().getConfigs()) { 485 int past = config.getNrPast(); 486 if (past == 0) return 0.0; 487 double factor = ((double)past) / config.getSubparts().size(); 488 if (factor < bound) bound = factor; 489 } 490 return bound; 491 } 492 493 /** 494 * Course title 495 */ 496 public String getTitle() { 497 return iTitle; 498 } 499 500 /** 501 * Course title 502 */ 503 public void setTitle(String title) { 504 iTitle = title; 505 } 506 507 /** 508 * Course type 509 */ 510 public String getType() { 511 return iType; 512 } 513 514 /** 515 * Course type 516 */ 517 public void setType(String type) { 518 iType = type; 519 } 520 521 /** 522 * Parent course is set if the course depends on the parent. 523 * If the student is requesting both this course and its parent, they cannot get this course without also getting the parent course. 524 * @return parent offering 525 */ 526 public Course getParent() { return iParent; } 527 528 /** 529 * Parent course is set if the course depends on the parent. 530 * If the student is requesting both this course and its parent, they cannot get this course without also getting the parent course. 531 * @return true if parent is set (this course depends on some other course) 532 */ 533 public boolean hasParent() { return iParent != null; } 534 535 /** 536 * Return true if this course is a parent of at least one other course. 537 */ 538 public boolean hasChildren() { return iChildren != null && !iChildren.isEmpty(); } 539 540 /** 541 * Courses that this course is a parent of. 542 */ 543 public Set<Course> getChildren() { return iChildren; } 544 545 /** 546 * Parent course is set if the course depends on the parent. 547 * If the student is requesting both this course and its parent, they cannot get this course without also getting the parent course. 548 * parameter parent parent course, null if this offering has no parent 549 */ 550 public void setParent(Course parent) { 551 iParent = parent; 552 if (parent.iChildren == null) { 553 parent.iChildren = new HashSet<Course>(); 554 parent.iChildren.add(this); 555 } 556 } 557}