9. Tabelas internas, matrizes e vetores

Tabela interna é o nome que frequentemente se usa em COBOL para fazer referência a arrays, matrizes e vetores, ou seja, um conjunto de variáveis homogêneas que se repetem em uma ou mais dimensões. Nesse capítulo veremos como construir, carregar e pesquisar informações nestas estruturas.

Imagine a seguinte situação: um programa precisa ler dezenas de milhões de registros e para cada registro lido precisa encontrar, digamos, a descrição correspondente a um código. Essas descrições estão em um arquivo externo (um banco de dados ou um arquivo convencional). O domínio de códigos e suas descrições é pequeno, mas para cada um dos milhões de registros do arquivo de entrada você teria que acessar o arquivo externo para encontrar a descrição.

Em situações desse tipo é muito mais eficiente carregar os códigos e descrições numa tabela interna e usá-la para encontrar as descrições correspondentes a cada um dos registros que foram lidos no arquivo principal.

Construindo tabelas internas

Tabelas internas são variáveis que se repetem, e como qualquer variável elas são definidas na DATA DIVISION. Se forem campos que se repetem em arquivos, serão definidas na FILE SECTION, se forem apenas variáveis de trabalho, serão definidas na WORKING-STORAGE, se forem argumentos recebidos por um subprograma, estarão na LINKAGE SECTION.

Quando queremos que uma variável se repita utilizamos a opção OCCURS na sua definição, como no exemplo abaixo. A palavra reservada TIMES é opcional:

03 VALOR-SALARIO PIC S9(013)V9(002) OCCURS 12 TIMES.

Na prática, temos 12 variáveis numéricas, com 13 dígitos inteiros e dois dígitos decimais, que podem ser referenciadas por um subscrito:

MOVE ZEROS TO VALOR-SALARIO(1)
ADD VALOR-SALARIO(5) TO SALARIO-ANUAL
MOVE VALOR-SALARIO(3) TO SALARIO-DE-MARCO

Diferentemente de algumas linguagens de programação, a primeira ocorrência de qualquer tabela interna é associada ao índice 1. A tentativa de acessar uma ocorrência menor que 1 ou maior que o limite de ocorrências provocará um erro em tempo de execução.

O subscrito também pode ser substituído por uma variável:

ADD VALOR-SALARIO(WT-SUBSCRITO) TO SALARIO-ANUAL

Naturalmente, WT-SUBSCRITO precisa ser uma variável numérica com valor entre 1 e o limite máximo de ocorrências da tabela interna VALOR-SALARIO.

A cláusula OCCURS não pode ser usada nem em variáveis de nível 01 nem em variáveis de nível 77. Além disso, não podemos declarar OCCURS e VALUE para uma mesma variável.

É possível criar tabelas internas com uma quantidade variável de ocorrências. Isso é útil quando precisamos economizar memória e sabemos que a quantidade de ocorrências pode variar significativamente. O exemplo abaixo mostra a declaração de uma tabela interna de tamanho variável:

03 VALOR-SALARIO PIC S9(013)V9(002) 
   OCCURS 1 TO 360 DEPENDING ON MESES-DE-CASA.

Essa tabela pode ter de 1 a 360 ocorrências, e seu tamanho real será definido em tempo de execução dependendo do valor da variável MESES-DE-CASA. Essa variável de controle deve atender às seguintes restrições:

  • Se a tabela interna variável foi declarada num registro da FILE SECTION, a variável de controle precisa ser declarada antes da tabela interna
  • O valor da variável de controle em tempo de execução não pode exceder o limite máximo definido pela cláusula OCCURS
  • A variável de controle precisa ser numérica, inteira e sem sinal (seu valor tem que ser sempre positivo)
  • A variável de controle não pode ser o elemento de uma outra tabela interna.

A cláusula OCCURS pode ser usada em itens de grupo. Nesse caso, todos os seus itens elementares também se repetem. Observe o exemplo abaixo:

01 ENDERECO OCCURS 3.
    03 LOGRADOURO      PIC X(030).
    03 BAIRRO          PIC X(020).
    03 CIDADE          PIC X(020).
    03 ESTADO          PIC X(002).
    03 CEP             PIC X(008).

Nessa tabela, o item de grupo ENDERECO se repete três vezes, assim como todos os seus itens elementares:

Cobol: Repetição de item de grupo
Figura 27. Exemplo de repetição de item de grupo

O uso de OCCURS em itens de grupo e itens elementares, ao mesmo tempo, nos permite criar tabelas multidimensionais:

01 TABELA-ANUAL.
    03 TABELA-MENSAL OCCURS 12.
        05 QUANTIDADE-DIARIA  PIC 9(009) OCCURS 31.

No exemplo acima, temos o item de grupo TABELA-MENSAL, que se repete 12 vezes. Dentro de cada ocorrência da tabela mensal temos o item elementar QUANTIDADE-DIARIA que se repete 31 vezes. Assim, temos uma tabela com duas dimensões (uma matriz) que pode ser acessada de diversas maneiras:

MOVE ZEROS TO QUANTIDADE-DIARIA(1, 1)
ADD 1 TO QUANTIDADE-DIARIA(8, 25)
SUBTRACT 1 FROM QUANTIDADE-DIARIA(INDICE-MES, INDICE-DIA)

Definindo valores iniciais para tabelas internas

Como já mencionamos, não é possível usar OCCURS e VALUE na mesma variável, mas algumas vezes precisamos preencher a tabela interna com algum conteúdo fixo que será usado pelo programa. Nesses casos usamos a cláusula REDEFINES.

Observe o exemplo abaixo:

01 MESES.
    03 FILLER   PIC X(010) VALUE “JANEIRO”.
    03 FILLER    PIC X(010) VALUE “FEVEREIRO”.
    03 FILLER    PIC X(010) VALUE “MARCO”.
    03 FILLER    PIC X(010) VALUE “ABRIL”.
    03 FILLER    PIC X(010) VALUE “MAIO”.
    03 FILLER    PIC X(010) VALUE “JUNHO”.
    03 FILLER    PIC X(010) VALUE “JULHO”.
    03 FILLER    PIC X(010) VALUE “AGOSTO”.
    03 FILLER    PIC X(010) VALUE “SETEMBRO”.
    03 FILLER    PIC X(010) VALUE “OUTUBRO”.
    03 FILLER    PIC X(010) VALUE “NOVEMBRO”.
    03 FILLER    PIC X(010) VALUE “DEZEMBRO”.

01 FILLER REDEFINES MESES.
    03 MES      PIC X(010) OCCURS 12.

A novidade aqui é o uso da palavra reservada FILLER. Quando usamos FILLER no lugar do nome de uma variável isso significa que esse campo é não será mencionado em nenhum comando da PROCEDURE DIVISION, mas é necessário para a composição do item de grupo.

A cláusula REDEFINES nos permite definir layouts alternativos para uma mesma variável, seja ela um item de grupo ou um item elementar. O exemplo acima mostra um item de grupo chamado MESES que contém 12 literais, um para cada nome de mês. O item de grupo depois dele define um novo layout para o campo MESES. Nesse layout alternativo existem 12 ocorrências de um item elementar chamado MES. Como, na prática, eles representam apenas layouts diferentes de um mesmo espaço em memória, se executarmos o comando DISPLAY MES(2) veremos “FEVEREIRO” como resultado.

O nível a ser redefinido não pode ser subordinado a um nível que contenha a cláusula OCCURS, nem ele próprio conter essa cláusula. Em outras palavras, um item de grupo com opção REDEFINES pode conter itens elementares com OCCURS (como vimos no exemplo anterior), mas aquele que está sendo redefinido (MESES, no nosso exemplo) não.

Carregando tabelas internas

Quando usamos a cláusula REDEFINES em nosso último exemplo, na prática, estávamos carregando uma tabela interna com valores definidos pelos VALUEs do item de grupo que foi redefinido.

Em muitas situações, porém, o conteúdo da tabela interna só será obtido em tempo de execução, proveniente de um arquivo externo, por exemplo. Vamos imaginar um programa que tenha que ler um arquivo com código e descrição de formas de pagamento, e que esse arquivo nunca terá mais de 100 registros. Para carregá-lo numa tabela interna (e assim agilizar futuras pesquisas) poderíamos adotar a seguinte solução:

Definimos o arquivo na FILE SECTION…

FILE SECTION.
FD  GAA0105.
01  GAA0105-REGISTRO.
    03 GAA0105-CD-PAGAMENTO  PIC X(003).
    03 GAA0105-DS-PAGAMENTO  PIC X(030).

Criamos na WORKING-STORAGE uma tabela interna chamada WV-FORMAS-PAGAMENTO com 100 ocorrências. Essa tabela possui dois itens elementares, WV-CD-PAGAMENTO (para guardar o código) e WV-DS-PAGAMENTO (para guardar a descrição).

WORKING-STORAGE SECTION.
01  WV-FORMAS-DE-PAGAMENTO OCCURS 100.
    03 WV-CD-PAGAMENTO       PIC X(003).
    03 WV-DS-PAGAMENTO       PIC X(030).

Criamos também uma variável de trabalho que usaremos como subscrito ou índice para acessar as ocorrências da tabela interna:

01  WT-AUXILIARES.
    03 WT-IX                  PIC 9(003) VALUE ZEROS.

Vamos ler o arquivo de entrada e executar um loop para carregar os registros na tabela interna. Esse loop será executado até o fim de arquivo ou até que sejam carregados mais de 100 registros (o que acreditamos que não vai acontecer[1]):

PROCEDURE DIVISION.
...
    READ GAA0105

    PERFORM 13-CARREGA-FORMAS-PAGAMENTO
      UNTIL WT-IX > 100 OR 
            WT-ST-GAA0105 NOT = “00”

No parágrafo de carga, somamos 1 ao subscrito e movimentamos os campos do registro lido para os campos correspondentes na tabela interna.

13-CARREGA-FORMAS-PAGAMENTO.

    ADD  1 TO WT-IX
    MOVE GAA0105-CD-PAGAMENTO TO WV-CD-PAGAMENTO(WT-IX)
    MOVE GAA0105-DS-PAGAMENTO TO WV-DS-PAGAMENTO(WT-IX)
    READ GAA0105.

Uma forma mais elegante de fazer essa mesma carga seria usar a opção VARYING do comando PERFORM. Desta forma ele se comportará como um loop de variação igual ao comando FOR que existe em várias linguagens.

A sintaxe mais simples do comando PERFORM com opção VARYING é a seguinte:

PERFORM nome-do-paragrafo
   VARYING subscrito FROM valor-inicial BY incremento
   UNTIL subscrito > limite

A variável “subscrito” começa com um valor inicial (FROM), e vai aumentando de incremento em incremento (BY), até que atinja um limite definido na cláusula UNTIL. No exemplo abaixo, WT-SUB começa com valor zero e é incrementado de 2 em 2 a cada iteração do loop NUMEROS-PARES:

PERFORM NUMEROS-PARES
   VARYING WT-SUB FROM 0 BY 2
     UNTIL WT-SUB > 100

A opção VARYING também pode ser usada no modo in-line:

PERFORM VARYING WT-SUB FROM 0 BY 2 UNTIL WT-SUB > 100
   Comando-1
   Comando-2
   Comando-3
END-PERFORM

Voltando ao exemplo sobre a carga de formas de pagamento, poderíamos reescrever nossa PROCEDURE DIVISION da seguinte forma:

FILE SECTION.
FD  GAA0105.
01  GAA0105-REGISTRO.
    03 GAA0105-CD-PAGAMENTO  PIC X(003).
    03 GAA0105-DS-PAGAMENTO  PIC X(030).

...

WORKING-STORAGE SECTION.
01  WV-FORMAS-DE-PAGAMENTO OCCURS 100.
    03 WV-CD-PAGAMENTO       PIC X(003).
    03 WV-DS-PAGAMENTO       PIC X(030).

01  WT-AUXILIARES.
    03 WT-IX                  PIC 9(003).

...

PROCEDURE DIVISION.

...


    READ GAA0105
    PERFORM 13-CARREGA-FORMAS-PAGAMENTO
            VARYING WT-IX FROM 1 BY 1
              UNTIL WT-IX > 100 OR 
                    WT-ST-GAA0105 NOT = “00”
...

13-CARREGA-FORMAS-PAGAMENTO.

    MOVE GAA0105-CD-PAGAMENTO TO WV-CD-PAGAMENTO(WT-IX)
    MOVE GAA0105-DS-PAGAMENTO TO WV-DS-PAGAMENTO(WT-IX)
    READ GAA0105.

Nesse caso, não precisamos incrementar o subscrito com um comando ADD a cada iteração. Como é um parágrafo bem pequeno, também poderíamos ter codificado o PERFORM no modo in-line:

PROCEDURE DIVISION.

...

    READ GAA0105
    PERFORM VARYING WT-IX FROM 1 BY 1
              UNTIL WT-IX > 100 OR
                    WT-ST-GAA0105 NOT = “00”
        MOVE GAA0105-CD-PAGAMENTO TO WV-CD-PAGAMENTO(WT-IX)
        MOVE GAA0105-DS-PAGAMENTO TO WV-DS-PAGAMENTO(WT-IX)
        READ GAA0105
    END-PERFORM.

...

Subscritos e indexadores

A indexação é um outro procedimento que permite acessar ocorrências de uma tabela interna de forma bem semelhante à que vimos até agora com subscritos. A principal diferença é que muitos compiladores otimizam as operações com índices uma vez que essas variáveis são usadas unicamente para acessar tabelas internas.

O índice não é definido como uma variável independente, como a WT-IX do nosso exemplo anterior. Ela é criada na mesma sentença que declara a tabela interna, como podemos ver nas linhas abaixo:

01  WV-FORMAS-DE-PAGAMENTO OCCURS 100 INDEXED BY WT-IX.
    03 WV-CD-PAGAMENTO       PIC X(003).
    03 WV-DS-PAGAMENTO       PIC X(030).

Por se tratar de um índice, e não de uma variável numérica comum, existem comandos especiais para atribuição de valores, incrementos e decrementos:

Cobol: Comandos diferentes para índices e subscritos
Figura 28. Comandos diferentes para índices e subscritos

Índices podem ser usados normalmente na opção VARYING do comando PERFORM. Além disso, o uso de índices nos permite fazer indexação relativa, usando expressões aritméticas:

01  TABELA-SALARIAL.
    03 VALOR-SALARIO PIC S9(013)V9(002) OCCURS 240 INDEXED BY IX-ATUAL.

...

SUBTRACT VALOR-SALARIO(IX-ATUAL - 1)
 FROM VALOR-SALARIO(IX-ATUAL)
     GIVING VALOR-AUMENTO

O uso de índices ao invés de subscritos também agiliza a pesquisa de valores na tabela interna, como veremos na próxima seção.

Pesquisando em tabelas internas

A forma mais rudimentar de pesquisas em tabelas internas acontece através de loops controlados e subscritos. Vamos supor que, depois de carregar nossa tabela interna de formas de pagamento no exemplo anterior, agora precisemos procurar um código e obter a descrição correspondente. Poderíamos fazer essa procura sequencialmente, usando PERFORM VARYING e um subscrito:

MOVE SPACES TO DESCRICAO-ENCONTRADA
PERFORM VARYING WT-IX FROM 1 BY 1
          UNTIL WT-IX > 100 OR
                DESCRICAO-ENCONTRADA NOT = SPACES
    IF WV-CD-PAGAMENTO(WT-IX) = CODIGO-PROCURADO
       MOVE WV-DS-PAGAMENTO(WT-IX) TO DESCRICAO-PROCURADA
    END-IF
END-PERFORM

O loop vai variar o subscrito WT-IX de 1 em 1 até que seja maior que 100 (limite de ocorrências da tabela interna) ou que o campo DESCRICAO-ENCONTRADA seja diferente de espaços.

Dentro do loop, cada ocorrência do campo WV-CD-PAGAMENTO é comparada com o valor da variável CODIGO-PROCURADO. Quando esse valor é igual, a ocorrência correspondente do campo WV-DS-PAGAMENTO é movida para a variável DESCRICAO-PROCURADA, que assim fica diferente de espaços, encerrando o loop.

Mas o COBOL possui um comando específico para fazer pesquisas em tabelas internas. Seu formato mais simples é o seguinte:

SEARCH nome-da-tabela VARYING nome-do-indice
  WHEN condição
       comando-1
       comando-2
       comando-N
END-SEARCH

Usando o comando SEARCH nossa pesquisa anterior ficaria assim:

MOVE SPACES TO DESCRICAO-ENCONTRADA
SET WT-IX TO 1
SEARCH WV-FORMAS-DE-PAGAMENTO VARYING WT-IX
  WHEN WV-CD-PAGAMENTO(WT-IX) = CODIGO-PROCURADO
       MOVE WV-DS-PAGAMENTO(WT-IX) TO DESCRICAO-ENCONTRADA
END-SEARCH

…muito mais simples e direta do que a construção com PERFORM. A única restrição é que a variável mencionada na cláusula VARYING do SEARCH precisar ser um índice (criada com INDEXED BY), e não uma variável numérica comum.

Repare que antes do SEARCH nós atribuímos 1 ao índice (SET WT-IX TO 1). Isso é necessário para garantir que a pesquisa começará efetivamente na primeira ocorrência.

O SEARCH também permite que se inclua uma cláusula AT END, cujos comandos serão executados quando o índice ultrapassar o limite de ocorrências da tabela. O exemplo abaixo mostra o uso dessa cláusula:

SEARCH WV-FORMAS-DE-PAGAMENTO VARYING WT-IX
    AT END 
       DISPLAY “CODIGO NAO ENCONTRADO”
  WHEN WV-CD-PAGAMENTO(WT-IX) = CODIGO-PROCURADO
       DISPLAY “ENCONTRADO. A DESCRICAO E’ “ WV-DS-PAGAMENTO(WT-IX)
END-SEARCH

Também é possível inserir várias condições WHEN no mesmo SEARCH. No entanto, a pesquisa será encerrada quando a primeira condição for satisfeita:

SEARCH WV-FORMAS-DE-PAGAMENTO VARYING WT-IX
    AT END
       DISPLAY “CODIGO NAO ENCONTRADO”
  WHEN WV-CD-PAGAMENTO(WT-IX) = CODIGO-PROCURADO
       DISPLAY “CODIGO ENCONTRADO NA OCORRENCIA “ WT-IX
  WHEN WV-DS-PAGAMENTO(WT-IX) = DESCRICAO-PROCURADA
       DISPLAY “DESCRICAO ENCONTRADA NA OCORRENCIA “ WT-IX
END-SEARCH

No exemplo acima o comando SEARCH procura por uma ocorrência onde o código da forma de pagamento seja igual ao valor que está na variável CODIGO-PROCURADO. Mas também procura uma ocorrência onde a descrição da forma de pagamento seja igual ao valor que está em outra variável, DESCRICAO-PROCURADA. Quando uma das condições for satisfeita a pesquisa se encerra. Se nem o código nem a descrição forem encontrados, a pesquisa sairá pela cláusula AT END.

Um segundo formato do comando SEARCH é indicado quando se tem algum controle sobre a ordem em que as ocorrências serão carregadas na tabela interna.

Vamos supor que possamos garantir que o arquivo de formas de pagamento do nosso exemplo (GAA0105) esteja sempre em ordem ascendente de código. Ao criar a tabela, poderíamos deixar isso claro na cláusula OCCURS:

01 WV-FORMAS-DE-PAGAMENTO OCCURS 100
                           ASCENDING KEY IS WV-CD-PAGAMENTO
                           INDEXED BY WT-IX.
    03 WV-CD-PAGAMENTO       PIC X(003).
    03 WV-DS-PAGAMENTO       PIC X(030).

Com isso podemos usar o comando SEARCH para fazer uma pesquisa binária. Em termos de sintaxe, a única diferença é a inclusão da opção ALL após o comando:

SEARCH ALL WV-FORMAS-DE-PAGAMENTO VARYING WT-IX
  WHEN WV-CD-PAGAMENTO(WT-IX) = CODIGO-PROCURADO
       MOVE WV-DS-PAGAMENTO(WT-IX) TO DESCRICAO-ENCONTRADA
END-SEARCH

A partir daí, ao invés de fazer uma pesquisa serial, ocorrência a ocorrência, o SEARCH começará a procura pelo meio da tabela, seguindo para frente e/ou para trás, sempre dividindo o espaço restante (que falta pesquisar) ao meio.

A eficiência de uma pesquisa serial é uma função de N, onde N é o número de ocorrências da tabela. Isso significa que no pior cenário (nenhum elemento encontrado) a pesquisa serial percorrerá as N ocorrências da tabela. Já numa pesquisa binária a eficiência é uma função de log2 N (logaritmo de N na base 2). Logo, se uma tabela tem 20 ocorrências a pesquisa binária será cerca de 4,6 vezes mais eficiente; se a tabela tiver 200 ocorrências a pesquisa binária será 26 vezes mais eficiente; se forem 1000 ocorrências ela será 101 vezes melhor, e assim por diante…

O SEARCH ALL, porém, tem algumas particularidades:

  • O campo testado na condição WHEN precisa ser o mesmo que foi mencionado na cláusula ASCENDING (ou DESCENDING) da declaração da tabela;
  • Apenas condições de igualdade podem ser testadas na cláusula WHEN; não é possível fazer comparações >, >=, <, <= etc.
  • Não é necessário atribuir um valor inicial ao índice antes do SEARCH. Ele será iniciado automaticamente pelo SEARCH.
  • Da mesma forma não é possível usar o valor do índice depois que o comando SEARCH ALL é executado.

[1] Num programa real deveríamos prever algum tratamento de exceção para o caso de haver mais de 100 registros no arquivo GAA0105. Mas para os objetivos desse exemplo podemos ignorar esse risco.


Anterior Conteúdo Próxima