This blog post will demonstrate many useful things:

1. How to implement a custom function for Spread.NET 12 Windows Forms that works just like the built-in functions in cell formulas
2. How to make a custom function return an array of values and how to use it in an array formula to show those values in a range of cells
3. How to make use of Parallel.For safely with Spread.NET 12 Windows Forms to optimize performance
4. How to port a legacy custom function using FarPoint.CalcEngine.FunctionInfo to a new custom function using GrapeCity.CalcEngine.Function
5. How to use GrapeCity.Spreadsheet.CalculationEngine to calculate formulas in code, outside the context of a worksheet

## The Riemann Zeta Function ζ(s) is the Most Interesting Function in the World

To illustrate the custom function feature of Spread.NET 12 Windows Forms, I wanted to choose a really cool example, so I picked the most awesome function possible, the Riemann Zeta Function ζ(s): This function is interesting for many reasons – here are just a few:

1. It can be expressed as an infinite sum or as an infinite product over the prime numbers – this was demonstrated by Leonhard Euler: 1. Bernhard Riemann used analytic continuation to extend ζ(s) to cover Complex Plane, converges everywhere except for a single pole at x = 1:  1. The Riemann Hypothesis is an unproven mathematical conjecture about the zeros of ζ(s) on the line x = ½ – there is a pending \$1M Millenium Prize for proving that conjecture.

2. Voronin's Universality Theorem (which is proven, unlike the Riemann Hypothesis) states that if the Riemann Hypothesis is true, then ζ(s) can approximate any analytical function arbitrarily well somewhere inside the strip between x = ½ and x = 1 – essentially, ζ(s) "contains" all analytic functions (including itself) like a fractal and all possible patterns of continuously varying complex values are represented in that region.

This blog will describe in detail how to create a Spread.NET custom function which calculates the Riemann Zeta Function for a specified set of points along the line x = ½ – where the nontrivial roots are located – and highlight the values calculated that are closest to the roots using a conditional format.

## Spread.NET Custom Functions

When you need a formula function that isn't available in Spread.NET, then you can implement a custom function class to calculate that function. In Spread.NET 12 Windows Forms, the base class for custom functions is GrapeCity.CalcEngine.Function. In earlier releases (before version 11), the base class for custom functions was FarPoint.CalcEngine.FunctionInfo, and that class is still supported for backwards compatibility, but it is better to refactor the code for the custom function to use the new Function class instead. This blog will show the ZETA function implemented both ways, to illustrate how easily a custom function class can be ported to Spread.NET 12 Windows Forms.

You can reuse custom functions between projects, and even create libraries of custom functions to use in your Spread.NET workbooks. Creating a custom function requires the following steps:

1. Decide exactly how you want the function to operate:
• What are the arguments for the function?
• Which arguments will accept references, and which will require values?
• For the arguments which accept references, which types of references are allowed?
• Will the argument accept only a single-cell reference like INDIRECT()?
• Will the argument allow a range reference like SUM() and use the number values in the whole range?
• Will the argument allow a range reference like ABS(), which normally operates only on a single cell, and return an array of values with ABS() applied on each value individually, like the internal releases of Excel 2019 which support "spilling" array values?
2. Implement the custom function – this requires writing a new class inheriting from GrapeCity.CalcEngine.Function.
3. Add the function to the workbook using FpSpread.AddCustomFunction:
``````[VB]
fpSpread1.AddCustomFunction(New ZetaFunction())
``````
``````[C#]
fpSpread1.AddCustomFunction(new ZetaFunction());
``````

## The ZETA Custom Function

The example custom function for this blog is focused on calculating the ZETA function values along the critical line x = ½ in search of the non-trivial roots. The calculation uses the equation in Figure 3 above, where the ZETA function is analytically extended to s > 0. This implementation of ZETA requires 4 arguments, with an optional fifth argument:

``````ZETA(real_part, imaginary_part, count, increment [, iterations=10000])
``````

The function returns an array of values with count rows and 2 columns, with the real component of the complex value in the first column and the complex component in the second column.

The real_part argument is the real value component of the complex number s for which ZETA will be computed – this value is always 0.5 since we're looking for the non-trivial root values, but the implementation of the function can work using any value > 0.

The imaginary_part argument is the imaginary component of the complex number s for which ZETA will be computed – this value is specified in a text box (textBoxStart) at the top of the form.

The count argument is the number of ZETA points to calculate and return – this value is specified in a text box (textBoxCount) at the top of the form. The default value for count is 1000. The function is designed to calculate the ZETA function for count points and return the function values as an array of values with count rows and 2 columns. Each row in the array contains a complex number value of ZETA computed for the point, with the real part in the first column and the imaginary part in the second column.

The increment argument is the distance between each ZETA point calculated and determines the resolution of the root search – this value is specified in a text box (textBoxIncrement) at the top of the form. The function is designed to calculate ZETA for the initial complex number s plus increment, then calculate the value for the complex number s + 2 increment, etc. until count* values have been computed. The default value for increment is 0.1 and searches for root values every tenth, and this is recommended for optimal efficiency. Changing the increment will change the resolution of the search – for example, using increment of 0.01 will search for root values every hundredth and thus finding more decimal places in the root value.

The iterations argument is the number of partial sums to compute for the infinite series – this value is specified in a text box (textboxIterations) at the top of the form. The default value for iterations is 10000 and this should be enough to calculate ZETA accurately to 2 decimal places for values of s < 1000. If calculating ZETA to more decimal places (e.g. changing increment to 0.01 or 0.001) or if calculating ZETA for large values of s (> 1000), then it might be necessary to increase iterations to 10000 (this will affect the performance of the calculation). In that case, you can compute ZETA for fewer points by decreasing the value for count in textBoxCount.

## The ZetaFunction Class

The ZetaFunction class inherits from GrapeCity.CalcEngine.Function and overrides the protected virtual Evaluate method with logic to handle calculating the ZETA function. This example also implements some logic to track the number of calls to the function and time the calculation. The array formula applied to calculate the ZETA function and produce the table of results is calculated just once to produce the array of results in the table. Since each point calculated in the array is independent of the other points being calculated, the loop calculating the point values can run in parallel, so Parallel.For is used to optimize calculation performance. This requires creating a Task which contains the Parallel.For logic inside a lambda function, and executing the Task synchronously (otherwise you get recurrence and stack overflow).

``````[VB]
Imports System.Threading
Imports GrapeCity.CalcEngine

Namespace SpreadNETZetaCalculator
Friend NotInheritable Class ZetaFunction
Inherits [Function]
' keep count of function calls
Friend Shared cnt As Integer = 0
' time calculation
Friend Shared t1 As DateTime
Friend Shared t2 As DateTime

Public Sub New()
MyBase.New("ZETA", 4, 5, FunctionAttributes.Array)
End Sub

Protected Overrides Sub Evaluate(ByVal args As IArguments, ByVal result As IValue)
cnt += 1 ' increment function call counter
t1 = DateTime.Now ' start timing evaluation
Dim l As Integer = 10000 ' default to 10K iterations if not specified
Dim d As Double = args(0).GetPrimitiveValue(args.EvaluationContext).GetNumber() ' real part
Dim d1 As Double = args(1).GetPrimitiveValue(args.EvaluationContext).GetNumber() ' imaginary part
Dim count As Integer = CInt(args(2).GetPrimitiveValue(args.EvaluationContext).GetNumber()) ' number of points to calculate
Dim incr As Double = args(3).GetPrimitiveValue(args.EvaluationContext).GetNumber() ' increment between points
' iterations is optional
If args.Count > 4 Then l = CInt(args(4).GetPrimitiveValue(args.EvaluationContext).GetNumber())
Dim array As IValuesArray = result.CreateValuesArray(CInt(count), 2) ' create array to return values
Dim arr As Double(,) = New Double(count - 1, 1) {} ' create local array to calculate values (can't set values to IValuesArray from another thread)
'  do the ZETA calculation in a Task using Parallel.For for better performance
Dim t As Task = New Task(
Sub()
Parallel.[For](0, count, Sub(i)
Dim d2 As Double = 0.0
Dim d3 As Double = 0.0
Dim d4 As Double = 0.0
Dim d5 As Double = 0.0
Dim dd1 As Double = d1 + i * incr

For l1 As Integer = 1 To l ' calculate Dirichlet Eta series partial sum of l iterations
Dim d6 As Double = 2 * l1 - 1
Dim d7 As Double = 2 * l1
Dim d14 As Double = 0.0
Dim d15 As Double = 0.0
Dim d16 As Double = 0.0
Dim d8 As Double = (dd1 * Math.Log(d6)) Mod 6.2831853071795862
Dim d9 As Double = (dd1 * Math.Log(d7)) Mod 6.2831853071795862
Dim d10 As Double = -d
Dim d11 As Double = Math.Pow(d6, d10)
Dim d12 As Double = Math.Pow(d7, d10)
d2 += d11 * Math.Cos(d8) - d12 * Math.Cos(d9)
d3 += d12 * Math.Sin(d9) - d11 * Math.Sin(d8)
d4 += Math.Pow(d6, -0.5) * Math.Cos(d8) - Math.Pow(d7, -0.5) * Math.Cos(d9)
d14 = Math.Pow(d7, -0.5) * Math.Sin(d9)
d15 = Math.Pow(d6, -0.5)
d16 = Math.Sin(d8)
d5 += d14 - d15 * d16
Next
' calculate Zeta from Eta value
Dim d17 As Double = 1.0 - Math.Pow(2, 1.0 - d) * Math.Cos(d1 * Math.Log(2))
Dim d18 As Double = Math.Pow(2, 1.0 - d) * Math.Sin(d1 * Math.Log(2))
Dim d19 As Double = (d17 * d2 + d18 * d3) / (Math.Pow(d17, 2) + Math.Pow(d18, 2))
Dim d20 As Double = (d17 * d3 – d18 * d2) / (Math.Pow(d17, 2) + Math.Pow(d18, 2))
Interlocked.Exchange(arr(i, 0), d19)
Interlocked.Exchange(arr(i, 1), d20)
End Sub)
End Sub)
t.RunSynchronously() ' need to run this task synchronously on the UI thread
' copy values from arr to the IValuesArray
For i As Integer = 0 To count - 1
array(i, 0).SetValue(arr(i, 0))
array(i, 1).SetValue(arr(i, 1))
Next
' set result -- this is important! If you don't return a result, then the cells remain marked dirty and Evaluate will be called again...
result.SetValue(array)
t2 = DateTime.Now
End Sub
End Class
End Namespace
``````
`````` [C#]
using System;
using System.Threading;
using System.Threading.Tasks;
using GrapeCity.CalcEngine;

namespace SpreadNETZetaCalculator
{
internal sealed class ZetaFunction : Function
{   // keep count of function calls
internal static int cnt = 0;
// time calculation
internal static DateTime t1;
internal static DateTime t2;
public ZetaFunction() : base("ZETA", 4, 5, FunctionAttributes.Array)
{ // minimum args 4, maximum args 5, returns array value
}
protected override void Evaluate(IArguments args, IValue result)
{
cnt++; // increment function call counter
t1 = DateTime.Now; // start timing evaluation
long l = 10000L; // default to 10K iterations if not specified
// get function arguments
double d = args.GetPrimitiveValue(args.EvaluationContext).GetNumber();  // real part
double d1 = args.GetPrimitiveValue(args.EvaluationContext).GetNumber(); // imaginary part
long count = (long)args.GetPrimitiveValue(args.EvaluationContext).GetNumber(); // number of points to calculate
double incr = args.GetPrimitiveValue(args.EvaluationContext).GetNumber(); // increment between points
if (args.Count > 4) // iterations is optional
l = (long)args.GetPrimitiveValue(args.EvaluationContext).GetNumber();
IValuesArray array = result.CreateValuesArray((int)count, 2); // create array to return values
double[,] arr = new double[count, 2]; // create local array to calculate values (can't set values to IValuesArray from another thread)
// do the ZETA calculation in a Task using Parallel.For for better performance
Task t = new Task(delegate
{
Parallel.For(0, count, i =>
{
var d2 = 0.0;
var d3 = 0.0;
var d4 = 0.0;
var d5 = 0.0;
var dd1 = d1 + i * incr;
for (var l1 = 1; l1 <= l; l1++)
{ // calculate Dirichlet Eta series partial sum of l iterations
var d6 = 2 * l1 - 1;
var d7 = 2 * l1;
var d14 = 0.0;
var d15 = 0.0;
var d16 = 0.0;
var d8 = (dd1 * Math.Log(d6)) % 6.2831853071795862;
var d9 = (dd1 * Math.Log(d7)) % 6.2831853071795862;
var d10 = -d;
var d11 = Math.Pow(d6, d10);
var d12 = Math.Pow(d7, d10);
d2 += d11 * Math.Cos(d8) - d12 * Math.Cos(d9);
d3 += d12 * Math.Sin(d9) - d11 * Math.Sin(d8);
d4 += Math.Pow(d6, -0.5) * Math.Cos(d8) - Math.Pow(d7, -0.5) * Math.Cos(d9);
d14 = Math.Pow(d7, -0.5) * Math.Sin(d9);
d15 = Math.Pow(d6, -0.5);
d16 = Math.Sin(d8);
d5 += d14 - d15 * d16;
}
// calculate Zeta from Eta value
var d17 = 1.0 - Math.Pow(2, 1.0 - d) * Math.Cos(d1 * Math.Log(2));
var d18 = Math.Pow(2, 1.0 - d) * Math.Sin(d1 * Math.Log(2));
var d19 = (d17 * d2 + d18 * d3) / (Math.Pow(d17, 2) + Math.Pow(d18, 2));
var d20 = (d17 * d3 – d18 * d2) / (Math.Pow(d17, 2) + Math.Pow(d18, 2));
Interlocked.Exchange(ref arr[i, 0], d19);
Interlocked.Exchange(ref arr[i, 1], d20);
});
});
t.RunSynchronously();  // need to run this task synchronously on the UI thread
// copy values from arr to the IValuesArray
for (int i = 0; i < count; i++)
{
array[i, 0].SetValue(arr[i, 0]);
array[i, 1].SetValue(arr[i, 1]);
}
result.SetValue(array); // set result -- this is important! If you don't return a result, then the cells remain marked dirty and Evaluate will be called again...
t2 = DateTime.Now; // time calculation
}
}
}
``````

The override for Evaluate uses the IArguments passed into the function to get the number of arguments specified and to get the value of each argument using IValue.GetPrimitiveValue, specifying the IArguments.EvaluationContext for relative reference resolution and context-sensitive functions. The IValuesArray array is created using IValue.CreateValuesArray on result – note that the logic does not directly call IValuesArray.SetValue inside the Parallel.For code, because calls from other threads are not allowed. Instead the Parallel.For executes synchronously, using Interlocked.Exchange to set the results to a captured temporary array variable, blocks the main UI thread until all calculations are complete, and then safely copies the results from the temporary array of double to array.

At the end of this post, I will show the same custom function implemented using the old FarPoint.CalcEngine.FunctionInfo, which is still supported for backwards compatibility. If you have custom functions using this class created for earlier versions of Spread.NET, then you can use that as a guide to help with porting your custom functions to using the new GrapeCity.CalcEngine.Function class instead.

## The Spread.NET Zeta Function Calculator The main form for the calculator has controls in a panel docked across the top and the spreadsheet control docked below. Simply clicking the Calculate button using the default values in the text box will find the first nontrivial root near the start value 14: The first column A in the result shows the imaginary values bi along the line x = ½ for which ζ will be calculated. The second and third columns B and C show the calculated result of ζ(0.5+bi), with the real part in column B and the imaginary part in column C. The fourth column D shows these values combined into one imaginary value in column D using the COMPLEX function, and the fifth column E shows the complex absolute value using the IMABS function. The complex absolute value is the distance between the complex value and the origin value 0.

Root values are highlighted with a conditional format when the function value is closer to the origin than increment. After calculating a set of values, the Filter Roots Only button becomes enabled and you can use it to show only the root values: The calculator can also perform a custom goal-seek operation to find the root value to 4 decimal places using the calculation engine to manually calculate values for ZETA by selecting a root value (move the active cell to a highlighted row) and using the GoalSeek Root button: ## The Form Code

The initialization code handles adding the custom ZetaFunction for calculations using AddCustomFunction, attaching the FormulaTextBox control using Attach, and showing the spreadsheet status bar using StatusBarVisible:

``````[VB]
Public Sub New()
InitializeComponent()
fpSpread1.AddCustomFunction(New ZetaFunction())
formulaTextBox1.Attach(fpSpread1)
fpSpread1.StatusBarVisible = True
End Sub
``````
``````[C#]
public ZetaCalculatorForm()
{
InitializeComponent();
fpSpread1.AddCustomFunction(new ZetaFunction());
formulaTextBox1.Attach(fpSpread1);
fpSpread1.StatusBarVisible = true;
}

``````

A helper function GetInputValues() handles getting the input values from the text box controls:

``````[VB]
Private Function GetInputValues(ByRef initval As Double, ByRef incr As Double, ByRef rc As Integer, ByRef iter As Long) As Boolean
If Not Double.TryParse(textBoxStart.Text, initval) Then
MessageBox.Show("Please enter a double value for Start Value.")
textBoxStart.Focus()
Return False
End If
If Not Double.TryParse(textBoxIncrement.Text, incr) Then
MessageBox.Show("Please enter a double value for Increment.")
textBoxIncrement.Focus()
Return False
End If
If Not Integer.TryParse(textBoxCount.Text, rc) Then
MessageBox.Show("Please enter an integer value for Count.")
textBoxCount.Focus()
Return False
End If
Long.TryParse(textBoxIter.Text, iter)
Return True
End Function
``````
``````[C#]
private bool GetInputValues(ref double initval, ref double incr, ref int rc, ref long iter)
{
if (!double.TryParse(textBoxStart.Text, out initval))
{
MessageBox.Show("Please enter a double value for Start Value.");
textBoxStart.Focus();
return false;
}
if (!double.TryParse(textBoxIncrement.Text, out incr))
{
MessageBox.Show("Please enter a double value for Increment.");
textBoxIncrement.Focus();
return false;
}
if (!int.TryParse(textBoxCount.Text, out rc))
{
MessageBox.Show("Please enter an integer value for Count.");
textBoxCount.Focus();
return false;
}
long.TryParse(textBoxIter.Text, out iter);
return true;
}
``````

Another helper function creates the number format string to format the values in the first column to show the number of decimal places used in the increment value:

``````[VB]
Private Function GetIncrementNumberFormat() As String
Dim incr As Double = 0.1
If Not Double.TryParse(textBoxIncrement.Text, incr) Then Return "0.0"
Dim sb As New StringBuilder
sb.Append("0.")
While incr < 1
sb.Append("0")
incr *= 10
End While
Return sb.ToString()
End Function
``````
``````[C#]
private string GetIncrementNumberFormat()
{
double incr = 0.1;
if (!double.TryParse(textBoxIncrement.Text, out incr))
return "0.0";
StringBuilder sb = new StringBuilder();
sb.Append("0.");
while (incr < 1)
{
sb.Append("0");
incr *= 10;
}
return sb.ToString();
}
``````

The event handler for the Calculate button click contains the most important code that generates the table of results. First the input values are retrieved to local variables using GetInputValues:

``````[VB]
Private Sub buttonCalculate_Click(ByVal sender As Object, ByVal e As EventArgs) Handles buttonCalculate.Click
Dim initval As Double = 0.0
Dim incr As Double = 0.1
Dim rc As Integer = 1000
Dim iter As Long = 10000
If Not GetInputValues(initval, incr, rc, iter) Then Return
``````
``````[C#]
private void buttonCalculate_Click(object sender, EventArgs e)
{
double initval = 0.0;
double incr = 0.01;
int rc = 12000;
long iter = 10000;
if (!GetInputValues(ref initval, ref incr, ref rc, ref iter))
return;
``````

Before starting the calculation, the spreadsheet layout is suspended using SuspendLayout to prevent repainting during the operation, and the cursor is updated to show the wait cursor and form text updated to indicate a calculation is happening:

``````[VB]
fpSpread1.SuspendLayout()
Cursor = Cursors.WaitCursor
Text = "Calculating, please wait..."
``````
``````[C#]
fpSpread1.SuspendLayout();
Cursor = Cursors.WaitCursor;
Text = "Calculating, please wait...";
``````

The sheet is initialized for a new calculation with the table headings using Reset, ColumnCount, RowCount, ReferenceStyle, Cell.Text and Cell.HorizontalAlignment:

``````[VB]
' inialize sheet with Zeta calculation table headers
Dim sheet As SheetView = fpSpread1.ActiveSheet
sheet.Reset()
sheet.RowCount = rc + 1
sheet.ColumnCount = 5
sheet.ReferenceStyle = FarPoint.Win.Spread.Model.ReferenceStyle.R1C1
sheet.Cells(0, 0).Text = "bi"
sheet.Cells(0, 1).Text = "Re( ζ(0.5+bi) )"
sheet.Cells(0, 2).Text = "Im( ζ(0.5+bi) )"
sheet.Cells(0, 3).Text = "ζ(0.5+bi)"
sheet.Cells(0, 4).Text = "IMABS( ζ(0.5+bi) )"
sheet.Cells(0, 0, 0, 4).HorizontalAlignment = CellHorizontalAlignment.Center
``````
``````[C#]
// inialize sheet with Zeta calculation table headers
SheetView sheet = fpSpread1.ActiveSheet;
sheet.Reset();
sheet.RowCount = rc + 1;
sheet.ColumnCount = 5;
sheet.ReferenceStyle = FarPoint.Win.Spread.Model.ReferenceStyle.R1C1;
sheet.Cells[0, 0].Text = "bi";
sheet.Cells[0, 1].Text = "Re( ζ(0.5+bi) )";
sheet.Cells[0, 2].Text = "Im( ζ(0.5+bi) )";
sheet.Cells[0, 3].Text = "ζ(0.5+bi)";
sheet.Cells[0, 4].Text = "IMABS( ζ(0.5+bi) )";
sheet.Cells[0, 0, 0, 4].HorizontalAlignment = CellHorizontalAlignment.Center;
``````

The table headings use built-in cell styles, which are applied using IRange.ApplyStyle:

``````[VB]
' apply built-in styles for table header cells
Dim workbook As IWorkbook = fpSpread1.AsWorkbook()
workbook.ActiveSheet.Cells(0, 0, 0, 4).ApplyStyle(BuiltInStyle.Percent20Accent3)
workbook.ActiveSheet.Cells(0, 0, 0, 4).ApplyStyle(BuiltInStyle.Heading3)
``````
``````[C#]
// apply built-in styles for table header cells
IWorkbook workbook = fpSpread1.AsWorkbook();
workbook.ActiveSheet.Cells[0, 0, 0, 4].ApplyStyle(BuiltInStyle.Percent20Accent3);
workbook.ActiveSheet.Cells[0, 0, 0, 4].ApplyStyle(BuiltInStyle.Heading3);
``````

The values to be calculated are set into the cells in the first column and formatted to show the number of decimal places appropriate for the increment value using IRange.NumberFormat:

``````[VB]
' initialize the values to be calculated
For r As Integer = 1 To rc
sheet.Cells(r, 0).Value = initval + incr * r
Next
' format the cells to show the number decimal places appropriate for increment
fpSpread1.AsWorkbook().Worksheets(0).Cells(1, 0, rc, 0).NumberFormat = GetIncrementNumberFormat()
``````
`````` [C#]
// initialize the values to be calculated
for (int r = 1; r <= rc; r++)
sheet.Cells[r, 0].Value = initval + incr * r;
// format the cells to show the number decimal places appropriate for increment
fpSpread1.AsWorkbook().Worksheets.Cells[1, 0, rc, 0].NumberFormat = GetIncrementNumberFormat();
``````

The formula to calculate the ZETA function for the specified set of points is an array formula, set using Cell.FormulaArray, and the formulas to calculate the COMPLEX and IMABS functions are regular formulas set using Cell.Formula:

``````[VB]
' create the formula to calculate the ZETA function along the critical line Re = 0.5 for the
' set of points starting with the initial value and using the specified increment and iterations
Dim formula As String = "ZETA(0.5,RC1," + rc.ToString().Trim() + "," + incr.ToString().Trim() + "," + iter.ToString().Trim() + ")"
sheet.Cells(1, 1, rc, 2).FormulaArray = formula
' apply the COMPLEX function to the two values returned for the real (in column 1) and imaginary (in column 2) parts to get the resulting complex number calculated
sheet.Cells(1, 3, rc, 3).Formula = "COMPLEX(RC2,RC3)"
' apply IMABS function to resulting complex number -- this is equivalent to SQRT(RC2*RC2+RC3*RC3) and computes the length of the complex vector
' IMABS values less than increment are considered "close" to a root and highlighted if the value is a local minimum
sheet.Cells(1, 4, rc, 4).Formula = "IMABS(RC4)"
``````
`````` [C#]
// create the formula to calculate the ZETA function along the critical line Re = 0.5 for the
// set of points starting with the initial value and using the specified increment and iterations
string formula = "ZETA(0.5,RC1," + rc.ToString().Trim() + "," + incr.ToString().Trim() + "," + iter.ToString().Trim() + ")";
sheet.Cells[1, 1, rc, 2].FormulaArray = formula;
// apply the COMPLEX function to the two values returned for the real (in column 1) and imaginary (in column 2) parts to get the resulting complex number calculated
sheet.Cells[1, 3, rc, 3].Formula = "COMPLEX(RC2,RC3)";
// apply IMABS function to resulting complex number -- this is equivalent to SQRT(RC2*RC2+RC3*RC3) and computes the length of the complex vector
// IMABS values less than increment are considered "close" to a root and highlighted if the value is a local minimum
sheet.Cells[1, 4, rc, 4].Formula = "IMABS(RC4)";
``````

The column widths for the results table are set using SetColumnWidth and GetPreferredColumnWidth, which returns the width required to show all the text in the column:

``````[VB]
' set column widths
sheet.SetColumnWidth(0, CInt(sheet.GetPreferredColumnWidth(0)))
sheet.SetColumnWidth(1, 150)
sheet.SetColumnWidth(2, 150)
sheet.SetColumnWidth(3, CInt(sheet.GetPreferredColumnWidth(3)))
sheet.SetColumnWidth(4, CInt(sheet.GetPreferredColumnWidth(4)))
``````
``````[C#]
// set column widths
sheet.SetColumnWidth(0, (int)sheet.GetPreferredColumnWidth(0));
sheet.SetColumnWidth(1, 150);
sheet.SetColumnWidth(2, 150);
sheet.SetColumnWidth(3, (int)sheet.GetPreferredColumnWidth(3));
sheet.SetColumnWidth(4, (int)sheet.GetPreferredColumnWidth(4));
``````

The filter and sort indicators are added using the new IRange.AutoFilter API – since the formula used in columns B and C is an array formula, and array formulas are not supported in table cells, this example cannot use a table for the results. Instead of using a table for filter and sort, a range filter can be set on the target range to make its contents filterable and sortable by the user:

``````[VB]
' enable auto filter on the range
workbook.ActiveSheet.Cells(0, 0, rc, 4).AutoFilter()
``````
``````[C#]
// enable auto filter on the range
workbook.ActiveSheet.Cells[0, 0, rc, 4].AutoFilter();
``````

The top row in the results table showing the table header cells should not scroll vertically with the sheet, so the top row is frozen using FrozenRowCount, and the row header cells are renumbered using StartingRowNumber to adjust for the extra header row for the table and make the table rows start at 1 instead of 2 (the text of the first row header cell is also cleared so it doesn’t show "0"):

``````[VB]
' freeze first row from scrolling to keep table headers visible
sheet.FrozenRowCount = 1
' renumber row headings
sheet.StartingRowNumber = 0
' clear the text in the first row header
sheet.RowHeader.Cells(0, 0).Text = " "
``````
``````[C#]
// freeze first row from scrolling to keep table headers visible
sheet.FrozenRowCount = 1;
// renumber row headings
sheet.StartingRowNumber = 0;
// clear the text in the first row header
sheet.RowHeader.Cells[0, 0].Text = " ";
``````

To ensure that the clipboard shortcut keys for Copy – Ctrl+C and Ctrl+Insert – are handled by the worksheet to copy only the visible cells and skip the filtered-out cells, those shortcut keys are mapped to the appropriate SpreadActions using GetInputMap, InputMapMode, and InputMap.Put:

``````[VB]
' map the clipboard shortcut keys to special actions that copy only visible values as text
' (so the sheet can be filtered and copy/paste works as expected)
fpSpread1.GetInputMap(InputMapMode.WhenFocused).Put(New Keystroke(Keys.C, Keys.Control), SpreadActions.ClipboardCopyAsStringSkipHidden)
fpSpread1.GetInputMap(InputMapMode.WhenFocused).Put(New Keystroke(Keys.Insert, Keys.Control), SpreadActions.ClipboardCopyAsStringSkipHidden)
``````
``````[C#]
// map the clipboard shortcut keys to special actions that copy only visible values as text
// (so the sheet can be filtered and copy/paste works as expected)
fpSpread1.GetInputMap(InputMapMode.WhenFocused).Put(new Keystroke(Keys.C, Keys.Control), SpreadActions.ClipboardCopyAsStringSkipHidden);
fpSpread1.GetInputMap(InputMapMode.WhenFocused).Put(new Keystroke(Keys.Insert, Keys.Control), SpreadActions.ClipboardCopyAsStringSkipHidden);
``````

To highlight the root values, a FormulaConditionalFormattingRule is applied to the cells which highlights the row when the IMABS value in column E is a local minimum (less than both preceding and succeeding values) that is less than the specified increment, using SetConditionalFormatting:

``````[VB]
' create a conditional format to highlight the root (local minima IMABS < 1) values with yellow
Dim conditionformula As String = "AND(RC5<" + incr.ToString() + ",RC5<RC5,RC5<R[-1]C5)"
Dim formulaRule As FormulaConditionalFormattingRule = New FormulaConditionalFormattingRule(conditionformula)
formulaRule.BackColor = System.Drawing.Color.Yellow
sheet.SetConditionalFormatting(1, 0, rc, 5, New IConditionalFormattingRule() {formulaRule})
``````
``````[C#]
// create a conditional format to highlight the root (local minima IMABS < increment) values with yellow
string conditionformula = "AND(RC5<" + incr.ToString() + ",RC5<RC5,RC5<R[-1]C5)";
FormulaConditionalFormattingRule formulaRule = new FormulaConditionalFormattingRule(conditionformula);
formulaRule.BackColor = System.Drawing.Color.Yellow;
sheet.SetConditionalFormatting(1, 0, rc, 5, new IConditionalFormattingRule[] { formulaRule });
``````

The last bit of code handles cleanup, resuming layout with ResumeLayout, resetting the cursor and updating the form title bar with statistics about the function call count and timing of the calculations, and enabling the Filter Roots Only button:

``````[VB]
' cleanup, update window text with function call count and timing stats, enable filter roots button
fpSpread1.ResumeLayout()
Cursor = Cursors.[Default]
If checkBoxFunctionInfo.Checked Then
Text = "Zeta Calculation Complete! [ZetaFunctionInfo Call #" + ZetaFunctionInfo.cnt.ToString() + " in " & (ZetaFunctionInfo.t2 - ZetaFunctionInfo.t1).ToString() & "]"
Else
Text = "Zeta Calculation Complete! [ZetaFunction Call #" + ZetaFunction.cnt.ToString() + " in " & (ZetaFunction.t2 - ZetaFunction.t1).ToString() & "]"
End If
buttonFilterRootsOnly.Enabled = True
End Sub
``````
`````` [C#]
// cleanup, update window text with function call count and timing stats, enable filter roots button'
fpSpread1.ResumeLayout();
Cursor = Cursors.Default;
if (checkBoxFunctionInfo.Checked)
Text = "Zeta Calculation Complete! [ZetaFunctionInfo Call #" + ZetaFunctionInfo.cnt.ToString() + " in " + (ZetaFunctionInfo.t2 - ZetaFunctionInfo.t1).ToString() + "]";
else
Text = "Zeta Calculation Complete! [ZetaFunction Call #" + ZetaFunction.cnt.ToString() + " in " + (ZetaFunction.t2 - ZetaFunction.t1).ToString() + "]";
buttonFilterRootsOnly.Enabled = true;
}
``````

The code for the Filter Roots Only button click event uses the new IRange.AutoFilter method with a yellow Fill specified, which has the same effect as the user opening the filter menu for the first column and selecting Filter By Color -> Filter By Cell Color :

``````[VB]
Private Sub buttonFilterRootsOnly_Click(ByVal sender As Object, ByVal e As EventArgs) Handles buttonFilterRootsOnly.Click
Dim workbook As IWorkbook = fpSpread1.AsWorkbook()
Dim worksheet As IWorksheet = workbook.ActiveSheet
Dim fill As Fill = New Fill(GrapeCity.Spreadsheet.Color.FromKnownColor(GrapeCity.Core.KnownColor.Yellow))
worksheet.Cells(0, 0, worksheet.RowCount - 1, 4).AutoFilter(0, fill)
End Sub
``````
``````[C#]
private void buttonFilterRootsOnly_Click(object sender, EventArgs e)
{
IWorkbook workbook = fpSpread1.AsWorkbook();
IWorksheet worksheet = workbook.ActiveSheet;
Fill fill = new Fill(GrapeCity.Spreadsheet.Color.FromKnownColor(GrapeCity.Core.KnownColor.Yellow));
worksheet.Cells[0, 0, worksheet.RowCount - 1, 4].AutoFilter(0, fill);
}
``````

The code for the Use ZetaFunctionInfo check box CheckChanged event handler switches the active function calculating the ZETA function between the GrapeCity.CalcEngine.Function and the FarPoint.CalcEngine.FunctionInfo implementations (which calculate the same thing using different APIs):

``````[VB]
Private Sub checkBoxFunctionInfo_CheckedChanged(ByVal sender As Object, ByVal e As EventArgs) Handles checkBoxFunctionInfo.CheckedChanged
If checkBoxFunctionInfo.Checked Then
' this method is marked obsolete and deprecated -- you should port your old FunctionInfo class to a Function class
fpSpread1.ActiveSheet.AddCustomFunction(New ZetaFunctionInfo())
Else
fpSpread1.AddCustomFunction(New ZetaFunction())
End If
End Sub
``````
``````[C#]
private void checkBoxFunctionInfo_CheckedChanged(object sender, EventArgs e)
{
if (checkBoxFunctionInfo.Checked)
// this method is marked obsolete and deprecated -- you should port your old FunctionInfo class to a Function class
fpSpread1.ActiveSheet.AddCustomFunction(new ZetaFunctionInfo());
else
fpSpread1.AddCustomFunction(new ZetaFunction());
}
``````

Note that the SheetView.AddCustomFunction method is now marked obsolete and deprecated – custom functions using FarPoint.CalcEngine.FunctionInfo should be ported to using the new GrapeCity.CalcEngine.Function class instead.

The event handler for the LeaveCell event handles checking the whether the cell becoming active is a highlighted root value and enabling or disabling the Goal Seek Root button:

``````[VB]
Private Sub fpSpread1_LeaveCell(ByVal sender As Object, ByVal e As LeaveCellEventArgs) Handles fpSpread1.LeaveCell
Dim workbook As IWorkbook = fpSpread1.AsWorkbook()
' get the interior color of the cell becoming active, enable goal seek button if it's a highlighted root value
Dim c As GrapeCity.Spreadsheet.Color = workbook.ActiveSheet.Cells(e.NewRow, e.NewColumn).DisplayFormat.Interior.Color
If c.Equals(GrapeCity.Spreadsheet.Color.FromKnownColor(GrapeCity.Core.KnownColor.Yellow)) Then
buttonGoalSeek.Enabled = True
Else
buttonGoalSeek.Enabled = False
End If
End Sub
``````
``````[C#]
private void fpSpread1_LeaveCell(object sender, LeaveCellEventArgs e)
{
IWorkbook workbook = fpSpread1.AsWorkbook();
// get the interior color of the cell becoming active, enable goal seek button if it's a highlighted root value
GrapeCity.Spreadsheet.Color c = workbook.ActiveSheet.Cells[e.NewRow, e.NewColumn].DisplayFormat.Interior.Color;
if (c.Equals(GrapeCity.Spreadsheet.Color.FromKnownColor(GrapeCity.Core.KnownColor.Yellow)))
buttonGoalSeek.Enabled = true;
else
buttonGoalSeek.Enabled = false;
}
``````

The code handling the Goal Seek Root button click implements logic to call into the CalculationEngine and perform manual calculations of formulas in code. First the code sets up variables for the calculations and gets the input values in the text box controls, overriding the iterations to use 100000 for improved accuracy finding root values:

``````[VB]
Private Sub buttonGoalSeek_Click(ByVal sender As Object, ByVal e As EventArgs) Handles buttonGoalSeek.Click
Dim t2 As DateTime, t1 As DateTime = DateTime.Now ' for timing the goal-seek
Dim wb As IWorkbook = fpSpread1.AsWorkbook()
Dim calcengine As CalculationEngine = wb.WorkbookSet.CalculationEngine
Dim ws As IWorksheet = wb.ActiveSheet
Dim sheet As SheetView = fpSpread1.ActiveSheet
Dim initval As Double = 0.0
Dim incr As Double = 0.1
Dim rc As Integer = 1000
Dim iter As Long = 10000
Dim row As Integer = sheet.ActiveRowIndex
Dim cnt As Integer ' for counting the number of ZETA function calls performed during goal-seek
Dim cnt2 As Integer
Dim maxchange As Double ' current decimal place of search
Dim formula, formulaNext As String ' formula for value and formula for next value to be tested
Dim result, resultNext As IValue ' calculated result of formula for value and formula for next value
' temp variables for calculating IMABS of result and resultNext
Dim temp, temp1, temp2, tempNext, tempNext1, tempNext2 As Double
Dim nextDecimalPlace As Boolean ' flag for moving to next decimal place
Dim value As Double ' current value being tested
' save initial ZETA call count
If checkBoxFunctionInfo.Checked Then
cnt = ZetaFunctionInfo.cnt
Else
cnt = ZetaFunction.cnt
End If
If Not GetInputValues(initval, incr, rc, iter) Then Return
' force 100K interations for improved accuracy
iter = 100000
``````
``````[C#]
private void buttonGoalSeek_Click(object sender, EventArgs e)
{
DateTime t2, t1 = DateTime.Now; // for timing the goal-seek
IWorkbook wb = fpSpread1.AsWorkbook();
CalculationEngine calcengine = wb.WorkbookSet.CalculationEngine;
IWorksheet ws = wb.ActiveSheet;
SheetView sheet = fpSpread1.ActiveSheet;
double initval = 0.0;
double incr = 0.01;
int rc = 12000;
long iter = 10000;
int row = sheet.ActiveRowIndex;
int cnt, cnt2; // for counting the number of ZETA function calls performed during goal-seek
double maxchange; // current decimal place of search
string formula, formulaNext; // formula for value and formula for next value to be tested
IValue result, resultNext; // calculated result of formula for value and formula for next value
// temp variables for calculating IMABS of result and resultNext
double temp, temp1, temp2, tempNext, tempNext1, tempNext2;
bool nextDecimalPlace; // flag for moving to next decimal place
double value; // current value being tested
// save initial ZETA call count
if (checkBoxFunctionInfo.Checked)
cnt = ZetaFunctionInfo.cnt;
else
cnt = ZetaFunction.cnt;
if (!GetInputValues(ref initval, ref incr, ref rc, ref iter))
return;
// force 100K iterations for improved accuracy
iter = 100000;
``````

Then the actual goal seek is performed using CalculationEngine.Evaluate and CellReference to evaluate ZETA for value and refine that value to 5 decimal places, then discard the last decimal place:

``````[VB]
' do goal seek on the cell (row, 0):
value = Math.Round(CDbl(ws.Cells(row, 0).Value), 1) + 0.04
ws.Cells(row, 0).NumberFormat = "0.0000"
Cursor = Cursors.WaitCursor
maxchange = 0.01 ' start changing the hundredths place, then move on to each sucessive decimal place until maxchange = 1e-5, then discard the last (unreliable) decimal place
Do
nextDecimalPlace = False ' reset flag
' calculate value
formula = "ZETA(0.5," + value.ToString().Trim() + ",1,0," + iter.ToString().Trim() + ")"
result = calcengine.Evaluate(formula, ws, New GrapeCity.Spreadsheet.CellReference(row, 1))
temp1 = result.GetValuesArray()(0, 0).GetNumber()
temp2 = result.GetValuesArray()(0, 1).GetNumber()
' calculate absolute values of results
temp = Math.Sqrt(temp1 * temp1 + temp2 * temp2)
Do
' calculate next value (value + maxchange) using CalculationEngine.Evaluate
formulaNext = "ZETA(0.5," + (value + maxchange).ToString().Trim() + ",1,0," + iter.ToString().Trim() + ")"
resultNext = calcengine.Evaluate(formulaNext, ws, New GrapeCity.Spreadsheet.CellReference(row, 1))
tempNext1 = resultNext.GetValuesArray()(0, 0).GetNumber()
tempNext2 = resultNext.GetValuesArray()(0, 1).GetNumber()
' calculate absolute values of results
tempNext = Math.Sqrt(tempNext1 * tempNext1 + tempNext2 * tempNext2)
If tempNext < temp Then
value += maxchange ' next value is closer, increment value and continue checking next higher value
temp = tempNext
Else ' next value is not closer, check previous value(s)
Do
' calculate previous value
formulaNext = "ZETA(0.5," + (value - maxchange).ToString().Trim() + ",1,0," + iter.ToString().Trim() + ")"
resultNext = calcengine.Evaluate(formulaNext, ws, New GrapeCity.Spreadsheet.CellReference(row, 1))
tempNext1 = resultNext.GetValuesArray()(0, 0).GetNumber()
tempNext2 = resultNext.GetValuesArray()(0, 1).GetNumber()
' calculate absolute value
tempNext = Math.Sqrt(tempNext1 * tempNext1 + tempNext2 * tempNext2)
If tempNext < temp Then
' previous value is closer, decrement value and continue checking next previous value
value -= maxchange
temp = tempNext
Else
' previous value is not closer, current value is closest -- continue to next decimal place
nextDecimalPlace = True
value = value + 4 * (maxchange / 10)
End If
Loop Until nextDecimalPlace
End If
Loop Until nextDecimalPlace
maxchange *= 0.1
Loop While maxchange > 0.00001
' discard last digit
value = Math.Truncate(value * 10000) / 10000
``````
`````` [C#]
// do goal seek on the cell [row, 0]:
value = Math.Round((double)ws.Cells[row, 0].Value, 1) + 0.04; // start with value XXX.X4
ws.Cells[row, 0].NumberFormat = "0.0000"; // format cell for 4 decimal places
Cursor = Cursors.WaitCursor;
for (maxchange = 0.01; maxchange > 1e-5; maxchange *= 0.1)
{ // start changing the hundredths place, then move on to each sucessive decimal place until maxchange = 1e-5, then discard the last (unreliable) decimal place
nextDecimalPlace = false; // reset flag
// calculate value
formula = "ZETA(0.5," + value.ToString().Trim() + ",1,0," + iter.ToString().Trim() + ")";
result = calcengine.Evaluate(formula, ws, new GrapeCity.Spreadsheet.CellReference(row, 1));
temp1 = result.GetValuesArray()[0, 0].GetNumber();
temp2 = result.GetValuesArray()[0, 1].GetNumber();
// calculate absolute values of results
temp = Math.Sqrt(temp1 * temp1 + temp2 * temp2);
do
{ // next value (value + maxchange) using CalculationEngine.Evaluate
formulaNext = "ZETA(0.5," + (value + maxchange).ToString().Trim() + ",1,0," + iter.ToString().Trim() + ")";
resultNext = calcengine.Evaluate(formulaNext, ws, new GrapeCity.Spreadsheet.CellReference(row, 1));
tempNext1 = resultNext.GetValuesArray()[0, 0].GetNumber();
tempNext2 = resultNext.GetValuesArray()[0, 1].GetNumber();
// calculate absolute values of results
tempNext = Math.Sqrt(tempNext1 * tempNext1 + tempNext2 * tempNext2);
if (tempNext < temp)
{
value += maxchange; // next value is closer, increment value and continue checking next higher value
temp = tempNext;
}
else
{ // next value is not closer, check previous value(s)
do
{ // calculate previous value
formulaNext = "ZETA(0.5," + (value - maxchange).ToString().Trim() + ",1,0," + iter.ToString().Trim() + ")";
resultNext = calcengine.Evaluate(formulaNext, ws, new GrapeCity.Spreadsheet.CellReference(row, 1));
tempNext1 = resultNext.GetValuesArray()[0, 0].GetNumber();
tempNext2 = resultNext.GetValuesArray()[0, 1].GetNumber();
// calculate absolute value
tempNext = Math.Sqrt(tempNext1 * tempNext1 + tempNext2 * tempNext2);
if (tempNext < temp)
{ // previous value is closer, decrement value and continue checking next previous value
value -= maxchange;
temp = tempNext;
}
else
{ // previous value is not closer, current value is closest -- continue to next decimal place
nextDecimalPlace = true;
value = value + 4 * (maxchange / 10);
}
} while (!nextDecimalPlace);
}
} while (!nextDecimalPlace);
}
// discard last digit
value = Math.Truncate(value * 10000) / 10000;
``````

After the calculations are complete, the logic sets the root value found to the cell, using Cell.Value, resets the column width to show the whole value, and shows a message box with statistics about how many ZETA function calls were required and the timing of the operation:

``````[VB]
' cleanup
t2 = DateTime.Now
Cursor = Cursors.[Default]
If checkBoxFunctionInfo.Checked Then
cnt2 = ZetaFunctionInfo.cnt
Else
cnt2 = ZetaFunction.cnt
End If
sheet.Cells(row, 0).Value = value
sheet.Columns(0).Width = sheet.GetPreferredColumnWidth(0)
MessageBox.Show("Zero Value: " + value.ToString() + vbCrLf + "Calculation completed in " + (t2 - t1).ToString() + " with " + (cnt2 - cnt).ToString() + " calls to ZETA function.")
End Sub
``````
``````[C#]
// cleanup
t2 = DateTime.Now;
Cursor = Cursors.Default;
if (checkBoxFunctionInfo.Checked)
cnt2 = ZetaFunctionInfo.cnt;
else
cnt2 = ZetaFunction.cnt;
sheet.Cells[row, 0].Value = value;
sheet.Columns.Width = sheet.GetPreferredColumnWidth(0);
MessageBox.Show("Zero Value: " + value.ToString() + "\r\nCalculation completed in " + (t2 - t1).ToString() + " with " + (cnt2 - cnt).ToString() + " calls to ZETA function.");
``````

This is certainly not the most optimal implementation for goal seek – there are much better ways to do that I'm sure – but the purpose of this code is really to show how easily you can call into the calculation engine to calculate formulas in code using the Spread.NET API.

## The ZetaFunctionInfo Class

For customers who have old custom function implementations using the FarPoint.CalcEngine.FunctionInfo class and need to port those implementations to using the new GrapeCity.CalcEngine.Function class, here is an implementation of the ZETA function using the old API for reference:

``````[VB]
Imports System.Threading
Imports FarPoint.CalcEngine

Namespace SpreadNETZetaCalculator
Friend Class ZetaFunctionInfo
Inherits FunctionInfo
' keep count of function calls
Friend Shared cnt As Integer = 0
' time calculation
Friend Shared t1 As DateTime
Friend Shared t2 As DateTime
' required overrides for FunctionInfo
Public Overrides ReadOnly Property Name As String
Get
Return "ZETA"
End Get
End Property
Public Overrides ReadOnly Property MinArgs As Integer
Get
Return 4
End Get
End Property
Public Overrides ReadOnly Property MaxArgs As Integer
Get
Return 5
End Get
End Property
Public Overrides Function Evaluate(ByVal args As Object()) As Object
cnt += 1
t1 = DateTime.Now
Dim l As Integer = 10000
Dim d As Double = CalcConvert.ToDouble(args(0))
Dim d1 As Double = CalcConvert.ToDouble(args(1))
Dim count As Integer = CalcConvert.ToInt(args(2))
Dim incr As Double = CalcConvert.ToDouble(args(3))
If args.Length > 4 Then l = CalcConvert.ToInt(args(4))
Dim arr As Double(,) = New Double(count - 1, 1) {}
Dim t As Task = New Task(
Sub()
Parallel.[For](0, count, Sub(i)
Dim d2 As Double = 0.0
Dim d3 As Double = 0.0
Dim d4 As Double = 0.0
Dim d5 As Double = 0.0
Dim dd1 As Double = d1 + i * incr

For l1 As Integer = 1 To l
Dim d6 As Double = 2 * l1 - 1
Dim d7 As Double = 2 * l1
Dim d14 As Double = 0.0
Dim d15 As Double = 0.0
Dim d16 As Double = 0.0
Dim d8 As Double = (dd1 * Math.Log(d6)) Mod 6.2831853071795862
Dim d9 As Double = (dd1 * Math.Log(d7)) Mod 6.2831853071795862
Dim d10 As Double = -1 * d
Dim d11 As Double = -1 * 0.5
Dim d12 As Double = Math.Pow(d6, d10)
Dim d13 As Double = Math.Pow(d7, d10)
d2 += d12 * Math.Cos(d8) - d13 * Math.Cos(d9)
d3 += d13 * Math.Sin(d9) - d12 * Math.Sin(d8)
d4 += Math.Pow(d6, d11) * Math.Cos(d8) - Math.Pow(d7, d11) * Math.Cos(d9)
d14 = Math.Pow(d7, d11) * Math.Sin(d9)
d15 = Math.Pow(d6, d11)
d16 = Math.Sin(d8)
d5 += d14 - d15 * d16
Next
Dim d17 As Double = 1.0 - Math.Pow(2, 1.0 - d) * Math.Cos(d1 * Math.Log(2))
Dim d18 As Double = Math.Pow(2, 1.0 - d) * Math.Sin(d1 * Math.Log(2))
Dim d19 As Double = (d17 * d2 + d18 * d3) / (Math.Pow(d17, 2) + Math.Pow(d18, 2))
Dim d20 As Double = (d17 * d3 – d18 * d2) / (Math.Pow(d17, 2) + Math.Pow(d18, 2))
Interlocked.Exchange(arr(i, 0), d19)
Interlocked.Exchange(arr(i, 1), d20)
End Sub)
End Sub)
t.RunSynchronously()
t2 = DateTime.Now
Return New ArrayResult(arr)
End Function
End Class
Public Class ArrayResult
Inherits CalcArray

Private values As Double(,)
Public Sub New(ByVal values As Double(,))
If values Is Nothing Then values = New Double(-1, -1) {}
Me.values = values
End Sub
Public Overrides ReadOnly Property RowCount As Integer
Get
Return values.GetUpperBound(0) - values.GetLowerBound(0) + 1
End Get
End Property
Public Overrides ReadOnly Property ColumnCount As Integer
Get
Return values.GetUpperBound(1) - values.GetLowerBound(1) + 1
End Get
End Property
Public Overrides Function GetValue(ByVal row As Integer, ByVal column As Integer) As Object
Return values(row, column)
End Function
End Class
End Namespace
``````
``````
[C#]
using System;
using System.Threading;
using System.Threading.Tasks;
using FarPoint.CalcEngine;

namespace SpreadNETZetaCalculator
{
internal sealed class ZetaFunctionInfo : FunctionInfo
{   // keep count of function calls
internal static int cnt = 0;
// time calculation
internal static DateTime t1;
internal static DateTime t2;
// required overrides for FunctionInfo
public override string Name => "ZETA";
public override int MinArgs => 4;
public override int MaxArgs => 5;
public override object Evaluate(object[] args)
{
cnt++;
t1 = DateTime.Now;
int l = 10000;
double d = CalcConvert.ToDouble(args);  // real part
double d1 = CalcConvert.ToDouble(args); // imaginary part
int count = CalcConvert.ToInt(args); // number of points to calculate
double incr = CalcConvert.ToDouble(args); // increment between points
if (args.Length > 4)
l = CalcConvert.ToInt(args); // iterations
double[,] arr = new double[count, 2]; // create array to return values
// do the ZETA calculation in a Task using Parallel.For for better performance
Task t = new Task(delegate
{
Parallel.For(0, count, i =>
{
var d2 = 0.0;
var d3 = 0.0;
var d4 = 0.0;
var d5 = 0.0;
var dd1 = d1 + i * incr;
for (var l1 = 1; l1 <= l; l1++)
{
var d6 = 2 * l1 - 1;
var d7 = 2 * l1;
var d14 = 0.0;
var d15 = 0.0;
var d16 = 0.0;
var d8 = (dd1 * Math.Log(d6)) % 6.2831853071795862;
var d9 = (dd1 * Math.Log(d7)) % 6.2831853071795862;
var d10 = -1 * d;
var d11 = -1 * 0.5;
var d12 = Math.Pow(d6, d10);
var d13 = Math.Pow(d7, d10);
d2 += d12 * Math.Cos(d8) - d13 * Math.Cos(d9);
d3 += d13 * Math.Sin(d9) - d12 * Math.Sin(d8);
d4 += Math.Pow(d6, d11) * Math.Cos(d8) - Math.Pow(d7, d11) * Math.Cos(d9);
d14 = Math.Pow(d7, d11) * Math.Sin(d9);
d15 = Math.Pow(d6, d11);
d16 = Math.Sin(d8);
d5 += d14 - d15 * d16;
}
var d17 = 1.0 - Math.Pow(2, 1.0 - d) * Math.Cos(d1 * Math.Log(2));
var d18 = Math.Pow(2, 1.0 - d) * Math.Sin(d1 * Math.Log(2));
var d19 = (d17 * d2 + d18 * d3) / (Math.Pow(d17, 2) + Math.Pow(d18, 2));
var d20 = (d17 * d3 – d18 * d2) / (Math.Pow(d17, 2) + Math.Pow(d18, 2));
Interlocked.Exchange(ref arr[i, 0], d19);
Interlocked.Exchange(ref arr[i, 1], d20);
});
});
t.RunSynchronously(); // need to run this task synchronously on the UI thread
t2 = DateTime.Now;
return new ArrayResult(arr); // return as ArrayResult (see below)
}
}
public class ArrayResult : CalcArray
{
double[,] values;
public ArrayResult(double[,] values)
{
if (values == null)
values = new double[0, 0];
this.values = values;
}
public override int RowCount => values.GetUpperBound(0) - values.GetLowerBound(0) + 1;
public override int ColumnCount => values.GetUpperBound(1) - values.GetLowerBound(1) + 1;
public override object GetValue(int row, int column)
{
return values[row, column];
}
}
}
``````

Note that the code inside the Task is identical, and only the outside code has changed – the logic which gets the function arguments and returns the array of results. The old FunctionInfo API required implementing a custom class inheriting from CalcArray to return the array of results; this is no longer required using the new API.

#### VB sample code | C# sample code

Thanks for following along, happy coding!

Try Spread.NET 12 today

Download your free 30-day trial version of Spread.NET 12 Windows Forms

Download Now!