AFC - Abacus Formula Compiler for Java

Binding To Parametrized Methods

If you have a generic system where the set of possible input and/or output values cannot be determined at compile-time, you have to resort to some sort of binding to parametrized methods. The parameter value is anything that helps you to identify a value, be it a String, int, or long value, or even a combination thereof.

Typically, you analyze the names defined in the spreadsheet and decompose them into the name of the method to call/implement, and the parameter values to that method. For example, USERFIELD_XY might mean to call the input method userField("XY"). Users will likely be more comfortable with an approach where they can set up mappings from cell names to input/output mappings in a dedicated GUI, rather than having to encode parameters in cell names. Whether to offer your users this comfort is up to you.

Inputs

You need to bind inputs to parametrized calls, possible to a single dispatch method, or multiple ones – it’s your choice. Here’s an example of such a method:

public static interface Input
{
  double getInput( String _valueName );
}

With this interface, you could, for instance, bind cell names as follows:

final Method inputMethod = Input.class.getMethod( "getInput", String.class );
for (Map.Entry<String, Spreadsheet.Range> def : spreadsheet.getRangeNames().entrySet()) {
  final Spreadsheet.Range range = def.getValue();
  if (range instanceof Spreadsheet.Cell) {
    final String cellName = def.getKey();
    if ("I_".equals( cellName.substring( 0, 2 ) )) {
      final Spreadsheet.Cell cell = (Spreadsheet.Cell) range;
      final String valueName = cellName.substring( 2 ).toUpperCase();
      binder.defineInputCell( cell, inputMethod, valueName );
    }
  }
}

In the implementation of getInput(String) you are free to look up the actual value of the named input in any way that seems fit. Like this, for example:

public double getInput( String _valueName )
{
  if (_valueName.equals( "ONE" )) return 1.0;
  if (_valueName.equals( "TWO" )) return 2.0;
  if (_valueName.equals( "THREE" )) return 3.0;
  return 0.0;
}

Supported Types

AFC currently supports the following parameter types:

// Native types
public double getInput( byte _param ) 
public double getInput( short _param ) 
public double getInput( int _param ) 
public double getInput( long _param ) 
public double getInput( double _param ) 
public double getInput( float _param ) 
public double getInput( char _param ) 
public double getInput( boolean _param ) 

// Boxed types
public double getInput( Byte _param ) 
public double getInput( Short _param ) 
public double getInput( Integer _param ) 
public double getInput( Long _param ) 
public double getInput( Double _param ) 
public double getInput( Float _param ) 
public double getInput( Character _param ) 
public double getInput( Boolean _param ) 

// Other types
public double getInput( String _param ) 

// Application-defined enumerations
public double getInput( MyEnum _param ) 

Note in particular that custom, application defined enumeration constants are supported. Here’s the definition for the MyEnum type bound above:

public static enum MyEnum {
  ZERO, ONE, TWO;
}

When binding, you have to take care that you are passing in values of the proper type (Integer for int and Integer, Byte for byte and Byte, etc.). This example uses autoboxing, but it still needs to take care to pass the proper native type to the boxing magic:

// Native types
bnd.defineInputCell( ss.getCell( "byte" ), ic.getMethod( "getInput", Byte.TYPE ), (byte) 123 );
bnd.defineInputCell( ss.getCell( "short" ), ic.getMethod( "getInput", Short.TYPE ), (short) 1234 );
bnd.defineInputCell( ss.getCell( "int" ), ic.getMethod( "getInput", Integer.TYPE ), 12345 );
bnd.defineInputCell( ss.getCell( "long" ), ic.getMethod( "getInput", Long.TYPE ), 123456L );
bnd.defineInputCell( ss.getCell( "double" ), ic.getMethod( "getInput", Double.TYPE ), 123.45 );
bnd.defineInputCell( ss.getCell( "float" ), ic.getMethod( "getInput", Float.TYPE ), 123.456F );
bnd.defineInputCell( ss.getCell( "char" ), ic.getMethod( "getInput", Character.TYPE ), 'a' );
bnd.defineInputCell( ss.getCell( "bool" ), ic.getMethod( "getInput", Boolean.TYPE ), true );

// Boxed types
bnd.defineInputCell( ss.getCell( "bbyte" ), ic.getMethod( "getInput", Byte.class ), (byte) 123 );
bnd.defineInputCell( ss.getCell( "bshort" ), ic.getMethod( "getInput", Short.class ), (short) 1234 );
bnd.defineInputCell( ss.getCell( "bint" ), ic.getMethod( "getInput", Integer.class ), 12345 );
bnd.defineInputCell( ss.getCell( "blong" ), ic.getMethod( "getInput", Long.class ), 123456L );
bnd.defineInputCell( ss.getCell( "bdouble" ), ic.getMethod( "getInput", Double.class ), 123.45 );
bnd.defineInputCell( ss.getCell( "bfloat" ), ic.getMethod( "getInput", Float.class ), 123.456F );
bnd.defineInputCell( ss.getCell( "bchar" ), ic.getMethod( "getInput", Character.class ), 'a' );
bnd.defineInputCell( ss.getCell( "bbool" ), ic.getMethod( "getInput", Boolean.class ), true );

// Other types
bnd.defineInputCell( ss.getCell( "string" ), ic.getMethod( "getInput", String.class ), "123.4567" );

// Application-defined enumerations
bnd.defineInputCell( ss.getCell( "enum" ), ic.getMethod( "getInput", MyEnum.class ), MyEnum.TWO );

Multiple Parameters

You can also bind to parametrized methods with multiple parameters as long as all the parameter types are supported. Here’s such a method:

public double getInput( int _a, boolean _b, String _c ) 

and how to bind to it:

Method mtd = ic.getMethod( "getInput", Integer.TYPE, Boolean.TYPE, String.class );
bnd.defineInputCell( ss.getCell( "comb" ), mtd, 12, true, "24" );

Outputs

To bind output values to parametrized methods, you use an analogous construct:

public static abstract class Output
{
  public double getOutput( String _valueName )
  {
    return -1;
  }
}

and:

final Method outputMethod = Output.class.getMethod( "getOutput", String.class );
for (Map.Entry<String, Spreadsheet.Range> def : spreadsheet.getRangeNames().entrySet()) {
  final Spreadsheet.Range range = def.getValue();
  if (range instanceof Spreadsheet.Cell) {
    final String cellName = def.getKey();
    if ("O_".equals( cellName.substring( 0, 2 ) )) {
      final Spreadsheet.Cell cell = (Spreadsheet.Cell) range;
      final String valueName = cellName.substring( 2 ).toUpperCase();
      binder.defineOutputCell( cell, outputMethod, valueName );
    }
  }
}

Here’s how you would use the generated engine:

assertEquals( 6.0, output.getOutput( "ONETWOTHREE" ), 0.001 );
assertEquals( 8.0, output.getOutput( "SUMINTER" ), 0.001 );
assertEquals( -1.0, output.getOutput( "UNDEF" ), 0.001 );

AFC must generate code that implements this by-parameter lookup for you to make this work. Here’s how AFC goes about it, assuming the spreadsheet contained two cells named O_ONETWOTHREE and O_SUMINTER:

@Override
public double getOutput( String _valueName )
{
  if (_valueName.equals( "ONETWOTHREE" )) return getOutput__1();
  if (_valueName.equals( "SUMINTER" )) return getOutput__2();
  // ... other bound outputs
  return super.getOutput( _valueName );
}

private double getOutput__1()
{
  // Generated computation for cell ONETWOTHREE (corresponds to test spreadsheet):
  return getInput__1() + getInput__2() + getInput__3();
}

private double getOutput__2()
{
  // Generated computation for cell SUMINTER (corresponds to test spreadsheet):
  return getInter__1() + getInter__2();
}

The example shows why we must supply a default implementation for getResult(String).

Supported Types

Parametrized outputs are supported for the following parameter types:

  • Object and subtypes that properly implement equals()
  • int, compared using ==
  • long, compared using ==

Multiple Parameters

When there are multiple parameters, AFC generates comparisons for all of the supplied values, joined by a logical and.

Thus, the complex output method:

public static abstract class ComplexOutput
{
  public double getComplex( int _int, long _long, String _string )
  {
    return -1;
  }
}

is bound as:

Method outputMethod = ComplexOutput.class.getMethod( "getComplex", Integer.TYPE, Long.TYPE, String.class );
binder.defineOutputCell( spreadsheet.getCell( "Complex" ), outputMethod, 1, 2, "THREE" );

and called as:

assertEquals( 5.0, output.getComplex( 1, 2, "THREE" ), 0.001 );

// Check undefined results by incrementing each argument in turn:
assertEquals( -1.0, output.getComplex( 2, 2, "THREE" ), 0.001 );
assertEquals( -1.0, output.getComplex( 1, 3, "THREE" ), 0.001 );
assertEquals( -1.0, output.getComplex( 1, 2, "FOUR" ), 0.001 );

Here’s an idea of what AFC generates in this situation:

@Override
public double getComplex( int _int, long _long, String _string )
{
  if (_int == 1 && _long == 2 && _string.equals( "THREE" )) return getComplex__1();
  // ... other bound outputs
  return super.getComplex( _int, _long, _string );
}

Why This Magic?

Why does AFC provide such black-box magic here when, for the inputs, it was stated as an explicit design goal to avoid such things? Well, providing the outputs is what AFC does. So the implementation of the output interface must be AFC’s responsibility. We could have chosen an implementation where you could register multiple output interfaces, each with a distinct string name. You might then bind output cells to such a named interface. However, to access the named interface on a computation, there would again have to be a by-name lookup:

Computation c = engine.newComputation();
Output o = (Output) c.getNamedOutput( "SomeOutput" );
double v = o.getValue();

So we gain nothing but lose the close duality of how the input and output definitions work.