Analizador lexico para expresiones SQL

En esta ocasion dejo un ejemplo de un analizador lexico de expresiones SQL, es una clase. En el constructor de esta clase recibe como entrada una cadena SQL y devuelve cada vez que se le llama a su metodo obtener_token() el siguiente token de la cadena SQL. podria usarse como punto de entrada para el analizador sintactico de cualquiera de las 4 operaciones SQL; INSERT, DELETE, UPDATE o SELECT. El codigo de la clase es el siguiente:


//		Instituto Tecnologico de Zacatepec
//  En el constructor de esta clase recibe como entrada una cadena SQL y devuelve cada vez que se 
//  se llama a su metodo  obtenerToken() devuelve el siguiente token de la cadena SQL.
//  Author: Gonzalo Silverio   gonzasilve@hotmail.com

public class Tokens
{
	//### Atributos de la clase ####
   private StringBuffer tok;    //Para guardar el lexema del token
   private char car;          	//Almacena un caracter de la expresionSQL
   public String token;    		//Para que el token este disponible como String y no como StringBuffer
   public int intTipoToken;    		//Guarda el tipo de token en forma numerica
   public String strTipoToken = new String();		//Guarda el Tipo de token en forma de cadena 
	public String strInstruccionSQL;		//Guarda la cadena SQL que se esta analizando

   //Tipos de token en formato numerico
   public final int 	FIN_DE_ARCHIVO  	=  -1;
   public final int 	NUMERO_ENTERO   	=  2;
   public final int 	DELIMITADOR     	=  3;
   public final int  SALTO_DE_LINEA  	=  4;
   public final int  NINGUNO         	=  5;
   public final int  IDENTIFICADOR   	=  6;
   public final int  PALABRA_CLAVE   	=  7;
   public final int  CADENA          	=  8;
   public final int  PARENTESIS      	=  9;
   public final int 	NUMERO_DECIMAL  	=  10;
   public final int 	ASTERISCO     		=  11;
   public final int 	ERROR   				=  12;
   public final int 	OPERADOR				=	13;

	//indice  para recorrer caracter por caracter la expresionSQL
   private int indexExpSQL;     

   //Arreglo de palabras clave
   private static String palabras_clave[] = {"update","insert","delete","select","set","from","where","and","or","not","into","values","group","having","by","between"};

    
    //             TIPOS DE TOKEN     
    //     Tipos de token que se reconocen...
    // 
    //    --Tipo de token--      --Incluye--
    //    Delimitador          Signos de puntuacion punto (.) y la coma (,)
    //    Palabras clave       Palabras clave
    //    Cadena               Cadenas entre comillas simples
    //    Identificador        Nombre de variable (nombres de tablas, campos, etc)  o nombre de funcion (de agregacion)
    //    Numero_entero        Constante numerica entera
    //    Numero_decimal       Constante numerica decimal
    //    Operador			 	 Operadores aritmeticos y relacionales
        

	 Tokens(String cadSQLAParsear)
	 {
	     strInstruccionSQL = cadSQLAParsear+"; ";		//Agregamos punto y coma ;
	     //Inicializar con por default los atributos
	     car = ' ';
	     indexExpSQL = 0;      //Iniciar analisis en el primer caracter de la cadena
	     tok  =  new StringBuffer();
		  token = "";
	     leerCaracter();
	 }
	//Constructor vacio
	Tokens()
	{

	}

	//Metodo que lee el siguiente token de la cadena SQL
    public void obtenerToken()
    {
    	tok = new StringBuffer();		// Reinicializar variable para leer otro token
    	token = new String();		// Reinicializar variable para leer otro token  
    	intTipoToken = NINGUNO;                 //Por defecto no es ningun tipo de token
    	strTipoToken = "ninguno";
		token = "";

        // SALTARSE ESPACIOS EN BLANCO Y TABULADORES  
        for(;; leerCaracter())
         {
            if( (char)car == ' ' || (char)car == '\t' )
               continue;
            else
               break;
         }
       
			//Comprobar si es un operador relacional
        switch(car)
        {
           case '<':
              if(leerCaracter('>'))
              {
                 tok.append('<');
                 tok.append('>');
              }
              else if( leerCaracter('=') )
              {
                 tok.append('<');
                 tok.append('=');
              }
              else
                 tok.append('<');
              break;

           case '>':
              if(leerCaracter('='))
              {
                 tok.append('>');
                 tok.append('=');
              }
              else
                 tok.append('>');
              break;

           case '!':						//Operador No
              if(leerCaracter('='))
              {
                 tok.append('!');
                 tok.append('=');
              }
              else
                 tok.append('!');
              break;

        }      //Fin comprobbar si es operador relacional
    
		 //Si la variable tiene algo se leyo un operador relacional
       if(tok.length()>0)
       {
          intTipoToken = OPERADOR;
          strTipoToken = "operador";
			 token = tok.toString().toLowerCase();
          leerCaracter();
          return;
       }

			//Comprobar si es un operador aritmetico
       String delims = new String("+-=");
       int d = delims.indexOf(car);
       if( d >= 0 )
       {
          tok.append(car);
          intTipoToken = OPERADOR;           //Es un OPERADOR (aritmetico)
          strTipoToken = "operador";
			 token = tok.toString().toLowerCase();
          leerCaracter();
          return;
       }

			//comprobar si es un delimitador
       delims = new String(",.;");
       d = delims.indexOf(car);
       if( d >= 0 )
       {
          tok.append(car);
          intTipoToken = DELIMITADOR;           //Es un delimitador punto o coma
          strTipoToken = "delimitador";
			 token = tok.toString().toLowerCase();
          leerCaracter();
          return;
       }

			//Comprobar si son parentesis
       String parentesis = new String("()");
       int p = parentesis.indexOf(car);
       if( p >= 0 )
       {
          tok.append(car);
          intTipoToken = PARENTESIS;           //Es un parentesis
          strTipoToken= "parentesis";
			 token = tok.toString().toLowerCase();
          leerCaracter();
          return;
       }

		// Comprobar si es una cadena
		// La cadena, se guarda sin comillas 
       if( (char)car == '\'' )
       {
           leerCaracter(); 	// Saltarse la comilla de apertura 
          while((char)car != '\'')
          {
              tok.append(car);
              leerCaracter();
          }
          leerCaracter();			// Saltarse la comilla de cierre

          intTipoToken = CADENA;
          strTipoToken= "char";
			 token = tok.toString().toLowerCase();
          return;
       }
       
       //Comprobar Si es una identificador de usuario o una palabra clave de SQL
       if(Character.isLetter((char)car))
       {
          do
          {
             tok.append(car);
             leerCaracter();
          } while(Character.isLetterOrDigit( (char)car) || (char)car == '-' || (char)car == '_' || (char)car == '.' );

           if( contarPuntos(tok)>1 )
           {
              mensaje_error("El identificador \'" +tok.toString()+ "\' tiene demasiados puntos");
              intTipoToken = ERROR;
              strTipoToken = "error";
				  token = tok.toString().toLowerCase();
              return;
           }
           
             //comprobar si es una palabra clave, sino, entonces es un identificador
          if( buscar_keyword( tok.toString().toLowerCase() ) )
          {
             intTipoToken = PALABRA_CLAVE;
             strTipoToken = "keyword";
				 token = tok.toString().toLowerCase();
          }
          else
          {
             intTipoToken = IDENTIFICADOR;
             strTipoToken = "identificador";
				 token = tok.toString().toLowerCase();
          }
        return;      
   	}
       
			//Si es un numero (entero o decimal)
	   if(Character.isDigit((char)car))
	   {      
	      do
	      {
	         tok.append(car);
	         leerCaracter();
	      } while(Character.isDigit( (char)car) || (char)car == '.' );
	      
				//Comprobar si es un entero
	      if(contarPuntos(tok) == 0 )
	      {
	          if(esEntero(tok.toString()))
	          {
	          	intTipoToken = NUMERO_ENTERO;
	          	strTipoToken = "int";
					token = tok.toString().toLowerCase();
	          }	              
	      }		//Comprobar si es un decimal
	      else if(contarPuntos(tok) == 1)
	      {
	          if(esDecimal(tok.toString()))
	          {
	          	intTipoToken = NUMERO_DECIMAL;
	          	strTipoToken = "decimal";
					token = tok.toString().toLowerCase();
	          }	              
	          else
	          {
	              //Informar al usuario del error de sintaxis en el decimal
	              mensaje_error("El token \'" +tok.toString()+ "\' parece un decimal pero tiene un error de sintaxis");
	              intTipoToken = ERROR;
	              strTipoToken = "error";
					  token = tok.toString().toLowerCase();
	          }
	      }
	      else if(contarPuntos(tok) > 1 )             //Si tiene mas de un punto
	      {
	          //Informar a usuario que un decimal no puede tener mas de 1 caracter punto
	          mensaje_error("El token \'" +tok.toString()+ "\' tiene mas de un punto");
	          intTipoToken = ERROR;
	          strTipoToken = "error";
				 token = tok.toString().toLowerCase();
	      }
	      return;
	   }          //Fin de validar si es un numero
       
			//Comprobar si es asterisco
	   if((char)car == '*' )
	   {
	      tok.append(car);
	      intTipoToken = ASTERISCO;
	      strTipoToken = "asterisco";
			token = tok.toString().toLowerCase();
	      leerCaracter();
	      return;
	   }
  }          //Fin del metodo obtenerToken

   //Devuelve true si en una cadena que llega todos son numeros, false en caso contrario
    public boolean esEntero(String cad)
    {
        for(int i = 0; i<cad.length(); i++)
            if( !Character.isDigit(cad.charAt(i)) )
                return false;

        return true;
    }

    //Devuelve true si la cadena que llega tiene la sintaxis de un decimal
    public boolean esDecimal(String cad)
    {
        boolean hayPunto=false;
        StringBuffer parteEntera = new StringBuffer();
        StringBuffer parteDecimal = new StringBuffer();
        int i=0, posicionDelPunto;

        for( i=0;i<cad.length(); i++ )
            if ( cad.charAt(i) == '.')                          //Detectar si hay un punto decimal en la cadena
                hayPunto=true;
        if(hayPunto)                                            //Si hay punto guardar la posicion donde se encuentra el carater punto
            posicionDelPunto=cad.indexOf('.');                  //(si la cadena tiene varios puntos, detecta donde esta el primero).
        else
            return false;                                       //Si no hay punto; no es decimal

        if( posicionDelPunto == 0 || posicionDelPunto == cad.length()-1  )    //Si el punto esta al principio o al final no es un decimal
            return false;

        for( i=0;i<posicionDelPunto; i++ )
            parteEntera.append(cad.charAt(i)) ;                 //Guardar la parte entera en una variable

        for(i = 0; i<parteEntera.length(); i++)
            if( ! Character.isDigit(parteEntera.charAt(i)) )    //Si alguno de los caracteres de la parte entera
                return false;									//no son digitos no es decimal

        for( i=posicionDelPunto+1;i<cad.length(); i++ )
            parteDecimal.append(cad.charAt(i));                 //Guardar la parte decimal en una variable

        for(i = 0; i<parteDecimal.length(); i++)
            if( ! Character.isDigit(parteDecimal.charAt(i)) )   //Si alguno de los caracteres de la parte decimal no es un digito no es decimal
                return false;                                   //Incluye el caso en el que la cadena tenga dos o mas puntos

        return true;                                            //Si paso todas las pruebas anterior, la cadena es un Numero decimal
    }
    
   //Lee un caracter de la cadenaSQL
   void leerCaracter()  throws StringIndexOutOfBoundsException
   {     
      
            car = strInstruccionSQL.charAt(indexExpSQL);		/* leer siguiente caracter */
                  
            //Verificar si se a sobrepasado el indice maximo de caracteres en la expresion SQL
            if( indexExpSQL < strInstruccionSQL.length() )
               indexExpSQL++;
            else
               indexExpSQL = strInstruccionSQL.length()-1;
   }

	// lee un caracter x adelantado de la cadena SQL, el caracter que llega lo compara con
	//el caracter leido y si son iguales devuelve true, si son diferentes devuelve false
   boolean  leerCaracter(char c)
   {
      leerCaracter();
      if( (char)car != (char)c )
      {
          indexExpSQL--;
          return false;
      }         
      
      car = ' ';
      return true;
             
   }

   //Cuenta el numero de caracteres punto que tiene una cadena especificada
   public int contarPuntos(StringBuffer cad)
   {
       int contador = 0;
       for(int i=0;i<cad.length();i++)
       {
           if(cad.charAt(i)=='.')
               contador++;
       }
       return contador;
   }
   
   //Devuelve true si la cadena que llega es una palabra clave, false en caso contrario
   //busca en la tabla de palabras clave la cadena que llega
   static boolean buscar_keyword(String identificador)
   {
      for(int i = 0;i< palabras_clave.length;i++)
      {
         if( palabras_clave[i].equals(identificador) )
               return true;
      }
      return false;
   }
   
	// Muestra un msg de error en el area de msgs del parser 
   public void mensaje_error(String strMsg)
   {
       System.out.print("\n" + strMsg );
   }   

}		//Fin de la clase Tokens.java

Como veo que a mas de uno le a resultado interesante mi clase a continuacion doy una breve explicacion de la clase Tokens. La Clase Tokens como se puede observar, tiene varios atributos pero solo 4 son publicos, los cuales son token,intTipoToken,strTipoToken,strInstruccionSQL de estos solo los primeros 3 son de utilidad para usarse para hacer un analizador sintacto de alguna sentencia SQL. Ademas de estos 4 atributos hay un unico metodo llamado obtenerToken() que no devuelve ningun valor (void). La clase UML se ve mas o menos asi si omitimos los demas atributos…
Clase Tokens

Cada vez que se llama al metodo las 3 variables que mencione arriba quedan con los valores que nos interesan, por ejemplo, suponiendo que la operacion SQL que se le pasa a esta clase es:

INSERT INTO alumnos VALUES('07680654','Carlos','Zarate', 23,78.0)

Para pasarle esta cadena SQL se debe crear una instancia de la clase, algo asi:

Tokens pruebaTokens = new Tokens("INSERT INTO alumnos VALUES('07680654','Carlos','Zarate', 23,78.0)");

y al llamar al metodo obtenerToken() del objeto pruebaTokens, las tres variables quedarian con los valores:

Variable Valor
token “insert”
intTipoToken PALABRA_CLAVE
strTipoToken “keyword”

si se llama al metodo por segunda vez, ahora los valores de estas variables son:

Variable Valor
token “into”
intTipoToken PALABRA_CLAVE
strTipoToken “keyword”

Si se llama al metodo por tercera vez, ahora los valores de estas variables son:

Variable Valor
token “alumnos”
intTipoToken IDENTIFICADOR
strTipoToken “identificador”

Las constantes sirven para tener una representacion en forma numerica de cada uno de los tokens, los puse por si me servian de algo mas adelante, y vaya que si me sirvieron. Ademas, como se observa hay un arreglo que contiene las palabras clave de SQL, si me falto alguna o se desean agregar mas (por ej. para ampliar las de SQL) pues ahi es donde se agregarian. Aca adelante un ejemplo del uso de esta clase…


//  	Instituto Tecnologico de Zacatepec, Morelos, Mexico
// Prueba de la clase Tokens
// Author:  Gonzalo Silverio   gonzasilve@gmail.com

public class PruebaTokens
{
   //Tipo de token que puede haber en la cadena
   public final int 	FIN_DE_ARCHIVO  =  -1;
   public final int 	NUMERO_ENTERO          =   2;
   public final int 	DELIMITADOR     =   3;
   public final int   	SALTO_DE_LINEA  =   4;
   public final int   	NINGUNO         =   5;
   public final int   	IDENTIFICADOR   =   6;
   public final int   	PALABRA_CLAVE   =   7;
   public final int   	CADENA          =   8;
   public final int   	PARENTESIS      =  9;
   public final int 	NUMERO_DECIMAL   =   10;
   public final int 	ASTERISCO   =   	11;
   public final int 	ERROR   =   	12;
   
   //El constructor espera recibir una cadena sql de la cual se quieren extraer sus tokens
   private void analizarCadenaSQL(String cadSQL)
   {
      Tokens tokens = new Tokens(cadSQL);

//      System.out.println("\n\n");
      //Este bucle imprime los tokens que se van leyendo y el tipo de token que se leyo de la cadena SQL
      tokens.obtenerToken();		//Obtener el primer token de la cadena SQL
      while( ! tokens.token.equals(";") )
      {
         switch(tokens.intTipoToken)
         {
            case NUMERO_ENTERO:
               System.out.println(tokens.token+" \t\t\tNUMERO_ENTERO");
               break;
            case NUMERO_DECIMAL:
               System.out.println(tokens.token+" \t\t\tNUMERO_DECIMAL");
               break;
            case PALABRA_CLAVE:
               System.out.println(tokens.token+" \t\t\tPALABRA_CLAVE");
               break;
            case IDENTIFICADOR:
               System.out.println(tokens.token+" \t\t\tIDENTIFICADOR");
               break;
            case CADENA:
               System.out.println(tokens.token+" \t\t\tCADENA");
               break;
            case PARENTESIS:
               System.out.println(tokens.token+" \t\t\tPARENTESIS");
               break;
            case DELIMITADOR:
               System.out.println(tokens.token+" \t\t\tDELIMITADOR");
               break;
            case ASTERISCO:
               System.out.println(tokens.token+" \t\t\tASTERISCO");
               break;
            case ERROR:
               System.out.println(tokens.token + " \t\t\t<-- ERROR !");
               break;
         }
			tokens.obtenerToken();
		}
   }

   public static void main(String args[])
   {
      PruebaTokens prueba1 = new PruebaTokens();
      prueba1.analizarCadenaSQL("insert into s values('s1','ford','mty', 23,58.42)");
   }
}

y el resultado de la ejecucion es la siguiente:

gonzasilve

Si les aparece algun error al compilar o desean el fuente, les puedo pasar el .java, basta con dejar un comentario con su e-mail

hasta la proxima, espero comentarios….
}

Acerca de gonzasilve
Freelance Web Developer.

107 Responses to Analizador lexico para expresiones SQL

  1. Anthony melgar dice:

    Me puedes pasar en .java por favor

  2. Allan Perigault dice:

    Para ver si me podia mandar el algoritmo a mi correo peri_sensacion@hotmail.com

  3. javier luna dice:

    genio podrias compartirme los documentos que conforman tremendo trabajo? javiereluna@gmail.com , desde ya muchas gracias

  4. Alberto dice:

    hola, me podrias proporcionar las fuentes y/o .java por favor, este es mi correo knowles_beto@hotmail.com

  5. axstalin dice:

    sería de gran ayuda, las fuentes, gracias de antemano mi correo es steduardito4@gmail.com,

  6. sapporo11 dice:

    Oye amigo, en el void leerCaracter() throws StringIndexOutOfBoundsException. Aunque tengo conocimiento de esta excepción, no conocía el uso que le estás dando.

    ¿Serías tan amable de explicarme esa sección de tu código?
    Gracias de antemano.

  7. Mauricio A. dice:

    Hola, crees que me puedas enviar el .java :D? Por favor jeje
    mauricio_288@live.com.mx

  8. Jesus dice:

    matajc9@gmail.com alguien que siga activo que me pueda enviar el archivo

  9. lala dice:

    Hoola, podrias ayudarme con un alaizador léxico para otro tipo de lenguaje plis. Gracias Dglubc@gmail.com

  10. Marlon Herrera dice:

    Hola Gonzalo, muchas gracias por tu aporte. Si puedes regalarme el fuente a marlon382@yahoo.com
    Agradecido de antemano. 😉

  11. codigo dice:

    hola como estás, excelente blog. yo ando buscando ese codigo. será que si me lo puedes enviar mi correo es andydar_20@outlook.com

  12. wramirez dice:

    Me lo podrias compartir, mi email es edied.ramirez@hotmail.com

    Gracias esta muy bueno.

    Saludos,

  13. amigo puedes pasarmelo este es mi correo marcoj20@hotmail.com

  14. Alejandro Diaz dice:

    Podrias mandame el codigo? Gracias Viejo jose_futbol14@hotmail.com

  15. jose miguel dice:

    DE casualidad sabrás
    como se realiza en delphi? he estado buscanddo pero bien explicado no sale nada

  16. Aquiles MH dice:

    te voy agradecer 1000,000 por favor enviame el codigo no logro realizar test. solucciones.it@gmail.com

  17. andres dice:

    Mi hermano me colabora con el codigo andres.fonbotello@gmail.com

  18. edwin dice:

    enviame el codigo fuente porfa a sotzedwin@yahoo.com

  19. hugoallan9 dice:

    Me podrías enviar el código, mi correo es hugoallangm@gmail.com

  20. Eduardo dice:

    Oye amigo podrias mandarme el codigo porfa me salvarias el semestre 😉 😀 ….. mi correo es eduardin_11@hotmail.com

    • gonzasilve dice:

      Amigo Eduardo al menos estudialo y estudia como funciona, agregale alguna que otra cosilla, te lo envie,

      Saludos

  21. carmen dice:

    podrias enviarmelo

    • gonzasilve dice:

      Hey Carmen, ya te lo envie espero te sirva

      Saludos!

  22. Sergio dice:

    Hola, muy buen articulo, me podrias enviar el .java?
    quisiera fucionar tu programa con uno que estoy desarrollando 😀
    mi correo es Chello_raul@hotmail.com

    • gonzasilve dice:

      Serch, te lo acabo de mandar, espero te sirva

      Saludos

  23. samuel dice:

    hola me lo podrias enviar tambien a mi te lo agradecere mucho mi correo es samuelmolina@outlook.com

    • gonzasilve dice:

      Claro que si Samuel, te lo envie ya

      Saludos

      • samuel molina dice:

        Gracias Amigo!!!! bendiciones!!! estoy por probarlo!!!

        • gonzasilve dice:

          no, gracias a ti por visitar.

          Saludos

  24. Pedro Vega dice:

    Hermanito esta excelente el codigo yo estoy haciendo uno parecido para sql en jcup pero me podrias enviar el .java para guiarme moejor te agradeceria saludos

    • gonzasilve dice:

      Amigo Peter, te lo acabo de adjuntar y enviar.

      Saludos y suerte!

  25. reneosolares dice:

    GRACIAS AMIGO MUY BUEN APORTE ME PUEDES MANDAR .JAVA AL CORREO
    reneosolares@gmail.com

    • gonzasilve dice:

      Ok, ya te lo envie amigo y disculpa por la demora.

      Saludos

  26. muy buen codigo carnal yo estoy haciendo uno, pero para reconocer instrucciones de ensamblador :s y tu codigo me ha servido para darme una idea de como hacerlo muchas gracias por subirlo ….

    • gonzasilve dice:

      Hey Jonathan gracias por visitar, que bueno que te dio alguna idea. Echale ganas, Saludos.

      • Hola que tal oie solo una duda sobre el metodo contarPuntos es que biene el for incompleto y no entiendo muy bien :s podrias terminar la linea del for ??? o explicarme en si lo que hace??

        • gonzasilve dice:

          Tienes razon, wordpress corto esa parte del codigo, desconozco el porqué, pero ya lo corregi.
          Para que sirve: en la linea 201 del metodo obtenerToken() uso ese metodo para determinar si un identificador tiene mas de un punto (lo cual yo considere como un error de sintaxis), puedes comentar de la linea 201 a 208 para omitir esa verificacion.

          En el archivo que te mande ya va completo el codigo.

          Saludos

          • Hola disculpa podrias enviarme el codigo?? para ver la corrida ?? por favor jjgonzalez.gnr@gmail.com Ok

            • gonzasilve dice:

              ..te lo envie. revisa tu inbox. Saludos

              • Genial muchas gracias por tu aporte man enserio 🙂

                Disculpa no se si sea mucha molestia pero quería ver si me podrías resolver una duda lo que pasa es que tengo un código que lee un archivo de texto y extrae palabras de el y las compara con unos arreglos String y los clasifica y eso ya lo tengo echo lo único que me falta es que me diga el numero de linea que se encuentra la palabra imagino que esta fácil de resolver pero no logro hacerlo si tienes tiempo podrías ayudarme ??

                De antemano muchas gracias por tu atención 🙂

  27. Daper dice:

    me podrias enviar a mi tambien el codigo fuente, te lo agredeceria mucho… my-space.black@hotmail.com

    • gonzasilve dice:

      ..te lo mande. Saludos Dap

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: