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