CodeBlocks em AdvPL – Parte 02

Introdução

No post anterior, CodeBlocks em Advpl – Parte 01, vimos como criar, parametrizar, chamar e obter retorno de um Codeblock. Agora, vamos ver como criar Codeblocks dinamicamente, e explicar o que é e como funciona as referências de variáveis que o Codeblock faz.

Codeblock dinâmico

Em determinadas situações, pode ser necessário criar um Codeblock com conteúdo dinâmico. Neste caso, usamos o operador de macro-execução (&) do AdvPL, por exemplo:

cFnCall := "u_minhafunc"
cBloco := " {|x| "+cFnCall+"(x) }"
bBloco := &(cBloco)

/* ----------------------------------------------------
cFnCall é uma variável caractere que tem o nome de uma 
        função a ser chamada. 
cBloco é uma variável caractere que você monta com o 
       corpo do Codeblock, montando a chamada da função
bBloco será o Bloco de Código, criado dinamicamente pelo
       operador de macro-execução.
---------------------------------------------------- */

Observação

Uma vez que eu crie um bloco de código dinamicamente, usando macro-execução, caso dentro do texto do Codeblock eu faça uma referência a uma variável local, ela não será visível dentro do Codeblock, pois o operador de macro-execução não acessa o escopo local.

Referência a variáveis do escopo local

O Codeblock não apenas é uma forma de criar uma função anônima encapsulada dentro de uma variável, que pode ser passada para outras funções como parâmetro ou pode ser retornado para funções da pilha de chamadas anterior. Quando ele é criado dentro de um determinado fonte, o corpo do Clodeblock pode fazer referência e usar variáveis do escopo local da função onde ele foi declarado — desde que as variáveis usadas no corpo do Codeblock já tenham sido declaradas. Vamos a um exemplo que fica mais fácil de compreender.

User Function Bloco()
Local bBloco
bBloco := GetBloco()
eval(bBloco,10)
MsgInfo("Total",eval(bBloco))
Return

Static Function GetBloco()
Local nTotal := 0 
Local bBloco := { |x| IIF( PCount() == 0 , nTotal , nTotal := x ) }
Return bBloco

Reparem que a variável nTotal é local, declarada dentro da função GetBloco(), e dentro da função, ela é usada dentro do Codeblock. O bloco de código montado é um Pattern do tipo GET-SET: Se for passado um parâmetro, a variável nTotal recebe um conteúdo, se nenhum parâmetro for passado, o bloco retorna a variável nTotal.

Porém, a variável nTotal é local dentro do escopo da função GetBloco(). Porém, como ela foi usada dentro do corpo do Codeblock, esta variável não vai ser descartada. Mesmo eu retornando o Codeblock para o nível anterior, o Codeblock continua funcionando. Na primeira chamada com Eval(), passando o valor 10, este valor é armazenado na variável nTotal. Na segunda chamada do Codeblock, sem passar parâmetros, o valor é retornado.

Justamente por fazer referências as variáveis de onde o Codeblock é criado, não é recomendável criar um Codeblock dentro de uma função e retorná-lo ao nível anterior da pilha de chamadas, pois ao retornar o Codeblock, o Runtime do AdvPL não pode limpar as variáveis e seus respectivos conteúdos daquela execução, pois o Codeblock retornado ao nível anterior arrasta com ele este contexto. Somente haverá a limpeza deste contexto quando o bloco de código não for mais referenciado — isto é, todas as variáveis que referenciam este Codeblock devem ser anuladas.

Da mesma forma, um Codeblock montado no nível atual, e passado para frente como parâmetro para outra função, ao referenciar uma variável local de um escopo, a chamada do bloco vai agir sobre esta variável. Vamos ao segundo exemplo:

User Function Bloco()
Local nTotal := 0 
Local bBloco := { |x| IIF( PCount() == 0 , nTotal , nTotal := x ) }
SetTotal(bBloco,10)
MSgInfo(nTotal,"Total")
Return

Static Function SetTotal(bBloco,nValor)
eval(bBloco,nValor)
Return

O bloco continua referenciando a variável nTotal, criada na função U_BLOCO(). Porém, o bloco foi passado como parâmetro para a função SetTotal(), e de dentro dela chamamos o Eval(), repassando o valor 10. O bloco vai fazer a atribuição na variável nTotal, do escopo anterior, pois ela foi referenciada no bloco.

A classe tGet ou o comando @ … GET do AdvPL recebe e/ou monta um Codeblock do tipo GET-SET amarrado na variável local. Isso permite que, ao declarar a variável a ser usada, o componente de interface faz referência a esta variável, de modo que esta variável pode ser passada como parâmetro por exemplo para um determinado botão. Uma vez que você atualize na interface o conteúdo daquele objeto tGet, a variável de memória usada é atualizada, sempre pegando o conteúdo atualizado quando você chama uma função usando a variável daquele escopo.

Recomendações

Existem componentes, principalmente de interface, que somente usam Codeblocks. Não têm problema nenhum em usá-los, apenas tome cuidado para não criar monstros. Se você tem uma sequencia de operações muito grande para chamar dentro de um Codeblock, o código fica mais elegante e legível se você fazer uma função — uma STATIC Function por exemplo — e chamá-la de dentro do Codeblock. Lembre-se dos princípios de simplicidade e legibilidade de código.

Conclusão

Acredito que este post contempla o que faltava mencionar sobre Codeblocks. O que vai fazer a diferença na sua aplicação é como você usa us recursos disponíveis da sua plataforma tecnológica.

Agradeço a todos pela audiência, e lhes desejo TERABYTES DE SUCESSO 😀

“Qualquer um pode escrever um código que o computador entenda. Bons programadores escrevem códigos que os humanos entendam.” — Martin Fowler

“Um excelente programador escreve códigos simples que realizam tarefas complexas.” — Júlio Wittwer

 

Arrays em AdvPL – Parte 03

Introdução

No post anterior, vimos algumas particularidades e detalhes sobre os Arrays em AdvPL, referências, atribuições e afins. Agora, pra fechar com chave de ouro — e sair do Array — vamos ver dicas, mais considerações, boas práticas e cuidados com uso de Arrays.

Dimensões e Elementos

Segundo a documentação oficial, você pode criar quantas dimensões você quiser em um array. Embora não exista limite implícito, não exagere. Lembre-se do principio da simplicidade, e quanto mais dimensões tiver um array, maior será a sua ocupação na memória. Embora exista um limite máximo documentado de 100 mil elementos em um array, lembre-se que a soma do tamanho de memoria de cada conteúdo multiplicado pelo número de elementos pode chegar a números assombrosos. Cem mil elementos de 20 KB devem ocupar aproximadamente 2GB de memória.

Componentes que usam lista em formato de Array na interface, e não fazem paginação, por exemplo o tListBox, vão trafegar o array inteiro para a interface armazenar e renderizar na tela. Desse modo, não abuse do recurso, pois você pode penalizar a aplicação em memória — e posteriormente em desempenho.

Passagem de parâmetros para outros processos

Quando passamos um array como parâmetro para qualquer função do AdvPL, um array sempre será passado e recebido por referência. Quando precisamos “duplicar” um array, para que uma outra variável aponte para uma cópia do conteúdo, usamos a função ACLONE().

Quando realizamos passagem de parâmetros através de uma conexão nativa RPC do AdvPL, ou mesmo para um outro processo no mesmo servidor, usando por exemplo a função IPCGO, nestes cados o array é internamente CLONADO. Desse modo, o processo chamado que recebeu o array recebeu um “clone” ou uma cópia, e não uma referência ao array original.

Busca Binária em Array Ordenado

Quando você trabalha com um array ordenado, existem formas bem rápidas de se fazer uma busca neste array. A mais conhecida é chamada de “Busca binária”.  Na prática, o funcionamento é bem simples. Se o array está ordenado em ordem crescente, primeiro você determina a posição do seu primeiro elemento do array — posição 1 — e depois o último — posição igual ao tamanho do Array. Então, com estes dois números, você tira a média simples (soma e divide por 2) considerando o resultado inteiro, e descobre o número do elemento que está no meio da lista. Então você compara com o conteúdo que você está buscando. Se for igual, você achou. Se for menor, refaça a busca, definindo que a nova posição do fim dos dados é a posição do meio atual menos uma unidade, e caso seja maior, refaça a busca considerando a posição inicial de busca como sendo o meio atual mais uma unidade.

Assim, a cada comparação que você faz enquanto não encontra o elemento, simplesmente DESCARTA da busca METADE da lista. Isto realmente é eficiente, porém traz alguns cuidados adicionais:

  1. Para arrays pequenos, a diferença é realmente insignificante, não justifica o esforço de implementação.
  2. Se durante o processamento são feitas manutenções nos valores e conteúdos do Array, você deve garantir que o elemento seja reposicionado na ordem certa. Caso um novo elemento seja inserido no array, ele deve entrar ou ser movido para a posição correta para manter o Array em ordem. Você vai ter que primeiro fazer uma busca, chegar ao elemento mais próximo daquele que você vai inserir, e usar ASIZE / AINS ou AADD / AINS para aumentar o tamanho do array e inserir o novo elemento na posição adequada. Por mais que o algoritmo de sorteio (ASORT) seja bem rápido, inserir fora de ordem e depois reordenar o array a cada manutenção tem um custo elevado e pode prejudicar o desempenho do AdvPL.

Como eu já havia mencionado, para busca performática, inclusive sem a necessidade de ordenação, dos dados, podemos criar um objeto do tipo HASHMAP. Veja os artigos nos links abaixo, bem como o link da TDN com a documentação oficial

Informações adicionais de Ordenação

Quando usamos a opção de sorteio de arrays, e passamos um CodeBlock para customizar uma regra de ordenação, é bem mais performático fazer um ASORT em um array simples — onde cada elemento é o conteúdo a ser sorteado — sem usar um CodedBlock. Se você têm um array multidimensional, não têm escolha a não ser criar e passar como parâmetro o Codeblock de regra de ordenação.  O mesmo se aplica a um Array de dimensão simples, quando a ordenação desejada não seja ascendente (do menor para o maior) ou precise ser customizada.

Estouro de referências em objetos e blocos de código

Quando acrescentamos um objeto ou Codeblock dentro de um array, o elemento em questão passa a ser referenciado pelo índice do array, que aponta para aquela instância de objeto ou bloco de código. Cada novo elemento que nós acrescentamos no array que faz referência a um determinado objeto, o contador de referências daquele objeto ou Codeblock é incrementado. Neste caso, se criarmos um array de 20 mil linhas, e em uma determinada coluna do array sempre acrescentarmos o mesmo CodeBlock, ele será referenciado 20 mil vezes. Se eu clonar este array usando ACLONE, as referências entre os arrays é quebrada mas não as referências ao objeto ou Codeblock. Logo,  aquele Codeblock referenciado 20 mil vezes passa a ser referenciado 40 mil vezes. Vamos ao exemplo:

User Function aTest24()
Local aDados, aCopia
Local bBloco := {|x| conout(x) }
Local nTot := 20000

aDados := Array(nTot)
AFILL(aDados,bBloco)
aCopia := ACLONE(aDados)

Return

Ao executar este fonte, será criado um Array com 20 mil elementos, e cada um deles será alimentado com o mesmo Codeblock através da função AFILL. No momento de executar a função ACLONE(), deve ocorrer o erro abaixo:

Reference counter overflow ( over 32700 ) on tLocalEnv::IncRefs()

Existe um limite interno de contador de referências no Protheus, limitado internamente a 32700 referências. Logo, cuidado ao acrescentar muitas vezes o mesmo objeto ou bloco de código em arrays, pois caso o limite de referências seja atingido, seu código terá que ser refatorado para encapsular as referências em outro array, para este limite não ser atingido. Se o conteúdo referenciado em questão for um Objeto do AdvPL, a mensagem de erro será:

Reference counter overflow ( over 32700 ) on tClassInstance::addRef()

Conclusão

Acho que eu já torci bem a caixa de neurônios — também conhecida por “cabeça” — e não achei mais nada sobre Arrays em AdvPL.  Se eu encontrar mais alguma coisa sobre esse assunto, sai mais um post do forno !!!

Espero que estas informações sejam úteis, e desejo a todos TERABYTES DE SUCESSO !!! 😀

Referências

 

 

 

Arrays em AdvPL – Parte 02

Introdução

No post anterior, vimos o conceito e propriedades de um Array. Agora, vamos ver algumas funções genérias e operadores que podem ser usados com variáveis do tipo ARRAY, e as demais funções da linguagem AdvPL específicas para manipular dinamicamente conteúdos e dimensões de um Array.

Funções genéricas

Para saber quantos elementos têm um array, usamos a função LEN(). Ela também é usada para descobrir, por exemplo, quantos caracteres existem dentro de uma variável ou expressão do tipo “C” caractere no AdvPL.

Operadores

Um array apenas permite o uso dos comparadores de igualdade (=) ou diferença (<> ou !=) entre uma expressão do tipo “A” Array e outra expressão do tipo “A” Array ou “U” NIL.

Quando comparamos uma variável do tipo Array com outro Array, usando os operadores de igualdade (=) ou diferença”( <> ou != ), a comparação não leva em conta o conteúdo ou tamanho do Array. Na verdade, o operador de igualdade somente será .T.  (verdadeiro) entre duas variáveis do tipo  “A” Array se os arrays forem uma referência ao mesmo conteúdo. Por exemplo:

User Function aTest8()
aTeste1 := {1,2,3}
aTeste2 := aTeste1
If aTeste1 = aTeste2
   MsgInfo("aTeste1 e aTeste2 são referencias ao mesmo conteúdo")
Else
  MsgStop("*** Resultado inesperado ***")
Endif
Return

Agora, vamos alterar um pouco o exemplo:

User Function aTest9()
aTeste1 := {1,2,3}
aTeste2 := {1,2,3} 
If aTeste1 = aTeste2
  MsgStop("*** Resultado inesperado ***")
Else
  MsgInfo("aTeste1 e aTeste2 são referencias ao mesmo conteúdo")
Endif
Return

Reparem que aTeste1 e aTeste2 possuem a mesma quantidade de elementos, e cada posição do array é exatamente igual a mesma posição do outro Array. Mas a operação de igualdade FALHOU, pois são dois Arrays totalmente independentes, um não é referência do outro. 

Se você precisa comparar se o conteúdo de dois arrays é o mesmo, mas não necessariamente os arrays têm a mesma referência, você pode construir uma função em AdvPL para realizar a comparação. Veja um exemplo abaixo, que compara dois arrays quaisquer.

STATIC Function ACompare(xVal1,xVal2)
Local cT1 := valtype(xVal1)
Local cT2 := valtype(xVal2)
Local nI 

If cT1 != cT2
	// Os tipos são diferentes ? 
	// A comparação retorna .F.
	Return .F.
Endif

If cT1 != 'A'
	// Nao estamos comparando arrays.
	// Utiliza o operador de igualdade 
	// entre as variáveis 
	Return xVal1 == xVal2
Endif

If len(xVal1) != len(xVal2)
	// Estamos comparando arrays 
	// O tamanho deles é diferente ? 
	// A comparação retorna .F. 
	Return .F.
Endif

// Compara cada elemento do array recursivamente 
For nI := 1 to len(xVal1)
	If !ACompare( xVal1[nI] , xVal2[nI] )
		// No primeiro elemento que a compparação 
		// não for verdadeira, já retorna .F. 
		Return .F. 
	Endif
Next

// Chegou até o final, os elementos 
// dos dois arrays tem o mesmo conteúdo 
Return .T.

Com esta função, conseguimos dizer se dois arrais que não são referência um do outro possuem a mesma estrutura e conteúdo. Vamos fazer um fonte de testes, com dois arrays com mesmo conteúdo, vide abaixo:

User function aTest9()
Local aTeste1 := {1,2,3}
Local aTeste2 := {1,2,3}
Local cMsg := ''
If aTeste1 == aTeste2
	cMsg += 'Os arrays referenciam o mesmo conteúdo' 
Else
	If ACompare(aTeste1,aTeste2)
		cMsg += 'Os arrays são iguais' 
	Else
		cMsg += 'Os arrays são diferentes' 
	Endif
Endif
MsgInfo(cMsg)
Return

O resultado a ser obtido é “Os arrays são iguais”, pois eles não são referência um do outro, mas tem exatamente o mesmo número de elementos, estrutura e conteúdo.

Funções de Manipulação de Arrays

Segue abaixo a lista de funções do AdvPL feitas para trabalhar com manutenção dos dados e consumo dos elementos de um array. Todas estas funções estão documentadas na TDN, cada uma das funções abaixo possui um link para a documentação oficial

Função AADD

Usando a função AADD, podemos acrescentar um novo elemento dentro de um Array já existente.

Sintaxe: AADD( <aArray> , <xValor>) 
-- aArray é o Array a ter um novo elemento acrescentado
-- xValor é a expressão ou valor a ser acrescentado em aArray

Por exemplo, vamos ver uma outra forma de criar o array aCores:

// Ao invés de:
aCores := { {0,"Preto"}, {1,"Azul"} , {2,"Verde"} }

// Podemos usar:
aCores := {}
aadd(aCores, {0,"Preto"} )
aadd(aCores, {1,"Azul"})
aadd(aCores, {3,"Verde"})

A função AADD aumenta (expande) o tamanho do array onde a informação foi inserida, e o novo elemento é SEMPRE inserido no final do Array, passando naturalmente a ser o último elemento do Array — até que um novo elemento seja acrescentado.

Já viu como isso é útil, certo ? Se você têm por exemplo uma lista de categorias dentro de uma tabela, e precisa ou quer mostrar ela em um componente de lista de interface, que trabalha com Array, você pode fazer uma consulta na base de dados e adicionar cada registro encontrado em um Array na memória para posterior utilização.

Função ACLONE

Lembra-se que simplesmente atribuir um array para outra variável faz uma referência ao array original, todos apontam para o mesmo conteúdo? Então, usando esta função, ela duplica os conteúdos e a estrutura do array, criando uma cópia idêntica do array original, com a mesma estrutura e a cópia dos conteúdos. Desta forma, eles passam a ser dois arrays independentes, com conteúdo igual.

Sintaxe: aDestino := ACLONE(aOrigem)
-- aDestino é o array com a cópia dos dados e estrutura de aOrigem
-- aOrigem é o array que será "clonado"

Vamos aproveitar algumas funções anteriores e criar um novo exemplo:

USER Function aTest11()
Local aTeste1, aTeste2 , aTeste3 
aTeste1 := {1,2,3}
aTeste2 := aTeste1
aTeste3 := aClone(aTeste1)
If aTeste1 == aTeste2
	MsgInfo("aTeste1 e aTeste2 SãO referências")
Else
	MsgInfo("aTeste1 e aTeste2 NãO SãO referências")
Endif
If aTeste2 == aTeste3
	MsgInfo("aTeste2 e aTeste3 SãO referências")
Else
	MsgInfo("aTeste2 e aTeste3 NãO SãO referências")
Endif
If ACompare( aTeste2 , aTeste3 )
	MsgInfo("aTeste2 e aTeste3 TEM conteúdo idêntico")
Else
	MsgInfo("aTeste2 e aTeste3 NAO TEM conteúdo idêntico")
Endif
Return

Pelo que vimos até agora, podemos prever que o resultado deste programa será:

  • aTeste1 e aTeste2 SãO referências
  • aTeste2 e aTeste3 NãO SãO referências
  • aTeste2 e aTeste3 TEM conteúdo idêntico

Observações 

Os tipos AdvPL “B” Codeblock e “O” Objeto também são tratados com referência implícita. Logo, ao atribuir uma variável contendo um Objeto ou Codeblock para outra variável ou para um elemento de array, o CodeBlock é referenciado, e não duplicado, INCLUSIVE quando você usa a função ACLONE() — Ela quebra a referência entre o array de origem e o de destino, porém os elementos do tipo Objeto e ClodeBlock não são “duplicados”, eles passam a ser referenciados pelos dois Arrays. Veja o exemplo abaixo, onde usamos um CodeBlock como um elemento do Array:

USER Function aTest12()
Local aTeste1, aTeste2 
Local bBLock := {|x| conout(x) }
aTeste1 := { bBLock  }
aTeste2 := aClone(aTeste1)
If aTeste1 == aTeste2
	MsgStop("Os arrays são referências")
Else
	MsgStop("Os arrays nao são referências")
Endif
If aTeste1[1] == aTeste2[1]
	MsgStop("O CodeBlock é referência")
Else
	MsgStop("O CodeBlock não é referência")
Endif
Return

O resultado deste programa será:

  • Os arrays não são referências
  • O CodeBlock é referência

Um Objeto e/ou um CodeBlock não são copiados ou duplicados, mas passam a serem referenciados por elementos de arrays distintos.

Função ACOPY

Através da função ACOPY, podemos copiar um ou mais elementos de um array de origem para um array de destino. Veja abaixo a sintaxe da função:

aCopy( <aOrigem>, <aDestino> , [nInicio] , [nQuant] , [nPosDest] )
-- aOrigem é o array de onde serão copiados os elementos
-- aDestino é o array para onde serão copiados os elementos
-- nInicio (default=1) indica a partir de qual elemento do array aOrigem que a cópia será iniciada
-- nQuant (default=tudo) indica quantos elementos do array de origem serão copiados para aDestino
-- aPosDest (default=1) indica a posição inicial do array de destino a partir de qual os elementos serão copiados

Observações

  • A função ACOPY() não mexe no tamanho ou quantidade de elementos do array de destino. Logo, o array de destino deve ter elementos suficientes para receber os elementos do array de origem. Somente serão copiados a quantidade de elementos que “cabem” no array de destino. Caso não haja espaço suficiente, os elementos excedentes serão ignorados na cópia, e a função não emite nenhum erro ou advertência.
  • Caso os elementos do array de origem sejam Arrays — no caso de um Array multidimensional — os elementos do array de origem serão REFERENCIADOS, e não CLONADOS. Internamente a função ACOPY() faz simplesmente atribuição do conteúdo dos elementos do array de origem para o array de destino.
  • A vantagem de uso da função ACOPY quando necessária é que você não precisa fazer um LOOP para fazer as atribuições, você apenas parametriza a função e ela realiza o loop e as atribuições internamente.

Vamos ao exemplo:

USER Function aTest13()
Local aOrigem := {"Um","Dois","Três"}
Local aDestino := {NIL,NIL}

aCopy(aOrigem,aDestino)

conout("Tamanho do destino .... "+cValToChar(len(aDestino)))
conout("Primeiro elemento do destino ... "+aDestino[1])
conout("Segundo elemento do destino .... "+aDestino[2])

Return

Baseado no comportamento esperado da função, o tamanho do array de destino após a cópia ainda será de dois elementos, e apenas os dois primeiros elementos de aOrigem serão copiados. O conteúdo já existente nos elementos do array de destino serão sobrescritos com o conteúdo dos elementos de origem. Agora vamos ver um exemplo usando todos os parâmetros:

USER Function aTest14()
Local aOrigem := {"Amarelo","Azul","Vermelho"}
Local aDestino := {"Verde",NIL,NIL,"Branco"}

// Partindo do array aOrigem, para o Array aDestino, 
// a partir do primeiro elemento, copie dois elementos 
// para o array aDestino, a partir da posição 2
aCopy(aOrigem,aDestino,1,2,2)

conout("Tamanho do destino ....... "+cValToChar(len(aDestino)))
conout("1o elemento do destino ... "+aDestino[1])
conout("2o elemento do destino ... "+aDestino[2])
conout("3o elemento do destino ... "+aDestino[3])
conout("4o elemento do destino ... "+aDestino[4])

Return

O resultado do programa acima será as cores da bandeira do Brasil. Os dois primeiros elementos do array aOrigem (“Amarelo” e ‘Azul”) serão copiados a partir do segundo elemento do array aDestino.

Função ADEL

Como o nome sugere, a função ADEL permite apagar — ou deletar — uma posição do array informado como parâmetro. Vamos a sintaxe:

aDel (aArray , nPos)
-- aArray é o array a ter um elemento apagado
-- nPos é a posição do elemento a ser apagado

Observações

  • Deve ser informada a posição do elemento a ser apagado — vale lembrar que os arrays em AdvPL começam na posição 1
  • A função ADEL não mexe no tamanho do array. Ela remove o elemento da posição informada, e o último elemento passa a ser NIL.
  • Como os elementos são acessados pela sua posição no array, ao remover uma posição, o elemento da próxima posição passa a ocupar esta posição, progressivamente até o final do array, onde o último elemento será NIL.
  • Apagar um elemento do array não significa destruir seu conteúdo, mas sim eliminar aquela posição do array que fazia referência para aquele conteúdo.

Por exemplo:

aDados := {1,2,3}
// Apagando a segunda posição 
aDel(aDados,2) 
// O array agora passou a ser { 1, 3, NIL}

Após apagar um elemento do Array, se eu quiser reduzir seu tamanho, eu posso usar a função ASIZE().

Função AEVAL

Esta função é bem prática, você informa um Array e um Codebloc, e a função AEVAL executa — faz um Eval() — do bloco de código para cada elemento do array, passando como parâmetro para o bloco aquele elemento. Vamos a sintaxe:

aEval( <aArray> , <bBloco> , [nInicio] , [nQuant]) 
-- aArray é o array a ser avaliado
-- bBloco é o Codeblock a ser executado 
-- nInicio (default=1) indica a primeira posição do Array a ser considerada para a execução
-- nQuant ( default=tudo) indica a quantidade de elementos a partir do inicial que serão avaliados

Vamos a um exemplo:

USER Function aTest16()
Local aTeste := { "Um" , "Dois" , "Três", "Quatro" }
Local bBLoco := {|x| conout(x) }

// Axecute para o array aTeste, o bloco de código 
// bBloco, a partir do segundo elemento, 
// executando apenas para dois elementos
aEval(aTeste,bBloco,2,2)

Return

O bloco de código declarado ” {|x| conout(x) } ” recebe o elemento atual do array como parâmetro na variável “x”, e executa a função conout() passando “x” como parâmetro. Desta forma, o resultado esperado deste programa é mostrar no console do servidor de aplicação apenas as strings “Dois” e “Três”.

Repare que, se o array em questão for multidimensional — array de arrays — cada elemento do array principal é um outro array, que será passado como parâmetro para o Codeblock. Logo, vamos a um novo exemplo:

USER Function aTest17()
Local aCores := { {0,"Preto"}, {1,"Azul"} , {2,"Verde"} }
Local bBloco := {|x| conout("Cor "+cValToChar(x[1])+ " = " + x[2]  ) }

aEval(aCores,bBloco)

Return

Repare que cada elemento do array aCores é um array com duas posições, a primeira é o número da cor, e a segunda um texto (string) com o nome da cor. Com o programa acima, eu mostro no log de console todas as cores, com o número e a sua descrição. No Codeblock, eu recebo x ( posso dar o nome que eu quiser, dentro do codeblock os parâmetros são informados entre “|” ), e como eu sei que cada parâmetro recebido será uma array de duas posições, eu endereço os elementos deste array usando x[1] e x[2].

Se você não tivesse a função AEval(), para você fazer algo parecido, você teria que fazer algo assim:

For nPos := 1 to len(aArray) 
   Eval( bBloco, aArray[nPos] )
Next

Muitas vezes você precisa executar muito mais coisas do que apenas uma ou outra instrução para cada elemento do Array, algo mais complexo para colocar dentro de um Codeblock. Neste caso, é recomendável mesmo que você escreva o LOOP e as operações que você quer fazer no array. Tentar enfiar um caminhão de instruções dentro de um Codeblock acaba gerando aquele código macarrônico que nem quem escreveu consegue entender direito.

Função AFILL

Podemos usar a função AFILL para atribuir um conteúdo a vários elementos de um Array. A função AFILL não muda o tamanho do Array, apenas realiza um loop varrendo os elementos do array, e atribuindo a cada elemento o valor informado. Vamos a sintaxe da função:

AFILL ( <aArray> , <xValue> , [nInicio] , [nQuant] )
-- aArray é o Array a ter elementos atribuídos
-- xValue é o valor -- de qualquer tipo -- a ser atribuído a cada elemento 
-- nInicio ( default=1 ) indica a partir de qual elemento deve ser iniciada a atribuição 
-- nQuant ( default = todos ) indica quantas atribuições serão realizadas

Lembrando que, de forma similar ao AEVAL() , um AFILL simplesmente faz atribuição direta de xValue para cada elemento do array informado, baseado nos parâmetros. Logo, se você informar um array em xValue, cada elemento de aArray que receber a atribuição vai fazer uma REFERENCIA ao conteúdo do array informado em xValue — e não uma cópia ( ou CLONE). Um AFILL() seria algo parecido com:

For nPos := 1 to len(aArray)
   aArray[nPos] := xValue
Next

Função AINS

Usamos a função AINS para inserir um elemento nulo (NIL) em uma determinada posição do Array, deslocando todos os elementos a partir da posição informada para a frente. Como a função AINS não mexe no tamanho do array, o último elemento do array é descartado. Vamos a sintaxe e exemplo:

aIns (aArray , nPos)
-- aArray é o array a ser manipulado
-- nPos é a posição do elemento NIL a ser inserido

aDados := {1,3,4}
// Inserindo NIL na segunda posição 
aIns(aDados,2) 
// O array agora passou a ser { 1, NIL, 3 }
// Como eu não aumentei o tamanho do array
// antes de fazer a inserção, o último elemento 
// foi descartado

Imagine que eu esteja usando um Array simples, de tamanho N, com uma sequência ordenada de valores, e eu quero acrescentar um valor a mais nesta sequência, mantendo a ordenação.

// Quero inserir o valor 12, ele deve entrar na 3a posição do Array
aValores := { 5 , 10 , 15, 20 }

aadd(aValores,NIL) 
// Aumento o array acrescentando um elemento NIL no final do array
// O array agora passou a ser { 5 , 10  , 15 , 20 , NIL }

aIns(aValores,3) 
// Insiro um elemento NIL na terceira posição, deslocando todas as posições 
// posteriores um elemento para a frente, descartando o último 
// O array agora passou a ser {5 , 10 , NIL , 15 , 20 }

aValores[3] := 12
// Atribuo o valor na posição adequada. 
// O Array agora passou a ser { 5 , 10, 12 , 15, 20 }

 

Função ARRAY

A função Array() serve para retornar um array com um número determinado de elementos. Por default cada elemento não é inicializado — seu valor é NIL. Esta função também pode ser usada para retornar arrays multi-dimensionais. Vamos aos exemplos:

// Cria o array aTeste1 com 5 elementos 
aTeste1 := Array(5) 

// Cria o array aTeste2 com três elementos, onde cada 
// um dos elementos é um array de dois elementos
aTeste2 := Array(3,2)

// Cria o array aTeste3, com 8 elementos, cada um deles 
// é um array de 2 elementos, e cada um destes elementos 
// é um array com 3 elementos. São 48 elementos distintos 
// ( 8 x 2 x 3 ) em três dimensões. 
aTeste3 := Array(8,2,3)

O Array aTeste2 possui a mesma estrutura do Array aCores, porém os conteúdos dos elementos está NIL (Nulo em AdvPL). Caso seja necessário inicializar os elementos do array, com um tipo de dado diferente de “N” Numérico, basta informar o último parâmetro da chamada da função Array() com o valor desejado. Por exemplo, vamos inicializar um array de dimensão simples com 10 elementos, cada um deles com uma string de 20 espaços em branco.

aTeste3 := Array(10 , space(20) )

