/*
* Copyright (c) 2017 OBiBa. All rights reserved.
*
* This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.obiba.magma.js.methods;
import java.text.ParsePosition;
import java.util.ResourceBundle;
import javax.annotation.Nullable;
import org.jscience.physics.unit.PhysicsUnit;
import org.jscience.physics.unit.format.SymbolMap;
import org.jscience.physics.unit.format.UCUMFormat;
import org.jscience.physics.unit.system.SI;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.Scriptable;
import org.obiba.magma.js.MagmaJsEvaluationRuntimeException;
import org.obiba.magma.js.ScriptableValue;
import org.obiba.magma.type.DecimalType;
import org.obiba.magma.type.TextType;
import org.unitsofmeasurement.unit.Unit;
import com.google.common.base.Strings;
public class UnitMethods {
/**
* UCUM does not have a default for overloaded units (international inches vs. us inches vs. british inches). This
* UCUMFormat instance maps some common symbols to the international standard or the US standard when no international
* standard exists. Mass units use the 'avoirdupoids' definitions.
* <p/>
* Only three units overlap with SI notation: ft, pt (pint) and yd. This results in masking the "femto-ton",
* "pico-ton" and the "yotta-day", so it should not have any impact as these are not common units and can be expressed
* differently.
*/
private static final UCUMFormat DEFAULTS = UCUMFormat
.getCaseSensitiveInstance(new SymbolMap(ResourceBundle.getBundle(UnitMethods.class.getName() + "_CS")));
private UnitMethods() {}
/**
* Forces the unit to a specific value (one argument) or returns the current unit value if no arguments are provided.
* <p/>
* <pre>
* $('HEIGHT').unit('cm')
* $('HEIGHT').unit('cm').unit().any('cm') // returns true
* </pre>
*/
public static ScriptableValue unit(Context ctx, Scriptable thisObj, Object[] args, Function funObj)
throws MagmaJsEvaluationRuntimeException {
ScriptableValue value = (ScriptableValue) thisObj;
if(args.length == 1) {
String newUnit = asString(args[0]);
return new ScriptableValue(value, value.getValue(), newUnit);
}
return new ScriptableValue(value, TextType.get().valueOf(value.getUnit()));
}
/**
* Converts the current value to the specified unit. The current value must have a non-empty unit for this method to
* succeed.
* <p/>
* <pre>
* $('HEIGHT').toUnit('cm')
* </pre>
*/
@SuppressWarnings("unchecked")
public static ScriptableValue toUnit(Context ctx, Scriptable thisObj, Object[] args, Function funObj)
throws MagmaJsEvaluationRuntimeException {
ScriptableValue value = (ScriptableValue) thisObj;
@SuppressWarnings("rawtypes")
Unit target = extractUnit(args[0]);
if(value.getValue().isNull()) {
return new ScriptableValue(thisObj, value.getValue(), target.toString());
}
Unit<?> source = extractUnit(value);
if(target == SI.ONE) {
throw new MagmaJsEvaluationRuntimeException(String.format("unknown target unit %s", args[0]));
}
if(source == SI.ONE) {
if(Strings.isNullOrEmpty(value.getUnit())) {
throw new MagmaJsEvaluationRuntimeException(
String.format("current unit is not specified. use unit() method to specify it."));
}
throw new MagmaJsEvaluationRuntimeException(String.format("current unit is unknown: '%s'.", value.getUnit()));
}
if(!target.isCompatible(source)) {
throw new MagmaJsEvaluationRuntimeException(String.format("unit %s cannot be converted to %s", source, target));
}
double sourceValue = (Double) DecimalType.get().convert(value.getValue()).getValue();
double newValue = source.getConverterTo(target).convert(sourceValue);
return new ScriptableValue(value, DecimalType.get().valueOf(newValue), target.toString());
}
@SuppressWarnings("ChainOfInstanceofChecks")
public static Unit<?> extractUnit(Object value) {
if(value == null) return SI.ONE;
if(value instanceof ScriptableValue) return extractUnit((ScriptableValue) value);
if(value instanceof String) return extractUnit((String) value);
return SI.ONE;
}
public static Unit<?> extractUnit(String value) {
if(value == null) return SI.ONE;
try {
// Try the common non-UCUM notation strings
// This is tried first to use commonly used units like ft and yd instead of uncommon femto-ton (ft), etc.
return DEFAULTS.parse(value, new ParsePosition(0));
} catch(IllegalArgumentException e) {
try {
return PhysicsUnit.valueOf(value);
} catch(IllegalArgumentException ignored) {
}
}
return SI.ONE;
}
public static Unit<?> extractUnit(ScriptableValue scriptableValue) {
if(scriptableValue.hasUnit()) {
return extractUnit(scriptableValue.getUnit());
}
return SI.ONE;
}
@Nullable
private static String asString(Object arg) {
if(arg == null) return null;
if(arg instanceof String) return (String) arg;
if(arg instanceof ScriptableValue) {
ScriptableValue value = (ScriptableValue) arg;
return value.getValue().toString();
}
return arg.toString();
}
}