.NET - como você pode dividir uma string delimitada por “maiúsculas” em uma matriz?

114

Como faço para sair desta string: "ThisIsMyCapsDelimitedString"

... para esta string: "This Is My Caps Delimited String"

O mínimo de linhas de código em VB.net é preferível, mas C # também é bem-vindo.

Felicidades!

Matias Nino
fonte
1
O que acontece quando você tem que lidar com "OldMacDonaldAndMrO'TooleWentToMcDonalds"?
Grant Wagner
2
Seu uso será limitado. Estarei usando principalmente para analisar nomes de variáveis, como ThisIsMySpecialVariable,
Matias Nino
Isso funcionou para mim: Regex.Replace(s, "([A-Z0-9]+)", " $1").Trim(). E se você quiser dividir em cada letra maiúscula, basta remover o sinal de mais.
Mladen B.

Respostas:

173

Eu fiz isso há um tempo. Corresponde a cada componente de um nome CamelCase.

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

Por exemplo:

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

Para converter isso para apenas inserir espaços entre as palavras:

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

Se você precisa lidar com dígitos:

/([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
fonte
1
CamelCase! Foi assim que se chamou! Eu amo isso! Muito obrigado!
Matias Nino
19
Na verdade, camelCase tem uma letra minúscula inicial. O que você está se referindo aqui é PascalCase.
Drew Noakes em
12
... e quando você se refere a algo que pode ser "caixa de camelo" ou "caixa de pascal", isso é chamado de "intercalado"
Chris
Não divide "Take5", o que falha no meu caso de uso
PandaWood
1
@PandaWood Digits não estava na pergunta, então minha resposta não os considerou. Eu adicionei uma variante dos padrões que leva em conta os dígitos.
Markus Jarderot
36
Regex.Replace("ThisIsMyCapsDelimitedString", "(\\B[A-Z])", " $1")
Wayne
fonte
Esta é a melhor solução até agora, mas você precisa usar \\ B para compilar. Caso contrário, o compilador tenta tratar o \ B como uma seqüência de escape.
Ferruccio
Ótima solução. Alguém consegue pensar em uma razão para que essa não seja a resposta aceita? É menos capaz ou tem menos desempenho?
Drew Noakes em
8
Este trata maiúsculas consecutivas como palavras separadas (por exemplo, ANZAC é 5 palavras), enquanto a resposta de MizardX o trata (corretamente IMHO) como uma palavra.
Ray
2
@Ray, eu diria que "ANZAC" deveria ser escrito como "Anzac" para ser considerada uma palavra caseira pascal, já que não é case inglesa.
Sam de
1
@Neaox, em inglês deveria ser, mas isso não é acronym-case ou normal-english-case; é delimitado por maiúsculas. Se o texto de origem deve ser capitalizado da mesma forma que em inglês normal, então as outras letras também não devem ser capitalizadas. Por exemplo, por que o "i" em "é" maiúsculo para caber no formato delimitado por maiúsculas, mas não o "NZAC" em "ANZAC"? Estritamente falando, se você interpretar "ANZAC" como delimitado por maiúsculas, terá 5 palavras, uma para cada letra.
Sam
19

Ótima resposta, MizardX! Eu ajustei um pouco para tratar os numerais como palavras separadas, de modo que "AddressLine1" se tornasse "Address Line 1" em vez de "Address Line1":

Regex.Replace(s, "([a-z](?=[A-Z0-9])|[A-Z](?=[A-Z][a-z]))", "$1 ")
JoshL
fonte
2
Excelente adição! Suspeito que não poucas pessoas ficarão surpresas com a maneira como as respostas aceitas tratam os números em strings. :)
Jordan Gray
Eu sei que já se passaram quase 8 anos desde que você postou isso, mas funcionou perfeitamente para mim também. :) Os números me surpreenderam no início.
Michael Armes
A única resposta que passa em meus 2 testes atípicos: "Take5" -> "Take 5", "PublisherID" -> "Publisher ID". Eu quero
votar a favor
18

Apenas para variar ... Aqui está um método de extensão que não usa 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
fonte
Para evitar o uso de Trim (), antes de foreach eu coloquei: int counter = -1. dentro, adicione contador ++. altere a seleção para: if (char.IsUpper (c) && counter> 0)
the Box Developer
Isso insere um espaço antes do primeiro caractere.
Zar Shardan
Tomei a liberdade de corrigir o problema apontado por @ZarShardan. Sinta-se à vontade para reverter ou editar para sua própria correção se não gostar da mudança.
jpmc26
Isso pode ser aprimorado para lidar com abreviações, por exemplo, adicionando um espaço antes da última maiúscula em uma série de letras maiúsculas, por exemplo, BOEForecast => BOE Forecast
Nepaluz
11

Deixando de lado o excelente comentário de Grant Wagner:

Dim s As String = RegularExpressions.Regex.Replace("ThisIsMyCapsDelimitedString", "([A-Z])", " $1")
Pseudo Masoquista
fonte
Bom ponto ... Sinta-se à vontade para inserir o .substring (), .trimstart (), .trim (), .remove (), etc. de sua escolha. :)
Pseudo Masoquista
9

Eu precisava de uma solução que suportasse siglas e números. Esta solução baseada em Regex trata os seguintes padrões como "palavras" individuais:

  • Uma letra maiúscula seguida por letras minúsculas
  • Uma sequência de números consecutivos
  • Letras maiúsculas consecutivas (interpretadas como acrônimos) - uma nova palavra pode começar usando a última maiúscula, por exemplo, HTMLGuide => "Guia HTML", "TheATeam" => "The A Team"

Você poderia fazer isso como uma linha:

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

Uma abordagem mais legível pode ser melhor:

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

Aqui está um extrato dos testes (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
fonte
1
+ 1 para explicar a regex e torná-la legível. E aprendi algo novo. Há um modo de espaçamento livre e comentários no .NET Regex. Obrigado!
Felix Keil
4

Para obter mais variedade, usando objetos C # simples e antigos, o seguinte produz a mesma saída que a excelente expressão regular 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
fonte
2
Uau, isso é feio. Agora eu me lembro porque amo tanto regex! 1 para o esforço, no entanto. ;)
Mark Brackett
3

Abaixo está um protótipo que converte o seguinte em caixa do título:

  • snake_case
  • camelCase
  • PascalCase
  • caso de sentença
  • Caixa do título (manter a formatação atual)

Obviamente, você só precisa do método "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);
    }
}

A saída do console seria a seguinte:

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

Postagem do blog referenciada

Brantley Blanchard
fonte
2
string s = "ThisIsMyCapsDelimitedString";
string t = Regex.Replace(s, "([A-Z])", " $1").Substring(1);
Ferruccio
fonte
Eu sabia que haveria uma maneira fácil de RegEx ... Tenho que começar a usá-la mais.
Max Schmeling
1
Não é um guru de regex, mas o que acontece com "HeresAWTFString"?
Nick
1
Você ganha "Heres AWTF String", mas é exatamente o que Matias Nino pediu na pergunta.
Max Schmeling
Sim, ele precisa adicionar que "várias capitais adjacentes são deixadas sozinhas". O que é obviamente necessário em muitos casos, por exemplo, "PublisherID" aqui vai para "Publisher I D" que é terrível
PandaWood
2

Regex é cerca de 10-12 vezes mais lento do que um loop simples:

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

Solução regex ingênua. Não suporta O'Conner e adiciona um espaço no início da string também.

s = "ThisIsMyCapsDelimitedString"
split = Regex.Replace(s, "[A-Z0-9]", " $&");
Geoff
fonte
Eu modifiquei você, mas as pessoas geralmente acham melhor uma smackdown se não começar com "ingênuo".
MusiGenesis
Eu não acho que foi um confronto direto. Nesse contexto, ingênuo geralmente significa óbvio ou simples (ou seja, não necessariamente a melhor solução). Não há intenção de insulto.
Ferruccio
0

Provavelmente há uma solução mais elegante, mas é isso que eu vim com o topo da minha cabeça:

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

Tente usar

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

O resultado será adequado para mistura de alfabeto com números

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

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

Implementando o código psudo 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
fonte
0

Implementação rápida e processual:

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

Testes:

  [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 da equipe NDepend
fonte
0

Uma solução simples, que deve ser de ordem (s) de magnitude mais rápida do que uma solução regex (com base nos testes que executei nas principais soluções neste segmento), especialmente conforme o tamanho da string de entrada aumenta:

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
fonte