diff --git a/Makefile b/Makefile index 31232488..e2a0de1c 100644 --- a/Makefile +++ b/Makefile @@ -129,6 +129,7 @@ tags: doc: docs docs: $(Q)$(MAKE) -C docs + @echo "Docs built." docs_doxygen_only: $(Q)$(MAKE) -C docs docs_doxygen_only diff --git a/dev/release/fix_example_docs.pl b/dev/release/fix_example_docs.pl index 664f9736..a3e4a811 100644 --- a/dev/release/fix_example_docs.pl +++ b/dev/release/fix_example_docs.pl @@ -26,6 +26,7 @@ my @examples = ( [ 'hyperlinks.c', 'A example of writing urls/hyperlinks' ], [ 'rich_strings.c', 'A example of writing "rich" multi-format strings' ], [ 'array_formula.c', 'A example of using array formulas' ], + [ 'dynamic_arrays.c', 'A example of using Excel 365 dynamic array formulas' ], [ 'utf8.c', 'A example of some UTF-8 text' ], [ 'constant_memory.c', 'Write a large file with constant memory usage' ], [ 'merge_range.c', 'Create a merged range of cells' ], @@ -53,6 +54,7 @@ my @examples = ( [ 'macro.c', 'Example of adding a VBA macro to a workbook' ], [ 'panes.c', 'Example of how to create worksheet panes' ], [ 'ignore_errors.c', 'Example of ignoring worksheet errors/warnings' ], + [ 'lambda.c', 'Example of using the EXCEL 365+ LAMBDA() function' ], [ 'chart.c', 'Example of a simple column chart' ], [ 'chart_area.c', 'Examples of area charts' ], [ 'chart_bar.c', 'Examples of bar charts' ], diff --git a/docs/images/dynamic_arrays01.png b/docs/images/dynamic_arrays01.png new file mode 100644 index 00000000..1ad33469 Binary files /dev/null and b/docs/images/dynamic_arrays01.png differ diff --git a/docs/images/dynamic_arrays02.png b/docs/images/dynamic_arrays02.png new file mode 100644 index 00000000..1f37efe6 Binary files /dev/null and b/docs/images/dynamic_arrays02.png differ diff --git a/docs/images/intersection01.png b/docs/images/intersection01.png new file mode 100644 index 00000000..8bd7570d Binary files /dev/null and b/docs/images/intersection01.png differ diff --git a/docs/images/intersection02.png b/docs/images/intersection02.png new file mode 100644 index 00000000..124c81eb Binary files /dev/null and b/docs/images/intersection02.png differ diff --git a/docs/images/intersection03.png b/docs/images/intersection03.png new file mode 100644 index 00000000..845cac1e Binary files /dev/null and b/docs/images/intersection03.png differ diff --git a/docs/images/lambda01.png b/docs/images/lambda01.png new file mode 100644 index 00000000..f7de6176 Binary files /dev/null and b/docs/images/lambda01.png differ diff --git a/docs/images/spill01.png b/docs/images/spill01.png new file mode 100644 index 00000000..9287b84a Binary files /dev/null and b/docs/images/spill01.png differ diff --git a/docs/src/examples.dox b/docs/src/examples.dox index 5a6eb519..99b85fd3 100644 --- a/docs/src/examples.dox +++ b/docs/src/examples.dox @@ -240,7 +240,7 @@ Example of writing "rich" multi-format strings to a worksheet. - +
@ref rich_strings.c "<< rich_strings.c"@ref utf8.c "utf8.c >>"@ref dynamic_arrays.c "dynamic_arrays.c >>"
@@ -251,11 +251,28 @@ Example of writing array formulas to a worksheet. -@example utf8.c + +@example dynamic_arrays.c + + +
@ref array_formula.c "<< array_formula.c"@ref utf8.c "utf8.c >>"
+ +Example of writing Excel 365 dynamic array formulas to a worksheet. + +@image html dynamic_arrays01.png + + + + +@example utf8.c + + + +
@ref dynamic_arrays.c "<< dynamic_arrays.c" @ref constant_memory.c "constant_memory.c >>"
@@ -685,7 +702,7 @@ An example of how to create panes in a worksheet, both "freeze" panes and - +
@ref panes.c "<< panes.c"@ref chart.c "chart.c >>"@ref lambda.c "lambda.c >>"
@@ -696,11 +713,31 @@ Example of hiding worksheet errors and warnings. -@example chart.c +@example lambda.c + + +
@ref ignore_errors.c "<< ignore_errors.c"@ref chart.c "chart.c >>"
+ +Example of using the new Excel `LAMBDA()` function. It demonstrates how to +create a lambda function in Excel and also how to assign a name to it so that +it can be called as a user defined function. This particular example converts +from Fahrenheit to Celsius. Note, this function is only currently available if +you are subscribed to the Microsoft Office Beta Channel program. + +@image html lambda01.png + + + + +@example chart.c + + + +
@ref lambda.c "<< lambda.c" @ref chart_area.c "chart_area.c >>"
diff --git a/docs/src/examples.txt b/docs/src/examples.txt index 657f2f2b..97fd8cdf 100644 --- a/docs/src/examples.txt +++ b/docs/src/examples.txt @@ -113,6 +113,14 @@ Example of writing array formulas to a worksheet. @image html array_formula.png +############################################################## + +@example dynamic_arrays.c + +Example of writing Excel 365 dynamic array formulas to a worksheet. + +@image html dynamic_arrays01.png + ############################################################## @example utf8.c A simple Unicode UTF-8 example. Note, the source file is UTF-8 encoded. @@ -315,6 +323,17 @@ Example of hiding worksheet errors and warnings. @image html ignore_errors2.png +############################################################## +@example lambda.c + +Example of using the new Excel `LAMBDA()` function. It demonstrates how to +create a lambda function in Excel and also how to assign a name to it so that +it can be called as a user defined function. This particular example converts +from Fahrenheit to Celsius. Note, this function is only currently available if +you are subscribed to the Microsoft Office Beta Channel program. + +@image html lambda01.png + ############################################################## @example chart.c diff --git a/docs/src/working_with_formulas.dox b/docs/src/working_with_formulas.dox index 07012888..6b14b388 100644 --- a/docs/src/working_with_formulas.dox +++ b/docs/src/working_with_formulas.dox @@ -48,6 +48,250 @@ multi-lingual [Formula Translator](http://en.excel-translator.de/language/) to help you convert the formula. It can also replace semi-colons with commas. + + +@section ww_formulas_dynamic_arrays Dynamic Array support + +Excel introduced the concept of "Dynamic Arrays" and new functions that use +them in Office 365. The new functions are: + +- `FILTER` +- `RANDARRAY` +- `SEQUENCE` +- `SORTBY` +- `SORT` +- `UNIQUE` +- `XLOOKUP` +- `XMATCH` + +The following special case functions were also added with Dynamic Arrays: + +- `SINGLE`: Explained below in @ref ww_formulas_intersection. +- `ANCHORARRAY`: Explained below in @ref ww_formulas_spill. +- `LAMBDA` and `LET`: Explained below in @ref ww_formulas_lambda. + +These functions are all "future functions" and need to written in +libxlsxwriter as follows: + +- `_xlfn.ANCHORARRAY` +- `_xlfn.LAMBDA` +- `_xlfn.RANDARRAY` +- `_xlfn.SEQUENCE` +- `_xlfn.SINGLE` +- `_xlfn.SORTBY` +- `_xlfn.UNIQUE` +- `_xlfn.XLOOKUP` +- `_xlfn.XMATCH` +- `_xlfn._xlws.FILTER` +- `_xlfn._xlws.SORT` + +Future functions are explained in the section below on @ref +ww_formulas_future. + +@subsection ww_formulas_intro Dynamic Arrays - An introduction + +Dynamic arrays in Excel are ranges of return values that can change in size +based on the results. For example, a function such as `FILTER()` returns an +array of values that can vary in size depending on the the filter results: + +@code + worksheet_write_dynamic_array_formula(worksheet, RANGE("F2:F2"), + "=_xlfn._xlws.FILTER(A1:D17,C1:C17=K2)", + NULL); +@endcode + +This formula gives the results shown in the image below. The dynamic range +here is "F2:I5" but it can vary based on the filter criteria. + +@image html dynamic_arrays02.png + + +It is also possible to get dynamic array behavior with older Excel +functions. For example, the Excel function `"=LEN(A1)"` applies to a single +cell and returns a single value but it can also apply to a range of cells and +return a range of values using an array formula like `"{=LEN(A1:A3)}"`. This +type of "static" array behavior is referred to as a CSE (Ctrl+Shift+Enter) +formula and has existed in Excel since early versions. In Office 365 Excel +updated and extended this behavior to create the concept of dynamic arrays. In +Excel 365 you can now write the previous LEN function as `"=LEN(A1:A3)"` and +get a dynamic range of return values. In libxlsxwriter you can use the +`worksheet_write_array_formula()` function to get a static/CSE range and +`worksheet_write_dynamic_array_formula()` to get a dynamic range. For example: + +@code + worksheet_write_dynamic_array_formula(worksheet, RANGE("B1:B3"), + "=LEN(A1:A3)", + NULL); +@endcode + +Which gives the following result: + +@image html intersection03.png + +The difference between the two types of array functions is explained in the +Microsoft documentation on [Dynamic array formulas vs. legacy CSE array +formulas](https://support.microsoft.com/en-us/office/dynamic-array-formulas-vs-legacy-cse-array-formulas-ca421f1b-fbb2-4c99-9924-df571bd4f1b4). Note +the use of the word "legacy" here. This, and the documentation itself, is a +clear indication of the future importance of dynamic arrays in Excel. + +For a wider and more general introduction to dynamic arrays see the following: +[Dynamic array formulas in Excel](https://exceljet.net/dynamic-array-formulas-in-excel). + +The `worksheet_write_dynamic_array_formula()` function takes a `(first_row, +first_col, last_row, last_col)` cell range to define the area that the formula +applies to. However, since the range is dynamic this generally won't be known +in advance in which case you can specify the range with the same start and end +cell. The following range is "F2:F2": + +@code + worksheet_write_dynamic_array_formula(worksheet, 1, 5, 1, 5, + "=_xlfn._xlws.FILTER(A1:D17,C1:C17=K2)", + NULL); +@endcode + +As a syntactic shortcut you can use the `worksheet_write_dynamic_formula()` +function which only requires the start cell: + +@code + worksheet_write_dynamic_formula(worksheet, 1, 5, + "=_xlfn._xlws.FILTER(A1:D17,C1:C17=K2)", + NULL); +@endcode + + +@subsection ww_formulas_intersection Dynamic Arrays - The Implicit Intersection Operator "@" + +The Implicit Intersection Operator, "@", is used by Excel 365 to indicate a +position in a formula that is implicitly returning a single value when a range +or an array could be returned. + +We can see how this operator works in practice by considering the formula we +used in the last section: `=LEN(A1:A3)`. In Excel versions without support for +dynamic arrays, i.e. prior to Excel 365, this formula would operate on a +single value from the input range and return a single value, like the +following in Excel 2011: + +@image html intersection01.png + +There is an implicit conversion here of the range of input values, "A1:A3", to +a single value "A1". Since this was the default behavior of older versions of +Excel this conversion isn't highlighted in any way. But if you open the same +file in Excel 365 it will appear as follows: + +@image html intersection02.png + +The result of the formula is the same (this is important to note) and it still +operates on, and returns, a single value. However the formula now contains a +"@" operator to show that it is implicitly using a single value from the given +range. + +Finally, if you entered this formula in Excel 365, or with +`worksheet_write_dynamic_array_formula()` in libxlsxwriter, it would operate +on the entire range and return an array of values: + +@image html intersection03.png + +If you are encountering the Implicit Intersection Operator "@" for the first +time then it is probably from a point of view of "why is Excel/libxlsxwriter +putting @s in my formulas". In practical terms if you encounter this operator, +and you don't intend it to be there, then you should probably write the +formula as a CSE or dynamic array function using +`worksheet_write_array_formula()` or `worksheet_write_dynamic_array_formula()`. + + +A full explanation of this operator is shown in the Microsoft documentation on +the [Implicit intersection operator: \@] +(https://support.microsoft.com/en-us/office/implicit-intersection-operator-ce3be07b-0101-4450-a24e-c1c999be2b34?ui=en-us&rs=en-us&ad=us>). + +One important thing to note is that the "@" operator isn't stored with the +formula. It is just displayed by Excel 365 when reading "legacy" +formulas. However, it is possible to write it to a formula, if necessary, +using `_xlfn.SINGLE()`. The unusual cases where this may be necessary are +shown in the linked document in the previous paragraph. + + +@subsection ww_formulas_spill Dynamic Arrays - The Spilled Range Operator "#" + +In the section above on @ref ww_formulas_intro we saw that dynamic array formulas +can return variable sized ranges of results. The Excel documentation refers to +this as a "Spilled" range/array from the idea that the results spill into the +required number of cells. This is explained in the Microsoft documentation on +[Dynamic array formulas and spilled array behavior] +(https://support.microsoft.com/en-us/office/dynamic-array-formulas-and-spilled-array-behavior-205c6b06-03ba-4151-89a1-87a7eb36e531). + +Since a spilled range is variable in size a new operator is required to refer +to the range. This operator is the [Spilled range operator] +(https://support.microsoft.com/en-us/office/spilled-range-operator-3dd5899f-bca2-4b9d-a172-3eae9ac22efd) +and it is represented by "#". For example, the range `F2#` in the image +below is used to refer to a dynamic array returned by `UNIQUE()` in the cell +`F2`: + +@image html spill01.png + +Unfortunately, Excel doesn't store the formula like this and in libxlsxwriter +you need to use the explicit function `_xlfn.ANCHORARRAY()` to refer to a +spilled range. The example in the image above was generated using the +following: + +@code + // Same as '=COUNTA(F2#)' in Excel. + worksheet_write_dynamic_formula(worksheet9, CELL("J2"), + "=COUNTA(_xlfn.ANCHORARRAY(F2))", + NULL); +@endcode + + +@subsection ww_formulas_lambda The Excel 365 LAMBDA() function + +@note At the time of writing the `LAMBDA()` function in Excel is only +available to Excel 365 users subscribed to the Beta Channel updates. + +Beta Channel versions of Excel 365 have introduced a powerful new +function/feature called `LAMBDA()`. This is similar to +[lambda expressions] +(https://docs.microsoft.com/en-us/cpp/cpp/lambda-expressions-in-cpp?view=msvc-160) +in C++ (and other languages). + +Consider the following Excel example which converts the variable `temp` from Fahrenheit to Celsius: + + LAMBDA(temp, (5/9) * (temp-32)) + +This could be called in Excel with an argument: + + =LAMBDA(temp, (5/9) * (temp-32))(212) + +Or assigned to a defined name and called as a user defined function: + + =ToCelsius(212) + + + An libxlsxwriter example that replicates the described Excel functionality is +shown below: + +@code + // Write the lambda as a function. + worksheet_write_dynamic_formula(worksheet, CELL("A2"), + "=_xlfn.LAMBDA(_xlpm.temp, (5/9) * (_xlpm.temp-32))(32)", + NULL); + + // Create the lambda function as a defined name and write it as a dynamic formula. + workbook_define_name(workbook, + "ToCelsius", + "=_xlfn.LAMBDA(_xlpm.temp, (5/9) * (_xlpm.temp-32))"); + + worksheet_write_dynamic_formula(worksheet, CELL("A3"), "=ToCelsius(212)", NULL); +@endcode + +Note, that the formula name must have a "_xlfn." prefix and the parameters in +the `LAMBDA()` function must have a "_xlpm." prefix for compatibility with +how the formulas are stored in Excel. These prefixes won't show up in the +formula, as shown in the image. + +@image html lambda01.png + +The `LET()` function is often used in conjunction with `LAMBDA()` to assign +names to calculation results. + @section ww_formulas_future Formulas added in Excel 2010 and later @@ -199,6 +443,25 @@ The following list is taken from | `_xlfn.Z.TEST` | +The dynamic array functions shown in the @ref ww_formulas_dynamic_arrays +section above are also future functions: + + + | Dynamic Array Functions | + | -------------------------------- | + | `_xlfn.ANCHORARRAY` | + | `_xlfn.LAMBDA` | + | `_xlfn.RANDARRAY` | + | `_xlfn.SEQUENCE` | + | `_xlfn.SINGLE` | + | `_xlfn.SORTBY` | + | `_xlfn.UNIQUE` | + | `_xlfn.XLOOKUP` | + | `_xlfn.XMATCH` | + | `_xlfn._xlws.FILTER` | + | `_xlfn._xlws.SORT` | + + @section ww_formulas_errors Dealing with formula errors If there is an error in the syntax of a formula it is usually displayed in @@ -220,6 +483,13 @@ follows: listed above (@ref ww_formulas_future). If it does then ensure that the correct prefix is used. +5. If the function loads in Excel but appears with one or more ``@`` symbols + added then it is probably an array function and should be written using + `worksheet_write_array_formula()` or + `worksheet_write_dynamic_array_formula()` (see the sections above on @ref + ww_formulas_dynamic_arrays and @ref ww_formulas_intersection). + + Finally if you have completed all the previous steps and still get a @c \#NAME? error you can examine a valid Excel file to see what the correct syntax should be. To do this you should create a valid formula in Excel and @@ -229,7 +499,7 @@ The following shows how to do that using Linux `unzip` and `libxml's [xmllint](http://xmlsoft.org/xmllint.html) to format the XML for clarity: $ unzip myfile.xlsx -d myfile - $ xmllint --format myfile/xl/worksheets/sheet1.xml | grep '' + $ xmllint --format myfile/xl/worksheets/sheet1.xml | grep '' SUM(1, 2, 3) @@ -249,7 +519,7 @@ some mobile device applications. If required, it is also possible to specify the calculated result of the formula using the `result` parameter for -:func:`worksheet_write_formula_num()`: +`worksheet_worksheet_write_formula_num()`: @code worksheet_write_formula_num(worksheet, 0, 0, "=2+2", NULL, 4); diff --git a/examples/dynamic_arrays.c b/examples/dynamic_arrays.c new file mode 100644 index 00000000..54bc4971 --- /dev/null +++ b/examples/dynamic_arrays.c @@ -0,0 +1,303 @@ +/* + * An example of how to use libxlsxwriter to write functions that create + * dynamic arrays. These functions are new to Excel 365. The examples mirror + * the examples in the Excel documentation on these functions. + * + * Copyright 2014-2021, John McNamara, jmcnamara@cpan.org + * + */ + +#include "xlsxwriter.h" + + +void write_worksheet_data(lxw_worksheet *worksheet, lxw_format *header); + + +int main() { + + lxw_workbook *workbook = workbook_new("dynamic_arrays.xlsx"); + lxw_worksheet *worksheet1 = workbook_add_worksheet(workbook, "Filter"); + lxw_worksheet *worksheet2 = workbook_add_worksheet(workbook, "Unique"); + lxw_worksheet *worksheet3 = workbook_add_worksheet(workbook, "Sort"); + lxw_worksheet *worksheet4 = workbook_add_worksheet(workbook, "Sortby"); + lxw_worksheet *worksheet5 = workbook_add_worksheet(workbook, "Xlookup"); + lxw_worksheet *worksheet6 = workbook_add_worksheet(workbook, "Xmatch"); + lxw_worksheet *worksheet7 = workbook_add_worksheet(workbook, "Randarray"); + lxw_worksheet *worksheet8 = workbook_add_worksheet(workbook, "Sequence"); + lxw_worksheet *worksheet9 = workbook_add_worksheet(workbook, "Spill ranges"); + lxw_worksheet *worksheet10 = workbook_add_worksheet(workbook, "Older functions"); + + lxw_format *header1 = workbook_add_format(workbook); + format_set_bg_color(header1, 0x74AC4C); + format_set_font_color(header1, 0xFFFFFF); + + lxw_format *header2 = workbook_add_format(workbook); + format_set_bg_color(header2, 0x528FD3); + format_set_font_color(header2, 0xFFFFFF); + + + /* + * Example of using the FILTER() function. + */ + worksheet_write_dynamic_formula(worksheet1, CELL("F2"), + "=_xlfn._xlws.FILTER(A1:D17,C1:C17=K2)", + NULL); + + /* Write the data the function will work on. */ + worksheet_write_string(worksheet1, CELL("K1"), "Product", header2); + worksheet_write_string(worksheet1, CELL("K2"), "Apple", NULL ); + worksheet_write_string(worksheet1, CELL("F1"), "Region", header2); + worksheet_write_string(worksheet1, CELL("G1"), "Sales Rep", header2); + worksheet_write_string(worksheet1, CELL("H1"), "Product", header2); + worksheet_write_string(worksheet1, CELL("I1"), "Units", header2); + + write_worksheet_data(worksheet1, header1); + worksheet_set_column_pixels(worksheet1, COLS("E:E"), 20, NULL); + worksheet_set_column_pixels(worksheet1, COLS("J:J"), 20, NULL); + + + /* + * Example of using the UNIQUE() function. + */ + worksheet_write_dynamic_formula(worksheet2, CELL("F2"), + "=_xlfn.UNIQUE(B2:B17)", + NULL); + + /* A more complex example combining SORT and UNIQUE. */ + worksheet_write_dynamic_formula(worksheet2, CELL("H2"), + "=_xlfn._xlws.SORT(_xlfn.UNIQUE(B2:B17))", + NULL); + + /* Write the data the function will work on. */ + worksheet_write_string(worksheet2, CELL("F1"), "Sales Rep", header2); + worksheet_write_string(worksheet2, CELL("H1"), "Sales Rep", header2); + + write_worksheet_data(worksheet2, header1); + worksheet_set_column_pixels(worksheet2, COLS("E:E"), 20, NULL); + worksheet_set_column_pixels(worksheet2, COLS("G:G"), 20, NULL); + + + /* + * Example of using the SORT() function. + */ + worksheet_write_dynamic_formula(worksheet3, CELL("F2"), + "=_xlfn._xlws.SORT(B2:B17)", + NULL); + + /* A more complex example combining SORT and FILTER. */ + worksheet_write_dynamic_formula(worksheet3, CELL("H2"), + "=_xlfn._xlws.SORT(_xlfn._xlws.FILTER(C2:D17,D2:D17>5000,\"\"),2,1)", + NULL); + + /* Write the data the function will work on. */ + worksheet_write_string(worksheet3, CELL("F1"), "Sales Rep", header2); + worksheet_write_string(worksheet3, CELL("H1"), "Product", header2); + worksheet_write_string(worksheet3, CELL("I1"), "Units", header2); + + write_worksheet_data(worksheet3, header1); + worksheet_set_column_pixels(worksheet3, COLS("E:E"), 20, NULL); + worksheet_set_column_pixels(worksheet3, COLS("G:G"), 20, NULL); + + + /* + * Example of using the SORTBY() function. + */ + worksheet_write_dynamic_formula(worksheet4, CELL("D2"), + "=_xlfn.SORTBY(A2:B9,B2:B9)", + NULL); + + /* Write the data the function will work on. */ + worksheet_write_string(worksheet4, CELL("A1"), "Name", header1); + worksheet_write_string(worksheet4, CELL("B1"), "Age", header1); + + worksheet_write_string(worksheet4, CELL("A2"), "Tom", NULL); + worksheet_write_string(worksheet4, CELL("A3"), "Fred", NULL); + worksheet_write_string(worksheet4, CELL("A4"), "Amy", NULL); + worksheet_write_string(worksheet4, CELL("A5"), "Sal", NULL); + worksheet_write_string(worksheet4, CELL("A6"), "Fritz", NULL); + worksheet_write_string(worksheet4, CELL("A7"), "Srivan", NULL); + worksheet_write_string(worksheet4, CELL("A8"), "Xi", NULL); + worksheet_write_string(worksheet4, CELL("A9"), "Hector", NULL); + + worksheet_write_number(worksheet4, CELL("B2"), 52, NULL); + worksheet_write_number(worksheet4, CELL("B3"), 65, NULL); + worksheet_write_number(worksheet4, CELL("B4"), 22, NULL); + worksheet_write_number(worksheet4, CELL("B5"), 73, NULL); + worksheet_write_number(worksheet4, CELL("B6"), 19, NULL); + worksheet_write_number(worksheet4, CELL("B7"), 39, NULL); + worksheet_write_number(worksheet4, CELL("B8"), 19, NULL); + worksheet_write_number(worksheet4, CELL("B9"), 66, NULL); + + worksheet_write_string(worksheet4, CELL("D1"), "Name", header2); + worksheet_write_string(worksheet4, CELL("E1"), "Age", header2); + + worksheet_set_column_pixels(worksheet4, COLS("C:C"), 20, NULL); + + + /* + * Example of using the XLOOKUP() function. + */ + worksheet_write_dynamic_formula(worksheet5, CELL("F1"), + "=_xlfn.XLOOKUP(E1,A2:A9,C2:C9)", + NULL); + + /* Write the data the function will work on. */ + worksheet_write_string(worksheet5, CELL("A1"), "Country", header1); + worksheet_write_string(worksheet5, CELL("B1"), "Abr", header1); + worksheet_write_string(worksheet5, CELL("C1"), "Prefix", header1); + + worksheet_write_string(worksheet5, CELL("A2"), "China", NULL); + worksheet_write_string(worksheet5, CELL("A3"), "India", NULL); + worksheet_write_string(worksheet5, CELL("A4"), "United States", NULL); + worksheet_write_string(worksheet5, CELL("A5"), "Indonesia", NULL); + worksheet_write_string(worksheet5, CELL("A6"), "Brazil", NULL); + worksheet_write_string(worksheet5, CELL("A7"), "Pakistan", NULL); + worksheet_write_string(worksheet5, CELL("A8"), "Nigeria", NULL); + worksheet_write_string(worksheet5, CELL("A9"), "Bangladesh", NULL); + + worksheet_write_string(worksheet5, CELL("B2"), "CN", NULL); + worksheet_write_string(worksheet5, CELL("B3"), "IN", NULL); + worksheet_write_string(worksheet5, CELL("B4"), "US", NULL); + worksheet_write_string(worksheet5, CELL("B5"), "ID", NULL); + worksheet_write_string(worksheet5, CELL("B6"), "BR", NULL); + worksheet_write_string(worksheet5, CELL("B7"), "PK", NULL); + worksheet_write_string(worksheet5, CELL("B8"), "NG", NULL); + worksheet_write_string(worksheet5, CELL("B9"), "BD", NULL); + + worksheet_write_number(worksheet5, CELL("C2"), 86, NULL); + worksheet_write_number(worksheet5, CELL("C3"), 91, NULL); + worksheet_write_number(worksheet5, CELL("C4"), 1, NULL); + worksheet_write_number(worksheet5, CELL("C5"), 62, NULL); + worksheet_write_number(worksheet5, CELL("C6"), 55, NULL); + worksheet_write_number(worksheet5, CELL("C7"), 92, NULL); + worksheet_write_number(worksheet5, CELL("C8"), 234, NULL); + worksheet_write_number(worksheet5, CELL("C9"), 880, NULL); + + worksheet_write_string(worksheet5, CELL("E1"), "Brazil", header2); + + worksheet_set_column_pixels(worksheet5, COLS("A:A"), 100, NULL); + worksheet_set_column_pixels(worksheet5, COLS("D:D"), 20, NULL); + + + /* + * Example of using the XMATCH() function. + */ + worksheet_write_dynamic_formula(worksheet6, CELL("D2"), + "=_xlfn.XMATCH(C2,A2:A6)", + NULL); + + /* Write the data the function will work on. */ + worksheet_write_string(worksheet6, CELL("A1"), "Product", header1); + + worksheet_write_string(worksheet6, CELL("A2"), "Apple", NULL); + worksheet_write_string(worksheet6, CELL("A3"), "Grape", NULL); + worksheet_write_string(worksheet6, CELL("A4"), "Pear", NULL); + worksheet_write_string(worksheet6, CELL("A5"), "Banana", NULL); + worksheet_write_string(worksheet6, CELL("A6"), "Cherry", NULL); + + worksheet_write_string(worksheet6, CELL("C1"), "Product", header2); + worksheet_write_string(worksheet6, CELL("D1"), "Position", header2); + worksheet_write_string(worksheet6, CELL("C2"), "Grape", NULL); + + worksheet_set_column_pixels(worksheet6, COLS("B:B"), 20, NULL); + + + /* + * Example of using the RANDARRAY() function. + */ + worksheet_write_dynamic_formula(worksheet7, CELL("A1"), + "=_xlfn.RANDARRAY(5,3,1,100, TRUE)", + NULL); + + /* + * Example of using the SEQUENCE() function. + */ + worksheet_write_dynamic_formula(worksheet8, CELL("A1"), + "=_xlfn.SEQUENCE(4,5)", + NULL); + + + /* + * Example of using the Spill range operator. + */ + worksheet_write_dynamic_formula(worksheet9, CELL("H2"), + "=_xlfn.ANCHORARRAY(F2)", + NULL); + + worksheet_write_dynamic_formula(worksheet9, CELL("J2"), + "=COUNTA(_xlfn.ANCHORARRAY(F2))", + NULL); + + /* Write the data the function will work on. */ + worksheet_write_dynamic_formula(worksheet9, CELL("F2"), + "=_xlfn.UNIQUE(B2:B17)", + NULL); + + worksheet_write_string(worksheet9, CELL("F1"), "Unique", header2); + worksheet_write_string(worksheet9, CELL("H1"), "Spill", header2); + worksheet_write_string(worksheet9, CELL("J1"), "Spill", header2); + + write_worksheet_data(worksheet9, header1); + worksheet_set_column_pixels(worksheet9, COLS("E:E"), 20, NULL); + worksheet_set_column_pixels(worksheet9, COLS("G:G"), 20, NULL); + worksheet_set_column_pixels(worksheet9, COLS("I:I"), 20, NULL); + + + /* + * Example of using dynamic ranges with older Excel functions. + */ + worksheet_write_dynamic_array_formula(worksheet10, RANGE("B1:B3"), + "=LEN(A1:A3)", + NULL); + + /* Write the data the function will work on. */ + worksheet_write_string(worksheet10, CELL("A1"), "Foo", NULL); + worksheet_write_string(worksheet10, CELL("A2"), "Food", NULL); + worksheet_write_string(worksheet10, CELL("A3"), "Frood", NULL); + + + return workbook_close(workbook); +} + + +/* A simple function and data structure to populate some of the worksheets. */ +struct worksheet_data { + char col1[10]; + char col2[10]; + char col3[10]; + int col4; +}; + +void write_worksheet_data(lxw_worksheet *worksheet, lxw_format *header) { + + struct worksheet_data data[160] = { + {"East", "Tom", "Apple", 6380}, + {"West", "Fred", "Grape", 5619}, + {"North", "Amy", "Pear", 4565}, + {"South", "Sal", "Banana", 5323}, + {"East", "Fritz", "Apple", 4394}, + {"West", "Sravan", "Grape", 7195}, + {"North", "Xi", "Pear", 5231}, + {"South", "Hector", "Banana", 2427}, + {"East", "Tom", "Banana", 4213}, + {"West", "Fred", "Pear", 3239}, + {"North", "Amy", "Grape", 6520}, + {"South", "Sal", "Apple", 1310}, + {"East", "Fritz", "Banana", 6274}, + {"West", "Sravan", "Pear", 4894}, + {"North", "Xi", "Grape", 7580}, + {"South", "Hector", "Apple", 9814}, + }; + + worksheet_write_string(worksheet, CELL("A1"), "Region", header); + worksheet_write_string(worksheet, CELL("B1"), "Sales Rep", header); + worksheet_write_string(worksheet, CELL("C1"), "Product", header); + worksheet_write_string(worksheet, CELL("D1"), "Units", header); + + for (int row = 0; row < 16; row++) { + worksheet_write_string(worksheet, row + 1, 0, data[row].col1, NULL); + worksheet_write_string(worksheet, row + 1, 1, data[row].col2, NULL); + worksheet_write_string(worksheet, row + 1, 2, data[row].col3, NULL); + worksheet_write_number(worksheet, row + 1, 3, data[row].col4, NULL); + } +} diff --git a/examples/lambda.c b/examples/lambda.c new file mode 100644 index 00000000..0c03b317 --- /dev/null +++ b/examples/lambda.c @@ -0,0 +1,40 @@ +/* + * An example of using the new Excel LAMBDA() function with the libxlsxwriter + * library. Note, this function is only currently available if you are + * subscribed to the Microsoft Office Beta Channel program. + * + * Copyright 2014-2021, John McNamara, jmcnamara@cpan.org + * + */ + +#include "xlsxwriter.h" + +int main() { + + lxw_workbook *workbook = workbook_new("lambda.xlsx"); + lxw_worksheet *worksheet = workbook_add_worksheet(workbook, NULL); + + worksheet_write_string(worksheet, 0, 0, + "Note: Lambda functions currently only work with " + "the Beta Channel versions of Excel 365", NULL); + + /* Note that the formula name is prefixed with "_xlfn." and that the + * lambda function parameters are prefixed with "_xlpm.". These prefixes + * won't show up in Excel. + */ + worksheet_write_dynamic_formula(worksheet, CELL("A2"), + "=_xlfn.LAMBDA(_xlpm.temp, (5/9) * (_xlpm.temp-32))(32)", + NULL); + + /* Create the lambda function as a defined name and write it as a dynamic formula. */ + workbook_define_name(workbook, + "ToCelsius", + "=_xlfn.LAMBDA(_xlpm.temp, (5/9) * (_xlpm.temp-32))"); + + worksheet_write_dynamic_formula(worksheet, CELL("A3"), "=ToCelsius(212)", NULL); + + + workbook_close(workbook); + + return 0; +} diff --git a/include/xlsxwriter/worksheet.h b/include/xlsxwriter/worksheet.h index 7cce2c62..22e80ba0 100644 --- a/include/xlsxwriter/worksheet.h +++ b/include/xlsxwriter/worksheet.h @@ -2020,16 +2020,16 @@ lxw_error worksheet_write_formula(lxw_worksheet *worksheet, * @brief Write an array formula to a worksheet cell. * * @param worksheet Pointer to a lxw_worksheet instance to be updated. - * @param first_row The first row of the range. (All zero indexed.) - * @param first_col The first column of the range. - * @param last_row The last row of the range. - * @param last_col The last col of the range. - * @param formula Array formula to write to cell. - * @param format A pointer to a Format instance or NULL. + * @param first_row The first row of the range. (All zero indexed.) + * @param first_col The first column of the range. + * @param last_row The last row of the range. + * @param last_col The last col of the range. + * @param formula Array formula to write to cell. + * @param format A pointer to a Format instance or NULL. * * @return A #lxw_error code. * - * The `%worksheet_write_array_formula()` function writes an array formula to + * The `%worksheet_write_array_formula()` function writes an array formula to * a cell range. In Excel an array formula is a formula that performs a * calculation on a set of values. * @@ -2066,6 +2066,94 @@ lxw_error worksheet_write_array_formula(lxw_worksheet *worksheet, const char *formula, lxw_format *format); +/** + * @brief Write an Excel 365 dynamic array formula to a worksheet range. + * + * @param worksheet Pointer to a lxw_worksheet instance to be updated. + * @param first_row The first row of the range. (All zero indexed.) + * @param first_col The first column of the range. + * @param last_row The last row of the range. + * @param last_col The last col of the range. + * @param formula Dynamic Array formula to write to cell. + * @param format A pointer to a Format instance or NULL. + * + * @return A #lxw_error code. + * + * + * The `%worksheet_write_dynamic_array_formula()` function writes an Excel 365 + * dynamic array formula to a cell range. Some examples of functions that + * return dynamic arrays are: + * + * - `FILTER` + * - `RANDARRAY` + * - `SEQUENCE` + * - `SORTBY` + * - `SORT` + * - `UNIQUE` + * - `XLOOKUP` + * - `XMATCH` + * + * Dynamic array formulas and their usage in libxlsxwriter is explained in + * detail @ref ww_formulas_dynamic_arrays. The following is a example usage: + * + * @code + * worksheet_write_dynamic_array_formula(worksheet, 1, 5, 1, 5, + * "=_xlfn._xlws.FILTER(A1:D17,C1:C17=K2)", + * NULL); + * @endcode + * + * This formula gives the results shown in the image below. + * + * @image html dynamic_arrays02.png + * + * The need for the `_xlfn._xlws.` prefix in the formula is explained in @ref + * ww_formulas_future. + */ +lxw_error worksheet_write_dynamic_array_formula(lxw_worksheet *worksheet, + lxw_row_t first_row, + lxw_col_t first_col, + lxw_row_t last_row, + lxw_col_t last_col, + const char *formula, + lxw_format *format); + +/** + * @brief Write an Excel 365 dynamic array formula to a worksheet cell. + * + * @param worksheet Pointer to a lxw_worksheet instance to be updated. + * @param row The zero indexed row number. + * @param col The zero indexed column number. + * @param formula Formula string to write to cell. + * @param format A pointer to a Format instance or NULL. + * + * @return A #lxw_error code. + * + * The `%worksheet_write_dynamic_formula()` function is similar to the + * `worksheet_write_dynamic_array_formula()` function, shown above, except + * that it writes a dynamic array formula to a single cell, rather than a + * range. This is a syntactic shortcut since the array range isn't generally + * known for a dynamic range and specifying the initial cell is sufficient for + * Excel, as shown in the example below: + * + * @code + * worksheet_write_dynamic_formula(worksheet, 7, 1, + * "=_xlfn._xlws.SORT(_xlfn.UNIQUE(B2:B17))", + * NULL); + * @endcode + * + * This formula gives the following result: + * + * @image html dynamic_arrays01.png + * + * The need for the `_xlfn.` and `_xlfn._xlws.` prefixes in the formula is + * explained in @ref ww_formulas_future. + */ +lxw_error worksheet_write_dynamic_formula(lxw_worksheet *worksheet, + lxw_row_t row, + lxw_col_t col, + const char *formula, + lxw_format *format); + lxw_error worksheet_write_array_formula_num(lxw_worksheet *worksheet, lxw_row_t first_row, lxw_col_t first_col, @@ -2075,14 +2163,6 @@ lxw_error worksheet_write_array_formula_num(lxw_worksheet *worksheet, lxw_format *format, double result); -lxw_error worksheet_write_dynamic_array_formula(lxw_worksheet *worksheet, - lxw_row_t first_row, - lxw_col_t first_col, - lxw_row_t last_row, - lxw_col_t last_col, - const char *formula, - lxw_format *format); - lxw_error worksheet_write_dynamic_array_formula_num(lxw_worksheet *worksheet, lxw_row_t first_row, lxw_col_t first_col, @@ -2092,6 +2172,13 @@ lxw_error worksheet_write_dynamic_array_formula_num(lxw_worksheet *worksheet, lxw_format *format, double result); +lxw_error worksheet_write_dynamic_formula_num(lxw_worksheet *worksheet, + lxw_row_t row, + lxw_col_t col, + const char *formula, + lxw_format *format, + double result); + /** * @brief Write a date or time to a worksheet cell. * diff --git a/src/worksheet.c b/src/worksheet.c index 2df20fa7..b660be88 100644 --- a/src/worksheet.c +++ b/src/worksheet.c @@ -7063,6 +7063,33 @@ worksheet_write_array_formula(lxw_worksheet *self, LXW_FALSE); } +/* + * Write a single cell dynamic array formula with a default result to a cell. + */ +lxw_error +worksheet_write_dynamic_formula(lxw_worksheet *self, + lxw_row_t row, + lxw_col_t col, + const char *formula, lxw_format *format) +{ + return _store_array_formula(self, row, col, row, col, formula, format, 0, + LXW_TRUE); +} + +/* + * Write a single cell dynamic array formula with a numerical result to a cell. + */ +lxw_error +worksheet_write_dynamic_formula_num(lxw_worksheet *self, + lxw_row_t row, + lxw_col_t col, + const char *formula, + lxw_format *format, double result) +{ + return _store_array_formula(self, row, col, row, col, formula, format, + result, LXW_TRUE); +} + /* * Write a dynamic array formula with a numerical result to a cell in Excel. */ diff --git a/test/functional/src/test_dynamic_array02.c b/test/functional/src/test_dynamic_array02.c new file mode 100644 index 00000000..7e7427ed --- /dev/null +++ b/test/functional/src/test_dynamic_array02.c @@ -0,0 +1,21 @@ +/***************************************************************************** + * Test cases for libxlsxwriter. + * + * Test to compare output against Excel files. + * + * Copyright 2014-2021, John McNamara, jmcnamara@cpan.org + * + */ + +#include "xlsxwriter.h" + +int main() { + + lxw_workbook *workbook = workbook_new("test_dynamic_array02.xlsx"); + lxw_worksheet *worksheet = workbook_add_worksheet(workbook, NULL); + + worksheet_write_dynamic_formula(worksheet, CELL("B1"), "=_xlfn.UNIQUE(A1)", NULL); + worksheet_write_number(worksheet, CELL("A1"), 0 , NULL); + + return workbook_close(workbook); +} diff --git a/test/functional/src/test_dynamic_array03.c b/test/functional/src/test_dynamic_array03.c new file mode 100644 index 00000000..410559b3 --- /dev/null +++ b/test/functional/src/test_dynamic_array03.c @@ -0,0 +1,20 @@ +/***************************************************************************** + * Test cases for libxlsxwriter. + * + * Test to compare output against Excel files. + * + * Copyright 2014-2021, John McNamara, jmcnamara@cpan.org + * + */ + +#include "xlsxwriter.h" + +int main() { + + lxw_workbook *workbook = workbook_new("test_dynamic_array03.xlsx"); + lxw_worksheet *worksheet = workbook_add_worksheet(workbook, NULL); + + worksheet_write_formula_num(worksheet, CELL("A1"), "=1+_xlfn.XOR(1)", NULL, 2); + + return workbook_close(workbook); +} diff --git a/test/functional/src/test_dynamic_array51.c b/test/functional/src/test_dynamic_array51.c new file mode 100644 index 00000000..c596efaa --- /dev/null +++ b/test/functional/src/test_dynamic_array51.c @@ -0,0 +1,22 @@ +/***************************************************************************** + * Test cases for libxlsxwriter. + * + * Test to compare output against Excel files. + * + * Copyright 2014-2021, John McNamara, jmcnamara@cpan.org + * + */ + +#include "xlsxwriter.h" + +int main() { + + lxw_workbook *workbook = workbook_new("test_dynamic_array51.xlsx"); + lxw_worksheet *worksheet = workbook_add_worksheet(workbook, NULL); + + worksheet_write_dynamic_array_formula_num(worksheet, RANGE("A1:A1"), "=AVERAGE(TIMEVALUE(B1:B2))", NULL, 0); + worksheet_write_string(worksheet, CELL("B1"), "12:00" , NULL); + worksheet_write_string(worksheet, CELL("B2"), "12:00" , NULL); + + return workbook_close(workbook); +} diff --git a/test/functional/src/test_dynamic_array52.c b/test/functional/src/test_dynamic_array52.c new file mode 100644 index 00000000..9186df87 --- /dev/null +++ b/test/functional/src/test_dynamic_array52.c @@ -0,0 +1,22 @@ +/***************************************************************************** + * Test cases for libxlsxwriter. + * + * Test to compare output against Excel files. + * + * Copyright 2014-2021, John McNamara, jmcnamara@cpan.org + * + */ + +#include "xlsxwriter.h" + +int main() { + + lxw_workbook *workbook = workbook_new("test_dynamic_array52.xlsx"); + lxw_worksheet *worksheet = workbook_add_worksheet(workbook, NULL); + + worksheet_write_dynamic_formula(worksheet, CELL("A1"), "=AVERAGE(TIMEVALUE(B1:B2))", NULL); + worksheet_write_string(worksheet, CELL("B1"), "12:00" , NULL); + worksheet_write_string(worksheet, CELL("B2"), "12:00" , NULL); + + return workbook_close(workbook); +} diff --git a/test/functional/src/test_dynamic_array53.c b/test/functional/src/test_dynamic_array53.c new file mode 100644 index 00000000..772c277f --- /dev/null +++ b/test/functional/src/test_dynamic_array53.c @@ -0,0 +1,22 @@ +/***************************************************************************** + * Test cases for libxlsxwriter. + * + * Test to compare output against Excel files. + * + * Copyright 2014-2021, John McNamara, jmcnamara@cpan.org + * + */ + +#include "xlsxwriter.h" + +int main() { + + lxw_workbook *workbook = workbook_new("test_dynamic_array53.xlsx"); + lxw_worksheet *worksheet = workbook_add_worksheet(workbook, NULL); + + worksheet_write_dynamic_formula_num(worksheet, CELL("A1"), "=AVERAGE(TIMEVALUE(B1:B2))", NULL, 0); + worksheet_write_string(worksheet, CELL("B1"), "12:00" , NULL); + worksheet_write_string(worksheet, CELL("B2"), "12:00" , NULL); + + return workbook_close(workbook); +} diff --git a/test/functional/test_dynamic_array.py b/test/functional/test_dynamic_array.py index e9abecd3..5c168849 100644 --- a/test/functional/test_dynamic_array.py +++ b/test/functional/test_dynamic_array.py @@ -15,3 +15,19 @@ class TestCompareXLSXFiles(base_test_class.XLSXBaseTest): def test_dynamic_array01(self): self.run_exe_test('test_dynamic_array01') + + def test_dynamic_array02(self): + self.run_exe_test('test_dynamic_array02') + + def test_dynamic_array03(self): + self.run_exe_test('test_dynamic_array03') + + # Some variant of the default test case. + def test_dynamic_array51(self): + self.run_exe_test('test_dynamic_array51', 'dynamic_array01.xlsx') + + def test_dynamic_array52(self): + self.run_exe_test('test_dynamic_array52', 'dynamic_array01.xlsx') + + def test_dynamic_array53(self): + self.run_exe_test('test_dynamic_array53', 'dynamic_array01.xlsx') diff --git a/test/functional/xlsx_files/dynamic_array02.xlsx b/test/functional/xlsx_files/dynamic_array02.xlsx new file mode 100644 index 00000000..37c92438 Binary files /dev/null and b/test/functional/xlsx_files/dynamic_array02.xlsx differ diff --git a/test/functional/xlsx_files/dynamic_array03.xlsx b/test/functional/xlsx_files/dynamic_array03.xlsx new file mode 100644 index 00000000..068919ba Binary files /dev/null and b/test/functional/xlsx_files/dynamic_array03.xlsx differ