.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!


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

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

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

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

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

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")]

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

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


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

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

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]", " $&");

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

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  

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


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

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();
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.