Conversione da lettere a cifre, l'algoritmo

Conversione da lettere a cifre, l'algoritmo

Premessa
Quel che segue ha soltanto valenza didattica e pochissima utilità pratica, pertanto siete ancora in tempo a fermarvi qui :o)
Non sono necessari requisiti particolari per la comprensione di questo articolo, tuttavia vi chiedo un leggero sforzo di adattamento per capire il mio italiano, laddove risultasse nebuloso od oscuro per mia mera incapacità espressiva...
Il codice è nato per essere utilizzato in un foglio di Excel e sfrutta la funzione nativa Evaluate per la tramutazione finale della stringa rielaborata. Per essere trasportabile in VB (che non riconosce Evaluate) ho aggiunto una piccola funzione che si appoggia però alla libreria Microsoft Script Control 1.0, che simula Evaluate. Il funzionamento è identico; io, a dire il vero, preferivo la funzione nativa ma tant'è :o)
Gli impazienti, se non vogliono leggersi la pappardella che segue, possono scaricare subito, da qui, il codice della funzione Let2Num.


Il cuore della questione
Il "problema" è nato per semplice curiosità. In giro esistono valide e operative funzioni che trasformano un numero in cifre nella corrispondente stringa letterale (ce ne sono anche sul sito di VB-T&T), ma la domanda è: si può ragionevolmente ottenere l'inverso? Partendo cioè da una stringa che descrive un numero in lettere, si può ricavare il corrispondente numero in cifre? Con "ragionevolmente" intendo: senza ricorso ad Assembler e senza sottoporre il codice ad arzigogoli strampalati (quindi con economia delle risorse e del tempo impiegato).
L'algoritmo da me escogitato non è certamente il migliore; ha dalle lacune che, in caso si scrivano stringhe particolarmente costruite, produce dei risultati imprevisti, ed è sicuramente ottimizzabile, ma rivela comunque alcuni aspetti che ritengo interessanti.
In sostanza, la funzione Let2Num è una gran manipolatrice di stringhe: fa largo ed intensivo uso di Replace, Instr e Mid, il che probabilmente comporta un leggero calo delle prestazioni (peraltro il tempo impiegato dal parser per analizzare una stringa anche molto lunga è davvero trascurabile, soprattutto per i pc di oggi).


Il procedimento adottato (metodologia)
Anzitutto una constatazione: è (relativamente) facile leggere una cifra numerica e trasformarla nella corrispondente stringa letterale che la rappresenta, visto che ogni cifra occupa una posizione nota a priori e quindi se ne può calcolare il valore moltiplicandola per la potenza del dieci corrispondente. Più complicato si fa il discorso quando si tratta di far scandire al parser una sequenza di caratteri alfabetici facendoglieli riconoscere per il loro valore intrinseco, più che per la posizione occupata nel testo. Posso scrivere infatti "duecentodiecimilamilionisedici" e "duecentodiecimiliardisedici" perchè il numero risultante è sempre "210.000.000.016".
Il ragionamento alla base dell'algoritmo che vi presento è dunque il seguente.
In più passaggi graduali consecutivi si devono tradurre le forme complesse in forme semplici, perciò si sostituiscono le forme numeriche riconosciute con le corrispondenti cifre. Si cerca di tradurre le decine come coppia "unità di decine + unità", le centinaia come coppia "unità di centinaia" ecc., in modo da avere come base sempre l'indicazione di una cifra unitaria. Una costante stringa contiene dei simboli (in lettere maiuscole) che rappresentano per posizione le potenze del dieci, la stringa letterale da convertire viene trasformata in passaggi successivi fino ad ottenere una nuova stringa, composta da numeri e lettere maiuscole, in cui ogni numero rappresenta se stesso e le lettere maiuscole invece la relativa potenza del dieci.


Come usare la funzione
La funzione (che ho chiamato con estrema fantasia Let2Num, per indicare la trasformazione da stringa letterale a stringa con caratteri solo numerici) accetta due parametri, il primo di tipo stringa e il secondo di tipo booleano. Il primo parametro rappresenta la stringa da interpretare, il secondo indica se l'output debba essere formattato con i separatori delle migliaia. Il risultato è comunque di tipo stringa.

Public Function Let2Num(ByVal sCifraInLettere As String, Optional Formatted As Boolean> = False) As String

La stringa da convertire viene passata in argomento tra parentesi e virgolette, il secondo parametro è opzionale; ecco come si può utilizzare la funzione:
cifra = Let2Num ("centoventisei") ' (restituisce 126)
cifra = Let2Num ("milleseicentotre") ' (restituisce 1603)
cifra = Let2Num ("unmilioneseicentootto", True) ' (restituisce 1.608.008)

Si possono inserire gli spazi nella stringa, perchè vengono automaticamente eliminati dal parser (quindi Let2Num("q u a t t r ocen todue") restituisce correttamente 402).
Si possono scrivere anche numeri con "di" (es. "tre milioni di miliardi di miliardi"). Quest’ultima forma però non restituirà un valore corretto per un problema nella moltiplicazione delle potenze uguali.


La funzione Let2Num nei dettagli
Definisco anzitutto un pattern che rappresenta le potenze del dieci: "UDCKILMARG". Per posizione ogni carattere identifica quindi le unità, le decine, le centinaia, le migliaia e così via. La "K" rappresenta mille perchè si trova in quarta posizione ed ha valore 3 (assunto come base lo zero: 10^3 = 1.000).
Quindi converto la stringa in minuscolo (il perchè sarà chiaro tra poco).
Vengono poi eliminati dalla stringa i caratteri inammissibili (sono ammesse solo le lettere). In prima battuta si sostituiscono nella stringa le occorrenze delle "e accentate" con "e non accentate" (perchè "trè" è sbagliato ma mi sembrava illogico frustrare l'utente stoppando l'esecuzione, sapendo benissimo che intendeva "tre"). L'uso di Replace sarà molto diffuso nel resto della funzione. Ricordo che Replace sostituisce, nella stringa passata come primo argomento, le occorrenze della sottostringa passata come secondo argomento con la stringa passata come terzo argomento. Si può specificare anche il numero di sostituzioni da effettuare (contando così le occorrenze della sottostringa all'interno della stringa), ma tale modalità di utilizzo di Replace non è stata sfruttata in questo algoritmo.

Da notare anche l'utilizzo intensivo in questa funzione dell'operatore Like (un operatore di confronto spesso sottoutilizzato o sottovalutato), che agisce similmente alle Regular Expressions (che sono più potenti ma vanno referenziate adeguatamente, io non ho voluto farlo per far lavorare l'algoritmo con le sole funzioni di base):

If sTemp Like "*[!a-z]*" Then
.
.
.
End If

L'operatore di confronto Like verifica se la stringa non contiene caratteri alfabetici minuscoli, cioè appartenenti al range compreso tra a e z; il punto esclamativo corrisponde a Not e due asterischi sono caratteri jolly. Quindi l'istruzione ritorna True se nella stringa da controllare esiste in qualunque posizione almeno un carattere diverso da un carattere alfabetico minuscolo compreso tra a e z.
Se esiste almeno un carattere non ammesso, viene eliminato dalla stringa sTemp. Per fare ciò, purtroppo, Replace non ci viene in aiuto, quindi ho dovuto eseguire un (doppio) ciclo per passare in rassegna carattere per carattere e scartare quelli il cui codice Ascii è esterno al range 97-122 (corrispondenti appunto a "a-z"). Forse proprio con le Regular Expressions esiste un modo per depurare una stringa dei caratteri non ammessi:

For i = 1 To 96
sTemp = Replace(sTemp, Chr(i), "")
Next
For i = 123 To 255
sTemp = Replace(sTemp, Chr(i), "")
Next

Se la stringa, così setacciata, corrisponde a "zero" o a una stringa nulla, è inutile andare avanti: restituisco 0, e buonanotte. Comunque vengono eliminate anche tutte le occorrenze di "zero" dalla stringa. Come miglioramento, si può prevedere di considerare lo "zero" come inizio di numero con virgola. Viste le (scarne) finalità didattiche di questa funzione, tuttavia, non mi sono addentrato oltre.

Ora siamo quasi pronti per passare nuovamente a setaccio la stringa, onde individuare e scomporre gli elementi fondamentali (che sono le unità).

Creo pertanto gli array fondamentali che dovranno essere individuati e riconosciuti: quelli delle unità (da "uno" a "nove"), dei numeri irregolari (da "undici" a "diciannove") e delle decine regolari (da "dieci" a "novanta"). In teoria, ogni altro numero è formato da una combinazione (letterale) di questi elementi fondamentali.

E finalmente inizia il lavoro vero e proprio di conversione della stringa originale, fin adesso opportunamente trattata, da lettere in numeri.
Vengono gestite le eccezioni e gli ulteriori casi particolari: sostituisco "unmil" con "unomil" ("ventunmila" diventa "ventunomila"), sostituisco "ntun" con "ntaun": "trentuno" diventa "trentauno", ma poiché "ventuno" diventerebbe "ventauno" gestisco l'eccezione: e quindi "ventauno" diventa "ventiuno"). Altre sostituzioni similari le potete vedere nel codice:

'sostituisce "unmil" con "unomil": serve per "ventunmila", " centunmilioni","unmiliardo", ecc.
sTemp = Replace(sTemp, "unmil", "unomil")
'sostituisce "ntuno" con "ntauno" e quindi "ventauno" con "ventiuno"
sTemp = Replace(sTemp, "ntun", "ntaun")
sTemp = Replace(sTemp, "ventaun", "ventiun")
'sostituisce "centott" con "centoott"
sTemp = Replace(sTemp, "centotto", "centootto")
'sostituisce "ntotto" con "ntaotto" e quindi "ventaotto" con "ventiotto"
sTemp = Replace(sTemp, "ntotto", ">ntaotto")
sTemp = Replace(sTemp, "ventaotto", "ventiotto")

Successivamente le forme irregolari (da undici a diciannove) vengono sostituite con le forme scomposte decine/unità, così "undici" diventa " unodecineuno", "quindici" diventa " unodecinecinque", ecc. Utilizzo un ciclo For Each per ciclare nell'array dei numeri irregolari, per individuare nella stringa eventuali occorrenze di questi numeri. Lo stesso viene poi fatto per i numeri che contengono le decine: "trenta" diventa " tredecine", "settanta" diventa " settedecine" e così via.

i = 0
For Each v In sIrregolari
    sTemp = Replace(sTemp, v, "unodecine" & sUnita(i))
    i = i + 1
Next

Quindi siamo partiti da una stringa del tipo "centoventunmilacentottantasei" e abbiamo ottenuto questo: " centoduedecineunomilacentoottodecinesei", che si legge così: "cento duedecine unomila cento ottodecine sei". Ora si deve pensare a sostituire ai suffissi che rappresentano le potenze del dieci altri simboli che poi verranno interpretati dal parser per le opportune moltiplicazioni. Nella costante PATTERN si possono leggere i simboli adottati, per convenzione. Quindi ogni parola "cento" viene sostituita da "C", le "decine" da "D", i "milioni" da "M" e così via (un po' come la notazione romana dei numeri).

Vengono gestiti anche casi particolari: "unmigliaio", "millecentinaia", "decinedimilioni", ecc.

sTemp = Replace(sTemp, "unmigliaio", "1K")
sTemp = Replace(sTemp, "millecentinaia", "I")
.
.
.
sTemp = Replace(sTemp, "milioni", "M")
sTemp = Replace(sTemp, "miliardi", "G")

Abbiamo ottenuto una stringa di questo genere: da "centoventunmilacentottantasei" a "CdueDunoKCottoDsei" :o) Adesso è il momento di interpretare la stringa che contiene i valori letterali delle unità, per ottenere la stringa con i numeri in cifra; utilizzerò il medesimo costrutto del ciclo For Each visto in precedenza per gli irregolari:

i = 0
For Each v In sUnita
    sTemp = Replace(sTemp, v, CStr(i + 1))
    i = i + 1
Next

Nel contesto poi ci preoccupiamo di effettuare altre piccole conversioni al volo (ad esempio, sappiamo che migliaia di milioni sono miliardi).

Prima di arrivare al cuore dell'algoritmo ci preoccupiamo di eseguire una piccola convalida, perchè se dopo tutto questo setaccio la stringa contiene ancora caratteri minuscoli, o diversi da quelli ammessi, allora la funzione si interrompe e avverte l'utente dell'esito infausto:

'(es. "quatro", "quattroZ", "quattrocinquecento") o simboli diversi da * e +
If sTemp Like "*[a-z]*" Or sTemp Like "*[!CDGKMU0-9[*]+]*" Or sTemp Like "*##*" Then
    Let2Num = "Stringa digitata male (probabili errori di battitura)."
    Exit Function
End If

Se tutto è andato bene la stringa di cui sopra diventa questa schifezza: "C2D1KC8D6"... i giochi sono quasi fatti! "Quasi", perchè adesso viene il bello: far riconoscere all'algoritmo le moltiplicazioni e le somme dove ci vanno! Ecco quindi che abbiamo bisogno di una funzione privata "parser" che si occuperà di capire il gioco delle potenze.

Private Function parser(ByVal s As String)

Ho pensato che il modo migliore di affrontare l'argomento fosse di trattare la stringa come scritta in chiaro: da sinistra a destra, moltiplicando le unità per le potenze cui si riferiscono, sommando i valori secondo il significato letterale. In pratica: mi serviva un sistema di parentesi. Allora ho cominciato definendo una Collection che memorizzasse via via i "gruppi" letterali, esattamente come facciamo quando mentalmente suddividiamo un numero in terzine indicanti milioni, migliaia e centinaia.
L'obiettivo era di leggere "centoventunmilacentottantasei" come se fosse "((cento più ventuno) per mille) più (cento più ottanta più sei)".
Detta così sembra logico, coerente e perfino naturale... :o) (oh bè, non so più quante prove ho fatto prima di arrivare ad una versione decentemente stabile di questo algoritmo!!)

Set gruppo = New Collection     'divido la stringa in "gruppi"

Come viene individuato un "gruppo"? Semplicisticamente individuando nella stringa la posizione di una costante letterale "forte" (migliaia, milioni, miliardi: K, M, G) e, sapendo che scrivendo la cifra in lettere si pospone l'indicazione di tale potenza al numero da moltiplicare cui si riferisce (esempio: "sedici mila", "quattro milioni cinquantaquattro mila"), assumo che quanto precede la posizione occupata nella stringa vada moltiplicato per tale costante letterale. La stringa viene quindi smembrata in gruppi più piccoli che dalle potenze superiori finisce alle unità.

i = InStr(s, "G")      'individuo la posizione del simbolo di "miliardo"
If i > 0 Then     'l'ho trovata! quanto precede va moltiplicato per un miliardo
    gruppo.Add "G*(" & Left(s, i - 1) &")"     'memorizzo nella collection il gruppo, preceduto dalla costante del miliardo e le parentesi
    s = Mid(s, i + 1)     'la stringa da elaborare viene depurata del gruppo appena individuato
End If
.
. (...stessa cosa si fa per i milioni e le migliaia...)
.
. (...infine il residuo costituisce un "gruppo" a sé
If s <> "" Then gruppo.Add s

Adesso nella nostra stringa di partenza ("centoventunmilacentottantasei") si sono individuati due gruppi:
gruppo(1) = K*(C2D1)     'si legge: mille per (cento più due decine più una unità)
gruppo(2) = C8D6     'si legge: cento più otto decine più sei unità

Per come è stato strutturato l'algoritmo, il gioco delle sequenze lettere-numeri funziona così: "un numero è sempre preceduto dal segno più e sempre seguito dal segno di moltiplicazione". Quindi devo scandire gruppo per gruppo ed effettuare le sostituzioni richieste da questa regola ("" = "+*"). I gruppi vanno infine com'è logico sommati tra loro.
La funzione Like ci viene ancora una volta in aiuto in questa fase di concatenazione di stringhe:

s = ""      'la stringa s conterrà la sequenza finale (concatenazione dei gruppi)
For Each itm In gruppo
    tmp = ""
    For j = 1 To Len(itm)
        char = Mid(itm, j, 1)
        If char Like "[0-9]" Then
            tmp = tmp & "+" & char & "*"
        Else
            tmp = tmp & char
        End If
    Next
    'infine, i gruppi si sommano tra loro
    s = s & tmp & "+"
Next

Adesso la nostra stringa finale è diventata "K*(C+2*D+1)+C+8*D+6" che ha una forma quasi comprensibile :o)

Siamo giunti quasi alla fine. Si tratta adesso di correggere alcune impurità che residuano dalla successiva concatenazione delle stringhe (si devono eliminare sequenze bizzarre, quali "*()", "*)", ecc.). Ci serviamo di nuovo della funzione Replace.

Finalmente, ecco il pezzettino di codice che ci permette di sostituire nella stringa finale le lettere con l'espressione di potenza del dieci che rappresentano:

For i = 1 To Len(PATTERN)
    char = Mid(PATTERN, i, 1)
    If InStr(s, char) Then s = Replace(s, char, "10^" & CStr(i - 1))
Next

Qui si scandisce la stringa carattere per carattere e, se troviamo una corrispondenza con una delle lettere del PATTERN (che esprimono ciascuna una crescente potenza del dieci), sostituiamo al carattere stesso la sequenza "10^..." dove al posto dei puntini ci sarà la posizione nella costante PATTERN della lettera trovata (ricordo che la posizione nel PATTERN ne identifica la potenza del dieci: per esempio, M è in settima posizione, quindi bisogna elevare dieci alla sesta potenza, visto che la base è zero: 10^0 fa 1).La conversione definitiva della stringa opportunamente trattata si fa valutando l'espressione ottenuta; questa valutazione si ottiene in modo nativo, in Excel, con Evaluate:

parser = Evaluate(s)     'valido solo in Excel

oppure tramite una piccola funzioncina che sfrutta, come dicevo all'inizio dell'articolo, la libreria Microsoft Script Control 1.0:

'naturalmente funziona anche in Excel!
Private Function Evaluate_Expression(expr As String) As String
Dim oScriptCtl As Object
    'creo un riferimento alla libreria Ms Script Control 1.0
    Set oScriptCtl = CreateObject("MSScriptControl.ScriptControl")
    oScriptCtl.Language = "VBScript"
    'valuto l'espressione e restituisco il risultato
    Evaluate_Expression = oScriptCtl.Eval(expr)
    Set oScriptCtl = Nothing
End Function

In entrambi i casi (con il metodo nativo di Excel Evaluate o con questa funzione basata sullo Script Control) viene restituito il valore in cifre della stringa letterale. Il valore restituito è comunque di tipo stringa.
Alla Function chiamante viene quindi ritornato, sotto forma di stringa, il numero formattato con i separatori delle migliaia (e zero decimali) se è stato specificato True ("VERO" se la funzione è utilizzata in una cella di Excel) come parametro opzionale:

Let2Num = IIf(Formatted, FormatNumber(sTemp, 0), sTemp)

That's all, folk! :o)

I difetti, i bachi e i limiti della funzione
Sicuramente non è coperta l'intera gamma della possibilità espressiva di un numero. Un numero può essere scritto in modi diversi, ho cercato di apportare qualche regola di interpretazione, ma naturalmente non si esauriscono tutti i casi possibili. Così, ad esempio, è riconosciuto "duedecine" come sinonimo di "venti", forse ci sono errori di interpretazione difficili da scovare (bisognerebbe effettuare un test approfondito, ma viste le finalità di questa funzione forse è meglio lasciar perdere e gustarsela per quello che è).

Problemi nel parser si hanno ad esempio con numeri quali "centoventisettemilamiliardiuno" o "mille miliardi di miliardi"; non è riconosciuto correttamente perché la stringa da esaminare (nel primo caso "2D7KG1", nel secondo "1KGG") contiene un gruppo che andrebbe risolto esaminando il sottogruppo ("2D7K" * "G" + 1 e "K*G*G"). Forse bisogna studiare un metodo di chiamata ricorsiva.

Certamente poi ci sono limiti dovuti al calcolo e alla precisione, così come al risultato visualizzato; per numeri molto grandi si perdono i riferimenti alle unità: non viene valutata correttamente un'espressione come " unmilionedimilardidue", che non mostra l'unità "due" finale, perchè il parser deve valutare l'espressione 1E15+2 (un numero davvero molto grande), quindi le unità vanno a farsi benedire :o)


Conclusioni
Attualmente mi sono fermato (sono passate quasi due settimane dal primo sabato in cui mi sono messo a giocherellare con questa funzione inutile; mi piacerebbe tuttavia rendere il parser più efficiente nel valutare l'espressione passata soprattutto per calcolare la ricorsività nei sottogruppi (quando si moltiplicano tra loro potenze uguali molto grandi).
Altri spunti di miglioramento:
si può prevedere l'introduzione del concetto di "virgola";
si può migliorare la gestione dello "zero", inteso come parte significativa di un eventuale decimale (quando scrivo "zerozerosette" potrei pretendere di vedere "0,07", mentre se scrivo " zerosette" lo zero iniziale può ignorarsi);
si possono gestire con una semplice implementazione i numeri negativi (preponendo alla stringa il convenzionale "meno");
se qualcuno vuol divertirsi a tradurre tutto per .Net, sarebbe bello vedere in atto anche le differenze sia concettuali che di implementazione (ad oggetti?) dell'intera faccenda;
in teoria non c'è limite superiore (range da zero a infinito), tranne la potenza di calcolo, tuttavia si potrebbero esplorare questi limiti :o)

Ricordo infine che questa funzione ha essenzialmente valore didattico e di curiosità, essendone l'utilità praticamente nulla.

Potete contattarmi per eventuale discussione tra una pausa caffè e l'altra :o)

Scaricate il listato completo della funzione Let2Num da qui.

posted @ venerdì 16 novembre 2007 22:03

Print

Comments on this entry:

# Excel sulla via di Damasco, ovvero: conversioni lungo la via

Left by Il blog di Francesco Cadin at 16/11/2007 22:07
Gravatar

# re: Conversione da lettere a cifre, l'algoritmo

Left by r at 15/04/2009 20:54
Gravatar
ciao francesco ... pensavo d'essere stato il primo ad affrontare l'inutile lavoro di conversione :-) ma oggi sono capitato qui (per caso ... cercando online notizie su valuta.testo) quindi ti lascio la mia testimonianza ... purtroppo mi accorgo d'essere arrivato secondo ma haime! nella convinzione d'essere primo ... non è la prima volta e forse nemmeno sarà l'ultima
lascio il mio link http://excelvba.altervista.org/blog/index.php/Excel-VBA/Convertire-numeri-da-lettere-a-cifre-UDF.html sicuro che ricambierai la lettura ...
ho trovato interessante la funzione Evaluate_Expression che in passato avevo cercato di scrivere (sapevo che in vbscript c'era ma non ero riuscito a recuperare il riferimento) ...
grazie
r

robb.menCHIOCCIOLAgmail.com

# re: Conversione da lettere a cifre, l'algoritmo

Left by Francesco Cadin at 19/08/2010 20:55
Gravatar
Mi rendi un grande e immeritato onore, te ne ringrazio tanto! Ti ho scritto all'indirizzo privato di cui sopra, spero che sia ancora valido :o)
Hai svolto un lavoro poderoso la cui grandezza sta nella sua astrusità didattica :o)
Complimenti e a presto!!

Your comment:



 (will not be displayed)


 
 
 
Please add 5 and 6 and type the answer here:
 

Live Comment Preview: