.NET - Comment diviser une chaîne délimitée par «majuscules» en un tableau?

114

Comment puis-je partir de cette chaîne: "ThisIsMyCapsDelimitedString"

... à cette chaîne: "This Is My Caps Delimited String"

Le moins de lignes de code dans VB.net est préférable, mais C # est également le bienvenu.

À votre santé!

Matias Nino
la source
1
Que se passe-t-il lorsque vous avez affaire à "OldMacDonaldAndMrO'TooleWentToMcDonalds"?
Grant Wagner
2
Il ne verra qu'une utilisation limitée. Je vais principalement l'utiliser pour analyser les noms de variables tels que ThisIsMySpecialVariable,
Matias Nino
Cela a fonctionné pour moi: Regex.Replace(s, "([A-Z0-9]+)", " $1").Trim(). Et si vous souhaitez diviser sur chaque lettre majuscule, supprimez simplement le plus.
Mladen B.

Réponses:

173

J'ai fait ça il y a quelque temps. Il correspond à chaque composant d'un nom CamelCase.

/([A-Z]+(?=$|[A-Z][a-z])|[A-Z]?[a-z]+)/g

Par exemple:

"SimpleHTTPServer" => ["Simple", "HTTP", "Server"]
"camelCase" => ["camel", "Case"]

Pour convertir cela en insérant simplement des espaces entre les mots:

Regex.Replace(s, "([a-z](?=[A-Z])|[A-Z](?=[A-Z][a-z]))", "$1 ")

Si vous devez gérer des chiffres:

/([A-Z]+(?=$|[A-Z][a-z]|[0-9])|[A-Z]?[a-z]+|[0-9]+)/g

Regex.Replace(s,"([a-z](?=[A-Z]|[0-9])|[A-Z](?=[A-Z][a-z]|[0-9])|[0-9](?=[^0-9]))","$1 ")
Markus Jarderot
la source
1
Affaire de chameau! C'est comme ça que ça s'appelait! J'aime cela! Merci beaucoup!
Matias Nino
19
En fait, camelCase a une lettre minuscule en tête. Ce à quoi vous faites référence ici, c'est PascalCase.
Drew Noakes le
12
... et quand vous faites référence à quelque chose qui peut être "cas de chameau" ou "cas de pascal", cela s'appelle "intercalé"
Chris
Ne divise pas "Take5", ce qui échouerait mon cas d'utilisation
PandaWood
1
@PandaWood Digits n'était pas dans la question, donc ma réponse n'en a pas tenu compte. J'ai ajouté une variante des modèles qui tient compte des chiffres.
Markus Jarderot
36
Regex.Replace("ThisIsMyCapsDelimitedString", "(\\B[A-Z])", " $1")
Wayne
la source
C'est la meilleure solution à ce jour, mais vous devez utiliser \\ B pour compiler. Sinon, le compilateur essaie de traiter le \ B comme une séquence d'échappement.
Ferruccio
Belle solution. Quelqu'un peut-il penser à une raison pour laquelle cela ne devrait pas être la réponse acceptée? Est-ce moins capable ou moins performant?
Drew Noakes
8
Celui-ci traite les majuscules consécutives comme des mots séparés (par exemple, ANZAC est de 5 mots) alors que la réponse de MizardX le traite (correctement à mon humble avis) comme un mot.
Ray
2
@Ray, je dirais que "ANZAC" devrait être écrit comme "Anzac" pour être considéré comme un mot en cas pascal puisque ce n'est pas en anglais.
Sam le
1
@Neaox, en anglais ça devrait l'être, mais ce n'est pas acronyme-case ou normal-english-case; il est délimité par des majuscules. Si le texte source doit être mis en majuscule de la même manière qu'il l'est en anglais normal, les autres lettres ne doivent pas non plus être en majuscule. Par exemple, pourquoi le «i» de «is» doit-il être mis en majuscule pour correspondre au format délimité par des majuscules, mais pas le «NZAC» de «ANZAC»? Strictement parlant, si vous interprétez "ANZAC" comme délimité par des majuscules, alors il s'agit de 5 mots, un pour chaque lettre.
Sam
19

Excellente réponse, MizardX! Je l'ai légèrement modifié pour traiter les chiffres comme des mots séparés, de sorte que "AddressLine1" devienne "Address Line 1" au lieu de "Address Line1":

Regex.Replace(s, "([a-z](?=[A-Z0-9])|[A-Z](?=[A-Z][a-z]))", "$1 ")
JoshL
la source
2
Excellent ajout! Je soupçonne que peu de gens seront surpris par la gestion acceptée des nombres dans les chaînes. :)
Jordan Grey
Je sais que cela fait presque 8 ans que vous avez publié ceci, mais cela a fonctionné parfaitement pour moi aussi. :) Les chiffres m'ont fait trébucher au début.
Michael Armes
La seule réponse qui passe mes 2 tests aberrants: "Take5" -> "Take 5", "PublisherID" -> "Publisher ID". Je veux voter deux fois
PandaWood
18

Juste pour un peu de variété ... Voici une méthode d'extension qui n'utilise pas de regex.

public static class CamelSpaceExtensions
{
    public static string SpaceCamelCase(this String input)
    {
        return new string(Enumerable.Concat(
            input.Take(1), // No space before initial cap
            InsertSpacesBeforeCaps(input.Skip(1))
        ).ToArray());
    }

    private static IEnumerable<char> InsertSpacesBeforeCaps(IEnumerable<char> input)
    {
        foreach (char c in input)
        {
            if (char.IsUpper(c)) 
            { 
                yield return ' '; 
            }

            yield return c;
        }
    }
}
Troy Howard
la source
Pour éviter d'utiliser Trim (), avant le foreach je mets: int counter = -1. à l'intérieur, ajoutez counter ++. changez le chèque en: if (char.IsUpper (c) && counter> 0)
Outside the Box Developer
Ceci insère un espace avant le 1er caractère.
Zar Shardan
J'ai pris la liberté de résoudre le problème signalé par @ZarShardan. N'hésitez pas à revenir en arrière ou à modifier votre propre correctif si vous n'aimez pas la modification.
jpmc26
Cela peut-il être amélioré pour gérer les abréviations, par exemple en ajoutant un espace avant la dernière majuscule dans une série de lettres majuscules, par exemple BOEForecast => BOE Forecast
Nepaluz
11

Mis à part l'excellent commentaire de Grant Wagner:

Dim s As String = RegularExpressions.Regex.Replace("ThisIsMyCapsDelimitedString", "([A-Z])", " $1")
Pseudo masochiste
la source
Bon point ... N'hésitez pas à insérer les .substring (), .trimstart (), .trim (), .remove (), etc. de votre choix. :)
Pseudo masochiste
9

J'avais besoin d'une solution prenant en charge les acronymes et les nombres. Cette solution basée sur Regex traite les modèles suivants comme des «mots» individuels:

  • Une lettre majuscule suivie de lettres minuscules
  • Une séquence de nombres consécutifs
  • Majuscules consécutives (interprétées comme des acronymes) - un nouveau mot peut commencer en utilisant la dernière majuscule, par exemple HTMLGuide => "HTML Guide", "TheATeam" => "The A Team"

Vous pouvez le faire en une seule ligne:

Regex.Replace(value, @"(?<!^)((?<!\d)\d|(?(?<=[A-Z])[A-Z](?=[a-z])|[A-Z]))", " $1")

Une approche plus lisible pourrait être meilleure:

using System.Text.RegularExpressions;

namespace Demo
{
    public class IntercappedStringHelper
    {
        private static readonly Regex SeparatorRegex;

        static IntercappedStringHelper()
        {
            const string pattern = @"
                (?<!^) # Not start
                (
                    # Digit, not preceded by another digit
                    (?<!\d)\d 
                    |
                    # Upper-case letter, followed by lower-case letter if
                    # preceded by another upper-case letter, e.g. 'G' in HTMLGuide
                    (?(?<=[A-Z])[A-Z](?=[a-z])|[A-Z])
                )";

            var options = RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled;

            SeparatorRegex = new Regex(pattern, options);
        }

        public static string SeparateWords(string value, string separator = " ")
        {
            return SeparatorRegex.Replace(value, separator + "$1");
        }
    }
}

Voici un extrait des tests (XUnit):

[Theory]
[InlineData("PurchaseOrders", "Purchase-Orders")]
[InlineData("purchaseOrders", "purchase-Orders")]
[InlineData("2Unlimited", "2-Unlimited")]
[InlineData("The2Unlimited", "The-2-Unlimited")]
[InlineData("Unlimited2", "Unlimited-2")]
[InlineData("222Unlimited", "222-Unlimited")]
[InlineData("The222Unlimited", "The-222-Unlimited")]
[InlineData("Unlimited222", "Unlimited-222")]
[InlineData("ATeam", "A-Team")]
[InlineData("TheATeam", "The-A-Team")]
[InlineData("TeamA", "Team-A")]
[InlineData("HTMLGuide", "HTML-Guide")]
[InlineData("TheHTMLGuide", "The-HTML-Guide")]
[InlineData("TheGuideToHTML", "The-Guide-To-HTML")]
[InlineData("HTMLGuide5", "HTML-Guide-5")]
[InlineData("TheHTML5Guide", "The-HTML-5-Guide")]
[InlineData("TheGuideToHTML5", "The-Guide-To-HTML-5")]
[InlineData("TheUKAllStars", "The-UK-All-Stars")]
[InlineData("AllStarsUK", "All-Stars-UK")]
[InlineData("UKAllStars", "UK-All-Stars")]
Dan Malcolm
la source
1
+ 1 pour expliquer le regex et le rendre lisible. Et j'ai appris quelque chose de nouveau. Il existe un mode d'espacement libre et des commentaires dans .NET Regex. Je vous remercie!
Felix Keil
4

Pour plus de variété, en utilisant des objets C # anciens, ce qui suit produit le même résultat que l'excellente expression régulière de @ MizardX.

public string FromCamelCase(string camel)
{   // omitted checking camel for null
    StringBuilder sb = new StringBuilder();
    int upperCaseRun = 0;
    foreach (char c in camel)
    {   // append a space only if we're not at the start
        // and we're not already in an all caps string.
        if (char.IsUpper(c))
        {
            if (upperCaseRun == 0 && sb.Length != 0)
            {
                sb.Append(' ');
            }
            upperCaseRun++;
        }
        else if( char.IsLower(c) )
        {
            if (upperCaseRun > 1) //The first new word will also be capitalized.
            {
                sb.Insert(sb.Length - 1, ' ');
            }
            upperCaseRun = 0;
        }
        else
        {
            upperCaseRun = 0;
        }
        sb.Append(c);
    }

    return sb.ToString();
}
Robert Paulson
la source
2
Wow, c'est moche. Maintenant, je me souviens pourquoi j'aime si profondément regex! +1 pour l'effort, cependant. ;)
Mark Brackett
3

Voici un prototype qui convertit ce qui suit en cas de titre:

  • snake_case
  • affaire de chameau
  • PascalCase
  • cas de phrase
  • Casse du titre (conserver la mise en forme actuelle)

Évidemment, vous n'avez besoin que de la méthode "ToTitleCase" vous-même.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;

public class Program
{
    public static void Main()
    {
        var examples = new List<string> { 
            "THEQuickBrownFox",
            "theQUICKBrownFox",
            "TheQuickBrownFOX",
            "TheQuickBrownFox",
            "the_quick_brown_fox",
            "theFOX",
            "FOX",
            "QUICK"
        };

        foreach (var example in examples)
        {
            Console.WriteLine(ToTitleCase(example));
        }
    }

    private static string ToTitleCase(string example)
    {
        var fromSnakeCase = example.Replace("_", " ");
        var lowerToUpper = Regex.Replace(fromSnakeCase, @"(\p{Ll})(\p{Lu})", "$1 $2");
        var sentenceCase = Regex.Replace(lowerToUpper, @"(\p{Lu}+)(\p{Lu}\p{Ll})", "$1 $2");
        return new CultureInfo("en-US", false).TextInfo.ToTitleCase(sentenceCase);
    }
}

La sortie de la console serait la suivante:

THE Quick Brown Fox
The QUICK Brown Fox
The Quick Brown FOX
The Quick Brown Fox
The Quick Brown Fox
The FOX
FOX
QUICK

Article de blog référencé

Brantley Blanchard
la source
2
string s = "ThisIsMyCapsDelimitedString";
string t = Regex.Replace(s, "([A-Z])", " $1").Substring(1);
Ferruccio
la source
Je savais qu'il y aurait un moyen RegEx simple ... Je dois commencer à l'utiliser davantage.
Max Schmeling
1
Pas un gourou de regex mais que se passe-t-il avec "HeresAWTFString"?
Nick
1
Vous obtenez "Heres AWTF String" mais c'est exactement ce que Matias Nino a demandé dans la question.
Max Schmeling
Ouais, il doit ajouter que "plusieurs capitales adjacentes sont laissées seules". Ce qui est assez évidemment nécessaire dans de nombreux cas, par exemple "PublisherID" va ici à "Publisher I D" qui est horrible
PandaWood
2

Regex est environ 10 à 12 fois plus lent qu'une simple boucle:

    public static string CamelCaseToSpaceSeparated(this string str)
    {
        if (string.IsNullOrEmpty(str))
        {
            return str;
        }

        var res = new StringBuilder();

        res.Append(str[0]);
        for (var i = 1; i < str.Length; i++)
        {
            if (char.IsUpper(str[i]))
            {
                res.Append(' ');
            }
            res.Append(str[i]);

        }
        return res.ToString();
    }
Zar Shardan
la source
1

Solution regex naïve. Ne gérera pas O'Conner et ajoute également un espace au début de la chaîne.

s = "ThisIsMyCapsDelimitedString"
split = Regex.Replace(s, "[A-Z0-9]", " $&");
Geoff
la source
Je vous ai modifié, mais les gens acceptent généralement mieux une claque si elle ne commence pas par «naïf».
MusiGenesis
Je ne pense pas que ce soit une claque. Dans ce contexte, naïf signifie généralement évident ou simple (c'est-à-dire pas nécessairement la meilleure solution). Il n'y a aucune intention d'insulte.
Ferruccio
0

Il y a probablement une solution plus élégante, mais c'est ce que je propose du haut de ma tête:

string myString = "ThisIsMyCapsDelimitedString";

for (int i = 1; i < myString.Length; i++)
{
     if (myString[i].ToString().ToUpper() == myString[i].ToString())
     {
          myString = myString.Insert(i, " ");
          i++;
     }
}
Max Schmeling
la source
0

Essayez d'utiliser

"([A-Z]*[^A-Z]*)"

Le résultat conviendra au mélange d'alphabet avec des nombres

Regex.Replace("AbcDefGH123Weh", "([A-Z]*[^A-Z]*)", "$1 ");
Abc Def GH123 Weh  

Regex.Replace("camelCase", "([A-Z]*[^A-Z]*)", "$1 ");
camel Case  
Erxin
la source
0

Implémentation du code psudo à partir de: https://stackoverflow.com/a/5796394/4279201

    private static StringBuilder camelCaseToRegular(string i_String)
    {
        StringBuilder output = new StringBuilder();
        int i = 0;
        foreach (char character in i_String)
        {
            if (character <= 'Z' && character >= 'A' && i > 0)
            {
                output.Append(" ");
            }
            output.Append(character);
            i++;
        }
        return output;
    }
Shinzou
la source
0

Implément procédural et rapide:

  /// <summary>
  /// Get the words in a code <paramref name="identifier"/>.
  /// </summary>
  /// <param name="identifier">The code <paramref name="identifier"/></param> to extract words from.
  public static string[] GetWords(this string identifier) {
     Contract.Ensures(Contract.Result<string[]>() != null, "returned array of string is not null but can be empty");
     if (identifier == null) { return new string[0]; }
     if (identifier.Length == 0) { return new string[0]; }

     const int MIN_WORD_LENGTH = 2;  //  Ignore one letter or one digit words

     var length = identifier.Length;
     var list = new List<string>(1 + length/2); // Set capacity, not possible more words since we discard one char words
     var sb = new StringBuilder();
     CharKind cKindCurrent = GetCharKind(identifier[0]); // length is not zero here
     CharKind cKindNext = length == 1 ? CharKind.End : GetCharKind(identifier[1]);

     for (var i = 0; i < length; i++) {
        var c = identifier[i];
        CharKind cKindNextNext = (i >= length - 2) ? CharKind.End : GetCharKind(identifier[i + 2]);

        // Process cKindCurrent
        switch (cKindCurrent) {
           case CharKind.Digit:
           case CharKind.LowerCaseLetter:
              sb.Append(c); // Append digit or lowerCaseLetter to sb
              if (cKindNext == CharKind.UpperCaseLetter) {
                 goto TURN_SB_INTO_WORD; // Finish word if next char is upper
              }
              goto CHAR_PROCESSED;
           case CharKind.Other:
              goto TURN_SB_INTO_WORD;
           default:  // charCurrent is never Start or End
              Debug.Assert(cKindCurrent == CharKind.UpperCaseLetter);
              break;
        }

        // Here cKindCurrent is UpperCaseLetter
        // Append UpperCaseLetter to sb anyway
        sb.Append(c); 

        switch (cKindNext) {
           default:
              goto CHAR_PROCESSED;

           case CharKind.UpperCaseLetter: 
              //  "SimpleHTTPServer"  when we are at 'P' we need to see that NextNext is 'e' to get the word!
              if (cKindNextNext == CharKind.LowerCaseLetter) {
                 goto TURN_SB_INTO_WORD;
              }
              goto CHAR_PROCESSED;

           case CharKind.End:
           case CharKind.Other:
              break; // goto TURN_SB_INTO_WORD;
        }

        //------------------------------------------------

     TURN_SB_INTO_WORD:
        string word = sb.ToString();
        sb.Length = 0;
        if (word.Length >= MIN_WORD_LENGTH) {  
           list.Add(word);
        }

     CHAR_PROCESSED:
        // Shift left for next iteration!
        cKindCurrent = cKindNext;
        cKindNext = cKindNextNext;
     }

     string lastWord = sb.ToString();
     if (lastWord.Length >= MIN_WORD_LENGTH) {
        list.Add(lastWord);
     }
     return list.ToArray();
  }
  private static CharKind GetCharKind(char c) {
     if (char.IsDigit(c)) { return CharKind.Digit; }
     if (char.IsLetter(c)) {
        if (char.IsUpper(c)) { return CharKind.UpperCaseLetter; }
        Debug.Assert(char.IsLower(c));
        return CharKind.LowerCaseLetter;
     }
     return CharKind.Other;
  }
  enum CharKind {
     End, // For end of string
     Digit,
     UpperCaseLetter,
     LowerCaseLetter,
     Other
  }

Tests:

  [TestCase((string)null, "")]
  [TestCase("", "")]

  // Ignore one letter or one digit words
  [TestCase("A", "")]
  [TestCase("4", "")]
  [TestCase("_", "")]
  [TestCase("Word_m_Field", "Word Field")]
  [TestCase("Word_4_Field", "Word Field")]

  [TestCase("a4", "a4")]
  [TestCase("ABC", "ABC")]
  [TestCase("abc", "abc")]
  [TestCase("AbCd", "Ab Cd")]
  [TestCase("AbcCde", "Abc Cde")]
  [TestCase("ABCCde", "ABC Cde")]

  [TestCase("Abc42Cde", "Abc42 Cde")]
  [TestCase("Abc42cde", "Abc42cde")]
  [TestCase("ABC42Cde", "ABC42 Cde")]
  [TestCase("42ABC", "42 ABC")]
  [TestCase("42abc", "42abc")]

  [TestCase("abc_cde", "abc cde")]
  [TestCase("Abc_Cde", "Abc Cde")]
  [TestCase("_Abc__Cde_", "Abc Cde")]
  [TestCase("ABC_CDE_FGH", "ABC CDE FGH")]
  [TestCase("ABC CDE FGH", "ABC CDE FGH")] // Should not happend (white char) anything that is not a letter/digit/'_' is considered as a separator
  [TestCase("ABC,CDE;FGH", "ABC CDE FGH")] // Should not happend (,;) anything that is not a letter/digit/'_' is considered as a separator
  [TestCase("abc<cde", "abc cde")]
  [TestCase("abc<>cde", "abc cde")]
  [TestCase("abc<D>cde", "abc cde")]  // Ignore one letter or one digit words
  [TestCase("abc<Da>cde", "abc Da cde")]
  [TestCase("abc<cde>", "abc cde")]

  [TestCase("SimpleHTTPServer", "Simple HTTP Server")]
  [TestCase("SimpleHTTPS2erver", "Simple HTTPS2erver")]
  [TestCase("camelCase", "camel Case")]
  [TestCase("m_Field", "Field")]
  [TestCase("mm_Field", "mm Field")]
  public void Test_GetWords(string identifier, string expectedWordsStr) {
     var expectedWords = expectedWordsStr.Split(' ');
     if (identifier == null || identifier.Length <= 1) {
        expectedWords = new string[0];
     }

     var words = identifier.GetWords();
     Assert.IsTrue(words.SequenceEqual(expectedWords));
  }
Patrick de l'équipe NDepend
la source
0

Une solution simple, qui devrait être d'un ordre de grandeur plus rapide qu'une solution regex (basée sur les tests que j'ai exécutés contre les meilleures solutions de ce thread), d'autant plus que la taille de la chaîne d'entrée augmente:

string s1 = "ThisIsATestStringAbcDefGhiJklMnoPqrStuVwxYz";
string s2;
StringBuilder sb = new StringBuilder();

foreach (char c in s1)
    sb.Append(char.IsUpper(c)
        ? " " + c.ToString()
        : c.ToString());

s2 = sb.ToString();
iliketocode
la source