Custom Formula Functions
Overview
Spreadsheet allows developers to implement their own custom functions
and use these functions like built-in ones. You can just enter your
custom function like =MYFFUNCTION(A1)
and it works like other
functions. To implement such a custom method is easy: just create a
static method and declare it in a ZUL page with EL method or tag
library.
Steps to add a custom function:
- Implement a custom function with a public static method.
- Declare a custom function in a ZUL page.
- You could declare it with xel-method or custom tag library. Of course, the ZUL page should contain a Spreadsheet component.
After completing above steps, you can write the custom function in a formula.
Only Work in Keikai spreadsheet
Notice that a custom function is implemented with Java in your project. That means a
custom function is only evaluated and recognized when you load a file
into Keikai spreadsheet. If you open the exported file with a custom
function by MS Excel (or other compatible editors), Excel cannot
recognize the custom function and will show #NAME?
(or might be other
error results) in a cell.
Implement a Custom Function
Basically, to implement a function is just like implementing a static Java method, but depending on your requirements there are several ways to do it. We will introduce them from easy to complicated one.
Basic - Simple Function
If your function accepts fixed number of arguments and each argument is a single value (or a reference to a single cell), you can use this way to implement your custom function.
Assume that we are going to create a money exchange function,
EXCHANGE()
, that accepts 2 double arguments and return exchange
result. The first one is money, and the second one is exchange rate.
The custom function can be implemented like:
1
2
3
4
5
6
public class MyCustomFunctions {
public static double exchange(double money, double exchangeRate) {
return money * exchangeRate;
}
}
After declaring it in a ZUL page, you can use this function like
=EXCHANGE(10, 31.3)
or =EXCHANGE(C3, D3)
. Spreadsheet will evaluate
each argument and pass them to exchange()
method. If you pass a
reference to a range of cells in this function, e.g. =EXCHANGE(C3:C4,
D3)
, only the first cell will be evaluated.
Intermediate - Multiple Numeric Arguments Function
If your function needs to accept a range of cells with numeric value or variable number of numeric arguments, you should follow the steps below:
- Create a class
MyNumericFunction
inherited fromorg.zkoss.poi.ss.formula.functions.MultiOperandNumericFunction
and override itsevaluate(double[])
.
MultiOperandNumericFunction
can evaluate various arguments to double including a range of cells, string, and boolean etc. You can benefit from this behavior instead of handling variousorg.zkoss.poi.ss.formula.eval.ValueEval
by yourself. - Create a public static method with specific signature:
public static ValueEval yourFunctionName(ValueEval[] , int , int )
You should not change this signature because Spreadsheet recognize your method by the signature. - In your static method (
yourFunctionName()
), invokeMyNumericFunction.evaluate(ValueEval[] , int , int)
to calculate.
Assume that we are going to create a custom function, MYSUBTOTAL()
, that
accepts variable number of numeric arguments and sums them all.
First, we create a class to implement my subtotal function:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class MySubtotal extends MultiOperandNumericFunction{
protected MySubtotal() {
// the first parameter determines whether to evaluate boolean value. If
// it's true, evaluator will evaluate boolean value to number. TRUE to 1
// and FALSE to 0.
// If it's false, boolean value is just ignored.
// The second parameter determines whether to evaluate blank value.
super(false, false);
}
/**
* inherited method, ValueEval evaluate(ValueEval[] args, int srcCellRow,
* int srcCellCol), will call this overridden method This function depends
* on MultiOperandNumericFunction that evaluates all arguments to double.
* This function sums all double values in cells.
*/
@Override
protected double evaluate(double[] values) throws EvaluationException {
double sum = 0;
for (int i = 0 ; i < values.length ; i++){
sum += values[i];
}
return sum;
}
}
- Line 19: The overloading
evaluate(ValueEval[], int, int)
it inherits fromMultiOperandNumericFunction
will process all arguments to a double array and pass it to your overridden method,evaluate(double[])
. It can save your effort to deal with each argument. If you encounter a situation that you don’t expect, please throworg.zkoss.poi.ss.formula.eval.EvaluationException
. Because Spreadsheet can handle the exception gracefully.
Then, create a static method with previously-mentioned signature and
delegate method calling to MySubtotal
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyCustomFunctions {
private static Function MY_SUBTOTAL = new MySubtotal();
/**
* This method delegates calling to MySubtotal which implements the function.
* @param args evaluation of all arguments
* @param srcCellRow row index of the cell containing the function under evaluation
* @param srcCellCol column index of the cell containing the function under evaluation
* @return function result
*/
public static ValueEval mySubtotal(ValueEval[] args, int srcCellRow, int srcCellCol){
return MY_SUBTOTAL.evaluate(args, srcCellRow, srcCellCol);
}
}
- Line 13: Delegate method calling to to
MySubtotal
which implements the function actually.
Advanced - Manually-Handled Arguments Function
If your function needs to handle arguments by yourself instead of always evaluating them as numbers, you should create a public static method with the signature:
public static ValueEval yourFunctionName(ValueEval[] , int , int )
You should not change this signature because Spreadsheet recognizes your
method by the signature. Spreadsheet will pass all arguments as an array
of ValueEval
, and you have to process each ValueEval
to fulfill your
function’s requirement.
Here, we demonstrate this approach with a function, CHAIN()
, that
chains multiple text cells into one text. Below screenshot shows its use
cases:
You can see that this function can accept various arguments including
string, a range of cells, and multiple cell references. Different kind
of arguments will be evaluated to different subclass of ValueEval
, and
you should handle them in your function method to make your custom
function support these use cases.
The function to chain text
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public class MyCustomFunctions {
/**
* Advanced - Manually-Handled Arguments Function.
* This method demonstrates how to evaluate arguments manually.
* The method implements a function that concatenates all texts in cells
* and ignores other types of value.
*
* @param args the evaluated function arguments
* @param srcCellRow unused
* @param srcCellCol unused
* @return calculated result, a subclass of ValueEval
*/
public static ValueEval chain(ValueEval[] args, int srcCellRow, int srcCellCol){
List<StringEval> stringList = new LinkedList<StringEval>();
for (int i = 0 ; i < args.length ; i++){
//process an argument like A1:B2
if (args[i] instanceof TwoDEval) {
TwoDEval twoDEval = (TwoDEval) args[i];
int width = twoDEval.getWidth();
int height = twoDEval.getHeight();
for (int rowIndex=0; rowIndex<height; rowIndex++) {
for (int columnIndex=0; columnIndex<width; columnIndex++){
ValueEval ve = twoDEval.getValue(rowIndex, columnIndex);
if (ve instanceof StringEval){
stringList.add((StringEval)ve);
}
}
}
continue;
}
//process an argument like C18
if (args[i] instanceof RefEval){
ValueEval valueEval = ((RefEval)args[i]).getInnerValueEval();
if (valueEval instanceof StringEval){
stringList.add((StringEval)valueEval);
}
continue;
}
if (args[i] instanceof StringEval){
stringList.add((StringEval)args[i]);
continue;
}
}
//chain all string value
StringBuffer result = new StringBuffer();
for (StringEval s: stringList){
result.append(s.getStringValue());
}
//throw EvaluationException if encounter error conditions
return new StringEval(result.toString());
}
}
- Line 14: You should create a public static method with the same signature because Spreadsheet recognizes your function method by the signature.
- Line 19:
org.zkoss.poi.ss.formula.TwoDEval
is a common interface that represents a range of cells. Process it to make your function accepts an argument like A1:B2. In our example, we just get each text cell of it and ignore others. - Line 34:
org.zkoss.poi.ss.formula.eval.RefEval
represents an evaluation of a cell reference like “C18”. - Line 41:
org.zkoss.poi.ss.formula.eval.StringEval
is the evaluation result of a string like “abc”. - Line 53: We recommend you to throw an
EvaluationException
when you encounter an error condition. Because Spreadsheet will catch and handle it gracefully for you. - Line 54: Return an object of
ValueEval
’s subtype according to your result.
Declare a Custom Function in a ZUL Page
After implementing a method for a custom function, we can use one of the following ways to bring it in a ZUL page before using it in Spreadsheet.
Using EL Method
Use xel-method directive is quite straight out. Just write it on the ZUL page with prefix keikai and that is done. Please refer to ZUML_Reference/ZUML/Processing_Instructions/xel-method for complete explanation of each attribute.
For our custom function, we can write:
1
2
3
4
5
6
7
8
9
<?xel-method prefix="keikai" name="EXCHANGE"
class="io.keikai.essential.advanced.MyCustomFunctions"
signature="double exchange(double,double)"?>
...
<zk>
<spreadsheet src="/WEB-INF/books/customFunction.xlsx"
maxVisibleRows="250" maxVisibleColumns="40" width="100%" height="100%"
showContextMenu="true" showSheetbar="true" showToolbar="true" />
</zk>
- Line 1: Notice that “prefix” attribute must be set to “keikai” for Keikai Spreadsheet to find custom functions.
After declaring them, you can use them in a cell like
=EXCHANGE(10, 31.3)
.
Using Tag Library
To use taglib directive, we should create a taglib file and specify its
file path in uri
attribute of taglib directive and set “prefix” to
keikai. Please refer to
ZUML_Reference/ZUML/Processing_Instructions/taglib/Custom_Taglib for details. We list our sample configuration here:
function.tld
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?xml version="1.0" encoding="UTF-8" ?>
<taglib>
<uri>http://www.zkoss.org/keikai/custom</uri>
<description>
User defined functions.
</description>
<import>
<import-name>MyCustomFunctions</import-name>
<import-class>io.keikai.essential.advanced.MyCustomFunctions
</import-class>
</import>
<function>
<name>MYEXCHANGE</name>
<function-class>
io.keikai.essential.advanced.MyCustomFunctions
</function-class>
<function-signature>
double exchange(double,double);
</function-signature>
<description>
Exchange one money to another one according to specified exchange rate.
</description>
</function>
</taglib>
Declare it in a ZUL page.
1
2
3
4
5
6
<?taglib uri="/WEB-INF/tld/function.tld" prefix="keikai" ?>
<zk>
<spreadsheet src="/WEB-INF/books/customFunction.xlsx"
maxVisibleRows="250" maxVisibleColumns="40" width="100%" height="100%"
showContextMenu="true" showSheetbar="true" showToolbar="true" />
</zk>
- Line 1: Notice that “prefix” attribute must be set to “keikai” for Keikai Spreadsheet to find custom functions.
After completing above steps, you can use this custom function in
Spreadsheet like =MYEXCHANGE(5, 31.1)
.
Override Built-in Functions
If you give your customized function the same name as built-in function’s, it will override Spreadsheet built-in functions. Your customized function will be invoked instead of the built-in one.
Override LEN()
1
2
3
4
5
6
7
8
9
10
<?xel-method prefix="keikai" name="LEN"
class="io.keikai.essential.advanced.MyCustomFunctions"
signature="org.zkoss.poi.ss.formula.eval.ValueEval myLen(org.zkoss.poi.ss.formula.eval.ValueEval[], int, int)"?>
<zk>
<window title="Keikai spreadsheet" border="normal" height="100%">
<spreadsheet src="/WEB-INF/books/overrideFunction.xlsx"
maxVisibleRows="250" maxVisibleColumns="40" width="100%" height="100%"
showContextMenu="true" showSheetbar="true" showToolbar="true" />
</window>
</zk>
- In above code snippet, if we use
LEN()
in a formula,myLen()
will be invoked.
Naming
If you want to declare a function name that contains a dot (.
) e.g. T.EXCHANGE
, you should replace the dot with dollar sign ($
) like:
<?xel-method prefix="keikai" name="T$EXCHANGE" ...?>
A Function That Can Access Cells
since 5.8.0
Those EL functions mentioned at the sections above can’t access arbitrary cells because Keikai just passes resolved values into a function. If your custom function needs to access any cell depending on an argument like CELL(“type”, B1), you need to implement FreeRefFunction.
public class MyCell implements FreeRefFunction {
@Override
public ValueEval evaluate(ValueEval[] valueEvals, OperationEvaluationContext context) {
...
}
Register a FreeRefFunction
After implementing your custom function, to make keikai find your function, you also need to register it by ZKUDFFinder.putFunction()
.
public class FunctionRegistration implements WebAppInit {
@Override
public void init(WebApp wapp) throws Exception {
ZKUDFFinder.putFunction("mycell", new MyCell());
}
}
Comments