+/* PSPP - a program for statistical analysis.
+ Copyright (C) 2017, 2018 Free Software Foundation, Inc.
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+#include <config.h>
+
+#include "output/spv/spv-legacy-decoder.h"
+
+#include <errno.h>
+#include <inttypes.h>
+#include <math.h>
+#include <limits.h>
+#include <stdlib.h>
+
+#include "data/data-out.h"
+#include "data/calendar.h"
+#include "data/format.h"
+#include "data/value.h"
+#include "libpspp/assertion.h"
+#include "libpspp/hash-functions.h"
+#include "libpspp/hmap.h"
+#include "libpspp/message.h"
+#include "output/pivot-table.h"
+#include "output/spv/detail-xml-parser.h"
+#include "output/spv/spv-legacy-data.h"
+#include "output/spv/spv.h"
+#include "output/spv/structure-xml-parser.h"
+
+#include "gl/c-strtod.h"
+#include "gl/xalloc.h"
+#include "gl/xmemdup0.h"
+
+#include <libxml/tree.h>
+
+#include "gettext.h"
+#define N_(msgid) msgid
+#define _(msgid) gettext (msgid)
+
+struct spv_legacy_properties
+ {
+ /* General properties. */
+ bool omit_empty;
+ int width_ranges[TABLE_N_AXES][2]; /* In 1/96" units. */
+ bool row_labels_in_corner;
+
+ /* Footnote display settings. */
+ bool show_numeric_markers;
+ bool footnote_marker_superscripts;
+
+ /* Styles. */
+ struct area_style areas[PIVOT_N_AREAS];
+ struct table_border_style borders[PIVOT_N_BORDERS];
+
+ /* Print settings. */
+ bool print_all_layers;
+ bool paginate_layers;
+ bool shrink_to_width;
+ bool shrink_to_length;
+ bool top_continuation, bottom_continuation;
+ char *continuation;
+ size_t n_orphan_lines;
+ };
+
+struct spv_series
+ {
+ struct hmap_node hmap_node; /* By name. */
+ char *name;
+ char *label;
+ struct fmt_spec format;
+
+ struct spv_series *label_series;
+ bool is_label_series;
+
+ const struct spvxml_node *xml;
+
+ struct spv_data_value *values;
+ size_t n_values;
+ struct hmap map; /* Contains "struct spv_mapping". */
+ bool remapped;
+
+ struct pivot_dimension *dimension;
+
+ struct pivot_category **index_to_category;
+ size_t n_index;
+
+ struct spvdx_affix **affixes;
+ size_t n_affixes;
+ };
+
+static void spv_map_destroy (struct hmap *);
+
+static struct spv_series *
+spv_series_first (struct hmap *series_map)
+{
+ struct spv_series *series;
+ HMAP_FOR_EACH (series, struct spv_series, hmap_node, series_map)
+ return series;
+ return NULL;
+}
+
+static struct spv_series *
+spv_series_find (const struct hmap *series_map, const char *name)
+{
+ struct spv_series *series;
+ HMAP_FOR_EACH_WITH_HASH (series, struct spv_series, hmap_node,
+ hash_string (name, 0), series_map)
+ if (!strcmp (name, series->name))
+ return series;
+ return NULL;
+}
+
+static struct spv_series *
+spv_series_from_ref (const struct hmap *series_map,
+ const struct spvxml_node *ref)
+{
+ const struct spvxml_node *node
+ = (spvdx_is_source_variable (ref)
+ ? &spvdx_cast_source_variable (ref)->node_
+ : &spvdx_cast_derived_variable (ref)->node_);
+ struct spv_series *series = spv_series_find (series_map, node->id);
+ if (!series)
+ printf ("missing series %s\n", node->id);
+ return series;
+}
+
+static void UNUSED
+spv_series_dump (const struct spv_series *series)
+{
+ printf ("series \"%s\"", series->name);
+ if (series->label)
+ printf (" (label \"%s\")", series->label);
+ printf (", %zu values:", series->n_values);
+ for (size_t i = 0; i < series->n_values; i++)
+ {
+ putchar (' ');
+ spv_data_value_dump (&series->values[i], stdout);
+ }
+ putchar ('\n');
+}
+
+static void
+spv_series_destroy (struct hmap *series_map)
+{
+ struct spv_series *series, *next_series;
+ HMAP_FOR_EACH_SAFE (series, next_series, struct spv_series, hmap_node,
+ series_map)
+ {
+ free (series->name);
+ free (series->label);
+
+ for (size_t i = 0; i < series->n_values; i++)
+ spv_data_value_uninit (&series->values[i]);
+ free (series->values);
+
+ spv_map_destroy (&series->map);
+
+ free (series->index_to_category);
+
+ hmap_delete (series_map, &series->hmap_node);
+ free (series);
+ }
+ hmap_destroy (series_map);
+}
+
+struct spv_mapping
+ {
+ struct hmap_node hmap_node;
+ double from;
+ struct spv_data_value to;
+ };
+
+static struct spv_mapping *
+spv_map_search (const struct hmap *map, double from)
+{
+ struct spv_mapping *mapping;
+ HMAP_FOR_EACH_WITH_HASH (mapping, struct spv_mapping, hmap_node,
+ hash_double (from, 0), map)
+ if (mapping->from == from)
+ return mapping;
+ return NULL;
+}
+
+static const struct spv_data_value *
+spv_map_lookup (const struct hmap *map, const struct spv_data_value *in)
+{
+ if (in->width >= 0)
+ return in;
+
+ const struct spv_mapping *m = spv_map_search (map, in->d);
+ return m ? &m->to : in;
+}
+
+static bool
+parse_real (const char *s, double *real)
+{
+ int save_errno = errno;
+ errno = 0;
+ char *end;
+ *real = c_strtod (s, &end);
+ bool ok = !errno && end > s && !*end;
+ errno = save_errno;
+
+ return ok;
+}
+
+static char * WARN_UNUSED_RESULT
+spv_map_insert (struct hmap *map, double from, const char *to,
+ bool try_strings_as_numbers, const struct fmt_spec *format)
+{
+ struct spv_mapping *mapping = spv_map_search (map, from);
+
+ if (mapping)
+ return xasprintf ("Duplicate relabeling for from=\"%.*g\"",
+ DBL_DIG + 1, from);
+ mapping = xmalloc (sizeof *mapping);
+ mapping->from = from;
+
+ if ((try_strings_as_numbers || (format && fmt_is_numeric (format->type)))
+ && parse_real (to, &mapping->to.d))
+ {
+ if (try_strings_as_numbers)
+ mapping->to.width = -1;
+ else
+ {
+ union value v = { .f = mapping->to.d };
+ mapping->to.s = data_out_stretchy (&v, NULL, format, NULL);
+ mapping->to.width = strlen (mapping->to.s);
+ }
+ }
+ else
+ {
+ mapping->to.width = strlen (to);
+ mapping->to.s = xstrdup (to);
+ }
+ hmap_insert (map, &mapping->hmap_node, hash_double (from, 0));
+ return NULL;
+}
+
+static void
+spv_map_destroy (struct hmap *map)
+{
+ struct spv_mapping *mapping, *next;
+ HMAP_FOR_EACH_SAFE (mapping, next, struct spv_mapping, hmap_node, map)
+ {
+ spv_data_value_uninit (&mapping->to);
+ hmap_delete (map, &mapping->hmap_node);
+ free (mapping);
+ }
+ hmap_destroy (map);
+}
+
+static char * WARN_UNUSED_RESULT
+spv_series_parse_relabels (struct hmap *map,
+ struct spvdx_relabel **relabels, size_t n_relabels,
+ bool try_strings_as_numbers,
+ const struct fmt_spec *format)
+{
+ for (size_t i = 0; i < n_relabels; i++)
+ {
+ const struct spvdx_relabel *relabel = relabels[i];
+ char *error = spv_map_insert (map, relabel->from, relabel->to,
+ try_strings_as_numbers, format);
+ if (error)
+ return error;
+ }
+ return NULL;
+}
+
+static char * WARN_UNUSED_RESULT
+spv_series_parse_value_map_entry (struct hmap *map,
+ const struct spvdx_value_map_entry *vme)
+{
+ for (const char *p = vme->from; ; p++)
+ {
+ int save_errno = errno;
+ errno = 0;
+ char *end;
+ double from = c_strtod (p, &end);
+ bool ok = !errno && end > p && strchr (";", *end);
+ errno = save_errno;
+ if (!ok)
+ return xasprintf ("Syntax error in valueMapEntry from=\"%s\".",
+ vme->from);
+
+ char *error = spv_map_insert (map, from, vme->to, true,
+ &(struct fmt_spec) { FMT_A, 40, 0 });
+ if (error)
+ return error;
+
+ p = end;
+ if (*p == '\0')
+ return NULL;
+ assert (*p == ';');
+ }
+}
+
+static struct fmt_spec
+decode_date_time_format (const struct spvdx_date_time_format *dtf)
+{
+ if (dtf->dt_base_format == SPVDX_DT_BASE_FORMAT_DATE)
+ {
+ enum fmt_type type
+ = (dtf->show_quarter > 0 ? FMT_QYR
+ : dtf->show_week > 0 ? FMT_WKYR
+ : dtf->mdy_order == SPVDX_MDY_ORDER_DAY_MONTH_YEAR
+ ? (dtf->month_format == SPVDX_MONTH_FORMAT_NUMBER
+ || dtf->month_format == SPVDX_MONTH_FORMAT_PADDED_NUMBER
+ ? FMT_EDATE : FMT_DATE)
+ : dtf->mdy_order == SPVDX_MDY_ORDER_YEAR_MONTH_DAY ? FMT_SDATE
+ : FMT_ADATE);
+
+ int w = fmt_min_output_width (type);
+ if (dtf->year_abbreviation <= 0)
+ w += 2;
+ return (struct fmt_spec) { .type = type, .w = w };
+ }
+ else
+ {
+ enum fmt_type type
+ = (dtf->dt_base_format == SPVDX_DT_BASE_FORMAT_DATE_TIME
+ ? (dtf->mdy_order == SPVDX_MDY_ORDER_YEAR_MONTH_DAY
+ ? FMT_YMDHMS
+ : FMT_DATETIME)
+ : (dtf->show_day > 0 ? FMT_DTIME
+ : dtf->show_hour > 0 ? FMT_TIME
+ : FMT_MTIME));
+ int w = fmt_min_output_width (type);
+ int d = 0;
+ if (dtf->show_second > 0)
+ {
+ w += 3;
+ if (dtf->show_millis > 0)
+ {
+ d = 3;
+ w += d + 1;
+ }
+ }
+ return (struct fmt_spec) { .type = type, .w = w, .d = d };
+ }
+}
+
+static struct fmt_spec
+decode_elapsed_time_format (const struct spvdx_elapsed_time_format *etf)
+{
+ enum fmt_type type
+ = (etf->dt_base_format != SPVDX_DT_BASE_FORMAT_TIME ? FMT_DTIME
+ : etf->show_hour > 0 ? FMT_TIME
+ : FMT_MTIME);
+ int w = fmt_min_output_width (type);
+ int d = 0;
+ if (etf->show_second > 0)
+ {
+ w += 3;
+ if (etf->show_millis > 0)
+ {
+ d = 3;
+ w += d + 1;
+ }
+ }
+ return (struct fmt_spec) { .type = type, .w = w, .d = d };
+}
+
+static struct fmt_spec
+decode_number_format (const struct spvdx_number_format *nf)
+{
+ enum fmt_type type = (nf->scientific == SPVDX_SCIENTIFIC_TRUE ? FMT_E
+ : nf->prefix && !strcmp (nf->prefix, "$") ? FMT_DOLLAR
+ : nf->suffix && !strcmp (nf->suffix, "%") ? FMT_PCT
+ : nf->use_grouping ? FMT_COMMA
+ : FMT_F);
+
+ int d = nf->maximum_fraction_digits;
+ if (d < 0 || d > 15)
+ d = 2;
+
+ struct fmt_spec f = (struct fmt_spec) { type, 40, d };
+ fmt_fix_output (&f);
+ return f;
+}
+
+/* Returns an *approximation* of IN as a fmt_spec.
+
+ Not for use with string formats, which don't have any options anyway. */
+static struct fmt_spec
+decode_format (const struct spvdx_format *in)
+{
+ if (in->f_base_format == SPVDX_F_BASE_FORMAT_DATE ||
+ in->f_base_format == SPVDX_F_BASE_FORMAT_TIME ||
+ in->f_base_format == SPVDX_F_BASE_FORMAT_DATE_TIME)
+ {
+ struct spvdx_date_time_format dtf = {
+ .dt_base_format = (in->f_base_format == SPVDX_F_BASE_FORMAT_DATE
+ ? SPVDX_DT_BASE_FORMAT_DATE
+ : in->f_base_format == SPVDX_F_BASE_FORMAT_TIME
+ ? SPVDX_DT_BASE_FORMAT_TIME
+ : SPVDX_DT_BASE_FORMAT_DATE_TIME),
+ .separator_chars = in->separator_chars,
+ .mdy_order = in->mdy_order,
+ .show_year = in->show_year,
+ .year_abbreviation = in->year_abbreviation,
+ .show_quarter = in->show_quarter,
+ .quarter_prefix = in->quarter_prefix,
+ .quarter_suffix = in->quarter_suffix,
+ .show_month = in->show_month,
+ .month_format = in->month_format,
+ .show_week = in->show_week,
+ .week_padding = in->week_padding,
+ .week_suffix = in->week_suffix,
+ .show_day_of_week = in->show_day_of_week,
+ .day_of_week_abbreviation = in->day_of_week_abbreviation,
+ .day_padding = in->day_padding,
+ .day_of_month_padding = in->day_of_month_padding,
+ .hour_padding = in->hour_padding,
+ .minute_padding = in->minute_padding,
+ .second_padding = in->second_padding,
+ .show_day = in->show_day,
+ .show_hour = in->show_hour,
+ .show_minute = in->show_minute,
+ .show_second = in->show_second,
+ .show_millis = in->show_millis,
+ .day_type = in->day_type,
+ .hour_format = in->hour_format,
+ };
+ return decode_date_time_format (&dtf);
+ }
+ else if (in->f_base_format == SPVDX_F_BASE_FORMAT_ELAPSED_TIME)
+ {
+ struct spvdx_elapsed_time_format etf = {
+ .dt_base_format = (in->f_base_format == SPVDX_F_BASE_FORMAT_DATE
+ ? SPVDX_DT_BASE_FORMAT_DATE
+ : in->f_base_format == SPVDX_F_BASE_FORMAT_TIME
+ ? SPVDX_DT_BASE_FORMAT_TIME
+ : SPVDX_DT_BASE_FORMAT_DATE_TIME),
+ .day_padding = in->day_padding,
+ .minute_padding = in->minute_padding,
+ .second_padding = in->second_padding,
+ .show_year = in->show_year,
+ .show_day = in->show_day,
+ .show_hour = in->show_hour,
+ .show_minute = in->show_minute,
+ .show_second = in->show_second,
+ .show_millis = in->show_millis,
+ };
+ return decode_elapsed_time_format (&etf);
+ }
+ else
+ {
+ assert (!in->f_base_format);
+ struct spvdx_number_format nf = {
+ .minimum_integer_digits = in->minimum_integer_digits,
+ .maximum_fraction_digits = in->maximum_fraction_digits,
+ .minimum_fraction_digits = in->minimum_fraction_digits,
+ .use_grouping = in->use_grouping,
+ .scientific = in->scientific,
+ .small = in->small,
+ .prefix = in->prefix,
+ .suffix = in->suffix,
+ };
+ return decode_number_format (&nf);
+ }
+}
+
+static void
+spv_series_execute_mapping (struct spv_series *series)
+{
+ if (!hmap_is_empty (&series->map))
+ {
+ series->remapped = true;
+ for (size_t i = 0; i < series->n_values; i++)
+ {
+ struct spv_data_value *value = &series->values[i];
+ if (value->width >= 0)
+ continue;
+
+ const struct spv_mapping *mapping = spv_map_search (&series->map,
+ value->d);
+ if (mapping)
+ {
+ value->index = value->d;
+ assert (value->index == floor (value->index));
+ value->width = mapping->to.width;
+ if (value->width >= 0)
+ value->s = xmemdup0 (mapping->to.s, mapping->to.width);
+ else
+ value->d = mapping->to.d;
+ }
+ }
+ }
+}
+
+static char * WARN_UNUSED_RESULT
+spv_series_remap_formats (struct spv_series *series,
+ struct spvxml_node **seq, size_t n_seq)
+{
+ spv_map_destroy (&series->map);
+ hmap_init (&series->map);
+ for (size_t i = 0; i < n_seq; i++)
+ {
+ struct spvxml_node *node = seq[i];
+ if (spvdx_is_format (node))
+ {
+ struct spvdx_format *f = spvdx_cast_format (node);
+ series->format = decode_format (f);
+ char *error = spv_series_parse_relabels (
+ &series->map, f->relabel, f->n_relabel,
+ f->try_strings_as_numbers > 0, &series->format);
+ if (error)
+ return error;
+
+ series->affixes = f->affix;
+ series->n_affixes = f->n_affix;
+ }
+ else if (spvdx_is_string_format (node))
+ {
+ struct spvdx_string_format *sf = spvdx_cast_string_format (node);
+ char *error = spv_series_parse_relabels (&series->map,
+ sf->relabel, sf->n_relabel,
+ false, NULL);
+ if (error)
+ return error;
+
+ series->affixes = sf->affix;
+ series->n_affixes = sf->n_affix;
+ }
+ else
+ NOT_REACHED ();
+ }
+ spv_series_execute_mapping (series);
+ return NULL;
+}
+
+static char * WARN_UNUSED_RESULT
+spv_series_remap_vmes (struct spv_series *series,
+ struct spvdx_value_map_entry **vmes,
+ size_t n_vmes)
+{
+ spv_map_destroy (&series->map);
+ hmap_init (&series->map);
+ for (size_t i = 0; i < n_vmes; i++)
+ {
+ char *error = spv_series_parse_value_map_entry (&series->map, vmes[i]);
+ if (error)
+ return error;
+ }
+ spv_series_execute_mapping (series);
+ return NULL;
+}
+
+static void
+decode_footnotes (struct pivot_table *table, const struct spvdx_footnotes *f)
+{
+ if (f->n_footnote_mapping > 0)
+ pivot_table_create_footnote__ (table, f->n_footnote_mapping - 1,
+ NULL, NULL);
+ for (size_t i = 0; i < f->n_footnote_mapping; i++)
+ {
+ const struct spvdx_footnote_mapping *fm = f->footnote_mapping[i];
+ pivot_table_create_footnote__ (table, fm->defines_reference - 1,
+ pivot_value_new_user_text (fm->to, -1),
+ NULL);
+ }
+}
+
+static struct cell_color
+optional_color (int color, struct cell_color default_color)
+{
+ return (color >= 0
+ ? (struct cell_color) CELL_COLOR (color >> 16, color >> 8, color)
+ : default_color);
+}
+
+static int
+optional_length (const char *s, int default_length)
+{
+ /* There is usually a "pt" suffix. We ignore it. */
+ int length;
+ return s && sscanf (s, "%d", &length) == 1 ? length : default_length;
+}
+
+static int
+optional_px (double inches, int default_px)
+{
+ return inches != DBL_MAX ? inches * 96.0 : default_px;
+}
+
+static int
+optional_pt (double inches, int default_pt)
+{
+ return inches != DBL_MAX ? inches * 72.0 + .5 : default_pt;
+}
+
+static void
+decode_spvdx_style_incremental (const struct spvdx_style *in,
+ const struct spvdx_style *bg,
+ struct area_style *out)
+{
+ if (in && in->font_weight)
+ out->font_style.bold = in->font_weight == SPVDX_FONT_WEIGHT_BOLD;
+ if (in && in->font_style)
+ out->font_style.italic = in->font_style == SPVDX_FONT_STYLE_ITALIC;
+ if (in && in->font_underline)
+ out->font_style.underline = in->font_underline == SPVDX_FONT_UNDERLINE_UNDERLINE;
+ if (in && in->color >= 0)
+ {
+ out->font_style.fg[0] = optional_color (
+ in->color, (struct cell_color) CELL_COLOR_BLACK);
+ out->font_style.fg[1] = out->font_style.fg[0];
+ }
+ if (bg && bg->color >= 0)
+ {
+ out->font_style.bg[0] = optional_color (
+ bg->color, (struct cell_color) CELL_COLOR_WHITE);
+ out->font_style.bg[1] = out->font_style.bg[0];
+ }
+ if (in && in->font_family)
+ {
+ free (out->font_style.typeface);
+ out->font_style.typeface = xstrdup (in->font_family);
+ }
+ if (in && in->font_size)
+ {
+ int size = optional_length (in->font_size, 0);
+ if (size)
+ out->font_style.size = size;
+ }
+ if (in && in->text_alignment)
+ out->cell_style.halign
+ = (in->text_alignment == SPVDX_TEXT_ALIGNMENT_LEFT
+ ? TABLE_HALIGN_LEFT
+ : in->text_alignment == SPVDX_TEXT_ALIGNMENT_RIGHT
+ ? TABLE_HALIGN_RIGHT
+ : in->text_alignment == SPVDX_TEXT_ALIGNMENT_CENTER
+ ? TABLE_HALIGN_CENTER
+ : in->text_alignment == SPVDX_TEXT_ALIGNMENT_DECIMAL
+ ? TABLE_HALIGN_DECIMAL
+ : TABLE_HALIGN_MIXED);
+ if (in && in->label_location_vertical)
+ out->cell_style.valign =
+ (in->label_location_vertical == SPVDX_LABEL_LOCATION_VERTICAL_NEGATIVE
+ ? TABLE_VALIGN_BOTTOM
+ : in->label_location_vertical == SPVDX_LABEL_LOCATION_VERTICAL_POSITIVE
+ ? TABLE_VALIGN_TOP
+ : TABLE_VALIGN_CENTER);
+ if (in && in->decimal_offset != DBL_MAX)
+ out->cell_style.decimal_offset = optional_px (in->decimal_offset, 0);
+#if 0
+ if (in && in->margin_left != DBL_MAX)
+ out->cell_style.margin[TABLE_HORZ][0] = optional_pt (in->margin_left, 8);
+ if (in && in->margin_right != DBL_MAX)
+ out->cell_style.margin[TABLE_HORZ][1] = optional_pt (in->margin_right, 11);
+ if (in && in->margin_top != DBL_MAX)
+ out->cell_style.margin[TABLE_VERT][0] = optional_pt (in->margin_top, 1);
+ if (in && in->margin_bottom != DBL_MAX)
+ out->cell_style.margin[TABLE_VERT][1] = optional_pt (in->margin_bottom, 1);
+#endif
+}
+
+static void
+decode_spvdx_style (const struct spvdx_style *in,
+ const struct spvdx_style *bg,
+ struct area_style *out)
+{
+ *out = (struct area_style) AREA_STYLE_INITIALIZER;
+ decode_spvdx_style_incremental (in, bg, out);
+}
+
+static void
+add_footnote (struct pivot_value *v, int idx, struct pivot_table *table)
+{
+ if (idx < 1 || idx > table->n_footnotes)
+ return;
+
+ pivot_value_add_footnote (v, table->footnotes[idx - 1]);
+}
+
+static char * WARN_UNUSED_RESULT
+decode_label_frame (struct pivot_table *table,
+ const struct spvdx_label_frame *lf)
+{
+ if (!lf->label)
+ return NULL;
+
+ struct pivot_value **target;
+ struct area_style *area;
+ if (lf->label->purpose == SPVDX_PURPOSE_TITLE)
+ {
+ target = &table->title;
+ area = &table->areas[PIVOT_AREA_TITLE];
+ }
+ else if (lf->label->purpose == SPVDX_PURPOSE_SUB_TITLE)
+ {
+ target = &table->caption;
+ area = &table->areas[PIVOT_AREA_CAPTION];
+ }
+ else if (lf->label->purpose == SPVDX_PURPOSE_FOOTNOTE)
+ {
+ if (lf->label->n_text > 0
+ && lf->label->text[0]->uses_reference != INT_MIN)
+ {
+ target = NULL;
+ area = &table->areas[PIVOT_AREA_FOOTER];
+ }
+ else
+ return NULL;
+ }
+ else if (lf->label->purpose == SPVDX_PURPOSE_LAYER)
+ {
+ target = NULL;
+ area = &table->areas[PIVOT_AREA_LAYERS];
+ }
+ else
+ return NULL;
+
+ area_style_uninit (area);
+ decode_spvdx_style (lf->label->style, lf->label->text_frame_style, area);
+
+ if (target)
+ {
+ struct pivot_value *value = xzalloc (sizeof *value);
+ value->type = PIVOT_VALUE_TEXT;
+ for (size_t i = 0; i < lf->label->n_text; i++)
+ {
+ const struct spvdx_text *in = lf->label->text[i];
+ if (in->defines_reference != INT_MIN)
+ add_footnote (value, in->defines_reference, table);
+ else if (!value->text.local)
+ value->text.local = xstrdup (in->text);
+ else
+ {
+ char *new = xasprintf ("%s%s", value->text.local, in->text);
+ free (value->text.local);
+ value->text.local = new;
+ }
+ }
+ pivot_value_destroy (*target);
+ *target = value;
+ }
+ else
+ for (size_t i = 0; i < lf->label->n_text; i++)
+ {
+ const struct spvdx_text *in = lf->label->text[i];
+ if (in->uses_reference == INT_MIN)
+ continue;
+ if (i % 2)
+ {
+ size_t length = strlen (in->text);
+ if (length && in->text[length - 1] == '\n')
+ length--;
+
+ pivot_table_create_footnote__ (
+ table, in->uses_reference - 1, NULL,
+ pivot_value_new_user_text (in->text, length));
+ }
+ else
+ {
+ size_t length = strlen (in->text);
+ if (length && in->text[length - 1] == '.')
+ length--;
+
+ pivot_table_create_footnote__ (
+ table, in->uses_reference - 1,
+ pivot_value_new_user_text (in->text, length), NULL);
+ }
+ }
+ return NULL;
+}
+
+/* Special return value for decode_spvdx_variable(). */
+static char BAD_REFERENCE;
+
+static char * WARN_UNUSED_RESULT
+decode_spvdx_source_variable (const struct spvxml_node *node,
+ struct spv_data *data,
+ struct hmap *series_map)
+{
+ const struct spvdx_source_variable *sv = spvdx_cast_source_variable (node);
+
+ struct spv_series *label_series = NULL;
+ if (sv->label_variable)
+ {
+ label_series = spv_series_find (series_map,
+ sv->label_variable->node_.id);
+ if (!label_series)
+ return &BAD_REFERENCE;
+
+ label_series->is_label_series = true;
+ }
+
+ const struct spv_data_variable *var = spv_data_find_variable (
+ data, sv->source, sv->source_name);
+ if (!var)
+ return xasprintf ("sourceVariable %s references nonexistent "
+ "source %s variable %s.",
+ sv->node_.id, sv->source, sv->source_name);
+
+ struct spv_series *s = xzalloc (sizeof *s);
+ s->name = xstrdup (node->id);
+ s->xml = node;
+ s->label = sv->label ? xstrdup (sv->label) : NULL;
+ s->label_series = label_series;
+ s->values = spv_data_values_clone (var->values, var->n_values);
+ s->n_values = var->n_values;
+ s->format = F_8_0;
+ hmap_init (&s->map);
+ hmap_insert (series_map, &s->hmap_node, hash_string (s->name, 0));
+
+ char *error = spv_series_remap_formats (s, sv->seq, sv->n_seq);
+ if (error)
+ return error;
+
+ if (label_series && !s->remapped)
+ {
+ for (size_t i = 0; i < s->n_values; i++)
+ if (s->values[i].width < 0)
+ {
+ char *dest;
+ if (label_series->values[i].width < 0)
+ {
+ union value v = { .f = label_series->values[i].d };
+ dest = data_out_stretchy (&v, "UTF-8", &s->format, NULL);
+ }
+ else
+ dest = label_series->values[i].s;
+ char *error = spv_map_insert (&s->map, s->values[i].d,
+ dest, false, NULL);
+ free (error); /* Duplicates are OK. */
+ if (label_series->values[i].width < 0)
+ free (dest);
+ }
+ }
+
+ return NULL;
+}
+
+static char * WARN_UNUSED_RESULT
+decode_spvdx_derived_variable (const struct spvxml_node *node,
+ struct hmap *series_map)
+{
+ const struct spvdx_derived_variable *dv = spvdx_cast_derived_variable (node);
+
+ struct spv_data_value *values;
+ size_t n_values;
+
+ struct substring value = ss_cstr (dv->value);
+ if (ss_equals (value, ss_cstr ("constant(0)")))
+ {
+ struct spv_series *existing_series = spv_series_first (series_map);
+ if (!existing_series)
+ return &BAD_REFERENCE;
+
+ n_values = existing_series->n_values;
+ values = xcalloc (n_values, sizeof *values);
+ for (size_t i = 0; i < n_values; i++)
+ values[i].width = -1;
+ }
+ else if (ss_starts_with (value, ss_cstr ("constant(")))
+ {
+ values = NULL;
+ n_values = 0;
+ }
+ else if (ss_starts_with (value, ss_cstr ("map("))
+ && ss_ends_with (value, ss_cstr (")")))
+ {
+ char *dependency_name = ss_xstrdup (ss_substr (value, 4,
+ value.length - 5));
+ struct spv_series *dependency
+ = spv_series_find (series_map, dependency_name);
+ free (dependency_name);
+ if (!dependency)
+ return &BAD_REFERENCE;
+
+ values = spv_data_values_clone (dependency->values,
+ dependency->n_values);
+ n_values = dependency->n_values;
+ }
+ else
+ return xasprintf ("Derived variable %s has unknown value \"%s\"",
+ node->id, dv->value);
+
+ struct spv_series *s = xzalloc (sizeof *s);
+ s->format = F_8_0;
+ s->name = xstrdup (node->id);
+ s->values = values;
+ s->n_values = n_values;
+ hmap_init (&s->map);
+ hmap_insert (series_map, &s->hmap_node, hash_string (s->name, 0));
+
+ char *error = spv_series_remap_vmes (s, dv->value_map_entry,
+ dv->n_value_map_entry);
+ if (error)
+ return error;
+
+ error = spv_series_remap_formats (s, dv->seq, dv->n_seq);
+ if (error)
+ return error;
+
+ if (n_values > 0)
+ {
+ for (size_t i = 0; i < n_values; i++)
+ if (values[i].width != 0)
+ goto nonempty;
+ for (size_t i = 0; i < n_values; i++)
+ spv_data_value_uninit (&s->values[i]);
+ free (s->values);
+
+ s->values = NULL;
+ s->n_values = 0;
+
+ nonempty:;
+ }
+ return NULL;
+}
+
+struct format_mapping
+ {
+ struct hmap_node hmap_node;
+ uint32_t from;
+ struct fmt_spec to;
+ };
+
+static const struct format_mapping *
+format_map_find (const struct hmap *format_map, uint32_t u32_format)
+{
+ if (format_map)
+ {
+ const struct format_mapping *fm;
+ HMAP_FOR_EACH_IN_BUCKET (fm, struct format_mapping, hmap_node,
+ hash_int (u32_format, 0), format_map)
+ if (fm->from == u32_format)
+ return fm;
+ }
+
+ return NULL;
+}
+
+static struct fmt_spec
+spv_format_from_data_value (const struct spv_data_value *data,
+ const struct hmap *format_map)
+{
+ if (!data)
+ return fmt_for_output (FMT_F, 40, 2);
+
+ uint32_t u32_format = data->width < 0 ? data->d : atoi (data->s);
+ const struct format_mapping *fm = format_map_find (format_map, u32_format);
+ return fm ? fm->to : spv_decode_fmt_spec (u32_format);
+}
+
+static struct pivot_value *
+pivot_value_from_data_value (const struct spv_data_value *data,
+ const struct spv_data_value *format,
+ const struct hmap *format_map)
+{
+ struct pivot_value *v = xzalloc (sizeof *v);
+ struct fmt_spec f = spv_format_from_data_value (format, format_map);
+ if (data->width >= 0)
+ {
+ if (format && fmt_get_category (f.type) == FMT_CAT_DATE)
+ {
+ int year, month, day, hour, minute, second, msec, len = -1;
+ if (sscanf (data->s, "%4d-%2d-%2dT%2d:%2d:%2d.%3d%n",
+ &year, &month, &day, &hour, &minute, &second,
+ &msec, &len) == 7
+ && len == 23
+ && data->s[len] == '\0')
+ {
+ double date = calendar_gregorian_to_offset (year, month, day,
+ NULL);
+ if (date != SYSMIS)
+ {
+ v->type = PIVOT_VALUE_NUMERIC;
+ v->numeric.x = (date * 60. * 60. * 24.
+ + hour * 60. * 60.
+ + minute * 60.
+ + second
+ + msec / 1000.0);
+ v->numeric.format = f;
+ return v;
+ }
+ }
+ }
+ else if (format && fmt_get_category (f.type) == FMT_CAT_TIME)
+ {
+ int hour, minute, second, msec, len = -1;
+ if (sscanf (data->s, "%d:%2d:%2d.%3d%n",
+ &hour, &minute, &second, &msec, &len) == 4
+ && len > 0
+ && data->s[len] == '\0')
+ {
+ v->type = PIVOT_VALUE_NUMERIC;
+ v->numeric.x = (hour * 60. * 60.
+ + minute * 60.
+ + second
+ + msec / 1000.0);
+ v->numeric.format = f;
+ return v;
+ }
+ }
+ v->type = PIVOT_VALUE_STRING;
+ v->string.s = xstrdup (data->s);
+ }
+ else
+ {
+ v->type = PIVOT_VALUE_NUMERIC;
+ v->numeric.x = data->d;
+ v->numeric.format = f;
+ }
+ return v;
+}
+
+static void
+add_parents (struct pivot_category *cat, struct pivot_category *parent,
+ size_t group_index)
+{
+ cat->parent = parent;
+ cat->group_index = group_index;
+ if (pivot_category_is_group (cat))
+ for (size_t i = 0; i < cat->n_subs; i++)
+ add_parents (cat->subs[i], cat, i);
+}
+
+static const struct spvdx_facet_level *
+find_facet_level (const struct spvdx_visualization *v, int facet_level)
+{
+ const struct spvdx_facet_layout *layout = v->graph->facet_layout;
+ for (size_t i = 0; i < layout->n_facet_level; i++)
+ {
+ const struct spvdx_facet_level *fl = layout->facet_level[i];
+ if (facet_level == fl->level)
+ return fl;
+ }
+ return NULL;
+}
+
+static bool
+should_show_label (const struct spvdx_facet_level *fl)
+{
+ return fl && fl->axis->label && fl->axis->label->style->visible != 0;
+}
+
+static size_t
+max_category (const struct spv_series *s)
+{
+ double max_cat = -DBL_MAX;
+ for (size_t i = 0; i < s->n_values; i++)
+ {
+ const struct spv_data_value *dv = &s->values[i];
+ double d = dv->width < 0 ? dv->d : dv->index;
+ if (d > max_cat)
+ max_cat = d;
+ }
+ assert (max_cat >= 0 && max_cat < SIZE_MAX - 1);
+
+ return max_cat;
+}
+
+static void
+add_affixes (struct pivot_table *table, struct pivot_value *value,
+ struct spvdx_affix **affixes, size_t n_affixes)
+{
+ for (size_t i = 0; i < n_affixes; i++)
+ add_footnote (value, affixes[i]->defines_reference, table);
+}
+
+static struct pivot_dimension *
+add_dimension (struct spv_series **series, size_t n,
+ enum pivot_axis_type axis_type,
+ const struct spvdx_visualization *v, struct pivot_table *table,
+ struct spv_series **dim_seriesp, size_t *n_dim_seriesp,
+ int base_facet_level)
+{
+ const struct spvdx_facet_level *fl
+ = find_facet_level (v, base_facet_level + n);
+ if (fl)
+ {
+ struct area_style *area = (axis_type == PIVOT_AXIS_COLUMN
+ ? &table->areas[PIVOT_AREA_COLUMN_LABELS]
+ : axis_type == PIVOT_AXIS_ROW
+ ? &table->areas[PIVOT_AREA_ROW_LABELS]
+ : NULL);
+ if (area && fl->axis->label)
+ {
+ area_style_uninit (area);
+ decode_spvdx_style (fl->axis->label->style,
+ fl->axis->label->text_frame_style, area);
+ }
+ }
+
+ if (axis_type == PIVOT_AXIS_ROW)
+ {
+ const struct spvdx_facet_level *fl2
+ = find_facet_level (v, base_facet_level + (n - 1));
+ if (fl2)
+ decode_spvdx_style_incremental (
+ fl2->axis->major_ticks->style,
+ fl2->axis->major_ticks->tick_frame_style,
+ &table->areas[PIVOT_AREA_ROW_LABELS]);
+ }
+
+ const struct spvdx_facet_level *fl3 = find_facet_level (v, base_facet_level);
+ if (fl3 && fl3->axis->major_ticks->label_angle == -90)
+ {
+ if (axis_type == PIVOT_AXIS_COLUMN)
+ table->rotate_inner_column_labels = true;
+ else
+ table->rotate_outer_row_labels = true;
+ }
+
+ /* Find the first row for each category. */
+ size_t max_cat = max_category (series[0]);
+ size_t *cat_rows = xnmalloc (max_cat + 1, sizeof *cat_rows);
+ for (size_t k = 0; k <= max_cat; k++)
+ cat_rows[k] = SIZE_MAX;
+ for (size_t k = 0; k < series[0]->n_values; k++)
+ {
+ const struct spv_data_value *dv = &series[0]->values[k];
+ double d = dv->width < 0 ? dv->d : dv->index;
+ if (d >= 0 && d < SIZE_MAX - 1)
+ {
+ size_t row = d;
+ if (cat_rows[row] == SIZE_MAX)
+ cat_rows[row] = k;
+ }
+ }
+
+ /* Drop missing categories and count what's left. */
+ size_t n_cats = 0;
+ for (size_t k = 0; k <= max_cat; k++)
+ if (cat_rows[k] != SIZE_MAX)
+ cat_rows[n_cats++] = cat_rows[k];
+ assert (n_cats > 0);
+
+ /* Make the categories. */
+ struct pivot_dimension *d = xzalloc (sizeof *d);
+ table->dimensions[table->n_dimensions++] = d;
+
+ series[0]->n_index = max_cat + 1;
+ series[0]->index_to_category = xcalloc (
+ max_cat + 1, sizeof *series[0]->index_to_category);
+ struct pivot_category **cats = xnmalloc (n_cats, sizeof **cats);
+ for (size_t k = 0; k < n_cats; k++)
+ {
+ struct spv_data_value *dv = &series[0]->values[cat_rows[k]];
+ int dv_num = dv ? dv->d : dv->index;
+ struct pivot_category *cat = xzalloc (sizeof *cat);
+ cat->name = pivot_value_from_data_value (
+ spv_map_lookup (&series[0]->map, dv), NULL, NULL);
+ cat->parent = NULL;
+ cat->dimension = d;
+ cat->data_index = k;
+ cat->presentation_index = cat_rows[k];
+ cats[k] = cat;
+ series[0]->index_to_category[dv_num] = cat;
+
+ add_affixes (table, cat->name, series[0]->affixes, series[0]->n_affixes);
+ }
+ free (cat_rows);
+
+ struct pivot_axis *axis = &table->axes[axis_type];
+ d->axis_type = axis_type;
+ d->level = axis->n_dimensions;
+ d->top_index = table->n_dimensions - 1;
+ d->root = xzalloc (sizeof *d->root);
+ *d->root = (struct pivot_category) {
+ .name = pivot_value_new_user_text (
+ series[0]->label ? series[0]->label : "", -1),
+ .dimension = d,
+ .show_label = should_show_label (fl),
+ .data_index = SIZE_MAX,
+ .presentation_index = SIZE_MAX,
+ };
+ d->data_leaves = xmemdup (cats, n_cats * sizeof *cats);
+ d->presentation_leaves = xmemdup (cats, n_cats * sizeof *cats);
+ d->n_leaves = d->allocated_leaves = n_cats;
+
+ /* Now group them, in one pass per grouping variable, innermost first. */
+ for (size_t j = 1; j < n; j++)
+ {
+ struct pivot_category **new_cats = xnmalloc (n_cats, sizeof **cats);
+ size_t n_new_cats = 0;
+
+ /* Allocate a category index. */
+ size_t max_cat = max_category (series[j]);
+ series[j]->n_index = max_cat + 1;
+ series[j]->index_to_category = xcalloc (
+ max_cat + 1, sizeof *series[j]->index_to_category);
+ for (size_t cat1 = 0; cat1 < n_cats; )
+ {
+ /* Find a sequence of categories cat1...cat2 (exclusive), that all
+ have the same value in series 'j'. (This might be only a single
+ category; we will drop unnamed 1-category groups later.) */
+ size_t row1 = cats[cat1]->presentation_index;
+ const struct spv_data_value *dv1 = &series[j]->values[row1];
+ size_t cat2;
+ for (cat2 = cat1 + 1; cat2 < n_cats; cat2++)
+ {
+ size_t row2 = cats[cat2]->presentation_index;
+ const struct spv_data_value *dv2 = &series[j]->values[row2];
+ if (!spv_data_value_equal (dv1, dv2))
+ break;
+ }
+ size_t n_subs = cat2 - cat1;
+
+ struct pivot_category *new_cat;
+ const struct spv_data_value *name
+ = spv_map_lookup (&series[j]->map, dv1);
+ if (n_subs == 1 && name->width == 0)
+ {
+ /* The existing category stands on its own. */
+ new_cat = cats[cat1++];
+ }
+ else
+ {
+ /* Create a new group with cat...cat2 as subcategories. */
+ new_cat = xzalloc (sizeof *new_cat);
+ *new_cat = (struct pivot_category) {
+ .name = pivot_value_from_data_value (name, NULL, NULL),
+ .dimension = d,
+ .subs = xnmalloc (n_subs, sizeof *new_cat->subs),
+ .n_subs = n_subs,
+ .show_label = true,
+ .data_index = SIZE_MAX,
+ .presentation_index = row1,
+ };
+ for (size_t k = 0; k < n_subs; k++)
+ new_cat->subs[k] = cats[cat1++];
+
+ int dv1_num = dv1->width < 0 ? dv1->d : dv1->index;
+ series[j]->index_to_category[dv1_num] = new_cat;
+ }
+
+ add_affixes (table, new_cat->name,
+ series[j]->affixes, series[j]->n_affixes);
+
+ /* Append the new group to the list of new groups. */
+ new_cats[n_new_cats++] = new_cat;
+ }
+
+ free (cats);
+ cats = new_cats;
+ n_cats = n_new_cats;
+ }
+
+ /* Now drop unnamed 1-category groups and add parent pointers. */
+ for (size_t j = 0; j < n_cats; j++)
+ add_parents (cats[j], d->root, j);
+
+ d->root->subs = cats;
+ d->root->n_subs = n_cats;
+
+ dim_seriesp[(*n_dim_seriesp)++] = series[0];
+ series[0]->dimension = d;
+
+ axis->dimensions = xnrealloc (axis->dimensions, axis->n_dimensions + 1,
+ sizeof *axis->dimensions);
+ axis->dimensions[axis->n_dimensions++] = d;
+ axis->extent *= d->n_leaves;
+
+ return d;
+}
+
+static void
+add_dimensions (struct hmap *series_map, const struct spvdx_nest *nest,
+ enum pivot_axis_type axis_type,
+ const struct spvdx_visualization *v, struct pivot_table *table,
+ struct spv_series **dim_seriesp, size_t *n_dim_seriesp,
+ int level_ofs)
+{
+ struct pivot_axis *axis = &table->axes[axis_type];
+ if (!axis->extent)
+ axis->extent = 1;
+
+ if (!nest)
+ return;
+
+ struct spv_series **series = xnmalloc (nest->n_vars, sizeof *series);
+ for (size_t i = 0; i < nest->n_vars; )
+ {
+ size_t n;
+ for (n = 0; i + n < nest->n_vars; n++)
+ {
+ series[n] = spv_series_from_ref (series_map, nest->vars[i + n]->ref);
+ if (!series[n] || !series[n]->n_values)
+ break;
+ }
+
+ if (n > 0)
+ add_dimension (series, n, axis_type, v, table,
+ dim_seriesp, n_dim_seriesp, level_ofs + i);
+ i += n + 1;
+ }
+ free (series);
+}
+
+static void
+add_layers (struct hmap *series_map,
+ struct spvdx_layer **layers, size_t n_layers,
+ const struct spvdx_visualization *v, struct pivot_table *table,
+ struct spv_series **dim_seriesp, size_t *n_dim_seriesp,
+ int level_ofs)
+{
+ struct pivot_axis *axis = &table->axes[PIVOT_AXIS_LAYER];
+ if (!axis->extent)
+ axis->extent = 1;
+
+ if (!n_layers)
+ return;
+
+ struct spv_series **series = xnmalloc (n_layers, sizeof *series);
+ for (size_t i = 0; i < n_layers; )
+ {
+ size_t n;
+ for (n = 0; i + n < n_layers; n++)
+ {
+ series[n] = spv_series_from_ref (series_map,
+ layers[i + n]->variable);
+ if (!series[n] || !series[n]->n_values)
+ break;
+ }
+
+ if (n > 0)
+ {
+ struct pivot_dimension *d = add_dimension (
+ series, n, PIVOT_AXIS_LAYER, v, table,
+ dim_seriesp, n_dim_seriesp, level_ofs + i);
+
+ int index = atoi (layers[i]->value);
+ assert (index < d->n_leaves);
+ table->current_layer = xrealloc (
+ table->current_layer,
+ axis->n_dimensions * sizeof *table->current_layer);
+ table->current_layer[axis->n_dimensions - 1] = index;
+ }
+ i += n + 1;
+ }
+ free (series);
+}
+
+static int
+optional_int (int x, int default_value)
+{
+ return x != INT_MIN ? x : default_value;
+}
+
+static enum pivot_area
+pivot_area_from_name (const char *name)
+{
+ static const char *area_names[PIVOT_N_AREAS] = {
+ [PIVOT_AREA_TITLE] = "title",
+ [PIVOT_AREA_CAPTION] = "caption",
+ [PIVOT_AREA_FOOTER] = "footnotes",
+ [PIVOT_AREA_CORNER] = "cornerLabels",
+ [PIVOT_AREA_COLUMN_LABELS] = "columnLabels",
+ [PIVOT_AREA_ROW_LABELS] = "rowLabels",
+ [PIVOT_AREA_DATA] = "data",
+ [PIVOT_AREA_LAYERS] = "layers",
+ };
+
+ enum pivot_area area;
+ for (area = 0; area < PIVOT_N_AREAS; area++)
+ if (!strcmp (name, area_names[area]))
+ break;
+ return area;
+}
+
+static enum pivot_border
+pivot_border_from_name (const char *name)
+{
+ static const char *border_names[PIVOT_N_BORDERS] = {
+ [PIVOT_BORDER_TITLE] = "titleLayerSeparator",
+ [PIVOT_BORDER_OUTER_LEFT] = "leftOuterFrame",
+ [PIVOT_BORDER_OUTER_TOP] = "topOuterFrame",
+ [PIVOT_BORDER_OUTER_RIGHT] = "rightOuterFrame",
+ [PIVOT_BORDER_OUTER_BOTTOM] = "bottomOuterFrame",
+ [PIVOT_BORDER_INNER_LEFT] = "leftInnerFrame",
+ [PIVOT_BORDER_INNER_TOP] = "topInnerFrame",
+ [PIVOT_BORDER_INNER_RIGHT] = "rightInnerFrame",
+ [PIVOT_BORDER_INNER_BOTTOM] = "bottomInnerFrame",
+ [PIVOT_BORDER_DATA_LEFT] = "dataAreaLeft",
+ [PIVOT_BORDER_DATA_TOP] = "dataAreaTop",
+ [PIVOT_BORDER_DIM_ROW_HORZ] = "horizontalDimensionBorderRows",
+ [PIVOT_BORDER_DIM_ROW_VERT] = "verticalDimensionBorderRows",
+ [PIVOT_BORDER_DIM_COL_HORZ] = "horizontalDimensionBorderColumns",
+ [PIVOT_BORDER_DIM_COL_VERT] = "verticalDimensionBorderColumns",
+ [PIVOT_BORDER_CAT_ROW_HORZ] = "horizontalCategoryBorderRows",
+ [PIVOT_BORDER_CAT_ROW_VERT] = "verticalCategoryBorderRows",
+ [PIVOT_BORDER_CAT_COL_HORZ] = "horizontalCategoryBorderColumns",
+ [PIVOT_BORDER_CAT_COL_VERT] = "verticalCategoryBorderColumns",
+ };
+
+ enum pivot_border border;
+ for (border = 0; border < PIVOT_N_BORDERS; border++)
+ if (!strcmp (name, border_names[border]))
+ break;
+ return border;
+}
+
+static struct pivot_category *
+find_category (struct spv_series *series, int index)
+{
+ return (index >= 0 && index < series->n_index
+ ? series->index_to_category[index]
+ : NULL);
+}
+
+static bool
+int_in_array (int value, const int *array, size_t n)
+{
+ for (size_t i = 0; i < n; i++)
+ if (array[i] == value)
+ return true;
+
+ return false;
+}
+
+static void
+apply_styles_to_value (struct pivot_table *table,
+ struct pivot_value *value,
+ const struct spvdx_set_format *sf,
+ const struct area_style *base_area_style,
+ const struct spvdx_style *fg,
+ const struct spvdx_style *bg)
+{
+ if (sf)
+ {
+ if (sf->reset > 0)
+ {
+ free (value->footnotes);
+ value->footnotes = NULL;
+ value->n_footnotes = 0;
+ }
+
+ struct fmt_spec format = { .w = 0 };
+ if (sf->format)
+ {
+ format = decode_format (sf->format);
+ add_affixes (table, value, sf->format->affix, sf->format->n_affix);
+ }
+ else if (sf->number_format)
+ {
+ format = decode_number_format (sf->number_format);
+ add_affixes (table, value, sf->number_format->affix,
+ sf->number_format->n_affix);
+ }
+ else if (sf->n_string_format)
+ {
+ for (size_t i = 0; i < sf->n_string_format; i++)
+ add_affixes (table, value, sf->string_format[i]->affix,
+ sf->string_format[i]->n_affix);
+ }
+ else if (sf->date_time_format)
+ {
+ format = decode_date_time_format (sf->date_time_format);
+ add_affixes (table, value, sf->date_time_format->affix,
+ sf->date_time_format->n_affix);
+ }
+ else if (sf->elapsed_time_format)
+ {
+ format = decode_elapsed_time_format (sf->elapsed_time_format);
+ add_affixes (table, value, sf->elapsed_time_format->affix,
+ sf->elapsed_time_format->n_affix);
+ }
+
+ if (format.w)
+ {
+ if (value->type == PIVOT_VALUE_NUMERIC)
+ value->numeric.format = format;
+
+ /* Possibly we should try to apply date and time formats too,
+ but none seem to occur in practice so far. */
+ }
+ }
+ if (fg || bg)
+ {
+ struct area_style area;
+ pivot_value_get_style (
+ value,
+ value->font_style ? value->font_style : &base_area_style->font_style,
+ value->cell_style ? value->cell_style : &base_area_style->cell_style,
+ &area);
+ decode_spvdx_style_incremental (fg, bg, &area);
+ pivot_value_set_style (value, &area);
+ area_style_uninit (&area);
+ }
+}
+
+static void
+decode_set_cell_properties__ (struct pivot_table *table,
+ struct hmap *series_map,
+ const struct spvdx_intersect *intersect,
+ const struct spvdx_style *interval,
+ const struct spvdx_style *graph,
+ const struct spvdx_style *labeling,
+ const struct spvdx_style *frame,
+ const struct spvdx_style *major_ticks,
+ const struct spvdx_set_format *set_format)
+{
+ if (graph && labeling && intersect->alternating
+ && !interval && !major_ticks && !frame && !set_format)
+ {
+ /* Sets alt_fg_color and alt_bg_color. */
+ struct area_style area;
+ decode_spvdx_style (labeling, graph, &area);
+ table->areas[PIVOT_AREA_DATA].font_style.fg[1]
+ = area.font_style.fg[0];
+ table->areas[PIVOT_AREA_DATA].font_style.bg[1]
+ = area.font_style.bg[0];
+ area_style_uninit (&area);
+ }
+ else if (graph
+ && !labeling && !interval && !major_ticks && !frame && !set_format)
+ {
+ /* 'graph->width' likely just sets the width of the table as a
+ whole. */
+ }
+ else if (!graph && !labeling && !interval && !frame && !set_format
+ && !major_ticks)
+ {
+ /* No-op. (Presumably there's a setMetaData we don't care about.) */
+ }
+ else if (((set_format && spvdx_is_major_ticks (set_format->target))
+ || major_ticks || frame)
+ && intersect->n_where == 1)
+ {
+ /* Formatting for individual row or column labels. */
+ const struct spvdx_where *w = intersect->where[0];
+ struct spv_series *s = spv_series_find (series_map, w->variable->id);
+ assert (s);
+
+ const char *p = w->include;
+
+ while (*p)
+ {
+ char *tail;
+ int include = strtol (p, &tail, 10);
+
+ struct pivot_category *c = find_category (s, include);
+ if (c)
+ {
+ const struct area_style *base_area_style
+ = (c->dimension->axis_type == PIVOT_AXIS_ROW
+ ? &table->areas[PIVOT_AREA_ROW_LABELS]
+ : &table->areas[PIVOT_AREA_COLUMN_LABELS]);
+ apply_styles_to_value (table, c->name, set_format,
+ base_area_style, major_ticks, frame);
+ }
+
+ if (tail == p)
+ break;
+ p = tail;
+ if (*p == ';')
+ p++;
+ }
+ }
+ else if ((set_format && spvdx_is_labeling (set_format->target))
+ || labeling || interval)
+ {
+ /* Formatting for individual cells or groups of them with some dimensions
+ in common. */
+ int **indexes = xcalloc (table->n_dimensions, sizeof *indexes);
+ size_t *n = xcalloc (table->n_dimensions, sizeof *n);
+ size_t *allocated = xcalloc (table->n_dimensions, sizeof *allocated);
+
+ for (size_t i = 0; i < intersect->n_where; i++)
+ {
+ const struct spvdx_where *w = intersect->where[i];
+ struct spv_series *s = spv_series_find (series_map, w->variable->id);
+ assert (s);
+ if (!s->dimension)
+ {
+ /* Group indexes may be included even though they are redundant.
+ Ignore them. */
+ continue;
+ }
+
+ size_t j = s->dimension->top_index;
+
+ const char *p = w->include;
+ while (*p)
+ {
+ char *tail;
+ int include = strtol (p, &tail, 10);
+
+ struct pivot_category *c = find_category (s, include);
+ if (c)
+ {
+ if (n[j] >= allocated[j])
+ indexes[j] = x2nrealloc (indexes[j], &allocated[j],
+ sizeof *indexes[j]);
+ indexes[j][n[j]++] = c->data_index;
+ }
+
+ if (tail == p)
+ break;
+ p = tail;
+ if (*p == ';')
+ p++;
+ }
+ }
+
+#if 0
+ printf ("match:");
+ for (size_t i = 0; i < table->n_dimensions; i++)
+ {
+ if (n[i])
+ {
+ printf (" %d=(", i);
+ for (size_t j = 0; j < n[i]; j++)
+ {
+ if (j)
+ putchar (',');
+ printf ("%d", indexes[i][j]);
+ }
+ putchar (')');
+ }
+ }
+ printf ("\n");
+#endif
+
+ /* XXX This is inefficient in the common case where all of the dimensions
+ are matched. We should use a heuristic where if all of the dimensions
+ are matched and the product of n[*] is less than
+ hmap_count(&table->cells) then iterate through all the possibilities
+ rather than all the cells. Or even only do it if there is just one
+ possibility. */
+
+ struct pivot_cell *cell;
+ HMAP_FOR_EACH (cell, struct pivot_cell, hmap_node, &table->cells)
+ {
+ for (size_t i = 0; i < table->n_dimensions; i++)
+ {
+ if (n[i] && !int_in_array (cell->idx[i], indexes[i], n[i]))
+ goto skip;
+ }
+ apply_styles_to_value (table, cell->value, set_format,
+ &table->areas[PIVOT_AREA_DATA],
+ labeling, interval);
+
+ skip: ;
+ }
+
+ for (size_t i = 0; i < table->n_dimensions; i++)
+ free (indexes[i]);
+ free (indexes);
+ free (n);
+ free (allocated);
+ }
+ else
+ NOT_REACHED ();
+}
+
+static void
+decode_set_cell_properties (struct pivot_table *table, struct hmap *series_map,
+ struct spvdx_set_cell_properties **scps,
+ size_t n_scps)
+{
+ for (size_t i = 0; i < n_scps; i++)
+ {
+ const struct spvdx_set_cell_properties *scp = scps[i];
+ const struct spvdx_style *interval = NULL;
+ const struct spvdx_style *graph = NULL;
+ const struct spvdx_style *labeling = NULL;
+ const struct spvdx_style *frame = NULL;
+ const struct spvdx_style *major_ticks = NULL;
+ const struct spvdx_set_format *set_format = NULL;
+ for (size_t j = 0; j < scp->n_seq; j++)
+ {
+ const struct spvxml_node *node = scp->seq[j];
+ if (spvdx_is_set_style (node))
+ {
+ const struct spvdx_set_style *set_style
+ = spvdx_cast_set_style (node);
+ if (spvdx_is_graph (set_style->target))
+ graph = set_style->style;
+ else if (spvdx_is_labeling (set_style->target))
+ labeling = set_style->style;
+ else if (spvdx_is_interval (set_style->target))
+ interval = set_style->style;
+ else if (spvdx_is_major_ticks (set_style->target))
+ major_ticks = set_style->style;
+ else
+ NOT_REACHED ();
+ }
+ else if (spvdx_is_set_frame_style (node))
+ frame = spvdx_cast_set_frame_style (node)->style;
+ else if (spvdx_is_set_format (node))
+ set_format = spvdx_cast_set_format (node);
+ else
+ assert (spvdx_is_set_meta_data (node));
+ }
+
+ if (scp->union_ && scp->apply_to_converse <= 0)
+ {
+ for (size_t j = 0; j < scp->union_->n_intersect; j++)
+ decode_set_cell_properties__ (
+ table, series_map, scp->union_->intersect[j],
+ interval, graph, labeling, frame, major_ticks, set_format);
+ }
+ else if (!scp->union_ && scp->apply_to_converse > 0)
+ {
+ if ((set_format && spvdx_is_labeling (set_format->target))
+ || labeling || interval)
+ {
+ struct pivot_cell *cell;
+ HMAP_FOR_EACH (cell, struct pivot_cell, hmap_node, &table->cells)
+ apply_styles_to_value (table, cell->value, set_format,
+ &table->areas[PIVOT_AREA_DATA],
+ NULL, NULL);
+ }
+ }
+ else if (!scp->union_ && scp->apply_to_converse <= 0)
+ {
+ /* Appears to be used to set the font for something--but what? */
+ }
+ else
+ NOT_REACHED ();
+ }
+}
+
+char * WARN_UNUSED_RESULT
+decode_spvsx_legacy_properties (const struct spvsx_table_properties *in,
+ struct spv_legacy_properties **outp)
+{
+ struct spv_legacy_properties *out = xzalloc (sizeof *out);
+ char *error;
+
+ if (!in)
+ {
+ error = xstrdup ("Legacy table lacks tableProperties");
+ goto error;
+ }
+
+ const struct spvsx_general_properties *g = in->general_properties;
+ out->omit_empty = g->hide_empty_rows != 0;
+ out->width_ranges[TABLE_HORZ][0] = optional_pt (g->minimum_column_width, -1);
+ out->width_ranges[TABLE_HORZ][1] = optional_pt (g->maximum_column_width, -1);
+ out->width_ranges[TABLE_VERT][0] = optional_pt (g->minimum_row_width, -1);
+ out->width_ranges[TABLE_VERT][1] = optional_pt (g->maximum_row_width, -1);
+ out->row_labels_in_corner
+ = g->row_dimension_labels != SPVSX_ROW_DIMENSION_LABELS_NESTED;
+
+ const struct spvsx_footnote_properties *f = in->footnote_properties;
+ out->footnote_marker_superscripts
+ = (f->marker_position != SPVSX_MARKER_POSITION_SUBSCRIPT);
+ out->show_numeric_markers
+ = (f->number_format == SPVSX_NUMBER_FORMAT_NUMERIC);
+
+ for (int i = 0; i < PIVOT_N_AREAS; i++)
+ pivot_area_get_default_style (i, &out->areas[i]);
+
+ const struct spvsx_cell_format_properties *cfp = in->cell_format_properties;
+ for (size_t i = 0; i < cfp->n_cell_style; i++)
+ {
+ const struct spvsx_cell_style *c = cfp->cell_style[i];
+ const char *name = CHAR_CAST (const char *, c->node_.raw->name);
+ enum pivot_area area = pivot_area_from_name (name);
+ if (area == PIVOT_N_AREAS)
+ {
+ error = xasprintf ("unknown area \"%s\" in cellFormatProperties",
+ name);
+ goto error;
+ }
+
+ struct area_style *a = &out->areas[area];
+ const struct spvsx_style *s = c->style;
+ if (s->font_weight)
+ a->font_style.bold = s->font_weight == SPVSX_FONT_WEIGHT_BOLD;
+ if (s->font_style)
+ a->font_style.italic = s->font_style == SPVSX_FONT_STYLE_ITALIC;
+ a->font_style.underline = false;
+ if (s->color >= 0)
+ a->font_style.fg[0] = optional_color (
+ s->color, (struct cell_color) CELL_COLOR_BLACK);
+ if (c->alternating_text_color >= 0 || s->color >= 0)
+ a->font_style.fg[1] = optional_color (c->alternating_text_color,
+ a->font_style.fg[0]);
+ if (s->color2 >= 0)
+ a->font_style.bg[0] = optional_color (
+ s->color2, (struct cell_color) CELL_COLOR_WHITE);
+ if (c->alternating_color >= 0 || s->color2 >= 0)
+ a->font_style.bg[1] = optional_color (c->alternating_color,
+ a->font_style.bg[0]);
+ if (s->font_family)
+ {
+ free (a->font_style.typeface);
+ a->font_style.typeface = xstrdup (s->font_family);
+ }
+
+ if (s->font_size)
+ a->font_style.size = optional_length (s->font_size, 0);
+
+ if (s->text_alignment)
+ a->cell_style.halign
+ = (s->text_alignment == SPVSX_TEXT_ALIGNMENT_LEFT
+ ? TABLE_HALIGN_LEFT
+ : s->text_alignment == SPVSX_TEXT_ALIGNMENT_RIGHT
+ ? TABLE_HALIGN_RIGHT
+ : s->text_alignment == SPVSX_TEXT_ALIGNMENT_CENTER
+ ? TABLE_HALIGN_CENTER
+ : s->text_alignment == SPVSX_TEXT_ALIGNMENT_DECIMAL
+ ? TABLE_HALIGN_DECIMAL
+ : TABLE_HALIGN_MIXED);
+ if (s->label_location_vertical)
+ a->cell_style.valign
+ = (s->label_location_vertical == SPVSX_LABEL_LOCATION_VERTICAL_NEGATIVE
+ ? TABLE_VALIGN_BOTTOM
+ : s->label_location_vertical == SPVSX_LABEL_LOCATION_VERTICAL_POSITIVE
+ ? TABLE_VALIGN_TOP
+ : TABLE_VALIGN_CENTER);
+
+ if (s->decimal_offset != DBL_MAX)
+ a->cell_style.decimal_offset = optional_px (s->decimal_offset, 0);
+
+ if (s->margin_left != DBL_MAX)
+ a->cell_style.margin[TABLE_HORZ][0] = optional_px (s->margin_left, 8);
+ if (s->margin_right != DBL_MAX)
+ a->cell_style.margin[TABLE_HORZ][1] = optional_px (s->margin_right,
+ 11);
+ if (s->margin_top != DBL_MAX)
+ a->cell_style.margin[TABLE_VERT][0] = optional_px (s->margin_top, 1);
+ if (s->margin_bottom != DBL_MAX)
+ a->cell_style.margin[TABLE_VERT][1] = optional_px (s->margin_bottom,
+ 1);
+ }
+
+ for (int i = 0; i < PIVOT_N_BORDERS; i++)
+ pivot_border_get_default_style (i, &out->borders[i]);
+
+ const struct spvsx_border_properties *bp = in->border_properties;
+ for (size_t i = 0; i < bp->n_border_style; i++)
+ {
+ const struct spvsx_border_style *bin = bp->border_style[i];
+ const char *name = CHAR_CAST (const char *, bin->node_.raw->name);
+ enum pivot_border border = pivot_border_from_name (name);
+ if (border == PIVOT_N_BORDERS)
+ {
+ error = xasprintf ("unknown border \"%s\" parsing borderProperties",
+ name);
+ goto error;
+ }
+
+ struct table_border_style *bout = &out->borders[border];
+ bout->stroke
+ = (bin->border_style_type == SPVSX_BORDER_STYLE_TYPE_NONE
+ ? TABLE_STROKE_NONE
+ : bin->border_style_type == SPVSX_BORDER_STYLE_TYPE_DASHED
+ ? TABLE_STROKE_DASHED
+ : bin->border_style_type == SPVSX_BORDER_STYLE_TYPE_THICK
+ ? TABLE_STROKE_THICK
+ : bin->border_style_type == SPVSX_BORDER_STYLE_TYPE_THIN
+ ? TABLE_STROKE_THIN
+ : bin->border_style_type == SPVSX_BORDER_STYLE_TYPE_DOUBLE
+ ? TABLE_STROKE_DOUBLE
+ : TABLE_STROKE_SOLID);
+ bout->color = optional_color (bin->color,
+ (struct cell_color) CELL_COLOR_BLACK);
+ }
+
+ const struct spvsx_printing_properties *pp = in->printing_properties;
+ out->print_all_layers = pp->print_all_layers > 0;
+ out->paginate_layers = pp->print_each_layer_on_separate_page > 0;
+ out->shrink_to_width = pp->rescale_wide_table_to_fit_page > 0;
+ out->shrink_to_length = pp->rescale_long_table_to_fit_page > 0;
+ out->top_continuation = pp->continuation_text_at_top > 0;
+ out->bottom_continuation = pp->continuation_text_at_bottom > 0;
+ out->continuation = xstrdup (pp->continuation_text
+ ? pp->continuation_text : "(cont.)");
+ out->n_orphan_lines = optional_int (pp->window_orphan_lines, 2);
+
+ *outp = out;
+ return NULL;
+
+error:
+ spv_legacy_properties_destroy (out);
+ *outp = NULL;
+ return error;
+}
+
+void
+spv_legacy_properties_destroy (struct spv_legacy_properties *props)
+{
+ if (props)
+ {
+ for (size_t i = 0; i < PIVOT_N_AREAS; i++)
+ area_style_uninit (&props->areas[i]);
+ free (props->continuation);
+ free (props);
+ }
+}
+
+static struct spv_series *
+parse_formatting (const struct spvdx_visualization *v,
+ const struct hmap *series_map, struct hmap *format_map)
+{
+ const struct spvdx_labeling *labeling = v->graph->interval->labeling;
+ struct spv_series *cell_format = NULL;
+ for (size_t i = 0; i < labeling->n_seq; i++)
+ {
+ const struct spvdx_formatting *f
+ = spvdx_cast_formatting (labeling->seq[i]);
+ if (!f)
+ continue;
+
+ cell_format = spv_series_from_ref (series_map, f->variable);
+ for (size_t j = 0; j < f->n_format_mapping; j++)
+ {
+ const struct spvdx_format_mapping *fm = f->format_mapping[j];
+
+ if (fm->format)
+ {
+ struct format_mapping *out = xmalloc (sizeof *out);
+ out->from = fm->from;
+ out->to = decode_format (fm->format);
+ hmap_insert (format_map, &out->hmap_node,
+ hash_int (out->from, 0));
+ }
+ }
+ }
+
+ return cell_format;
+}
+
+static void
+format_map_destroy (struct hmap *format_map)
+{
+ struct format_mapping *fm, *next;
+ HMAP_FOR_EACH_SAFE (fm, next, struct format_mapping, hmap_node, format_map)
+ {
+ hmap_delete (format_map, &fm->hmap_node);
+ free (fm);
+ }
+ hmap_destroy (format_map);
+}
+
+char * WARN_UNUSED_RESULT
+decode_spvdx_table (const struct spvdx_visualization *v,
+ const struct spv_legacy_properties *props,
+ struct spv_data *data, struct pivot_table **outp)
+{
+ struct pivot_table *table = pivot_table_create__ (NULL);
+
+ struct hmap series_map = HMAP_INITIALIZER (series_map);
+ struct hmap format_map = HMAP_INITIALIZER (format_map);
+ struct spv_series **dim_series = NULL;
+ char *error;
+
+ /* First get the legacy properties. */
+ table->omit_empty = props->omit_empty;
+ for (enum table_axis axis = 0; axis < TABLE_N_AXES; axis++)
+ for (int i = 0; i < 2; i++)
+ if (props->width_ranges[axis][i] > 0)
+ table->sizing[axis].range[i] = props->width_ranges[axis][i];
+ table->row_labels_in_corner = props->row_labels_in_corner;
+
+ table->footnote_marker_superscripts = props->footnote_marker_superscripts;
+ table->show_numeric_markers = props->show_numeric_markers;
+
+ for (size_t i = 0; i < PIVOT_N_AREAS; i++)
+ {
+ area_style_uninit (&table->areas[i]);
+ area_style_copy (&table->areas[i], &props->areas[i]);
+ }
+ for (size_t i = 0; i < PIVOT_N_BORDERS; i++)
+ table->borders[i] = props->borders[i];
+
+ table->print_all_layers = props->print_all_layers;
+ table->paginate_layers = props->paginate_layers;
+ table->shrink_to_fit[TABLE_HORZ] = props->shrink_to_width;
+ table->shrink_to_fit[TABLE_VERT] = props->shrink_to_length;
+ table->top_continuation = props->top_continuation;
+ table->bottom_continuation = props->bottom_continuation;
+ table->continuation = xstrdup (props->continuation);
+ table->n_orphan_lines = props->n_orphan_lines;
+
+ struct spvdx_visualization_extension *ve = v->visualization_extension;
+ table->show_grid_lines = ve && ve->show_gridline;
+
+ /* Sizing from the legacy properties can get overridden. */
+ if (v->graph->cell_style->width)
+ {
+ int min_width, max_width, n = 0;
+ if (sscanf (v->graph->cell_style->width, "%*d%%;%dpt;%dpt%n",
+ &min_width, &max_width, &n)
+ && v->graph->cell_style->width[n] == '\0')
+ {
+ table->sizing[TABLE_HORZ].range[0] = min_width;
+ table->sizing[TABLE_HORZ].range[1] = max_width;
+ }
+ }
+
+ /* Footnotes.
+
+ Any pivot_value might refer to footnotes, so it's important to process the
+ footnotes early to ensure that those references can be resolved. There is
+ a possible problem that a footnote might itself reference an
+ as-yet-unprocessed footnote, but that's OK because footnote references
+ don't actually look at the footnote contents but only resolve a pointer to
+ where the footnote will go later.
+
+ Before we really start, create all the footnotes we'll fill in. This is
+ because sometimes footnotes refer to themselves or to each other and we
+ don't want to reject those references. */
+ if (v->container)
+ for (size_t i = 0; i < v->container->n_label_frame; i++)
+ {
+ const struct spvdx_label_frame *lf = v->container->label_frame[i];
+ if (lf->label
+ && lf->label->purpose == SPVDX_PURPOSE_FOOTNOTE
+ && lf->label->n_text > 0
+ && lf->label->text[0]->uses_reference > 0)
+ {
+ pivot_table_create_footnote__ (
+ table, lf->label->text[0]->uses_reference - 1,
+ NULL, NULL);
+ }
+ }
+
+ if (v->graph->interval->footnotes)
+ decode_footnotes (table, v->graph->interval->footnotes);
+
+ struct spv_series *footnotes = NULL;
+ for (size_t i = 0; i < v->graph->interval->labeling->n_seq; i++)
+ {
+ const struct spvxml_node *node = v->graph->interval->labeling->seq[i];
+ if (spvdx_is_footnotes (node))
+ {
+ const struct spvdx_footnotes *f = spvdx_cast_footnotes (node);
+ footnotes = spv_series_from_ref (&series_map, f->variable);
+ decode_footnotes (table, f);
+ }
+ }
+ for (size_t i = 0; i < v->n_lf1; i++)
+ {
+ error = decode_label_frame (table, v->lf1[i]);
+ if (error)
+ goto exit;
+ }
+ for (size_t i = 0; i < v->n_lf2; i++)
+ {
+ error = decode_label_frame (table, v->lf2[i]);
+ if (error)
+ goto exit;
+ }
+ if (v->container)
+ for (size_t i = 0; i < v->container->n_label_frame; i++)
+ {
+ error = decode_label_frame (table, v->container->label_frame[i]);
+ if (error)
+ goto exit;
+ }
+ if (v->graph->interval->labeling->style)
+ {
+ area_style_uninit (&table->areas[PIVOT_AREA_DATA]);
+ decode_spvdx_style (v->graph->interval->labeling->style,
+ v->graph->cell_style,
+ &table->areas[PIVOT_AREA_DATA]);
+ }
+
+ /* Decode all of the sourceVariable and derivedVariable */
+ struct spvxml_node **nodes = xmemdup (v->seq, v->n_seq * sizeof *v->seq);
+ size_t n_nodes = v->n_seq;
+ while (n_nodes > 0)
+ {
+ bool progress = false;
+ for (size_t i = 0; i < n_nodes; )
+ {
+ error = (spvdx_is_source_variable (nodes[i])
+ ? decode_spvdx_source_variable (nodes[i], data, &series_map)
+ : decode_spvdx_derived_variable (nodes[i], &series_map));
+ if (!error)
+ {
+ nodes[i] = nodes[--n_nodes];
+ progress = true;
+ }
+ else if (error == &BAD_REFERENCE)
+ i++;
+ else
+ {
+ free (nodes);
+ goto exit;
+ }
+ }
+
+ if (!progress)
+ {
+ free (nodes);
+ error = xasprintf ("Table has %zu variables with circular or "
+ "unresolved references, including variable %s.",
+ n_nodes, nodes[0]->id);
+ goto exit;
+ }
+ }
+ free (nodes);
+
+ const struct spvdx_cross *cross = v->graph->faceting->cross;
+
+ assert (cross->n_seq == 1);
+ const struct spvdx_nest *columns = spvdx_cast_nest (cross->seq[0]);
+ size_t max_columns = columns ? columns->n_vars : 0;
+
+ assert (cross->n_seq2 == 1);
+ const struct spvdx_nest *rows = spvdx_cast_nest (cross->seq2[0]);
+ size_t max_rows = rows ? rows->n_vars : 0;
+
+ size_t max_layers = (v->graph->faceting->n_layers1
+ + v->graph->faceting->n_layers2);
+
+ size_t max_dims = max_columns + max_rows + max_layers;
+ table->dimensions = xnmalloc (max_dims, sizeof *table->dimensions);
+ dim_series = xnmalloc (max_dims, sizeof *dim_series);
+ size_t n_dim_series = 0;
+ add_dimensions (&series_map, columns, PIVOT_AXIS_COLUMN, v, table,
+ dim_series, &n_dim_series, 1);
+ add_dimensions (&series_map, rows, PIVOT_AXIS_ROW, v, table,
+ dim_series, &n_dim_series, max_columns + 1);
+ add_layers (&series_map,
+ v->graph->faceting->layers1, v->graph->faceting->n_layers1,
+ v, table, dim_series, &n_dim_series, max_rows + max_columns + 1);
+ add_layers (&series_map,
+ v->graph->faceting->layers2, v->graph->faceting->n_layers2,
+ v, table, dim_series, &n_dim_series,
+ max_rows + max_columns + v->graph->faceting->n_layers1 + 1);
+
+ struct spv_series *cell = spv_series_find (&series_map, "cell");
+ if (!cell)
+ {
+ error = xstrdup (_("Table lacks cell data."));
+ goto exit;
+ }
+
+ struct spv_series *cell_format = parse_formatting (v, &series_map,
+ &format_map);
+
+ assert (table->n_dimensions == n_dim_series);
+ size_t *dim_indexes = xnmalloc (table->n_dimensions, sizeof *dim_indexes);
+ for (size_t i = 0; i < cell->n_values; i++)
+ {
+ for (size_t j = 0; j < table->n_dimensions; j++)
+ {
+ const struct spv_data_value *value = &dim_series[j]->values[i];
+ const struct pivot_category *cat = find_category (
+ dim_series[j], value->width < 0 ? value->d : value->index);
+ if (!cat)
+ goto skip;
+ dim_indexes[j] = cat->data_index;
+ }
+
+ struct pivot_value *value = pivot_value_from_data_value (
+ &cell->values[i], cell_format ? &cell_format->values[i] : NULL,
+ &format_map);
+ if (footnotes)
+ {
+ const struct spv_data_value *d = &footnotes->values[i];
+ if (d->width >= 0)
+ {
+ const char *p = d->s;
+ while (*p)
+ {
+ char *tail;
+ int idx = strtol (p, &tail, 10);
+ add_footnote (value, idx, table);
+ if (tail == p)
+ break;
+ p = tail;
+ if (*p == ',')
+ p++;
+ }
+ }
+ }
+
+ if (value->type == PIVOT_VALUE_NUMERIC
+ && value->numeric.x == SYSMIS
+ && !value->n_footnotes)
+ {
+ /* Apparently, system-missing values are just empty cells? */
+ pivot_value_destroy (value);
+ }
+ else
+ pivot_table_put (table, dim_indexes, table->n_dimensions, value);
+ skip:;
+ }
+ free (dim_indexes);
+
+ decode_set_cell_properties (table, &series_map, v->graph->facet_layout->scp1,
+ v->graph->facet_layout->n_scp1);
+ decode_set_cell_properties (table, &series_map, v->graph->facet_layout->scp2,
+ v->graph->facet_layout->n_scp2);
+
+ pivot_table_assign_label_depth (table);
+
+ format_map_destroy (&format_map);
+
+exit:
+ free (dim_series);
+ spv_series_destroy (&series_map);
+ if (error)
+ {
+ pivot_table_unref (table);
+ *outp = NULL;
+ }
+ else
+ *outp = table;
+ return error;
+}