venerdì 8 maggio 2015

Corso Lua - puntata 10 - Applicazioni


Un po' di ripasso

Negli esempi della puntata ci servirà rinfrescarci la memoria su una particolarità di Lua: si possono omettere le parentesi tonde nella sintassi della chiamata a funzione quando viene passato un unico argomento di tipo stringa o di tipo tabella.

Pertanto un codice come questo viene interpretato come la richiesta di esecuzione della funzione 'book' con argomento la tabella definita esplicitamente con il costruttore:
book{
    author = "Luo Shan Minh",
    title = "Great planet"
    pages = 123.00 * 1.10, -- :-)
    editor = "Father & Sons",
}

Dal punto di vista dell'utente invece, una serie di elementi 'book' di questo tipo salvati in un file di testo rappresentano una sorta di database bibliografico.
Comunque sia i dati così descritti corrispondono a codice Lua perfettamente lecito pronto per essere letto ed eseguito dall'interprete con la funzione predefinita 'dofile()'.

Lua può quindi essere un formato per descrivere i dati ed elaborarli definendo opportunamente funzioni --- nell'esempio la funzione 'book()' --- senza che sia necessario effettuare il parsing che sarebbe invece necessario se i dati fossero per esempio nel formato JSON.

L'inventario

Moltissimi esercizi commerciali alla fine dell'anno redigono l'inventario del magazzino, l'elenco della merce con le quantità rilevate e il costo di acquisto dal fornitore.

Il formato dei dati d'inventario

Prevediamo che il record di un articolo sia il seguente:
  • codice: identificativo univoco articolo;
  • descrizione: breve testo descrittivo;
  • classe: codice del tipo di articolo;
  • fornitore: codice univoco del fornitore;
  • reparto: codice univoco del reparto;
  • costo: costo di acquisto di un pezzo;
  • quantità: pezzi rilevati a magazzino.

Caliamo direttamente questa struttura nel formato auto-descrittivo di Lua:

item{code="C123",
     descr="NOME MODELLO",
     class = "Sciarpa",
     supplier="MYSUP",
     department="MYDEP",
     cost=123.45,
     qty=1,
}

Per eseguire il codice è necessario un file dati di esempio. Scaricatelo da questo link. Nel file già dotato di estensione '.lua' i dati sono casuali rispetto a un insieme limitato di liste. Di seguito vi riporto le prime sei righe nel formato key=value con le parole chiavi che sono sempre le stesse --- spazi, tabulazioni ritorni a capo tra i campi sono liberi:
item{code="546556", descr="546556 - Puzzle",   class="Puzzle",    supplier="Vivo",    department="ARTE",cost=5.00, qty=29,}
item{code="815720", descr="815720 - Forbici",  class="Forbici",   supplier="Multi",   department="SCU", cost=11.00,qty=26,}
item{code="067225", descr="67225 - Diario",    class="Diario",    supplier="Lifestar",department="CAR", cost=12.00,qty=12,}
item{code="034371", descr="34371 - Cucitrice", class="Cucitrice", supplier="Vartech", department="TEC", cost=6.00, qty=70,}
item{code="070835", descr="70835 - Pennarello",class="Pennarello",supplier="Vivo",    department="COL", cost=12.00,qty=69,}
item{code="343307", descr="343307 - Buste",    class="Buste",     supplier="MySheet", department="CAR", cost=7.00, qty=67,}
...

Come è evidente nell'esempio, i valori testuali sono racchiusi tra doppi apici mentre i valori numerici sono rappresentati con il punto decimale (non possiamo usare la virgola, simbolo già previsto dalla sintassi per la separazione dei campi (a proposito l'ultima virgola è opzionale perché in generale è più semplice aggiungere campi alla tabella)).

Cominciamo con lo scrivere il programma che conti gli articoli in inventario e che accerti o meno che il codice degli articoli per ciascuna riga è effettivamente univoco. Ammetteremo che il file 'dati.lua' si trovi nella stessa directory degli script Lua.
Fatelo per esercizio e poi proseguite nella lettura perciò inserisco un segnalibro in modo che possiate riprendere la lettura dopo aver compiuto l'esercizio.

--- Segnalibro esercizio 1 ---

Ecco come contare gli articoli:
-- codice errato
local i = 0
local function item(t) -- funzione locale?
    i = i + 1
end

dofile "dati.lua" -- esecuzione file Lua
print(i)

Per prima cosa osservo che la funzione non può essere locale, e infatti il compilatore Lua ci avverte con "lua: dati.lua:1: attempt to call global 'item' (a nil value)". Quando Lua esegue il file esterno si trova in un secondo chunk in cui non sono visibili le variabili locali al primo pertanto solo la funzione di elaborazione dovrà essere globale.

La seconda osservazione è questa: la funzione 'item()' deve far ricorso a una variabile esterna tramite closure, necessariamente. Infatti può ogni volta avere il solo argomento della tabella del record dell'articolo. Non possiamo poi contare sulla restituzione di un parametro che andrebbe perso quando Lua valuta le espressioni delle chiamate a funzione nel file dati.

Per accertare l'univocità del codice possiamo usare una tabella in cui memorizzare i codici stessi fermandoci se il prossimo codice da includere come chiave è già presente. Il valore può essere un tipo qualsiasi e nell'esempio ho inserito il valore booleano 'true':
local dati = {}
function item(t)
    i = i + 1
    if dati[t.code] then
        error(t.code.." errore riga "..i)
    else
        dati[t.code] = true
    end
end

dofile "dati.lua"
print(i)

Scriviamo una versione della funzione item() perché restituisca il totale di magazzino in termini di costo e quantità. Al solito provate a scrivere il codice e proseguite.

--- Segnalibro esercizio 2 ---

Per ora il codice è immediato:
local qty, tot = 0, 0
function item(t)
    qty = qty + t.qty
    tot = tot + t.qty * t.cost
end

dofile "dati.lua"
print(qty, tot)

Costruire l'albero ovvero mettere tabella dentro tabella

Normalmente il report dell'inventario viene strutturato per reparti o per fornitori, oppure ancora per classi di articolo, riportando i relativi totali. In generale un lavoro piuttosto noioso se non è supportato da software opportuni.

Ammettiamo che il magazzino sia suddiviso in reparti e che a sua volta i reparti siano suddivisi per fornitore, a cui apparterranno i relativi articoli. Questa struttura è un albero.

Bene, scriveremo la nostra funzione item() in modo che costruisca l'albero corrispondente all'inventario di magazzino utilizzando una tabella che conterrà le tabelle dei reparti ciascuno contenente le tabelle dei fornitori, che ancora conterranno le tabelle degli articoli, traducendo esattamente la struttura con cui abbiamo deciso di organizzare il magazzino. Provateci prima di continuare.

--- Segnalibro esercizio 3 ---

Presa confidenza con la struttura a livelli il codice non è ne difficile ne lungo.
Invece di processare i dati record per record, creare l'albero significa caricare in memoria tutti i dati con l'obiettivo di tradurli in un report successivamente:
-- nuovo oggetto tabella d'inventario
local store = {}

-- main data processing function
function item(record)
    -- livello principale: reparti
    -- creo per comodità una variabile stringa per l'ID del reparto
    local dep = record.department
    
    -- creo la tabella relativa al reparto se ancora non esiste
    if not store[dep] then
        store[dep] = {}
    end
    
    -- var di comodo per il riferimento alla tabella di reparto
    local tabdep = store[dep]
    
    -- livello fornitore
    -- variabile di comodo che contiene il codice del fornitore
    local sup = record.supplier
    
    -- se non esiste la tabella fornitore nel reparto la creo
    if not tabdep[sup] then
        tabdep[sup] = {}
    end
    
    -- livello articolo
    -- vars di comodo alla tabella fornitore e al codice articolo
    local tabsup = tabdep[sup]
    local codeart = record.code
    
    -- se il codice esiste aggiorniamo per quantità
    -- altrimenti creiamo nuova tabella articolo per
    -- la descrizione, il costo e la quantità
    if tabsup[codeart] then
        tabsup[codeart].qty = tabsup[codeart].qty + record.qty
    else
        tabsup[codeart] = {}
        local ta = tabsup[codeart]
        ta.descr = record.descr
        ta.cost = record.cost
        ta.qty = record.qty
        ta.class = record.class
    end
end

Raccolta dati

I vantaggi della struttura ad albero creata dalla funzione item() sono due: oltre a classificare per livelli gli articoli è possibile inserire lo stesso articolo con successive chiamate alla funzione. Durante la raccolta dati infatti capita che pezzi diversi dello stesso articolo vengano registrati in tempi diversi.

La raccolta dati può essere effettuata connettendo un lettore di codici a barre a un notebook e gestendo i dati con un foglio elettronico o meglio con un database. In questo modo l'inventario procede molto velocemente e senza fatica, a condizione che sugli articoli sia presente il codice a barre tramite un etichetta per esempio, e che siano già stati inseriti i dati corrispondenti ai codici.

Visitare l'albero

Vorremo conoscere la quantità totale dei pezzi e il costo totale per ogni tipo di articolo (per esempio 'Diario', 'Forbici' o 'Quaderno').
Disegnate la struttura della tabella creata dalla funzione 'item()' e poi scrivete il codice di reporting.

--- Segnalibro esercizio 4 ---

Ecco la mia versione del programma basata su una tabella dove le chiavi sono i nomi delle varie classi articolo e i corrispondenti valori sono tabelle con i campi quantità (qty) e costo (cost).
Il codice non è completo perché non vi ho riportato la funzione 'item()' che è la stessa vista prima, la creazione della variabile 'store' e la chiamata alla funzione 'dofile()'.

local function class_rpt(tree)
    local class_data = {}
    for _, dep in pairs(tree) do
        for _, sup in pairs(dep) do
            for code, item in pairs(sup) do
                if class_data[item.class] then
                    local t = class_data[item.class]
                    t.qty = t.qty + item.qty
                    t.cost = t.cost + item.qty*item.cost
                else
                    class_data[item.class] = {
                        qty  = item.qty,
                        cost = item.qty*item.cost,
                    }
                end
            end
        end
    end
    return class_data
end

-- print report
local class_data = class_rpt(store)

for class, tab in pairs(class_data) do
    local fmt = "Class %20s: qty = %d, cost = %0.2f"
    print(string.format(fmt, class, tab.qty, tab.cost))
end

Memorizzare tutto in un file

Per costruire il file si utilizzano le funzioni della libreria standard di Lua nel modulo io. Le impiegheremo per svolgere il prossimo passo che consiste nel memorizzare i dati ottenuti nell'esercizio precedente nel file "Classi.txt".

La massima efficienza prevede di scrivere il file in un volta sola e di costruirne il contenuto con la funzione 'table.concat()' per evitare di dover concatenare stringhe cosa che produrebbe un enorme trasferimento di memoria come abbiamo già evidenziato in una delle puntate del corso.

Lascio a voi la tastiera e vi aspetto tra qualche riga dopo il segnalibro per presentarvi la mia soluzione del quesito.

--- Segnalibro esercizio 6 ---

Ecco la mia versione che sfrutta la funzione 'class_rpt()' precedentemente definita:
-- create the text file
local function make_class_rpt(fn, data, fmt)
    local fmt = fmt or "Classe %-20s: qty=%6d, cost=%10.2f"
    
    -- data assembly
    local t = {}
    for class, tab in pairs(data) do
        t[#t+1] = string.format(fmt, class, tab.qty, tab.cost)
    end
    local s = table.concat(t, "\n")
    
    -- apertura file
    local f = assert(io.open(fn  .. ".txt", "w"))    
    -- scrittura dati
    f:write(s)
    
    -- chiusura del file
    f:close()
end

local filename = "Classi"
make_class_rpt(filename, class_rpt(store))


L'inventario

L'ultima parte del problema è generare il report principale. Occorre infatti visitare l'albero per trarne i totali dei vari livelli e produrre con essi la stampa. Per ragioni di tempo terremo semplice il codice lasciando irrisolti alcuni problemi come l'ordinamento in ordine alfabetico delle classi del nostro magazzino.

Chiamiamo report() la funzione Lua che visita l'albero iterativamente. Come abbiamo già fatto prima useremo l'iteratore pairs per leggere i valori nelle tabelle dei vari livelli, iteratore che restituisce due argomenti la chiave e il suo valore senza un ordine predefinito.

Ecco le specifiche: l'inventario deve essere suddiviso per reparto con i totali di quantità e costo. Ciascun reparto deve essere suddiviso per fornitore con i relativi totali di quantità e costo. Per ogni fornitore è necessario creare la lista degli articoli con i quattro campi "Descrizione", "Quantità", "Costo". Infine il report dovrà essere memorizzato in formato testo in un file chiamato "Inventario.txt".

Disegnate la struttura della tabella creata dalla funzione 'item()' e poi scrivete il codice di reporting.

--- Segnalibro esercizio 7 ---

Bentornati!
La soluzione da conforntare con la vostra che vi riporto di seguito ha la possibilità di regolare la larghezza delle tre colonne dei dati impostando le tre variabili interne alla funzione 'report()'.
I campi non sono separati da un carattere di tabulazione perché il report cambierebbe allineamento in funzione del numero di caratteri spazio che l'editor in uso assegna alla tabulazione.

Inoltre il calcolo dei subtotali risulta essere il più semplice ed efficiente possibile perché il codice esegue somme di somme.
Ecco il programma Lua completo:
-- nuovo oggetto tabella d'inventario
local store = {}

-- main data processing function
function item(record)
    -- livello principale: reparti
    -- creo per comodità una variabile stringa per l'ID del reparto
    local dep = record.department
    
    -- creo la tabella relativa al reparto se ancora non esiste
    if not store[dep] then
        store[dep] = {}
    end
    
    -- var di comodo per il riferimento alla tabella di reparto
    local tabdep = store[dep]
    
    -- livello fornitore
    -- variabile di comodo che contiene il codice del fornitore
    local sup = record.supplier
    
    -- se non esiste la tabella fornitore nel reparto la creo
    if not tabdep[sup] then
        tabdep[sup] = {}
    end
    
    -- livello articolo
    -- vars di comodo alla tabella fornitore e al codice articolo
    local tabsup = tabdep[sup]
    local codeart = record.code
    
    -- se il codice esiste aggiorniamo per quantità
    -- altrimenti creiamo nuova tabella articolo per
    -- la descrizione, il costo e la quantità
    if tabsup[codeart] then
        tabsup[codeart].qty = tabsup[codeart].qty + record.qty
    else
        tabsup[codeart] = {}
        local ta = tabsup[codeart]
        ta.descr = record.descr
        ta.cost = record.cost
        ta.qty = record.qty
        ta.class = record.class
    end
end

-- main reporting function
local function report(store)
    local rpt = {}
    local inv_qty, inv_cost = 0, 0
    local inv_title = "Inventory report"
    
    rpt[#rpt+1] = inv_title
    rpt[#rpt+1] = string.rep("=", #inv_title).."\n\n"
    
    -- column spacing
    local descr_space, qty_space, cost_space = 28, 10, 12
    local line_spaces = 2 + descr_space + qty_space + cost_space
    -- in format string %% means '%'
    local fmt_line = string.format("%%-%ds %%%dd %%%d.2f", descr_space, qty_space, cost_space)
    
    for dep_name, tab_dep in pairs(store) do
        local dep_qty, dep_cost = 0, 0
        local dep_title = string.format("Department %s:", dep_name)
        
        rpt[#rpt+1] = dep_title
        rpt[#rpt+1] = string.rep("=", #dep_title).."\n"
        
        for sup_name, sup_tab in pairs(tab_dep) do
            local sup_qty, sup_cost = 0, 0
            local sup_title = string.format("Supplier %s:", sup_name)
            
            rpt[#rpt+1] = sup_title
            rpt[#rpt+1] = string.rep("-", #sup_title)
            
            for code, item in pairs(sup_tab) do
                sup_qty = sup_qty + item.qty
                sup_cost = sup_cost + item.qty*item.cost
                rpt[#rpt+1] = string.format(fmt_line, item.descr, item.qty, item.cost)
            end
            
            rpt[#rpt+1] = string.rep("-", line_spaces)
            rpt[#rpt+1] = string.format(fmt_line, "Total supplier", sup_qty, sup_cost)
            rpt[#rpt+1] = ""
            
            dep_qty = dep_qty + sup_qty
            dep_cost = dep_cost + sup_cost
        end
        rpt[#rpt+1] = string.rep("-", line_spaces)
        rpt[#rpt+1] = string.format(fmt_line, "Total department", dep_qty, dep_cost)
        rpt[#rpt+1] = string.rep("-", line_spaces).."\n"
        
        inv_qty = inv_qty + dep_qty
        inv_cost = inv_cost + dep_cost
    end
    
    rpt[#rpt+1] = string.rep("=", line_spaces)
    rpt[#rpt+1] = string.format(fmt_line, "Total inventory", inv_qty, inv_cost)
    rpt[#rpt+1] = string.rep("=", line_spaces).."\n"
    
    return rpt
end

-- create the text file
local function make_txt_file(fn, data)
    -- apertura file
    local f = assert(io.open( fn  .. ".txt", "w"))
    
    -- scrittura dati
    f:write(table.concat(data, "\n"))
    
    -- chiusura del file
    f:close()
end

-- loading data
dofile "dati.lua"

-- report building
local rpt = report(store)
local filename = "Inventario"
make_txt_file(filename, rpt)

-- end


E questo è un frammento del file risultato che si ottiene con il codice precedente e i dati contenuti nel file:
Inventory report
================


Department SCRI:
================

Supplier BestWrite:
-------------------
971495 - Astuccio                    80        16.00
678574 - Astuccio                    81         5.00
643586 - Stilografica                11         1.00
934211 - Stilografica                61        24.00
596141 - Penna                       41        13.00
...
... a lot of lines
...
290676 - Bianchetto                 118        14.00
498096 - Cucitrice                  108         8.00
518327 - Portamine                   30         4.00
639929 - PuntiCucitrice              28        14.00
98644 - Gomma                         4         1.00
113027 - PuntiCucitrice              90        12.00
----------------------------------------------------
Total supplier                     5244     55662.00

----------------------------------------------------
Total department                  44600    538322.00
----------------------------------------------------

====================================================
Total inventory                  308783   3858599.00
====================================================


Conclusioni

Lo script è veloce e indipendente dal sistema operativo: legge un file di testo puro contenente i dati, li assembla nella struttura che corrisponde all'organizzazione reale del nostro magazzino e restituisce un file pronto per essere stampato nella forma voluta e nulla vieta con un ulteriore passaggio di compilazione, che l'inventario sia un report in formato PDF composto con LaTeX.

L'idea si applica alla costruzione di report di dati complessi come un database cinematografico o un bilancio aziendale, oppure a inventari ancora più complessi per esempio per una catena di negozi.

Riassunto della puntata

Nella puntata abbiamo risolto alcuni problemi concreti con Lua. In particolare gli esempi di codice per l'elaborazione di dati mostrano interessanti applicazioni basate su file testuali in formato data description.

Epilogo

Oggi finisce il corso che spero sia stato o vi sarà utile.
Sono soddisfatto del lavoro compiuto e credo di avervi mostrato l'utilità pratica di Lua, questo fantastico piccolo grande linguaggio.
Rimangono inesplorati alcuni temi base di Lua come le coroutine, chissà se ci sarà una puntata extra in merito... magari con l'aiuto di qualcuno di voi.

Ringrazio Luigi che mi chiese di scrivere queste chiaccherate informatiche da pubblicare sul Blog "Lubit Linux" e ovviamente tutti i lettori.

-- exec with Lua
print("Alla prossima.\nR.")

Ciao

3 commenti: