Classe ZDBFTABLE – Implementação de Filtro AdvPL

Introdução

Já que a classe ZDBFTABLE permite a navegação em uma tabela DBF, vamos ver como seria implementar um filtro ? E ver como ele funciona por dentro.

Filtros de dados em xBASE / Clipper

Quando se trabalha diretamente com o arquivo DBF diretamente, sem ter um SGDB ou um programa intermediário de gerenciamento, a implementação de um filtro é uma forma de criar uma expressão usando campos da tabela e operadores lógicos, que retorne .T. (Verdadeiro) caso o registro deva ser considerado nas operações de navegação da tabela e posicionamento de registros. Para isso são usados os comandos SET FILTER ou a função DBSetFilter().

A expressão de filtro em AdvPL informada é traduzida para um Codeblock, que passa a ser executado quando você por exemplo faz um DBSkip(). Ao ler o próximo registro a ser posicionado, caso exista uma condição de filtro especificada, internamente o driver executa o Codeblock, e caso o mesmo retorne .F., o driver avança mais um registro e repete a operação até encontrar o primeiro registro que atenda a condição de filtro ou a tabela chegue ao final (EOF).

Método DBSetFilter

METHOD DBSetFilter( cFilter ) CLASS ZDBFTABLE
Local aCampos := {}
Local cTemp
Local nI, nPos

// Cria lista de campos 
aEval( ::aStruct , {|x| aadd(aCampos , x[1]) } )

// Ordena pelos maiores campos primeiro 
aSort( aCampos ,,, {|x,y| alltrim(len(x)) > alltrim(len(y)) } )

// Copia a expressao filtro
cTemp := cFilter

// Troca os campos por o:Fieldget(nCpo)
// Exemplo : CAMPO > 123 será trocado para o:FieldGet(1) > 123

For nI := 1 to len(aCampos)
	cCampo := alltrim(aCampos[nI])
	nPos := ::Fieldpos(cCampo)
	cTemp := Strtran( cTemp , cCampo,"o:FieldGet(" +cValToChar(nPos)+ ")")
Next

// Monta a string com o codeblock para filtro 
cTemp := "{|o| "+cTemp+"}"

// Monta efetivamente o codeblock 
::bFilter := &(cTemp)

Return

A implementação foi feita de forma similar ao comportamento original do DBF com Clipper ou mesmo com AdvPL. Porém, como eu não tenho um parser léxico para destrinchar a expressão de filtro, eu fiz da forma mais simples: Primeiro, qualquer campo da tabela atual usado na expressão de filtro deve ser escrito em letras maiúsculas.

Então, eu crio a lista de campos da tabela baseado na estrutura, ordeno a lista pelos campos com o maior nome, para depois trocar cada ocorrência de campo por uma chamada do método Fieldget() do campo, já passando o número do campo da estrutura como parâmetro. Os campos tem que ser ordenados pelo maior nome primeiro, pois eu posso ter por exemplo dois campos com o mesmo começo no nome : XIS e XIS2. Eu devo começar a troca sempre pelos campos de maior nome.

E, finalmente, eu crio um Codeblock com a expressão resultante, recebendo em “o” a instância do objeto da tabela, e nos métodos de navegação — DbSkip(), DBGoTop() e DBGoBottom() — eu passo a verificar se o registro deve ser considerado ou não, fazendo um Eval() do filtro informando o “self” — minha própria instância — como parâmetro.

Por exemplo, imagine que eu sete a condição de filtro “!Empty(X3_CBOX)”, vamos ver como ficaria o Codeblock resultante:

cFilter --> "!Empty(X3_CBOX)"
cTemp   --> "{|o| !Empty(o:FieldGet(28))}"

Método DBSkip()

METHOD DbSkip( nQtd ) CLASS ZDBFTABLE 
Local lForward := .T. 

If nQtd  == NIL
	nQtd := 1
ElseIF nQtd < 0  	lForward := .F.  Endif // Quantidade de registros para mover o ponteiro // Se for negativa, remove o sinal  nQtd := abs(nQtd) While nQtd > 0 
	If lForward
		IF ::_SkipNext()
			nQtd--
		Else
			// Bateu EOF()
			::_ClearRecord()
			Return
		Endif
	Else
		IF ::_SkipPrev()
			nQtd--
		Else
			// Bateu BOF()
			Return
		Endif
	Endif
Enddo

// Traz o registro atual para a memória
::_ReadRecord()

Return

O método DBSkip em si ficou simples. Ele recebe como parâmetro o número de registros que devem ser movimentados. Porém, em caso de filtro, eu preciso contar que as movimentações foram feitas apenas com registros válidos. Desse modo, quem faz o controle de navegação e filtro são os métodos internos _SkipNext e _SkipPrev.

Método interno _SkipNext

METHOD _SkipNext() CLASS ZDBFTABLE
Local nNextRecno

While (!::lEOF)

	// Parte do registro atual , soma 1 
	nNextRecno := ::Recno() + 1 

	// Passou do final de arquivo, esquece
	If nNextRecno > ::nLastRec
		::lEOF := .T.
		::_ClearRecord()
		Return .F. 
	Endif

	// ----------------------------------------
	// Atualiza o numero do registro atual 
	::nRecno := nNextRecno

	// Traz o registro atual para a memória
	::_ReadRecord()

	// Passou na checagem de filtro ? Tudo certo 
	// Senao , continua lendo ate achar um registro valido 
	If ::_CheckFilter()
		Return .T. 
	Endif

Enddo

Return .F.

Como a navegação (por enquanto) não usa índice, o próximo registro sempre será o atual mais um. Então, a função incrementa o registro, verifica se não atingiu EOF, lê o registro para a memória, e então verifica se o registro não está filtrado, usando o método interno _CheckFilter(). — Calma que a gente já chega nele .. é o próximo.

Método interno _CheckFilter

METHOD _CheckFilter() CLASS ZDBFTABLE
Local lOk := .T. 
If ::bFilter != NIL 
	lOk := Eval(::bFilter , self )	
Endif
Return lOk

O filtro setado foi guardado na propriedade ::bFilter. Caso ela não seja NIL, existe um filtro setado. Ao fazer o Eval() da condição de filtro, se o registro não atende a condição, o método _SkipNext() continua lendo os próximos registros até encontrar um registro válido.

Desempenho de filtro

Como a verificação de registro válido no filtro somente é realizada após o registro ser lido, durante o posicionamento. quando mais simples a expressão de filtro, mais rápida será a validação da visibilidade do registro. E, no momento de navegar pela tabela, quanto maior for a tabela, e quanto menos registros atenderem a condição de filtro, mais registros serão lidos na navegação para encontrar um registro válido.

Por exemplo, imagine uma tabela com 100 mil registros, onde você vai realizar um determinado processamento filtrado. Você seta o filtro, faz um DBGoTop() e um While !Eof() — DBSkip(). Mesmo que na sua tabela existam por exemplo apenas mil registros que atentam a condição de filtro, os 100 mil registros serão lidos e verificados se eles fazem parte do filtro ou não. Logo, cuidado com filtros e tabelas grandes.

Comportamento do filtro ISAM

Ao setar um filtro, o registro atualmente posicionado não é alterado, mesmo que ele não faça parte do filtro. Logo, normalmente reposicionamos a tabela no primeiro registro válido contemplando o filtro usando a função DBGoTop() — ou no nosso caso da classe ZDBFTABLE, o método DBGoTop().

Todas as funções de navegação ISAM ( DbGoTop(), DBGoBottom(), DBSkip() — e inclusive a DBSeek() — respeitam a condição de filtro. A única função que não respeita nada é a DBGoto(). Uma vez que eu posicione diretamente em um registro pelo seu número de RECNO, ele será lido e posicionado, mesmo que esteja deletado ou que não faça parte da seleção do filtro. Uma vez posicionado neste registro, um próximo DbSkip() vai posicionar no registro imediatamente posterior, considerando o filtro setado.

Para limpar o filtro, usamos o método DbClearFilter(), que também não mexe no posicionamento da tabela, apenas coloca NIL na propriedade onde têm o Clodeblock do filtro.

Conclusão

O fonte atualizado sempre está no GITHUB. Entre um post e outro sempre têm coisas novas. O uso é livre, fique a vontade !!!

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

 

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Google

Você está comentando utilizando sua conta Google. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s