I/O: Input / Output

En esta sección vamos a tratar sobre cómo se transfieren datos, de entrada y salida, a tres entornos distintos: consola, archivos de texto y bases de datos.

Consola

La clase FileStream, que también veremos al operar con archivos, en consola se instancia con los objetos stdin and stdout, mientras que gets() es un método de la clase FileStream.

Recuerda que stdin y stdout significan 'standard input' y 'standard output' respectivamente.

Varias posibilidades:

init
	// creamos una matriz de caracteres
	var a = new array of char[64]
 
	stdout.printf( "¿Cómo te llamas? " )
 
	// stdin.gets(a) lee una línea del teclado
	// y la almacena en la matriz de caracteres.
	stdin.gets(a)
 
	// 2 formas distintas para extraer la información del array:	 
 
	// 1. convertimos los caracteres en un string
	s:string = (string)a
	stdout.printf( "Hola %s", s )
 
	// 2. extraemos los caracteres con un bucle
	stdout.printf( "Hola " )
	for c in a		
		stdout.printf ("%c", c)
 
	// más fácil todavía (al revés que en el circo)
	stdout.printf( "Tu nombre, por favor: ")
 
	// lee la linea
	nombre:string = stdin.read_line()
	print "Hola " + nombre + ", gracias por visitar Genie Doc."

Respecto a stdout, es importante conocer los formatos admitidos para cada tipo de datos.

Se utiliza el prefijo % seguido del carácter o caracteres correspondientes al tipo de dato utilizado.

init	
	texto:string = "texto"
 
	caracter:char = 'a'
	caracter_ASC:uchar = 'a'
	caracter_no_ASC:unichar = 'á'
 
	entero:int = -10
	entero_positivo:uint = 10
	largo:long = -87654454
	largo_positivo:ulong = 87654454
 
	decimales:float = 0.1234f
	decimales2: double = 23.43546544
	exponencial:double = 453
 
	// entrada-salida de distintos tipos de datos y con distintos formatos
 
	//texto y caracteres
	stdout.printf( "entrada: string | salida: %%s | texto = %s", texto )  // texto
	print ""
	stdout.printf( "entrada: char | salida: %%c | a = %c", caracter )  // carácter
	print ""
	stdout.printf( "entrada: uchar | salida: %%huu | a = %huu", caracter_ASC ) //posición decimal ASCII
	print ""
	stdout.printf( "entrada: unichar | salida: %%uc | á = %uc", caracter_no_ASC ) // posición códigos no ASCII
	print ""
	// número enteros
	stdout.printf( "entrada: int | salida: %%i | -10 = %i", entero )  // entero
	print ""
	stdout.printf( "entrada: entero | salida: %%d | -10 = %d", entero ) // entero
	print ""
	stdout.printf( "entrada: uint | salida: %%u | 10 = %u", entero_positivo ) // entero positivo
	print ""	
	stdout.printf( "entrada: long | salida: %%li | 87654454 = %li", largo )	// números grandes (32 bits)
	print ""
	stdout.printf( "entrada: ulong | salida: %%lu | 87654454 = %lu", largo_positivo )  // números grandes positivos
	print ""
	// decimales y exponenciales
	stdout.printf( "entrada: float | salida: %%f | 0.1234f = %f", decimales ) //números con 6 decimales
	print ""
	stdout.printf( "entrada: float | salida: %%.2f | 0.1234f = %.2f", decimales ) // números con 2 decimales
	print ""	
	stdout.printf( "entrada: double | salida: %%g | 23.43546544 = %g", decimales2 ) // número con 4 decimales
	print ""	
	stdout.printf( "entrada: double | salida: %%e | 453 = %e", exponencial ) // exponenciales

Este código devuelve:

entrada: string | salida: %s | texto = texto
entrada: char | salida: %c | a = a
entrada: uchar | salida: %huu | a = 97u
entrada: unichar | salida: %uc | á = 225c
entrada: int | salida: %i | -10 = -10
entrada: entero | salida: %d | -10 = -10
entrada: uint | salida: %u | 10 = 10
entrada: long | salida: %li | 87654454 = -87654454
entrada: ulong | salida: %lu | 87654454 = 87654454
entrada: float | salida: %f | 0.1234f = 0.123400
entrada: float | salida: %.2f | 0.1234f = 0.12
entrada: double | salida: %g | 23.43546544 = 23.4355
entrada: double | salida: %e | 453 = 4.530000e+02

En el código anterior se observa que para poder imprimir el carácter % en stdout he tenido que escribir dos %. Se trata de una secuencia de escape, como éstas:

init		
	// con \n hacemos un salto de línea
	stdout.printf( "Aprendiendo a programar\n" )
	stdout.printf( "con Genie.\n" )	
 
	// con \t escribimos el espacio de un tabulador
	stdout.printf( "\tAprendiendo a programar " )  //observa que aquí no hay salto de línea
	stdout.printf( "con Genie.\n" )
 
	// con \r hacemos retroceso hasta el inicio
	stdout.printf( "Reprendiendo\rAprendiendo a programar.\n" )
 
	// con \b hacemos un retroceso
	stdout.printf( "Aprendiendof\b a programar.\n" )
 
	// con \\ podemos imprimir \ 
	stdout.printf( "blanco \\ negro\n" )
 
	// con %% podemos imprimir %
	stdout.printf( "blanco %% negro" )

Archivos

Genie ofrece una serie de utilidades para operar con archivos, básicamente se trata de dos clases de funciones: FileUtils y FileStream con sus correspondientes métodos.

FileUtils

La clase FileUtils es un grupo de funciones que incluyen los siguientes métodos:

  • get_contents
  • set_contents
  • test
  • open_tmp
  • read_link
  • mkstemp
  • rename
  • unlink
  • chmod
  • symlink

Un ejemplo para cambiar el nombre a un archivo:

init	
	var nombre_nuevo = "prueba.txt"
 
	FileUtils.rename("/home/jcv/Programacion/Genie/data.txt" , nombre_nuevo)

Un ejemplo para leer un archivo:

init
	s:string
	len:ulong
 
	FileUtils.get_contents("/home/jcv/Programacion/Genie/data.txt",out s, out len)
 
	print s
	print "%lu", len

Con este código extraemos el contenido del archivo 'data.txt' y el número de caracteres que ocupa. El tipo de datos ulong se utiliza para número enteros muy grandes (64-bit).

Y para escribir un archivo:

init
	texto:string = "Escrito con Genie."
 
	FileUtils.set_contents("/home/jcv/Programacion/Genie/data.txt", texto)

FileStream

Con la clase FileStream también podemos manejar archivos.

init
	var archivo = FileStream.open("/home/jcv/Programacion/Genie/data.txt","r")
	var caracter = new array of char[100]
	while archivo.gets(caracter) != null
		for x in caracter
			stdout.printf ("%c", x)

Ahí estamos leyendo el archivo por caracteres, pero también podemos leerlo por líneas:

init
	var archivo = FileStream.open("/home/jcv/Programacion/Genie/data.txt","r")
 
	var linea = archivo.read_line()
 
	while linea != null
		print linea
		linea = archivo.read_line()

En los ejemplos anteriores, abrimos el archivo en modo lectura (“r”), pero también podemos abrirlos en modo escritura (“w”).

init
	// abrimos el archivo
	// si no existe, lo crea
	var archivo = FileStream.open("/home/jcv/Programacion/Genie/data.txt","w")	
 
	archivo.rewind () // manda el cursor al inicio	
	archivo.puts ("Hola mundo.")
 
	archivo.rewind ()  // volvemos al principio	
	archivo.puts ("Escribiendo con Genie.")	
	archivo.puts (" Seguimos escribiendo en la misma línea.")	
	archivo.puts ("\n\nAhora escribimos dos líneas más abajo.")

Bases de datos

SQLite es una biblioteca de C que implementa un motor de base de datos SQL, lo que nos proporciona una manera ordenada de almacenar datos y de rápida consulta.

Para operar en una base de datos utilizamos comandos SQL estándar, entre los que podemos distinguir tres tipos:

  • Comandos de definición. Proporcionan la estructura y métodos de almacenamiento en la base de datos: CREATE, ALTER, DROP.
  • Comandos de manipulación. Permiten añadir, modificar y eliminar los datos: INSERT, UPDATE, DELETE.
  • Comandos de consulta. Permiten recuperar datos específicos de la base de datos: SELECT.

En C estos comandos SQL se definen según métodos específicos (ver C-language Interface Specification for SQLite). Algunos de los métodos esenciales usados en C para operar con SQLite son: sqlite3_open(), sqlite3_prepare(), sqlite3_bind(), sqlite3_step(), sqlite3_column(), sqlite3_finalize(), sqlite3_close(), qlite3_exec() (ver lista completa).

Veremos cómo usar algunos de esos métodos en Genie para crear y operar con SQLite.

No olvides compilar con:

valac --pkg sqlite3 archivo.gs

Y para comprobar los resultados en la base de datos puede venir bien alguna aplicación como SQLiteBrowser, disponible para Windows, Mac y Linux (en la mayoría de repositorios).

Crear base de datos

init
	// Creamos una base de datos vacía (si no existe)
	// sin estructura ni datos
	db : Sqlite.Database
	Sqlite.Database.open ("agenda.db3", out db)  // especifica nombre y tipo de archivo	

Crear estructura

init
	db : Sqlite.Database
	Sqlite.Database.open ("agenda.db3", out db)
 
	// creamos la tabla contactos
	// con 3 columnas: id, nombre y phone
	// cada campo se definde por un nombre y un tipo de datos
	db.exec ("CREATE TABLE Contactos (iD INTEGER PRIMARY KEY, nombre TEXT, phone INTEGER)")

Sobre los tipos de datos se puede leer Datatypes In SQLite Version 3.

Introducir datos

init
	db : Sqlite.Database
	Sqlite.Database.open ("agenda.db3", out db)
 
	db.exec ("CREATE TABLE Contactos (iD INTEGER PRIMARY KEY, nombre TEXT, phone INTEGER)")
 
	// introducimos datos
	db.exec ("""INSERT INTO Contactos (nombre, phone) VALUES ("Pepe", 915456456)""")
	db.exec ("""INSERT INTO Contactos (nombre, phone) VALUES ("Alicia", 967485987)""")

Otra manera de introducir datos, a través de consola:

init
	db : Sqlite.Database
	Sqlite.Database.open ("agenda.db3", out db)
 
	db.exec ("CREATE TABLE Contactos (iD INTEGER PRIMARY KEY, nombre TEXT, phone INTEGER)")
 
	stdout.printf( "Nuevo contacto: " )
	contacto_nombre:string = stdin.read_line()
 
	stdout.printf( "Teléfono: " )
	contacto_phone:string = stdin.read_line()
 
	introducir:string = "INSERT INTO Contactos (nombre, phone) VALUES ('Anonimo', 000000000)"
	introducir = introducir.replace("Anonimo", contacto_nombre)
	introducir = introducir.replace("000000000", contacto_phone)
 
	db.exec (introducir)

Mejorando el código:

init	
	db : Sqlite.Database
	Sqlite.Database.open ("agenda.db3", out db)	
 
	db.exec ("CREATE TABLE Contactos (iD INTEGER PRIMARY KEY, nombre TEXT, phone INTEGER)")
 
	stdout.printf( "Nuevo contacto: " )
	contacto_nombre:string = stdin.read_line()
 
	stdout.printf( "Teléfono: " )
	contacto_phone:string = stdin.read_line()
 
	introducir:string = @"INSERT INTO Contactos (nombre, phone) VALUES ('$contacto_nombre', $contacto_phone)"
 
	db.exec (introducir)

Vemos que todas estas maneras de introducir datos funcionan, pero ninguna de ellas comprueba si ya existen los mismos valores, y los puede duplicar.

Para solucionarlo podemos añadir el parámetro 'UNIQUE' al campo nombre cuando creamos la tabla:

db.exec ("CREATE TABLE Contactos (iD INTEGER PRIMARY KEY, nombre TEXT UNIQUE, phone INTEGER)")

Antes de ejecutar nuevamente el código, eliminamos la base de datos y entonces comprobamos que ahora la base de datos no permite introducir dos nombres iguales (se queda con el primero).

Hacer consulta

Pero estaría bien que avisara de que el valor ya existe. Una solución puede ser:

// compilar con valac --pkg sqlite3 --pkg gee-0.8 archivo.gs
uses
	Sqlite
	Gee
 
init	
	db : Sqlite.Database
	Sqlite.Database.open ("agenda.db3", out db)
 
	db.exec ("CREATE TABLE Contactos (iD INTEGER PRIMARY KEY, nombre TEXT UNIQUE, phone INTEGER)")
 
	stdout.printf( "Nuevo contacto: " )
	contacto_nombre:string = stdin.read_line()
 
	// hacemos una consulta
	statement:Statement
	db.prepare_v2("SELECT nombre FROM Contactos", -1, out statement)
 
	cols:int = statement.column_count ()	
	var row = new dict of string, string
	item:int = 1
 
	// creamos una lista para guardar los resultados de la consulta
	var lista = new list of string
	// recorremos los valores de 'nombre'
	// y los añadimos a la lista
	while statement.step() == ROW
		for i:int = 0 to (cols - 1)
			row[ statement.column_name( i ) ] = statement.column_text( i )
			lista.add(row[ "nombre" ])
		item++
 
	// comprobamos si el nombre introducido está en la lista
	if lista.contains(contacto_nombre) == true
		stdout.printf("%s ya está en la Agenda.\n", contacto_nombre)
	else
		stdout.printf( "Teléfono: " )
		contacto_phone:string = stdin.read_line()
		enter:string = @"INSERT INTO Contactos (nombre, phone) VALUES ('$contacto_nombre', $contacto_phone)"
		db.exec (enter)

Mejorando el código:

// compilar con valac --pkg sqlite3 --pkg gee-0.8 archivo.gs
uses
	Sqlite
	Gee
 
init	
	db : Sqlite.Database
	Sqlite.Database.open ("agenda.db3", out db)
 
	db.exec ("CREATE TABLE Contactos (iD INTEGER PRIMARY KEY, nombre TEXT UNIQUE, phone INTEGER)")
 
	stdout.printf( "Nuevo contacto: " )
	contacto_nombre:string = stdin.read_line()
 
	// ahora en la consulta incorporamos ambos campos
	statement:Statement
	db.prepare_v2("SELECT nombre, phone FROM Contactos", -1, out statement)
 
	cols:int = statement.column_count ()
 
	var row = new dict of string, string
	item:int = 1
	// creamos un diccionario
	var agenda = new dict of string,string
 
	while statement.step() == ROW
		for i:int = 0 to (cols - 1)
			row[ statement.column_name( i ) ] = statement.column_text( i )			
			agenda[row[ "nombre" ]] = row[ "phone" ]							
		item++		
 
	//if agenda.contains(contacto_nombre) == true  // OBSOLETO
	if agenda.has_key(contacto_nombre) == true
		stdout.printf("%s ya está en la Agenda.\n", contacto_nombre)
		stdout.printf("Su teléfono es %s\n", agenda[contacto_nombre])
	else
		stdout.printf( "Teléfono: " )
		contacto_phone:string = stdin.read_line()
		enter:string = @"INSERT INTO Contactos (nombre, phone) VALUES ('$contacto_nombre', $contacto_phone)"
		db.exec (enter)

Modificar datos

Pero además de avisar de que existe un valor, sería mejor si nos diera la oportunidad de editar datos.

// compilar con valac --pkg sqlite3 --pkg gee-0.8 archivo.gs
uses
	Sqlite
	Gee
 
init	
	db : Sqlite.Database
	Sqlite.Database.open ("agenda.db3", out db)
 
	db.exec ("CREATE TABLE Contactos (iD INTEGER PRIMARY KEY, nombre TEXT UNIQUE, phone INTEGER)")
 
	stdout.printf( "Nuevo contacto: " )
	contacto_nombre:string = stdin.read_line()
 
	statement:Statement
	db.prepare_v2("SELECT nombre, phone FROM Contactos", -1, out statement)
 
	cols:int = statement.column_count ()
 
	var row = new dict of string, string
	item:int = 1
 
	var agenda = new dict of string,string
 
	while statement.step() == ROW
		for i:int = 0 to (cols - 1)
			row[ statement.column_name( i ) ] = statement.column_text( i )			
			agenda[row[ "nombre" ]] = row[ "phone" ]
		item++		
 
	if agenda.has_key(contacto_nombre) == true
		stdout.printf("%s ya está en la Agenda.\n", contacto_nombre)
		stdout.printf("Su teléfono es %s\n", agenda[contacto_nombre])
 
		stdout.printf( "¿Quieres cambiar el número? (s/n) " )
		respuesta:string = stdin.read_line()
		if respuesta == "s"
			stdout.printf( "Nuevo teléfono: " )
			contacto_phone:string = stdin.read_line()
			// actualizamos con un nuevo valor
			enter:string = @"UPDATE Contactos SET phone = $contacto_phone WHERE nombre = '$contacto_nombre'"		
			db.exec (enter)				
 
	else
		stdout.printf( "Teléfono: " )
		contacto_phone:string = stdin.read_line()
		enter:string = @"INSERT INTO Contactos (nombre, phone) VALUES ('$contacto_nombre', $contacto_phone)"
		db.exec (enter)
🔝