JSON em AdvPL e TLPP

Introdução

O primeiro post sobre JSON (JavasSript Object Notation) aqui no Blog já faz um bom tempo, foi em Outubro de 2019, e mesmo na época existiam recursos que eu desconhecia. E de lá para cá, esse novo tipo nativo do AdvPL está sendo muito utilizado, mesmo em programas que não tem nenhuma relação com integrações com REST, por ser um objeto prático, rápido, e flexível.

De qualquer modo, a postagem atual não vai repetir toda a introdução ao tipo e suas funcionalidades, mas sim atualizar e complementar as informações dos posts anteriores sobre esse assunto, que servem de base:

JSON no TLPP

Um novo objeto do tipo “J” (JSON), tanto em AdvPL como em TLPP, pode ser criado simplesmente usando o construtor do objeto e atribuindo o resultado a uma varíavel:

oJson1 := JsonObject():New()

Uma vez feito isso, podemos popular o objeto em memória a partir de uma string contendo o objeto, ou criar cada novo par de chave X valor usando por exemplo a sintaxe abaixo:

oJson1["Id"] := '001'
oSon1["Chave"] := "Valor"

O mesmo resultado seria também obtido usando o código abaixo:

oJson := JsonObject():New()
oJson:fromJson('{ "ID" : "01" , "Nome" : "Usuário"}')

Agora, o que eu não sabia, é que você pode criar um Objeto Json, inclusive com conteúdo, diretamente em um fonte TLPP, sem o construtor, diretamente através de uma string Json, sem chamar o construtor e o parser.

Se você ainda não sabe ou têm dúvidas sobre o que é o TLPP, dê uma lida na documentação oficial da TOTVS, disponivel aqui. Em breve teremos mais posts aqui dedicados às novidades do TLPP.

Por exemplo, para criar o mesmo objeto JSON do exemplo acima, é possível usar a seguinte sintaxe — dentro de um fonte com extensão TLPP:

oJson := { "ID" : "01" , "Nome" : "Usuário"}

O resultado final será o mesmo: Será criado um objeto JSON, com as propriedades “ID” e “Nome”, respectivamente com os conteúdos “01” e “Usuário”.

Estendendo o exemplo acima, imagine que os valores “01” e “Usuário” estão dentro de duas variáveis do AdvPL, cID e cNome, respectivamente. É possível especificar o valor dinamicamente simplesmente usando a variável, por exemplo:

User Function Json01()
Local oJson 
Local cId := '01'
Local cNome := 'Usuário'

oJson :=  { "ID" : cId , "Nome" : cNome }

msgInfo(oJson:ToJson(),"RESULTADO")

Return 

Vale lembrar que, usando a linguagem AdvPL, a linha de código pode ser estendida para a linha de baixo, colocando ‘;’ ponto-e-vírgula no final da linha, então você pode deixar o código mais “legível”, colocando por exemplo apenas duas ou três propriedades em cada linha.

A interpretação de JSON nativo literal somente é compreendida e interpretada em um fonte de extensão TLPP. Se isso for usado dessa forma em um fonte AdvPL, com as extensões padrão (PRG,PRW ou PRX), será exibido um erro de compilação:

Invalid use of Json Syntax in ADVPL source file

NIL x NULL

Uma variável sem valor no AdvPL, é uma variavel nula, ou “NIL”. De forma similar, uma propriedade de um determinado objeto JSON pode existir, mas não ter conteúdo, ou melhor, ter conteúdo “null”. Logo, uma string JSON com uma propriedade “null”, será representado em Advpl com o valor NIL. E vice-versa.

User Function Json02()
Local oJson 
Local cId := '01'

oJson :=  { "ID" : cId , "Nome" : NIL }

msgInfo(oJson:ToJson(),"RESULTADO")

Return 

/*
O resultado em tela será a seguinte string :

         {"ID":"01","Nome":null}

*/

Ordem das Propriedades

Até o APPServer Build 20.3.0.x , ao criar um objeto JSON, seja criando ele vazio e depois criando e populando suas propriedades, ou criando ele a partir de uma representação JSON em String (caractere), ao exportar novamente o objeto da memória para uma nova String, a ordem das propriedades do objeto não respeitava a ordem de criação das propriedades.

Isso na prática não muda nada no USO do objeto, ele ainda vai conter as propriedades e valores, mas em uma ordem arbitrária, o que dificultava a leitura do objeto. O Primeiro exemplo do post JSON – O que é e como usar em AdvPL – Parte 02 já mostrava isso.

A partir do AppServer Build 20.3.1.0 (Notas de Release AQUI), foi introduzida uma ordenação natural do objeto na memória, partindo da ordem de criação de suas propriedades. Então, ao ler um objeto JSON em String, a ordem é mantida na exportação do objeto, e se ele for manipulado, e novas propriedades forem acrescentadas ao objeto, elas sempre virão no final da string, na ordem em que elas foram criadas. Uma propriedade alterada terá apenas o seu valor alterado, mas a ordem será mantida. Se uma propriedade for removida, e então acrescentada novamente, ela virá para o final.

Dicas e Cuidados

Se eu precisar identificar se um determinado objeto JSON possui uma determinada propriedade, o recomendável é sempre usar o método HasProperty()

Afinal, se eu simplesmente verificar se uma propriedade têm algum conteúdo, existem duas condições diferentes com o mesmo retorno: oJson[“abc”] pode retornar NIL caso a propriedade abc não exista, e também pode retornar NIL caso a propriedade exista, mas seu valor seja NIL.

E, a segunda consideração importantíssima: Ao usar a notação oJson[“propriedade”], mesmo que seja apenas uma consulta, isso cria no objeto a propriedade consultada, com valor NIL. Se o seu programa não vai montar novamente a representação do objeto em String, não há maiores problemas. Mas se este for o caso, cada propriedade consultada que não existia, passa a existir com valor “null” na memória, veja o exemplo abaixo:

User Function Json03()
Local oJson 
Local cId := '01'
Local cNome := 'Usuário '

oJson :=  { "ID" : cId , "Nome" : NIL }

if empty(oJson["Idade"])
   // eu estou "consultando" a propriedade "Idade" 
   MsgInfo("Idade nao encontrada.","ATENÇÃO")
endif

// Agora veja o que acontece com o objeto
// ao exportar para string novamente 
// A propriedade "Idade" passou a existir

msgInfo(oJson:ToJson(),"RESULTADO")

Return 

O resultado da execução dessa função será primeiro a mensagem que a Idade não foi encontrada, e logo abaixo, a exportar oJson para string novamente, repare que a propriedade “Idade” passou a existir, e com valor “null”

Dito isso, para evitar a criação acidental de propriedades dentro do objeto, se a intenção é apenas verificar se a propriedade existe, procure usar o método HasProperty().

JSON como parâmetro de função

Da mesma forma que um Objeto Advpl (ValueType = “O”) ou um Array (ValueType=”A”), uma variável contendo um objeto JSON (ValueType=”J”), quando informado como parâmetro de uma função, implicitamente passa uma REFERÊNCIA desse objeto. Logo, uma função que recebe uma variavel JSON como parâmetro, pode alterar valores de propriedades, adicionar ou remover propriedades, e isso vai refletir na variável do fonte onde ela foi criada, pois o objeto não foi clonado ou duplicado na memória ao ser passado como parâmetro.

CP1252, UTF8 , Unicode ?

Então, a especificação do formato JSON diz que os caracteres devem ser Unicode, isto é, permitir tudo que é letra de qualquer idioma. A troca de dados dados em ecossistemas normalmente usa a codificação UTF-8, e adicionalmente um caractere também pode usar um escape sequence “\uXXXX”, onde XXXX é o código hexadecimal do caractere na tabela UNICODE.

O mecanismo do AdvPL suporta UTF8, e não faz nenhuma conversão implícita. Isto é, se você está lendo uma string JSON do disco, ou recebeu ela de um REST, onde o nome “Usuário” foi codificado em UTF-8, essa string no Advpl vai ter 8 caracteres, pois no lugar da letra “ú” (u minúsculo com acento agudo), são lidos dois bytes, que representam essa letra na codificação UTF-8 ( ASCII 195 e ASCII 161, respectivamente). Se você precisa dessa string em um codepage CP1252 por exemplo, e essa string contém caracteres que podem ser representados nesse codepage, você deve decodificar a informação usando por exemplo a função DecodeUTF8() do AdvPL

A única conversão implícita feita no parser é automaticamente converter caracteres especificados usando a escape de unicode “\xFFFF” vindas em uma string JSON, para a codificação UTF-8.

Bateu aquela dúvida sobre codificação, encoding e afins ? Têm uma sequência de posts que fala sobre isso:

Conclusão

Como a implementação do JSON precede os recursos do TLPP, seus fundamentos e métodos estão documentados no TDN, dentro das classes AdvPL: https://tdn.totvs.com/display/tec/Classe+JsonObject

A Linguagem AdvPL não parou no tempo, muito pelo contrário, ela está trazendo aos poucos tecnologias, sintaxes e APIS, estendendo mais as capacidades da linguagem. O respeito ao legado e comportamentos esperados são o maior desafio nesse quesito, e é nesse ponto que entra o TL++ (ou TLPP). E isso defintivamente vai render uma sequência de novos posts 😀

E, como de costume, desejo sempre a todos TERABYTES DE SUCESSO !!!

Referências

Todas as referências desse post apontam para a documentação oficial do ADVPL e TLPP, disponibilizada com acesso público no site TDN – Totvs Developer Network, e os posts anteriores sobre JSON e Encoding publicados aqui no Blog.

Só pra abrir o apetite

Faz tempo que não publico nada por aqui. Dividir e organizar o tempo entre trabalho, família e petz, customizar a motoca, afazeres diários e afins, é realmente um desafio. Mas, depois de um hiato de quase um ano, pretendo voltar a postar novos conteúdos com frequência.

Existe bastante informação na Internet sobre o ERP Protheus, rotinas do padrão, customizações e afins, fóruns de trocas de informação entre desenvolvedores, mas pouco sobre os fundamentos da linguagem, e muita demanda por profissionais no mercado que tenham uma base sólida de conhecimento.

Funções, classes, algoritmos, protocolos e especificações, bibliotecas de funções, APIS para todos os gostos, Frameworks de todos os sabores, cada vez mais linguagens de programação, processadores e plataformas, sistemas operacionais, é um universo em constante expansão.

Sabe o que não mudou nas últimas 8 (oito) décadas, desde o primeiro computador eletrônico (valvulado), até hoje ? Um BIT ainda pode ter 2 valores, 0 (zero) ou 1 (um) — Desconsidere o computador quântico, isso tá pano para outro(s) post(s).

Desde os primeiros microprocessadores, qualquer programa escrito em qualquer linguagem é convertido internamente para “código de máquina”, são instruções codificadas em sequencias de bits. Um microprocessador, não importa a arquitetura, marca, modelo ou plataforma, precisa e usa um barramento de dados, um barramento de endereços, uma memória volátil, e um conjunto específico de instruções, para comunicação de entrada e saída com outros dispositivos (também conhecidos como periféricos). O que mais se aproxima de um código de máquina é a linguagem ASSEMBLY.

Essa é a base de tudo. Em cima dela, as ultimas décadas foram marcados por processadores com mais transístores por milimetro quadrado, memórias intermediárias internas, armazenamentos magnéticos e SSD (discos de estado sólido), instruções mais simples (RISC) ou mais complexas ( CISC), maior velocidade (Clock), processamentos assíncronos, processamento vetorial (SIMD), mas a BASE continua a mesma.

Containeres, máquinas virtuais, Kubernetes e Docker, são mais camadas para segmentar e compartimentalizar um equipamento, para atender a demanda de sistemas criados com fudamentos diferentes — como microserviços. Mas a BASE é a mesma.

Filas, pilhas e listas não são recursos novos das linguagens novas, são implementações de algoritmos e funcionalidades que são usadas para construir sistemas para atender melhor a demanda e necessidade de organização de processamento. Orientação a objetos existe desde os anos 60, e isso é explorado desde então por inúmeras linguagens, até mesmo em um nível de abstração para gerenciamento de projetos. Sim, gerenciamento de projetos moderno segue uma lógica de orientação a objetos e eventos.

Enfim, longo é o caminho do desenvolvedor, para compreender e assimilar esse universo em expansão. E, se meus posts podem ajudar nessa jornada, em breve o blog trará novidades.

E, como sempre, desejo a todos de coração TERABYTES de sucesso !!!!

Referências

Number Sums | Implementação de Algoritmo | Parte 04

No post anterior (Number Sums | Implementação de Algoritmo | Parte 03), o algoritmo foi totalmente implementado, e depois de alguns ajustes finais, o miolo do programa já está operacional. Agora, vamos ver como podemos implementar uma interface em AdvPL para ser possivel fazer a entrada de dados do grid a ser resolvido 😀

User Function NumberSum()
	Local cTitle := 'NumberSum - Interface'
	Local nLinhas := 6
	Local nColunas := 6

	DEFINE DIALOG oDlg TITLE (cTitle) ;
		FROM 0,0 TO 240,240  PIXEL

	@ 14,05 SAY "Linhas" SIZE 60,12 OF oDlg  PIXEL

	@ 12,50 GET nLinhas PICTURE "99" SIZE 30,12 OF oDlg  PIXEL

	@ 28,05 SAY "Colunas" SIZE 60,12 OF oDlg  PIXEL

	@ 26,50 GET nColunas PICTURE "99" SIZE 30,12 OF oDlg  PIXEL

	@ 44,05  BUTTON oBtn1 PROMPT "Proximo" SIZE 60,15 ;
		ACTION (GuiData(nLinhas,nColunas)) OF oDlg  PIXEL

	ACTIVATE DIALOG oDlg CENTER

return

Primeiro passo, vamos criar uma caixa de diálogo, para perguntar o tamanho do Grid de dados que será recebido. O jogo Number Sums usa grids de tamanho 6×6, 7×7 ou 8×8. Como eu somente descobri isso depois de resolver várias fases, fiz uma interface genérica, que pergunta quantas linhas e quantas colunas tem o grid a ser resolvido. Agora, vamos implementar a função que vai fazer efetivamente a entrada de dados do Grid.

static function GuiData(nLinhas,nColunas)
  Local oDlg2
  Local nC , nL , x := 0
  Local cTitle := 'Informe os valores'
  Local aGuiData := {}
  Local aGuiCols := {}
  Local aGuiRows := {}

  For nL := 1 to nLinhas
    aadd(aGuiRows,0)
  Next
  For nC := 1 to nColunas
    aadd(aGuiCols,0)
  Next
  For nL := 1 to nLinhas
    aadd(aGuiData,aclone(aGuiCols))
  Next

  DEFINE DIALOG oDlg2 TITLE (cTitle) ;
    FROM 0,0 TO 480,480  PIXEL

  // Input dos valores das somas das colunas 
  For nC := 1 to nColunas

    @ 02, (nC*20)+5 GET oGet VAR x PICTURE "999" SIZE 10,12 OF oDlg2  PIXEL
    oGet:bSetGet := &('{|x| if(pcount()=0, aGuiCols['+cvaltochar(nC)+'] ,  aGuiCols['+cvaltochar(nC)+'] := x )}')

  Next

  // Input das linhas 
  For nL := 1 to nLinhas

    // A primeira coluna, mais à esquerda, recebe o valor esperado da soma 
    @ nL*20, 1 GET oGet VAR x PICTURE "999" SIZE 10,12 OF oDlg2  PIXEL
    oGet:bSetGet := &('{|x| if(pcount()=0, aGuiRows['+cvaltochar(nL)+'] ,  aGuiRows['+cvaltochar(nL)+'] := x )}')

    // As demais colunas são os valores daquela linha 
    For nC := 1 to nColunas

      @ nL*20, (nC*20)+5 GET oGet VAR x PICTURE "999" SIZE 10,12 OF oDlg2  PIXEL
      oGet:bSetGet := &('{|x| if(pcount()=0, aGuiData['+cvaltochar(nL)+']['+cvaltochar(nC)+'] ,  aGuiData['+cvaltochar(nL)+']['+cvaltochar(nC)+'] := x )}')

    Next

  Next

  @ nL*20,05  BUTTON oBtn1 PROMPT "Resolver" SIZE 60,15 ;
    ACTION (SolveGrid(aGuiData,aGuiCols,aGuiRows), oDlg2:Refresh()) OF oDlg2  PIXEL

  ACTIVATE DIALOG oDlg2 CENTER

return

Neste código existe um truque com a manipulação de valores. Como a interface é dinâmica, e originalmente o construtor do componente tGET recebe o nome de uma variável, e monta dinamicamente um bloco de codigo para fazer o “meio de campo” da consulta e atribuição de valores, conheciso por bSetGet, nao é possível usar uma variável como indice de array para montar cada componente. Então, todos os GETs sao montados para a variável X, e após o componente ser construído, é usada macro-execução para criar um novo codeblock de SET/GET para cada GET, para alimentar e consultar diretamente o elemento do array desejado.

Desse modo, após a entrada dos dados, o array aGuiCols deve conter os elementos com os valores de soma esperados para cada coluna, o array aGuiRows deve conter os elementos com os valores de soma esperados para cada linha, e o array aGuiData será um array de duas dimensões, onde cada elemento contém o valor da linha e coluna correspondente. Veja abaixo como fica a entrada de dados da fase usada como exemplo de resolução:

Sim, dá para fazer uma interface mais “caprichada”, eu optei pela simplicidade e não acrescentei grandes perfumarias, nem maiores validações. O exemplo serve para o propósito ..risos.. E a parte de “RESOLVER” o problema, o botão “Resolver” já chama a função anteriormente implementada — SolveGrid(aGuiData,aGuiCols,aGuiRows)

Agora, após informar os dados, e apertar o botão “RESOLVER” … magicamente, a solução do grid é calculada, e mostrada no log de console do Application Server, E TAMBÉM é mostrada na interface, nos mesmos campos de INPUT DE DADOS, onde todos os numeros removidos estão com valor ZERO, as somas com valor ZERO, e os numeros confirmados estão com os valores NEGATIVOS :

E, a pergunta de 1 milhão … COMO ISSO ACONTECEU ??

  @ nL*20,05  BUTTON oBtn1 PROMPT "Resolver" SIZE 60,15 ;
    ACTION (SolveGrid(aGuiData,aGuiCols,aGuiRows), oDlg2:Refresh()) OF oDlg2  PIXEL

A mágica está aqui , na chamada da função SolveGrid(). Os arrays de dados que contém os dados do grid, e das somas das linhas e colunas, sempre é passado implicitamente por referência na linguagem AdvPL. Durante a resolução do grid, os arrays são alterados. Toda a interface mostrada na tela faz referência a cada elemento desses arrays. Após rodar a função, e os arrays terem sido alterados, a instrução oDlg2:Refresh() faz uma atualização de todos os componentes da interface, colocando na tela os novos valores dos elementos 😀

Resolvendo um grid 8×8

Agora, usando um grid 8×8 do jogo, desafio díario de 11 de agosto, com os seguintes dados:

Após digitar os números e clicar em “Resolver”, obtemos o seguinte resultado:

Conclusão

Quer rodar esse fonte em seu ambiente ? Baixe o código completo no GITHUB, no link https://github.com/siga0984/Blog/blob/master/Numbersum.prw . Esse fonte roda em Builds Lobo Guará, Harpia, não possui dependências.

A interface pode ser melhorada, tentei manter o codigo modular e simples. Num primeiro momento, optei por criar a aplicação estruturada com funções estáticas. Em um próximo post, vou aproveitar tudo o que foi escrito, e fazer o porte do núcleo para uma classe em TLPP — agora que já temos o código em funções, e funciona, tudo fica mais fácil 😀

Espero que todos tirem o máximo de proveito desse exemplo, e desejo a todos TERABYTES DE SUCESSO 😛

Number Sums | Implementação de Algoritmo | Parte 03

Continuando …

No post anterior (Number Sums | Implementação de Algoritmo | Parte 02), estamos na etapa 02 do algoritmo — determinar as alternativas de uso dos números de um conjunto informado, para atingir um valor de soma esperado. Na análise do conjunto de numeros 6,3,2,7,7,5, onde é esperado uma seleção cuja soma resulte no número 8, foram identificadas duas possibilidades, enumeradas na forma de um “Bit Mask” de 6 posições, onde cada posição com o número1 (um) indica os números que devem ser selecionados, e as demais posições com 0 (zero) indicam numeros que não entram na operação, a seguir:

Possibilidades : 2
1. 101000
2. 010001

Agora, precisamos que o programa seja capaz de mostrar, dentre as possibilidades, quais “bits” estao ligados em todas as posibilidades, e quais estão desligados em todas as possibilidades — Isso vai nos indicar os numeros que podemos confirmar, e que podemos remover, respectivamente, mesmo que ainda não seja possível escolher uma das possibilidades 😀

Embora isso seja possível de ser feito com funções de aritmética binária, eu acho mais simples avaliar todas as posições. Temos um total de 2 possibilidades, e seis numeros do conjunto a avaliar. Vamos avaliar cada numero em cada possibilidade, usando um contador para as possibilidades de uso: se o contador de uso for igual ao numero de possibilidades, esse numero aparece em todas as possibilidades e deve ser confirmado, e se o contador de uso for igual a zero, ele nao aparece em nenhuma , e pode ser removido.

Lembrando que a função Analyze() deve ser projetada para retornar um novo conjunto de dados, então ela vai confirmar, manter ou remover numeros, acrescentando eles no retorno com valor negativo, com o valor original, ou com zero, respectivamente. Lembrando que temos tês cenários de tratamento, que são : Uma possibilidade, mais de uma possibilidade, e em caso de erro nos dados ou na logica do programa, nenhuma possibildade calculada. Com tudo isso, acrescentamos o trecho abaixo na função Analyze():

  nPossible := len(aPossible)
  conout("Possibilidades : "+cvaltochar(nPossible))
  for nI := 1 to len(aPossible)
    conout(cvaltochar(nI)+". "+aPossible[nI])
  Next

  If nPossible < 1

    MsgStop("Nenhuma possibilidade. Verifique os dados informados ")
    QUIT

  ElseIF nPossible == 1

    // So tem uma possibilidade,
    // efetiva essa possibilidade no retorno 
    // "1" confirma o numero, "0" remove
    cBitMask := aPossible[1]
    For nI := 1 to nTam
      if substr(cBitMask,nI,1) == '1'
        // Confirma o numero, tornando ele negativo 
        nValor := aData[nPos]
        nValor := ABS(nValor) * (-1)
        aadd(aNewData , nValor )
      Else
        // remove o numero, acrescentando 0 no retorno 
        aadd(aNewData , 0 )
      Endif
    Next


  ElseIF nPossible > 1

    // Mais de uma possibilidade, 
    // avalia cada numero do conjunto de possibilidades
    // verifica se existem bits que estão sempre ligados ou sempre 
    // desligados em todas as possibilidades
    For nPos := 1 to nTam
      nUso := 0
      for nI := 1 to len(aPossible)
        cBitMask := aPossible[nI]
        if substr(cBitMask,nPos,1) == '1'
          nUso++
        Endif
      Next
      nValor :=  aData[nPos]
      if nUso == len(aPossible)
        // Esta "1" em todas as possibilidades
        // Confirma o numero, tornando ele negativo 
        nValor := ABS(nValor) * (-1)
        aadd(aNewData , nValor )
      ElseIf nUso == 0
        // Está "0" em todas as possibilidades
        // remove o numero, acrescentando 0 no retorno 
        aadd(aNewData , 0 )
      Else
        // valor inalterado, mantem no retorno 
        aadd(aNewData , nValor )
      Endif
    Next

  Endif

  conout("Novo conjunto : ")
  For nI := 1 to len(aNewData)
    conout(cvaltochar(nI)+". = "+cvaltochar(aNewData[nI]))
  Next

  return aNewData

Com isso, a função consegue retornar um novo conjunto de dados, que substitui o conjunto anterior, onde cada numero confirmado está com o valor negativo, e cada numero removido vira zero. E, se nao for possivel confirmar ou remover o numero, ele é mantido com o valor original (positivo). Essa segunda parte da implementação da função Analyze() atente os itens de 2 a 7 do algotimo, definido no post anterior: Enumerar as possibilidades, confirmar e remover numeros baseado nas regras definidas, etc…

Na camada anterior, cada chamada da função Analyze() obtem o conjunto de dados do GRID para informar como parâmetro, e ao receber um novo conjunto de dados como retorno, o grid é atualizado com o novo conjunto. A lógica de resolução ainda não está totalmente fechada, MAS, com algumas alterações no fonte, para gerar um resultado de “echo” de mensagens no console, já temos um bom resultado parcial, vamos analizar o resultado abaixo:

Analizando Linha 1
Soma Esperada : 8
Conjunto :  6 3 2 7 7 5
Possibilidades : 2
1. 101000
2. 010001
Resultado :  6 3 2 0 0 5

// Na primeira linha, o algoritmo eliminou os numeros 7 da 4a e 5a colunas

Analizando Linha 2
Soma Esperada : 12
Conjunto :  8 9 6 5 6 1
Possibilidades : 3
1. 001010
2. 001101
3. 000111
Resultado :  0 0 6 5 6 1

// Na segunda linha, o algoritmo eliminou os numeros 8 e 9, da 1a e 2a colunas  

Analizando Linha 3
Soma Esperada : 18
Conjunto :  3 7 5 1 6 7
Possibilidades : 3
1. 011010
2. 110101
3. 001011
Resultado :  3 7 5 1 6 7

// Na terceira linha, nada a fazer

Analizando Linha 4
Soma Esperada : 20
Conjunto :  4 8 5 7 7 4
Possibilidades : 4
1. 011100
2. 011010
3. 101101
4. 101011
Resultado :  4 8 -5 7 7 4

// Na quarta linha, o numero 5, da 3a coluna, foi confirmado -- pois aparece em todas as possibilidades. 

Analizando Linha 5
Soma Esperada : 23
Conjunto :  8 8 7 8 3 8
Possibilidades : 6
1. 111000
2. 101100
3. 011100
4. 101001
5. 011001
6. 001101
Resultado :  8 8 -7 8 0 8

// Na quinta linha, o numero 7, da 3a coluna foi confirmado. 

Analizando Linha 6
Soma Esperada : 17
Conjunto :  9 9 7 5 4 8
Possibilidades : 3
1. 100001
2. 010001
3. 000111
Resultado :  9 9 0 5 4 -8

// Na sexta linha, o numero 7 da 3a coluna foi removido, e o numero 8 da 6a coluna foi confirmado. 

Analizando Coluna 1
Soma Esperada : 12
Conjunto :  6 0 3 4 8 9
Possibilidades : 2
1. 000110
2. 001001
Resultado :  0 0 3 4 8 9

// Quando comecamos a analisar as colunas, repare que ja estamos encontrando valores removidos pela analise das linhas. O valor 6 na 1a linha foi removido. 

Analizando Coluna 2
Soma Esperada : 19
Conjunto :  3 0 7 8 8 9
Possibilidades : 2
1. 100110
2. 101001
Resultado :  -3 0 7 8 8 9

// Na coluna 2 , o valor 3 da primeira linha foi confirmado.

Analizando Coluna 3
Soma Esperada : 23
Conjunto :  2 6 5 -5 -7 0
Possibilidades : 2
1. 011110
2. 011111
Resultado :  0 -6 -5 -5 -7 0

// NA coluna 3, ja haviam dois valores confirmados, agora temos mais duas confirmações e um valor removido. 

Analizando Coluna 4
Soma Esperada : 20
Conjunto :  0 5 1 7 8 5
Possibilidades : 2
1. 010110
2. 000111
Resultado :  0 5 0 -7 -8 5

// Na 4a coluna, dois valores confirmados e um removido.

Analizando Coluna 5
Soma Esperada : 6
Conjunto :  0 6 6 7 0 4
Possibilidades : 6
1. 010000
2. 001000
3. 01001
4. 00101
5. 01001
6. 00101
Resultado :  0 6 6 0 0 0

// Na 5a coluna, mais dois valores removidos.

Analizando Coluna 6
Soma Esperada : 18
Conjunto :  5 1 7 4 8 -8
Possibilidades : 2
1. 110110
2. 110101
Resultado :  -5 -1 0 -4 8 -8

// E na 6a coluna, 3 valores confirmados e um removido. 

Ao final de uma avaliação de todas as linhas e colunas, nessa ordem, ainda não chegamos a um resultado final, mas um resultado parcial — e chegamos a esse resultado em uma ordem diferente da ordem que fizemos manualmente, pois aqui seguimos a definição ordenada de avaliar primeiro os conjuntos de dados de todas as linhas e depois de todas as colunas. Apos criar uma função para imprimir o Grid no inicio do processo, e no final do processo com o programa atual, temos o seguinte resultado :

Grid Inicial :
    6    3    2    7    7    5
    8    9    6    5    6    1
    3    7    5    1    6    7
    4    8    5    7    7    4
    8    8    7    8    3    8
    9    9    7    5    4    8

Grid Final :
    0   -3    0    0    0   -5
    0    0   -6    5    6   -1
    3    7   -5    0    6    0
    4    8   -5   -7    0   -4
    8    8   -7   -8    0    8
    9    9    0    5    0   -8

Todos os numeros negativos são confirmações, e todos os numeros zerados são numeros removidos. O programa até agora foi capaz de chegar neste cenário, onde 12 números foram removidos, e 11 números confirmados:

O programa ainda nao avalia automaticamente se a análise acabou, ele faz apenas uma verificação em todas as linhas e colunas … e se repetissemos esse processo, será que já teremos um resultado final? Após alterar o fonte para executar duas vezes a analise de linhas e colunas, chegamos no resultado final!

Grid Final :
    0   -3    0    0    0   -5
    0    0   -6   -5    0   -1
    0   -7   -5    0   -6    0
   -4    0   -5   -7    0   -4
   -8    0   -7   -8    0    0
    0   -9    0    0    0   -8

Sabemos que esse é o resultado final, pois não existe mais nenhum número positivo no GRID, mas somente números negativos (confirmados) e zero (removidos).

Última parte do algoritmo

No cenário de testes proposto, com um grid de números de 6×6, o programa determinou a solução em duas execuções. Porém, o programa não “sabe” disso, precisamos implementar uma forma de ele avaliar se ele já chegou no resultado esperado. É a etapa 08 do algoritmo:

Continue a avaliação das N linhas e N colunas, até que o numero total de conjuntos resolvidos seja 2 x N, ou seja, todas as linhas e todas as colunas sem nenhum numero pendente de análise.

No momento atual, o programa fez as avaliações de linhas e colunas duas vezes, e já chegou ao resultado. Agora, vamos fazer o programa entender quando a resposta foi encontrada e parar de executar.

Vamos voltar na logica de varrer as linhas e colunas. Quando a função Analyze() identifica apenas uma possibilidade de solução, esse conjunto está resolvido. Basta eu fazer com que essa informação também seja retornada e tratada pela camanda anterior que chamou a função, para que um loop de execução das análises permaneca em execução enquanto houver pendências, e cada linha ou coluna resolvida decrementa o numero de pendencias, e ainda vamos usar o valor de soma esperado como um sinalizador, para evitar de analizar duas vezes um conjunto já resolvido, atualizando o total de soma esperado da linha ou coluna para zero. E, também na parte de análise, durante a avaliação das possibilidades, se durante a avaliação de uma possibilidade, o valor da soma atual já ultrapassou o valor esperado, essa possiblidade pode ser ignorada sem avaliar os demais valores do conjunto.

A versão final do fonte será colocada no GITHUB, ainda estou fazendo uns ajustes nos fontes conforme estou fazendo os posts. E, para efeitos de tempo e eficiencia de processamento, após comentar todas as chamadas de “conout” do miolo da rotina, e colocar uma marcação de tempo de execução, o resultado foi a resolução completa da fase 124, em 0,008 segundos, ou seja , OITO MILISSEGUNDOS !!!

Grid Inicial :
    6    3    2    7    7    5
    8    9    6    5    6    1
    3    7    5    1    6    7
    4    8    5    7    7    4
    8    8    7    8    3    8
    9    9    7    5    4    8

Grid Final :
    0   -3    0    0    0   -5
    0    0   -6   -5    0   -1
    0   -7   -5    0   -6    0
   -4    0   -5   -7    0   -4
   -8    0   -7   -8    0    0
    0   -9    0    0    0   -8

Tempo de processamento :    0.008 s.

E, essa parte de visualizar os dados do Grid, foi construída apenas uma função auxiliar chamada ShowGrid(), que recebe como parametro o array aGrid, e monta as linhas para mostrar os dados no log de console, vide abaixo:

Static function ShowGrid(aGrid)
  Local nL, nC, nTam
  Local cMsg := ''

  nTam := len(aGrid)
  For nL := 1 to nTam
    cMsg := ''
    For nC := 1 to nTam
      cMsg += str(aGrid[nL][nC],5)
    Next
    conout(cMsg)
  Next

return

E agora, tá pronto ou não ?

Neste momento, eu diria que a prova de conceito passou com louvor. O programa atual está com 300 linhas, incluindo código e comentários, e resolve um Grid no Number Sums de 6×6 em menos de 10 milissegundos. Porém, ele ainda é um “POC” — prova de conceito. Ainda não dá para chamar isso de um “MPV” (Minimo Produto Viável), pois ele não tem interface para entrar com os dados e mostrar o resultado. O “cérebro” ou “core” funciona, mas precisa de uma “casquinha” mais elegante para USABILIDADE. Essa implementação vamos abordar no próximo post. 😀

Mais uma vez, e como sempre, desejo a todos TERABYES DE SUCESSO !!!

Number Sums | Implementação de Algoritmo | Parte 02

Definindo o algoritmo

