forked from profectus/Profectus
Implement conditional formulas
This commit is contained in:
parent
675b30fdd0
commit
30aec8a93c
2 changed files with 106 additions and 10 deletions
|
@ -1,5 +1,5 @@
|
||||||
import Decimal, { DecimalSource } from "util/bignum";
|
import Decimal, { DecimalSource } from "util/bignum";
|
||||||
import { ProcessedComputable } from "util/computed";
|
import { Computable, convertComputable, ProcessedComputable } from "util/computed";
|
||||||
import { ref, Ref, unref } from "vue";
|
import { ref, Ref, unref } from "vue";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
@ -439,26 +439,27 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
|
||||||
* @param start The value at which to start applying the step
|
* @param start The value at which to start applying the step
|
||||||
* @param formulaModifier How this step should modify the formula. The incoming value will be the unmodified formula value _minus the start value_. So for example if an incoming formula evaluates to 200 and has a step that starts at 150, the formulaModifier would be given 50 as the parameter
|
* @param formulaModifier How this step should modify the formula. The incoming value will be the unmodified formula value _minus the start value_. So for example if an incoming formula evaluates to 200 and has a step that starts at 150, the formulaModifier would be given 50 as the parameter
|
||||||
*/
|
*/
|
||||||
public static step<T extends FormulaSource>(
|
public static step(
|
||||||
value: T,
|
value: FormulaSource,
|
||||||
start: ProcessedComputable<DecimalSource>,
|
start: Computable<DecimalSource>,
|
||||||
formulaModifier: (value: Ref<DecimalSource>) => GenericFormula
|
formulaModifier: (value: Ref<DecimalSource>) => GenericFormula
|
||||||
) {
|
) {
|
||||||
const lhsRef = ref<DecimalSource>(0);
|
const lhsRef = ref<DecimalSource>(0);
|
||||||
const formula = formulaModifier(lhsRef);
|
const formula = formulaModifier(lhsRef);
|
||||||
|
const processedStart = convertComputable(start);
|
||||||
function evalStep(lhs: DecimalSource) {
|
function evalStep(lhs: DecimalSource) {
|
||||||
if (Decimal.lt(lhs, unref(start))) {
|
if (Decimal.lt(lhs, unref(processedStart))) {
|
||||||
return lhs;
|
return lhs;
|
||||||
}
|
}
|
||||||
lhsRef.value = Decimal.sub(lhs, unref(start));
|
lhsRef.value = Decimal.sub(lhs, unref(processedStart));
|
||||||
return Decimal.add(formula.evaluate(), unref(start));
|
return Decimal.add(formula.evaluate(), unref(processedStart));
|
||||||
}
|
}
|
||||||
function invertStep(value: DecimalSource, lhs: FormulaSource) {
|
function invertStep(value: DecimalSource, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
if (Decimal.gt(value, unref(start))) {
|
if (Decimal.gt(value, unref(processedStart))) {
|
||||||
value = Decimal.add(
|
value = Decimal.add(
|
||||||
formula.invert(Decimal.sub(value, unref(start))),
|
formula.invert(Decimal.sub(value, unref(processedStart))),
|
||||||
unref(start)
|
unref(processedStart)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return lhs.invert(value);
|
return lhs.invert(value);
|
||||||
|
@ -472,6 +473,46 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static if(
|
||||||
|
value: FormulaSource,
|
||||||
|
condition: Computable<boolean>,
|
||||||
|
formulaModifier: (value: Ref<DecimalSource>) => GenericFormula
|
||||||
|
) {
|
||||||
|
const lhsRef = ref<DecimalSource>(0);
|
||||||
|
const formula = formulaModifier(lhsRef);
|
||||||
|
const processedCondition = convertComputable(condition);
|
||||||
|
function evalStep(lhs: DecimalSource) {
|
||||||
|
if (unref(processedCondition)) {
|
||||||
|
lhsRef.value = lhs;
|
||||||
|
return formula.evaluate();
|
||||||
|
} else {
|
||||||
|
return lhs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function invertStep(value: DecimalSource, lhs: FormulaSource) {
|
||||||
|
if (!hasVariable(lhs)) {
|
||||||
|
throw "Could not invert due to no input being a variable";
|
||||||
|
}
|
||||||
|
if (unref(processedCondition)) {
|
||||||
|
return lhs.invert(formula.invert(value));
|
||||||
|
} else {
|
||||||
|
return lhs.invert(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Formula(
|
||||||
|
[value],
|
||||||
|
evalStep,
|
||||||
|
formula.isInvertible() && !formula.hasVariable() ? invertStep : undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
public static conditional(
|
||||||
|
value: FormulaSource,
|
||||||
|
condition: Computable<boolean>,
|
||||||
|
formulaModifier: (value: Ref<DecimalSource>) => GenericFormula
|
||||||
|
) {
|
||||||
|
return Formula.if(value, condition, formulaModifier);
|
||||||
|
}
|
||||||
|
|
||||||
public static constant(value: InvertibleFormulaSource): InvertibleFormula {
|
public static constant(value: InvertibleFormulaSource): InvertibleFormula {
|
||||||
return new Formula([value]) as InvertibleFormula;
|
return new Formula([value]) as InvertibleFormula;
|
||||||
}
|
}
|
||||||
|
|
|
@ -530,3 +530,58 @@ describe("Step-wise", () => {
|
||||||
).compare_tolerance(10));
|
).compare_tolerance(10));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Conditionals", () => {
|
||||||
|
let variable: GenericFormula;
|
||||||
|
let constant: GenericFormula;
|
||||||
|
beforeAll(() => {
|
||||||
|
variable = Formula.variable(10);
|
||||||
|
constant = Formula.constant(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Formula without variable is marked as such", () => {
|
||||||
|
expect(Formula.if(constant, true, value => Formula.sqrt(value)).isInvertible()).toBe(true);
|
||||||
|
expect(Formula.if(constant, true, value => Formula.sqrt(value)).hasVariable()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Formula with variable is marked as such", () => {
|
||||||
|
expect(Formula.if(variable, true, value => Formula.sqrt(value)).isInvertible()).toBe(true);
|
||||||
|
expect(Formula.if(variable, true, value => Formula.sqrt(value)).hasVariable()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Non-invertible formula modifier marks formula as such", () => {
|
||||||
|
expect(Formula.if(constant, true, value => Formula.abs(value)).isInvertible()).toBe(false);
|
||||||
|
expect(Formula.if(constant, true, value => Formula.abs(value)).hasVariable()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Formula modifiers with variables mark formula as non-invertible", () => {
|
||||||
|
expect(
|
||||||
|
Formula.if(constant, true, value => Formula.add(value, variable)).isInvertible()
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
Formula.if(constant, true, value => Formula.add(value, variable)).hasVariable()
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Pass-through with condition false", () => {
|
||||||
|
test("Evaluates correctly", () =>
|
||||||
|
expect(
|
||||||
|
Formula.if(constant, false, value => Formula.sqrt(value)).evaluate()
|
||||||
|
).compare_tolerance(10));
|
||||||
|
test("Inverts correctly with variable in input", () =>
|
||||||
|
expect(
|
||||||
|
Formula.if(variable, false, value => Formula.sqrt(value)).invert(10)
|
||||||
|
).compare_tolerance(10));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Evaluates correctly with condition true", () => {
|
||||||
|
test("Evaluates correctly", () =>
|
||||||
|
expect(
|
||||||
|
Formula.if(variable, true, value => Formula.add(value, 2)).evaluate()
|
||||||
|
).compare_tolerance(12));
|
||||||
|
test("Inverts correctly", () =>
|
||||||
|
expect(
|
||||||
|
Formula.if(variable, true, value => Formula.add(value, 2)).invert(12)
|
||||||
|
).compare_tolerance(10));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
Loading…
Reference in a new issue