// Copyright 2015 The Project Buendia Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy
// of the License at: http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distrib-
// uted under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
// OR CONDITIONS OF ANY KIND, either express or implied. See the License for
// specific language governing permissions and limitations under the License.
package org.projectbuendia.client.models;
import android.support.annotation.NonNull;
import com.google.common.collect.ImmutableMap;
import org.joda.time.DateTime;
import org.projectbuendia.client.json.ConceptType;
import org.projectbuendia.client.utils.Utils;
import java.util.Map;
import java.util.Objects;
import javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkNotNull;
/** A simple bean class representing an observation with localized names and values. */
// TODO: Make ObsPoint a member of Obs; change the structure of Obs to be simply:
// { final @Nonnull String uuid; String name; final @Nonnull ObsPoint point; } then delete
// getObsPoint(), getObsValue(), compareTo(), getTypeOrdering(), getCodedValueOrdering().
public final class Obs implements Comparable<Obs> {
/** The time at which this observation was taken. */
public final DateTime time;
/** The UUID of the concept that was observed. */
public final String conceptUuid;
/** The data type of the concept that was observed. */
public final ConceptType conceptType;
/** The observed value (a string, number as a string, or answer concept UUID). */
public final @Nullable String value;
/** The name of the answer concept, if the value is an answer concept. */
public final @Nullable String valueName;
public Obs(
long millis,
String conceptUuid,
ConceptType conceptType,
@Nullable String value,
@Nullable String valueName) {
this.time = new DateTime(millis);
this.conceptUuid = checkNotNull(conceptUuid);
this.conceptType = conceptType;
this.value = value;
this.valueName = valueName;
}
/** Returns the time and value of this observation as an ObsPoint. */
public @Nullable ObsPoint getObsPoint() {
ObsValue ov = getObsValue();
return ov == null ? null : new ObsPoint(time, getObsValue());
}
/** Returns the value of this observation as an ObsValue. */
public @Nullable ObsValue getObsValue() {
if (value == null || conceptType == null) return null;
switch (conceptType) {
case CODED:
return ObsValue.newCoded(value, valueName);
case NUMERIC:
return ObsValue.newNumber(Double.valueOf(value));
case TEXT:
return ObsValue.newText(value);
case BOOLEAN:
return ObsValue.newCoded(ConceptUuids.YES_UUID.equals(value));
case DATE:
return ObsValue.newDate(Utils.toLocalDate(value));
case DATETIME:
return ObsValue.newTime(Long.valueOf(value));
}
return null;
}
@Override public String toString() {
return "Obs(time=" + time
+ ", conceptUuid=" + conceptUuid
+ ", conceptType=" + conceptType
+ ", value=" + value
+ ", valueName=" + valueName + ")";
}
@Override public boolean equals(Object other) {
if (other instanceof Obs) {
Obs o = (Obs) other;
return Objects.equals(time, o.time)
&& Objects.equals(conceptUuid, o.conceptUuid)
&& Objects.equals(conceptType, o.conceptType)
&& Objects.equals(value, o.value)
&& Objects.equals(valueName, o.valueName);
} else {
return false;
}
}
@Override public int hashCode() {
return (int) time.getMillis() + conceptUuid.hashCode()
+ conceptType.hashCode() + (value == null ? 0 : value.hashCode())
+ (valueName == null ? 0 : valueName.hashCode());
}
/**
* Compares value instances according to a total ordering such that:
* - The empty value (present == false) is ordered before all others.
* - The Boolean value false is ordered before all other values and types.
* - Numeric values are ordered from least to greatest magnitude.
* - Text values are ordered lexicographically from A to Z.
* - Coded values are ordered from least severe to most severe (if they can
* be interpreted as having a severity); or from first to last (if they can
* be interpreted as having a typical temporal sequence).
* - The Boolean value true is ordered after all other values and types.
* @param other The other Value to compare to.
* @return
*/
@Override public int compareTo(@NonNull Obs other) {
if (value == null || other.value == null) {
return value == other.value ? 0 : value == null ? -1 : 1;
}
if (conceptType != other.conceptType) {
return getTypeOrdering().compareTo(other.getTypeOrdering());
}
if (conceptType == ConceptType.NUMERIC) {
return Double.valueOf(value).compareTo(Double.valueOf(other.value));
}
if (conceptType == ConceptType.CODED || conceptType == ConceptType.BOOLEAN) {
return getCodedValueOrdering().compareTo(other.getCodedValueOrdering());
}
return value.compareTo(other.value);
}
/** Gets a number specifying the ordering of Values of different types. */
public Integer getTypeOrdering() {
switch (conceptType) {
case BOOLEAN:
return ConceptUuids.YES_UUID.equals(value) ? 5 : 1;
case NUMERIC:
return 2;
case TEXT:
return 3;
case CODED:
return 4;
}
return 0;
}
/**
* Gets a number specifying the ordering of coded values. These are
* arranged from least to most severe so that using the Pebble "max" filter
* will select the most severe value from a list of values.
*/
public Integer getCodedValueOrdering() {
final Map<String, Integer> CODED_VALUE_ORDERING = new ImmutableMap.Builder<String, Integer>()
.put(ConceptUuids.NO_UUID, 0)
.put(ConceptUuids.NONE_UUID, 1)
.put(ConceptUuids.NORMAL_UUID, 2)
.put(ConceptUuids.SOLID_FOOD_UUID, 3)
.put(ConceptUuids.MILD_UUID, 4)
.put(ConceptUuids.MODERATE_UUID, 5)
.put(ConceptUuids.SEVERE_UUID, 6)
.put(ConceptUuids.YES_UUID, 7).build();
Integer cvo = CODED_VALUE_ORDERING.get(value);
return cvo == null ? 0 : cvo;
}
}