venerdì 6 giugno 2008

Automatizzare a tutti i costi 2

Mi è capitato di recente di dover completare una matrice di copertura dei requisiti. In breve: il documento dei requisiti aveva un certo numero di tabelle con i requisiti di dettaglio contrassegnati da un codice a 21 caratteri e da un riferimento al requisito utente da cui aveva avuto origine:
N.CodiceDescrizione
1. xxx-xxx-xxx-xxx-00100 Rif: yyy-yyy-yyy-yyy-01234
Descrizione...
2. xxx-xxx-xxx-xxx-00110 Rif: yyy-yyy-yyy-yyy-01234
Descrizione...
3. xxx-xxx-xxx-xxx-00120 Rif: yyy-yyy-yyy-yyy-01235
Descrizione...
...

L'ultima tabella rappresentava invece i requisiti utente originari da cui i requisiti software di dettaglio erano stati estrapolati:
CodiceDescrizioneDettaglio
yyy-yyy-yyy-yyy-01234blah blah blah 
yyy-yyy-yyy-yyy-01235bluh bluh bluh 

In pratica manualmente avrei dovuto cercare tutti i riferimenti e riportare nella colonna "Dettaglio" l'elenco dei requisiti con quel riferimento:
CodiceDescrizioneDettaglio
yyy-yyy-yyy-yyy-01234blah blah blah xxx-xxx-xxx-xxx-00100
xxx-xxx-xxx-xxx-00110
yyy-yyy-yyy-yyy-01235bluh bluh bluh xxx-xxx-xxx-xxx-00120

Seguendo la regola automatizzare a tutti i costi mi sono rivolto a una macro in VBA non semplice, sicuramente da ottimizzare, ma che ha raggiunto lo scopo:
Option Explicit

Const rifTag = "Rif: " ' Tag che identifica il requisito di riferimento
Const firstTable As Integer = 18 ' Indice della prima tabella da esaminare
Const reqDettLength = 21 ' Lunghezza del codice del requisito di dettaglio

' Compila la tabella con la mappa dei requisiti
Sub requirementsMap()
    
    Dim element As String ' Elemento estratto dalla tabella
        
    Dim lastTable As Integer ' Ultima tabella da esaminare
    lastTable = ActiveDocument.Tables.Count - 1
    
    Const firstRowIndex As Integer = 1 ' Prima riga da esaminare
    Dim lastRowIndex As Integer ' Ultima riga da esaminare
    
    Dim reqId As Integer ' Contatore dei requisiti
    reqId = 1
    
    Dim tableIndex As Integer ' Cursore delle tabelle
    Dim currentTable
    
    ' Scorre le tabelle
    For tableIndex = firstTable To lastTable
        currentTable = ActiveDocument.Tables(tableIndex)
        lastRowIndex = currentTable.Rows.Count
        
        Dim currentRowIndex As Integer ' Cursore delle righe
        Dim currentRow
        
        ' Scorre le righe della tabella corrente
        For currentRowIndex = firstRowIndex To lastRowIndex
            
            currentRow = currentTable.Rows(currentRowIndex)
            
            Dim reqDett As String ' Requisito di dettaglio (prima colonna della tabella in esame)
            reqDett = extractReqDett(currentRow)
            
            Dim reqGen As String ' Requisito generale (indicato dalla parola "Rif. ")
            reqGen = extractReqGen(currentRow)
            
            Dim reqIndex ' Indice del requisito generale
            Dim reqMap ' Tabella contenente la mappa dei requisiti
            Dim foundRow ' Riga del requisito generale (-1 se non trovato)
            Dim cellContent ' Contenuto della cella DETTAGLIO
          
            If (Len(reqDett) = reqDettLength) Then
                reqIndex = findInTable(reqGen)
                ' Se il requisito generale è stato trovato
                If (reqIndex > -1) Then
                    reqMap = ActiveDocument.Tables(ActiveDocument.Tables.Count)
                    foundRow = reqMap.Rows(reqIndex)
                    cellContent = foundRow.Cells(3)
                    foundRow.Cells(3) = cellContent + reqDett
                End If
                
                reqId = reqId + 1
            End If
        Next
    Next
End Sub

' Prendo solo il codice di 21 caratteri del
' requisito di dettaglio escludendo la descrizione
Function extractReqDett(currentRow)
    Dim element As String
    element = extractCell(currentRow, 2)
    If Len(element) > 21 Then
        extractReqDett = Left(element, 21)
    Else
        extractReqDett = element
    End If
End Function

' Prendo solo il codice del requisito
' generale tagliando via "Rif. "
Function extractReqGen(currentRow)
    Dim reqGen As String
    reqGen = extractCell(currentRow, 3)
    If (Len(reqGen) > Len(rifTag)) Then
        Dim beginsWith As String
        beginsWith = Left(reqGen, Len(rifTag))
        If (beginsWith = rifTag) Then
            extractReqGen = Right(reqGen, Len(reqGen) - Len(rifTag)) ' Prendo solo il codice escludendo "Rif. "
        Else
            extractReqGen = "???"
        End If
    Else
        extractReqGen = "???"
    End If
End Function

' Estrae una cella di una tabella tagliando al primo
' ritorno a capo ed eliminando gli spazi a contorno
Function extractCell(row, cellIndex) As String
    extractCell = Trim(truncateAtCRLF(row.Cells(cellIndex)))
End Function

' Tronca una stringa al primo CR/LF
Function truncateAtCRLF(text As String) As String
    truncateAtCRLF = truncate(text, Chr(13))
End Function

' Tronca una stringa alla prima occorrenza del delimitatore
Function truncate(text As String, delimiter As String) As String
    Dim delimiterIndex
    delimiterIndex = InStr(1, text, delimiter, vbTextCompare)
    Dim temp As String
    If (delimiterIndex > 1) Then
        temp = Left(text, delimiterIndex - 1)
    Else
        temp = text
    End If
    truncate = temp
End Function

' Ritorna l'indice della riga della tabella in cui si trova S
Function findInTable(S As String) As Integer
    Dim reqMap
    reqMap = ActiveDocument.Tables(ActiveDocument.Tables.Count)
    Dim j As Integer
    j = 2 ' Skip riga d'intestazione
    Dim rowFound As Integer
    rowFound = -1
    Dim currentRow
    Dim reqId
    While (j <= reqMap.Rows.Count And rowFound = -1)
        currentRow = reqMap.Rows(j)
        reqId = truncate(currentRow.Cells(1), " ")
        If (reqId = S) Then rowFound = j
        j = j + 1
    Wend
    findInTable = rowFound
End Function
Sempre bene accette le idee per migliorare il codice, ovviamente (per esempio: la ricerca lineare sulla tabella di destinazione non mi piace molto, in Java avrei usato una HashTable per indicizzare i contenuti).

Nessun commento: