Add initial support for dynamic arrays in formulas.

Feature request #327
This commit is contained in:
John McNamara 2021-04-23 07:25:50 +01:00
parent 3577fdd9fb
commit e478365508
18 changed files with 587 additions and 17 deletions

1
.indent.pro vendored
View File

@ -116,6 +116,7 @@
-T lxw_image_md5
-T lxw_image_options
-T lxw_merged_range
-T lxw_metadata
-T lxw_object_properties
-T lxw_packager
-T lxw_panes

View File

@ -60,6 +60,7 @@ void lxw_ct_add_vml_name(lxw_content_types *content_types);
void lxw_ct_add_shared_strings(lxw_content_types *content_types);
void lxw_ct_add_calc_chain(lxw_content_types *content_types);
void lxw_ct_add_custom_properties(lxw_content_types *content_types);
void lxw_ct_add_metadata(lxw_content_types *content_types);
/* Declarations required for unit testing. */
#ifdef TESTING

View File

@ -0,0 +1,49 @@
/*
* libxlsxwriter
*
* Copyright 2014-2021, John McNamara, jmcnamara@cpan.org. See LICENSE.txt.
*
* metadata - A libxlsxwriter library for creating Excel XLSX metadata files.
*
*/
#ifndef __LXW_METADATA_H__
#define __LXW_METADATA_H__
#include <stdint.h>
#include "common.h"
/*
* Struct to represent a metadata object.
*/
typedef struct lxw_metadata {
FILE *file;
} lxw_metadata;
/* *INDENT-OFF* */
#ifdef __cplusplus
extern "C" {
#endif
/* *INDENT-ON* */
lxw_metadata *lxw_metadata_new(void);
void lxw_metadata_free(lxw_metadata *metadata);
void lxw_metadata_assemble_xml_file(lxw_metadata *self);
/* Declarations required for unit testing. */
#ifdef TESTING
STATIC void _metadata_xml_declaration(lxw_metadata *self);
#endif /* TESTING */
/* *INDENT-OFF* */
#ifdef __cplusplus
}
#endif
/* *INDENT-ON* */
#endif /* __LXW_METADATA_H__ */

View File

@ -31,6 +31,7 @@
#include "relationships.h"
#include "vml.h"
#include "comment.h"
#include "metadata.h"
#define LXW_ZIP_BUFFER_SIZE (16384)

View File

@ -321,6 +321,7 @@ typedef struct lxw_workbook {
uint8_t has_bmp;
uint8_t has_vml;
uint8_t has_comments;
uint8_t has_metadata;
lxw_hash_table *used_xf_formats;
lxw_hash_table *used_dxf_formats;

View File

@ -602,6 +602,7 @@ enum cell_types {
INLINE_RICH_STRING_CELL,
FORMULA_CELL,
ARRAY_FORMULA_CELL,
DYNAMIC_ARRAY_FORMULA_CELL,
BLANK_CELL,
BOOLEAN_CELL,
COMMENT,
@ -1697,6 +1698,7 @@ typedef struct lxw_worksheet {
uint8_t vcenter;
uint8_t zoom_scale_normal;
uint8_t num_validations;
uint8_t has_dynamic_arrays;
char *vba_codename;
lxw_color_t tab_color;
@ -2069,6 +2071,23 @@ 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,
lxw_row_t last_row,
lxw_col_t last_col,
const char *formula,
lxw_format *format,
double result);
/**
* @brief Write a date or time to a worksheet cell.
*

View File

@ -370,3 +370,13 @@ lxw_ct_add_custom_properties(lxw_content_types *self)
lxw_ct_add_override(self, "/docProps/custom.xml",
LXW_APP_DOCUMENT "custom-properties+xml");
}
/*
* Add the metadata file to the ContentTypes overrides.
*/
void
lxw_ct_add_metadata(lxw_content_types *self)
{
lxw_ct_add_override(self, "/xl/metadata.xml",
LXW_APP_DOCUMENT "spreadsheetml.sheetMetadata+xml");
}

283
src/metadata.c Normal file
View File

@ -0,0 +1,283 @@
/*****************************************************************************
* metadata - A library for creating Excel XLSX metadata files.
*
* Used in conjunction with the libxlsxwriter library.
*
* Copyright 2014-2021, John McNamara, jmcnamara@cpan.org. See LICENSE.txt.
*
*/
#include "xlsxwriter/xmlwriter.h"
#include "xlsxwriter/metadata.h"
#include "xlsxwriter/utility.h"
/*
* Forward declarations.
*/
/*****************************************************************************
*
* Private functions.
*
****************************************************************************/
/*
* Create a new metadata object.
*/
lxw_metadata *
lxw_metadata_new(void)
{
lxw_metadata *metadata = calloc(1, sizeof(lxw_metadata));
GOTO_LABEL_ON_MEM_ERROR(metadata, mem_error);
return metadata;
mem_error:
lxw_metadata_free(metadata);
return NULL;
}
/*
* Free a metadata object.
*/
void
lxw_metadata_free(lxw_metadata *metadata)
{
if (!metadata)
return;
free(metadata);
}
/*****************************************************************************
*
* XML functions.
*
****************************************************************************/
/*
* Write the XML declaration.
*/
STATIC void
_metadata_xml_declaration(lxw_metadata *self)
{
lxw_xml_declaration(self->file);
}
/*
* Write the <metadata> element.
*/
STATIC void
_metadata_write_metadata(lxw_metadata *self)
{
struct xml_attribute_list attributes;
struct xml_attribute *attribute;
char xmlns[] = "http://schemas.openxmlformats.org/"
"spreadsheetml/2006/main";
char xmlns_xda[] = "http://schemas.microsoft.com/office/"
"spreadsheetml/2017/dynamicarray";
LXW_INIT_ATTRIBUTES();
LXW_PUSH_ATTRIBUTES_STR("xmlns", xmlns);
LXW_PUSH_ATTRIBUTES_STR("xmlns:xda", xmlns_xda);
lxw_xml_start_tag(self->file, "metadata", &attributes);
LXW_FREE_ATTRIBUTES();
}
/*
* Write the <metadataType> element.
*/
STATIC void
_metadata_write_metadata_type(lxw_metadata *self)
{
struct xml_attribute_list attributes;
struct xml_attribute *attribute;
LXW_INIT_ATTRIBUTES();
LXW_PUSH_ATTRIBUTES_STR("name", "XLDAPR");
LXW_PUSH_ATTRIBUTES_INT("minSupportedVersion", 120000);
LXW_PUSH_ATTRIBUTES_INT("copy", 1);
LXW_PUSH_ATTRIBUTES_INT("pasteAll", 1);
LXW_PUSH_ATTRIBUTES_INT("pasteValues", 1);
LXW_PUSH_ATTRIBUTES_INT("merge", 1);
LXW_PUSH_ATTRIBUTES_INT("splitFirst", 1);
LXW_PUSH_ATTRIBUTES_INT("rowColShift", 1);
LXW_PUSH_ATTRIBUTES_INT("clearFormats", 1);
LXW_PUSH_ATTRIBUTES_INT("clearComments", 1);
LXW_PUSH_ATTRIBUTES_INT("assign", 1);
LXW_PUSH_ATTRIBUTES_INT("coerce", 1);
LXW_PUSH_ATTRIBUTES_INT("cellMeta", 1);
lxw_xml_empty_tag(self->file, "metadataType", &attributes);
LXW_FREE_ATTRIBUTES();
}
/*
* Write the <metadataTypes> element.
*/
STATIC void
_metadata_write_metadata_types(lxw_metadata *self)
{
struct xml_attribute_list attributes;
struct xml_attribute *attribute;
LXW_INIT_ATTRIBUTES();
LXW_PUSH_ATTRIBUTES_INT("count", 1);
lxw_xml_start_tag(self->file, "metadataTypes", &attributes);
/* Write the metadataType element. */
_metadata_write_metadata_type(self);
lxw_xml_end_tag(self->file, "metadataTypes");
LXW_FREE_ATTRIBUTES();
}
/*
* Write the <xda:dynamicArrayProperties> element.
*/
STATIC void
_metadata_write_xda_dynamic_array_properties(lxw_metadata *self)
{
struct xml_attribute_list attributes;
struct xml_attribute *attribute;
LXW_INIT_ATTRIBUTES();
LXW_PUSH_ATTRIBUTES_STR("fDynamic", "1");
LXW_PUSH_ATTRIBUTES_STR("fCollapsed", "0");
lxw_xml_empty_tag(self->file, "xda:dynamicArrayProperties", &attributes);
LXW_FREE_ATTRIBUTES();
}
/*
* Write the <ext> element.
*/
STATIC void
_metadata_write_ext(lxw_metadata *self)
{
struct xml_attribute_list attributes;
struct xml_attribute *attribute;
LXW_INIT_ATTRIBUTES();
LXW_PUSH_ATTRIBUTES_STR("uri", "{bdbb8cdc-fa1e-496e-a857-3c3f30c029c3}");
lxw_xml_start_tag(self->file, "ext", &attributes);
/* Write the xda:dynamicArrayProperties element. */
_metadata_write_xda_dynamic_array_properties(self);
lxw_xml_end_tag(self->file, "ext");
LXW_FREE_ATTRIBUTES();
}
/*
* Write the <futureMetadata> element.
*/
STATIC void
_metadata_write_future_metadata(lxw_metadata *self)
{
struct xml_attribute_list attributes;
struct xml_attribute *attribute;
LXW_INIT_ATTRIBUTES();
LXW_PUSH_ATTRIBUTES_STR("name", "XLDAPR");
LXW_PUSH_ATTRIBUTES_INT("count", 1);
lxw_xml_start_tag(self->file, "futureMetadata", &attributes);
lxw_xml_start_tag(self->file, "bk", NULL);
lxw_xml_start_tag(self->file, "extLst", NULL);
/* Write the ext element. */
_metadata_write_ext(self);
lxw_xml_end_tag(self->file, "extLst");
lxw_xml_end_tag(self->file, "bk");
lxw_xml_end_tag(self->file, "futureMetadata");
LXW_FREE_ATTRIBUTES();
}
/*
* Write the <rc> element.
*/
STATIC void
_metadata_write_rc(lxw_metadata *self)
{
struct xml_attribute_list attributes;
struct xml_attribute *attribute;
LXW_INIT_ATTRIBUTES();
LXW_PUSH_ATTRIBUTES_STR("t", "1");
LXW_PUSH_ATTRIBUTES_STR("v", "0");
lxw_xml_empty_tag(self->file, "rc", &attributes);
LXW_FREE_ATTRIBUTES();
}
/*
* Write the <cellMetadata> element.
*/
STATIC void
_metadata_write_cell_metadata(lxw_metadata *self)
{
struct xml_attribute_list attributes;
struct xml_attribute *attribute;
LXW_INIT_ATTRIBUTES();
LXW_PUSH_ATTRIBUTES_STR("count", "1");
lxw_xml_start_tag(self->file, "cellMetadata", &attributes);
lxw_xml_start_tag(self->file, "bk", NULL);
/* Write the rc element. */
_metadata_write_rc(self);
lxw_xml_end_tag(self->file, "bk");
lxw_xml_end_tag(self->file, "cellMetadata");
LXW_FREE_ATTRIBUTES();
}
/*****************************************************************************
*
* XML file assembly functions.
*
****************************************************************************/
/*
* Assemble and write the XML file.
*/
void
lxw_metadata_assemble_xml_file(lxw_metadata *self)
{
/* Write the XML declaration. */
_metadata_xml_declaration(self);
/* Write the metadata element. */
_metadata_write_metadata(self);
/* Write the metadataTypes element. */
_metadata_write_metadata_types(self);
/* Write the futureMetadata element. */
_metadata_write_future_metadata(self);
/* Write the cellMetadata element. */
_metadata_write_cell_metadata(self);
lxw_xml_end_tag(self->file, "metadata");
}

View File

@ -755,6 +755,43 @@ mem_error:
return err;
}
/*
* Write the metadata.xml file.
*/
STATIC lxw_error
_write_metadata_file(lxw_packager *self)
{
lxw_error err = LXW_NO_ERROR;
lxw_metadata *metadata;
if (!self->workbook->has_metadata)
return LXW_NO_ERROR;
metadata = lxw_metadata_new();
if (!metadata) {
err = LXW_ERROR_MEMORY_MALLOC_FAILED;
goto mem_error;
}
metadata->file = lxw_tmpfile(self->tmpdir);
if (!metadata->file) {
err = LXW_ERROR_CREATING_TMPFILE;
goto mem_error;
}
lxw_metadata_assemble_xml_file(metadata);
err = _add_file_to_zip(self, metadata->file, "xl/metadata.xml");
fclose(metadata->file);
mem_error:
lxw_metadata_free(metadata);
return err;
}
/*
* Write the custom.xml file.
*/
@ -983,6 +1020,9 @@ _write_content_types_file(lxw_packager *self)
if (!STAILQ_EMPTY(self->workbook->custom_properties))
lxw_ct_add_custom_properties(content_types);
if (workbook->has_metadata)
lxw_ct_add_metadata(content_types);
lxw_content_types_assemble_xml_file(content_types);
err = _add_file_to_zip(self, content_types->file, "[Content_Types].xml");
@ -1046,6 +1086,9 @@ _write_workbook_rels_file(lxw_packager *self)
lxw_add_ms_package_relationship(rels, "/vbaProject",
"vbaProject.bin");
if (workbook->has_metadata)
lxw_add_document_relationship(rels, "/sheetMetadata", "metadata.xml");
lxw_relationships_assemble_xml_file(rels);
err = _add_file_to_zip(self, rels->file, "xl/_rels/workbook.xml.rels");
@ -1507,6 +1550,9 @@ lxw_create_package(lxw_packager *self)
error = _write_core_file(self);
RETURN_AND_ZIPCLOSE_ON_ERROR(error);
error = _write_metadata_file(self);
RETURN_AND_ZIPCLOSE_ON_ERROR(error);
error = _write_app_file(self);
RETURN_AND_ZIPCLOSE_ON_ERROR(error);

View File

@ -2030,7 +2030,7 @@ workbook_close(lxw_workbook *self)
}
}
/* Set the active sheet. */
/* Set the active sheet and check if a metadata file is needed. */
STAILQ_FOREACH(sheet, self->sheets, list_pointers) {
if (sheet->is_chartsheet)
continue;
@ -2039,6 +2039,9 @@ workbook_close(lxw_workbook *self)
if (worksheet->index == self->active_sheet)
worksheet->active = 1;
if (worksheet->has_dynamic_arrays)
self->has_metadata = LXW_TRUE;
}
/* Set workbook and worksheet VBA codenames if a macro has been added. */

View File

@ -800,18 +800,22 @@ _new_formula_cell(lxw_row_t row_num,
*/
STATIC lxw_cell *
_new_array_formula_cell(lxw_row_t row_num, lxw_col_t col_num, char *formula,
char *range, lxw_format *format)
char *range, lxw_format *format, uint8_t is_dynamic)
{
lxw_cell *cell = calloc(1, sizeof(lxw_cell));
RETURN_ON_MEM_ERROR(cell, cell);
cell->row_num = row_num;
cell->col_num = col_num;
cell->type = ARRAY_FORMULA_CELL;
cell->format = format;
cell->u.string = formula;
cell->user_data1 = range;
if (is_dynamic)
cell->type = DYNAMIC_ARRAY_FORMULA_CELL;
else
cell->type = ARRAY_FORMULA_CELL;
return cell;
}
@ -3845,6 +3849,12 @@ _write_cell(lxw_worksheet *self, lxw_cell *cell, lxw_format *row_format)
_write_array_formula_num_cell(self, cell);
lxw_xml_end_tag(self->file, "c");
}
else if (cell->type == DYNAMIC_ARRAY_FORMULA_CELL) {
LXW_PUSH_ATTRIBUTES_STR("cm", "1");
lxw_xml_start_tag(self->file, "c", &attributes);
_write_array_formula_num_cell(self, cell);
lxw_xml_end_tag(self->file, "c");
}
LXW_FREE_ATTRIBUTES();
}
@ -6823,16 +6833,16 @@ worksheet_write_formula(lxw_worksheet *self,
}
/*
* Write a formula with a numerical result to a cell in Excel.
* Internal shared function for various array formula functions.
*/
lxw_error
worksheet_write_array_formula_num(lxw_worksheet *self,
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, double result)
_store_array_formula(lxw_worksheet *self,
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, double result,
uint8_t is_dynamic)
{
lxw_cell *cell;
lxw_row_t tmp_row;
@ -6881,7 +6891,7 @@ worksheet_write_array_formula_num(lxw_worksheet *self,
else
formula_copy = lxw_strdup(formula + 1);
else
formula_copy = lxw_strdup(formula);
formula_copy = lxw_strdup_formula(formula);
/* Strip trailing "}" from formula. */
if (formula_copy[strlen(formula_copy) - 1] == '}')
@ -6889,12 +6899,15 @@ worksheet_write_array_formula_num(lxw_worksheet *self,
/* Create a new array formula cell object. */
cell = _new_array_formula_cell(first_row, first_col,
formula_copy, range, format);
formula_copy, range, format, is_dynamic);
cell->formula_result = result;
_insert_cell(self, first_row, first_col, cell);
if (is_dynamic)
self->has_dynamic_arrays = LXW_TRUE;
/* Pad out the rest of the area with formatted zeroes. */
if (!self->optimize) {
for (tmp_row = first_row; tmp_row <= last_row; tmp_row++) {
@ -6911,7 +6924,24 @@ worksheet_write_array_formula_num(lxw_worksheet *self,
}
/*
* Write an array formula with a default result to a cell in Excel .
* Write an array formula with a numerical result to a cell in Excel.
*/
lxw_error
worksheet_write_array_formula_num(lxw_worksheet *self,
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, double result)
{
return _store_array_formula(self, first_row, first_col,
last_row, last_col, formula, format, result,
LXW_FALSE);
}
/*
* Write an array formula with a default result to a cell in Excel.
*/
lxw_error
worksheet_write_array_formula(lxw_worksheet *self,
@ -6921,9 +6951,42 @@ worksheet_write_array_formula(lxw_worksheet *self,
lxw_col_t last_col,
const char *formula, lxw_format *format)
{
return worksheet_write_array_formula_num(self, first_row, first_col,
last_row, last_col, formula,
format, 0);
return _store_array_formula(self, first_row, first_col,
last_row, last_col, formula, format, 0,
LXW_FALSE);
}
/*
* Write a dynamic array formula with a numerical result to a cell in Excel.
*/
lxw_error
worksheet_write_dynamic_array_formula_num(lxw_worksheet *self,
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, double result)
{
return _store_array_formula(self, first_row, first_col,
last_row, last_col, formula, format, result,
LXW_TRUE);
}
/*
* Write a dynamic array formula with a default result to a cell in Excel.
*/
lxw_error
worksheet_write_dynamic_array_formula(lxw_worksheet *self,
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)
{
return _store_array_formula(self, first_row, first_col,
last_row, last_col, formula, format, 0,
LXW_TRUE);
}
/*

View File

@ -0,0 +1,22 @@
/*****************************************************************************
* Test cases for libxlsxwriter.
*
* Test to compare output against Excel files.
*
* Copyright 2014-2020, John McNamara, jmcnamara@cpan.org
*
*/
#include "xlsxwriter.h"
int main() {
lxw_workbook *workbook = workbook_new("test_dynamic_array01.xlsx");
lxw_worksheet *worksheet = workbook_add_worksheet(workbook, NULL);
worksheet_write_dynamic_array_formula(worksheet, RANGE("A1: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);
}

View File

@ -0,0 +1,17 @@
###############################################################################
#
# Tests for libxlsxwriter.
#
# Copyright 2014-2021, John McNamara, jmcnamara@cpan.org
#
import base_test_class
class TestCompareXLSXFiles(base_test_class.XLSXBaseTest):
"""
Test file created with libxlsxwriter against a file created by Excel.
"""
def test_dynamic_array01(self):
self.run_exe_test('test_dynamic_array01')

Binary file not shown.

View File

@ -40,6 +40,7 @@ SRCS += $(wildcard custom/test*.c)
SRCS += $(wildcard chartsheet/test*.c)
SRCS += $(wildcard vml/test*.c)
SRCS += $(wildcard comment/test*.c)
SRCS += $(wildcard metadata/test*.c)
# End of SRCS
OBJS = $(patsubst %.c,%.o,$(SRCS))
@ -76,6 +77,7 @@ all :
$(Q)$(MAKE) -C chartsheet
$(Q)$(MAKE) -C vml
$(Q)$(MAKE) -C comment
$(Q)$(MAKE) -C metadata
# END make all
clean :
@ -96,6 +98,7 @@ clean :
$(Q)$(MAKE) clean -C chartsheet
$(Q)$(MAKE) clean -C vml
$(Q)$(MAKE) clean -C comment
$(Q)$(MAKE) clean -C metadata
# END make clean

View File

@ -0,0 +1,8 @@
###############################################################################
#
# Makefile for libxlsxwriter library.
#
# Copyright 2014-2015, John McNamara, jmcnamara@cpan.org
#
include ../Makefile.unit

15
test/unit/metadata/main.c Normal file
View File

@ -0,0 +1,15 @@
/*
* Test runner for xmlwriter using ctest.
*
* Copyright 2014-2021 John McNamara, jmcnamara@cpan.org
*
*/
#define CTEST_MAIN
#include "../ctest.h"
int main(int argc, const char *argv[])
{
return ctest_main(argc, argv);
}

View File

@ -0,0 +1,28 @@
/*
* Tests for the libxlsxwriter library.
*
* Copyright 2014-2021, John McNamara, jmcnamara@cpan.org
*
*/
#include "../ctest.h"
#include "../helper.h"
#include "xlsxwriter/metadata.h"
// Test _xml_declaration().
CTEST(metadata, xml_declaration) {
char* got;
char exp[] = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n";
FILE* testfile = tmpfile();
lxw_metadata *metadata = lxw_metadata_new();
metadata->file = testfile;
_metadata_xml_declaration(metadata);
RUN_XLSX_STREQ(exp, got);
lxw_metadata_free(metadata);
}