Numeric Ordering para comparação de Strings no C#
O numeric ordering é a nova opção de comparação de strings do .NET 10 que ordena sequências de dígitos pelo valor numérico, e não caractere por caractere. Com ela, "Windows 8" finalmente aparece antes de "Windows 10" em uma lista ordenada no C#.
Esse recurso atende a um pedido antigo da comunidade, registrado no repositório do runtime do .NET como uma das sugestões mais votadas sobre strings. Ele resolve um problema comum: ordenar textos que contêm números de forma intuitiva e alinhada com a lógica humana.
Neste artigo, você vai entender por que a ordenação tradicional falha, como ativar a nova opção e quais limitações ela tem.
O que é numeric ordering
O numeric ordering é representado pelo valor CompareOptions.NumericOrdering, um novo membro do enum CompareOptions, no namespace System.Globalization. O valor numérico do flag é 32, e ele chegou ao .NET 10 já no Preview 1.
Segundo a documentação oficial, a opção ordena sequências de dígitos da categoria Unicode "Nd" com base no seu valor numérico. Assim, "2" vem antes de "10", porque 2 é menor que 10.
A mesma regra vale para igualdade. Como "2" e "02" representam o mesmo número, o comparador com numeric ordering considera as duas strings equivalentes.
Por ser um enum do tipo flags, a nova opção pode ser combinada com outras, como IgnoreCase. A exceção são Ordinal e OrdinalIgnoreCase, que a documentação define como valores que devem ser usados sozinhos.
Por que a ordenação lexicográfica falha
Na maioria dos cenários, o C# compara strings de forma lexicográfica. Ou seja, a comparação considera a ordem dos caracteres, e não o valor numérico que eles representam.
Isso pode levar a resultados inesperados ao ordenar nomes que contêm números. Considere o exemplo abaixo:
var items = new[] { "Windows 10", "Windows 8", "Windows 11" };
// Ordenação padrão, caractere por caractere
Array.Sort(items);
foreach (var item in items)
{
Console.WriteLine(item);
}
A saída mostra o problema com clareza:
Windows 10
Windows 11
Windows 8
Aqui, "Windows 10" aparece antes de "Windows 8" porque a comparação avança caractere por caractere. Depois do prefixo igual, o caractere "1" de "10" vem antes do caractere "8".
Para o usuário, porém, essa ordem não faz sentido. Ele espera ver "Windows 8" antes de "Windows 10", exatamente como faria ao organizar a lista à mão.
Onde o problema aparece no dia a dia
O mesmo comportamento atinge listas de arquivos como file1.txt, file10.txt e file2.txt. Também afeta nomes de capítulos, códigos de produtos, números de notas fiscais e versões de software exibidas em uma tela.
Outro caso frequente envolve números armazenados como texto, como "02" e "2". Na comparação tradicional, essas strings são diferentes, embora representem o mesmo valor.
Lexicográfica, ordinal e numeric ordering lado a lado
A documentação do .NET descreve três formas clássicas de ordenar texto: word sort, string sort e ordinal sort. As duas primeiras são linguísticas e seguem as regras de uma cultura, enquanto a ordinal compara o valor binário de cada caractere.
Desde o .NET 5, o runtime usa a biblioteca ICU em todas as plataformas suportadas. Com isso, a opção padrão None passou a equivaler ao StringSort.
Nenhuma dessas formas, porém, interpreta números. A tabela abaixo resume o resultado da mesma lista em cada modo de comparação.
| Modo de comparação | Resultado para "Windows 8", "Windows 10" e "Windows 11" |
|---|---|
CompareOptions.None |
Windows 10, Windows 11, Windows 8 |
CompareOptions.Ordinal |
Windows 10, Windows 11, Windows 8 |
CompareOptions.NumericOrdering |
Windows 8, Windows 10, Windows 11 |
Na prática, apenas o numeric ordering entrega a ordem natural. As outras opções continuam úteis, mas para objetivos diferentes, como comparações de segurança ou de identificadores técnicos.
Como resolver antes do numeric ordering
Antes do .NET 10, não existia uma opção nativa e multiplataforma para esse tipo de comparação. Por isso, cada equipe criava a própria solução.
Quando a string inteira é um número, a saída mais simples é converter o valor antes de ordenar. Contudo, essa abordagem quebra assim que aparece um texto misturado aos dígitos.
var codes = new[] { "10", "8", "11" };
// Funciona apenas quando toda a string é numérica
var ordered = codes.OrderBy(code => int.Parse(code));
Para textos mistos, o caminho comum era escrever um IComparer<string> próprio. Esse comparador separava trechos numéricos e textuais e comparava cada parte de um jeito diferente.
Comparadores caseiros também costumavam falhar em detalhes, como números longos demais para caber em um int ou dígitos de outros sistemas de escrita. Além disso, cada implementação tinha regras próprias para zeros à esquerda, o que gerava resultados diferentes entre projetos da mesma empresa.
No Windows, outra alternativa era chamar a função nativa StrCmpLogicalW via P/Invoke, a mesma lógica que o Explorador de Arquivos usa. No entanto, essa solução prendia o código ao Windows e não funcionava no Linux nem no macOS.
Todas essas abordagens exigiam código extra, testes e manutenção. É exatamente essa lacuna que o numeric ordering fecha.
Como usar o numeric ordering no C#
Para ativar o numeric ordering, basta criar um comparador com a nova opção. O método StringComparer.Create recebe uma cultura e as opções de comparação desejadas.
using System.Globalization;
StringComparer numericStringComparer =
StringComparer.Create(CultureInfo.CurrentCulture, CompareOptions.NumericOrdering);
Console.WriteLine(numericStringComparer.Equals("02", "2"));
// Output: True
foreach (string os in new[] { "Windows 8", "Windows 10", "Windows 11" }.Order(numericStringComparer))
{
Console.WriteLine(os);
}
// Output:
// Windows 8
// Windows 10
// Windows 11
HashSet<string> set = new HashSet<string>(numericStringComparer) { "007" };
Console.WriteLine(set.Contains("7"));
// Output: True
O exemplo mostra três usos do mesmo comparador. Primeiro, a igualdade entre "02" e "2" retorna true.
Depois, o método Order do LINQ devolve a lista na sequência natural. Por fim, o HashSet<string> encontra "7" mesmo que o item armazenado seja "007".
Esse último ponto merece atenção. Como o StringComparer também implementa IEqualityComparer<string>, o hash das duas strings coincide e a coleção as trata como iguais.
Outras formas de aplicar o numeric ordering
O StringComparer não é o único ponto de entrada. Qualquer API que aceite um CompareOptions também aceita o numeric ordering.
O método string.Compare tem uma sobrecarga que recebe cultura e opções. Ele retorna um número negativo, zero ou positivo, como qualquer comparação.
using System.Globalization;
int result = string.Compare(
"Chapter 2",
"Chapter 10",
CultureInfo.InvariantCulture,
CompareOptions.NumericOrdering);
Console.WriteLine(result < 0);
// Output: True
Também é possível usar o CompareInfo da cultura diretamente. Essa forma é útil quando você já trabalha com um objeto CultureInfo específico.
using System.Globalization;
CompareInfo compareInfo = CultureInfo.InvariantCulture.CompareInfo;
var files = new List<string> { "file10.txt", "file2.txt", "file1.txt" };
// Ordena a lista no próprio lugar com a nova opção
files.Sort((x, y) => compareInfo.Compare(x, y, CompareOptions.NumericOrdering));
// Output: file1.txt, file2.txt, file10.txt
Console.WriteLine(string.Join(", ", files));
Combinando opções
Como CompareOptions é um enum de flags, você pode somar o numeric ordering a outras opções com o operador |. Um caso comum é ignorar maiúsculas e minúsculas ao mesmo tempo.
using System.Globalization;
StringComparer comparer = StringComparer.Create(
CultureInfo.InvariantCulture,
CompareOptions.NumericOrdering | CompareOptions.IgnoreCase);
var products = new[] { "item 10", "ITEM 9", "Item 1" };
// Output: Item 1, ITEM 9, item 10
Console.WriteLine(string.Join(", ", products.Order(comparer)));
Nesse cenário, o comparador ignora a diferença de caixa e, ao mesmo tempo, ordena os números pelo valor. O resultado é a ordem que um usuário esperaria em uma tela de catálogo.
Da mesma forma, IgnoreNonSpace pode entrar na combinação quando acentos não devem influenciar a ordem.
Coleções ordenadas e dicionários
Coleções como SortedSet<string>, SortedList<string, TValue> e Dictionary<string, TValue> aceitam um comparador no construtor. Dessa forma, o comportamento do numeric ordering fica embutido na própria estrutura.
Por exemplo, um SortedSet<string> criado com esse comparador mantém os itens sempre na ordem natural, sem chamadas extras de ordenação a cada inserção.
Antes de usar a opção em chaves de dicionário, porém, avalie o efeito da igualdade numérica. Se "007" e "7" forem identificadores distintos no seu domínio, esse comparador vai tratá-los como a mesma chave.
Numeric ordering com objetos e LINQ
Em aplicações reais, raramente você ordena uma lista de strings soltas. O mais comum é ordenar objetos por uma propriedade de texto, como o código de um produto.
Os métodos OrderBy e ThenBy do LINQ aceitam um IComparer<TKey> como segundo argumento. Assim, basta passar o comparador criado com numeric ordering.
using System.Globalization;
public record Product(string Code, string Name);
public static class ProductCatalog
{
private static readonly StringComparer NaturalComparer =
StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering);
public static IEnumerable<Product> SortByCode(IEnumerable<Product> products) =>
products
.OrderBy(product => product.Code, NaturalComparer)
.ThenBy(product => product.Name, NaturalComparer);
}
Nesse exemplo, um produto com código SKU-9 aparece antes de outro com código SKU-10. Além disso, o ThenBy mantém o mesmo critério natural no desempate pelo nome.
Vale lembrar que o OrderBy realiza uma ordenação estável. Em outras palavras, itens que o comparador considera iguais preservam a ordem original da sequência.
Como o numeric ordering trata os dígitos
A documentação descreve uma regra simples: apenas dígitos da categoria Unicode "Nd" formam uma sequência numérica. Isso inclui os dígitos de 0 a 9 e também dígitos decimais de outros sistemas de escrita.
Caracteres que não são dígitos encerram a sequência. A lista oficial cita explicitamente o ponto decimal, o sinal de menos e o sinal de mais.
Na prática, isso tem duas consequências importantes. Primeiro, "1.9" e "1.10" viram duas sequências separadas pelo ponto, então "1.9" vem antes de "1.10", como acontece com números de versão.
Em segundo lugar, o numeric ordering não interpreta números decimais nem negativos. Como o sinal de menos não faz parte da sequência, "-2" fica antes de "-10", que é o oposto da ordem matemática.
Portanto, se os dados representam valores com casas decimais ou sinal, converta para decimal ou double antes de ordenar. A nova opção foi pensada para textos que contêm números inteiros positivos.
Limitações do numeric ordering
É importante observar que o CompareOptions.NumericOrdering não se aplica a operações baseadas em índices. A lista oficial inclui os seguintes métodos:
IndexOfLastIndexOfStartsWithEndsWithIsPrefixIsSuffix
Isso significa que, apesar de permitir comparações e ordenações mais intuitivas, buscas dentro de strings continuam seguindo a lógica tradicional. Em outras palavras, a opção vale para comparar e ordenar, não para localizar trechos.
Outro ponto é a dependência da cultura. Como a comparação é linguística, o resultado usa as regras da cultura informada e a biblioteca de globalização da plataforma.
Desde o .NET 5, essa biblioteca é o ICU em todas as plataformas suportadas. Por isso, vale testar a ordenação no ambiente de produção, em especial quando a aplicação roda em containers com configurações de globalização reduzidas.
Boas práticas ao usar numeric ordering
A recomendação geral da Microsoft continua valendo. Comparações exibidas ao usuário funcionam melhor com opções linguísticas, enquanto comparações de segurança devem usar Ordinal ou OrdinalIgnoreCase.
O numeric ordering se encaixa no primeiro grupo. Ele é ideal para listas, menus, relatórios e qualquer tela em que a ordem visual importa.
Para dados persistidos ou ordenações que precisam ser estáveis entre máquinas, prefira CultureInfo.InvariantCulture. Assim, a ordem não muda quando o servidor roda com outra cultura.
Também vale criar o comparador uma única vez e reutilizá-lo. Por exemplo, um campo static readonly evita recriar o objeto a cada ordenação.
using System.Globalization;
public static class Comparers
{
// Comparador reutilizável para ordenações exibidas ao usuário
public static readonly StringComparer Natural =
StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering);
}
Se você quer aprofundar o tema de comparação e cultura, o artigo sobre globalization e localization no .NET complementa bem este conteúdo. Para uma visão geral da API de texto, o guia de manipulação de strings em C# também ajuda.
Quando usar e quando evitar o numeric ordering
O numeric ordering brilha em qualquer lista que uma pessoa vai ler. Listagens de arquivos, nomes de capítulos, rótulos de versões, salas, lotes e códigos de catálogo são bons exemplos.
Também faz sentido em filtros e menus de interfaces administrativas. Nesses casos, a ordem natural reduz o esforço de quem procura um item específico.
Por outro lado, evite a opção em comparações de segurança, como tokens, senhas, nomes de permissões e caminhos que o sistema valida. Para esses cenários, a recomendação oficial continua sendo Ordinal ou OrdinalIgnoreCase.
Evite também o recurso quando "007" e "7" precisam ser valores diferentes, como em códigos fiscais ou identificadores externos. Afinal, a igualdade numérica faria o sistema confundir os dois registros.
Por fim, não use o numeric ordering para valores monetários, medidas com casas decimais ou números com sinal. Nesses casos, armazene e ordene os dados em tipos numéricos, que expressam a intenção com mais clareza.
Como testar o numeric ordering
Como a ordenação depende da cultura e da biblioteca de globalização, vale proteger o comportamento com testes automatizados. Um teste simples já garante que uma atualização de ambiente não altere a ordem exibida ao usuário.
using System.Globalization;
using Xunit;
public class NaturalSortTests
{
private static readonly StringComparer Comparer =
StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering);
[Fact]
public void Order_ShouldPlaceSmallerNumbersFirst()
{
var items = new[] { "Windows 10", "Windows 8", "Windows 11" };
var ordered = items.Order(Comparer).ToArray();
Assert.Equal(new[] { "Windows 8", "Windows 10", "Windows 11" }, ordered);
}
[Fact]
public void Equals_ShouldIgnoreLeadingZeros()
{
Assert.True(Comparer.Equals("02", "2"));
}
}
O primeiro teste valida a ordem natural da lista. Já o segundo confirma que zeros à esquerda não alteram a igualdade, exatamente como mostra a documentação do .NET 10.
Conclusão
O numeric ordering do .NET 10 resolve um problema recorrente na comparação de strings que contêm números. Com uma única opção, o C# passa a ordenar "Windows 8" antes de "Windows 10" sem comparadores caseiros.
O recurso melhora a ordenação e a igualdade em diversos cenários, tornando o código mais previsível e alinhado às expectativas dos usuários. Ainda assim, ele não substitui a conversão numérica quando os dados têm sinal ou casas decimais.
Se você lida com dados que misturam texto e números, o numeric ordering é um avanço real para suas aplicações. Comece pelo StringComparer.Create e consulte a documentação do CompareOptions para conhecer os detalhes.
FAQ
A partir de qual versão o numeric ordering está disponível?
A opção CompareOptions.NumericOrdering faz parte do .NET 10. Ela apareceu pela primeira vez no .NET 10 Preview 1.
O numeric ordering funciona com StringComparison?
Não diretamente. O enum StringComparison não ganhou um membro novo, então use CompareOptions com StringComparer.Create, string.Compare ou CompareInfo.Compare.
Posso combinar NumericOrdering com Ordinal?
Não. A documentação define Ordinal e OrdinalIgnoreCase como opções que devem ser usadas sozinhas.
O numeric ordering ordena números negativos e decimais corretamente?
Não. O ponto decimal e os sinais de mais e menos encerram a sequência de dígitos, então converta esses valores para tipos numéricos antes de ordenar.
Por que "02" e "2" são considerados iguais?
Porque a opção compara as sequências de dígitos pelo valor numérico. Como ambas valem 2, o comparador considera as strings equivalentes.