{"record":{"id":"2b88680de20564b6","repo":"pentaho/pentaho-kettle","slug":"the-a-b-function-only-works-on-numeric-data","errorCode":null,"errorMessage":"The 'A-B%' function only works on numeric data","messagePattern":"The 'A-B%' function only works on numeric data","errorType":"exception","errorClass":"KettleValueException","httpStatus":null,"severity":"error","filePath":"core/src/main/java/org/pentaho/di/core/row/ValueDataUtil.java","lineNumber":1035,"sourceCode":"  public static Object percent2( ValueMetaInterface metaA, Object dataA, ValueMetaInterface metaB, Object dataB ) throws KettleValueException {\n    if ( dataA == null || dataB == null ) {\n      return null;\n    }\n\n    switch ( metaA.getType() ) {\n      case ValueMetaInterface.TYPE_NUMBER:\n        return new Double( metaA.getNumber( dataA ).doubleValue()\n          - divideDoubles( multiplyDoubles( metaA.getNumber( dataA ), metaB.getNumber( dataB ) ), 100.0D ) );\n      case ValueMetaInterface.TYPE_INTEGER:\n        return new Long( metaA.getInteger( dataA ).longValue()\n          - divideLongs( multiplyLongs( metaA.getInteger( dataA ), metaB.getInteger( dataB ) ), 100L ) );\n      case ValueMetaInterface.TYPE_BIGNUMBER:\n        return metaA.getBigNumber( dataA ).subtract(\n          divideBigDecimals( multiplyBigDecimals(\n            metaB.getBigNumber( dataB ), metaA.getBigNumber( dataA ), null ), new BigDecimal( 100 ),\n            (MathContext) null ) );\n      default:\n        throw new KettleValueException( \"The 'A-B%' function only works on numeric data\" );\n    }\n  }\n\n  public static Object percent2( ValueMetaInterface metaA, Object dataA, ValueMetaInterface metaB, Object dataB, VariableSpace space ) throws KettleValueException {\n    if ( dataA == null || dataB == null ) {\n      return null;\n    }\n\n    switch ( metaA.getType() ) {\n      case ValueMetaInterface.TYPE_NUMBER:\n        return new Double( metaA.getNumber( dataA ).doubleValue()\n          - divideDoubles( multiplyDoubles( metaA.getNumber( dataA ), metaB.getNumber( dataB ) ), 100.0D ) );\n      case ValueMetaInterface.TYPE_INTEGER:\n        return new Long( metaA.getInteger( dataA ).longValue()\n          - divideLongs( multiplyLongs( metaA.getInteger( dataA ), metaB.getInteger( dataB ) ), 100L ) );\n      case ValueMetaInterface.TYPE_BIGNUMBER:\n        return metaA.getBigNumber( dataA ).subtract(\n          divideBigDecimals( multiplyBigDecimals(","sourceCodeStart":1017,"sourceCodeEnd":1053,"githubUrl":"https://github.com/pentaho/pentaho-kettle/blob/f3058517a153da500bf4551f46d79b91bf8ec552/core/src/main/java/org/pentaho/di/core/row/ValueDataUtil.java#L1017-L1053","documentation":"The 'A-B%' operation computes A - (A*B/100) and is implemented only for numeric types (Number, Integer, BigNumber). A non-numeric operand falls through to the default branch and throws this KettleValueException. The variable-space-less MathContext-null path is the one throwing here.","triggerScenarios":"ValueDataUtil.percent2-style 'A-B%' calculation where metaA or metaB type is String/Date/Boolean, e.g. a discount-percent field read as text.","commonSituations":"Discount or tax percentage columns imported as strings from Excel/CSV, JSON input fields typed as string, mapping mistakes in the Calculator step after schema changes.","solutions":["Convert the non-numeric field to Number/Integer (Select values step) before the 'A-B%' calculation","Fix the input step's field type so percentages are parsed as numeric at read time","Check the Calculator step's field A/B mappings for the 'A-B%' line","Prefer BigNumber for monetary A values to also avoid rounding surprises"],"exampleFix":"// before\nValueMetaInterface pctMeta = new ValueMetaString(\"discount_pct\");\nValueDataUtil.percent2(numMeta, dataA, pctMeta, dataB); // throws\n// after\nValueMetaInterface pctMeta = new ValueMetaNumber(\"discount_pct\");\nObject pct = ValueDataUtil.convertData(new ValueMetaString(\"discount_pct\"), dataB, pctMeta);\nValueDataUtil.percent2(numMeta, dataA, pctMeta, pct);","handlingStrategy":"type-guard","validationCode":"if (isNumericMeta(metaA) && isNumericMeta(metaB)) {\n  result = /* 'A-B%' call */;\n} else {\n  ValueMetaInterface np = new ValueMetaNumber(metaB != null ? metaB.getName() : \"b\");\n  Object p = ValueDataUtil.convertData(metaB, dataB, np);\n  // call with numeric metas\n}","typeGuard":"boolean isNumericMeta(ValueMetaInterface meta) {\n  if (meta == null) return false;\n  int t = meta.getType();\n  return t == ValueMetaInterface.TYPE_NUMBER\n    || t == ValueMetaInterface.TYPE_INTEGER\n    || t == ValueMetaInterface.TYPE_BIGNUMBER;\n}","tryCatchPattern":"try {\n  result = percentFn(metaA, dataA, metaB, dataB);\n} catch (KettleValueException e) {\n  if (e.getMessage().contains(\"A-B%\")) {\n    logError(\"Non-numeric operand for 'A-B%': \" + (metaB != null ? metaB.getName() + \"=\" + metaB.getTypeDesc() : \"?\"));\n  }\n  throw e;\n}","preventionTips":["Percent fields (discount, tax) imported from spreadsheets are the usual string culprits — type them at read time","Use BigNumber for the base amount A in financial contexts","Verify Calculator mappings after upstream renames","Fail fast with a Filter/validation step on typeof/Is Numeric conditions"],"tags":["kettle","pdi","calculator","type-mismatch","percent"],"backgroundTag":"type-mismatch","analyzedSha":"f3058517a153da500bf4551f46d79b91bf8ec552","analyzedAt":"2026-09-13T14:04:16.340Z","contentChangedAt":"2026-09-13T14:04:16.340Z","schemaVersion":2},"datasetVersion":"2026-09-20T23:17:15.980Z"}