Como: Usar o LINQ para consultar cadeias de caracteres

As cadeias de caracteres são armazenadas como uma sequência de caracteres. Como uma sequência de caracteres, eles podem ser consultados usando LINQ. Neste artigo, há várias consultas de exemplo que consultam cadeias de caracteres para caracteres ou palavras diferentes, cadeias de caracteres de filtro ou misturam consultas com expressões regulares.

Como consultar caracteres em uma cadeia de caracteres

O exemplo a seguir consulta uma cadeia de caracteres para determinar o número de dígitos numéricos que ela contém.

string aString = "ABCDE99F-J74-12-89A";

// Select only those characters that are numbers
var stringQuery = from ch in aString
                  where Char.IsDigit(ch)
                  select ch;

// Execute the query
foreach (char c in stringQuery)
    Console.Write(c + " ");

// Call the Count method on the existing query.
int count = stringQuery.Count();
Console.WriteLine($"Count = {count}");

// Select all characters before the first '-'
var stringQuery2 = aString.TakeWhile(c => c != '-');

// Execute the second query
foreach (char c in stringQuery2)
    Console.Write(c);
/* Output:
  Output: 9 9 7 4 1 2 8 9
  Count = 8
  ABCDE99F
*/

A consulta anterior mostra como você pode tratar uma cadeia de caracteres como uma sequência de caracteres.

Como contar ocorrências de uma palavra em uma cadeia de caracteres

O exemplo a seguir mostra como usar uma consulta LINQ para contar as ocorrências de uma palavra especificada em uma cadeia de caracteres. Para executar a contagem, primeiro o Split método é chamado para criar uma matriz de palavras. Há um custo de desempenho para o Split método. Se a única operação na cadeia de caracteres for contar as palavras, considere usar os Matches métodos ou IndexOf em vez disso.

string text = """
    Historically, the world of data and the world of objects 
    have not been well integrated. Programmers work in C# or Visual Basic 
    and also in SQL or XQuery. On the one side are concepts such as classes, 
    objects, fields, inheritance, and .NET APIs. On the other side 
    are tables, columns, rows, nodes, and separate languages for dealing with 
    them. Data types often require translation between the two worlds; there are 
    different standard functions. Because the object world has no notion of query, a 
    query can only be represented as a string without compile-time type checking or 
    IntelliSense support in the IDE. Transferring data from SQL tables or XML trees to 
    objects in memory is often tedious and error-prone. 
    """;

string searchTerm = "data";

//Convert the string into an array of words
char[] separators = ['.', '?', '!', ' ', ';', ':', ','];
string[] source = text.Split(separators, StringSplitOptions.RemoveEmptyEntries);

// Create the query.  Use the InvariantCultureIgnoreCase comparison to match "data" and "Data"
var matchQuery = from word in source
                 where word.Equals(searchTerm, StringComparison.InvariantCultureIgnoreCase)
                 select word;

// Count the matches, which executes the query.
int wordCount = matchQuery.Count();
Console.WriteLine($"""{wordCount} occurrences(s) of the search term "{searchTerm}" were found.""");
/* Output:
   3 occurrences(s) of the search term "data" were found.
*/

A consulta anterior mostra como você pode exibir cadeias de caracteres como uma sequência de palavras, depois de dividir uma cadeia de caracteres em uma sequência de palavras.

Como classificar ou filtrar dados de texto por qualquer palavra ou campo

O exemplo a seguir mostra como classificar linhas de texto estruturado, como valores separados por vírgula, por qualquer campo na linha. O campo pode ser especificado dinamicamente em tempo de execução. Suponha que os campos em scores.csv representam o número de identificação de um aluno, seguido por uma série de quatro pontuações de teste:

111, 97, 92, 81, 60
112, 75, 84, 91, 39
113, 88, 94, 65, 91
114, 97, 89, 85, 82
115, 35, 72, 91, 70
116, 99, 86, 90, 94
117, 93, 92, 80, 87
118, 92, 90, 83, 78
119, 68, 79, 88, 92
120, 99, 82, 81, 79
121, 96, 85, 91, 60
122, 94, 92, 91, 91

A consulta a seguir classifica as linhas com base na pontuação do primeiro exame, armazenadas na segunda coluna:

// Create an IEnumerable data source
string[] scores = File.ReadAllLines("scores.csv");

// Change this to any value from 0 to 4.
int sortField = 1;

Console.WriteLine($"Sorted highest to lowest by field [{sortField}]:");

// Split the string and sort on field[num]
var scoreQuery = from line in scores
                 let fields = line.Split(',')
                 orderby fields[sortField] descending
                 select line;

foreach (string str in scoreQuery)
{
    Console.WriteLine(str);
}
/* Output (if sortField == 1):
   Sorted highest to lowest by field [1]:
    116, 99, 86, 90, 94
    120, 99, 82, 81, 79
    111, 97, 92, 81, 60
    114, 97, 89, 85, 82
    121, 96, 85, 91, 60
    122, 94, 92, 91, 91
    117, 93, 92, 80, 87
    118, 92, 90, 83, 78
    113, 88, 94, 65, 91
    112, 75, 84, 91, 39
    119, 68, 79, 88, 92
    115, 35, 72, 91, 70
 */

A consulta anterior mostra como você pode manipular cadeias de caracteres dividindo-as em campos e consultando os campos individuais.

Como consultar frases que palavras específicas

O exemplo a seguir mostra como localizar frases em um arquivo de texto que contêm correspondências para cada um de um conjunto especificado de palavras. Embora a matriz de termos de pesquisa seja codificada, ela também pode ser preenchida dinamicamente em tempo de execução. A consulta retorna as frases que contêm as palavras "Historicamente", "dados" e "integrado".

string text = """
Historically, the world of data and the world of objects 
have not been well integrated. Programmers work in C# or Visual Basic 
and also in SQL or XQuery. On the one side are concepts such as classes, 
objects, fields, inheritance, and .NET APIs. On the other side 
are tables, columns, rows, nodes, and separate languages for dealing with 
them. Data types often require translation between the two worlds; there are 
different standard functions. Because the object world has no notion of query, a 
query can only be represented as a string without compile-time type checking or 
IntelliSense support in the IDE. Transferring data from SQL tables or XML trees to 
objects in memory is often tedious and error-prone.
""";

// Split the text block into an array of sentences.
string[] sentences = text.Split(['.', '?', '!']);

// Define the search terms. This list could also be dynamically populated at run time.
string[] wordsToMatch = [ "Historically", "data", "integrated" ];

// Find sentences that contain all the terms in the wordsToMatch array.
// Note that the number of terms to match is not specified at compile time.
char[] separators = ['.', '?', '!', ' ', ';', ':', ','];
var sentenceQuery = from sentence in sentences
                    let w = sentence.Split(separators,StringSplitOptions.RemoveEmptyEntries)
                    where w.Distinct().Intersect(wordsToMatch).Count() == wordsToMatch.Count()
                    select sentence;

foreach (string str in sentenceQuery)
{
    Console.WriteLine(str);
}
/* Output:
Historically, the world of data and the world of objects have not been well integrated
*/

A consulta primeiro divide o texto em frases e, em seguida, divide cada frase em uma matriz de cadeias de caracteres que contêm cada palavra. Para cada uma dessas matrizes, o Distinct método remove todas as palavras duplicadas e, em seguida, a consulta executa uma Intersect operação na matriz de palavras e na wordsToMatch matriz. Se a contagem da interseção for a mesma que a contagem da wordsToMatch matriz, todas as palavras foram encontradas nas palavras e a frase original será retornada.

A chamada para Split usa sinais de pontuação como separadores para removê-los da cadeia de caracteres. Se você não removesse a pontuação, por exemplo, poderia ter uma cadeia de caracteres "Historicamente", que não corresponderia wordsToMatch a "Historicamente" na matriz. Poderá ter de utilizar separadores adicionais, dependendo dos tipos de pontuação encontrados no texto de partida.

Como combinar consultas LINQ com expressões regulares

O exemplo a seguir mostra como usar a Regex classe para criar uma expressão regular para correspondência mais complexa em cadeias de texto. A consulta LINQ facilita a filtragem exata dos arquivos que você deseja pesquisar com a expressão regular e a forma dos resultados.

string startFolder = """C:\Program Files\dotnet\sdk""";
// Or
// string startFolder = "/usr/local/share/dotnet/sdk";

// Take a snapshot of the file system.
var fileList = from file in Directory.GetFiles(startFolder, "*.*", SearchOption.AllDirectories)
                let fileInfo = new FileInfo(file)
                select fileInfo;

// Create the regular expression to find all things "Visual".
System.Text.RegularExpressions.Regex searchTerm =
    new System.Text.RegularExpressions.Regex(@"microsoft.net.(sdk|workload)");

// Search the contents of each .htm file.
// Remove the where clause to find even more matchedValues!
// This query produces a list of files where a match
// was found, and a list of the matchedValues in that file.
// Note: Explicit typing of "Match" in select clause.
// This is required because MatchCollection is not a
// generic IEnumerable collection.
var queryMatchingFiles =
    from file in fileList
    where file.Extension == ".txt"
    let fileText = File.ReadAllText(file.FullName)
    let matches = searchTerm.Matches(fileText)
    where matches.Count > 0
    select new
    {
        name = file.FullName,
        matchedValues = from System.Text.RegularExpressions.Match match in matches
                        select match.Value
    };

// Execute the query.
Console.WriteLine($"""The term "{searchTerm}" was found in:""");

foreach (var v in queryMatchingFiles)
{
    // Trim the path a bit, then write
    // the file name in which a match was found.
    string s = v.name.Substring(startFolder.Length - 1);
    Console.WriteLine(s);

    // For this file, write out all the matching strings
    foreach (var v2 in v.matchedValues)
    {
        Console.WriteLine($"  {v2}");
    }
}

Você também pode consultar o MatchCollection objeto retornado por uma RegEx pesquisa. Apenas o valor de cada partida é produzido nos resultados. No entanto, também é possível usar o LINQ para executar todos os tipos de filtragem, classificação e agrupamento nessa coleção. Como MatchCollection é uma coleção não genérica IEnumerable , você precisa declarar explicitamente o tipo da variável de intervalo na consulta.