No post anterior (Number Sums | Implementação de Algoritmo | Parte 01), vimos um exemplo prático de resolução de uma fase do jogo “Number Sums”. Agora, vamos escrever um roteiro genérico (também chamado de algoritmo, que deve ser capaz de resolver qualquer fase do jogo. Esta é uma parte fundamental antes de implementar o algoritmo em alguma linguagem de programação, precisamos ter uma visao clara dos passos que devem ser seguidos. Por exemplo:

  1. Para resolver um grid de dados de N linhas por N colunas, execute a avaliação das N linhas, e depois a avaliação das N colunas. Cada avaliação recebe o valor da somatória esperado, e um conunto de números para análise.
  2. Cada avaliação deve determinar quantas combinações possiveis de USO dos numeros do conjunto que ainda não foram confirmados, podem ser somados com os numeros confirmados, para chegar ao resultado de soma esperados.
  3. Caso a análise retorne apenas uma combinação válida, ela deve ser escolhida, confirmando todos os numeros no grid de dados, e cancelando os numeros que nao estão na combinação, e este conjunto (linha OU coluna) está resolvido.
  4. Caso a análise retorne mais de uma combinação, devem ser avaliados um ou mais números entraram em todas as combinações propostas, e se um ou mais números nao entraram em nenhuma possibilidade.
  5. Todos os números que apareceram em todas as possibilidades devem ser confirmados.
  6. Todos os números que não entraram em nenhuma combinação devem ser removidos, pois com certeza não serão usados.
  7. Caso nenhuma possibilidade for encontrada para um conjunto, houve erro na entrada de dados, ou na implementação do algoritmo.
  8. Continue a avaliação das N linhas e N colunas, até que o numero total de conjuntos resolvidos seja 2 x N, ou seja, todas as linhas e todas as colunas sem nenhum numero pendente de análise.

Em linhas gerais, é isso. Se eu escrever um programa de computador que consiga receber como entrada um grid de dados de um tamanho NxN, junto com os N resultados de soma esperados para cada linha do conjunto, e os N resultados esperados de soma para as N colunas do grid de dados, e execute estas etapas, quando o programa confirmar os números corretos e remover os numeros desnecessários, o resultado será o grid de dados, contendo apenas os numeros a serem confirmados 😀

Quando damos um problena dessa natureza para um grupo de pessoas, cada uma pode chegar a um algoritmo parecido com este, porém escrito de outra forma, com os mais ou menos passos, com mais ou menos detalhes de cada etapa. Se eles chegam no resultado esperado, nesse momento é o que precisamos.

Implementação ?! Como ?!

Bem, agora entra a parte divertida… Quem domina uma ou mais linguagens de programação e tem conhecimentos em coisas como análise combinatória, lógica binária e matrizes de dados, pode ter dificuldades em escolher entre as alternativas possiveis de implementar tudo isso, enquanto quem está dando os primeiros passos em desenvolvimento, pode ter muitas dificuldades. Vamos começar do arroz com feijão, pense simples e resolva simples.

QUALQUER função, rotina, sub-rotina ou programa de computador, basicamente terá 3 passos:

  1. Entrada (de dados)
  2. Processamento
  3. Saída (de dados)

A entrada, no nosso caso, é uma forma de passar ao programa o grid de numeros e as somas esperadas. Pode ser uma interface gráfica que permita informar os números e as posições, pode ser um arquivo JSON ou TXT, com uma estrutura pré-definida, ou mesmo uma estrutura de dados alimentada diretamente no inicio do programa.

O processamento pode ser estruturado, em uma ou mais funções e sub-rotinas, que vão avaliar os dados e alterar o grid de dados, confirmando e removendo numeros, até que todos os numeros que atinjam a soma estejam confirmados, e todos os demais removidos. Pode também usar uma abordagem estruturada porém orientada a objetos, usando classes, propriedades e métodos, que chegue ao resultado esperado.

A saída é uma forma do programa ou rotina devolver esse grid resolvido ao usuário. Pode ser o grid de números final mostrado na tela, ou salvo em arquivo texto, ou em arquivo JSON, ou da forma que você bem entender, que deva ser capaz de explicar ao usuário a resolução do grid de dados proposto.

Existe jeito CERTO ou ERRADO ao desenvolver ?

Então, sendo bem prático e pragmático, certo deve ser o resultado do programa. Se você deu um jeito de chegar no resultado, está certo. Se não chegou no resultado esperado, então está errado – ou o programa, ou os dados, ou ambos, algo não está certo.

Ao desenvolver, existem boas práticas, existem implementações mais ou menos performáticas, mais simples e mais complexas, eventualmente pode ocorrer ambiguidades, duplicidades de código ou funcionalidade. Eu procuro seguir algumas diretrizes…

  1. Simplicidade e clareza. Pense simples, e implemente de forma simples. Escreva um código que outras pessoas sejam capazes de entender. Você está escrevendo a primeira versão de uma prova de conceito, o objetivo dela é funcionar, e você entender como funciona. Códigos podem se tornar tao complexos que você mesmo pode se perder em suas rotinas. Não acrescente complexidade onde não precisa.
  2. Cada problema traz consigo um determinado grau de incerteza, e ao codificar, voce pode perceber que o codigo está ficando complexo, ou que você não está chegando a lugar nenhum. Não se apegue a código, voce pode precisar jogar fora trechos de fonte e precisar fazer de novo de outra forma por várias vezes durante o desenvolvimento.
  3. Tudo dá par fazer melhor, certo ? Sim, sempre dá. Voce escreve um pedaço da rotina, voce testa, e verifica que funciona. Mas voce ainda não chegou no resultado final. Então, não gaste ficha tentando melhorar o que já funciona, faça o conjunto todo funcionar e voce chegar ao resultado final, e depois você avalia se a melhoria é realmente necessária.
  4. Começe pelo mais complicado. Assim, a dificuldade tende a diminuir conforme você avança no processo. Isso traz menos dor de cabeça e faz você gastar a energia extra que todo mundo tem em um inicio de desafio, nas etapas que realmente importam e fazem a diferença.

Essas quatro diretrizes, para essa etapa de POC (prova de conceito) ou “versão 1.0” do programa, é o que precisamos no momento.

Começando a batalha em AdvPL

Comecei criando um fonte AdvPL, chamei de “NumberSum.PRW”. Como para mim a parte mais complexa é o processamento do jogo em si, a “entrada de dados” foi feita “chumbada no fonte”, de uma forma mais elegante , “dados de entrada pré-definidos no código”. 😀

Lembram-se do post anterior, “Level 124” ? Então, optei por criar 3 arrays, um para conter os numeros de soma esperados das linhas do grid de dados, um para a soma esperada das colunas do grid de dados, e um array de duas dimensões (ou bi-dimensional) para conter o grid de dados, com todos os números propostos para o problema:

        
User Function NumberSum()

// Grid de dados 
aGrid := {}
aadd(aGrid ,{6,3,2,7,7,5})
aadd(aGrid ,{8,9,6,5,6,1})
aadd(aGrid ,{3,7,5,1,6,7})
aadd(aGrid ,{4,8,5,7,7,4})
aadd(aGrid ,{8,8,7,8,3,8})
aadd(aGrid ,{9,9,7,5,4,8})

// Linha superior, soma esperada de cada coluna
aSumCol := {12,19,23,20,6,18}

// Coluna a esquerda, soma esperada de cada linha 
aSumRow := {8,12,18,20,23,17}

SolveGrid(aGrid,aSumCol,aSumRow)

Return 

Pronto, feita a entrada. A função SolveGrid() é o “miolo” da implementação, que vamos começar a fazer em breve. Com isso, temos os arrays de dados de entrada com todos os numeros de uma fase do jogo. Agora, vamos implementar o item 1 do algoritmo (avaliação das N linhas, e depois a avaliação das N colunas.):

Static Function SolveGrid(aGrid,aSumCol,aSumRow)
Local nL, nC,  nTam, 
Local nSoma
Local aData,aNewData

// O grid é sempre "quadrado", então o numero de linhas e colunas é sempre igual 
nTam := len(aSumCol)

For nL := 1 to nTam

	// Avalia as linhas do Grid
	// Primeiro pega o valor da soma esperado dessa linha
	nSoma := aSumRow[nL]

	// Agora determina os numeros do conjunto ( colunas )
	aData := {}
	For nC := 1 to nTam
		aadd(aData,aGrid[nL][nC])
	Next

	// Faz a analise considerando os dados 
	// E retorna o novo conjunto de dados 
	aNewData := Analyze(nSoma,aData)

	// com o conjunto de dados retornado, atualiza o grid 
	For nC := 1 to nTam
		aGrid[nL][nC] := aNewData[nC]
	Next

Next

// Faz agora a mesma coisa para as colunas

For nC := 1 to nTam

	// Avalia as colunas do Grid
	// Primeiro pega o valor da soma esperado dessa coluna
	nSoma := aSumCol[nC]

	// Agora determina os numeros do conjunto ( linhas )
	aData := {}
	For nL := 1 to nTam
		aadd(aData,aGrid[nL][nC])
	Next

	// Faz a analise considerando os dados 
	// E retorna o novo conjunto de dados 
	aNewData := Analyze(nSoma,aData)

	// com o conjunto de dados retornado, atualiza o grid 
	For nL := 1 to nTam
		aGrid[nL][nC] := aNewData[nL]
	Next

Next

return 

Repare que a função de análise, que ainda vamos implementar, vai receber o valor de soma do conjunto, e os elementos do conjunto. Logo, a função de análise é a mesma para linhas e colunas, e vai se chamar Analyze(). Cada análise pode alterar os valores do grid. Como cada numero pode ter 3 estados, eu imagino usar cada posição da seguinte forma:

  • Um elemento de aGrid possui valor positivo: É um numero pendente de avaliação, não foi confirmado ou removido.
  • Um elemento de aGrid possui valor 0 (ZERO) : Corresponde a um número removido / cancelado. Nao preciso mais dele, somente preciso saber que ele não entra mais na conta.
  • Um valor negativo: É um número confirmado, que eu tenho certeza que vai entrar na solução do problema.

Agora, vamos para a implementação da etapa 02 do algoritmo, como foi descrito: Cada avaliação deve determinar quantas combinações possiveis de USO dos numeros do conjunto que ainda não foram confirmados, podem ser somados com os numeros confirmados, para chegar ao resultado de soma esperados.Aqui começa a nascer a função Analyze().

static function Analyze(nSoma,aData)
	Local nI
	conout("Soma Esperada : "+cvaltochar(nSoma))
	conout("Elementos do conjunto : ")
	For nI := 1 to len(aData)
		conout(cvaltochar(nI)+". = "+cvaltochar(aData[nI]))
	Next

	// A Fazer ... 

return

Inicialmente, apenas mostro no console os dados que serão analizados na função. Neste caso, deve ser o conjunto de dados da primeira linha do grid:

Soma Esperada : 8
Elementos do conjunto :
1. = 6
2. = 3
3. = 2
4. = 7
5. = 7
6. = 5

Agora, sabendo que cada um desses seis numeros, pode ou não entrar na soma, temos 64 possibilidades. Como construir um programa para calcular essas possibilidades ? E, lembrando que, nesta fase são 6×6 (seis linhas por seis colunas), existem fases com 7 elementos no conjunto ( 128 possibilidades ) e com 8 elementos ( 256 possibilidades ) …

Nós precisamos calcular todas as possibilidades, para verificar quantas e quais dessas possibilidades conseguem satisfazer a soma esperada do conjunto. Me passou pela cabeça análise combinatória, e eu achei complicado… deve ter forma mais simples.

E TEM SIM … LÓGICA BINÁRIA !!!

Em um conjunto de seis números, eu tenho 64 possibilidades diferentes, de escolher entre eles para entrar na soma. Imagine que cada combinação tenha apenas seis valores, que podem ser ZERO ou UM, onde zero significa que o numero daquela posição do conjunto não será somado, e um significa que o numero será somado:

000001 = soma o 6o numero 
000010 = Soma o 5o numero
000011 = Soma o 5o e o 6o numeros
000100 = Soma o 4o numero
000101 = Soma o 4o e o 6o numero 
000110 = Soma o 4o e 5o numeros
000111 = Soma o 4o, 5o e 6o numeros
001000 = Soma o 3o numero ... 
( ...demais combinações... )
111111 = Soma todos os numeros 

A primeira vista, e para quem não está familiarizado com isso, pode parecer complexo, m as a forma de lidar com isso é relativamente simples. A representação dos números de 1 a 64, em binário, contemplam todas as combinações possiveis de soma de um a todos os numeros do conjunto, e suas variações. Então, vamos fazer nosso programa contar de 1 a 64, em valor decimal mesmo, e então vamos usar operadores (na verdade, em ADVPL, usaremos funções de comparação binárias) para saber se os bits de uma determinada posição estão ligados ou não, para cada número.

Por exemplo, se pegarmos o numero 57, sabemos que a representação dele em binário é 00111001. Para saber se o primeiro bit ( da direita para a esquerda ) está ligado, usamos um operador binário “E” ( ou BITWISE AND ), comparando o valor com 00000001 . Se o resultado for maior que zero, o primeiro bit está ligado. Para saber se o segundo bit está ligado, fazemos a mesma coisa com o valor 00000010, o terceiro comparamos com 00000100, e assim por diante.

A função AdvPL que faz o papel do comparador binário AND , é a função NAND (https://tdn.totvs.com.br/display/tec/NAnd). Ela recebe dois ( ou mais ) números, para fazer a comparação de AND binário. Para saber o bit que estamos comparando, vamos converter os valores desejados em binário, para um número decimal:

/*
  Binário | Decimal
  00000001 = 1 
  00000010 = 2
  00000100 = 4
  00001000 = 8
  00010000 = 16
  00100000 = 32
  01000000 = 64
  10000000 = 128 
*/

Convertendo a tabela acima em codigo, teremos algo assim 

If NAND(nNum,1) > 0 // Bit 1 ligado
If NAND(nNum,2) > 0 // Bit 2 ligado
If NAND(nNum,4) > 0 // Bit 3 ligado
If NAND(nNum,8) > 0 // Bit 4 ligado
If NAND(nNum,16) > 0 // Bit 5 ligado
If NAND(nNum,32) > 0 // Bit 6 ligado
If NAND(nNum,64) > 0 // Bit 7 ligado
If NAND(nNum,128) > 0 // Bit 8 ligado

Agora, implementando o contador numérico com a lógica binária, damos um passo interessante, e calculamos todas as máscaras de bits, dos numeros que, caso selecionados, o resultado da soma desses numeros é o esperado, dessa forma:

STATIC Function Analyze(nSoma,aData)
  Local nI
  Local nTam := len(aData)
  Local aBitCmp := {1,2,4,8,16,32,64,128}
  Local nMaxBit := 2 ** nTam  // Total de combinações
  Local nTemp , cBitMask
  Local aPossible := {}
  Local nNum , nPos

  conout("Soma Esperada : "+cvaltochar(nSoma))
  conout("Elementos do conjunto : ")
  For nI := 1 to len(aData)
    conout(cvaltochar(nI)+". = "+cvaltochar(aData[nI]))
  Next

  // Avalia do numero 1 ao numero de combinações possivel
  For nNum := 1 to nMaxBit

    // Variavel temporaria para avaliar a soma dessa possibilidade
    // E a mascara de bits dessa possibilidade
    nTemp := 0
    cBitMask := ''

    // Conta de 1 até a quantidade de elementos do conjunto
    For nPos := 1 to nTam

      // Pega o valor do elemento do conjunto
      nValor :=  aData[nPos]

      if ( NAND(nNum , aBitCmp[nPos] ) > 0 )

        cBitMask += '1'

        // Se o bit dessa posição está ligado, o valor deve ser considerado
        // Se o valor nao foi removido, considera . 
        // Se foi removido, essa possibilidade não é válida para análise
        If nValor != 0
          nTemp += ABS(nValor)
        Else
          EXIT
        Endif

      Else

        cBitMask += '0'

        // Se o bit dessa posição NÃO está ligado, essa possibilidade 
        // nao considera esse valor
        // Se esse valor já está confirmado ( valor negativo ), 
        // essa possibilidade não é válida para analise 
        if nValor < 0
          EXIT
        Endif

      Endif

    Next

    If nTemp = nSoma
      // Essa combinação de numeros atingiu a soma
      // acrescenta essa mascara de bits no array de possibilidades
      aadd( aPossible , cBitMask )
    Endif

  Next

  conout("Possibilidades : "+cvaltochar(len(aPossible)))
  for nI := 1 to len(aPossible)
    conout(cvaltochar(nI)+". "+aPossible[nI])
  Next

  // A FAZER 

return

Agora, vamos ver o que aconteceu… Quando o programa foi executado até aqui, considerando a primeira linha de dados, ele vai mostrar o seguilte resultado na tela de console :

Soma Esperada : 8
Elementos do conjunto :
1. = 6
2. = 3
3. = 2
4. = 7
5. = 7
6. = 5

Possibilidades : 2
1. 101000
2. 010001

Reparem que as duas unicas possibilidades descobertas foram: Somar o 1o com o 3o numero ( 6 + 2 = 8 ) , ou somar o 2o com 6o numero ( 3 + 5 = 8 ), exatamente o que foi identificado “manualmente” no primeiro post ! Estamos no caminho certo 😀

Detalhes interessantes e Importantes

nMaxBit := 2 ** nTam

Cada grid no jogo pode ter de 6 a 8 linhas e colunas. Para saber a quantidade de possibilidades a ser avaliada, calculamos 2 elevado a quantidade de elementos do conjunto. Em Advpl, a sequencia de dois asteriscos é o operador de potência.

aBitCmp := {1,2,4,8,16,32,64,128}

Cada posição de BIT a ser comparada, tem um numero equivalente para uso com a função NAND. É mais fácil e rápido já deixar esses numeros armazenados em uma lista, para tornar mais dinamica a logica de identificar um numero qualquer de posições. Com esses valores pre-calculados até 128, consideramos avaliar até no máximo 8 bits, ou seja, conjuntos de até 8 numeros.

nTemp += ABS(nValor)

Quando somamos o valor do número do conjunto em um acumulador temporário, para avaliar a soma dos elementos considerados, devemos lembrar que nValor pode ser positivo, no caso de um numero ainda não confirmado, ou NEGATIVO, caso o número já tenha sido confirmado. Por isso, usamos a função ABS(), para sempre obter a representação positiva do número.

Próximos passos

Certo, até aqui levantamos as possibilidades de uso dos numeros que atingem exatamente o valor de soma esperado. Agora, precisamos implementar as tomadas de decisões definidas no algorimto, para uma possibilidade encontrada, mais de uma possibilidade, ou nenhuma possibilidade. E, isso fica para o próximo post, ok ?

Desejo novamente a todos TERABYTES DE SUCESSO !!!

Number Sums | Implementação de Algoritmo | Parte 01

Introdução

Em matemática e ciência da computação, um algoritmo é uma sequência finita de ações executáveis, que visam obter uma solução para um determinado tipo de problema. Sendo assim, um algoritmo é independente de linguagem de programação. Ele pode ser implementado em várias linguagens, desde que, utilizando os recursos da linguagem escolhida, seja possível realizar todas as ações do algoritmo.

O desafio do desenvolvedor de software, é entender a necessidade ou propósito do problema, as informações que compõe o problema, para então construir um algoritmo que resolva o problema ou chegue ao resultado esperado, para então implementar um programa ou rotina, usando uma linguagem de progração, que dê conta da tarefa.

Number Sums

Neste post, e nos posts subsequentes, a idéia é fazer um programa de computador que consiga resolver um Puzzle do Tipo “Number Sums” — Um jogo de lógica matemática disponivel apra IOs e Adroid. Para conhecer mais detalhes desse jogo, veja as páginas na Google Play e Apple Store.

Trata-se de um jogo de raciocínio lógico, onde é mostrado um grid de 6, 7 ou 8 linhas e colunas, com números dentro, e na parte externa do grid, a linha superior contem um valor por coluna, que deve ser atingido com a soma de todos os números em cada coluna, e na coluna à esquerda do grid, são mostrados um número por linha, que deve ser atingido com a soma de todos os números daquela linha.

Para chegar no resultado esperado, você deve confirmar ou remover cada um dos números do grid, usando a borracha para remover, e a caneta para confirmar o número. Veja abaixo um print de uma tela do jogo:

É apresentado um grid de dados, com 6 linhas e 6 colunas de números, totalizando 36 números, e um valor esperado de soma para todas as linhas, e todas as colunas do grid.

Como resolver isso ?

Como temos 6 linhas , e 6 colunas, na prática o problema envolve 12 conjuntos de 6 números, que ao serem somados, devem chegar ao valor estipulado de soma de cada conjunto. Como cada numero pertence a dois conjuntos — um da linha e o outro da coluna — cada número, ao ser confirmado ou removido, influencia o resultado da soma da linha e da coluna onde o número se encontra.

Arbitrariamente, vamos começcar a resolução pela primeira linha do grid. Para esta linha, é esperado o número 8 como resultado da soma dos numeros escolhidos. A primeira linha contém a sequência de valores 6, 3, 2, 7, 7 e 5. Para decidir quais números serão confirmados ou apagados dessa linha, precisamos avaliar quais desses números que, quando somados, resultam no número 8.

Cada um dos números desse conjunto deve ser escolhido (ou confirmado), ou removido. Logo, se tivéssemos apenas um número nesse conjunto, teriamos 2 alternativas: Confirma ou Remove. Como cada número existente no conjunto possui duas alternativas (confirmado ou removido), e temos 6 números nesse conjunto, temos 2^6 (dois elevado a 6a potência), ou 64 possibilidades de escolher os numeros.

Nao precisamos fazer as 64 combinações, mas precisamos avaliar sistematicamente quais são os números que não entram em nenhuma combinação que atinja o valor da soma, para podemos removê-los e avaliar o problema novamente, bem como confirmar os números que entram em todas as combinações válidas que atingem o valor da soma.

Começando com o primeiro número do conjunto, o número 6. Para chegar no numero 8, ele precisaria ser somado com o numero 2. O numero 2 existe na 3a posição do conjunto, então os numeros 6 e 2 são possíveis candidatos, ainda não podem ser confirmados oiu removidos. Olhando o segundo número do conjunto, o número 3, ele precisaria ser somado com o número 5, para chegar ao número 8. O número 5 existe na ultima posição do conjunto, então eles também são possíveis candidatos, e portando ainda não podem ser removidos ou confirmados. Agora, olhando a 4a e 5a posições, onde existe o número 7 … para ele chegar ao número 8, ele precisaria ser somado com o número 1 … e como não tem nenhum número 1 no conjunto, não existe nenhuma combinação de uso dos numeros 7 que cheguem ao resultado esperado. Logo, ambos podem ser eliminados / descartados / removidos. Apos selecionar a borracha e limpar essas posições, nosso grid ficaria assim:

Agora, como removemos dois números 7, na 4a e 5a colunas, vamos avaliar se podemos tirar proveito disso, olhando a 5a coluna, onde a soma de todas as linhas dessa coluna deve ser o número 6, e pertencem ao conjunto a sequëncia de números 6, 6, 7, 3 e 4 .

Esse conjunto tem apenas 5 elementos, pois o primeiro elemento foi removido na etapa anterior, ao avaliar a linha 1. A soma deve ser o número 6, e a 2a e 3a linhas já contém o número 6, então eles nao podem ser descartados. A 4a linha tem o numero 7, que é maior que 6, então pode ser eliminado. As duas ultimas posições, com os números 3 e 4, não possuem nenhuma possibilidade de serem somados com quaisquer outros numeros desse conjunto para chegar ao falor 6, logo também podem ser eliminados. Após eliminar do conjunto os três ultimos numeros (7, 3 e 4 ), agora o grid está assim:

A 5a coluna agora ficou com apenas dois números 6. Sabemos que apenas um deles vai ser confirmado, e que o outro vai ser removido, mas ainda não sabemos o que fazer com cada um deles.

Ao remover os três últimos números da 5a coluna, alteramos as linhas 4, 5 e 6 do grid. Vamos avaliar cada uma delas, para tomar novas decisões, comecando pela 4a linha: Espera-se uma soma de 20, usando os numeros 4, 8, 5, 7 e 4.

De cabeça, somando apenas os numeros 8, 7 e 5, chegamos ao valor 20. Porem, também chegamos a 20 somando os dois números 4, mais 7 e 5. São apenas duas possibilidades : 4+5+7+4, ou 8+5+7. Como são apenas duas possibilidades, e em AMBAS as possibilidades, os números 5 e 7 estão presentes em ambas, COM CERTEZA eles entram nessa soma, então eles podem e deve ser confirmados. O que falta descobrir depois, é se vai ser usado o numero 8, ou os dois números 4. Após confirmar os numeros 5 e 7, teremos o seguinte cenário:

Na linha 4 não conseguimos remover mais nenhum número, mas ja conseguimos confirmar dois números… Vamos agora avaliar a 5a linha: Soma 23, com quatro ocorrencias do número 8, e uma ocorrencia do número 7. Somente é possível chegar ao numero 23, usando dois dos quatro números 8 disponiveis, e com certeza usando o número 7. Logo, podemos confirmar a participação do número 7 no grid.

E, continuando para a 6a linha, com a soma 17, usando os números 9, 9, 7, 5 e 8 . Com este conjunto, somente chegamos ao número 17, se somarmos 9+8. Como existem dois numeros 9 nesse conjunto, temos apenas certeza que o numero 8 será utilizado, então podemos confirmá-lo também.

Feitas as confirmações do numero 7 na 5a linha e 3a coluna, e do número 8 na 6a linha, 6a coluna, temos um novo cenário:

Na 3a coluna, linhas 4 e 5, confirmamos a presença de dois números nessa coluna, o número 5 e o número 7. Nessa coluna, espera-se a soma 23, usando COM CERTEZA os numeros 5 e 7… Nesse conjunto ainda tem os numeros 2, 6, 5 e 7

Quais desses numeros podem ser somandos com 5+7, que chegaria a 23 ? Como 5+7 = 12, e para chegar em 23, faltam 11 , a pergunta pode ser mais simples : Quais desses numeros somam 11 unidades ? Apenas 6+5 soma 11 … logo, o número 2 na primeira linha pode ser removido, e o numero 7 da ultima linha também podem ser removidos, ficando o novo cenário assim:

Reparem que, quanto mais confirmamos e removemos os números, menores são as combinações que chegam nos valores esperados das somas. Repare na 6a linha .. ela já tinha sido analisada anteriormente, quando as possibilidades envolviam os numeros 9, 9, 7, 5 e 8 .Agora, o número 7 nao está mais nesse conjunto, e o numero 8 está confirmado. Logo, para chegar ao numero 17, considerando o numero 8 , ainda precisamos de mais 9 unidades. O número 5 não pode ser usado de jeito nenhum , então pode ser eliminado da 6a linha, 4a coluna.

Ao eliminarmos esse número, e reavaliarmos a 4a coluna, para a soma 20, temos o numero 7 confirmado, faltando 13 unidades para serem somadas, considerando apenas os numeros 5, 1 e 8 Para isso, somente conseguimos somar 5+8, o número 1 nunca será usado, e pode ser removido da 4a coluna, 3a linha.

Analisando a 3a linha, onde acabamos de remover o numero 1, temos uma soma esperada igual a 18 , usando os numeros 3, 7, 5, 6 e 7. Obter esse numero somente será possível somando um número 5 , com um numero 6 e um numero 7 (5+6+7=18). Como temos apenas uma ocorrencia do numero 5, e uma do numero 6, podemos confirmá-los, e como o numero e não será usado em nenhuma combinação, ele pode ser removido. Agora, temos o seguinte cenário:

Repare na primeira coluna: Espera-se uma soma de 12, usando os numeros 6, 8, 4, 8 e 9. O número 9 é muito grande, nao tem quem some com ele para chegar no 12, pode ser elimnado. O numero 6 exigiria outro numero 6, ou outros dois numeros que somados cheguem a 6, o que não acontece , pode ser eliminado também. Sobraram os numero 8 e 4 , que somados chegam a 12. Podemos confirmar o numero 4, mas nao sabemos quais desses dois numeros 8 serão usados. Apos limpar os numeros 6 e 9, cada vez temos um cenário menos complexo:

Reparem na 6a linha, para uma soma de 17, temos um valor 8 confirmado, e apenas um valor 9 na linha. 8+9=17 , podemos confirmar esse 9 ( 6a linha, 2a coluna)! Reparem tambem na 4a coluna, sobraram apenas os valores 5,7 e 8, que somados chegam ao valor esperado 20 . Confirmamos todos esses valores, e chegamos ao novo cenário abaixo:

Olhando na segunda linha, embora apenas o valor 5 esteja confirmado, para atingir a soma esperada de 12 unidades, nao existe possibilidade de uso dos valores 8 e 9 do conjunto, que podem ser removidos, apenas um dos numeros 6 disponiveis, e com certeza o numero 1, que pode ser confirmado, gerando um novo cenário:

Olhando novamente a coluna 1, os dois numeros restantes justamente correspondem a soma 12… logo, podenos confirmar os numeros 4 e 8, na primeira coluna, linhas 4 e 5. Ao fazer isso, acabamos resolvendo a 5a linha , pois a soma dos numeros confirmados já alcancou 23, entao podemos eliminar os valores da 2a e da 6a colunas, da 5a linha, gerando o cenário abaixo:

A 5a coluna, cuja soma deve ser 6, ja possui um numero 6 confirmado, logo o outro numero 6, na 2a linha, deve ser descartado. Na 4a linha, a soma dos valores confirmados 4+5+7= 16, para 20 falta 4, podemos descartar o numero 8 da 2a coluna, e confirmar o numero 4 na 6a coluna. E, na 3a coluna, ao confirmar o numero 6 , chegamos a soma 23, que resolve ao mesmo tempo a 2a linha.

E, pra fechar com chave de ouro, confirmando todos os valorerestantes na 1a linha, chegamos ao valor de soma 8, e confirmando todos os valores restantes da 2a coluna, chegamos ao valor 19:

E, finalmente, precisamos confirmar ou remover a última posição a ser avaliada, na 3a linha, 6a coluna, para que a soma dos numeros da linha seja 18, e da coluna seja 18 .. Ao somar os numeros que já foram confirmados na linha ( 7+5+6 ) e na coluna ( 5+1+4+8 ), constatamos que os numeros confirmados somados já atingem 18 unidades, logo, removemos o numero 7, e finalizamos essa fase !!!

Próximos passos

Com o que foi abordado até aqui, você agora tem um tutorial com exemplos passo a passo para resolver “de cabeça” as fases desse jogo. Eu resolvi começar pela primeira linha do grid, pois ela era a que tinha o menor valor de soma esperado. E, cada valor confirmado ou removido de uma linha, melhora a analise dos elementos de soma de uma coluna, e vice-versa. Se voce chegar em uma linha ou coluna, onde nao é possivel confirmar ou remover nenhum numero, avalie outras linhas e colunas, cada numero confirmado ou removido diminui o escopo de análise, e deixa você mais perto de resolver o puzzle.

Conclusão

Esse post foi somente para abriri o apetite … No próximo post, vamos estudar COMO implementar um algoritmo em AdvPL, que receba como parâmetros a quantidade de linhas e de colunas do grid, o valor de cada um dos elementos do grid, e os valores de soma esperados para todas as linhas e para todas as colunas, e com esses dados, obter como resultado os numeros do grid que precisam ser confirmados, para atingir o resultado esperado das somas de todas as linhas e todas as colunas

E, como sempre, desejo a todos TERABYTES DE SUCESSO !!!!

QR Code em AdvPL – Parte 04

Introdução

No post anterior, vimos sem grandes detalhes as etapas de criar um QR Code “from scratch” (do zero), e as áreas reservadas da imagem. Agora vamos ver os passos necessários com mais detalhes.

Passo 01 – Escolha da correção de erro

Como vimos no post anterior, quanto maior o nível de correção de erro, mais módulos (quadradinhos) são necessários para representar a informação. Como cada versão de QR Code possui um tamanho fixo, a quantidade de informações que podemos colocar dentro de um QR Code varia de acordo com o tipo do alfabeto usado para representar os dados (numérico, alfanumérico, bytes). Por exemplo, a versão 1 com correção de erro “L” (baixa), cabem 25 símbolos alfanuméricos ou 17 bytes. Quando usada a mesma versão, mas o nível de correção de erro “H” (alto), cabem 10 símbolos alfanuméricos ou 7 bytes.

Desse modo, primeiro verificamos qual é o tipo de alfabeto que podemos utilizar, e escolhemos uma correção de erro que atenda a necessidade. Vários sites de informações e recursos de QR Code comentam que níveis mais altos são recomendáveis para utilização em ambientes onde a impressão está mais sujeita a danos — como etiquetas e manifestos colados em caixas, para um chão de fábrica. Tão importante quanto isso, é a escolha posterior de um tamanho para a imagem impressa, deve-se certificar que o equipamento usado para fazer a leitura (scanner) seja capaz de ler a informação.

Uma vez sabendo a quantidade de símbolos no alfabeto utilizado, e o nível de correção de erro desejado, podemos escolher uma versão de QR Code que as informações a serem representadas “caibam dentro”.

Dentro da classe ZQRCODE() existe uma função chamada GetQRLimits(), que justamente retorna um array pré-calculado com as versões, correção de erro, e capacidade de símbolos por modo de codificação (alfabeto) utilizado.

Passo 02 – Codificar os dados

Seguindo uma receita de bolo para cada codificação usada, os dados a serem representados no QR Code devem passar por uma primeira etapa de processamento, gerando um stream ou sequência de BITS (os zeros e uns) para então ser re-codificado em uma sequência de números inteiros de tamanho igual a capacidade da versão do QR Code em uso. Para isso são usadas conversões de numérico decimal para binário e vice-versa. No final desta etapa, teremos um array de números inteiros com valores entre 0 e 255, que vamos chamar de “Code Words de Dados”.

O método responsável pelas etapas de codificação de dados chama-se BuildData(). O dicionário alfanumérico “restrito” de 44 símbolos é armazenado em um array estático (_Alpha) e alimentado na carga do programa através da função GetAlpha(). Para chamá-lo é necessário no mínimo informar a correção de erro desejada e atribuir os dados que vão compor o QR Code. De posse dessa informação, caso você ainda não tenha escolhido uma versão para o QR Code, internamente será chamado o método BestVersion(), que vai identificar se os dados informados podem ser codificados com o alfabeto restrito ou codificados em bytes mesmo, buscando a menor versão necessária para caber as informações desejadas.

** Por hora este método assume que os dados serão codificados em bytes, e calcula a menor versão necessária para a codificação neste formato ***

Passo 03 – Codificar a recuperação de erros

Feita a codificação dos dados, consultamos uma outra lista pronta, através da função GetECCW(), que a partir da versão e da correção de erro utilizada, retorna a quantidade de CodeWords de correção de erro que devem ser geradas para fazer parte do QR Code.

O menor número de CodeWords de erro a ser gerado são 7, quando usamos a versão 1 com correção de erro “L”. Embora a quantidade de CodeWords aumente significativamente em versões mais altas, existe uma regra para divisão de blocos de dados em agrupamento de dois tipos, a partir da versão 3 com correção de erro “H”, onde a lista de CodeWords para correção de erro são geradas por grupo. Logo, mesmo que um QR Code de versão 40 com correção de erro “L” suporte 2956 CodeWords de dados, cada grupo terá no máximo 30 CodeWords para correção de erro, geradas para 19 blocos tipo 1 com 118 CodeWords de dados cada, e 6 blocos tipo 2 com 119 CodeWords de dados cada. Logo, serão 30 gerações de 30 CodeWords de correção de erro. Todos estes valores podem ser obtidos na lista retornada pela função GetECCW().

A geração destes códigos envolve algumas fórmulas matemáticas com polinômios (equações), porém como as fórmulas de base para o cálculo são montadas de acordo com a quantidade de CodeWords de correção de erro a serem montados, basta criar uma lista com as fórmulas prontas para cada número necessário para cada versão. As fórmulas estão representadas em arrays bidimensionais, retornados pela função GetPolyGen(), que recebe como parâmetro o número de bytes (ou CodeWords) de correção de erro necessários.

Cada cálculo de correção de erro parte de um grupo de dados — representado por um array de números (que convenientemente vamos chamar de “mensagem”) — e uma sequência de operações polinomiais feitas com a mensagem. O resultado final destas operações será justamente um array com a lista de bytes (CodeWords) de correção de erro gerados para aquela mensagem.

As etapas do cálculo são multiplicações e divisões polinomiais, algo relativamente complexo. Porém, as operações necessárias tornam-se possíveis de ser feitas com um programa simples, graças ao uso de uma representação aritmética de campos finitos (ou Galois Field). Na prática, e visto de forma superficial, a utilização de duas listas de conversão pré-calculadas , é possível transformar o polinômio com fatores gerados com os dados da mensagem para a aritmética GF(256). Com isso, ao invés de precisar criar ou usar um algoritmo complexo para fazer uma multiplicação ou divisão polinomial, a representação de fatores usando essa aritmética permite que as operações de multiplicação sejam feitas apenas somando expoentes na equação, e a divisão polinomial longa seja feita usando o operador binário XOR (“ou exclusivo”) diretamente com os fatores. As conversões entre os fatores decimais e os fatores em notação GF(256) são feitas com base nas funções GetGFA2Int() e GetGFInt2A(), usando listas pré-calculadas.

Passo 04 – Estruturação da mensagem final

Após calcular as duas listas (ou arrays) de CodeWords (dados e correção de erro), a estruturação da mensagem final terá mais etapas quanto maior for a versão do QR Code a ser emitida. As etapas consistem na criação de um ou mais grupos de dados, de tamanho limitado e pré-definido, com os dados e a correção de erro seqüenciados ou intercalados. A função GetECCW() também retorna a informação de quantos grupos de dados são necessários para cada versão, a intercalação de dados e códigos de erro somente é necessária quando temos mais de um grupo de dados, e a regra aplicada é a mesma, independente de quantos grupos de dados são necessários para estruturar a mensagem final.

O objetivo desta etapa é obter a sequência final de CodeWords ou bytes, contemplando o array de dados e o array de correção de erro, que será usado para gerar a sequência de módulos binária para compor a matriz final do QR Code.

Passo 05 – Disposição dos módulos da mensagem final

O método DeployBits() gera a seqüencia binária de dados para o QR Code, alimentando as áreas de dados do QRCode, ignorando as áreas reservadas. É um malabarismo de bits em zigue-zague, comecando de baixo para cima, da direita para a esquerda, preenchendo sempre 2 colunas de bits.

Passo 06 – Mascaramento de dados

Aqui a coisa enrosca … após distribuir os dados, você já tem um QRCode … MAS, como os dados usados para compor o QRCode podem gerar seqüencias longas ou contínuas de blocos preenchidos ou blocos vazios, e até mesmo gerar algo parecido com uma marca de controle, o IDEAL [e que a aplicação que emita o QRCode seja capaz de aplicar os 8 (oito) fórmulas de mascaramento de dados sobre os dados originais do QRCode, e escolha qual a melhor alternativa de apresentação. Essa etapa ainda está em desenvolvimento, por hora a classe permite eu definir ou escolher um método, ela ainda não escolhe sozinha … Essa tarefa, quando completa, será feita pelo método SelectMask().

Passo 07 – Informações de versão e formato

As informações de versão e formato do QR Code são colocadas em áreas pré-determinadas, de acordo com a versão e formato escolhidos. Existem máscaras de dados prontas já armazenadas no fonte para informar esses dados nos lugares corretos.

Após aplicar a máscara com o SelectMask(), o array aGrid da classe vai conter as informações necessárias para a plotagem do QRCode. Basta varrer todas as linhas e colunas do array, e onde o elemento oObj:aGrid[nRow][nCol] for igual a 1 ou 3 ( dado preenchido ou dado de controle ), deve ser plotado um módulo preenchido, senão o módulo deve ser vazio. No fonte de exemplo do post anterior (QR Code em AdvPL – Parte 03), a imagem do QRCode é gerada com um bitmap em memória, onde cada módulo ocupa um pixel, e depois de gerada, é usado o método Stretch() da classe de Bitmap, para aumentar a imagem 12x — assim , cada módulo tera 12×12 pixels — definido em

MODULE_PIXEL_SIZE 

Conclusão

Todo esse esforço para gerar um QRCode tem apenas um objetivo: Mostrar que é possível de ser feito … e mostrar quais as dificuldades em lidar com blocos de informações em baixo nível — de pixels a bits — e que dá para fazer em AdvPL 😀

Espero que todas essas informações sejam de utilidade e apreciação de todos aqueles que buscam saber e fazer sempre mais !!! E conhecer cada vez mais !!!

Desejo a todos, como sempre, TERABYTES DE SUCESSO !!!!

Referências

O Protocolo HTTP – Parte 06

HTTP Parte 6, demais conteúdos e uso de UTF-8

Introdução

No post anterior (O Protocolo HTTP – Parte 05), o Web Site local de testes com o TOTVS Application Server ganhou mais uma funcionalidade, vimos como o Browse montou uma requisição de POST de um formulário HTML com campos de preenchimento do usuário, e mais detalhes sobre GET e POST. Agora, vamos fazer alguns ajustes nas páginas HTML do site de testes, e ver se ou quais impactos terão estes ajustes, e acrescentar mais algumas coisas …

Utilizando HTML5

HTML versão 5 traz novos identificadores, recursos, e já está bem suportado pelos Web Browses de mercado. Embora marcadores foram declarados “obsoletos”, já que é para colocar a mão na massa, é elegante declarar na primeira linha de cada página HTML, que a versão usada é a HTML5. Para isso, basta inserir a linha abaixo como primeira linha do arquivo:

<!DOCTYPE html>

HTML Head e HTML Body

De forma similar ao protocolo HTTP, uma página HTML possui identificadores para declaração de elementos de cabeçalho de página (Header), e para delimitar o corpo do HTML (Body), inclusive delimitar o inicio e fim do HTML. Se nada disso for colocado, e voce simplesmente criar um arquivo HTML sem estes identificadores, normalmente o Web Browse vai interpretar tudo como HTML Body… Mas é deveras recomendável que você monte as suas páginas HTML declarando onde fica o que, vide exemplo abaixo:

<!DOCTYPE html>
<html>
<head>
<title>Título do Documento</title>
</head>
<body>
Conteúdo do Documento...
</body>
</html>

Entre o inicio e final do cabeçalho HTML, delimitado pelos marcadores <head> e </head>, usamos outros marcadores HTML para identificar características (ou meta-dados) de um documento e/ou página HTML, como um título — usado na barra de navegação e por mecanismos de busca –, scripts — executados pelo web browser na estação onde a página e/ou documento HTML está sendo mostrado, entre outras coisas interessantes.

Salve esse arquivo com o nome “html5.htm” na pasta htto-root, e com o Application Server em execução, abra a URL http://locahost/html5.htm, e vejamos como a tela apareceu:

Se o arquivo foi salvo usando CodePage 1252, os caracteres acentuados serão mostrados corretamente. Se ele foi salvo pelo NOTEPAD usando UTF-8, serão mostrados caracteres “estranhos” nos acentos … Isso é UTF-8 interpretado como CP1252 …

Agora, vamos pegar esse “padrão”, e refazer os arquivos do nosso site de testes. Eles devem ficar assim:

  • Arquivo default.htm
<!DOCTYPE html>
<html>
<head>
<title>Meu primeiro Web Site - Página Inicial</title>
</head>
<body>
<h3>Meu primeiro Web Site</h3>
<a href="testepost.htm">Teste de Post</a>
</body>
</html>
  • Arquivo testepost.htm
<!DOCTYPE html>
<html>
<head>
<title>Meu primeiro Web Site - Teste de POST</title>
</head>
<body>
<h3>Teste de Post</h3>
<hr>
<form name="mydataform" method="post" action="testepost.htm">
<input type="text" name="CAMPO1"></input>
<input type="text" name="CAMPO2"></input>
input type="submit"/>
</form>
<hr>
<a href="/">Voltar ao Início</a>
</body>
</html>

Agora, com tudo funcionando, vamos passar a usar a codificação UTF-8 nos nossos arquivos HTML, bastando para isso suas coisas :

  • Usando por exemplo o editor NOTEPAD++, insira logo após a linha de abertura do header <head>, antes do <title>, a linha abaixo:
<meta charset="UTF-8">
  • Agora, use a opção Encoding -> Convert to UTF-8, e salve o arquivo. ( Verifique se o arquivo já não está com esta codificação … os editores de textos simples mais recentes, mesmo o NOTEPAD do Windows, já codifica o arquivo como UTF-8 se você cria o arquivo e preenche com algum caractere acentuado.)

E, finalmente, vamos abrir novamente os arquivos no Web Browser. Com o conteúdo codificado em UTF-8, e a informação meta charset no HTML head, tudo deve ser mostrado corretamente pelo Web Browser.

Executando o POST novamente, agora informando caracteres acentuados

No teste de post anterior, sem utilizar o charset UTF-8 nos nossos HTML(s), a letra “a” minúsculo acentuado foi enviada pelo Web Browser ao realizar o POST usando o (Codepage) CP-1252, e enviada pelo post codificada como “%E1″. Agora que estamos usando UTF-8, se acessarmos a tela de teste de post, digitarmos no primeiro campo “Olá” e no segundo “Mundo”, e verificarmos usando o painel do desenvolvedor como o Google Chrome codificou a letra “á”, veremos novidades:

Reparem que agora a letra “á” foi codificada como “%C3%A1” .. Essa é a seqüência de bytes no padrão UTF-8 que representa a letra a minúscula com acento agudo no alfabeto latino. Deste ponto em diante, todos os arquivos HTML dos nossos testes passam a declarar HTML5 e codificação em UTF-8. 😀 E, conseqüentemente, o Web Browser vai entender que, se o conteúdo do arquivo é UTF-8, e existe um formulário de entrada de dados, esses dados serão codificados como UTF-8.

Mas, o que eu ganho com isso ?

Utilizando UTF-8, é possível codificar e representar múltiplos alfabetos e caracteres de outros idiomas — mais de 1 milhão — do formato internacional “Unicode”, até “emoticons”. UTF-8 signfica “Unicode Transformation Format – 8 bits”. Eu posso por exemplo, usar uma ferramenta como o Google Translator, e traduzir “olá” de português para Russo e Chinês, e colocar dois parágrafos no arquivo “default.htm”, copiando e colando os caracteres da tradução, usando o Notepad++ ou o NOTEPAD do Windows … veja como o ficou o arquivo:

E, ao ser mostrado no Web Browser, ficou assim:

Vamos colocar uma imagem no HTML ?

Eu peguei uma foto do meu perfil do Facebook, salvei como uma uma imagem JPG, usei o PaintBrush do Windows para fazer um “resize” na imagem .. e ela ficou um “quadrado” de 192×192 pixels…. Salvei essa imagem no disco, com o nome “mypicture.jpg”, dentro de uma nova pasta chamada “pictures”, criada dentro da pasta http-root.

Agora, vamos editar a nossa pagina “default.htm”, e colocar essa imagem para ser mostrada no HTML, acrescentando as duas linha em negrito abaixo, antes do </body>:

<hr>
<img src="./pictures/mypicture.jpg">

A linha com <hr> insere uma barra horizontal no documento, e o marcador <img permite a inserção de uma imagem dentro do HTML. Reparem que, para a imagem ser localizada, eu informo na propriedade “src=” a informação “./pictures/mypicture.jpg”. Vamos ver como ficou ?

Agora, ao usar as ferramentas do desenvolvedor do Google Chrome, e pedir para recarregar essa página, vamos ver uma informação nova:

Quando o Web Browser pediu a página “default.htm” ao HTTP Server, ele recebeu apenas a página HTML… Após receber a página e começar a “desenhar” o HTML na tela, o Web Browse encontrou o marcador de imagem, para mostrar a imagem “./pictures/mypicture.jpg” … Então, o Web Browse abre internamente uma nova solicitação de GET, pedindo esse arquivo para o HTTP Server. O HTTP Server, ao receber o pedido, verifica que o arquivo existe em disco, na pasta identificada, e retorna o arquivo ao Browser, que mostra a imagem na tela.

Como o HTTP server sabia que a imagem era do tipo JPEG ?

Então, existem algumas extensões de arquivo conhecidas e tratadas pelo Totvs Application Server como servidor de HTTP. Ao receber uma instrução GET, foi informado no protocolo que o recurso “mypicture.jpg” foi solicitado .. Ao receber uma solicitação, o servidor verifica qual o path, nome e extensão do recurso solicitado, e baseado na extensão informada (.jpg ou .jpeg), ele já sabe que é um arquivo em disco, e que o formato do arquivo é “image/jpeg”. Veja abaixo a lista de arquivos tratadas pelo HTTP Server — Totvs Application Server, e o content-type retornado para cada uma:

HTM ou HTML : text/html
XML         : text/xml
TXT         : text/plain
GIF         : image/gif
JPG ou JPEG : image/jpeg
BMP         : image/bmp
SVG         : image/svg+xml
ZIP         : application/zip
DOC         : application/msword
AU          : audio/au
WAV         : audio/wav
MPEG ou MPG : video/mpeg
AVI         : video/avi
CSS         : text/css
JS          : application/x-javascript
TODAS AS DEMAIS : application/octet-stream";

Dessa forma, baseado na extensão utilizada no nome do recurso solicitado pelo Browser, verificando a extensão do recurso, ele já sabe qual Content-type de retorno deve ser utilizado para o Web Browser interpretar o recurso corretamente. Caso seja um arquivo de extensão desconhecida pelo Application Server, ele retorna ao Browser que o conteúdo do arquivo é uma seqüencia de bytes (
octet stream“, ou “seqüencia de octetos” — afinal 1 byte corresponde a uma seqüência de 8 bits) 😛

Por que foi criada a pasta “pictures” ? A imagem não poderia estar na pasta “http-root”?

Sim, claro que poderia. Criar pastas ou subdiretórios a partir da pasta raiz de publicação (no nosso caso, http-root) permite ORGANIZAR melhor os arquivos dentro do site. Podemos criar uma pasta para centralizar ou agrupar as folhas de estilo utilizadas (CSS), imagens, scripts (JavaScript), etc …. Calma que a gente chega lá ..rs..

Referências

O Protocolo HTTP – Parte 05

Introdução

No post anterior (O Protocolo HTTP – Parte 04), vimos como funciona por dentro uma requisição GET de HTTP, como passar parâmetros pela URL, e como fazer um fonte em AdvPL com os tratamentos de códigos de retorno necessários. Agora, vamos ver mais alguns detalhes do GET , e ver por dentro o método POST do HTTP, primeiro pelo visão do protocolo e pelo Web Browse.

HTTP GET – Mais informações

Como já vimos anteriormente, uma requisição do tipo GET do HTTP possui restrições de tamanho e não deve ser usada para trafegar dados sensíveis. Um outro ponto muito importante é : Uma requisição do tipo GET somente é usada para CONSULTAR uma informação, e não atualizar ou mudar o estado de uma informação. Como o “dite de uma página” que começou a ser criado no post anterior ainda está muito simples, ainda tem muita coisa a ser vista sobre o HTML e como as páginas são processadas pelo Web Browser.

Quando precisamos mudar o estado de uma informação em um Web Site, usamos uma requisição POST, não apenas por ser mais flexível quando ao tamanho dos dados, mas também pela forma de passagem dos dados — dentro do “Corpo do HTTP” — não visível na URL — mas também pelos comportamentos das aplicações intermediárias envolvidas, a começar pelo Web Browser.

Fazendo um POST pelo Web Browser

Lembrando que o Web Browser carrega páginas HTML e arquivos do Web Server, para fazer uma requisição POST, precisamos fazer no mínimo uma página HTML com um formulário (form), indicando para onde (qual URL) os dados desse formulário será enviado, e como será enviado — GET ou POST. E, antes de mais nada, para conhecimento: Ao abrir o seu Web Browser e digitar uma URL qualquer, o Web Browse vai sempre fazer um GET daquela URL.

Agora, vamos incrementar a página de entrada do Web Site de testes, editando o arquivo “default.htm” da pasta “http-root” com o NOTEPAD ou outro editor de textos ASCII simples, e acrescentar a linha em negrito no arquivo … a nova página deve ficar assim:

<html>
<body">
<h3>Meu primeiro Web Site</h3>
<a href="testepost.htm">Teste de Post</a>
</body>
</html>

Ao executar o Totvs Application Server configurado para servidor de HTTP, e acessar o endereço http://localhost/, deve ser mostrado o seguinte:

O elemento <a > no HTML é uma “âncora”, onde podemos referenciar um conteúdo qualquer, e o Web Browser vai criar um LINK na tela para este conteúdo. No caso, criamos um link para uma nova página no Web Site, chamada “testepost.htm” (que ainda não existe no servidor). Se clicarmos no link “Teste de Post”, deve ser apresentada a tela de erro 404 – Not Found pelo Web Browser.

Agora, vamos criar — usando o editor de textos simples — o arquivo “testepost.htm”, dentro da pasta http-root, com o seguinte conteúdo:

<html>
<body>
<h3>Teste de Post</h3>
<hr>
<form name="mydataform" method="post" action="testepost.htm">
<input type="text" name="CAMPO1"></input>
<input type="text" name="CAMPO2"></input>
<input type="submit"/>
</form>
<hr>
<a href="/">Voltar ao Início</a>
</body>
</html>

Neste exemplo simples, criamos um formulário HTML, nomeado “mydataform”, definimos que o método de submeter esse formulário ao Web Server será “POST”, e na propriedade “action” definimos que ele deve abrir novamente o arquivo “testepost.htm”. Agora sim, ao clicar no link “Teste de Post” da página inicial do nosso site de testes, devemos ver o seguinte no Web Browser:

Testando o formulário

Preencha o primeiro campo na tela com “Ola”, o segundo com “Mundo”, e clique no botão “Enviar”. E, para nossa surpresa …

O que aconteceu ?

Bem, primeiramente … o Web Browse está “reclamando” do retorno que ele recebeu do servidor … ele fez a requisição POST do HTTP, para o arquivo “/testepost.htm”, os dados foram enviados — já vamos ver como confirmar isso –, mas o HTTP Server não retornou “nada”… sim, “nada”. Agora, vamos ver POR QUE.

  • Uma requisição de POST recebida pelo HTTP Server — no nosso caso estamos usando o Totvs Application Server — somente será “atendida” corretamente pelo Totvs Application Server, se ele estiver configurado para execução de páginas DINÂMICAS — conhecidas por “AdvPL ASP”. Calma, vamos ver isso em muitos detalhes mais para a frente. Lembre-se dos posts anteriores, que por hora o Totvs Application Server está configurado como servidor HTTP de páginas e arquivos ESTÁTICOS. Isto é, você pode montar um site “fixo”, com páginas e fotos … mas ele vai aceitar apenas GET, para mostrar as páginas e fazer download de arquivos. — Também vou montar exemplo disso …

De qualquer forma, o Web Browse não sabe disso… e ele vai fazer o que o formulário mandou .. POST para “testepost.htm”. Agora, vamos entrar nos detalhes de como o Web Browse montou o POST, usando neste caso o F12 do Google Chrome — que abre as “ferramentas do desenvolvedor” na página que você está acessando.

Refazendo o teste no Chrome

Estamos com o Google Chrome aberto em “http://localhost/testepost.htm&#8221;, vendo aquela tela de erro … clique no botão “Voltar” na parte superior esquerda da janela do Web Browser, e você deve ver novamente a tela “testepost.htm”, inclusive com os campos preenchidos.

Agora, pressione a tecla F12 do teclado, OU, pressione a combinação de teclas CONTROL + SHIFT + I — Isso abre a janela de ferramentas do desenvolvedor. Se a janela de desenvolvedor aparecer “dentro” da janela do Browse, aumente o tamanho da tela ( maximize a janela ). E você deve ver algo assim:

Agora,

Na janela do desenvolvedor, existem varias “abas”. Clique na aba “Network”, ela é que nos interessa nesse momento. Agora, vamos fazer a mágica acontecer … clique novamente no botão “Enviar”… e vamos ver o que aparece na janela do desenvolvedor, do lado direito:

Repare que a primeira linha das atividades de rede (Network) do Web Browser aparece em vermelho, indicando uma falha. O retorno recebido do HTTP Server foi um pacote de 0 bytes — ele não retornou “nada”… Agora, vamos ver o que foi enviado, clicando com o mouse em cima no nome “testepost.htm” que aparece em vermelho … Vou redimensionar a tela para a janela do desenvolvedor ficar maior ..

Ao clicar sobre uma requisição na lista de requisições feitas pelo Web Browser, ele mostra os detalhes dessa requisição, mostrando primeiro definições gerais, e logo depois em “Request Headers” tudo o que ele enviou no HTTP Header da requisição realizada — são muitas informações. As duas mais importantes agora são “Content-Type: application/x-www-form-urlencoded” , e “Content-Length: 23“.

Elas indicam que essa requisição de POST possui um Http Body, onde o conteúdo possui 23 bytes, e está CODIFICADO no formato “x-www-form-urlencoded“. Agora, se “rolarmos” essa tela para baixo, veremos algo interessante no final da página:

O Web Browse mostra que os valores preenchidos nos campos de input dentro do formulário foram enviados … Se clicarmos em “view source”, veremos COMO ele realmente enviou o corpo (Http Body) dessa requisição:

CAMPO1=Ola&CAMPO2=Mundo

Agora, conte quantas letras e símbolos aparecem nessa linha … Exatamente 23, que é o Content-length indicado no HTTP Header 😀 Após o HTTP Header, foi enviado um HTTP Body.

E, você também reparou ? Os dados estão “codificados” como se fossem os dados de uma requisição do tipo GET — URL Encoded … por isso o Web Browser informou que o Content-Type é “x-www-form-urlencoded” ( dados codificados igual a uma URL).

E AGORA ?

Faça um teste… volte para a página anterior, e no lugar da palavra “Mundo”, digite “Você S&A”.. e veja novamente nos detalhes da requisição, como os dados foram enviados pelo Web Browser:

CAMPO1=Ola&CAMPO2=Voc%EA+S%26A

Repare QUATRO coisas importantes:

  • Ao usar um caractere acentuado (ê – letra e minúscula com acento circunflexo), ela foi enviada utilizando o PERCENT ENCODING contendo o valor hexadecimal “EA” (recomendo fortemente a leitura de O que é CODEPAGE e ENCODING – Parte 01 e demais posts). Se consultarmos a tabela de caracteres do Windows (em português ou inglês, CP1252 ou CodePage 1252), veremos que o código Hexadecimal EA representa a letra “ê”
  • O espaço em branco foi codificado com o sinal de “+” (mais)
  • Como usamos no conteúdo do texto um caractere separador de parâmetros do protocolo “&”, ele foi codificado como “%26” — que na prática é o código hexadecimal do caractere “&”
  • E , por fim, o Content-length aumentou para 33 … veja, o tamanho do HTTP Body é o numero de bytes usados para representar os dados CODIFICADOS.

Mais informações, mais …

Se você ligar o log de requisições HTTP do Totvs Application Server, você vai reparar que o conteúdo do POST não e mostrado no log … ele não é mostrado pois pode ser muito grande, e codificado de formas que ele não pode ser visualmente visto no log, por isso o LOG das requisições recebidas mostra apenas o HTTP Header no Application Server.

Já existem outros posts sobre AdvPL ASP, mas que não entram “tão fundo” no protocolo … se voce quiser complementar seus conhecimentos, recomendo a leitura a partir do Protheus e AdvPL ASP – Parte 01 e demais posts da seqüência. Mais para frente vamos abordar boa parte do que foi mostrado sobre AdvPL ASP aqui no protocolo HTTP, porém com uma visão mais “interna” de como as coisas funcionam 😀

F.A.Q.

E, como cada post pode levantar muitas dúvidas, vamos ver algumas… respondidas de forma simples.

  • Esses exemplos de HTML, estão “corretos” ? Que versão de HTML esses arquivos estão usando ?

Então, os exemplos funcionam, o Web Browse “engole”, mas eles não estão usando as melhores práticas de codificação. Por convenção, a primeira linha de um arquivo HTML “elegante” informa a versão do HTML utilizada. Para indicar o uso de HTML5, a primeira linha do arquivo HTML deve ser

<!DOCTYPE html>

A partir de agora, nossos exemplos vão ser “elegantes”, e declarar o uso de HTML5.

  • Por quê foi usado o CodePage CP-1252 ? Em nenhum lugar isso aparece ?

Essa é a pergunta de 1 milhão ! No meio do HTTP Header da requisição feita pelo Web Browser, ele informa em alguns pontos o que ele “aceita” de retorno …

Accept-Encoding: gzip, deflate, br
Accept-Language: pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7,es;q=0.6,sv;q=0.5

Como o Browser está configurado para Português do Brasil, rodando em uma máquina Windows, e em nenhum momento o HTTP Server informa um detalhamento sobre quais caracteres estão sendo utilizados — ao pedir o arquivo “default.htm” ou “testepost.htm”, é informado no HTTP Header apenas que o content-type é “text/html”, sem mais detalhes inclusive no corpo do HTML — o Web Browser assume que ele deve tratar o retorno baseado no que ele aceita .. e trata os caracteres acentuados enviados na página como “CP1252”.

  • CP-1252, WINDOWS-1252 e ANSI são a mesma coisa ?

R: Na prática, SIM. Em versões anteriores do Windows, os CodePages utilizados pelo DOS ( 437 e 850, para inglês e português, respectivamente) foram nomeados como code-pages OEM, e a nova tabela ASCII do Windows como ANSI, e posteriormente WINDOWS-1252 e CP-1252. Existe um codepage muito parecido com o CP1252, usado ainda em algumas aplicações, chamado IS8-8859-15 — mas que possui diferenças na interpretação gráficas dos caracteres em faixas específicas. ** Curiosidade: Desde a primvera versão do Totvs Application Server, em meados de 1999, para recompilar e executar os códigos do ERP da Microsiga – ainda escritos para MS-DOS — havia a necessidade de conversões de OEM para ANSI e vice-versa, desde arquivos de texto e dados. Para isso, foram disponibilizadas as funções OemToAnsi() e AnsiToOem() na linguagem AdvPL, que estão presentes desde então.

  • Posso usar UTF8 dentro do meu arquivo HTML ?

SIM, mas isso exige que você informe dentro do HTML que os caracteres do corpo do HTML ( HTML Body – não confunda com HTTP Body, o corpo do HTML começa após a declaração “<body>” e termina na declaração “</body>” ). Vamos entrar em mais detalhes disso mais para a frente, mas a título de curiosidade, uma das formas de você especificar que o texto dentro do seu HTML está codificado como UTF-8, é acrescentar antes do <body> a sequencia de linhas abaixo:

 <head>
 <meta charset="UTF-8">
 </head>

Quer fazer um teste ? Abra o arquivo “testepost.htm” com um editor um pouco mais avançado ( recomendo o NOTEPAD++), troque a codificação do arquivo para UTF-8, e coloque acento na palavra início … faça isso, salve e reabra a página no Web Browse — sem colocar o meta-charset .. Você vai ver que no lugar do “í” vão aparecer duas letras esquisitas … ( UTF8 interpretado erroneamente como CP1252), e depois de acrescentar as linhas head e meta-charset, salvar e recarregar o arquivo pelo Web Browser … o acento aparece certinho ! Depois vamos ver como fazer isso usando o HTTP Header 😀

*** Usar UTF-8 traz outras implicações, que veremos mais adiante. A mais imediata é que, ao abrir um campo de digitação de informação para o usuário preencher, os dados enviados para o servidor HTTP estarão codificados em UTF-8. E isso exige que você trate isso adequadamente ao receber os dados para a execução de páginas dinâmicas. Não se preocupe com isso agora, veremos esses detalhes mais para a frente.

  • Existem outros Content-Type usados para POST ?

R: SIM, para muitas outras finalidades. Normalmente o Web Browser usa o Content-Type “multipart/form-data” para submeter arquivos (POST de formulários HTML com upload de arquivos para o HTTP Server), e diversos outros Content-Types são usados na comunicação entre aplicações por serviços SOAP ou REST sobre HTTP, como text/xml, text/json … calma que veremos isso em detalhes mais pra frente.

Conclusão

Quanto mais informações são apresentadas, mais informações são necessárias. Aos poucos vamos abrindo o que tem por baixo do capô do Web Browse e dos protocolos, e vamos ver várias formas de se fazer muita coisa ! Espero que este conteúdo sempre lhe seja útil, e novamente desejo a todos TERABYTES DE SUCESSO ❤

Referências

O Protocolo HTTP – Parte 04

Introdução

No post anterior, vimos as requisições GET que um Web Browse fez a um Totvs Application Server configurado como um servidor de páginas estáticas, e mais detalhes do protocolo, criando uma página html bem simples, e colocando ela como “tela de entrada” do site. Agora, vamos ver como fazer um programa AdvPL recuperar essa página do Web Site.

Função HTTPGET()

De uma forma simples, a função HttpGet() do AdvPL permite ao servidor de aplicação conectar em um Web Site, e recuperar um conteúdo usando a instrução GET do protocolo HTTP. Vamos ver isso um pouco mais a fundo, usando o Protheus Server como servidor de HTTP, e também dando atenção a funções auxiliares para os tratamentos dos possíveis retornos, entre outros truques interessantes. Para isso, vamos começar com um programinha bem simples:

#include "protheus.ch"

USER Function Myget1()
cHtml := HttpGet("http://localhost/")
conout("HTTP Body retornado:")
conout(cHtml)
return

Antes de mais nada, remova ou comente as configurações de log de requisições da seção http, senão o log vai ficar muito cheio … Ao compilar e executar o programa acima — pelo SmartClient , e verificar o log de console do Application Server, no nosso ambiente HTTP montado no post anterior, devemos obter o seguinte resultado:

HTTP Body retornado:
<html>
<body>
Meu primeiro Web Site 
</body>
</html>

Então, onde estão os Headers de retorno ?

A função HttpGet() retorna o “Html Body” retornado pelo Servidor Http acessado, mas também é possível retornar o Header da requisição usando o 5o parâmetro por referência.. vamos melhorar o programa, criando agora a função MyGet2():

USER Function Myget2()
cHeadRet := ''
cHtml := HttpGet("http://localhost/",,,,@cHEadRet)
conout("HTTP Header retornado:")
conout(cHEadRet)
conout("HTTP Body retornado:")
conout(cHtml)
return

Agora, ao executar o programa, vamos conferir o retorno:

HTTP Header retornado:
HTTP/1.1 200 OK
Date: Sat, 23 Jan 2021 02:33:11 GMT
Server: Application Web Server
MIME-version: 1.0
Content-type: text/html
Last-modified: Fri, 22 Jan 2021 23:33:29 GMT
Content-length: 55
X-Frame-Options: SAMEORIGIN

HTTP Body retornado:
<html>
<body>
Meu primeiro Web Site 
</body>
</html>

E se houver um erro ? O que pode dar errado ?

Então, muita coisa pode dar errado, desde não ser possível estabelecer a conexão HTTP com o host/servidor informado na URL, até ser possível estabelecer a conexão, mas o servidor não entender o pedido, ou não conseguir processar a requisição … Vamos começar com uma requisição de algo que não existe, alterando a linha 2 desse fonte, para pedir o arquivo “naoexiste.htm”:

cHtml := HttpGet("http://localhost/naoexiste.htm",,,,@cHEadRet)

Agora, vamos ver o que é retornado ao executar esse código, no log de console:

HTTP Header retornado:

HTTP Body retornado:
NIL

Então, com esse retorno a gente fica simplesmente “a pé”, não temos idéia do que aconteceu … Vimos apenas que o Header retornado por referencia está vazio, e o corpo HTTP retornado para a variável cHTML não contém uma string, mas o valor “NIL” (nulo) AdvPL.

Agora, vamos usar algumas funções auxiliares de tratamento de retorno de requisição.. e criar a função MyGet3():

USER Function Myget3()
  cHeadRet := ''
  cHtml := HttpGet("http://localhost/naoexiste.htm",,,,@cHEadRet)

  // Apos chamar a função HttpGet(), podemos obter alguns resultados \
  // usando algumas funções auxiliares 
  cHttpStatus := ''
  nHttpCode := HTTPGETSTATUS(@cHttpStatus)

  conout("HTTP Return Code ... "+cValToChar(nHttpCode))
  conout("HTTP Return Status ... "+cValToChar(cHttpStatus))

  If empty(cHEadRet)
    conout("HTTP Header nao retornado...")
  Else
    conout("HTTP Header retornado:")
    conout(cHEadRet)
  Endif

  if empty(cHtml)
    conout("HTTP Body nao retornado...")
  Else
    conout("HTTP Body retornado:")
    conout(cHtml)
  Endif

return

E, agora, ao executar esse código, temos algumas novas informações:

HTTP Return Code … 404
HTTP Return Status … Requested file not found
HTTP Header nao retornado:
HTTP Body nao retornado…

Repare que agora a gente sabe o que aconteceu … o status 404 retornado indica que a página solicitada (naoexiste.htm) realmente não existe no servidor … risos … Agora, vamos mexer no fonte novamente, para buscar explicitamente a página de entrada do site, chamada “default.htm”.

cHtml := HttpGet("http://localhost/default.htm",,,,@cHEadRet)

Agora, ao rodar o U_MYGET3 com esta alteração, obtemos o seguinte resultado:

HTTP Return Code … 200
HTTP Return Status … OK

( o resto está igual ) 

E se der erro de conexão ?

Vamos ver … alteramos o programa para buscar a página “default.htm”, mas agora vamos colocar uma porta DIFERENTE da 80 … Simples assim:

cHtml := HttpGet("http://localhost:8080/default.htm",,,,@cHEadRet)

Se não tiver nenhum serviço HTTP na porta 8080 do seu computador, essa requisição deve falhar .. então, vamos ver o resultado da execução do programa com esta alteração no log de console:

HTTP Return Code … 10061
HTTP Return Status … Connection refused.
HTTP Header nao retornado…
HTTP Body nao retornado…

O erro retornado não é do HTTP, mas da interface de SOCKETS do sistema operacional … A conexão foi “recusada” pois não tem nenhum serviço “ouvindo” na porta 8080. Agora, vamos colocar um nome de host ou de um site que não existe …

cHtml := HttpGet("http://naoexiste/default.htm",,,,@cHEadRet)

E, temos então o seguinte retorno:

HTTP Return Code … 11001
HTTP Return Status … Host not found.
HTTP Header nao retornado…
HTTP Body nao retornado… ... 

E os outros parâmetros ?

Sim, tem vários …. Vamos ver primeiro o segundo parâmetro da função. Ele chama-se “cGetParms” — vide documentação completa na TDN no link https://tdn.totvs.com/display/tec/HTTPGet — e serve para a passagem de parâmetros pela URL.

A operação de GET do HTTP permite eu solicitar um arquivo ou página ao servidor HTTP, e na própria URL podemos colocar um ou mais parâmetros, na forma “?chave=valor[,chave2=valor2…]” . Estes parametros são conhecidos como parâmetros de busca ou “search parameters”. E existem duas formas de passar estes parâmetros no AdvPL : Eles podem estar diretamente na URL — informada no primeiro parâmetro — ou separados da URL, no segundo parâmetro da chamada da função HttpGet() — mas não podem estar em ambas ao mesmo tempo.

A passagem de parâmetros via URL deve ser feita após a página solicitada, usando o separador “?”(interrogação), seguido de um identificador ou chave, o sinal de igual “=” e um determinado valor. Para especificar mais combinações de chave e valor, use o separador “&” (e comercial). Por exemplo:

http://localhost/default.htm?id=no&page=1

Dessa forma, estou solicitando uma requisição para o protocolo HTTP, para o host “localhost”, solicitando a página “default.htm”, e informando pela URL os parâmetros “id” com o valor “no”, e “page” com o valor “1”. A nossa página default não tem avaliação de parâmetros, então qualquer coisa informada será “ignorada”, mas será transmitida ao HTTP Server.

Para passar a URL acima para a função HTTPGET, podemos passar os parâmetros juntos da URL ou separados. Vale lembrar que, não é necessário colocar a “?” interrogação para passar os parâmetros separados, veja abaixo os exemplos, o resultado final será o mesmo :

cReturn := HttpGet("http://localhost/default.htm?id=no&page=1")
cReturn := HttpGet("http://localhost/default.htm","id=no&page=1")

Observações muito importantes

Além dos separadores a serem utilizados para indicar o inicio dos parâmetros de busca (“?”), o separador entre o identificador (ou chave) “=” e o separador para informar mais parâmetros “&”, existem alguns pontos muito importantes:

  • Nos parâmetros de busca, em uma chave deve-se evitar usar caracteres especiais, símbolos e acentuação.
  • Caso seja necessário o uso de caracteres deferentes do alfabeto alfanumérico (letras maiúsculas e/ou minúsculas não acentuadas e números), quaisquer caracteres diferentes disso devem ser escritos em um formato chamado “URL Encoding” ou “Percent Encoding”

PERCENT QUEM ?

Cada caractere especial ou diferenciado pode ser convertido no formato “%HH”, onde HH é a representação HEXADECIMAL — com letras maiúsculas — do código ASCII do caractere a ser enviado. Por exemplo, se eu quero passar a chave “nome” com o valor “C&A” — lembrando que “&” é um caractere separador da lista de parâmetros, eu devo obter o código ASCII da letra “&” — que é 38 — e converter para hexadecimal ( 26 ), então informar na URL http://localhost/default.htm?nome=C%26A

Outro detalhe importante, é que o caractere “espaço em branco”, pode ser passado de duas formas como parâmetro da URL: Pode ser %20, ou o sinal de soma “+”.

E como isso entra no HTTP Header ?

Simples assim, todos os parâmetros passados por GET são colocados na primeira linha do HTTP Header:

GET /default.htm?id=no&page=1 HTTP/1.1
Host: localhost
(... other fields ...)

Isso tem limite ?

Essa é uma pergunta importante… sim, têm limite sim. Mas o limite é arbitrário, depende da capacidade do servidor HTTP e do Web Browser utilizado. Em linhas gerais, a recomendação é que não se use mais do que 2000 caracteres em uma URL. A maioria dos Web Browses e HTTP Servers lida tranqüilamente dentro desse limite. Veja mais informações das referências.

Posso passar dados sensíveis pela URL ?

Então, poder você pode, mas não deve … afinal os dados ficam expostos na URL … para passar dados sensíveis, e maiores que 2000 bytes, veremos mais pra frente o comando POST do protocolo HTTP, que permite receber parâmetros pela URL (igual ao GET), mas também permite receber parâmetros pelo HTTP Body 😀

Conclusão

Aos poucos vamos mergulhando no HTTP, e ao mesmo tempo vamos ver o que o AdvPL nos oferece para lidar com essa camada. Espero que as informações lhe sejam muito úteis, e como de costume lhes desejo TERABYTES DE SUCESSSO !!!

Referências