Archive for the ‘Gambas’ Category

La formula di Luhn

Mercoledì, Febbraio 18th, 2009

La formula di Luhn (detta anche algoritmo di Luhn o mod(10)) serve a verificare la validità di alcuni identificativi numerici, come i numeri delle carte di credito.

Il procedimento è il seguente:

  • Partiamo dall’ultima cifra, che per noi sarà la prima, e ci spostiamo verso sinistra. Moltiplichiamo per 2 tutte le cifre che si trovano in posizioni pari (2, 4, 6 etc). Se il risultato è composto da due cifre, le addizioniamo tra loro, ottenendo così un risultato composto da una cifra sola (14 diventa: 1+4=5)
  • Sommiamo tra loro tutte le cifre, sia che si trovino in posizione pari, sia che siano in posizione dispari.
  • Se il risultato ottenuto è divisibile per 10 (il risultato è un numero intero e non c’è resto), il numero è valido.

Si noti che questa formula non verifica di quanti numeri è composto il numero. Per controllare che sia un numero di carta di credito bisognerebbe almeno controllare che sia composto da 16 cifre. Inoltre la formula di Luhn non effettua alcun controllo riguardo la data di scadenza della carta nè altri dati importanti.

Deve essere quindi considerata come una formula generica atta a verificare che non ci siano stati errori di digitazione, nient’altro.

Ora vediamo le implementazioni in linguaggio PHP e in Gambas.

Formula di Luhn in Gambas

PUBLIC FUNCTION Luhn(StrInput AS String) AS Boolean

  DIM i AS Integer                ' digit index
  DIM CurrentDigit AS Integer     ' current digit being processed
  DIM isEven AS Boolean           ' if true, the digit must be processed
  DIM IntTotal AS Integer         ' total for the last computation

  isEven = FALSE
  i = Len(StrInput) - 1

  WHILE i >= 0
    CurrentDigit = CInt(CharAt(StrInput, i))
    IF isEven THEN
      CurrentDigit *= 2
      IF CurrentDigit > 9 THEN
        CurrentDigit -= 9
      ENDIF
    ENDIF
    IntTotal += CurrentDigit
    isEven = NOT isEven
    i -= 1
  WEND

  IF (IntTotal MOD 10) = 0 THEN
    RETURN TRUE
  ELSE
    RETURN FALSE
  ENDIF

END

PUBLIC FUNCTION IsCreditCard(StrInput AS String) AS Boolean
  IF Len(StrInput) <> 16 THEN
    RETURN FALSE
  ENDIF
  RETURN Luhn(StrInput)
END

Formula di Luhn in PHP

function mod_10($code)
{
  setType($code, 'string');
  $isEven = false;
  $total = 0;

  for ($i=strlen($code)-1; $i>=0; $i--) {
    $digit = substr($code, $i, 1);
    if ($isEven) {
      $digit *= 2;
      if ($digit>9)
        $digit -= 9;
    }
    $total += $digit;
    $isEven = !$isEven;
  }

  return (bool) !($total % 10);
}

function is_credit_card($code)
{
  return (strlen($code)==16) && mod_10($code);
}

Note

Il procedimento è lo stesso in entrambi i linguaggi.

Per motivi di performance, ho scritto un unico ciclo nel quale viene eseguita la moltiplicazione per due quando è il caso, per poi sommare il risultato (o il numero non moltiplicato) al totale.

Ho semplificato anche la somma delle due cifre che compongono il risultato della moltiplicazione. Ho potuto farlo perchè so che la cifra di sinistra è sempre 1. Se la togliessimo bisognerebbe sottrarre 10. Siccome dopo bisognerebbe aggiungere 1, è meglio fin dall’inizio sottrarre 9 al numero a due cifre.

Per controllare se un risultato è divisibile per 10 si utilizza l’operatore modulus (’MOD’ in Gambas e ‘%’ in PHP), che restituisce il resto di una divisione.

Generare numeri validi

Abbiamo visto che, perchè un numero sia valido, è sufficiente che il procedimento sopra spiegato generi un totale divisibile per 10. Si può quindi generare un ID valido (UNO: mai 0, mai 2), di n cifre, partendo da una qualsiasi combinazione di cifre lunga n-1.

Il procedimento per ottenerlo è molto semplice: aggiungiamo uno 0 a destra e applichiamo all’id la formula di Luhn. Se il risultato è divisibile per 10 abbiamo già ottenuto ciò che vogliamo. Altrimenti occorre sostituire lo 0 con la cifra corretta. Prendiamo la cifra più a destra del risultato ottenuto (per 64 è 4); la chiameremo d. Eseguiamo 10 - d. Il risultato è la cifra che dovrà sostituire lo 0 che abbiamo inserito.

Essendo la formula molto semplice, non è il caso di riscriverla in entrambi i linguaggi.

Generare id di Luhn validi in PHP

function luhn_generate($code)
{
  setType($code, 'string');
  $code .= '0';
  $isEven = false;
  $total = 0;

  for ($i=strlen($code)-1; $i>=0; $i--) {
  $digit = substr($code, $i, 1);
  if ($isEven) {
    $digit *= 2;
    if ($digit>9)
      $digit -= 9;
  }
  $total += $digit;
  $isEven = !$isEven;
  }

  if ($total % 10) {
    $digit = $total % 10;
    $digit = 10 - $digit;
    $code = substr($code, 0, strlen($code)-1) . $digit;
  }

  return $code;
}

Si potrebbero apportare un paio di ottimizzazioni ma, siccome non sono fondamentali e renderebbero il codice un po’ meno chiaro, lascio al lettore volenteroso il compito di trovarle. Il chè mi porta a dare un consiglio generale: se volete aggiungere modifiche che riducono la leggibilità documentate il tutto (come io non ho fatto in questi esempi) e apportatele solo dopo aver scritto un codice un po’ meno ottimizzato ma ben funzionante.

Dimostrazione

Vedi il mio demo per la verifica e la generazione di id validi. Utilizza le funzioni PHP esposte in questa pagina:

Buon divertimento e restate liberi.

La distanza Levenshtein

Martedì, Gennaio 13th, 2009

La distanza Levenshtein tra due stringhe, dal nome di Vladimir Levenshtein, è detta anche distanza edit. Si tratta del numero minimo di modifiche elementari che deve essere compiuto su una delle due stringhe per renderla uguale all’altra. Per modifiche elementari si intende:

  • l’inserimento di un carattere;
  • la cancellazione di un carattere;
  • la sostituzione di un carattere con un altro carattere.

Questo algoritmo è presente in una funzione PHP chiamata appunto levenshtein(). Ecco un esempio di implementazione in Gambas 2.0:

PUBLIC FUNCTION Levenshtein(Str1 AS String, Str2 AS String) AS Integer

  DIM Index1 AS Integer ' for Str1
  DIM Index2 AS Integer ' for Str2
  DIM Grid AS NEW Integer[Len(Str1) + 1, Len(Str2) + 1]
  DIM Cost AS Integer ‘ edit cost

  ' initialize grid
  Grid[0, 0] = 0
  FOR Index1 = 1 TO Len(str1)
    Grid[Index1, 0] = Index1
  NEXT
  FOR Index2 = 1 TO Len(str2)
    Grid[0, Index2] = Index2
  NEXT

  ' compare all chars
  FOR Index1 = 1 TO Len(str1)
    FOR Index2 = 1 TO Len(str2)
      IF CharAt(Str1, Index1 - 1) = CharAt(Str2, Index2 - 1) THEN
        Cost = 0 ' no edit
      ELSE
        Cost = 1 ' edit
      ENDIF
      Grid[Index1, Index2] = Min3(Grid[Index1 - 1, Index2] + 1, Grid[Index1, Index2 - 1] + 1, Grid[Index1 - 1, Index2 - 1] + Cost)
    NEXT
  NEXT

  RETURN Grid[Len(Str1), Len(Str2)]

END

L’algoritmo Soundex

Giovedì, Ottobre 23rd, 2008

Soundex è un algoritmo fonetico pensato per la lingua inglese. Il suo scopo è fornire una rappresentazione approssimativa di come un termine inglese viene pronunciato. E’ stato pensato e provato soprattutto sui cognomi, perchè è stato inizialmente sviluppato per il primo censimento della popolazione USA; ad ogni modo funziona abbastanza bene con ogni tipo di parola e mi sembra funzionare discretamente anche con lo spagnolo. Soundex è ben lungi dall’essere perfetto, ma nella maggior parte dei casi è adatto allo scopo. Si ricordi però di utilizzarlo con le singole parole e non con intere frasi, avendo cura inoltre di eliminare la punteggiatura del caso.

L’algoritmo

Ecco come funziona. L’output è presentato in lettere maiuscole, in ogni caso ovviamente Soundex non è case-sensitive. La prima lettera dell’output corrisponde sempre alla prima lettera della parola presa in input. Successivamente, si continua a scorrere i caratteri che compongono la parola. Si ignorano le vocali. Le consonanti vengono tradotte in un numero, secondo il seguente schema:

B,F,P,V => 1
C,G,J,K,Q,S,X,Z => 2
D,T => 3
L =>
4
M,N => 5
R => 6

Come si vede, le consonanti dal suono simile corrispondono allo stesso numero, mentre le lettere H e W vengono ignorate.Nella “traduzione” però bisogna evitare di affiancare numeri simili. Se si trova una C seguita da una vocale e poi da una G, ad esempio, il numero 2 deve comparire una sola sola volta. La stringa restituita deve essere di 4 caratteri. Nel caso sia più breve, vengono aggiunti degli 0 fino ad arrivare a quattro caratteri. Nel caro la parola sia lunga, l’output viene comunque troncato.

L’algoritmo è estremamente semplice da implementare e molto diffuso.

Varianti

Il Reverse Soundex è identico all’algoritmo originale, eccetto che la prima lettera dell’output è l’ULTIMA lettera dell’input. Esiste un certo numero di altre varianti, ma per quanto ne so sono tutte pensate per la lingua inglese.

Molte implementazioni, come quella usata da MySQL, possono restituire più di 4 caratteri nel caso la parola contenga un maggior numero di consonanti con suoni differenti. 4 caratteri sembra essere comunque il numero minimo utilizzato in ogni implementazione.

Adattarlo alla lingua italiana richiederebbe un certo studio, accompagnato da test su migliaia di termini. Innanzitutto bisognerebbe impostare una diversa tabella di associazioni tra consonanti e numeri. Inoltre, bisognerebbe pensare se è il caso di ignorare le consonanti doppia (in inglese questo problema non esiste, ma noi dovremmo distinguere tra copia e coppia…). Inoltre le vocali dovrebbero influenzare il modo in cui le consonanti vicine vengono tradotte in numero e probabilmente anche gli accenti dovrebbero in qualche modo influire.

In Gambas, ragazzis!

Perdonate la battuta penosa, ultimamente ho riletto un po’ di vecchi Dylan Dog e visto un film dei fratelli Marx.

Riporto qui, a titolo di esempio, un’implementazione in Gambas di Soundex. Essa può restituire più di quattro caratteri ma non meno, è pertanto pienamente compatibile con MySQL. Aggiungere il limite di 4 caratteri è estremamente semplice anche per chi dovesse conoscere poco questo linguaggio libero basato su BASIC. Buon divertimento!

‘—–
‘ Returns the soundex representation of a string. Output may be longer than 4
‘ Requires: CharAt

‘ @strInput : String : Input

‘ @RETURN : String : Soundex string

PUBLIC FUNCTION Soundex(strInput AS String) AS String

  DIM strOutput AS String
  DIM Char AS String
  DIM PrevChar AS Integer ‘ previously examined char
  DIM LastChar AS Integer ‘ position of the string’s last char
  DIM IndexChar AS Integer
  DIM IndexCode AS Integer
  DIM CharCode AS NEW String[7]

  ’ initialize character codes
  CharCode[1] = “BPFV”
  CharCode[2] = “CSGJKQXZ”
  CharCode[3] = “DT”
  CharCode[4] = “L”
  CharCode[5] = “MN”
  CharCode[6] = “R”

  strInput = UCase(strInput)

  Char = CharAt(strInput, 0)
  ’ first char code (to avoid duplicates)
  FOR IndexCode = 1 TO 6
    IF InStr(CharCode[IndexCode], Char) THEN
      PrevChar = IndexCode
      BREAK
    ENDIF
  NEXT
  strOutput = Char

  LastChar = Len(strInput)
  FOR IndexChar = 1 TO LastChar ‘ first char is already done
    Char = CharAt(strInput, IndexChar)
    FOR IndexCode = 1 TO 6
      IF InStr(CharCode[IndexCode], Char) AND IndexCode <> PrevChar THEN
        strOutput &= IndexCode
        PrevChar = IndexCode
        BREAK
      ENDIF
    NEXT
  NEXT

   WHILE Len(strOutput) < 4
    strOutput &= “0″
  WEND

  RETURN strOutput

END