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 name
s = subscript of array element
elemByName(GLOB.arrptr,10)
elemByNum(v, s) v = variable containing array number
s = 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, GLOBal or ENVironmental. 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
MJVariableNamespace localNamespace =
  new MJVariableNamespace(VariableScope.LOCAL);
INamedVariable localA = new MJNamedInteger("a", VariableScope.LOCAL,
  5, Long.valueOf(365));
localNamespace.addVariable(localA);
LOCAL.a <a>
INamedVariable localFoo = new MJNamedString("foo", VariableScope.LOCAL,
  MaprptVariableType.STRING, 5, EnumSet.noneOf(LoadOption.class), "a");
localNamespace.addVariable(localFoo);
varByName(foo) <<foo>>
INumberedVariable localV1 = new MJNumberedInteger(1, VariableScope.LOCAL,
  5, Long.valueOf(34));
localNamespace.addVariable(localV1);
LOCAL.v1 v1
INumberedVariable localV97 = new MJNumberedInteger(97, VariableScope.LOCAL,
  3, Long.valueOf(1));
localNamespace.addVariable(localV1);
varByNum(v97) VV97
MJVariableNamespace globalNamespace =
  new MJVariableNamespace(VariableScope.GLOBAL);
INamedVariable globA = new MJNamedInteger("a", VariableScope.GLOBAL,
  5, Long.valueOf(1001));
globalNamespace.addVariable(globA);
GLOB.a <*a>
INamedVariable globFoo = new MJNamedString("foo", VariableScope.GLOBAL,
  MaprptVariableType.STRING, 5, EnumSet.noneOf(LoadOption.class), "a");
globalNamespace.addVariable(globFoo);
varByName(GLOB.foo) <<*foo>>
MJVariableNamespace envNamespace =
  new MJVariableNamespace(VariableScope.ENVIRONMENT);
INamedVariable envA = new MJNamedInteger("a", VariableScope.ENVIRONMENT,
  5, Long.valueOf(-88));
envNamespace.addVariable(envA);
ENV.a <$a>
INamedVariable envFoo = new MJNamedString("foo", VariableScope.ENVIRONMENT,
  MaprptVariableType.STRING, 5, EnumSet.noneOf(LoadOption.class), "a");
envNamespace.addVariable(envFoo);
varByName(ENV.foo) <<$foo>>
INamedVariable ptrGlob = new MJNamedString("ptrGlob", VariableScope.LOCAL,
  MaprptVariableType.STRING, 5, EnumSet.noneOf(LoadOption.class), "*a");
localNamespace.addVariable(ptrGlob);
varByName(ptrGlob) <<ptrGlob>>
INamedVariable ptrEnv = new MJNamedString("ptrEnv", VariableScope.LOCAL,
  MaprptVariableType.STRING, 5, EnumSet.noneOf(LoadOption.class), "$a");
localNamespace.addVariable(ptrEnv);
varByName(ptrEnv) <<ptrEnv>>

Arrays

Like variables, arrays are referenced in a formula by array name and namespace; the namespace identifies the array as LOCAL, GLOBal or ENVironmental. 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
MJVariableNamespace localNamespace =
  new MJVariableNamespace(VariableScope.LOCAL);
List localArr = MJSubscriptProxyFactory.countFromOneListProxy(
  Arrays.asList(new MJInteger[1]));
localArr.set(1, new MJInteger(VariableScope.LOCAL, 3, Long.valueOf(277)));
localNamespace.addArray("arr", localArr);
LOCAL.arr[1] <arr>[1]
INamedVariable localPtr = new MJNamedString("ptr", VariableScope.LOCAL,
  MaprptVariableType.STRING, 5, EnumSet.noneOf(LoadOption.class), "arr");
localNamespace.addVariable(localPtr);
elemByName(ptr,1) <<ptr>>[1]
List localV1 = MJSubscriptProxyFactory.countFromOneListProxy(
  Arrays.asList(new MJInteger[2]));
localV1.set(1, new MJInteger(VariableScope.LOCAL, 5, Long.valueOf(1856)));
localV1.set(2, new MJInteger(VariableScope.LOCAL, 5, Long.valueOf(2525)));
localNamespace.addArray(1, localV1);
LOCAL.v1[2] V1[2]
INumberedVariable localV99 = new MJNumberedInteger(99, VariableScope.LOCAL,
 3, Long.valueOf(1));
localNamespace.addVariable(localV99);
elemByNum(v99,2) VV99
MJVariableNamespace globalNamespace =
  new MJVariableNamespace(VariableScope.GLOBAL);
List _arr = MJSubscriptProxyFactory.countFromOneListProxy(
  Arrays.asList(new MJInteger[1]));
_arr.set(1, new MJInteger(VariableScope.LOCAL, 5, Long.valueOf(9801)));
globalNamespace.addArray("_arr", _arr);
GLOB._arr[1] <*arr>[1]
INamedVariable globRef = new MJNamedString("ref", VariableScope.GLOBAL,
  MaprptVariableType.STRING, 5, EnumSet.noneOf(LoadOption.class), "*arr");
globalNamespace.addVariable(globRef);
elemByName(GLOB.ref,1) <<*ref>>[1]
MJVariableNamespace envNamespace =
  new MJVariableNamespace(VariableScope.ENVIRONMENT);
List $arr = MJSubscriptProxyFactory.countFromOneListProxy(
  Arrays.asList(new MJInteger[3]));
$arr.set(1, new MJInteger(VariableScope.LOCAL, 3, Long.valueOf(123)));
$arr.set(2, new MJInteger(VariableScope.LOCAL, 3, Long.valueOf(456)));
$arr.set(3, new MJInteger(VariableScope.LOCAL, 3, Long.valueOf(789)));
envNamespace.addArray("$arr", $arr);
ENV.$arr[3] <$arr>[3]
INumberedVariable v5 = new MJNumberedInteger(5, VariableScope.LOCAL,
  3, Long.valueOf(1));
localNamespace.addVariable(v5);
INamedVariable envptr = new MJNamedString("envptr", VariableScope.LOCAL,
  MaprptVariableType.STRING, 5, EnumSet.noneOf(LoadOption.class), "$arr");
localNamespace.addVariable(envptr);
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());