/*
* Copyright (C) 2012 Jimmy Theis. Licensed under the MIT License:
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.jetheis.android.grades.model;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import android.os.Parcel;
import android.os.Parcelable;
import com.jetheis.android.grades.storage.CourseStorageAdapter;
import com.jetheis.android.grades.storage.CourseStorageAdapter.CourseStorageIterator;
import com.jetheis.android.grades.storage.GradeComponentStorageAdapter;
import com.jetheis.android.grades.storage.Storable;
/**
* Representation of a single academic course. Each course has a name and a
* {@link CourseType} (given by {@link #getCourseType()}), which represents how
* the courses final grade is calculated: either by adding points for a total
* score ({@link CourseType#POINT_TOTAL}), or by weighting categories'
* percentages ({@link CourseType#PERCENTAGE_WEIGHTING}) together.
*/
public class Course extends Storable implements Comparable<Course>, Parcelable {
/**
* A simple representation of a course type.
*
*/
public enum CourseType {
/**
* The course calculates its final grade by summing the points of its
* included grade components
*/
POINT_TOTAL(1),
/**
* The course calculates its final grade by applying a specific weight
* to the percentage grade of each of its included grade components.
*/
PERCENTAGE_WEIGHTING(2);
private final int mIntIdentifier;;
/**
* Constructor for {@link CourseType}.
*
* @param intIdenfifier
* The integer that will be used to uniquely identify the
* {@link CourseType} when it is stored as a database value.
*/
CourseType(int intIdenfifier) {
mIntIdentifier = intIdenfifier;
}
/**
* Convert this {@link CourseType} to an integer for storage in a
* database row.
*
* @return This {@link CourseType}'s unique integer identifier.
*/
public int toInt() {
return mIntIdentifier;
}
/**
* Convert an integer identifier from a database row back into a proper
* {@link CourseType}.
*
* @param intIdentifier
* The integer identifier from a database row.
* @return The {@link CourseType} corresponding to the given integer
* identifier.
*/
public static CourseType fromInt(int intIdentifier) {
switch (intIdentifier) {
case 1:
return POINT_TOTAL;
case 2:
return PERCENTAGE_WEIGHTING;
}
return null;
}
}
private String mName;
private CourseType mCourseType;
private double mOverallScore;
private double mTotalPossibleScore;
private Collection<GradeComponent> mGradeComponents;
/**
* Default constructor (has no side effects)
*/
public Course() {
}
/**
* Get the human readable name of the course.
*
* @return The human readable name of the course.
*/
public String getName() {
return mName;
}
/**
* Set the human readable name of the course.
*
* @param name
* The new human readable name of the course.
*/
public void setName(String name) {
mName = name;
}
/**
* Get the "type" ({@link CourseType}) of the course.
*
* @return The {@link CourseType} that designates how this course calculates
* its final score.
*/
public CourseType getCourseType() {
return mCourseType;
}
/**
* Set the "type" ({@link CourseType}) of the course. If this setting is a
* change from this {@link Course}'s current type, all
* {@link GradeComponent}s will be destroyed as a side effect.
*
* @param courseType
* The new {@link CourseType} of this course.
*/
public void setCourseType(CourseType courseType) {
if (mCourseType != courseType) {
initializeGradeComponents();
for (GradeComponent gradeComponent : mGradeComponents) {
gradeComponent.destroy();
}
}
mCourseType = courseType;
}
/**
* Calculate the total score for this course by combining scores for all
* contained {@link GradeComponent}s.
*
* @return The overall score for this course, as a double less than or equal
* to {@code 1.0}.
*/
public double getOverallScore() {
if (getGradeComponents().size() == 0) {
loadConnectedObjects();
}
calculateOverallScore();
if (Double.isNaN(mOverallScore)) {
return 0;
}
return mOverallScore;
}
/**
* Get the total possible score for this course, based on the
* {@link CourseType} of this course (given by {@link #getCourseType()}). If
* this course is of type {@link CourseType#POINT_TOTAL}, this value will be
* the total available points. If this course of is type
* {@link CourseType#PERCENTAGE_WEIGHTING} , this value will be the total
* sum of all the weights of the contained {@link PercentageGradeComponent}
* s. In this case, the expected value will be {@code 1.0}.
*
* @return The total possible score for this course, based on the
* {@link CourseType} of this course (given by
* {@link #getCourseType()}).
*/
public double getTotalPossibleScore() {
calculateOverallScore();
return mTotalPossibleScore;
}
/**
* Calculate the overall and total score values, saving them off in the
* private member variables {@link #mOverallScore} and
* {@link #mTotalPossibleScore}.
*/
private void calculateOverallScore() {
mTotalPossibleScore = 0;
if (getCourseType() == CourseType.POINT_TOTAL) {
// Sum total points
double totalPoints = 0;
double totalEarned = 0;
for (GradeComponent component : getGradeComponents()) {
PointTotalGradeComponent pointComponent = (PointTotalGradeComponent) component;
totalPoints += pointComponent.getTotalPoints();
totalEarned += pointComponent.getPointsEarned();
}
// Store the point total for reference
mTotalPossibleScore = totalPoints;
mOverallScore = totalEarned / totalPoints;
} else {
// Sum weighted individual scores
double totalScore = 0;
double totalWeight = 0;
for (GradeComponent component : getGradeComponents()) {
PercentageGradeComponent percentageComponent = (PercentageGradeComponent) component;
totalScore += percentageComponent.getEarnedPercentage() / 100.0
* percentageComponent.getWeight() / 100.0;
totalWeight += percentageComponent.getWeight();
}
// Store the weighting total for reference
mTotalPossibleScore = totalWeight;
mOverallScore = totalScore;
}
}
/**
* Get the collection of grade components that this course contains. If this
* course contains no grade components, an empty collection will be
* returned. If this object is persisted in the database, grade components
* will be loaded automatically (if they have not be already) here.
*
* @return The collection of grade components that this course contains.
*/
public List<GradeComponent> getGradeComponents() {
initializeGradeComponents();
// Copy components to a new list so the original can't be modified
ArrayList<GradeComponent> result = new ArrayList<GradeComponent>();
result.addAll(mGradeComponents);
Collections.sort(result, new Comparator<GradeComponent>() {
@Override
public int compare(GradeComponent lhs, GradeComponent rhs) {
return (int) Math.signum(lhs.getId() - rhs.getId());
}
});
return result;
}
/**
* Add a grade component to this course. This method also takes care of
* calling {@link GradeComponent#setCourse(Course)} on the added component,
* thus establishing a proper back link between the objects. This method
* also prevents duplicate {@link GradeComponent}s from being added to the
* course, simply by ignoring components that have already been added.
*
* @param gradeComponent
* The grade component to add to this course.
*/
public void addGradeComponent(GradeComponent gradeComponent) throws IllegalArgumentException {
initializeGradeComponents();
validateGradeComponent(gradeComponent);
if (!mGradeComponents.contains(gradeComponent)) {
mGradeComponents.add(gradeComponent);
gradeComponent.setCourse(this);
}
}
/**
* Add several grade components to this course. This method also takes care
* of calling {@link GradeComponent#setCourse(Course)} on the added
* components, thus establishing a proper back link between the objects.
* This method also prevents duplicate {@link GradeComponent}s from being
* added to the course, simply by ignoring components that have already been
* added.
*
* @param gradeComponents
* The grade components to add to this course.
*/
public void addGradeComponents(Collection<GradeComponent> gradeComponents)
throws IllegalArgumentException {
initializeGradeComponents();
for (GradeComponent gradeComponent : gradeComponents) {
validateGradeComponent(gradeComponent);
if (!mGradeComponents.contains(gradeComponent)) {
mGradeComponents.add(gradeComponent);
gradeComponent.setCourse(this);
}
}
}
/**
* Validate a to-be-added {@link GradeComponent}, making sure its type
* corresponds with this {@link Course}'s {@link CourseType}. If there is a
* mismatch, a {@link IllegalArgumentException} is thrown.
*
* @param gradeComponent
* The {@link GradeComponent} to be added.
* @throws IllegalArgumentException
* When the given {@link GradeComponent}'s type does not
* correspond to this {@link Course}'s {@link CourseType}.
*/
private void validateGradeComponent(GradeComponent gradeComponent)
throws IllegalArgumentException {
if (gradeComponent instanceof PercentageGradeComponent
&& getCourseType() == CourseType.POINT_TOTAL
|| gradeComponent instanceof PointTotalGradeComponent
&& getCourseType() == CourseType.PERCENTAGE_WEIGHTING) {
throw new IllegalArgumentException("Grading type argument mismatch");
}
}
/**
* Remove a {@link GradeComponent} from this {@link Course}. If the
* component is not a member of this course, nothing is done.
*
* @param gradeComponent
* The grade component to remove from this course.
*/
public void removeGradeComponent(GradeComponent gradeComponent) {
initializeGradeComponents();
if (mGradeComponents.contains(gradeComponent)) {
mGradeComponents.remove(gradeComponent);
}
}
/**
* Remove several grade components from this course.
*
* @param gradeComponents
* The grade components to remove from this course.
*/
public void removeGradeComponents(Collection<GradeComponent> gradeComponents) {
initializeGradeComponents();
for (GradeComponent gradeComponent : gradeComponents) {
if (mGradeComponents.contains(gradeComponent)) {
mGradeComponents.remove(gradeComponent);
}
}
}
/**
* Clear all {@link GradeComponent}s from this {@link Course}.
*/
public void clearGradeComponents() {
removeGradeComponents(getGradeComponents());
}
/**
* A helper method to load connected {@link GradeComponent}s or instantiate
* a new empty {@link Collection} in the {@link #mGradeComponents} member
* variable.
*/
private void initializeGradeComponents() {
if (mGradeComponents == null) {
mGradeComponents = new HashSet<GradeComponent>();
}
}
/**
* Get all database-tracked {@link Course}s.
*
* @return A {@link List} of all database-tracked {@link Course}s, sorted
* alphabetically by name.
*/
public static List<Course> getAllCourses() {
CourseStorageIterator allCourses = new CourseStorageAdapter().getAllCourses();
ArrayList<Course> result = new ArrayList<Course>(allCourses.getCount());
for (Course course : allCourses) {
result.add(course);
}
return result;
}
/**
* Retrieve a specific {@link Course} from the database, using its unique
* identifier.
*
* @param id
* The unique identifier of the {@link Course} to be retrieved.
* @return The {@link Course} if found, null otherwise.
*/
public static Course getCourseById(long id) {
return new CourseStorageAdapter().getCourseById(id);
}
/**
* Delete all {@link Course} entries from the database
*
* @return The number of database records affected.
*/
public static int destroyAllCourses() {
return new CourseStorageAdapter().deleteAllCourses();
}
@Override
public void save() {
new CourseStorageAdapter().saveCourse(this);
if (mGradeComponents != null) {
for (GradeComponent component : mGradeComponents) {
component.save();
}
}
}
@Override
public void destroy() {
new CourseStorageAdapter().deleteCourse(this);
if (mGradeComponents != null) {
for (GradeComponent component : mGradeComponents) {
component.destroy();
}
mGradeComponents.clear();
}
}
@Override
public void loadConnectedObjects() {
clearGradeComponents();
if (getCourseType() == CourseType.POINT_TOTAL) {
for (PointTotalGradeComponent gradeComponent : new GradeComponentStorageAdapter()
.getPointTotalGradeComponentsByCourse(this)) {
addGradeComponent(gradeComponent);
}
} else {
for (PercentageGradeComponent gradeComponent : new GradeComponentStorageAdapter()
.getPercentageGradeComponentsByCourse(this)) {
addGradeComponent(gradeComponent);
}
}
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Course))
return false;
return compareTo((Course) o) == 0;
}
@Override
public int compareTo(Course other) {
if (getId() > 0 && other.getId() > 0) {
return (int) Math.signum(getId() - other.getId());
}
if (!getName().equals(other.getName())) {
return getName().compareTo(other.getName());
}
return (int) Math.signum(getCourseType().toInt() - other.getCourseType().toInt());
}
@Override
public String toString() {
return "Course " + getName();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(getId());
dest.writeString(getName());
dest.writeInt(getCourseType().toInt());
}
public Course(Parcel parcel) {
setId(parcel.readLong());
setName(parcel.readString());
setCourseType(CourseType.fromInt(parcel.readInt()));
}
public static final Parcelable.Creator<Course> CREATOR = new Parcelable.Creator<Course>() {
@Override
public Course createFromParcel(Parcel source) {
return new Course(source);
}
@Override
public Course[] newArray(int size) {
return new Course[size];
}
};
}