+ }
+ else
+ {
+ struct ctables_nest *nest = xmalloc (sizeof *nest);
+ *nest = (struct ctables_nest) { .n = 0 };
+ t->stacks[a] = (struct ctables_stack) { .nests = nest, .n = 1 };
+ }
+
+ struct ctables_stack *stack = &t->stacks[t->summary_axis];
+ for (size_t i = 0; i < stack->n; i++)
+ {
+ struct ctables_nest *nest = &stack->nests[i];
+ if (!nest->specs[CSV_CELL].n)
+ {
+ struct ctables_summary_spec_set *specs = &nest->specs[CSV_CELL];
+ specs->specs = xmalloc (sizeof *specs->specs);
+ specs->n = 1;
+
+ enum ctables_summary_function function
+ = specs->var ? CTSF_MEAN : CTSF_COUNT;
+ struct ctables_var var = { .is_mrset = false, .var = specs->var };
+
+ *specs->specs = (struct ctables_summary_spec) {
+ .function = function,
+ .format = ctables_summary_default_format (function, &var),
+ .label = ctables_summary_default_label (function, 0),
+ };
+ if (!specs->var)
+ specs->var = nest->vars[0];
+
+ ctables_summary_spec_set_clone (&nest->specs[CSV_TOTAL],
+ &nest->specs[CSV_CELL]);
+ }
+ else if (!nest->specs[CSV_TOTAL].n)
+ ctables_summary_spec_set_clone (&nest->specs[CSV_TOTAL],
+ &nest->specs[CSV_CELL]);
+ }
+
+ struct ctables_summary_spec_set *merged = &t->summary_specs;
+ struct merge_item *items = xnmalloc (2 * stack->n, sizeof *items);
+ size_t n_left = 0;
+ for (size_t j = 0; j < stack->n; j++)
+ {
+ const struct ctables_nest *nest = &stack->nests[j];
+ if (nest->n)
+ for (enum ctables_summary_variant sv = 0; sv < N_CSVS; sv++)
+ items[n_left++] = (struct merge_item) { .set = &nest->specs[sv] };
+ }
+
+ while (n_left > 0)
+ {
+ struct merge_item min = items[0];
+ for (size_t j = 1; j < n_left; j++)
+ if (merge_item_compare_3way (&items[j], &min) < 0)
+ min = items[j];
+
+ if (merged->n >= merged->allocated)
+ merged->specs = x2nrealloc (merged->specs, &merged->allocated,
+ sizeof *merged->specs);
+ merged->specs[merged->n++] = min.set->specs[min.ofs];
+
+ for (size_t j = 0; j < n_left; )
+ {
+ if (merge_item_compare_3way (&items[j], &min) == 0)
+ {
+ struct merge_item *item = &items[j];
+ item->set->specs[item->ofs].axis_idx = merged->n - 1;
+ if (++item->ofs >= item->set->n)
+ {
+ items[j] = items[--n_left];
+ continue;
+ }
+ }
+ j++;
+ }
+ }
+
+#if 0
+ for (size_t j = 0; j < merged->n; j++)
+ printf ("%s\n", ctables_summary_function_name (merged->specs[j].function));
+
+ for (size_t j = 0; j < stack->n; j++)
+ {
+ const struct ctables_nest *nest = &stack->nests[j];
+ for (enum ctables_summary_variant sv = 0; sv < N_CSVS; sv++)
+ {
+ const struct ctables_summary_spec_set *specs = &nest->specs[sv];
+ for (size_t k = 0; k < specs->n; k++)
+ printf ("(%s, %zu) ", ctables_summary_function_name (specs->specs[k].function),
+ specs->specs[k].axis_idx);
+ printf ("\n");
+ }
+ }
+#endif
+
+ return (ctables_check_label_position (t, PIVOT_AXIS_ROW)
+ && ctables_check_label_position (t, PIVOT_AXIS_COLUMN));
+}
+
+static void
+ctables_insert_clabels_values (struct ctables_table *t, const struct ccase *c,
+ enum pivot_axis_type a)
+{
+ struct ctables_stack *stack = &t->stacks[a];
+ for (size_t i = 0; i < stack->n; i++)
+ {
+ const struct ctables_nest *nest = &stack->nests[i];
+ const struct variable *var = nest->vars[nest->n - 1];
+ int width = var_get_width (var);
+ const union value *value = case_data (c, var);
+
+ if (var_is_numeric (var) && value->f == SYSMIS)
+ continue;
+
+ if (!ctables_categories_match (t->categories [var_get_dict_index (var)],
+ value, var))
+ continue;
+
+ unsigned int hash = value_hash (value, width, 0);
+
+ struct ctables_value *clv = ctables_value_find__ (t, value, width, hash);
+ if (!clv)
+ {
+ clv = xmalloc (sizeof *clv);
+ value_clone (&clv->value, value, width);
+ hmap_insert (&t->clabels_values_map, &clv->node, hash);
+ }
+ }
+}
+
+static int
+compare_clabels_values_3way (const void *a_, const void *b_, const void *width_)
+{
+ const struct ctables_value *const *ap = a_;
+ const struct ctables_value *const *bp = b_;
+ const struct ctables_value *a = *ap;
+ const struct ctables_value *b = *bp;
+ const int *width = width_;
+ return value_compare_3way (&a->value, &b->value, *width);
+}
+
+static void
+ctables_sort_clabels_values (struct ctables_table *t)
+{
+ int width = var_get_width (t->clabels_example);
+
+ size_t n = hmap_count (&t->clabels_values_map);
+ t->clabels_values = xnmalloc (n, sizeof *t->clabels_values);
+
+ struct ctables_value *clv;
+ size_t i = 0;
+ HMAP_FOR_EACH (clv, struct ctables_value, node, &t->clabels_values_map)
+ t->clabels_values[i++] = clv;
+ t->n_clabels_values = n;
+ assert (i == n);
+
+ sort (t->clabels_values, n, sizeof *t->clabels_values,
+ compare_clabels_values_3way, &width);
+
+ for (size_t i = 0; i < n; i++)
+ t->clabels_values[i]->leaf = i;
+}
+
+static void
+ctables_add_category_occurrences (const struct variable *var,
+ struct hmap *occurrences,
+ const struct ctables_categories *cats)
+{
+ const struct val_labs *val_labs = var_get_value_labels (var);
+
+ for (size_t i = 0; i < cats->n_cats; i++)
+ {
+ const struct ctables_category *c = &cats->cats[i];
+ switch (c->type)
+ {
+ case CCT_NUMBER:
+ ctables_add_occurrence (var, &(const union value) { .f = c->number },
+ occurrences);
+ break;
+
+ case CCT_STRING:
+ abort (); /* XXX */
+
+ case CCT_RANGE:
+ assert (var_is_numeric (var));
+ for (const struct val_lab *vl = val_labs_first (val_labs); vl;
+ vl = val_labs_next (val_labs, vl))
+ if (vl->value.f >= c->range[0] && vl->value.f <= c->range[1])
+ ctables_add_occurrence (var, &vl->value, occurrences);
+ break;
+
+ case CCT_MISSING:
+ for (const struct val_lab *vl = val_labs_first (val_labs); vl;
+ vl = val_labs_next (val_labs, vl))
+ if (var_is_value_missing (var, &vl->value))
+ ctables_add_occurrence (var, &vl->value, occurrences);
+ break;
+
+ case CCT_OTHERNM:
+ for (const struct val_lab *vl = val_labs_first (val_labs); vl;
+ vl = val_labs_next (val_labs, vl))
+ ctables_add_occurrence (var, &vl->value, occurrences);
+ break;
+
+ case CCT_SUBTOTAL:
+ case CCT_HSUBTOTAL:
+ case CCT_TOTAL:
+ break;
+
+ case CCT_VALUE:
+ case CCT_LABEL:
+ case CCT_FUNCTION:
+ for (const struct val_lab *vl = val_labs_first (val_labs); vl;
+ vl = val_labs_next (val_labs, vl))
+ if (c->include_missing || !var_is_value_missing (var, &vl->value))
+ ctables_add_occurrence (var, &vl->value, occurrences);
+ break;
+ }
+ }
+}
+
+static void
+ctables_section_recurse_add_empty_categories (
+ struct ctables_section *s,
+ const struct ctables_category *cats[PIVOT_N_AXES][10], struct ccase *c,
+ enum pivot_axis_type a, size_t a_idx)
+{
+ if (a >= PIVOT_N_AXES)
+ ctables_cell_insert__ (s, c, cats);
+ else if (!s->nests[a] || a_idx >= s->nests[a]->n)
+ ctables_section_recurse_add_empty_categories (s, cats, c, a + 1, 0);
+ else
+ {
+ const struct variable *var = s->nests[a]->vars[a_idx];
+ int width = var_get_width (var);
+ const struct hmap *occurrences = &s->occurrences[a][a_idx];
+ const struct ctables_section_value *sv;
+ HMAP_FOR_EACH (sv, struct ctables_section_value, node, occurrences)
+ {
+ union value *value = case_data_rw (c, var);
+ value_destroy (value, width);
+ value_clone (value, &sv->value, width);
+ cats[a][a_idx] = ctables_categories_match (
+ s->table->categories[var_get_dict_index (var)], value, var);
+ assert (cats[a][a_idx] != NULL);
+ ctables_section_recurse_add_empty_categories (s, cats, c, a, a_idx + 1);
+ }
+ }
+}
+
+static void
+ctables_section_add_empty_categories (struct ctables_section *s)
+{
+ bool show_empty = false;
+ for (size_t a = 0; a < PIVOT_N_AXES; a++)
+ if (s->nests[a])
+ for (size_t k = 0; k < s->nests[a]->n; k++)
+ if (k != s->nests[a]->scale_idx)