Acerca de:

Este blog contiene los códigos, ejemplos y bases de datos que he usado cuando aprendía acerca de algún tema específico. En lugar de borrarlos (una vez dominado ya el tema), he decidido publicarlos :)

viernes, 4 de abril de 2025

El test FizzBuzz en C# y cómo abusar del lenguaje C# versión 8+

Rebuscando en mis archivos viejos me encontré una implementación del algoritmo FizzBuzz. Lo mencionan en esta entrada https://www.variablenotfound.com/2007/02/dnde-se-han-ido-los-programadores.html y también lo mencionan acá https://exponentis.es/el-test-de-fizz-buzz-para-contratar-programadores como uno de los tests básicos para contratar programadores.

Me divertí con este test hace casi veinte años, y por pura nostalgia pongo acá mi primera implementación de aquellos años y que es tan aburrida y genérica que hasta una AI podría hacerla:

for (int i = 1; i <= 100; i++)
{
    t = i % 3;
    c = i % 5;
    tc = i % 15;
    if (t == 0 || c == 0 || tc == 0)
    {
if (t == 0)
    Console.WriteLine("Fizz");
if (c == 0)
    Console.WriteLine("Buzz");
if (tc == 0)
    Console.WriteLine("Fizz Buzz");
    }
    else
Console.WriteLine(i);
}

Esta primera implementación tiene un detalle: para los múltiplos de 15 imprime
Fizz
Buzz
Fizz Buzz

Si sólo se quiere "Fizz Buzz" para los múltiplos de 15 debe ser:

int t = 0, c = 0, tc = 0;

for (int i = 1; i <= 100; i++)
{
    t = i % 3;
    c = i % 5;
    tc = i % 15;

    if (t == 0 || c == 0 || tc == 0)
    {
        if (tc == 0)
            Console.WriteLine("Fizz Buzz");
        else if (c == 0)
            Console.WriteLine("Buzz");
        else if (t == 0)
            Console.WriteLine("Fizz");
    }
    else
        Console.WriteLine(i);
}

Console.ReadLine();

La trampa en este test es que 15 es múltiplo de 3 y 5, así que se debe evaluar si es múltiplo de 15 primero si no se desea imprimir lo demás, de todos modos esto depende de si se considera correcto o no imprimir sólo "Fizz Buzz" con los múltiplos de 15 o si se acepta también que imprima "Fizz" y "Buzz" para este caso; todo depende de la salida que se desee, lo que se considera correcto en este contexto, más las pruebas unitarias... lo clásico del desarrollo de software.

Si se ignoran estos detalles y nos concentramos en el verdadero objetivo del test: saber si se es capaz de escribir un programita que haga algo y que implique un bucle con condicionales, sigue siendo mortalmente aburrido.


Para no aburrirnos tanto, acá hay una implementación que acabo de hacer, en una lambda recursiva porque le tengo manía a las lambdas recursivas:

Func<int, int> F2 = null;
F2 = i =>
{
    int t2, c2, tc2;
    if (i == 101)
        return 0;
    else
    {
        t2 = i % 3;
        c2 = i % 5;
        tc2 = i % 15;
        if (t2 == 0 || c2 == 0 || tc2 == 0)
        {
            if (tc2 == 0)
                Console.WriteLine("Fizz Buzz");
            else if (c2 == 0)
                Console.WriteLine("Buzz");
            else if (t2 == 0)
                Console.WriteLine("Fizz");
        }
        else
            Console.WriteLine(i);
        return i = F2(i + 1);
    }
};
Console.WriteLine("llamando a Función Lambda Recursiva");
F2(0);

Lo sigo viendo aburrido, es el mismo código pero metido en una función lambda. 

Por otro lado, estos días he estado revisando algunas novedades del lenguaje C# y encontré que hay otra forma de escribir la declaración switch, más info en estos enlaces:

https://www.csharp.com/article/c-sharp-12s-switch-expressions-a-more-powerful-alternative-to-traditional-switch-st/

https://stackoverflow.com/questions/44355630/how-to-use-c-sharp-tuple-value-types-in-a-switch-statement

Lo bonito es que también admite tuplas, y esos "ifs/elses" están que me piden ser convertidos en un switch al estilo C# a partir de la versión 8. Un detalle de los nuevos switch es que ya vienen con los breaks de forma implícita, de modo que si cumplen con una de las condiciones, el código no sigue cayendo en las demás. 
Luego de juguetear un poco, otro detalle que encontré es que las tuplas no necesitan ser de sólo dos valores, pueden tener más. En este caso tengo tres variables qué evaluar: tc2, c2 y tc2, además del contador. El resultado: una tupla de cuatro valores (yo la llamaría "cuadrupla").

El código es:

Func<int, int> F2 = null;
F2 = i =>
{
    int t2, c2, tc2;
    if (i == 101)
        return 0;
    else
    {
        t2 = i % 3;
        c2 = i % 5;
        tc2 = i % 15;
                           
        var resultado = (t2, c2, tc2, i);
        Console.WriteLine(resultado switch
        {
            (_, _, 0, > 14) => "Fizz Buzz",
            (_, 0, _, > 4) => "Buzz",
            (0, _, _, > 2) => "Fizz",
            _ => i,
        });
        return i = F2(i + 1);
    }
};
Console.WriteLine("llamando a Función Lambda Recursiva");
Console.WriteLine();
F2(0);

Se ve rarísimo y se puede poner peor, nos podemos deshacer de tanta variable y acortar el condicional:
Func<int, int> F2 = null;
F2 = i =>
{
    var resultado = (i % 3, i % 5, i % 15, i);
    Console.WriteLine(resultado switch
    {
        (_, _, 0, > 14) => "Fizz Buzz",
        (_, 0, _, > 4) => "Buzz",
        (0, _, _, > 2) => "Fizz",
        _ => i,
    });
    return i == 100 ? 0 : F2(i + 1);
};
Console.WriteLine("llamando a Función Lambda Recursiva");
Console.WriteLine();
F2(0);

Y puede ser aún peor, ya dejando de lado el switch podemos abusar de los IEnumerables de C# y terminar con esperpentos como el siguiente:

    Func<IEnumerable<string>, IEnumerable<string>> F3 =
    f =>
       f.Select(c => int.TryParse(c, out _) && int.Parse(c) % 15 == 0 ? c = "Fizz Buzz" : c)
       .Select(c => int.TryParse(c, out _) && int.Parse(c) % 3 == 0 ? c = "Fizz" : c)
       .Select(c => int.TryParse(c, out _) && int.Parse(c) % 5 == 0 ? c = "Buzz" : c);

Console.WriteLine("llamando a Función Lambda que ya no es Recursiva pero se ve más fea");
Console.WriteLine();

IEnumerable<string> f0 = F3(Enumerable.Range(1, 100).Select(c=> c.ToString()));

foreach (var item in f0)
{
    Console.WriteLine(item);
}

Console.ReadLine();

La función TryParse la uso sólo para evaluar si la variable c se puede convertir a entero, de otro modo arroja error de formato. Esa característica del C# de usar el guión largo para ignorar parámetros le da al código un aspecto medio esotérico que me encanta :)

No hay comentarios: