
Cuando hablamos de comparar números enteros, está claro que 1 es menor que 2, y éste menor que 10:
1 < 2 < 10
Sin embargo, si estos valores están en una cadena de texto, la comparación se hace carácter a carácter, y en ese caso "10" es menor que "2":
"1" < "10" < "2"
Este criterio de ordenación puede ser molesto en muchos escenarios, sobre todo cuando queremos ordenar o comparar valores numéricos se encuentran en el interior de cadenas de texto donde hay otros contenidos. En todos estos casos, la comparación lexicográfica tradicional no nos dará los resultados esperados:
- Una lista de nombres de archivo como "file1.txt", "file2.txt"... así hasta "file10.txt". Al ordenarlos, "file10.txt" aparecerá antes que "file2.txt".
- Versiones de un software, como "1.2" y "1.10". Si las comparamos, la versión "1.10" será anterior a la "1.2", cuando en realidad es al revés.
- De la misma forma, una lista de direcciones IP, como "10.10.2.10", "10.10.10.10", si queremos mostrarlas de forma ordenada, obtendremos "10.10.10.10" y "10.10.2.10", que no es su orden numérico real.
- O simplemente, si comparamos las horas "01:10" con "1:10", la primera será anterior a la segunda, cuando en realidad se trata de la misma hora.
Hasta la versión 10 de .NET no existía una forma sencilla de resolver estos casos, por lo que teníamos que implementar soluciones propias, bastante farragosas en algunos casos, o bien utilizar bibliotecas de terceros. Sin embargo, en .NET 10 se ha añadido una forma nativa para conseguirlo, que vemos a continuación.
Comparación numérica de cadenas en .NET 10
.NET 10 ha ampliado la enumeración CompareOptions
, utilizada a la hora de crear objetos StringComparer
, añadiéndole el miembro NumericOrdering
, gracias al cual podemos crear objetos comparadores que utilicen la ordenación natural, o comparación numérica de cadenas.
La forma de utilizarlo es muy sencilla:
var comparer = StringComparer.Create(
CultureInfo.CurrentCulture,
CompareOptions.NumericOrdering
);
Este objeto podemos utilizarlo luego en cualquier método que acepte un comparador de cadenas, como Equals()
, Compare()
, Sort()
, OrderBy()
, etc.
Veamos algunos ejemplos. A continuación, podemos observar cómo usando la comparación por caracteres tradicional, en este caso StringComparer.CurrentCulture
, los valores se ordenan alfabéticamente, mientras que con la comparación numérica, CompareOptions.NumericOrdering
, se ordenan correctamente:
var standardComparer = StringComparer.CurrentCulture;
var numericComparer = StringComparer.Create(CultureInfo.CurrentCulture, CompareOptions.NumericOrdering);
// Ordenamos nombres de archivo:
var values = new[] { "file-1.txt", "file-10.txt", "file-2.txt" };
Console.WriteLine(string.Join(",", values.Order(standardComparer))); // 1,10,2 (desordenado)
Console.WriteLine(string.Join(",", values.Order(numericComparer))); // 1,2,10 (ordenado)
// Ordenamos direcciones IP:
values = new[] { "10.10.2.10", "10.10.10.10" };
Console.WriteLine(string.Join(",", values.Order(standardComparer))); // 10.10.10.10,10.10.2.10 (mal)
Console.WriteLine(string.Join(",", values.Order(numericComparer))); // 10.10.2.10,10.10.10.10 (bien)
Y como podemos ver en el siguiente fragmento de código, podemos usar el mismo StringComparer
para realizar comparaciones entre valores de cadenas de texto, como versiones de software u horas:
var standardComparer = StringComparer.CurrentCulture;
var numericComparer = StringComparer.Create(CultureInfo.CurrentCulture, CompareOptions.NumericOrdering);
// Comparamos dos versiones de software:
Console.WriteLine(standardComparer.Compare("1.2", "1.10")); // 1: "1.2">"1.10" (❌)
Console.WriteLine(numericComparer.Compare("1.2", "1.10")); // -1: "1.2"<"1.10" (✅)
// Comparamos horas:
Console.WriteLine(standardComparer.Equals("01:10", "1:10")); // False: 01:10 no es igual a 1:10 (❌)
Console.WriteLine(numericComparer.Equals("01:10", "1:10")); // True: 01:10 es igual a 1:10 (✅)
Aparte, dado que StringComparer
es un mecanismo estándar en .NET y se utiliza en bastantes sitios, podemos usarlo por ejemplo para crear diccionarios o hash sets con claves de tipo string
que utilicen estas fórmulas de comparación. Por ejemplo:
// Uso en hash sets:
var numericComparerSet = new HashSet<string>(numericComparer) { "007", "10.1" };
Console.WriteLine(numericComparerSet.Contains("7")); // true
Console.WriteLine(numericComparerSet.Contains("10.01")); // true
// Uso en diccionarios:
var numericComparerDict = new Dictionary<string, string>(numericComparer));
numericComparerDict["1"] = "one";
Console.WriteLine(numericComparerDict["1"]); // one
Console.WriteLine(numericComparerDict["01"]); // one
Console.WriteLine(numericComparerDict["000000001"]); // one
Console.WriteLine(numericComparerDict.ContainsKey("001")); // true
¿Cuál la lógica de comparación?
Los detalles de la lógica pueden leerse echando un vistazo a la propuesta de Peter-Juhasz en GitHub, donde se discutió la implementación, así como en la pull request que derivó de esta.
Las líneas principales son:
- Las cadenas de texto son divididas en secuencias. Por ejemplo, "file10.txt" se dividiría en las cuatro secuencias "file", "10", "." y "txt".
- Las secuencias pueden ser numéricas o no numéricas. En las primeras, todos los caracteres deben ser dígitos 0-9, sólo se soportan valores positivos sin separadores.
- Al comparar elementos, se compara cada secuencia independientemente, en el orden en que aparecen en la cadena.
- Las secuencias no numéricas se evaluarán según la cultura proporcionada al crear el
StringComparer
. - Las secuencias numéricas se compararán por su valor numérico, no por su valor alfabético.
- En secuencias numéricas, no se tendrán en cuenta los ceros iniciales, por lo que "01" se considera igual que "1".
- Cuando se compara una secuencia numérica con una no numérica, la secuencia numérica siempre será menor.
- La implementación utiliza la infraestructura de globalización de las plataformas de ejecución, por lo que podrían existir diferencias entre ellas. De hecho, a día de hoy hay plataformas como iOS o Mac Catalyst donde aún no está soportado.
¡Espero que os sea útil!
Publicado en: www.variablenotfound.com.
Aún no hay comentarios, ¡sé el primero!
Enviar un nuevo comentario