diumenge, 18 de setembre del 2011

Taules hash amb C a Linux

Les taules hash , hash maps són una estructura de dades que, mitjançant una funció hash (o funció resum) identifica uns valors, anomenats claus, amb uns valors associats.

L'avantatge de les taules hash sobre altres estructures de dades és la gran rapidesa d'accés a la informació i el cost constant d'aquest accés. Per això,  les taules hash tenen un munt d'aplicacions.

En tot cas, les taules hash són una solució molt senzilla i eficient per a l'emmagatzematge i l'accés a la informació que es pugui modelar directament amb taules de  parelles clau valor.

Amb C sobre Linux (en el meu cas Ubuntu 11.04) apareixen almenys tres opcions ràpides per a implementar taules hash:
Amb funcions de la llibreria estàndar GlibC
Amb funcions de la llibreria GTK, Glib

Amb "bases de dades" del tipus DBM, a Linux Gdbm; o amb l'evolució amb capacitat concurrent de Gdbm desenvolupada per a SAMBA: TDB

Anem a repassar les opcions:


Amb Glibc (La llibreria estàndard de C)

Un exemple de taules hash amb les funcions de la llibreria estàndard Glibc (hcreate, hsearch i hdestroy)

/* aquest exemple està adaptat de la documentació del man */
#include <stdio.h>
#include <search.h>
#include <string.h>

struct info {        /* aquesta estructura és la dels valors emmagatzemats a la taula */
    int age, room;  
};

#define NUM_EMPL    5000    /* # número d'elements. */

int main(void)
{
    char string_space[NUM_EMPL*20];       /* espai d'emmagatzematge de la taula de valors de les claus. */
    struct info info_space[NUM_EMPL];     /* espai per emmagatzemar la informació de claus. */
    char *str_ptr = string_space;         /* següent posició a l'espai de valors. */
    struct info *info_ptr = info_space;   /* següent posició a l'espai de claus. */
    ENTRY item;
    ENTRY *found_item;                    /* nom a buscar. */
    char name_to_find[30];
    int i = 0;

    /* Create table; no error checking is performed. */
    printf("Crea la taula\n");
    (void) hcreate(NUM_EMPL);

    printf("Introdueix clau age room a la taula. Per acabar d'entrar valors prem CTRL-D\n");
    while (scanf("%s %d %d", str_ptr, &info_ptr->age,
           &info_ptr->room) != EOF && i++ < NUM_EMPL) {


        /* Put information in structure, and structure in item. */
        printf("Afegeix %s %d %d a la taula\n", str_ptr, info_ptr->age, info_ptr->room);
        item.key = str_ptr;
        item.data = info_ptr;
        str_ptr += strlen(str_ptr) + 1;
        info_ptr++;


        /* Put item into table. */
        printf("Afegeix - hsearch(item, ENTER)\n");
        (void) hsearch(item, ENTER);
    }



    /* Access table. */
    printf("Busca valors a la taula. Per acabar CTRL-C\n");
    item.key = name_to_find;
    while (scanf("%s", item.key) != EOF) {
        printf("Buscant %s amb hsearch(item, FIND)\n", item.key);
        if ((found_item = hsearch(item, FIND)) != NULL) {
           
            /* If item is in the table. */
            (void)printf("trobat %s, age = %d, room = %d\n",
                found_item->key,
                ((struct info *)found_item->data)->age,
                ((struct info *)found_item->data)->room);
        } else
            (void)printf("no trobat %s\n", name_to_find);
    }
    return 0;
}


Una excució podria ser, per exemple:


albert@atenea:~/wk-c/prova-hash-glibc$ ./prova_hash_glibc
Crea la taula
Introdueix clau age room a la taula. Per acabar d'entrar valors prem CTRL-D
clau1 1 2
Afegeix clau1 1 2 a la taula
Afegeix - hsearch(item, ENTER)
clau2 3 4
Afegeix clau2 3 4 a la taula
Afegeix - hsearch(item, ENTER)
clau3 5 6
Afegeix clau3 5 6 a la taula
Afegeix - hsearch(item, ENTER)
Busca valors a la taula. Per acabar CTRL-C
prova
Buscant prova amb hsearch(item, FIND)
no trobat prova
clau1
Buscant clau1 amb hsearch(item, FIND)
trobat clau1, age = 1, room = 2
clau3
Buscant clau3 amb hsearch(item, FIND)
trobat clau3, age = 5, room = 6
^C
albert@atenea:~/wk-c/prova-hash-glibc$



Amb GLib (la llibreria de GTK)

La referència per a taules hash amb Glib es troba a http://developer.gnome.org/glib/2.28/glib-Hash-Tables.html

En resum:

Per a crear una taula hash de Glib (una GHashTable), fem servir: g_hash_table_new().

Per afegir una clau i el seu valor associat a la GHashTable, fem servir: g_hash_table_insert().

Per a cercar el valor associat a una clar fem servir  g_hash_table_lookup()  i g_hash_table_lookup_extended().

Per a eliminar una aprella clau-valor, g_hash_table_remove().

Per a  invocar una funció per a cada parella clau-valor,  g_hash_table_foreach().

Per a esborrar del tot una GHashTable,  g_hash_table_destroy().


Vet aquí l'exemple amb GLibC implementat amb les funcions de GLib. L'exemple ha estat desenvolupat fent servir Anjuta. He partit d'un projecte genèric mínim amb C. M'ha calgut afegir la llibreria glib2.0-0 i l'include a glib.h al fitxer Makefile.am (automake)

## Process this file with automake to produce Makefile.in

## Created by Anjuta

AM_CPPFLAGS = \
    -DPACKAGE_DATA_DIR=\""$(datadir)"\"

AM_CFLAGS =\
     -Wall\
     -g\
     -I/usr/include/glib-2.0\
     -I/usr/lib/glib-2.0/include
     
bin_PROGRAMS = hashtables

hashtables_SOURCES = \
    main.c

hashtables_LDFLAGS = -lglib-2.0

hashtables_LDADD =



El codi és el següent. Pràcticament només he tingut que canviar les funcions:


#include <stdio.h>
#include <string.h>
#include <glib.h>


struct value {        /* aquesta estructura és la dels valors emmagatzemats a la taula */
    gint age, room;  /* gint és el tipus glib que encapsula a int*/ 
};

#define NUM_KEYS    5000    /* número d'elements. */
#define KEY_SIZE    20      /* tamanys de la clau*/

int main(void)
{
    GHashTable *hashTable;

    gchar keys[NUM_KEYS * KEY_SIZE];    /* espai d'emmagatzematge de la taula de valors de les claus. */
    struct value values[NUM_KEYS];      /* espai per emmagatzemar la informació de claus. */
    gchar *keyPtr = keys;               /* següent posició a l'espai de valors. */
    struct value *valuePtr = values;    /* següent posició a l'espai de claus. */
    gchar keyToFind[KEY_SIZE];          /* clau a buscar, màxim de KEY_SIZE caràcters */
    int i=0;                   

    /* Crea taula hash.
      g_str_hash és una funció hash que es proporciona amb Glib
      per a claus del tipus (gchar *)
      g_str_equal és una funció que es proporciona amb Glib per verificar
      l'igualtat de dues claus del tipus (gchar *)
    */
    printf("Crea la taula\n");
    hashTable = g_hash_table_new(g_str_hash, g_str_equal);

    printf("Introdueix clau age room a la taula. Per acabar d'entrar valors prem CTRL-D\n");
    while (scanf("%s %d %d",
                 keyPtr,
                 &valuePtr->age,
                 &valuePtr->room) != EOF && i++ < NUM_KEYS) {
                 
        /* traça */
        printf("Afegeix %s %d %d a la taula\n", keyPtr, valuePtr->age, valuePtr->room);

        /* els posa a la taula */
        printf("Afegeix - g_hash_table_insert\n");
        /* afegeix clau-valor a taula hash.
          g_hash_table_insert rep tres paràmetres
          punter a la taula hash
          punter a la clau
          punter al valor
        */        
        g_hash_table_insert(hashTable, keyPtr, valuePtr);

        /* prepara els punters per a nova clau valor*/
        keyPtr += strlen(keyPtr) + 1;
        valuePtr++;
    }


    /* Ara busca per la taula. */
    printf("Busca valors a la taula. Per acabar CTRL-C\n");
    keyPtr = keyToFind;
    while (scanf("%s", keyPtr) != EOF) {
        printf("Buscant %s amb g_hash_table_lookup\n", keyPtr);
        if ((valuePtr = g_hash_table_lookup(hashTable, keyPtr)) != NULL) {
           
            /* si ha trobat diferent de nul, el mostra. */
            /* nota: això pot ser un problema, perquè no es pot distingir
               el "no trobat" del "trobat el valor nul".
               En aquests casos, cal fer servir g_hash_table_lookup_extended
            */
            printf("found %s, age = %d, room = %d\n",
                keyPtr,
                valuePtr->age,
                valuePtr->room);
        } else
            printf("not found %s\n", keyPtr);
    }
    return 0;
}


L'execució del programa anterior segueix les mateixes indicacions que al primer exemple:

albert@atenea:~/wk-c/prova-hash-glib$  hashtables
Crea la taula
Introdueix clau age room a la taula. Per acabar d'entrar valors prem CTRL-D
albert 42 192
Afegeix albert 42 192 a la taula
Afegeix - g_hash_table_insert
montse 40 201
Afegeix montse 40 201 a la taula
Afegeix - g_hash_table_insert

^D
Busca valors a la taula. Per acabar CTRL-C
kok
Buscant kok amb g_hash_table_lookup
not found kok
albert
Buscant albert amb g_hash_table_lookup
found albert, age = 42, room = 192
montse
Buscant montse amb g_hash_table_lookup
found montse, age = 40, room = 201
^C



GDBM i TDB

En el desenvolupament de SAMBA es va veure que calia un sistema de base de dades senzill del tipus hash: una taula de claus i valors.

DBM (la BD de BSD) oferia la interfase adequada, però no permetia l'accés concurrent. Aleshores, es va reescriure DBM permetent aquest accés concurrent i el resultat va ser TDB. TDB es pot utilitzar com un alternativa concurrent de DBM.

A Ubuntu es poden instal·lar des del Centre de Programari tant la versió GNU de DBM, la GDBM, com la TDB.

TDB i GDBM ocupen molt poc espai i poden ser una bona sol·lució per a implementar taules hash amb C allà on calguin, en comptes de fer servir les funcions que ofereixen la llibreria estàndard de C, GLibc, o les de la llibreria de GTK, GLib.

Una preacució a tenir en compte és que existeixen diferents formats de fitxer dbm.

La instal·ació de TDB inclou, a més  dels llibreries i dels include,  els executables següents:

tdbbackup: és una eina que es pot fer servir per fer el backup des fitxers .tdb de Samba i verificar-ne la integritat. Si troba un fitxer .tdb de Samba fet malbé i un fitxer de backup previ, aleshores restaura el fitxer anterior.
tdbdump: és una eina que fa el volcat, o dump, d'un fitxer .tdb en un format legible. A més també pot fer el volcat d'una clau específica.           
tdbtool: permet manipular fitxers .tdb. Les opcions que ofereix aquest programa són:

albert@atenea:~$ tdbtool
tdb> help
database not open

tdbtool:
  create    dbname     : create a database
  open      dbname     : open an existing database
  transaction_start    : start a transaction
  transaction_commit   : commit a transaction
  transaction_cancel   : cancel a transaction
  erase                : erase the database
  dump                 : dump the database as strings
  keys                 : dump the database keys as strings
  hexkeys              : dump the database keys as hex values
  info                 : print summary info about the database
  insert    key  data  : insert a record
  move      key  file  : move a record to a destination tdb
  store     key  data  : store a record (replace)
  show      key        : show a record by key
  delete    key        : delete a record by key
  list                 : print the database hash table and freelist
  free                 : print the database freelist
  check                : check the integrity of an opened database
  speed                : perform speed tests on the database
  ! command            : execute system command
  1 | first            : print the first record
  n | next             : print the next record
  q | quit             : terminate
  \n                   : repeat 'next' command

per exemple, puc fer

albert@atenea:~$ tdbtool
tdb> create prova.tdb
tdb> insert clau1 valor1
tdb> insert clau2 valor2
tdb> insert clau3 valor3
tdb> dump

key 5 bytes
clau1
data 6 bytes
[000] 76 61 6C 6F 72 31                                 valor1

key 5 bytes
clau2
data 6 bytes
[000] 76 61 6C 6F 72 32                                 valor2

key 5 bytes
clau3
data 6 bytes
[000] 76 61 6C 6F 72 33                                 valor3
 tdb> quit
albert@atenea:~$ tdbdump prova.tdb
{
key(5) = "clau1"
data(6) = "valor1"
}
{
key(5) = "clau2"
data(6) = "valor2"
}
{
key(5) = "clau3"
data(6) = "valor3"
}


Amb TDB

Anem a repetir el mateix cas de prova de GLibC i GLib, però implementant-lo amb l'API C de TDB. Amb GDBM seria molt semblant.

El package de TDB em proporciona eines per a examinar fitxers de bases de dades TDB. Aquest pot ser un avantatge decisiu a l'hora de fer decantar-se per TDB per a implementar les taules hash d'una aplicació. No cal dir que l'API C de TDB permet volcar, i recuperar, taules hash a, i de, fitxers.

Com en els casos anteriors, he desenvolupat el programa amb Anjuta, amb un projecte C genèric mínim. M'ha calgut tocar el fitxer makefile.am (per a l'automake) per a afegir-li la referència a la
llibreria libtdb:


## Process this file with automake to produce Makefile.in


## Created by Anjuta


AM_CPPFLAGS = \
-DPACKAGE_DATA_DIR=\""$(datadir)"\" 


AM_CFLAGS =\
-Wall\
-g


bin_PROGRAMS = hashtables_tdb


hashtables_tdb_SOURCES = \
main.c


hashtables_tdb_LDFLAGS = -ltdb 


hashtables_tdb_LDADD = 



Vet aquí el codi (consulteu API TDB)


#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>     /* mode_t */
#include <tdb.h>


struct value {         /* aquesta estructura és la dels valors */
    int age, room;          /* emmagatzemats a la taula */
};


#define NUM_KEYS    5000    /* número d'elements. */
#define KEY_SIZE    20      /* tamanys de la clau*/


int main() {   
    TDB_CONTEXT *tdbHashTable;
    TDB_DATA tdbKey, tdbValue;
    char keysData[NUM_KEYS * KEY_SIZE]; /* espai d'emmagatzematge de la taula de claus. */
    struct value valuesData[NUM_KEYS]; /* espai d'emmagatzematge de la taula de valors. */
    char *keyPtr = keysData;     /* següent posició a l'espai de valors. */
    struct value *valuePtr = valuesData; /* següent posició a l'espai de claus. */   
    char keyToFind[KEY_SIZE]; /* clau a buscar, màxim de KEY_SIZE caràcters */
    int i=0; /* comptador de claus afegides*/


    /* Crea taula hash.*/
    printf("Crea la taula\n");
    tdbHashTable = tdb_open("prova.tdb", 
    0,
                            TDB_CLEAR_IF_FIRST, 
    O_RDWR | O_CREAT | O_TRUNC, 
    0666);


    printf("Introdueix clau age room a la taula. Per acabar d'entrar valors prem CTRL-D\n");
    while (scanf("%s %d %d", 
 keyPtr, 
 &valuePtr->age, 
 &valuePtr->room) != EOF && i++ < NUM_KEYS) {  
        /* traça */
        printf("Afegeix %s %d %d a la taula\n", 
               keyPtr, 
               valuePtr->age, 
               valuePtr->room);


        /* els posa a la taula */
        printf("Afegeix - tdb_store\n");
        
tdbKey.dptr = keyPtr;
tdbKey.dsize = strlen(keyPtr) + 1; /* caràcter 0 de final*/

tdbValue.dptr = valuePtr;
tdbValue.dsize = sizeof(struct value);

        tdb_store(tdbHashTable, tdbKey, tdbValue, TDB_INSERT);


/* prepara els punters per a nova clau valor*/
        keyPtr += strlen(keyPtr) + 1;
        valuePtr++;
    }




    /* Ara busca per la taula. */
    printf("Busca valors a la taula. Per acabar CTRL-C\n");
    keyPtr = keyToFind;
    while (scanf("%s", keyPtr) != EOF) {
        printf("Buscant %s amb tdb_fetch\n", keyPtr);
/* prepara la clau per a la cerca*/
tdbKey.dptr = keyPtr;
tdbKey.dsize = strlen(keyPtr) + 1;   
   
        tdbValue = tdb_fetch(tdbHashTable, tdbKey);
        if (tdbValue.dptr != NULL) {
            /* si ha trobat diferent de nul, el mostra. */
    /* obté el punter al valor a partir del TDB_DATA retornat*/
    valuePtr = tdbValue.dptr;


            printf("found %s, age = %d, room = %d\n",
                   keyPtr,
                   valuePtr->age,
                   valuePtr->room);
        } else
            printf("not found %s\n", keyPtr);
    }


    tdb_close(tdbHashTable);
return (0);
}


No hi han gaires diferències amb els casos anteriors. Si de cas remarcar l'ús de l'estructura TDB_DATA, per a passar, i recuperar, les claus i els valors a, i de, les funcions de l'API.

Amb la funció tdb_open es crea físicament un fitxer prova.tdb que podrem examinar (i modificar) amb tdbtool. Tanmateix, hauria pogut fer servir l'opció TDB_INTERNAL per a crear  la taula hash només en memòria.

Una execució del programa anterior:


albert@atenea:~/wk-c/prova-hash-tdb$ hashtables_tdb 
Crea la taula
Introdueix clau age room a la taula. Per acabar d'entrar valors prem CTRL-D
albert 42 125
Afegeix albert 42 125 a la taula
Afegeix - tdb_store
montse 40 125
Afegeix montse 40 125 a la taula
Afegeix - tdb_store
artiom 6 125
Afegeix artiom 6 125 a la taula
Afegeix - tdb_store
^D
Busca valors a la taula. Per acabar CTRL-C
prova
Buscant prova amb tdb_fetch
not found prova
albert
Buscant albert amb tdb_fetch
found albert, age = 42, room = 125
montse
Buscant montse amb tdb_fetch
found montse, age = 40, room = 125
artiom
Buscant artiom amb tdb_fetch
found artiom, age = 6, room = 125
^C
albert@atenea:~/wk-c/prova-hash-tdb$ 

I, a més, tenim un fitxer prova.tdb amb la taula generada. Aquest fitxer el podem examinar amb tdbtool:

albert@atenea:~/wk-c/prova-hash-tdb$ tdbtool prova.tdb
tdb> dump

key 7 bytes
artiom
data 8 bytes
[000] 06 00 00 00 7D 00 00 00                           ....}.. 

key 7 bytes
montse
data 8 bytes
[000] 28 00 00 00 7D 00 00 00                           (...}.. 

key 7 bytes
albert
data 8 bytes
[000] 2A 00 00 00 7D 00 00 00                           *...}.. 
tdb> quit
albert@atenea:~/wk-c/prova-hash-tdb$ 

Dump fa un volcat de la taula en format de cadena. Què obtenim? pèr a cada registre de la taula obtenim: la clau i 8 bytes de dades en hexadecimal. No cal dir que 7D és 125; 06 hex. és 6 decimal; 28 hex. és 42 decimal i 2A hex, 40 decimal.


Conclusió

Evidentment, aquests petits escripts només són una aproximació a la implementació de taules hash amb GlibC, Glib i TDB.

Segurament, per a la majoria d'aplicacions n'hi ha prou i són més que suficients les funcions per a hashtables de la llibreria estàndard de C (GLibC); tanmateix, en aplicacions  GTK, la GLib hi és present i aporta funcions més flexibles -per exemple, permetent definir la funció hash- i, a més, fan servir els tipus de dades de GTK, sent consistents amb la resta de l'aplicació.
Finalment, TDB pot ser una bona tria en el cas que ens interessi poder disposar de la taula hash en un fitxer i, a més, ens proporciona eines addicionals, la tdbtool, per a manipular aquests fitxers. 

divendres, 2 de setembre del 2011

Python i Tkinter

En un post anterior vaig presentar la construcció d'interfases gràfiques amb Python i Glade. En ewl post d'avui presento la construcció d'interfaces gràfiques amb Python fent us d'una llibreria clàssica i que agermana el llenguatge amb el Tcl/Tk: em refereixo a Tkinter (o tkinter, amb el nom en minúscula, a partir de Python 3.0). Tkinter és una interface entre Python i Tk, d'aqui el nom. Tk és quelcom de diferrent de Python, però tots dos es troben a sistemes Unix. Tkinter permet a Python utilitzar Tk.

Tkinter permet desenvolupar  interfases gràfiques senzilles de forma ràpida. En aquest post faig una senzilla GUI d'exemple. Es tracta d'una pantalla que té dues caixa d'entrada, superior i inferior, i dos botons. En prémer el botó de l'esquerra la informació que hi ha a la caixa superior es mourà a la caixa inferior; i si no hi ha informació a la caixa superior es mostrarà un missatge d'avís. De forma similar, en prémer el botó de la dreta  la informació que hi ha la caixa inferior es mourà a la caixa superior; i si no hi ha  informació a la caixa inferior es mostrarà un missatge d'avís.

El codi és el següent:

#!/usr/bin/python
# coding: latin-1
#
# http://www.tutorialspoint.com/python/
# http://www.astro.washington.edu/users/rowen/TkinterSummary.html
# http://effbot.org/tkinterbook/

import tkMessageBox
from Tkinter import *
# alternativa
# import Tkinter
# Tkinter.Tk()

class App:
	def __init__(self, root):
		Label(root, text='Caixa superior').grid(row=1,column=1, ipadx=5, ipady=5, padx=5, pady=5)
		self.EntryDalt = Entry(root)
		self.EntryDalt.grid(row=1,column=2, ipadx=5, ipady=5, padx=5, pady=5)
		Label(root, text='Caixa inferior').grid(row=2,column=1, ipadx=5, ipady=5, padx=5, pady=5)
		self.EntryBaix = Entry(root)
		self.EntryBaix.grid(row=2,column=2, ipadx=5, ipady=5, padx=5, pady=5)
		Label(root, text='---------------').grid(row=3,column=1, columnspan=2, ipadx=5, ipady=5, padx=5, pady=5)
		Button1 = Button(root, text="Passa de dalt a baix", command=self.Mostra1)
		Button1.grid(row=4,column=1, ipadx=5, ipady=5, padx=5, pady=5)
		Button2 = Button(root, text="Passa de baix a dalt", command=self.Mostra2)
		Button2.grid(row=4,column=2, ipadx=5, ipady=5, padx=5, pady=5)
		
	# de dalt a baix.
	# no passa si no hi ha res a la caixa de dalt.
	def Mostra1(self):
		sEntryDalt = self.EntryDalt.get()

		if sEntryDalt == '':
			tkMessageBox.showinfo("Avís!", "No hi ha text a la caixa superior")
		else:
			# passa de dalt a baix
			self.EntryBaix.delete(0, END)		
			self.EntryDalt.delete(0, END)		
			self.EntryBaix.insert(0,sEntryDalt)

	# de baix a dalt
	# no passa si no hi ha text a la caixa de baix
	def Mostra2(self):
		sEntryBaix = self.EntryBaix.get()

		if sEntryBaix == '':
			tkMessageBox.showinfo("Avís!", "No hi ha text a la caixa inferior")
		else:
			# passa de baix a dalt
			self.EntryDalt.delete(0, END)		
			self.EntryBaix.delete(0, END)		
			self.EntryDalt.insert(0,sEntryBaix)

if __name__ == "__main__":
	root = Tk()
	root.title("Prova TK")
	App(root)	
	root.mainloop()

Uns comentaris al codi anterior,

Primer de tot, la importació de les llibreries: he utilitzat Tkinter i tkMessageBox. tkMessageBox em proporciona un popup de missatges que es pot configurar per a que sigui  d'informació, d'avís, d'error, amb botons d'OK, Cancel, Sí, no... tkMessageBox no és Tkinter, tot i que també es recolza en Tk per a proporcionar el widget de caixa de missatges.

Segon. M'en vaig al final, al main:


if __name__ == "__main__":
	root = Tk()
	root.title("Prova Tkinter")
	App(root)	
	root.mainloop()


Primer de tot, cal dir que una interface amb Tk es pot conceptualitzar com un arbre de widgets, en els que uns són fills (estan inclosos) en d'altres, fins arribar a un primer nivell on tindrem un widget arrel. El primer de tot, doncs, és obtenir  el widget arrel, amb


	root = Tk()

Ara  ja puc afegir (incloure) widgets  fills.

Un cop tinc el widget arrel, el puc configurar. Per exemple, li dono un títol "Prova Tkinter"

	root.title("Prova Tkinter")


A continuació, fent us d'una classe que anomeno App creo la interface.

	App(root)


Finalment, inicio l'interface  invocant el "bucle d'esdeveniments"

	root.mainloop()


Ara és el moment d'analitzar la classe App.

A la classe App he fet que el constructor sigui l'encarregat de col·locar els diferents widgtets a la finestra.
A més, he creat  dos mètodes Mostra1 i Mostra2 que seran la resposta als esdeveniments de prémer el botó esquerra i dret respectivament.

Vet aquí el constructor:

	def __init__(self, root):
		Label(root, text='Caixa superior').grid(row=1,column=1, ipadx=5, ipady=5, padx=5, pady=5)
		self.EntryDalt = Entry(root)
		self.EntryDalt.grid(row=1,column=2, ipadx=5, ipady=5, padx=5, pady=5)
		Label(root, text='Caixa inferior').grid(row=2,column=1, ipadx=5, ipady=5, padx=5, pady=5)
		self.EntryBaix = Entry(root)
		self.EntryBaix.grid(row=2,column=2, ipadx=5, ipady=5, padx=5, pady=5)
		Label(root, text='---------------').grid(row=3,column=1, columnspan=2, ipadx=5, ipady=5, padx=5, pady=5)
		Button1 = Button(root, text="Passa de dalt a baix", command=self.Mostra1)
		Button1.grid(row=4,column=1, ipadx=5, ipady=5, padx=5, pady=5)
		Button2 = Button(root, text="Passa de baix a dalt", command=self.Mostra2)
		Button2.grid(row=4,column=2, ipadx=5, ipady=5, padx=5, pady=5)


El que obtinc és

Els constructors dels widgets Label, Entry i Button prenen un primer paràmetre que és el widget pare del que pengen. Labels i Buttons, a més, prenen una paràmetre de configuració que, en aquest cas es refereix al text a mostrar en Labels i Buttons; els buttons, a més, prenent un tercer paràmetre de configuració que fa apunta al mètode que respondrà a l'esdeveniment per defecte del botó, que és fer-hi clic.

Com és que cada widget va al lloc que li correspon? la clau està al mètode grid() El mètode grid ens permet accedir al gestor de finestra Grid. Hi han d'altres tipus de gestor, com el gestor de finestra Pack, o el gestor de Finestra Place.

Grid és molt senzill de fer servir. La forma com ubica els widgets em recorda una mica a la maquetació amb taules en HTML. El mètode grid rep un conjunt de paràmetres. Per exemple:


Label(root, text='Caixa superior').grid(row=1,column=1, ipadx=5, ipady=5, padx=5, pady=5)


Grid conceptualitza la finestra com una graella rectangular de cel·les.  Cada cel·la pot acollir un widget. La posició d'una cel·la ve definida per la fila/columna. Les files es numeren de dalt a baix començant per l'1. Les columnes d'esquerra a dreta, començant per l'1. Puc agrupar cel·les adjacents en horitzontal i/o vertical.

Tenint en compte això, els paràmetres del Label "Caixa superior" s'expliquen fàcilment: posa el label a la cel·la de la fila 1, columna 1. L'ample de la cel·la s'ajusta automàticament al contingut. Tanmateix, per a fer que la interfase "respiri" dono un marge interior ("padding") entre el límit de la cel·la i la caixa d'entrada de 5píxels (ipadx=5, ipady=5) i un d'exterior entre cel·les, també de 5 píxels (padx=5, pady=5).

Amb la resta de widgets funciona igual. El resultat és que la meva finestra, conceptualment és una graella de dues columnes per quatre files.

A la fila 3 hi ha un exemple del que deia d'agrupar cel·les adjacents

Label(root, text='---------------').grid(row=3,column=1, columnspan=2, ipadx=5, ipady=5, padx=5, pady=5)

en aquest cas, el paràmetre columnspan em diu que la cel·la ocupa dues cel·les en horitzontal. Hi ha un paràmetre rowspan que funciona de la mateixa forma agrupant cel·les en vertical.

Per acabar, els mètodes que responen als esdeveniments Mostra1 i Mostra2 són gairebé idèntics,


	# de dalt a baix.
	# no passa si no hi ha res a la caixa de dalt.
	def Mostra1(self):
		sEntryDalt = self.EntryDalt.get()

		if sEntryDalt == '':
			tkMessageBox.showinfo("Avís!", "No hi ha text a la caixa superior")
		else:
			# passa de dalt a baix
			self.EntryBaix.delete(0, END)		
			self.EntryDalt.delete(0, END)		
			self.EntryBaix.insert(0,sEntryDalt)

	# de baix a dalt
	# no passa si no hi ha text a la caixa de baix
	def Mostra2(self):
		sEntryBaix = self.EntryBaix.get()

		if sEntryBaix == '':
			tkMessageBox.showinfo("Avís!", "No hi ha text a la caixa inferior")
		else:
			# passa de baix a dalt
			self.EntryDalt.delete(0, END)		
			self.EntryBaix.delete(0, END)		
			self.EntryDalt.insert(0,sEntryBaix)


A Mostra1, per exemple, primer de tot, s'obté el contingut del EntryDalt amb el mètode get.

		sEntryDalt = self.EntryDalt.get()


Si el valor obtingut és nul, mostra l'avís

		if sEntryDalt == '':
			tkMessageBox.showinfo("Avís!", "No hi ha text a la caixa superior")

En cas contrari, primer esborra les Entry (mètode delete(0,END))

		else:
			# passa de baix a dalt
			self.EntryDalt.delete(0, END)		
			self.EntryBaix.delete(0, END)	

I, finalment, emplena la caixa inferior

			self.EntryDalt.insert(0,sEntryBaix)


I fins aquí aquest senzill experiment per a prendre contacte amb Tkinter.

Per a més informació, podeu consultar:

La pàgina de Tkinter, http://www.python.org/topics/tkinter/doc.html
Thinking in Tkinter, http://www.ferg.org/thinking_in_tkinter/index.html
Tkinter: GUI programming with Python, http://infohost.nmt.edu/tcc/help/lang/python/tkinter.html
Python. GUI Programming (Tkinter), http://www.tutorialspoint.com/python/python_gui_programming.htm

dimecres, 10 d’agost del 2011

Retardador de subtítols SubRip (again bis). Groovy version.

El darrer post va ser la versió BeanShell del retardador [1] [2]. No he pogut resistir-me a fer  la versió amb Groovy.


import java.text.DateFormat
import java.text.SimpleDateFormat

// els arguments estan a la variable args

// verifica que té 3 arguments
if (args.length != 3 ) {
    sUsage = "Usage:\tdelayer filein.srt fileout.srt delay" +
             "\n\texample 1: delayer filein.srt fileout.srt 10000 --> delays 10 seconds" +
             "\n\texample 2: delayer filein.srt fileout.srt -10000 --> advances 10 seconds"

    print "incorrect number of arguments"
    print(sUsage)
    System.exit(1)
}

// l'anterior també es podria fer amb CliBuilder

// calcula el delta
lDelta = Long.parseLong(args[2])

// prepara el SimpleDateFormat
sdf = new SimpleDateFormat("HH:mm:ss,SSS")

// obre els fitxers
fFileIn = new File(args[0])
fFileOut = new File(args[1])

// buffer
sFileOut = ""

fFileIn.eachLine { sLine -> 
                sTokens = sLine.split("-->")
                if (sTokens.length == 2) {
                    // si té dos elements, és la línia de temps
                              
                    // els "pela"
                    sTempsInici = sTokens[0].trim()
                    sTempsFi = sTokens[1].trim()

                    // els converteix a Date
                    dTempsInici = sdf.parse(sTempsInici)
                    dTempsFi = sdf.parse(sTempsFi)
                              
                    // Els suma el retard
                    dTempsIniciDelayed = new Date(dTempsInici.getTime() + lDelta)
                    dTempsFiDelayed = new Date(dTempsFi.getTime() + lDelta)

                    // converteix els Date a String amb el mateix SimpleDateFormat
                    sTempsIniciDelayed = sdf.format(dTempsIniciDelayed)
                    sTempsFiDelayed = sdf.format(dTempsFiDelayed)

                    // Munta la línia
                    sLine = "${sTempsIniciDelayed} --> ${sTempsFiDelayed}"
    }
               
    sFileOut += "${sLine}\r\n"
}
// escriu el buffer
fFileOut.write(sFileOut)

// no hi ha close . L'objecte File no en té. ni li cal.

// acaba aquí


La versió en Groovy té un aspecte sensiblement diferent de la versió BeanShell. Ara bé, cal dir que per a escriure el codi Groovy he parti del codi BeanShell, que, excepte per la presa d'arguments l'intèrpret Groovy l'ha agafat sense cap més queixa. Diguem que amb el BeanShell ja tenia el 99% del Groovy fet. Per la seva banda, el BeanShell era, pràcticament, Java.

Però, és clar, el codi resultant no tenia l'aspecte d'un script Groovy.  Així que vaig començar a modificar-lo per a seguir un estil de codificació més Groovy.

Primer de tot, el tractament dels arguments és més senzill que amb BeanShell, doncs args ésdisponible directament. Com amb la vesió Python, hauria pogut utilitzar un mòdul per a tractar els arguments de la línia de comandes, el CliBuilder, però ho deixo per a una altre ocasió.

Observar que no he fet servir ";" .

Importació de llibreries Java i ús de SimpleDateFormat per a fer el parseig de ñes cadenes i el formateig dels dates, com amb BeanShell.

Simplificació de la lectura i escriptura dels fitxers amb l'objecte Groovy File. Aquest objecte NO és l'objecte Java File. File de Groovy simplifica el tractament dels fitxers. Fixem-nos com n'hi ha prou amb obrir el fitxer per nom i després, fent us d'una closure,  llegeix línia per línia el fitxer. M'he estalviat els BufferedReader.

Per a cada línia fa el mateix tractament  que amb l'script BeanShell però, en comptes d'escriure línia a línia, el que fa és construir una cadena sFile, un buffer, amb tota la informació del fitxer. En comptes de fer servir l'operador '+' per a concatenar, opta per una solució més Groovyana:

sLine = "${sTempsIniciDelayed} --> ${sTempsFiDelayed}"

Un cop processades totes les línies, escriu el buffer de cop, amb el mètode write de l'objecte File.

És evident la simplificació comparant amb BeanShell, o que l'equivalent amb Java. M'atreviria a dir que, fins i tot, és més simplificat que amb Python.

Però tampoc aniré més enllà. Es tracta d'un script molt senzill i no seria correcte treure'n conclusions sobre la potència dels tres llenguatges. Segur que en totes tres versions, Python, BeanShell, Groovy es podrien buscar simplificacions addicionals. I en tot cas, a l'hora de triar una solució o altre, hi han factors que poden ser molt més determinants que la potència del llenguatge: disponibilitat d'entorns de desenvolupament, coneixements i experiència dels programadors...

Prenguem, doncs, aquest script d'avui només com un petit tast d'un altre aroma del cafè: Groovy, un altre llenguatge per a la JVM.

Retardador de subtítols SubRip (again). BeanShell version.

El mateix retardador de subtítols del post anterior, ara fet amb BeanShell.

Suposant que guardo l'script al fitxer delayer.bsh i que tinc el fitxer prova.srt del post anterior, aleshores puc retardar l'aparició dels subtítols amb la següent línia de comandes:

./delayer.bsh prova.srt output.srt 10000

Vet aquí el codi:

#!/usr/bin/bsh
 

import java.text.DateFormat;
import java.text.SimpleDateFormat;

// obté els arguments
args = this.interpreter.get("bsh.args");

// verifica que té 3 arguments
if (args.length != 3 ) {
    sUsage = "Usage:\tdelayer filein.srt fileout.srt delay" +
             "\n\texample 1: delayer filein.srt fileout.srt 10000 --> delays 10 seconds" +
             "\n\texample 2: delayer filein.srt fileout.srt -10000 --> advances 10 seconds";

    print("incorrect number of arguments");
    print(sUsage);
    System.exit(1);
}

// calcula el delta
lDelta = Long.parseLong(args[2]);

// prepara el SimpleDateFormat
sdf = new SimpleDateFormat("HH:mm:ss,SSS");

// obre els fitxers args[0] per a lectura i args[1] per a escriptura
fFileIn = new FileReader(args[0]);
fFileOut = new FileWriter(args[1]);
brFileIn = new BufferedReader(fFileIn);
brFileOut = new BufferedWriter(fFileOut);

while((sLine = brFileIn.readLine()) != null) {
    sTokens= sLine.split("-->");   // mètode split() milor que StringTokenizer des de JDK1.4.
    if (sTokens.length == 2) {
    // si té dos elements, és la línia de temps

    // els "pela"
    sTempsInici = sTokens[0].trim();
    sTempsFi = sTokens[1].trim();

    // els converteix a Date
    dTempsInici = sdf.parse(sTempsInici);
    dTempsFi = sdf.parse(sTempsFi);

    // Els suma el retard
    dTempsIniciDelayed = new Date(dTempsInici.getTime() + lDelta);
    dTempsFiDelayed = new Date(dTempsFi.getTime() + lDelta);

    // converteix els Date a String amb el mateix SimpleDateFormat
    sTempsIniciDelayed = sdf.format(dTempsIniciDelayed);
    sTempsFiDelayed = sdf.format(dTempsFiDelayed);

    // Munta la línia
    sLine = sTempsIniciDelayed + " --> " + sTempsFiDelayed;
    }

    // escriu la línia
    brFileOut.write(sLine);
    // i salta de línia
    brFileOut.newLine();
}

// tanca fitxers i buffered readers / writers
brFileOut.close();
brFileIn.close();
fFileIn.close();
fFileOut.close();

// i acaba aquí


Molt poques coses a dir. Si de cas destacar que:

No he declarat els tipus de cap variable.

No ha calgut importar les classes de java.io ni java.util.En canvi sí les de java.text

Us de

// obté els arguments
args = this.interpreter.get("bsh.args");


per obtenir els arguments de la línia de comandes.

Evidentment, la versió amb Python i optparse era més potent funcionalment pel que fa al tractament dels arguments d'entrada. Ara bé, a la simplicitat en aquest cas juga a favor i fa que l'argument de retard negatiu es tracti de forma més natural que amb la solució Python.

Us del format HH:mm:ss,MMM (hores de  0 a 23:minuts:segons,milisegons) al SimpleDateFormat, tant per a parsejar strings com per a formatar dates.

I finalment, l'us d'Split, en comptes del clàssic StringTokenizer per a obtenir els temps d'inici i final de presentació dels subtítols.

Què és més eficient? Python o BeanShell? Per a aquest cas, ho deixo en empat. Per mi ha estat més fàcil BeanShell que Python, però crec que això respon al fet que estic molt més familiaritzat amb Java.

El proper pas podria ser escriure el mateix script en Groovy. Sospito que només caldrà tocar el pas d'arguments i poca cosa més.

dilluns, 8 d’agost del 2011

Un retardador de subtítols SubRip amb Python

EL cap de setmana passat em vaig dedicar a buscar subtítols en anglès per a pel·lícules en anglès. Va molt bé per a l'aprenentatge i la pràctica de l'idioma el poder veure la pel·lícula en versió original i, a l'hora, poder llegir el que estan dient en el mateix idioma. I després, tornar a veure la pel·lícula sense subtítols. Són tècniques d'aprenentatge.

El cas és que vaig trobar alguns subtítols però als posar-los al VLC amb la pel·lícula corresponent resultava que no estaven sincronitzats amb la imatge. El VLC permet adelantar o retardar el temps d'aparició dels subtítols, però és poc pràctic, ja que cal sincronitzar cada cop que obres la pel·lícula. O, almenys, no vaig veure com guardar la sincronització; i amb el reproductor de DVD del menjador és pitjor perquè no tinc la  possibilitat de sincronitzar.

Per tant, vaig decidir que la solució era fer un retardador per als subtítols i així generar fitxers de subtítols per a les pel·lícules que tinc i que estiguin correctament sincronitzats amb la imatge.


En el post d'avui, doncs,programo una petita aplicació amb python (versió 2.6.5, un pel antiga, però és la que tinc instal·lada a l'Ubuntu) que servirà per retardar o avançar el moment d'aparició de subtítols en un fitxer de subtítols del tipus SubRip (extensió .srt).

La WikiPedia diu del format SubRip qu és "probablement, el més senzill dels formats de subtítols". Els fitxers SubRip amb extensió .srt, són de text pla. El format de temps emprat és "hores:minuts:segons,milisegons". El separador decimal és una coma, en comptes d'un punt perquè l'especificació original és francesa. El salt de línia és, tot sovint,CR+LF. Els subtítols es numeren en seqüència, començant per 1. Els subtítols se separen per una línia en blanc.

En resum:

Número del subtítol
temps d'inici --> temps de fi
text del subtítol (una o més línies) 
Línia en blanc.

un exemple de fixer SubRip (prova.srt)

1
00:00:20,000 --> 00:00:24,400
Altocumulus clouds occur between six thousand

2
00:00:24,600 --> 00:00:27,800
and twenty thousand feet above ground level.

 Doncs bé, amb el següent script Python podré retardar o avançar l'aparició dels subtítols.

#!/usr/bin/python
# coding: latin-1

# L'objectiu del programa és retardar o adelantar el temps d'aparició del subtítol
# el programa pren tres arguments: fitxer original, fitxer de sortida i
# temps expressat en mili segons a avançar o retardar (un negatiu, vol dir avançar)
# El fitxer de sortida serà un fitxer .srt igual a l'original, però amb tots els
# temps retardats en la quantitat de mili segons indicada

import optparse 
from datetime import datetime
from datetime import timedelta

def main():
    # analitza els arguments rebuts
    usage = "\t%prog filein.srt fileout.srt delay"
    usage = usage + "\n\texample 1: %prog filein.srt fileout.srt 10000 --> delays 10 seconds"
    usage = usage + "\n\texample 2: %prog filein.srt fileout.srt -- -10000 --> advances 10 seconds"
    parser = optparse.OptionParser(usage)
    (options, args) = parser.parse_args()

 
    # if args != 3 (fitxer d'entrada, fitxer de sortida, delay) aleshores error
    if len(args) != 3:
        parser.error("incorrect number of arguments")

    # calcula el delta
        delta =  timedelta(milliseconds=int(args[2]))

    # obre el fitxer per llegir-lo
    f1 = open(args[0])
    f2 = open(args[1], "w")
    for line in f1:
            tokens = line.split(" --> ")
            if len(tokens) == 2:
            
        # strip() --> trim spaces
           
        TempsInici = datetime.strptime(tokens[0].strip(), "%H:%M:%S,%f") + delta
           
        TempsFi = datetime.strptime(tokens[1].strip(), "%H:%M:%S,%f") + delta

           
        # multilínia amb "\"
           
        line = \
           
        str(TempsInici.hour).zfill(2) + ":" + \
           
        str(TempsInici.minute).zfill(2) + ":" + \
           
        str(TempsInici.second).zfill(2) + "," + \
           
        str(int(TempsInici.microsecond / 1000)).zfill(3) + " --> " + \
           
        str(TempsFi.hour).zfill(2) + ":" + \
           
        str(TempsFi.minute).zfill(2) + ":" + \
           
        str(TempsFi.second).zfill(2) + "," + \
           
        str(int(TempsFi.microsecond/ 1000)).zfill(3) + "\r\n"
   
        
            f2.write(line)
    

    f1.close()
    f2.close()

#main
if __name__ == "__main__":
    main()


El programa és trivial, però en faré alguns comentaris.

Us d'optparse per a fer el tractament dels arguments d'entrada.  A partir de la versió 2.7 aquest mòdul es considera obsolet i cal fer servir argparse. Ara bé, la forma com funcionen tots dos mòduls és similar. A destacar com es fa el missatge d'us, i com es tornen en un diccionari les opcions (que en aquest cas, no n'hi han) i en un array els arguments posicionals (fitxer-d'entrada, fitxer de sortida, retard).

A destacar com passar arguments negatius (en aquest cas, això vol dir avançar l'aparició dels subtítols) fent servir  "--". El exemple que acompanya l'explicació de l'ús és aclaridor:

delayer.py entrada.srt sortida.srt -- -10000

Avança l'aparició dels subtítols 10 segons

En cas de no disposar dels arguments suficient, finalitza l'execució de l'script amb parser.error

Immediatament que ha tractat els arguments d'entrada, procedeix a calcular el retard amb:

delta =  timedelta(milliseconds=int(args[2]))

A continuació obre el fitxer original per a lectura i el fitxer de sortida.

La iteració línia a línia pel fitxer original es senzilla amb la potent sintaxi del for:

for line in f1:

Per a cada línia, verifica si es pot dividir amb -->. Les línies que tinguin --> seran les de temps i les que cal tractar.

El càlcul dels nous temps es fa amb el parell de línies

TempsInici = datetime.strptime(tokens[0].strip(), "%H:%M:%S,%f") + delta
TempsFi = datetime.strptime(tokens[1].strip(), "%H:%M:%S,%f") + delta

A destacar l'us de strptime  que converteix una cadena en un objecte datetime fent servir una cadena de format. El format "%H:%M:%S,%f" correspon a  hora:minut:segon,milisegons. El mètode strip(), per la seva part, elimina espais en blanc per davant i pel darrera de la cadena, com el trim de java.

Finalment, els camps útils dels datetime TempsInici i TempsFi es reordenen i s'ajunten en una cadena de text en la següent línia

line = \
str(TempsInici.hour).zfill(2) + ":" + \
str(TempsInici.minute).zfill(2) + ":" + \
str(TempsInici.second).zfill(2) + "," + \
str(int(TempsInici.microsecond / 1000)).zfill(3) + " --> " + \
str(TempsFi.hour).zfill(2) + ":" + \
str(TempsFi.minute).zfill(2) + ":" + \
str(TempsFi.second).zfill(2) + "," + \
str(int(TempsFi.microsecond/ 1000)).zfill(3) + "\r\n"



Aquesta instrucció llarga es divideix en vàries línies fent servir \.
Els camps numèrics es converteixen a cadena amb str().
El mètode de cadena zfill(longitud) zero fill posa zeros per l'esquerra fins a que la cadena té la longitud indicada.
Els datetime no tenen un camp millisecond, però sí que el tenen microsecond. aleshores divideixo aquest camp per 1000 i el converteixo a enter amb int(). Finalment afegeixo un CR+LF

Per acabar, fora de l'if, s'escriu al fitxer de sortida la línia. Si era una línia amb --> haurà estat processada, si no, es manté com a l'original.

Es tanquen els fitxers i s'invoca el main. Aquest if __name__ == "__main__":  és un "truc" de python que permet que aquest script pugui ser utilitzat com a llibreria des d'un altre script: en aquest cas, la variable __name__ prendria un valor diferent de __main__.

dissabte, 30 de juliol del 2011

El calendari en mode text de l'Ubuntu Linux

Hi han molts bons programes d'agenda i de calendari. Amb Linux (Ubuntu) n'hi ha un en mode text que ja ve de fàbrica: Es tracta del programa calendar.

calendar és un altre dels molts petits programes que es poden trobar als sistemes UNIX que fan una cosa senzilla, que la fan bé, i que es poden ajuntar amb altres petits programes per a realitzar tasques útils per a l'usuari. La tradicional i efectiva filosofia UNIX, ni més ni menys.

N'hi ha prou amb crear el fitxer calendar a la carpeta .calendar i afegir-hi línies amb les dates que ens interessin amb el format data <tabulació> descripció de la data. Finalment, si afegeixo calendar al fitxer .bashrc sempre que obri una finestra de terminal em revisarà el calendari.

Una altre forma d'utilitzar aquest petit programa podria ser invocant-lo des d'una aplicació que fem servir de forma habitual, potser una macro d'OpenOffice.org/LibreOffice o des d'un menú de llançament d'aplicacions fet amb zenity.

El que ve a continuació és la traducció lliure de la pàgina del manual de calendar.

Us:

calendar [-ab] [-A num] [-B num] [-l num] [-w num] [-f fitxer_de_calendari] [-t [[[cc]aa][mm]]dd]

Descripció:

Aquesta utilitat busca al directori actual, o al directori especificat a la variable d'entorn CALENDAR_DIR, un fitxer amb el nom calendar i mostra les línies que comencen amb la data d'avui i demà. Els divendres es mostren els events de tot el cap de setmana.

Les opcions són:

-A num  Mostra els propers esdeveniments d'avui i dels propers num dies. Per defecte és un dia.

-a      Processa els fitxers “calendar” de tots els usuaris i els envia un corru amb els resultats a cadascun d'ells. requereix privilegis de súper-usuari.

-B num  Mostra els fets esdevinguts entre d'avui i els passats num dies.

-b      Força mode de càlcul especial de dates per als calendaris KOI8.

-l num  Mostra els propers esdeveniments d'avui i dels propers num dies. Per defecte és un dia.

-w num  Mostra els propers esdeveniments d'avui i dels propers dos dies, però només si avui és divendres. És dir, mostra els esdeveniments del cap de setmana.

-f fitxer_de_calendari    fa servir fitxer_de_calendari en comptes del fitxer per defecte calendar.

-t [[[cc]aa][mm]]dd    fa servir la data especificada per cc, centúria; aa, any; mm, mes; dd, dia; en comptes de la data d'avui.

Les línies poden començar amb mes i dia. Si estan ben definides les característiques locals del sistema es poden fer servir els noms dels mesos i els dies de la setmana.

Un asterisc ('*') vol dir cada mes.
Un dia sense mes vol dir aquell dia cada setmana
Un mes sense dia vol dir el primer dia d'aquell mes.
Dos números volen dir mes i dia.
les línies que comencen amb un caràcter de tabulació es refereixen a l'última data entrada, permetent d'aquesta forma línies d'especificacions múltiples per a una data
Els dies de la setmana poden  anar seguits de “-4” ... “+5” (àlies last, first, second, third, fourth) per esdeveniments amb data variable del tipus “el darrer dilluns d'abril”.
El diumenge de pasqua és 'Easter' en anglès. Es pot posar un àlies fent Easter=àlies i fer servir l'àlies

Per conveni, les dates seguides d'un asterisc (‘*’) no són fixes, és dir, canvien d'any en any.

Les descripcions del dia inicien després del primer caràcter de tabulació de la línia. Si una línia no té caràcter de tabulació, no es mostra. Si el primer caràcter d'una línia és una tabulació aleshores es tracta de la continuació de la descripció prèvia.

El calendari és preprocessa amb el preprocessador de C, cpp, permetent la inclusió de fitxers compartits com poden ser, per exemple, les vacances de l'empresa, el calendari de reunions, o l'agenda de cursos.

Si el fitxer compartit no es referencia per una ubicació absoluta aleshores es mira primer al directori actual (o home) i després a /etc/calendar, i finalment a /usr/share/calendar.

Les línies buides o protegides amb comentaris amb la sintaxi de C son ignorades.

On busca el calendari?
calendar              fitxers al directori actual.
~/.calendar           directori ocult ubicat al directori de l'usuri.
~/.calendar/calendar  fitxers que fa servir si no hi un fitxer calendar al directori actual.
~/.calendar/nomail    si el fitxer nomail existeix, no s'envia el calendari del dia a aquel usuari (opció -a).

Els següents fitxers es poden trobar a /usr/share/calendar i contenen llistes d'efemèrides
calendar.all          fitxers calendar nacionals i internacionals.
calendar.birthday     dates de naixement i decés de personatges famosos.
calendar.christian    calendari cristià (però no està de més revisar-ne les dates).
calendar.computer     efemèrides rel·lacionades amb el món de la informàtica.
calendar.croatian     calendari croata.
calendar.discordian   el calendari de la discòrdia.
calendar.fictional    dates i efemèrides fantàstiques i de ficció .
calendar.french       calendari francès.
calendar.german       calendari alemany.
calendar.history      miscel·lània d'efemèrides.
calendar.holiday      altres festes.
calendar.judaic       calendari jueu.
calendar.music        dates del món del rock and roll.
calendar.openbsd      esdeveniments rel·lacionats amb el bsd.
calendar.pagan        festes paganes.
calendar.russian      calendari rus.
calendar.space        història de cursa espacial.
calendar.ushistory    esdeveniments històrics dels EUA.
calendar.usholiday    festes dels EUA.
calendar.world        esdeveniments mundials.

Exemple (\t és el caràcter de tabulació):

LANG=C
Easter=DiumengePasqua
#include <calendar.usholiday>
#include <calendar.birthday>

6/15\tJune 15 (Si és ambigu, es prem mes/dia).
Jun. 15\tJuny 15.
15 June\tJuny 15.
Thursday\tCada dijous.
June\tCada primer de juny.
15 *\tEl 16 de cada mes.

May Sun+2\tsegon diumenge de maig (Muttertag)
04/SunLast\túltim diumenge d'abril,
\tentra l'horari d'estiu a europa
Easter\tdiumenge de pasqua
DiumengePasqua\tdiumenge de pasqua (amb àlies)
Easter-2\tdivendres sant (2 dies abans de diumenge de pasqua)
DiumengePasqua\t-2\tdivendres sant(com abans, però amb àlies)
Paskha\tPasqua ortodoxa


Un altre exemple, més senzill:

#include <calendar.usholiday>
#include <calendar.russian>
#include <calendar.discordian>
#include <calendar.christian>
#include <calendar.computer>
#include <calendar.birthday>

07/31    últim dia de les vacances d'estiu 2011
08/01    Sant Tornem-Hi


El resultat avui és (30 de juliol):


dijous, 21 de juliol del 2011

Groovy, un altre llenguatge per a la JVM

Al darrer post on parlava de BeanShell vaig esmentar Groovy. Però esmentar-lo no és suficient. Groovy es mereix un post. Vostès perdonaran, però fent el joc de paraules, Groovy és meravellós.

Groovy és un llenguatge per a la Java Virtual Machine (JVM). Si la seva sintaxi fos molt diferent a la de Java no tindríem cap dubte en veure'l com quelcom de diferent a la família Java. El cas, però, és que tot programa en Java és també un programa en Groovy.  La inversa no és certa perquè Groovy incorpora canvis i extensions diverses. Groovy pot importar i utilitzar els packages de Java.

Igual com ho fa BeanShell importa un grup de packages java per defecte (els d'ús més habitual).

Moltes de les característiques del llenguatge que vaig comentar per a BeanShell segueixen sent vàlides per a Groovy. Però BeanShell, a diferència de Groovy, està pensat i dissenyat per a ser un llenguatge d'scripting per a Java. Mentre que Groovy, tot i que també pot actuar com llenguatge d'scripting per a Java, és un llenguatge de construcció d'aplicacions.

Ara bé, el mode de funcionament habitual de Groovy és interpretat (realment es fa un compilat JIT a bytecode de Java, però es descarta el bytecode un cop s'ha utilitzat), a diferència del Java que és compilat. Això situa Groovy en el grup de llenguatges com Python, o Perl, o Ruby que es compilen al moment. Tanmateix, amb el JDK de Groovy s'entrega el compilador groovyc que permet compilar a bytecode de Java. És dir que podrem generar fitxers .class i executar-los a una JVM.

Per començar amb Groovy n'hi ha prou amb crear un script amb l'extensió .groovy i executar-lo amb l'aplicació groovy. De cara a depurar-ne el funcionament, però, pot ser útil fer servir l'intèrpret interactiu groovysh. Una versió  gràfica de groovysh és la groovyConsole. El JDK inclou els quatre programes esmentats: groovy, groovyc, groovysh i groovyConsole.



Al cas de Linux Ubuntu, la instal·lació de Groovy es pot realitzar des del Centre de Programari de l'Ubuntu. Convé instal·lar també la documentació, que queda ubicada a /usr/share/doc/groovy-doc. La documentació inclou els javadoc. Per a una introducció al llenguatge, o guies d'usuari cal anar a la web de Groovy, a la secció de documentació.

La forma fàcil d'accedir al llenguatge és partint de Java, si més no per a tots aquells que coneixem aquest llenguatge.

Una classe Java o un programa Java són executables per Groovy sense fer res més. Ara bé, si es vol treure profit de Groovy cal utilitzar-ne les seves característiques, aleshores el que es pot fer és anar despullant el Java de tot allò que és redundant o no necessari en Groovy:

- Els següents packages estan importats per defecte:
  • java.io.*
  • java.lang.*
  • java.math.BigDecimal
  • java.math.BigInteger
  • java.net.*
  • java.util.*
  • groovy.lang.*
  • groovy.util.*
- '==' funciona sempre com em mètode  equals().

- 'in' és paraula clau.

- A Java un array es pot declarar com int[] a= {1,2,3}; a Groovy cal fer-ho int[] a= [1,2,3]; és dir amb [].

- Els llaços 'for' admeten construccions alternatives.
Per exemple, en Java es pot fer   for (int i=0; i < len; i++) {...}
A Groovy també i,  a més, són possibles les construccions alternatives amb 'in'
for (i in 0..len-1) {...}
for (i in 0..<len) {...}


i aquesta última que aprofita que a Groovy tot són objectes
len.times {...}
Aquesta última és una diferència important. A Java hi han uns tipus primitius, per exemple int, que en ocasions cal envoltar amb uns wrapper, per exemple, Integer per a poder utilitzar-los com objectes. Igualment, en ocasionsm cal passar de Integer a int. Això és el boxing/unboxing. A Groovy no cal. Tot són objectes. Un número té mètodes, com el mètode times, per exemple.

- El ';' al final de línia és opcional (però recomanable si es vol claredat).

- El 'return' de les funcions és opcional (però, també, recomanable per claredat).

- 'this' es pot fer servir dins de mètodes estàtics.

- Per defecte, les classes i mètodes són públics.

- Groovy, com BeanShell, o com es fa JavaScript, permet definir objectes a partir de funcions. En realitat, pot fer-ho a partir de blocs. Això és el que s'anomenen closures (o clausures).

- A Java cal posar blocs try-catch, o throws. A Groovy no cal (evidentment, si es produeix una excepció, no serà capturada).

- Groovy només detectara errors com l'ús de mètodes no declarats, o el pas d'arguments de tipus equivocat en temps d'execució. A Java aquesta mena d'errors es detecten en temps de compilació.

Una bona referència per veure  diferències entre Java i Groovy (i també amb Ruby) són aquestes pàgines de A. Sundararajan's Weblog. No és un llistat exhaustiu de les diferències.

http://blogs.oracle.com/sundararajan/entry/java_groovy_and_j_ruby

http://blogs.oracle.com/sundararajan/entry/java_groovy_and_j_ruby1

Finalment, un altre bon lloc on trobar snippets de Groovy és http://www.groovyexamples.org/

I fins aquí la presentació de Groovy. El següent pas és, doncs, posar a treballar Groovy. El llenguatge és potent. Prou potent com per ser la base de Grails. Grails ve a ser l'equivalent amb Groovy al framework web Ruby on Rails, basat en Ruby. De Grails, com no, caldrà parlar-ne en un proper post.