Browse Source

Increase the autofilter

viest 7 years ago
parent
commit
70f25d1ad3
4 changed files with 398 additions and 7 deletions
  1. 41 0
      kernel/excel.c
  2. 2 0
      kernel/excel.h
  3. 345 7
      kernel/write.c
  4. 10 0
      kernel/write.h

+ 41 - 0
kernel/excel.c

@@ -61,6 +61,21 @@ ZEND_BEGIN_ARG_INFO_EX(excel_insert_formula_arginfo, 0, 0, 3)
                 ZEND_ARG_INFO(0, formula)
 ZEND_END_ARG_INFO()
 
+ZEND_BEGIN_ARG_INFO_EX(excel_auto_filter_arginfo, 0, 0, 1)
+                ZEND_ARG_INFO(0, range)
+ZEND_END_ARG_INFO()
+
+excel_resource_t * zval_get_resource(zval *handle)
+{
+    excel_resource_t *res;
+
+    if((res = (excel_resource_t *)zend_fetch_resource(Z_RES_P(handle), VTIFUL_RESOURCE_NAME, le_vtiful)) == NULL) {
+        zend_throw_exception(vtiful_exception_ce, "Excel resources resolution fail", 210);
+    }
+
+    return res;
+}
+
 /* {{{ \Vtiful\Kernel\Excel::__construct(array $config)
  */
 PHP_METHOD(vtiful_excel, __construct)
@@ -310,6 +325,31 @@ PHP_METHOD(vtiful_excel, insertFormula)
 }
 /* }}} */
 
+/* {{{ \Vtiful\Kernel\Excel::autoFilter(int $rowStart, int $rowEnd, int $columnStart, int $columnEnd)
+ */
+PHP_METHOD(vtiful_excel, autoFilter)
+{
+    zval rv, res_handle;
+    zval *attr_handle;
+    zend_string *range;
+    excel_resource_t *res;
+
+    ZEND_PARSE_PARAMETERS_START(1, 1)
+            Z_PARAM_STR(range)
+    ZEND_PARSE_PARAMETERS_END();
+
+    ZVAL_COPY(return_value, getThis());
+
+    attr_handle = zend_read_property(vtiful_excel_ce, return_value, ZEND_STRL(V_EXCEL_HANDLE), 0, &rv TSRMLS_DC);
+    res = zval_get_resource(attr_handle);
+
+    auto_filter(range, res);
+
+    ZVAL_RES(&res_handle, zend_register_resource(res, le_vtiful));
+    zend_update_property(vtiful_excel_ce, return_value, ZEND_STRL(V_EXCEL_HANDLE), &res_handle);
+}
+/* }}} */
+
 zend_function_entry excel_methods[] = {
         PHP_ME(vtiful_excel, __construct, excel_construct_arginfo, ZEND_ACC_PUBLIC|ZEND_ACC_CTOR)
         PHP_ME(vtiful_excel, fileName,    excel_file_name_arginfo, ZEND_ACC_PUBLIC)
@@ -317,6 +357,7 @@ zend_function_entry excel_methods[] = {
         PHP_ME(vtiful_excel, data,        excel_data_arginfo,      ZEND_ACC_PUBLIC)
         PHP_ME(vtiful_excel, output,      NULL,                    ZEND_ACC_PUBLIC)
         PHP_ME(vtiful_excel, getHandle,   NULL,                    ZEND_ACC_PUBLIC)
+        PHP_ME(vtiful_excel, autoFilter,    excel_auto_filter_arginfo,    ZEND_ACC_PUBLIC)
         PHP_ME(vtiful_excel, insertText,    excel_insert_text_arginfo,    ZEND_ACC_PUBLIC)
         PHP_ME(vtiful_excel, insertImage,   excel_insert_image_arginfo,   ZEND_ACC_PUBLIC)
         PHP_ME(vtiful_excel, insertFormula, excel_insert_formula_arginfo, ZEND_ACC_PUBLIC)

+ 2 - 0
kernel/excel.h

@@ -29,4 +29,6 @@ extern zend_class_entry *vtiful_excel_ce;
 
 VTIFUL_STARTUP_FUNCTION(excel);
 
+excel_resource_t * zval_get_resource(zval *handle);
+
 #endif

+ 345 - 7
kernel/write.c

@@ -16,10 +16,6 @@
 #include "xlsxwriter.h"
 #include "xlsxwriter/packager.h"
 
-STATIC void _prepare_defined_names(lxw_workbook *self);
-STATIC void _prepare_drawings(lxw_workbook *self);
-STATIC void _add_chart_cache_data(lxw_workbook *self);
-
 /*
  * According to the zval type written to the file
  */
@@ -54,6 +50,14 @@ void formula_writer(zval *value, zend_long row, zend_long columns, excel_resourc
     worksheet_write_formula(res->worksheet, row, columns, ZSTR_VAL(zval_get_string(value)), NULL);
 }
 
+/*
+ * Add the autofilter.
+ */
+void auto_filter(zend_string *range, excel_resource_t *res)
+{
+    worksheet_autofilter(res->worksheet, RANGE(ZSTR_VAL(range)));
+}
+
 /*
  * Call finalization code and close file.
  */
@@ -66,7 +70,7 @@ workbook_file(excel_resource_t *self, zval *handle)
 
     /* Add a default worksheet if non have been added. */
     if (!self->workbook->num_sheets)
-        workbook_add_worksheet(self, NULL);
+        workbook_add_worksheet(self->workbook, NULL);
 
     /* Ensure that at least one worksheet has been selected. */
     if (self->workbook->active_sheet == 0) {
@@ -184,8 +188,7 @@ _prepare_defined_names(lxw_workbook *self)
                          worksheet->quoted_name, area);
 
             /* Autofilters are the only defined name to set the hidden flag. */
-            _store_defined_name(self, "_xlnm._FilterDatabase", app_name,
-                                range, worksheet->index, LXW_TRUE);
+            _store_defined_name(self, "_xlnm._FilterDatabase", app_name, range, worksheet->index, LXW_TRUE);
         }
 
         /*
@@ -368,4 +371,339 @@ _add_chart_cache_data(lxw_workbook *self)
             _populate_range(self, series->title.range);
         }
     }
+}
+
+/*
+ * Process and store the defined names. The defined names are stored with
+ * the Workbook.xml but also with the App.xml if they refer to a sheet
+ * range like "Sheet1!:A1". The defined names are store in sorted
+ * order for consistency with Excel. The names need to be normalized before
+ * sorting.
+ */
+STATIC lxw_error
+_store_defined_name(lxw_workbook *self, const char *name, const char *app_name, const char *formula, int16_t index, uint8_t hidden)
+{
+    lxw_worksheet *worksheet;
+    lxw_defined_name *defined_name;
+    lxw_defined_name *list_defined_name;
+    char name_copy[LXW_DEFINED_NAME_LENGTH];
+    char *tmp_str;
+    char *worksheet_name;
+
+    /* Do some checks on the input data */
+    if (!name || !formula)
+        return LXW_ERROR_NULL_PARAMETER_IGNORED;
+
+    if (lxw_utf8_strlen(name) > LXW_DEFINED_NAME_LENGTH ||
+        lxw_utf8_strlen(formula) > LXW_DEFINED_NAME_LENGTH) {
+        return LXW_ERROR_128_STRING_LENGTH_EXCEEDED;
+    }
+
+    /* Allocate a new defined_name to be added to the linked list of names. */
+    defined_name = calloc(1, sizeof(struct lxw_defined_name));
+    RETURN_ON_MEM_ERROR(defined_name, LXW_ERROR_MEMORY_MALLOC_FAILED);
+
+    /* Copy the user input string. */
+    lxw_strcpy(name_copy, name);
+
+    /* Set the worksheet index or -1 for a global defined name. */
+    defined_name->index = index;
+    defined_name->hidden = hidden;
+
+    /* Check for local defined names like like "Sheet1!name". */
+    tmp_str = strchr(name_copy, '!');
+
+    if (tmp_str == NULL) {
+        /* The name is global. We just store the defined name string. */
+        lxw_strcpy(defined_name->name, name_copy);
+    }
+    else {
+        /* The name is worksheet local. We need to extract the sheet name
+         * and map it to a sheet index. */
+
+        /* Split the into the worksheet name and defined name. */
+        *tmp_str = '\0';
+        tmp_str++;
+        worksheet_name = name_copy;
+
+        /* Remove any worksheet quoting. */
+        if (worksheet_name[0] == '\'')
+            worksheet_name++;
+        if (worksheet_name[strlen(worksheet_name) - 1] == '\'')
+            worksheet_name[strlen(worksheet_name) - 1] = '\0';
+
+        /* Search for worksheet name to get the equivalent worksheet index. */
+        STAILQ_FOREACH(worksheet, self->worksheets, list_pointers) {
+            if (strcmp(worksheet_name, worksheet->name) == 0) {
+                defined_name->index = worksheet->index;
+                lxw_strcpy(defined_name->normalised_sheetname,
+                           worksheet_name);
+            }
+        }
+
+        /* If we didn't find the worksheet name we exit. */
+        if (defined_name->index == -1)
+            goto mem_error;
+
+        lxw_strcpy(defined_name->name, tmp_str);
+    }
+
+    /* Print titles and repeat title pass in the name used for App.xml. */
+    if (app_name) {
+        lxw_strcpy(defined_name->app_name, app_name);
+        lxw_strcpy(defined_name->normalised_sheetname, app_name);
+    }
+    else {
+        lxw_strcpy(defined_name->app_name, name);
+    }
+
+    /* We need to normalize the defined names for sorting. This involves
+     * removing any _xlnm namespace  and converting it to lowercase. */
+    tmp_str = strstr(name_copy, "_xlnm.");
+
+    if (tmp_str)
+        lxw_strcpy(defined_name->normalised_name, defined_name->name + 6);
+    else
+        lxw_strcpy(defined_name->normalised_name, defined_name->name);
+
+    lxw_str_tolower(defined_name->normalised_name);
+    lxw_str_tolower(defined_name->normalised_sheetname);
+
+    /* Strip leading "=" from the formula. */
+    if (formula[0] == '=')
+        lxw_strcpy(defined_name->formula, formula + 1);
+    else
+        lxw_strcpy(defined_name->formula, formula);
+
+    /* We add the defined name to the list in sorted order. */
+    list_defined_name = TAILQ_FIRST(self->defined_names);
+
+    if (list_defined_name == NULL ||
+        _compare_defined_names(defined_name, list_defined_name) < 1) {
+        /* List is empty or defined name goes to the head. */
+        TAILQ_INSERT_HEAD(self->defined_names, defined_name, list_pointers);
+        return LXW_NO_ERROR;
+    }
+
+    TAILQ_FOREACH(list_defined_name, self->defined_names, list_pointers) {
+        int res = _compare_defined_names(defined_name, list_defined_name);
+
+        /* The entry already exists. We exit and don't overwrite. */
+        if (res == 0)
+            goto mem_error;
+
+        /* New defined name is inserted in sorted order before other entries. */
+        if (res < 0) {
+            TAILQ_INSERT_BEFORE(list_defined_name, defined_name,
+                                list_pointers);
+            return LXW_NO_ERROR;
+        }
+    }
+
+    /* If the entry wasn't less than any of the entries in the list we add it
+     * to the end. */
+    TAILQ_INSERT_TAIL(self->defined_names, defined_name, list_pointers);
+    return LXW_NO_ERROR;
+
+    mem_error:
+    free(defined_name);
+    return LXW_ERROR_MEMORY_MALLOC_FAILED;
+}
+
+/*
+ * Compare two defined_name structures.
+ */
+static int
+_compare_defined_names(lxw_defined_name *a, lxw_defined_name *b)
+{
+    int res = strcmp(a->normalised_name, b->normalised_name);
+
+    /* Primary comparison based on defined name. */
+    if (res)
+        return res;
+
+    /* Secondary comparison based on worksheet name. */
+    res = strcmp(a->normalised_sheetname, b->normalised_sheetname);
+
+    return res;
+}
+
+/* Convert a chart range such as Sheet1!$A$1:$A$5 to a sheet name and row-col
+ * dimensions, or vice-versa. This gives us the dimensions to read data back
+ * from the worksheet.
+ */
+STATIC void
+_populate_range_dimensions(lxw_workbook *self, lxw_series_range *range)
+{
+
+    char formula[LXW_MAX_FORMULA_RANGE_LENGTH] = { 0 };
+    char *tmp_str;
+    char *sheetname;
+
+    /* If neither the range formula or sheetname is defined then this probably
+     * isn't a valid range.
+     */
+    if (!range->formula && !range->sheetname) {
+        range->ignore_cache = LXW_TRUE;
+        return;
+    }
+
+    /* If the sheetname is already defined it was already set via
+     * chart_series_set_categories() or  chart_series_set_values().
+     */
+    if (range->sheetname)
+        return;
+
+    /* Ignore non-contiguous range like (Sheet1!$A$1:$A$2,Sheet1!$A$4:$A$5) */
+    if (range->formula[0] == '(') {
+        range->ignore_cache = LXW_TRUE;
+        return;
+    }
+
+    /* Create a copy of the formula to modify and parse into parts. */
+    lxw_snprintf(formula, LXW_MAX_FORMULA_RANGE_LENGTH, "%s", range->formula);
+
+    /* Check for valid formula. TODO. This needs stronger validation. */
+    tmp_str = strchr(formula, '!');
+
+    if (tmp_str == NULL) {
+        range->ignore_cache = LXW_TRUE;
+        return;
+    }
+    else {
+        /* Split the formulas into sheetname and row-col data. */
+        *tmp_str = '\0';
+        tmp_str++;
+        sheetname = formula;
+
+        /* Remove any worksheet quoting. */
+        if (sheetname[0] == '\'')
+            sheetname++;
+        if (sheetname[strlen(sheetname) - 1] == '\'')
+            sheetname[strlen(sheetname) - 1] = '\0';
+
+        /* Check that the sheetname exists. */
+        if (!workbook_get_worksheet_by_name(self, sheetname)) {
+            LXW_WARN_FORMAT2("workbook_add_chart(): worksheet name '%s' "
+                                     "in chart formula '%s' doesn't exist.",
+                             sheetname, range->formula);
+            range->ignore_cache = LXW_TRUE;
+            return;
+        }
+
+        range->sheetname = lxw_strdup(sheetname);
+        range->first_row = lxw_name_to_row(tmp_str);
+        range->first_col = lxw_name_to_col(tmp_str);
+
+        if (strchr(tmp_str, ':')) {
+            /* 2D range. */
+            range->last_row = lxw_name_to_row_2(tmp_str);
+            range->last_col = lxw_name_to_col_2(tmp_str);
+        }
+        else {
+            /* 1D range. */
+            range->last_row = range->first_row;
+            range->last_col = range->first_col;
+        }
+
+    }
+}
+
+/*
+ * Populate the data cache of a chart data series by reading the data from the
+ * relevant worksheet and adding it to the cached in the range object as a
+ * list of points.
+ *
+ * Note, the data cache isn't strictly required by Excel but it helps if the
+ * chart is embedded in another application such as PowerPoint and it also
+ * helps with comparison testing.
+ */
+STATIC void
+_populate_range_data_cache(lxw_workbook *self, lxw_series_range *range)
+{
+    lxw_worksheet *worksheet;
+    lxw_row_t row_num;
+    lxw_col_t col_num;
+    lxw_row *row_obj;
+    lxw_cell *cell_obj;
+    struct lxw_series_data_point *data_point;
+    uint16_t num_data_points = 0;
+
+    /* If ignore_cache is set then don't try to populate the cache. This flag
+     * may be set manually, for testing, or due to a case where the cache
+     * can't be calculated.
+     */
+    if (range->ignore_cache)
+        return;
+
+    /* Currently we only handle 2D ranges so ensure either the rows or cols
+     * are the same.
+     */
+    if (range->first_row != range->last_row
+        && range->first_col != range->last_col) {
+        range->ignore_cache = LXW_TRUE;
+        return;
+    }
+
+    /* Check that the sheetname exists. */
+    worksheet = workbook_get_worksheet_by_name(self, range->sheetname);
+    if (!worksheet) {
+        LXW_WARN_FORMAT2("workbook_add_chart(): worksheet name '%s' "
+                                 "in chart formula '%s' doesn't exist.",
+                         range->sheetname, range->formula);
+        range->ignore_cache = LXW_TRUE;
+        return;
+    }
+
+    /* We can't read the data when worksheet optimization is on. */
+    if (worksheet->optimize) {
+        range->ignore_cache = LXW_TRUE;
+        return;
+    }
+
+    /* Iterate through the worksheet data and populate the range cache. */
+    for (row_num = range->first_row; row_num <= range->last_row; row_num++) {
+        row_obj = lxw_worksheet_find_row(worksheet, row_num);
+
+        for (col_num = range->first_col; col_num <= range->last_col;
+             col_num++) {
+
+            data_point = calloc(1, sizeof(struct lxw_series_data_point));
+            if (!data_point) {
+                range->ignore_cache = LXW_TRUE;
+                return;
+            }
+
+            cell_obj = lxw_worksheet_find_cell(row_obj, col_num);
+
+            if (cell_obj) {
+                if (cell_obj->type == NUMBER_CELL) {
+                    data_point->number = cell_obj->u.number;
+                }
+
+                if (cell_obj->type == STRING_CELL) {
+                    data_point->string = lxw_strdup(cell_obj->sst_string);
+                    data_point->is_string = LXW_TRUE;
+                    range->has_string_cache = LXW_TRUE;
+                }
+            }
+            else {
+                data_point->no_data = LXW_TRUE;
+            }
+
+            STAILQ_INSERT_TAIL(range->data_cache, data_point, list_pointers);
+            num_data_points++;
+        }
+    }
+
+    range->num_data_points = num_data_points;
+}
+
+/* Set the range dimensions and set the data cache.
+ */
+STATIC void
+_populate_range(lxw_workbook *self, lxw_series_range *range)
+{
+    _populate_range_dimensions(self, range);
+    _populate_range_data_cache(self, range);
 }

+ 10 - 0
kernel/write.h

@@ -13,9 +13,19 @@
 #ifndef VTIFUL_EXCEL_WRITE_H
 #define VTIFUL_EXCEL_WRITE_H
 
+STATIC lxw_error _store_defined_name(lxw_workbook *self, const char *name, const char *app_name, const char *formula, int16_t index, uint8_t hidden);
+
+STATIC void _prepare_defined_names(lxw_workbook *self);
+STATIC void _prepare_drawings(lxw_workbook *self);
+STATIC void _add_chart_cache_data(lxw_workbook *self);
+STATIC int  _compare_defined_names(lxw_defined_name *a, lxw_defined_name *b);
+STATIC void _populate_range(lxw_workbook *self, lxw_series_range *range);
+STATIC void _populate_range_dimensions(lxw_workbook *self, lxw_series_range *range);
+
 void type_writer(zval *value, zend_long row, zend_long columns, excel_resource_t *res);
 void image_writer(zval *value, zend_long row, zend_long columns, excel_resource_t *res);
 void formula_writer(zval *value, zend_long row, zend_long columns, excel_resource_t *res);
+void auto_filter(zend_string *range, excel_resource_t *res);
 lxw_error workbook_file(excel_resource_t *self, zval *handle);
 
 #endif