From 3ce390ce7cbbcf38b7e6d5ddd6426deaf73135ba Mon Sep 17 00:00:00 2001 From: John Darrington Date: Sat, 20 Jun 2020 20:54:28 +0200 Subject: [PATCH] Allow /RENAME target to use the TO keyword. Allow code such as SAVE OUTFILE = "foo.sav" /RENAME= (A B C = v1 TO v2). Closes bug: #57752 --- src/language/data-io/trim.c | 154 ++++++++++++++++++++++++++----- tests/language/data-io/update.at | 27 +++++- 2 files changed, 158 insertions(+), 23 deletions(-) diff --git a/src/language/data-io/trim.c b/src/language/data-io/trim.c index d2384b901a..2ef085e796 100644 --- a/src/language/data-io/trim.c +++ b/src/language/data-io/trim.c @@ -1,6 +1,6 @@ /* PSPP - a program for statistical analysis. Copyright (C) 1997-9, 2000, 2006, 2007, 2008, 2010, 2011, - 2019 Free Software Foundation, Inc. + 2019, 2020 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 @@ -26,6 +26,7 @@ #include "language/lexer/lexer.h" #include "language/lexer/variable-parser.h" #include "libpspp/message.h" +#include "libpspp/misc.h" #include "gl/xalloc.h" @@ -59,6 +60,91 @@ parse_dict_trim (struct lexer *lexer, struct dictionary *dict, bool relax) } } +/* Check that OLD_NAME can be renamed to NEW_NAME in DICT. */ +static bool +check_rename (const struct dictionary *dict, const char *old_name, const char *new_name) +{ + if (dict_lookup_var (dict, new_name) != NULL) + { + msg (SE, _("Cannot rename %s as %s because there already exists " + "a variable named %s. To rename variables with " + "overlapping names, use a single RENAME subcommand " + "such as `/RENAME (A=B)(B=C)(C=A)', or equivalently, " + "`/RENAME (A B C=B C A)'."), + old_name, new_name, new_name); + return false; + } + return true; +} + +/* Parse a "VarX TO VarY" sequence where X and Y are integers + such that X >= Y. + If successfull, returns a string to the prefix Var and sets FIRST + to X and LAST to Y. Returns NULL on failure. + The caller must free the return value. */ +static char * +try_to_sequence (struct lexer *lexer, const struct dictionary *dict, + int *first, int *last) +{ + /* Check that the next 3 tokens are of the correct type. */ + if (lex_token (lexer) != T_ID + || lex_next_token (lexer, 1) != T_TO + || lex_next_token (lexer, 2) != T_ID) + return NULL; + + /* Check that the first and last tokens are suitable as + variable names. */ + const char *s0 = lex_tokcstr (lexer); + if (!id_is_valid (s0, dict_get_encoding (dict), true)) + return NULL; + + const char *s1 = lex_next_tokcstr (lexer, 2); + if (!id_is_valid (s1, dict_get_encoding (dict), true)) + return NULL; + + int x0 = strcspn (s0, "0123456789"); + int x1 = strcspn (s1, "0123456789"); + + /* The non-digit parts of s0 and s1 must be the same length. */ + if (x0 != x1) + return NULL; + + /* Both s0 and s1 must have some digits. */ + if (strlen (s0) <= x0) + return NULL; + + if (strlen (s1) <= x1) + return NULL; + + /* The non-digit parts of s0 and s1 must be identical. */ + if (0 != strncmp (s0, s1, x0)) + return NULL; + + /* Both names must end with digits. */ + int len_s0_pfx = strspn (s0 + x0, "0123456789"); + if (len_s0_pfx + x0 != strlen (s0)) + return NULL; + + int len_s1_pfx = strspn (s1 + x1, "0123456789"); + if (len_s1_pfx + x1 != strlen (s1)) + return NULL; + + const char *n_start = s0 + x0; + const char *n_stop = s1 + x1; + + /* The first may not be greater than the last. */ + if (atoi (n_start) > atoi (n_stop)) + return NULL; + + char *prefix = xstrndup (s0, x1); + + *first = atoi (n_start); + *last = atoi (n_stop); + + return prefix; +} + + /* Parses and performs the RENAME subcommand of GET, SAVE, and related commands. If RELAX is true, then the new variable names need not conform to the normal dictionary rules. @@ -90,28 +176,52 @@ parse_dict_rename (struct lexer *lexer, struct dictionary *dict, goto fail; newnames = xmalloc (sizeof *newnames * n_oldvars); + + char *prefix = NULL; + int first, last; + /* First attempt to parse v1 TO v10 format. */ + if ((prefix = try_to_sequence (lexer, dict, &first, &last))) + { + /* These 3 tokens have already been checked in the + try_to_sequence function. */ + lex_get (lexer); + lex_get (lexer); + lex_get (lexer); + + /* Make sure the new names are suitable. */ + for (int i = first; i <= last; ++i) + { + int sz = strlen (prefix) + intlog10 (last) + 1; + char *vn = malloc (sz); + snprintf (vn, sz, "%s%d", prefix, i); + + if (!check_rename (dict, var_get_name (oldvars[n_newvars]), vn)) + { + free (prefix); + goto fail; + } + + newnames[i - first] = vn; + n_newvars++; + } + } + else while (lex_token (lexer) == T_ID || lex_token (lexer) == T_STRING) - { - if (n_newvars >= n_oldvars) - break; - const char *new_name = lex_tokcstr (lexer); - if (!relax && ! id_is_plausible (new_name, true)) - goto fail; - - if (dict_lookup_var (dict, new_name) != NULL) - { - msg (SE, _("Cannot rename %s as %s because there already exists " - "a variable named %s. To rename variables with " - "overlapping names, use a single RENAME subcommand " - "such as `/RENAME (A=B)(B=C)(C=A)', or equivalently, " - "`/RENAME (A B C=B C A)'."), - var_get_name (oldvars[n_newvars]), new_name, new_name); - goto fail; - } - newnames[n_newvars] = strdup (new_name); - lex_get (lexer); - n_newvars++; - } + { + if (n_newvars >= n_oldvars) + break; + const char *new_name = lex_tokcstr (lexer); + if (!relax && ! id_is_plausible (new_name, true)) + goto fail; + + if (!check_rename (dict, var_get_name (oldvars[n_newvars]), new_name)) + goto fail; + newnames[n_newvars] = strdup (new_name); + lex_get (lexer); + n_newvars++; + } + free (prefix); + if (n_newvars != n_oldvars) { msg (SE, _("Number of variables on left side of `=' (%zu) does not " diff --git a/tests/language/data-io/update.at b/tests/language/data-io/update.at index c04b39a46f..661b5a1e34 100644 --- a/tests/language/data-io/update.at +++ b/tests/language/data-io/update.at @@ -1,5 +1,5 @@ dnl PSPP - a program for statistical analysis. -dnl Copyright (C) 2017 Free Software Foundation, Inc. +dnl Copyright (C) 2017, 2020 Free Software Foundation, Inc. dnl dnl This program is free software: you can redistribute it and/or modify dnl it under the terms of the GNU General Public License as published by @@ -88,3 +88,28 @@ AT_BANNER([UPDATE]) CHECK_UPDATE([sav], [sav]) CHECK_UPDATE([sav], [inline]) CHECK_UPDATE([inline], [sav]) + + +AT_SETUP([SAVE RENAME with TO]) +AT_DATA([save-rename-to.sps], [dnl +data list notable list /a b c fxo9*. +begin data +1 2 3 8 +end data. + +SAVE OUTFILE = "renamed.sav" + /RENAME=(A B C = fdo9 TO fdo11). + + +NEW FILE. +GET FILE = "renamed.sav". +LIST. +]) + +AT_CHECK([pspp -O format=csv save-rename-to.sps], [0], [dnl +Table: Data List +fdo9,fdo10,fdo11,fxo9 +1.00,2.00,3.00,8.00 +]) + +AT_CLEANUP \ No newline at end of file -- 2.30.2