#retosMSDN: Solución al Reto 7 – Procesando Json en C#
Aquí tienes la solución (bueno, las soluciones) que proponemos para el séptimo de nuestros #retosMSDN: Reto 7 – Procesando Json en C#. ¡Muchas gracias a todos los que habéis participado en el reto y en especial a @rf1souto y a @angel_g_santos por las ideas que lo han hecho posible!
Posibles Soluciones
Para este reto hay muchas soluciones posibles. En este caso buscábamos la más rápida y/o compacta (con menos sentencias de código, pero que fuera rápida también).
Nuestra primera aproximación fue intentar hacer una solución compacta, sencilla, pero lo suficientemente rápida. Para ello usamos Json.NET, una de las librerías más conocidas para trabajar con Json en .NET, y que soporta Linq. Lo hicimos en un par de sentencias de código gracias también a la clase Tuple que nos permite devolver más de un valor en un método sin tener que crearnos una clase propia:
public static Tuple<string, string> SplitShowsByGenre(string json, string genre)
{
var split =
JArray.Parse(json)
.GroupBy(s => s["genres"].Values<string>().Contains(genre))
.OrderBy(g => g.Key)
.Select(g => (new JArray(g)).ToString(Formatting.None))
.ToList();
return new Tuple<string, string>(split[1], split[0]);
}
Como referencia, en mi máquina este método tarda unos 259,000 ticks en ejecutarse. Ahora, como usar Linq no siempre es la solución más rápida, implementamos el método usando también Json.NET pero con foreach:
public static Tuple<string, string> SplitShowsByGenre(string json, string genre)
{
JArray selectedShowsJson = new JArray();
JArray otherShowsJson = new JArray();
foreach (JObject showJson in JArray.Parse(json))
{
bool genreFound = false;
foreach (JValue genreJson in showJson.Value<JArray>("genres"))
{
if (genreJson.ToString() == genre)
{
selectedShowsJson.Add(showJson);
genreFound = true;
break;
}
}
if (!genreFound)
{
otherShowsJson.Add(showJson);
}
}
return new Tuple<string, string>(selectedShowsJson.ToString(Formatting.None), otherShowsJson.ToString(Formatting.None));
}
Esta nueva implementación no es tan compacta, pero es algo más rápida, tardando unos 237,000 ticks en ejecutarse.
Como lo que estamos manejando son strings, muchos habéis pensado en usar directamente expresiones regulares para procesarlos. Y efectivamente soluciones como esta propuesta por @lantoli son más rápidas que usando Json.NET, aunque mucho más complejas:
public class JsonSplitter
{
public static JsonSplitter SplitShowsByGenre(string json, string genre)
{
json = DecodeUnicodeChars(json);
var find = '"' + genre + '"';
var str1 = new StringBuilder();
var str2 = new StringBuilder();
int firstPosShow = -1, lastPosShow = -1;
while (json[lastPosShow + 1] != ']')
{
firstPosShow = lastPosShow + 2; // skip [{ or ,{
lastPosShow = json.IndexOf('}', json.IndexOf('}', json.IndexOf('}', lastPosShow + 1) + 1) + 1); // skip images, rating, show itself
var genreIni = json.IndexOf('[', firstPosShow + 1); // genre is the only array
var genreIndex = json.IndexOf(find, genreIni + 1);
var choose = (genreIndex != -1 && genreIndex < json.IndexOf(']', genreIni + 1)) ? str1 : str2;
choose.Append(choose.Length == 0 ? '[' : ',');
choose.Append(json, firstPosShow, lastPosShow - firstPosShow + 1);
}
return new JsonSplitter
{
Item1 = str1.Append(']').ToString(),
Item2 = str2.Append(']').ToString()
};
}
private static Regex _regex = new Regex(@"\\u(?<Value>[a-zA-Z0-9]{4})", RegexOptions.Compiled);
public static string DecodeUnicodeChars(string value)
{
return _regex.Replace(
value,
m => ((char)int.Parse(m.Groups["Value"].Value, NumberStyles.HexNumber)).ToString()
);
}
public string Item1 { get; private set; }
public string Item2 { get; private set; }
}
Este método tarda unos 190,000 ticks en mi máquina. Mejor, aunque luego habría que evaluar si la complejidad adicional merece la pena.
Hasta que @angel_g_santos nos ha propuesto una solución similar a la segunda de nuestras soluciones, pero en lugar de usar Json.NET usa fastJSON:
public static Tuple<string, string> SplitShowsByGenre(string json, string genre)
{
var genreList = new ArrayList();
var otherList = new ArrayList();
foreach (Dictionary<string, object> item in (object[])JSON.ToObject(json))
{
if (((List<object>)item["genres"]).Contains(genre))
{
genreList.Add(item);
continue;
}
otherList.Add(item);
}
JSONParameters param = new JSONParameters { UseEscapedUnicode = false };
return new Tuple<string, string>(
JSON.ToJSON(genreList, param),
JSON.ToJSON(otherList, param));
}
¡Y esta solución tarda tan solo unos 50,000 ticks en mi máquina! Un ahorro considerable con tan sólo cambiar de librería y con un código muy limpio y sencillo de entender.
Usando fastJSON también, nosotros hemos implementado un método más compacto, en la misma línea que nuestra primera solución con Json.NET:
public static Tuple<string, string> SplitShowsByGenre(string json, string genre)
{
var split =
((object[])JSON.ToObject(json))
.GroupBy(s => ((List<object>)((Dictionary<string, object>)s)["genres"]).Contains(genre))
.OrderBy(g => g.Key)
.Select(g => JSON.ToJSON(g, new JSONParameters() { UseEscapedUnicode = false }))
.ToList();
return new Tuple<string, string>(split[1], split[0]);
}
Esta solución que usa Linq es algo más lenta que la propuesta por @angel_g_santos, ya que tarda unos 63,000 ticks en mi máquina, pero es también mucho más rápida que la misma solución usando Json.NET.
El código completo de nuestras soluciones lo puedes encontrar en esta solución de Visual Studio 2013 que puedes descargarte de GitHub.
¡El próximo viernes 12 de diciembre publicaremos el siguiente de nuestros #retosMSDN! Y si quieres retar al resto de la comunidad con tu propio reto, recuerda que puedes enviárnoslo a esmsdn@microsoft.com.
Un saludo,
Alejandro Campos Magencio (@alejacma)
Technical Evangelist
PD: Mantente informado de todas las novedades de Microsoft para los desarrolladores españoles a través del Twitter de MSDN, el Facebook de MSDN, el Blog de MSDN y la Newsletter MSDN Flash.