expressions: Major work to improve error messages.
[pspp] / src / data / format.c
index 1bab775ed6ea533f25d76ff4ffbcc406b23c88bc..79c6240a9125423ec986148a82d067701c4e048c 100644 (file)
@@ -1,5 +1,5 @@
 /* PSPP - a program for statistical analysis.
-   Copyright (C) 1997-9, 2000, 2006, 2010, 2011 Free Software Foundation, Inc.
+   Copyright (C) 1997-9, 2000, 2006, 2010, 2011, 2012 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
@@ -20,6 +20,7 @@
 
 #include <ctype.h>
 #include <stdlib.h>
+#include <time.h>
 #include <uniwidth.h>
 
 #include "data/identifier.h"
 #include "libpspp/misc.h"
 #include "libpspp/str.h"
 
+#include "gl/c-strcase.h"
 #include "gl/minmax.h"
 #include "gl/xalloc.h"
+#include "gl/xmemdup0.h"
 
 #include "gettext.h"
 #define _(msgid) gettext (msgid)
 
-struct fmt_settings
-  {
-    struct fmt_number_style styles[FMT_NUMBER_OF_FORMATS];
-  };
-
 bool is_fmt_type (enum fmt_type);
 
-static bool valid_width (enum fmt_type, int width, bool for_input);
+static bool valid_width (enum fmt_type, int width, enum fmt_use);
 
 static int max_digits_for_bytes (int bytes);
-static void fmt_clamp_width (struct fmt_spec *, bool for_input);
-static void fmt_clamp_decimals (struct fmt_spec *, bool for_input);
+static void fmt_clamp_width (struct fmt_spec *, enum fmt_use);
+static void fmt_clamp_decimals (struct fmt_spec *, enum fmt_use);
 
-static void fmt_affix_set (struct fmt_affix *, const char *);
-static void fmt_affix_free (struct fmt_affix *);
-
-static void fmt_number_style_init (struct fmt_number_style *);
-static void fmt_number_style_clone (struct fmt_number_style *,
-                                    const struct fmt_number_style *);
-static void fmt_number_style_destroy (struct fmt_number_style *);
-
-/* Creates and returns a new struct fmt_settings with default format styles. */
-struct fmt_settings *
-fmt_settings_create (void)
+void
+fmt_settings_init (struct fmt_settings *settings)
 {
-  struct fmt_settings *settings;
-  int t;
-
-  settings = xzalloc (sizeof *settings);
-  for (t = 0 ; t < FMT_NUMBER_OF_FORMATS ; ++t )
-    fmt_number_style_init (&settings->styles[t]);
-  fmt_settings_set_decimal (settings, '.');
-
-  return settings;
+  *settings = (struct fmt_settings) FMT_SETTINGS_INIT;
 }
 
-/* Destroys SETTINGS. */
 void
-fmt_settings_destroy (struct fmt_settings *settings)
+fmt_settings_uninit (struct fmt_settings *settings)
 {
-  if (settings != NULL)
-    {
-      int t;
-
-      for (t = 0 ; t < FMT_NUMBER_OF_FORMATS ; ++t )
-        fmt_number_style_destroy (&settings->styles[t]);
-
-      free (settings->styles);
-    }
+  for (int i = 0; i < FMT_N_CCS; i++)
+    fmt_number_style_destroy (settings->ccs[i]);
 }
 
-/* Returns a copy of SETTINGS. */
-struct fmt_settings *
-fmt_settings_clone (const struct fmt_settings *old)
+struct fmt_settings
+fmt_settings_copy (const struct fmt_settings *old)
 {
-  struct fmt_settings *new;
-  int t;
-
-  new = xmalloc (sizeof *new);
-  for (t = 0 ; t < FMT_NUMBER_OF_FORMATS ; ++t )
-    fmt_number_style_clone (&new->styles[t], &old->styles[t]);
-
+  struct fmt_settings new = *old;
+  for (int i = 0; i < FMT_N_CCS; i++)
+    new.ccs[i] = fmt_number_style_clone (old->ccs[i]);
   return new;
 }
 
+static size_t
+fmt_type_to_cc_index (enum fmt_type type)
+{
+  switch (type)
+    {
+    case FMT_CCA: return 0;
+    case FMT_CCB: return 1;
+    case FMT_CCC: return 2;
+    case FMT_CCD: return 3;
+    case FMT_CCE: return 4;
+    default: NOT_REACHED ();
+    }
+}
+
 /* Returns the number formatting style associated with the given
    format TYPE. */
 const struct fmt_number_style *
 fmt_settings_get_style (const struct fmt_settings *settings,
                         enum fmt_type type)
 {
-  assert (is_fmt_type (type));
-  return &settings->styles[type];
-}
-
-/* Sets the number style for TYPE to have the given DECIMAL and GROUPING
-   characters, negative prefix NEG_PREFIX, prefix PREFIX, suffix SUFFIX, and
-   negative suffix NEG_SUFFIX.  All of the strings are UTF-8 encoded. */
-void
-fmt_settings_set_style (struct fmt_settings *settings, enum fmt_type type,
-                        char decimal, char grouping,
-                        const char *neg_prefix, const char *prefix,
-                        const char *suffix, const char *neg_suffix)
-{
-  struct fmt_number_style *style = &settings->styles[type];
-  int total_bytes, total_width;
+  verify (FMT_F < 6);
+  verify (FMT_COMMA < 6);
+  verify (FMT_DOT < 6);
+  verify (FMT_DOLLAR < 6);
+  verify (FMT_PCT < 6);
+  verify (FMT_E < 6);
+
+#define OPPOSITE(C) ((C) == ',' ? '.' : ',')
+#define AFFIX(S) { .s = (char *) (S), .width = sizeof (S) - 1 }
+#define NS(PREFIX, SUFFIX, DECIMAL, GROUPING) { \
+    .neg_prefix = AFFIX ("-"),                  \
+    .prefix = AFFIX (PREFIX),                   \
+    .suffix = AFFIX (SUFFIX),                   \
+    .neg_suffix = AFFIX (""),                   \
+    .decimal = DECIMAL,                         \
+    .grouping = GROUPING,                       \
+  }
+#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),            \
+  }
+
+  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);
 
-  assert (grouping == '.' || grouping == ',' || grouping == 0);
-  assert (decimal == '.' || decimal == ',');
-  assert (decimal != grouping);
+  switch (type)
+    {
+    case FMT_F:
+    case FMT_COMMA:
+    case FMT_DOT:
+    case FMT_DOLLAR:
+    case FMT_PCT:
+    case FMT_E:
+      return (settings->decimal == '.'
+              ? &period_styles[type]
+              : &comma_styles[type]);
 
-  fmt_number_style_destroy (style);
+    case FMT_CCA:
+    case FMT_CCB:
+    case FMT_CCC:
+    case FMT_CCD:
+    case FMT_CCE:
+      {
+        size_t idx = fmt_type_to_cc_index (type);
+        return settings->ccs[idx] ? settings->ccs[idx] : &default_style;
+      }
 
-  fmt_affix_set (&style->neg_prefix, neg_prefix);
-  fmt_affix_set (&style->prefix, prefix);
-  fmt_affix_set (&style->suffix, suffix);
-  fmt_affix_set (&style->neg_suffix, neg_suffix);
-  style->decimal = decimal;
-  style->grouping = grouping;
+    default:
+      return &default_style;
+    }
+}
 
-  total_bytes = (strlen (neg_prefix) + strlen (prefix)
-                 + strlen (suffix) + strlen (neg_suffix));
-  total_width = (style->neg_prefix.width + style->prefix.width
-                 + style->suffix.width + style->neg_suffix.width);
-  style->extra_bytes = MAX (0, total_bytes - total_width);
+static int
+default_epoch (void)
+{
+  static int epoch = 0;
+  if (!epoch)
+    {
+      time_t t = time (0);
+      struct tm *tm = localtime (&t);
+      epoch = (tm != NULL ? tm->tm_year + 1900 : 2000) - 69;
+    }
+  return epoch;
 }
 
-/* Sets the decimal point character for the settings in S to DECIMAL.
+int
+fmt_settings_get_epoch (const struct fmt_settings *settings)
+{
+  return !settings->epoch ? default_epoch () : settings->epoch;
+}
 
-   This has no effect on custom currency formats. */
 void
-fmt_settings_set_decimal (struct fmt_settings *s, char decimal)
+fmt_settings_set_cc (struct fmt_settings *settings, enum fmt_type type,
+                     struct fmt_number_style *style)
 {
-  int grouping = decimal == '.' ? ',' : '.';
-  assert (decimal == '.' || decimal == ',');
+  size_t idx = fmt_type_to_cc_index (type);
 
-  fmt_settings_set_style (s, FMT_F,      decimal,        0, "-",  "",  "", "");
-  fmt_settings_set_style (s, FMT_E,      decimal,        0, "-",  "",  "", "");
-  fmt_settings_set_style (s, FMT_COMMA,  decimal, grouping, "-",  "",  "", "");
-  fmt_settings_set_style (s, FMT_DOT,   grouping,  decimal, "-",  "",  "", "");
-  fmt_settings_set_style (s, FMT_DOLLAR, decimal, grouping, "-", "$",  "", "");
-  fmt_settings_set_style (s, FMT_PCT,    decimal,        0, "-",  "", "%", "");
+  fmt_number_style_destroy (settings->ccs[idx]);
+  settings->ccs[idx] = style;
 }
 
+\f
 /* Returns an input format specification with type TYPE, width W,
    and D decimals. */
 struct fmt_spec
 fmt_for_input (enum fmt_type type, int w, int d)
 {
-  struct fmt_spec f;
-  f.type = type;
-  f.w = w;
-  f.d = d;
+  struct fmt_spec f = { .type = type, .w = w, .d = d };
   assert (fmt_check_input (&f));
   return f;
 }
@@ -181,10 +194,7 @@ fmt_for_input (enum fmt_type type, int w, int d)
 struct fmt_spec
 fmt_for_output (enum fmt_type type, int w, int d)
 {
-  struct fmt_spec f;
-  f.type = type;
-  f.w = w;
-  f.d = d;
+  struct fmt_spec f = { .type = type, .w = w, .d = d };
   assert (fmt_check_output (&f));
   return f;
 }
@@ -192,7 +202,8 @@ fmt_for_output (enum fmt_type type, int w, int d)
 /* 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 (const struct fmt_spec *input,
+                           const struct fmt_settings *settings)
 {
   struct fmt_spec output;
 
@@ -221,7 +232,7 @@ fmt_for_output_from_input (const struct fmt_spec *input)
     case FMT_PCT:
       {
         const struct fmt_number_style *style =
-         settings_get_style (input->type);
+         fmt_settings_get_style (settings, input->type);
 
         output.w += fmt_affix_width (style);
         if (style->grouping != 0 && input->w - input->d >= 3)
@@ -292,6 +303,16 @@ fmt_for_output_from_input (const struct fmt_spec *input)
     case FMT_MONTH:
       break;
 
+    case FMT_MTIME:
+      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);
+      break;
+
     default:
       NOT_REACHED ();
     }
@@ -313,74 +334,128 @@ fmt_default_for_width (int width)
           : fmt_for_output (FMT_A, width, 0));
 }
 
-/* Checks whether SPEC is valid as an input format (if FOR_INPUT)
-   or an output format (otherwise) and returns nonzero if so.
-   Otherwise, emits an error message and returns zero. */
-bool
-fmt_check (const struct fmt_spec *spec, bool for_input)
+/* 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__ (const struct fmt_spec *spec, enum fmt_use use)
 {
-  const char *io_fmt = for_input ? _("Input format") : _("Output format");
   char str[FMT_STRING_LEN_MAX + 1];
   int min_w, max_w, max_d;
 
   assert (is_fmt_type (spec->type));
   fmt_to_string (spec, str);
 
-  if (for_input && !fmt_usable_for_input (spec->type))
-    {
-      msg (SE, _("Format %s may not be used for input."), str);
-      return false;
-    }
+  if (use == FMT_FOR_INPUT && !fmt_usable_for_input (spec->type))
+    return xasprintf (_("Format %s may not be used for input."), str);
 
   if (spec->w % fmt_step_width (spec->type))
     {
       assert (fmt_step_width (spec->type) == 2);
-      msg (SE, _("%s specifies width %d, but %s requires an even width."),
-           str, spec->w, fmt_name (spec->type));
-      return false;
+      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)));
     }
 
-  min_w = fmt_min_width (spec->type, for_input);
-  max_w = fmt_max_width (spec->type, for_input);
+  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)
-    {
-      msg (SE, _("%s %s specifies width %d, but "
-                 "%s requires a width between %d and %d."),
-           io_fmt, str, spec->w, fmt_name (spec->type), min_w, max_w);
-      return false;
-    }
-
-  max_d = fmt_max_decimals (spec->type, spec->w, for_input);
+    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)
-    {
-      msg (SE, ngettext ("%s %s specifies %d decimal place, but "
-                         "%s does not allow any decimals.",
-                         "%s %s specifies %d decimal places, but "
-                         "%s does not allow any decimals.",
-                         spec->d),
-           io_fmt, str, spec->d, fmt_name (spec->type));
-      return false;
-    }
+    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)));
   else if (spec->d > max_d)
     {
       if (max_d > 0)
-        msg (SE, ngettext ("%s %s specifies %d decimal place, but "
-                           "the given width allows at most %d decimals.",
-                           "%s %s specifies %d decimal places, but "
-                           "the given width allows at most %d decimals.",
-                           spec->d),
-             io_fmt, str, spec->d, max_d);
+        return (use == FMT_FOR_INPUT
+                ? xasprintf (ngettext (
+                               "Input format %s specifies %d decimal place, "
+                               "but the given width 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)
+                : xasprintf (ngettext (
+                               "Output format %s specifies %d decimal place, "
+                               "but the given width 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));
       else
-        msg (SE, ngettext ("%s %s specifies %d decimal place, but "
-                           "the given width does not allow for any decimals.",
-                           "%s %s specifies %d decimal places, but "
-                           "the given width does not allow for any decimals.",
-                           spec->d),
-             io_fmt, str, spec->d);
+        return (use == FMT_FOR_INPUT
+                ? xasprintf (ngettext (
+                               "Input format %s specifies %d decimal place, "
+                               "but the given width 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)
+                : xasprintf (ngettext (
+                               "Output format %s specifies %d decimal place, "
+                               "but the given width 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));
+    }
+
+  return NULL;
+}
+
+static bool
+fmt_emit_and_free_error (char *error)
+{
+  if (error)
+    {
+      msg (SE, "%s", error);
+      free (error);
       return false;
     }
+  else
+    return true;
+}
 
-  return true;
+/* Checks whether SPEC is valid for USE and returns nonzero if so.  Otherwise,
+   emits an error message for the current source location and returns zero. */
+bool
+fmt_check (const struct fmt_spec *spec, enum fmt_use use)
+{
+  return fmt_emit_and_free_error (fmt_check__ (spec, use));
 }
 
 /* Checks whether SPEC is valid as an input format and returns
@@ -389,7 +464,7 @@ fmt_check (const struct fmt_spec *spec, bool for_input)
 bool
 fmt_check_input (const struct fmt_spec *spec)
 {
-  return fmt_check (spec, true);
+  return fmt_check (spec, FMT_FOR_INPUT);
 }
 
 /* Checks whether SPEC is valid as an output format and returns
@@ -397,7 +472,25 @@ fmt_check_input (const struct fmt_spec *spec)
 bool
 fmt_check_output (const struct fmt_spec *spec)
 {
-  return fmt_check (spec, false);
+  return fmt_check (spec, FMT_FOR_OUTPUT);
+}
+
+/* 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
+   calelr must eventually free(). */
+char *
+fmt_check_type_compat__ (const struct fmt_spec *format, enum val_type var_type)
+{
+  assert (val_type_is_valid (var_type));
+  if ((var_type == VAL_STRING) != (fmt_is_string (format->type) != 0))
+    {
+      char str[FMT_STRING_LEN_MAX + 1];
+      return xasprintf (_("%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 NULL;
 }
 
 /* Checks that FORMAT is appropriate for a variable of the given
@@ -406,17 +499,28 @@ fmt_check_output (const struct fmt_spec *spec)
 bool
 fmt_check_type_compat (const struct fmt_spec *format, enum val_type var_type)
 {
-  assert (val_type_is_valid (var_type));
-  if ((var_type == VAL_STRING) != (fmt_is_string (format->type) != 0))
+  return fmt_emit_and_free_error (fmt_check_type_compat__ (format, 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
+   calelr must eventually free(). */
+char *
+fmt_check_width_compat__ (const struct fmt_spec *format, int width)
+{
+  char *error = fmt_check_type_compat__ (format, val_type_from_width (width));
+  if (error)
+    return error;
+
+  if (fmt_var_width (format) != width)
     {
       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;
+      return xasprintf (_("String variable with width %d is not compatible "
+                          "with format %s."),
+                        width, fmt_to_string (format, str));
     }
-  return true;
+
+  return NULL;
 }
 
 /* Checks that FORMAT is appropriate for a variable of the given
@@ -425,17 +529,7 @@ fmt_check_type_compat (const struct fmt_spec *format, enum val_type var_type)
 bool
 fmt_check_width_compat (const struct fmt_spec *format, int width)
 {
-  if (!fmt_check_type_compat (format, val_type_from_width (width)))
-    return false;
-  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;
-    }
-  return true;
+  return fmt_emit_and_free_error (fmt_check_width_compat__ (format, width));
 }
 
 /* Returns the width corresponding to FORMAT.  The return value
@@ -474,8 +568,10 @@ fmt_equal (const struct fmt_spec *a, const struct fmt_spec *b)
   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. */
-void
+/* Adjusts FMT to be valid for a value of the given WIDTH if necessary.
+   If nothing needed to be changed the return value is false
+ */
+bool
 fmt_resize (struct fmt_spec *fmt, int width)
 {
   if ((width > 0) != fmt_is_string (fmt->type))
@@ -493,31 +589,31 @@ fmt_resize (struct fmt_spec *fmt, int width)
   else
     {
       /* Still numeric. */
+      return false;
     }
+  return true;
 }
 
-/* Adjusts FMT's width and decimal places to be valid for an
-   input format (if FOR_INPUT) or an output format (if
-   !FOR_INPUT).  */
+/* Adjusts FMT's width and decimal places to be valid for USE.  */
 void
-fmt_fix (struct fmt_spec *fmt, bool for_input)
+fmt_fix (struct fmt_spec *fmt, enum fmt_use use)
 {
   /* Clamp width to those allowed by format. */
-  fmt_clamp_width (fmt, for_input);
+  fmt_clamp_width (fmt, use);
 
   /* If FMT has more decimal places than allowed, attempt to increase FMT's
      width until that number of decimal places can be achieved. */
-  if (fmt->d > fmt_max_decimals (fmt->type, fmt->w, for_input)
+  if (fmt->d > fmt_max_decimals (fmt->type, fmt->w, use)
       && fmt_takes_decimals (fmt->type))
     {
-      int max_w = fmt_max_width (fmt->type, for_input);
+      int max_w = fmt_max_width (fmt->type, use);
       for (; fmt->w < max_w; fmt->w++)
-        if (fmt->d <= fmt_max_decimals (fmt->type, fmt->w, for_input))
+        if (fmt->d <= fmt_max_decimals (fmt->type, fmt->w, use))
           break;
     }
 
   /* Clamp decimals to those allowed by format and width. */
-  fmt_clamp_decimals (fmt, for_input);
+  fmt_clamp_decimals (fmt, use);
 }
 
 /* Adjusts FMT's width and decimal places to be valid for an
@@ -525,7 +621,7 @@ fmt_fix (struct fmt_spec *fmt, bool for_input)
 void
 fmt_fix_input (struct fmt_spec *fmt)
 {
-  fmt_fix (fmt, true);
+  fmt_fix (fmt, FMT_FOR_INPUT);
 }
 
 /* Adjusts FMT's width and decimal places to be valid for an
@@ -533,27 +629,27 @@ fmt_fix_input (struct fmt_spec *fmt)
 void
 fmt_fix_output (struct fmt_spec *fmt)
 {
-  fmt_fix (fmt, false);
+  fmt_fix (fmt, FMT_FOR_OUTPUT);
 }
 
 /* Sets FMT's width to WIDTH (or the nearest width allowed by FMT's type) and
    reduces its decimal places as necessary (if necessary) for that width.  */
 void
-fmt_change_width (struct fmt_spec *fmt, int width, bool for_input)
+fmt_change_width (struct fmt_spec *fmt, int width, enum fmt_use use)
 {
   fmt->w = width;
-  fmt_clamp_width (fmt, for_input);
-  fmt_clamp_decimals (fmt, for_input);
+  fmt_clamp_width (fmt, use);
+  fmt_clamp_decimals (fmt, use);
 }
 
 /* Sets FMT's decimal places to DECIMALS (or the nearest number of decimal
    places allowed by FMT's type) and increases its width as necessary (if
    necessary) for that number of decimal places.  */
 void
-fmt_change_decimals (struct fmt_spec *fmt, int decimals, bool for_input)
+fmt_change_decimals (struct fmt_spec *fmt, int decimals, enum fmt_use use)
 {
   fmt->d = decimals;
-  fmt_fix (fmt, for_input);
+  fmt_fix (fmt, use);
 }
 \f
 /* Describes a display format. */
@@ -583,7 +679,7 @@ fmt_from_name (const char *name, enum fmt_type *type)
   int i;
 
   for (i = 0; i < FMT_NUMBER_OF_FORMATS; i++)
-    if (!strcasecmp (name, get_fmt_desc (i)->name))
+    if (!c_strcasecmp (name, get_fmt_desc (i)->name))
       {
         *type = i;
         return true;
@@ -599,20 +695,20 @@ fmt_takes_decimals (enum fmt_type type)
   return fmt_max_output_decimals (type, fmt_max_output_width (type)) > 0;
 }
 
-/* Returns the minimum width of the given format TYPE,
-   for input if FOR_INPUT is true,
-   for output otherwise. */
+/* Returns the minimum width of the given format TYPE for the given USE. */
 int
-fmt_min_width (enum fmt_type type, bool for_input)
+fmt_min_width (enum fmt_type type, enum fmt_use use)
 {
-  return for_input ? fmt_min_input_width (type) : fmt_min_output_width (type);
+  return (use == FMT_FOR_INPUT
+          ? fmt_min_input_width (type)
+          : fmt_min_output_width (type));
 }
 
 /* Returns the maximum width of the given format TYPE,
    for input if FOR_INPUT is true,
    for output otherwise. */
 int
-fmt_max_width (enum fmt_type type, bool for_input UNUSED)
+fmt_max_width (enum fmt_type type, enum fmt_use use UNUSED)
 {
   /* Maximum width is actually invariant of whether the format is
      for input or output, so FOR_INPUT is unused. */
@@ -642,11 +738,9 @@ fmt_max_width (enum fmt_type type, bool for_input UNUSED)
 }
 
 /* Returns the maximum number of decimal places allowed for the
-   given format TYPE with a width of WIDTH places,
-   for input if FOR_INPUT is true,
-   for output otherwise. */
+   given format TYPE with a width of WIDTH places, for the given USE. */
 int
-fmt_max_decimals (enum fmt_type type, int width, bool for_input)
+fmt_max_decimals (enum fmt_type type, int width, enum fmt_use use)
 {
   int max_d;
 
@@ -655,16 +749,16 @@ fmt_max_decimals (enum fmt_type type, int width, bool for_input)
     case FMT_F:
     case FMT_COMMA:
     case FMT_DOT:
-      max_d = for_input ? width : width - 1;
+      max_d = use == FMT_FOR_INPUT ? width : width - 1;
       break;
 
     case FMT_DOLLAR:
     case FMT_PCT:
-      max_d = for_input ? width : width - 2;
+      max_d = use == FMT_FOR_INPUT ? width : width - 2;
       break;
 
     case FMT_E:
-      max_d = for_input ? width : width - 7;
+      max_d = use == FMT_FOR_INPUT ? width : width - 7;
       break;
 
     case FMT_CCA:
@@ -672,7 +766,7 @@ fmt_max_decimals (enum fmt_type type, int width, bool for_input)
     case FMT_CCC:
     case FMT_CCD:
     case FMT_CCE:
-      assert (!for_input);
+      assert (use == FMT_FOR_OUTPUT);
       max_d = width - 1;
       break;
 
@@ -718,6 +812,14 @@ fmt_max_decimals (enum fmt_type type, int width, bool for_input)
       max_d = width - 21;
       break;
 
+    case FMT_YMDHMS:
+      max_d = width - 20;
+      break;
+
+    case FMT_MTIME:
+      max_d = width - 6;
+      break;
+
     case FMT_TIME:
       max_d = width - 9;
       break;
@@ -757,7 +859,7 @@ fmt_min_input_width (enum fmt_type type)
 int
 fmt_max_input_width (enum fmt_type type)
 {
-  return fmt_max_width (type, true);
+  return fmt_max_width (type, FMT_FOR_INPUT);
 }
 
 /* Returns the maximum number of decimal places allowed in an
@@ -766,7 +868,7 @@ int
 fmt_max_input_decimals (enum fmt_type type, int width)
 {
   assert (valid_width (type, width, true));
-  return fmt_max_decimals (type, width, true);
+  return fmt_max_decimals (type, width, FMT_FOR_INPUT);
 }
 
 /* Returns the minimum acceptable width for an output field
@@ -782,7 +884,7 @@ fmt_min_output_width (enum fmt_type type)
 int
 fmt_max_output_width (enum fmt_type type)
 {
-  return fmt_max_width (type, false);
+  return fmt_max_width (type, FMT_FOR_OUTPUT);
 }
 
 /* Returns the maximum number of decimal places allowed in an
@@ -791,7 +893,7 @@ int
 fmt_max_output_decimals (enum fmt_type type, int width)
 {
   assert (valid_width (type, width, false));
-  return fmt_max_decimals (type, width, false);
+  return fmt_max_decimals (type, width, FMT_FOR_OUTPUT);
 }
 
 /* Returns the width step for a field formatted with the given
@@ -876,6 +978,40 @@ fmt_from_io (int io, enum fmt_type *fmt_type)
     }
 }
 
+/* Translate U32, which is in the form found in SAV and SPV files, into a
+   format specification, and stores the new specification in *F.
+
+   If LOOSE is false, checks that the format specification is appropriate as an
+   output format for a variable with the given WIDTH and reports an error if
+   not.  If LOOSE is true, instead adjusts the format's width and decimals as
+   necessary to be suitable.
+
+   Return true if successful, false if there was an error.. */
+bool
+fmt_from_u32 (uint32_t u32, int width, bool loose, struct fmt_spec *f)
+{
+  uint8_t raw_type = u32 >> 16;
+  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 ();
+
+  return ok;
+}
+
 /* Returns true if TYPE may be used as an input format,
    false otherwise. */
 bool
@@ -885,38 +1021,91 @@ fmt_usable_for_input (enum fmt_type type)
   return fmt_get_category (type) != FMT_CAT_CUSTOM;
 }
 
-/* For time and date formats, returns a template used for input
-   and output. */
+/* For time and date formats, returns a template used for input and output in a
+   field of the given WIDTH.
+
+   WIDTH only affects whether a 2-digit year or a 4-digit year is used, that
+   is, whether the returned string contains "yy" or "yyyy", and whether seconds
+   are include, that is, whether the returned string contains ":SS".  A caller
+   that doesn't care whether the returned string contains "yy" or "yyyy" or
+   ":SS" can just specify 0 to omit them. */
 const char *
-fmt_date_template (enum fmt_type type)
+fmt_date_template (enum fmt_type type, int width)
 {
+  const char *s1, *s2;
+
   switch (type)
     {
     case FMT_DATE:
-      return "dd-mmm-yy";
+      s1 = "dd-mmm-yy";
+      s2 = "dd-mmm-yyyy";
+      break;
+
     case FMT_ADATE:
-      return "mm/dd/yy";
+      s1 = "mm/dd/yy";
+      s2 = "mm/dd/yyyy";
+      break;
+
     case FMT_EDATE:
-      return "dd.mm.yy";
+      s1 = "dd.mm.yy";
+      s2 = "dd.mm.yyyy";
+      break;
+
     case FMT_JDATE:
-      return "yyddd";
+      s1 = "yyddd";
+      s2 = "yyyyddd";
+      break;
+
     case FMT_SDATE:
-      return "yy/mm/dd";
+      s1 = "yy/mm/dd";
+      s2 = "yyyy/mm/dd";
+      break;
+
     case FMT_QYR:
-      return "q Q yy";
+      s1 = "q Q yy";
+      s2 = "q Q yyyy";
+      break;
+
     case FMT_MOYR:
-      return "mmmXyy";
+      s1 = "mmm yy";
+      s2 = "mmm yyyy";
+      break;
+
     case FMT_WKYR:
-      return "ww WK yy";
+      s1 = "ww WK yy";
+      s2 = "ww WK yyyy";
+      break;
+
     case FMT_DATETIME:
-      return "dd-mmm-yyyy HH:MM";
+      s1 = "dd-mmm-yyyy HH:MM";
+      s2 = "dd-mmm-yyyy HH:MM:SS";
+      break;
+
+    case FMT_YMDHMS:
+      s1 = "yyyy-mm-dd HH:MM";
+      s2 = "yyyy-mm-dd HH:MM:SS";
+      break;
+
+    case FMT_MTIME:
+      s1 = "MM";
+      s2 = "MM:SS";
+      break;
+
     case FMT_TIME:
-      return "H:MM";
+      s1 = "HH:MM";
+      s2 = "HH:MM:SS";
+      break;
+
     case FMT_DTIME:
-      return "D HH:MM";
+      s1 = "D HH:MM";
+      s2 = "D HH:MM:SS";
+      break;
+
     default:
       NOT_REACHED ();
     }
+
+  return width >= strlen (s2) ? s2 : s1;
 }
 
 /* Returns a string representing the format TYPE for use in a GUI dialog. */
@@ -946,6 +1135,8 @@ fmt_gui_name (enum fmt_type type)
     case FMT_MOYR:
     case FMT_WKYR:
     case FMT_DATETIME:
+    case FMT_YMDHMS:
+    case FMT_MTIME:
     case FMT_TIME:
     case FMT_DTIME:
     case FMT_WKDAY:
@@ -979,14 +1170,12 @@ is_fmt_type (enum fmt_type type)
 }
 
 /* Returns true if WIDTH is a valid width for the given format
-   TYPE,
-   for input if FOR_INPUT is true,
-   for output otherwise. */
+   TYPE, for the given USE. */
 static bool
-valid_width (enum fmt_type type, int width, bool for_input)
+valid_width (enum fmt_type type, int width, enum fmt_use use)
 {
-  return (width >= fmt_min_width (type, for_input)
-          && width <= fmt_max_width (type, for_input));
+  return (width >= fmt_min_width (type, use)
+          && width <= fmt_max_width (type, use));
 }
 
 /* Returns the maximum number of decimal digits in an unsigned
@@ -1001,13 +1190,13 @@ max_digits_for_bytes (int bytes)
 
 /* Clamp FMT's width to the range and values allowed by FMT's type. */
 static void
-fmt_clamp_width (struct fmt_spec *fmt, bool for_input)
+fmt_clamp_width (struct fmt_spec *fmt, enum fmt_use use)
 {
   unsigned int step;
   int min_w, max_w;
 
-  min_w = fmt_min_width (fmt->type, for_input);
-  max_w = fmt_max_width (fmt->type, for_input);
+  min_w = fmt_min_width (fmt->type, use);
+  max_w = fmt_max_width (fmt->type, use);
   if (fmt->w < min_w)
     fmt->w = min_w;
   else if (fmt->w > max_w)
@@ -1020,59 +1209,151 @@ fmt_clamp_width (struct fmt_spec *fmt, bool for_input)
 
 /* Clamp FMT's decimal places to the range allowed by FMT's type and width. */
 static void
-fmt_clamp_decimals (struct fmt_spec *fmt, bool for_input)
+fmt_clamp_decimals (struct fmt_spec *fmt, enum fmt_use use)
 {
-  int max_d;
-
-  max_d = fmt_max_decimals (fmt->type, fmt->w, for_input);
-  if (fmt->d < 0)
-    fmt->d = 0;
-  else if (fmt->d > max_d)
+  int max_d = fmt_max_decimals (fmt->type, fmt->w, use);
+  if (fmt->d > max_d)
     fmt->d = max_d;
 }
 \f
-/* Sets AFFIX's string value to S, a UTF-8 encoded string. */
-static void
-fmt_affix_set (struct fmt_affix *affix, const char *s)
+static struct fmt_affix
+fmt_affix_clone (const struct fmt_affix *old)
 {
-  affix->s = s[0] == '\0' ? CONST_CAST (char *, "") : xstrdup (s);
-  affix->width = u8_strwidth (CHAR_CAST (const uint8_t *, s), "UTF-8");
+  return (struct fmt_affix) {
+    .s = xstrdup_if_nonnull (old->s),
+    .width = old->width,
+  };
 }
 
 /* Frees data in AFFIX. */
 static void
 fmt_affix_free (struct fmt_affix *affix)
 {
-  if (affix->s[0])
+  if (affix->s)
     free (affix->s);
 }
 
-static void
-fmt_number_style_init (struct fmt_number_style *style)
+/* Find and returns the grouping character in CC_STRING (either '.' or ',') or
+   0 on error. */
+static int
+find_cc_separators (const char *cc_string)
 {
-  fmt_affix_set (&style->neg_prefix, "");
-  fmt_affix_set (&style->prefix, "");
-  fmt_affix_set (&style->suffix, "");
-  fmt_affix_set (&style->neg_suffix, "");
-  style->decimal = '.';
-  style->grouping = 0;
+  /* Count commas and periods.  There must be exactly three of
+     one or the other, except that an apostrophe escapes a
+     following comma or period. */
+  int n_commas = 0;
+  int n_dots = 0;
+  for (const char *p = cc_string; *p; p++)
+    if (*p == ',')
+      n_commas++;
+    else if (*p == '.')
+      n_dots++;
+    else if (*p == '\'' && (p[1] == '.' || p[1] == ',' || p[1] == '\''))
+      p++;
+
+  return (n_commas == 3 ? (n_dots != 3 ? ',' : 0)
+          : n_dots == 3 ? '.'
+          : 0);
+}
+
+/* Extracts a token from IN into a newly allocated string AFFIXP.  Tokens are
+   delimited by GROUPING.  Returns the first character following the token. */
+static struct fmt_affix
+extract_cc_token (const char **sp, int grouping, size_t *extra_bytes)
+{
+  const char *p = *sp;
+  for (; *p && *p != grouping; p++)
+    if (*p == '\'' && p[1] == grouping)
+      p++;
+
+  size_t length = p - *sp;
+  char *affix = xmemdup0 (*sp, length);
+  size_t width = u8_strwidth (CHAR_CAST (const uint8_t *, affix), "UTF-8");
+  if (length > width)
+    *extra_bytes += length - width;
+
+  *sp = p + (*p != 0);
+
+  return (struct fmt_affix) { .s = affix, .width = width };
+}
+
+struct fmt_number_style *
+fmt_number_style_from_string (const char *s)
+{
+  char grouping = find_cc_separators (s);
+  if (!grouping)
+    return NULL;
+
+  size_t extra_bytes = 0;
+  struct fmt_affix neg_prefix = extract_cc_token (&s, grouping, &extra_bytes);
+  struct fmt_affix prefix = extract_cc_token (&s, grouping, &extra_bytes);
+  struct fmt_affix suffix = extract_cc_token (&s, grouping, &extra_bytes);
+  struct fmt_affix neg_suffix = extract_cc_token (&s, grouping, &extra_bytes);
+
+  struct fmt_number_style *style = xmalloc (sizeof *style);
+  *style = (struct fmt_number_style) {
+    .neg_prefix = neg_prefix,
+    .prefix = prefix,
+    .suffix = suffix,
+    .neg_suffix = neg_suffix,
+    .decimal = grouping == '.' ? ',' : '.',
+    .grouping = grouping,
+    .extra_bytes = extra_bytes,
+  };
+  return style;
 }
 
 static void
-fmt_number_style_clone (struct fmt_number_style *new,
-                        const struct fmt_number_style *old)
+format_cc (struct string *out, const char *in, char grouping)
+{
+  while (*in != '\0')
+    {
+      char c = *in++;
+      if (c == grouping || c == '\'')
+        ds_put_byte (out, '\'');
+      else if (c == '"')
+        ds_put_byte (out, '"');
+      ds_put_byte (out, c);
+    }
+}
+
+char *
+fmt_number_style_to_string (const struct fmt_number_style *cc)
+{
+  struct string out = DS_EMPTY_INITIALIZER;
+  format_cc (&out, cc->neg_prefix.s, cc->grouping);
+  ds_put_byte (&out, cc->grouping);
+  format_cc (&out, cc->prefix.s, cc->grouping);
+  ds_put_byte (&out, cc->grouping);
+  format_cc (&out, cc->suffix.s, cc->grouping);
+  ds_put_byte (&out, cc->grouping);
+  format_cc (&out, cc->neg_suffix.s, cc->grouping);
+  return ds_steal_cstr (&out);
+}
+
+struct fmt_number_style *
+fmt_number_style_clone (const struct fmt_number_style *old)
 {
-  fmt_affix_set (&new->neg_prefix, old->neg_prefix.s);
-  fmt_affix_set (&new->prefix, old->prefix.s);
-  fmt_affix_set (&new->suffix, old->suffix.s);
-  fmt_affix_set (&new->neg_suffix, old->neg_suffix.s);
-  new->decimal = old->decimal;
-  new->grouping = old->grouping;
-  new->extra_bytes = old->extra_bytes;
+  if (old)
+    {
+      struct fmt_number_style *new = xmalloc (sizeof *new);
+      *new = (struct fmt_number_style) {
+        .neg_prefix = fmt_affix_clone (&old->neg_prefix),
+        .prefix = fmt_affix_clone (&old->prefix),
+        .suffix = fmt_affix_clone (&old->suffix),
+        .neg_suffix = fmt_affix_clone (&old->neg_suffix),
+        .decimal = old->decimal,
+        .grouping = old->grouping,
+        .extra_bytes = old->extra_bytes,
+      };
+      return new;
+    }
+  else
+    return NULL;
 }
 
 /* Destroys a struct fmt_number_style. */
-static void
+void
 fmt_number_style_destroy (struct fmt_number_style *style)
 {
   if (style != NULL)
@@ -1081,6 +1362,7 @@ fmt_number_style_destroy (struct fmt_number_style *style)
       fmt_affix_free (&style->prefix);
       fmt_affix_free (&style->suffix);
       fmt_affix_free (&style->neg_suffix);
+      free (style);
     }
 }
 
@@ -1115,4 +1397,7 @@ get_fmt_desc (enum fmt_type type)
   return &formats[type];
 }
 
-const struct fmt_spec F_8_0 = {FMT_F, 8, 0};
+const struct fmt_spec F_8_0 = { .type = FMT_F, .w = 8, .d = 0 };
+const struct fmt_spec F_8_2 = { .type = FMT_F, .w = 8, .d = 2 };
+const struct fmt_spec F_4_3 = { .type = FMT_F, .w = 4, .d = 3 };
+const struct fmt_spec F_5_1 = { .type = FMT_F, .w = 5, .d = 1 };