miércoles, 15 de octubre de 2008

Numeros aleatorios sin repeticion (una clase)

image006

Supongamos que tenemos que rellenar una lotería primitiva: tenemos que hallar 6 números aleatorios de entre 69 números pero sin que se repitan.
¿Como lo hacemos?, pues nada, aplicando la clase contenida en este truco.
En concreto acabo de hacer esto pues estoy haciendo una página web y necesito mostrar en la misma página 3 artículos aleatorios de una base de datos... y claro, que se me repitan en la misma página web 3 articulos no es muy estético que digamos...
Como en el asunto este se mezclan datos y funciones y procedures, he decidido meterlo todo en una clase para facilitar su reutilización.
He colocado todo en una unit, (llamada uTAlea) para que con sólo añadirla en la clausula uses de nuestra aplicación pueda ser utillizada.
Aqui está el código de la unit uTAlea que contiene la clase TAlea:

 unit uTAlea;
{Radikal, Q3 para Trucomania.}

interface

uses Windows,SysUtils;

type
{Array de booleanos de longitud variable}
TArrayBool = array[0..0] of boolean;
PArrayBool = ^TArrayBool;

{Clase para manejar numeros aleatorios}
TAlea = class
Maximo : integer; //Valor maximo de los numeros obtenidos
Lista : PArrayBool;
FaltanDeSacar : integer;
constructor Create(Rango:integer);
destructor Destroy; override;
procedure Reset; //Resetea la lista de numeros
function PillaNumero:integer; //Devuelve un numero aletorio sin repeticion
end;



implementation


constructor TAlea.Create(Rango:integer);
begin
{
Si los números a extraer no pueden ser repetidos
hemos de crear un array para almacenar cual ha salido y cual no
Como el array ha de ser variable, usamos el GetMem y
los punteros para que sirva tambien para versiones
anteriores a Delphi 4, que no incorporan los arrays
de longitud variable...
}

inherited Create;
Maximo:=Rango;
FaltanDeSacar:=Rango;
{Reservamos memoria para el array de longitud variable}
GetMem(Lista, 1+Maximo * SizeOf(Boolean));
ZeroMemory(Lista,1+Maximo * SizeOf(Boolean));
end;

destructor TAlea.Destroy;
begin
if Assigned(Lista) then FreeMem(Lista, 1+Maximo * SizeOf(Boolean));
inherited Destroy;
end;

function TAlea.PillaNumero:integer;
var
Numero: integer;
begin
if FaltanDeSacar=0 then raise exception.create( 'Error. No se pude sacar otro numero sin repetir'+#13+#10+
'Error. No more numbers are available');

{Buscamos un número que no haya salido ya}
repeat Numero:=Random(Maximo) until NOT Lista^[Numero];
{Lo apuntamos en la lista de numeros ya usados}
Lista^[Numero]:=TRUE;
{Decrementamos la cantidad de numeros que faltan por salir}
Dec(FaltanDeSacar);
Result:=Numero;
end;

procedure TAlea.Reset;
begin
ZeroMemory(Lista,1+Maximo * SizeOf(Boolean));
FaltanDeSacar:=Maximo;
end;

end.

Ya sabes, la grabas como uTAlea.PAS para poder usarla en tus proyectos.
Vamos con una aplicación de ejemplo para probar la clase:

  • Crea una aplicacion nueva, con una form que contenga un TMemo (Memo1) y un TButton (Button1)

  • Copia el uTAlea.PAS en el directorio de esa aplicacion

  • Añade TAlea en el uses de tu form

  • Mete el siguiente código en el OnCLick del Button1:
     procedure TForm1.Button1Click(Sender: TObject);
    var
    Ristra : TAlea;
    n : integer;
    begin
    Memo1.Lines.Clear;

    Ristra:=TAlea.Create(10);

    {Sacamos 10 numero sin repetir}
    for n:=1 to 10 do begin
    memo1.Lines.Add( IntToStr(Ristra.PillaNumero) );
    end;

    Memo1.lines.Add('Otros 5 numeros de entre 10...');

    Ristra.Reset;
    for n:=1 to 5 do begin
    memo1.Lines.Add( IntToStr(Ristra.PillaNumero) );
    end;

    Ristra.Free;
    end;
  • Calcular la Edad (en años) segun la fecha de nacimiento

    edad_cinturon
    procedure TForm1.Button1Click(Sender: TObject);

    function Edad(FechaNacimiento:string):integer;
    var
    iTemp,iTemp2,Nada:word;
    Fecha:TDate;
    begin
    Fecha:=StrToDate(FechaNacimiento);
    DecodeDate(Date,itemp,Nada,Nada);
    DecodeDate(Fecha,itemp2,Nada,Nada);
    if FormatDateTime('mmdd',Date) <
    FormatDateTime('mmdd',Fecha) then Result:=iTemp-iTemp2-1
    else Result:=iTemp-iTemp2;
    end;

    begin
    Label1.Caption:=intToStr(Edad('07/09/1969'));
    end;

    Mantener una Spash Screen unos segundos tras cargar la aplicación

    SplashScreen 

    En tu form principal, declara una variable pública llamada SplashScreenHandle:

     	var
    SplashScreenHandle:integer;

    En el fuente del projecto, añade una sección var como la siguiente:

     	VAR
    SplashScreen:TSplashScreen;

    Dentro de la sección begin-end, añade el siguiente código al principio:

     Begin
    {Mostramos la Splash Screen}
    {Show the splash screen}
    SplashScreen:=TSplashScreen.Create(Application);
    Splashscreen.show;
    SplashScreen.update;
    {Creamos la form principal}
    {Create the main form}
    Application.createform(MainForm,MainFormUnit);
    MainFormUnit.SplashScreenHandle:=SplashScreen.handle;
    {Aqui el resto de tu projecto...}
    {rest of your code goes here....}
    end.

    Entonces, en tu form principal, mediante un timer, ejecutamos éste código para cerrar la Splash Screen:

         SendMessage(SplashScreenHandle,WM_CLOSE,0,0);

    Agilizar la carga de tu aplicacion

    ¿ Tu aplicacion tarda mucho en cargar ?. Igual te interesa este truco...

    No crees todos los forms de golpe, crea sólo el inicial, y desde el crea dinámicamente los que vayas a utilizar, sólo cuando los vayas a utilizar. Es decir, en el IDE, en Project->Options tienes dos ventanas, una la de 'Autocreate forms' y otra, la de 'Available forms'. Pues pon sólo la principal en 'Autocreate forms'.

    Después, en el uses de la primera form, añades las units del resto de forms, y en el var de la primera form, declaras las variables TForm del resto de forms que vayas a crear.

    Un ejemplo de Form1 que crea una Form2:

    El uses de Form1:

     uses
    Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, Unit2;

    El var de Form1:

     var
    Form1: TForm1;
    Form2: TForm2;

    Y cuando quieras llamar a la Form2 desde Form1 usa:

     procedure TForm1.Button1Click(Sender: TObject);
    begin
    Form2:=TForm2.Create(self);
    Form2.Show;
    end;

    Si tras hacer todo esto, te sigue tardando mucho en cargar el primer form, puedes ponerle una Splash Screen (una pantalla inicial) en la que le pones el típico mensaje de 'cargando'.

    SplashScreen con progressbar

    SplashScreenProgressBarNoMessage Hacer que aparezca una ventana mientras se carga mi aplicacion (Splash Screen)
    se explica cómo hacer una SplashScreen.
    "SplashScreen con progressbar", esta pensado sobre todo para cuando abres bases de datos y se tarda un mundo en cargar el programa. Con el progressbar queda bastante bien...
    Primero debemos definir cuantos "pasos" va a tener el progressbar, en este caso el numero de bases de datos que vamos a abrir, luego establecemos por ejemplo el valor Max progressbar a 40 y el valor Step a 10 y hacemos 4 "stepit", seria de la siguiente manera:
    Este codigo esta en el evento OnCreate de una form principal, pero podriamos mandarlo llamar de otras forms siempre en el evento OnCreate.

     

     procedure TForm1.FormCreate(Sender: TObject);   
     begin
    Try
    DataBase1.Connected:=True
    Except
    ShowMessage('Cannot open DB1')
    End;
    SplashForm.ProgressBar.StepIt;
    SplashForm.update;
    Try
    DataBase2.Connected:=True
    Except
    ShowMessage('Cannot open DB2')
    End;
    SplashForm.ProgressBar.StepIt;
    SplashForm.update;
    Try
    Table1.Active:=True
    Except
    ShowMessage('Cannot open Table 1')
    End;
    SplashForm.ProgressBar.StepIt;
    SplashForm.update;
    Try
    Table2.Connected:=True
    Except
    ShowMessage('Cannot open Table 2')
    End;
    SplashForm.ProgressBar.StepIt;
    SplashForm.update;
    ...
    SplashForm.Free
    end;