package hirondelle.predict.main.prediction;
import static hirondelle.web4j.util.Consts.FAILS;
import hirondelle.predict.main.codes.CodeTable;
import hirondelle.web4j.model.Check;
import hirondelle.web4j.model.Code;
import hirondelle.web4j.model.DateTime;
import hirondelle.web4j.model.Id;
import hirondelle.web4j.model.ModelCtorException;
import hirondelle.web4j.model.ModelUtil;
import hirondelle.web4j.security.SafeText;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
/**
Model Object for a Prediction.
<P>This class is interesting since it does not include the foreign key to the PredictionList table;
that is taken only from the session, not from the database, nor from user input.
<P>This object is immutable, and makes defensive copies where needed.
*/
public final class Prediction {
/**
Full constructor. Used when fetching from the database.
@param aId internal database identifier (optional)
@param aParentId internal database identifier (optional)
@param aText main text of the prediction max 255 characters (required)
@param aCreationDate date-time the prediction was created (required)
@param aRemark any remark the user may wish to make (justification, special remark on the outcome, etc.) max 2000 chars (optional)
@param aOutcome final outcome of the prediction (optional)
@param aOutcomeDate Date the final outcome was established, if ever (optional); if outcome is specified, then the
outcome date must also be specified
*/
public Prediction(
Id aId, Id aParentId, SafeText aText, DateTime aCreationDate,
SafeText aRemark, Id aOutcome, DateTime aOutcomeDate
) throws ModelCtorException {
fId = aId;
fParentId = aParentId;
fText = aText;
fCreationDate = aCreationDate;
fRemark = aRemark;
fOutcome = CodeTable.codeFor(aOutcome, CodeTable.OUTCOMES);
fOutcomeDate = aOutcomeDate;
validateState(CheckDates.YES);
}
/**
Partial constructor, used for user input.
<P>Similar to the full constructor, but the user never inputs the creation date, or the outcome date.
Here, those dates are simply set to <tt>null</tt>.
*/
public Prediction(
Id aId, Id aParentId, SafeText aText, SafeText aRemark, Id aOutcome
) throws ModelCtorException {
fId = aId;
fParentId = aParentId;
fText = aText;
fRemark = aRemark;
fOutcome = CodeTable.codeFor(aOutcome, CodeTable.OUTCOMES);
fCreationDate = null;
fOutcomeDate = null;
validateState(CheckDates.NO);
}
public Id getId() { return fId; }
public Id getParentId() { return fParentId; }
public SafeText getText() { return fText; }
public DateTime getCreationDate() { return fCreationDate; }
public SafeText getRemark() { return fRemark; }
public Code getOutcome() { return fOutcome; }
/**
Returns <tt>null</tt> if there is no outcome.
Otherwise, returns the score attached to the outcome (see {@link CodeTable#OUTCOMES}).
*/
public Integer getOutcomeScore() {
Integer result = null;
if (fOutcome != null) {
//Undecidable items will have the short text as null
SafeText value = fOutcome.getShortText();
if( value != null ) {
result = Integer.valueOf(value.getRawString());
}
}
return result;
}
public DateTime getOutcomeDate() {
return fOutcomeDate;
}
/**
Calculate the average of {@link #getOutcomeScore()} for all of the predictions in a list.
<P>Predictions which have a <tt>null</tt> outcome do not contribute to the result.
If <i>none</i> of the predictions in the list have an outcome, then <tt>null</tt> is returned.
Integer division is used to calculate the result, so there will be some rounding.
*/
public static Integer calculateAverageScore(List<Prediction> aPredictions){
Integer result = null;
int sum = 0;
int numScores = 0;
for(Prediction prediction : aPredictions){
if(prediction.getOutcomeScore() != null){
++numScores;
sum = sum + prediction.getOutcomeScore();
}
}
if ( numScores > 0 ) {
result = sum / numScores ; //integer division is close enough
}
return result;
}
@Override public String toString(){
return ModelUtil.toStringFor(this);
}
@Override public boolean equals(Object aThat){
Boolean result = ModelUtil.quickEquals(this, aThat);
if ( result == null ) {
Prediction that = (Prediction)aThat;
result = ModelUtil.equalsFor(this.getSignificantFields(), that.getSignificantFields());
}
return result;
}
@Override public int hashCode(){
if ( fHashCode == 0 ) {
fHashCode = ModelUtil.hashCodeFor(getSignificantFields());
}
return fHashCode;
}
// PRIVATE
private final Id fId;
private final Id fParentId;
private final SafeText fText;
private final DateTime fCreationDate;
private final SafeText fRemark;
private final Code fOutcome;
private final DateTime fOutcomeDate;
private int fHashCode;
private enum CheckDates{YES, NO};
private void validateState(CheckDates aCheckDates) throws ModelCtorException {
ModelCtorException ex = new ModelCtorException();
if ( FAILS == Check.required(fParentId) ) {
ex.add("Parent Id is required. Programmer error.");
}
if ( FAILS == Check.required(fText, Check.range(1, 255)) ) {
ex.add("You must input text for your prediction (maximum 255 characters)");
}
if ( FAILS == Check.optional(fRemark, Check.range(1, 2000)) ) {
ex.add("Remark must have visible text, maximum 2,000 chars.");
}
if(CheckDates.YES == aCheckDates){
if ( FAILS == Check.required(fCreationDate) ) {
ex.add("Creation Date is missing (programmer error)");
}
if( fOutcome != null && fOutcomeDate == null) {
ex.add("Outcome and Outcome Date must appear together.");
}
if( fOutcome == null && fOutcomeDate != null) {
ex.add("Outcome and Outcome Date must appear together.");
}
}
if ( ! ex.isEmpty() ) throw ex;
}
private Object[] getSignificantFields(){
return new Object[] {fText, fCreationDate, fRemark, fOutcome, fOutcomeDate};
}
}