areapct_sum
[pspp] / src / language / stats / ctables.c
index ee35fd6bd3e790ba6a17446ed30139769819e97b..930c09c6331e8a7eb33e38726ae447e85cc0a8aa 100644 (file)
@@ -93,6 +93,30 @@ enum ctables_vlabel
     S(CTSF_LAYERROWPCT_TOTALN, "LAYERROWPCT.TOTALN", N_("Layer Row Total N %"), CTF_PERCENT, CTFA_ALL) \
     S(CTSF_LAYERCOLPCT_TOTALN, "LAYERCOLPCT.TOTALN", N_("Layer Column Total N %"), CTF_PERCENT, CTFA_ALL) \
                                                                         \
+    /* All variables (unweighted.) */ \
+    S(CTSF_UCOUNT, "UCOUNT", N_("Unweighted Count"), CTF_COUNT, CTFA_ALL)            \
+    S(CTSF_UROWPCT_COUNT, "UROWPCT.COUNT", N_("Unweighted Row %"), CTF_PERCENT, CTFA_ALL) \
+    S(CTSF_UCOLPCT_COUNT, "UCOLPCT.COUNT", N_("Unweighted Column %"), CTF_PERCENT, CTFA_ALL) \
+    S(CTSF_UTABLEPCT_COUNT, "UTABLEPCT.COUNT", N_("Unweighted Table %"), CTF_PERCENT, CTFA_ALL) \
+    S(CTSF_USUBTABLEPCT_COUNT, "USUBTABLEPCT.COUNT", N_("Unweighted Subtable %"), CTF_PERCENT, CTFA_ALL) \
+    S(CTSF_ULAYERPCT_COUNT, "ULAYERPCT.COUNT", N_("Unweighted Layer %"), CTF_PERCENT, CTFA_ALL) \
+    S(CTSF_ULAYERROWPCT_COUNT, "ULAYERROWPCT.COUNT", N_("Unweighted Layer Row %"), CTF_PERCENT, CTFA_ALL) \
+    S(CTSF_ULAYERCOLPCT_COUNT, "ULAYERCOLPCT.COUNT", N_("Unweighted Layer Column %"), CTF_PERCENT, CTFA_ALL) \
+    S(CTSF_UROWPCT_VALIDN, "UROWPCT.VALIDN", N_("Unweighted Row Valid N %"), CTF_PERCENT, CTFA_ALL) \
+    S(CTSF_UCOLPCT_VALIDN, "UCOLPCT.VALIDN", N_("Unweighted Column Valid N %"), CTF_PERCENT, CTFA_ALL) \
+    S(CTSF_UTABLEPCT_VALIDN, "UTABLEPCT.VALIDN", N_("Unweighted Table Valid N %"), CTF_PERCENT, CTFA_ALL) \
+    S(CTSF_USUBTABLEPCT_VALIDN, "USUBTABLEPCT.VALIDN", N_("Unweighted Subtable Valid N %"), CTF_PERCENT, CTFA_ALL) \
+    S(CTSF_ULAYERPCT_VALIDN, "ULAYERPCT.VALIDN", N_("Unweighted Layer Valid N %"), CTF_PERCENT, CTFA_ALL) \
+    S(CTSF_ULAYERROWPCT_VALIDN, "ULAYERROWPCT.VALIDN", N_("Unweighted Layer Row Valid N %"), CTF_PERCENT, CTFA_ALL) \
+    S(CTSF_ULAYERCOLPCT_VALIDN, "ULAYERCOLPCT.VALIDN", N_("Unweighted Layer Column Valid N %"), CTF_PERCENT, CTFA_ALL) \
+    S(CTSF_UROWPCT_TOTALN, "UROWPCT.TOTALN", N_("Unweighted Row Total N %"), CTF_PERCENT, CTFA_ALL) \
+    S(CTSF_UCOLPCT_TOTALN, "UCOLPCT.TOTALN", N_("Unweighted Column Total N %"), CTF_PERCENT, CTFA_ALL) \
+    S(CTSF_UTABLEPCT_TOTALN, "UTABLEPCT.TOTALN", N_("Unweighted Table Total N %"), CTF_PERCENT, CTFA_ALL) \
+    S(CTSF_USUBTABLEPCT_TOTALN, "USUBTABLEPCT.TOTALN", N_("Unweighted Subtable Total N %"), CTF_PERCENT, CTFA_ALL) \
+    S(CTSF_ULAYERPCT_TOTALN, "ULAYERPCT.TOTALN", N_("Unweighted Layer Total N %"), CTF_PERCENT, CTFA_ALL) \
+    S(CTSF_ULAYERROWPCT_TOTALN, "ULAYERROWPCT.TOTALN", N_("Unweighted Layer Row Total N %"), CTF_PERCENT, CTFA_ALL) \
+    S(CTSF_ULAYERCOLPCT_TOTALN, "ULAYERCOLPCT.TOTALN", N_("Unweighted Layer Column Total N %"), CTF_PERCENT, CTFA_ALL) \
+                                                                        \
     /* Scale variables, totals, and subtotals. */                       \
     S(CTSF_MAXIMUM, "MAXIMUM", N_("Maximum"), CTF_GENERAL, CTFA_SCALE)  \
     S(CTSF_MEAN, "MEAN", N_("Mean"), CTF_GENERAL, CTFA_SCALE)           \
@@ -117,6 +141,26 @@ enum ctables_vlabel
     S(CTSF_LAYERPCT_SUM, "LAYERPCT.SUM", N_("Layer Sum %"), CTF_PERCENT, CTFA_SCALE) \
     S(CTSF_LAYERROWPCT_SUM, "LAYERROWPCT.SUM", N_("Layer Row Sum %"), CTF_PERCENT, CTFA_SCALE) \
     S(CTSF_LAYERCOLPCT_SUM, "LAYERCOLPCT.SUM", N_("Layer Column Sum %"), CTF_PERCENT, CTFA_SCALE) \
+                                                                        \
+    /* Scale variables, totals, and subtotals (unweighted). */                       \
+    S(CTSF_UMEAN, "UMEAN", N_("Unweighted Mean"), CTF_GENERAL, CTFA_SCALE)           \
+    S(CTSF_UMEDIAN, "UMEDIAN", N_("Unweighted Median"), CTF_GENERAL, CTFA_SCALE)     \
+    S(CTSF_UMISSING, "UMISSING", N_("Unweighted Missing"), CTF_GENERAL, CTFA_SCALE)  \
+    S(CTSF_UMODE, "UMODE", N_("Unweighted Mode"), CTF_GENERAL, CTFA_SCALE)           \
+    S(CTSF_UPTILE, "UPTILE", N_("Unweighted Percentile"), CTF_GENERAL, CTFA_SCALE)   \
+    S(CTSF_USEMEAN, "USEMEAN", N_("Unweighted Std Error of Mean"), CTF_GENERAL, CTFA_SCALE) \
+    S(CTSF_USTDDEV, "USTDDEV", N_("Unweighted Std Deviation"), CTF_GENERAL, CTFA_SCALE) \
+    S(CTSF_USUM, "USUM", N_("Unweighted Sum"), CTF_GENERAL, CTFA_SCALE)              \
+    S(CSTF_UTOTALN, "UTOTALN", N_("Unweighted Total N"), CTF_COUNT, CTFA_SCALE)      \
+    S(CTSF_UVALIDN, "UVALIDN", N_("Unweighted Valid N"), CTF_COUNT, CTFA_SCALE)      \
+    S(CTSF_UVARIANCE, "UVARIANCE", N_("Unweighted Variance"), CTF_GENERAL, CTFA_SCALE) \
+    S(CTSF_UROWPCT_SUM, "UROWPCT.SUM", N_("Unweighted Row Sum %"), CTF_PERCENT, CTFA_SCALE) \
+    S(CTSF_UCOLPCT_SUM, "UCOLPCT.SUM", N_("Unweighted Column Sum %"), CTF_PERCENT, CTFA_SCALE) \
+    S(CTSF_UTABLEPCT_SUM, "UTABLEPCT.SUM", N_("Unweighted Table Sum %"), CTF_PERCENT, CTFA_SCALE) \
+    S(CTSF_USUBTABLEPCT_SUM, "USUBTABLEPCT.SUM", N_("Unweighted Subtable Sum %"), CTF_PERCENT, CTFA_SCALE) \
+    S(CTSF_ULAYERPCT_SUM, "ULAYERPCT.SUM", N_("Unweighted Layer Sum %"), CTF_PERCENT, CTFA_SCALE) \
+    S(CTSF_ULAYERROWPCT_SUM, "ULAYERROWPCT.SUM", N_("Unweighted Layer Row Sum %"), CTF_PERCENT, CTFA_SCALE) \
+    S(CTSF_ULAYERCOLPCT_SUM, "ULAYERCOLPCT.SUM", N_("Unweighted Layer Column Sum %"), CTF_PERCENT, CTFA_SCALE) \
 
 #if 0         /* Multiple response sets not yet implemented. */
   S(CTSF_RESPONSES, "RESPONSES", N_("Responses"), CTF_COUNT, CTFA_MRSETS) \
@@ -187,6 +231,16 @@ struct ctables_domain
     double e_valid;             /* Effective weight */
     double e_count;
     double e_total;
+    double u_valid;             /* Unweighted. */
+    double u_count;
+    double u_total;
+    struct ctables_sum *sums;
+  };
+
+struct ctables_sum
+  {
+    double e_sum;
+    double u_sum;
   };
 
 enum ctables_summary_variant
@@ -294,7 +348,8 @@ struct ctables_pcexpr
         CTPO_CONSTANT,          /* 5 */
         CTPO_CAT_NUMBER,        /* [5] */
         CTPO_CAT_STRING,        /* ["STRING"] */
-        CTPO_CAT_RANGE,         /* [LO THRU 5] */
+        CTPO_CAT_NRANGE,        /* [LO THRU 5] */
+        CTPO_CAT_SRANGE,        /* ["A" THRU "B"] */
         CTPO_CAT_MISSING,       /* MISSING */
         CTPO_CAT_OTHERNM,       /* OTHERNM */
         CTPO_CAT_SUBTOTAL,      /* SUBTOTAL */
@@ -318,8 +373,11 @@ struct ctables_pcexpr
         /* CTPO_CAT_STRING, in dictionary encoding. */
         struct substring string;
 
-        /* CTPO_CAT_RANGE. */
-        double range[2];
+        /* CTPO_CAT_NRANGE. */
+        double nrange[2];
+
+        /* CTPO_CAT_SRANGE. */
+        struct substring srange[2];
 
         /* CTPO_CAT_SUBTOTAL. */
         size_t subtotal_index;
@@ -416,6 +474,8 @@ struct ctables_table
     size_t n_sections;
     enum pivot_axis_type summary_axis;
     struct ctables_summary_spec_set summary_specs;
+    struct variable **sum_vars;
+    size_t n_sum_vars;
 
     const struct variable *clabels_example;
     struct hmap clabels_values_map;
@@ -726,6 +786,7 @@ struct ctables_summary_spec
     bool is_ctables_format;       /* Is 'format' one of CTEF_*? */
 
     size_t axis_idx;
+    size_t sum_var_idx;
   };
 
 static void
@@ -733,7 +794,7 @@ ctables_summary_spec_clone (struct ctables_summary_spec *dst,
                             const struct ctables_summary_spec *src)
 {
   *dst = *src;
-  dst->label = xstrdup (src->label);
+  dst->label = xstrdup_if_nonnull (src->label);
 }
 
 static void
@@ -826,6 +887,14 @@ ctables_summary_function_is_count (enum ctables_summary_function f)
     case CTSF_LAYERPCT_COUNT:
     case CTSF_LAYERROWPCT_COUNT:
     case CTSF_LAYERCOLPCT_COUNT:
+    case CTSF_UCOUNT:
+    case CTSF_UROWPCT_COUNT:
+    case CTSF_UCOLPCT_COUNT:
+    case CTSF_UTABLEPCT_COUNT:
+    case CTSF_USUBTABLEPCT_COUNT:
+    case CTSF_ULAYERPCT_COUNT:
+    case CTSF_ULAYERROWPCT_COUNT:
+    case CTSF_ULAYERCOLPCT_COUNT:
       return true;
 
     case CTSF_ROWPCT_VALIDN:
@@ -865,6 +934,38 @@ ctables_summary_function_is_count (enum ctables_summary_function f)
     case CTSF_LAYERPCT_SUM:
     case CTSF_LAYERROWPCT_SUM:
     case CTSF_LAYERCOLPCT_SUM:
+    case CTSF_UROWPCT_VALIDN:
+    case CTSF_UCOLPCT_VALIDN:
+    case CTSF_UTABLEPCT_VALIDN:
+    case CTSF_USUBTABLEPCT_VALIDN:
+    case CTSF_ULAYERPCT_VALIDN:
+    case CTSF_ULAYERROWPCT_VALIDN:
+    case CTSF_ULAYERCOLPCT_VALIDN:
+    case CTSF_UROWPCT_TOTALN:
+    case CTSF_UCOLPCT_TOTALN:
+    case CTSF_UTABLEPCT_TOTALN:
+    case CTSF_USUBTABLEPCT_TOTALN:
+    case CTSF_ULAYERPCT_TOTALN:
+    case CTSF_ULAYERROWPCT_TOTALN:
+    case CTSF_ULAYERCOLPCT_TOTALN:
+    case CTSF_UMEAN:
+    case CTSF_UMEDIAN:
+    case CTSF_UMISSING:
+    case CTSF_UMODE:
+    case CTSF_UPTILE:
+    case CTSF_USEMEAN:
+    case CTSF_USTDDEV:
+    case CTSF_USUM:
+    case CSTF_UTOTALN:
+    case CTSF_UVALIDN:
+    case CTSF_UVARIANCE:
+    case CTSF_UROWPCT_SUM:
+    case CTSF_UCOLPCT_SUM:
+    case CTSF_UTABLEPCT_SUM:
+    case CTSF_USUBTABLEPCT_SUM:
+    case CTSF_ULAYERPCT_SUM:
+    case CTSF_ULAYERROWPCT_SUM:
+    case CTSF_ULAYERCOLPCT_SUM:
       return false;
   }
   NOT_REACHED ();
@@ -982,19 +1083,40 @@ ctables_summary_default_format (enum ctables_summary_function function,
     }
 }
 
-static char *
-ctables_summary_default_label (enum ctables_summary_function function,
-                               double percentile)
+static struct pivot_value *
+ctables_summary_label (const struct ctables_summary_spec *spec, double cilevel)
 {
-  static const char *default_labels[] = {
+  if (!spec->label)
+    {
+      static const char *default_labels[] = {
 #define S(ENUM, NAME, LABEL, FORMAT, AVAILABILITY) [ENUM] = LABEL,
-    SUMMARIES
+        SUMMARIES
 #undef S
-  };
+      };
+
+      return (spec->function == CTSF_PTILE
+              ? pivot_value_new_text_format (N_("Percentile %.2f"),
+                                             spec->percentile)
+              : pivot_value_new_text (default_labels[spec->function]));
+    }
+  else
+    {
+      struct substring in = ss_cstr (spec->label);
+      struct substring target = ss_cstr (")CILEVEL");
 
-  return (function == CTSF_PTILE
-          ? xasprintf (_("Percentile %.2f"), percentile)
-          : xstrdup (gettext (default_labels[function])));
+      struct string out = DS_EMPTY_INITIALIZER;
+      for (;;)
+        {
+          size_t chunk = ss_find_substring (in, target);
+          ds_put_substring (&out, ss_head (in, chunk));
+          ss_advance (&in, chunk);
+          if (!in.length)
+            return pivot_value_new_user_text_nocopy (ds_steal_cstr (&out));
+          
+          ss_advance (&in, target.length);
+          ds_put_format (&out, "%g", cilevel);
+        }
+    }
 }
 
 static const char *
@@ -1055,7 +1177,7 @@ add_summary_spec (struct ctables_axis *axis,
       *dst = (struct ctables_summary_spec) {
         .function = function,
         .percentile = percentile,
-        .label = xstrdup (label),
+        .label = xstrdup_if_nonnull (label),
         .format = (format ? *format
                    : ctables_summary_default_format (function, axis->var)),
         .is_ctables_format = is_ctables_format,
@@ -1195,14 +1317,12 @@ ctables_axis_parse_postfix (struct ctables_axis_parse_ctx *ctx)
         }
 
       /* Parse label. */
-      char *label;
+      char *label = NULL;
       if (lex_is_string (ctx->lexer))
         {
           label = ss_xstrdup (lex_tokss (ctx->lexer));
           lex_get (ctx->lexer);
         }
-      else
-        label = ctables_summary_default_label (function, percentile);
 
       /* Parse format. */
       struct fmt_spec format;
@@ -1599,10 +1719,17 @@ ctables_find_category_for_postcompute (const struct ctables_categories *cats,
             best = cat;
           break;
 
-        case CTPO_CAT_RANGE:
+        case CTPO_CAT_NRANGE:
           if (cat->type == CCT_NRANGE
-              && cat->nrange[0] == e->range[0]
-              && cat->nrange[1] == e->range[1])
+              && cat->nrange[0] == e->nrange[0]
+              && cat->nrange[1] == e->nrange[1])
+            best = cat;
+          break;
+
+        case CTPO_CAT_SRANGE:
+          if (cat->type == CCT_SRANGE
+              && nullable_substring_equal (&cat->srange[0], &e->srange[0])
+              && nullable_substring_equal (&cat->srange[1], &e->srange[1]))
             best = cat;
           break;
 
@@ -1657,7 +1784,7 @@ ctables_recursive_check_postcompute (const struct ctables_pcexpr *e,
     {
     case CTPO_CAT_NUMBER:
     case CTPO_CAT_STRING:
-    case CTPO_CAT_RANGE:
+    case CTPO_CAT_NRANGE:
     case CTPO_CAT_MISSING:
     case CTPO_CAT_OTHERNM:
     case CTPO_CAT_SUBTOTAL:
@@ -2291,6 +2418,31 @@ ctables_summary_init (union ctables_summary *s,
     case CTSF_ETOTALN:
     case CTSF_VALIDN:
     case CTSF_EVALIDN:
+    case CTSF_UCOUNT:
+    case CTSF_UROWPCT_COUNT:
+    case CTSF_UCOLPCT_COUNT:
+    case CTSF_UTABLEPCT_COUNT:
+    case CTSF_USUBTABLEPCT_COUNT:
+    case CTSF_ULAYERPCT_COUNT:
+    case CTSF_ULAYERROWPCT_COUNT:
+    case CTSF_ULAYERCOLPCT_COUNT:
+    case CTSF_UROWPCT_VALIDN:
+    case CTSF_UCOLPCT_VALIDN:
+    case CTSF_UTABLEPCT_VALIDN:
+    case CTSF_USUBTABLEPCT_VALIDN:
+    case CTSF_ULAYERPCT_VALIDN:
+    case CTSF_ULAYERROWPCT_VALIDN:
+    case CTSF_ULAYERCOLPCT_VALIDN:
+    case CTSF_UROWPCT_TOTALN:
+    case CTSF_UCOLPCT_TOTALN:
+    case CTSF_UTABLEPCT_TOTALN:
+    case CTSF_USUBTABLEPCT_TOTALN:
+    case CTSF_ULAYERPCT_TOTALN:
+    case CTSF_ULAYERROWPCT_TOTALN:
+    case CTSF_ULAYERCOLPCT_TOTALN:
+    case CTSF_UMISSING:
+    case CSTF_UTOTALN:
+    case CTSF_UVALIDN:
       s->count = 0;
       break;
 
@@ -2312,12 +2464,27 @@ ctables_summary_init (union ctables_summary *s,
     case CTSF_LAYERPCT_SUM:
     case CTSF_LAYERROWPCT_SUM:
     case CTSF_LAYERCOLPCT_SUM:
+    case CTSF_UMEAN:
+    case CTSF_USEMEAN:
+    case CTSF_USTDDEV:
+    case CTSF_USUM:
+    case CTSF_UVARIANCE:
+    case CTSF_UROWPCT_SUM:
+    case CTSF_UCOLPCT_SUM:
+    case CTSF_UTABLEPCT_SUM:
+    case CTSF_USUBTABLEPCT_SUM:
+    case CTSF_ULAYERPCT_SUM:
+    case CTSF_ULAYERROWPCT_SUM:
+    case CTSF_ULAYERCOLPCT_SUM:
       s->moments = moments1_create (MOMENT_VARIANCE);
       break;
 
     case CTSF_MEDIAN:
     case CTSF_MODE:
     case CTSF_PTILE:
+    case CTSF_UMEDIAN:
+    case CTSF_UMODE:
+    case CTSF_UPTILE:
       {
         struct caseproto *proto = caseproto_create ();
         proto = caseproto_add_width (proto, 0);
@@ -2370,6 +2537,31 @@ ctables_summary_uninit (union ctables_summary *s,
     case CTSF_ETOTALN:
     case CTSF_VALIDN:
     case CTSF_EVALIDN:
+    case CTSF_UCOUNT:
+    case CTSF_UROWPCT_COUNT:
+    case CTSF_UCOLPCT_COUNT:
+    case CTSF_UTABLEPCT_COUNT:
+    case CTSF_USUBTABLEPCT_COUNT:
+    case CTSF_ULAYERPCT_COUNT:
+    case CTSF_ULAYERROWPCT_COUNT:
+    case CTSF_ULAYERCOLPCT_COUNT:
+    case CTSF_UROWPCT_VALIDN:
+    case CTSF_UCOLPCT_VALIDN:
+    case CTSF_UTABLEPCT_VALIDN:
+    case CTSF_USUBTABLEPCT_VALIDN:
+    case CTSF_ULAYERPCT_VALIDN:
+    case CTSF_ULAYERROWPCT_VALIDN:
+    case CTSF_ULAYERCOLPCT_VALIDN:
+    case CTSF_UROWPCT_TOTALN:
+    case CTSF_UCOLPCT_TOTALN:
+    case CTSF_UTABLEPCT_TOTALN:
+    case CTSF_USUBTABLEPCT_TOTALN:
+    case CTSF_ULAYERPCT_TOTALN:
+    case CTSF_ULAYERROWPCT_TOTALN:
+    case CTSF_ULAYERCOLPCT_TOTALN:
+    case CTSF_UMISSING:
+    case CSTF_UTOTALN:
+    case CTSF_UVALIDN:
       break;
 
     case CTSF_MAXIMUM:
@@ -2389,12 +2581,27 @@ ctables_summary_uninit (union ctables_summary *s,
     case CTSF_LAYERPCT_SUM:
     case CTSF_LAYERROWPCT_SUM:
     case CTSF_LAYERCOLPCT_SUM:
+    case CTSF_UMEAN:
+    case CTSF_USEMEAN:
+    case CTSF_USTDDEV:
+    case CTSF_USUM:
+    case CTSF_UVARIANCE:
+    case CTSF_UROWPCT_SUM:
+    case CTSF_UCOLPCT_SUM:
+    case CTSF_UTABLEPCT_SUM:
+    case CTSF_USUBTABLEPCT_SUM:
+    case CTSF_ULAYERPCT_SUM:
+    case CTSF_ULAYERROWPCT_SUM:
+    case CTSF_ULAYERCOLPCT_SUM:
       moments1_destroy (s->moments);
       break;
 
     case CTSF_MEDIAN:
     case CTSF_MODE:
     case CTSF_PTILE:
+    case CTSF_UMEDIAN:
+    case CTSF_UMODE:
+    case CTSF_UPTILE:
       casewriter_destroy (s->writer);
       break;
     }
@@ -2439,6 +2646,17 @@ ctables_summary_add (union ctables_summary *s,
       s->count += d_weight;
       break;
 
+    case CSTF_UTOTALN:
+    case CTSF_UROWPCT_TOTALN:
+    case CTSF_UCOLPCT_TOTALN:
+    case CTSF_UTABLEPCT_TOTALN:
+    case CTSF_USUBTABLEPCT_TOTALN:
+    case CTSF_ULAYERPCT_TOTALN:
+    case CTSF_ULAYERROWPCT_TOTALN:
+    case CTSF_ULAYERCOLPCT_TOTALN:
+      s->count += 1.0;
+      break;
+
     case CTSF_COUNT:
     case CTSF_ROWPCT_COUNT:
     case CTSF_COLPCT_COUNT:
@@ -2451,6 +2669,18 @@ ctables_summary_add (union ctables_summary *s,
         s->count += d_weight;
       break;
 
+    case CTSF_UCOUNT:
+    case CTSF_UROWPCT_COUNT:
+    case CTSF_UCOLPCT_COUNT:
+    case CTSF_UTABLEPCT_COUNT:
+    case CTSF_USUBTABLEPCT_COUNT:
+    case CTSF_ULAYERPCT_COUNT:
+    case CTSF_ULAYERROWPCT_COUNT:
+    case CTSF_ULAYERCOLPCT_COUNT:
+      if (is_scale || !excluded_missing)
+        s->count += 1.0;
+      break;
+
     case CTSF_VALIDN:
     case CTSF_ROWPCT_VALIDN:
     case CTSF_COLPCT_VALIDN:
@@ -2465,11 +2695,30 @@ ctables_summary_add (union ctables_summary *s,
         s->count += d_weight;
       break;
 
+    case CTSF_UVALIDN:
+    case CTSF_UROWPCT_VALIDN:
+    case CTSF_UCOLPCT_VALIDN:
+    case CTSF_UTABLEPCT_VALIDN:
+    case CTSF_USUBTABLEPCT_VALIDN:
+    case CTSF_ULAYERPCT_VALIDN:
+    case CTSF_ULAYERROWPCT_VALIDN:
+    case CTSF_ULAYERCOLPCT_VALIDN:
+      if (is_scale
+          ? !is_scale_missing
+          : !is_missing)
+        s->count += 1.0;
+      break;
+
     case CTSF_MISSING:
       if (is_missing)
         s->count += d_weight;
       break;
 
+    case CTSF_UMISSING:
+      if (is_missing)
+        s->count += 1.0;
+      break;
+
     case CTSF_ECOUNT:
       if (is_scale || !excluded_missing)
         s->count += e_weight;
@@ -2515,6 +2764,27 @@ ctables_summary_add (union ctables_summary *s,
         moments1_add (s->moments, value->f, e_weight);
       break;
 
+    case CTSF_UMEAN:
+    case CTSF_USEMEAN:
+    case CTSF_USTDDEV:
+    case CTSF_USUM:
+    case CTSF_UVARIANCE:
+    case CTSF_UROWPCT_SUM:
+    case CTSF_UCOLPCT_SUM:
+    case CTSF_UTABLEPCT_SUM:
+    case CTSF_USUBTABLEPCT_SUM:
+    case CTSF_ULAYERPCT_SUM:
+    case CTSF_ULAYERROWPCT_SUM:
+    case CTSF_ULAYERCOLPCT_SUM:
+      if (!is_scale_missing)
+        moments1_add (s->moments, value->f, 1.0);
+      break;
+
+    case CTSF_UMEDIAN:
+    case CTSF_UMODE:
+    case CTSF_UPTILE:
+      d_weight = e_weight = 1.0;
+      /* Fall through. */
     case CTSF_MEDIAN:
     case CTSF_MODE:
     case CTSF_PTILE:
@@ -2554,54 +2824,193 @@ ctables_function_domain (enum ctables_summary_function function)
     case CTSF_MEDIAN:
     case CTSF_PTILE:
     case CTSF_MODE:
+    case CTSF_UCOUNT:
+    case CTSF_UMISSING:
+    case CSTF_UTOTALN:
+    case CTSF_UVALIDN:
+    case CTSF_UMEAN:
+    case CTSF_USEMEAN:
+    case CTSF_USTDDEV:
+    case CTSF_USUM:
+    case CTSF_UVARIANCE:
+    case CTSF_UMEDIAN:
+    case CTSF_UPTILE:
+    case CTSF_UMODE:
       NOT_REACHED ();
 
     case CTSF_COLPCT_COUNT:
     case CTSF_COLPCT_SUM:
     case CTSF_COLPCT_TOTALN:
     case CTSF_COLPCT_VALIDN:
+    case CTSF_UCOLPCT_COUNT:
+    case CTSF_UCOLPCT_SUM:
+    case CTSF_UCOLPCT_TOTALN:
+    case CTSF_UCOLPCT_VALIDN:
       return CTDT_COL;
 
     case CTSF_LAYERCOLPCT_COUNT:
     case CTSF_LAYERCOLPCT_SUM:
     case CTSF_LAYERCOLPCT_TOTALN:
     case CTSF_LAYERCOLPCT_VALIDN:
+    case CTSF_ULAYERCOLPCT_COUNT:
+    case CTSF_ULAYERCOLPCT_SUM:
+    case CTSF_ULAYERCOLPCT_TOTALN:
+    case CTSF_ULAYERCOLPCT_VALIDN:
       return CTDT_LAYERCOL;
 
     case CTSF_LAYERPCT_COUNT:
     case CTSF_LAYERPCT_SUM:
     case CTSF_LAYERPCT_TOTALN:
     case CTSF_LAYERPCT_VALIDN:
+    case CTSF_ULAYERPCT_COUNT:
+    case CTSF_ULAYERPCT_SUM:
+    case CTSF_ULAYERPCT_TOTALN:
+    case CTSF_ULAYERPCT_VALIDN:
       return CTDT_LAYER;
 
     case CTSF_LAYERROWPCT_COUNT:
     case CTSF_LAYERROWPCT_SUM:
     case CTSF_LAYERROWPCT_TOTALN:
     case CTSF_LAYERROWPCT_VALIDN:
+    case CTSF_ULAYERROWPCT_COUNT:
+    case CTSF_ULAYERROWPCT_SUM:
+    case CTSF_ULAYERROWPCT_TOTALN:
+    case CTSF_ULAYERROWPCT_VALIDN:
       return CTDT_LAYERROW;
 
     case CTSF_ROWPCT_COUNT:
     case CTSF_ROWPCT_SUM:
     case CTSF_ROWPCT_TOTALN:
     case CTSF_ROWPCT_VALIDN:
+    case CTSF_UROWPCT_COUNT:
+    case CTSF_UROWPCT_SUM:
+    case CTSF_UROWPCT_TOTALN:
+    case CTSF_UROWPCT_VALIDN:
       return CTDT_ROW;
 
     case CTSF_SUBTABLEPCT_COUNT:
     case CTSF_SUBTABLEPCT_SUM:
     case CTSF_SUBTABLEPCT_TOTALN:
     case CTSF_SUBTABLEPCT_VALIDN:
+    case CTSF_USUBTABLEPCT_COUNT:
+    case CTSF_USUBTABLEPCT_SUM:
+    case CTSF_USUBTABLEPCT_TOTALN:
+    case CTSF_USUBTABLEPCT_VALIDN:
       return CTDT_SUBTABLE;
 
     case CTSF_TABLEPCT_COUNT:
     case CTSF_TABLEPCT_SUM:
     case CTSF_TABLEPCT_TOTALN:
     case CTSF_TABLEPCT_VALIDN:
+    case CTSF_UTABLEPCT_COUNT:
+    case CTSF_UTABLEPCT_SUM:
+    case CTSF_UTABLEPCT_TOTALN:
+    case CTSF_UTABLEPCT_VALIDN:
       return CTDT_TABLE;
     }
 
   NOT_REACHED ();
 }
 
+static enum ctables_domain_type
+ctables_function_is_pctsum (enum ctables_summary_function function)
+{
+  switch (function)
+    {
+    case CTSF_COUNT:
+    case CTSF_ECOUNT:
+    case CTSF_MISSING:
+    case CSTF_TOTALN:
+    case CTSF_ETOTALN:
+    case CTSF_VALIDN:
+    case CTSF_EVALIDN:
+    case CTSF_MAXIMUM:
+    case CTSF_MINIMUM:
+    case CTSF_RANGE:
+    case CTSF_MEAN:
+    case CTSF_SEMEAN:
+    case CTSF_STDDEV:
+    case CTSF_SUM:
+    case CTSF_VARIANCE:
+    case CTSF_MEDIAN:
+    case CTSF_PTILE:
+    case CTSF_MODE:
+    case CTSF_UCOUNT:
+    case CTSF_UMISSING:
+    case CSTF_UTOTALN:
+    case CTSF_UVALIDN:
+    case CTSF_UMEAN:
+    case CTSF_USEMEAN:
+    case CTSF_USTDDEV:
+    case CTSF_USUM:
+    case CTSF_UVARIANCE:
+    case CTSF_UMEDIAN:
+    case CTSF_UPTILE:
+    case CTSF_UMODE:
+    case CTSF_COLPCT_COUNT:
+    case CTSF_COLPCT_TOTALN:
+    case CTSF_COLPCT_VALIDN:
+    case CTSF_UCOLPCT_COUNT:
+    case CTSF_UCOLPCT_TOTALN:
+    case CTSF_UCOLPCT_VALIDN:
+    case CTSF_LAYERCOLPCT_COUNT:
+    case CTSF_LAYERCOLPCT_TOTALN:
+    case CTSF_LAYERCOLPCT_VALIDN:
+    case CTSF_ULAYERCOLPCT_COUNT:
+    case CTSF_ULAYERCOLPCT_TOTALN:
+    case CTSF_ULAYERCOLPCT_VALIDN:
+    case CTSF_LAYERPCT_COUNT:
+    case CTSF_LAYERPCT_TOTALN:
+    case CTSF_LAYERPCT_VALIDN:
+    case CTSF_ULAYERPCT_COUNT:
+    case CTSF_ULAYERPCT_TOTALN:
+    case CTSF_ULAYERPCT_VALIDN:
+    case CTSF_LAYERROWPCT_COUNT:
+    case CTSF_LAYERROWPCT_TOTALN:
+    case CTSF_LAYERROWPCT_VALIDN:
+    case CTSF_ULAYERROWPCT_COUNT:
+    case CTSF_ULAYERROWPCT_TOTALN:
+    case CTSF_ULAYERROWPCT_VALIDN:
+    case CTSF_ROWPCT_COUNT:
+    case CTSF_ROWPCT_TOTALN:
+    case CTSF_ROWPCT_VALIDN:
+    case CTSF_UROWPCT_COUNT:
+    case CTSF_UROWPCT_TOTALN:
+    case CTSF_UROWPCT_VALIDN:
+    case CTSF_SUBTABLEPCT_COUNT:
+    case CTSF_SUBTABLEPCT_TOTALN:
+    case CTSF_SUBTABLEPCT_VALIDN:
+    case CTSF_USUBTABLEPCT_COUNT:
+    case CTSF_USUBTABLEPCT_TOTALN:
+    case CTSF_USUBTABLEPCT_VALIDN:
+    case CTSF_TABLEPCT_COUNT:
+    case CTSF_TABLEPCT_TOTALN:
+    case CTSF_TABLEPCT_VALIDN:
+    case CTSF_UTABLEPCT_COUNT:
+    case CTSF_UTABLEPCT_TOTALN:
+    case CTSF_UTABLEPCT_VALIDN:
+      return false;
+
+    case CTSF_COLPCT_SUM:
+    case CTSF_UCOLPCT_SUM:
+    case CTSF_LAYERCOLPCT_SUM:
+    case CTSF_ULAYERCOLPCT_SUM:
+    case CTSF_LAYERPCT_SUM:
+    case CTSF_ULAYERPCT_SUM:
+    case CTSF_LAYERROWPCT_SUM:
+    case CTSF_ULAYERROWPCT_SUM:
+    case CTSF_ROWPCT_SUM:
+    case CTSF_UROWPCT_SUM:
+    case CTSF_SUBTABLEPCT_SUM:
+    case CTSF_USUBTABLEPCT_SUM:
+    case CTSF_TABLEPCT_SUM:
+    case CTSF_UTABLEPCT_SUM:
+      return true;
+    }
+
+  NOT_REACHED ();
+}
+
 static double
 ctables_summary_value (const struct ctables_cell *cell,
                        union ctables_summary *s,
@@ -2611,6 +3020,7 @@ ctables_summary_value (const struct ctables_cell *cell,
     {
     case CTSF_COUNT:
     case CTSF_ECOUNT:
+    case CTSF_UCOUNT:
       return s->count;
 
     case CTSF_ROWPCT_COUNT:
@@ -2627,6 +3037,20 @@ ctables_summary_value (const struct ctables_cell *cell,
                 : SYSMIS);
       }
 
+    case CTSF_UROWPCT_COUNT:
+    case CTSF_UCOLPCT_COUNT:
+    case CTSF_UTABLEPCT_COUNT:
+    case CTSF_USUBTABLEPCT_COUNT:
+    case CTSF_ULAYERPCT_COUNT:
+    case CTSF_ULAYERROWPCT_COUNT:
+    case CTSF_ULAYERCOLPCT_COUNT:
+      {
+        enum ctables_domain_type d = ctables_function_domain (ss->function);
+        return (cell->domains[d]->u_count
+                ? s->count / cell->domains[d]->u_count * 100
+                : SYSMIS);
+      }
+
     case CTSF_ROWPCT_VALIDN:
     case CTSF_COLPCT_VALIDN:
     case CTSF_TABLEPCT_VALIDN:
@@ -2641,6 +3065,20 @@ ctables_summary_value (const struct ctables_cell *cell,
                 : SYSMIS);
       }
 
+    case CTSF_UROWPCT_VALIDN:
+    case CTSF_UCOLPCT_VALIDN:
+    case CTSF_UTABLEPCT_VALIDN:
+    case CTSF_USUBTABLEPCT_VALIDN:
+    case CTSF_ULAYERPCT_VALIDN:
+    case CTSF_ULAYERROWPCT_VALIDN:
+    case CTSF_ULAYERCOLPCT_VALIDN:
+      {
+        enum ctables_domain_type d = ctables_function_domain (ss->function);
+        return (cell->domains[d]->u_valid
+                ? s->count / cell->domains[d]->u_valid * 100
+                : SYSMIS);
+      }
+
     case CTSF_ROWPCT_TOTALN:
     case CTSF_COLPCT_TOTALN:
     case CTSF_TABLEPCT_TOTALN:
@@ -2655,16 +3093,27 @@ ctables_summary_value (const struct ctables_cell *cell,
                 : SYSMIS);
       }
 
-    case CTSF_MISSING:
-      return s->count;
+    case CTSF_UROWPCT_TOTALN:
+    case CTSF_UCOLPCT_TOTALN:
+    case CTSF_UTABLEPCT_TOTALN:
+    case CTSF_USUBTABLEPCT_TOTALN:
+    case CTSF_ULAYERPCT_TOTALN:
+    case CTSF_ULAYERROWPCT_TOTALN:
+    case CTSF_ULAYERCOLPCT_TOTALN:
+      {
+        enum ctables_domain_type d = ctables_function_domain (ss->function);
+        return (cell->domains[d]->u_total
+                ? s->count / cell->domains[d]->u_total * 100
+                : SYSMIS);
+      }
 
+    case CTSF_MISSING:
+    case CTSF_UMISSING:
     case CSTF_TOTALN:
     case CTSF_ETOTALN:
-      return s->count;
-
+    case CSTF_UTOTALN:
     case CTSF_VALIDN:
-      return s->count;
-
+    case CTSF_UVALIDN:
     case CTSF_EVALIDN:
       return s->count;
 
@@ -2678,6 +3127,7 @@ ctables_summary_value (const struct ctables_cell *cell,
       return s->max != SYSMIS && s->min != SYSMIS ? s->max - s->min : SYSMIS;
 
     case CTSF_MEAN:
+    case CTSF_UMEAN:
       {
         double mean;
         moments1_calculate (s->moments, NULL, &mean, NULL, NULL, NULL);
@@ -2685,6 +3135,7 @@ ctables_summary_value (const struct ctables_cell *cell,
       }
 
     case CTSF_SEMEAN:
+    case CTSF_USEMEAN:
       {
         double weight, variance;
         moments1_calculate (s->moments, &weight, NULL, &variance, NULL, NULL);
@@ -2692,6 +3143,7 @@ ctables_summary_value (const struct ctables_cell *cell,
       }
 
     case CTSF_STDDEV:
+    case CTSF_USTDDEV:
       {
         double variance;
         moments1_calculate (s->moments, NULL, NULL, &variance, NULL, NULL);
@@ -2699,6 +3151,7 @@ ctables_summary_value (const struct ctables_cell *cell,
       }
 
     case CTSF_SUM:
+    case CTSF_USUM:
       {
         double weight, mean;
         moments1_calculate (s->moments, &weight, &mean, NULL, NULL, NULL);
@@ -2706,6 +3159,7 @@ ctables_summary_value (const struct ctables_cell *cell,
       }
 
     case CTSF_VARIANCE:
+    case CTSF_UVARIANCE:
       {
         double variance;
         moments1_calculate (s->moments, NULL, NULL, &variance, NULL, NULL);
@@ -2719,10 +3173,38 @@ ctables_summary_value (const struct ctables_cell *cell,
     case CTSF_LAYERPCT_SUM:
     case CTSF_LAYERROWPCT_SUM:
     case CTSF_LAYERCOLPCT_SUM:
-      NOT_REACHED ();
+      {
+        double weight, mean;
+        moments1_calculate (s->moments, &weight, &mean, NULL, NULL, NULL);
+        if (weight == SYSMIS || mean == SYSMIS)
+          return SYSMIS;
+        enum ctables_domain_type d = ctables_function_domain (ss->function);
+        double num = weight * mean;
+        double denom = cell->domains[d]->sums[ss->sum_var_idx].e_sum;
+        return denom != 0 ? num / denom * 100 : SYSMIS;
+      }
+    case CTSF_UROWPCT_SUM:
+    case CTSF_UCOLPCT_SUM:
+    case CTSF_UTABLEPCT_SUM:
+    case CTSF_USUBTABLEPCT_SUM:
+    case CTSF_ULAYERPCT_SUM:
+    case CTSF_ULAYERROWPCT_SUM:
+    case CTSF_ULAYERCOLPCT_SUM:
+      {
+        double weight, mean;
+        moments1_calculate (s->moments, &weight, &mean, NULL, NULL, NULL);
+        if (weight == SYSMIS || mean == SYSMIS)
+          return SYSMIS;
+        enum ctables_domain_type d = ctables_function_domain (ss->function);
+        double num = weight * mean;
+        double denom = cell->domains[d]->sums[ss->sum_var_idx].u_sum;
+        return denom != 0 ? num / denom * 100 : SYSMIS;
+      }
 
     case CTSF_MEDIAN:
     case CTSF_PTILE:
+    case CTSF_UMEDIAN:
+    case CTSF_UPTILE:
       if (s->writer)
         {
           struct casereader *reader = casewriter_make_reader (s->writer);
@@ -2738,6 +3220,7 @@ ctables_summary_value (const struct ctables_cell *cell,
       return s->ovalue;
 
     case CTSF_MODE:
+    case CTSF_UMODE:
       if (s->writer)
         {
           struct casereader *reader = casewriter_make_reader (s->writer);
@@ -2897,8 +3380,12 @@ ctables_domain_insert (struct ctables_section *s, struct ctables_cell *cell,
     not_equal: ;
     }
 
+  struct ctables_sum *sums = (s->table->n_sum_vars
+                              ? xzalloc (s->table->n_sum_vars * sizeof *sums)
+                              : NULL);
+
   d = xmalloc (sizeof *d);
-  *d = (struct ctables_domain) { .example = cell };
+  *d = (struct ctables_domain) { .example = cell, .sums = sums };
   hmap_insert (&s->domains[domain], &d->node, hash);
   return d;
 }
@@ -3176,15 +3663,31 @@ ctables_cell_add__ (struct ctables_section *s, const struct ccase *c,
         struct ctables_domain *d = cell->domains[dt];
         d->d_total += d_weight;
         d->e_total += e_weight;
+        d->u_total += 1.0;
         if (!excluded_missing)
           {
             d->d_count += d_weight;
             d->e_count += e_weight;
+            d->u_count += 1.0;
           }
         if (!is_missing)
           {
             d->d_valid += d_weight;
             d->e_valid += e_weight;
+            d->u_count += 1.0;
+
+            for (size_t i = 0; i < s->table->n_sum_vars; i++)
+              {
+                /* XXX listwise_missing??? */
+                const struct variable *var = s->table->sum_vars[i];
+                double addend = case_num (c, var);
+                if (!var_is_num_missing (var, addend))
+                  {
+                    struct ctables_sum *sum = &d->sums[i];
+                    sum->e_sum += addend * e_weight;
+                    sum->u_sum += addend;
+                  }
+              }
           }
       }
 }
@@ -3359,7 +3862,10 @@ merge_item_compare_3way (const struct merge_item *a, const struct merge_item *b)
     return as->function > bs->function ? 1 : -1;
   else if (as->percentile != bs->percentile)
     return as->percentile < bs->percentile ? 1 : -1;
-  return strcmp (as->label, bs->label);
+
+  const char *as_label = as->label ? as->label : "";
+  const char *bs_label = bs->label ? bs->label : "";
+  return strcmp (as_label, bs_label);
 }
 
 static struct pivot_value *
@@ -3546,6 +4052,7 @@ struct ctables_pcexpr_evaluate_ctx
     const struct ctables_categories *cats;
     enum pivot_axis_type pc_a;
     size_t pc_a_idx;
+    size_t summary_idx;
   };
 
 static double ctables_pcexpr_evaluate (
@@ -3626,8 +4133,8 @@ found: ;
   const struct ctables_table *t = s->table;
   const struct ctables_nest *specs_nest = s->nests[t->summary_axis];
   const struct ctables_summary_spec_set *specs = &specs_nest->specs[tc->sv];
-  size_t j = 0 /* XXX */;
-  return ctables_summary_value (tc, &tc->summaries[j], &specs->specs[j]);
+  return ctables_summary_value (tc, &tc->summaries[ctx->summary_idx],
+                                &specs->specs[ctx->summary_idx]);
 }
 
 static double
@@ -3639,7 +4146,8 @@ ctables_pcexpr_evaluate (const struct ctables_pcexpr_evaluate_ctx *ctx,
     case CTPO_CONSTANT:
       return e->number;
 
-    case CTPO_CAT_RANGE:
+    case CTPO_CAT_NRANGE:
+    case CTPO_CAT_SRANGE:
       {
         struct ctables_cell_value cv = {
           .category = ctables_find_category_for_postcompute (ctx->cats, e)
@@ -3697,27 +4205,61 @@ ctables_pcexpr_evaluate (const struct ctables_pcexpr_evaluate_ctx *ctx,
   NOT_REACHED ();
 }
 
+/* XXX what if there is a postcompute in more than one dimension?? */
+static const struct ctables_postcompute *
+ctables_cell_postcompute (const struct ctables_section *s,
+                          const struct ctables_cell *cell,
+                          enum pivot_axis_type *pc_a_p,
+                          size_t *pc_a_idx_p)
+{
+  assert (cell->postcompute);
+  for (enum pivot_axis_type pc_a = 0; ; pc_a++)
+    {
+      assert (pc_a < PIVOT_N_AXES);
+      for (size_t pc_a_idx = 0; pc_a_idx < s->nests[pc_a]->n; pc_a_idx++)
+        {
+          const struct ctables_cell_value *cv = &cell->axes[pc_a].cvs[pc_a_idx];
+          if (cv->category->type == CCT_POSTCOMPUTE)
+            {
+              if (pc_a_p)
+                *pc_a_p = pc_a;
+              if (pc_a_idx_p)
+                *pc_a_idx_p = pc_a_idx;
+              return cv->category->pc;
+            }
+        }
+    }
+
+  NOT_REACHED ();
+}
+
 static double
 ctables_cell_calculate_postcompute (const struct ctables_section *s,
-                                    const struct ctables_cell *cell)
+                                    const struct ctables_cell *cell,
+                                    const struct ctables_summary_spec *ss,
+                                    struct fmt_spec *format,
+                                    bool *is_ctables_format,
+                                    size_t summary_idx)
 {
   enum pivot_axis_type pc_a;
   size_t pc_a_idx;
-  const struct ctables_postcompute *pc;
-  for (pc_a = 0; ; pc_a++)
+  const struct ctables_postcompute *pc = ctables_cell_postcompute (
+    s, cell, &pc_a, &pc_a_idx);
+
+  if (pc->specs)
     {
-      assert (pc_a < PIVOT_N_AXES);
-      for (pc_a_idx = 0; pc_a_idx < s->nests[pc_a]->n; pc_a_idx++)
+      for (size_t i = 0; i < pc->specs->n; i++)
         {
-          const struct ctables_cell_value *cv = &cell->axes[pc_a].cvs[pc_a_idx];
-          if (cv->category->type == CCT_POSTCOMPUTE)
+          const struct ctables_summary_spec *ss2 = &pc->specs->specs[i];
+          if (ss->function == ss2->function
+              && ss->percentile == ss2->percentile)
             {
-              pc = cv->category->pc;
-              goto found;
+              *format = ss2->format;
+              *is_ctables_format = ss2->is_ctables_format;
+              break;
             }
         }
     }
-found: ;
 
   const struct variable *var = s->nests[pc_a]->vars[pc_a_idx];
   const struct ctables_categories *cats = s->table->categories[
@@ -3728,6 +4270,7 @@ found: ;
     .cats = cats,
     .pc_a = pc_a,
     .pc_a_idx = pc_a_idx,
+    .summary_idx = summary_idx,
   };
   return ctables_pcexpr_evaluate (&ctx, pc->expr);
 }
@@ -3759,7 +4302,7 @@ ctables_table_output (struct ctables *ct, struct ctables_table *t)
         d->hide_all_labels = true;
       for (size_t i = 0; i < specs->n; i++)
         pivot_category_create_leaf (
-          d->root, pivot_value_new_text (specs->specs[i].label));
+          d->root, ctables_summary_label (&specs->specs[i], t->cilevel));
     }
 
   bool categories_dimension = t->clabels_example != NULL;
@@ -3954,7 +4497,8 @@ ctables_table_output (struct ctables *ct, struct ctables_table *t)
                       for (size_t m = 0; m < specs->n; m++)
                         {
                           int leaf = pivot_category_create_leaf (
-                            parent, pivot_value_new_text (specs->specs[m].label));
+                            parent, ctables_summary_label (&specs->specs[m],
+                                                           t->cilevel));
                           if (!m)
                             prev_leaf = leaf;
                         }
@@ -4034,15 +4578,18 @@ ctables_table_output (struct ctables *ct, struct ctables_table *t)
 
               const struct ctables_summary_spec *ss = &specs->specs[j];
 
+              struct fmt_spec format = specs->specs[j].format;
+              bool is_ctables_format = ss->is_ctables_format;
               double d = (cell->postcompute
-                          ? ctables_cell_calculate_postcompute (s, cell)
-                          : ctables_summary_value (cell, &cell->summaries[j], ss));
+                          ? ctables_cell_calculate_postcompute (
+                            s, cell, ss, &format, &is_ctables_format, j)
+                          : ctables_summary_value (cell, &cell->summaries[j],
+                                                   ss));
+
               struct pivot_value *value;
               if (ct->hide_threshold != 0
                   && d < ct->hide_threshold
-                  && (cell->postcompute
-                      ? false /* XXX */
-                      : ctables_summary_function_is_count (ss->function)))
+                  && ctables_summary_function_is_count (ss->function))
                 {
                   value = pivot_value_new_user_text_nocopy (
                     xasprintf ("<%d", ct->hide_threshold));
@@ -4051,18 +4598,17 @@ ctables_table_output (struct ctables *ct, struct ctables_table *t)
                 value = pivot_value_new_user_text (ct->zero, SIZE_MAX);
               else if (d == SYSMIS && ct->missing)
                 value = pivot_value_new_user_text (ct->missing, SIZE_MAX);
-              else if (specs->specs[j].is_ctables_format)
+              else if (is_ctables_format)
                 {
                   char *s = data_out_stretchy (&(union value) { .f = d },
-                                               "UTF-8",
-                                               &specs->specs[j].format,
+                                               "UTF-8", &format,
                                                &ct->ctables_formats, NULL);
                   value = pivot_value_new_user_text_nocopy (s);
                 }
               else
                 {
                   value = pivot_value_new_number (d);
-                  value->numeric.format = specs->specs[j].format;
+                  value->numeric.format = format;
                 }
               pivot_table_put (pt, dindexes, n_dindexes, value);
             }
@@ -4159,6 +4705,47 @@ ctables_check_label_position (struct ctables_table *t, enum pivot_axis_type a)
   return true;
 }
 
+static size_t
+add_sum_var (struct variable *var,
+             struct variable ***sum_vars, size_t *n, size_t *allocated)
+{
+  for (size_t i = 0; i < *n; i++)
+    if (var == (*sum_vars)[i])
+      return i;
+
+  if (*n >= *allocated)
+    *sum_vars = x2nrealloc (*sum_vars, allocated, sizeof **sum_vars);
+  (*sum_vars)[*n] = var;
+  return (*n)++;
+}
+
+static void
+enumerate_sum_vars (const struct ctables_axis *a,
+                    struct variable ***sum_vars, size_t *n, size_t *allocated)
+{
+  if (!a)
+    return;
+
+  switch (a->op)
+    {
+    case CTAO_VAR:
+      for (size_t i = 0; i < N_CSVS; i++)
+        for (size_t j = 0; j < a->specs[i].n; j++)
+          {
+            struct ctables_summary_spec *spec = &a->specs[i].specs[j];
+            if (ctables_function_is_pctsum (spec->function))
+              spec->sum_var_idx = add_sum_var (a->var, sum_vars, n, allocated);
+          }
+      break;
+
+    case CTAO_STACK:
+    case CTAO_NEST:
+      for (size_t i = 0; i < 2; i++)
+        enumerate_sum_vars (a->subs[i], sum_vars, n, allocated);
+      break;
+    }
+}
+
 static bool
 ctables_prepare_table (struct ctables_table *t)
 {
@@ -4243,7 +4830,6 @@ ctables_prepare_table (struct ctables_table *t)
           *specs->specs = (struct ctables_summary_spec) {
             .function = function,
             .format = ctables_summary_default_format (function, specs->var),
-            .label = ctables_summary_default_label (function, 0),
           };
           if (!specs->var)
             specs->var = nest->vars[0];
@@ -4284,7 +4870,7 @@ ctables_prepare_table (struct ctables_table *t)
     }
 
   struct ctables_summary_spec_set *merged = &t->summary_specs;
-  struct merge_item *items = xnmalloc (2 * stack->n, sizeof *items);
+  struct merge_item *items = xnmalloc (N_CSVS * stack->n, sizeof *items);
   size_t n_left = 0;
   for (size_t j = 0; j < stack->n; j++)
     {
@@ -4340,6 +4926,10 @@ ctables_prepare_table (struct ctables_table *t)
     }
 #endif
 
+  size_t allocated_sum_vars = 0;
+  enumerate_sum_vars (t->axes[t->summary_axis],
+                      &t->sum_vars, &t->n_sum_vars, &allocated_sum_vars);
+
   return (ctables_check_label_position (t, PIVOT_AXIS_ROW)
           && ctables_check_label_position (t, PIVOT_AXIS_COLUMN));
 }
@@ -4628,6 +5218,11 @@ ctables_pcexpr_destroy (struct ctables_pcexpr *e)
           ss_dealloc (&e->string);
           break;
 
+        case CTPO_CAT_SRANGE:
+          for (size_t i = 0; i < 2; i++)
+            ss_dealloc (&e->srange[i]);
+          break;
+
         case CTPO_ADD:
         case CTPO_SUB:
         case CTPO_MUL:
@@ -4640,7 +5235,7 @@ ctables_pcexpr_destroy (struct ctables_pcexpr *e)
 
         case CTPO_CONSTANT:
         case CTPO_CAT_NUMBER:
-        case CTPO_CAT_RANGE:
+        case CTPO_CAT_NRANGE:
         case CTPO_CAT_MISSING:
         case CTPO_CAT_OTHERNM:
         case CTPO_CAT_SUBTOTAL:
@@ -4675,7 +5270,8 @@ struct operator
   };
 
 static const struct operator *
-match_operator (struct lexer *lexer, const struct operator ops[], size_t n_ops)
+ctable_pcexpr_match_operator (struct lexer *lexer,
+                              const struct operator ops[], size_t n_ops)
 {
   for (const struct operator *op = ops; op < ops + n_ops; op++)
     if (lex_token (lexer) == op->token)
@@ -4690,15 +5286,16 @@ match_operator (struct lexer *lexer, const struct operator ops[], size_t n_ops)
 }
 
 static struct ctables_pcexpr *
-parse_binary_operators__ (struct lexer *lexer, struct dictionary *dict,
-                          const struct operator ops[], size_t n_ops,
-                          parse_recursively_func *parse_next_level,
-                          const char *chain_warning,
-                          struct ctables_pcexpr *lhs)
+ctable_pcexpr_parse_binary_operators__ (
+  struct lexer *lexer, struct dictionary *dict,
+  const struct operator ops[], size_t n_ops,
+  parse_recursively_func *parse_next_level,
+  const char *chain_warning, struct ctables_pcexpr *lhs)
 {
   for (int op_count = 0; ; op_count++)
     {
-      const struct operator *op = match_operator (lexer, ops, n_ops);
+      const struct operator *op
+        = ctable_pcexpr_match_operator (lexer, ops, n_ops);
       if (!op)
         {
           if (op_count > 1 && chain_warning)
@@ -4719,32 +5316,35 @@ parse_binary_operators__ (struct lexer *lexer, struct dictionary *dict,
 }
 
 static struct ctables_pcexpr *
-parse_binary_operators (struct lexer *lexer, struct dictionary *dict,
-                        const struct operator ops[], size_t n_ops,
-                        parse_recursively_func *parse_next_level,
-                        const char *chain_warning)
+ctable_pcexpr_parse_binary_operators (struct lexer *lexer,
+                                      struct dictionary *dict,
+                                      const struct operator ops[], size_t n_ops,
+                                      parse_recursively_func *parse_next_level,
+                                      const char *chain_warning)
 {
   struct ctables_pcexpr *lhs = parse_next_level (lexer, dict);
   if (!lhs)
     return NULL;
 
-  return parse_binary_operators__ (lexer, dict, ops, n_ops, parse_next_level,
-                                   chain_warning, lhs);
+  return ctable_pcexpr_parse_binary_operators__ (lexer, dict, ops, n_ops,
+                                                 parse_next_level,
+                                                 chain_warning, lhs);
 }
 
-static struct ctables_pcexpr *parse_add (struct lexer *, struct dictionary *);
+static struct ctables_pcexpr *ctable_pcexpr_parse_add (struct lexer *,
+                                                       struct dictionary *);
 
 static struct ctables_pcexpr
-ctpo_cat_range (double low, double high)
+ctpo_cat_nrange (double low, double high)
 {
   return (struct ctables_pcexpr) {
-    .op = CTPO_CAT_RANGE,
-    .range = { low, high },
+    .op = CTPO_CAT_NRANGE,
+    .nrange = { low, high },
   };
 }
 
 static struct ctables_pcexpr *
-parse_primary (struct lexer *lexer, struct dictionary *dict)
+ctable_pcexpr_parse_primary (struct lexer *lexer, struct dictionary *dict)
 {
   int start_ofs = lex_ofs (lexer);
   struct ctables_pcexpr e;
@@ -4781,7 +5381,7 @@ parse_primary (struct lexer *lexer, struct dictionary *dict)
         {
           if (!lex_force_match_id (lexer, "THRU") || lex_force_num (lexer))
             return false;
-          e = ctpo_cat_range (-DBL_MAX, lex_number (lexer));
+          e = ctpo_cat_nrange (-DBL_MAX, lex_number (lexer));
           lex_get (lexer);
         }
       else if (lex_is_number (lexer))
@@ -4791,12 +5391,12 @@ parse_primary (struct lexer *lexer, struct dictionary *dict)
           if (lex_match_id (lexer, "THRU"))
             {
               if (lex_match_id (lexer, "HI"))
-                e = ctpo_cat_range (number, DBL_MAX);
+                e = ctpo_cat_nrange (number, DBL_MAX);
               else
                 {
                   if (!lex_force_num (lexer))
                     return false;
-                  e = ctpo_cat_range (number, lex_number (lexer));
+                  e = ctpo_cat_nrange (number, lex_number (lexer));
                   lex_get (lexer);
                 }
             }
@@ -4828,7 +5428,7 @@ parse_primary (struct lexer *lexer, struct dictionary *dict)
     }
   else if (lex_match (lexer, T_LPAREN))
     {
-      struct ctables_pcexpr *ep = parse_add (lexer, dict);
+      struct ctables_pcexpr *ep = ctable_pcexpr_parse_add (lexer, dict);
       if (!ep)
         return NULL;
       if (!lex_force_match (lexer, T_RPAREN))
@@ -4862,7 +5462,7 @@ ctables_pcexpr_allocate_neg (struct ctables_pcexpr *sub,
 }
 
 static struct ctables_pcexpr *
-parse_exp (struct lexer *lexer, struct dictionary *dict)
+ctable_pcexpr_parse_exp (struct lexer *lexer, struct dictionary *dict)
 {
   static const struct operator op = { T_EXP, CTPO_POW };
 
@@ -4872,8 +5472,9 @@ parse_exp (struct lexer *lexer, struct dictionary *dict)
       "To disable this warning, insert parentheses.");
 
   if (lex_token (lexer) != T_NEG_NUM || lex_next_token (lexer, 1) != T_EXP)
-    return parse_binary_operators (lexer, dict, &op, 1,
-                                   parse_primary, chain_warning);
+    return ctable_pcexpr_parse_binary_operators (lexer, dict, &op, 1,
+                                                 ctable_pcexpr_parse_primary,
+                                                 chain_warning);
 
   /* Special case for situations like "-5**6", which must be parsed as
      -(5**6). */
@@ -4887,8 +5488,9 @@ parse_exp (struct lexer *lexer, struct dictionary *dict)
   };
   lex_get (lexer);
 
-  struct ctables_pcexpr *node = parse_binary_operators__ (
-    lexer, dict, &op, 1, parse_primary, chain_warning, lhs);
+  struct ctables_pcexpr *node = ctable_pcexpr_parse_binary_operators__ (
+    lexer, dict, &op, 1,
+    ctable_pcexpr_parse_primary, chain_warning, lhs);
   if (!node)
     return NULL;
 
@@ -4897,13 +5499,13 @@ parse_exp (struct lexer *lexer, struct dictionary *dict)
 
 /* Parses the unary minus level. */
 static struct ctables_pcexpr *
-parse_neg (struct lexer *lexer, struct dictionary *dict)
+ctable_pcexpr_parse_neg (struct lexer *lexer, struct dictionary *dict)
 {
   int start_ofs = lex_ofs (lexer);
   if (!lex_match (lexer, T_DASH))
-    return parse_exp (lexer, dict);
+    return ctable_pcexpr_parse_exp (lexer, dict);
 
-  struct ctables_pcexpr *inner = parse_neg (lexer, dict);
+  struct ctables_pcexpr *inner = ctable_pcexpr_parse_neg (lexer, dict);
   if (!inner)
     return NULL;
 
@@ -4912,7 +5514,7 @@ parse_neg (struct lexer *lexer, struct dictionary *dict)
 
 /* Parses the multiplication and division level. */
 static struct ctables_pcexpr *
-parse_mul (struct lexer *lexer, struct dictionary *dict)
+ctable_pcexpr_parse_mul (struct lexer *lexer, struct dictionary *dict)
 {
   static const struct operator ops[] =
     {
@@ -4920,13 +5522,14 @@ parse_mul (struct lexer *lexer, struct dictionary *dict)
       { T_SLASH, CTPO_DIV },
     };
 
-  return parse_binary_operators (lexer, dict, ops, sizeof ops / sizeof *ops,
-                                 parse_neg, NULL);
+  return ctable_pcexpr_parse_binary_operators (lexer, dict, ops,
+                                               sizeof ops / sizeof *ops,
+                                               ctable_pcexpr_parse_neg, NULL);
 }
 
 /* Parses the addition and subtraction level. */
 static struct ctables_pcexpr *
-parse_add (struct lexer *lexer, struct dictionary *dict)
+ctable_pcexpr_parse_add (struct lexer *lexer, struct dictionary *dict)
 {
   static const struct operator ops[] =
     {
@@ -4935,8 +5538,9 @@ parse_add (struct lexer *lexer, struct dictionary *dict)
       { T_NEG_NUM, CTPO_ADD },
     };
 
-  return parse_binary_operators (lexer, dict, ops, sizeof ops / sizeof *ops,
-                                 parse_mul, NULL);
+  return ctable_pcexpr_parse_binary_operators (lexer, dict,
+                                               ops, sizeof ops / sizeof *ops,
+                                               ctable_pcexpr_parse_mul, NULL);
 }
 
 static struct ctables_postcompute *
@@ -4971,7 +5575,7 @@ ctables_parse_pcompute (struct lexer *lexer, struct dictionary *dict,
     }
 
   int expr_start = lex_ofs (lexer);
-  struct ctables_pcexpr *expr = parse_add (lexer, dict);
+  struct ctables_pcexpr *expr = ctable_pcexpr_parse_add (lexer, dict);
   int expr_end = lex_ofs (lexer) - 1;
   if (!expr || !lex_force_match (lexer, T_RPAREN))
     {
@@ -5038,9 +5642,8 @@ ctables_parse_pproperties_format (struct lexer *lexer,
 
       /* Parse format. */
       struct fmt_spec format;
-      if (!parse_format_specifier (lexer, &format)
-          || !fmt_check_output (&format)
-          || !fmt_check_type_compat (&format, VAL_NUMERIC))
+      bool is_ctables_format;
+      if (!parse_ctables_format_specifier (lexer, &format, &is_ctables_format))
         goto error;
 
       if (sss->n >= sss->allocated)
@@ -5050,6 +5653,7 @@ ctables_parse_pproperties_format (struct lexer *lexer,
         .function = function,
         .percentile = percentile,
         .format = format,
+        .is_ctables_format = is_ctables_format,
       };
     }
   return true;