Hilando

Hilando

Desde .NET 4, los hilos en C# se convirtieron en una tarea de lo más fácil. En este post vamos a realizar una par de muestras, una prueba sencilla donde vamos a ejecutar una tarea que usa mucho tiempo en terminarse como es la obtención de π(x) o la cantidad de números primos menores que un número.Vamos a usar una función lambda para obtener el codiciado π(x) y para ello vamos a usar la clase Enumerable del espacio System.Linq y un método llamado Range para obtener un conjunto de números enteros entre dos rangos numerable.Range(2, max), contamos los elementos con el predicado con el que obtengamos todos los números entre el rango de 2 y la raiz del número Count(n => Enumerable.Range(2, (int)Math.Sqrt(n) - 1) que cumplan la condición de tener un módulo mayor que 0 All(i => n % i > 0))o lo que es lo mismo todos los números que no son múltiplos.

Enumerable.Range(2, max).Count(n => Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0))

Linq es bestial, ¿no?
Si le damos un valor alto a la variable max com por ejemplo 10.000.000, la tarea dejará la interfaz bloqueada y por tanto tendremos que crear un hilo para evitar este problema y aquí viene lo bueno de la clase Task.

Esta clase tiene un método Factory.StartNew al que se le pasa un método como delegado, el cual se ejecutará de forma asincrónica devolviendo la tarea iniciada, es decir no devuelve un valor sino la tarea Task en si con sus propiedades y métodos.

Task.Factory.StartNew(() => TareaMuyCostosa());
En nuestro código vamos a ver solo dos aspectos de los muchos de los que podemos extraerle a Task, uno es la propiedad IsCompleted y otra sería el método Wait.
Nuestra aplicación como he dicho, generará números primos en una tarea y mientras que no esté finalizada, mostrará un mensaje indicando el tiempo empleado y una vez que finalice, mostrará el resultado que lo mostraremos de dos modos, uno esperando a que finalice y otro con el método ContinueWith.

La primera

static void primeNumbersCount(int max)
        {
            Stopwatch stw = new Stopwatch();
            stw.Start();
            int sec = 0;
            var piXTask = Task.Factory.StartNew(() => Enumerable.Range(2, max).Count(n => Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));

            var calculando = "Calculando ";
            while (!piXTask.IsCompleted || piXTask.IsFaulted || piXTask.IsCanceled)
            {
                Thread.Sleep(1000);
                Console.Clear();
                Console.WriteLine(calculando.PadRight(sec % 5 + 11,'.'));
                Console.WriteLine(string.Format("Tiempo empleado: {0} sengundos", sec++));
            }

            piXTask.Wait();

            string text = string.Format("Calculo de {0} números primos menores de {1}", piXTask.Result, max);
            Console.WriteLine(text);
            Console.WriteLine(string.Format("Tiempo empleado: {0}", stw.Elapsed));
            Console.Read();
        }

En este código, vemos que se lanza la tarea, luego pasamos a un bucle del que debemos asegurarnos que saldremos algún día (esta opción no la aconsejo, pero para mostrar que la app se queda en sus cosas mientras la tarea se está realizando, está bien), mediante la propiedad IsCompleted, IsFaulted o IsCanceled, es decir cuando pase alguna de estas cosas, sale del bucle.
Luego pasamos al método Wait, que lo que hace es lo mismo que el bucle, hasta que la tarea no haya finalizado, espera y no sobrepasa a la siguiente línea de código. Una vez acabado, muestra el resultado con la propiedad Result del tipo int, porque es el resultado que entrega el delegado que lanzó. Y el resultadotask03task01

Pero para mi, en este caso usaría lo mismo pero con el método ContinueWith.

static void primeNumbersCount(int max)
        {
            Stopwatch stw = new Stopwatch();
            stw.Start();
            int sec = 0;
            var piXTask = Task.Factory.StartNew(() => Enumerable.Range(2, max).Count(n => Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
            var calculando = "calculando ";
            piXTask.ContinueWith((p) =>
            {
                string text = string.Format("Calculo de {0} números primos menores de {1}", piXTask.Result, max);
                Console.WriteLine(text); Console.WriteLine(string.Format("Tiempo empleado: {0}", stw.Elapsed));
            });
            Console.WriteLine("Esto no ha terminado...");
            Thread.Sleep(1000); sec++;
            while (!piXTask.IsCompleted)
            {
                Thread.Sleep(1000); Console.Clear();
                Console.WriteLine(calculando.PadRight(sec % 5 + 11, '.'));
                Console.WriteLine(string.Format("Tiempo empleado: {0} segundos", sec++));
            }
          }

Aquí podemos apreciar que se lanza la tarea, vemos el método ContinueWith que contiene código en su interior mediante otro delegado y posteriormente el código ese que hemos creado para mostrar cosas mientras la tarea se realiza. Al ejecutarlo, se lanza la tarea y se obvia el código del método ContinueWith en cuestión, continua y pasa al bucle, una vez que ha terminado la tarea, continua con lo que le queda dentro del bloque ContinueWith y se acabó, ¿fácil? facilísimo!!

Video4
Ya veremos en otra ocasión el uso de Arrays de Task, esperas, sincronías entre ellos y alguna otra cosa más.
Anuncio publicitario

Sucesión de Fibonacci

Sucesión de Fibonacci
Publiqué en mi otro blog sobre mates, física y otros temas un post acerca de la sucesión y la espiral de Fibonacci y gustándome como me gustan las funciones recursivas, no podía quedar atrás una implementación en C# de esta sucesión.
Para ellos, en este artículo expondré diversos métodos para calcular la sucesión de Fibonacci a los cuales se les pasará como argumento el valor de n de la función y que a continuación os muestro:
El primero de ellos será mediante la función recursiva. Este es el método natural de cálculo de la sucesión
espiral01.png

        public double GetRecursiveFunction(int n)
        {
            if ((n == 0) || (n == 1)) return 1;
            else return GetRecursiveFunction(n -1) + GetRecursiveFunction(n - 2);
        }

Este método tiene el inconveniente de la complejidad recurrente que se aplica sobre su cálculo, es decir que si solicitamos el valor del elemento 39, haríamos una llamada a GetRecursiveFunction(39) , la cual una vez dentro de esta, llamará a los elementos GetRecursiveFunction(38) y GetRecursiveFunction(37) de la misma función para sumarlos, y estos a su vez a sus dos anteriores hasta llegar a GetRecursiveFunction(0) o GetRecursiveFunction(1) devolviendo el valor de 1 a partir de entonces comenzará a sumar. Esto significa que para cada cálculo se realiza la suma de las iteraciones de los dos elementos anteriores más 1. Si para n igual a 6 se llama a la función 15 veces y para n igual a 7, 25 veces, para n igual a 8 se llamaría a la función 15 más 25 y más 1 de la primera llamada. Esto supone un gasto de recursos enorme ya que para n igual a 40 el número de iteraciones se elevaría a la suma de 78.176.337, 126.491.971 y 1, un total de 204.668.309 iteraciones para obtener el elemento 40 de la sucesión; en tiempo, aproximadamente unos 8 segundos en un dual core con SSD. A todos los efectos, no es eficiente.El siguiente método de cálculo de la sucesión de Fibonacci, será el de la función general extraída mediante la ecuación de recurrencia y sus raíces.espiral14.png

Esta función tiene como inconveniente la precisión y muestro el por qué. En primero lugar creo una propiedad de solo lectura para calcular ϕ (Phi) o el número áureo, el cual es igual a
Espiral04.png
provocando una falta de precisión; por otra parte el uso de la exponenciación a n, lo que a su vez provoca que un número excesivamente grande, sea tratado exponencialmente. Todo esto unido para un n grande, nos hace obtener un número extremadamente aproximado a cualquier elemento de la sucesión, pero sin llegar a serlo, por lo que debemos auxiliarnos de la función de redondeo para obtener unos resultados satisfactorios. A pesar de los complejos cálculos, es muy rápida.

        public double Phi
        {
            get
            {
                return (1 + Math.Sqrt(5)) / 2;
            }
        }

        public double GetGeneralFuncion(int n)
        {
            double result = (double)((Math.Pow(Phi, n) - Math.Pow((1 - Phi), n)) / Math.Sqrt(5));
            return Math.Round(result);
        }

La siguiente función es la iterativa. Esta parte de los dos primeros valores 0 y 1 y va obteniendo el siguiente en cada iteración, sencilla y muy rápida. No tiene problemas de redondeo. El inconveniente que tiene es que no almacena los valores anteriores, sino que simplemente los calcula.

        public double GetIterativeFunction(int n)
        {
            double f0 = 0;
            double f1 = 1;
            for (int i = 1; i < n; i++)
            {
                f1 = f0 + f1;
                f0 = f1 - f0;
            }

            return f1;
        }

Por último, una función parecida a la iterativa pero en la cual quedan almacenados los elementos de la sucesión mediante un objeto List, de este modo se asignan los dos primeros valores y se van generando los siguientes añadiéndolos a la lista según se van calculando.

        public List numbers = new List();
        public void GetArrayListFunction(int n)
        {
            numbers.Clear();
            numbers.Add(0);
            numbers.Add(1);
            for (int i = 2; i < n; i++)
            {
                numbers.Add(numbers[i - 1] + numbers[i - 2]);
            }
        }

Una vez que hemos visto las funciones para obtener los elementos de la sucesión de Fibonacci, vamos a calcular la espiral de Fibonacci mediante dos métodos, XAML y código puro y duro.
Si lo hacemos con XAML, creamos un objeto Path y dentro de Path.Data con el siguiente código:

        <Path Stroke="Red" StrokeThickness="2" >
        <Path.Data>
            <PathGeometry>
                <PathGeometry.Figures>
                    <PathFigureCollection>
                            <PathFigure StartPoint="610,610">
                                <PathFigure.Segments>
                                <PathSegmentCollection>
                                        <ArcSegment Point="600, 600" Size="10 10" />
                                        <ArcSegment Point="590, 610" Size="10 10" />
                                        <ArcSegment Point="610, 630" Size="20 20" />
                                        <ArcSegment Point="640, 600" Size="30 30" />
                                        <ArcSegment Point="590, 550" Size="50 50" />
                                        <ArcSegment Point="510, 630" Size="80 80" />
                                        <ArcSegment Point="640, 760" Size="130 130" />
                                        <ArcSegment Point="850, 550" Size="210 210" />
                                        <ArcSegment Point="510, 210" Size="340 340" />
                                        <ArcSegment Point="-40, 760" Size="550 550" />
                                        <ArcSegment Point="850, 1650" Size="890 890" />
                                        <ArcSegment Point="2290, 210" Size="1440 1440" />
                                    </PathSegmentCollection>
                            </PathFigure.Segments>
                        </PathFigure>
                    </PathFigureCollection>
                </PathGeometry.Figures>
            </PathGeometry>
        </Path.Data>
        </Path>

Lo que hacemos es crear objetos ArcSegment como tantos elementos de la sucesión queramos incluir, pero su límite es evidente, por lo que podríamos crear un método genérico que creara una espiral en base al argumento n, de modo que llamamos por ejemplo a la función iterativa basada en List y con los datos almacenados en esta lista, usarlos para crear tanto objetos ArcSegment como elementos tenga la sucesión. El problema es que la sucesión de Fibonacci tiene un crecimiento alto y la espiral desaparecerá rápidamente de nuestra ventana, pero calcular, calcula correctamente la espiral.
He creado dos propiedades para el centro de la espiral, x e y un ángulo como variable angular. (También podríamos haber usado la función polar de la espiral, pero he escogido esta para mostrar cada arco de segmento).
Para seleccionar el centro de cada arco, he ingeniado un método que mediante el seno y coseno del ángulo de la función, sume o reste los valores de la sucesión y asigne correctamente el centro.
Una vez creados los arcos, los añadimos al objeto Figure y como en XAML vamos añadiendo a la colección.

        public double CenterX { get; set; } = 512;
        public double CenterY { get; set; } = 384;
        public double Angle { get; set; } = Math.PI;

        void create(int n)
        {

            Fibonacci fib = new Fibonacci();
            fib.GetArrayListFunction(n);

            PathGeometry pathGeometry = new PathGeometry();

            PathFigure figure = new PathFigure();
            figure.StartPoint = new Point(CenterX, CenterY);

            for (int i =0; i < fib.numbers.Count; i++)
            {
                double sign = (i % 2 == 0 ? 1 : -1);
                double x = sign==1? Math.Round(Math.Cos(Angle)) : Math.Round(Math.Sin(Angle));
                double y = sign == 1 ? Math.Round(Math.Sin(Angle - Math.PI / 2)) : Math.Round(Math.Cos(Angle-Math.PI/2));
                double a = Math.Round(fib.numbers[i] * sign * x);
                double b = Math.Round(fib.numbers[i] * sign * y);

                CenterX = CenterX + a;
                CenterY = CenterY + b;

                ArcSegment arcSegment = new ArcSegment(new Point(CenterX, CenterY),
                    new Size(fib.numbers[i], fib.numbers[i]),
                    0,false,SweepDirection.Counterclockwise,true);

                figure.Segments.Add(arcSegment);
                Angle += Math.PI/2;

            }

            pathGeometry.Figures.Add(figure);
            path.Data = pathGeometry;
            path.Stroke = Brushes.Green;
        }

el resultado en ambos casos

fib02.png
y esto es todo,
saludos!

Sudoku

Sudoku
El post de hoy va sobre la generación de un sudoku.
Para el que no lo sepa, un sudoku consta de una matriz de 9 cuadrados de 3×3, los cuales contienen 9 números del 1 al 9 cada uno.
La característica principal del sudoku, es que no se repite ningún número en las filas, columnas y cuadrados de 3×3. Un ejemplo de ello, sería el siguiente:sudoku01Podéis comprobar que en todas las filas, se encuentran los números del 1 al 9
los cuales no se repiten; si analizamos las columnas, ocurre lo mismo y si lo hacemos con los cuadrados de 3×3, pasa igual.Para generar una matriz completa de sudoku, debemos crear un método que inicie una secuencia y en el caso de que no sea idónea, es decir que se repita algún número o que no se pueda incluir, debe iniciarla de nuevo hasta encontrar una que lo sea, pero para ello, debemos ayudarle a escoger el buen camino y que el número de repeticiones sea el menor posible.Llevándolo a la práctica, el método que he utilizado para generar la matriz es el siguiente:Lo primero de todo, es optimizar el rendimiento de nuestra aplicación y para hacerlo, nos ayudaremos del objeto List, el cual permite añadir (Add), eliminar(remove) y obtener un objeto mediante su índice. En él, añado los números del 1 al 9 y obteniendo un número aleatorio desde 0 al número de objetos de la lista, puedo ir extrayendo como si de un bombo se tratara cada número, así nos aseguramos que los números no se repiten.En primer lugar, esta primera secuencia la creamos por filas.

En cada una de las posiciones, elimino los elementos que están a la izquierda o en su misma fila, elimino los que están por encima o los que pertenecen a la misma columna y por último, elimino los que se encuentran en el mismo cuadrado de 3×3.

De estos números candidatos, debemos elegir el más idóneo, pero… dejando que el azar haga su trabajo y para realizar esto, calculo la probabilidad de incluir el número en la fila o lo que es lo mismo, determinar el número de veces que puede incluirse en las siguientes posiciones. Recordemos que la probabilidad de un suceso es igual al número de casos favorables o el número de casos que cumplen con nuestro objetivo, dividido entre el número de casos totales o posibles. En nuestro caso, si estamos decidiendo obtener el número central del cuadrado, el número con coordenadas 4,4 (en la imagen equivale al número 7 central, las coordenadas van desde 0 hasta 8), la probabilidad de incluir el número 7 en las siguientes columnas sería de 3/5, es decir 3 columnas en las que es posible incluir el número 7 de las 5 que quedan.

sudoku02

Pongamos un ejemplo

sudoku03

La posición con coordenadas 4,5, tiene como números candidatos el 1 y el 6; el 1 tiene un 50 % de probabilidad de colocarse en las siguientes columnas, (puede incluirse en la columna 5 y 8 sobre 4 columnas)  y el 6 un 75% (puede incluirse en la columna 5,6 y 8 sobre 4 columnas), por tanto lo que hacemos es «incentivar» a los que tienen menos probabilidad con un número igual a 1-probabilidad o probabilidad de no ocurrencia, en este caso creo un conjunto con un número igual a la probabilidad de no ocurrencia multiplicado por 10; en el ejemplo creo un conjunto de 0.5 x 10=5 números 1 y 0.25 x 10=2,5 o 2 números 6 {1,1,1,1,1,6,6}, seleccionamos un número al azar y ya tenemos nuestra elección, el número 1 con más probabilidad de incluirse que el 6, pero ambos están en el bombo y así no descartamos futuras combinaciones válidas.

La secuencia va aumentando hasta que, o no tiene ningún candidato en alguna de las posiciones y por tanto debe comenzar de nuevo o llega al final de la secuencia con una combinación de números adecuada gracias a nuestra ayuda probabilística.

Esto aplicado a código, lo hago con C# y WPF, me encanta escribir y no escribir código! me encanta escribir clases independientes que no tengan ninguna relación con la interfaz gráfica y me encanta crear formularios sin código (C# o VB .NET).

Como clase ViewModel, la clase SudokuManager, las dos implementan la interfaz INotifiPropertyChanged para que podamos enlazar los datos de nuestro código XAML y de las cuales podemos mostrarlas mediante el siguiente diagrama de clases

sudoku04.png

Creo una clase llamada Point para cada posición de la matriz con las coordenadas, el valor de la casilla, una propiedad de solo lectura para calcular el cudrado de 3×3 al que pertenece y si la posición es visible para ocultarla en el futuro y poder calcularla.

En la clase SudokuManager, tenemos una propiedad que es una colección de estas 81 posiciones, dos propiedades una para obtener el tiempo de generación en milisegundos y otra para ver el número de iteraciones que ha necesitado para generar el sudoku completo y una vez que este ha sido creado correctamente, la propiedad IsGood pasa a True.

Como métodos públicos, Start para generar una matriz nueva y ToString para obtener la lista completa textualmente. Tengo otros métodos para ocultar los números en base a la dificultad y poder jugar o poder calcular el Sudoku automáticamente, pero eso quedará para otro día.

Como maquinaria interna o métodos privados, el principal es el método setSelection al cual le pasamos como parámetro el objeto Point o cuadrícula de la matriz, de la lista inicial del 1 al 9, le quita los elementos de su izquierda o de su misma fila, los elementos superiores o de su columna y los elementos de su cuadrícula de 3×3, una vez hecho esto, solo quedarían los números candidatos y llamaría al método getSelection, el cual calcula la probabilidad que hemos explicado y obtiene el número definitivo que se le asignará a la posición.

Una vez hecho esto, vamos a ver si el rendimiento es adecuado, ya que imaginad que para generar una matriz, necesitara 5 segundos, algo inaceptable para los tiempos que corren, por lo que he realizado varias muestras de 50 generaciones y de todas ellas, obtengo un promedio de 53,58 iteraciones y un promedio de 192 milisegundos por cada una, estando el valor mínimo de 1 iteración y 5 ms (con un I5 2400, que no es un I7 6500!)  y unos valores máximos de 165 iteraciones y medio segundo de los cuales hay que decir que tienen una probabilidad de salir de un 2%, o una probabilidad de un 16% de superar las 100 iteraciones y los 350ms.

Bueno, para ser el primero, creo que no está mal, seguro que se pueden arañar algún milisegundo en alguna operación.

En cuanto al XAML, he creado un control de usuario para que no sea muy extenso el código de la página principal, al cual le pasamos como DataContext la clase SudokuManager y a cada posición, le asignamos como DataContext el objeto Point que le pertenece, de modo que desde el estilo, podemos hacer un Binding a sus propiedades, un poco de código como muestra

<UserControl x:Class="Ramanujan.Matriz"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:Ramanujan"
             mc:Ignorable="d" 
             d:DesignHeight="490" d:DesignWidth="490">
    <Grid x:Name="grid">        
        <Grid.Resources>
            <!-- CONVERTER -->
            <local:BoolToVisibleConverter x:Key="b2vc"/>
            <!-- ESTILO DE TEXTBLOCK -->
            <Style x:Key="textBlockStyle" TargetType="TextBlock">
                <Setter Property="HorizontalAlignment" Value="Center"/>
                <Setter Property="VerticalAlignment" Value="Center"/>
                <Setter Property="TextAlignment" Value="Center"/>
                <Setter Property="FontSize" Value="24"/>
                <Setter Property="Visibility" Value="{Binding IsVisible,Converter={StaticResource b2vc}}"/>
                <Setter Property="Text" Value="{Binding Value}"/>
            </Style>
        </Grid.Resources>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="20"/>
            <ColumnDefinition Width="50"/>
            <ColumnDefinition Width="50"/>
            <ColumnDefinition Width="50"/>
            <ColumnDefinition Width="50"/>
            <ColumnDefinition Width="50"/>
            <ColumnDefinition Width="50"/>
            <ColumnDefinition Width="50"/>
            <ColumnDefinition Width="50"/>
            <ColumnDefinition Width="50"/>
            <ColumnDefinition Width="20*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="20"/>
            <RowDefinition Height="50"/>
            <RowDefinition Height="50"/>
            <RowDefinition Height="50"/>
            <RowDefinition Height="50"/>
            <RowDefinition Height="50"/>
            <RowDefinition Height="50"/>
            <RowDefinition Height="50"/>
            <RowDefinition Height="50"/>
            <RowDefinition Height="50"/>
            <RowDefinition Height="20*"/>
        </Grid.RowDefinitions>
        <Border Grid.Column="1" Grid.Row="1" Grid.RowSpan="9" Grid.ColumnSpan="9" BorderBrush="WhiteSmoke" BorderThickness="2"/>
        <Border Grid.Column="1" Grid.Row="1" Grid.RowSpan="3" Grid.ColumnSpan="3" BorderBrush="WhiteSmoke" BorderThickness="1"/>
        <Border Grid.Column="4" Grid.Row="1" Grid.RowSpan="3" Grid.ColumnSpan="3" BorderBrush="WhiteSmoke" BorderThickness="1"/>
        <Border Grid.Column="7" Grid.Row="1" Grid.RowSpan="3" Grid.ColumnSpan="3" BorderBrush="WhiteSmoke" BorderThickness="1"/>
        <Border Grid.Column="1" Grid.Row="4" Grid.RowSpan="3" Grid.ColumnSpan="3" BorderBrush="WhiteSmoke" BorderThickness="1"/>
        <Border Grid.Column="4" Grid.Row="4" Grid.RowSpan="3" Grid.ColumnSpan="3" BorderBrush="WhiteSmoke" BorderThickness="1"/>
        <Border Grid.Column="7" Grid.Row="4" Grid.RowSpan="3" Grid.ColumnSpan="3" BorderBrush="WhiteSmoke" BorderThickness="1"/>
        <Border Grid.Column="1" Grid.Row="7" Grid.RowSpan="3" Grid.ColumnSpan="3" BorderBrush="WhiteSmoke" BorderThickness="1"/>
        <Border Grid.Column="4" Grid.Row="7" Grid.RowSpan="3" Grid.ColumnSpan="3" BorderBrush="WhiteSmoke" BorderThickness="1"/>
        <Border Grid.Column="7" Grid.Row="7" Grid.RowSpan="3" Grid.ColumnSpan="3" BorderBrush="WhiteSmoke" BorderThickness="1"/>
        <TextBlock x:Name="textBlock00" DataContext="{Binding Points[0]}" Grid.Column="1" Grid.Row="1" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock01" DataContext="{Binding Points[1]}" Grid.Column="2" Grid.Row="1" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock02" DataContext="{Binding Points[2]}" Grid.Column="3" Grid.Row="1" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock03" DataContext="{Binding Points[3]}" Grid.Column="4" Grid.Row="1" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock04" DataContext="{Binding Points[4]}" Grid.Column="5" Grid.Row="1" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock05" DataContext="{Binding Points[5]}" Grid.Column="6" Grid.Row="1" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock06" DataContext="{Binding Points[6]}" Grid.Column="7" Grid.Row="1" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock07" DataContext="{Binding Points[7]}" Grid.Column="8" Grid.Row="1" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock08" DataContext="{Binding Points[8]}" Grid.Column="9" Grid.Row="1" Style="{StaticResource textBlockStyle}"/>

        <TextBlock x:Name="textBlock10" DataContext="{Binding Points[9]}" Grid.Column="1" Grid.Row="2" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock11" DataContext="{Binding Points[10]}" Grid.Column="2" Grid.Row="2" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock12" DataContext="{Binding Points[11]}" Grid.Column="3" Grid.Row="2" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock13" DataContext="{Binding Points[12]}" Grid.Column="4" Grid.Row="2" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock14" DataContext="{Binding Points[13]}" Grid.Column="5" Grid.Row="2" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock15" DataContext="{Binding Points[14]}" Grid.Column="6" Grid.Row="2" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock16" DataContext="{Binding Points[15]}" Grid.Column="7" Grid.Row="2" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock17" DataContext="{Binding Points[16]}" Grid.Column="8" Grid.Row="2" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock18" DataContext="{Binding Points[17]}" Grid.Column="9" Grid.Row="2" Style="{StaticResource textBlockStyle}"/>

        <TextBlock x:Name="textBlock20" DataContext="{Binding Points[18]}" Grid.Column="1" Grid.Row="3" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock21" DataContext="{Binding Points[19]}" Grid.Column="2" Grid.Row="3" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock22" DataContext="{Binding Points[20]}" Grid.Column="3" Grid.Row="3" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock23" DataContext="{Binding Points[21]}" Grid.Column="4" Grid.Row="3" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock24" DataContext="{Binding Points[22]}" Grid.Column="5" Grid.Row="3" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock25" DataContext="{Binding Points[23]}" Grid.Column="6" Grid.Row="3" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock26" DataContext="{Binding Points[24]}" Grid.Column="7" Grid.Row="3" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock27" DataContext="{Binding Points[25]}" Grid.Column="8" Grid.Row="3" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock28" DataContext="{Binding Points[26]}" Grid.Column="9" Grid.Row="3" Style="{StaticResource textBlockStyle}"/>

        <TextBlock x:Name="textBlock30" DataContext="{Binding Points[27]}" Grid.Column="1" Grid.Row="4" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock31" DataContext="{Binding Points[28]}" Grid.Column="2" Grid.Row="4" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock32" DataContext="{Binding Points[29]}" Grid.Column="3" Grid.Row="4" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock33" DataContext="{Binding Points[30]}" Grid.Column="4" Grid.Row="4" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock34" DataContext="{Binding Points[31]}" Grid.Column="5" Grid.Row="4" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock35" DataContext="{Binding Points[32]}" Grid.Column="6" Grid.Row="4" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock36" DataContext="{Binding Points[33]}" Grid.Column="7" Grid.Row="4" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock37" DataContext="{Binding Points[34]}" Grid.Column="8" Grid.Row="4" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock38" DataContext="{Binding Points[35]}" Grid.Column="9" Grid.Row="4" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock40" DataContext="{Binding Points[36]}" Grid.Column="1" Grid.Row="5" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock41" DataContext="{Binding Points[37]}" Grid.Column="2" Grid.Row="5" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock42" DataContext="{Binding Points[38]}" Grid.Column="3" Grid.Row="5" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock43" DataContext="{Binding Points[39]}" Grid.Column="4" Grid.Row="5" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock44" DataContext="{Binding Points[40]}" Grid.Column="5" Grid.Row="5" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock45" DataContext="{Binding Points[41]}" Grid.Column="6" Grid.Row="5" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock46" DataContext="{Binding Points[42]}" Grid.Column="7" Grid.Row="5" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock47" DataContext="{Binding Points[43]}" Grid.Column="8" Grid.Row="5" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock48" DataContext="{Binding Points[44]}" Grid.Column="9" Grid.Row="5" Style="{StaticResource textBlockStyle}"/>

        <TextBlock x:Name="textBlock50" DataContext="{Binding Points[45]}" Grid.Column="1" Grid.Row="6" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock51" DataContext="{Binding Points[46]}" Grid.Column="2" Grid.Row="6" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock52" DataContext="{Binding Points[47]}" Grid.Column="3" Grid.Row="6" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock53" DataContext="{Binding Points[48]}" Grid.Column="4" Grid.Row="6" Style="{StaticResource textBlockStyle}"/> 
        <TextBlock x:Name="textBlock54" DataContext="{Binding Points[49]}" Grid.Column="5" Grid.Row="6" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock55" DataContext="{Binding Points[50]}" Grid.Column="6" Grid.Row="6" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock56" DataContext="{Binding Points[51]}" Grid.Column="7" Grid.Row="6" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock57" DataContext="{Binding Points[52]}" Grid.Column="8" Grid.Row="6" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock58" DataContext="{Binding Points[53]}" Grid.Column="9" Grid.Row="6" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock60" DataContext="{Binding Points[54]}" Grid.Column="1" Grid.Row="7" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock61" DataContext="{Binding Points[55]}" Grid.Column="2" Grid.Row="7" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock62" DataContext="{Binding Points[56]}" Grid.Column="3" Grid.Row="7" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock63" DataContext="{Binding Points[57]}" Grid.Column="4" Grid.Row="7" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock64" DataContext="{Binding Points[58]}" Grid.Column="5" Grid.Row="7" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock65" DataContext="{Binding Points[59]}" Grid.Column="6" Grid.Row="7" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock66" DataContext="{Binding Points[60]}" Grid.Column="7" Grid.Row="7" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock67" DataContext="{Binding Points[61]}" Grid.Column="8" Grid.Row="7" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock68" DataContext="{Binding Points[62]}" Grid.Column="9" Grid.Row="7" Style="{StaticResource textBlockStyle}"/>

        <TextBlock x:Name="textBlock70" DataContext="{Binding Points[63]}" Grid.Column="1" Grid.Row="8" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock71" DataContext="{Binding Points[64]}" Grid.Column="2" Grid.Row="8" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock72" DataContext="{Binding Points[65]}" Grid.Column="3" Grid.Row="8" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock73" DataContext="{Binding Points[66]}" Grid.Column="4" Grid.Row="8" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock74" DataContext="{Binding Points[67]}" Grid.Column="5" Grid.Row="8" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock75" DataContext="{Binding Points[68]}" Grid.Column="6" Grid.Row="8" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock76" DataContext="{Binding Points[69]}" Grid.Column="7" Grid.Row="8" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock77" DataContext="{Binding Points[70]}" Grid.Column="8" Grid.Row="8" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock78" DataContext="{Binding Points[71]}" Grid.Column="9" Grid.Row="8" Style="{StaticResource textBlockStyle}"/>

        <TextBlock x:Name="textBlock80" DataContext="{Binding Points[72]}" Grid.Column="1" Grid.Row="9" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock81" DataContext="{Binding Points[73]}" Grid.Column="2" Grid.Row="9" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock82" DataContext="{Binding Points[74]}" Grid.Column="3" Grid.Row="9" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock83" DataContext="{Binding Points[75]}" Grid.Column="4" Grid.Row="9" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock84" DataContext="{Binding Points[76]}" Grid.Column="5" Grid.Row="9" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock85" DataContext="{Binding Points[77]}" Grid.Column="6" Grid.Row="9" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock86" DataContext="{Binding Points[78]}" Grid.Column="7" Grid.Row="9" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock87" DataContext="{Binding Points[79]}" Grid.Column="8" Grid.Row="9" Style="{StaticResource textBlockStyle}"/>
        <TextBlock x:Name="textBlock88" DataContext="{Binding Points[80]}" Grid.Column="9" Grid.Row="9" Style="{StaticResource textBlockStyle}"/>

    </Grid>

en cuanto a la clase generadora del Sudoku, os dejo el código de generación, si alguien lo necesita, que no dude en solicitarme el proyecto que gustosamente se lo pasaré.

Os dejo un gif de como funciona. Un saludos amig@s

sudoku06.gif

Pd: El espacio de nombres no tiene nada que ver con los sudokus, pero a cada proyecto de visual studio, le asigno un nombre de un ilustre matemático o físico y en este caso le ha tocado a Ramanujan.

/* ****************************************************************
* © JOAQUIN MARTINEZ RUS 2016
* PROYECTO:        Clase generadora de Sudoku
* Archivo:         SudokuManager.cs
* Descripción:     Clase SudokuManager.
 *
* Historial:
*                  1. 09 sep 2016. Creación
*
* Comentarios:      Genera un sudoku mediante iteraciones erróneas
*
*
*******************************************************************/
using System;
using System.Collections.Generic;
using System.Linq;
using System.ComponentModel;

namespace Ramanujan
{
    public class SudokuManager : INotifyPropertyChanged
    {
        #region Constructor

        public SudokuManager()
        {
            Points = new List();
        }

        #endregion

        #region Properties and fields

        public List Points { get; set; }

        List initialCollection = new List();
        List selection = new List();
        public bool IsGood { get; set; }
        private int iterations;
        public int Iterations
        {
            get { return iterations; }
            set
            {
                iterations = value;
                OnPropertyChanged("Iterations");
            }
        }
        private double miliseconds;

        public double Miliseconds
        {
            get { return miliseconds; }
            set
            {
                miliseconds = value;
                OnPropertyChanged("Miliseconds");
                OnPropertyChanged("Points");
            }
        }

        #endregion

        #region Events

        public event PropertyChangedEventHandler PropertyChanged;

        public void OnPropertyChanged(string _PropertyName)
        {
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs(_PropertyName));
            }
        }

        #endregion

        #region Public methods

        public bool Start()
        {
            reset();
            Iterations++;
            foreach (var item in Points)
            {
                int number = setSelection(item);
                if (number != -1)
                {
                    item.Value = number;
                    item.IsVisible = true;
                }
                else
                {
                    IsGood = false;
                    return IsGood;
                }
            }
            IsGood = true;

            return IsGood;
        }

        public string ToString()
        {
            string textOut = "";
            foreach (var item in Points)
            {
                textOut += item.Value.ToString() + " ";
                if (item.YCoor == 8)
                {
                    textOut += "\n";
                }

            }
            return textOut;
        }

        #endregion

        #region Private Methods

        private void reset()
        {
            initialCollection.Clear();
            selection.Clear();
            Points.Clear();
            for (int i = 1; i < 10; i++)
            {
                initialCollection.Add(i);
            }

            setCoordinates();
        }

        private void setCoordinates()
        {
            for (int i = 0; i  Points.Where(k => k.YCoor == y).Select(k => k.Value).Contains(n));

            // eliminar los elementos cuya coordenada x sea igual que la del punto
            var xx = Points.Where(k => k.XCoor == x && k.Value != 0);
            selection.RemoveAll(n => Points.Where(k => k.XCoor == x).Select(k => k.Value).Contains(n));

            // eliminar los elementos del cuadrado
            var square = getFirstSquareNumber(Points.IndexOf(p));
            selection.RemoveAll(n => square.Contains(n));

            return getElection(p);
        }

        private KeyValuePair getProbability(int n, Point p)
        {
            int nColExistNumber = 0;
            for (int i = p.YCoor + 1; i  h.Value).First();
                    if (k == n)
                    {
                        nColExistNumber++;
                    }
                }
            }
            double casosTotales = 9 - p.XCoor;
            double probability = (casosTotales - nColExistNumber) / casosTotales;
            return new KeyValuePair(n, probability);
        }

        private int getElection(Point p)
        {
            List values = new List();
            foreach (var item in selection)
            {
                values.Add(getProbability(item, p));

            }

            List candidatosTemporales = new List();
            foreach (var item in values)
            {
                if (item.Value == 1)
                {
                    candidatosTemporales.Add(item.Key);
                }
                else
                {
                    for (int i = 0; i  0 ? candidatosTemporales[index] : -1;
            return r;
        }

        private List getFirstSquareNumber(int index)
        {
            List numbers = new List();
            int restoX = index % 3;
            int multiplo = (index / 9) % 3;
            int first = index - restoX - 9 * multiplo;
            int nextIndex = 0;

            for (int i = 0; i < 3; i++)
            {
                for (int j = 0; j < 3; j++)
                {
                    nextIndex = first + i * 9 + j;
                    if (Points[nextIndex].Value != 0)
                    {
                        numbers.Add(Points[nextIndex].Value);
                    }
                }
            }
            return numbers;
        }

        #endregion

    }

    public class Point:INotifyPropertyChanged
    {
        public Point(int x, int y)
        {
            XCoor = x;
            YCoor = y;
        }

        public int XCoor { get; set; }
        public int YCoor { get; set; }
        public int Value { get; set; }
        private bool isVisible=true;

        public bool IsVisible
        {
            get { return isVisible; }
            set
            {
                isVisible = value;
                OnPropertyChanged("IsVisible");
            }
        }

        public int Square
        {
            get
            {
                int x = XCoor % 3;
                int y = YCoor % 3;
                return y + 3 * x ;
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        public void OnPropertyChanged(string _PropertyName)
        {
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs(_PropertyName));
            }
        }

    }
}

Cartón de bingo

Cartón de bingo

Hoy toca la composición y como no teclear código con WPF.Digo esto porque no pretendo explicar las diferencias entre composición y agregación, lo que está claro es que la composición quiere decir que se compone, un coche se compone de motor, ruedas, chasis, etc y el coche no es nada sin estas piezas y estas piezas, podrían serlo por su cuenta, pero fueron diseñadas para funcionar en el coche, por tanto aplicándolo al cartón de bingo (que no tiene nada que ver con el coche en cuanto a fisonomía pero si a composición), este se compone de líneas y cada línea de números. Los números tienen aplicación en la vida, pero un cartón sin estos poco valdría y por supuesto un cartón sin líneas, ¿que cantaríamos? números! no, el bingo tiene sus reglas y ahí estamos los programadores para aplicar las reglas al software y llevarlas a cabo.

Si realizamos un diagrama de clases (simplificado) de un cartón de bingo, sería algo como esto

bingo01

La línea que une las clases o la asociación con el rombo relleno de negro, indica eso, que es una composición.

Sabiendo esto, vamos a diseñar un cartón de bingo más en serio y crearemos

bingo02.png

Esto es lo que se me ha ocurrido (ahora solo el diagrama), un objeto Number, el cual tiene dos propiedades, una el valor y otra si el número está marcado, un objeto Row (Línea) que contiene una colección de números y un objeto Card que contiene una colección de líneas. Por partes.

La clase Number, como ya he dicho tiene dos propiedades y además tiene dos constructores y un solo método; los constructores uno sin parámetros y otro para asignarle el número. Como la clase implementa la interfaz Icomparable<Number>, obligatoriamente el método CompareTo debemos crearlo para que posteriormente pueda ordenarse la lista de números como nosotros queremos. Y visto esto, aquí el código

    public class Number:IComparable<Number>
    {
        #region Constructor

        /// <summary>
        /// Representa un número de cartón de bingo
        /// </summary>
        public Number() { }

        /// <summary>
        /// Representa un número de cartón de bingo
        /// </summary>
        /// <param name="number">Número que se asigna como valor</param>
        public Number(int number)
        {
            this.Value = number;
        }

        #endregion

        #region Fields, properties and constants

        /// <summary>
        /// Valor del número
        /// </summary>
        public int Value { get; set; }

        /// <summary>
        /// Indica si el número está marcado o no
        /// </summary>
        public bool IsChecked { get; set; } = false;
        
        #endregion

        #region Public Methods

        /// <summary>
        /// Compara esta instancia con un objeto específico y retorna un entero que indica
        /// </summary>
        /// <param name="other">Objeto a comparar</param>
        /// <returns>Retorna un entero con signo. Negativo si es menor, un 0 si es igual y un entero positivo si es mayor.</returns>
        public int CompareTo(Number other)
        {
            int result = 0;
            if (this.Value > other.Value)
            {
                result = 1;
            }
            else if (this.Value < other.Value)
            {
                result = -1;
            }
            return result;
        }

        #endregion
    }

la clase Row o la clase de la línea le he incluido dos constructores, uno que que genera una línea en base a una lista de números disponibles, esto es porque a la línea le interesa saber que números hay que escoger para que no se repitan y el otro al que le pasamos los números en un array como parámetro.

Tiene una constante que indica la cantidad de números que tiene una línea y una colección ¿de que?, pues de la clase Number. El motor de la clase o los métodos privados, son dos, uno para obtener una lista de decenas y otro para crear la línea con sus números, los cuales no deben repetir decena ni repetir números de otras líneas. Para esto creo una lista de las decenas, 0, 10, 20, … 70 y 80, instancio la clase Random con una semilla sobre la suma de los milisegundos del tiempo actual y las decenas pendientes, esto provoca una semilla distinta en cada iteración y por tanto algo más parecido a la aleatoriedad, además le incluyo un número impar de milisegundos de retardo. Esto también nos va a generar cartones más reales y con una distribución más normal, imaginad un cartón con los 15 números agrupados desde las unidades a la columna de 40 y el resto de columnas (50, 60, 70 y 80) vacías, esos cartones son válidos, pero poca gente los querría, es como si nos dieran un billete de lotería con el 11111, está en el bombo, pero poca gente lo quiere. Después de esto,

  • Sobre la lista de decenas escojo una al azar,
  • Filtro los números disponibles para acotarlos en esa decena y vuelvo a escoger uno aleatorio
  • Elimino el número escogido de la lista de números disponibles (para que en el siguiente número no lo esté)
  • Añado el número a la colección de la línea
  • Ordeno la línea.

con esto, cada vez que genero una línea, me aseguro que no se repitan las decenas y los números por línea.

Los métodos públicos, son:

  • ToString(). Para devolver una cadena con los números de la línea, sustituyendo los espacios por asteriscos (este lo he ppuesto por si alguien lo quiere obtener por consola)
  • GetRowNumbersToString(). Este igual que el anterior, pero con la diferencia de que solo aparecen los números separados por tabulaciones (Cabe destacar el uso de ForEach en una sola línea)
  • GetRowNumber(). Que devuelve una lista de objetos Number con todos los números de la línea.

El código…

    public class Row
    {
        #region Constructor

        /// <summary>
        /// Representa una línea de cartón de bingo
        /// </summary>
        /// <param name="_availableNumbers">Lista de números disponibles desde la cual se generará los objetos Number</param>
        public Row(List<int> _availableNumbers)
        {
            CreateRow(_availableNumbers);
        }

        /// <summary>
        /// Representa una línea de cartón de bingo
        /// </summary>
        /// <param name="args">Array con los números que se usarán para generar los objetos Number</param>
        public Row(int[] args)
        {
            if (args.Length > numbersCount) throw new OverflowException(String.Format("El número máximo de números que acepta una línea es de {0}", numbersCount));
            foreach (var item in args)
            {
                Number number = new Number();
                number.Value = item;
                rowNumbers.Add(number);
            }

            rowNumbers.Sort();
        }

        #endregion

        #region Fields, properties and constants

        /// <summary>
        /// Constante que indica la cantida de números de una linea
        /// </summary>
        private const int numbersCount = 5;

        /// <summary>
        /// Lista de objetos Number
        /// </summary>
        public List<Number> rowNumbers = new List<Number>();

        #endregion

        #region Public Methods

        /// <summary>
        /// Retorna una cadena formateada por decenas con los valores de los objetos Number contenidos en esta instancia
        /// </summary>
        /// <returns>Cadena de texto con los valores de los objetos Number</returns>
        public override string ToString()
        {
            string textToString = "";

            for (int i = 0; i < 9; i++)
            {
                int decena = i * 10;
                int incremento = i == 8 ? decena + 11 : decena + 10;
                var query = this.rowNumbers.Where(k => k.Value > decena && k.Value < incremento);

                if (query.Count() > 0)
                {
                    textToString += query.First().Value + "\t";
                }
                else
                {
                    textToString += "*\t";
                }

            }

            return textToString;
        }

        /// <summary>
        /// Retorna una cadena con los valores  de los objetos Number contenidos en esta instancia
        /// </summary>
        /// <returns>Cadena de texto con los valores de los objetos Number</returns>
        public string GetRowNumbersToString()
        {
            string textToString = "";
            this.rowNumbers.ForEach(k => textToString += k.Value + "\t");
            return textToString;
        }

        public List<Number> GetRowNumber()
        {
            List<Number> numbers = new List<Number>();

            for (int i = 0; i < 9; i++)
            {
                int decena = i * 10;
                int incremento = i == 8 ? decena + 11 : decena + 10;
                var query = this.rowNumbers.Where(k => k.Value > decena && k.Value < incremento);

                if (query.Count() > 0)
                {
                    numbers.Add(query.First());
                }
                else
                {
                    numbers.Add(new Number(-1));
                }

            }

            return numbers;
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Inicia la instancia actual con objetos Number a partir de una lista de números disponibles
        /// </summary>
        /// <param name="_availableNumbers">Lista de números disponibles desde la cual se generará los objetos Number</param>
        private void CreateRow(List<int> _availableNumbers)
        {
            List<int> decenas = GetTen();
            int number = 0;
            Random rnd=new Random(DateTime.Now.Millisecond + _availableNumbers.Count);
            for (int i = 0; i < numbersCount; i++)
            {
                System.Threading.Thread.Sleep(3);
                int decena = decenas[rnd.Next(0, decenas.Count)];
                
                // Eliminar la decena de la lista para no solicitarla en la próxima petición
                decenas.Remove(decena);

                int incremento = decena == 80 ? decena + 11 : decena + 10;
                var temp = _availableNumbers.Where(k => k > decena && k < incremento);
                number = temp.ElementAt(rnd.Next(0, temp.Count()));
                _availableNumbers.Remove(number);

                rowNumbers.Add(new Number(number));
            }

            this.rowNumbers.Sort();
        }

        /// <summary>
        /// Retorna una lista con las decenas desde 0 al 80
        /// </summary>
        /// <returns></returns>
        private List<int> GetTen()
        {
            List<int> decenas = new List<int>();
            for (int i = 0; i < 9; i++)
            {
                decenas.Add(i*10);
            }
            return decenas;
        }

        #endregion
    }

Por último, la clase Card (Cartón, no ibas a pensar que algo tan sencillo como un cartón iba a tener tanta complejidad). Esta realiza lo siguiente:

  • Rellena la lista de números disponibles, los números del 1 al 90.
  • Crea un nuevo cartón, el cual instancia tres veces la clase Row,envíandole la lista de números disponibles, si en la primera línea se han añadido el 7, 23, 35,67,77 y 89, estos números una vez que se ha creado la fila, se eliminan de la lista de disponibilidad, esto lo hago para seleccionar solo los números que quedan y aumentar el rendimiento; os imagináis escoger un número del 1 al 90 y solo queda el número 43, hasta que acierte la aplicación puede pasar un tiempo no computado, mientras que si vamos extrayendo números, el tiempo siempre será el mismo
  • Llena una lista de cadenas de texto con los números del cartón con el fin de usarlos para la visualización.

Además, he implementado la interfaz IEquatable<Card> para en el futuro por si hubiera que comprobar un cartón, poder hacerlo comparando el cartón ganador. el código de la clase lo muestro a continuación (si teneís alguna duda con el código, no dudéis en preguntame y añadir un comentario, lo responderé gustosamente)

    public class Card: IEquatable<Card>,INotifyPropertyChanged
    {
        #region Constructor

        /// <summary>
        /// Representa un cartón de bingo con 3 líneas y 5 números por línea
        /// </summary>
        public Card()
        {
            FillAvailableNumbers();
            CreateNewCard();
            cardNumbers = new List<string>();

            foreach (var item in GetRowNumber().Select(k => k.Value))
            {
                cardNumbers.Add(item==-1?"":item.ToString());
            }
        }

        #endregion

        #region Fields, properties and constants

        /// <summary>
        /// Número de líneas que alberga un cartón
        /// </summary>
        private const int rowCount = 3;

        /// <summary>
        /// Lista de Líneas del cartón
        /// </summary>
        List<Row> cardRows = new List<Row>();

        /// <summary>
        /// Lista de números disponibles que se pueden agregar a un cartón 
        /// </summary>
        List<int> availableNumbers = new List<int>();

        /// <summary>
        /// Listado de número ordenados por filas para visualización
        /// </summary>
        public List<string> cardNumbers { get; set; }

        public event PropertyChangedEventHandler PropertyChanged;

        public int CardNumber { get; set; }
        public int SerialNumber { get; set; }

        #endregion

        #region Public Methods

        /// <summary>
        /// Retorna una cadena con los datos del cartón formateados
        /// </summary>
        /// <returns>Cadena detexto con los datos del cartón</returns>
        public override string ToString()
        {
            string textToString = "";

            foreach (var row in this.cardRows)
            {
                textToString += row.ToString() + "\n";
            }

            textToString += "\n";
            foreach (var row in this.cardRows)
            {
                textToString += row.GetRowNumbersToString() + "\n";
            }

            return textToString;
        }

        /// <summary>
        /// Retorna un valor que indica que esta instancia es igual a un objeto específico
        /// </summary>
        /// <param name="other">Un objeto Card a comparar con esta instancia</param>
        /// <returns>True si tiene el mismo valor que esta instancia; en caso contrario false</returns>
        public bool Equals(Card other)
        {
            bool isEquals = true;
            List<int> cardNumbersThis = GetCardNumbers(this);
            List<int> cardNumbersOther = GetCardNumbers(other);

            cardNumbersThis.ForEach(k => cardNumbersOther.Remove(k));

            if (cardNumbersOther.Count > 0) isEquals = false;

            return isEquals;
        }

        /// <summary>
        /// Retorna una lista con los números ordenados de cada fila y asignados los huecos con el valor -1
        /// </summary>
        /// <returns></returns>
        public List<Number> GetRowNumber()
        {
            List<Number> numbers = new List<Number>();

            foreach (var row in this.cardRows)
            {
                numbers.AddRange(row.GetRowNumber());
            }

            return numbers;
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Retorna una lista de todos los números del cartón, indicando los elementos vacíos con -1
        /// </summary>
        /// <param name="card">Cartón de bingo del que se extraen los datos</param>
        /// <returns>Lista de enteros con todos los números del cartón. A los elementos vacíos, se les asigna -1</returns>
        private List<int> GetCardNumbers(Card card)
        {
            List<int> cardNumbers = new List<int>();

            foreach (var row in card.cardRows)
            {
                cardNumbers.AddRange(row.rowNumbers.Select(k => k.Value));
            }

            return cardNumbers;
        }

        /// <summary>
        /// crea las tres líneas y las asigna al cartón
        /// </summary>
        private void CreateNewCard()
        {

            for (int i = 0; i < rowCount; i++)
            {
                Row row = new Row(availableNumbers);

                foreach (var item in row.rowNumbers)
                {
                    availableNumbers.Remove(item.Value);
                }

                cardRows.Add(row);
            }
        }

        /// <summary>
        /// Rellena los números disponibles para generar cartones
        /// </summary>
        private void FillAvailableNumbers()
        {
            for (int i = 1; i < 91; i++)
            {
                availableNumbers.Add(i);
            }
        }

        #endregion

    }

Y ahora  vamos a diseñar el cartón con WPF sin escribir código C#, XAML si, pero este se encarga de los errores y la robustez.

Creo un control de usuario y le añado lo siguiente

<UserControl x:Class="Bingo.CardForm"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:Pascal"
             mc:Ignorable="d" Height="250" Width="650">
    <Grid x:Name="CardGrid">
        <Grid.Resources>
            <Style x:Key="baseStyle" TargetType="TextBlock">
                <Setter Property="TextAlignment" Value="Center"/>
                <Setter Property="VerticalAlignment" Value="Stretch"/>
                <Setter Property="HorizontalAlignment" Value="Stretch"/>
                <Setter Property="Margin" Value="3"/>
                <Setter Property="FontSize" Value="14"/>
            </Style>
            <Style x:Key="numberStyle" TargetType="TextBlock" BasedOn="{StaticResource baseStyle}">
                <Setter Property="FontSize" Value="40"/>
                <Setter Property="Foreground" Value="DarkGray"/>
                <Setter Property="Background" Value="LightGray"/>
            </Style>
        </Grid.Resources>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="10"/>
            <ColumnDefinition Width="70"/>
            <ColumnDefinition Width="70"/>
            <ColumnDefinition Width="70"/>
            <ColumnDefinition Width="70"/>
            <ColumnDefinition Width="70"/>
            <ColumnDefinition Width="70"/>
            <ColumnDefinition Width="70"/>
            <ColumnDefinition Width="70"/>
            <ColumnDefinition Width="70"/>
            <ColumnDefinition Width="10"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="30"/>
            <RowDefinition Height="70"/>
            <RowDefinition Height="70"/>
            <RowDefinition Height="70"/>
            <RowDefinition Height="10"/>
        </Grid.RowDefinitions>
        <TextBlock Text="Cartón Nº:" Grid.Column="1" Grid.Row="0" HorizontalAlignment="Right" Style="{StaticResource baseStyle}"/>
        <TextBlock x:Name="CardNumber" Text="{Binding CardNumber}" Grid.Column="2" Grid.Row="0" Style="{StaticResource baseStyle}"/>
        <TextBlock Text="Serie:" Grid.Column="4" Grid.Row="0" HorizontalAlignment="Right" Style="{StaticResource baseStyle}"/>
        <TextBlock x:Name="SerialNumber" Text="{Binding SerialNumber}" Grid.Column="5" Grid.Row="0" Style="{StaticResource baseStyle}"/>

        <TextBlock x:Name="Row11" Text="{Binding cardNumbers[0]}" Grid.Column="1" Grid.Row="1" Style="{StaticResource numberStyle}"/>
        <TextBlock x:Name="Row12" Text="{Binding cardNumbers[1]}" Grid.Column="2" Grid.Row="1" Style="{StaticResource numberStyle}"/>
        <TextBlock x:Name="Row13" Text="{Binding cardNumbers[2]}" Grid.Column="3" Grid.Row="1" Style="{StaticResource numberStyle}"/>
        <TextBlock x:Name="Row14" Text="{Binding cardNumbers[3]}" Grid.Column="4" Grid.Row="1" Style="{StaticResource numberStyle}"/>
        <TextBlock x:Name="Row15" Text="{Binding cardNumbers[4]}" Grid.Column="5" Grid.Row="1" Style="{StaticResource numberStyle}"/>
        <TextBlock x:Name="Row16" Text="{Binding cardNumbers[5]}" Grid.Column="6" Grid.Row="1" Style="{StaticResource numberStyle}"/>
        <TextBlock x:Name="Row17" Text="{Binding cardNumbers[6]}" Grid.Column="7" Grid.Row="1" Style="{StaticResource numberStyle}"/>
        <TextBlock x:Name="Row18" Text="{Binding cardNumbers[7]}" Grid.Column="8" Grid.Row="1" Style="{StaticResource numberStyle}"/>
        <TextBlock x:Name="Row19" Text="{Binding cardNumbers[8]}" Grid.Column="9" Grid.Row="1" Style="{StaticResource numberStyle}"/>
        <TextBlock x:Name="Row21" Text="{Binding cardNumbers[9]}" Grid.Column="1" Grid.Row="2" Style="{StaticResource numberStyle}"/>
        <TextBlock x:Name="Row22" Text="{Binding cardNumbers[10]}" Grid.Column="2" Grid.Row="2" Style="{StaticResource numberStyle}"/>
        <TextBlock x:Name="Row23" Text="{Binding cardNumbers[11]}" Grid.Column="3" Grid.Row="2" Style="{StaticResource numberStyle}"/>
        <TextBlock x:Name="Row24" Text="{Binding cardNumbers[12]}" Grid.Column="4" Grid.Row="2" Style="{StaticResource numberStyle}"/>
        <TextBlock x:Name="Row25" Text="{Binding cardNumbers[13]}" Grid.Column="5" Grid.Row="2" Style="{StaticResource numberStyle}"/>
        <TextBlock x:Name="Row26" Text="{Binding cardNumbers[14]}" Grid.Column="6" Grid.Row="2" Style="{StaticResource numberStyle}"/>
        <TextBlock x:Name="Row27" Text="{Binding cardNumbers[15]}" Grid.Column="7" Grid.Row="2" Style="{StaticResource numberStyle}"/>
        <TextBlock x:Name="Row28" Text="{Binding cardNumbers[16]}" Grid.Column="8" Grid.Row="2" Style="{StaticResource numberStyle}"/>
        <TextBlock x:Name="Row29" Text="{Binding cardNumbers[17]}" Grid.Column="9" Grid.Row="2" Style="{StaticResource numberStyle}"/>
        <TextBlock x:Name="Row31" Text="{Binding cardNumbers[18]}" Grid.Column="1" Grid.Row="3" Style="{StaticResource numberStyle}"/>
        <TextBlock x:Name="Row32" Text="{Binding cardNumbers[19]}" Grid.Column="2" Grid.Row="3" Style="{StaticResource numberStyle}"/>
        <TextBlock x:Name="Row33" Text="{Binding cardNumbers[20]}" Grid.Column="3" Grid.Row="3" Style="{StaticResource numberStyle}"/>
        <TextBlock x:Name="Row34" Text="{Binding cardNumbers[21]}" Grid.Column="4" Grid.Row="3" Style="{StaticResource numberStyle}"/>
        <TextBlock x:Name="Row35" Text="{Binding cardNumbers[22]}" Grid.Column="5" Grid.Row="3" Style="{StaticResource numberStyle}"/>
        <TextBlock x:Name="Row36" Text="{Binding cardNumbers[23]}" Grid.Column="6" Grid.Row="3" Style="{StaticResource numberStyle}"/>
        <TextBlock x:Name="Row37" Text="{Binding cardNumbers[24]}" Grid.Column="7" Grid.Row="3" Style="{StaticResource numberStyle}"/>
        <TextBlock x:Name="Row38" Text="{Binding cardNumbers[25]}" Grid.Column="8" Grid.Row="3" Style="{StaticResource numberStyle}"/>
        <TextBlock x:Name="Row39" Text="{Binding cardNumbers[26]}" Grid.Column="9" Grid.Row="3" Style="{StaticResource numberStyle}"/>
    </Grid>
</UserControl>

El código XAML del formulario principal

   
<Window x:Class="Bingo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Bingo"
        mc:Ignorable="d"
        Title="MainWindow" Height="401.567" Width="812.461">
    <Grid>
        <local:CardForm x:Name="cardform"/>
        <Button x:Name="button" Content="Generar" HorizontalAlignment="Left" Height="38" Margin="591,306,0,0" VerticalAlignment="Top" Width="136" Click="button_Click"/>
    </Grid>
</Window>

en este bloque de código, debemos fijarnos en como estamos declarando el control de usuario que hemos creado y que ahora estamos insertando en el formulario principal; primero declaramos el espacio clr-namespace:Bingo con la key local (línea 6) y luego llamamos al control desde esta key que hace referencia al espacio de nombres donde se encuentra el control, si no hacemos esto, jamás podremos declarar el control de usuario dentro de la ventana principal, porque no lo encontraría.

y el code behind de este form, solo esto, el resto lo hace WPF. En este bloque cabe destacar que los único que hacemos desde la ventana principal es instanciar un objeto Card (Cartón) y asignar al DataContext del grid del control de usuario, el objeto instanciado card. El botón, lo único que hace es llamar al método CartonNuevo mostrando cada vez que se pulse, un nuevo cartón aleatorio.

namespace Bingo
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        void CartonNuevo()
        {
                Card card = new Card();
                card.CardNumber = 1729;
                card.SerialNumber = 1;
                cardform.CardGrid.DataContext = card;
        }

        private void button_Click(object sender, RoutedEventArgs e)
        {
            CartonNuevo();            
        }
    }
}

Volvemos al código XAML del control de usuario, en el control grid hay varias cosas que ver

  • En el bloque Resources del Grid, creo un Style para los Textblock llamado baseStyle
  • Luego creo otro estilo llamado numberStyle que hereda de baseStyle y modifica algunas propiedades (Está subrayado desde la línea 9 a la 22)
  • En las propiedades Text de los controles Textblock, hacemos un Binding al elemento concreto de la colección List (Text="{Binding cardNumbers[0]}") del objeto Card, al número de cartón y a la serie, de modo que sin ningún código en C#, el formulario nos mostrará el cartón, solo debemos decirle desde el formulario principal, que el DataContext es el objeto instanciado card. (Dejo subrayado como ejemplo el primer TextBlock de la línea 48)

He aquí como quedaría el resultado

bingo03

como siempre, si tenéis alguna duda o pregunta, comentario al canto

Saludos

Sistema de numeración en base primorial

Sistema de numeración en base primorial

En mi otro blog, hace poco estuve hablando de un sistema de posicionamiento mixto como era el sistema de numeración en base primorial, el cual varía en cada posición la base, bueno pues he creado una estructura en la cual se incluye además de la conversión, los operadores de suma, resta, multiplicación y división y los de comparación. Es una estructura sencilla en la que no me he esmerado mucho, pero espero que quede claro el uso de estructuras, interfaces y sobrescritura de operadores. Lo primero de todo, ¿por que una estructura y no una clase?. Antes de nada vamos a enumerar las características de las estructuras:

  • Una estructura hereda de la clase abstracta System.ValueType y esta a su vez de object
  • Una estructura no permite la herencia, solo pueden implementar interfaces
  • Una estructura no puede ser abstracta
  • No permite un constructor sin parámetros y no puede tener destructor.
  • Si contiene campos o propiedades, tienen que ser asignados en el constructor
  • Las variables no pueden ser inicializadas fuera del constructor
  • Es posible crear una estructura sin usar el operador new, en tal caso, no es posible usar la estructura hasta que todos los campos se inicialicen.

¿Para que se utilizan? Para representar objetos de poca monta que no tengan grandes cálculos (también podríamos hacerlo con una clase), pero lo primero, nos aseguramos que se han inicializado todas las variables y campos y en el caso de tener un constructor, contiene un parámetro de entrada como mínimo, además no es necesario instanciar la estructura mediante new. El típico ejemplo de una estructura sería un sistema de coordenadas, como podemos ver en el ejemplo, se asignan los valores en el constructor y podemos tener

    public struct Cooordenadas3D
    {
        public int x, y, z;

        public Cooordenadas3D(int _x, int _y, int _z)
        {
            x = _x;
            y = _y;
            z = _z;
        }

        public override string ToString()
        {
            return "(" + x + ", " + y + ", " + z + ")";
        }

        public double GetModulo()
        {
            //.... return ...
        }
        public double GetAngulo()
        {
            //.... return ...
        }

Y yo con mi sistema de numeración en base primorial lo he hecho, he implementado el sistema mediante una estructura y que vamos a detallar con detenimiento.

Lo primero, hemos dicho que no puede heredar de ninguna clase, pero si puede implementar interfaces. Yo implemento las interfaces IComparable, IComparable<PrimorialBaseNumber>, IEquatable<PrimorialBaseNumber>. La primera y la segunda implementan el método CompareTo que nos hará falta para los operadores y la tercera, que implementa el método Equals que nos hará falta también para los operadores.

He acotado el código en regiones y dentro de estas, explico que hay:

  • Constructor (contructor). En el constructor, le pasamos como parámetro el número decimal que queremos convertir a nuestro sistema y dentro de este inicializamos el array de los primero 45 números primos y ¿por qué 45? esta lista se usará para generar el primorial y el primorial 45 o lo que es lo mismo 199# equivale a 7.7999220416834615532491991063298e+81 que para este ejemplo creo que va bien, al mismo tiempo, este número tan grande, lo he usado como valor máximo del sistema, (por acotar)
  • Constants (constantes). He declarado cinco constantes, de la cuales realmente solo usamos dos, el valor máximo y mínimo (el resto las tengo de otras estructuras y clases y las he dejado). como he comentado, el valor máximo y mínimo, es el valor del primorial de 199 (199#)
  • Properties and Fields (Campos y propiedades). Un array estática con los 45 primeros números primos, un propiedad para almacenar el valor en decimal (DecimalValue) y otra el valor en base primorial (Value). Dentro de la propiedad DecimalValue, se calcula el valor de Value, de modo que si cambiamos el valor del primero, se calcula el del segundo.
  • Public Methods (Métodos públicos).
    • Comparisons (comparaciones). En esta región entran los métodos CompareTo de objeto y del tipo PrimorialBaseNumber, obligatoriamente implementados ya que la interface IComparable, IComparable así lo marcan. Estos devuelven 1 si es mayor, 0 si es igual y -1 si es menor y el método Equals por la interfaz IEquatable<PrimorialBaseNumber>; esta devuelve true si los dos objetos son iguales.
    • Conversions (conversiones). En esta región, tres métodos y uno de ellos sobrecargado. ToString() que devuelve el valor en base primorial, ToDecimal() que devuelve el valor en base decimal y ToPrimorialBase()ToPrimorialBase(decimal _number), el primero hace lo mismo que ToString() pero devuelve un valor tipo decimal y el segundo asigna el valor en base decimal a DecimalValue y en consecuencia lo convierte y devuelve el número convertido.
    • Operators (operadores). Un ejemplo de sobrecarga de los operadores lo expuse en el post de calculadora de números complejos, en el que si yo sumo el objeto complex con otro complex, el compilador no sabe como ejecutar el cálculo, mientras que si sobrecargamos el método, podemos decirle como efectuar esa operación y cada vez que se use, así lo hará. He sobrecargado todos los operadores posibles, para hacerlo lo más genérico posible. Para realizar las operaciones podría haber usado un método más elemental matemáticamente hablando, sumando dígito a dígito, acarreos, etc., pero lo he hecho con las operaciones en base decimal, es más fácil, ¿no?.
    • Static (Estáticos). En esta región incluyo la obtención del primorial mediante un método estático, el cual puede ser accesible desde fuera y dentro de la estructura.
  • Private methods (métodos privados). aquí iría el motor de la estructura, que para el usuario debe estar oculto (tu no ves en tu coche los pistones moverse o las explosiones en los cilindros, ¿verdad?), donde se calculan las conversiones a base decimal, a base primorial.

y ahora el código

/* ************************************************************************************
* © JOAQUIN MARTINEZ RUS 2016
* PROYECTO:         PrimorialBaseNumber
* Nombre:           Mersenne
* Archivo:          PrimorialBaseNumber.cs
* Descripción:      Estructura de un sistema de numeración mixto primorial
* Historial:
*                   1. Joaquin Martínez Rus - 19 ago 2016. Creación
*
* Comentarios:      Contempla la conversión de números en base decimal a base primorial y viceversa,
*                   así como los operadores de suma, resta, multiplicación, división, resto, igualdad,
*                   distinto, complemento y comparativos
*
*
************************************************************************************/
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace System
{
    public struct PrimorialBaseNumber : IComparable, IComparable, IEquatable
    {
        #region Constructor

        public PrimorialBaseNumber(decimal _DecimalNumber)
        {
            // Inicializando todas las propiedades y variables
            PrimorialBaseNumber.primeNumber = new decimal[] {
                2, 3, 5, 7, 11, 13, 17, 19, 23, 29,
                31, 37, 43, 47, 53, 59, 61, 67, 71, 73,
                79, 83, 89, 97, 101, 103, 107, 109, 113, 127,
                131, 137, 139, 149, 151, 157, 163, 167, 173, 179,
                181, 191, 193, 197, 199 };
            Value = 0;
            _decimalValue = 0;
            DecimalValue = _DecimalNumber;

        }

        #endregion

        #region Constants

        public const double MaxValue = 7.7999220416834615532491991063298e+81; // 199#
        public const double MinValue = -7.7999220416834615532491991063298e+81;
        public const double NaN = 0D / 0D;
        public const double NegativeInfinity = -1D / 0D;
        public const double PositiveInfinity = 1D / 0D;

        #endregion

        #region Properties and Fields

        private decimal _decimalValue;

        public decimal DecimalValue
        {
            get { return _decimalValue; }
            set
            {
                _decimalValue = value;
                Value= ConvertToPrimorialBaseNumber(_decimalValue);
            }
        }

        public decimal Value { get; set; }

        public static decimal[] primeNumber { get; set; }

        #endregion

        #region Public Methods

        #region Comparisons

        /// <summary>
        /// Compara esta instancia con un objeto específico y retorna un entero que indica
        /// si el valor es menor que, igual a o mayor que el valor de objeto específico
        /// </summary>

        /// Objeto a comparar a null
        /// Retorna un entero con signo. Negativo si es menor, un 0 si es igual y un entero positivo si es mayor.
        /// Si el valor es null, retorna error
        public int CompareTo(object obj)
        {
            try
            {
                if (obj == null) throw new ArgumentNullException();

                return CompareTo((PrimorialBaseNumber)obj);
            }
            catch (Exception)
            {
                throw new FormatException("Error en la conversión del tipo Object al tipo PrimorialBaseNumber");
            }
        }

        /// <summary>
        /// Compara esta instancia con un objeto específico y retorna un entero que indica
        /// si el valor es menor que, igual a o mayor que el valor de objeto específico
        /// </summary>

        /// Objeto a comparar a null
        /// Retorna un entero con signo. Negativo si es menor, un 0 si es igual y un entero positivo si es mayor.
        /// Si el valor es null, se devuelve NaN
        public int CompareTo(PrimorialBaseNumber obj)
        {
            int value = 0;

            if (DecimalValue &gt; obj.DecimalValue)
            {
                value = 1;
            }
            else if (DecimalValue &lt; obj.DecimalValue)
            {
                value = -1;
            }

            return value;
        }

        /// <summary>
        /// Retorna un valor que indica si esta instancia es igual a un PrimorialBaseNumber específico
        /// </summary>

        /// Un objeto PrimorialBaseNumber a comparar con esta instancia
        /// True si tiene el mismo valor que esta instancia
        public bool Equals(PrimorialBaseNumber other)
        {
            try
            {
                return this == other;
            }
            catch (Exception)
            {

                throw new FormatException();
            }

        }

        /// <summary>
        /// Retorna un valor que indica que esta instancia es igual a un objeto específico
        /// </summary>

        /// Un objeto a comparar con esta instancia
        /// True si es una instancia del tipo PrimorialBaseNumber y tiene el mismo valor que esta instancia
        public override bool Equals(Object other)
        {
            try
            {
                return Equals((PrimorialBaseNumber)other);
            }
            catch (Exception)
            {
                throw new FormatException("Error en la conversión del tipo Object al tipo PrimorialBaseNumber");
            }
        }

        #endregion

        #region Conversions

        /// <summary>
        /// Convierte el valor numérico de esta instancia a su equivalente string
        /// </summary>

        /// El equivalente a string de esta instancia
        public override string ToString()
        {
            return Value.ToString();
        }

        /// <summary>
        /// Obtiene el valor numérico en base decimal
        /// </summary>

        /// El equivalente al número en base decimal
        public decimal ToDecimal()
        {
            return DecimalValue;
        }

        /// <summary>
        /// Obtiene el valor numérico en base primorial
        /// </summary>

        /// El equivalente al número en base primorial
        public decimal ToPrimorialBase()
        {
            return Value;
        }

        /// <summary>
        /// Convierte el número del parámetro en base decimal a base primorial y
        /// retorna el número convertido asignándolo al valor de esta instancia
        /// </summary>

        /// Número en base decimal que se va a convertir a base primorial
        /// Número en base primorial
        public decimal ToPrimorialBase(decimal _number)
        {
            DecimalValue = _number;
            return Value;
        }

        #endregion        

        #region Operators

        /// <summary>
        /// Retorna un valor que indica la suma de ambos términos
        /// </summary>

        /// Primer valor de la suma
        /// Segundo valor de la suma
        /// Valor de la suma de ambos términos en base Primorial
        public static PrimorialBaseNumber operator + (PrimorialBaseNumber left, PrimorialBaseNumber right)
        {
            decimal value = left.DecimalValue + right.DecimalValue;

            if ((double)value &gt; MaxValue || (double)value &lt; MinValue)
            {
                throw new OverflowException(&quot;El valor de la suma es superior o inferior al valor máximo o mínimo respectivamente de la instancia&quot;);
            }
            return new PrimorialBaseNumber(value);
        }

        /// <summary>
        /// Retorna un valor que indica la resta de ambos términos
        /// </summary>

        /// Primer valor de la resta
        /// Segundo valor de la resta
        /// Valor de la resta de ambos términos en base Primorial
        public static PrimorialBaseNumber operator -(PrimorialBaseNumber left, PrimorialBaseNumber right)
        {
            decimal value = left.ConvertToDecimal() - right.ConvertToDecimal();

            if ((double)value &gt; MaxValue || (double)value &lt; MinValue)
            {
                throw new OverflowException(&quot;El valor de la resta es superior o inferior al valor máximo o mínimo respectivamente de la instancia&quot;);
            }
            return new PrimorialBaseNumber(value);
        }

        /// <summary>
        /// Retorna un valor que indica el producto de ambos términos
        /// </summary>

        /// Primer valor del producto
        /// Segundo valor del producto
        /// Valor del producto de ambos términos en base Primorial
        public static PrimorialBaseNumber operator * (PrimorialBaseNumber left, PrimorialBaseNumber right)
        {
            decimal value = left.ConvertToDecimal() * right.ConvertToDecimal();

            if ((double)value &gt; MaxValue || (double)value &lt; MinValue)
            {
                throw new OverflowException(&quot;El valor del producto es superior o inferior al valor máximo o mínimo respectivamente de la instancia&quot;);
            }
            return new PrimorialBaseNumber(value);
        }

        /// <summary>
        /// Retorna un valor que indica la división de ambos términos
        /// </summary>

        /// Primer valor de la división
        /// Segundo valor de la división
        /// Valor de la división de ambos términos en base Primorial
        public static PrimorialBaseNumber operator / (PrimorialBaseNumber left, PrimorialBaseNumber right)
        {
            if (right.ConvertToDecimal()==0)
            {
                throw new DivideByZeroException();
            }

            decimal value = left.ConvertToDecimal() / right.ConvertToDecimal();

            if ((double)value &gt; MaxValue || (double)value &lt; MinValue)
            {
                throw new OverflowException(&quot;El valor de la división es superior o inferior al valor máximo o mínimo respectivamente de la instancia&quot;);
            }
            return new PrimorialBaseNumber(value);
        }

        /// <summary>
        /// Retorna un valor que indica el resto de la división de ambos términos
        /// </summary>

        /// Primer valor de la operación resto
        /// Segundo valor de la operación resto
        /// Valor del resto de la división de ambos términos en base Primorial
        public static PrimorialBaseNumber operator % (PrimorialBaseNumber left, PrimorialBaseNumber right)
        {
            decimal value = left.DecimalValue % right.DecimalValue;
            return new PrimorialBaseNumber(value);
        }

        /// <summary>
        /// Retorna un valor que indica el complemento de esta instancia
        /// </summary>

        /// Valor del número al que se le calcula el complemento
        /// Valor del complemento del número
        public static PrimorialBaseNumber operator !(PrimorialBaseNumber _number)
        {
            int index = 0;
            decimal primorial = 1;
            decimal complemento = 0;
            // Buscar el primer primorial que sea mayor que el número
            do
            {
                primorial = GetPrimorial(index);
                index++;
            } while (primorial &lt; _number.DecimalValue);

            complemento = primorial - _number.DecimalValue;

            return new PrimorialBaseNumber(complemento);
        }

        /// <summary>
        /// Retorna un valor que indica que ambos valores son iguales
        /// </summary>

        /// Primer valor de la comparación
        /// Segundo valor de la comparación&lt;
        /// Verdadero si el valor de la izquierda y el de la derecha son iguales; en otro caso, retorna false
        public static bool operator ==(PrimorialBaseNumber left, PrimorialBaseNumber right)
        {
            bool isEqual = true;

            if (left.ToString().Length!= right.ToString().Length)
            {
                isEqual = false;
            }
            for (int i = 0; i &lt; left.ToString().Length; i++)
            {
                if (left.ToString()[i]!=right.ToString()[i])
                {
                    isEqual = false;
                    break;
                }
            }

            return isEqual;
        }

        /// <summary>
        /// Retorna un valor que indica que ambos valores no son iguales
        /// </summary>

        /// Primer valor de la comparación
        /// Segundo valor de la comparación
        /// Verdadero si el valor de la izquierda y el de la derecha no son iguales; en otro caso, retorna false
        public static bool operator !=(PrimorialBaseNumber left, PrimorialBaseNumber right)
        {
            return !(left==right);
        }

        /// <summary>
        /// Retorna un valor que indica que un valor específico es menor que otro
        /// </summary>

        /// Primer valor de la comparación
        /// Segundo valor de la comparación
        /// Verdadero si el valor de la izquierda es menor que el de la derecha; en otro caso, retorna false
        public static bool operator &lt;(PrimorialBaseNumber left, PrimorialBaseNumber right)
        {
            if (left.CompareTo(right) ==-1)
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        /// <summary>
        /// Retorna un valor que indica que un valor específico es mayor que otro
        /// </summary>

        /// Primer valor de la comparación
        /// Segundo valor de la comparación
        /// Verdadero si el valor de la izquierda es mayor que el de la derecha; en otro caso, retorna false
        public static bool operator &gt;(PrimorialBaseNumber left, PrimorialBaseNumber right)
        {
            if (left.CompareTo(right) == 1)
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        /// <summary>
        /// Retorna un valor que indica que un valor específico es menor o igual que otro
        /// </summary>

        /// Primer valor de la comparación
        /// Segundo valor de la comparación
        /// Verdadero si el valor de la izquierda es menor o igual que el de la derecha; en otro caso, retorna false
        public static bool operator &lt;=(PrimorialBaseNumber left, PrimorialBaseNumber right)
        {
            if (left.CompareTo(right) == -1 || left.CompareTo(right)==0)
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        /// <summary>
        /// Retorna un valor que indica que un valor específico es mayor o igual que otro
        /// </summary>

        /// Primer valor de la comparación
        /// Segundo valor de la comparación
        /// Verdadero si el valor de la izquierda es mayor o igual que el de la derecha; en otro caso, retorna false
        public static bool operator &gt;=(PrimorialBaseNumber left, PrimorialBaseNumber right)
        {
            if (left.CompareTo(right) == 1 || left.CompareTo(right) == 0)
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        #endregion

        #region Static

        /// <summary>
        /// Retorna el valor del primorial de n
        /// </summary>

        /// Valor de n en el conjunto de los números primos
        /// Retorna el valor correspondiente al primorial de n
        public static decimal GetPrimorial(int n)
        {

            if (n &gt; primeNumber.Length)
            {
                throw new OverflowException();
            }

            decimal result = 1;

            for (int i = 0; i &lt; n; i++)
            {
                Decimal prime = primeNumber[i];
                result *= prime;
            }
            return result;
        }

        #endregion

        #endregion

        #region Private Methods

        /// <summary>
        /// Convierte un número en base decimal a base primorial
        /// </summary>

        /// Número que se va a convertir
        /// Valor decimal del número en base primorial
        private decimal ConvertToDecimal(decimal _number)
        {
            IEnumerable arrayToString = _number.ToString().ToCharArray().Reverse();
            Decimal result = 0;
            int contador = 0;
            foreach (var item in arrayToString)
            {
                Decimal primorial = GetPrimorial(contador++);
                int digit = Convert.ToInt32(item.ToString());
                result += digit * primorial;

            }
            return result;
        }

        /// <summary>
        /// Convierte el valor en base decimal de esta instancia a base primorial
        /// y lo asigna al valor en base primorial de esta instancia
        /// </summary>

        /// Valor decimal del número en base primorial
        private decimal ConvertToDecimal()
        {
            return ConvertToDecimal(Value);
        }

        /// <summary>
        /// Convierte un número decimal en base 10 a base primorial
        /// </summary>

        /// Número a convertir
        /// Número en base primorial
        private decimal ConvertToPrimorialBaseNumber(decimal _number)
        {
            StringBuilder result = new StringBuilder();
            decimal dividendo = _number;
            decimal resto = 0;

            int index = 0;

            do
            {
                resto = dividendo % primeNumber[index];
                dividendo = Math.Truncate(dividendo / primeNumber[index]);
                result.Insert(0, resto);
                index++;
            } while (dividendo &gt; 0);

            return Convert.ToDecimal(result.ToString());
        }

        #endregion

    }
}

y para ver como funciona, o creáis un formulario y le añadis la estructura o creáis una aplicación de consola y hacéis lo mismo. Yo he creado un formulario, le he añadido un textbox y le he ido añadiendo los calculos para ver que funciona

            // Iniciando estructuras
            PrimorialBaseNumber pbnn1 = new PrimorialBaseNumber(907);
            PrimorialBaseNumber pbnn2 = new PrimorialBaseNumber(487);            

            textBox.AppendText("487=> " + pbnn1.ToPrimorialBase(487) + "\n");
            textBox.AppendText("907=> " + pbnn1.ToPrimorialBase(907) + "\n");

            textBox.AppendText("suma 487+907 => " + (pbnn1 + pbnn2) + "\n");
            textBox.AppendText("resta 907-487 => " + (pbnn1 - pbnn2) + "\n");
            textBox.AppendText("multi 487*907 =>" + (pbnn1 * pbnn2) + "\n");
            textBox.AppendText("!1544=> " + (2310-1544) + "\n");
            pbnn1.ToPrimorialBase(1544);
            textBox.AppendText("!1544=> " + !pbnn1 + "\n");
            textBox.AppendText("766=> " + !pbnn1 + "\n");

y este el resultado

487=> 22101
907=> 42101
suma 487+907 => 64210
resta 907-487 => 20000
multi 487*907 => 14922301
!1544=> 766
!1544=> 34220
766=> 34220

como siempre, comentad el código y mantenedlo organizado y si tenéis alguna pregunta, no dudéis en dejar un comentario

saludos

Pd: Repito, esta estructura está realizada en una tarde, por lo que entiendo que es muy mejorable y debe abarcar todos las cotas y aspectos, pero para aprender , a mi parecer, está bien, además como mejor se aprende, es picando código!

Nomenclatura en la escritura de código

Nomenclatura en la escritura de código

Existen múltiples aspectos que hay que tener en cuenta en el desarrollo de software com pueda ser la planificación, el diseño de la aplicación, diseño de datos, la estructura de nombres de espacios, estructura de clases, los algoritmos, su eficiencia, la modularidad, por supuesto la abstracción, el encasulapmiento, la herencia, el polimorfismo, la limpieza en el código, el orden, la nomenclatura, etc.En este post, vamos a dedicarnos a ver la nomenclatura en los lenguajes de Java y .NET.

Y, ¿que es la nomenclantura? pues son los procedimientos empleados para nombrar los elementos del código y estos nombres empleados por convención Sun para Java y Microsoft para .NET recomiendan usar. El motivo por el cual debemos usar la convención recomendada nos va a permitir grandes ventajas en el futuro a la hora de modificar, ampliar o simplemente usar nuestras clases.

Para el nombramiento de elementos, utilizaremos un método llamado Camelcase o nomenclatura camello; esto quiere decir que escribiremos las palabras sin espacios y cada vez que se inicia una nueva palabra, se comienza en mayúsculas como por ejemplo, «EstoEsUnaPruebaDeCamelCase». Además, existen dos tipos dependiendo si la primera letra comienza por mayúsculas (UpperCamelCase), o la primera letra comienza por minúsculas (lowerCamelCase).

Nos recomiendan también no usar la notación húngara en la que se nombran los tipos dependiendo de lo que sean añadiéndole un prefijo a cada objeto u otra recomendación es no simplificar o abreviar; si llamo a una propiedad PuedeVerseDeDia, no sería correcto llamarla PuedeVerDia o PVerDia. El procedimiento de nombrar el código de esta manera tan natural como si hablaramos, SalirPorLaPuertaPrincipal, le llamamos nomenclatura Pascal.

De todos estos elementos, también podemos decir que cada uno de estos elementos deben nombrarse del siguiente modo:

  • Espacios de nombres.  Para nombrar un espacio de nombres deberíamos usar un nombre que indique la funcionalidad que proporcionan los tipos incluidos en el espacio de nombres, por ejemplo si tuvieramos una serie de clases que supieran pescar o tuvieran algo que ver con la pesca, mi espacio de nombres se llamaría Waki.Pesca. UpperCamelCase para .NET y lowerCamelCase para Java (en Java se llaman package y agrupan clases.
  • Clases. Para las clases debemos usar nombres que identifiquen el objeto o sintagmas nominales, por ejemplo la clase Persona, Coche, Avion, DireccionIP, Electrodomestico, etc y para el caso de herencia, se aconseja que termine con el nombre de la clase que hereda, de modo que EmpleadoPersona, sería la clase Empleado que hereda de Persona o bien en .NET una clase que hereda de System.EventArgs finalizará con EventArgs o los que heredan de System.Exception que finalizan con Exception. Ahora bien, un objeto List o un ArrayList de la clase Persona debería llamarse personas para indicar que contiene objetos de este tipo aunque también se puede ver acabado con Collection como PersonasCollectionUpperCamelCase para .NET y Java
  • Interfaces. Para las interfaces se usaría el mismo método que las clases pero añadiéndoles una I al inicio del nombre y finalizando con «able» para indicar que un objeto que implementa una interfaz es capaz de realizar las funciones que la interfaz le dice que tiene que hacer. Si tenemos una clase que implementa la interfaz ISpeakable, esta debería hablar porque la interfaz así lo requiere.En .NET puedes encontrar interfaces con la letra I al inicio pero no se les añade able como sufijo como ICloneable e IComparable, IQueryable, simplemente se les trata igual que las clases, pero con la letra I, por ejemplo NotifyPropertyChanged o IAsyncResult. UpperCamelCase para .NET y Java
  • Métodos y funciones. Para los métodos debemos usar verbos o sintagmas verbales, por ejemplo avanzar, volver, venderCoche, añadirArticulo, etc. UpperCamelCase para .NET y lowerCamelCase para Java.
  • Variables y propiedades. Debemos nombralos con un sustantivo, sintagma nominal o un adjetivo, por ejemplo para nombrar la altura de un triángulo, podemos hacerlo con la propiedad Height o Altura, para el radio de una circunferencia Radio, etc. Las propiedades booleanas con un tru o false, es muy recomendable usar Is o Es, Can o Puede y Has o Tiene, por ejemplo si determinamos la propiedad de visibilidad de un componente podemos hacerlo con IsVisible, comprobar si una lista de objetos contiene estos HasObject, o si un objeto puede saltar CanJump Para el caso de .NET es muy recomendable usar propiedades en vez de variables de clase o campos. Las variables privadas comienzan por minúscula y en los parámetros aunque Microsoft no lo recomienda, comienzo los nombres con un guión bajo. si llamo al método Avanzar(50), el método lo escribo como
    
    void Avanzar(double _value){}.
    
    

    UpperCamelCase para .NET y lowerCamelCase para Java.

  • Constantes. Las constantes en Java son nombradas con mayúsculas y los espacios son sustituidos por el guión bajo, como CONSTANTE_GRAVITACIONAL. En .NET se tratan igual que las variables.
  • Eventos. Al igual que los métodos, usaremos verbos o sintagmas verbales pero haciendo alusión al tiempo presente y pasado. Aquí podemos poner como ejemplo de un objeto que salta y el evento se produce, cuando este comienza a saltar genera el evento Saltando o Jumping en ese mismo momento y Saltado o Jumped cuando ha dejado de hacerlo. En .NET si declaramos el evento EventHandler<T> este debería nombrarse acabando en EventHandler, por ejemplo si declaramos un cambio en el texto de una propiedad, podriamos escribir public event EventHandler<EventArgs> ChangedTextEventHandler mientras que para invocar el evento nombraríamos el método de invocación como OnChangedText.
  • Enumeraciones. Las nombraremos como las propiedades pero en plural, por ejemplo si creamos una enumeración de colores, la llamaremos Colores. (que original)

Otra cosa es ¿los escribo en inglés o español? pues como desees, yo escribo mi código en inglés porque es más universal (aunque el español también lo sea, el inglés es más, ya nos gustaría), pero lo que tiene que estar claro es que sigas las recomendaciones. Lo normal es que si creas una clase para que pueda ser usada por otras personas, los miembros públicos de la clase los escribiría en inglés, es decir cuando instancie ese objeto, reconozca y entienda el programador que quiere hacer o a que pertenece; el motor interior o miembros privados ya son cosa tuya, pero si mañana tienes que modificar o ampliar la clase y no has escrito y comentado correctamente el código mal vas a ir.

Bueno pues aún hay más, las bases de datos también tienen su nomenclatura, existen más tipos como delegados, recursos y otros, pero lo principal es el orden y el entendimiento. Espero que os sirva y seguid las recomendaciones.

Un saludo amig@s

Pd: He cambiado mi modo de escribir código muchas veces, pero siempre adaptándolo a las recomendaciones, no he reescrito lo anterior, pero lo nuevo si lo he hecho. Esto es un no parar.

ConsoleTools (2). Java

ConsoleTools (2). Java

En la segunda parte de las herramientas de consola, voy a exponeros un grupo de utilidades y herramientas de uso de archivos y serialización.En el grupo de utilidades disponemos de los siguientes métodos:

  • waiting.Método que realiza una espera controlada; lo utilizo cuando se visualiza un texto en la consola, se realiza una espera y se vuelve al control de programa. Este método contiene varias sobrecargas, una sin parámetros en la que se realzia una espera estándar de 3 segundos, otra con un parámetro de tiempo de espera, una más con tiempo de espera y mensaje inicial en pantalla y por último una con tres, tiempo de espera, mensaje inicial y mensaje final una vez agotado el tiempo. Creo que con estas sobrecargas están cubiertas nuestras necesidades.
        /**
         * Realiza una espera controlada de 3 segundos
         */
        public static void waiting(){
            waiting(3, "", "");
        }
    
        /**
         * Realiza una espera controlada
         * @param seconds Número de segundos de espera
         */
        public static void waiting(long seconds){
            waiting(seconds, "", "");
        }
    
         /**
         * Realiza una espera controlada
         * @param seconds Número de segundos de espera
         * @param initialMessage Mensaje visualizado antes de la espera
         */
        public static void waiting(long seconds, String initialMessage){
            waiting(seconds, initialMessage, "");
        }
    
        /**
         * Realiza una espera controlada
         * @param seconds Número de segundos de espera
         * @param initialMessage Mensaje visualizado antes de la espera
         * @param finalMessage Mensaje visualizado después de la espera
         */
        public static void waiting(long seconds, String initialMessage, String finalMessage){
    
            long initialTime=System.currentTimeMillis();
            long currentTime=0;
    
            System.out.println(initialMessage);
    
            do {
                currentTime=System.currentTimeMillis();
            } while (currentTime-initialTime<seconds*1000);
    
            System.out.println(finalMessage);
        }
    
  • keyDownToContinue. Este método visualiza en pantalla ‘Pulse INTRO para continuar’ y en realidad lo que hace es capturar una entrada vacía que no usa y nos permite continuar.
         /**
         * Solicita una pulsación de la tecla INTRO desde teclado para continuar
         * @throws IOException
         */
        public static void keyDownToContinue() throws IOException{
            Scanner in=new Scanner(System.in);
            System.out.println("Pulse INTRO para continuar...");
            in.nextLine();
        }
    
  • confirmationMessage. Mensaje que se visualiza en pantalla solicitando continuar o no mediante la pulsación de S o N devolviendo true o false.
        /**
         * Visualiza un mensaje a espera de confirmación S/N
         * @param message Mensaje que se visualiza
         * @return True para S o s. False para N o n.
         * @throws IOException
         */
        public static boolean confirmationMessage(String message) throws IOException{
            // Declaración de variables
            BufferedReader keyboardIn=new BufferedReader(new InputStreamReader(System.in));
            String textIn=null;
    
            boolean yes=false;
            boolean boolIn=false;
            System.out.println(message);
            do {
                System.out.println("\n¿Desea continuar?\nIntroduzca S o N");
                textIn = keyboardIn.readLine();
                boolIn=!"S".equals(textIn) && !"s".equals(textIn) && !"N".equals(textIn) && !"n".equals(textIn);
            } while (boolIn);
    
            if (textIn.equals("S") || textIn.equals("s")) {
                yes=true;
            }
    
            return yes;
        }
    
  • clearScreen. En las aplicaciones de consola, no podemos hacer una limpieza de la pantalla como tal, por lo que para hacer este proceso, simplemente añadimos una serie de líneas en blanco. Este método se encuentra swobrecargado dos veces con una inclusión de 20 líneas y otro donde se pueda indicar este número.
         /**
         * Borra la pantalla añadiendo lineas nuevas
         * Valor por defecto 20
         */
        public static void clearScreen(){
            clearScreen(20);
        }
    
        /**
         * Borra la pantalla añadiendo lineas nuevas
         * @param lines Número de líneas que se añadirán
         */
        public static void clearScreen(int lines){
            for (int i = 0; i < lines; i++) {
                System.out.println("");
            }
        }
    
  • getRandomInteger y getRandomDouble. Pocas explicaciones tienen estos métodos. Obtención de un número aleatorio entero u obtención de un número aleatorio doble.
         /**
         * Devuelve un valor aleatorio entre dos números
         * @param m Valor inferior
         * @param n Valor Superior
         * @return Número entero aleatorio
         */
        public static int getRandomInteger(int m, int n){
             return (int)(getRandomDouble(m,n));
        }
    
        /**
         * Devuelve un valor aleatorio entre dos números
         * @param m Valor inferior
         * @param n Valor Superior
         * @return Número double aleatorio
         */
        public static double getRandomDouble(int m, int n){
            double number=(Math.random()*(n-m+1)+m);
            return number;
        }
    
    
  • createMenu. Este método crea un menú en base a un array de texto que será en realidad el que contiene los items del menú. Esto sería el equivalente a un ComboBox con unos items y de los cuales escogemos uno de ellos. Tiene 3 sobrecargas, la primera solo pasa como parámetro el array de texto, la segunda además incluye un texto que se usará como cabecera de menú y por último uno más que añade el caracter utilizado como marco del menú, el cual en su defecto se usa un asterisco ‘*’. Mostramos el código y una muestra de como quedaría.
        /**
         * Crea un menú desde un array de cadenas con asterisco por defecto
         * @param arg Array String. Items del menú
         * @return Cadena de texto con menú de consola
         */
        public static String createMenu(String arg[]){
            return createMenu(arg,"*","");
        }
    
        /**
         * Crea un menú desde un array de cadenas
         * @param arg Array String. Items del menú
         * @param header Cabecera usada antes del menu
         * @return Cadena de texto con menú de consola
         */
        public static String createMenu(String arg[], String header){
            return createMenu(arg,"*",header);
        }
    
        /**
         * Crea un menú desde un array de cadenas
         * @param arg Array String. Items del menú
         * @param stringMenu Caracter utilizado para el marco del menú
         * @param header Cabecera usada antes del menu
         * @return Cadena de texto con menú de consola
         */
        public static String createMenu(String arg [], String stringMarcoMenu, String header){
    
            // Advertencias y validación
            if (stringMarcoMenu.length()>1){
                return "La cadena de menú debe contener solo un carácter.";
            }
            if (arg.length>99) {
                return "La longitud máxima del menú, debe ser inferior a 100";
            }
    
            // Declaración de variables
            int maxLong=0;
            String asterisk="";
            String textOut="";
    
            // Determinar la longitud máxima del texto
            for (int i = 0; i maxLong?arg[i].length():maxLong;
            }
    
            String [] temp=header.split("\\n");
            for (int i = 0; i maxLong?temp[i].length():maxLong;
            }
    
            // Cadena marco del menú
            // Sumo 10 para compensar los espacios, stringMenu, paréntesis y el número
    
            // crear el menú
            for (int i = 0; i < maxLong + 10; i++) {
                asterisk += stringMarcoMenu;
            }
    
            textOut +=asterisk + "\n";
    
            // cabecera si la hay
            textOut += header + dumpChar(" ", maxLong-header.length()) + "\n" + asterisk + "\n";
    
            for (int i = 0; i < arg.length; i++) {
                textOut += stringMarcoMenu + " (" + (i+1) + ") " + arg[i];
                for (int j = arg[i].length(); j <maxLong+3 ; j++) {
                    textOut += " ";
                }
                textOut += stringMarcoMenu+ "\n";
            }
            textOut +=asterisk + "\n";
            return textOut;
        }
    
    

    una muestra de como quedaría el menú del sistema planetario pasándole como parámetro el array String[] menu={"Calcular distancia entre dos objetos", "Calcular atracción gravitacional", "Calcular tiempo (línea recta)", "Listar objetos", "Salir"}; y como texto de cabecera ‘Sistema Solar’.

    
    **********************************************
    Sistema Solar
    **********************************************
    * (1) Calcular distancia entre dos objetos   *
    * (2) Calcular atracción gravitacional       *
    * (3) Calcular tiempo (línea recta)          *
    * (4) Listar objetos                         *
    * (5) Salir                                  *
    **********************************************
    
    Introduzca la opción...

Grupo de herramientas de archivos:

  • createFile.Pasando la ruta de un archivo, crea este.
  • createFolder.Pasando la ruta de la carpeta, crea esta.
  • existFile. Comprueba si un archivo existe en la ruta especificada
    /**
     * Crea un nuevo archivo
     * @param path Ruta del archivo
     * @throws IOException
     */
    public static void createFile(String path) throws IOException{

        try {
            File myFile=new File(path);
            if (myFile.createNewFile())
                System.out.println("El fichero se ha creado correctamente");
            else
                System.out.println("No ha podido ser creado el fichero");
        }
            catch (IOException ioe) {
                    ioe.printStackTrace();
                    }

    }

    /**
     * Crea una nueva carpeta
     * @param path Ruta de la carpeta
     * @throws IOException
     */
    public static void createFolder(String path) throws IOException{
        try
        {
            File myFile=new File(path);
            if (myFile.mkdir())
                System.out.println("La carpeta se ha creado correctamente");
            else
                System.out.println("No ha podido ser creada la carpeta");
        }
        catch (Exception ex){
            System.out.println(ex.getMessage());
        }
    }

    /**
     * Comprueba si existe un archivo
     * @param path Ruta del archivo
     * @return True si existe. False si no existe
     */
    public static boolean existFile(String path){
        File myFile=new File(path);
        return myFile.exists();
    }

Serialización. Por si alguien no sabe lo que es serialización, es guardar en un archivo, en memoria o en una base de datos, un objeto en bytes y deserializar es el proceso inverso, obtener un objeto desde un archivo, memoria o base de datos, de modo que podemos guardar objetos para luego extraerlos y usarlos de nuevo. Por tanto, los métodos de este grupo son serializar y deserializar:

    /**
     * Serializa un objeto en un archivo
     * @param iObject Objeto que se serializará
     * @param filePath Ruta del archivo
     */
    public static void serializar(Object iObject, String filePath){
        FileOutputStream fileOutputStream = null;
        ObjectOutputStream salidaSerializada = null;

        try {
            fileOutputStream = new FileOutputStream(filePath);
            salidaSerializada = new ObjectOutputStream(fileOutputStream);
            salidaSerializada.writeObject(iObject);  

            salidaSerializada.close();
            fileOutputStream.close();

        } catch (FileNotFoundException e) {
            System.out.println(e.getMessage());
        } catch (IOException e) {
            System.out.println(e.getMessage());
        } finally {
            try {
                if(fileOutputStream!=null) fileOutputStream.close();
                if(salidaSerializada!=null) salidaSerializada.close();
            } catch (IOException e) {
                System.out.println(e.getMessage());
            }
        }
    }

    /**
     * Retorna un objeto deserializado desde un archivo
     * @param filePath Ruta del archivo
     * @return Objeto deserializado
     * @throws IOException
     * @throws ClassNotFoundException
     */
    public static Object deserializar(String filePath) throws IOException, ClassNotFoundException{

        FileInputStream fileInputStream = null;
        ObjectInputStream entradaSerializada = null;
	Object iObject=null;
        try {
            fileInputStream=new FileInputStream(filePath);
            entradaSerializada=new ObjectInputStream(fileInputStream);

            iObject=entradaSerializada.readObject();
            entradaSerializada.close();
            fileInputStream.close();
            return iObject;
        } catch (FileNotFoundException e) {
            System.out.println(e.getMessage());
        } catch (IOException e) {
            System.out.println(e.getMessage());
        } finally {
            try {
                if(fileInputStream!=null) fileInputStream.close();
                if(entradaSerializada!=null) entradaSerializada.close();
            } catch (IOException e) {
                System.out.println(e.getMessage());
            }
	    return iObject;
        }
    }

Pues ya que tenemos nuestras herramientas,os muestro el método utilizado para crear el menú en el sistema planetario el cual crea el menú, entra en un bucle do while y dentro de este se encuentra una instrucción switch con las opciones que podría seleccionar el usario y dentro de cada caso, las acciones a realizar, las cuales utilizan muchos de los métodos incluidos en esta clase.

Saludos «der Waki»

    private static void menuPrincipal() throws IOException, ParseException{
        // DECLARACIÓN DE VARIABLES
        String opcionMenu = "";
        String[] menu={"Calcular distancia entre dos objetos", "Calcular atracción gravitacional",
            "Calcular tiempo (línea recta)", "Listar objetos", "Salir"};

        // MENU, INICIO Y FIN
        do {
            System.out.println(ConsoleUtilities.createMenu(menu,sl.nombre));
            opcionMenu = String.valueOf(ConsoleUtilities.readText("Introduzca la opción..."));

            switch (opcionMenu) {
                case "1":
                    // Calcular distancia entre objetos
                    String str1 = ConsoleUtilities.readText("Introduzca el nombre del primer objeto...");
                    String str2="";
                    ObjetoSistema obj1=sl.getObjeto(str1);
                    ObjetoSistema obj2=null;
                    if (obj1==null) {
                        System.out.println("El objeto no existe.");
                    }
                    else {
                        str2 = ConsoleUtilities.readText("Introduzca el nombre del segundo objeto...");
                        obj2=sl.getObjeto(str2);
                        if (obj2==null) {
                            System.out.println("El objeto no existe.");
                        }
                        else {
                            System.out.println(ConsoleUtilities.toNumber(obj1.calcularDistancia(obj2)/1000) + " Km");
                        }
                    }
                    ConsoleUtilities.keyDownToContinue();
                    break;
                case "2":
                    // Calcular atracción gravitacional entre dos objetos
                    String str3 = ConsoleUtilities.readText("Introduzca el nombre del primer objeto...");
                    String str4="";
                    ObjetoSistema obj3=sl.getObjeto(str3);
                    ObjetoSistema obj4=null;
                    if (obj3==null) {
                        System.out.println("El objeto no existe.");
                    }
                    else {
                        str4 = ConsoleUtilities.readText("Introduzca el nombre del segundo objeto...");
                        obj4=sl.getObjeto(str4);
                        if (obj4==null) {
                            System.out.println("El objeto no existe.");
                        }
                        else {
                            double calculo=obj3.getAtraccion(obj4);
                            System.out.println(ConsoleUtilities.toNumber(calculo) + " Newton");
                        }
                    }
                    ConsoleUtilities.keyDownToContinue();
                    break;

                case "3":
                    // Calcular velocidad
                    String str5 = ConsoleUtilities.readText("Introduzca el nombre del primer objeto...");
                    String str6="";
                    ObjetoSistema obj5=sl.getObjeto(str5);
                    ObjetoSistema obj6=null;
                    if (obj5==null) {
                        System.out.println("El objeto no existe.");
                    }
                    else {
                        str6 = ConsoleUtilities.readText("Introduzca el nombre del segundo objeto...");
                        obj6=sl.getObjeto(str6);
                        if (obj6==null) {
                            System.out.println("El objeto no existe.");
                        }
                        else {
                            double calculoKm=obj5.calcularDistancia(obj6)/1000;
                            double velocidad = ConsoleUtilities.readDouble("Introduzca la velocidad km/h (Si pulsa INTRO, se asignará 28000 km/h... ",28000);
                            if (velocidad>0){
                                double tiempo=(calculoKm / velocidad)/(24);

                                System.out.printf("Invertirá para recorrer %s Km a una velocidad de %s Km/h, %s días\n",
                                        ConsoleUtilities.toNumber(calculoKm),ConsoleUtilities.toNumber(velocidad), ConsoleUtilities.convertToTime(tiempo*60*60*24));

                            }
                            else {
                                System.out.println("Velocidad no válida");
                            }
                        }
                    }
                    ConsoleUtilities.keyDownToContinue();
                    break;
                case "4":
                    // Listar objetos
                    menuListado();
                    break;
            }

        } while (!opcionMenu.equals("5"));

        ConsoleUtilities.clearScreen();
        System.out.println("La aplicación ha finalizado");
    }

ConsoleTools (1). Java

ConsoleTools (1). Java

Como ya os comenté en un post anterior, es importante que vayamos creando nuestras clases de código reutilizable, como por ejemplo en este caso os voy a mostrar una clase de herramientas y utilidades que uso para aplicaciones de consola en Java. Como las aplicaciones de consola suelen ser eso, texto puro en consola, a veces es necesario obtener claridad en los textos o en otro caso simplificar acciones que comunmente realizamos, para ello, inlcuyo dentro de la clase una serie de métodos estáticos que me van a permitir crear más fácil y rápido mis aplicaciones.

Dentro de la clase he creado distintos grupos de código, lo primero para aclararme y no repetir métodos o saber que es lo que tengo y en segundo lugar cuando el número de métodos es grande, es conveniente haber ordenado y agrupado nuestro código. A continuación expongo los grupos y método a método.

  • Solicitudes desde teclado. En una aplicación de consola es normal solicitar datos desde teclado ya que no tenemos controles TextBox, ComboBox ni nada por el estilo, por tanto tenemos que ingeniarnos un método fácil y práctico para solicitar información desde el teclado y pasarla a la aplicación. En este caso, he creado varios métodos estáticos y sus sobrecargas para obtener texto, obtener números, textos o fechas.
    • Lectura de texto. En este caso, existen dos métodos de lectura de texto, uno sobrecargado para leer texto y otro para leer varias veces el texto y devolver un array.readText es un método al cual se le pasan dos parámetros, el primero es el texto que se muestra por pantalla al solicitar el texto y el segundo parámetro, un valor booleano que permite si el valor que se captura desde el teclado puede ser un elemento vacío. El método crea un objeto BufferedReader, muestra por pantalla el textoy entra en un bucle do... while hasta obtener el texto. Una vez capturado desde el teclado lo devuelve. readText tiene otro método sobrecargado con un solo parámetro enviando el parámetro allowEmpty con un false. Por último el método readArrayText solicita mediante el método readText tantas cadenas de texto como necesite el usuario devolviendo una array de cadena.( Lo he limitado a 8 cadenas como podía haberlo hecho a 100). Un ejemplo. String stringFromKeyboard = ConsoleUtilities.readText("Introduzca el nombre del usuario...");, solicitando por pantalla el texto del primer parámetro y asignando e la variable stringFromKeyboard el valor obtenido desde el teclado.
          /**
          * Devuelve un valor desde el teclado
          * @param textIn Texto que se visualiza en la consola
          * @return Texto procedente del teclado
          * @throws IOException
          */
          public static String readText(String textIn, boolean allowEmpty) throws IOException{
      
              // Declaración de variables
              BufferedReader keyboardIn=new BufferedReader(new InputStreamReader(System.in));
              String textOut=null;
      
              // Imprimir texto de consola
              System.out.println(textIn);
      
              // Solicitar datos desde teclado
      
              do {
                  textOut=keyboardIn.readLine();
              } while (ConsoleTools.isNullOrEmpty(textOut) &amp;&amp; !allowEmpty);
      
              // Retornar texto
              return textOut;
          }
      
          /**
          * Devuelve un valor desde el teclado
          * @param textIn Texto que se visualiza en la consola
          * @return Texto procedente del teclado
          * @throws IOException
          */
          public static String readText(String textIn) throws IOException{
              return ConsoleTools.readText(textIn, false);
          }
      
          /**
          * Devuelve un array de textos. Limitado a 8 textos
          * @param textIn Texto que se visualiza en la consola
          * @return Array de texto
          */
          public static String[] readArrayText(String textIn) throws IOException{
              int n=0;
              int indexMax=8;
      
              String[] texts=new  String[indexMax];
      
              do {
                  texts[n]=ConsoleTools.readText(textIn);
                  n++;
              } while (ConsoleTools.confirmationMessage("¿Desea introducir más textos?") || n==indexMax);
      
              return texts;
      
          }
      
    • Lectura de números. Para obtener números desde teclado y asegurarnos que estos son lo que son y del tipo que son, uso un método sobrecargado para leer números enteros, otro para enteros positivos y por último otro sobrecargado para leer números con precisión doble. En todos ellos, se validan los datos comprobando que son del tipo que se solicita y que realmente son números y no otro objeto.
      Existe un método sobrecargado para lectura de números enteros, uno que tiene dos parámetros readInteger(String textIn, int defaultValue)pasando una cadena que se visualizará por pantalla y otro que pasará un valor por defecto en caso de no introducir ningún dato y otro método readInteger(String textIn)que solo pasa la cadena. Otro método para enteros es readPositiveInteger(String textIn) el cual además ed validar que es numérico y entero, comprueba que es positivo.
      Para lectura de números con precisión doble, disponemos de dos métodos sobrecargados igualmente que para números enteros.

          /**
          * Devuelve un valor entero desde el teclado
          * @param textIn Texto que se visualiza en la consola
          * @return Número procedente desde el teclado
          */
          public static int readInteger(String textIn) throws IOException{
              // Declaración de variables
              BufferedReader keyboardIn=new BufferedReader(new InputStreamReader(System.in));
              int numberOut=0;
              String numberOutToString=null;
              boolean validate=false;
      
              // Imprimir texto de consola
              System.out.println(textIn );
      
              // Solicitar datos desde teclado
              do {
                  numberOutToString=keyboardIn.readLine();
                  validate=ConsoleTools.isInteger(numberOutToString);
                  // Mensaje de Validación errónea
                  if (!validate) {
                      System.out.println("No es un valor válido. Introduzca un número entero.");
                  }
              } while (!validate);
      
              numberOut=Integer.parseInt(numberOutToString);
              // Retornar número
              return numberOut;
          }
      
          public static int readInteger(String textIn, int defaultValue) throws IOException{
              // Declaración de variables
              BufferedReader keyboardIn=new BufferedReader(new InputStreamReader(System.in));
              int numberOut=0;
              String numberOutToString=null;
              boolean validate=false;
      
              // Imprimir texto de consola
              System.out.println(textIn );
      
              // Solicitar datos desde teclado
              do {
                  numberOutToString=keyboardIn.readLine();
                  validate=ConsoleTools.isInteger(numberOutToString);
      
                  if (!validate) {
                      numberOutToString=String.valueOf(defaultValue);
                      validate=true;
                  }
              } while (!validate);
      
              numberOut=Integer.parseInt(numberOutToString);
              // Retornar número
              return numberOut;
          }
      
          /**
          * Devuelve un valor entero y positivo desde el teclado
          * @param textIn Texto que se visualiza en la consola
          * @return Número procedente desde el teclado
          */
          public static int readPositiveInteger(String textIn) throws IOException{
              // Declaración de variables
              BufferedReader keyboardIn=new BufferedReader(new InputStreamReader(System.in));
              int numberOut=0;
              String numberOutToString=null;
              boolean validate=false;
      
              // Imprimir texto de consola
              System.out.println(textIn );
      
              // Solicitar datos desde teclado
              do {
                  numberOutToString=keyboardIn.readLine();
                  validate=ConsoleTools.isPositiveInteger(numberOutToString);
                  // Mensaje de Validación errónea
                  if (!validate) {
                      System.out.println("No es un valor válido. Introduzca un número entero.");
                  }
              } while (!validate);
      
              numberOut=Integer.parseInt(numberOutToString);
              // Retornar número
              return numberOut;
          }
      
          /**
           * Devuelve un valor entero desde el teclado
           * @param textIn Texto que se visualiza en la consola
           * @return Número procedente desde el teclado
           * @throws java.io.IOException
           */
          public static double readDouble(String textIn) throws IOException{
              // Declaración de variables
              BufferedReader keyboardIn=new BufferedReader(new InputStreamReader(System.in));
              double numberOut=0;
              String numberOutToString=null;
              boolean validate=false;
      
              // Imprimir texto de consola
              System.out.println(textIn );
      
              // Solicitar datos desde teclado
              do {
                  numberOutToString=keyboardIn.readLine();
                  validate=ConsoleTools.isNumeric(numberOutToString);
                  // Mensaje de Validación errónea
                  if (!validate) {
                      System.out.println("No es un valor válido. Introduzca un número con doble precisión.");
                  }
              } while (!validate);
      
              numberOut=Double.parseDouble(numberOutToString);
              // Retornar número
              return numberOut;
          }
      
          /**
           * Devuelve un valor entero desde el teclado
           * @param textIn Texto que se visualiza en la consola
           * @param defaultValue Valor por defecto que se asigna en caso de no validez de entrada
           * @return Número procedente desde el teclado o valor por defecto
           * @throws IOException
           */
          public static double readDouble(String textIn, double defaultValue) throws IOException{
              // Declaración de variables
              BufferedReader keyboardIn=new BufferedReader(new InputStreamReader(System.in));
              double numberOut=0;
              String numberOutToString=null;
              boolean validate=false;
      
              // Imprimir texto de consola
              System.out.println(textIn );
              // Solicitar datos desde teclado
              do {
                  numberOutToString=keyboardIn.readLine();
                  validate=ConsoleTools.isNumeric(numberOutToString);
                  // Mensaje de Validación errónea
                  if (!validate) {
                      numberOutToString=String.valueOf(defaultValue);
                      validate=true;
                  }
              } while (!validate);
      
              numberOut=Double.parseDouble(numberOutToString);
      
              // Retornar número
              return numberOut;
          }
      
    • Lectura de fechas. Para leer fechas, uso un solo método readDate(String textIn) que comprueba que es una fecha y que contiene el formato correcto (dd/MM/yyyy).
      public static Date readDate(String textIn) throws IOException, ParseException{
              // Declaración de variables
              BufferedReader keyboardIn=new BufferedReader(new InputStreamReader(System.in));
              SimpleDateFormat df = new SimpleDateFormat("dd/MM/yyyy");
              boolean exit=false;
              Date dateTemp=new Date();
              String date;
              // Imprimir texto de consola
              System.out.println(textIn);
      
              // Solicitar datos desde teclado
      
              do {
                  date = keyboardIn.readLine();
                  try{
                      dateTemp = df.parse(date);
      
                  } catch (Exception ex){
                  }
      
                  if (!df.format(dateTemp).equals(date)){
                      if (ConsoleTools.confirmationMessage("El formato es inválido, ¿desea introducir una nueva fecha?. " +
                              "En caso contrario, se insertará la fecha actual")) {
                      }
                      else{
                          dateTemp=new Date();
                          exit=true;
                      }
                  } else {
                      exit=true;
                  }
      
              } while (!exit);
      
              // Retornar fecha
              return dateTemp;
          }
      
  • Validación. En este grupo se incluyen métodos de validación que devuelven un valor booleano con true si se cumple la validación o false en caso contrario, como por ejemplo si el valor de una cadena es vacía o nulaboolean isNullOrEmpty(String textIn), si un número es positivo entero boolean isPositiveInteger(String numberIn), si es número entero boolean isInteger(String numberIn), si es un valor númerico doble boolean isInteger(String numberIn) o comprobando si es una fecha es válida boolean isDate(String date).
        /**
         * Comprueba si un texto es vacio o nulo
         * @param textIn Texto de entrada
         * @return True. Si es vació o nulo. False. Si contiene texto
         */
        public static boolean isNullOrEmpty(String textIn){
    
            return textIn=="" || textIn.isEmpty() || textIn==null?true:false;
    
        }
    
        /**
         * Comprueba si un número es entero y positivo
         * @param numberIn. Número de entrada
         * @return
         * True. Si es positivo y entero.
         * False. No es positivo o no es un número entero
         */
        public static boolean isPositiveInteger(String numberIn){
    
            int numberOut=0;
            // Primera validación convirtiendo el número a entero
            try {
                numberOut=Integer.parseInt(numberIn);
            } catch (Exception ex) {
                return false;
            }
            // Segunda validación comprobando que es positivo
            return numberOut&gt;0;
        }
    
        /**
         * Comprueba si un número es entero
         * @param numberIn. Número de entrada
         * @return
         * True. Si es positivo y entero.
         * False. No es positivo o no es un número entero
         */
        public static boolean isInteger(String numberIn){
    
            int numberOut=0;
            // Primera validación convirtiendo el número a entero
            try {
                numberOut=Integer.parseInt(numberIn);
            } catch (Exception ex) {
                return false;
            }
    
            return true;
        }
    
        /**
         * Comprueba si la cadena es un número
         * @param numberIn. Número de entrada
         * @return
         * True. Si es un número.
         * False. No es un número
         */
        public static boolean isNumeric(String numberIn){
    
            double numberOut=0;
            // Primera validación convirtiendo el número a double
            try {
                numberOut=Double.parseDouble(numberIn);
            } catch (Exception ex) {
                return false;
            }
    
            return true;
        }
    
        /**
         * Comprueba si la cadena es una fecha válida
         * @param date Fecha de compración
         * @return
         * True. Si es una fecha.
         * False. No es una fecha
         */
        public static boolean isDate(String date){
            try{
                SimpleDateFormat df = new SimpleDateFormat("dd/MM/yyyy");
                Date dateTemp=new Date();
                dateTemp = df.parse(date);
                return true;
                } catch (Exception ex){
                    return false;
                }
        }
    
  • Formatos y conversiones. En este grupo, se incluyen métodos que permiten mostrar los resultados de nuestras aplicaciones de un modo más legible por los que podemos disponer de un método que convierte un texto o número a un texto con formato de la moneda local String toCurrency(String number), un método que convierte a número con formato pudiendo especificar un número de dígitos String toNumber(double number) o String toNumber(double number, int digits), métodos para rellenar por la izquierda o por la derecha de un caracter String padRight(String text, int number) y String padLeft(String text, int number), un método que retorna una cadena un número de veces String dumpChar(String string, int lenght), un método que añade un borde sobre un texto String addBorder( String text, int longLine), método que convierte una fecha en formato corto String toShortDate(Date date), o convertir un tiempo en segundos en una cadena expresada en segundos, minutos, horas, días, meses y años String convertToTime(double tiempoSegundos).
    /**
         * Convierte un número a texto con formato de moneda.
         * Separador de miles, dos decimales y moneda
         * @param number Número a convertir
         * @return Cadena con formato de moneda
         */
        public static String toCurrency(String number){
            return ConsoleTools.toCurrency(Integer.parseInt(number));
        }
    
        /**
         * Convierte un número a texto con formato de moneda.
         * Separador de miles, dos decimales y moneda
         * @param number Número a convertir
         * @return Cadena con formato de moneda
         */
        public static String toCurrency(int number){
            NumberFormat fn=NumberFormat.getCurrencyInstance();
            return fn.format(number);
        }
    
        /**
         * Convierte un número a texto con formato de moneda.
         * Separador de miles, dos decimales y moneda
         * @param number Número a convertir
         * @return Cadena con formato de moneda
         */
        public static String toCurrency(double number){
            NumberFormat fn=NumberFormat.getCurrencyInstance();
            return fn.format(number);
        }
    
        /**
         * Convierte un número a texto con formato
         * @param number Número a convertir
         * @return Cadena con formato
         */
        public static String toNumber(double number){
            NumberFormat fn=NumberFormat.getNumberInstance();
            return fn.format(number);
        }
    
        /**
         * Convierte un número a texto con formato de número de decimales específico
         * @param number Número a convertir
         * @param digits Número de decimales
         * @return Cadena con formato
         */
        public static String toNumber(double number, int digits){
            NumberFormat fn=NumberFormat.getNumberInstance();
            fn.setMaximumFractionDigits(digits);
            return fn.format(number);
        }
        /**
         * Rellena por la derecha n espacios y
         * recorta el texto cuando es mayor añadiéndole
         * puntos suspensivos
         * @param text Texto formateado
         * @param number Número de espacios
         * @return Cadena de texto formateada
         */
        public static String padRight(String text, int number){
            if (text.length()&gt;number) {
                text=text.substring(0, number-3) + "...";
            }
            return String.format("%1$-" + number + "s", text);
        }
    
        /**
         * Rellena por la izquierda n espacios y
         * recorta el texto cuando es mayor añadiéndole
         * puntos suspensivos
         * @param text Texto formateado
         * @param number Número de espacios
         * @return Cadena de texto formateada
         */
        public static String padLeft(String text, int number){
            if (text.length()&gt;number) {
                text=text.substring(0, number-3) + "...";
            }
            return String.format("%1$" + number + "s", text);
        }
    
        /**
         * Retorna un caracter repetido un número de veces
         * @param string Cadena de texto
         * @param lenght Número de veces que se repite
         * @return Cadena de texto
         */
        public static String dumpChar(String string, int lenght){
            String textOut="";
    
            for (int i = 0; i &lt; lenght; i++) {
                textOut +=string;
            }
            return textOut;
        }
    
         /**
         * Devuelve un borde de texto superior e inferior
         * @param text Texto al que se le añade el borde
         * @param longLine Longitud del borde
         * @return Cadena de texto formateada con borde
         */
        public static String addBorder( String text, int longLine){
            String border = &quot;&quot;;
            border += &quot;+&quot;;
            for (int i = 0; i &lt; longLine; i++) {
                border += &quot;-&quot;;
            }
            border += &quot;+\n&quot;;
    
             return border + text + border;
        }
    
        /**
         * Convierte una fecha en formato corto
         * @param date Fecha a convertir
         * @return Cadena de texto con la fecha
         */
        public static String toShortDate(Date date){
            SimpleDateFormat df=new SimpleDateFormat(&quot;dd/MM/yyyy&quot;);
    
            return df.format(date);
        }
    
         /**
         * Convierte un tiempo en segundos en sg, min, horas, días, meses y años
         * @param tiempoSegundos Tiempo en segundos
         * @return Texto
         */
        public static String convertToTime(double tiempoSegundos){
    
            double segundos =  Math.floor(tiempoSegundos % 60);
            double aux1 =((tiempoSegundos-segundos) / 60);
            double minutos = Math.floor(aux1 % 60);
            aux1 = (aux1-minutos) / (60);
            double horas = Math.floor(aux1 % 24);
            aux1 = (aux1-horas) / 24;
            double dias = Math.floor(aux1 % 30);
            aux1 = (aux1-dias) / 30;
            double meses = Math.floor(aux1 % 12);
            aux1 = (aux1 - meses)/12;
            double años = Math.floor(aux1);
    
            String sAños = años &gt; 0? (años==1?&quot;1 año&quot;:(int)años + &quot; años &quot;):&quot;&quot;;
            String sMes = meses &gt; 0? (meses==1?&quot;1 mes&quot;:(int)meses + &quot; meses &quot;):&quot;&quot;;
            String sDias = dias &gt; 0? (dias==1?&quot;1 día&quot;:(int)dias + &quot; días &quot;):&quot;&quot;;
            String sHoras = horas &gt; 0? (horas==1?&quot;1 hora&quot;:(int)horas + &quot; horas &quot;):&quot;&quot;;
            String sMinutos = minutos &gt; 0? (minutos==1?&quot;1 minuto&quot;:(int)minutos + &quot; minutos &quot;):&quot;&quot;;
            String sSegundos = segundos &gt; 0? (segundos==1?&quot;1 segundo&quot;:(int)segundos + &quot; segundos &quot;):&quot;&quot;;
    
            return sAños + sMes + sDias + sHoras + sMinutos + sSegundos;
        }
    

Como podéis apreciar, pequeños métodos que nos permiten crear aplicaciones mucho más rápido.
En la próxima entrega, algunas utilidades de consola más como un método que crea menús automáticamente.

Un saludo

Calculadora Números Complejos (C# y WPF)

Calculadora Números Complejos (C# y WPF)

Java y C# son lenguajes muy parecidos, de hecho C# deriva de C++, Java y otros lenguajes alimentándose de lo bueno de cada uno; particularmente para mi es el mejor por su potencia y sus increíbles mejoras que nos hacen la programación mucho más fácil.

En el anterior post, describí una calculadora de números complejos en Java y en este, lo implemento con C# y WPF. Con esto intento tres cosas, en primer lugar demostrar que la clase (muy pero que muy similar a la de Java) es invariable porque me daría igual usar una aplicación de consola o gráfica, otra es que podáis ver la diferencia entre lenguajes pero la similitud estructural y por último implementarla mediante WPF con el cual nos vamos a ahorrar mucho código quedando la aplicación muy limpia y robusta y de camino os enseño como se aplican estilos a los controles.

Lo primero vayamos a la clase Complex. Las principales diferencias con la de Java son las que voy a enumerar.

  1. Implemento la interfaz INotifyPropertyChanged de modo que la clase debe contener el evento public event PropertyChangedEventHandler PropertyChanged; el cual invocamos mediante el método OnPropertyChanged(string p_PropertyName) cada vez que cambia la propiedad de la parte real o imaginaria de un número complejo.
  2. En esta clase, he sobrecargado las operaciones básicas de suma, resta, multiplicación, división y negación (a este operador le he asignado el opuesto). Para ver como lo he hecho, os paso el código de la operación suma con el que con un método
            public static Complex operator + (Complex _complex1, Complex _complex2)
            {
                return new Complex(_complex1.Real + _complex2.Real, _complex1.Imaginary + _complex2.Imaginary);
            }
    

    permitiéndome efectuar sumas sobre dos complejos.

A continuación paso el código completo de la clase.

/*************************************************************************************************************************************************
* © JOAQUIN MARTINEZ RUS 2015
* PROYECTO:        ComplexCalc. Calculadora de números complejos
* Archivo:         Complex.cs
* Descripción:     Clase de números complejos
* Historial:
*                  1. Joaquin Martínez Rus - 18 jul 2016. Creación
*
* Comentarios:
*
*
**************************************************************************************************************************************************/

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ComplexCal
{
    public class Complex: INotifyPropertyChanged
    {
        #region Constructor

        public Complex(double _real=0, double _imaginary=0)
        {
            this.Real = _real;
            this.Imaginary = _imaginary;
        }

        public Complex(Complex _complex)
        {
            this.Real = _complex.Real;
            this.Imaginary = _complex.Imaginary;
        }

        #endregion

        #region Propiedades

        private double real;
        private double imaginary;

        public event PropertyChangedEventHandler PropertyChanged;

        public double Module { get; set; }
        public double DegreesAngle { get; set; }
        public double RadiansAngle { get; set; }

        public double Imaginary
        {
            get { return imaginary; }
            set
            {
                imaginary = value;
                SetPolarComplex();
                OnPropertyChanged("Imaginary");
                OnPropertyChanged("ToString");
            }
        }

        public double Real
        {
            get { return real; }
            set
            {
                real = value;
                SetPolarComplex();
                OnPropertyChanged("Real");
                OnPropertyChanged("ToString");
            }
        }

        public string ToBinomialString
        {
            get
            {
                return "z = " + this.Real.ToString("F2") + (this.Imaginary < 0 ? " - " : " + ") + Math.Abs(this.Imaginary).ToString("F2") + " i";
            }
        }

        public string ToPolarString
        {
            get
            {
                return "z = " + this.Module.ToString("F2") + ") " + this.DegreesAngle.ToString("F1") + "°";
            }
        }

        public string ToString
        {
            get { return this.ToBinomialString + new string(' ',10) + this.ToPolarString; }
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Asigna el módulo y el argumento de un número complejo
        /// </summary>
        void SetPolarComplex()
        {
            this.Module= Math.Sqrt(Math.Pow(this.Real, 2) + Math.Pow(this.Imaginary, 2));
            this.RadiansAngle= Math.Atan2(this.Imaginary, this.Real);
            this.DegreesAngle = this.RadiansAngle * 180 / Math.PI;
        }

        #endregion

        #region Static Public Methods

        // SOBRECARGA DE LOS OPERADORES BÁSICOS

        public static Complex operator + (Complex _complex1, Complex _complex2)
        {
            return new Complex(_complex1.Real + _complex2.Real, _complex1.Imaginary + _complex2.Imaginary);
        }

        public static Complex operator - (Complex _complex1, Complex _complex2)
        {
            return new Complex(_complex1.Real - _complex2.Real, _complex1.Imaginary - _complex2.Imaginary);
        }

        public static Complex operator * (Complex _complex1, Complex _complex2)
        {
            return new Complex((_complex1.Real * _complex2.Real - _complex1.Imaginary * _complex2.Imaginary),
                (_complex1.Real * _complex2.Imaginary + _complex1.Imaginary * _complex2.Real));
        }

        public static Complex operator / (Complex _complex1, Complex _complex2)
        {
            double xx = (_complex1.Real * _complex2.Real + _complex1.Imaginary * _complex2.Imaginary) / (Math.Pow(_complex2.Real, 2) + Math.Pow(_complex2.Imaginary, 2));
            double yy = (_complex1.Imaginary * _complex2.Real - _complex1.Real * _complex2.Imaginary) / (Math.Pow(_complex2.Real, 2) + Math.Pow(_complex2.Imaginary, 2));
            return new Complex(xx, yy);
        }

        public static Complex operator ! (Complex _complex)
        {
            return new Complex(-_complex.Real, -_complex.Imaginary);
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Inicia el evento de cambio de propiedad
        /// </summary>
        /// <param name="p_PropertyName">Nombre de la propiedad</param>
        public void OnPropertyChanged(string p_PropertyName)
        {
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs(p_PropertyName));
            }
        }

        /// <summary>
        /// Efectúa una copia del objeto
        /// </summary>
        /// <param name="_complex">Objeto desde el cual se efectúa la copia</param>
        public void Clone(Complex _complex)
        {
            this.Real = _complex.Real;
            this.Imaginary = _complex.Imaginary;
        }

        /// <summary>
        /// Devuelve el conjugado de un número complejo
        /// </summary>
        /// <param name="complex">Número complejo al que se le calcula</param>
        /// <returns>Objeto Complex</returns>
        public Complex Conjugate(Complex complex)
        {
            return new Complex(complex.Real, -complex.Imaginary);
        }

        /// <summary>
        /// Devuelve el conjugado de un número complejo
        /// </summary>
        /// <returns>Objeto Complex</returns>
        public Complex Conjugate()
        {
            return Conjugate(this);
        }

        /// <summary>
        /// Determina cuando una instancia y un objeto tienen los mismos valores
        /// </summary>
        /// <param name="_complex">El objeto a comparar con la instancia</param>
        /// <returns>Valor booleano</returns>
        public bool Equals(Complex _complex)
        {
            if (_complex == null)
            {
                return false;
            }
            if (this.GetType() != _complex.GetType())
            {
                return false;
            }

            return this.Real == _complex.Real && this.Imaginary == _complex.Imaginary;
        }

        /// <summary>
        /// Obtiene el inverso del objeto Complex actual
        /// </summary>
        /// <returns>Objeto Complex</returns>
        public Complex Reverse()
        {
            return Reverse(this);
        }

        /// <summary>
        /// Obtiene el inverso de un objeto Complex
        /// </summary>
        /// <param name="_complex">Objeto Complex del que se obtiene el inverso</param>
        /// <returns>Objeto Complex</returns>
        public Complex Reverse(Complex _complex)
        {
            double denominador = Math.Pow(_complex.Real, 2) + Math.Pow(_complex.Imaginary, 2);
            return new Complex(_complex.Real / denominador, -_complex.Imaginary / denominador);
        }

        /// <summary>
        /// Retorna el objeto Complex en formato Polar
        /// </summary>
        /// <param name="isDegrees">La salida se muestra en grados sexagesimales</param>
        /// <returns>String</returns>
        public string PolarToString(bool isDegrees)
        {
            return this.Module.ToString("D1") + ")" + (isDegrees?this.DegreesAngle.ToString("D1"):this.RadiansAngle.ToString("D2"));
        }

        /// <summary>
        /// Retorna el objeto Complex en formato Polar
        /// </summary>
        /// <returns>String</returns>
        public string PolarToString()
        {
            return PolarToString(true);
        }

        #endregion
    }
}


Ahora damos paso al código XAML del formulario. Lo primero declaramos los recursos de la ventana con &lt;code&gt;Window.Resources&lt;/code&gt; y en este lugar, incluimos los estilos. He creado tres estilos, uno llamado &lt;span style='font-family: &quot;;&quot;;' data-mce-style='font-family: &quot;;'&gt;normal&lt;/span&gt; aplicado a los controles &lt;code&gt;TextBlock&lt;/code&gt;, otro llamado &lt;code&gt;header&lt;/code&gt; que hereda de normal y por tanto es aplicado a los mismos controles y un tercer estilo llamado &lt;code&gt;textBox&lt;/code&gt; aplicado a controles &lt;code&gt;TextBox&lt;/code&gt; (muy original, eh?), por tanto a cada control que le asigne este estilo se le aplicarán los valores de las propiedades incluidas en el estilo a no ser que se sobreescriban al crear el control.( En el control &lt;code&gt;TextBox&lt;/code&gt; he incluido un validador para que no se incluya texto que no sea numérico, pero eso lo veremos otro día)Una vez creados los estilos vamos a centrarnos en&amp;nbsp;el control &lt;code&gt;TextBlock&lt;/code&gt; del siguiente código: 
 
      <StackPanel x:Name="stackResult" Orientation="Horizontal" Grid.Column="2" Grid.Row="5" Grid.ColumnSpan="5">
            <TextBlock x:Name="result"  Style="{StaticResource header}" Text="Resultado" VerticalAlignment="Center"/>
            <TextBlock x:Name="labelComplexNumberResult" Style="{StaticResource normal}" Text="{Binding ToString}" Margin="123,0,0,0"/>
</StackPanel>  
  1. Le asignamos un nombre
  2. Le asignamos el estilo con Style="{StaticResource textBox}"
  3. Le asignamos el valor enlazado de la propiedad Text con  la propiedad Real de la clase Complex. Style="{StaticResource textBox}" Text="{Binding Real}"¿Y de donde extrae los datos la propiedad Text?

Para que los controles sepan de donde tienen que extraer los datos de la clase Complex, al crear la ventana principal, asigno a cada StackPanel donde se encuentran contenidos los controles de cada número, la clase desde donde se alimentará y cada vez que se alteren los datos de cada clase, se visualizarán automáticamente en los controles sin necesidad de código. Por tanto en el code-behind de la ventana principal solo y exclusivamente incluyo la declaración de los tres objetos Complex, los dos de cálculo y el resultado, la asignación del DataContext de los StackPanel, los métodos de los eventos de cambio de propiedad y cambio de operación y el cálculo  de la operación.

/* ************************************************************************************************************************************************
* © JOAQUIN MARTINEZ RUS 2015
* PROYECTO:        ComplexCalc. Calculadora de números complejos
* Archivo:         MainWindow.cs
* Descripción:     Clase de la ventana principal
* Historial:
*                  1. Joaquin Martínez Rus - 18 jul 2016. Creación
*
* Comentarios:
*
*
**************************************************************************************************************************************************/
using System.Windows;
using System.Windows.Controls;

namespace ComplexCal
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        Complex c1 = new Complex(2,4);
        Complex c2 = new Complex(3,5);
        Complex ComplexResult=new Complex();

        public MainWindow()
        {
            InitializeComponent();

            // asignar a los Stackpanel el Datacontext de cada objeto Complex

            stackC1.DataContext = c1;
            stackC2.DataContext = c2;
            stackResult.DataContext = ComplexResult;

            // Crear evento para que cada vez que se cambie una propiedad, se calcule la operación
            c1.PropertyChanged += Complex_PropertyChanged;
            c2.PropertyChanged += Complex_PropertyChanged;

        }

        private void Complex_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            Calculate(comboBox.SelectionBoxItem.ToString());
        }

        private void comboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            ComboBoxItem op = e.AddedItems[0] as ComboBoxItem;
            Calculate(op.Content.ToString());
        }

        /// <summary>
        /// Efectúa el cálculo entre dos complejos
        /// </summary>
        /// <param name="operatorToString">Operador</param>
        void Calculate(string operatorToString)
        {
            switch (operatorToString)
            {
                case "+":
                    ComplexResult.Clone(c1 + c2);
                    break;
                case "-":
                    ComplexResult.Clone(c1 - c2);
                    break;
                case "x":
                    ComplexResult.Clone(c1 * c2);
                    break;
                case "÷":
                    ComplexResult.Clone(c1 / c2);
                    break;
                case "Inverso":
                    ComplexResult.Clone(c1.Reverse());
                    break;
                case "Conjugado":
                    ComplexResult.Clone(c1.Conjugate());
                    break;
                case "Opuesto":
                    ComplexResult.Clone(!c1);
                    break;
            }

        }

    }
}

y el código XAML full:

<Window x:Class="ComplexCal.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ComplexCal"
        mc:Ignorable="d"
        Title="Complex Calc" Height="480" Width="640">
    <Window.Resources>

        <Style x:Key="normal" TargetType="TextBlock">
            <Setter Property="VerticalAlignment" Value="Center"/>            
            <Setter Property="FontSize" Value="14"/>
            <Setter Property="FontFamily" Value="Segoe UI"/>
            <Setter Property="Height" Value="25"/>
            <Setter Property="Width" Value="Auto"/>
        </Style>
        <Style x:Key="header" TargetType="TextBlock" BasedOn="{StaticResource normal}">
            <Setter Property="FontWeight" Value="Bold"/>
            <Setter Property="VerticalAlignment" Value="Bottom"/>
            <Setter Property="HorizontalAlignment" Value="Center"/>            
        </Style>
        <Style x:Key="textBox" TargetType="TextBox">
            <Setter Property="VerticalAlignment" Value="Center"/>
            <Setter Property="Height" Value="25"/>
            <Setter Property="Width" Value="50"/>
            <Setter Property="HorizontalAlignment" Value="Center"/>
            <Setter Property="Validation.ErrorTemplate">
                <Setter.Value>
                    <ControlTemplate>
                        <DockPanel LastChildFill="true">
                            <Border Background="OrangeRed" DockPanel.Dock="right" Margin="5,0,0,0" 
                                Width="20" Height="20" CornerRadius="5"
                                ToolTip="{Binding ElementName=customAdorner, 
                                          Path=AdornedElement.(Validation.Errors)[0].ErrorContent}">
                                <TextBlock Text="!" VerticalAlignment="center" HorizontalAlignment="center" 
                                   FontWeight="Bold" Foreground="white" />
                            </Border>
                            <AdornedElementPlaceholder Name="customAdorner" VerticalAlignment="Center" >
                                <Border BorderBrush="red" BorderThickness="1" />
                            </AdornedElementPlaceholder>
                        </DockPanel>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Setter Property="Margin" Value="20,0,0,0"/>
        </Style>
    </Window.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="28*"/>
            <ColumnDefinition Width="27*"/>
            <ColumnDefinition Width="82*"/>
            <ColumnDefinition Width="132*"/>
            <ColumnDefinition Width="154*"/>
            <ColumnDefinition Width="60*"/>
            <ColumnDefinition Width="149*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="49*"/>
            <RowDefinition Height="49*"/>
            <RowDefinition Height="49*"/>
            <RowDefinition Height="49*"/>
            <RowDefinition Height="49*"/>
            <RowDefinition Height="49*"/>
            <RowDefinition Height="155*"/>
        </Grid.RowDefinitions>
        <TextBlock x:Name="labelReal" Grid.Column="2" Grid.Row="1" Style="{StaticResource header}" Text="Real"/>
        <TextBlock x:Name="labelImaginaria" Grid.Column="3" Grid.Row="1" Style="{StaticResource header}" Text="Imaginaria"/>
        <TextBlock x:Name="labelNumberHeader" Grid.Column="4" Grid.Row="1" Style="{StaticResource header}" Text="Número complejo"/>
        <TextBlock x:Name="z1" Grid.Column="1" Grid.Row="2" Style="{StaticResource normal}" Text="z1" Margin="0,12"/>
        <TextBlock x:Name="z2" Grid.Column="1" Grid.Row="4" Style="{StaticResource normal}" Text="z2" Margin="0,12" />
        <TextBlock x:Name="op" Grid.Column="2" Grid.Row="3" Style="{StaticResource normal}" Text="Operación" Margin="0,12"/>
        <ComboBox x:Name="comboBox" Grid.Column="3" HorizontalAlignment="Left" Grid.Row="3" VerticalAlignment="Center" Width="80" Height="25" SelectionChanged="comboBox_SelectionChanged">
            <ComboBoxItem Content="+" IsSelected="True"/>
            <ComboBoxItem Content="-"/>
            <ComboBoxItem Content="x"/>
            <ComboBoxItem Content="÷"/>
            <ComboBoxItem Content="Inverso"/>
            <ComboBoxItem Content="Conjugado"/>
            <ComboBoxItem Content="Opuesto"/>
        </ComboBox>
        <StackPanel x:Name="stackC1" Orientation="Horizontal" Grid.Column="2" Grid.Row="2" Grid.ColumnSpan="5">
            <TextBox x:Name="textBoxReal1"  Style="{StaticResource textBox}" Text="{Binding Real}"/>
            <TextBox x:Name="textBoxImagin1" Style="{StaticResource textBox}" Text="{Binding Imaginary}"/>
            <TextBlock x:Name="labelComplexNumber1" Style="{StaticResource normal}" Text="{Binding ToString}" Margin="50,0,0,0" />
        </StackPanel>
        <StackPanel x:Name="stackC2" Orientation="Horizontal" Grid.Column="2" Grid.Row="4" Grid.ColumnSpan="5">
            <TextBox x:Name="textBoxReal2" Style="{StaticResource textBox}" Text="{Binding Real}"/>
            <TextBox x:Name="textBoxImagin2" Style="{StaticResource textBox}" Text="{Binding Imaginary}"/>
            <TextBlock x:Name="labelComplexNumber2" Style="{StaticResource normal}" Text="{Binding ToString}" Margin="50,0,0,0" />
        </StackPanel>
        <StackPanel x:Name="stackResult" Orientation="Horizontal" Grid.Column="2" Grid.Row="5" Grid.ColumnSpan="5">
            <TextBlock x:Name="result"  Style="{StaticResource header}" Text="Resultado" VerticalAlignment="Center"/>
            <TextBlock x:Name="labelComplexNumberResult" Style="{StaticResource normal}" Text="{Binding ToString}" Margin="123,0,0,0"/>
        </StackPanel>
    </Grid>
</Window>


Pues esto es todo, la clase Complex en C# y su ejecución bajo WPF con los controles enlazados.

Nos vemos. Saludos

Calculadora de números complejos en Java

Calculadora de números complejos en Java

En este caso, voy a detallar una clase sencilla que implementa una calculadora de números complejos en Java.

Para los que no lo recuerden, un número complejo consta de dos partes, una parte real y una parte imaginaria; la parte imaginaria se compone del número √-1 o lo que es lo mismo i. Un número complejo lo podemos representar vectorialmente como z = (a,b), binomial z = a + bi, polar mediante un módulo y un ángulo z = r α, trigonométrica z = r (cos α + i sin α) y por último la forma exponencial y que no voy a incluir en la clase aunque a mi parecer es la más elegante e = cos α + i sin α.

Cada forma tiene su fin, por ejemplo la suma y las resta son más fáciles con la forma binomial mientras que la multiplicación, división y exponenciación lo son con la forma polar; en el caso de la clase todos los cálculos los he realizado con la forma binomial.

Si pasamos a la clase, como ya he comentado está implementada en Java y consta de cuatro propiedades, parte real mediante x, parte imaginaria mediante y, el módulo que es calculado en base de x e y, y por último el argumento o ángulo.A continuación paso el diagrama de la clase donde se exponen los métodos con la mayoría de las operaciones:

Pasando un ejemplo

// Pasamos dos números complejos
        // Efectuamos los cálculos
        Complex c1=new Complex(4,5);
        Complex c2=new Complex(7,6);

        System.out.println("(" + c1.toString() + ") + (" + c2.toString() + ") = " + c1.addComplex(c2));
        System.out.println("(" +c1.toString() + ") - (" + c2.toString() + ") = " + c1.substractComplex(c2));
        System.out.println("(" +c1.toString() + ") x (" + c2.toString() + ") = " + c1.multiplyComplex(c2));
        System.out.println("(" +c1.toString() + ") / (" + c2.toString() + ") = " + c1.divideComplex(c2));
        System.out.println("Opuesto de " + c1.toString() + " = " + c1.opposite().toString());
        System.out.println("Inverso  de " + c1.toString() + " = " + c1.reverse().toString());
        System.out.println(c1.toString() + " = " + c1.polarToString());
        System.out.println(c1.toString() + " = " + c1.polarToString(true));
        System.out.println(c2.toString() + " = " + c2.polarToString());
        System.out.println(c2.toString() + " = " + c2.polarToString(true));
        c2.convertToBinomial(5, 36.86*Math.PI/180);
        System.out.println("Convertir 5) 36.86º a binomial = " + c2.toString());<span id="mce_SELREST_start" style="overflow:hidden;line-height:0;"></span>

Resultado

(4 + 5i) + (7 + 6i) = 11 + 11i
(4 + 5i) - (7 + 6i) = -3 -1i
(4 + 5i) x (7 + 6i) = -2 + 59i
(4 + 5i) / (7 + 6i) = 0,68 + 0,13i
Opuesto de 4 + 5i = -4 -5i
Inverso  de 4 + 5i = 0,1 -0,12i
4 + 5i = 5,7)0,896 radians
4 + 5i = 5,7)51,3º
7 + 6i = 9,9)0,709 radians
7 + 6i = 9,9)40,6º
Convertir 5) 36.86º a binomial = 4 -3i

A continuación paso el código:

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package complex;

/******************************************************
 *  @author:    Joaquín Martínez Rus (c) 2016
 *  @version:   1.0 
 *  File:       Estrella.java
 *  Created:    17/07/2016
 *  Project:    Calculadora de números complejos
 *  Comments:   Clase Complex.
 *******************************************************/
public class Complex {
    
    /**
     * Inicia una nueva instancia de la clase Complex con valor z = 0 + 0i;
     */
    public Complex(){
        this(0,0);
    }
    
    /**
     * Inicia una nueva instancia de la clase Complex
     * @param _x Parte real
     * @param _y Parte imaginaria
     */
    public Complex(double _x, double _y){
        this.x=_x;
        this.y=_y;
        this.setModule();
        this.setGrades();
    }
    
    double x;
    double y;
    double module;
    double grades;

    public double getX() {
        return x;
    }

    public void setX(double x) {
        this.x = x;
    }

    public double getY() {
        return y;
    }

    public void setY(double y) {
        this.y = y;
    }

    public double getModule() {
        
        return module;
    }

    /**
     * Asigna y calcula el valor del módulo
     */
    public void setModule(){
        this.module=Math.sqrt(Math.pow(x, 2)+ Math.pow(x, 2));
    }
    
    /**
     * Asigna el valor del módulo
     */
    public void setModule(double module) {
        this.module = module;
    }

    public double getGrades() {
        return grades;
    }

    public void setGrades() {
        this.grades = Math.atan2(y, x);
    }
    
    public void setGrades(double grades) {
        this.grades = grades;
    }
    
    /**
     * Suma al número complejo otro número complejo
     * @param _complex Número complejo de la suma
     * @return Objeto Complex
     */
    public Complex addComplex(Complex _complex){
        return new Complex(this.x + _complex.x, this.y + _complex.y);
    }
    
    /**
     * Resta al número complejo otro número complejo
     * @param _complex Número complejo de la resta
     * @return Objeto Complex
     */
    public Complex substractComplex(Complex _complex){
        return new Complex(this.x - _complex.x, this.y - _complex.y);
    }
    
    /**
     * Multiplica al número complejo otro número complejo
     * @param _complex Número complejo de la multiplicación
     * @return Objeto Complex
     */
    public Complex multiplyComplex(Complex _complex){
        return new Complex((this.x * _complex.x - this.y * _complex.y), 
                (this.x * _complex.y + this.y * _complex.x));
    }
    
    /**
     * Divide el número complejo actual entre el valor del parámetro
     * @param _complex Denominador
     * @return Objeto Complex
     */
    public Complex divideComplex(Complex _complex){
        return divideComplex(this,_complex);
    }
    
    /**
     * Divide dos números complejos
     * @param _complex1 Numerador
     * @param _complex2 Denominado
     * @return Objeto Complex
     */
    public Complex divideComplex(Complex _complex1, Complex _complex2){
        double xx = (_complex1.x * _complex2.x + _complex1.y * _complex2.y)/(Math.pow(_complex2.x,2)+Math.pow(_complex2.y,2));
        double yy = (_complex1.y * _complex2.x - _complex1.x * _complex2.y)/(Math.pow(_complex2.x,2)+Math.pow(_complex2.y,2));
        return new Complex(xx, yy);
    }
    
    /**
     * Obtiene un número complejo en forma vectorial
     * @return Cadena de texto
     */
    public String vectorialtoString(){
        return "(" + ConsoleTools.toNumber(this.x,2) + ", " + ConsoleTools.toNumber(this.y,2) + ")";
    }
    
    /**
     * Obtiene un número complejo en forma binomial
     * @return Cadena de texto
     */
    @Override
    public String toString(){
        return ConsoleTools.toNumber(this.x,2) + (this.y < 0? "": " + ") + ConsoleTools.toNumber(this.y,2) + "i";
    }
    
    /**
     * Obtiene un número complejo en forma polar
     * @return Cadena de texto
     */
    public String polarToString(){
        return  polarToString(false);
    }
    
    /**
     * Obtiene un número complejo en forma polar
     * @param isDegrees Mostrar como grados centigrados o radianes
     * @return Cadena de texto
     */
    public String polarToString(boolean isDegrees){
        double angle=isDegrees?this.getGrades()*180/Math.PI:this.getGrades();
        String angleToString = (isDegrees?ConsoleTools.toNumber(angle,1):ConsoleTools.toNumber(angle,3)) + (isDegrees? "º": " radians");
        return  ConsoleTools.toNumber(this.module,1) + ")" + angleToString;
    }
    
    /**
     * Obtiene el conjugado de un número complejo
     * @param complex Número complejo
     * @return Objeto Complex
     */
    public Complex conjugate(Complex complex){
        return new Complex(complex.x, - complex.y);
    }
    
    /**
     * Calcula el opuesto de un número complejo
     * @return Objeto Complex
     */
    public Complex opposite(){
        return opposite(this);
    }
    
    /**
     * Calcula el opuesto de un número complejo
     * @param _complex Número complejo
     * @return Objeto Complex
     */
    public Complex opposite(Complex _complex){
        return new Complex(-_complex.x, - _complex.y);
    }
    /**
     * Obtiene el conjugado de un número complejo
     * @return Objeto Complex
     */
    public Complex conjugate(){
        return conjugate(this);
    }
    
    /**
     * Obtiene el inverso de un número complejo
     * @return Objeto Complex
     */
    public Complex reverse(){
        return reverse(this);
    }
    
    /**
     * Obtiene el inverso de un número complejo
     * @param complex Número complejo del cálculo
     * @return Objeto Complex
     */
    public Complex reverse(Complex complex){
        double denominador=Math.pow(complex.x, 2) + Math.pow(complex.y, 2);  
        return new Complex( complex.x/denominador, - complex.y/denominador);
    }
    
    /**
     * Convierte un número complejo de forma polar a binomial
     * @param module Modulo del número complejo
     * @param argument Argumento en radianes del número complejo
     * @return Objeto Complex
     */
    public Complex convertPolarToBinomial(double module, double argument){
        double _x = Math.abs(module) * Math.cos(argument);
        double _y = Math.abs(module) * Math.sin(argument);
        return new Complex(_x,-_y);
    } 
    
    /**
     * Asigna los valores real e imaginario en base al módulo y el argumento
     * @param module Modulo del número complejo
     * @param argument Argumento en radianes del número complejo
     */
    public void convertToBinomial(double module, double argument){
        Complex _complex=convertPolarToBinomial(module, argument);
        this.x=_complex.x;
        this.y=_complex.y;
    }
    
    /**
     * Comprueba dos números complejos
     * @param _complex Número complejo a comparar
     * @return Objeto Complex
     */
    public boolean equals(Complex _complex){
        if (_complex==null) {
            return false;
        }
        if (this.getClass()!=_complex.getClass()) {
            return false;
        }
        
        return this.x ==_complex.x && this.y == _complex.y;
    }
}


Consejos.

  • La clase debe estar bien definida. Constructores, propiedades y métodos
  • Antes de escribir código, genera un diagrama de clases como mínimo (esto implica pensar que vas a hacer), además una buena estructura puede hacer nuestro código más robusto.
  • Piensa en las cuatro características de la Programación Orientada a Objetos, Abstracción, Encapsulamiento, Herencia y Polimorfismo (hay alguna más, pero estas son las principales y debes tenerlas muy claras)
  • Los métodos deben contener el código justo. Un método con mucho código no hace la clase legible y lo vuelve débil.
  • Las clases deben funcionar en cualquier medio. Si usara esta clase en modo gráfico con ventanas en vez de modo consola, debería de funcionar del igual modo, solo debo llamar al método de operación y obtener su resultado mediante los métodos apropiados.
  • Usa los comentarios, tanto dentro de los métodos como en su documentación. Por ejemplo, si llamo al método divideComplex de la clase Complex, cuando estoy escribiendo el método, aparecerá el método y el texto que nosotros escribimos, en este caso yo escribí «Divide el número complejo actual entre el valor del parámetro» junto con los parámetros y el valor retornado. Cuando las clases se hacen muy grandes y complejas, es necesario documentarlas todo lo que se pueda.
  • Además de los comentarios, a mi me gusta agrupar el código por regiones con Java uso
    // 

    /**
     * Devuelve un valor desde el teclado
     * @param textIn Texto que se visualiza en la consola
     * @return Texto procedente del teclado
     * @throws IOException
     */
    public static String readText(String textIn, boolean allowEmpty) throws IOException{

        // Declaración de variables
        BufferedReader keyboardIn=new BufferedReader(new InputStreamReader(System.in));
        String textOut=null;

        // Imprimir texto de consola
        System.out.println(textIn);

        // Solicitar datos desde teclado

        do {
            textOut=keyboardIn.readLine();
        } while (ConsoleUtilities.isNullOrEmpty(textOut) && !allowEmpty);

        // Retornar texto
        return textOut;
    }

    // 

Esto me va permitir expandir o contraer todo el código contenido entre las etiquetas o para el caso de C#

#region Private Methods
      // Aquí iría el código en C#
#endregion

o Visual Basic

#Región MiRegion
     // Aquí iría el código en Visual Basic
#End Region
  • Cíñete a la nomenclatura estándar con cada lenguaje de programación, por ejemplo Java o C# o Visual Basic.
  • Créate un clase con herramientas, por ejemplo, para aplicaciones del tipo consola en Java, tengo una clase con métodos estáticos donde implemento herramientas que puedo usar en este medio como un método que genera un menú automáticamente, un lector desde teclado de texto o números, un formateador de texto, un serializador, etc. Para C# tengo otro tipo de clases donde extiendo funcionalidades a las clases creadas, en fin, código que reutilizaré más a menudo de lo que me pienso.
  • Si hacemos todo esto, en un futuro nos será más fácil, modificar, entender que hicimos o ampliar nuestras clases.
Y esto es todo por hoy. Saludos!