Comment découper les espaces blancs de début / de fin de manière standard?


177

Existe-t-il une méthode propre, de préférence standard, pour couper les espaces de début et de fin d'une chaîne en C? Je ferais le mien, mais je pense que c'est un problème commun avec une solution tout aussi commune.

Réponses:


164

Si vous pouvez modifier la chaîne:

// Note: This function returns a pointer to a substring of the original string.
// If the given string was allocated dynamically, the caller must not overwrite
// that pointer with the returned value, since the original pointer must be
// deallocated using the same allocator with which it was allocated.  The return
// value must NOT be deallocated using free() etc.
char *trimwhitespace(char *str)
{
  char *end;

  // Trim leading space
  while(isspace((unsigned char)*str)) str++;

  if(*str == 0)  // All spaces?
    return str;

  // Trim trailing space
  end = str + strlen(str) - 1;
  while(end > str && isspace((unsigned char)*end)) end--;

  // Write new null terminator character
  end[1] = '\0';

  return str;
}

Si vous ne pouvez pas modifier la chaîne, vous pouvez utiliser essentiellement la même méthode:

// Stores the trimmed input string into the given output buffer, which must be
// large enough to store the result.  If it is too small, the output is
// truncated.
size_t trimwhitespace(char *out, size_t len, const char *str)
{
  if(len == 0)
    return 0;

  const char *end;
  size_t out_size;

  // Trim leading space
  while(isspace((unsigned char)*str)) str++;

  if(*str == 0)  // All spaces?
  {
    *out = 0;
    return 1;
  }

  // Trim trailing space
  end = str + strlen(str) - 1;
  while(end > str && isspace((unsigned char)*end)) end--;
  end++;

  // Set output size to minimum of trimmed string length and buffer size minus 1
  out_size = (end - str) < len-1 ? (end - str) : len-1;

  // Copy trimmed string and add null terminator
  memcpy(out, str, out_size);
  out[out_size] = 0;

  return out_size;
}

6
Désolé, la première réponse n'est pas du tout bonne, sauf si vous ne vous souciez pas des fuites de mémoire. Vous avez maintenant deux chaînes qui se chevauchent (l'original, dont les espaces de fin sont coupés, et la nouvelle). Seule la chaîne d'origine peut être libérée, mais si vous le faites, la seconde pointe vers la mémoire libérée.
David Nehme

7
@nvl: Il n'y a pas de mémoire allouée, donc il n'y a pas de mémoire à libérer.
Adam Rosenfield

15
@nvl: No. strest une variable locale et sa modification ne change pas le pointeur d'origine transmis. Les appels de fonction en C sont toujours pass-by-value, jamais pass-by-reference.
Adam Rosenfield

11
@Raj: Il n'y a rien de mal en soi à renvoyer une adresse différente de celle qui a été transmise. Il n'est pas nécessaire ici que la valeur retournée soit un argument valide de la free()fonction. Bien au contraire, j'ai conçu cela pour éviter le besoin d'allocation de mémoire pour plus d'efficacité. Si l'adresse transmise a été allouée dynamiquement, l'appelant est toujours responsable de la libération de cette mémoire, et l'appelant doit être sûr de ne pas écraser cette valeur par la valeur renvoyée ici.
Adam Rosenfield

3
Vous devez convertir l'argument pour isspaceen unsigned char, sinon vous invoquez un comportement non défini.
Roland Illig

37

En voici un qui déplace la chaîne dans la première position de votre tampon. Vous pouvez souhaiter ce comportement pour que si vous allouiez dynamiquement la chaîne, vous puissiez toujours la libérer sur le même pointeur que trim () renvoie:

char *trim(char *str)
{
    size_t len = 0;
    char *frontp = str;
    char *endp = NULL;

    if( str == NULL ) { return NULL; }
    if( str[0] == '\0' ) { return str; }

    len = strlen(str);
    endp = str + len;

    /* Move the front and back pointers to address the first non-whitespace
     * characters from each end.
     */
    while( isspace((unsigned char) *frontp) ) { ++frontp; }
    if( endp != frontp )
    {
        while( isspace((unsigned char) *(--endp)) && endp != frontp ) {}
    }

    if( frontp != str && endp == frontp )
            *str = '\0';
    else if( str + len - 1 != endp )
            *(endp + 1) = '\0';

    /* Shift the string so that it starts at str so that if it's dynamically
     * allocated, we can still free it on the returned pointer.  Note the reuse
     * of endp to mean the front of the string buffer now.
     */
    endp = str;
    if( frontp != str )
    {
            while( *frontp ) { *endp++ = *frontp++; }
            *endp = '\0';
    }

    return str;
}

Test d'exactitude:

#include <stdio.h>
#include <string.h>
#include <ctype.h>

/* Paste function from above here. */

int main()
{
    /* The test prints the following:
    [nothing to trim] -> [nothing to trim]
    [    trim the front] -> [trim the front]
    [trim the back     ] -> [trim the back]
    [    trim front and back     ] -> [trim front and back]
    [ trim one char front and back ] -> [trim one char front and back]
    [ trim one char front] -> [trim one char front]
    [trim one char back ] -> [trim one char back]
    [                   ] -> []
    [ ] -> []
    [a] -> [a]
    [] -> []
    */

    char *sample_strings[] =
    {
            "nothing to trim",
            "    trim the front",
            "trim the back     ",
            "    trim front and back     ",
            " trim one char front and back ",
            " trim one char front",
            "trim one char back ",
            "                   ",
            " ",
            "a",
            "",
            NULL
    };
    char test_buffer[64];
    char comparison_buffer[64];
    size_t index, compare_pos;

    for( index = 0; sample_strings[index] != NULL; ++index )
    {
        // Fill buffer with known value to verify we do not write past the end of the string.
        memset( test_buffer, 0xCC, sizeof(test_buffer) );
        strcpy( test_buffer, sample_strings[index] );
        memcpy( comparison_buffer, test_buffer, sizeof(comparison_buffer));

        printf("[%s] -> [%s]\n", sample_strings[index],
                                 trim(test_buffer));

        for( compare_pos = strlen(comparison_buffer);
             compare_pos < sizeof(comparison_buffer);
             ++compare_pos )
        {
            if( test_buffer[compare_pos] != comparison_buffer[compare_pos] )
            {
                printf("Unexpected change to buffer @ index %u: %02x (expected %02x)\n",
                    compare_pos, (unsigned char) test_buffer[compare_pos], (unsigned char) comparison_buffer[compare_pos]);
            }
        }
    }

    return 0;
}

Le fichier source était trim.c. Compilé avec 'cc -Wall trim.c -o trim'.


2
Vous devez convertir l'argument pour isspaceen unsigned char, sinon vous invoquez un comportement non défini.
Roland Illig

@RolandIllig: Merci, je n'ai jamais réalisé que c'était nécessaire. Corrigé.
indiv

@Simas: Pourquoi dites-vous ça? La fonction appelle isspace()alors pourquoi y aurait-il une différence entre " "et "\n"? J'ai ajouté des tests unitaires pour les nouvelles lignes et il semble OK pour moi ... ideone.com/bbVmqo
indiv

1
@indiv il accédera au bloc de mémoire invalide lorsqu'il est alloué manuellement. A savoir cette ligne: *(endp + 1) = '\0';. L'exemple de test sur la réponse utilise un buffer de 64 qui évite ce problème.
Simas

1
@nolandda: Merci pour le détail. Je l'ai corrigé et mis à jour le test pour détecter le dépassement de la mémoire tampon car je n'ai pas accès à valgrind pour le moment.
indiv

23

Ma solution. La chaîne doit être modifiable. L'avantage par rapport à certaines des autres solutions est qu'il déplace la partie non-espace au début afin que vous puissiez continuer à utiliser l'ancien pointeur, au cas où vous auriez à le libérer () plus tard.

void trim(char * s) {
    char * p = s;
    int l = strlen(p);

    while(isspace(p[l - 1])) p[--l] = 0;
    while(* p && isspace(* p)) ++p, --l;

    memmove(s, p, l + 1);
}   

Cette version crée une copie de la chaîne avec strndup () au lieu de la modifier sur place. strndup () nécessite _GNU_SOURCE, donc peut-être que vous devez créer votre propre strndup () avec malloc () et strncpy ().

char * trim(char * s) {
    int l = strlen(s);

    while(isspace(s[l - 1])) --l;
    while(* s && isspace(* s)) ++s, --l;

    return strndup(s, l);
}

4
trim()invoque UB si sest ""comme le premier isspace()appel isspace(p[-1])et p[-1]ne fait pas nécessairement référence à un emplacement légal.
chux

1
Vous devez convertir l'argument pour isspaceen unsigned char, sinon vous invoquez un comportement non défini.
Roland Illig

1
devrait ajouter if(l==0)return;pour éviter une
chaîne de

11

Voici ma mini bibliothèque C pour rogner à gauche, à droite, les deux, tous, en place et séparés, et rogner un ensemble de caractères spécifiés (ou un espace blanc par défaut).

contenu de strlib.h:

#ifndef STRLIB_H_
#define STRLIB_H_ 1
enum strtrim_mode_t {
    STRLIB_MODE_ALL       = 0, 
    STRLIB_MODE_RIGHT     = 0x01, 
    STRLIB_MODE_LEFT      = 0x02, 
    STRLIB_MODE_BOTH      = 0x03
};

char *strcpytrim(char *d, // destination
                 char *s, // source
                 int mode,
                 char *delim
                 );

char *strtriml(char *d, char *s);
char *strtrimr(char *d, char *s);
char *strtrim(char *d, char *s); 
char *strkill(char *d, char *s);

char *triml(char *s);
char *trimr(char *s);
char *trim(char *s);
char *kill(char *s);
#endif

contenu de strlib.c:

#include <strlib.h>

char *strcpytrim(char *d, // destination
                 char *s, // source
                 int mode,
                 char *delim
                 ) {
    char *o = d; // save orig
    char *e = 0; // end space ptr.
    char dtab[256] = {0};
    if (!s || !d) return 0;

    if (!delim) delim = " \t\n\f";
    while (*delim) 
        dtab[*delim++] = 1;

    while ( (*d = *s++) != 0 ) { 
        if (!dtab[0xFF & (unsigned int)*d]) { // Not a match char
            e = 0;       // Reset end pointer
        } else {
            if (!e) e = d;  // Found first match.

            if ( mode == STRLIB_MODE_ALL || ((mode != STRLIB_MODE_RIGHT) && (d == o)) ) 
                continue;
        }
        d++;
    }
    if (mode != STRLIB_MODE_LEFT && e) { // for everything but trim_left, delete trailing matches.
        *e = 0;
    }
    return o;
}

// perhaps these could be inlined in strlib.h
char *strtriml(char *d, char *s) { return strcpytrim(d, s, STRLIB_MODE_LEFT, 0); }
char *strtrimr(char *d, char *s) { return strcpytrim(d, s, STRLIB_MODE_RIGHT, 0); }
char *strtrim(char *d, char *s) { return strcpytrim(d, s, STRLIB_MODE_BOTH, 0); }
char *strkill(char *d, char *s) { return strcpytrim(d, s, STRLIB_MODE_ALL, 0); }

char *triml(char *s) { return strcpytrim(s, s, STRLIB_MODE_LEFT, 0); }
char *trimr(char *s) { return strcpytrim(s, s, STRLIB_MODE_RIGHT, 0); }
char *trim(char *s) { return strcpytrim(s, s, STRLIB_MODE_BOTH, 0); }
char *kill(char *s) { return strcpytrim(s, s, STRLIB_MODE_ALL, 0); }

La seule routine principale fait tout. Il coupe en place si src == dst , sinon, il fonctionne comme les strcpyroutines. Il supprime un ensemble de caractères spécifié dans la chaîne de délimitation, ou un espace blanc si nul. Il coupe à gauche, à droite, les deux et tout (comme tr). Il n'y a pas grand-chose à faire et il ne parcourt la chaîne qu'une seule fois. Certaines personnes pourraient se plaindre que la coupe à droite commence à gauche, cependant, aucun strlen n'est nécessaire qui commence de toute façon à gauche. (D'une manière ou d'une autre, vous devez arriver à la fin de la chaîne pour obtenir les bons ajustements, vous pouvez donc aussi bien faire le travail au fur et à mesure.) Il peut y avoir des arguments à faire sur le pipelining et la taille du cache, etc. . Étant donné que la solution fonctionne de gauche à droite et n'itère qu'une seule fois, elle peut être étendue pour fonctionner également sur les flux. Limitations: cela ne fonctionne pas sur les chaînes unicode .


2
J'ai voté pour cela et je sais que c'est vieux mais je pense qu'il y a un bug. dtab[*d]ne pas jeter *dà unsigned intavant de l' utiliser comme un index de tableau. Sur un système avec un caractère signé, cela lira dtab[-127]ce qui provoquera des bogues et éventuellement des plantages.
Zan Lynx

2
Comportement potentiel indéfini activé dtab[*delim++]car charles valeurs d'index doivent être converties unsigned char. Le code suppose 8 bits char. delimdoit être déclaré comme const char *. dtab[0xFF & (unsigned int)*d]serait plus clair que dtab[(unsigned char)*d]. Le code fonctionne sur les chaînes encodées en UTF-8, mais ne supprime pas les séquences d'espacement non ASCII.
chqrlie

@ michael-plainer, cela semble intéressant. Pourquoi ne pas le tester et le mettre sur GitHub?
Daisuke Aramaki

9

Voici ma tentative d'une fonction de garniture en place simple mais correcte.

void trim(char *str)
{
    int i;
    int begin = 0;
    int end = strlen(str) - 1;

    while (isspace((unsigned char) str[begin]))
        begin++;

    while ((end >= begin) && isspace((unsigned char) str[end]))
        end--;

    // Shift all characters back to the start of the string array.
    for (i = begin; i <= end; i++)
        str[i - begin] = str[i];

    str[i - begin] = '\0'; // Null terminate string.
}

2
Suggérer de changer en while ((end >= begin) && isspace(str[end]))pour empêcher UB lorsque str is "" . Prevents str [-1] `.
chux

Btw, je dois changer cela en str [i - begin + 1] pour fonctionner
truongnm

1
Vous devez convertir l'argument pour isspaceen unsigned char, sinon vous invoquez un comportement non défini.
Roland Illig

@RolandIllig, pourquoi serait-ce un comportement indéfini? La fonction est conçue pour fonctionner avec des caractères.
wovano

@wovano Non, ce n'est pas le cas. Les fonctions de <ctype.h>sont destinées à fonctionner avec des ints, qui représentent soit unsigned charsoit la valeur spéciale EOF. Voir stackoverflow.com/q/7131026/225757 .
Roland Illig

8

Tard à la soirée

Caractéristiques:
1. Coupez le début rapidement, comme dans un certain nombre d'autres réponses.
2. Après être allé à la fin, coupez la droite avec un seul test par boucle. Comme @ jfm3, mais fonctionne pour une chaîne entièrement composée d'espaces blancs)
3. Pour éviter un comportement indéfini lorsque charest signé char, transtypez *sen unsigned char.

Gestion des caractères "Dans tous les cas, l'argument est un int, dont la valeur doit être représentable en tant que unsigned charou doit être égale à la valeur de la macro EOF. Si l'argument a une autre valeur, le comportement n'est pas défini." C11 §7.4 1

#include <ctype.h>

// Return a pointer to the trimmed string
char *string_trim_inplace(char *s) {
  while (isspace((unsigned char) *s)) s++;
  if (*s) {
    char *p = s;
    while (*p) p++;
    while (isspace((unsigned char) *(--p)));
    p[1] = '\0';
  }

  // If desired, shift the trimmed string

  return s;
}

@chqrlie a commenté ce qui précède ne déplace pas la chaîne tronquée . Faire cela....

// Return a pointer to the (shifted) trimmed string
char *string_trim_inplace(char *s) {
  char *original = s;
  size_t len = 0;

  while (isspace((unsigned char) *s)) {
    s++;
  } 
  if (*s) {
    char *p = s;
    while (*p) p++;
    while (isspace((unsigned char) *(--p)));
    p[1] = '\0';
    // len = (size_t) (p - s);   // older errant code
    len = (size_t) (p - s + 1);  // Thanks to @theriver
  }

  return (s == original) ? s : memmove(original, s, len + 1);
}

3
Ouais, enfin quelqu'un qui connaît le comportement indéfini de ctype.
Roland Illig

2
@chux Je pense que ça devrait être len = (size_t) (ps) +1; sinon la dernière lettre se chevauche.
theriver

4

Voici une solution similaire à la routine de modification sur place @ adam-rosenfields, mais sans recourir inutilement à strlen (). Comme @jkramer, la chaîne est ajustée à gauche dans le tampon afin que vous puissiez libérer le même pointeur. Pas optimal pour les grosses chaînes car il n'utilise pas memmove. Inclut les opérateurs ++ / - que @ jfm3 mentionne. Tests unitaires basés sur FCTX inclus.

#include <ctype.h>

void trim(char * const a)
{
    char *p = a, *q = a;
    while (isspace(*q))            ++q;
    while (*q)                     *p++ = *q++;
    *p = '\0';
    while (p > a && isspace(*--p)) *p = '\0';
}

/* See http://fctx.wildbearsoftware.com/ */
#include "fct.h"

FCT_BGN()
{
    FCT_QTEST_BGN(trim)
    {
        { char s[] = "";      trim(s); fct_chk_eq_str("",    s); } // Trivial
        { char s[] = "   ";   trim(s); fct_chk_eq_str("",    s); } // Trivial
        { char s[] = "\t";    trim(s); fct_chk_eq_str("",    s); } // Trivial
        { char s[] = "a";     trim(s); fct_chk_eq_str("a",   s); } // NOP
        { char s[] = "abc";   trim(s); fct_chk_eq_str("abc", s); } // NOP
        { char s[] = "  a";   trim(s); fct_chk_eq_str("a",   s); } // Leading
        { char s[] = "  a c"; trim(s); fct_chk_eq_str("a c", s); } // Leading
        { char s[] = "a  ";   trim(s); fct_chk_eq_str("a",   s); } // Trailing
        { char s[] = "a c  "; trim(s); fct_chk_eq_str("a c", s); } // Trailing
        { char s[] = " a ";   trim(s); fct_chk_eq_str("a",   s); } // Both
        { char s[] = " a c "; trim(s); fct_chk_eq_str("a c", s); } // Both

        // Villemoes pointed out an edge case that corrupted memory.  Thank you.
        // http://stackoverflow.com/questions/122616/#comment23332594_4505533
        {
          char s[] = "a     ";       // Buffer with whitespace before s + 2
          trim(s + 2);               // Trim "    " containing only whitespace
          fct_chk_eq_str("", s + 2); // Ensure correct result from the trim
          fct_chk_eq_str("a ", s);   // Ensure preceding buffer not mutated
        }

        // doukremt suggested I investigate this test case but
        // did not indicate the specific behavior that was objectionable.
        // http://stackoverflow.com/posts/comments/33571430
        {
          char s[] = "         foobar";  // Shifted across whitespace
          trim(s);                       // Trim
          fct_chk_eq_str("foobar", s);   // Leading string is correct

          // Here is what the algorithm produces:
          char r[16] = { 'f', 'o', 'o', 'b', 'a', 'r', '\0', ' ',                     
                         ' ', 'f', 'o', 'o', 'b', 'a', 'r', '\0'};
          fct_chk_eq_int(0, memcmp(s, r, sizeof(s)));
        }
    }
    FCT_QTEST_END();
}
FCT_END();

Cette solution est carrément dangereuse! Si la chaîne d'origine ne contient pas de caractères non blancs, la dernière ligne de trim écrase heureusement tout ce qui précède a, si ces octets contiennent des octets «d'espaces blancs». Compilez ceci sans optimisations et voyez ce qui arrive à y: unsigned x = 0x20202020; char s [4] = ""; non signé y = 0x20202020; printf ("& x, & s, & y =% p,% p,% p \ n", & x, & s, & y); printf ("x, [s], y =% 08x, [% s],% 08x \ n", x, s, y); trim_whitespace (s); printf ("x, [s], y =% 08x, [% s],% 08x \ n", x, s, y);
Villemoes

@Villemoes, merci pour le rapport de bogue. J'ai mis à jour la logique pour éviter de quitter le côté gauche du tampon lorsque la chaîne ne contient que des espaces. Cette nouvelle version répond-elle à vos préoccupations?
Rhys Ulerich

Les juristes du langage vous crieraient probablement dessus pour la simple pensée de spéculer sur la création d'un pointeur vers le caractère précédant celui vers lequel «a» pointe (ce que fera votre «--p»). Dans le monde réel, vous allez probablement bien. Mais vous pouvez aussi simplement changer '> =' en '>' et déplacer le décrément de p vers 'isspace (* - p)'.
Villemoes

Je pense que les avocats seraient d'accord, car il s'agit simplement de comparer une adresse sans y toucher, mais j'aime aussi votre suggestion sur la diminution. Je l'ai mis à jour en conséquence. Merci.
Rhys Ulerich

1
doukremt, craignez-vous que tout le tampon après foobar ne soit pas rempli de zéros? Si tel est le cas, ce serait un peu plus utile si vous le disiez explicitement plutôt que de lancer de vagues pierres.
Rhys Ulerich

3

Un autre, avec une ligne faisant le vrai travail:

#include <stdio.h>

int main()
{
   const char *target = "   haha   ";
   char buf[256];
   sscanf(target, "%s", buf); // Trimming on both sides occurs here
   printf("<%s>\n", buf);
}

1
Bonne idée d'utiliser scanf; mais son ne fonctionnera qu'avec un seul mot, ce qui n'est peut-être pas ce que l'OP voulait (c'est-à-dire que le découpage de "abc" devrait probablement aboutir à "ab c", alors que votre scanf unique aboutit simplement à "a"). Nous avons donc besoin d'une boucle et d'un compteur pour les caractères ignorés avec le %nspécificateur de conversion, et à la fin, c'est juste plus simple de le faire à la main, j'en ai peur.
Peter - Réintègre Monica le

Très utile lorsque vous voulez que le premier mot de la chaîne ne tienne pas compte des espaces initiaux.
J ... S

3

Je n'ai pas aimé la plupart de ces réponses parce qu'elles faisaient une ou plusieurs des choses suivantes ...

  1. Renvoyé un pointeur différent à l'intérieur de la chaîne du pointeur d'origine (un peu pénible de jongler avec deux pointeurs différents vers la même chose).
  2. Utilisation gratuite de choses comme strlen () qui pré-itèrent toute la chaîne.
  3. Utilisé des fonctions lib spécifiques au système d'exploitation non portables.
  4. Rétro-numérisé.
  5. Comparaison utilisée avec «» au lieu de isspace () afin que TAB / CR / LF soient préservés.
  6. Mémoire gaspillée avec de grands tampons statiques.
  7. Cycles gaspillés avec des fonctions coûteuses telles que sscanf / sprintf .

Voici ma version:

void fnStrTrimInPlace(char *szWrite) {

    const char *szWriteOrig = szWrite;
    char       *szLastSpace = szWrite, *szRead = szWrite;
    int        bNotSpace;

    // SHIFT STRING, STARTING AT FIRST NON-SPACE CHAR, LEFTMOST
    while( *szRead != '\0' ) {

        bNotSpace = !isspace((unsigned char)(*szRead));

        if( (szWrite != szWriteOrig) || bNotSpace ) {

            *szWrite = *szRead;
            szWrite++;

            // TRACK POINTER TO LAST NON-SPACE
            if( bNotSpace )
                szLastSpace = szWrite;
        }

        szRead++;
    }

    // TERMINATE AFTER LAST NON-SPACE (OR BEGINNING IF THERE WAS NO NON-SPACE)
    *szLastSpace = '\0';
}

2
Vous devez convertir l'argument pour isspaceen unsigned char, sinon vous invoquez un comportement non défini.
Roland Illig

Comme cette réponse concerne les «cycles gaspillés», notez que le code copie inutilement la piqûre entière lorsqu'il n'y a pas d'espace. Un leader while (isspace((unsigned char) *szWrite)) szWrite++;empêcherait cela. Le code copie également tout l'espace blanc de fin.
chux - Réintégrer Monica le

@chux cette implémentation mute sur place avec des pointeurs de lecture et d'écriture séparés (au lieu de renvoyer un nouveau pointeur dans un emplacement différent), donc la suggestion de sauter szWrite au premier non-espace sur la première ligne laisserait l'espace de début dans la chaîne d'origine.
Jason Stewart

@chux, vous avez raison de dire qu'il copie les espaces blancs de fin (avant d'ajouter un nul après le dernier caractère non espace), mais c'est le prix que j'ai choisi de payer pour éviter de pré-scanner la chaîne. Pour des quantités modestes de WS de fin, il est moins coûteux de simplement copier les octets plutôt que de pré-scanner la chaîne entière pour le dernier caractère non-WS. Pour de grandes quantités de WS de fin, la pré-numérisation vaudrait probablement la réduction des écritures.
Jason Stewart

@chux, pour la situation "copies quand il n'y a pas d'espace", n'effectuer que *szWrite = *szReadlorsque les pointeurs ne sont pas égaux sauterait les écritures dans ce cas, mais nous avons ajouté une autre comparaison / branche. Avec les CPU / MMU / BP modernes, je n'ai aucune idée si ce contrôle serait une perte ou un gain. Avec des processeurs et des architectures de mémoire plus simples, il est moins coûteux de simplement faire la copie et d'ignorer la comparaison.
Jason Stewart

2

Très tard à la fête ...

Solution de numérisation vers l'avant en un seul passage sans retour en arrière. Chaque caractère de la chaîne source est testé exactement une fois deux fois. (Donc, cela devrait être plus rapide que la plupart des autres solutions ici, surtout si la chaîne source a beaucoup d'espaces de fin.)

Cela inclut deux solutions, l'une pour copier et découper une chaîne source dans une autre chaîne de destination, et l'autre pour découper la chaîne source sur place. Les deux fonctions utilisent le même code.

La chaîne (modifiable) est déplacée sur place, de sorte que le pointeur d'origine vers elle reste inchangé.

#include <stddef.h>
#include <ctype.h>

char * trim2(char *d, const char *s)
{
    // Sanity checks
    if (s == NULL  ||  d == NULL)
        return NULL;

    // Skip leading spaces        
    const unsigned char * p = (const unsigned char *)s;
    while (isspace(*p))
        p++;

    // Copy the string
    unsigned char * dst = (unsigned char *)d;   // d and s can be the same
    unsigned char * end = dst;
    while (*p != '\0')
    {
        if (!isspace(*dst++ = *p++))
            end = dst;
    }

    // Truncate trailing spaces
    *end = '\0';
    return d;
}

char * trim(char *s)
{
    return trim2(s, s);
}

1
Chaque caractère de la chaîne source est testé exactement une fois : pas vraiment, la plupart des caractères de la chaîne source sont testés deux fois: comparés à '\0'et ensuite testés avec isspace(). Il semble inutile de tester tous les personnages avec isspace(). Le retour en arrière à partir de la fin de la chaîne devrait être plus efficace pour les cas non pathologiques.
chqrlie

@chqrlie - Oui, chaque personnage est testé deux fois. Je voudrais voir ce code réellement testé, en particulier des chaînes avec beaucoup d'espaces de fin, par rapport à d'autres algorithmes ici.
David R Tribble

trim()D'ACCORD. Cas d'angle: trim2(char *d, const char *s)a des problèmes lors du d,schevauchement et s < d.
chux - Réintégrer Monica le

@chux - Dans ce cas de coin, comment devrait-il trim()se comporter? Vous demandez de couper et de copier une chaîne dans la mémoire occupée par la chaîne elle-même. Contrairement à memmove()cela, cela nécessite de déterminer la longueur de la chaîne source avant d'effectuer le découpage lui-même, ce qui nécessite de scanner la chaîne entière une fois supplémentaire. Mieux vaut écrire une rtrim2()fonction différente qui sait copier la source vers la destination vers l'arrière, et prend probablement un argument de longueur de chaîne source supplémentaire.
David R Tribble

1

Je ne suis pas sûr de ce que vous considérez comme «indolore».

Les cordes C sont assez douloureuses. Nous pouvons trouver la première position de caractère non-espace de manière triviale:

while (isspace (* p)) p ++;

Nous pouvons trouver la dernière position de caractère non-espace avec deux mouvements triviaux similaires:

tandis que (* q) q ++;
faire {q--; } while (isspace (* q));

(Je vous ai épargné la peine d'utiliser les opérateurs *et ++en même temps.)

La question maintenant est que faites-vous avec cela? Le type de données à portée de main n'est pas vraiment un gros résumé robuste Stringauquel il est facile de penser, mais au contraire à peine plus qu'un tableau d'octets de stockage. Faute d'un type de données robuste, il est impossible d'écrire une fonction qui fera la même chose que la chompfonction de PHperytonby . Que renverrait une telle fonction en C?


Cela fonctionne bien à moins que la chaîne ne soit composée de tous les espaces blancs. Besoin d'un contrôle unique avant do { q--; } ...de savoir *q != 0.
chux

1

Utilisez une bibliothèque de chaînes , par exemple:

Ustr *s1 = USTR1(\7, " 12345 ");

ustr_sc_trim_cstr(&s1, " ");
assert(ustr_cmp_cstr_eq(s1, "12345"));

... comme vous dites que c'est un problème "commun", oui vous devez inclure un #include ou quelque chose comme ça et il n'est pas inclus dans la libc mais n'inventez pas votre propre travail de hack en stockant des pointeurs aléatoires et size_t de cette façon ne mène qu'à débordements de tampon.



1

Juste pour que cela continue de grandir, une autre option avec une chaîne modifiable:

void trimString(char *string)
{
    size_t i = 0, j = strlen(string);
    while (j > 0 && isspace((unsigned char)string[j - 1])) string[--j] = '\0';
    while (isspace((unsigned char)string[i])) i++;
    if (i > 0) memmove(string, string + i, j - i + 1);
}

1
strlen()renvoie un size_tqui peut dépasser la plage de int. l'espace blanc n'est pas limité au caractère d'espace. Enfin mais le plus important: Comportement indéfini activé strcpy(string, string + i * sizeof(char));car les tableaux source et destination se chevauchent. Utilisez à la memmove()place de strcpy().
chqrlie

@chqrlie vous avez raison, venez d'inclure vos suggestions. Je comprends que la copie lorsque la source et la destination se chevauchent peut entraîner un comportement indéfini, mais je veux juste souligner que dans ce cas particulier, cela ne devrait pas poser de problème puisque nous allons toujours copier d'une position ultérieure de la mémoire au début, Merci pour les commentaires.
wallek876

1
peu importe comment les tableaux source et destination se chevauchent, il s'agit d'un comportement indéfini. Ne vous fiez pas à l'hypothèse que la copie peut avoir lieu un octet à la fois avec des adresses croissantes. J'ai également oublié de mentionner que while (isspace((int)string[i])) string[i--] = '\0';peut boucler au-delà du début de la chaîne. Vous devez combiner cette boucle avec les lignes précédentes et suivantes et écrirewhile (i > 0 && isspace((unsigned char)string[--i])) { string[i] = '\0'; } size_t end = i;
chqrlie

@chqrlie bon point, une chaîne avec tous les espaces blancs aurait provoqué une boucle au-delà du début, je n'y ai pas pensé.
wallek876

En fait, ma suggestion était incorrecte car endelle ne pointait pas vers l'octet nul de fin et vous end = ++i;aviez toujours un problème pour les chaînes contenant tous les caractères d'espacement. Je viens de corriger le code.
chqrlie

1

Je sais qu'il y a beaucoup de réponses, mais je poste ma réponse ici pour voir si ma solution est assez bonne.

// Trims leading whitespace chars in left `str`, then copy at almost `n - 1` chars
// into the `out` buffer in which copying might stop when the first '\0' occurs, 
// and finally append '\0' to the position of the last non-trailing whitespace char.
// Reture the length the trimed string which '\0' is not count in like strlen().
size_t trim(char *out, size_t n, const char *str)
{
    // do nothing
    if(n == 0) return 0;    

    // ptr stop at the first non-leading space char
    while(isspace(*str)) str++;    

    if(*str == '\0') {
        out[0] = '\0';
        return 0;
    }    

    size_t i = 0;    

    // copy char to out until '\0' or i == n - 1
    for(i = 0; i < n - 1 && *str != '\0'; i++){
        out[i] = *str++;
    }    

    // deal with the trailing space
    while(isspace(out[--i]));    

    out[++i] = '\0';
    return i;
}

2
Remarque: isspace(*str)UB quand *str < 0.
chux - Réintégrer Monica le

1
L'utilisation de size_t nest bonne, mais l'interface n'informe en aucun cas l'appelant lorsqu'il nest trop petit pour une chaîne complète coupée. Considérertrim(out, 12, "delete data not")
chux - Réintégrer Monica

1

Le moyen le plus simple d'ignorer les espaces de début dans une chaîne est, à mon humble avis,

#include <stdio.h>

int main()
{
char *foo="     teststring      ";
char *bar;
sscanf(foo,"%s",bar);
printf("String is >%s<\n",bar);
    return 0;
}

1
Cela ne fonctionnera pas pour les chaînes avec des espaces au milieu, comme " foo bar ".
David R Tribble

1

Ok, c'est mon point de vue sur la question. Je crois que c'est la solution la plus concise qui modifie la chaîne en place ( freefonctionnera) et évite tout UB. Pour les petites chaînes, c'est probablement plus rapide qu'une solution impliquant memmove.

void stripWS_LT(char *str)
{
    char *a = str, *b = str;
    while (isspace((unsigned char)*a)) a++;
    while (*b = *a++)  b++;
    while (b > str && isspace((unsigned char)*--b)) *b = 0;
}

Le b > strtest n'est nécessaire qu'une seule fois. *b = 0;nécessaire une seule fois.
chux - Réintégrer Monica le

1
#include <ctype.h>
#include <string.h>

char *trim_space(char *in)
{
    char *out = NULL;
    int len;
    if (in) {
        len = strlen(in);
        while(len && isspace(in[len - 1])) --len;
        while(len && *in && isspace(*in)) ++in, --len;
        if (len) {
            out = strndup(in, len);
        }
    }
    return out;
}

isspace aide à couper tous les espaces blancs.

  • Exécutez une première boucle pour vérifier à partir du dernier octet le caractère d'espace et réduire la variable de longueur
  • Exécutez une deuxième boucle pour vérifier à partir du premier octet le caractère d'espace et réduire la variable de longueur et incrémenter le pointeur de caractère.
  • Enfin, si la variable de longueur est supérieure à 0, utilisez strnduppour créer un nouveau tampon de chaîne en excluant les espaces.

Juste un petit bout, strndup()ne fait pas partie du standard C mais seulement Posix. Mais comme il est assez facile à mettre en œuvre, ce n'est pas un gros problème.
Patrick Schlüter

trim_space("")revient NULL. Je m'attendrais à un pointeur vers "". int len;devrait être size_t len;. isspace(in[len - 1])UB quand in[len - 1] < 0.
chux - Réintégrer Monica le

Un premier while (isspace((unsigned char) *in) in++;avant len = strlen(in);serait plus efficace que le plus tardwhile(len && *in && isspace(*in)) ++in, --len;
chux - Reinstate Monica

0

Personnellement, je roulerais le mien. Vous pouvez utiliser strtok, mais vous devez faire attention de le faire (en particulier si vous supprimez les caractères principaux) pour savoir ce qu'est la mémoire.

Se débarrasser des espaces de fin est facile et assez sûr, car vous pouvez simplement mettre un 0 au-dessus du dernier espace, en comptant à rebours à partir de la fin. Se débarrasser des espaces de premier plan signifie déplacer les choses. Si vous voulez le faire en place (probablement judicieux), vous pouvez simplement continuer à tout reculer d'un caractère jusqu'à ce qu'il n'y ait plus d'espace de début. Ou, pour être plus efficace, vous pouvez trouver l'index du premier caractère non espace et tout décaler de ce nombre. Ou, vous pouvez simplement utiliser un pointeur vers le premier caractère non espace (mais vous devez alors faire attention de la même manière que vous le faites avec strtok).


4
strtok n'est généralement pas un très bon outil à utiliser - notamment parce qu'il n'est pas rentrant. Si vous restez à l'intérieur d'une seule fonction, elle peut être utilisée en toute sécurité, mais s'il y a une possibilité de threads ou d'appeler d'autres fonctions qui pourraient elles-mêmes utiliser strtok, vous avez des problèmes.
Jonathan Leffler

0
#include "stdafx.h"
#include "malloc.h"
#include "string.h"

int main(int argc, char* argv[])
{

  char *ptr = (char*)malloc(sizeof(char)*30);
  strcpy(ptr,"            Hel  lo    wo           rl   d G    eo rocks!!!    by shahil    sucks b i          g       tim           e");

  int i = 0, j = 0;

  while(ptr[j]!='\0')
  {

      if(ptr[j] == ' ' )
      {
          j++;
          ptr[i] = ptr[j];
      }
      else
      {
          i++;
          j++;
          ptr[i] = ptr[j];
      }
  }


  printf("\noutput-%s\n",ptr);
        return 0;
}

3
Cela m'a fait rire parce que je pensais que dreamlax avait édité la chaîne de test pour inclure "sucks big time". Nan. L'auteur original est juste honnête.
James Morris

1
N'utilisez pas ce code. Cela produit un débordement de tampon.
Roland Illig

0

Un peu tard dans le match, mais je vais jeter mes routines dans la mêlée. Ils ne sont probablement pas les plus efficaces, mais je pense qu'ils sont corrects et simples (en rtrim()repoussant les limites de la complexité):

#include <ctype.h>
#include <string.h>

/*
    Public domain implementations of in-place string trim functions

    Michael Burr
    michael.burr@nth-element.com
    2010
*/

char* ltrim(char* s) 
{
    char* newstart = s;

    while (isspace( *newstart)) {
        ++newstart;
    }

    // newstart points to first non-whitespace char (which might be '\0')
    memmove( s, newstart, strlen( newstart) + 1); // don't forget to move the '\0' terminator

    return s;
}


char* rtrim( char* s)
{
    char* end = s + strlen( s);

    // find the last non-whitespace character
    while ((end != s) && isspace( *(end-1))) {
            --end;
    }

    // at this point either (end == s) and s is either empty or all whitespace
    //      so it needs to be made empty, or
    //      end points just past the last non-whitespace character (it might point
    //      at the '\0' terminator, in which case there's no problem writing
    //      another there).    
    *end = '\0';

    return s;
}

char*  trim( char* s)
{
    return rtrim( ltrim( s));
}

1
vous devez convertir l' charargument en isspace()pour (unsigned char)éviter un comportement indéfini sur des valeurs potentiellement négatives. Évitez également de déplacer la chaîne si ltrim()elle n'est pas nécessaire.
chqrlie

0

La plupart des réponses à ce jour font l'une des choses suivantes:

  1. Revenir en arrière à la fin de la chaîne (c'est-à-dire trouver la fin de la chaîne, puis rechercher en arrière jusqu'à ce qu'un caractère non espace soit trouvé) ou
  2. Appelez d' strlen()abord, en effectuant un deuxième passage sur toute la chaîne.

Cette version ne fait qu'un seul passage et ne revient pas en arrière. Par conséquent, il peut fonctionner mieux que les autres, mais seulement s'il est courant d'avoir des centaines d'espaces de fin (ce qui n'est pas inhabituel lorsqu'il s'agit de la sortie d'une requête SQL.)

static char const WHITESPACE[] = " \t\n\r";

static void get_trim_bounds(char  const *s,
                            char const **firstWord,
                            char const **trailingSpace)
{
    char const *lastWord;
    *firstWord = lastWord = s + strspn(s, WHITESPACE);
    do
    {
        *trailingSpace = lastWord + strcspn(lastWord, WHITESPACE);
        lastWord = *trailingSpace + strspn(*trailingSpace, WHITESPACE);
    }
    while (*lastWord != '\0');
}

char *copy_trim(char const *s)
{
    char const *firstWord, *trailingSpace;
    char *result;
    size_t newLength;

    get_trim_bounds(s, &firstWord, &trailingSpace);
    newLength = trailingSpace - firstWord;

    result = malloc(newLength + 1);
    memcpy(result, firstWord, newLength);
    result[newLength] = '\0';
    return result;
}

void inplace_trim(char *s)
{
    char const *firstWord, *trailingSpace;
    size_t newLength;

    get_trim_bounds(s, &firstWord, &trailingSpace);
    newLength = trailingSpace - firstWord;

    memmove(s, firstWord, newLength);
    s[newLength] = '\0';
}

1
Si vous êtes préoccupé par les performances, ne l'utilisez pas strspn()et strcspn()dans une boucle serrée. Ceci est très inefficace et la surcharge éclipsera l'avantage non prouvé de la passe avant unique. strlen()est généralement étendu en ligne avec un code très efficace, ce n'est pas un réel problème. Le découpage du début et de la fin de la chaîne sera beaucoup plus rapide que de tester la blancheur de chaque caractère de la chaîne, même dans le cas particulier des chaînes avec très peu ou pas de caractères non blancs.
chqrlie

0

C'est la mise en œuvre la plus courte possible à laquelle je puisse penser:

static const char *WhiteSpace=" \n\r\t";
char* trim(char *t)
{
    char *e=t+(t!=NULL?strlen(t):0);               // *e initially points to end of string
    if (t==NULL) return;
    do --e; while (strchr(WhiteSpace, *e) && e>=t);  // Find last char that is not \r\n\t
    *(++e)=0;                                      // Null-terminate
    e=t+strspn (t,WhiteSpace);                           // Find first char that is not \t
    return e>t?memmove(t,e,strlen(e)+1):t;                  // memmove string contents and terminator
}

1
Que diriez-vous de ceci:char *trim(char *s) { char *p = s, *e = s + strlen(s); while (e > s && isspace((unsigned char)e[-1])) { *--e = '\0'; } while (isspace((unsigned char)*p)) { p++; } if (p > s) { memmove(s, p, e + 1 - p); } return s; }
chqrlie

0

Ces fonctions modifieront le tampon d'origine, donc si elles sont allouées dynamiquement, le pointeur d'origine peut être libéré.

#include <string.h>

void rstrip(char *string)
{
  int l;
  if (!string)
    return;
  l = strlen(string) - 1;
  while (isspace(string[l]) && l >= 0)
    string[l--] = 0;
}

void lstrip(char *string)
{
  int i, l;
  if (!string)
    return;
  l = strlen(string);
  while (isspace(string[(i = 0)]))
    while(i++ < l)
      string[i-1] = string[i];
}

void strip(char *string)
{
  lstrip(string);
  rstrip(string);
}

rstrip()invoque un comportement non défini sur la chaîne vide. lstrip()est inutilement lent sur une chaîne avec une longue partie initiale de caractères d'espacement. isspace()ne doit pas recevoir d' charargument car il invoque un comportement non défini sur des valeurs négatives différentes de EOF.
chqrlie


0

Pour couper mes cordes des deux côtés, j'utilise l'ancien mais le gooody;) Il peut couper n'importe quoi avec ascii moins d'un espace, ce qui signifie que les caractères de contrôle seront également coupés!

char *trimAll(char *strData)
{
  unsigned int L = strlen(strData);
  if(L > 0){ L--; }else{ return strData; }
  size_t S = 0, E = L;
  while((!(strData[S] > ' ') || !(strData[E] > ' ')) && (S >= 0) && (S <= L) && (E >= 0) && (E <= L))
  {
    if(strData[S] <= ' '){ S++; }
    if(strData[E] <= ' '){ E--; }
  }
  if(S == 0 && E == L){ return strData; } // Nothing to be done
  if((S >= 0) && (S <= L) && (E >= 0) && (E <= L)){
    L = E - S + 1;
    memmove(strData,&strData[S],L); strData[L] = '\0';
  }else{ strData[0] = '\0'; }
  return strData;
}

Vous devriez utiliser à la size_tplace de unsigned int. Le code a beaucoup de tests redondants et appelle un comportement indéfini strncpy(strData,&strData[S],L)car les tableaux source et destination se chevauchent. Utilisez à la memmove()place de strncpy().
chqrlie

Dans ce cas, c'est correct car l'adresse de destination a toujours un index plus petit que la source, mais oui, memmove sera vraiment meilleur.
Деян Добромиров

non ce n'est pas OK. peu importe la façon dont les tableaux source et de destination se chevauchent, cela invoque un comportement indéfini car vous ne pouvez pas faire d'hypothèses sur l'implémentation des fonctions de la bibliothèque au-delà de leur spécification standard. Les compilateurs modernes ont tendance à tirer parti injustement des situations avec un comportement potentiel indéfini, à jouer la sécurité et à rester à l'écart d'UB, et à ne pas laisser les débutants faire des suppositions dangereuses.
chqrlie

0

Je n'inclus que du code car le code publié jusqu'à présent semble sous-optimal (et je n'ai pas encore le représentant à commenter.)

void inplace_trim(char* s)
{
    int start, end = strlen(s);
    for (start = 0; isspace(s[start]); ++start) {}
    if (s[start]) {
        while (end > 0 && isspace(s[end-1]))
            --end;
        memmove(s, &s[start], end - start);
    }
    s[end - start] = '\0';
}

char* copy_trim(const char* s)
{
    int start, end;
    for (start = 0; isspace(s[start]); ++start) {}
    for (end = strlen(s); end > 0 && isspace(s[end-1]); --end) {}
    return strndup(s + start, end - start);
}

strndup()est une extension GNU. Si vous ne l'avez pas ou quelque chose d'équivalent, lancez le vôtre. Par exemple:

r = strdup(s + start);
r[end-start] = '\0';

1
isspace(0)est défini comme étant faux, vous pouvez simplifier les deux fonctions. Déplacez également l' memmove()intérieur du ifbloc.
chqrlie

0

Ici, j'utilise l'allocation de mémoire dynamique pour couper la chaîne d'entrée à la fonction trimStr. Tout d'abord, nous trouvons combien de caractères non vides existent dans la chaîne d'entrée. Ensuite, nous allouons un tableau de caractères avec cette taille et en prenant soin du caractère terminé par null. Lorsque nous utilisons cette fonction, nous devons libérer la mémoire à l'intérieur de la fonction principale.

#include<stdio.h>
#include<stdlib.h>

char *trimStr(char *str){
char *tmp = str;
printf("input string %s\n",str);
int nc = 0;

while(*tmp!='\0'){
  if (*tmp != ' '){
  nc++;
 }
 tmp++;
}
printf("total nonempty characters are %d\n",nc);
char *trim = NULL;

trim = malloc(sizeof(char)*(nc+1));
if (trim == NULL) return NULL;
tmp = str;
int ne = 0;

while(*tmp!='\0'){
  if (*tmp != ' '){
     trim[ne] = *tmp;
   ne++;
 }
 tmp++;
}
trim[nc] = '\0';

printf("trimmed string is %s\n",trim);

return trim; 
 }


int main(void){

char str[] = " s ta ck ove r fl o w  ";

char *trim = trimStr(str);

if (trim != NULL )free(trim);

return 0;
}

0

Voici comment je le fais. Il coupe la chaîne en place, donc pas de souci de désallouer une chaîne retournée ou de perdre le pointeur vers une chaîne allouée. Ce n'est peut-être pas la réponse la plus courte possible, mais elle devrait être claire pour la plupart des lecteurs.

#include <ctype.h>
#include <string.h>
void trim_str(char *s)
{
    const size_t s_len = strlen(s);

    int i;
    for (i = 0; i < s_len; i++)
    {
        if (!isspace( (unsigned char) s[i] )) break;
    }

    if (i == s_len)
    {
        // s is an empty string or contains only space characters

        s[0] = '\0';
    }
    else
    {
        // s contains non-space characters

        const char *non_space_beginning = s + i;

        char *non_space_ending = s + s_len - 1;
        while ( isspace( (unsigned char) *non_space_ending ) ) non_space_ending--;

        size_t trimmed_s_len = non_space_ending - non_space_beginning + 1;

        if (s != non_space_beginning)
        {
            // Non-space characters exist in the beginning of s

            memmove(s, non_space_beginning, trimmed_s_len);
        }

        s[trimmed_s_len] = '\0';
    }
}

absolument clair pour les lecteurs, mais strlen effectue une autre boucle .. :)
ingconti

0
char* strtrim(char* const str)
{
    if (str != nullptr)
    {
        char const* begin{ str };
        while (std::isspace(*begin))
        {
            ++begin;
        }

        auto end{ begin };
        auto scout{ begin };
        while (*scout != '\0')
        {
            if (!std::isspace(*scout++))
            {
                end = scout;
            }
        }

        auto /* std::ptrdiff_t */ const length{ end - begin };
        if (begin != str)
        {
            std::memmove(str, begin, length);
        }

        str[length] = '\0';
    }

    return str;
}

2
Bien que ce code puisse répondre à la question, fournir un contexte supplémentaire sur la manière et / ou la raison pour laquelle il résout le problème améliorerait la valeur à long terme de la réponse.
Nic3500
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.