.NET - Jak podzielić ciąg rozdzielany „kapslami” na tablicę?

114

Jak przejść z tego ciągu: „ThisIsMyCapsDelimitedString”

... do tego ciągu: „To jest ciąg rozdzielany dużymi literami”

Preferowana jest najmniejsza liczba wierszy kodu w VB.net, ale mile widziany jest również język C #.

Twoje zdrowie!

Matias Nino
źródło
1
Co się dzieje, gdy masz do czynienia z „OldMacDonaldAndMrO'TooleWentToMcDonalds”?
Grant Wagner
2
Będzie miał ograniczone zastosowanie. Będę głównie używał go do analizowania nazw zmiennych, takich jak ThisIsMySpecialVariable,
Matias Nino
Ten pracował dla mnie Regex.Replace(s, "([A-Z0-9]+)", " $1").Trim(). A jeśli chcesz podzielić każdą wielką literę, po prostu usuń plus.
Mladen B.

Odpowiedzi:

173

Zrobiłem to jakiś czas temu. Pasuje do każdego składnika nazwy CamelCase.

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

Na przykład:

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

Aby przekonwertować to, aby po prostu wstawić spacje między słowami:

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

Jeśli potrzebujesz obsługi cyfr:

/([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
źródło
1
CamelCase! Tak to się nazywało! Kocham to! Dzięki wielkie!
Matias Nino
19
W rzeczywistości camelCase ma wiodącą małą literę. Masz na myśli PascalCase.
Drew Noakes,
12
... a kiedy odnosisz się do czegoś, co może być „przypadkiem wielbłąda” lub „przypadkiem pascala”, nazywa się to „intercapped”
Chris
Nie rozdziela „Take5”, co mogłoby zawieść mój przypadek użycia
PandaWood
1
@PandaWood Digits nie było w pytaniu, więc moja odpowiedź nie uwzględniła ich. Dodałem wariant wzorców uwzględniający cyfry.
Markus Jarderot
36
Regex.Replace("ThisIsMyCapsDelimitedString", "(\\B[A-Z])", " $1")
Wayne
źródło
Jak dotąd jest to najlepsze rozwiązanie, ale do kompilacji musisz użyć \\ B. W przeciwnym razie kompilator próbuje traktować \ B jako sekwencję ucieczki.
Ferruccio
Niezłe rozwiązanie. Czy ktoś może wymyślić powód, dla którego nie powinna to być akceptowana odpowiedź? Czy jest mniej sprawny czy mniej wydajny?
Drew Noakes
8
Ten traktuje kolejne wielkie litery jako oddzielne słowa (np. ANZAC to 5 słów), podczas gdy odpowiedź MizardX traktuje je (poprawnie IMHO) jako jedno słowo.
Ray
2
@Ray, argumentowałbym, że „ANZAC” powinno być zapisane jako „Anzac”, aby było traktowane jako słowo pascalowe, ponieważ nie jest to angielskie słowo.
Sam,
1
@Neaox, po angielsku powinno być, ale to nie jest akronim-przypadek ani normalny-angielski-przypadek; jest rozdzielany wielkimi literami. Jeśli tekst źródłowy powinien być pisany wielkimi literami w taki sam sposób, jak w normalnym języku angielskim, inne litery również nie powinny być pisane wielką literą. Na przykład, dlaczego „i” w ”jest„ pisane wielką literą, aby pasowało do formatu rozdzielanego wielkimi literami, ale nie „NZAC” w „ANZAC”? Ściśle mówiąc, jeśli interpretujesz „ANZAC” jako rozdzielany wielkimi literami, będzie to 5 słów, po jednym na każdą literę.
Sam
19

Świetna odpowiedź, MizardX! Poprawiłem go nieznacznie, aby traktować cyfry jako oddzielne słowa, tak aby „Wiersz adresu1” stał się „Wiersz adresu 1” zamiast „Wiersz adresu 1”:

Regex.Replace(s, "([a-z](?=[A-Z0-9])|[A-Z](?=[A-Z][a-z]))", "$1 ")
JoshL
źródło
2
Świetny dodatek! Podejrzewam, że wiele osób będzie zaskoczonych obsługą liczb w łańcuchach przez zaakceptowaną odpowiedź. :)
Jordan Grey
Wiem, że minęło prawie 8 lat, odkąd to opublikowałeś, ale dla mnie też zadziałało idealnie. :) Liczby mnie zaskoczyły.
Michael Armes
Jedyna odpowiedź, która przechodzi moje dwa testy odstających: „Take5” -> „Take 5”, „PublisherID” -> „Publisher ID”. Chcę to dwukrotnie
zagłosować
18

Tylko dla małej odmiany ... Oto metoda rozszerzenia, która nie używa wyrażenia regularnego.

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
źródło
Aby uniknąć używania Trim (), przed foreach wstawiłem: int counter = -1. wewnątrz, dodaj licznik ++. zmień czek na: if (char.IsUpper (c) && counter> 0)
Outside the Box Developer
Wstawia spację przed pierwszym znakiem.
Zar Shardan
Pozwoliłem sobie rozwiązać problem wskazany przez @ZarShardan. Jeśli nie podoba Ci się ta zmiana, możesz cofnąć lub edytować własną poprawkę.
jpmc26
Czy można to ulepszyć w celu obsługi skrótów, na przykład dodając spację przed ostatnią wielką literą w serii wielkich liter, np. BOEForecast => BOE Forecast
Nepaluz
11

Pomijając doskonały komentarz Granta Wagnera:

Dim s As String = RegularExpressions.Regex.Replace("ThisIsMyCapsDelimitedString", "([A-Z])", " $1")
Pseudo masochista
źródło
Słuszna uwaga ... Nie wahaj się wstawić dowolnych plików .substring (), .trimstart (), .trim (), .remove () itp. :)
Pseudo Masochist
9

Potrzebowałem rozwiązania obsługującego akronimy i liczby. To rozwiązanie oparte na Regex traktuje następujące wzorce jako pojedyncze „słowa”:

  • Wielka litera, po której następują małe litery
  • Sekwencja kolejnych liczb
  • Kolejne wielkie litery (interpretowane jako akronimy) - nowe słowo może zaczynać się od ostatniej dużej litery, np. HTMLGuide => "Przewodnik HTML", "TheATeam" => "Drużyna A"

Możesz to zrobić jako jednowierszowy:

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

Bardziej czytelne podejście mogłoby być lepsze:

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

Oto wyciąg z testów (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
źródło
1
+1 za wyjaśnienie wyrażenia regularnego i uczynienie go tak czytelnym. I nauczyłem się czegoś nowego. W .NET Regex jest dostępny tryb wolnych odstępów i komentarze. Dziękuję Ci!
Felix Keil
4

Aby uzyskać większą różnorodność, używając zwykłych starych obiektów C #, następujące dane wyjściowe dają te same dane wyjściowe, co doskonałe wyrażenie regularne @ 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
źródło
2
Wow, to brzydkie. Teraz pamiętam, dlaczego tak bardzo uwielbiam wyrażenia regularne! Jednak +1 za wysiłek. ;)
Mark Brackett
3

Poniżej znajduje się prototyp, który konwertuje następujące elementy na wielkość liter:

  • snake_case
  • camelCase
  • PascalCase
  • przypadek zdania
  • Wielkość liter (zachowaj aktualne formatowanie)

Oczywiście potrzebowałbyś tylko metody „ToTitleCase”.

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

Konsola wyglądałaby następująco:

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

Odwołano się do posta na blogu

Brantley Blanchard
źródło
2
string s = "ThisIsMyCapsDelimitedString";
string t = Regex.Replace(s, "([A-Z])", " $1").Substring(1);
Ferruccio
źródło
Wiedziałem, że będzie łatwy sposób na RegEx ... Muszę zacząć go częściej używać.
Max Schmeling
1
Nie guru wyrażeń regularnych, ale co się dzieje z „HeresAWTFString”?
Nick
1
Dostajesz „Heres AWTF String”, ale właśnie o to zapytał Matias Nino w pytaniu.
Max Schmeling
Tak, musi dodać, że „wiele sąsiednich stolic pozostaje w spokoju”. Co jest dość oczywiste w wielu przypadkach, np. „PublisherID” oznacza tutaj „Publisher I D”, co jest okropne
PandaWood
2

Regex jest około 10-12 razy wolniejszy niż prosta pętla:

    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
źródło
1

Naiwne rozwiązanie regex. Nie obsługuje O'Conner i dodaje również spację na początku łańcucha.

s = "ThisIsMyCapsDelimitedString"
split = Regex.Replace(s, "[A-Z0-9]", " $&");
Geoff
źródło
Zmodyfikowałem cię, ale ludzie generalnie lepiej przyjmują klapsa, jeśli nie zaczyna się od „naiwności”.
MusiGenesis,
Nie sądzę, żeby to był klaps. W tym kontekście naiwność zwykle oznacza oczywistość lub prostotę (czyli niekoniecznie najlepsze rozwiązanie). Nie ma zamiaru obrażać.
Ferruccio,
0

Prawdopodobnie jest bardziej eleganckie rozwiązanie, ale oto, co wymyśliłem od razu:

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
źródło
0

Spróbuj użyć

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

Wynik będzie pasował do mieszanki alfabetu z liczbami

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

Regex.Replace("camelCase", "([A-Z]*[^A-Z]*)", "$1 ");
camel Case  
Erxin
źródło
0

Implementacja kodu psudo z: 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
źródło
0

Proceduralne i szybkie impl:

  /// <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
  }

Testy:

  [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 z zespołu NDepend
źródło
0

Proste rozwiązanie, które powinno być o rząd (y) wielkości szybsze niż rozwiązanie regex (na podstawie testów, które przeprowadziłem z najlepszymi rozwiązaniami w tym wątku), szczególnie gdy rozmiar ciągu wejściowego rośnie:

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
źródło