Layered split file for FREQUENCIES works.
[pspp] / src / output / pivot-table.c
index 6f49cd2e6486bb0145eb72d1ea7ae62eb5d1bd4a..1e7d2fbb5ac6b72308f54ace365ade5c4a6053f9 100644 (file)
 
 #include "output/pivot-table.h"
 
+#include <libxml/parser.h>
+#include <libxml/tree.h>
+#include <math.h>
 #include <stdlib.h>
 
 #include "data/data-out.h"
+#include "data/dictionary.h"
 #include "data/settings.h"
 #include "data/value.h"
 #include "data/variable.h"
+#include "data/file-name.h"
+#include "libpspp/array.h"
+#include "libpspp/assertion.h"
 #include "libpspp/hash-functions.h"
 #include "libpspp/i18n.h"
+#include "output/driver.h"
+#include "output/spv/spv-table-look.h"
 
 #include "gl/c-ctype.h"
+#include "gl/configmake.h"
 #include "gl/intprops.h"
 #include "gl/minmax.h"
+#include "gl/relocatable.h"
 #include "gl/xalloc.h"
 #include "gl/xmemdup0.h"
 #include "gl/xsize.h"
@@ -38,8 +49,8 @@
 #define _(msgid) gettext (msgid)
 #define N_(msgid) msgid
 
-static const struct fmt_spec *pivot_table_get_format (
-  const struct pivot_table *, const char *s);
+static void pivot_table_use_rc (const struct pivot_table *, const char *s,
+                                struct fmt_spec *, bool *honor_small);
 \f
 /* Pivot table display styling. */
 
@@ -61,70 +72,6 @@ pivot_area_to_string (enum pivot_area area)
     }
 }
 
-void
-pivot_area_get_default_style (enum pivot_area area, struct area_style *style)
-{
-#define STYLE(BOLD, H, V, L, R, T, B) {                         \
-    .cell_style = {                                             \
-      .halign = TABLE_HALIGN_##H,                               \
-      .valign = TABLE_VALIGN_##V,                               \
-      .margin = { [TABLE_HORZ][0] = L, [TABLE_HORZ][1] = R,     \
-                  [TABLE_VERT][0] = T, [TABLE_VERT][1] = B },   \
-    },                                                          \
-    .font_style = {                                             \
-      .bold = BOLD,                                             \
-      .fg = { [0] = CELL_COLOR_BLACK, [1] = CELL_COLOR_BLACK},  \
-      .bg = { [0] = CELL_COLOR_WHITE, [1] = CELL_COLOR_WHITE},  \
-      .size = 9,                                                \
-    },                                                          \
-  }
-  static const struct area_style default_area_styles[PIVOT_N_AREAS] = {
-    [PIVOT_AREA_TITLE]         = STYLE( true, CENTER, CENTER,  8,11,1,8),
-    [PIVOT_AREA_CAPTION]       = STYLE(false, LEFT,   TOP,     8,11,1,1),
-    [PIVOT_AREA_FOOTER]        = STYLE(false, LEFT,   TOP,    11, 8,2,3),
-    [PIVOT_AREA_CORNER]        = STYLE(false, LEFT,   BOTTOM,  8,11,1,1),
-    [PIVOT_AREA_COLUMN_LABELS] = STYLE(false, CENTER, BOTTOM,  8,11,1,3),
-    [PIVOT_AREA_ROW_LABELS]    = STYLE(false, LEFT,   TOP,     8,11,1,3),
-    [PIVOT_AREA_DATA]          = STYLE(false, MIXED,  TOP,     8,11,1,1),
-    [PIVOT_AREA_LAYERS]        = STYLE(false, LEFT,   BOTTOM,  8,11,1,3),
-    };
-#undef STYLE
-
-  *style = default_area_styles[area];
-  style->font_style.typeface = xstrdup ("SansSerif");
-}
-
-void
-pivot_border_get_default_style (enum pivot_border border,
-                                struct table_border_style *style)
-{
-  static const enum table_stroke default_strokes[PIVOT_N_BORDERS] = {
-    [PIVOT_BORDER_TITLE]        = TABLE_STROKE_NONE,
-    [PIVOT_BORDER_OUTER_LEFT]   = TABLE_STROKE_NONE,
-    [PIVOT_BORDER_OUTER_TOP]    = TABLE_STROKE_NONE,
-    [PIVOT_BORDER_OUTER_RIGHT]  = TABLE_STROKE_NONE,
-    [PIVOT_BORDER_OUTER_BOTTOM] = TABLE_STROKE_NONE,
-    [PIVOT_BORDER_INNER_LEFT]   = TABLE_STROKE_THICK,
-    [PIVOT_BORDER_INNER_TOP]    = TABLE_STROKE_THICK,
-    [PIVOT_BORDER_INNER_RIGHT]  = TABLE_STROKE_THICK,
-    [PIVOT_BORDER_INNER_BOTTOM] = TABLE_STROKE_THICK,
-    [PIVOT_BORDER_DATA_LEFT]    = TABLE_STROKE_THICK,
-    [PIVOT_BORDER_DATA_TOP]     = TABLE_STROKE_THICK,
-    [PIVOT_BORDER_DIM_ROW_HORZ] = TABLE_STROKE_SOLID,
-    [PIVOT_BORDER_DIM_ROW_VERT] = TABLE_STROKE_NONE,
-    [PIVOT_BORDER_DIM_COL_HORZ] = TABLE_STROKE_SOLID,
-    [PIVOT_BORDER_DIM_COL_VERT] = TABLE_STROKE_SOLID,
-    [PIVOT_BORDER_CAT_ROW_HORZ] = TABLE_STROKE_NONE,
-    [PIVOT_BORDER_CAT_ROW_VERT] = TABLE_STROKE_NONE,
-    [PIVOT_BORDER_CAT_COL_HORZ] = TABLE_STROKE_SOLID,
-    [PIVOT_BORDER_CAT_COL_VERT] = TABLE_STROKE_SOLID,
-  };
-  *style = (struct table_border_style) {
-      .stroke = default_strokes[border],
-      .color = CELL_COLOR_BLACK,
-  };
-}
-
 /* Returns the name of BORDER. */
 const char *
 pivot_border_to_string (enum pivot_border border)
@@ -192,6 +139,203 @@ pivot_table_sizing_uninit (struct pivot_table_sizing *sizing)
     }
 }
 \f
+/* Pivot table looks. */
+
+static const struct pivot_table_look *
+default_look (const struct pivot_table_look *new)
+{
+  static struct pivot_table_look *look;
+  if (new)
+    {
+      pivot_table_look_unref (look);
+      look = pivot_table_look_ref (new);
+    }
+  else if (!look)
+    {
+      char *error = pivot_table_look_read ("default.stt", &look);
+      if (error)
+        {
+          free (error);
+          look = pivot_table_look_ref (pivot_table_look_builtin_default ());
+        }
+    }
+  return look;
+}
+
+const struct pivot_table_look *
+pivot_table_look_get_default (void)
+{
+  return default_look (NULL);
+}
+
+void
+pivot_table_look_set_default (const struct pivot_table_look *look)
+{
+  default_look (look);
+}
+
+char * WARN_UNUSED_RESULT
+pivot_table_look_read (const char *name, struct pivot_table_look **lookp)
+{
+  *lookp = NULL;
+
+  /* Construct search path. */
+  const char *path[4];
+  size_t n = 0;
+  path[n++] = ".";
+  const char *home = getenv ("HOME");
+  char *allocated = NULL;
+  if (home != NULL)
+    path[n++] = allocated = xasprintf ("%s/.pspp/looks", home);
+  char *allocated2;
+  path[n++] = relocate2 (PKGDATADIR "/looks", &allocated2);
+  path[n++] = NULL;
+
+  /* Search path. */
+  char *file = fn_search_path (name, (char **) path);
+  if (!file)
+    {
+      char *name2 = xasprintf ("%s.stt", name);
+      file = fn_search_path (name2, (char **) path);
+      free (name2);
+    }
+  free (allocated);
+  free (allocated2);
+  if (!file)
+    return xasprintf ("%s: not found", name);
+
+  /* Read file. */
+  char *error = spv_table_look_read (file, lookp);
+  free (file);
+  return error;
+}
+
+const struct pivot_table_look *
+pivot_table_look_builtin_default (void)
+{
+  static struct pivot_table_look look = {
+    .ref_cnt = 1,
+
+    .omit_empty = true,
+    .row_labels_in_corner = true,
+    .width_ranges = {
+      [TABLE_HORZ] = { 36, 72 },
+      [TABLE_VERT] = { 36, 120 },
+    },
+
+    .areas = {
+#define AREA(BOLD, H, V, L, R, T, B) {                         \
+    .cell_style = {                                             \
+      .halign = TABLE_HALIGN_##H,                               \
+      .valign = TABLE_VALIGN_##V,                               \
+      .margin = { [TABLE_HORZ][0] = L, [TABLE_HORZ][1] = R,     \
+                  [TABLE_VERT][0] = T, [TABLE_VERT][1] = B },   \
+    },                                                          \
+    .font_style = {                                             \
+      .bold = BOLD,                                             \
+      .fg = { [0] = CELL_COLOR_BLACK, [1] = CELL_COLOR_BLACK},  \
+      .bg = { [0] = CELL_COLOR_WHITE, [1] = CELL_COLOR_WHITE},  \
+      .size = 9,                                                \
+      .typeface = (char *) "Sans Serif",                        \
+    },                                                          \
+  }
+      [PIVOT_AREA_TITLE]         = AREA(true,  CENTER, CENTER,  8,11,1,8),
+      [PIVOT_AREA_CAPTION]       = AREA(false, LEFT,   TOP,     8,11,1,1),
+      [PIVOT_AREA_FOOTER]        = AREA(false, LEFT,   TOP,    11, 8,2,3),
+      [PIVOT_AREA_CORNER]        = AREA(false, LEFT,   BOTTOM,  8,11,1,1),
+      [PIVOT_AREA_COLUMN_LABELS] = AREA(false, CENTER, BOTTOM,  8,11,1,3),
+      [PIVOT_AREA_ROW_LABELS]    = AREA(false, LEFT,   TOP,     8,11,1,3),
+      [PIVOT_AREA_DATA]          = AREA(false, MIXED,  TOP,     8,11,1,1),
+      [PIVOT_AREA_LAYERS]        = AREA(false, LEFT,   BOTTOM,  8,11,1,3),
+#undef AREA
+    },
+
+    .borders = {
+#define BORDER(STROKE) { .stroke = STROKE, .color = CELL_COLOR_BLACK }
+      [PIVOT_BORDER_TITLE]        = BORDER(TABLE_STROKE_NONE),
+      [PIVOT_BORDER_OUTER_LEFT]   = BORDER(TABLE_STROKE_NONE),
+      [PIVOT_BORDER_OUTER_TOP]    = BORDER(TABLE_STROKE_NONE),
+      [PIVOT_BORDER_OUTER_RIGHT]  = BORDER(TABLE_STROKE_NONE),
+      [PIVOT_BORDER_OUTER_BOTTOM] = BORDER(TABLE_STROKE_NONE),
+      [PIVOT_BORDER_INNER_LEFT]   = BORDER(TABLE_STROKE_THICK),
+      [PIVOT_BORDER_INNER_TOP]    = BORDER(TABLE_STROKE_THICK),
+      [PIVOT_BORDER_INNER_RIGHT]  = BORDER(TABLE_STROKE_THICK),
+      [PIVOT_BORDER_INNER_BOTTOM] = BORDER(TABLE_STROKE_THICK),
+      [PIVOT_BORDER_DATA_LEFT]    = BORDER(TABLE_STROKE_THICK),
+      [PIVOT_BORDER_DATA_TOP]     = BORDER(TABLE_STROKE_THICK),
+      [PIVOT_BORDER_DIM_ROW_HORZ] = BORDER(TABLE_STROKE_SOLID),
+      [PIVOT_BORDER_DIM_ROW_VERT] = BORDER(TABLE_STROKE_NONE),
+      [PIVOT_BORDER_DIM_COL_HORZ] = BORDER(TABLE_STROKE_SOLID),
+      [PIVOT_BORDER_DIM_COL_VERT] = BORDER(TABLE_STROKE_SOLID),
+      [PIVOT_BORDER_CAT_ROW_HORZ] = BORDER(TABLE_STROKE_NONE),
+      [PIVOT_BORDER_CAT_ROW_VERT] = BORDER(TABLE_STROKE_NONE),
+      [PIVOT_BORDER_CAT_COL_HORZ] = BORDER(TABLE_STROKE_SOLID),
+      [PIVOT_BORDER_CAT_COL_VERT] = BORDER(TABLE_STROKE_SOLID),
+    },
+  };
+
+  return &look;
+}
+
+struct pivot_table_look *
+pivot_table_look_new_builtin_default (void)
+{
+  return pivot_table_look_unshare (
+    pivot_table_look_ref (pivot_table_look_builtin_default ()));
+}
+
+struct pivot_table_look *
+pivot_table_look_ref (const struct pivot_table_look *look_)
+{
+  assert (look_->ref_cnt > 0);
+
+  struct pivot_table_look *look = CONST_CAST (struct pivot_table_look *, look_);
+  look->ref_cnt++;
+  return look;
+}
+
+static char *
+xstrdup_if_nonempty (const char *s)
+{
+  return s && s[0] ? xstrdup (s) : NULL;
+}
+
+struct pivot_table_look *
+pivot_table_look_unshare (struct pivot_table_look *old)
+{
+  assert (old->ref_cnt > 0);
+  if (old->ref_cnt == 1)
+    return old;
+
+  pivot_table_look_unref (old);
+
+  struct pivot_table_look *new = xmemdup (old, sizeof *old);
+  new->ref_cnt = 1;
+  new->name = xstrdup_if_nonempty (old->name);
+  for (size_t i = 0; i < PIVOT_N_AREAS; i++)
+    table_area_style_copy (NULL, &new->areas[i], &old->areas[i]);
+  new->continuation = xstrdup_if_nonempty (old->continuation);
+
+  return new;
+}
+
+void
+pivot_table_look_unref (struct pivot_table_look *look)
+{
+  if (look)
+    {
+      assert (look->ref_cnt > 0);
+      if (!--look->ref_cnt)
+        {
+          free (look->name);
+          for (size_t i = 0; i < PIVOT_N_AREAS; i++)
+            table_area_style_uninit (&look->areas[i]);
+          free (look->continuation);
+          free (look);
+        }
+    }
+}
+\f
 /* Axes. */
 
 /* Returns the name of AXIS_TYPE. */
@@ -232,7 +376,8 @@ pivot_axis_iterator_next (size_t *indexes, const struct pivot_axis *axis)
           if (axis->dimensions[i]->n_leaves == 0)
             return NULL;
 
-      return xcalloc (axis->n_dimensions, sizeof *indexes);
+      size_t size = axis->n_dimensions * sizeof *indexes;
+      return xzalloc (MAX (size, 1));
     }
 
   for (size_t i = 0; i < axis->n_dimensions; i++)
@@ -253,10 +398,19 @@ pivot_axis_iterator_next (size_t *indexes, const struct pivot_axis *axis)
 static void
 pivot_category_set_rc (struct pivot_category *category, const char *s)
 {
-  const struct fmt_spec *format = pivot_table_get_format (
-    category->dimension->table, s);
-  if (format)
-    category->format = *format;
+  if (!s)
+    return;
+
+  pivot_table_use_rc (category->dimension->table, s,
+                      &category->format, &category->honor_small);
+
+  /* Ensure that the category itself, in addition to the cells within it, takes
+     the format.  (It's kind of rare for a category to have a numeric format
+     though.) */
+  struct pivot_value *name = category->name;
+  if (name->type == PIVOT_VALUE_NUMERIC && !name->numeric.format.w)
+    pivot_table_use_rc (category->dimension->table, s,
+                        &name->numeric.format, &name->numeric.honor_small);
 }
 
 static void
@@ -350,7 +504,7 @@ pivot_dimension_create__ (struct pivot_table *table,
                                       sizeof *table->current_layer);
     }
 
-  /* XXX extent and label_depth need to be calculated later. */
+  /* axis->extent and axis->label_depth will be calculated later. */
 
   return d;
 }
@@ -595,11 +749,11 @@ struct result_class
 /* Formats for most of the result classes. */
 static struct result_class result_classes[] =
   {
-    { PIVOT_RC_INTEGER,      { FMT_F,   40, 0 } },
-    { PIVOT_RC_PERCENT,      { FMT_PCT, 40, 1 } },
-    { PIVOT_RC_CORRELATION,  { FMT_F,   40, 3 } },
-    { PIVOT_RC_SIGNIFICANCE, { FMT_F,   40, 3 } },
-    { PIVOT_RC_RESIDUAL,     { FMT_F,   40, 2 } },
+    { PIVOT_RC_INTEGER,      { .type = FMT_F,   .w = 40, .d = 0 } },
+    { PIVOT_RC_PERCENT,      { .type = FMT_PCT, .w = 40, .d = 1 } },
+    { PIVOT_RC_CORRELATION,  { .type = FMT_F,   .w = 40, .d = 3 } },
+    { PIVOT_RC_SIGNIFICANCE, { .type = FMT_F,   .w = 40, .d = 3 } },
+    { PIVOT_RC_RESIDUAL,     { .type = FMT_F,   .w = 40, .d = 2 } },
     { PIVOT_RC_COUNT,        { 0, 0, 0 } },
     { PIVOT_RC_OTHER,        { 0, 0, 0 } },
   };
@@ -616,19 +770,35 @@ pivot_result_class_find (const char *s)
   return NULL;
 }
 
-static const struct fmt_spec *
-pivot_table_get_format (const struct pivot_table *table, const char *s)
+static void
+pivot_table_use_rc (const struct pivot_table *table, const char *s,
+                    struct fmt_spec *format, bool *honor_small)
 {
-  if (!s)
-    return NULL;
-  else if (!strcmp (s, PIVOT_RC_OTHER))
-    return settings_get_format ();
-  else if (!strcmp (s, PIVOT_RC_COUNT) && !overridden_count_format)
-    return &table->weight_format;
-  else
+  if (s)
     {
-      const struct result_class *rc = pivot_result_class_find (s);
-      return rc ? &rc->format : NULL;
+      if (!strcmp (s, PIVOT_RC_OTHER))
+        {
+          *format = *settings_get_format ();
+          *honor_small = true;
+        }
+      else if (!strcmp (s, PIVOT_RC_COUNT) && !overridden_count_format)
+        {
+          *format = table->weight_format;
+          *honor_small = false;
+        }
+      else
+        {
+          const struct result_class *rc = pivot_result_class_find (s);
+          if (rc)
+            {
+              *format = rc->format;
+              *honor_small = false;
+            }
+          else
+            {
+              printf ("unknown class %s\n", s);
+            }
+        }
     }
 }
 
@@ -650,101 +820,66 @@ pivot_result_class_change (const char *s_, const struct fmt_spec *format)
 
   return rc != NULL;
 }
-\f
-/* One piece of data within a pivot table. */
-struct pivot_cell
-  {
-    struct hmap_node hmap_node; /* In struct pivot_table's 'cells' hmap. */
-    struct pivot_value *value;
-    unsigned int idx[];         /* One index per table dimension. */
-  };
 
+bool
+is_pivot_result_class (const char *s)
+{
+  return pivot_result_class_find (s) != NULL;
+}
+\f
 /* Pivot tables. */
 
+static struct pivot_cell *pivot_table_insert_cell (struct pivot_table *,
+                                                   const size_t *dindexes);
+static void pivot_table_delete_cell (struct pivot_table *,
+                                     struct pivot_cell *);
+
 /* Creates and returns a new pivot table with the given TITLE.  TITLE should be
    a text string marked for translation but not actually translated yet,
-   e.g. N_("Descriptive Statistics").
-
-   Operations commonly performed on the new pivot_table:
-
-   - If empty rows or columns should not be displayed, set ->omit_empty to
-     true.
-
-   - Set the format to use for "count" values with pivot_table_set_weight_var()
-     or pivot_table_set_weight_format().
+   e.g. N_("Descriptive Statistics").  The un-translated text string is used as
+   the pivot table's subtype.
 
    This function is a shortcut for pivot_table_create__() for the most common
    case.  Use pivot_table_create__() directly if the title should be some kind
-   of value other than an ordinary text string.
+   of value other than an ordinary text string, or if the subtype should be
+   different from the title.
 
    See the large comment at the top of pivot-table.h for general advice on
    creating pivot tables. */
 struct pivot_table *
 pivot_table_create (const char *title)
 {
-  return pivot_table_create__ (pivot_value_new_text (title));
+  return pivot_table_create__ (pivot_value_new_text (title), title);
 }
 
 /* Creates and returns a new pivot table with the given TITLE, and takes
-   ownership of TITLE.
-
-   Operations commonly performed on the new pivot_table:
+   ownership of TITLE.  The new pivot table's subtype is SUBTYPE, which should
+   be an untranslated English string that describes the contents of the table
+   at a high level without being specific about the variables or other context
+   involved.
 
-   - If empty rows or columns should not be displayed, set ->omit_empty to
-     true.
-
-   - Set the format to use for "count" values with pivot_table_set_weight_var()
-     or pivot_table_set_weight_format().
+   TITLE and SUBTYPE may be NULL, but in that case the client must add them
+   later because they are both mandatory for a pivot table.
 
    See the large comment at the top of pivot-table.h for general advice on
    creating pivot tables. */
 struct pivot_table *
-pivot_table_create__ (struct pivot_value *title)
-{
-  struct pivot_table *table = xzalloc (sizeof *table);
-  table->ref_cnt = 1;
-  table->weight_format = (struct fmt_spec) { FMT_F, 40, 0 };
-  table->title = title;
-
-  table->sizing[TABLE_HORZ].range[0] = 50;
-  table->sizing[TABLE_HORZ].range[1] = 72;
-  table->sizing[TABLE_VERT].range[0] = 36;
-  table->sizing[TABLE_VERT].range[1] = 120;
-
-  for (size_t i = 0; i < PIVOT_N_AREAS; i++)
-    pivot_area_get_default_style (i, &table->areas[i]);
-
-  /* Set default border styles. */
-  static const enum table_stroke default_strokes[PIVOT_N_BORDERS] = {
-    [PIVOT_BORDER_TITLE]        = TABLE_STROKE_NONE,
-    [PIVOT_BORDER_OUTER_LEFT]   = TABLE_STROKE_NONE,
-    [PIVOT_BORDER_OUTER_TOP]    = TABLE_STROKE_NONE,
-    [PIVOT_BORDER_OUTER_RIGHT]  = TABLE_STROKE_NONE,
-    [PIVOT_BORDER_OUTER_BOTTOM] = TABLE_STROKE_NONE,
-    [PIVOT_BORDER_INNER_LEFT]   = TABLE_STROKE_THICK,
-    [PIVOT_BORDER_INNER_TOP]    = TABLE_STROKE_THICK,
-    [PIVOT_BORDER_INNER_RIGHT]  = TABLE_STROKE_THICK,
-    [PIVOT_BORDER_INNER_BOTTOM] = TABLE_STROKE_THICK,
-    [PIVOT_BORDER_DATA_LEFT]    = TABLE_STROKE_THICK,
-    [PIVOT_BORDER_DATA_TOP]     = TABLE_STROKE_THICK,
-    [PIVOT_BORDER_DIM_ROW_HORZ] = TABLE_STROKE_SOLID,
-    [PIVOT_BORDER_DIM_ROW_VERT] = TABLE_STROKE_NONE,
-    [PIVOT_BORDER_DIM_COL_HORZ] = TABLE_STROKE_SOLID,
-    [PIVOT_BORDER_DIM_COL_VERT] = TABLE_STROKE_SOLID,
-    [PIVOT_BORDER_CAT_ROW_HORZ] = TABLE_STROKE_NONE,
-    [PIVOT_BORDER_CAT_ROW_VERT] = TABLE_STROKE_NONE,
-    [PIVOT_BORDER_CAT_COL_HORZ] = TABLE_STROKE_SOLID,
-    [PIVOT_BORDER_CAT_COL_VERT] = TABLE_STROKE_SOLID,
+pivot_table_create__ (struct pivot_value *title, const char *subtype)
+{
+  struct pivot_table *table = xmalloc (sizeof *table);
+  *table = (struct pivot_table) {
+    .ref_cnt = 1,
+    .show_title = true,
+    .show_caption = true,
+    .weight_format = (struct fmt_spec) { .type = FMT_F, .w = 40 },
+    .title = title,
+    .subtype = subtype ? pivot_value_new_text (subtype) : NULL,
+    .command_c = xstrdup_if_nonempty (output_get_command_name ()),
+    .look = pivot_table_look_ref (pivot_table_look_get_default ()),
+    .settings = fmt_settings_copy (settings_get_fmt_settings ()),
+    .small = settings_get_small (),
+    .cells = HMAP_INITIALIZER (table->cells),
   };
-  for (size_t i = 0; i < PIVOT_N_BORDERS; i++)
-    table->borders[i] = (struct table_border_style) {
-      .stroke = default_strokes[i],
-      .color = CELL_COLOR_BLACK,
-    };
-
-  table->row_labels_in_corner = true;
-  hmap_init (&table->cells);
-
   return table;
 }
 
@@ -756,7 +891,7 @@ struct pivot_table *
 pivot_table_create_for_text (struct pivot_value *title,
                              struct pivot_value *content)
 {
-  struct pivot_table *table = pivot_table_create__ (title);
+  struct pivot_table *table = pivot_table_create__ (title, "Error");
 
   struct pivot_dimension *d = pivot_dimension_create (
     table, PIVOT_AXIS_ROW, N_("Error"));
@@ -779,6 +914,222 @@ pivot_table_ref (const struct pivot_table *table_)
   return table;
 }
 
+static struct pivot_table_sizing
+clone_sizing (const struct pivot_table_sizing *s)
+{
+  return (struct pivot_table_sizing) {
+    .widths = (s->n_widths
+               ? xmemdup (s->widths, s->n_widths * sizeof *s->widths)
+               : NULL),
+    .n_widths = s->n_widths,
+
+    .breaks = (s->n_breaks
+               ? xmemdup (s->breaks, s->n_breaks * sizeof *s->breaks)
+               : NULL),
+    .n_breaks = s->n_breaks,
+
+    .keeps = (s->n_keeps
+              ? xmemdup (s->keeps, s->n_keeps * sizeof *s->keeps)
+              : NULL),
+    .n_keeps = s->n_keeps,
+  };
+}
+
+static struct pivot_footnote **
+clone_footnotes (struct pivot_footnote **old, size_t n)
+{
+  if (!n)
+    return NULL;
+
+  struct pivot_footnote **new = xmalloc (n * sizeof *new);
+  for (size_t i = 0; i < n; i++)
+    {
+      new[i] = xmalloc (sizeof *new[i]);
+      *new[i] = (struct pivot_footnote) {
+        .idx = old[i]->idx,
+        .content = pivot_value_clone (old[i]->content),
+        .marker = pivot_value_clone (old[i]->marker),
+        .show = old[i]->show,
+      };
+    }
+  return new;
+}
+
+static struct pivot_category *
+clone_category (struct pivot_category *old,
+                struct pivot_dimension *new_dimension,
+                struct pivot_category *new_parent)
+{
+  struct pivot_category *new = xmalloc (sizeof *new);
+  *new = (struct pivot_category) {
+    .name = pivot_value_clone (old->name),
+    .parent = new_parent,
+    .dimension = new_dimension,
+    .label_depth = old->label_depth,
+    .extra_depth = old->extra_depth,
+
+    .subs = (old->n_subs
+             ? xcalloc (old->n_subs, sizeof *new->subs)
+             : NULL),
+    .n_subs = old->n_subs,
+    .allocated_subs = old->n_subs,
+
+    .show_label = old->show_label,
+    .show_label_in_corner = old->show_label_in_corner,
+
+    .format = old->format,
+    .group_index = old->group_index,
+    .data_index = old->data_index,
+    .presentation_index = old->presentation_index,
+  };
+
+  if (pivot_category_is_leaf (old))
+    {
+      assert (new->data_index < new_dimension->n_leaves);
+      new->dimension->data_leaves[new->data_index] = new;
+
+      assert (new->presentation_index < new_dimension->n_leaves);
+      new->dimension->presentation_leaves[new->presentation_index] = new;
+    }
+
+  for (size_t i = 0; i < new->n_subs; i++)
+    new->subs[i] = clone_category (old->subs[i], new_dimension, new);
+
+  return new;
+}
+
+static struct pivot_dimension *
+clone_dimension (struct pivot_dimension *old, struct pivot_table *new_pt)
+{
+  struct pivot_dimension *new = xmalloc (sizeof *new);
+  *new = (struct pivot_dimension) {
+    .table = new_pt,
+    .axis_type = old->axis_type,
+    .level = old->level,
+    .top_index = old->top_index,
+    .data_leaves = xcalloc (old->n_leaves , sizeof *new->data_leaves),
+    .presentation_leaves = xcalloc (old->n_leaves
+                                    , sizeof *new->presentation_leaves),
+    .n_leaves = old->n_leaves,
+    .allocated_leaves = old->n_leaves,
+    .hide_all_labels = old->hide_all_labels,
+    .label_depth = old->label_depth,
+  };
+
+  new->root = clone_category (old->root, new, NULL);
+
+  return new;
+}
+
+static struct pivot_dimension **
+clone_dimensions (struct pivot_dimension **old, size_t n,
+                  struct pivot_table *new_pt)
+{
+  if (!n)
+    return NULL;
+
+  struct pivot_dimension **new = xmalloc (n * sizeof *new);
+  for (size_t i = 0; i < n; i++)
+    new[i] = clone_dimension (old[i], new_pt);
+  return new;
+}
+
+struct pivot_table *
+pivot_table_unshare (struct pivot_table *old)
+{
+  assert (old->ref_cnt > 0);
+  if (old->ref_cnt == 1)
+    return old;
+
+  pivot_table_unref (old);
+
+  struct pivot_table *new = xmalloc (sizeof *new);
+  *new = (struct pivot_table) {
+    .ref_cnt = 1,
+
+    .look = pivot_table_look_ref (old->look),
+
+    .rotate_inner_column_labels = old->rotate_inner_column_labels,
+    .rotate_outer_row_labels = old->rotate_outer_row_labels,
+    .show_grid_lines = old->show_grid_lines,
+    .show_title = old->show_title,
+    .show_caption = old->show_caption,
+    .current_layer = (old->current_layer
+                      ? xmemdup (old->current_layer,
+                                 old->axes[PIVOT_AXIS_LAYER].n_dimensions
+                                 * sizeof *new->current_layer)
+                      : NULL),
+    .show_values = old->show_values,
+    .show_variables = old->show_variables,
+    .weight_format = old->weight_format,
+
+    .sizing = {
+      [TABLE_HORZ] = clone_sizing (&old->sizing[TABLE_HORZ]),
+      [TABLE_VERT] = clone_sizing (&old->sizing[TABLE_VERT]),
+    },
+
+    .settings = fmt_settings_copy (&old->settings),
+    .grouping = old->grouping,
+    .small = old->small,
+
+    .command_local = xstrdup_if_nonnull (old->command_local),
+    .command_c = xstrdup_if_nonnull (old->command_c),
+    .language = xstrdup_if_nonnull (old->language),
+    .locale = xstrdup_if_nonnull (old->locale),
+
+    .dataset = xstrdup_if_nonnull (old->dataset),
+    .datafile = xstrdup_if_nonnull (old->datafile),
+    .date = old->date,
+
+    .footnotes = clone_footnotes (old->footnotes, old->n_footnotes),
+    .n_footnotes = old->n_footnotes,
+    .allocated_footnotes = old->n_footnotes,
+
+    .title = pivot_value_clone (old->title),
+    .subtype = pivot_value_clone (old->subtype),
+    .corner_text = pivot_value_clone (old->corner_text),
+    .caption = pivot_value_clone (old->caption),
+    .notes = xstrdup_if_nonnull (old->notes),
+
+    .dimensions = clone_dimensions (old->dimensions, old->n_dimensions, new),
+    .n_dimensions = old->n_dimensions,
+
+    .cells = HMAP_INITIALIZER (new->cells),
+  };
+
+  for (size_t i = 0; i < PIVOT_N_AXES; i++)
+    {
+      struct pivot_axis *new_axis = &new->axes[i];
+      const struct pivot_axis *old_axis = &old->axes[i];
+
+      *new_axis = (struct pivot_axis) {
+        .dimensions = xmalloc (old_axis->n_dimensions
+                               * sizeof *new_axis->dimensions),
+        .n_dimensions = old_axis->n_dimensions,
+        .extent = old_axis->extent,
+        .label_depth = old_axis->label_depth,
+      };
+
+      for (size_t i = 0; i < new_axis->n_dimensions; i++)
+        new_axis->dimensions[i] = new->dimensions[
+          old_axis->dimensions[i]->top_index];
+    }
+
+  const struct pivot_cell *old_cell;
+  size_t *dindexes = xmalloc (old->n_dimensions * sizeof *dindexes);
+  HMAP_FOR_EACH (old_cell, struct pivot_cell, hmap_node, &old->cells)
+    {
+      for (size_t i = 0; i < old->n_dimensions; i++)
+        dindexes[i] = old_cell->idx[i];
+      struct pivot_cell *new_cell
+        = pivot_table_insert_cell (new, dindexes);
+      new_cell->value = pivot_value_clone (old_cell->value);
+    }
+  free (dindexes);
+
+  return new;
+}
+
 /* Decreases TABLE's reference count, indicating that it has one fewer owner.
    If TABLE no longer has any owners, it is freed. */
 void
@@ -791,15 +1142,12 @@ pivot_table_unref (struct pivot_table *table)
     return;
 
   free (table->current_layer);
-  free (table->table_look);
+  pivot_table_look_unref (table->look);
 
   for (int i = 0; i < TABLE_N_AXES; i++)
     pivot_table_sizing_uninit (&table->sizing[i]);
 
-  free (table->continuation);
-
-  for (int i = 0; i < sizeof table->ccs / sizeof *table->ccs; i++)
-    free (table->ccs[i]);
+  fmt_settings_uninit (&table->settings);
 
   free (table->command_local);
   free (table->command_c);
@@ -817,9 +1165,7 @@ pivot_table_unref (struct pivot_table *table)
   pivot_value_destroy (table->subtype);
   pivot_value_destroy (table->corner_text);
   pivot_value_destroy (table->caption);
-
-  for (size_t i = 0; i < PIVOT_N_AREAS; i++)
-    area_style_uninit (&table->areas[i]);
+  free (table->notes);
 
   for (size_t i = 0; i < table->n_dimensions; i++)
     pivot_dimension_destroy (table->dimensions[i]);
@@ -831,11 +1177,8 @@ pivot_table_unref (struct pivot_table *table)
   struct pivot_cell *cell, *next_cell;
   HMAP_FOR_EACH_SAFE (cell, next_cell, struct pivot_cell, hmap_node,
                       &table->cells)
-    {
-      hmap_delete (&table->cells, &cell->hmap_node);
-      pivot_value_destroy (cell->value);
-      free (cell);
-    }
+    pivot_table_delete_cell (table, cell);
+
   hmap_destroy (&table->cells);
 
   free (table);
@@ -849,6 +1192,174 @@ pivot_table_is_shared (const struct pivot_table *table)
   return table->ref_cnt > 1;
 }
 
+static void
+pivot_table_set_value__ (struct pivot_value **dstp, struct pivot_value *src)
+{
+  pivot_value_destroy (*dstp);
+  *dstp = src;
+}
+
+/* Changes the title of TABLE to TITLE.  Takes ownership of TITLE. */
+void
+pivot_table_set_title (struct pivot_table *table, struct pivot_value *title)
+{
+  pivot_table_set_value__ (&table->title, title);
+}
+
+/* Changes the subtype of TABLE to SUBTYPE.  Takes ownership of SUBTYPE. */
+void
+pivot_table_set_subtype (struct pivot_table *table, struct pivot_value *subtype)
+{
+  pivot_table_set_value__ (&table->subtype, subtype);
+}
+
+/* Changes the corner text of TABLE to CORNER_TEXT.  Takes ownership of
+   CORNER_TEXT. */
+void
+pivot_table_set_corner_text (struct pivot_table *table,
+                             struct pivot_value *corner_text)
+{
+  pivot_table_set_value__ (&table->corner_text, corner_text);
+}
+
+/* Changes the caption of TABLE to CAPTION.  Takes ownership of CAPTION. */
+void
+pivot_table_set_caption (struct pivot_table *table, struct pivot_value *caption)
+{
+  pivot_table_set_value__ (&table->caption, caption);
+}
+
+/* Swaps axes A and B in TABLE. */
+void
+pivot_table_swap_axes (struct pivot_table *table,
+                       enum pivot_axis_type a, enum pivot_axis_type b)
+{
+  if (a == b)
+    return;
+
+  struct pivot_axis tmp = table->axes[a];
+  table->axes[a] = table->axes[b];
+  table->axes[b] = tmp;
+
+  for (int a = 0; a < PIVOT_N_AXES; a++)
+    {
+      struct pivot_axis *axis = &table->axes[a];
+      for (size_t d = 0; d < axis->n_dimensions; d++)
+        axis->dimensions[d]->axis_type = a;
+    }
+
+  if (a == PIVOT_AXIS_LAYER || b == PIVOT_AXIS_LAYER)
+    {
+      free (table->current_layer);
+      table->current_layer = xzalloc (
+        table->axes[PIVOT_AXIS_LAYER].n_dimensions
+        * sizeof *table->current_layer);
+    }
+}
+
+/* Swaps the row and column axes in TABLE. */
+void
+pivot_table_transpose (struct pivot_table *table)
+{
+  pivot_table_swap_axes (table, PIVOT_AXIS_ROW, PIVOT_AXIS_COLUMN);
+}
+
+static void
+pivot_table_update_axes (struct pivot_table *table)
+{
+  for (int a = 0; a < PIVOT_N_AXES; a++)
+    {
+      struct pivot_axis *axis = &table->axes[a];
+
+      for (size_t d = 0; d < axis->n_dimensions; d++)
+        {
+          struct pivot_dimension *dim = axis->dimensions[d];
+          dim->axis_type = a;
+          dim->level = d;
+        }
+    }
+}
+
+/* Moves DIM from its current location in TABLE to POS within AXIS.  POS of 0
+   is the innermost dimension, 1 is the next one out, and so on. */
+void
+pivot_table_move_dimension (struct pivot_table *table,
+                            struct pivot_dimension *dim,
+                            enum pivot_axis_type axis, size_t pos)
+{
+  assert (dim->table == table);
+
+  struct pivot_axis *old_axis = &table->axes[dim->axis_type];
+  struct pivot_axis *new_axis = &table->axes[axis];
+  pos = MIN (pos, new_axis->n_dimensions);
+
+  if (old_axis == new_axis && pos == dim->level)
+    {
+      /* No change. */
+      return;
+    }
+
+  /* Update the current layer, if necessary.  If we're moving within the layer
+     axis, preserve the current layer. */
+  if (dim->axis_type == PIVOT_AXIS_LAYER)
+    {
+      if (axis == PIVOT_AXIS_LAYER)
+        {
+          /* Rearranging the layer axis. */
+          move_element (table->current_layer, old_axis->n_dimensions,
+                        sizeof *table->current_layer,
+                        dim->level, pos);
+        }
+      else
+        {
+          /* A layer is becoming a row or column. */
+          remove_element (table->current_layer, old_axis->n_dimensions,
+                          sizeof *table->current_layer, dim->level);
+        }
+    }
+  else if (axis == PIVOT_AXIS_LAYER)
+    {
+      /* A row or column is becoming a layer. */
+      table->current_layer = xrealloc (
+        table->current_layer,
+        (new_axis->n_dimensions + 1) * sizeof *table->current_layer);
+      insert_element (table->current_layer, new_axis->n_dimensions,
+                      sizeof *table->current_layer, pos);
+      table->current_layer[pos] = 0;
+    }
+
+  /* Remove DIM from its current axis. */
+  remove_element (old_axis->dimensions, old_axis->n_dimensions,
+                  sizeof *old_axis->dimensions, dim->level);
+  old_axis->n_dimensions--;
+
+  /* Insert DIM into its new axis. */
+  new_axis->dimensions = xrealloc (
+    new_axis->dimensions,
+    (new_axis->n_dimensions + 1) * sizeof *new_axis->dimensions);
+  insert_element (new_axis->dimensions, new_axis->n_dimensions,
+                  sizeof *new_axis->dimensions, pos);
+  new_axis->dimensions[pos] = dim;
+  new_axis->n_dimensions++;
+
+  pivot_table_update_axes (table);
+}
+
+
+const struct pivot_table_look *
+pivot_table_get_look (const struct pivot_table *table)
+{
+  return table->look;
+}
+
+void
+pivot_table_set_look (struct pivot_table *table,
+                      const struct pivot_table_look *look)
+{
+  pivot_table_look_unref (table->look);
+  table->look = pivot_table_look_ref (look);
+}
+
 /* Sets the format used for PIVOT_RC_COUNT cells to the one used for variable
    WV, which should be the weight variable for the dictionary whose data or
    statistics are being put into TABLE.
@@ -933,8 +1444,9 @@ pivot_table_insert_cell (struct pivot_table *table, const size_t *dindexes)
 }
 
 /* Puts VALUE in the cell in TABLE whose indexes are given by the N indexes in
-   DINDEXES.  N must be the number of dimensions in TABLE.  Takes ownership of
-   VALUE.
+   DINDEXES.  The order of the indexes is the same as the order in which the
+   dimensions were created.  N must be the number of dimensions in TABLE.
+   Takes ownership of VALUE.
 
    If VALUE is a numeric value without a specified format, this function checks
    each of the categories designated by DINDEXES[] and takes the format from
@@ -945,6 +1457,8 @@ pivot_table_put (struct pivot_table *table, const size_t *dindexes, size_t n,
                  struct pivot_value *value)
 {
   assert (n == table->n_dimensions);
+  for (size_t i = 0; i < n; i++)
+    assert (dindexes[i] < table->dimensions[i]->n_leaves);
 
   if (value->type == PIVOT_VALUE_NUMERIC && !value->numeric.format.w)
     {
@@ -957,11 +1471,13 @@ pivot_table_put (struct pivot_table *table, const size_t *dindexes, size_t n,
               if (c->format.w)
                 {
                   value->numeric.format = c->format;
+                  value->numeric.honor_small = c->honor_small;
                   goto done;
                 }
             }
         }
       value->numeric.format = *settings_get_format ();
+      value->numeric.honor_small = true;
 
     done:;
     }
@@ -1024,20 +1540,35 @@ pivot_table_create_footnote (struct pivot_table *table,
                                         NULL, content);
 }
 
-static struct pivot_value *
-pivot_make_default_footnote_marker (int idx, bool show_numeric_markers)
-{
-  char text[INT_BUFSIZE_BOUND (size_t)];
-  if (show_numeric_markers)
-    snprintf (text, sizeof text, "%d", idx + 1);
+void
+pivot_footnote_format_marker (const struct pivot_footnote *f,
+                              const struct pivot_table *pt,
+                              struct string *s)
+{
+  if (f->marker)
+    pivot_value_format_body (f->marker, pt, s);
+  else if (pt->look->show_numeric_markers)
+    ds_put_format (s, "%zu", f->idx + 1);
   else
-    str_format_26adic (idx + 1, false, text, sizeof text);
-  return pivot_value_new_user_text (text, -1);
+    {
+      char text[INT_BUFSIZE_BOUND (size_t)];
+      str_format_26adic (f->idx + 1, false, text, sizeof text);
+      ds_put_cstr (s, text);
+    }
 }
 
-/* Creates or modifies a footnote in TABLE with 0-based number IDX.  If MARKER
-   is nonnull, sets the footnote's marker; if CONTENT is nonnull, sets the
-   footnote's content. */
+char *
+pivot_footnote_marker_string (const struct pivot_footnote *f,
+                              const struct pivot_table *pt)
+{
+  struct string s = DS_EMPTY_INITIALIZER;
+  pivot_footnote_format_marker (f, pt, &s);
+  return ds_steal_cstr (&s);
+}
+
+/* Creates or modifies a footnote in TABLE with 0-based number IDX (and creates
+   all lower indexes as a side effect).  If MARKER is nonnull, sets the
+   footnote's marker; if CONTENT is nonnull, sets the footnote's content. */
 struct pivot_footnote *
 pivot_table_create_footnote__ (struct pivot_table *table, size_t idx,
                                struct pivot_value *marker,
@@ -1052,11 +1583,10 @@ pivot_table_create_footnote__ (struct pivot_table *table, size_t idx,
       while (idx >= table->n_footnotes)
         {
           struct pivot_footnote *f = xmalloc (sizeof *f);
-          f->idx = table->n_footnotes;
-          f->marker = pivot_make_default_footnote_marker (
-            f->idx, table->show_numeric_markers);
-          f->content = NULL;
-
+          *f = (struct pivot_footnote) {
+            .idx = table->n_footnotes,
+            .show = true,
+          };
           table->footnotes[table->n_footnotes++] = f;
         }
     }
@@ -1101,8 +1631,8 @@ pivot_table_convert_indexes_ptod (const struct pivot_table *table,
       for (size_t j = 0; j < axis->n_dimensions; j++)
         {
           const struct pivot_dimension *d = axis->dimensions[j];
-          dindexes[d->top_index]
-            = d->presentation_leaves[pindexes[i][j]]->data_index;
+          size_t pindex = pindexes[i][j];
+          dindexes[d->top_index] = d->presentation_leaves[pindex]->data_index;
         }
     }
 }
@@ -1136,7 +1666,7 @@ pivot_table_enumerate_axis (const struct pivot_table *table,
                                                 axis->n_dimensions), 1),
                                   sizeof *enumeration);
   size_t *p = enumeration;
-  size_t *dindexes = xcalloc (table->n_dimensions, sizeof *dindexes);
+  size_t *dindexes = XCALLOC (table->n_dimensions, size_t);
 
   size_t *axis_indexes;
   PIVOT_AXIS_FOR_EACH (axis_indexes, axis)
@@ -1166,6 +1696,14 @@ pivot_table_enumerate_axis (const struct pivot_table *table,
       memcpy (p, axis_indexes, axis->n_dimensions * sizeof *p);
       p += axis->n_dimensions;
     }
+  if (omit_empty && p == enumeration)
+    {
+      PIVOT_AXIS_FOR_EACH (axis_indexes, axis)
+        {
+          memcpy (p, axis_indexes, axis->n_dimensions * sizeof *p);
+          p += axis->n_dimensions;
+        }
+    }
   *p = SIZE_MAX;
   if (n)
     *n = (p - enumeration) / axis->n_dimensions;
@@ -1174,9 +1712,9 @@ pivot_table_enumerate_axis (const struct pivot_table *table,
   return enumeration;
 }
 
-static const struct pivot_cell *
+static struct pivot_cell *
 pivot_table_lookup_cell (const struct pivot_table *table,
-                          const size_t *dindexes)
+                         const size_t *dindexes)
 {
   unsigned int hash = pivot_cell_hash_indexes (dindexes, table->n_dimensions);
   return pivot_table_lookup_cell__ (table, dindexes, hash);
@@ -1198,6 +1736,27 @@ pivot_table_get_rw (struct pivot_table *table, const size_t *dindexes)
   return cell->value;
 }
 
+static void
+pivot_table_delete_cell (struct pivot_table *table, struct pivot_cell *cell)
+{
+  hmap_delete (&table->cells, &cell->hmap_node);
+  pivot_value_destroy (cell->value);
+  free (cell);
+}
+
+bool
+pivot_table_delete (struct pivot_table *table, const size_t *dindexes)
+{
+  struct pivot_cell *cell = pivot_table_lookup_cell (table, dindexes);
+  if (cell)
+    {
+      pivot_table_delete_cell (table, cell);
+      return true;
+    }
+  else
+    return false;
+}
+
 static void
 distribute_extra_depth (struct pivot_category *category, size_t extra_depth)
 {
@@ -1272,17 +1831,13 @@ pivot_table_assign_label_depth (struct pivot_table *table)
 {
   pivot_axis_assign_label_depth (table, PIVOT_AXIS_COLUMN, false);
   if (pivot_axis_assign_label_depth (
-        table, PIVOT_AXIS_ROW, (table->row_labels_in_corner
+        table, PIVOT_AXIS_ROW, (table->look->row_labels_in_corner
                                 && !table->corner_text))
       && table->axes[PIVOT_AXIS_COLUMN].label_depth == 0)
     table->axes[PIVOT_AXIS_COLUMN].label_depth = 1;
   pivot_axis_assign_label_depth (table, PIVOT_AXIS_LAYER, false);
 }
 \f
-/* Footnotes. */
-
-\f
-\f
 static void
 indent (int indentation)
 {
@@ -1291,23 +1846,23 @@ indent (int indentation)
 }
 
 static void
-pivot_value_dump (const struct pivot_value *value)
+pivot_value_dump (const struct pivot_value *value,
+                  const struct pivot_table *pt)
 {
-  char *s = pivot_value_to_string (value, SETTINGS_VALUE_SHOW_DEFAULT,
-                                   SETTINGS_VALUE_SHOW_DEFAULT);
+  char *s = pivot_value_to_string (value, pt);
   fputs (s, stdout);
   free (s);
 }
 
 static void
 pivot_table_dump_value (const struct pivot_value *value, const char *name,
-                      int indentation)
+                        const struct pivot_table *pt, int indentation)
 {
   if (value)
     {
       indent (indentation);
       printf ("%s: ", name);
-      pivot_value_dump (value);
+      pivot_value_dump (value, pt);
       putchar ('\n');
     }
 }
@@ -1323,11 +1878,12 @@ pivot_table_dump_string (const char *string, const char *name, int indentation)
 }
 
 static void
-pivot_category_dump (const struct pivot_category *c, int indentation)
+pivot_category_dump (const struct pivot_category *c,
+                     const struct pivot_table *pt, int indentation)
 {
   indent (indentation);
   printf ("%s \"", pivot_category_is_leaf (c) ? "leaf" : "group");
-  pivot_value_dump (c->name);
+  pivot_value_dump (c->name, pt);
   printf ("\" ");
 
   if (pivot_category_is_leaf (c))
@@ -1338,23 +1894,24 @@ pivot_category_dump (const struct pivot_category *c, int indentation)
       printf ("\n");
 
       for (size_t i = 0; i < c->n_subs; i++)
-        pivot_category_dump (c->subs[i], indentation + 1);
+        pivot_category_dump (c->subs[i], pt, indentation + 1);
     }
 }
 
 void
-pivot_dimension_dump (const struct pivot_dimension *d, int indentation)
+pivot_dimension_dump (const struct pivot_dimension *d,
+                      const struct pivot_table *pt, int indentation)
 {
   indent (indentation);
   printf ("%s dimension %zu (where 0=innermost), label_depth=%d:\n",
           pivot_axis_type_to_string (d->axis_type), d->level, d->label_depth);
 
-  pivot_category_dump (d->root, indentation + 1);
+  pivot_category_dump (d->root, pt, indentation + 1);
 }
 
 static void
-area_style_dump (enum pivot_area area, const struct area_style *a,
-                 int indentation)
+table_area_style_dump (enum pivot_area area, const struct table_area_style *a,
+                       int indentation)
 {
   indent (indentation);
   printf ("%s: ", pivot_area_to_string (area));
@@ -1376,10 +1933,9 @@ table_border_style_dump (enum pivot_border border,
 }
 
 static char ***
-compose_headings (const struct pivot_axis *axis,
-                  const size_t *column_enumeration,
-                  enum settings_value_show show_values,
-                  enum settings_value_show show_variables)
+compose_headings (const struct pivot_table *pt,
+                  const struct pivot_axis *axis,
+                  const size_t *column_enumeration)
 {
   if (!axis->n_dimensions || !axis->extent || !axis->label_depth)
     return NULL;
@@ -1406,8 +1962,7 @@ compose_headings (const struct pivot_axis *axis,
               if (pivot_category_is_leaf (c) || (c->show_label
                                                  && !c->show_label_in_corner))
                 {
-                  headings[row][column] = pivot_value_to_string (
-                    c->name, show_values, show_variables);
+                  headings[row][column] = pivot_value_to_string (c->name, pt);
                   if (!*headings[row][column])
                     headings[row][column] = xstrdup ("<blank>");
                   row--;
@@ -1423,6 +1978,8 @@ compose_headings (const struct pivot_axis *axis,
 static void
 free_headings (const struct pivot_axis *axis, char ***headings)
 {
+  if (!headings)
+    return;
   for (size_t i = 0; i < axis->label_depth; i++)
     {
       for (size_t j = 0; j < axis->extent; j++)
@@ -1432,44 +1989,90 @@ free_headings (const struct pivot_axis *axis, char ***headings)
   free (headings);
 }
 
+static void
+pivot_table_sizing_dump (const char *name,
+                         const int width_ranges[2],
+                         const struct pivot_table_sizing *s,
+                         int indentation)
+{
+  indent (indentation);
+  printf ("%ss: min=%d, max=%d\n", name, width_ranges[0], width_ranges[1]);
+  if (s->n_widths)
+    {
+      indent (indentation + 1);
+      printf ("%s widths:", name);
+      for (size_t i = 0; i < s->n_widths; i++)
+        printf (" %d", s->widths[i]);
+      printf ("\n");
+    }
+  if (s->n_breaks)
+    {
+      indent (indentation + 1);
+      printf ("break after %ss:", name);
+      for (size_t i = 0; i < s->n_breaks; i++)
+        printf (" %zu", s->breaks[i]);
+      printf ("\n");
+    }
+  if (s->n_keeps)
+    {
+      indent (indentation + 1);
+      printf ("keep %ss together:", name);
+      for (size_t i = 0; i < s->n_keeps; i++)
+        printf (" [%zu,%zu]",
+                s->keeps[i].ofs,
+                s->keeps[i].ofs + s->keeps[i].n - 1);
+      printf ("\n");
+    }
+}
+
 void
 pivot_table_dump (const struct pivot_table *table, int indentation)
 {
   if (!table)
     return;
 
-  int old_decimal = settings_get_decimal_char (FMT_COMMA);
-  if (table->decimal == '.' || table->decimal == ',')
-    settings_set_decimal_char (table->decimal);
+  pivot_table_assign_label_depth (CONST_CAST (struct pivot_table *, table));
 
-  pivot_table_dump_value (table->title, "title", indentation);
+  pivot_table_dump_value (table->title, "title", table, indentation);
+  pivot_table_dump_value (table->subtype, "subtype", table, indentation);
   pivot_table_dump_string (table->command_c, "command", indentation);
   pivot_table_dump_string (table->dataset, "dataset", indentation);
   pivot_table_dump_string (table->datafile, "datafile", indentation);
   pivot_table_dump_string (table->notes, "notes", indentation);
-  pivot_table_dump_string (table->table_look, "table-look", indentation);
+  pivot_table_dump_string (table->look->name, "table-look", indentation);
   if (table->date)
     {
       indent (indentation);
-      char buf[26];
-      printf ("date: %s", ctime_r (&table->date, buf));
+
+      struct tm *tm = localtime (&table->date);
+      printf ("date: %d-%02d-%02d %d:%02d:%02d\n", tm->tm_year + 1900,
+              tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min,
+              tm->tm_sec);
     }
 
+  indent (indentation);
+  printf ("sizing:\n");
+  pivot_table_sizing_dump ("column", table->look->width_ranges[TABLE_HORZ],
+                           &table->sizing[TABLE_HORZ], indentation + 1);
+  pivot_table_sizing_dump ("row", table->look->width_ranges[TABLE_VERT],
+                           &table->sizing[TABLE_VERT], indentation + 1);
+
   indent (indentation);
   printf ("areas:\n");
   for (enum pivot_area area = 0; area < PIVOT_N_AREAS; area++)
-    area_style_dump (area, &table->areas[area], indentation + 1);
+    table_area_style_dump (area, &table->look->areas[area], indentation + 1);
 
   indent (indentation);
   printf ("borders:\n");
   for (enum pivot_border border = 0; border < PIVOT_N_BORDERS; border++)
-    table_border_style_dump (border, &table->borders[border], indentation + 1);
+    table_border_style_dump (border, &table->look->borders[border],
+                             indentation + 1);
 
   for (size_t i = 0; i < table->n_dimensions; i++)
-    pivot_dimension_dump (table->dimensions[i], indentation);
+    pivot_dimension_dump (table->dimensions[i], table, indentation);
 
   /* Presentation and data indexes. */
-  size_t *dindexes = xcalloc (table->n_dimensions, sizeof *dindexes);
+  size_t *dindexes = XCALLOC (table->n_dimensions, size_t);
 
   const struct pivot_axis *layer_axis = &table->axes[PIVOT_AXIS_LAYER];
   if (layer_axis->n_dimensions)
@@ -1480,12 +2083,9 @@ pivot_table_dump (const struct pivot_table *table, int indentation)
       for (size_t i = 0; i < layer_axis->n_dimensions; i++)
         {
           const struct pivot_dimension *d = layer_axis->dimensions[i];
-          char *name = pivot_value_to_string (d->root->name,
-                                              table->show_values,
-                                              table->show_variables);
+          char *name = pivot_value_to_string (d->root->name, table);
           char *value = pivot_value_to_string (
-            d->data_leaves[table->current_layer[i]]->name,
-            table->show_values, table->show_variables);
+            d->data_leaves[table->current_layer[i]]->name, table);
           printf (" %s=%s", name, value);
           free (value);
           free (name);
@@ -1507,11 +2107,10 @@ pivot_table_dump (const struct pivot_table *table, int indentation)
           const struct pivot_dimension *d = layer_axis->dimensions[i];
 
           fputs (i == 0 ? " " : ", ", stdout);
-          pivot_value_dump (d->root->name);
+          pivot_value_dump (d->root->name, table);
           fputs (" =", stdout);
 
-          struct pivot_value **names = xnmalloc (layer_axis->label_depth,
-                                               sizeof *names);
+          struct pivot_value **names = xnmalloc (d->n_leaves, sizeof *names);
           size_t n_names = 0;
           for (const struct pivot_category *c
                  = d->presentation_leaves[layer_indexes[i]];
@@ -1522,43 +2121,50 @@ pivot_table_dump (const struct pivot_table *table, int indentation)
                 names[n_names++] = c->name;
             }
 
-          for (size_t i = n_names; i-- > 0; )
+          for (size_t i = n_names; i-- > 0;)
             {
               putchar (' ');
-              pivot_value_dump (names[i]);
+              pivot_value_dump (names[i], table);
             }
           free (names);
         }
       putchar ('\n');
 
       size_t *column_enumeration = pivot_table_enumerate_axis (
-        table, PIVOT_AXIS_COLUMN, layer_indexes, table->omit_empty, NULL);
+        table, PIVOT_AXIS_COLUMN, layer_indexes, table->look->omit_empty, NULL);
       size_t *row_enumeration = pivot_table_enumerate_axis (
-        table, PIVOT_AXIS_ROW, layer_indexes, table->omit_empty, NULL);
+        table, PIVOT_AXIS_ROW, layer_indexes, table->look->omit_empty, NULL);
 
+      /* Print column headings.
+
+         Ordinarily the test for nonnull 'column_headings' would be
+         unnecessary, because 'column_headings' is null only if the axis's
+         label_depth is 0, but there is a special case for the column axis only
+         in pivot_table_assign_label_depth(). */
       char ***column_headings = compose_headings (
-        &table->axes[PIVOT_AXIS_COLUMN], column_enumeration,
-        table->show_values, table->show_variables);
-      for (size_t y = 0; y < table->axes[PIVOT_AXIS_COLUMN].label_depth; y++)
+        table, &table->axes[PIVOT_AXIS_COLUMN], column_enumeration);
+      if (column_headings)
         {
-          indent (indentation + 1);
-          for (size_t x = 0; x < table->axes[PIVOT_AXIS_COLUMN].extent; x++)
+          for (size_t y = 0; y < table->axes[PIVOT_AXIS_COLUMN].label_depth; y++)
             {
-              if (x)
-                fputs ("; ", stdout);
-              if (column_headings[y][x])
-                fputs (column_headings[y][x], stdout);
+              indent (indentation + 1);
+              for (size_t x = 0; x < table->axes[PIVOT_AXIS_COLUMN].extent; x++)
+                {
+                  if (x)
+                    fputs ("; ", stdout);
+                  if (column_headings && column_headings[y] && column_headings[y][x])
+                    fputs (column_headings[y][x], stdout);
+                }
+              putchar ('\n');
             }
-          putchar ('\n');
+          free_headings (&table->axes[PIVOT_AXIS_COLUMN], column_headings);
         }
-      free_headings (&table->axes[PIVOT_AXIS_COLUMN], column_headings);
 
       indent (indentation + 1);
       printf ("-----------------------------------------------\n");
 
       char ***row_headings = compose_headings (
-        &table->axes[PIVOT_AXIS_ROW], row_enumeration,
-        table->show_values, table->show_variables);
+        table, &table->axes[PIVOT_AXIS_ROW], row_enumeration);
 
       size_t x = 0;
       const size_t *pindexes[PIVOT_N_AXES]
@@ -1591,7 +2197,7 @@ pivot_table_dump (const struct pivot_table *table, int indentation)
               const struct pivot_value *value = pivot_table_get (
                 table, dindexes);
               if (value)
-                pivot_value_dump (value);
+                pivot_value_dump (value, table);
             }
           printf ("\n");
 
@@ -1603,7 +2209,7 @@ pivot_table_dump (const struct pivot_table *table, int indentation)
       free_headings (&table->axes[PIVOT_AXIS_ROW], row_headings);
     }
 
-  pivot_table_dump_value (table->caption, "caption", indentation);
+  pivot_table_dump_value (table->caption, "caption", table, indentation);
 
   for (size_t i = 0; i < table->n_footnotes; i++)
     {
@@ -1611,16 +2217,15 @@ pivot_table_dump (const struct pivot_table *table, int indentation)
       indent (indentation);
       putchar ('[');
       if (f->marker)
-        pivot_value_dump (f->marker);
+        pivot_value_dump (f->marker, table);
       else
         printf ("%zu", f->idx);
       putchar (']');
-      pivot_value_dump (f->content);
+      pivot_value_dump (f->content, table);
       putchar ('\n');
     }
 
   free (dindexes);
-  settings_set_decimal_char (old_decimal);
 }
 \f
 static const char *
@@ -1636,8 +2241,7 @@ static size_t
 pivot_format_inner_template (struct string *out, const char *template,
                              char escape,
                              struct pivot_value **values, size_t n_values,
-                             enum settings_value_show show_values,
-                             enum settings_value_show show_variables)
+                             const struct pivot_table *pt)
 {
   size_t args_consumed = 0;
   while (*template && *template != ':')
@@ -1653,8 +2257,7 @@ pivot_format_inner_template (struct string *out, const char *template,
           template = consume_int (template + 1, &index);
           if (index >= 1 && index <= n_values)
             {
-              pivot_value_format (values[index - 1], show_values,
-                                  show_variables, out);
+              pivot_value_format (values[index - 1], pt, out);
               args_consumed = MAX (args_consumed, index);
             }
         }
@@ -1685,8 +2288,7 @@ pivot_extract_inner_template (const char *template, const char **p)
 static void
 pivot_format_template (struct string *out, const char *template,
                        const struct pivot_argument *args, size_t n_args,
-                       enum settings_value_show show_values,
-                       enum settings_value_show show_variables)
+                       const struct pivot_table *pt)
 {
   while (*template)
     {
@@ -1700,8 +2302,7 @@ pivot_format_template (struct string *out, const char *template,
           size_t index;
           template = consume_int (template + 1, &index);
           if (index >= 1 && index <= n_args && args[index - 1].n > 0)
-            pivot_value_format (args[index - 1].values[0],
-                                show_values, show_variables, out);
+            pivot_value_format (args[index - 1].values[0], pt, out);
         }
       else if (*template == '[')
         {
@@ -1723,8 +2324,7 @@ pivot_format_template (struct string *out, const char *template,
               int tmpl_idx = left == arg->n && *tmpl[0] != ':' ? 0 : 1;
               char escape = "%^"[tmpl_idx];
               size_t used = pivot_format_inner_template (
-                out, tmpl[tmpl_idx], escape, values, left,
-                show_values, show_variables);
+                out, tmpl[tmpl_idx], escape, values, left, pt);
               if (!used || used > left)
                 break;
               left -= used;
@@ -1747,14 +2347,43 @@ interpret_show (enum settings_value_show global_show,
           : global_show);
 }
 
-/* Appends a text representation of the body of VALUE to OUT.  SHOW_VALUES and
-   SHOW_VARIABLES control whether variable and value labels are included.
+/* Appends to OUT the actual text content from the given Pango MARKUP. */
+static void
+get_text_from_markup (const char *markup, struct string *out)
+{
+  xmlParserCtxt *parser = xmlCreatePushParserCtxt (NULL, NULL, NULL, 0, NULL);
+  if (!parser)
+    {
+      ds_put_cstr (out, markup);
+      return;
+    }
+
+  xmlParseChunk (parser, "<xml>", strlen ("<xml>"), false);
+  xmlParseChunk (parser, markup, strlen (markup), false);
+  xmlParseChunk (parser, "</xml>", strlen ("</xml>"), true);
+
+  if (parser->wellFormed)
+    {
+      xmlChar *s = xmlNodeGetContent (xmlDocGetRootElement (parser->myDoc));
+      ds_put_cstr (out, CHAR_CAST (char *, s));
+      xmlFree (s);
+    }
+  else
+    ds_put_cstr (out, markup);
+  xmlFreeDoc (parser->myDoc);
+  xmlFreeParserCtxt (parser);
+}
+
+/* Appends a text representation of the body of VALUE to OUT.  Settings on
+   PT control whether variable and value labels are included.
 
-   The "body" omits subscripts and superscripts and footnotes. */
+   The "body" omits subscripts and superscripts and footnotes.
+
+   Returns true if OUT is a number (or a number plus a value label), false
+   otherwise.  */
 bool
 pivot_value_format_body (const struct pivot_value *value,
-                         enum settings_value_show show_values,
-                         enum settings_value_show show_variables,
+                         const struct pivot_table *pt,
                          struct string *out)
 {
   enum settings_value_show show;
@@ -1764,13 +2393,22 @@ pivot_value_format_body (const struct pivot_value *value,
     {
     case PIVOT_VALUE_NUMERIC:
       show = interpret_show (settings_get_show_values (),
-                             show_values,
+                             pt->show_values,
                              value->numeric.show,
                              value->numeric.value_label != NULL);
       if (show & SETTINGS_VALUE_SHOW_VALUE)
         {
+          const struct fmt_spec *f = &value->numeric.format;
+          const struct fmt_spec *format
+            = (f->type == FMT_F
+               && value->numeric.honor_small
+               && value->numeric.x != 0
+               && fabs (value->numeric.x) < pt->small
+               ? &(struct fmt_spec) { .type = FMT_E, .w = 40, .d = f->d }
+               : f);
+
           char *s = data_out (&(union value) { .f = value->numeric.x },
-                              "UTF-8", &value->numeric.format);
+                              "UTF-8", format, &pt->settings);
           ds_put_cstr (out, s + strspn (s, " "));
           free (s);
         }
@@ -1785,7 +2423,7 @@ pivot_value_format_body (const struct pivot_value *value,
 
     case PIVOT_VALUE_STRING:
       show = interpret_show (settings_get_show_values (),
-                             show_values,
+                             pt->show_values,
                              value->string.show,
                              value->string.value_label != NULL);
       if (show & SETTINGS_VALUE_SHOW_VALUE)
@@ -1809,7 +2447,7 @@ pivot_value_format_body (const struct pivot_value *value,
 
     case PIVOT_VALUE_VARIABLE:
       show = interpret_show (settings_get_show_variables (),
-                             show_variables,
+                             pt->show_variables,
                              value->variable.show,
                              value->variable.var_label != NULL);
       if (show & SETTINGS_VALUE_SHOW_VALUE)
@@ -1823,71 +2461,143 @@ pivot_value_format_body (const struct pivot_value *value,
       break;
 
     case PIVOT_VALUE_TEXT:
-      ds_put_cstr (out, value->text.local);
+      if (value->ex && value->ex->font_style && value->ex->font_style->markup)
+        get_text_from_markup (value->text.local, out);
+      else
+        ds_put_cstr (out, value->text.local);
       break;
 
     case PIVOT_VALUE_TEMPLATE:
-      pivot_format_template (out, value->template.s, value->template.args,
-                             value->template.n_args, show_values,
-                             show_variables);
+      pivot_format_template (out, value->template.local, value->template.args,
+                             value->template.n_args, pt);
       break;
     }
 
   return numeric;
 }
 
-/* Appends a text representation of VALUE to OUT.  SHOW_VALUES and
-   SHOW_VARIABLES control whether variable and value labels are included.
+/* Appends a text representation of VALUE to OUT.  Settings on
+   PT control whether variable and value labels are included.
 
-   Subscripts and superscripts and footnotes are included. */
-void
+   Subscripts and footnotes are included.
+
+   Returns true if OUT is a number (or a number plus a value label), false
+   otherwise.  */
+bool
 pivot_value_format (const struct pivot_value *value,
-                    enum settings_value_show show_values,
-                    enum settings_value_show show_variables,
+                    const struct pivot_table *pt,
                     struct string *out)
 {
-  pivot_value_format_body ( value, show_values, show_variables, out);
+  bool numeric = pivot_value_format_body (value, pt, out);
 
-  if (value->subscript)
-    ds_put_format (out, "_%s", value->subscript);
+  const struct pivot_value_ex *ex = value->ex;
+  if (ex)
+    {
+      if (ex->n_subscripts)
+        {
+          for (size_t i = 0; i < ex->n_subscripts; i++)
+            ds_put_format (out, "%c%s", i ? ',' : '_', ex->subscripts[i]);
+        }
+
+      for (size_t i = 0; i < ex->n_footnotes; i++)
+        {
+          ds_put_byte (out, '[');
 
-  if (value->superscript)
-    ds_put_format (out, "^%s", value->superscript);
+          size_t idx = ex->footnote_indexes[i];
+          const struct pivot_footnote *f = pt->footnotes[idx];
+          pivot_footnote_format_marker (f, pt, out);
 
-  for (size_t i = 0; i < value->n_footnotes; i++)
-    {
-      ds_put_byte (out, '^');
-      pivot_value_format (value->footnotes[i]->marker,
-                          show_values, show_variables, out);
+          ds_put_byte (out, ']');
+        }
     }
+
+  return numeric;
 }
 
 /* Returns a text representation of VALUE.  The caller must free the string,
    with free(). */
 char *
 pivot_value_to_string (const struct pivot_value *value,
-                       enum settings_value_show show_values,
-                       enum settings_value_show show_variables)
+                       const struct pivot_table *pt)
 {
   struct string s = DS_EMPTY_INITIALIZER;
-  pivot_value_format (value, show_values, show_variables, &s);
+  pivot_value_format (value, pt, &s);
   return ds_steal_cstr (&s);
 }
 
+char *
+pivot_value_to_string_defaults (const struct pivot_value *value)
+{
+  static const struct pivot_table pt = {
+    .show_values = SETTINGS_VALUE_SHOW_DEFAULT,
+    .show_variables = SETTINGS_VALUE_SHOW_DEFAULT,
+    .settings = FMT_SETTINGS_INIT,
+  };
+  return pivot_value_to_string (value, &pt);
+}
+
+struct pivot_value *
+pivot_value_clone (const struct pivot_value *old)
+{
+  if (!old)
+    return NULL;
+
+  struct pivot_value *new = xmemdup (old, sizeof *new);
+  if (old->ex)
+    new->ex = pivot_value_ex_clone (old->ex);
+
+  switch (new->type)
+    {
+    case PIVOT_VALUE_NUMERIC:
+      new->numeric.var_name = xstrdup_if_nonnull (new->numeric.var_name);
+      new->numeric.value_label = xstrdup_if_nonnull (new->numeric.value_label);
+      break;
+
+    case PIVOT_VALUE_STRING:
+      new->string.s = xstrdup (new->string.s);
+      new->string.var_name = xstrdup_if_nonnull (new->string.var_name);
+      new->string.value_label = xstrdup_if_nonnull (new->string.value_label);
+      break;
+
+    case PIVOT_VALUE_VARIABLE:
+      new->variable.var_name = xstrdup_if_nonnull (new->variable.var_name);
+      new->variable.var_label = xstrdup_if_nonnull (new->variable.var_label);
+      break;
+
+    case PIVOT_VALUE_TEXT:
+      new->text.local = xstrdup (old->text.local);
+      new->text.c = (old->text.c == old->text.local ? new->text.local
+                     : xstrdup_if_nonnull (old->text.c));
+      new->text.id = (old->text.id == old->text.local ? new->text.local
+                      : old->text.id == old->text.c ? new->text.c
+                      : xstrdup_if_nonnull (old->text.id));
+      break;
+
+    case PIVOT_VALUE_TEMPLATE:
+      new->template.local = xstrdup (old->template.local);
+      new->template.id = (old->template.id == old->template.local
+                          ? new->template.local
+                          : xstrdup (old->template.id));
+      new->template.args = xmalloc (new->template.n_args
+                                    * sizeof *new->template.args);
+      for (size_t i = 0; i < old->template.n_args; i++)
+        pivot_argument_copy (&new->template.args[i],
+                             &old->template.args[i]);
+      break;
+
+    default:
+      NOT_REACHED ();
+    }
+  return new;
+}
+
 /* Frees the data owned by V. */
 void
 pivot_value_destroy (struct pivot_value *value)
 {
   if (value)
     {
-      font_style_uninit (value->font_style);
-      free (value->font_style);
-      free (value->cell_style);
-      /* Do not free the elements of footnotes because VALUE does not own
-         them. */
-      free (value->footnotes);
-      free (value->subscript);
-
+      pivot_value_ex_destroy (value->ex);
       switch (value->type)
         {
         case PIVOT_VALUE_NUMERIC:
@@ -1916,11 +2626,16 @@ pivot_value_destroy (struct pivot_value *value)
           break;
 
         case PIVOT_VALUE_TEMPLATE:
-          free (value->template.s);
+          free (value->template.local);
+          if (value->template.id != value->template.local)
+            free (value->template.id);
           for (size_t i = 0; i < value->template.n_args; i++)
             pivot_argument_uninit (&value->template.args[i]);
           free (value->template.args);
           break;
+
+        default:
+          NOT_REACHED ();
         }
       free (value);
     }
@@ -1930,31 +2645,58 @@ pivot_value_destroy (struct pivot_value *value)
    DEFAULT_STYLE for the parts of the style that VALUE doesn't override. */
 void
 pivot_value_get_style (struct pivot_value *value,
-                       const struct area_style *default_style,
-                       struct area_style *area)
+                       const struct font_style *base_font_style,
+                       const struct cell_style *base_cell_style,
+                       struct table_area_style *area)
 {
-  font_style_copy (&area->font_style, (value->font_style
-                                       ? value->font_style
-                                       : &default_style->font_style));
-  area->cell_style = (value->cell_style
-                      ? *value->cell_style
-                      : default_style->cell_style);
+  const struct pivot_value_ex *ex = pivot_value_ex (value);
+  font_style_copy (NULL, &area->font_style,
+                   ex->font_style ? ex->font_style : base_font_style);
+  area->cell_style = *(ex->cell_style ? ex->cell_style : base_cell_style);
 }
 
 /* Copies AREA into VALUE's style. */
 void
 pivot_value_set_style (struct pivot_value *value,
-                       const struct area_style *area)
+                       const struct table_area_style *area)
 {
-  if (value->font_style)
-    font_style_uninit (value->font_style);
+  pivot_value_set_font_style (value, &area->font_style);
+  pivot_value_set_cell_style (value, &area->cell_style);
+}
+
+void
+pivot_value_set_font_style (struct pivot_value *value,
+                            const struct font_style *font_style)
+{
+  struct pivot_value_ex *ex = pivot_value_ex_rw (value);
+  if (ex->font_style)
+    font_style_uninit (ex->font_style);
   else
-    value->font_style = xmalloc (sizeof *value->font_style);
-  font_style_copy (value->font_style, &area->font_style);
+    ex->font_style = xmalloc (sizeof *ex->font_style);
+  font_style_copy (NULL, ex->font_style, font_style);
+}
+
+void
+pivot_value_set_cell_style (struct pivot_value *value,
+                            const struct cell_style *cell_style)
+{
+  struct pivot_value_ex *ex = pivot_value_ex_rw (value);
+  if (!ex->cell_style)
+    ex->cell_style = xmalloc (sizeof *ex->cell_style);
+  *ex->cell_style = *cell_style;
+}
+
+void
+pivot_argument_copy (struct pivot_argument *dst,
+                     const struct pivot_argument *src)
+{
+  *dst = (struct pivot_argument) {
+    .n = src->n,
+    .values = xmalloc (src->n * sizeof *dst->values),
+  };
 
-  if (!value->cell_style)
-    value->cell_style = xmalloc (sizeof *value->cell_style);
-  *value->cell_style = area->cell_style;
+  for (size_t i = 0; i < src->n; i++)
+    dst->values[i] = pivot_value_clone (src->values[i]);
 }
 
 /* Frees the data owned by ARG (but not ARG itself). */
@@ -1982,8 +2724,8 @@ pivot_value_new_user_text_nocopy (char *text)
 {
   struct pivot_value *value = xmalloc (sizeof *value);
   *value = (struct pivot_value) {
-    .type = PIVOT_VALUE_TEXT,
     .text = {
+      .type = PIVOT_VALUE_TEXT,
       .local = text,
       .c = text,
       .id = text,
@@ -2001,9 +2743,9 @@ pivot_value_new_user_text_nocopy (char *text)
    that pivot_value_new_variable() should be used for variable names).  For
    strings that are part of the PSPP user interface, such as names of
    procedures, statistics, annotations, error messages, etc., use
-   pivot_value_new_text().j
+   pivot_value_new_text().
 
-   The caller retains ownership of TEXT.*/
+   The caller retains ownership of TEXT. */
 struct pivot_value *
 pivot_value_new_user_text (const char *text, size_t length)
 {
@@ -2025,8 +2767,8 @@ pivot_value_new_text (const char *text)
 
   struct pivot_value *value = xmalloc (sizeof *value);
   *value = (struct pivot_value) {
-    .type = PIVOT_VALUE_TEXT,
     .text = {
+      .type = PIVOT_VALUE_TEXT,
       .local = local,
       .c = c,
       .id = c,
@@ -2037,7 +2779,7 @@ pivot_value_new_text (const char *text)
 }
 
 /* Same as pivot_value_new_text() but its argument is a printf()-like format
-   string. */
+   string.  The format string should generally be enclosed in N_(). */
 struct pivot_value * PRINTF_FORMAT (1, 2)
 pivot_value_new_text_format (const char *format, ...)
 {
@@ -2052,8 +2794,8 @@ pivot_value_new_text_format (const char *format, ...)
 
   struct pivot_value *value = xmalloc (sizeof *value);
   *value = (struct pivot_value) {
-    .type = PIVOT_VALUE_TEXT,
     .text = {
+      .type = PIVOT_VALUE_TEXT,
       .local = local,
       .c = c,
       .id = xstrdup (c),
@@ -2063,12 +2805,6 @@ pivot_value_new_text_format (const char *format, ...)
   return value;
 }
 
-static char *
-xstrdup_if_nonempty (const char *s)
-{
-  return s && s[0] ? xstrdup (s) : NULL;
-}
-
 /* Returns a new pivot_value that represents X.
 
    The format to use for X is unspecified.  Usually the easiest way to specify
@@ -2080,8 +2816,10 @@ pivot_value_new_number (double x)
 {
   struct pivot_value *value = xmalloc (sizeof *value);
   *value = (struct pivot_value) {
-    .type = PIVOT_VALUE_NUMERIC,
-    .numeric = { .x = x, },
+    .numeric = {
+      .type = PIVOT_VALUE_NUMERIC,
+      .x = x
+    },
   };
   return value;
 }
@@ -2091,7 +2829,7 @@ struct pivot_value *
 pivot_value_new_integer (double x)
 {
   struct pivot_value *value = pivot_value_new_number (x);
-  value->numeric.format = (struct fmt_spec) { FMT_F, 40, 0 };
+  value->numeric.format = (struct fmt_spec) { .type = FMT_F, .w = 40 };
   return value;
 }
 
@@ -2130,7 +2868,7 @@ struct pivot_value *
 pivot_value_new_value (const union value *value, int width,
                        const struct fmt_spec *format, const char *encoding)
 {
-  struct pivot_value *pv = xzalloc (sizeof *pv);
+  struct pivot_value *pv = XZALLOC (struct pivot_value);
   if (width > 0)
     {
       char *s = recode_string (UTF8, encoding, CHAR_CAST (char *, value->s),
@@ -2156,13 +2894,22 @@ pivot_value_new_value (const union value *value, int width,
 /* Returns a new pivot_value for VARIABLE. */
 struct pivot_value *
 pivot_value_new_variable (const struct variable *variable)
+{
+  return pivot_value_new_variable__ (var_get_name (variable),
+                                     var_get_label (variable));
+}
+
+/* Returns a new pivot_value for a variable with the given NAME and optional
+   LABEL. */
+struct pivot_value *
+pivot_value_new_variable__ (const char *name, const char *label)
 {
   struct pivot_value *value = xmalloc (sizeof *value);
   *value = (struct pivot_value) {
-    .type = PIVOT_VALUE_VARIABLE,
     .variable = {
-      .var_name = xstrdup (var_get_name (variable)),
-      .var_label = xstrdup_if_nonempty (var_get_label (variable)),
+      .type = PIVOT_VALUE_VARIABLE,
+      .var_name = xstrdup (name),
+      .var_label = xstrdup_if_nonempty (label),
     },
   };
   return value;
@@ -2173,9 +2920,41 @@ void
 pivot_value_add_footnote (struct pivot_value *v,
                           const struct pivot_footnote *footnote)
 {
-  v->footnotes = xrealloc (v->footnotes,
-                           (v->n_footnotes + 1) * sizeof *v->footnotes);
-  v->footnotes[v->n_footnotes++] = footnote;
+  struct pivot_value_ex *ex = pivot_value_ex_rw (v);
+
+  /* Some legacy tables include numerous duplicate footnotes.  Suppress
+     them. */
+  for (size_t i = 0; i < ex->n_footnotes; i++)
+    if (ex->footnote_indexes[i] == footnote->idx)
+      return;
+
+  ex->footnote_indexes = xrealloc (
+    ex->footnote_indexes,
+    (ex->n_footnotes + 1) * sizeof *ex->footnote_indexes);
+  ex->footnote_indexes[ex->n_footnotes++] = footnote->idx;
+  pivot_value_sort_footnotes (v);
+}
+
+static int
+compare_footnote_indexes (const void *a_, const void *b_)
+{
+  const size_t *ap = a_;
+  const size_t *bp = b_;
+  size_t a = *ap;
+  size_t b = *bp;
+  return a < b ? -1 : a > b;
+}
+
+/* Sorts the footnote references in V in the standard ascending order.
+
+   This is only necessary if code adds (plural) footnotes to a pivot_value by
+   itself, because pivot_value_add_footnote() does it automatically. */
+void
+pivot_value_sort_footnotes (struct pivot_value *v)
+{
+  if (v->ex && v->ex->n_footnotes > 1)
+    qsort (v->ex->footnote_indexes, v->ex->n_footnotes,
+           sizeof *v->ex->footnote_indexes, compare_footnote_indexes);
 }
 
 /* If VALUE is a numeric value, and RC is a result class such as
@@ -2185,10 +2964,264 @@ pivot_value_set_rc (const struct pivot_table *table, struct pivot_value *value,
                     const char *rc)
 {
   if (value->type == PIVOT_VALUE_NUMERIC)
+    pivot_table_use_rc (table, rc,
+                        &value->numeric.format, &value->numeric.honor_small);
+}
+\f
+/* pivot_value_ex. */
+
+struct pivot_value_ex *
+pivot_value_ex_rw (struct pivot_value *value)
+{
+  if (!value->ex)
+    value->ex = xzalloc (sizeof *value->ex);
+  return value->ex;
+}
+
+struct pivot_value_ex *
+pivot_value_ex_clone (const struct pivot_value_ex *old)
+{
+  struct font_style *font_style = NULL;
+  if (old->font_style)
+    {
+      font_style = xmalloc (sizeof *font_style);
+      font_style_copy (NULL, font_style, old->font_style);
+    }
+
+  char **subscripts = NULL;
+  if (old->n_subscripts)
+    {
+      subscripts = xnmalloc (old->n_subscripts, sizeof *subscripts);
+      for (size_t i = 0; i < old->n_subscripts; i++)
+        subscripts[i] = xstrdup (old->subscripts[i]);
+    }
+
+  struct pivot_value_ex *new = xmalloc (sizeof *new);
+  *new = (struct pivot_value_ex) {
+    .font_style = font_style,
+    .cell_style = (old->cell_style
+                   ? xmemdup (old->cell_style, sizeof *new->cell_style)
+                   : NULL),
+    .subscripts = subscripts,
+    .n_subscripts = old->n_subscripts,
+    .footnote_indexes = (
+      old->n_footnotes
+      ? xmemdup (old->footnote_indexes,
+                 old->n_footnotes * sizeof *new->footnote_indexes)
+      : NULL),
+    .n_footnotes = old->n_footnotes
+  };
+  return new;
+}
+
+void
+pivot_value_ex_destroy (struct pivot_value_ex *ex)
+{
+  if (ex)
+    {
+      font_style_uninit (ex->font_style);
+      free (ex->font_style);
+      free (ex->cell_style);
+      free (ex->footnote_indexes);
+
+      for (size_t i = 0; i < ex->n_subscripts; i++)
+        free (ex->subscripts[i]);
+      free (ex->subscripts);
+      free (ex);
+    }
+}
+\f
+/* pivot_splits */
+
+struct pivot_splits_value
+  {
+    struct hmap_node hmap_node;
+    union value value;
+    int leaf;
+  };
+
+struct pivot_splits_var
+  {
+    struct pivot_dimension *dimension;
+    int width;
+    size_t idx;
+    struct fmt_spec format;
+    struct hmap values;
+  };
+
+struct pivot_splits
+  {
+    struct pivot_splits_var *vars;
+    size_t n;
+    char *encoding;
+
+    size_t dindexes[MAX_SPLITS];
+  };
+
+struct pivot_splits *
+pivot_splits_create (struct pivot_table *pt,
+                     enum pivot_axis_type axis,
+                     const struct dictionary *dict)
+{
+  if (dict_get_split_type (dict) != SPLIT_LAYERED)
+    return NULL;
+
+  size_t n = dict_get_n_splits (dict);
+  assert (n <= MAX_SPLITS);
+
+  const struct variable *const *vars = dict_get_split_vars (dict);
+  struct pivot_splits_var *psvars = xnmalloc (n, sizeof *psvars);
+  for (size_t i = 0; i < n; i++)
+    {
+      const struct variable *var = vars[i];
+      struct pivot_splits_var *psvar = &psvars[i];
+
+      struct pivot_dimension *d = pivot_dimension_create__ (
+        pt, axis, pivot_value_new_variable (var));
+      d->root->show_label = true;
+
+      *psvar = (struct pivot_splits_var) {
+        .width = var_get_width (var),
+        .idx = var_get_case_index (var),
+        .format = *var_get_print_format (var),
+        .values = HMAP_INITIALIZER (psvar->values),
+        .dimension = d,
+      };
+    }
+
+  struct pivot_splits *ps = xmalloc (sizeof *ps);
+  *ps = (struct pivot_splits) {
+    .vars = psvars,
+    .n = n,
+    .encoding = xstrdup (dict_get_encoding (dict)),
+    .dindexes = { [0] = SIZE_MAX },
+  };
+  return ps;
+}
+
+void
+pivot_splits_destroy (struct pivot_splits *ps)
+{
+  if (!ps)
+    return;
+
+  for (size_t i = 0; i < ps->n; i++)
     {
-      const struct fmt_spec *f = pivot_table_get_format (table, rc);
-      if (f)
-        value->numeric.format = *f;
+      struct pivot_splits_var *psvar = &ps->vars[i];
+      struct pivot_splits_value *psval, *next;
+      HMAP_FOR_EACH_SAFE (psval, next, struct pivot_splits_value, hmap_node,
+                          &psvar->values)
+        {
+          value_destroy (&psval->value, psvar->width);
+          hmap_delete (&psvar->values, &psval->hmap_node);
+          free (psval);
+        }
+      hmap_destroy (&psvar->values);
     }
+  free (ps->vars);
+  free (ps->encoding);
+  free (ps);
 }
 
+static struct pivot_splits_value *
+pivot_splits_value_find (struct pivot_splits_var *psvar,
+                         const union value *value)
+{
+  struct pivot_splits_value *psval;
+  HMAP_FOR_EACH_WITH_HASH (psval, struct pivot_splits_value, hmap_node,
+                           value_hash (value, psvar->width, 0), &psvar->values)
+    if (value_equal (&psval->value, value, psvar->width))
+      return psval;
+  return NULL;
+}
+
+void
+pivot_splits_new_split (struct pivot_splits *ps, const struct ccase *c)
+{
+  if (!ps)
+    return;
+
+  for (size_t i = ps->n - 1; i < ps->n; i--)
+    {
+      struct pivot_splits_var *psvar = &ps->vars[i];
+      const union value *value = case_data_idx (c, psvar->idx);
+      struct pivot_splits_value *psval = pivot_splits_value_find (psvar, value);
+      if (!psval)
+        {
+          psval = xmalloc (sizeof *psval);
+          hmap_insert (&psvar->values, &psval->hmap_node,
+                       value_hash (value, psvar->width, 0));
+          value_clone (&psval->value, value, psvar->width);
+          psval->leaf = pivot_category_create_leaf (
+            psvar->dimension->root,
+            pivot_value_new_value (value, psvar->width, &psvar->format,
+                                   ps->encoding));
+        }
+
+      ps->dindexes[i] = psval->leaf;
+    }
+}
+
+size_t
+pivot_splits_get_dindexes (const struct pivot_splits *ps, size_t *dindexes)
+{
+  if (!ps)
+    return 0;
+
+  assert (ps->dindexes[0] != SIZE_MAX);
+  for (size_t i = 0; i < ps->n; i++)
+    dindexes[i] = ps->dindexes[i];
+  return ps->n;
+}
+
+void
+pivot_splits_put1 (struct pivot_splits *ps, struct pivot_table *table,
+                   size_t idx1, struct pivot_value *value)
+{
+  size_t dindexes[1 + MAX_SPLITS];
+  size_t *p = dindexes;
+  *p++ = idx1;
+  p += pivot_splits_get_dindexes (ps, p);
+  pivot_table_put (table, dindexes, p - dindexes, value);
+}
+
+void
+pivot_splits_put2 (struct pivot_splits *ps, struct pivot_table *table,
+                   size_t idx1, size_t idx2, struct pivot_value *value)
+{
+  size_t dindexes[2 + MAX_SPLITS];
+  size_t *p = dindexes;
+  *p++ = idx1;
+  *p++ = idx2;
+  p += pivot_splits_get_dindexes (ps, p);
+  pivot_table_put (table, dindexes, p - dindexes, value);
+}
+
+void
+pivot_splits_put3 (struct pivot_splits *ps, struct pivot_table *table,
+                   size_t idx1, size_t idx2, size_t idx3,
+                   struct pivot_value *value)
+{
+  size_t dindexes[3 + MAX_SPLITS];
+  size_t *p = dindexes;
+  *p++ = idx1;
+  *p++ = idx2;
+  *p++ = idx3;
+  p += pivot_splits_get_dindexes (ps, p);
+  pivot_table_put (table, dindexes, p - dindexes, value);
+}
+
+void
+pivot_splits_put4 (struct pivot_splits *ps, struct pivot_table *table,
+                   size_t idx1, size_t idx2, size_t idx3, size_t idx4,
+                   struct pivot_value *value)
+{
+  size_t dindexes[4 + MAX_SPLITS];
+  size_t *p = dindexes;
+  *p++ = idx1;
+  *p++ = idx2;
+  *p++ = idx3;
+  *p++ = idx4;
+  p += pivot_splits_get_dindexes (ps, p);
+  pivot_table_put (table, dindexes, p - dindexes, value);
+}