/**
* @copyright 2015 Tridium, Inc. All Rights Reserved.
* @author Gareth Johnson
*/
/**
* Defines {@link baja.Unit}.
* @module baja/obj/Unit
*/
define([ "bajaScript/sys",
"bajaScript/baja/obj/Dimension",
"bajaScript/baja/obj/Simple",
"bajaScript/baja/obj/numberUtil",
"bajaScript/baja/obj/objUtil",
"bajaPromises" ], function (
baja,
Dimension,
Simple,
numberUtil,
objUtil,
Promise) {
'use strict';
var SCALE_OFFSET_REGEX = /([*+])([^*+]*)/g,
SEMICOLON_REGEX = /;/,
cacheDecode = objUtil.cacheDecode,
cacheEncode = objUtil.cacheEncode,
subclass = baja.subclass,
callSuper = baja.callSuper;
/**
* @returns {Promise}
*/
var getUnitConversion = (function () {
var UC;
return function getUnitConversion() {
if (UC) { return UC; }
return (UC = baja.importTypes([ 'baja:UnitConversion' ])
.then(function () {
return (UC = Promise.resolve(baja.$('baja:UnitConversion')));
}));
};
}());
/**
* @inner
* @param str the scale/offset segment of a string-encoded `BUnit`. Will look
* like one of four things: *1+1, *1, +1, or empty.
* @returns {{scale: number, offset: number}} object containing the parsed
* scale and offset numbers (defaulting to 1 and 0, respectively)
*/
function parseScaleAndOffset(str) {
var matches = (str || '').match(SCALE_OFFSET_REGEX),
obj = { scale: 1, offset: 0 },
match, numType, numStr, num, i;
if (!matches) {
if (str) {
throw new Error("invalid scale/offset string " + str);
} else {
return obj;
}
}
for (i = 0; i < matches.length; i++) {
match = matches[i];
numType = match[0] === '*' ? 'scale' : 'offset';
numStr = match.substr(1);
num = parseFloat(numStr);
if (isNaN(num)) {
throw new Error("invalid " + numType + " " + numStr);
}
obj[numType] = num;
}
return obj;
}
/**
* Represents a `baja:Unit` in BajaScript.
*
* When creating a `Simple`, always use the `make()` method instead of
* creating a new Object.
*
* @class
* @alias baja.Unit
* @extends baja.Simple
*/
var Unit = function Unit(name, symbol, dimension, scale, offset, isPrefix) {
callSuper(Unit, this, arguments);
this.$name = name;
this.$symbol = symbol;
this.$dimension = dimension;
this.$scale = scale || 1;
this.$offset = offset || 0;
this.$isPrefix = isPrefix || false;
};
subclass(Unit, Simple);
/**
* Default `Unit` instance.
*
* @type {baja.Unit}
*/
Unit.DEFAULT = new Unit("null", "null", Dimension.DEFAULT);
/**
* Null `Unit` instance (same as `DEFAULT`).
*
* @type {baja.Unit}
*/
Unit.NULL = Unit.DEFAULT;
/**
* Used to maintain an internal cache of `Unit` objects, keyed by name.
*
* @private
* @type {Object}
*/
Unit.$cache = {
"null": Unit.NULL
};
/**
* Creates a new instance of `baja.Unit`.
*
* Note that if calling this function directly, multiple calls with the same
* name but different dimensions will result in an error (no messing with
* the laws of physics!). Be careful not to manually create `Unit`s with
* incorrect dimensions that might clash with units specified in the unit
* database stationside.
*
* @param {String} name the unit name. Must be unique and may not contain a
* semicolon.
* @param {String} [symbol=name] the unit symbol. If omitted will default to
* the unit name. May not contain a semicolon.
* @param {baja.Dimension} dimension the unit dimension
* @param {Number} [scale=1] the unit scale
* @param {Number} [offset=0] the unit offset
* @param {Boolean} [isPrefix=false] true if the unit should be prefixed /
* displayed on the left of the actual value
* @returns {baja.Unit}
* @throws if name or symbol contain a semicolon, the dimension is omitted,
* or if duplicate units are created with the same name but different
* dimensions
*/
Unit.make = function (name, symbol, dimension, scale, offset, isPrefix) {
if (!name || name.match(SEMICOLON_REGEX)) {
throw new Error("invalid unit name " + name);
}
if (!symbol) {
symbol = name;
} else if (symbol.match(SEMICOLON_REGEX)) {
throw new Error("invalid symbol " + symbol);
}
baja.strictArg(dimension, Dimension, 'dimension required');
var unit = Unit.$cache[name];
if (unit) {
if (!dimension.equals(unit.$dimension)) {
throw new Error("Cannot change dimensions of existing unit " + name);
}
Unit.call(unit, name, symbol, dimension, scale, offset, isPrefix);
return unit;
}
unit = new Unit(name, symbol, dimension, scale, offset, isPrefix);
Unit.$cache[name] = unit;
return unit;
};
/**
* @see baja.Unit.make
* @returns {baja.Unit}
*/
Unit.prototype.make = function () {
return Unit.make.apply(this, arguments);
};
/**
* Parse a `baja.Unit` from a `String`.
*
* @method
* @param {String} str
* @returns {baja.Unit}
* @throws if string is malformed or contains invalid unit parameters
*/
Unit.prototype.decodeFromString = cacheDecode(function (str) {
if (!str) {
return Unit.DEFAULT;
}
var split = str.split(';'),
name = split[0],
symbol = split[1],
dimension = Dimension.DEFAULT.decodeFromString(split[2]),
scoff = parseScaleAndOffset(split[3]);
return Unit.make(name, symbol, dimension, scoff.scale, scoff.offset);
});
/**
* Returns the unit symbol.
*
* @returns {String}
*/
Unit.prototype.toString = function () {
return this.getSymbol();
};
/**
* Encode a `baja.Unit` to a `String`.
*
* @method
* @returns {String}
*/
Unit.prototype.encodeToString = cacheEncode(function () {
var that = this,
name = that.$name,
symbol = that.$symbol,
dimension = that.$dimension,
scale = that.$scale,
offset = that.$offset,
scoff = '';
if (scale !== 1) {
scoff += '*' + scale;
}
if (offset !== 0) {
scoff += '+' + offset;
}
return [
name,
symbol === name ? null : symbol,
dimension.encodeToString(),
scoff,
null
].join(';');
});
/**
* Returns the unit name.
*
* @returns {String}
*/
Unit.prototype.getUnitName = function () {
return this.$name;
};
/**
* Returns the unit symbol.
*
* @returns {String}
*/
Unit.prototype.getSymbol = function () {
return this.$symbol;
};
/**
* Returns the unit dimension.
*
* @returns {baja.Dimension}
*/
Unit.prototype.getDimension = function () {
return this.$dimension;
};
/**
* Returns the unit scale.
*
* @returns {Number}
*/
Unit.prototype.getScale = function () {
return this.$scale;
};
/**
* Returns the unit offset.
*
* @returns {Number}
*/
Unit.prototype.getOffset = function () {
return this.$offset;
};
/**
* Returns true if the unit should be prefixed.
*
* @returns {Boolean}
*/
Unit.prototype.isPrefix = Unit.prototype.getIsPrefix = function () {
return this.$isPrefix;
};
/**
* Returns the data type symbol (`u`) for `Facets` encoding.
*
* @returns {String}
*/
Unit.prototype.getDataTypeSymbol = function () {
return 'u';
};
/**
* Convert a scalar value from this unit to another.
*
* @param {baja.Unit} toUnit
* @param {number} scalar
* @returns {number} the converted scalar value
* @throws {Error} if the two Units are not of the same Dimension
*/
Unit.prototype.convertTo = function (toUnit, scalar) {
if (!this.getDimension().equals(toUnit.getDimension())) {
throw new Error('Not convertible: ' +
this.getSymbol() + " -> " + toUnit.getSymbol());
}
if (this === toUnit) {
return scalar;
}
return ((scalar * this.getScale() + this.getOffset()) - toUnit.getOffset()) / toUnit.getScale();
};
/**
* Friendly API to convert a scalar value to a desired unit.
*
* @param {number} scalar a scalar value to convert
* @param {object} params
* @param {baja.Unit|string} [params.fromUnits=params.units] the units the scalar is _currently_ in. Can be a unit name, or Units instance. If omitted, considered to be `toUnits` with `unitConversion` applied.
* @param {string|number|baja.FrozenEnum} [params.unitConversion] used to convert between systems of measure.
* @param {baja.Unit|string} [params.toUnits] the desired units. If omitted, will be considered to be `fromUnits` with `unitConversion` applied.
* @param {number} [params.precision=8] specify how many decimal places the resulting scalar should be rounded to.
* @returns {Promise.<number>} the converted unit.
*
* @since Niagara 4.8
*
* @example
* <caption>What is 32 Fahrenheit, as shown in metric units?</caption>
*
* return baja.Unit.convert(32, { units: 'fahrenheit', unitConversion: 'metric' })
* .then(function (result) {
* console.log(result); // 0
* });
*
* @example
* <caption>How many centimeters are in 1 inch?</caption>
*
* return baja.Unit.convert(1, { fromUnits: 'inch', toUnits: 'centimeter' })
* .then(function (result) {
* console.log(result); // 2.54
* });
*
* @example
* <caption>My data is "12 inches", but the user wants to see metric units.
* What number should I show them in the UI?</caption>
*
* return baja.Unit.convert(12, { fromUnits: 'inch', unitConversion: 'metric' })
* .then(function (result) {
* console.log(result); // 30.48
* });
*
* @example
* <caption>My user entered "100 cm", but my station logic is in inches. What
* number should I store in the station?</caption>
*
* return baja.Unit.convert(100, { unitConversion: 'metric', toUnits: 'inch', precision: 2 })
* .then(function (result) {
* console.log(result); // 39.37
* });
*/
Unit.convert = function (scalar, params) {
params = params || {};
return baja.UnitDatabase.get()
.then(function (db) {
var fromUnits = params.fromUnits || params.units;
var toUnits = params.toUnits;
var unitConversion = params.unitConversion;
var precision = params.precision;
var nullUnit = baja.Unit.NULL;
if (toUnits) {
toUnits = db.getUnit(toUnits);
fromUnits = db.convertUnit(unitConversion, db.getUnit(fromUnits || toUnits));
} else {
fromUnits = db.getUnit(fromUnits || nullUnit);
toUnits = db.convertUnit(unitConversion, fromUnits);
}
return toPrecision(fromUnits.convertTo(toUnits, scalar), precision);
});
};
function toPrecision(number, precision) {
if (typeof precision !== 'number') { precision = 8; }
var f = Math.pow(10, precision);
return Math.round(number * f) / f;
}
/**
* Get the desired unit as specified by the input context.
*
* @param {Object} [cx]
* @param {baja.Unit} [cx.units]
* @param {baja.FrozenEnum|number|string} [cx.unitConversion] accepts a
* `baja:UnitConversion` enum, ordinal, or tag. If omitted,
* `baja.getUnitConversion()` (the user-configured unit conversion) will be
* used.
* @param {boolean} [cx.showUnits]
* @returns {Promise.<baja.Unit|null>} resolves the desired display unit as
* specified. If `showUnits` is false or the units are not specified or
* `NULL`, resolves `null` to indicate that units are not used for display in
* this context.
* @since Niagara 4.8
* @see baja.Unit#toDesiredUnit
* @example
* <caption>Calculate display units based on facets.</caption>
* var facetsObj = point.getFacets().toObject();
* return baja.Unit.toDisplayUnits(facetsObj)
* .then(function (displayUnits) {
* console.log('this point wants to be displayed with units ' + displayUnits);
* });
*/
Unit.toDisplayUnits = function (cx) {
cx = cx || {};
var units = cx.units;
if (cx.showUnits === false || !(units instanceof baja.Unit) || units.equals(Unit.NULL)) {
return Promise.resolve(null);
}
return units.toDesiredUnit(numberUtil.getUnitConversion(cx));
};
/**
* Get the unit that this unit converts to as specified by the given
* conversion.
* @param {baja.FrozenEnum|number|string} unitConversion accepts a
* `baja:UnitConversion` enum, ordinal, or tag.
* @returns {Promise.<baja.Unit>}
* @since Niagara 4.8
* @example
* return fahrenheitUnit.toDesiredUnit('metric')
* .then(function (unit) {
* console.log(unit.getUnitName()); // Celsius
* });
*/
Unit.prototype.toDesiredUnit = function (unitConversion) {
var that = this;
if (!unitConversion) { return Promise.resolve(that); }
return baja.UnitDatabase.get()
.then(function (db) {
return Promise.resolve(getConversionOrdinal(unitConversion))
.then(function (ordinal) {
var tag = baja.$('baja:UnitConversion', ordinal).getTag();
return db.convertUnit(tag, that);
});
});
};
/**
* @param {baja.Enum|Number|String} unitConversion - the `baja:UnitConversion`
* enum, an ordinal, or tag.
*
* @returns {Number|Promise}
*/
function getConversionOrdinal(unitConversion) {
if (baja.hasType(unitConversion, 'baja:Enum')) {
return unitConversion.getOrdinal();
}
if (baja.hasType(unitConversion, 'baja:Number')) {
return unitConversion.valueOf();
}
if (typeof unitConversion === 'string') {
return getUnitConversion()
.then(function (UC) {
if (!UC.getRange().isTag(unitConversion)) {
throw new Error('invalid BUnitConversion tag ' + unitConversion);
}
return UC.get(unitConversion).getOrdinal();
});
}
return null;
}
return Unit;
});