+ const struct ctables_cell_value *p_cv
+ = (a == ctx->pc_a && i == ctx->pc_a_idx ? pc_cv
+ : &ctx->cell->axes[a].cvs[i]);
+ const struct ctables_cell_value *t_cv = &tc->axes[a].cvs[i];
+ if (p_cv->category != t_cv->category
+ || (p_cv->category->type != CCT_TOTAL
+ && p_cv->category->type != CCT_SUBTOTAL
+ && p_cv->category->type != CCT_POSTCOMPUTE
+ && !value_equal (&p_cv->value,
+ &t_cv->value,
+ var_get_width (nest->vars[i]))))
+ goto not_equal;
+ }
+ }
+
+ goto found;
+
+ not_equal: ;
+ }
+ return 0;
+
+found: ;
+ const struct ctables_table *t = s->table;
+ const struct ctables_nest *specs_nest = s->nests[t->summary_axis];
+ const struct ctables_summary_spec_set *specs = &specs_nest->specs[tc->sv];
+ return ctables_summary_value (tc->areas, &tc->summaries[ctx->summary_idx],
+ &specs->specs[ctx->summary_idx]);
+}
+
+static double
+ctables_pcexpr_evaluate (const struct ctables_pcexpr_evaluate_ctx *ctx,
+ const struct ctables_pcexpr *e)
+{
+ switch (e->op)
+ {
+ case CTPO_CONSTANT:
+ return e->number;
+
+ case CTPO_CAT_NRANGE:
+ case CTPO_CAT_SRANGE:
+ case CTPO_CAT_MISSING:
+ case CTPO_CAT_OTHERNM:
+ {
+ struct ctables_cell_value cv = {
+ .category = ctables_find_category_for_postcompute (ctx->section->table->ctables->dict, ctx->cats, ctx->parse_format, e)
+ };
+ assert (cv.category != NULL);
+
+ struct hmap *occurrences = &ctx->section->occurrences[ctx->pc_a][ctx->pc_a_idx];
+ const struct ctables_occurrence *o;
+
+ double sum = 0.0;
+ const struct variable *var = ctx->section->nests[ctx->pc_a]->vars[ctx->pc_a_idx];
+ HMAP_FOR_EACH (o, struct ctables_occurrence, node, occurrences)
+ if (ctables_categories_match (ctx->cats, &o->value, var) == cv.category)
+ {
+ cv.value = o->value;
+ sum += ctables_pcexpr_evaluate_category (ctx, &cv);
+ }
+ return sum;
+ }
+
+ case CTPO_CAT_NUMBER:
+ case CTPO_CAT_SUBTOTAL:
+ case CTPO_CAT_TOTAL:
+ {
+ struct ctables_cell_value cv = {
+ .category = ctables_find_category_for_postcompute (ctx->section->table->ctables->dict, ctx->cats, ctx->parse_format, e),
+ .value = { .f = e->number },
+ };
+ assert (cv.category != NULL);
+ return ctables_pcexpr_evaluate_category (ctx, &cv);
+ }
+
+ case CTPO_CAT_STRING:
+ {
+ int width = var_get_width (ctx->section->nests[ctx->pc_a]->vars[ctx->pc_a_idx]);
+ char *s = NULL;
+ if (width > e->string.length)
+ {
+ s = xmalloc (width);
+ buf_copy_rpad (s, width, e->string.string, e->string.length, ' ');
+ }
+
+ const struct ctables_category *category
+ = ctables_find_category_for_postcompute (
+ ctx->section->table->ctables->dict,
+ ctx->cats, ctx->parse_format, e);
+ assert (category != NULL);
+
+ struct ctables_cell_value cv = { .category = category };
+ if (category->type == CCT_NUMBER)
+ cv.value.f = category->number;
+ else if (category->type == CCT_STRING)
+ cv.value.s = CHAR_CAST (uint8_t *, s ? s : e->string.string);
+ else
+ NOT_REACHED ();
+
+ double retval = ctables_pcexpr_evaluate_category (ctx, &cv);
+ free (s);
+ return retval;
+ }
+
+ case CTPO_ADD:
+ return ctables_pcexpr_evaluate_nonterminal (ctx, e, 2, ctpo_add);
+
+ case CTPO_SUB:
+ return ctables_pcexpr_evaluate_nonterminal (ctx, e, 2, ctpo_sub);
+
+ case CTPO_MUL:
+ return ctables_pcexpr_evaluate_nonterminal (ctx, e, 2, ctpo_mul);
+
+ case CTPO_DIV:
+ return ctables_pcexpr_evaluate_nonterminal (ctx, e, 2, ctpo_div);
+
+ case CTPO_POW:
+ return ctables_pcexpr_evaluate_nonterminal (ctx, e, 2, ctpo_pow);
+
+ case CTPO_NEG:
+ return ctables_pcexpr_evaluate_nonterminal (ctx, e, 1, ctpo_neg);
+ }
+
+ NOT_REACHED ();
+}
+
+static const struct ctables_category *
+ctables_cell_postcompute (const struct ctables_section *s,
+ const struct ctables_cell *cell,
+ enum pivot_axis_type *pc_a_p,
+ size_t *pc_a_idx_p)
+{
+ assert (cell->postcompute);
+ const struct ctables_category *pc_cat = NULL;
+ for (enum pivot_axis_type pc_a = 0; pc_a < PIVOT_N_AXES; pc_a++)
+ for (size_t pc_a_idx = 0; pc_a_idx < s->nests[pc_a]->n; pc_a_idx++)
+ {
+ const struct ctables_cell_value *cv = &cell->axes[pc_a].cvs[pc_a_idx];
+ if (cv->category->type == CCT_POSTCOMPUTE)
+ {
+ if (pc_cat)
+ {
+ /* Multiple postcomputes cross each other. The value is
+ undefined. */
+ return NULL;
+ }
+
+ pc_cat = cv->category;
+ if (pc_a_p)
+ *pc_a_p = pc_a;
+ if (pc_a_idx_p)
+ *pc_a_idx_p = pc_a_idx;
+ }
+ }
+
+ assert (pc_cat != NULL);
+ return pc_cat;
+}
+
+static double
+ctables_cell_calculate_postcompute (const struct ctables_section *s,
+ const struct ctables_cell *cell,
+ const struct ctables_summary_spec *ss,
+ struct fmt_spec *format,
+ bool *is_ctables_format,
+ size_t summary_idx)
+{
+ enum pivot_axis_type pc_a = 0;
+ size_t pc_a_idx = 0;
+ const struct ctables_category *pc_cat = ctables_cell_postcompute (
+ s, cell, &pc_a, &pc_a_idx);
+ if (!pc_cat)
+ return SYSMIS;
+
+ const struct ctables_postcompute *pc = pc_cat->pc;
+ if (pc->specs)
+ {
+ for (size_t i = 0; i < pc->specs->n; i++)
+ {
+ const struct ctables_summary_spec *ss2 = &pc->specs->specs[i];
+ if (ss->function == ss2->function
+ && ss->weighting == ss2->weighting
+ && ss->calc_area == ss2->calc_area
+ && ss->percentile == ss2->percentile)
+ {
+ *format = ss2->format;
+ *is_ctables_format = ss2->is_ctables_format;
+ break;
+ }
+ }
+ }
+
+ const struct variable *var = s->nests[pc_a]->vars[pc_a_idx];
+ const struct ctables_categories *cats = s->table->categories[
+ var_get_dict_index (var)];
+ struct ctables_pcexpr_evaluate_ctx ctx = {
+ .cell = cell,
+ .section = s,
+ .cats = cats,
+ .pc_a = pc_a,
+ .pc_a_idx = pc_a_idx,
+ .summary_idx = summary_idx,
+ .parse_format = pc_cat->parse_format,
+ };
+ return ctables_pcexpr_evaluate (&ctx, pc->expr);
+}
+
+static char *
+ctables_format (double d, const struct fmt_spec *format,
+ const struct fmt_settings *settings)
+{
+ const union value v = { .f = d };
+ char *s = data_out_stretchy (&v, "UTF-8", format, settings, NULL);
+
+ /* The custom-currency specifications for NEQUAL, PAREN, and PCTPAREN don't
+ produce the results we want for negative numbers, putting the negative
+ sign in the wrong spot, before the prefix instead of after it. We can't,
+ in fact, produce the desired results using a custom-currency
+ specification. Instead, we postprocess the output, moving the negative
+ sign into place:
+
+ NEQUAL: "-N=3" => "N=-3"
+ PAREN: "-(3)" => "(-3)"
+ PCTPAREN: "-(3%)" => "(-3%)"
+
+ This transformation doesn't affect NEGPAREN. */
+ char *minus_src = strchr (s, '-');
+ if (minus_src && (minus_src == s || minus_src[-1] != 'E'))
+ {
+ char *n_equals = strstr (s, "N=");
+ char *lparen = strchr (s, '(');
+ char *minus_dst = n_equals ? n_equals + 1 : lparen;
+ if (minus_dst)
+ move_element (s, minus_dst - s + 1, 1, minus_src - s, minus_dst - s);
+ }
+ return s;
+}
+
+static bool
+all_hidden_vlabels (const struct ctables_table *t, enum pivot_axis_type a)
+{
+ for (size_t i = 0; i < t->stacks[a].n; i++)
+ {
+ struct ctables_nest *nest = &t->stacks[a].nests[i];
+ if (nest->n != 1 || nest->scale_idx != 0)
+ return false;
+
+ enum ctables_vlabel vlabel
+ = t->ctables->vlabels[var_get_dict_index (nest->vars[0])];
+ if (vlabel != CTVL_NONE)
+ return false;
+ }
+ return true;
+}
+
+static void
+ctables_table_output (struct ctables *ct, struct ctables_table *t)
+{
+ struct pivot_table *pt = pivot_table_create__ (
+ (t->title
+ ? pivot_value_new_user_text (t->title, SIZE_MAX)
+ : pivot_value_new_text (N_("Custom Tables"))),
+ "Custom Tables");
+ if (t->caption)
+ pivot_table_set_caption (
+ pt, pivot_value_new_user_text (t->caption, SIZE_MAX));
+ if (t->corner)
+ pivot_table_set_corner_text (
+ pt, pivot_value_new_user_text (t->corner, SIZE_MAX));
+
+ bool summary_dimension = (t->summary_axis != t->slabels_axis
+ || (!t->slabels_visible
+ && t->summary_specs.n > 1));
+ if (summary_dimension)
+ {
+ struct pivot_dimension *d = pivot_dimension_create (
+ pt, t->slabels_axis, N_("Statistics"));
+ const struct ctables_summary_spec_set *specs = &t->summary_specs;
+ if (!t->slabels_visible)
+ d->hide_all_labels = true;
+ for (size_t i = 0; i < specs->n; i++)
+ pivot_category_create_leaf (
+ d->root, ctables_summary_label (&specs->specs[i], t->cilevel));
+ }
+
+ bool categories_dimension = t->clabels_example != NULL;
+ if (categories_dimension)
+ {
+ struct pivot_dimension *d = pivot_dimension_create (
+ pt, t->label_axis[t->clabels_from_axis],
+ t->clabels_from_axis == PIVOT_AXIS_ROW
+ ? N_("Row Categories")
+ : N_("Column Categories"));
+ const struct variable *var = t->clabels_example;
+ const struct ctables_categories *c = t->categories[var_get_dict_index (var)];
+ for (size_t i = 0; i < t->n_clabels_values; i++)
+ {
+ const struct ctables_value *value = t->clabels_values[i];
+ const struct ctables_category *cat = ctables_categories_match (c, &value->value, var);
+ assert (cat != NULL);
+ pivot_category_create_leaf (
+ d->root, ctables_category_create_value_label (c, cat,
+ t->clabels_example,
+ &value->value));
+ }
+ }
+
+ pivot_table_set_look (pt, ct->look);
+ struct pivot_dimension *d[PIVOT_N_AXES];
+ for (enum pivot_axis_type a = 0; a < PIVOT_N_AXES; a++)
+ {
+ static const char *names[] = {
+ [PIVOT_AXIS_ROW] = N_("Rows"),
+ [PIVOT_AXIS_COLUMN] = N_("Columns"),
+ [PIVOT_AXIS_LAYER] = N_("Layers"),
+ };
+ d[a] = (t->axes[a] || a == t->summary_axis
+ ? pivot_dimension_create (pt, a, names[a])
+ : NULL);
+ if (!d[a])
+ continue;
+
+ assert (t->axes[a]);
+
+ for (size_t i = 0; i < t->stacks[a].n; i++)
+ {
+ struct ctables_nest *nest = &t->stacks[a].nests[i];
+ struct ctables_section **sections = xnmalloc (t->n_sections,
+ sizeof *sections);
+ size_t n_sections = 0;
+
+ size_t n_total_cells = 0;
+ size_t max_depth = 0;
+ for (size_t j = 0; j < t->n_sections; j++)
+ if (t->sections[j].nests[a] == nest)
+ {
+ struct ctables_section *s = &t->sections[j];
+ sections[n_sections++] = s;
+ n_total_cells += hmap_count (&s->cells);
+
+ size_t depth = s->nests[a]->n;
+ max_depth = MAX (depth, max_depth);
+ }
+
+ struct ctables_cell **sorted = xnmalloc (n_total_cells,
+ sizeof *sorted);
+ size_t n_sorted = 0;
+
+ for (size_t j = 0; j < n_sections; j++)
+ {
+ struct ctables_section *s = sections[j];
+
+ struct ctables_cell *cell;
+ HMAP_FOR_EACH (cell, struct ctables_cell, node, &s->cells)
+ if (!cell->hide)
+ sorted[n_sorted++] = cell;
+ assert (n_sorted <= n_total_cells);
+ }
+
+ struct ctables_cell_sort_aux aux = { .nest = nest, .a = a };
+ sort (sorted, n_sorted, sizeof *sorted, ctables_cell_compare_3way, &aux);
+
+ struct ctables_level
+ {
+ enum ctables_level_type
+ {
+ CTL_VAR, /* Variable label for nest->vars[var_idx]. */
+ CTL_CATEGORY, /* Category for nest->vars[var_idx]. */
+ CTL_SUMMARY, /* Summary functions. */
+ }
+ type;
+
+ enum settings_value_show vlabel; /* CTL_VAR only. */
+ size_t var_idx;
+ };
+ struct ctables_level *levels = xnmalloc (1 + 2 * max_depth, sizeof *levels);
+ size_t n_levels = 0;
+ for (size_t k = 0; k < nest->n; k++)
+ {
+ enum ctables_vlabel vlabel = ct->vlabels[var_get_dict_index (nest->vars[k])];
+ if (vlabel == CTVL_NONE && nest->scale_idx == k)
+ vlabel = CTVL_NAME;
+ if (vlabel != CTVL_NONE)
+ {
+ levels[n_levels++] = (struct ctables_level) {
+ .type = CTL_VAR,
+ .vlabel = (enum settings_value_show) vlabel,
+ .var_idx = k,
+ };
+ }
+
+ if (nest->scale_idx != k
+ && (k != nest->n - 1 || t->label_axis[a] == a))
+ {
+ levels[n_levels++] = (struct ctables_level) {
+ .type = CTL_CATEGORY,
+ .var_idx = k,
+ };
+ }
+ }
+
+ if (!summary_dimension && a == t->slabels_axis)
+ {
+ levels[n_levels++] = (struct ctables_level) {
+ .type = CTL_SUMMARY,
+ .var_idx = SIZE_MAX,
+ };
+ }
+
+ /* Pivot categories:
+
+ - variable label for nest->vars[0], if vlabel != CTVL_NONE
+ - category for nest->vars[0], if nest->scale_idx != 0
+ - variable label for nest->vars[1], if vlabel != CTVL_NONE
+ - category for nest->vars[1], if nest->scale_idx != 1
+ ...
+ - variable label for nest->vars[n - 1], if vlabel != CTVL_NONE
+ - category for nest->vars[n - 1], if t->label_axis[a] == a && nest->scale_idx != n - 1.
+ - summary function, if 'a == t->slabels_axis && a ==
+ t->summary_axis'.
+
+ Additional dimensions:
+
+ - If 'a == t->slabels_axis && a != t->summary_axis', add a summary
+ dimension.
+ - If 't->label_axis[b] == a' for some 'b != a', add a category
+ dimension to 'a'.
+ */
+
+
+ struct pivot_category **groups = xnmalloc (1 + 2 * max_depth, sizeof *groups);
+ int prev_leaf = 0;
+ for (size_t j = 0; j < n_sorted; j++)
+ {
+ struct ctables_cell *cell = sorted[j];
+ struct ctables_cell *prev = j > 0 ? sorted[j - 1] : NULL;
+
+ size_t n_common = 0;
+ if (j > 0)
+ {
+ for (; n_common < n_levels; n_common++)
+ {
+ const struct ctables_level *level = &levels[n_common];
+ if (level->type == CTL_CATEGORY)
+ {
+ size_t var_idx = level->var_idx;
+ const struct ctables_category *c = cell->axes[a].cvs[var_idx].category;
+ if (prev->axes[a].cvs[var_idx].category != c)
+ break;
+ else if (c->type != CCT_SUBTOTAL
+ && c->type != CCT_TOTAL
+ && c->type != CCT_POSTCOMPUTE
+ && !value_equal (&prev->axes[a].cvs[var_idx].value,
+ &cell->axes[a].cvs[var_idx].value,
+ var_get_type (nest->vars[var_idx])))
+ break;
+ }
+ }
+ }
+
+ for (size_t k = n_common; k < n_levels; k++)
+ {
+ const struct ctables_level *level = &levels[k];
+ struct pivot_category *parent = k ? groups[k - 1] : d[a]->root;
+ if (level->type == CTL_SUMMARY)
+ {
+ assert (k == n_levels - 1);
+
+ const struct ctables_summary_spec_set *specs = &t->summary_specs;
+ for (size_t m = 0; m < specs->n; m++)
+ {
+ int leaf = pivot_category_create_leaf (
+ parent, ctables_summary_label (&specs->specs[m],
+ t->cilevel));
+ if (!m)
+ prev_leaf = leaf;
+ }
+ }
+ else
+ {
+ const struct variable *var = nest->vars[level->var_idx];
+ struct pivot_value *label;
+ if (level->type == CTL_VAR)
+ {
+ label = pivot_value_new_variable (var);
+ label->variable.show = level->vlabel;
+ }
+ else if (level->type == CTL_CATEGORY)
+ {
+ const struct ctables_cell_value *cv = &cell->axes[a].cvs[level->var_idx];
+ label = ctables_category_create_value_label (
+ t->categories[var_get_dict_index (var)],
+ cv->category, var, &cv->value);
+ }
+ else
+ NOT_REACHED ();