Poderíamos colocar no lugar da função space(20) uma string entre aspas com vinte espaços em branco, mas isso não tornaria nosso código facilmente legível. No caso de um array multi-dimensional criado pela função Array(), o valor de inicialização será usado para preencher os elementos do último nível. Por exemplo:

aTeste4 := Array(3,2,.F.)

A instrução acima vai criar um array de 3 elementos, onde dentro de cada um deles existe um array de dois elementos, onde cada elemento foi inicializado com o valor booleano Falso em AdvPL.

Observações

  • Qualquer valor numérico passado para a função Array() será interpretado como o número de elementos do array na dimensão informada.
  • Se o valor informado para a inicialização dos elementos for um array, todos os elementos serão inicializados com uma REFERÊNCIA a este Array, e não uma cópia.
  • Outra forma de colocar o mesmo conteúdo em múltiplos elementos de um Array de uma vez é usar a função AFILL().
  • Caso o array já exista e tenha conteúdo, podemos alocar mais elementos no final do Array usando a função AADD() especificando um valor NIL, a ser acrescentado no final do array, ou mesmo a função ASIZE(), que quando usada para aumentar um array, acrescenta ao final do array quantos elementos NIL forem necessários para o Array chegar  ao tamanho informado.

Função ASCAN

A exemplo das demais funções de manipulação de Arrays, a função ASCAN permite informar um Array e um valor de tipo simples a ser pesquisado. Internamente a função faz uma busca sequencial comparando cada elemento do array com o valor de busca informado. Podem ser informados valores do tipo “C” Caractere , “N” Numérico, “D” Data ou “L” lógico. Podemos especificar também um Codeblock no lugar do valor a ser pesquisado. Neste caso, a busca sequencial será feita executando o Codeblock informado como parâmetro para cada elemento do Array, até que o resultado seja verdadeiro, ou acabem os elementos do Array.

A busca sequencial em Array tende a ser progressivamente mais lenta quanto maior é o array, ou quanto mais execuções forem necessárias até que o elemento procurado seja localizado. O melhor caso é o valor pesquisado ser o primeiro elemento do array, e o pior caso é o valor pesquisado não existir — e para chegar a esta conclusão, o ASCAN varreu o array inteiro, comparando elemento por elemento.

Existem outros métodos de busca em Array no AdvPL, como por exemplo a criação de um Hash Map para os elementos de um array, que acelera absurdamente a busca por um valor — porém a busca não é feita pelo ASCAN, mas sim por uma função da classe de HASHMAP do AdvPL, onde a instância desse objeto objeto foi criado por um programa AdvPL a partir de um Array — Que não precisa estar previamente ordenado.

nPos := ASCAN( <aArray> , <xValue> , [nInicio] , [nQuant] )
-- nPos é o número da primeira posição do array onde o valor informado em xValue foi encontrado. 
-- xValue = Se for dos tipos C,D,N,L é um valor a ser procurado. Pode ser infromado também 
   um Codeblock que recebe um elemento do Array, e roda uma expressão de resultado booleano para 
   cada elemento do Array pesquisado. 
-- nInicio ( dafeult=1) indica a posição do elemento em aArray 
-- nQuant (default = todos ) indica quantos elementos serão pesquisados a partir do primeiro.

Quando usamos um array de arrays (multimensional) em AdvPL, somente conseguimos fazer uma busca com ASCAN() se for informado um Codeblock, que receberá em cada chamada um elemento do Array algo da busca, e deve tratar as colunas deste array para montar a condição de busca. Por exemplo:

USER Function aTest19()
Local aCores := { {0,"Preto"}, {1,"Azul"} , {2,"Verde"} }
Local nBusca, nPos
nBusca := 2
nPos := ascan( aCores , {|x| x[1] == nBusca })
If nPos > 0 
	MsgInfo("A cor pesquisada "+cValToChar(nBusca) + ;
	        " foi encontrada na posição "+ cValToChaR(nPos))
Else
	MsgStop("Cor nao encontrada.")
Endif
Return

No exemplo acima, o Codeblock utilizado compara a primeira coluna de cada elemento recebido durante a execução do ASCAN, retornando .T. (verdadeiro) assim que o primeiro elemento com a cor 2 (nBusca) for encontrada.

Função ASIZE

A função ASIZE permite alterar o tamanho (quantidade de elementos)  de um Array. Caso especificado um valor menor que o tamanho atual, os últimos elementos além do tamanho especificado são eliminados do Array. Caso o tamanho informado seja maior que o atual, novos elementos NIL serão acrescentados após o último elemento do Array.

ASIZE ( <aArray> , <nTamanho> )
-- aArray é o array a ser redimensionado 
-- nTamanho é a nova quantidade de elementos que este array deve ter

Ao aumentar o tamanho do array dinamicamente, a vantagem de se usar ASIZE ao invés de AADD é que precisamos apenas de uma chamada da função ASIZE para aumentar o array, sem mexer nos elementos já existentes. Por outro lado, embora a função AADD aumente em uma unidade o tamanho do Array, ela ao mesmo tempo aumenta o array e acrescenta um conteúdo novo ao final do Array, e quando usamos ASIZE devemos fazer uma atribuição para cada elemento.

A função ASIZE também é usada para limpar todos os elementos do Array da lista — ASIZE ( aArray , 0 ). Caso o conteúdo dos elementos não esteja sendo referenciado por outras variáveis, a memória utilizada é liberada. É interessante (e até recomendável) fazer isso para ajudar o gerenciador de memória do Protheus Server, quando o array não será mais utilizado. No caso de uma classe em AdvPL que tenha propriedades como Array, é interessante também limpá-los, dentro de um método criado para fazer o Clean-Up da instância antes de deletá-la.

Função ASORT

A função ASORT permite reordenar todos ou parte dos elementos de um array simples em ordem crescente de conteúdo, ou um array multidimensional usando uma expressão de ordenação customizada através de um Codeblock.

aSort( aArray , [nInicio] , [nQtant] , [bOrdBlock] )
-- aArray é o array a ter os elementos reordenados
-- nInicio (default=1) é a partir de qual posição ele será ordenado
-- nQuant (default=tudo) indica quantos elementos a partir do primeiro serão ordenados
-- bOrdBlock indica um cloco de código para customizar a ordenação

Vamos ao exemplo simples:

User Function aTest20()
Local aFrutas := {"Laranja","Banana","Pera","Maçã"}
Local nI

conout("Antes de ordenar")
For nI := 1 to len(aFrutas)
  conout("aFrutas ["+cValToChar(nI)+"] = "+aFrutas[nI])
Next

aSort(aFrutas) // Ordena o array em ordem crescente

conout("Depois de ordenar")
For nI := 1 to len(aFrutas)
  conout("aFrutas ["+cValToChar(nI)+"] = "+aFrutas[nI])
Next
Return

No exemplo acima, o resultado esperado é:

Antes de ordenar
aFrutas [1] = Laranja
aFrutas [2] = Banana
aFrutas [3] = Pera
aFrutas [4] = Maτπ
Depois de ordenar
aFrutas [1] = Banana
aFrutas [2] = Laranja
aFrutas [3] = Maτπ
aFrutas [4] = Pera

Agora, vamos a um exemplo usando um array multidimensional de 2 colunas.

USER Function aTest21()
Local aItens := { {"Régua" , 10.0 }, {"Caneta" , 2.0 } , {"Borracha", 3.0 } , {"Apagador" , 10.0 } }
Local nI

conout("Antes de ordenar")
For nI := 1 to len(aItens)
	conout("aItens ["+cValToChar(nI)+"] = { "+aItens[nI][1]+" , " + cValToChar(aItens[nI][2])+ "}")
Next

aSort(aItens,,, { |x,y| x[1] < y[1] })

conout("Depois de ordenar")
For nI := 1 to len(aItens)
	conout("aItens ["+cValToChar(nI)+"] = { "+aItens[nI][1]+" , " + cValToChar(aItens[nI][2])+ "}")
Next

Return

Agora, vamos entender o que faz o Codeblock que permite customizar a ordenação. O Codeblock vai receber dois parâmetros — que por exemplo eu chamei de x e y — , onde cada um deles pode ser qualquer elemento do Array a ser ordenado. O Codeblock deve retornar .T. (verdadeiro) se para efeito de ordenação o elemento x deve vir antes do elemento y, caso contrário deve retornar .F. (falso)

Como o array é multidimensional, cada elemento de aItens é um array de duas posições, então eu comparo a primeira coluna de x com a primeira coluna de y. Se x[1] < y[1], então estes elementos estão ordenados corretamente. A função ASORT internamente vai avaliar os elementos do Array quantas vezes forem necessárias para reordenar os elementos.

No caso de uma condição de sorteio composta, por exemplo eu quero ordenar um array de 3 colunas pela primeira e segunda colunas (chave composta), onde ambas as colunas sejam por exemplo do tipo “C” caractere, eu posso montar a seguinte expressão de ordenação do CodeBlock:

aSort( aItens ,,, {|x,y| x[1]+x[2] < y[1]+y[2] })

Existe uma segunda forma de montar esta expressão, um pouco menos intuitiva do que a primeira, porém sem a necessidade de concatenação dos campos, inclusive pode ser usada para colunas de tipos diferentes.

aSort(aItens ,,, { |x,y| ( x[1] < y[1] ) .OR. ; 
                 ( x[1] == y[1] .AND. x[2] < y[2] ) } )

Neste caso, eu considero que x está antes que y, caso a primeira coluna de x seja menor que a primeira coluna de y, OU , caso elas sejam iguais, mas a a segunda coluna de x seja menor que a segunda coluna de y. Veja o exemplo abaixo:

USER Function aTest22()
Local nI 
Local aItens := {}

aadd(aItens , { 1 , 3 } )
aadd(aItens , { 2 , 2 } )
aadd(aItens , { 3 , 1 } )
aadd(aItens , { 1 , 2 } )
aadd(aItens , { 3 , 3 } )
aadd(aItens , { 2 , 1 } )
aadd(aItens , { 3 , 2 } )
aadd(aItens , { 2 , 3 } )
aadd(aItens , { 1 , 1 } )

aSort(aItens ,,, { |x,y| ( x[1] < y[1] ) .OR. ;
                 ( x[1] == y[1] .AND. x[2] < y[2] ) } )

For nI := 1 to len(aItens)
	conout("N1 = " + cValToChar(aItens[nI][1]) + ;
               " N2 = " + cValToChar(aItens[nI][2]) )
Next

Return

O resultado obtido deve ser todos os elementos ordenados em ordem crescente da primeira coluna, e quando os elementos da primeira coluna forem iguais, a ordenação é feita considerando a segunda coluna.

N1 = 1 N2 = 1
N1 = 1 N2 = 2
N1 = 1 N2 = 3
N1 = 2 N2 = 1
N1 = 2 N2 = 2
N1 = 2 N2 = 3
N1 = 3 N2 = 1
N1 = 3 N2 = 2
N1 = 3 N2 = 3

Caso eu queira, por exemplo, ordem crescente pela primeira coluna, porém ordem decrescente pela segunda coluna, basta inverter a comparação feita com a segunda coluna, por exemplo:

aSort(aItens ,,, { |x,y| ( x[1] < y[1] ) .OR. ;
                         ( x[1] == y[1] .AND. x[2] > y[2] ) } )

Função ATAIL

A função ATAIL é uma forma de você acessar diretamente o último elemento do Array. Normalmente quando você quer acessar o último elemento de um Array, você usa:

xValue := aArray[len(aArray)]

Usando ATAIL(), você escreveria:

xValue := ATAIL(aArray)

Conclusão

Isso é praticamente tudo o que podemos fazer com um Array em AdvPL. Sim, ainda teremos mais um post sobre esse assunto, sobre aspectos gerais, tamanhos e dimensões, e inclusive boas práticas.

Espero que estas informações lhe sejam muito úteis, e desejo a todos TERABYTES DE SUCESSO !!! 😀

Referências

 

Arrays em AdvPL – Parte 01

Introdução

O Array (ou Matriz) no AdvPL é um tipo básico de dado que permite criar listas de tamanho e conteúdo dinâmico. É muito útil e prático trabalhar com arrays em AdvPL. Neste post, vamos ver tudo — ou quase tudo — que podemos fazer com um Array.

Criando um Array

Como o AdvPL padrão possui tipagem de variável dinâmica, criamos um array atribuindo ele a uma variável. Declaramos um array vazio usando abertura e fechamento de chaves  { }. Para declarar um array com conteúdo, basta informar o conteúdo de cada elemento dentro das chaves, e separar os elementos por vírgula. Vide exemplos abaixo:

Local aNumVazio := {}
Local aPrimos := {2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97}
Local aNomes := {"José","João"}

Nos exemplos mencionados, aNumVazio foi inicializado como um array sem elementos. Já o array aPrimos contém os números primos menores que 100, e o Array aNomes contém duas strings. Cada array criado como uma lista com elementos do mesmo tipo é conhecido como “Array de Dimensão Única”, ou um Array simples.

Cada elemento de um array pode conter elementos de qualquer tipo do AdvPL, inclusive os tipos “B” (CodeBlock), “O” (Objeto), “U” (NIL — ou Nulo), inclusive o tipo “A” (Array). Porém, existem ressalvas quanto a alguns tipos e algumas composições com Arrays, que veremos mais para a frente.

O conteúdo de cada elemento do array pode ser endereçado diretamente pela sua posição ordinal no Array, bastando para isso colocar em frente ao array a posição desejada entre [ ] (colchetes). Os Arrays em AdvPL começam na posição 1.

conout(aPrimos[1]) // deve mostrar o numero 2
conout(aPrimos[6]) // deve mostrar o numero 13
conout(aPrimos[26]) // deve dar erro de execução "Array out of bounds"

Se um array possui 25 elementos, como é o caso do aPrimos, eles são endereçados do elemento 1 ao 25. Caso eu tente acessar um elemento menor ou igual a zero, ou maior que o tamanho — ou número de elementos — do Array, a aplicação AdvPL é interrompida com o erro fatal de execução “Array out of bounds” — ou seja, tentativa de acesso de um elemento fora dos limites do Array.

Mais para frente veremos como acrescentar, remover e realizar diversas operações com um ou mais elementos de um array, usando funções básicas da linguagem AdvPL. Neste momento vamos focar no Array em si. Praticamente todos os exemplos deste tópico vão utilizar arrays com conteúdo declarado.

Arrays de Arrays

Um elemento de um Array pode ser outro Array. Isto permite criarmos array de múltiplas dimensões. Por exemplo, imagine uma lista de códigos numéricos, onde cada número possui uma descrição — por exemplo uma lista de cores. Podemos criar um array onde cada um dos elementos é um outro array, contendo a cor e seu nome.

aCores := { {0,"Preto"}, {1,"Azul"} , {2,"Verde"} }

Para endereçar os elementos do array, usamos a posição do elemento entre colchetes, certo? Logo, se acessarmos aCores[1], estamos acessando o array que está dentro da primeira posição do aCores — { 0 , “Preto” }. Se quisermos acessar um elemento de dentro desse array, podemos fazer de duas formas:

conout(aCores[1][1]) // Deve mostrar o numero 0 (zero)
conout(aCores[1][2]) // Deve mostrar a string "Preto"
conout(aCores[3][2]) // Deve mostrar a string "verde"

A segunda forma é, ao invés de usar colchetes em seguida de colchetes, podemos apenas usar uma abertura e fechamento de colchetes, e colocar os números dos elementos separados por vírgula. No caso, partindo do exemplo acima, temos:

conout(aCores[1,1]) // Deve mostrar o numero 0 (zero)
conout(aCores[1,2]) // Deve mostrar a string "preto"
conout(aCores[3,2]) // Deve mostrar a string "verde"

Como a estrutura do array é dinâmica, podemos criar arrays com mais sub-níveis. Por exemplo, imagine um array contendo uma lista de remédios, onde cada remédio é um array contendo o código do remédio, o nome dele, a quantidade de caixas em estoque, e um array de lotes, onde cada lote é um array com três posições, contendo o numero do lote, a quantidade de caixas deste lote, e a validade deste lote.

aRemedios := { { 1 , "Aspirina" , 5 , { { 21031 , 2 , "201906" } , { 21032 , 3, "201907" } } } }

Agora, sabendo que o Array têm esta estrutura, vamos varrer este array e mostrar o conteúdo dele no log de console:

For nI := 1 to len(aRemedios)
   aRem := aRemedios[nI]
   conout("Remedio ... "+cValToChaR(aRem[1])+ ;
          " : "+aRem[2] +" ( Estoque "+ cValToChaR(aRem[3])+ " )")
   For nJ := 1 to len(aRem[4])
      aLote := aRem[4][nJ]
      conout("   Lote "+cValToChar(aLote[1])+;
             " Estoque "+CValToChar(aLote[2])+" Validade "+aLote[3])
   Next
   conout('')
Next

O resultado esperado no console do Application Server é:

Remedio ... 1 : Aspirina ( Estoque 5 )
   Lote 21031 Estoque 2 Validade 201906
   Lote 21032 Estoque 3 Validade 201907

Observações importantes

Repare no exemplo de leitura do Array aRemedios, que eu criei dentro do primeiro loop uma variável chamada aRem, onde eu atribuo nela o array que está na posição atual do array aRemedios. E, ao mostrar os elementos deste array, como o quatro elemento é também um array de lotes, eu crio outra variável para acessar este conteúdo. Eu poderia ter escrito isso de outro modo, mas não o fiz por uma razão deveras interessante. Se eu tivesse escrito o programa sem criar estas variáveis intermediárias, o programa para mostrar os remédios ficaria assim:

For nI := 1 to len(aRemedios)
   conout("Remedio ... "+cValToChaR(aRemedios[nI][1])+ ;
          " : "+aRemedios[nI][2] +" ( Estoque "+ ;
          cValToChaR(aRemedios[nI][3])+ " )")
   For nJ := 1 to len(aRemedios[nI][4])
      conout(" Lote "+cValToChar(aRemedios[nI][4][nJ][1])+;
                " Estoque "+CValToChar(aRemedios[nI][4][nJ][2])+;
                " Validade "+aRemedios[nI][4][nJ][3])
   Next
   conout('')
Next

As duas formas estão corretas e vão funcionar. Porém a segunda forma será mais lenta. Vou explicar por quê: Dentro do loop eu uso a variável nI para acessar o remédio, e nJ para acessar os lotes. Cada propriedade de um determinado lote de um remédio é acessada com a expressão:

aRemedios[nI][4][nJ][1] // Codigo do lote nJ do remédio nI
aRemedios[nI][4][nJ][2] // Estoque do lote nJ do remédio nI
aRemedios[nI][4][nJ][3] // Validade do lote nJ do remédio nI

Desta forma, a primeira linha do exemplo acessa o elemento [nI] do Array aRemedios — que é um array — depois resgata o [4] elemento desse array , depois resgata o elemento [nJ] — que é outro array — de dentro desse array, e depois resgata a posição [1] desse array. Estas buscas internas para acessar o remédio, depois a lista de lotes, e depois um determinado lote, vai se repetir nas três linhas deste exemplo. Quando usamos por exemplo os arrays temporários dentro do loop, eliminamos a repetição desnecessária dessas estas etapas, vejamos o primeiro fonte de exemplo:

For nJ := 1 to len(aRem[4])
   aLote := aRem[4][nJ]
   conout(" Lote "+cValToChar(aLote[1])+;
          " Estoque "+CValToChar(aLote[2])+" Validade "+aLote[3])
Next

Ao entrar em um lote, como eu preciso de três informações deste lote, para não fazer a busca a cada elemento no array aRemedios, ou mesmo no aRem[4], o remédio atual já está na variável aRem, a lista de lotes estão em aRem[4], e cada elemento da lista é lido no array aLote. Logo, dentro do loop eu vou acessar direto os elementos de aLote[1], aLote[2] e aLote[3] para recuperar os dados do lote atual, sem precisar repetir a busca em todos os níveis caso eu usasse diretamente apenas o Array aRemedios.

Como os arrays são acessados diretamente pela posição do elemento na memória, seu acesso é muito rápido. Quando eu menciono que o segundo exemplo de código será mais lento, estamos falando de frações de milissegundo. Porém, em um código onde os loops mais internos são executados milhões de vezes, este tempo torna-se perceptível.

Características Especiais – Referência

O Tipo de dado “A” Array no AdvPL é tratado como uma variável de referência implícita. Desse modo, quando fazemos uma atribuição de uma variável que contém um array para outra variável, é criada uma referência ao array armazenado na primeira variável. Isto significa que, na memória o array é único, e agora eu tenho duas variáveis que apontam para o mesmo conteúdo. Se eu alterar o conteúdo de um elemento dentro desse array, as duas variáveis vão apontar para o mesmo conteúdo alterado. Vamos a um exemplo que é mais fácil de identificar.

aCores := { {0,"Preto"}, {1,"Azul"} , {2,"Verde"} }
aCor := aCores[1]
conout(aCor[1]) // deve mostrar 0 (zero)
conout(aCor[2]) // deve mostrar "Preto"
aCor[2] := "VERMELHO"
conout(aCores[1][2]) // deve mostrar VERMELHO

Quando criamos a variável aCor, ela passa a referenciar o primeiro elemento do array aCores. Desse modo, quando alteramos o conteúdo do segundo elemento do array aCor, isto reflete no array aCores. Veja bem, não foram dois conteúdos que foram alterados, a questão é que a referência de um array aponta para o mesmo conteúdo.

Quando passamos como parâmetro um array para uma função qualquer, é passado sempre uma referência desse array. Uma referência, em grosso modo, é como se duas variáveis apontassem para o mesmo endereço de memória onde está o conteúdo.

Se, dentro da função, fizermos alguma alteração no conteúdo do Array passado como parâmetro, estamos alterando o conteúdo do array original, no escopo da função que passou o parâmetro. Vamos refazer o exemplo acima de outra forma:

User Function aTest3()
Local aCores, aCor
aCores := { {0,"Preto"}, {1,"Azul"} , {2,"Verde"} }
aCor := aCores[1]
conout(aCor[1]) // deve mostrar 0 (zero)
conout(aCor[2]) // deve mostrar "Preto"
TrocaCor(aCor,"VERMELHO")
conout(aCores[1][2]) // deve mostrar VERMELHO 
Return

Static function TrocaCor(aArr,cVal)
aArr[2] := cVal
Return

O resultado será o mesmo, pois o array aCor foi passado implicitamente por referência para a função TrocaCor(). Se eu criar uma nova variável chamada aPaleta, e atribuir nela o conteúdo de aCores (aPaleta := aCores), a partir deste momento aPaleta é uma referência ao array aCores. Logo, ambas apontam para o mesmo conteúdo. Logo, como o conteúdo na pratica (e na memória) é o mesmo, mexer nele usando aPaleta ou aCores dá na mesma.

Quebra de Referência

Existem algumas situações onde é desejável que a referência com o array original seja quebrada. Para isso, pegamos a variável que contém o Array (não um elemento para o qual ele aponta, o Array mesmo) e atribuímos na variável um novo conteúdo. Vamos voltar ao exemplo original e fazer algumas alterações.

User Function aTest4()
Local aCores, aCor
aCores := { {0,"Preto"}, {1,"Azul"} , {2,"Verde"} }
aCor := aCores[1]
conout(aCor[1]) // deve mostrar 0 (zero)
conout(aCor[2]) // deve mostrar "Preto"
aCor := NIL
conout(aCores[1][1]) // ainda mostra 0 (zero)
conout(aCores[1][2]) // ainda mostra "Preto"
Return

aCor é uma variável que faz referência ao array contido no primeiro elemento do array aCores. Porém, quando eu atribuo nesta variável o conteúdo NIL, o AdvPL quebra a referência com o primeiro elemento do array aCores, e passa a ter o valor NIL.  Como vemos no exemplo, o array aCores não foi afetado.

Agora, quando fazemos uma atribuição endereçada especificamente a um elemento do array contido na variável, o conteúdo daquele elemento é alterado. Vamos fazer isso criando uma referência do array aCores, que vamos chamar de aPaleta.

User Function aTest5()
Local aCores, aPaleta
aCores := { {0,"Preto"}, {1,"Azul"} , {2,"Verde"} }
aPaleta := aCores
conout(aPaleta[2][1]) // deve mostrar 1 (um)
conout(aPaleta[2][2]) // deve mostrar "Azul"
aPaleta[2] := NIL
conout(valtype(aPaleta[2])) // deve mostrar "U" (Nil)
conout(valtype(aCores[2])) // também deve mostrar "U" (Nil)
Return

Neste caso, estamos fazendo uma atribuição direta a um elemento do array. O conteúdo daquela posição foi alterado, e como as duas variáveis apontam para o mesmo conteúdo … temos o resultado acima. aCores[2] e aPaleta[2] apontam para o mesmo conteúdo.

Quando queremos realizar criar uma nova variável de array, que não aponte para o conteúdo do array original, mas que crie uma cópia do conteúdo do array original e aponte para a cópia, usamos a função aClone(). No próximo post vamos ver em detalhes várias funções dedicadas à manutenção e conteúdo de arrays.

Referência circular

Ao usar Arrays com múltiplas dimensões (Arrays de Arrays) e trabalhar com referências deste Array, deve-se tomar cuidado para você não criar uma referência de um dos elementos de uma dimensão do array para uma dimensão superior. Este tipo de referência é conhecido por “referência circular”. Vamos ao exemplo mais simples:

User Function aTest6()
Local aTeste := { NIL }
aTeste[1] := aTeste
Return

Você cria o array aTeste com um elemento NIL. Agora, atribui dentro deste elemento uma referência ao próprio array. Logo, o array contém ele mesmo. Como eu disse, este é o exemplo mais simples. Se eu criar dois arrays, e fazer uma referência de um elemento para o outro, e vice-versa, eu também crio uma referência circular.

User Function aTest7()
Local aTeste1 := { NIL }
Local aTeste2 := { NIL }
aTeste1[1] := aTeste2
aTeste2[1] := aTeste1
Return

Se você criar uma referência circular, e fizer uma função para varrer os elementos de um array, e caso um deles seja um array, ao entrar em um nível inferior da busca, você está entrando em um nível superior que você já passou. Uma função recursiva de varredura dos elementos desse array com referência circular, caso não seja preparada para ligar com esta situação — marcando por exemplo cada nó visitado da estrutura — fatalmente pode ficar em LOOP por algum tempo, até esgotar o nível de recursão de chamadas do AdvPL — algo em torno de 200 chamadas empilhadas.

Conclusão

Pra molhar o bico, acho que esta introdução já têm bastante informação. Nos próximos posts, vamos começar a ver as funções básicas do AdvPL que permitem realizar manutenção dinamicamente em um Array.

Desejo a todos TERABYTES DE SUCESSO !!! 

Referências

 

 

Acelerando o AdvPL – Importação de tabelas

Introdução

Existem muitas situações onde existe a necessidade de alimentar ou importar tabelas para uso do ERP Microsiga / Protheus. Quando esta necessidade envolve um grande número de registros, e um curto espaço de tempo, precisamos fazer esta operação ser o mais rápida possível. Nesse post vamos abordar algumas técnicas para realizar este tipo de operação.

  • Crie os índices da tabela apenas após a importação dos dados. 

Se a tabela está vazia, e os indices de trabalho desta tabela já existem, cada novo registro acrescentado faz o banco de dados atualizar todos os índices durante a inserção dos dados. Sem os índices, o banco apenas insere os dados. A inserção vai ser muito mais rápida, e os índices também são criados mais rápido no final do processo de importação, quando você já inseriu todos os registros na tabela. Se você precisa ter um índice na tabela, no processo de importação, por exemplo para fazer alguma validação ou query na tabela enquanto ela está sendo importada, crie apenas este índice.

  • Trabalhe com blocos de inserções transacionadas

Os bancos de dados relacionais garantem a integridade dos dados através do registro de um “LOG Transacional”. Cada operação de inserção primeiro é gravada no LOG do SGDB, e depois é “efetivada” — o Banco de Dados faz o COMMIT da informação. Quando não estamos usando uma transação explícita, cada instrução executada no Banco de Dados faz COMMIT automaticamente no final da instrução. É mais eficiente abrir uma transação — BEGIN TRANSACTION — pois enquanto a transação está aberta, cada instrução gera apenas o LOG Transacional, e depois de fazer por exemplo 1000 inserções na base de dados, você encerra a transação — END TRANSACTION no AdvPL — e neste momento o Banco de dados faz o COMMIT de todas as linhas do LOG de uma vez.

  • A nova tabela que vai receber os dados deve ser criada pelo DBAccess 

Mesmo que você saiba quais são os tipos dos campos da estrutura da tabela final, onde os dados serão gravados, quem sabe criar a tabela nos padrões e moldes necessários para o correto funcionamento da aplicação é o Protheus, que passa para o DBAccess a estrutura da tabela a ser criada. Internamente, o DBAccess cria constraints e outros elementos, além de alimentar algumas tabelas de controle interno. Deixe o DBAccess fazer a criação da tabela. Você pode até apagar os índices de dados para fazer a importação da tabela após eles terem sido criados, porém não apague o índice da primary key (R_E_C_N_O_), e se a tabela possui índice único (sufixo _UNQ), mantenha ele também, caso você queira que o próprio banco de dados aborte a operação no caso de haver uma chave única duplicada nos dados sendo importados.

  • Usando as funções de baixo nível de tabelas do AdvPL 

Caso a importação dos dados seja realizada por uma aplicação AdvPL, que foi criada para esta finalidade, que não vai concorrer com os processos do ERP, podemos usar diretamente as funções de baixo nível de tabelas do AdvPL. As funções de mais alto nível — como RECLOCK e MSUNLOCK devem ser usadas dentro dos programas do ERP, no ambiente do ERP, pois elas têm tratamentos adicionais ligados ao FrameWork do ERP. Se, ao invés disso, usarmos diretamente as funções diretas de manutenção de dados, em um cenário controlado, podemos obter mais ganhos de desempenho — DBAppend() para acrescentar um novo registro, DBRUnlock() — sem nenhum parâmetro — para soltar o bloqueio (lock) de todos os registros obtidos durante aquele processo (lembrando que eu não posso soltar o lock dentro de transação), abrir e fechar transação sem usar BEGIN e END TRANSACTION — Usando diretamente a função TCCommit().

IMPORTANTE

O Uso das funções básicas do AdvPL para a manutenção de tabelas NÃO DEVE SER MISTURADO com o uso das funções do Framework. Dentro de um processo preparado para o contexto de execução do ERP, BEGIN TRANSACTION trabalha junto com END TRANSACTION, Reclock() e MSUnlock() trabalham juntas, ambas possuem tratamentos diferenciados e automáticos quando você programa dentro do ambiente do ERP e/ou dentro de um bloco transacionado, e todas elas internamente fazem uso das funções de baixo nível do AdvPL. Se você vai fazer uma aplicação que não vai rodar dentro do contexto do ERP, como por exemplo as aplicações que eu publico no BLOG — CRUD em AdvPL por exemplo — você deve usar apenas as funções de baixo nível. Misturar chamadas diretas das funções de Framework com chamadas das funções de baixo nível NO MESMO PROCESSO pode causar efeitos imprevisíveis.

  • Abra a tabela de destino em modo exclusivo

