Work on support for variable sets.
[pspp] / src / data / format.c
index f323424d0868fbf128629bbf845e7bb94726a813..87a40e2eed32a75785a9ef7302bd86bfcbe95f59 100644 (file)
@@ -101,26 +101,38 @@ fmt_settings_get_style (const struct fmt_settings *settings,
 
 #define OPPOSITE(C) ((C) == ',' ? '.' : ',')
 #define AFFIX(S) { .s = (char *) (S), .width = sizeof (S) - 1 }
-#define NS(PREFIX, SUFFIX, DECIMAL, GROUPING) { \
+#define NS(PREFIX, SUFFIX, DECIMAL, GROUPING, INCLUDE_LEADING_ZERO) {        \
     .neg_prefix = AFFIX ("-"),                  \
     .prefix = AFFIX (PREFIX),                   \
     .suffix = AFFIX (SUFFIX),                   \
     .neg_suffix = AFFIX (""),                   \
     .decimal = DECIMAL,                         \
     .grouping = GROUPING,                       \
+    .include_leading_zero = INCLUDE_LEADING_ZERO \
   }
-#define ANS(DECIMAL, GROUPING) {                        \
-    [FMT_F]      = NS( "",  "", DECIMAL, 0),            \
-    [FMT_E]      = NS( "",  "", DECIMAL, 0),            \
-    [FMT_COMMA]  = NS( "",  "", DECIMAL, GROUPING),     \
-    [FMT_DOT]    = NS( "",  "", GROUPING, DECIMAL),     \
-    [FMT_DOLLAR] = NS("$",  "", DECIMAL, GROUPING),     \
-    [FMT_PCT]    = NS( "", "%", DECIMAL, 0),            \
+#define ANS(DECIMAL, GROUPING, INCLUDE_LEADING_ZERO) {                  \
+    [FMT_F]      = NS( "",  "", DECIMAL, 0, INCLUDE_LEADING_ZERO),      \
+    [FMT_E]      = NS( "",  "", DECIMAL, 0, INCLUDE_LEADING_ZERO),      \
+    [FMT_COMMA]  = NS( "",  "", DECIMAL, GROUPING, INCLUDE_LEADING_ZERO), \
+    [FMT_DOT]    = NS( "",  "", GROUPING, DECIMAL, INCLUDE_LEADING_ZERO), \
+    [FMT_DOLLAR] = NS("$",  "", DECIMAL, GROUPING, false),              \
+    [FMT_PCT]    = NS( "", "%", DECIMAL, 0, false),                     \
   }
+#define ANS2(DECIMAL, GROUPING) {               \
+    ANS(DECIMAL, GROUPING, false),              \
+    ANS(DECIMAL, GROUPING, true),               \
+  }
+
+  /* First index: 0 for ',' decimal point, 1 for '.' decimal point.
+     Second index: 0 for no leading zero, 1 for leading zero.
+     Third index: TYPE.
+  */
+  static const struct fmt_number_style styles[2][2][6] = {
+    ANS2 (',', '.'),
+    ANS2 ('.', ','),
+  };
 
