MJMathFormula
In Java, performing math with objects like MJ variable instances can be tedious and involve rigging
numerous binary operations in code (similar to java.math.BigDecimal
), not to mention dealing with
type conversion and widening. MJ offers an alternative approach called formulas. An MJ formula is based
on SpEL
(Spring Expression Language) and supports expressions similar to those used
in @CHG
and @ART
.
As a simple example, the MJ formula m * c ^ 2
is equivalent
to @ART
expression <m>*<c>**2
.
Formula Operators
The following operators may be applied to terms within a formula:
MJMathFormula Operator | Description | Corresponding IMathOperators Method | MAPPER Operator |
---|---|---|---|
+ | Addition: a+b |
add |
+ |
- | Subtraction: a-b |
subtract |
- |
* | Multiplication: a*b |
multiply |
* |
/ | Division: a/b |
divide |
/ |
div(a,b) | Integer division, gives the unrounded integer portion of the dividend of
value a divided by value b . |
divideToIntegralValue |
// |
^ | Exponentiation: a^b . Gives the result of value a raised
to the power of value b . |
pow |
** |
% | Modulus: a%b . Remainder value of a /b . |
MOD |
MOD |
-(a) | Unary minus, gives the negative of value a . |
negate |
- |
n(x) | Numeric literal, where x is either a number or quoted string. |
java.lang.Number |
integer or fractional number |
dec(x) | Numeric literal as java.math.BigDecimal , where
x is either a number or quoted string. |
java.math.BigDecimal |
fractional number |
Operator Precedence
When coding a @CHG
expression as a formula, be aware that normal, mathematical operator
precedence shown below is in effect (the same precedence used by @ART
).
To preserve's @CHG
left-to-right evaluation, parentheses may be required.
Lowest-to-Highest Precedence |
---|
+ - |
* / |
^ % |
div n dec varByName varByNum elemByName elemByNum |
Functions
Each function in the IMathFunctions interface may also
be invoked from a formula using the name of the method in IMathFunctions
. For example,
a simple formula that finds the absolute value of
variable x
would be abs(x)
. As noted above,
MJ formulas also support built-in functions div
, n
,
and dec
.
The additional, built-in functions varByName
, varByNum
,
elemByName
and elemByNum
resolve
indirect references to named and numbered variables and arrays:
Indirect Reference Function | Description of Arguments | MJMathFormula Example |
---|---|---|
varByName(v) | v = variable containing another variable name |
varByName(LOCAL.ptr) |
varByNum(v) | v = variable containing another variable number |
varByNum(v9) |
elemByName(v, s) | v = variable containing array names = subscript of array element |
elemByName(GLOB.arrptr,10) |
elemByNum(v, s) | v = variable containing array numbers = subscript of array element |
elemByNum(v5, LOCAL.idx) |
More examples of these functions follow under Variables and Arrays.
Variables
Variables are referenced in a formula by variable name and namespace; the namespace identifies
the variable as LOCAL
, GLOB
al
or ENV
ironmental. If a namespace is not specified, the variable is
assumed to be LOCAL
. Some examples of formula variable references
and their MAPPER equivalents are:
MJ variable instantiation | MJMathFormula reference | MAPPER reference |
---|---|---|
|
LOCAL.a |
<a> |
|
varByName(foo) |
<<foo>> |
|
LOCAL.v1 |
v1 |
|
varByNum(v97) |
VV97 |
|
GLOB.a |
<*a> |
|
varByName(GLOB.foo) |
<<*foo>> |
|
ENV.a |
<$a> |
|
varByName(ENV.foo) |
<<$foo>> |
|
varByName(ptrGlob) |
<<ptrGlob>> |
|
varByName(ptrEnv) |
<<ptrEnv>> |
Arrays
Like variables, arrays are referenced in a formula by array name and namespace; the namespace
identifies the array as LOCAL
, GLOB
al
or ENV
ironmental. If a namespace is not specified, the array is
assumed to be LOCAL
. Some examples of formula array references and their MAPPER equivalents are:
MJ array instantiation | MJMathFormula reference | MAPPER reference |
---|---|---|
|
LOCAL.arr[1] |
<arr>[1] |
|
elemByName(ptr,1) |
<<ptr>>[1] |
|
LOCAL.v1[2] |
V1[2] |
|
elemByNum(v99,2) |
VV99 |
|
GLOB._arr[1] |
<*arr>[1] |
|
elemByName(GLOB.ref,1) |
<<*ref>>[1] |
|
ENV.$arr[3] |
<$arr>[3] |
|
elemByName(envptr,v5) |
<<envptr>>[V5] |
Numeric Literals
A side effect of using SpEL is that numeric literals in a formula should be encoded with
the n
function to apply
type conversion and widening appropriately.
A subtle distinction exists between a quoted numeric literal such as n('5')
and
an unquoted literal like n(5)
. When the literal is quoted,
the math options PREFER_DECIMAL_OVER_FLOAT and
PREFER_FLOAT_OVER_DECIMAL specified when the formula
was created are fully respected.
In other words, the quoted numeric literal is treated as java.math.BigDecimal
or java.lang.Double
, respectively.
When left unquoted, SpEL intercedes, converting the numeric literal to java.lang.Long
or java.lang.Double
before passing it to n()
.
TEMP Namespace
MAPPER statements like @ART
support evaluation of multiple expressions in a
single statement and use of variables created internally by the statement (a
,
b
, and so on). In MJ, the TEMP
namespace enables a formula
to access values returned from previously evaluated formulas. By itself, a formula can only reference a variable
in the TEMP
namespace; it is up to the caller of the formula to instantiate temporary variables and otherwise
manage the TEMP
namespace. This example illustrates usage of
the TEMP
namespace:
// Create MJMathFormula which can evaluate formulas converted from @ART
MJVariableNamespace globalNS = new MJVariableNamespace(VariableScope.GLOBAL);
MJVariableNamespace envNS = new MJVariableNamespace(VariableScope.ENVIRONMENT);
MJVariableNamespace localNS = new MJVariableNamespace(VariableScope.LOCAL);
MJVariableNamespace tempNS = new MJVariableNamespace(VariableScope.TEMP);
MJMathFormulaContext formulaContext = new MJMathFormulaContext(globalNS, envNS, localNS, tempNS);
IFormula<Long, MJMathFormulaContext> formula = new MJMathFormulaFactory().createIntegerFormula(
EnumSet.of(MathOption.EVALUATE_MAPPER_TYPE, MathOption.PREFER_DECIMAL_OVER_FLOAT));
// @art 43-2*7;12-2;count=a-b <answer1>i2,<answer2>i2,<answer3>i2 .
MJFormulaResult<Long> longFmlResult = formula.evaluate("43-2*7", formulaContext);
MJNamedInteger a = new MJNamedInteger("a", VariableScope.TEMP, MJVariable.MAX_INTEGER_SIZE,
longFmlResult.result());
tempNS.addVariable(a);
assert "29".equals(a.toMapperNumericString());
longFmlResult = formula.evaluate("12-2", formulaContext);
MJNamedInteger b = new MJNamedInteger("b", VariableScope.TEMP, MJVariable.MAX_INTEGER_SIZE,
longFmlResult.result());
tempNS.addVariable(b);
assert "10".equals(b.toMapperNumericString());
longFmlResult = formula.evaluate("TEMP.a-TEMP.b", formulaContext);
MJNamedInteger count = new MJNamedInteger("count", VariableScope.LOCAL, MJVariable.MAX_INTEGER_SIZE,
longFmlResult.result());
assert "19".equals(count.toMapperNumericString());
Example (@ART
Numeric Literals and Formula Re-use)
The two math formulae below are equivalent to @ART
statements (shown
in comments) that contain only numbers. Factory
method MJMathFormulaFactory.createDecimalFormula
instantiates a math formula that returns a java.math.BigDecimal
and
is passed MathOption.EVALUATE_MAPPER_TYPE
to best approximate how
@ART
performs mathematical calculations:
// Create MJMathFormula which can evaluate formulas converted from @ART
MJVariableIdentifierTransformer varIdentXform = new TestVariableIdentifierTransformer();
MJVariableNamespace globalNS = new MJVariableNamespace(VariableScope.GLOBAL);
MJVariableNamespace envNS = new MJVariableNamespace(VariableScope.ENVIRONMENT);
MJVariableNamespace localNS = new MJVariableNamespace(VariableScope.LOCAL);
MJVariableNamespace tempNS = new MJVariableNamespace(VariableScope.TEMP);
MJMathFormulaContext formulaContext = new MJMathFormulaContext(globalNS, envNS, localNS, tempNS);
IFormula<BigDecimal, MJMathFormulaContext> formula = new MJMathFormulaFactory().createDecimalFormula(
EnumSet.of(MathOption.EVALUATE_MAPPER_TYPE, MathOption.PREFER_DECIMAL_OVER_FLOAT));
// @ART 5+3**4/2 <result>f12.9 .
String equation = "n(5) + n(3) ^ n(4) / n(2)";
MJFormulaResult<BigDecimal> decFmlResult = formula.evaluate(equation, formulaContext);
MJDecimal result = new MJDecimal(VariableScope.LOCAL, 12, 9, decFmlResult.result());
assert "45.500000000".equals(result.toMapperNumericString());
// @ART (2/3)**4 <result>f12.10 .
equation = "(n(2) / n(3)) ^ n(4)";
decFmlResult = formula.evaluate(equation, formulaContext);
result = new MJDecimal(VariableScope.LOCAL, 12, 10, decFmlResult.result());
assert "0.1975308642".equals(result.toMapperNumericString());
Example (@CHG
Quoted Numeric Literals)
This example illustrates the use of quoted numeric literals with n()
to avoid floating
point imprecision in formulas. Factory method
MJMathFormulaFactory.createFloatFormula
instantiates a math formula that returns a java.lang.Double
and is passed
MathOption.EVALUATE_JAVA_TYPE to use Java primitive values in calculations,
emulating how @CHG
performs calculations:
// Create MJMathFormula which can evaluate formulas converted from @CHG
MJMathFormulaContext formulaContext = new MJMathFormulaContext(globalNS, envNS, localNS, tempNS);
IFormula<Double, MJMathFormulaContext> formula = new MJMathFormulaFactory().createFloatFormula(
EnumSet.of(MathOption.EVALUATE_JAVA_TYPE, MathOption.PREFER_DECIMAL_OVER_FLOAT));
// @CHG <result>f18.17 2.8 + 0.17 .
String equation = "2.8 + 0.17";
MJFormulaResult<Double> dblFmlResult = formula.evaluate(equation, formulaContext);
assert "2.9699999999999998".equals(BigDecimal.valueOf(dblFmlResult.result()).toPlainString()) :
"formula result is NOT IMPRECISE";
// same formula with quoted numeric literals, interpreted as BigDecimal
equation = "n('2.8') + n('0.17')";
dblFmlResult = formula.evaluate(equation, formulaContext);
assert "2.97".equals(BigDecimal.valueOf(dblFmlResult.result()).toPlainString()) :
"formula result is IMPRECISE";
Example (Simple @CHG
Expression with Variables)
This example formula multiplies two local variables and is equivalent to the @CHG
statement
shown in the comment below. EVALUATE_JAVA_TYPE is
passed to MJMathFormulaFactory.createDecimalFormula
to use Java primitive values in calculations, emulating @CHG
:
// Create MJMathFormula which can evaluate formulas converted from @CHG
MJMathFormulaContext formulaContext = new MJMathFormulaContext(globalNS, envNS, localNS, tempNS);
IFormula<BigDecimal, MJMathFormulaContext> formula = new MJMathFormulaFactory().createDecimalFormula(
EnumSet.of(MathOption.EVALUATE_JAVA_TYPE, MathOption.PREFER_DECIMAL_OVER_FLOAT));
// @LDV <sales>i9=78900, <percent>f6.4=0.125 .
MJNamedInteger sales = new MJNamedInteger("sales", VariableScope.LOCAL, 9,
Long.valueOf(78900));
localNS.addVariable(sales);
MJNamedDecimal percent = new MJNamedDecimal("percent", VariableScope.LOCAL, 6, 4,
BigDecimal.valueOf(125, 3));
localNS.addVariable(percent);
// @CHG <commission>f12.2 <sales> * <percent> .
String equation = "sales * percent";
MJFormulaResult<BigDecimal> decFmlResult = formula.evaluate(equation, formulaContext);
MJDecimal commission = new MJDecimal(VariableScope.LOCAL, 12, 2, decFmlResult.result());
assert "9862.50".equals(commission.toMapperNumericString());
Example (@CHG
Expression Requires Parentheses)
The following formula is equivalent to the @CHG
statement in the
comment, except that parentheses are required to retain the left-to-right order of evaluation
in the @CHG
statement:
// Create MJMathFormula which can evaluate formulas converted from @CHG
MJMathFormulaContext formulaContext = new MJMathFormulaContext(globalNS, envNS, localNS, tempNS);
IFormula<BigDecimal, MJMathFormulaContext> formula = new MJMathFormulaFactory().createDecimalFormula(
EnumSet.of(MathOption.EVALUATE_JAVA_TYPE, MathOption.PREFER_DECIMAL_OVER_FLOAT));
// @LDV <amt>f12.2=10000, <irate>f9.6=0.0825 .
MJNamedDecimal amt = new MJNamedDecimal("amt", VariableScope.LOCAL, 12, 2,
BigDecimal.valueOf(10000, 0));
localNS.addVariable(amt);
MJNamedDecimal irate = new MJNamedDecimal("irate", VariableScope.LOCAL, 9, 6,
BigDecimal.valueOf(825, 4));
localNS.addVariable(irate);
// @CHG <fv>f14.3 1.0 + <irate> * <amt> .
String equation = "(1.0 + irate) * amt";
MJFormulaResult<BigDecimal> decFmlResult = formula.evaluate(equation, formulaContext);
MJDecimal fv = new MJDecimal(VariableScope.LOCAL, 14, 3, decFmlResult.result());
assert "10825.000".equals(fv.toMapperNumericString());
Example (@ART
Expression with Indirect Global and Environmental Variables)
This example shows how direct, indirect, named, numbered, global, environmental and
local variables in an @ART
statement are referenced in an equivalent math formula. More
specifically, the example shows usage of built-in namespace constants GLOB
and
ENV
and built-in functions varByName
and varByNum
.
// Create MJMathFormula which can evaluate formulas converted from @ART
MJMathFormulaContext formulaContext = new MJMathFormulaContext(
globalNS, envNS, localNS, tempNS, varIdentXform);
IFormula<BigDecimal, MJMathFormulaContext> formula = new MJMathFormulaFactory().createDecimalFormula(
EnumSet.of(MathOption.EVALUATE_MAPPER_TYPE, MathOption.PREFER_DECIMAL_OVER_FLOAT));
// @LDV <$bVal>f10.5=234.3462 .
INamedVariable bVal = new MJNamedDecimal("bVal", VariableScope.ENVIRONMENT, 10, 5,
new BigDecimal("234.3462"));
envNS.addVariable(bVal);
// @LDV,P <$b>h10='$bVal' .
INamedVariable b = new MJNamedString("b", VariableScope.ENVIRONMENT,
MaprptVariableType.HOLLERITH, 10, EnumSet.of(LoadOption.PACK), bVal.getVariableName());
envNS.addVariable(b);
// @LDV <*cVal>f10.5=84124.52 .
INamedVariable cVal = new MJNamedDecimal("cVal", VariableScope.GLOBAL, 10, 5,
new BigDecimal("84124.52"));
globalNS.addVariable(cVal);
// @LDV <*c>h10='*cVal' .
INamedVariable c = new MJNamedString("c", VariableScope.GLOBAL,
MaprptVariableType.HOLLERITH, 10, EnumSet.noneOf(LoadOption.class), cVal.getVariableName());
globalNS.addVariable(c);
// @LDV V1i5=34, V22i3=1 .
INumberedVariable V1 = new MJNumberedInteger(1, VariableScope.LOCAL, 5, Long.valueOf(34));
localNS.addVariable(V1);
INumberedVariable V22 = new MJNumberedInteger(22, VariableScope.LOCAL, 3, Long.valueOf(1));
localNS.addVariable(V22);
// @ART 3548.85+<<$b>>-<<*c>>+VV22 <result>f11.5 .
String equation = "n('3548.85') + varByName(ENV.b) - varByName(GLOB.c) + varByNum(v22)";
MJFormulaResult<BigDecimal> decFmlResult = formula.evaluate(equation, formulaContext);
MJDecimal result = new MJDecimal(VariableScope.LOCAL, 11, 5, decFmlResult.result());
assert "-80307.3238".equals(result.toMapperNumericString());
Example (@ART
Expression with MOD and Integer Division)
This example illustrates the replacement of integer division (//
)
and MOD
(modulus)
in an @ART
statement with div()
and
the modulus operator (%
) in an equivalent math formula:
// Create MJMathFormula which can evaluate formulas converted from @ART
MJMathFormulaContext formulaContext = new MJMathFormulaContext(globalNS, envNS, localNS, tempNS);
IFormula<BigDecimal, MJMathFormulaContext> formula = new MJMathFormulaFactory().createDecimalFormula(
EnumSet.of(MathOption.EVALUATE_MAPPER_TYPE, MathOption.PREFER_DECIMAL_OVER_FLOAT));
// @LDV <divisor>i5=34 .
INamedVariable divisor = new MJNamedInteger("divisor", VariableScope.LOCAL, 5, Long.valueOf(34));
localNS.addVariable(divisor);
// @ART MOD('184.352328'//<divisor>,2) <result>f12.7
String equation = "div(n('184.352328'), divisor) % n(2)";
MJFormulaResult<BigDecimal> decFmlResult = formula.evaluate(equation, formulaContext);
MJDecimal result = new MJDecimal(VariableScope.LOCAL, 12, 7, decFmlResult.result());
assert "1.0000000".equals(result.toMapperNumericString());
Example (@ART
Expression with Array)
This example shows how an array in an @ART
statement
is referenced in an equivalent math formula.
// Create MJMathFormula which can evaluate formulas converted from @ART
MJMathFormulaContext formulaContext = new MJMathFormulaContext(
globalNS, envNS, localNS, tempNS);
IFormula<BigDecimal, MJMathFormulaContext> formula = new MJMathFormulaFactory().createDecimalFormula(
EnumSet.of(MathOption.EVALUATE_MAPPER_TYPE, MathOption.PREFER_DECIMAL_OVER_FLOAT));
// @LDV <dscLvl>i3=2, <price>f10.2=2500.00 .
MJNamedInteger dscLvl = new MJNamedInteger("dscLvl", VariableScope.LOCAL, 3, 2L);
localNS.addVariable(dscLvl);
MJNamedDecimal price = new MJNamedDecimal("price", VariableScope.LOCAL,
10, 2, BigDecimal.valueOf(2500, 0));
localNS.addVariable(price);
// @LDA <disc>f7.3[4]=0.05,0.10, 0.275, 0.45 .
List<MJDecimal> disc = MJSubscriptProxyFactory.countFromOneListProxy(
new ArrayList<MJDecimal>(Arrays.asList(new MJDecimal[] {
new MJDecimal(VariableScope.LOCAL, 7, 3, BigDecimal.valueOf(0.05)),
new MJDecimal(VariableScope.LOCAL, 7, 3, BigDecimal.valueOf(0.10)),
new MJDecimal(VariableScope.LOCAL, 7, 3, BigDecimal.valueOf(0.275)),
new MJDecimal(VariableScope.LOCAL, 7, 3, BigDecimal.valueOf(0.45))
}))
);
localNS.addArray("disc", disc);
// @ART <disc>[<dscLvl>]*<price> <dscAmt>f10.2 .
String equation = "disc[LOCAL.dscLvl] * price";
MJFormulaResult<BigDecimal> decFmlResult = formula.evaluate(equation, formulaContext);
MJDecimal dscAmt = new MJDecimal(VariableScope.LOCAL, 10, 2, decFmlResult.result());
assert "250.00".equals(dscAmt.toMapperNumericString());