Visualizzazione post con etichetta VBA Excel. Mostra tutti i post
Visualizzazione post con etichetta VBA Excel. Mostra tutti i post

venerdì 28 novembre 2014

Visualizzazione di percentuali sugli istogrammi in pila 100% in Excel.

Per far figurare le percentuali in un istogramma in pila su Excel.

Età > 65 641 727 636 694 635 483  
Età <=65 99 109 104 109 151 54  
Queste sono le righe di Excel, riga 3 e riga 4, per le quali ho costruito, prendendone i dati, due istogrammi in pila, uno con valori assoluti e uno in percentuale.
Voglio che le etichette dati facciano figurare non i valori assoluti ma i valori in percentuale.
Dunque costruisco l'istogramma in pila partendo da valori in percentuale, e non da questi qui che invece sono i valori assoluti.

Predispongo due righe, e le chiamo Età > 65 percentuale e Età <=65 percentuale.

Nella cella corrispondente alla prima della riga con Età > 65 metto la formula: =B3/(B3+B4)*100

Nella cella corrispondente alla prima della riga con Età <=65 metto la formula: =B4/(B3+B4)*100

Trascino le formule su tutte le righe e ottengo:
Età > 65 percentuale 86,62162162 86,96172249 85,89951378 86,42590286 80,78880407 89,94413408  
Età <=65 percentuale 13,37837838 13,03827751 14,10048622 13,57409714 19,21119593 10,05586592  
Mostrando le etichette dati come "Valore" ottengo la rappresentazione, ovviamente, di queste percentuali.

giovedì 27 novembre 2014

Grafici di Excel: problemi di etichette.

L'altezza dei valori di un istogramma in Excel è troppo differente, perché ho valori di 30.000 accanto a valori di 100, per cui la visualizzazione dei valori più bassi è scarsissima.
Che fare?

Fare grafici separati, è l'unica cosa.
O dividere opportunamente i valori in modo da farli venire più vicini ai valori meno grandi.

Adesso studiamo del grafico come far apparire e sparire le etichette sugli assi dei grafici.

Cliccando lontano dal grafico, sull'area stessa del grafico, ottengo un menu "lungo", nel quale appare "Opzioni grafico".
Apro questo menu e apro la seconda scheda "Assi".
Ora, al posto dei numeri progressivi, voglio far apparire l'anno.
Come faccio?

Torno a quel menu "lungo", e vado su "Dati di origine", quindi su "Etichette asse categorie" si seleziona la parte del grafico che contiene i nomi da far apparire nelle ascisse.

lunedì 24 novembre 2014

Funzioni che scompongono un numero con gli slash e lettera finale

Convertire la lettera dopo lo slash in numero.
Ecco due funzioni:
Function sinistra(s As String) As Integer
    Dim numero As Integer
    If InStr(s, "/") <> 0 Then
        numero = InStr(s, "/") - 1
    Else
        numero = Len(s)
    End If
    sinistra = CInt(Left(s, numero))
End Function
Isola il numero precedente lo slash.

Per l'altra ci devo ragionare.
Se non c'è slash, InStr è pari a zero, e quindi il valore della funzione destra deve essere zero.
Eccola:
Function destra(s As String) As Integer
    If InStr(s, "/") <> 0 Then
        destra = Asc(Right(s, Len(s) - InStr(s, "/")))
    Else
        destra = 0
    End If
End Function

Funzione Asc(String) per ricavare il codice ascii di un carattere.

Adesso troviamo il modo di ricavare il codice ASCII di un carattere...

Facile:
Sub ascii()
    MsgBox Asc("A")
End Sub
che restituisce 65.
Ricordo che il codice ASCII esadecimale di A era 41H, che appunto corrisponde a 65 decimale.

domenica 23 novembre 2014

Codice per individuare il numero di protocollo più alto fra le varie occorrenze di una singola persona nel database.

...e ho buttato giù il codice per selezionare il numero di protocollo più alto fra le varie occorrenze di una stessa persona.
Ecco il foglio con un ipotetico numero di protocollo sulla colonna A:



Ed ecco il codice:
Sub main()
    Dim indirizzo As String
    Dim numero As Long
    Set c = Cells.Find("antonio cacchioni")
    If c Is Nothing = False Then
        indirizzo = c.Address
        Do
        Set c = Cells.FindNext(c)
        
        If Cells(c.Row, c.Column + 1).Formula = "56" And Cells(c.Row, c.Column - 1).Formula > numero _
            Then numero = Cells(c.Row, c.Column - 1).Formula
        Loop While c.Address <> indirizzo
    End If
    MsgBox numero
End Sub
Risultato:



Okay! Funziona!

Ripasso metodi Find e FindNext di un oggetto Range in Excel

Ecco... riprendo confidenza con i metodi Find e FindNext dell'oggetto Range.

Mi creo un foglio di lavoro con dei nomi di fantasia e delle età...



Ed ecco il codice:
Sub main()
    Dim indirizzo As String
    Set c = Cells.Find("antonio cacchioni")
    If c Is Nothing = False Then
        indirizzo = c.Address
        Do
        Set c = Cells.FindNext(c)
        c.Interior.Color = vbCyan
        Loop While c.Address <> indirizzo
    End If
End Sub
Il FindNext, una volta trovato l'ultimo indirizzo, ricomincia daccapo.
Per questo è importante interrompere il Loop quando l'indirizzo del range trovato coincide con l'indirizzo iniziale.

Ed ecco il codice eseguito, con la colorazione in celeste di tutte le occorrenze del nome prescelto:

martedì 18 novembre 2014

Risolto problema dell'amministrativo della ASL per i compensi orari, con il formato delle celle tipo "ora".

La formula che ho inserito nella cella "Compenso orario" è =F5*60, ossia moltiplica per 60 il valore contenuto nella cella F5.
Così, se in F5 ho 1.5 il risultato nella cella "Compenso orario" è 90.
Se in F5 ho 2, il risultato della cella "Compenso orario" è 120.

Lapalissiano!

Il problema è se il formato di F5 è formato ora e non generico

Ho conferito a F5 il formato "ora", e se inserisco 1:00, nella casella Compenso Orario ottengo adesso 2,50.
Perché?

Proviamo ancora con altri valori...

Con 2:00 ottengo 5,00.
Con 3:00 ottengo 7,50 (me l'aspettavo!)

Perché? Mi sembra ovvio che la cella Compenso Orario non moltiplichi 60 per 1, 2 o 3, ma per altri numeri.
Riconvertendo il formato di F5 con il numero scritto dentro, vediamo a cosa corrisponde quel numero in formato generico...

1:00 ---> 0,0416666666666667
2:00 ---> 0,0833333333333333
3:00 ---> 0,125
Googliamo con questi valori.


Nella cella in formato Ora è rappresentato il numero di ore, ma in realtà il valore della cella è pari al numero di giorni che entrano in quel numero di ore, ossia numero di ore / 24.
Quando si moltiplica dunque, nella cella Compenso Orario 60 per il valore della cella, non lo si moltiplica per il numero di ore rappresentato, ma per il numero di giorni compreso in quelle ore.
Per ottenere il valore reale, bisogna moltiplicare per 24 il prodotto di 60 per il valore della cella.

giovedì 6 novembre 2014

Esercizio sui nomi dei campi in Excel con VBA

Codice per definire i nomi dei campi in Excel con VBA(esercizio):
Sub main()
Cells.Select
Selection.Interior.Color = vbWhite
On Error Resume Next
ActiveWorkbook.Names("mioCampo").Delete
num = 1 + Int(Rnd() * 10)
num2 = 1 + Int(Rnd() * 10)
ActiveWorkbook.Names.Add Name:="mioCampo", RefersTo:=Range(Cells(1, 1), Cells(num2, num))
Range("miocampo").Interior.Color = vbRed
Range("A1").Select
End Sub

mercoledì 15 ottobre 2014

Arrotondamenti in VBA Excel

Me ne sono andato in crisi profondissima di fronte al modo in cui VBA Excel opera gli arrotondamenti.

Poi, andando in crisi, non riesco più a ragionare lucidamente, e non riuscendo a ragionare lucidamente me ne vado ancora più in crisi, fin quando collasso completamente e mi ci sento male da un punto di vista emotivo.
Meglio sempre razionalizzare per iscritto, altrimenti mi imprigiono dentro queste "strutture emotive patologiche" che mi paralizzano del tutto.

In fondo, è per questo che ho deciso di tenere un diario, solo che poi non lo compilo perché ho paura di essere troppo prolisso su argomenti banali e di appesantirlo.


Ecco.
Quello che mi serve è approssimare per difetto le frazioni da 0,1 a 0,4 e per eccesso quelle da 0.5 a 0.9.
Aiutandomi con spunti che ho trovato in rete, ecco la conclusione cui sono arrivato.
Mi creo un codice apposta per sperimentare la cosa.

Sub main()
    Dim n As Double
    For n = 1 To 5 Step 0.1
        Debug.Print n & " -----> " & Int(n + 0.5)
    Next n
End Sub
1 -----> 1
1,1 -----> 1
1,2 -----> 1
1,3 -----> 1
1,4 -----> 1
1,5 -----> 2
1,6 -----> 2
1,7 -----> 2
1,8 -----> 2
1,9 -----> 2
2 -----> 2
2,1 -----> 2
2,2 -----> 2
2,3 -----> 2
2,4 -----> 2
2,5 -----> 3
2,6 -----> 3
2,7 -----> 3
2,8 -----> 3
2,9 -----> 3
3 -----> 3
3,1 -----> 3
3,2 -----> 3
3,3 -----> 3
3,4 -----> 3
3,5 -----> 4
3,6 -----> 4
3,7 -----> 4
3,8 -----> 4
3,9 -----> 4
4 -----> 4
4,1 -----> 4
4,2 -----> 4
4,3 -----> 4
4,4 -----> 4
4,5 -----> 5
4,6 -----> 5
4,7 -----> 5
4,8 -----> 5
4,9 -----> 5
5 -----> 5
Sembra che risponda egregiamente ai miei piani. Era così banale, ma nel circolo vizioso in cui mi ero messo non ci avevo pensato: sono stato spiazzato dal metodo apparentemente assurdo di approssimazione che Excel opera, convertendo 0.5 ora nell'intero superiore ora in quello inferiore a seconda che la parte intera sia pari o dispari (Banker, pare che si chiami, ossia metodo "del banchiere"...)

Adesso applichiamolo alla bisogna.

Io ho un numero di turni da attribuire a due reparti che hanno rispettivamente un numero variabile di medici.
Faccio prima senza approssimazione:
Sub main()
    Dim turni As Integer
    Dim medCar As Integer, medMed As Integer
    Dim turniCar As Double, turniMed As Double
    Dim turniPerMedico As Double
    
    turni = 15
    medCar = 4
    medMed = 5
    
    turniPerMedico = turni / (medCar + medMed)
    
    turniCar = turniPerMedico * medCar
    turniMed = turniPerMedico * medMed
    
    Debug.Print turniCar
    Debug.Print turniMed
End Sub
 6,66666666666667 
 8,33333333333333 


Vario le cifre:
    medCar = 5
    medMed = 5
 7,5 
 7,5 



    medCar = 6
    medMed = 5
 8,18181818181818 
 6,81818181818182 


Adesso introduciamo un arrotondamento come quello che ho ricavato.

Sub main()
    Dim turni As Integer
    Dim medCar As Integer, medMed As Integer
    Dim turniCar As Double, turniMed As Double
    Dim turniPerMedico As Double
    
    turni = 15
    medCar = 4
    medMed = 5
    
    turniPerMedico = turni / (medCar + medMed)
    
    turniCar = Int(turniPerMedico * medCar + 0.5)
    turniMed = Int(turniPerMedico * medMed + 0.5)
    
    Debug.Print turniCar
    Debug.Print turniMed
End Sub
 7 
 8 



    medCar = 5
    medMed = 5
 8 
 8 



    medCar = 6
    medMed = 5
 8 
 7 


Facciamo un confronto con i risultati che avevo ottenuto senza approssimazione:
  1. Primo caso: 4 e 5:
    Senza arrotondamento:
     6,66666666666667 
     8,33333333333333 
    
    Con arrotondamento:
     7 
     8 
    somma: 15
    
    
  2. Secondo caso: 5 e 5:
    Senza arrotondamento:
     7,5 
     7,5 
    
    Con arrotondamento:
     8 
     8 
    somma:16
    
  3. Terzo caso: 6 e 5:
    Senza arrotondamento:
     8,18181818181818 
     6,81818181818182
    
    Con arrotondamento:
     8 
     7 
    somma:15
    


Solo nel caso in cui i due gruppi siano uguali, dal momento che la cifra decimale è 0.5, viene arrotondata in alto in ambedue, "creando" un turno in più: in tutti gli altri casi, essendo le cifre decimali diverse e necessariamente una al di sopra di 0.5 e l'altra al di sotto, i turni vengono divisi bene con l'arrotondamento, venendo attribuiti al gruppo cui ne "spetterebbe" la parte maggiore.


Dunque dal momento che l'unica situazione in cui la somma dei turni per reparto non corrisponde al totale dei turni pare sia questa, bisogna trovare un modo per normalizzare la cosa.

martedì 7 ottobre 2014

Arrotondamento dei turni teorici.

Ecco il piccolo codice che mi sono costruito per studiare le approssimazioni per difetto e per eccesso:
Sub main()
Dim cifraDouble As Double
Dim cifraInteger As Integer
cifraDouble = 1.4
cifraInteger = cifraDouble

Debug.Print ""
Debug.Print "Il valore della variabile Double è " & cifraDouble
Debug.Print "Valore della variabile double copiato in una variabile Integer " & cifraInteger
Debug.Print "Uso di Int " & Int(cifraDouble)
Debug.Print "Uso di CInt " & CInt(cifraDouble)
End Sub
Cambiando il valore di cifraDouble ottengo nella finestra immediata il valore ottenuto ponendo il valore in una variabile di tipo Integer o usando rispettivamente Int e CInt.
Il valore della variabile Double è 1,4
Valore della variabile double copiato in una variabile Integer 1
Uso di Int 1
Uso di CInt 1

Il valore della variabile Double è 1,5
Valore della variabile double copiato in una variabile Integer 2
Uso di Int 1
Uso di CInt 2

Il valore della variabile Double è 1,9
Valore della variabile double copiato in una variabile Integer 2
Uso di Int 1
Uso di CInt 2

Usare la variabile di tipo Integer o la parola chiave CInt ottiene gli stessi risultati, ossia arrotonda una cifra decimale fino a 0,5 di decimali all'intero inferiore, mentre la arrotonda se ha 0,5 o più di decimali all'intero superiore.
Invece Int arrotonda sempre per difetto, all'intero inferiore.
Per il mio programma sarà più conveniente usare CInt o porre il valore in una variabile di tipo Integer.


Resta il problema se c'è un numero uguale di elementi nei due gruppi.
Torno a considerare la cosa:
turni attribuiti card 17
turni attribuiti med 18
turni teorici card 18
turni teorici med 18
Ecco! Dal momento che ambedue le cifre in questo caso hanno 0.5 di decimali il numero viene arrotondato per eccesso in ambedue i turni, e si ha la "creazione" di un turno in più che non dovrebbe esserci.

Come risolvere la cosa?

La soluzione più giusta sarebbe risolvere la cosa a caso, togliendo casualmente un turno a uno dei due.
Dim turniTot As Integer
Dim medCar, medMed As Integer
Dim turniCar As Integer, turniMed As Integer
turniTot = k - 1 + f
medCar = 6
medMed = 6

turniCar = turniTot / (medCar + medMed) * medCar
turniMed = turniTot / (medCar + medMed) * medMed

If turniCar + turniMed > turniTot Then
    Dim estratto As Integer
    estratto = Int(Rnd() * 2)
    If estratto = 0 Then turniCar = turniCar - 1
    If estratto = 1 Then turniMed = turniMed - 1
End If
        
Debug.Print "turni teorici card " & turniCar
Debug.Print "turni teorici med " & turniMed
Così andiamo pure bene... ma diamo un'occhiata in diverse "estrazioni", nella finestra immediata, ai turni attribuiti e a quelli teorici ottenuti sottraendo a caso al numero falsamente gonfiato un turno:
turni attribuiti card 18
turni attribuiti med 17
turni teorici card 18
turni teorici med 17
turni attribuiti card 17
turni attribuiti med 18
turni teorici card 17
turni teorici med 18
turni attribuiti card 17
turni attribuiti med 18
turni teorici card 18
turni teorici med 17
...e mi rendo conto che una distribuzione "equa" era già stata fatta, in quanto il primo turno del mese era stato estratto casualmente mentre gli altri venivano in rigorosa alternanza.
Dunque non è il caso di andare avanti in elucubrazioni cervellotiche.
Basta fare che se il numero dei due gruppi è uguale non è necessario operare una correzione, e le cosa sono già sistemate da sé.

Calcolo dei turni teorici per reparto.

Ora devo conoscere, per fare i calcoli opportuni sulla scrittura dei turni alternati come ho fatto finora:
  • il numero totale dei turni disponibili
  • il numero dei medici di ciascun reparto
turniTot è il numero di turni totali disponibili.
medCar è il numero di medici della cardiologia.
medMed è il numero di medici della medicina.
turniCar è il numero di turni della cardiologia.
turniMed è il numero di turni della medicina.
turniTot / (medCar + medMed) = numero dei turni per ciascun medico.
Moltiplicando il numero di turni per ciascun medico per il numero di medici di ciascun reparto ottengo il numero di turni per ciascun reparto.

turniCar = turniTot / (medCar + medMed) * medCar

turniMed = turniTot / (medCar + medMed) * medMed
Calcoliamoli...
Sub main()
    Dim d As Date, nomeFoglio As String
    
    d = DateAdd("m", 1, Date)      'attribuisce alla variabile locale d il valore del mese successivo alla data attuale
    nomeFoglio = MonthName(Month(d)) & " " & Year(d)    'attribuisce alla stringa nomeFoglio mese e anno della variabile locale d
    
    Sheets.Add          'aggiunge un nuovo foglio
    On Error GoTo x     'gestisce l'errore di tentata rinominazione del foglio
    ActiveSheet.Name = nomeFoglio   'rinomina il foglio con la stringa nomeFoglio.
    ActiveSheet.Move after:=Sheets(Sheets.Count) 'sposta alla fine della cartella il nuovo foglio

        
    formattaFoglio Month(d), Year(d)  'formatta il foglio secondo lo schema
    attribuzioneTurni
    Exit Sub
x:     'gestione dell'errore di tentata rinominazione del foglio: elimina il foglio senza suscitare avvisi di eliminazione.
    Application.DisplayAlerts = False
    ActiveSheet.Delete
    Application.DisplayAlerts = True
End Sub

Private Sub attribuzioneTurni()
Dim reparti(1) As String
Dim n As Integer
Dim f As Integer
Dim tCar, tMed As Integer

reparti(0) = "CAR"
reparti(1) = "MED"
 
n = Int(Rnd() * 2)
Range("Reparto").Cells(1, 1).Formula = reparti(n)
If reparti(n) = "CAR" Then
    tCar = tCar + 1
Else
    tMed = tMed + 1
End If

For k = 2 To Range("Reparto").Rows.Count
    If n = 0 Then
        n = 1
    Else
        n = 0
    End If
    Range("Reparto").Cells(k, 1).Formula = reparti(n)
    If reparti(n) = "CAR" Then
        tCar = tCar + 1
    Else
        tMed = tMed + 1
    End If
    
    If festivo(Range("Reparto").Cells(k, 1).Offset(0, -2).Formula) Then
        If Range("Reparto").Cells(k, 1).Formula = "MED" Then
            Range("Reparto").Cells(k, 1).Formula = "CAR - MED"
            tCar = tCar + 1
        Else
            Range("Reparto").Cells(k, 1).Formula = "MED - CAR"
            tMed = tMed + 1
        End If
        f = f + 1
    End If
Next k
Debug.Print "turni attribuiti card " & tCar
Debug.Print "turni attribuiti med " & tMed


Dim turniTot As Integer
Dim medCar, medMed As Integer
Dim turniCar, turniMed As Integer
turniTot = k - 1 + f
medCar = 7
medMed = 6

turniCar = turniTot / (medCar + medMed) * medCar
turniMed = turniTot / (medCar + medMed) * medMed
Debug.Print "turni teorici card " & turniCar
Debug.Print "turni teorici med " & turniMed
End Sub
Risultato
turni attribuiti card 18
turni attribuiti med 17
turni teorici card 18,8461538461538
turni teorici med 16

Ecco, questo è il calcolo completo (quello marcato in rosso), ma c'è qualcosa che non va: il fatto che i turni siano espressi in una cifra decimale, essendo indivisibili.
Per giunta, la somma non combina, essendo espresso uno solo in frazioni di turno.
Sicuramente non sono appropriate le variabili che ho usato per immagazzinarne il valore.
Rifeccio:
Dim turniTot As Integer
Dim medCar, medMed As Integer
Dim turniCar, turniMed As Double
turniTot = k - 1 + f
medCar = 7
medMed = 6

turniCar = turniTot / (medCar + medMed) * medCar
turniMed = turniTot / (medCar + medMed) * medMed
Debug.Print "turni teorici card " & turniCar
Debug.Print "turni teorici med " & turniMed
Proviamo:
turni attribuiti card 17
turni attribuiti med 18
turni teorici card 18,8461538461538
turni teorici med 16,1538461538462
Perfetto! Si tratta di numeri periodici, per cui la verifica non darà mai il risultato esatto, comunque la somma fra i due mi restituisce 34,99999999..... che si può considerare esatto! I turni totali sono infatti 35.
Adesso dobbiamo vedere come liberarci delle frazioni di turno...
Intanto devo correggere un errore di codice che ho commesso: il motivo per cui uno dei risultati era frazionario e l'altro no è stato che io ho dichiarato le variabili in questo modo:
Dim turniCar, turniMed As Integer
credendo, in quanto non ricordavo bene questo aspetto della sintassi del VBA, che anche turniCar sarebbe stata dichiarata come Integer, mentre invece in realtà questa veniva dichiarata come Variant, e quindi mi dava un risultato decimale.
Correggendo la cosa...
Dim turniCar As Integer, turniMed As Integer
ottengo:
turni attribuiti card 17
turni attribuiti med 18
turni teorici card 19
turni teorici med 16
La somma è sempre 35, ma il risultato viene arrotondato.

Sarebbe il caso di vedere come VBA arrotonda... Mi costruisco un programmino dedicato.

Scrittura dei turni in forma alternata con riserva di modifica successiva

Ma forse mi conviene alternare, come ho fatto prima, per poi sostituire alcuni turni per far quadrare il conto.

L'algoritmo era questo:
Dim reparti(1) As String
Dim n As Integer
Dim f As Integer

reparti(0) = "CAR"
reparti(1) = "MED"
 
n = Int(Rnd() * 2)
Range("Reparto").Cells(1, 1).Formula = reparti(n)

For k = 2 To Range("Reparto").Rows.Count
    If n = 0 Then
        n = 1
    Else
        n = 0
    End If
    Range("Reparto").Cells(k, 1).Formula = reparti(n)
    If festivo(Range("Reparto").Cells(k, 1).Offset(0, -2).Formula) Then
        If Range("Reparto").Cells(k, 1).Formula = "MED" Then
            Range("Reparto").Cells(k, 1).Formula = "CAR - MED"
        Else
            Range("Reparto").Cells(k, 1).Formula = "MED - CAR"
        End If
        f = f + 1
    End If
Next k
in cui predispongo un array di due elementi stringa, "CAR" e "MED", quindi estraggo casualmente uno dei due e lo metto nella prima cella:
reparti(0) = "CAR"
reparti(1) = "MED"
 
n = Int(Rnd() * 2)
Range("Reparto").Cells(1, 1).Formula = reparti(n)
Quindi alterno la disposizione successiva in questo modo:
For k = 2 To Range("Reparto").Rows.Count
    If n = 0 Then
        n = 1
    Else
        n = 0
    End If
    Range("Reparto").Cells(k, 1).Formula = reparti(n)
Next k
Ossia prendo alternativamente i due elementi dell'array, "CAR" e "MED".

Per i festivi uso questo:
If festivo(Range("Reparto").Cells(k, 1).Offset(0, -2).Formula) Then
        If Range("Reparto").Cells(k, 1).Formula = "MED" Then
            Range("Reparto").Cells(k, 1).Formula = "CAR - MED"
        Else
            Range("Reparto").Cells(k, 1).Formula = "MED - CAR"
        End If
        f = f + 1
    End If
mettendo il giorno a CAR se la notte è MED e viceversa.
In tutto questo, sapendo il numero teorico di turni per CAR e MED, posso inserire un codice che conti i vari CAR e MED in modo da operare poi le sostituzioni nel modo opportuno.
Proviamo...
Private Sub attribuzioneTurni()
    Dim reparti(1) As String
    Dim n As Integer
    Dim f As Integer
    Dim tCar, tMed As Integer

    reparti(0) = "CAR"
    reparti(1) = "MED"
 
     n = Int(Rnd() * 2)
     Range("Reparto").Cells(1, 1).Formula = reparti(n)
    If reparti(n) = "CAR" Then
        tCar = tCar + 1
    Else
        tMed = tMed + 1
    End If

    For k = 2 To Range("Reparto").Rows.Count
        If n = 0 Then
            n = 1
        Else
            n = 0
        End If
        Range("Reparto").Cells(k, 1).Formula = reparti(n)
        If reparti(n) = "CAR" Then
            tCar = tCar + 1
        Else
            tMed = tMed + 1
        End If
    
        If festivo(Range("Reparto").Cells(k, 1).Offset(0, -2).Formula) Then
            If Range("Reparto").Cells(k, 1).Formula = "MED" Then
                Range("Reparto").Cells(k, 1).Formula = "CAR - MED"
                tCar = tCar + 1
            Else
                Range("Reparto").Cells(k, 1).Formula = "MED - CAR"
                tMed = tMed + 1
            End If
            f = f + 1
        End If
    Next k
    Debug.Print "turni card " & tCar
    Debug.Print "turni med " & tMed

End Sub
Ecco: le aggiunte contrassegnate in rosso su giallo sono quelle che ho fatto per contare i turni CAR e i turni MED del mese.
Ho contrassegnato peraltro in bianco su blu una variabile che mi serve per contare il numero dei festivi e aggiungerlo al numero delle celle in modo da avere il totale dei turni disponibili.

Distribuzione casuale dei turni.

Questo è un modo di distribuire casualmente un numero num di contenuti di una cella in modo casuale all'interno di un range formato da numTot celle.
Sub main()
Dim num, numTot, contatore As Integer

    num = 13
    numTot = 30
    contatore = 0
    Do While contatore < num
        k = 1 + Int(Rnd() * numTot)
            If Range(Cells(1, 1), Cells(numTot, 1)).Cells(k, 1).Formula = "" Then
                Range(Cells(1, 1), Cells(numTot, 1)).Cells(k, 1).Formula = "CAR"
                contatore = contatore + 1
            End If
    Loop
End Sub
La distribuzione è assolutamente casuale.

lunedì 29 settembre 2014

Scrittura in rosso dei turni straordinari

Ora devo individuare come fare per catturare i multipli di tre, nei punteggi che figurano nella tabella.

Sembra di aver risolto...
Sub calcola()
    Dim x As Range
    Set x = Range("ListaNomiMese").Find(lastcaption)
    If x Is Nothing = False Then
        x.Offset(0, 2).Formula = x.Offset(0, 2).Formula - 1
        Straordinario x
    End If
    Set x = Range("ListaNomiMese").Find(Bersaglio.Formula)
    If x Is Nothing = False Then
        x.Offset(0, 2).Formula = x.Offset(0, 2).Formula + 1
        Straordinario x
    End If
End Sub

Private Sub Straordinario(x As Range)
    If x.Offset(0, 2).Formula Mod 4 = 0 Then
        colore = vbRed
    Else
        colore = vbBlack
    End If
    Bersaglio.Font.Color = colore
End Sub
Sì, funziona! Praticamente ogni tre turni ne scrive uno, sul Calendario, in rosso, a significare un turno straordinario.

La routine che calcola i turni.

Non ci perdiamo in nebulosità mentali, altrimenti non andiamo avanti!

Calcoliamo la quantità di turni fatti nel mese.
A quanto ho capito, non interessa sapere quanti turni di giorno o di notte siano stati fatti, e questo facilita le cose.
Possiamo risistemare il modulo formattaFoglio togliendo la tabella dei turni di giorno e nominando soltanto i turni fatti di notte (la quantità relativa di diurni e notturni sarà un problema che dovranno scornarsi a livello di reparto).

Bene.
Mi salvo il modulo, però...

Ecco la routine copiaLista rimaneggiata:
Private Sub copiaLista()
    Dim elemento As Range
    Dim k As Integer, z As Integer
    z = 15 '(posizione iniziale della lista dei nomi del mese)
    For k = 1 To Range("ListaNomi").Rows.Count
        ActiveSheet.Cells(z, 6).FormulaR1C1 = Range("ListaNomi").Cells(k, 1).FormulaR1C1
        ActiveSheet.Cells(z, 7).FormulaR1C1 = Range("ListaNomi").Cells(k, 2).FormulaR1C1
        z = z + 1
    Next k
    ActiveWorkbook.Names.Add Name:="ListaNomiMese", RefersTo:="=" & Range(Cells(15, 6), Cells(z - 1, 6)).Address(True, True)
    Griglia Range("ListaNomiMese")

    ActiveWorkbook.Names.Add Name:="ListaReparti", RefersTo:="=" & Range(Cells(15, 7), Cells(z - 1, 7)).Address(True, True)
    Griglia Range("ListaReparti")
    
    ActiveWorkbook.Names.Add Name:="NumeroTurni", RefersTo:="=" & Range(Cells(15, 8), Cells(z - 1, 8)).Address(True, True)
    Griglia Range("NumeroTurni")
    For Each elemento In Range("NumeroTurni")
        elemento.HorizontalAlignment = xlCenter
        elemento.Formula = 0
    Next
    
    
    With ActiveSheet
        With .Cells(Range("ListaNomiMese").Row - 1, 6)
            .FormulaR1C1 = "NOME"
            .HorizontalAlignment = xlCenter
        End With
        Griglia Cells(Range("ListaNomiMese").Row - 1, 6)
        With .Cells(Range("ListaNomiMese").Row - 1, 7)
            .FormulaR1C1 = "REP."
        End With
         Griglia Cells(Range("ListaNomiMese").Row - 1, 7)
        With .Cells(Range("ListaNomiMese").Row - 1, 8)
            .FormulaR1C1 = "GIORNO"
        End With
        Griglia Cells(Range("ListaNomiMese").Row - 1, 8)
        .Columns(6).ColumnWidth = 26
        Exit Sub
        With .Columns(7)
            .ColumnWidth = 6
            .HorizontalAlignment = xlCenter
            .Font.Bold = True
        End With
        
       
        
        With .Columns(8)
            .ColumnWidth = 6
            .HorizontalAlignment = xlCenter
            .Font.Bold = True
        End With
    End With
End Sub
Praticamente ho tolto la colonna dei turni di mattina, rinominando la colonna NumeriNotti in NumeroTurni e basta.

Inoltre ho aumentato un po' l'altezza delle righe.


Adesso cerchiamo di calcolare il numero di turni come avevo fatto precedentemente.

Il principio che avevo usato era:
Conservare in una variabile la stringa eliminata dalla cella;
Considerare la nuova stringa della cella come Bersaglio.Formula;
Cercare nella lista la stringa eliminata dalla cella e ridurre di 1 il numero dei turni;
Cercare nella lista la stringa nuova della cella e aumentare di 1 il numero dei turni.


In fondo è semplicissimo!

Proviamo...
Sub calcola()
    Dim x As Range
    Set x = Range("ListaNomiMese").Find(lastcaption)
    If x Is Nothing = False Then x.Offset(0, 2).Formula = x.Offset(0, 2).Formula - 1
    Set x = Range("ListaNomiMese").Find(Bersaglio.Formula)
    If x Is Nothing = False Then x.Offset(0, 2).Formula = x.Offset(0, 2).Formula + 1
End Sub
Eccola: dovrebbe essere perfetta.

Se vado su una cella vuota, lastcaption è vuota, quindi il range Find di lastcaption è Nothing.
In questo caso, però, non succede nulla perché si agisce sui punteggi solo se il range Find non è Nothing. Quindi si provvererà soltanto a sistemare il punteggio della nuova caption di Bersaglio.
Analogamente, se io vado su una cella con un nome e ci voglio trascrivere il menu vuoto, prima trovo lastcaption, e aggiusto il suo punteggio, quindi il range Find della stringa vuota è Nothing, quindi non succede niente al punteggio.

Dovrebbe funzionare alla perfezione. Molto semplice ed evita i casini di contare ogni volta i nomi, che è di un lento e di un dispendioso pazzesco!

sabato 27 settembre 2014

Ripristino dell'aggiunta intelligente di fogli calendario e creazione di menu personalizzati.

Adesso cosa mi rimane da fare?
La signora della direzione sanitaria invia lo scheletro dei turni, con l'attribuzione dei reparti, mentre l'attribuzione dei medici la fa il primario di ogni singolo reparto.
Una volta che ogni reparto ha stabilito i medici per ogni turno, la signora li trascrive e calcola i turni totali per l'attribuzione anche dei turni straordinari.
Predisponiamo i menu, dunque.

Ma prima devo ripristinare quel meccanismo del caricamento di nuovi fogli solo se sono del mese successivo, senza ripetizioni.
Vediamo la routine nel programma vecchio...

Sub main()
    Dim d As Date, nomeFoglio As String
    
    d = DateAdd("m", 1, Date)      'attribuisce alla variabile locale d il valore del mese successivo alla data attuale
    nomeFoglio = MonthName(Month(d)) & " " & Year(d)    'attribuisce alla stringa nomeFoglio mese e anno della variabile locale d
    
    Sheets.Add          'aggiunge un nuovo foglio
    On Error GoTo x     'gestisce l'errore di tentata rinominazione del foglio
    ActiveSheet.Name = nomeFoglio   'rinomina il foglio con la stringa nomeFoglio.
    ActiveSheet.Move after:=Sheets(Sheets.Count) 'sposta alla fine della cartella il nuovo foglio

        
    formattaFoglio Month(d), Year(d)  'formatta il foglio secondo lo schema
    attribuzioneTurni
    Exit Sub
x:     'gestione dell'errore di tentata rinominazione del foglio: elimina il foglio senza suscitare avvisi di eliminazione.
    Application.DisplayAlerts = False
    ActiveSheet.Delete
    Application.DisplayAlerts = True
End Sub
Mi sembra ben congegnata.
Ho fatto l'aggiunta della chiamata alla routine attribuzioneTurni(), e funziona egregiamente!
Bene! In questo modo, in un paio di secondi, la signora si troverà i turni predisposti, da mandare ai reparti perché essi li compilino con i nomi dei singoli medici.

Ora mi devo occupare dei menu.
Per prima cosa, devo definire di quali reparti i medici fanno parte.
E' bene che mi crei un elenco di medici con l'appartenenza ai relativi reparti.

Mario Sistoletti CAR
Giovanni Mitrali CAR
Arnaldo Coronari CAR
Luigi Di Astoli         CAR
Fernando Ventricoli CAR
Lucio Interni         MED
Marco Medici         MED
Luciano Luminari MED
Antonio Scienziatoni MED
Anselmo Nuvoloni MED
Ovviamente, nomi fittizi, stando qui in ambiente pubblico.

Ecco, ho trasferito dal vecchio abbozzo del programma tutto il blocco di codice che gestisce i menu e mi sono trovato anche qui perfettamente rappresentati i menu.
Ma ora devo intervenire per visualizzare solo i cardiologi o solo gli internisti a seconda del reparto che ha il turno.

Devo intervenire su questa routine del modulo gestoreMenu.
Private Sub caricaMenu(menu As CommandBar, rigaIniziale As Integer, col As Integer)
    Dim button As CommandBarButton
    For k = 1 To Range("ListaNomiMese").Rows.Count
        Set button = menu.Controls.Add(Type:=msoControlButton)
        button.Caption = Range("ListaNomiMese").Cells(k, 1).FormulaR1C1
        button.OnAction = "'scrivi """ & button.Caption & """, """ & button.Tag & """'"
    Next k
    Set button = menu.Controls.Add(Type:=msoControlButton)
    button.Caption = ""
    button.OnAction = "'scrivi """ & button.Caption & """, """ & button.Tag & """'"
End Sub
In particolare su quella riga di codice evidenziata.

Mi chiedo se sia possibile selezionare le celle in relazione a un criterio...

Ecco: sembra che sia riuscito a produrre una routine efficiente:
Private Sub caricaMenu(menu As CommandBar, rigaIniziale As Integer, col As Integer)
    Dim SiglaReparto As String
    Dim button As CommandBarButton
    If Intersect(Bersaglio, Range("TurnoNotte")) Is Nothing = False Then
            SiglaReparto = Right(Range("Reparto").Cells(Bersaglio.Row - Range("Reparto").Row + 1, 1).Formula, 3)
        Else
            SiglaReparto = Left(Range("Reparto").Cells(Bersaglio.Row - Range("Reparto").Row + 1, 1).Formula, 3)
        End If
    For k = 1 To Range("ListaNomiMese").Rows.Count
        If Range("ListaNomiMese").Cells(k, 1).Offset(0, 1).Formula = SiglaReparto Then
            Set button = menu.Controls.Add(Type:=msoControlButton)
            button.Caption = Range("ListaNomiMese").Cells(k, 1).FormulaR1C1
            button.OnAction = "'scrivi """ & button.Caption & """, """ & button.Tag & """'"
        End If
    Next k
    Set button = menu.Controls.Add(Type:=msoControlButton)
    button.Caption = ""
    button.OnAction = "'scrivi """ & button.Caption & """, """ & button.Tag & """'"
End Sub
Con questa, se il turno è attribuito alla cardiologia appare solo un menu di cardiologi, mentre se è attribuito alla medicina appare un menu solo di internisti.

Aggiunta dei turni festivi, con la mia funzione che individua se una data è festiva o no.

Ho ripreso la funzione festivo(valore as String) As Boolean che, data una stringa, ne prende il valore numerico e testa se esprime una data.
Function festivo(valore As String) As Boolean
    If Weekday(CDate(Val(valore) & " " & ActiveSheet.Cells(4, 1).Formula)) = 1 Then festivo = True
End Function
Con questo, individuo nel Calendario quali sono i giorni festivi (per il momento le domeniche, ma la funzione va ampliata a seconda dei giorni festivi diversi dalle domeniche), e vi inserisco anche il reparto che fa il turno festivo.
Mi sembra adeguato inserire il reparto diverso da quello che fa la notte.
La funzione completa ha questo codice:
Dim reparti(1) As String
Dim n As Integer

reparti(0) = "CAR"
reparti(1) = "MED"
 
n = Int(Rnd() * 2)
Range("Reparto").Cells(1, 1).Formula = reparti(n)

For k = 2 To Range("Reparto").Rows.Count
    If n = 0 Then
        n = 1
    Else
        n = 0
    End If
    Range("Reparto").Cells(k, 1).Formula = reparti(n)
    If festivo(Range("Reparto").Cells(k, 1).Offset(0, -2).Formula) Then
        If Range("Reparto").Cells(k, 1).Formula = "MED" Then
            Range("Reparto").Cells(k, 1).Formula = "CAR - MED"
        Else
            Range("Reparto").Cells(k, 1).Formula = "MED - CAR"
        End If
    End If
Next k
E questo è il risultato:

venerdì 26 settembre 2014

Algoritmi di rimescolamento: Knuth.

Vediamo questo "algoritmo di Knuth"...

Ecco: sono riuscito a costruire una routine che, credo, operi secondo questo algoritmo:
Sub funzione()
    Dim carte(3) As Integer
    carte(0) = 123
    carte(1) = 234
    carte(2) = 345
    carte(3) = 456
    Dim i As Integer, j As Integer
    Dim tampone As Integer
    i = 3
    Do While i <> 0
        j = Int(Rnd() * i)
        tampone = carte(j)
        carte(j) = carte(i)
        carte(i) = tampone
        i = i - 1
    Loop
        
    For k = 0 To 3
        Debug.Print carte(k)
    Next k
    Debug.Print "----------------------"
End Sub
Ed ecco una serie di "rimescolamenti"
 345 
 456 
 234 
 123 
----------------------
 234 
 456 
 123 
 345 
----------------------
 456 
 123 
 234 
 345 
----------------------
 456 
 123 
 234 
 345 
----------------------
 234 
 456 
 123 
 345 
----------------------
 234 
 456 
 123 
 345 
----------------------
 456 
 345 
 123 
 234 
----------------------
 456 
 345 
 123 
 234 
----------------------
 456 
 123 
 234 
 345 
----------------------
 456 
 123 
 234 
 345 
----------------------
 456 
 123 
 234 
 345 
----------------------
 456 
 345 
 123 
 234 
----------------------
 234 
 456 
 123 
 345 
----------------------
 234 
 345 
 456 
 123 
----------------------
 234 
 345 
 456 
 123 
----------------------
 456 
 345 
 123 
 234 
----------------------
 234 
 456 
 123 
 345 
----------------------
 234 
 345 
 456 
 123 
----------------------
Sembra che siano abbastanza "casuali"!

Numeri casuali e algoritmo per alternare i due membri di un array con inizio casuale.

Devo attribuire i turni non festivi con un meccanismo casuale, alla medicina o alla cardiologia.
Inizio con uno, quindi alterno i due reparti.

Quindi prima formatto il foglio, quindi sul range Reparto metto il nome del reparto con questo meccanismo casuale.
Ma vediamo come fare una sequenza casuale...

Forse mi conviene fare un array con i nomi dei due reparti.
 Sub main()
    Dim reparti(1) As String
    reparti(0) = "MED"
    reparti(1) = "CAR"
 End Sub
...semplicissimamente elementare e immediato...

Adesso rivediamo un po' come si fanno i numeri casuali...

Sì: ecco un codice che mi permette di vedere la quantità di 1 e di 0 che sono stati estratti a sorte con 50 estrazioni:
 Sub main()
 Dim n As Integer
 Dim nzero As Integer, nuno As Integer
 
 For k = 1 To 50
    n = Int(Rnd() * 2)
    If n = 0 Then nzero = nzero + 1
    If n = 1 Then nuno = nuno + 1
 Next k
 
 Debug.Print nzero
 Debug.Print nuno
 Debug.Print "-----"
 End Sub
Ed ecco i risultati, che mi pare ci stiano piuttosto bene con la "casualità" della cosa:
 26 
 24 
-----
 24 
 26 
-----
 29 
 21 
-----
 24 
 26 
-----
 25 
 25 
-----
 22 
 28 
-----
 27 
 23 
-----
 25 
 25 
-----
 30 
 20 
-----



Fatto questo, estraiamo a sorte dagli elementi del piccolo array:
 Sub main()
 Dim reparti(1) As String
 Dim n As Integer

 
 reparti(0) = "CAR"
 reparti(1) = "MED"
 
 
 For k = 1 To 50
    n = Int(Rnd() * 2)
    Debug.Print reparti(n)
 Next k

 End Sub
...che però mi dà lunghe sequenze sia dell'uno che dell'altro...
CAR
CAR
CAR
MED
MED
CAR
CAR
CAR
CAR
MED
CAR
CAR
MED
CAR
MED
CAR
CAR
MED
CAR
CAR
MED
CAR
MED
CAR
CAR
MED
MED
MED
CAR
CAR
MED
MED
MED
MED
CAR
MED
MED
CAR
MED
MED
MED
CAR
CAR
CAR
CAR
CAR
CAR
MED
CAR
CAR



Meglio ancora, invece, è rendere casuale solo la prima estrazione e successivamente andare per alternanza:
Sub main()
Dim reparti(1) As String
Dim n As Integer

 
reparti(0) = "CAR"
reparti(1) = "MED"
 
n = Int(Rnd() * 2)
Debug.Print reparti(n)

For k = 1 To 50
    If n = 0 Then
        n = 1
    Else
        n = 0
    End If
    Debug.Print reparti(n)
Next k
debug.print "-----"
End Sub
Ed ecco alcune estrazioni ripetute:
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
-----
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
-----
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
-----
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
MED
CAR
-----
Sì: inizia casualmente, quindi si alternano regolarmente.

Calcolo domeniche del mese.

Ho creato dunque, sulla base di un esempio trovato in rete, la funzione che trova il numero di giorni del mese.
Adesso ho elaborato io la funzione che trova il numero delle domeniche.
Function sundaysInMonth(anno As Integer, mese As Integer) As Integer
    Dim dt As Date
    Dim numero As Integer
    dt = DateSerial(anno, mese, 1)
    Do While Month(dt) = mese
        If WeekdayName(Weekday(dt), , 1) = "domenica" Then numero = numero + 1
        dt = dt + 1
    Loop
    sundaysInMonth = numero
End Function
L'ho sperimentato con vari mesi del calendario, e funziona.
Bene.

Adesso ho creato un modulo contenente le funzioni per i calcoli relativi alle date, e l'ho esportato in una cartella.
Ci aggiungo anche questa funzione.

Stabilite le domeniche del mese, facciamo i turni totali, semplicemente:
 Sub main()
    Dim turniTotali As Integer, anno As Integer, mese As Integer
    anno = 2014
    mese = 9
    turniTotali = daysInMonth(mese, anno) + sundaysInMonth(mese, anno)
    MsgBox turniTotali
 End Sub
che, ovviamente, dà il numero giusto.

Adesso mi resta da attribuire ai due reparti i turni...