001package org.cpsolver.studentsct.report; 002 003import java.text.DecimalFormat; 004import java.util.ArrayList; 005import java.util.Comparator; 006import java.util.HashMap; 007import java.util.HashSet; 008import java.util.List; 009import java.util.Map; 010import java.util.Set; 011import java.util.TreeSet; 012 013import org.cpsolver.ifs.assignment.Assignment; 014import org.cpsolver.ifs.model.GlobalConstraint; 015import org.cpsolver.ifs.util.CSVFile; 016import org.cpsolver.ifs.util.DataProperties; 017import org.cpsolver.studentsct.StudentSectioningModel; 018import org.cpsolver.studentsct.constraint.SectionLimit; 019import org.cpsolver.studentsct.model.Course; 020import org.cpsolver.studentsct.model.CourseRequest; 021import org.cpsolver.studentsct.model.Enrollment; 022import org.cpsolver.studentsct.model.FreeTimeRequest; 023import org.cpsolver.studentsct.model.Request; 024import org.cpsolver.studentsct.model.Section; 025import org.cpsolver.studentsct.model.Student; 026 027 028/** 029 * This class computes time and availability conflicts on classes in a {@link CSVFile} comma separated 030 * text file. <br> 031 * <br> 032 * The first report (type OVERLAPS) shows time conflicts between pairs of classes. Each such enrollment 033 * is given a weight of 1/n, where n is the number of available enrollments of the student into the course. 034 * This 1/n is added to each class that is present in a conflict. These numbers are aggregated on 035 * individual classes and on pairs of classes (that are in a time conflict). 036 * <br> 037 * The second report (type UNAVAILABILITIES) shows for each course how many students could not get into 038 * the course because of the limit constraints. It considers all the not-conflicting, but unavailable enrollments 039 * of a student into the course. For each such an enrollment 1/n is added to each class. So, in a way, the 040 * Availability Conflicts column shows how much space is missing in each class. The Class Potential column 041 * can be handy as well. If the class would be unlimited, this is the number of students (out of all the 042 * conflicting students) that can get into the class. 043 * <br> 044 * The last report (type OVERLAPS_AND_UNAVAILABILITIES) show the two reports together. It is possible that 045 * there is a course where some students cannot get in because of availabilities (all not-conflicting enrollments 046 * have no available space) as well as time conflicts (all available enrollments are conflicting with some other 047 * classes the student has). 048 * <br> 049 * <br> 050 * 051 * Usage: new SectionConflictTable(model, type),createTable(true, true).save(aFile); 052 * 053 * <br> 054 * <br> 055 * 056 * @version StudentSct 1.3 (Student Sectioning)<br> 057 * Copyright (C) 2013 - 2014 Tomáš Müller<br> 058 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br> 059 * <a href="http://muller.unitime.org">http://muller.unitime.org</a><br> 060 * <br> 061 * This library is free software; you can redistribute it and/or modify 062 * it under the terms of the GNU Lesser General Public License as 063 * published by the Free Software Foundation; either version 3 of the 064 * License, or (at your option) any later version. <br> 065 * <br> 066 * This library is distributed in the hope that it will be useful, but 067 * WITHOUT ANY WARRANTY; without even the implied warranty of 068 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 069 * Lesser General Public License for more details. <br> 070 * <br> 071 * You should have received a copy of the GNU Lesser General Public 072 * License along with this library; if not see 073 * <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>. 074 */ 075public class SectionConflictTable implements StudentSectioningReport { 076 private static DecimalFormat sDF1 = new DecimalFormat("0.####"); 077 private static DecimalFormat sDF2 = new DecimalFormat("0.0000"); 078 079 private StudentSectioningModel iModel = null; 080 private Type iType; 081 private boolean iOverlapsAllEnrollments = true; 082 083 /** 084 * Report type 085 */ 086 public static enum Type { 087 /** Time conflicts */ 088 OVERLAPS(true, false), 089 /** Availability conflicts */ 090 UNAVAILABILITIES(false, true), 091 /** Both time and availability conflicts */ 092 OVERLAPS_AND_UNAVAILABILITIES(true, true), 093 ; 094 095 boolean iOveralps, iUnavailabilities; 096 Type(boolean overlaps, boolean unavailabilities) { 097 iOveralps = overlaps; 098 iUnavailabilities = unavailabilities; 099 } 100 101 /** Has time conflicts 102 * @return include time conflicts 103 **/ 104 public boolean hasOverlaps() { return iOveralps; } 105 106 /** Has availability conflicts 107 * @return include unavailabilities 108 **/ 109 public boolean hasUnavailabilities() { return iUnavailabilities; } 110 } 111 112 /** 113 * Constructor 114 * 115 * @param model 116 * student sectioning model 117 * @param type report type 118 */ 119 public SectionConflictTable(StudentSectioningModel model, Type type) { 120 iModel = model; 121 iType = type; 122 } 123 124 public SectionConflictTable(StudentSectioningModel model) { 125 this(model, Type.OVERLAPS_AND_UNAVAILABILITIES); 126 } 127 128 /** Return student sectioning model 129 * @return problem model 130 **/ 131 public StudentSectioningModel getModel() { 132 return iModel; 133 } 134 135 private boolean canIgnore(Assignment<Request, Enrollment> assignment, Enrollment enrollment, Section section, List<Enrollment> other) { 136 e: for (Enrollment e: other) { 137 Section a = null; 138 for (Section s: e.getSections()) { 139 if (s.getSubpart().equals(section.getSubpart())) { 140 if (s.equals(section)) continue e; 141 a = s; 142 } else if (!enrollment.getSections().contains(s)) 143 continue e; 144 } 145 if (a == null) continue e; 146 for (Request r: enrollment.getStudent().getRequests()) { 147 Enrollment curr = assignment.getValue(r); 148 if (!enrollment.getRequest().equals(r) && curr != null && r instanceof CourseRequest && !curr.isAllowOverlap()) 149 for (Section b: curr.getSections()) 150 if (!b.isAllowOverlap() && !b.isToIgnoreStudentConflictsWith(section.getId()) && b.getTime() != null && a.getTime() != null && !a.isAllowOverlap() && b.getTime().hasIntersection(a.getTime())) 151 continue e; 152 } 153 return true; 154 } 155 return false; 156 } 157 158 /** 159 * Create report 160 * 161 * @param assignment current assignment 162 * @param includeLastLikeStudents 163 * true, if last-like students should be included (i.e., 164 * {@link Student#isDummy()} is true) 165 * @param includeRealStudents 166 * true, if real students should be included (i.e., 167 * {@link Student#isDummy()} is false) 168 * @param useAmPm use 12-hour format 169 * @return report as comma separated text file 170 */ 171 public CSVFile createTable(Assignment<Request, Enrollment> assignment, boolean includeLastLikeStudents, boolean includeRealStudents, boolean useAmPm) { 172 HashMap<Course, Map<Section, Double[]>> unavailabilities = new HashMap<Course, Map<Section,Double[]>>(); 173 HashMap<Course, Set<Long>> totals = new HashMap<Course, Set<Long>>(); 174 HashMap<CourseSection, Map<CourseSection, Double>> conflictingPairs = new HashMap<CourseSection, Map<CourseSection,Double>>(); 175 HashMap<CourseSection, Double> sectionOverlaps = new HashMap<CourseSection, Double>(); 176 177 for (Request request : new ArrayList<Request>(getModel().unassignedVariables(assignment))) { 178 if (request.getStudent().isDummy() && !includeLastLikeStudents) continue; 179 if (!request.getStudent().isDummy() && !includeRealStudents) continue; 180 if (request instanceof CourseRequest) { 181 CourseRequest courseRequest = (CourseRequest) request; 182 if (courseRequest.getStudent().isComplete(assignment)) continue; 183 184 List<Enrollment> values = courseRequest.values(assignment); 185 186 SectionLimit limitConstraint = null; 187 for (GlobalConstraint<Request, Enrollment> c: getModel().globalConstraints()) { 188 if (c instanceof SectionLimit) { 189 limitConstraint = (SectionLimit)c; 190 break; 191 } 192 } 193 if (limitConstraint == null) { 194 limitConstraint = new SectionLimit(new DataProperties()); 195 limitConstraint.setModel(getModel()); 196 } 197 List<Enrollment> notAvailableValues = new ArrayList<Enrollment>(values.size()); 198 List<Enrollment> availableValues = new ArrayList<Enrollment>(values.size()); 199 for (Enrollment enrollment : values) { 200 if (limitConstraint.inConflict(assignment, enrollment)) 201 notAvailableValues.add(enrollment); 202 else 203 availableValues.add(enrollment); 204 } 205 206 if (!notAvailableValues.isEmpty() && iType.hasUnavailabilities()) { 207 List<Enrollment> notOverlappingEnrollments = new ArrayList<Enrollment>(values.size()); 208 enrollments: for (Enrollment enrollment: notAvailableValues) { 209 for (Request other : request.getStudent().getRequests()) { 210 if (other.equals(request) || assignment.getValue(other) == null || other instanceof FreeTimeRequest) continue; 211 if (assignment.getValue(other).isOverlapping(enrollment)) continue enrollments; 212 } 213 // not overlapping 214 notOverlappingEnrollments.add(enrollment); 215 } 216 217 if (notOverlappingEnrollments.isEmpty() && availableValues.isEmpty() && iOverlapsAllEnrollments) { 218 double fraction = request.getWeight() / notAvailableValues.size(); 219 Set<CourseSection> ones = new HashSet<CourseSection>(); 220 for (Enrollment enrollment: notAvailableValues) { 221 boolean hasConflict = false; 222 for (Section s: enrollment.getSections()) { 223 if (s.getLimit() >= 0 && s.getEnrollmentWeight(assignment, request) + request.getWeight() > s.getLimit()) { 224 hasConflict = true; 225 break; 226 } 227 } 228 229 Map<Section, Double[]> sections = unavailabilities.get(enrollment.getCourse()); 230 if (sections == null) { 231 sections = new HashMap<Section, Double[]>(); 232 unavailabilities.put(enrollment.getCourse(), sections); 233 } 234 for (Section s: enrollment.getSections()) { 235 if (hasConflict && s.getLimit() < 0 || s.getEnrollmentWeight(assignment, request) + request.getWeight() <= s.getLimit()) continue; 236 Double[] total = sections.get(s); 237 sections.put(s, new Double[] { 238 fraction + (total == null ? 0.0 : total[0].doubleValue()), 239 (total == null ? 0.0 : total[1].doubleValue()) 240 }); 241 ones.add(new CourseSection(enrollment.getCourse(), s)); 242 } 243 Set<Long> total = totals.get(enrollment.getCourse()); 244 if (total == null) { 245 total = new HashSet<Long>(); 246 totals.put(enrollment.getCourse(), total); 247 } 248 total.add(enrollment.getStudent().getId()); 249 } 250 } else if (!notOverlappingEnrollments.isEmpty()) { 251 double fraction = request.getWeight() / notOverlappingEnrollments.size(); 252 Set<CourseSection> ones = new HashSet<CourseSection>(); 253 for (Enrollment enrollment: notOverlappingEnrollments) { 254 boolean hasConflict = false; 255 for (Section s: enrollment.getSections()) { 256 if (s.getLimit() >= 0 && s.getEnrollmentWeight(assignment, request) + request.getWeight() > s.getLimit()) { 257 hasConflict = true; 258 break; 259 } 260 } 261 262 Map<Section, Double[]> sections = unavailabilities.get(enrollment.getCourse()); 263 if (sections == null) { 264 sections = new HashMap<Section, Double[]>(); 265 unavailabilities.put(enrollment.getCourse(), sections); 266 } 267 for (Section s: enrollment.getSections()) { 268 if (hasConflict && s.getLimit() < 0 || s.getEnrollmentWeight(assignment, request) + request.getWeight() <= s.getLimit()) continue; 269 Double[] total = sections.get(s); 270 sections.put(s, new Double[] { 271 fraction + (total == null ? 0.0 : total[0].doubleValue()), 272 (total == null ? 0.0 : total[1].doubleValue()) 273 }); 274 ones.add(new CourseSection(enrollment.getCourse(), s)); 275 } 276 Set<Long> total = totals.get(enrollment.getCourse()); 277 if (total == null) { 278 total = new HashSet<Long>(); 279 totals.put(enrollment.getCourse(), total); 280 } 281 total.add(enrollment.getStudent().getId()); 282 } 283 for (CourseSection section: ones) { 284 Map<Section, Double[]> sections = unavailabilities.get(section.getCourse()); 285 Double[] total = sections.get(section.getSection()); 286 sections.put(section.getSection(), new Double[] { 287 (total == null ? 0.0 : total[0].doubleValue()), 288 request.getWeight() + (total == null ? 0.0 : total[1].doubleValue()) 289 }); 290 } 291 } 292 } 293 294 if (iOverlapsAllEnrollments) 295 availableValues = values; 296 if (!availableValues.isEmpty() && iType.hasOverlaps()) { 297 List<Map<CourseSection, List<CourseSection>>> conflicts = new ArrayList<Map<CourseSection, List<CourseSection>>>(); 298 for (Enrollment enrollment: availableValues) { 299 Map<CourseSection, List<CourseSection>> overlaps = new HashMap<CourseSection, List<CourseSection>>(); 300 for (Request other : request.getStudent().getRequests()) { 301 Enrollment otherEnrollment = assignment.getValue(other); 302 if (other.equals(request) || otherEnrollment == null || other instanceof FreeTimeRequest) continue; 303 if (enrollment.isOverlapping(otherEnrollment)) 304 for (Section a: enrollment.getSections()) 305 for (Section b: otherEnrollment.getSections()) 306 if (a.getTime() != null && b.getTime() != null && !a.isAllowOverlap() && !b.isAllowOverlap() && !a.isToIgnoreStudentConflictsWith(b.getId()) && a.getTime().hasIntersection(b.getTime()) && !canIgnore(assignment, enrollment, a, availableValues)) { 307 List<CourseSection> x = overlaps.get(new CourseSection(enrollment.getCourse(), a)); 308 if (x == null) { x = new ArrayList<CourseSection>(); overlaps.put(new CourseSection(enrollment.getCourse(), a), x); } 309 x.add(new CourseSection(otherEnrollment.getCourse(), b)); 310 } 311 } 312 if (!overlaps.isEmpty()) { 313 conflicts.add(overlaps); 314 Set<Long> total = totals.get(enrollment.getCourse()); 315 if (total == null) { 316 total = new HashSet<Long>(); 317 totals.put(enrollment.getCourse(), total); 318 } 319 total.add(enrollment.getStudent().getId()); 320 } 321 } 322 323 double fraction = request.getWeight() / conflicts.size(); 324 for (Map<CourseSection, List<CourseSection>> overlaps: conflicts) { 325 for (Map.Entry<CourseSection, List<CourseSection>> entry: overlaps.entrySet()) { 326 CourseSection a = entry.getKey(); 327 Double total = sectionOverlaps.get(a); 328 sectionOverlaps.put(a, fraction + (total == null ? 0.0 : total.doubleValue())); 329 Map<CourseSection, Double> pair = conflictingPairs.get(a); 330 if (pair == null) { 331 pair = new HashMap<CourseSection, Double>(); 332 conflictingPairs.put(a, pair); 333 } 334 for (CourseSection b: entry.getValue()) { 335 Double prev = pair.get(b); 336 pair.put(b, fraction + (prev == null ? 0.0 : prev.doubleValue())); 337 } 338 } 339 } 340 } 341 } 342 } 343 Comparator<Course> courseComparator = new Comparator<Course>() { 344 @Override 345 public int compare(Course a, Course b) { 346 int cmp = a.getName().compareTo(b.getName()); 347 if (cmp != 0) return cmp; 348 return a.getId() < b.getId() ? -1 : a.getId() == b.getId() ? 0 : 1; 349 } 350 }; 351 Comparator<Section> sectionComparator = new Comparator<Section>() { 352 @Override 353 public int compare(Section a, Section b) { 354 int cmp = a.getSubpart().getConfig().getOffering().getName().compareTo(b.getSubpart().getConfig().getOffering().getName()); 355 if (cmp != 0) return cmp; 356 cmp = a.getSubpart().getInstructionalType().compareTo(b.getSubpart().getInstructionalType()); 357 // if (cmp != 0) return cmp; 358 // cmp = a.getName().compareTo(b.getName()); 359 if (cmp != 0) return cmp; 360 return a.getId() < b.getId() ? -1 : a.getId() == b.getId() ? 0 : 1; 361 } 362 }; 363 364 CSVFile csv = new CSVFile(); 365 List<CSVFile.CSVField> headers = new ArrayList<CSVFile.CSVField>(); 366 headers.add(new CSVFile.CSVField("Course")); 367 headers.add(new CSVFile.CSVField("Total\nConflicts")); 368 if (iType.hasUnavailabilities()) { 369 headers.add(new CSVFile.CSVField("Course\nEnrollment")); 370 headers.add(new CSVFile.CSVField("Course\nLimit")); 371 } 372 headers.add(new CSVFile.CSVField("Class")); 373 headers.add(new CSVFile.CSVField("Meeting Time")); 374 if (iType.hasUnavailabilities()) { 375 headers.add(new CSVFile.CSVField("Availability\nConflicts")); 376 headers.add(new CSVFile.CSVField("% of Total\nConflicts")); 377 } 378 if (iType.hasOverlaps()) { 379 headers.add(new CSVFile.CSVField("Time\nConflicts")); 380 headers.add(new CSVFile.CSVField("% of Total\nConflicts")); 381 } 382 if (iType.hasUnavailabilities()) { 383 headers.add(new CSVFile.CSVField("Class\nEnrollment")); 384 headers.add(new CSVFile.CSVField("Class\nLimit")); 385 if (!iType.hasOverlaps()) 386 headers.add(new CSVFile.CSVField("Class\nPotential")); 387 } 388 if (iType.hasOverlaps()) { 389 headers.add(new CSVFile.CSVField("Conflicting\nClass")); 390 headers.add(new CSVFile.CSVField("Conflicting\nMeeting Time")); 391 headers.add(new CSVFile.CSVField("Joined\nConflicts")); 392 headers.add(new CSVFile.CSVField("% of Total\nConflicts")); 393 } 394 csv.setHeader(headers); 395 396 TreeSet<Course> courses = new TreeSet<Course>(courseComparator); 397 courses.addAll(totals.keySet()); 398 399 for (Course course: courses) { 400 Map<Section, Double[]> sectionUnavailability = unavailabilities.get(course); 401 Set<Long> total = totals.get(course); 402 403 TreeSet<Section> sections = new TreeSet<Section>(sectionComparator); 404 if (sectionUnavailability != null) 405 sections.addAll(sectionUnavailability.keySet()); 406 for (Map.Entry<CourseSection, Double> entry: sectionOverlaps.entrySet()) 407 if (course.equals(entry.getKey().getCourse())) 408 sections.add(entry.getKey().getSection()); 409 410 boolean firstCourse = true; 411 for (Section section: sections) { 412 Double[] sectionUnavailable = (sectionUnavailability == null ? null : sectionUnavailability.get(section)); 413 Double sectionOverlap = sectionOverlaps.get(new CourseSection(course, section)); 414 Map<CourseSection, Double> pair = conflictingPairs.get(new CourseSection(course, section)); 415 416 if (pair == null) { 417 List<CSVFile.CSVField> line = new ArrayList<CSVFile.CSVField>(); 418 line.add(new CSVFile.CSVField(firstCourse ? course.getName() : "")); 419 line.add(new CSVFile.CSVField(firstCourse ? total.size() : "")); 420 if (iType.hasUnavailabilities()) { 421 line.add(new CSVFile.CSVField(firstCourse ? sDF1.format(course.getEnrollmentWeight(assignment, null)) : "")); 422 line.add(new CSVFile.CSVField(firstCourse ? course.getLimit() < 0 ? "" : String.valueOf(course.getLimit()) : "")); 423 } 424 425 line.add(new CSVFile.CSVField(section.getSubpart().getName() + " " + section.getName(course.getId()))); 426 line.add(new CSVFile.CSVField(section.getTime() == null ? "" : section.getTime().getDayHeader() + " " + section.getTime().getStartTimeHeader(useAmPm) + " - " + section.getTime().getEndTimeHeader(useAmPm))); 427 428 if (iType.hasUnavailabilities()) { 429 line.add(new CSVFile.CSVField(sectionUnavailable != null ? sDF2.format(sectionUnavailable[0]) : "")); 430 line.add(new CSVFile.CSVField(sectionUnavailable != null ? sDF2.format(sectionUnavailable[0] / total.size()) : "")); 431 } 432 if (iType.hasOverlaps()) { 433 line.add(new CSVFile.CSVField(sectionOverlap != null ? sDF2.format(sectionOverlap) : "")); 434 line.add(new CSVFile.CSVField(sectionOverlap != null ? sDF2.format(sectionOverlap / total.size()) : "")); 435 } 436 if (iType.hasUnavailabilities()) { 437 line.add(new CSVFile.CSVField(sDF1.format(section.getEnrollmentWeight(assignment, null)))); 438 line.add(new CSVFile.CSVField(section.getLimit() < 0 ? "" : String.valueOf(section.getLimit()))); 439 if (!iType.hasOverlaps()) 440 line.add(new CSVFile.CSVField(sectionUnavailable != null ? sDF1.format(sectionUnavailable[1]) : "")); 441 } 442 443 csv.addLine(line); 444 } else { 445 boolean firstClass = true; 446 for (CourseSection other: new TreeSet<CourseSection>(pair.keySet())) { 447 List<CSVFile.CSVField> line = new ArrayList<CSVFile.CSVField>(); 448 line.add(new CSVFile.CSVField(firstCourse && firstClass ? course.getName() : "")); 449 line.add(new CSVFile.CSVField(firstCourse && firstClass ? total.size() : "")); 450 if (iType.hasUnavailabilities()) { 451 line.add(new CSVFile.CSVField(firstCourse && firstClass ? sDF1.format(course.getEnrollmentWeight(assignment, null)) : "")); 452 line.add(new CSVFile.CSVField(firstCourse && firstClass ? course.getLimit() < 0 ? "" : String.valueOf(course.getLimit()) : "")); 453 } 454 455 line.add(new CSVFile.CSVField(firstClass ? section.getSubpart().getName() + " " + section.getName(course.getId()): "")); 456 line.add(new CSVFile.CSVField(firstClass ? section.getTime() == null ? "" : section.getTime().getDayHeader() + " " + section.getTime().getStartTimeHeader(useAmPm) + " - " + section.getTime().getEndTimeHeader(useAmPm): "")); 457 458 if (iType.hasUnavailabilities()) { 459 line.add(new CSVFile.CSVField(firstClass && sectionUnavailable != null ? sDF2.format(sectionUnavailable[0]): "")); 460 line.add(new CSVFile.CSVField(sectionUnavailable != null ? sDF2.format(sectionUnavailable[0] / total.size()) : "")); 461 } 462 line.add(new CSVFile.CSVField(firstClass && sectionOverlap != null ? sDF2.format(sectionOverlap): "")); 463 line.add(new CSVFile.CSVField(firstClass && sectionOverlap != null ? sDF2.format(sectionOverlap / total.size()) : "")); 464 if (iType.hasUnavailabilities()) { 465 line.add(new CSVFile.CSVField(firstClass ? sDF1.format(section.getEnrollmentWeight(assignment, null)): "")); 466 line.add(new CSVFile.CSVField(firstClass ? section.getLimit() < 0 ? "" : String.valueOf(section.getLimit()): "")); 467 } 468 469 line.add(new CSVFile.CSVField(other.getCourse().getName() + " " + other.getSection().getSubpart().getName() + " " + other.getSection().getName(other.getCourse().getId()))); 470 line.add(new CSVFile.CSVField(other.getSection().getTime().getDayHeader() + " " + other.getSection().getTime().getStartTimeHeader(useAmPm) + " - " + other.getSection().getTime().getEndTimeHeader(useAmPm))); 471 line.add(new CSVFile.CSVField(sDF2.format(pair.get(other)))); 472 line.add(new CSVFile.CSVField(sDF2.format(pair.get(other) / total.size()))); 473 474 csv.addLine(line); 475 firstClass = false; 476 } 477 } 478 479 firstCourse = false; 480 } 481 482 csv.addLine(); 483 } 484 return csv; 485 } 486 487 @Override 488 public CSVFile create(Assignment<Request, Enrollment> assignment, DataProperties properties) { 489 iType = Type.valueOf(properties.getProperty("type", iType.name())); 490 iOverlapsAllEnrollments = properties.getPropertyBoolean("overlapsIncludeAll", true); 491 return createTable(assignment, properties.getPropertyBoolean("lastlike", false), properties.getPropertyBoolean("real", true), properties.getPropertyBoolean("useAmPm", true)); 492 } 493}