X-Git-Url: https://pintos-os.org/cgi-bin/gitweb.cgi?a=blobdiff_plain;f=src%2Foutput%2Fspv%2Fspv-legacy-decoder.c;fp=src%2Foutput%2Fspv%2Fspv-legacy-decoder.c;h=7dcef691ab6adf7d089d3d8b2999e76ad8c8c07f;hb=bcaaee5f0bd21f443c8dcb5f67114e63d43673af;hp=0000000000000000000000000000000000000000;hpb=1abd7f599dd0d773add0a98fa3b612bc15aaf422;p=pspp diff --git a/src/output/spv/spv-legacy-decoder.c b/src/output/spv/spv-legacy-decoder.c new file mode 100644 index 0000000000..7dcef691ab --- /dev/null +++ b/src/output/spv/spv-legacy-decoder.c @@ -0,0 +1,2215 @@ +/* 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 . */ + +#include + +#include "output/spv/spv-legacy-decoder.h" + +#include +#include +#include +#include +#include + +#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 + +#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; +}