import { Measure } from "../data/Measure.js";
import { Density } from "../data/Density.js";
import { CaloricContent } from "../data/CaloricContent.js";
import { Component } from "./Component.js";
import { CalculationError } from "./CalculationError.js";
import { AlcoholTable } from "../data/alcohol.table.js";
import { SyrupTable } from "../data/syrup.table.js";
/**
* Conversions between units
*
* Available conversions:
*
* Alcohol:
*
* | ↓ from \ → to | DENSITY | ABV | VV | WV | WW | CW |
* |----------------|:-------:|:---:|:---:|:---:|:---:|:---:|
* | **DENSITY** | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
* | **ABV** | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
* | **VV** | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
* | **WV** | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
* | **WW** | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
* | **CW** | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
*
* Syrup:
*
* | ↓ from \ → to | DENSITY | BRIX | VV | WV | WW | CW |
* |----------------|:-------:|:----:|:---:|:---:|:---:|:---:|
* | **DENSITY** | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
* | **BRIX** | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
* | **VV** | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
* | **WV** | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
* | **WW** | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
* | **CW** | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
*
* @hideconstructor
*/
class Conversion {
/**
* @static
* @prop {number} correction - multiplier for complex compositions to avoid rounding errors while mixing
*/
static correction = 0.9999;
/**
* Facade function for converting from anything to anything
*
* @static
* @param {IngredientType} ingredient
* @param {MeasureVariant} from
* @param {MeasureVariant} to
* @param {number} value
* @returns {number}
* @throws {CalculationError}
*
* @example
* // returns 0.61501
* Conversion.convert('syrup', Measure.BRIX, Measure.WV, 50)
*/
static convert(ingredient, from, to, value) {
if (from == to) return value;
if (
typeof Conversion.conversion_map[ingredient][from][to] != 'function'
)
throw new CalculationError('CONVERSION_UNAVAILABLE');
if (typeof Conversion.validation_map[ingredient][from] == 'function') {
if (!Conversion.validation_map[ingredient][from](value))
throw new CalculationError('INVALID_VALUE', value);
}
let result = Conversion.conversion_map[ingredient][from][to](value);
return result;
}
/**
* Facade function for validation
*
* @static
* @param {IngredientType} ingredient
* @param {MeasureVariant} measure
* @param {number} value
* @returns {number}
* @throws {CalculationError}
*
* @example
* // returns true
* Conversion.validate('syrup', Measure.BRIX, 50)
* // returns false
* Conversion.validate('syrup', Measure.BRIX, 150)
*/
static validate(ingredient, measure, value) {
if (typeof Conversion.validation_map[ingredient][measure] != 'object')
throw new CalculationError('VALIDATION_UNAVAILABLE');
let result =
(Conversion.validation_map[ingredient][measure].min <= value) &&
(Conversion.validation_map[ingredient][measure].max >= value);
return result;
}
static validation_map = {
syrup: {
[Measure.DENSITY]: { min: Density.WATER, max: Density.SUCROSE },
[Measure.BRIX]: { min: 0, max: 100 },
[Measure.WV]: { min: 0, max: Density.SUCROSE },
[Measure.WW]: { min: 0, max: 1 },
[Measure.VV]: { min: 0, max: 1 },
[Measure.CW]: { min: 0, max: CaloricContent.SUCROSE, },
},
alcohol: {
[Measure.DENSITY]: { min: Density.ETHANOL, max: Density.WATER },
[Measure.ABV]: { min: 0, max: 100 },
[Measure.WV]: { min: 0, max: Density.ETHANOL },
[Measure.WW]: { min: 0, max: 1 },
[Measure.VV]: { min: 0, max: 1 },
[Measure.CW]: { min: 0, max: CaloricContent.ETHANOL },
},
};
static conversion_map = {
syrup: {
[Measure.DENSITY]: {
[Measure.WW]: (value) => SyrupTable.lookup(value, SyrupTable.COL_DENSITY, SyrupTable.COL_WW),
[Measure.WV]: (value) => SyrupTable.lookup(value, SyrupTable.COL_DENSITY, SyrupTable.COL_WV),
[Measure.BRIX]: (value) => SyrupTable.lookup(value, SyrupTable.COL_DENSITY, SyrupTable.COL_WW) * 100,
[Measure.VV]: (value) => SyrupTable.lookup(value, SyrupTable.COL_DENSITY, SyrupTable.COL_VV),
[Measure.CW]: (value) => SyrupTable.lookup(value, SyrupTable.COL_DENSITY, SyrupTable.COL_WW) * CaloricContent.SUCROSE,
},
[Measure.WW]: {
[Measure.DENSITY]: (value) => SyrupTable.lookup(value, SyrupTable.COL_WW, SyrupTable.COL_DENSITY),
[Measure.WV]: (value) => SyrupTable.lookup(value, SyrupTable.COL_WW, SyrupTable.COL_WV),
[Measure.BRIX]: (value) => value * 100,
[Measure.VV]: (value) => SyrupTable.lookup(value, SyrupTable.COL_WW, SyrupTable.COL_VV),
[Measure.CW]: (value) => value * CaloricContent.SUCROSE,
},
[Measure.BRIX]: {
[Measure.DENSITY]: (value) => SyrupTable.lookup(value * 0.01, SyrupTable.COL_WW, SyrupTable.COL_DENSITY),
[Measure.WV]: (value) => SyrupTable.lookup(value * 0.01, SyrupTable.COL_WW, SyrupTable.COL_WV),
[Measure.WW]: (value) => value * 0.01,
[Measure.VV]: (value) => SyrupTable.lookup(value * 0.01, SyrupTable.COL_WW, SyrupTable.COL_VV),
[Measure.CW]: (value) => value * 0.01 * CaloricContent.SUCROSE,
},
[Measure.WV]: {
[Measure.DENSITY]: (value) => SyrupTable.lookup(value, SyrupTable.COL_WV, SyrupTable.COL_DENSITY),
[Measure.WW]: (value) => SyrupTable.lookup(value, SyrupTable.COL_WV, SyrupTable.COL_WW),
[Measure.BRIX]: (value) => SyrupTable.lookup(value, SyrupTable.COL_WV, SyrupTable.COL_WW) * 100,
[Measure.VV]: (value) => SyrupTable.lookup(value, SyrupTable.COL_WV, SyrupTable.COL_VV),
[Measure.CW]: (value) => SyrupTable.lookup(value, SyrupTable.COL_WV, SyrupTable.COL_WW) * CaloricContent.SUCROSE,
},
[Measure.VV]: {
[Measure.DENSITY]: (value) => SyrupTable.lookup(value, SyrupTable.COL_VV, SyrupTable.COL_DENSITY),
[Measure.WW]: (value) => SyrupTable.lookup(value, SyrupTable.COL_VV, SyrupTable.COL_WW),
[Measure.BRIX]: (value) => SyrupTable.lookup(value, SyrupTable.COL_VV, SyrupTable.COL_WW) * 100,
[Measure.WV]: (value) => SyrupTable.lookup(value, SyrupTable.COL_VV, SyrupTable.COL_WV),
[Measure.CW]: (value) => SyrupTable.lookup(value, SyrupTable.COL_VV, SyrupTable.COL_WW) * CaloricContent.SUCROSE,
},
[Measure.CW]: {
[Measure.WW]: (value) => value / CaloricContent.SUCROSE,
[Measure.DENSITY]: (value) => SyrupTable.lookup(Conversion.convert('syrup', Measure.CW, Measure.WW, value), SyrupTable.COL_WW, SyrupTable.COL_DENSITY),
[Measure.BRIX]: (value) => Conversion.convert('syrup', Measure.CW, Measure.WW, value) * 100,
[Measure.WV]: (value) => SyrupTable.lookup(Conversion.convert('syrup', Measure.CW, Measure.WW, value), SyrupTable.COL_WW, SyrupTable.COL_WV),
[Measure.VV]: (value) => SyrupTable.lookup(Conversion.convert('syrup', Measure.CW, Measure.WW, value), SyrupTable.COL_WW, SyrupTable.COL_VV),
},
},
alcohol: {
[Measure.DENSITY]: {
[Measure.ABV]: (value) => AlcoholTable.lookup(value, AlcoholTable.COL_DENSITY, AlcoholTable.COL_VV, true) * 100,
[Measure.VV]: (value) => AlcoholTable.lookup(value, AlcoholTable.COL_DENSITY, AlcoholTable.COL_VV, true),
[Measure.WW]: (value) => AlcoholTable.lookup(value, AlcoholTable.COL_DENSITY, AlcoholTable.COL_WW, true),
[Measure.WV]: (value) => AlcoholTable.lookup(value, AlcoholTable.COL_DENSITY, AlcoholTable.COL_WV, true),
[Measure.CW]: (value) => AlcoholTable.lookup(value, AlcoholTable.COL_DENSITY, AlcoholTable.COL_WW, true) * CaloricContent.ETHANOL,
},
[Measure.VV]: {
[Measure.ABV]: (value) => value * 100,
[Measure.DENSITY]: (value) => AlcoholTable.lookup(value, AlcoholTable.COL_VV, AlcoholTable.COL_DENSITY),
[Measure.WW]: (value) => AlcoholTable.lookup(value, AlcoholTable.COL_VV, AlcoholTable.COL_WW),
[Measure.WV]: (value) => AlcoholTable.lookup(value, AlcoholTable.COL_VV, AlcoholTable.COL_WV),
[Measure.CW]: (value) => AlcoholTable.lookup(value, AlcoholTable.COL_VV, AlcoholTable.COL_WW) * CaloricContent.ETHANOL,
},
[Measure.ABV]: {
[Measure.DENSITY]: (value) => AlcoholTable.lookup(value * 0.01, AlcoholTable.COL_VV, AlcoholTable.COL_DENSITY),
[Measure.WW]: (value) => AlcoholTable.lookup(value * 0.01, AlcoholTable.COL_VV, AlcoholTable.COL_WW, ),
[Measure.WV]: (value) => AlcoholTable.lookup(value * 0.01, AlcoholTable.COL_VV, AlcoholTable.COL_WV, ),
[Measure.VV]: (value) => value * 0.01,
[Measure.CW]: (value) => AlcoholTable.lookup(value * 0.01, AlcoholTable.COL_VV, AlcoholTable.COL_WW, ) * CaloricContent.ETHANOL,
},
[Measure.WW]: {
[Measure.ABV]: (value) => AlcoholTable.lookup(value, AlcoholTable.COL_WW, AlcoholTable.COL_VV, ) * 100,
[Measure.DENSITY]: (value) => AlcoholTable.lookup(value, AlcoholTable.COL_WW, AlcoholTable.COL_DENSITY),
[Measure.VV]: (value) => AlcoholTable.lookup(value, AlcoholTable.COL_WW, AlcoholTable.COL_VV, ),
[Measure.WV]: (value) => AlcoholTable.lookup(value, AlcoholTable.COL_WW, AlcoholTable.COL_WV, ),
[Measure.CW]: (value) => value * CaloricContent.ETHANOL,
},
[Measure.WV]: {
[Measure.ABV]: (value) => AlcoholTable.lookup(value, AlcoholTable.COL_WV, AlcoholTable.COL_VV, ) * 100,
[Measure.DENSITY]: (value) => AlcoholTable.lookup(value, AlcoholTable.COL_WV, AlcoholTable.COL_DENSITY, ),
[Measure.WW]: (value) => AlcoholTable.lookup(value, AlcoholTable.COL_WV, AlcoholTable.COL_WW, ),
[Measure.VV]: (value) => AlcoholTable.lookup(value, AlcoholTable.COL_WV, AlcoholTable.COL_VV, ),
[Measure.CW]: (value) => AlcoholTable.lookup(value, AlcoholTable.COL_WV, AlcoholTable.COL_WW, ) * CaloricContent.ETHANOL,
},
[Measure.CW]: {
[Measure.WW]: (value) => value / CaloricContent.ETHANOL,
[Measure.DENSITY]: (value) => AlcoholTable.lookup(Conversion.convert('alcohol', Measure.CW, Measure.WW, value), AlcoholTable.COL_WW, AlcoholTable.COL_DENSITY),
[Measure.ABV]: (value) => Conversion.convert('alcohol', Measure.CW, Measure.VV, value) * 100,
[Measure.WV]: (value) => AlcoholTable.lookup(Conversion.convert('alcohol', Measure.CW, Measure.WW, value), AlcoholTable.COL_WW, AlcoholTable.COL_WV),
[Measure.VV]: (value) => AlcoholTable.lookup(Conversion.convert('alcohol', Measure.CW, Measure.WW, value), AlcoholTable.COL_WW, AlcoholTable.COL_VV),
},
},
};
/**
* Binary search method for really complex things
*
* @static
* @param {Function} callback - callback function, must take 1 number argument
* @param {number} target - value to get from closure
* @param {number} min - min value of argument to search
* @param {number} max - max value of argument
* @param {number} [precision=0.00001] - precision to target
* @param {boolean} [inverse=false] - inverse search (if result is bigger than target, the argument gets smaller)
*
* @returns {number}
* @throws {CalculationError}
*/
static binary_search(callback, target, min, max, precision, inverse) {
precision = precision || 0.00001;
let result, now, diff;
let m = inverse ? -1 : 1;
let gmin = min,
gmax = max;
let iteration_limit = 100;
do {
result = (min + max) * 0.5;
now = callback(result);
diff = Math.abs(now - target);
if (diff < precision) break;
if (
Math.abs(result - gmin) < precision ||
Math.abs(result - gmax) < precision
) {
console.error(' -- binary search failed at: ', {
now,
target,
result,
diff,
precision,
});
throw new CalculationError('ERROR_BINARY_SEARCH_OUT_OF_BOUNDS');
}
if (m * now > m * target) {
max = result;
} else {
min = result;
}
iteration_limit--;
if (iteration_limit <= 0) {
console.error(' -- binary search failed at: ', {
now,
target,
result,
diff,
precision,
});
throw new CalculationError('ERROR_BINARY_SEARCH_OUT_OF_BOUNDS');
}
} while (diff > precision);
return Math.round(result / precision) * precision;
}
}
export { Conversion };