pivot-table: Add some more utility functions for pivot data structures.
[pspp] / src / output / pivot-table.c
index ba3d98c916ad3f798d8980f5a8562cee9e269f70..3bd0911b46e7adec6bbd8cda843f3c6d9a060360 100644 (file)
 #include "data/settings.h"
 #include "data/value.h"
 #include "data/variable.h"
+#include "data/file-name.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"
@@ -128,6 +134,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. */
@@ -168,7 +371,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++)
@@ -193,6 +397,13 @@ pivot_category_set_rc (struct pivot_category *category, const char *s)
     category->dimension->table, s);
   if (format)
     category->format = *format;
+
+  /* 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)
+    name->numeric.format = format ? *format : *settings_get_format ();
 }
 
 static void
@@ -286,7 +497,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;
 }
@@ -586,118 +797,57 @@ 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. */
 
 /* 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.
+   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.
 
-   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().
+   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)
+pivot_table_create__ (struct pivot_value *title, const char *subtype)
 {
   struct pivot_table *table = xzalloc (sizeof *table);
+  table->ref_cnt = 1;
+  table->show_title = true;
+  table->show_caption = true;
   table->weight_format = (struct fmt_spec) { FMT_F, 40, 0 };
   table->title = title;
+  table->subtype = subtype ? pivot_value_new_text (subtype) : NULL;
+  table->command_c = output_get_command_name ();
+  table->look = pivot_table_look_ref (pivot_table_look_get_default ());
 
-  /* Set default area styles. */
-#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},  \
-    },                                                          \
-         }
-  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
-  for (size_t i = 0; i < PIVOT_N_AREAS; i++)
-    table->areas[i] = default_area_styles[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,
-  };
-  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;
@@ -711,7 +861,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"));
@@ -723,21 +873,34 @@ pivot_table_create_for_text (struct pivot_value *title,
   return table;
 }
 
-/* Destroys TABLE and frees everything it points to. */
+/* Increases TABLE's reference count, indicating that it has an additional
+   owner.  A pivot table that is shared among multiple owners must not be
+   modified. */
+struct pivot_table *
+pivot_table_ref (const struct pivot_table *table_)
+{
+  struct pivot_table *table = CONST_CAST (struct pivot_table *, table_);
+  table->ref_cnt++;
+  return table;
+}
+
+/* Decreases TABLE's reference count, indicating that it has one fewer owner.
+   If TABLE no longer has any owners, it is freed. */
 void
-pivot_table_destroy (struct pivot_table *table)
+pivot_table_unref (struct pivot_table *table)
 {
   if (!table)
     return;
+  assert (table->ref_cnt > 0);
+  if (--table->ref_cnt)
+    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]);
 
@@ -757,9 +920,7 @@ pivot_table_destroy (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]);
@@ -781,6 +942,28 @@ pivot_table_destroy (struct pivot_table *table)
   free (table);
 }
 
+/* Returns true if TABLE has more than one owner.  A pivot table that is shared
+   among multiple owners must not be modified. */
+bool
+pivot_table_is_shared (const struct pivot_table *table)
+{
+  return table->ref_cnt > 1;
+}
+
+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.
@@ -877,6 +1060,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)
     {
@@ -967,9 +1152,9 @@ pivot_make_default_footnote_marker (int idx, bool show_numeric_markers)
   return pivot_value_new_user_text (text, -1);
 }
 
-/* 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. */
+/* 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,
@@ -986,8 +1171,9 @@ pivot_table_create_footnote__ (struct pivot_table *table, size_t idx,
           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->idx, table->look->show_numeric_markers);
           f->content = NULL;
+          f->show = true;
 
           table->footnotes[table->n_footnotes++] = f;
         }
@@ -1068,7 +1254,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)
@@ -1098,6 +1284,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;
@@ -1204,17 +1398,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)
 {
@@ -1285,8 +1475,8 @@ pivot_dimension_dump (const struct pivot_dimension *d, int indentation)
 }
 
 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));
@@ -1364,6 +1554,42 @@ 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)
 {
@@ -1375,32 +1601,45 @@ pivot_table_dump (const struct pivot_table *table, int indentation)
     settings_set_decimal_char (table->decimal);
 
   pivot_table_dump_value (table->title, "title", indentation);
+  pivot_table_dump_value (table->subtype, "subtype", 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);
-      printf ("date: %s", ctime (&table->date)); /* XXX thread unsafe */
+
+      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);
 
   /* 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)
@@ -1441,8 +1680,7 @@ pivot_table_dump (const struct pivot_table *table, int indentation)
           pivot_value_dump (d->root->name);
           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]];
@@ -1453,7 +1691,7 @@ 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]);
@@ -1463,9 +1701,9 @@ pivot_table_dump (const struct pivot_table *table, int indentation)
       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);
 
       char ***column_headings = compose_headings (
         &table->axes[PIVOT_AXIS_COLUMN], column_enumeration,
@@ -1681,7 +1919,10 @@ interpret_show (enum settings_value_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.
 
-   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,
@@ -1758,7 +1999,7 @@ pivot_value_format_body (const struct pivot_value *value,
       break;
 
     case PIVOT_VALUE_TEMPLATE:
-      pivot_format_template (out, value->template.s, value->template.args,
+      pivot_format_template (out, value->template.local, value->template.args,
                              value->template.n_args, show_values,
                              show_variables);
       break;
@@ -1770,20 +2011,20 @@ pivot_value_format_body (const struct pivot_value *value,
 /* Appends a text representation of VALUE to OUT.  SHOW_VALUES and
    SHOW_VARIABLES control whether variable and value labels are included.
 
-   Subscripts and superscripts and footnotes are included. */
+   Subscripts and footnotes are included. */
 void
 pivot_value_format (const struct pivot_value *value,
                     enum settings_value_show show_values,
                     enum settings_value_show show_variables,
                     struct string *out)
 {
-  pivot_value_format_body ( value, show_values, show_variables, out);
-
-  if (value->subscript)
-    ds_put_format (out, "_%s", value->subscript);
+  pivot_value_format_body (value, show_values, show_variables, out);
 
-  if (value->superscript)
-    ds_put_format (out, "^%s", value->superscript);
+  if (value->n_subscripts)
+    {
+      for (size_t i = 0; i < value->n_subscripts; i++)
+        ds_put_format (out, "%c%s", i ? ',' : '_', value->subscripts[i]);
+    }
 
   for (size_t i = 0; i < value->n_footnotes; i++)
     {
@@ -1805,6 +2046,78 @@ pivot_value_to_string (const struct pivot_value *value,
   return ds_steal_cstr (&s);
 }
 
+static char *
+xstrdup_if_nonnull (const char *s)
+{
+  return s ? xstrdup (s) : NULL;
+}
+
+struct pivot_value *
+pivot_value_clone (const struct pivot_value *old)
+{
+  struct pivot_value *new = xmemdup (old, sizeof *new);
+  if (old->font_style)
+    {
+      new->font_style = xmalloc (sizeof *new->font_style);
+      font_style_copy (NULL, new->font_style, old->font_style);
+    }
+  if (old->cell_style)
+    new->font_style = xmemdup (old->font_style, sizeof *new->font_style);
+  if (old->n_subscripts)
+    {
+      new->subscripts = xnmalloc (old->n_subscripts, sizeof *new->subscripts);
+      for (size_t i = 0; i < old->n_subscripts; i++)
+        new->subscripts[i] = xstrdup (old->subscripts[i]);
+    }
+  if (old->n_footnotes)
+    new->footnotes = xmemdup (old->footnotes,
+                              old->n_footnotes * sizeof *new->footnotes);
+
+  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 (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 (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)
@@ -1817,7 +2130,10 @@ pivot_value_destroy (struct pivot_value *value)
       /* Do not free the elements of footnotes because VALUE does not own
          them. */
       free (value->footnotes);
-      free (value->subscript);
+
+      for (size_t i = 0; i < value->n_subscripts; i++)
+        free (value->subscripts[i]);
+      free (value->subscripts);
 
       switch (value->type)
         {
@@ -1847,11 +2163,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);
     }
@@ -1861,31 +2182,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);
+  font_style_copy (NULL, &area->font_style, (value->font_style
+                                             ? value->font_style
+                                             : base_font_style));
+  area->cell_style = *(value->cell_style
+                       ? value->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)
+{
+  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)
 {
   if (value->font_style)
     font_style_uninit (value->font_style);
   else
     value->font_style = xmalloc (sizeof *value->font_style);
-  font_style_copy (value->font_style, &area->font_style);
+  font_style_copy (NULL, value->font_style, font_style);
+}
 
+void
+pivot_value_set_cell_style (struct pivot_value *value,
+                            const struct cell_style *cell_style)
+{
   if (!value->cell_style)
     value->cell_style = xmalloc (sizeof *value->cell_style);
-  *value->cell_style = area->cell_style;
+  *value->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),
+  };
+
+  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). */
@@ -1994,12 +2342,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
@@ -2104,6 +2446,12 @@ void
 pivot_value_add_footnote (struct pivot_value *v,
                           const struct pivot_footnote *footnote)
 {
+  /* Some legacy tables include numerous duplicate footnotes.  Suppress
+     them. */
+  for (size_t i = 0; i < v->n_footnotes; i++)
+    if (v->footnotes[i] == footnote)
+      return;
+
   v->footnotes = xrealloc (v->footnotes,
                            (v->n_footnotes + 1) * sizeof *v->footnotes);
   v->footnotes[v->n_footnotes++] = footnote;