.NET - Wie können Sie eine durch "Großbuchstaben" getrennte Zeichenfolge in ein Array aufteilen?

114

Wie gehe ich von dieser Zeichenfolge aus: "ThisIsMyCapsDelimitedString"

... zu dieser Zeichenfolge: "This Is My Caps Delimited String"

Es werden nur wenige Codezeilen in VB.net bevorzugt, aber auch C # ist willkommen.

Prost!

Matias Nino
quelle
1
Was passiert, wenn Sie mit "OldMacDonaldAndMrO'TooleWentToMcDonalds" zu tun haben?
Grant Wagner
2
Es wird nur eine begrenzte Verwendung geben. Ich werde es hauptsächlich nur verwenden, um Variablennamen wie ThisIsMySpecialVariable,
Matias Nino
Das hat bei mir funktioniert : Regex.Replace(s, "([A-Z0-9]+)", " $1").Trim(). Und wenn Sie jeden Großbuchstaben aufteilen möchten, entfernen Sie einfach das Pluszeichen.
Mladen B.

Antworten:

173

Ich habe das vor einiger Zeit gemacht. Es entspricht jeder Komponente eines CamelCase-Namens.

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

Beispielsweise:

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

So konvertieren Sie das, indem Sie einfach Leerzeichen zwischen die Wörter einfügen:

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

Wenn Sie mit Ziffern umgehen müssen:

/([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
quelle
1
CamelCase! So hieß es! Ich liebe es! Vielen Dank!
Matias Nino
19
Tatsächlich hat camelCase einen führenden Kleinbuchstaben. Was Sie hier meinen, ist PascalCase.
Drew Noakes
12
... und wenn Sie sich auf etwas beziehen, das "Kamelfall" oder "Pascalfall" sein kann, heißt es "intercapped"
Chris
Teilt nicht "Take5", was meinen Anwendungsfall nicht erfüllen würde
PandaWood
1
@PandaWood Digits war nicht in der Frage, daher berücksichtigte meine Antwort sie nicht. Ich habe eine Variante der Muster hinzugefügt, die Ziffern berücksichtigt.
Markus Jarderot
36
Regex.Replace("ThisIsMyCapsDelimitedString", "(\\B[A-Z])", " $1")
Wayne
quelle
Dies ist die bisher beste Lösung, aber Sie müssen \\ B zum Kompilieren verwenden. Andernfalls versucht der Compiler, \ B als Escape-Sequenz zu behandeln.
Ferruccio
Schöne Lösung. Kann sich jemand einen Grund vorstellen, warum dies nicht die akzeptierte Antwort sein sollte? Ist es weniger fähig oder weniger performant?
Drew Noakes
8
In diesem Fall werden aufeinanderfolgende Großbuchstaben als separate Wörter behandelt (z. B. ANZAC besteht aus 5 Wörtern), während die Antwort von MizardX sie (meiner Meinung nach korrekt) als ein Wort behandelt.
Ray
2
@ Ray, ich würde argumentieren, dass "ANZAC" als "Anzac" geschrieben werden sollte, um als Pascal-Fallwort betrachtet zu werden, da es kein englischer Fall ist.
Sam
1
@Neaox, auf Englisch sollte es sein, aber dies ist kein Akronym-Fall oder normaler-Englisch-Fall; Es ist durch Großbuchstaben getrennt. Wenn der Quelltext genauso groß geschrieben werden soll wie im normalen Englisch, sollten auch andere Buchstaben nicht groß geschrieben werden. Warum sollte beispielsweise das "i" in "is" großgeschrieben werden, um dem durch Großbuchstaben getrennten Format zu entsprechen, nicht jedoch das "NZAC" in "ANZAC"? Wenn Sie "ANZAC" als durch Großbuchstaben getrennt interpretieren, sind es genau genommen 5 Wörter, eines für jeden Buchstaben.
Sam
19

Tolle Antwort, MizardX! Ich habe es leicht angepasst, um Ziffern als separate Wörter zu behandeln, sodass "AddressLine1" zu "Address Line 1" anstelle von "Address Line1" wird:

Regex.Replace(s, "([a-z](?=[A-Z0-9])|[A-Z](?=[A-Z][a-z]))", "$1 ")
JoshL
quelle
2
Tolle Ergänzung! Ich vermute, nicht wenige Leute werden von dem Umgang der akzeptierten Antwort mit Zahlen in Strings überrascht sein. :)
Jordan Gray
Ich weiß, es ist fast 8 Jahre her, seit du das gepostet hast, aber es hat auch perfekt für mich funktioniert. :) Die Zahlen haben mich zuerst gestolpert.
Michael Armes
Die einzige Antwort, die meine 2 Ausreißertests besteht: "Take5" -> "Take 5", "PublisherID" -> "Publisher ID". Ich möchte dies zweimal
positiv
18

Nur für ein bisschen Abwechslung ... Hier ist eine Erweiterungsmethode, die keinen regulären Ausdruck verwendet.

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
quelle
Um die Verwendung von Trim () zu vermeiden, habe ich vor dem foreach Folgendes gesetzt: int counter = -1. innen Zähler hinzufügen ++. Ändern Sie die Prüfung in: if (char.IsUpper (c) && counter> 0)
Außerhalb der Box Entwickler
Dies fügt ein Leerzeichen vor dem 1. Zeichen ein.
Zar Shardan
Ich habe mir erlaubt, das von @ZarShardan aufgezeigte Problem zu beheben. Wenn Sie die Änderung nicht mögen, können Sie sie jederzeit zurücksetzen oder bearbeiten.
jpmc26
Kann dies verbessert werden, um Abkürzungen zu verarbeiten, indem beispielsweise ein Leerzeichen vor dem letzten Großbuchstaben in einer Reihe von Großbuchstaben eingefügt wird, z. B. BOEForecast => BOE Forecast
Nepaluz
11

Grant Wagners ausgezeichneter Kommentar beiseite:

Dim s As String = RegularExpressions.Regex.Replace("ThisIsMyCapsDelimitedString", "([A-Z])", " $1")
Pseudo-Masochist
quelle
Guter Punkt ... Bitte fügen Sie die Zeichenfolge .substring (), .trimstart (), .trim (), .remove () usw. Ihrer Wahl ein. :)
Pseudo Masochist
9

Ich brauchte eine Lösung, die Akronyme und Zahlen unterstützt. Diese Regex-basierte Lösung behandelt die folgenden Muster als einzelne "Wörter":

  • Ein Großbuchstabe gefolgt von Kleinbuchstaben
  • Eine Folge von fortlaufenden Nummern
  • Aufeinanderfolgende Großbuchstaben (als Akronyme interpretiert) - Ein neues Wort kann mit dem letzten Großbuchstaben beginnen, z. B. HTMLGuide => "HTML Guide", "TheATeam" => "The A Team".

Sie könnten es als Einzeiler tun:

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

Ein besser lesbarer Ansatz könnte besser sein:

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");
        }
    }
}

Hier ist ein Auszug aus den (XUnit) -Tests:

[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
quelle
1
+ 1, um den regulären Ausdruck zu erklären und lesbar zu machen. Und ich habe etwas Neues gelernt. In .NET Regex gibt es einen Freiraummodus und Kommentare. Danke dir!
Felix Keil
4

Für mehr Abwechslung mit einfachen alten C # -Objekten wird im Folgenden die gleiche Ausgabe wie für den hervorragenden regulären Ausdruck von @ MizardX erzeugt.

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
quelle
2
Wow, das ist hässlich. Jetzt erinnere ich mich, warum ich Regex so sehr liebe! +1 für Mühe. ;)
Mark Brackett
3

Unten sehen Sie einen Prototyp, der Folgendes in Title Case konvertiert:

  • snake_case
  • camelCase
  • PascalCase
  • Satz Fall
  • Titel Groß- / Kleinschreibung (aktuelle Formatierung beibehalten)

Natürlich würden Sie nur die "ToTitleCase" -Methode selbst benötigen.

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);
    }
}

Die Konsole wäre wie folgt:

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

Blog Post referenziert

Brantley Blanchard
quelle
2
string s = "ThisIsMyCapsDelimitedString";
string t = Regex.Replace(s, "([A-Z])", " $1").Substring(1);
Ferruccio
quelle
Ich wusste, dass es einen einfachen RegEx-Weg geben würde ... Ich muss mehr damit anfangen.
Max Schmeling
1
Kein Regex-Guru, aber was passiert mit "HeresAWTFString"?
Nick
1
Sie erhalten "Heres AWTF String", aber genau darum hat Matias Nino in der Frage gebeten.
Max Schmeling
Ja, er muss hinzufügen, dass "mehrere benachbarte Hauptstädte in Ruhe gelassen werden". Was in vielen Fällen ziemlich offensichtlich erforderlich ist, zB "PublisherID" geht hier zu "Publisher I D", was schrecklich ist
PandaWood
2

Regex ist ungefähr 10-12 mal langsamer als eine einfache Schleife:

    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
quelle
1

Naive Regex-Lösung. Behandelt O'Conner nicht und fügt am Anfang der Zeichenfolge ein Leerzeichen hinzu.

s = "ThisIsMyCapsDelimitedString"
split = Regex.Replace(s, "[A-Z0-9]", " $&");
Geoff
quelle
Ich habe dich modifiziert, aber die Leute nehmen einen Smackdown besser, wenn es nicht mit "naiv" beginnt.
MusiGenesis
Ich denke nicht, dass das ein Knaller war. In diesem Zusammenhang bedeutet naiv normalerweise offensichtlich oder einfach (dh nicht unbedingt die beste Lösung). Es besteht keine Absicht der Beleidigung.
Ferruccio
0

Es gibt wahrscheinlich eine elegantere Lösung, aber das habe ich mir auf den ersten Blick ausgedacht:

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
quelle
0

Versuchen zu benutzen

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

Das Ergebnis passt zum Alphabet-Mix mit Zahlen

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

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

Implementieren des Psudo-Codes von: 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
quelle
0

Prozedurale und schnelle Implikation:

  /// <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 vom NDepend-Team
quelle
0

Eine einfache Lösung, die um Größenordnungen schneller sein sollte als eine Regex-Lösung (basierend auf den Tests, die ich mit den Top-Lösungen in diesem Thread durchgeführt habe), insbesondere wenn die Größe der Eingabezeichenfolge zunimmt:

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
quelle