Il blog di Gianni Giaccaglini

Blog su VBA e VSTO
Gianni Giaccaglini

My Links

News

NB - V. anche gli ARTICOLI (in fondo a questa barra)
Solo quesiti validi a: giannigiac@tin.it
Il mio Best seller su VBA
(v. www.hoepli.it)


Il mio ultimo libro su Open XML
(v. www.FAG.it):



La mia nipotina ELISA

Foto con dedica a ME di
Bill Gates giovanissimo
nei mitici anni 80!

Categorie Post

Categorie Articoli

Archivio

Immagini

Blog Stats

Ordinamento di matrici in Visual Studio 2005/2008 (e VSTO 2005/2008)

Ordinamento di matrici in Visual Studio 2005/2008

Utilizzando i VSTO (Visual Studio Tools per Office) è senz’altro importante sfruttare le notevoli potenzialità di Visual Basic 2008 o, almeno, 2005. In particolare quelle relative al trattamento di matrici su RAM. La cosa può essere utile persino in Excel, per non parlare di Word, nell’ipotesi di sfruttare matrici di dati interni, ad esempio relative a statistiche, dati più o meno standard, senza ricorrere, specie nel caso di Excel, a tabelle inserite nel documento o modello.

Questo articolo di tipo didattico si occupa di un aspetto particolare, l’ordinamento (Sort in inglese), parlando di diversi punti non sempre ben documentati né chiari nemmeno a chi si occupa di Visual Basic in generale.

Il metodo Sort applicabile senza problemi a ogni tipo di elenco monodimensionale, tramite la classe Array è particolarmente utile e d’uso immediato. Ma come effettuarlo quando si hanno matrici di due o più dimensioni? La cosa è fattibile - e i manuali specifici lo spiegano abbastanza bene – anche se concettualmente delicata e complicata. In ogni caso, il metodo Sort a prima vista non sembra si applichi direttamente a matrici pluridimensionali. Esaminiamo gradualmente cosa si può fare, partendo da un caso semplice per giungere a due delle più articolate soluzioni.

Ordinamento di due vettori correlati

Per chi ha fretta e magari è un principiante ecco una ricetta di facile consumo e comprensione, in forma di applicazione console, che serve a ordinare una coppia di vettori correlati, con il primo che fa da chiave, ergo funge da base per il sort, che ovviamente deve riguardare anche il secondo vettore.

Module Module1

  Function OrdinaDueVettori(ByVal Vett1 As Array, ByVal Vett2 As Array)

    ' Ordinamento in base a Vett1 anche del secondo Vett2

    Dim Vett1Old = Vett1.Clone

    Dim Vett2Old = Vett2.Clone

    Array.Sort(Vett1) ' Ordinamento del vettore chiave

    Dim k = 0

    For i = 0 To UBound(Vett1) ' oppure: To V1.Length - 1

        k = Array.IndexOf(Vett1Old, Vett1(i))

        Vett2(i) = Vett2Old(k)

    Next

    ' Restituisci entrambi i vettori, ordinati

    Return Vett1

    Return Vett2

  End Function

 

  Sub Main()

    Dim V1() = {3, 5, 4, 6, 2}

    Dim V2() = {"Mele", "Pere", "Susine", "Pesche", "Fichi"}

    OrdinaDueVettori(V1, V2)

    Dim ind = 0

    For ind = 0 To UBound(V1) ' oppure: To V1.Length - 1

      Console.WriteLine("Codice: {0} Frutto: {1}", V1(ind).ToString, V2(ind))

    Next

    Console.ReadLine()  Next

. . . . ECCETERA . . .

  End Sub

End Module

 

L’estensione al caso Excel o Word + VSTO è abbastanza immediata: basta sostituire a Main un opportuno evento come tipicamente il Click su un qualche pulsante, ovvero in parole povere un qualche Button1_Click con quel che segue.

Commenti stringati. Va premesso che un’alternativa cui di sicuro avranno pensato in molti è l’impiego di un classico algoritmo specifico, a partire dal pur banale bubble sort, nel qual caso il procedimento può applicarsi a un numero qualsiasi di dimensioni. Ad esempio lo spostamento di “bolle” nel bubble sort procederebbe, in parallelo, su tutte le altre dimensioni oltre che sulla chiave. Esercizio lasciato a chi legge queste noterelle (e comun que richiamato in fondo a questo post).

Ma qui l’obiettivo didattico è, di proposito, l’impiego del metodo Sort offerto gratis da Visual Studio 2005 e 2008. Il nostro procedimento di cui è facile rendersi conto si basa su due punti:

  • La copia dei valori dei due vettori originari, rispettivamente in Vett1Old e Vett2Old;
  • La trascrizione, dopo il sort di Vett1, dei valori di Vett2Old in Vett2, tenendo conto della posizione (indice) originaria ricavata dalla posizione (IndexOf) di ciascun nuovo valore Vett1(i) nel vecchio Vett2Old.

Più difficile da dire che da capire, no? Di un qualche interesse – magari anche per chi si trova nel mezzo del cammin di sua vita di principiante – l’utilizzo del metodo Clone che crea una copia di valori del duo Vett1 e Vett2. Esso, in primo luogo, sintetizza un loop del genere seguente (idem con patate per Vett2):

For i = 0 To UBound(Vett1)

  Vett1Old(i) = Vett1(i)

Next

Ma Clone serve anche per un motivo più sottile (e da non dimenticare). Se infatti, per distrazione, si ricorresse a istruzioni Vett1Old = Vett1 e Vett2Old = Vett2 il nostro bell’algoritmo fallirebbe! Questo perché si otterrebbero copie per riferimento, per cui entrambi Vett1Old e Vett1 verrebbero ordinati, ergo il vecchio legame tra i due vettori originari andrebbe di fatto perduto.

Nota Questa prima ricetta, in realtà ha un’alternativa che sarà più chiara Al termine dell’articolo. Suspense...

Una possibile disillusione

Si sta sempre parlando a principianti del mondo .NET, che magari provengono dal mondo Excel le cui tabelle, strutturate o semplici, si possono ordinare sulla base di uno o più campi (ovvero colonne di quel certo intervallo).

La prima tentazione nasce da una tabellina 2D come la seguente:

Dim Tab(,) = {{3, 5, 4, 6, 2}, {"Mele", "Pere", "Susine", "Pesche", "Fichi"}}

For Each t In Tab

    MsgBox(t.ToString)

Next

(notare il ciclo For Each singolo, che a partire da VS 2005, se non vado errato e come non molti sanno, riuassume e sintetizza due loop annidati classici spazzolando per righe e per colonne, in quest’ordine.

Come anticipato, l’applicazione del metodo Sort a una siffatta Tab, viene subito delusa da uno sdegnoso rigetto del compilatore, in quanto Sort non sembrerebbe che preveda un argomento per distinguere la chiave di ordinamento fra i due (o più) campi della matrice, la qual cosa è invece possibile in ambiente Excel, come ben sa chi proviene dal mondo Office ed è, probabilmente, più portato di altri a sperare che l’ordinamento avvengo con altrettanta semplicità anche altrove. Peccato che simili cose non sono, perlomeno direttamente, possibili nel più nobile mondo .NET.

Nota Ma attenzione! Non si faccia confusione tra tabelle insorporate sul foglio e matrici VB.

Bubble sort rivisitato. Per la gioia dei più... piccini e, magari, per risvegliare la memoria di distratti e immemori, riporto infine una possibile routine del genere, applicata a una generica matrice pluridimensionale Matr.

Dim swScambio = False

Do

  swScambio = False

  For i = 0 To UBound(Matr) - 1

    If Matr(i, 0) > Matr(i + 1, 0) Then

      For j = 0 To UBound(Matr, 2)

          Temp = Matr(i, j)

          Matr(i, j) = Matr(i + 1, j)

          Matr(i + 1, j) = Temp

      Next

      swScambio = True

    End If

  Next igli oggettiLoop Until Not swScambio

Ordinamento di matrici, in due modalità

Personalmente sarei portato ad auspicare che in un qualche rilascio futuro venga supportato uno specifico oggetto che chiamerei “Table”, formalmente e sostanzialmente analogo a quello di Excel.

Nota Ricordo che anche in Word è presente un oggetto del genere, concettualmente analogo anche se strutturato in modo un po’ diverso...

Tale oggetto “Table” sarebbe l’equivalente naturale, in memoria, dei ben noti, omonimi parenti dei database relazionali che risiedono su disco.

Ma lasciando perdere sogni e aspirazioni, che lasciano il tempo che trovano, e rimanendo ancorati al tema sort, i procedimenti che qui suggerisco sono due, il secondo, basato sul nuovo linguaggio LINQ ergo valido soltanto col Framework 3.5, vale a dire con Visual Studio 2008 (e l’imminente 2010, sicuramente).

Il primo, almeno a quanto mi consta, è noto a pochi. Infatti tra i vari manuali di cui dispongo solo la Bibbia di Francesco Balena Programmare Microsoft Visual Basic .NET”- ed. Mondadori. Eccone subito un esempio, in veste di Console application (ma anche qui – come nei successivi esempi – dovrebbe essere facile l’estensione a un evento di Excel/Word scatenato da qualche controllo, secondo quanto già detto sopra):

Module Module1

  Structure Persona ' Oppure Class

    Public Cognome As String

    Public Nome As String

    Public DataNascita As Date

    Sub New(ByVal cognome As String, ByVal nome As String, ByVal datanascita As Date)

      Me.Cognome = cognome

      Me.Nome = nome

      Me.DataNascita = datanascita

    End Sub

  End Structure ' Oppure End Class

 

  Sub Main

  ' Crea una matrice di prova

  Dim Persone() As Persona = _

  {New Persona("Rossi", "Paolo", #4/2/1980#), _

   New Persona("Bianchi", "Luisa", #7/12/1985#), _

   New Persona("Verdi", "Emilio", #8/5/1975#), _

   New Persona("Landi", "Remo", #5/10/1981#), _

   New Persona("Renzi", "Paola", #4/2/1980#)}

  ' Crea un vettore correlato contenente le date di nascita

  Dim DateNascita(UBound(Persone)) As Date

  Dim j As Integer = 0

  For j = 0 To UBound(Persone)

      DateNascita(j) = Persone(j).DataNascita

  Next

  ' Ordina Persone su data di nascita, usando DateNascit come chiavi

  Array.Sort(DateNascita, Persone)

  ' Mostra il risultato

  Dim P As Persona = Nothing

  For Each P In Persone

      Console.WriteLine(P.Cognome & " " & P.Nome & " " & P.DataNascita)

  Next

  Console.ReadLine()

  End Sub

End Module

 

I commenti inseriti dovrebbero rendere eloquente il procedimento. Due sono le cose che si apprendono:

  • Una matrice che, in buona sostanza, corrisponde a una tabella viene definita partendo da una struttura che ne definisce i campi e su tale base è possibile creare una matrice nel modo indicato nello snippet;

·         Il sospirato metodo Sort si applica a una siffatta matrice (evviva!) ma per farlo – ecco il segreto ignoto ai più – bisogna creare un vettore parallelo contenente gli elementi del campo che vogliamo sia la chiave di ordinamento.

Nota Di passaggio ricordo che per ottenere l’ordinamento in senso discendente si deve ricorrere al metodo Reverse.

Si noti poi che anche il vettore parallelo viene ordinato, per cui non è indispensabile caricarlo di nuovo le volte successive alla prima:

' Amplia di un’unità la matrice Persone

Dim MaxInd As Integer = Ubound(Persone) + 1

Redim Preserve Persone(MaxInd)

' Amplia pure il vettore correlato (non occorre ricaricarlo)

Redim Preserve DateNascita(MaxInd)

' Aggiungi una Persona

Persone(MaxInd) = New Persona("Brambilla", "Mauro", #5/7/1982#)

' Ordina di nuovo la matrice estesa

  Array.Sort(DateNascita, Persone)

 

Intrichi a parte, si può dire tutto risolto per il sospirato oggetto “Tabella” ? Non del tutto. Infatti stiamo parlando solo dell’ordinamento. Infatti vanno gestite a parte altre funzionalità, in primo luogo la selezione, filtro in gergo database, di elementi dotati di determinate caratteristiche.

A questi più ampi requisiti risponde la seconda soluzione, che si affida al nuovo linguaggio LINQ (ripeto e insisto: supportato solo a partire di VS 2008), Questo di regola si occupa di database relazionali, essendo derivato dal noto linguaggio di query SQL, ma fornisce anche “motori” (“provider”, in gergo) per altre fonti di dati, come gli archivi XML. Nell’edizione  LINQ To Objects si rivolge appunto ai più svariati oggetti in memoria e promette (e mantiene) fra l’altro articolate opzioni di ordinamento, valide in particolare per liste di classi. Vediamo subito una tipica ricetta, che si rifà in parte all’esempietto di apertura:

Module Module1

  Class Ordine ' Oppure Structure

    Public COD As Integer

    Public Frutto As String

    Public Giacenza As Integer

  End Class 'Oppure Structure

 

  Sub Main()

    Dim ElencoOrdini As New List(Of Ordine)

    Dim Codici() = {3, 5, 4, 6, 2}

    Dim Frutti() = {"Mele", "Pere", "Susine", "Pesche", "Fichi"}

    Dim Giacenze() = {250, 150, 100, 75, 90}

    Dim i = 0

    For i = 0 To UBound(Codici)

      ElencoOrdini.Add(New Ordine With _

                  {.COD = Codici(i), _

                   .Frutto = Frutti(i), _

                   .Giacenza = Giacenze(i)})

    Next

' Mostra situazione prima del Sort

    For Each Ord In ElencoOrdini

      Console.WriteLine(Ord.COD.ToString & " - " _

      & Ord.Frutto & " - " & Ord.Giacenza.ToString)

    Next

    Console.WriteLine()

' Esegui il Sort in linguaggio LINQ

    Dim Ordini = From Ord In ElencoOrdini _

                 Order By Ord.COD _

                 Select Ord

' Mostra situazione dopo il Sort

    Console.WriteLine("Ordini ordinati per CODice")

    For Each Ord In Ordini

      Console.WriteLine(Ord.COD.ToString & " - " _

      & Ord.Frutto & " - " & Ord.Giacenza.ToString)

    Next

    Console.ReadLine()

  End Sub

 

End Module

 

Anche in questo caso si parte con la definizione di una struttura o classe ad hoc e, rispetto al caso precedente ora si ricorre al List(Of <T>) per definire e successivamente popolare un elenco di oggetti Ordine. Si noti la nuova particolare sintassi relativa all’aggiunta di elementi alla lista, con il punto che segue With: New Ordine With {.COD =... .Frutto =... }. Come dovrebbe essere limpido a tutti, stavolta si è fatto ricorso a tre vettori di costanti Codici, Frutti e Giacenze, per amore di variante.

Il clou del programmino è dato dalle istruzioni specifiche in LINQ, che do per auto esplicative. Aggiungo piuttosto che, stavolta sono offerte ulteriori possibilità, in particolare con le clausole Where (per filtrare secondo vari criteri) e Join (unione di tabelle correlate). La materia esula dagli scopi di questo post, comunque terminiamolo con un esempio di filtro, che palesemente oltre al riordino restituisce tutti e soli gli elemento il cui codice è maggiore di 3:

Dim Ordini = From Ord In ElencoOrdini _

             Where Ord.COD > 3

             Order By Ord.COD _

             Select Ord

 

Due ultimissime annotazioni. La prima è l’intercambiabilità fra struttura e classe espressa nei commenti di entrambi gli esempi. La seconda tratta la Classe definendone le proprietà semplicemente come variabili Public. La cosa apparirà brutale a chi ha il vezzo (o malvezzo?) di definire sempre e in modo completo le Property, mediante Get e Set anche quando non servono.

Nota L’edizione imminente 2010 di Visual Studio ora permette di definire le Property anche senza Get e Set (come già accade in C#). A mio avviso è un mero orpello sintattico, che mira solo a scoraggiare il malvezzo di cui sopra... Beninteso la distinzione fra struttura e classe è importante, almeno nei casi meno semplici rispetto a quelli qui trattati.

La seconda osservazione è relativa a una differenza sostanziale fra i due procedimenti, che potrebbe essere sfuggita ai più distratti. Nel primo l’ordinamento modifica la matrice originaria, mentre LINQ crea una nuova versione dell’elenco. La cosa è facilmente verificabile e si spiega altrettanto bene, per il fatto che LINQ può anche filtrare, con Where.

Riflettete, gente.

Chiarimento finale sul metodo Array a due argomenti

Riflettendo ho poi compreso il preciso significato del metodo Sort dell’oggetto System.Array. La sintassi è la seguente, col secondo argomento opzionale:

Array.Sort(VettChiavi, Matrice)

Ove, si badi bene!, l’ordinamento, per così dire in prima battuta, si applica alla matrice monodimensionale VettChiavi mentre viene ordinata, in parallelo, anche la Matrice che – essa soltanto – può avere più dimensioni o campi che dir si voglia. La cosa è stata ben chiarita nella penultima parte dell’articolo. Ma allora perché ritornarci?

Il motivo è una sottigliezza che sfugge facilmente: VettChiavi NON è obbligatoriamente una replica del campo di Matrice scelto per il sort e può anche essere un vettore esterno, ovviamente di pari potenza, anche diverso dai campi della Matrice.

L’esempio seguente estensibile al caso in cui il secondo argomento è una matrice a più dimensioni chiarisce la faccenda:

Dim Codici()= {3, 5, 4, 6, 2}

Dim Frutti()= {"Mele", "Pere", "Susine", "Pesche", "Fichi"}

Array.Sort(Codici, Frutti)

Dim k = 0

For k = 0 To UBound(Codici)

  Console.WriteLine(Codici(k).ToString & " - " & Frutti(k))

Next

 

Non sfuggirà che si tratta del problemino esposto in apertura, che diventa perciò obsoleto (anche se didatticamente resta valido, spero).

 

?>

posted on sabato 27 giugno 2009 13.27