.NET - Come puoi dividere una stringa delimitata da "maiuscole" in un array?


114

Come faccio a passare da questa stringa: "ThisIsMyCapsDelimitedString"

... a questa stringa: "Questa è la mia stringa delimitata da maiuscole"

È preferibile il minor numero di righe di codice in VB.net, ma anche C # è il benvenuto.

Saluti!


1
Cosa succede quando hai a che fare con "OldMacDonaldAndMrO'TooleWentToMcDonalds"?
Grant Wagner,

2
Vedrà solo un uso limitato. Lo userò principalmente per analizzare nomi di variabili come ThisIsMySpecialVariable,
Matias Nino

Questo ha funzionato per me: Regex.Replace(s, "([A-Z0-9]+)", " $1").Trim(). E se vuoi dividere su ogni lettera maiuscola, rimuovi il segno più.
Mladen B.

Risposte:


173

L'ho fatto tempo fa. Corrisponde a ogni componente di un nome CamelCase.

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

Per esempio:

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

Per convertirlo per inserire semplicemente spazi tra le parole:

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

Se devi gestire le cifre:

/([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 ")

1
CamelCase! Ecco come si chiamava! Lo adoro! Grazie mille
Matias Nino

19
In realtà camelCase ha una lettera minuscola iniziale. Quello a cui ti riferisci qui è PascalCase.
Drew Noakes,

12
... e quando ti riferisci a qualcosa che può essere "custodia cammello" o "custodia pascal" si chiama "intercapped"
Chris

Non divide "Take5", il che
fallirebbe nel

1
@ PandaWood Digits non era nella domanda, quindi la mia risposta non ne ha tenuto conto. Ho aggiunto una variante dei modelli che tiene conto delle cifre.
Markus Jarderot

36
Regex.Replace("ThisIsMyCapsDelimitedString", "(\\B[A-Z])", " $1")

Questa è la soluzione migliore finora, ma è necessario utilizzare \\ B per compilare. In caso contrario, il compilatore tenta di trattare \ B come una sequenza di escape.
Ferruccio

Bella soluzione. Qualcuno può pensare a una ragione per cui questa non dovrebbe essere la risposta accettata? È meno capace o meno performante?
Drew Noakes

8
Questo tratta maiuscole consecutive come parole separate (es. ANZAC è di 5 parole) mentre la risposta di MizardX la tratta (correttamente IMHO) come una parola.
Ray

2
@ Ray, direi che "ANZAC" dovrebbe essere scritto come "Anzac" per essere considerato una parola case pascal poiché non è inglese.
Sam

1
@Neaox, in inglese dovrebbe essere, ma questo non è acronimo-case o normal-english-case; è delimitato da maiuscole. Se il testo di origine deve essere in maiuscolo nello stesso modo in cui è in inglese normale, anche altre lettere non dovrebbero essere maiuscole. Ad esempio, perché la "i" in "è" in maiuscolo per adattarsi al formato delimitato da maiuscole ma non la "NZAC" in "ANZAC"? A rigor di termini, se interpreti "ANZAC" come delimitato da maiuscole, si tratta di 5 parole, una per ogni lettera.
Sam

19

Ottima risposta, MizardX! L'ho modificato leggermente per trattare i numeri come parole separate, in modo che "AddressLine1" diventasse "Address Line 1" invece di "Address Line1":

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

2
Ottima aggiunta! Sospetto che non poche persone saranno sorprese dalla gestione dei numeri in stringhe da parte della risposta accettata. :)
Jordan Grey

So che sono passati quasi 8 anni da quando l'hai postato, ma ha funzionato perfettamente anche per me. :) All'inizio i numeri mi hanno fatto inciampare.
Michael Armes

L'unica risposta che supera i miei 2 test anomali: "Take5" -> "Take 5", "PublisherID" -> "Publisher ID". Voglio
esprimere un voto positivo

18

Solo per una piccola varietà ... Ecco un metodo di estensione che non utilizza un'espressione regolare.

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

Per evitare di usare Trim (), prima di foreach metto: int counter = -1. all'interno, aggiungi counter ++. cambia il segno di spunta in: if (char.IsUpper (c) && counter> 0)
Outside the Box Developer

Questo inserisce uno spazio prima del primo carattere.
Zar Shardan

Mi sono preso la libertà di risolvere il problema segnalato da @ZarShardan. Sentiti libero di ripristinare o modificare la tua correzione se non ti piace la modifica.
jpmc26

Può essere migliorato per gestire le abbreviazioni, ad esempio aggiungendo uno spazio prima dell'ultima maiuscola in una serie di lettere maiuscole, ad esempio BOEForecast => Previsione BOE
Nepaluz

11

A parte l'eccellente commento di Grant Wagner:

Dim s As String = RegularExpressions.Regex.Replace("ThisIsMyCapsDelimitedString", "([A-Z])", " $1")

Buon punto ... Sentiti libero di inserire .substring (), .trimstart (), .trim (), .remove (), ecc. Di tua scelta. :)
Pseudo Masochista

9

Avevo bisogno di una soluzione che supportasse acronimi e numeri. Questa soluzione basata su Regex tratta i seguenti modelli come singole "parole":

  • Una lettera maiuscola seguita da lettere minuscole
  • Una sequenza di numeri consecutivi
  • Lettere maiuscole consecutive (interpretate come acronimi): una nuova parola può iniziare usando l'ultima maiuscola, ad esempio HTMLGuide => "Guida HTML", "TheATeam" => "The A Team"

Si potrebbe farlo come un one-liner:

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

Un approccio più leggibile potrebbe essere migliore:

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

Ecco un estratto dei test (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")]

1
+1 per aver spiegato la regex e averla resa leggibile. E ho imparato qualcosa di nuovo. Esiste una modalità di spaziatura libera e commenti in .NET Regex. Grazie!
Felix Keil

4

Per una maggiore varietà, utilizzando semplici vecchi oggetti C #, quanto segue produce lo stesso output dell'eccellente espressione regolare di @ 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();
}

2
Wow, è brutto. Ora ricordo perché amo così tanto le espressioni regolari! +1 per lo sforzo, però. ;)
Mark Brackett

3

Di seguito è riportato un prototipo che converte quanto segue in maiuscolo / minuscolo:

  • snake_case
  • camelCase
  • PascalCase
  • caso della frase
  • Maiuscole / minuscole (mantieni la formattazione corrente)

Ovviamente avresti bisogno solo del metodo "ToTitleCase" da solo.

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

L'uscita della console sarebbe la seguente:

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

Post del blog referenziato


2
string s = "ThisIsMyCapsDelimitedString";
string t = Regex.Replace(s, "([A-Z])", " $1").Substring(1);

Sapevo che ci sarebbe stato un modo RegEx semplice ... devo iniziare a usarlo di più.
Max Schmeling,

1
Non un guru delle espressioni regolari, ma cosa succede con "HeresAWTFString"?
Nick

1
Ottieni "Heres AWTF String" ma è esattamente ciò che Matias Nino ha chiesto nella domanda.
Max Schmeling

Sì, ha bisogno di aggiungere che "più capitali adiacenti vengono lasciati soli". Che è abbastanza ovviamente richiesto in molti casi, ad esempio "PublisherID" qui va a "Publisher I D", il che è orribile
PandaWood

2

Regex è circa 10-12 volte più lento di un semplice ciclo:

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

1

Soluzione regex ingenua. Non gestisce O'Conner e aggiunge anche uno spazio all'inizio della stringa.

s = "ThisIsMyCapsDelimitedString"
split = Regex.Replace(s, "[A-Z0-9]", " $&");

Ti ho modificato, ma la gente generalmente prende uno schiaffo meglio se non inizia con "ingenuo".
MusiGenesis

Non credo che sia stato uno schianto. In questo contesto, ingenuo di solito significa ovvio o semplice (cioè non necessariamente la soluzione migliore). Non c'è intenzione di insultare.
Ferruccio

0

Probabilmente c'è una soluzione più elegante, ma questo è ciò che mi viene in mente dalla cima della mia testa:

string myString = "ThisIsMyCapsDelimitedString";

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

0

Prova ad usare

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

Il risultato si adatterà al mix di alfabeto con i numeri

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

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

0

Implementazione del codice psudo da: 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;
    }


0

Implant procedurale e veloce:

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

test:

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

0

Una soluzione semplice, che dovrebbe essere un ordine di grandezza più veloce di una soluzione regex (in base ai test che ho eseguito contro le soluzioni migliori in questo thread), soprattutto quando la dimensione della stringa di input cresce:

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();
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.