/**
* Alligation Module
* @module alligation
* @requires module:util
* @requires module:logger
* @since v1.2.0
*/
import { checkValue, roundTo } from "./util.js";
import * as LOG from "./logger.js";
$( ".input-alligation" ).on( "keyup", () => calculate() );
$( "[name='alligation-type']" ).on( "change", () => changedType() );
$( "#btnReset" ).on( "click", () => clearOutput() );
$('#button--alligation-example').on('click', () => {
if ( isSingle() ) {
$('#alligation-lowerName').val('D5W');
$('#alligation-lowerConc').val(5);
$('#alligation-higherName').val('D50W');
$('#alligation-higherConc').val(50);
$('#alligation-finalConc2').val(20);
$('#alligation-finalVolume').val(1000);
} else {
$('#alligation-lowerConc').val(0);
$('#alligation-higherConc').val(50);
$('#alligation-lowerName').val('sterile water');
$('#alligation-higherName').val('D50W');
$('#alligation-secondConc').val(23.4);
$('#alligation-secondName').val('concentrated NaCl');
$('#alligation-finalConc1').val(10);
$('#alligation-finalConc2').val(0.9);
$('#alligation-finalVolume').val(1000);
$('#alligation-name1').val('dextrose');
$('#alligation-name2').val('sodium chloride');
}
calculate();
});
/**
* Update DOM based on Single/Double selection, clear inputs, then recalculate
*/
function changedType() {
clearOutput();
if ( isSingle() ) {
$( "#card--alligation" ).addClass( "alligation-single" );
} else {
$( "#card--alligation" ).removeClass( "alligation-single" );
}
$( ".input-alligation" ).val( "" );
calculate();
}
function clearOutput() {
$('#col--alligation-result').html('');
}
/**
* Run calculation and display or clear result
*/
function calculate() {
clearOutput();
const res = getValues();
let txt = '';
if ( res.hasOwnProperty('err') ) {
txt = res.err;
} else {
res.components.forEach( (c, i, arr) => {
const last = i + 1 === arr.length ? true : false;
txt += `<div class="form-row${last ? ' border-bottom' : ''}">
${last ? '<div class="col-1">+</div>' : ''}
<div class="col-2 ${last ? '' : 'offset-1 '}text-right">${c.amount}</div>
<div class="col-1 text-center">of</div>
<div class="col-8">${c.description}</div>
</div>`;
});
txt += `<div class="form-row">
<div class="col-2 offset-1 text-right">${res.final.amount}</div>
<div class="col-1 text-center">of a</div>
<div class="col-8">${res.final.description}</div>
</div>`;
}
$('#col--alligation-result').html(txt);
}
/**
* @function
* @returns {Boolean} Is single alligation selected
*/
const isSingle = () => $( "#alligation-type--single" ).prop( "checked" );
/**
* Check values and return result or undefined if invalid inputs
*
* @returns {AlligationResult}
*/
function getValues() {
LOG.group('Alligation Calculator');
$('#card--alligation .input-alligation').removeClass('invalid');
const single = isSingle();
let lowerConc = +checkValue($('#alligation-lowerConc').val(), 0, 100, true);
if ( $('#alligation-lowerConc').val() === '' ) lowerConc = 0;
const higherConc = +checkValue($('#alligation-higherConc').val(), 0, 100);
const secondConc = +checkValue($('#alligation-secondConc').val(), 0, 100);
const finalConc1 = +checkValue($('#alligation-finalConc1').val(), 0, 100);
const finalConc2 = +checkValue($('#alligation-finalConc2').val(), 0, 100);
const finalVolume = +checkValue($('#alligation-finalVolume').val(), 0);
let lowerName = $('#alligation-lowerName').val();
if ( lowerName.length === 0 ) {
if ( lowerConc === 0 ) {
lowerName = 'sterile water';
} else {
lowerName = `${lowerConc}%${single ? '' : ' first solution'}`;
}
}
let higherName = $('#alligation-higherName').val();
if ( higherName.length === 0 ) higherName = `${higherConc}%${single ? '' : ' first solution'}`;
let name1 = $('#alligation-name1').val();
if ( name1.length === 0 ) name1 = `(FIRST)`;
let name2 = $('#alligation-name2').val();
if ( name2.length === 0 ) name2 = `(SECOND)`;
let secondName = $('#alligation-secondName').val();
if ( secondName.length === 0 ) secondName = `${secondConc}% second solution`;
LOG.groupCollapsed(`Inputs for ${single ? 'Single' : 'Double'} Alligation`);
if ( single) LOG.log(`name1: ${name1}`);
if ( single) LOG.log(`finalConc1: ${finalConc1}`);
LOG.log(`lowerName: ${lowerName}`);
LOG.log(`lowerConc: ${lowerConc}`);
LOG.log(`higherName: ${higherName}`);
LOG.log(`higherConc: ${higherConc}`);
if ( single) LOG.log(`name2: ${name2}`);
if ( single) LOG.log(`secondName: ${secondName}`);
if ( single) LOG.log(`secondConc: ${secondConc}`);
LOG.log(`finalConc2: ${finalConc2}`);
LOG.log(`finalVolume: ${finalVolume}`);
LOG.groupEnd();
/** @type {AlligationCheck[]} */
const checks = [
{
errMessage: "Lower concentration must be 0 or more",
check: lowerConc >= 0,
},
{
errMessage: "Higher concentration must be greater than 0",
check: higherConc > 0,
},
{
errMessage: "Second concentration must be greater than 0",
check: secondConc > 0,
ignoreSingle: true,
},
{
errMessage: "Final concentration must be greater than 0",
check: finalConc2 > 0,
ignoreDouble: true,
},
{
errMessage: "Final concentration of second component must be greater than 0",
check: finalConc2 > 0,
ignoreSingle: true,
},
{
errMessage: "Final volume must be greater than 0",
check: finalVolume > 0,
},
{
errMessage: "Higher concentration must be greater than lower concentration",
check: lowerConc < higherConc,
markInvalid: '#alligation-higherConc',
},
{
errMessage: "Final concentration must be between lower and higher concentrations",
check: finalConc2 > lowerConc && finalConc2 < higherConc,
ignoreDouble: true,
markInvalid: '#alligation-finalConc2',
},
{
errMessage: "Second component's final concentration must be less than its original concentration",
check: finalConc2 < secondConc,
ignoreSingle: true,
markInvalid: secondConc > 0 ? '#alligation-finalConc2' : '',
},
{
errMessage: "First component's final concentration must be between the lower and higher original concentrations",
check: lowerConc < finalConc1 && higherConc > finalConc1,
ignoreSingle: true,
markInvalid: higherConc > 0 ? '#alligation-finalConc1' : '',
},
{
errMessage: "First component's final concentration must be greater than 0",
check: finalConc1 > 0,
ignoreSingle: true,
},
];
let passedChecks = true;
LOG.groupCollapsed("Checks");
checks.forEach( c => {
let runCheck = true;
if ( c.hasOwnProperty('ignoreSingle') ) {
if ( c.ignoreSingle && single) runCheck = false;
}
if ( c.hasOwnProperty('ignoreDouble') ) {
if ( c.ignoreDouble && !single ) runCheck = false;
}
if ( runCheck && !c.check ) {
passedChecks = false;
if ( c.hasOwnProperty('markInvalid') ) {
if ( c.markInvalid !== '' ) {
if ( $(c.markInvalid).val() !== 0 && $(c.markInvalid).val() !== '' ) {
$(c.markInvalid).addClass('invalid');
LOG.log(`[${c.markInvalid}] ${c.errMessage}`);
} else {
LOG.log(c.errMessage);
}
} else {
LOG.log(c.errMessage);
}
} else {
LOG.log(c.errMessage);
}
}
});
LOG.groupEnd();
if ( passedChecks ) {
LOG.logArgs("%cPassed Checks", 'color:green');
} else {
LOG.logArgs("%cFailed Checks", 'color: red');
}
if ( passedChecks ) {
LOG.groupCollapsed('Calculations');
if ( single ) {
const partsLow = higherConc - finalConc2;
const partsHi = finalConc2 - lowerConc;
const partsTotal = partsLow + partsHi;
const lowerAmount = partsLow / partsTotal * finalVolume;
const higherAmount = partsHi / partsTotal * finalVolume;
LOG.log(`partsLow: ${partsLow}`);
LOG.log(`partsHi: ${partsHi}`);
LOG.log(`partsTotal: ${partsTotal}`);
LOG.log(`lowerAmount: ${lowerAmount}`);
LOG.log(`higherAmount: ${higherAmount}`);
LOG.groupEnd();
LOG.groupEnd();
return {
components: [
{ amount: `${roundTo(lowerAmount, 0.1)} mL`, description: lowerName },
{ amount: `${roundTo(higherAmount, 0.1)} mL`, description: higherName },
],
final: { amount: `${finalVolume} mL`, description: `${finalConc2}% solution` },
};
} else {
const secondAmount = finalConc2 / 100 * ( finalVolume / ( secondConc / 100 ) );
const lowerAmount = -( finalConc1 / 100 * finalVolume - higherConc / 100 * ( finalVolume - secondAmount ) ) / ( higherConc / 100 - lowerConc / 100 );
const higherAmount = finalVolume - secondAmount - lowerAmount;
LOG.log(`secondAmount: ${secondAmount}`);
LOG.log(`lowerAmount: ${lowerAmount}`);
LOG.log(`higherAmount: ${higherAmount}`);
LOG.groupEnd();
LOG.groupEnd();
if ( lowerAmount < 0 || higherAmount < 0 || secondAmount < 0 ) {
return { err: "The desired solution cannot be made with the defined stock solutions." };
} else {
return {
components: [
{ amount: `${roundTo(lowerAmount, 0.1)} mL`, description: lowerName },
{ amount: `${roundTo(higherAmount, 0.1)} mL`, description: higherName },
{ amount: `${roundTo(secondAmount, 0.1)} mL`, description: secondName },
],
final: { amount: `${finalVolume} mL`, description: `${name1} ${finalConc1}% and ${name2} ${finalConc2}% solution` },
};
}
}
} else {
LOG.groupEnd();
return { err: '' };
}
}
/**
* Alligation Result Row
*
* @typedef {Object} AlligationResultRow
* @property {String} amount - Amount and units of the solution
* @property {String} description - Description of the solution
*/
/**
* Alligation Result
*
* @typedef {Object} AlligationResult
* @property {String} err - Error text if calculation is not possible
* @property {AlligationResultRow[]} components - Components that make the final solution
* @property {AlligationResultRow} final - Details of final solution
*/
/**
* Input validity check
*
* @typedef {Object} AlligationCheck
* @property {String} errMessage - Error message to display if check fails
* @property {Boolean} check - Expression to evaluate that returns true if valid
* @property {Boolean} [ignoreSingle=false] - Ignore this check for single alligation
* @property {Boolean} [ignoreDouble=false] - Ignore this check for double alligation
* @property {String} [markInvalid] - jQuery selector of element to mark as invalid if check fails
*/