Crea tu propia calculadora con Delphi (Parte III)

Buen camino amigo  mío.

😀

Tercer día. Finalizando los últimos retoques de la entrada, pero ya me pesa la falta de tiempo para prepararla como a mi me gusta. Estamos celebrando nuestro #DelphiWeek, un poco ajenos a lo que pasa fuera de esta diminuta ventana.

Capturar las imágenes. Pensar en el hilo argumental de la entrada. Tenerlo todo a punto y que no falle nada. Es complicado. Pero hoy nos toca dar un paso mas. No renunciamos a ese compromiso. Seguimos buscando la forma de mejorar nuestro código inicial y buscamos una vía para construir nuevas operaciones, afectando de una forma mínima al cuerpo central de código que las discriminaba.

Hagamos memoria nuevamente. Este era nuestro punto de partida.

case Operador of
     ' ' : Operando:= StrToFloat(lbDisplay.Caption);
     '+' : Operando:= Operando + StrToFloat(lbDisplay.Caption);
     '-' : Operando:= Operando - StrToFloat(lbDisplay.Caption);
     'x' : Operando:= Operando * StrToFloat(lbDisplay.Caption);
     '/' : Operando:= Operando / StrToFloat(lbDisplay.Caption);
   end;

Es nuestro caballo de batalla hoy, el corazón del método ProcesarOperacion()

Lo que a mi se me ocurrió, en la sana idea de mejorar o salvar ese tropiezo, fue la idea de que siendo nuestro operador una clase, en lugar de una mísera y vulgar variable (*), quizás podría conocer como debía interpretar el carácter recibido. Es decir, me hice la composición de lugar de llegar tener un catalogo de operaciones, que nos permitiera relacionar cada carácter o caracteres con las distintas clases ligadas a las mismas.

(*) Dicho esto en tono cariñoso, con empatía.  😉

Para mi esa estructura podría formarse por:

  1. Un lexema (un identificador único para cada operación)
  2. La clase asociada al lexema
  3. La representación textual del lexema (podría dar contenido al texto de los botones)
  4. Un valor booleano sobre si es un operador inmediato u opera con dos operandos de la forma que ya implementada en la función original.

Los tres primeros son mas evidentes pero el cuarto se añadió a posteriori.

Verás:

Si prestas atención a la implementación completa del método ProcesarOperacion() que habíamos compartido en la parte II:

function TCalculadoraBasica.ProcesarOperacion(const ALexema: Char): String;
begin
   case Operador of
     ' ','=' : Operando:= StrToFloat(FDisplay);
     '+' : Operando:= Operando + StrToFloat(FDisplay);
     '-' : Operando:= Operando - StrToFloat(FDisplay);
     'x' : Operando:= Operando * StrToFloat(FDisplay);
     '/' : Operando:= Operando / StrToFloat(FDisplay);
   end;
   ActualizaDisplay(FloatToStr(Operando));
   FlagNumero:= False;
   Operador:= ALexema;
...

Descubriremos un pequeño problema en el caso de operaciones como la raíz cuadrada, que aunque tiene dos operandos (como las anteriores), el índice de la raíz y el radicando, a diferencia de las operaciones habituales, en las que existe un orden lógico respecto al modo en el que se introducen y solo devolvemos el resultado al conocer el segundo operando, no entraría en ese grupo, con un comportamiento particular. La operación raíz no va a esperar ese segundo operando sino que retornará inmediatamente el resultado (pues se supone que el valor intrínseco que tiene el indice, que vale 2 en cualquier caso). Elegimos una vía sencilla para interpretar esta «anomalía» dejando que conozca en función de esa operación si debe retornar o no inmediatamente.

En la practica, ha supuesto crear un array de registros, para las operaciones básicas que ya conocíamos. Un array del tipo TEtiqueta que será un registro con los campos ya comentados y una funciones de apoyo a la manipulación.

TEtiqueta = record
  Lexema : String;
  Clase: TDigitoClass;
  Representacion: String;
  EsOperadorInmediato: Boolean;
  function Init: TEtiqueta;
  function Assign(ASource: TEtiqueta): TEtiqueta;
end;

El catalogo se inicializará con el total de operaciones básicas que asumimos:


const
  CatalogoSimbolosBasicos: Array[0..3] of TEtiqueta = (
   (Lexema:'x'; Clase: TDigitoOperadorMulti;Representacion: '*'; EsOperadorInmediato: False),         
   (Lexema:'+'; Clase: TDigitoOperadorSuma; Representacion: '+'; EsOperadorInmediato: False),         
   (Lexema:'-'; Clase: TDigitoOperadorResta;Representacion: '-'; EsOperadorInmediato: False ),        
   (Lexema:'/'; Clase: TDigitoOperadorDivision; Representacion: #$2797; EsOperadorInmediato: False)
); 

 Y una etiqueta adicional, que crearemos en tiempo de ejecución, demostrando así que somos capaces de añadir operaciones y determinar qué queremos que hagan. Ahora, esa etiqueta forma parte del mismo proyecto, pero quizás en un futuro decidamos que sea aislada en una librería adicional, algo razonable y factible.

const
  EtiquetaRaizCuadrada: TEtiqueta = (Lexema: 'RC';
                                     Clase: TDigitoOperadorRaizCuadrada;
                                     Representacion: #$221A;
                                     EsOperadorInmediato: True);  // digito raiz

Uno de los pequeños cambios que he tenido que valorar, es permitir que nuestro lexema tenga mas de un carácter de forma que, ante la incertidumbre de que número máximo admitir, te permitiría identificar operaciones mediante una cadena textual indeterminada. Podría haber elegido por ejemplo SEN para calcular el seno(), o como este que nos ocupa, RC, para la Raíz cuadrada.

Por otro lado, si nos fijamos en un determinado caso de la estructura

     '+' : Operando:= Operando + StrToFloat(FDisplay);

necesitaría probablemente una función que devolviera una valor decimal, recibiendo como parámetro de entrada cada uno de los operandos.

A ver que te parece ésta:

    function DoOperar(AOperador1, AOperador2: Extended): Double; 

Resumiendo todos estos conceptos, puedo presentarte ya la unidad UCBExtendTypes.pas que va a alojar nuestra clase TDigitoRaizCuadrada, que hereda de un ascendente comun a todas las operaciones TDigitoOperador. El motivo de hacerlo así no es otro que el de poder trabajar a nivel interno con la clase ascendente (en el interior del corazón de nuestro proceso de calculo) y que sea cada descendiente el que decida como debe operar.

Sencillo, ¿no?

😀

Apunta el siguiente código:


unit UCBExtendTypes;

interface

uses System.Classes, UCBClasses;

type
  TDigitoOperadorRaizCuadrada = class(TDigitoOperador)
  public
    function DoOperar(AOperador1, AOperador2: Extended): Double; override;
  end;

const
  EtiquetaRaizCuadrada: TEtiqueta = (Lexema: 'RC';
                                     Clase: TDigitoOperadorRaizCuadrada;
                                     Representacion: #$221A;
                                     EsOperadorInmediato: True);  // digito raiz

implementation
uses Math;
{ TDigitoOperadorRaizCuadrada }

function TDigitoOperadorRaizCuadrada.DoOperar(AOperador1,
  AOperador2: Extended): Double;
begin
   Result:= Sqrt(AOperador2);
end;
end.

 Volviendo la vista al código principal…

¿Recuerdas la interfaz de nuestro usuario?

Podemos añadir el nuevo botón que representa la operación raíz cuadrada, acomodando el resto de botones al espacio disponible. No hay mas cambios en nuestro formulario.

portada_calculadora_pIII

Eso sí, el código ahora debe considerar la nueva operación, por lo que hemos necesitado establecer una vía para registrar esa etiqueta, y seguidamente, asignamos el evento de respuesta de una forma similar a como lo hicimos en tiempo de diseño: al evento onClick le vinculamos un nuevo manejador para evaluar la respuesta de ProcesarOperacion( ) para el nuevo lexema. No olvides añadir en el uses de la implementación de UMain la unidad UCBExtendTypes.


procedure TfrmCalculadoraTrad.FormCreate(Sender: TObject);
begin
   FCalculadora:= TCalculadoraBasica.Create(Self);
   FCalculadora.RegisterEtiqueta(EtiquetaRaizCuadrada);
   bnRC.OnClick:= TeclaRaizCuadradaClick;
   bnRC.Text:= EtiquetaRaizCuadrada.Representacion;
end;

procedure TfrmCalculadoraTrad.TeclaRaizCuadradaClick(Sender: TObject);
begin
  lbDisplay.Text:= FCalculadora.ProcesarOperacion(EtiquetaRaizCuadrada.Lexema) +
      FCalculadora.OperadorText;
end;

Y también debemos hacer unos cambios en el módulo donde residía la especificación de la clase TCalculadoraBasica, que justamente habíamos añadido al proyecto en la parte anterior de la serie.

Añadimos una clase capaz de guardar nuestro catalogo

  TCalculadoraBasica = class(TComponent)
  private
    FDisplay: String;
    FlagNumero: Boolean;
    Operando: Double;
    Operador: String;
    FCatalogo: TObjectDictionary<String, TDigitoOperador>;
...

Y añadimos las tres piezas claves y una opcional (marcadas en color rojo en el texto). La tres primeras son respectivamente: Una función (EsOperadorInmediato( )) que evaluará si debe retornar de forma inmediata, y dos procedimientos que te permitirán añadir el array de registros con las operaciones básicas (RegisterCatalogo( )), de uso interno de la clase (o de sus descendientes) y una función publica para registrar la etiqueta individual (RegisterEtiqueta( )).

    function EsOperadorInmediato(const ALexema: String): Boolean;
    function GetOperadorText: String;
  protected
    procedure ActualizaDisplay(const AOperando: String); virtual;
    procedure DoReset; virtual;
    procedure RegisterCatalogo(ACatalogo: Array of TEtiqueta); virtual;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    function ExisteError: Boolean;
    function LeeDisplay: String;
    function ProcesarDigito(const ALexema: Char): String;
    function ProcesarOperacion(const ALexema: String): String;
    procedure RegisterEtiqueta(AEtiqueta: TEtiqueta);
    function Reset: String;
    property OperadorText: String read GetOperadorText;
  end;

Nos queda ver que cambios ha supuesto, a nivel de la implementación del módulo.

No te preocupes ya que son bastante sencillos:

Añadimos nuestro catalogo, representado en una instancia de la clase TObjectDictionary, que se adapta bastante bien a lo que buscamos. Esta clase, es una de las clases genéricas pertenecientes a la categoría de colecciones que ya tenemos a nuestra disposición en el entorno, lista para que la uses. Imagínala de un modo similar a como hubieras usado un TStringList. La diferencia y la ventaja es la parametrización. Nuestro diccionario va a relacionar una cadena textual con una instancia de la clase TDigitoOperador (la clase base sobre la que trabajamos al nivel de TCalculadoraBasica, que nunca conocerá a sus descendientes ni parentesco).

constructor TCalculadoraBasica.Create(AOwner: TComponent);
begin
  inherited;
  FCatalogo:= TObjectDictionary<String, TDigitoOperador>.Create;
  RegisterCatalogo(CatalogoSimbolosBasicos);
  Operando:= 0;
  Operador:= ' ';
  FlagNumero:= False;
end;

Igualmente debemos liberar la memoria asociada al contenido del catalogo antes de que se destruya la clase. Esa parte pertenecería al ámbito del evento Destroy de la calculadora.(*)

Un matiz: En muchos casos podremos tomar ventaja del recuento de referencias automático, dado que la instancia creada de la clase hereda de TInterfaceObject, y no sería necesario liberar expresamente.

destructor TCalculadoraBasica.Destroy;
var KeyLexema: String; 
begin
  for KeyLexema in FCatalogo.Keys do FCatalogo.Items[KeyLexema].Free; 
  FCatalogo.Clear;
  FreeAndNil(FCatalogo);
  inherited Destroy;
end;

(*) Ahondaremos en ese punto más adelante y nos serviremos de una herramienta adicional integrada en el IDE de Delphi, como puede ser CodeSite, de la que ya hemos hablado días atrás.

Añadimos igualmente una función que nos permita evaluar si la operación debería retornar de forma inmediata y para ello, consideramos todos los casos posibles, respecto al parámetro de entrada:

function TCalculadoraBasica.EsOperadorInmediato(const ALexema: String): Boolean;
begin
  if (ALexema = ' ') or (ALexema = '=') then
    Result:= False
  else Result:= FCatalogo.Items[ALexema].EsOperadorInmediato;
end;

Y de una forma similar, también crearía un método que nos retorne la representación textual.

function TCalculadoraBasica.GetOperadorText: String;
begin
  if (Operador = ' ') or (Operador = '=') then
    Result:= Operador
  else Result:= FCatalogo.Items[Operador].GetRepresentacion;
end;

Y el punto fuerte, sería la forma en la que jugando con la herencia y el polimorfismo respondemos a la invocación de ExecuteAction( ), sustituyendo así nuestra estructura case

function TCalculadoraBasica.ProcesarOperacion(const ALexema: String): String;
begin
   if EsOperadorInmediato(ALexema) then Operador:= ALexema;

   if ((Operador = ' ') or (Operador = '=')) then
     Operando:= StrToFloat(FDisplay)
   else
     Operando:= FCatalogo.Items[Operador].ExecuteAction(Self, Operando, StrToFloat(FDisplay));

   ActualizaDisplay(FloatToStr(Operando));
   FlagNumero:= False;
   Operador:= ALexema;

   Result:= LeeDisplay;
end;

Piensa como se va a ejecutar en la clase base:

function TDigitoOperador.ExecuteAction(ACalculadora: TComponent;
  AOperando1, AOperando2: Double): Double;
begin
  Result:= DoOperar(AOperando1, AOperando2);
end;

Y DoOperar() virtual por naturaleza en la clase base, no hará nada y será sobrescrito por cada operación añadida. Toma como ejemplo la operación Resta:

  TDigitoOperadorResta = class(TDigitoOperador)
  protected
  public
    function DoOperar(AOperador1, AOperador2: Extended): Double; override;
  end;

Nos falta algo…

Veo que estás bastante atento.

😀

Olvidaba mostarte los métodos que vamos a añadir para registrar las etiquetas. El primero, RegisterCatalogo( ), lo hemos definido como protegido y lo usaremos para añadir el catalogo inicial de operaciones en el momento en el que se crea la clase. Por ello hemos considerado una rutina que nos permita recorre cada registro de ese catálogo. Mientras que en el segundo caso, RegisterEtiqueta(), actuamos a nivel individual y con la mirada puesta a que pueda ser utilizado de forma pública, a nivel de interfaz, en base al diseño de la aplicación.

procedure TCalculadoraBasica.RegisterCatalogo(ACatalogo: array of TEtiqueta);
var
  i: Integer;
  FClass: TDigitoClass;
begin
  for i:= Low(ACatalogo) to High(ACatalogo) do
  begin
    FClass:= ACatalogo[i].Clase;
    FCatalogo.Add(ACatalogo[i].Lexema, FClass.Create(ACatalogo[i], self));
  end;
end;

procedure TCalculadoraBasica.RegisterEtiqueta(AEtiqueta: TEtiqueta);
var
  FClass: TDigitoClass;
begin
  FClass:= AEtiqueta.Clase;
  FCatalogo.Add(AEtiqueta.Lexema, FClass.Create(AEtiqueta, self));
end;

¿Y el resto de código?

Con lo comentado en las lineas anteriores, no deberías tener problema para entender la última de las unidades que vamos a añadir al proyecto. En esta unidad, profundizamos en la interpretación de esa clase base que representa la abstracción  del concepto de «operación» en el marco de la calculadora.

Tu parte esta ahora en leerla y devolverme cualquier comentario que creas conveniente.

Estamos en #DelphiWeek, nuestro tercer día compartiendo como Construir una calculadora con Delphi.



unit UCBClasses;

interface

uses System.SysUtils, System.Variants, System.Classes;


type
  TDigitoOperador = class;
  TDigitoClass = class of TDigitoOperador;

  TEtiqueta = record
    Lexema : String;
    Clase: TDigitoClass;
    Representacion: String;
    EsOperadorInmediato: Boolean;
    function Init: TEtiqueta;
    function Assign(ASource: TEtiqueta): TEtiqueta;
   end;

  TDigitoOperador = class(TInterfacedObject)
  private
    FEtiqueta: TEtiqueta;
    FCalculadora: TComponent;
    function GetClass: TDigitoClass;
    function GetEtiqueta: TEtiqueta;
  protected
  public
    constructor Create(AEtiqueta: TEtiqueta; ACalculadora: TComponent); virtual;
    destructor Destroy; override;
    function DoOperar(AOperador1, AOperador2: Extended): Double; virtual;
    function EsOperadorInmediato: Boolean;
    function ToString: String; override;
    procedure Init;
    function GetRepresentacion: String;
    function ExecuteAction(ACalculadora: TComponent; AOperando1, AOperando2: Double): Double;
    property ClassOfDigito: TDigitoClass read GetClass;
    property Etiqueta: TEtiqueta read GetEtiqueta;
  end;

  TDigitoOperadorSuma = class(TDigitoOperador)
  protected
  public
    function DoOperar(AOperador1, AOperador2: Extended): Double; override;
  end;

  TDigitoOperadorResta = class(TDigitoOperador)
  protected
  public
    function DoOperar(AOperador1, AOperador2: Extended): Double; override;
  end;

  TDigitoOperadorMulti = class(TDigitoOperador)
  protected
  public
    function DoOperar(AOperador1, AOperador2: Extended): Double; override;
  end;

  TDigitoOperadorDivision = class(TDigitoOperador)
  protected
  public
    function DoOperar(AOperador1, AOperador2: Extended): Double; override;
  end;


const
  CatalogoSimbolosBasicos: Array[0..3] of TEtiqueta = (
   (Lexema:'x'; Clase: TDigitoOperadorMulti;Representacion: '*'; EsOperadorInmediato: False),         
   (Lexema:'+'; Clase: TDigitoOperadorSuma; Representacion: '+'; EsOperadorInmediato: False),         
   (Lexema:'-'; Clase: TDigitoOperadorResta;Representacion: '-'; EsOperadorInmediato: False ),       
 (Lexema:'/'; Clase: TDigitoOperadorDivision; Representacion: #$2797; EsOperadorInmediato: False)
); 
implementation
{ TDigitoOperador }

uses UCalculadora;

constructor TDigitoOperador.Create(AEtiqueta: TEtiqueta;
  ACalculadora: TComponent);
begin
  Assert(Assigned(ACalculadora),'Error Instancia Clase TClassCalculadora no definida');
  FCalculadora:= ACalculadora;
  FEtiqueta.Assign(AEtiqueta);
end;

destructor TDigitoOperador.Destroy;
begin
  FCalculadora:= Nil;
  inherited Destroy;
end;

function TDigitoOperador.DoOperar(AOperador1, AOperador2: Extended): Double;
begin
end;

function TDigitoOperador.EsOperadorInmediato: Boolean;
begin
  Result:= FEtiqueta.EsOperadorInmediato;
end;

function TDigitoOperador.ExecuteAction(ACalculadora: TComponent;
  AOperando1, AOperando2: Double): Double;
begin
  Result:= DoOperar(AOperando1, AOperando2);
end;

function TDigitoOperador.GetClass: TDigitoClass;
begin
  Result:= FEtiqueta.Clase;
end;

function TDigitoOperador.GetEtiqueta: TEtiqueta;
begin
  Result:= FEtiqueta;
end;

function TDigitoOperador.GetRepresentacion: String;
begin
  Result:= FEtiqueta.Representacion;
end;

procedure TDigitoOperador.Init;
begin
  FEtiqueta.Init;
end;

function TDigitoOperador.ToString: String;
begin
  Result:= GetRepresentacion;
end;

{ TDigitoOperadorSuma }

function TDigitoOperadorSuma.DoOperar(AOperador1, AOperador2: Extended): Double;
begin
  Result:= AOperador1 + AOperador2;
end;

{ TDigitoOperadorResta }

function TDigitoOperadorResta.DoOperar(AOperador1,
  AOperador2: Extended): Double;
begin
  Result:= AOperador1 - AOperador2;
end;

{ TDigitoOperadorMulti }

function TDigitoOperadorMulti.DoOperar(AOperador1,
  AOperador2: Extended): Double;
begin
  Result:= AOperador1 * AOperador2;
end;

{ TDigitoOperadorDivision }

function TDigitoOperadorDivision.DoOperar(AOperador1,
  AOperador2: Extended): Double;
begin
  Result:= AOperador1 / AOperador2;
end;

{ TEtiqueta }

function TEtiqueta.Assign(ASource: TEtiqueta): TEtiqueta;
begin
  with Self do
  begin
    Lexema             := ASource.Lexema;
    Clase              := ASource.Clase;
    Representacion     := ASource.Representacion;
    EsOperadorInmediato:= ASource.EsOperadorInmediato;
  end;
  Result:= Self;
end;

function TEtiqueta.Init: TEtiqueta;
begin
  with Self do
  begin
    Lexema             := '';
    Clase              := Nil;
    Representacion     := '';
    EsOperadorInmediato:= False;
  end;
  Result:= Self;
end;

end.


Este es el código fuente: calculadorafmx_3

Finalizando la entrada: ahora te toca a tí…

 Si estás siguiendo la serie, te propongo una pausa de dos días en los que te invitaría a intentar poner en práctica lo que hemos comentado. Monta un pequeño proyecto que resuma estas tres entradas, y me lo remites por correo añadiendo alguna operación adicional o mejora. Tienes jueves y viernes para disfrutarlo.

La dirección de  correo es la asociada a este blog:

salvador[at]delphibasico[dot]com  (salvador[arroba]delphibasico[punto]com)

Viviendo #DelphiWeek!!!!!

PD: Si te decides remitirme el código no incluyas ejecutables. Solo las unidades pas y el dpr del proyecto.

Deja un comentario

Blog de WordPress.com.

Subir ↑

Marina Casado

Escritora y Doctora en Literatura Española. Periodista cultural. Madrid, España

Sigo aqui

Mi rincon del cuadrilatero, ahi donde al creer que me he rendido, aun sigo peleando.

Recetas y consejos nutricionales

Indicadas para personas con diabetes, recomendadas para todos.

¡Buen camino!

ANÉCDOTAS Y REFLEXIONES SOBRE UN VIAJE A SANTIAGO…

https://lfgonzalez.visiblogs.com/

Algunas reflexiones y comentarios sobre Delphi

It's All About Code!

A blog about Delphi, C++ Builder and related technologies...

The Podcast at Delphi.org

The Podcast about the Delphi programming language, tools, news and community.

Blog de Carlos G

Algunas reflexiones y comentarios sobre Delphi

The Road to Delphi

Delphi - Free Pascal - Oxygene

La web de Seoane

Algunas reflexiones y comentarios sobre Delphi

El blog de cadetill

Cosas de programación....... y de la vida

Delphi-losophy

A Lover of Delphic Wisdom

Delphi en Movimiento

Algunas reflexiones y comentarios sobre Delphi

marcocantu.blog

Algunas reflexiones y comentarios sobre Delphi

Press F9

Algunas reflexiones y comentarios sobre Delphi

El blog de jachguate

Un blog sobre tecnología y la vida en general