-  static const struct fmt_number_style period_styles[6] = ANS ('.', ',');
-  static const struct fmt_number_style comma_styles[6] = ANS (',', '.');
-  static const struct fmt_number_style default_style = NS ("", "", '.', 0);
+  static const struct fmt_number_style default_style = NS ("", "", '.', 0, false);
 
   switch (type)
     {
@@ -130,9 +142,11 @@ fmt_settings_get_style (const struct fmt_settings *settings,
     case FMT_DOLLAR:
     case FMT_PCT:
     case FMT_E:
-      return (settings->decimal == '.'
-              ? &period_styles[type]
-              : &comma_styles[type]);
+      {
+        int decimal_idx = settings->decimal == '.';
+        int leadzero_idx = settings->include_leading_zero;
+        return &styles[decimal_idx][leadzero_idx][type];
+      }
 
     case FMT_CCA:
     case FMT_CCB:
@@ -185,7 +199,7 @@ struct fmt_spec
 fmt_for_input (enum fmt_type type, int w, int d)
 {
   struct fmt_spec f = { .type = type, .w = w, .d = d };
-  assert (fmt_check_input (&f));
+  assert (fmt_check_input (f));
   return f;
 }
 
@@ -195,29 +209,29 @@ struct fmt_spec
 fmt_for_output (enum fmt_type type, int w, int d)
 {
   struct fmt_spec f = { .type = type, .w = w, .d = d };
-  assert (fmt_check_output (&f));
+  assert (fmt_check_output (f));
   return f;
 }
 
 /* Returns the output format specifier corresponding to input
    format specifier INPUT. */
 struct fmt_spec
-fmt_for_output_from_input (const struct fmt_spec *input,
+fmt_for_output_from_input (struct fmt_spec input,
                            const struct fmt_settings *settings)
 {
   struct fmt_spec output;
 
   assert (fmt_check_input (input));
 
-  output.type = fmt_input_to_output (input->type);
-  output.w = input->w;
+  output.type = fmt_input_to_output (input.type);
+  output.w = input.w;
   if (output.w > fmt_max_output_width (output.type))
     output.w = fmt_max_output_width (output.type);
   else if (output.w < fmt_min_output_width (output.type))
     output.w = fmt_min_output_width (output.type);
-  output.d = input->d;
+  output.d = input.d;
 
-  switch (input->type)
+  switch (input.type)
     {
     case FMT_Z:
       output.w++;
@@ -232,11 +246,11 @@ fmt_for_output_from_input (const struct fmt_spec *input,
     case FMT_PCT:
       {
         const struct fmt_number_style *style =
-         fmt_settings_get_style (settings, input->type);
+         fmt_settings_get_style (settings, input.type);
 
         output.w += fmt_affix_width (style);
-        if (style->grouping != 0 && input->w - input->d >= 3)
-          output.w += (input->w - input->d - 1) / 3;
+        if (style->grouping != 0 && input.w - input.d >= 3)
+          output.w += (input.w - input.d - 1) / 3;
         if (output.d > 0)
           output.w++;
       }
@@ -248,12 +262,12 @@ fmt_for_output_from_input (const struct fmt_spec *input,
       break;
 
     case FMT_E:
-      output.d = MAX (input->d, 3);
-      output.w = MAX (input->w, output.d + 7);
+      output.d = MAX (input.d, 3);
+      output.w = MAX (input.w, output.d + 7);
       break;
 
     case FMT_PIBHEX:
-      output.w = max_digits_for_bytes (input->w / 2) + 1;
+      output.w = max_digits_for_bytes (input.w / 2) + 1;
       break;
 
     case FMT_RB:
@@ -264,12 +278,12 @@ fmt_for_output_from_input (const struct fmt_spec *input,
 
     case FMT_P:
     case FMT_PK:
-      output.w = 2 * input->w + (input->d > 0);
+      output.w = 2 * input.w + (input.d > 0);
       break;
 
     case FMT_IB:
     case FMT_PIB:
-      output.w = max_digits_for_bytes (input->w) + 1;
+      output.w = max_digits_for_bytes (input.w) + 1;
       if (output.d > 0)
         output.w++;
       break;
@@ -285,7 +299,7 @@ fmt_for_output_from_input (const struct fmt_spec *input,
       break;
 
     case FMT_AHEX:
-      output.w = input->w / 2;
+      output.w = input.w / 2;
       break;
 
     case FMT_DATE:
@@ -304,13 +318,13 @@ fmt_for_output_from_input (const struct fmt_spec *input,
       break;
 
     case FMT_MTIME:
-      if (input->d)
-        output.w = MAX (input->w, input->d + 6);
+      if (input.d)
+        output.w = MAX (input.w, input.d + 6);
       break;
 
     case FMT_YMDHMS:
-      if (input->w)
-        output.w = MAX (input->w, input->d + 20);
+      if (input.w)
+        output.w = MAX (input.w, input.d + 20);
       break;
 
     default:
@@ -320,7 +334,7 @@ fmt_for_output_from_input (const struct fmt_spec *input,
   if (output.w > fmt_max_output_width (output.type))
     output.w = fmt_max_output_width (output.type);
 
-  assert (fmt_check_output (&output));
+  assert (fmt_check_output (output));
   return output;
 }
 
@@ -334,183 +348,242 @@ fmt_default_for_width (int width)
           : fmt_for_output (FMT_A, width, 0));
 }
 
-/* Checks whether SPEC is valid for USE and returns nonzero if so.
-   Otherwise, emits an error message and returns zero. */
-bool
-fmt_check (const struct fmt_spec *spec, enum fmt_use use)
+/* Checks whether SPEC is valid for USE and returns NULL if so.  Otherwise,
+   returns a malloc()'d string that describes the error.  The caller must
+   eventually free() the string. */
+char *
+fmt_check__ (struct fmt_spec spec, enum fmt_use use)
 {
   char str[FMT_STRING_LEN_MAX + 1];
   int min_w, max_w, max_d;
 
-  assert (is_fmt_type (spec->type));
+  assert (is_fmt_type (spec.type));
   fmt_to_string (spec, str);
 
-  if (use == FMT_FOR_INPUT && !fmt_usable_for_input (spec->type))
-    {
-      msg (SE, _("Format %s may not be used for input."), str);
-      return false;
-    }
-
-  if (spec->w % fmt_step_width (spec->type))
-    {
-      assert (fmt_step_width (spec->type) == 2);
-      if (use == FMT_FOR_INPUT)
-        msg (SE, _("Input format %s specifies width %d, "
-                   "but %s requires an even width."),
-             str, spec->w, fmt_name (spec->type));
-      else
-        msg (SE, _("Output format %s specifies width %d, "
-                   "but %s requires an even width."),
-             str, spec->w, fmt_name (spec->type));
-      return false;
-    }
+  if (use == FMT_FOR_INPUT && !fmt_usable_for_input (spec.type))
+    return xasprintf (_("Format %s may not be used for input."), str);
 
-  min_w = fmt_min_width (spec->type, use);
-  max_w = fmt_max_width (spec->type, use);
-  if (spec->w < min_w || spec->w > max_w)
+  if (spec.w % fmt_step_width (spec.type))
     {
-      if (use == FMT_FOR_INPUT)
-        msg (SE, _("Input format %s specifies width %d, but "
-                   "%s requires a width between %d and %d."),
-             str, spec->w, fmt_name (spec->type), min_w, max_w);
-      else
-        msg (SE, _("Output format %s specifies width %d, but "
-                   "%s requires a width between %d and %d."),
-             str, spec->w, fmt_name (spec->type), min_w, max_w);
-      return false;
+      assert (fmt_step_width (spec.type) == 2);
+      return (use == FMT_FOR_INPUT
+              ? xasprintf (_("Input format %s specifies width %d, "
+                             "but %s requires an even width."),
+                           str, spec.w, fmt_name (spec.type))
+              : xasprintf (_("Output format %s specifies width %d, "
+                             "but %s requires an even width."),
+                           str, spec.w, fmt_name (spec.type)));
     }
 
-  max_d = fmt_max_decimals (spec->type, spec->w, use);
-  if (!fmt_takes_decimals (spec->type) && spec->d != 0)
-    {
-      if (use == FMT_FOR_INPUT)
-        msg (SE, ngettext ("Input format %s specifies %d decimal place, but "
-                           "%s does not allow any decimals.",
-                           "Input format %s specifies %d decimal places, but "
-                           "%s does not allow any decimals.",
-                           spec->d),
-             str, spec->d, fmt_name (spec->type));
-      else
-        msg (SE, ngettext ("Output format %s specifies %d decimal place, but "
-                           "%s does not allow any decimals.",
+  min_w = fmt_min_width (spec.type, use);
+  max_w = fmt_max_width (spec.type, use);
+  if (spec.w < min_w || spec.w > max_w)
+    return (use == FMT_FOR_INPUT
+            ? xasprintf (_("Input format %s specifies width %d, but "
+                           "%s requires a width between %d and %d."),
+                         str, spec.w, fmt_name (spec.type), min_w, max_w)
+            : xasprintf (_("Output format %s specifies width %d, but "
+                           "%s requires a width between %d and %d."),
+                         str, spec.w, fmt_name (spec.type), min_w, max_w));
+
+  max_d = fmt_max_decimals (spec.type, spec.w, use);
+  if (!fmt_takes_decimals (spec.type) && spec.d != 0)
+    return (use == FMT_FOR_INPUT
+            ? xasprintf (ngettext (
+                           "Input format %s specifies %d decimal "
+                           "place, but %s does not allow any decimals.",
+                           "Input format %s specifies %d decimal "
+                           "places, but %s does not allow any "
+                           "decimals.",
+                           spec.d),
+                         str, spec.d, fmt_name (spec.type))
+            : xasprintf (ngettext (
+                           "Output format %s specifies %d decimal "
+                           "place, but %s does not allow any decimals.",
                            "Output format %s specifies %d decimal places, but "
                            "%s does not allow any decimals.",
-                           spec->d),
-             str, spec->d, fmt_name (spec->type));
-      return false;
-    }
-  else if (spec->d > max_d)
+                           spec.d),
+                         str, spec.d, fmt_name (spec.type)));
+  else if (spec.d > max_d)
     {
       if (max_d > 0)
-        {
-          if (use == FMT_FOR_INPUT)
-            msg (SE, ngettext ("Input format %s specifies %d decimal place, "
-                               "but the given width allows at most "
-                               "%d decimals.",
+        return (use == FMT_FOR_INPUT
+                ? xasprintf (ngettext (
+                               "Input format %s specifies %d decimal place, "
+                               "but width %d allows at most %d decimals.",
                                "Input format %s specifies %d decimal places, "
-                               "but the given width allows at most "
-                               "%d decimals.",
-                               spec->d),
-                 str, spec->d, max_d);
-          else
-            msg (SE, ngettext ("Output format %s specifies %d decimal place, "
-                               "but the given width allows at most "
-                               "%d decimals.",
+                               "but width %d allows at most %d decimals.",
+                               spec.d),
+                             str, spec.d, spec.w, max_d)
+                : xasprintf (ngettext (
+                               "Output format %s specifies %d decimal place, "
+                               "but width %d allows at most %d decimals.",
                                "Output format %s specifies %d decimal places, "
-                               "but the given width allows at most "
-                               "%d decimals.",
-                               spec->d),
-                 str, spec->d, max_d);
-        }
+                               "but width %d allows at most %d decimals.",
+                               spec.d),
+                             str, spec.d, spec.w, max_d));
       else
-        {
-          if (use == FMT_FOR_INPUT)
-            msg (SE, ngettext ("Input format %s specifies %d decimal place, "
-                               "but the given width does not allow "
-                               "for any decimals.",
+        return (use == FMT_FOR_INPUT
+                ? xasprintf (ngettext (
+                               "Input format %s specifies %d decimal place, "
+                               "but width %d does not allow for any decimals.",
                                "Input format %s specifies %d decimal places, "
-                               "but the given width does not allow "
-                               "for any decimals.",
-                               spec->d),
-                 str, spec->d);
-          else
-            msg (SE, ngettext ("Output format %s specifies %d decimal place, "
-                               "but the given width does not allow "
-                               "for any decimals.",
+                               "but width %d does not allow for any decimals.",
+                               spec.d),
+                             str, spec.d, spec.w)
+                : xasprintf (ngettext (
+                               "Output format %s specifies %d decimal place, "
+                               "but width %d does not allow for any decimals.",
                                "Output format %s specifies %d decimal places, "
-                               "but the given width does not allow "
-                               "for any decimals.",
-                               spec->d),
-                 str, spec->d);
-        }
+                               "but width %d does not allow for any decimals.",
+                               spec.d),
+                             str, spec.d, spec.w));
+    }
+
+  return NULL;
+}
+
+char *
+fmt_check_input__ (struct fmt_spec spec)
+{
+  return fmt_check__ (spec, FMT_FOR_INPUT);
+}
+
+char *
+fmt_check_output__ (struct fmt_spec spec)
+{
+  return fmt_check__ (spec, FMT_FOR_OUTPUT);
+}
+
+static bool
+error_to_bool (char *error)
+{
+  if (error)
+    {
+      free (error);
       return false;
     }
+  else
+    return true;
+}
 
-  return true;
+/* Returns true if SPEC is valid for USE, false otherwise. */
+bool
+fmt_check (struct fmt_spec spec, enum fmt_use use)
+{
+  return error_to_bool (fmt_check__ (spec, use));
 }
 
-/* Checks whether SPEC is valid as an input format and returns
-   nonzero if so.  Otherwise, emits an error message and returns
-   zero. */
+/* Returns true if SPEC is valid as an input format, otherwise false. */
 bool
-fmt_check_input (const struct fmt_spec *spec)
+fmt_check_input (struct fmt_spec spec)
 {
   return fmt_check (spec, FMT_FOR_INPUT);
 }
 
-/* Checks whether SPEC is valid as an output format and returns
-   true if so.  Otherwise, emits an error message and returns false. */
+/* Returnst true SPEC is valid as an output format, false otherwise. */
 bool
-fmt_check_output (const struct fmt_spec *spec)
+fmt_check_output (struct fmt_spec spec)
 {
   return fmt_check (spec, FMT_FOR_OUTPUT);
 }
 
-/* Checks that FORMAT is appropriate for a variable of the given
-   VAR_TYPE and returns true if so.  Otherwise returns false and
-   emits an error message. */
-bool
-fmt_check_type_compat (const struct fmt_spec *format, enum val_type var_type)
+/* Checks that FORMAT is appropriate for a variable of the given VAR_TYPE and
+   returns NULL if so.  Otherwise returns a malloc()'d error message that the
+   caller must eventually free().  VARNAME is optional and only used in the
+   error message.*/
+char *
+fmt_check_type_compat__ (struct fmt_spec format, const char *varname,
+                         enum val_type var_type)
 {
   assert (val_type_is_valid (var_type));
-  if ((var_type == VAL_STRING) != (fmt_is_string (format->type) != 0))
+  if ((var_type == VAL_STRING) != (fmt_is_string (format.type) != 0))
     {
       char str[FMT_STRING_LEN_MAX + 1];
-      msg (SE, _("%s variables are not compatible with %s format %s."),
-           var_type == VAL_STRING ? _("String") : _("Numeric"),
-           var_type == VAL_STRING ? _("numeric") : _("string"),
-           fmt_to_string (format, str));
-      return false;
+      fmt_to_string (format, str);
+      if (var_type == VAL_STRING)
+        {
+          if (varname)
+            return xasprintf (_("String variable %s is not compatible with "
+                                "numeric format %s."), varname, str);
+          else
+            return xasprintf (_("String variables are not compatible with "
+                                "numeric format %s."), str);
+        }
+      else
+        {
+          if (varname)
+            return xasprintf (_("Numeric variable %s is not compatible with "
+                                "string format %s."), varname, str);
+          else
+            return xasprintf (_("Numeric variables are not compatible with "
+                                "string format %s."), str);
+        }
     }
-  return true;
+  return NULL;
 }
 
-/* Checks that FORMAT is appropriate for a variable of the given
-   WIDTH and returns true if so.  Otherwise returns false and
-   emits an error message. */
+/* Returns FORMAT is appropriate for a variable of the given VAR_TYPE and
+   returns true if so, otherwise false. */
 bool
-fmt_check_width_compat (const struct fmt_spec *format, int width)
+fmt_check_type_compat (struct fmt_spec format, enum val_type var_type)
 {
-  if (!fmt_check_type_compat (format, val_type_from_width (width)))
-    return false;
+  return error_to_bool (fmt_check_type_compat__ (format, NULL, var_type));
+}
+
+/* Checks that FORMAT is appropriate for a variable of the given WIDTH and
+   returns NULL if so.  Otherwise returns a malloc()'d error message that the
+   caller must eventually free().  VARNAME is optional and only used in the
+   error message. */
+char *
+fmt_check_width_compat__ (struct fmt_spec format, const char *varname,
+                          int width)
+{
+  char *error = fmt_check_type_compat__ (format, varname,
+                                         val_type_from_width (width));
+  if (error)
+    return error;
+
   if (fmt_var_width (format) != width)
     {
-      char str[FMT_STRING_LEN_MAX + 1];
-      msg (SE, _("String variable with width %d is not compatible with "
-                 "format %s."),
-           width, fmt_to_string (format, str));
-      return false;
+      char format_str[FMT_STRING_LEN_MAX + 1];
+      fmt_to_string (format, format_str);
+
+      char better_str[FMT_STRING_LEN_MAX + 1];
+      if (format.type == FMT_A)
+        snprintf (better_str, sizeof better_str, "A%d", width);
+      else
+        snprintf (better_str, sizeof better_str, "AHEX%d", width * 2);
+
+      if (varname)
+        return xasprintf (_("String variable %s with width %d is not "
+                            "compatible with format %s.  "
+                            "Use format %s instead."),
+                          varname, width, format_str, better_str);
+      else
+        return xasprintf (_("String variable with width %d is not compatible "
+                            "with format %s.  Use format %s instead."),
+                          width, format_str, better_str);
     }
-  return true;
+
+  return NULL;
+}
+
+/* Checks that FORMAT is appropriate for a variable of the given WIDTH and
+   returns true if so, otherwise false. */
+bool
+fmt_check_width_compat (struct fmt_spec format, int width)
+{
+  return error_to_bool (fmt_check_width_compat__ (format, NULL, width));
 }
 
 /* Returns the width corresponding to FORMAT.  The return value
    is the width of the `union value's required by FORMAT. */
 int
-fmt_var_width (const struct fmt_spec *format)
+fmt_var_width (struct fmt_spec format)
 {
-  return (format->type == FMT_AHEX ? format->w / 2
-          : format->type == FMT_A ? format->w
+  return (format.type == FMT_AHEX ? format.w / 2
+          : format.type == FMT_A ? format.w
           : 0);
 }
 
@@ -521,23 +594,23 @@ fmt_var_width (const struct fmt_spec *format)
    even if F's format type does not allow decimals, to allow
    accurately presenting incorrect formats to the user. */
 char *
-fmt_to_string (const struct fmt_spec *f, char buffer[FMT_STRING_LEN_MAX + 1])
+fmt_to_string (struct fmt_spec f, char buffer[FMT_STRING_LEN_MAX + 1])
 {
-  if (fmt_takes_decimals (f->type) || f->d > 0)
+  if (fmt_takes_decimals (f.type) || f.d > 0)
     snprintf (buffer, FMT_STRING_LEN_MAX + 1,
-              "%s%d.%d", fmt_name (f->type), f->w, f->d);
+              "%s%d.%d", fmt_name (f.type), f.w, f.d);
   else
     snprintf (buffer, FMT_STRING_LEN_MAX + 1,
-              "%s%d", fmt_name (f->type), f->w);
+              "%s%d", fmt_name (f.type), f.w);
   return buffer;
 }
 
 /* Returns true if A and B are identical formats,
    false otherwise. */
 bool
-fmt_equal (const struct fmt_spec *a, const struct fmt_spec *b)
+fmt_equal (struct fmt_spec a, struct fmt_spec b)
 {
-  return a->type == b->type && a->w == b->w && a->d == b->d;
+  return a.type == b.type && a.w == b.w && a.d == b.d;
 }
 
 /* Adjusts FMT to be valid for a value of the given WIDTH if necessary.
@@ -966,22 +1039,18 @@ fmt_from_u32 (uint32_t u32, int width, bool loose, struct fmt_spec *f)
   uint8_t w = u32 >> 8;
   uint8_t d = u32;
 
-  msg_disable ();
-  f->w = w;
-  f->d = d;
-  bool ok = fmt_from_io (raw_type, &f->type);
-  if (ok)
-    {
-      if (loose)
-        fmt_fix_output (f);
-      else
-        ok = fmt_check_output (f);
-    }
-  if (ok)
-    ok = fmt_check_width_compat (f, width);
-  msg_enable ();
+  enum fmt_type type;
+  if (!fmt_from_io (raw_type, &type))
+    return false;
+
+  *f = (struct fmt_spec) { .type = type, .w = w, .d = d };
+
+  if (loose)
+    fmt_fix_output (f);
+  else if (!fmt_check_output (*f))
+    return false;
 
-  return ok;
+  return fmt_check_width_compat (*f, width);
 }
 
 /* Returns true if TYPE may be used as an input format,
@@ -1270,6 +1339,7 @@ fmt_number_style_from_string (const char *s)
     .neg_suffix = neg_suffix,
     .decimal = grouping == '.' ? ',' : '.',
     .grouping = grouping,
+    .include_leading_zero = false,
     .extra_bytes = extra_bytes,
   };
   return style;