Se a sua aplicação vai usar o ambiente do ERP — Prepare Environment, Reclock e afins, abra a tabela de destino em modo exclusivo — veja como na documentação da função ChkFile() do Framework AdvPL. Caso você vá utilizar as funções de baixo nível de acesso a tabelas, veja como abrir uma tabela em modo exclusivo na documentação da função DBUseArea() da TDN ou do comando USE da TDN. Caso a tabela esteja aberta em modo compartilhado, cada inserção de um novo registro faz o DBAccess registrar um bloqueio desse registro na inserção, e cada desbloqueio gera uma requisição a mais ao DBAccess para soltar o bloqueio. Quando usamos a tabela em modo EXCLUSIVO, somente o meu processo está usando a tabela, e não é feito nenhum lock no DBAccess.

  • Use um BULK INSERT ou ferramenta de apoio do próprio Banco de Dados

Não há forma mais rápida de inserir dados em uma tabela senão usando uma ferramenta do próprio Banco de Dados. Porém, nem sempre é possível fazer isso de forma automática, e alguns cuidados são necessários. Primeiro, a tabela já deve ter sido previamente criada pelo DBAccess. Depois, a gravação dos dados pelo Banco deve ser feita da mesma forma que o DBAccess faria — Qualquer campo do tipo caractere deve estar preenchido com espaços em branco até o final do tamanho do campo, nenhum campo pode ter valor NULL — exceto campo MEMO –, um campo “D” Data em AdvPL em uma tabela do DBAccess é gravado como “C” Caractere com 8 posições, no formato AAAAMMDD, uma data vazia são 8 espaços em branco, um campo “L” lógico do AdvPL é criado pelo DBAccess no SGDB como um campo de 1 Caractere, contento a letra “T” para valor verdadeiro e “F” para falso, um campo numérico têm o valor default = 0 (zero).

  • Diminua a distância entre as informações de origem e destino 

Normalmente uma ferramenta do próprio banco de dados que faz inserção deve ser executada na própria máquina onde está o SGDB. Isto a torna ainda mais rápida. Se você está fazendo uma importação de dados que estão sendo lidos pelo Protheus de alguma fonte de dados, e depois repassadas ao Banco de Dados através do DBAccess, e cada um destes componentes (Protheus, DBAccess e o SGDB) estão em máquinas distintas, onde existe uma interface de rede entre cada um destes componentes, fatalmente a rede vai influenciar no desempenho desta operação. Se for possível eliminar pelo menos uma camada de rede entre as aplicações, pelo menos na hora de fazer a importação, ela tende a ser mais rápida.

  • Se posssível, leia as informações da origem com Query

Se a origem dos dados for uma tabela da base de dados, ao invés de abrir a tabela no modo ISAM — com ChkFile() ou DBUseArea() — leia os dados da tabela com um SELECT — O DBAccess a partir de 2017 passou a trafegar blocos de linhas entre o DBAccess e o Protheus, o que é muito mais performático principalmente quando ambos estão em máquinas diferentes. No acesso ISAM emulado, cada DBSkip() na tabela traz apenas um registro. Quando usamos a Query, várias linhas são trafegadas em uma unica requisição, e cada DBSkip() na Query consome a próxima linha em cache sem usar a rede. Quando as linhas do cache terminarem, o DBAccess envia mais um bloco de linhas.

Inserção direta via TCSqlExec()

Da mesma forma que é possível usar uma aplicação externa para inserir dados direto no banco, é possível também realizar a inserção direta no banco em AdvPL usando TCSqlEXec() — MAS, POREM, TODAVIA, CONTUDO, ENTRETANTO, você precisa tomar os mesmos cuidados como se você estivesse alimentando estes dados por fora, E, você somente vai ter algum ganho de desempenho se você montar uma string de parâmetro com mais de uma inserção — por exemplo, inserindo múltiplas linhas na mesma instrução de insert — caso o banco de dados tenha suporte para isso — ou concatenando mais de uma instrução de inserção na mesma requisição, separando as instruções com “;” — ponto e vírgula.

cStmt := "INSERT INTO TABELA(CPO1,CPO2,R_E_C_N_O_) VALUES ('00001','Nome',1);"
cStmt += "INSERT INTO TABELA(CPO1,CPO2,R_E_C_N_O_) VALUES ('00002','Endereço',2);"
(...)
nRet := TCSqlExec(cStmt)

No exemplo acima, você pode por exemplo montar uma string de inserção com por exemplo 32 KB de dados, e enviar isso de uma vez para o banco de dados. Mas, inserir dados desta forma possui vantagens e desvantagens:

  • Uma requisição insere vários registros na mesma instrução. Usando DBAppend() / Reclock(), é inserido um registro por vez.
  • Uma requisição maior no lunar de várias requisições menores aproveita muito mais a banda de rede disponível.

Por outro lado…

  • Para montar a inserção dos valores de forma literal, você deve tratar manualmente a existência de caracteres especiais, aspas simples como conteúdo de campo, e para alguns bancos tratar “escape sequences” e caracteres especiais.
  • Campos MEMO (BLOB/CLOB/LONGVARBINARY) normalmente não são suportados com envio literal pela instrução de INSERT feita desta forma. Você acaba fazendo a inserção primeiro dos dados dos campos de tamanho fixo, e depois tem que usar o modo ISAM de acesso para alterar o registro em AdvPL, para então alimentar o conteúdo do campo.

IMPORTANTE

Nenhum dos métodos acima é recomendável de ser feito em processos concorrentes, em modo compartilhado, com outros programas inserindo informações na tabela em questão. As sugestões deste post mostram apenas jeitos diferentes e alternativas de importação de dados puros direto para tabelas de um banco de dados. Como nestes exemplos os dados são praticamente escritos “direto” nas tabelas, a aplicação que está importando os dados deve ser a responsável por criticar os dados de entrada, e garantir que a sua escrita esteja em conformidade com as regras do ERP. Mesmo uma operação de cadastro feita no ERP pode disparar integrações e processos auxiliares que alimentam outras tabelas. Mexer diretamente na base de dados sem respeitar as regras de negócio e/ou as validações do produto podem gerar comportamentos inesperados e indesejáveis no produto e nas rotinas que consomem estes dados. A forma mais segura de importar dados em tabelas do ERP é a utilização de rotinas automáticas, onde fatalmente vai existir um pênalti de desempenho para validar todos os valores de todos os campos informados, mas desta forma evita-se a alimentação incompleta ou inconsistente de dados que pode prejudicar o comportamento do produto.

Conclusão

Não existe “almoço grátis”, alguém está sempre pagando a conta. Uma inserção de dados é um processo deveras simples, você obtêm maior desempenho basicamente realizando requisições em blocos, e pode ganhar mais reduzindo as operações intermediárias intrínsecas ao processo — porém este ganho normalmente abre mão de validações que visam garantir a integridade das informações imputadas, ou acabam exigindo mais trabalho e etapas adicionais para a conclusão do processo.

Novamente agradeço pela audiência, curtidas e compartilhamentos, e desejo a todos TERABYTES DE SUCESSO !!! 😀

Referências

 

Manipulação de arquivos em AdvPL – Parte 02

Introdução

No post anterior, Manipulação de arquivos em AdvPL – Parte 01, vimos em detalhes os comportamentos das funções de baixo nível de arquivo, acompanhados de um exemplo de manipulação simples de um arquivo texto. Neste post, vamos rever alguns detalhes, e ver como as funções de baixo nível de arquivo do AdvPL foram adaptadas para o Linux e MAC OS.

Vale a pena lembrar de novo

  • As funções FCreate() e FOpen() retornam um identificador numérico, chamado de Handler do arquivo. Ele é usado como parâmetro para as demais funções. Se o número retornado for -1 (menos um), isto indica uma falha na criação e/ou abertura da tabela, respectivamente.
  • Caso o nome do arquivo informado inicie com uma unidade de disco, o Application Server assume que o arquivo está na estação onde o SmartClient está sendo executado. Se você criou um JOB — processo sem interface — este JOB somente consegue abrir arquivos a partir do RootPath do ambiente no Protheus Server, onde o path deve começar com uma “\” barra inversa.

Novas informações

  • Um Handler de arquivo no AdvPL não pode ser compartilhado entre processos. Isto é, o handler retornado na criação ou abertura da tabela é válido apenas dentro do processo (ou Thread) que obteve o Handler.
  • Um Handler de arquivo válido obtido pelas funções FCreate() e/ou FOpen() mantém o arquivo aberto até que ele seja explicitamente fechado pela função FClose(). Após fechar o arquivo, este Handler deve ser descartado, pois ene não é mais válido após o fechamento da tabela.
  • Um Handler de arquivo não fechado explicitamente pelo programa AdvPL somente será fechado automaticamente pelo Application Server no término do processo / Thread — Seja ela finalizada com sucesso ou com erro de execução.

Comportamentos em ambientes Multi-Plataforma

Atualmente o Application Server é homologado para Windows e Linux, e o SmartClient é homologado para Windows, Linux e Mac OS. Porém, o sistema de arquivos destes sistemas operacionais difere em alguns pontos em relação ao Windows.

  • Não existe o conceito ou a implementação de letras para designar unidades de disco.
  • A barra separadoras de diretórios utilizada é a barra para a direita “/”, enquanto no Windows é usada a barra para a esquerda “\”.
  • O nome dos arquivos é case-sensitive. Isto é, chamou o arquivo de “Jose.txt”, e tentou abrir como “jose.TXT”, o sistema operacional retorna erro de “Arquivo não encontrado.”
  • O sistema de arquivos no Linux não prevê o tratamento de acesso exclusivo a um determinado arquivo por um determinado processo.
  • O compartilhamento de arquivos via rede é feito utilizando NFS, diferente do compartilhamento de arquivos do Windows, Você não tem nenhum prefixo ou identificador informando que seu arquivo está sendo acessado pela rede. O caminho do arquivo é escrito da mesma forma, porém a partir da pasta que deve ser acessada pela rede,deve ser feito no Linux e MAC OS um “ponto de montagem”, que indica ao sistema operacional que a partir daquela pasta, os arquivos estão fisicamente em outra máquina.

Devido a estas diferenças, foram adotadas algumas convenções para o sistema de arquivos do AdvPL no Linux.

  • Todos os arquivos do ambiente, a partir do RootPath, devem estar em capitulação baixa — isto é, em letras minúsculas — tanto no nome como na extensão, inclusive todas as pastas e/ou diretórios do ambiente devem estar com letras minúsculas. Mesmo que o seu fonte AdvPL passe como parâmetro para as funções de arquivo um nome com letras maiúsculas, internamente ele será convertido para minúsculas caso a plataforma seja Unix-Like (Linux).
  • No caso de utilização de serviços de Application Server em máquinas diferentes, para balanceamento de carga, por exemplo, o nome do RootPath do ambiente deve ser o mesmo para todas as instâncias, em todas as máquinas. Por exemplo, na máquina onde são gravados os arquivos  dentro o RootPath do ambiente, o RootPath é “/totvs/protheus12/envtop”. Este deve ser também o RootPath do ambiente em outras máquinas. A diferença para uma máquina “Slave” é que a pasta “envtop” vai existir como um ponto de montagem Client de compartilhamento de rede NFS (Network File System), que aponta para a pasta “/totvs/protheus12/envtop” da máquina principal.
  • No caso da utilização de mais de um serviço do Protheus compartilhando o mesmo RootPath de ambiente, mesmo que estes serviços estejam na mesma máquina, ‘é necessário haver a configuração de um “LockServer Protheus” — Configurar um serviço dedicado a obtenção de bloqueios exclusivos (Locks() para criação e abertura de arquivos, onde todos os Serviços de Protheus que acessam este RootPath devem apontar para um único LockServer.
  • A necessidade de uso de um Application Server Protheus configurado como LockServer no Linux existe para justamente emular o controle de acesso exclusivo a arquivos, que o Linux não possui nativamente, mas vários programas em AdvPL contam com este comportamento.
  • Mesmo que em Linux eu não tenha o conceito de “unidade de disco”, a forma de eu indicar que um arquivo deve ser aberto pelo SmartClient continua a mesma — informar uma unidade de disco no path do arquivo a ser acessado. Caso o SmartClient em uso seja Linux ou Mac OS, a unidade de disco informada será ignorada, e o arquivo será localizado pelo path e nome informados.
  • Mesmo que você tenha escrito o seu programa AdvPL usando as barras separadoras de arquivo para a esquerda “\”, as funções básicas da linguagem que realizam acesso a disco foram preparadas para converter automaticamente para “/”  caso a plataforma em uso seja Unix-like (como Linux e MAC OS).
  • Caso exista alguma diferença não tratada, ou seja necessário um tratamento diferenciado durante a execução de uma aplicação AdvPL, onde exista mudança ou indisponibilidade do recurso em algum sistema operacional ou plataforma, existem funções de diagnóstico no AdvPL que retornam por exemplo se o Application Server é Linux ou Windows, e qual é o SmartClient que está sendo usado — Linux, Windows ou MAX OS. Veja informações adicionais das referências, no final do post.
  • A lista de códigos de erro da função FError() possui uma numeração própria, diferente dos códigos de erro de manipulação de arquivos do Sistema Operacional em questão. Essa lista está documentada na própria função FError() na TDN.

Fonte de Testes

No post anterior, para cada função testada foi feita uma parte de um código, realizando as operações em sequência. Veja abaixo o fonte de testes e demonstração das funções de baixo nível de arquivo unificado — fonte tstfile.prw

#include 'protheus.ch'
#include 'fileio.ch'

USER Function TSTFILE()
Local nHnd 
Local cFile := '\temp\teste.txt'
Local cLine , nTamFile
Local cNewLine , nRead, nWrote

// Cria o arquivo 
// Automaticamente o arquivo é aberto em modo exclusivo para gravação
nHnd := fCreate(cFile)

If nHnd == -1
  MsgStop("Falha ao criar arquivo ["+cFile+"]","FERROR "+cValToChar(fError()))
  Return
Endif

// Cria uma linha em memória
cLine := "Olá sistema de arquivos" + CRLF

// Grava três linhas iguais, com 25 bytes cada 
fWrite(nHnd,cLine)
fWrite(nHnd,cLine)
fWrite(nHnd,cLine)

// Fecha o arquivo 
fClose(nHnd)

// Abre novamente para leitura e ecrita em modo exclusivo 
nHnd := fOpen(cFile,FO_READWRITE + FO_EXCLUSIVE )
If nHnd == -1
  MsgStop("Falha ao abrir ["+cFile+"]","Ferror " + cValToChar(fError()) )
  Return
Endif

// Identifica o tamanho do arquivo 
nTamFile := fSeek(nHnd,0,FS_END)
fSeek(nHnd,0,FS_SET)

conout("Tamanho do Arquivo = "+cValToChaR(nTamFile)+" byte(s)." )

// Lê a primeira linha do arquivo 
cBuffer := ""
nRead := FRead( nHnd, @cBuffer, 25 )
conout("Byte(s) lido(s) : "+cValToChar(nRead))

If nRead < 25
  MsgStop("Falha na leitura da primeira linha.","Ferror "+cValToChar(ferror()))
  Return
Endif

// Agora vamos trocar a segunda linha 
cNewLine := replicate('-',23)

nWrote := fWrite(nHnd , cNewLine )
conout("Byte(s) gravado(s) : "+cValToChar(nWrote))

If nWrote < 23
  MsgStop("Falha na gravação da segunda linha.","Ferror "+cValToChar(ferror()))
  Return
Endif

// fecha o arquivo 
fClose(nHnd)

Return

Este fonte está disponível para download no GITHUB.

Conclusão

Com este post, passamos a abranger praticamente todos os detalhes da implementação de baixo nível de arquivos do AdvPL. Ainda não testei este fonte no Linux, porém dada a natureza da implementação, o comportamento esperado é o mesmo.

Novamente agradeço a audiência, e desejo a todos TERABYTES DE SUCESSO 😀

Referências

 

 

 

 

 

 

Resolvendo o limite da função Randomize()

Introdução

No post Boas Práticas de Programação – Código Simples, Resultados Rápidos, eu mencionei um limite operacional da função Randomize() do AdvPL. Caso a diferença entre o maior e o menor número a ser sorteado for maior que 32767, a função vai sortear um número maior ou igual ao número inicial informado, e menor que o número inicial mais 32767.

Programa original

A chamada no fonte original era para sortear um número entre 100000 e 999999. Porém, devido a limitação operacional da função Randomize(), o maior resultado seria sempre menor que 132767.

nTmp := Randomize(100000,999999)

Para resolver esta limitação da função, podemos criar uma segunda função de sorteio, onde podemos tratar este limite de uma forma elegante e performática. Vamos criar a USER Function Randomic(). 

USER Function Randomic(nMin,nMax)
Local nDiff := nMax-nMin
Local nDec := 0
If nDiff < 32766
   Return Randomize(nMin,nMax)
Endif
While nDiff > 32765
   nDiff /= 10 
   nDec++
Enddo
nTmp := randomize(0,int(nDiff))
While nDec > 0 
   nTmp *= 10 
   nTmp += randomize(0,10) 
   nDec--
Enddo
Return nMin+nTmp

Como a U_Randomic() funciona

Inicialmente, determinamos usando a variável local nDiff qual é a diferença do maior para o menor número. Caso a diferença seja suportada pela função Randomize(), retornamos direto a chamada para a função Randomize(), passando os parâmetros originais.

Caso a diferença não seja contemplada pela função Randomize(), dividimos a diferença por 10 até que ela entre dentro do intervalo. Cada divisão realizada incrementa uma unidade na variável local nDec, que indica quantas casas da diferença original foram reduzidas. Uma vez que a diferença se enquadre nos valores suportados, chamamos a função Randomize(), para sortear um número entre 0 e o valor inteiro da diferença apurada após as divisões.

Sorteado este número, agora precisamos sortear mais alguns números para completar as casas decimais que foram “cortadas” da diferença original. A cada iteração, para quantas casas decimais foram cortadas — valor guardado na variável nDec — o valor anteriormente sorteado é multiplicado por 10, e um novo valor sorteado entre 0 e 9 é adicionado ao resultado, decrementando uma unidade em nDec. Terminado o processo, o número final sorteado será o número inicial somando com o número  final sorteado, armazenado em nTmp.

Exemplo

Vamos rodar o programa de testes abaixo, chamando uma vez a função U_RANDOMIC() usando o IDE/TDS em modo de depuração.

User Function TstRand()
Local nRand
nRand := U_Randomic(100000,1000000)
conout(nRand)
Return

Ao entrarmos na função U_Randomic(), a diferença entre o numero final e o inicial será de 900000 (novecentos mil). Ao passar pelo primeiro loop, a primeira divisão por 10 faz o número baixar para 90000 (noventa mil), e nDec é incrementado para uma unidade. Como o número ainda é maior que 32765, este loop é executado novamente, onde nDiff baixa agora para 9000 (nove mil) e nDec é incrementado para 2. Como agora nDiff é menor que 32765, o programa continua.

O sorteio do novo número, a ser armazenado na variável nTmp, será feito usando a função Randomize(), informando o valor mínimo 0 e o máximo 9000. Durante a depuração, o número sorteado por exemplo foi 1271.

Agora, como houve a redução de duas casas decimais, o próximo loop será executado duas vezes. Na primeira execução, o número nTmp é multiplicado por 10 — resultando em 12710 — e um novo dígito entre 0 e 9 será sorteado e acrescentado em nTmp. foi sorteado o número 7, e acrescentado em nTmp, fazendo seu valor atual ser 12717. Na segunda execução, este valor foi multiplicado novamente por 10 — resultando em 127170 — e um novo número foi sorteado e acrescentado — foi sorteado o número 4, e nTmp passou a conter o valor 127174.  Nas duas execuções, nDec foi decrementado duas vezes, voltando a ser 0 (zero). Pronto, na ultima linha, acrescentamos o número inicial (100000) ao número sorteado (127174), resultando no número 227174.

Da forma que a função foi escrita, ela torna muito rápido e seguro o processo de sorteio, não havendo chance do valor sorteado ser maior que o especificado como parâmetro para a função.

Operadores especiais utilizados

Quando queremos realizar uma operação aritmética com uma variável, e queremos atualizar esta mesma variável com o valor resultante da operação, normalmente utilizamos uma sintaxe como:

variavel := variavel <operador> <operando>

Por exemplo:

nVar := nVar * 10 
nVar := nVar + 10 
nVar := nVar + 1

Em AdvPL, existem operadores compostos  especiais, que ao mesmo tempo fazem a atribuição e a operação aritmética. Por exemplo, as operações acima podem ser escritas da seguinte forma:

nVar *= 10
nVar += 10 
nVar++

Os operadores +=-=*=  e  /= são binários — exigem dois argumentos, a variável do lado esquerdo que será usada como base para a operação e receberá o resultado, e a expressão do lado direito (variável ou constante), que será utilizada para realizar a operação. Respectivamente são os operadores de soma (+), subtração (-), multiplicação (*) e divisão (/). Já os operadores ++ e — são unários — têm apenas um argumento, que é a variável em questão, e respectivamente somam ou subtraem o valor 1 da variável informada.

Conclusão

Mesmo que alguma função básica da linguagem AdvPL possua alguma restrição operacional, o conhecimento das demais funcionalidades e capacidades da linguagem podem tornar fácil uma implementação de um novo recurso que atenda a sua necessidade.

Agradeço novamente a audiência, e desejo a todos TERABYTES DE SUCESSO !!! 😀

Referências