Algoritmos – Parte 02 – Permutações

Introdução

No post anterior (Algoritmos – Parte 01 – Loterias), vimos a criação de um algoritmo para realizar combinações simples, que pode ser usado na maioria das loterias numéricas. Agora, vamos ver um algoritmo de permutação — Algoritmo de Heap — e ver como fazer a portabilidade de um pseudo-código para AdvPL.

Algoritmo de Heap

O Algoritmo de Heap é até hoje a forma mais optimizada de gerar todas as possibilidades de permutações em um conjunto de elementos. A permutação é um processo pelo qual podemos criar sequências não repedidas dos elementos de um conjunto, alterando a sua ordem. Por exemplo, partindo de um conjunto de três números (1, 2 e 3), podemos criar as seguintes permutações:

1 2 3 
1 3 2 
2 1 3
2 3 1 
3 1 2
3 2 1

Calcula-se o número total de possibilidades de permutação de um conjunto com a fórmula P(m) = m! –> Onde P é o número de possibilidades de permutação e m é o número de elementos do conjunto, e  “!” é o símbolo da operação fatorial. Por exemplo, em um conjunto de 3 elementos, temos 3! ( 3! = 3*2 = 6) conjuntos ordenados resultantes.

Pseudocódigo

A partir da Wikipedia, por exemplo, podemos obter o pseudocódigo do algoritmo — uma representação em linguagem quase natural da sequência de operações nos conjuntos de dados para se chegar ao resultado. Esta versão do pseudo-código é a não-recursiva.

procedure generate(n : integer, A : array of any):
    c : array of int

    for i := 0; i < n; i += 1 do
        c[i] := 0
    end for

    output(A)

    i := 0;
    while i < n do
        if  c[i] < i then
            if i is even then
                swap(A[0], A[i])
            else
                swap(A[c[i]], A[i])
            end if
            output(A)
            c[i] += 1
            i := 0
        else
            c[i] := 0
            i += 1
        end if
    end while

Agora, vamos converter isso para AdvPL, primeiro de uma forma bem simples, depois de uma forma mais elaborada. Inicialmente, vamos fazer uma tradução “crua” para o AdvPL, porém funcional.

STATIC Function Generate( n , A )
Local c := {} , i
For i := 1 to n
  aadd(c,0)
Next
output(A)
i := 0 
While i < n
  If  c[i+1] < i
    if ( i % 2 ) == 0 
      swap(A, 1 , i+1)
    else
      swap(A, c[i+1]+1, i+1)
    end if
    output(A)
    c[i+1]++
    i := 0
  Else
    c[i+1] := 0
    i++
  EndIf
Enddo


STATIC Function swap(aData,nPos1,nPos2)
Local nTemp := aData[nPos1]
aData[nPos1] := aData[nPos2] 
aData[nPos2] := nTemp
Return

STATIC Function output(A)
Local i, R := ''
For i := 1 to len(A)
  If !empty(R)
    R += ', '
  Endif
  R += cValToChaR(A[i])
Next
conout(R)
Return

Diferenças na Implementação

A primeira diferença nós vemos logo de início ao usar os Arrays em AdvPL. O pseudo-código parte da premissa que um Array de N posições é endereçado de 0 a N-1 — Isto é, o primeiro elemento do Array é o elemento 0 (zero.) Já em AdvPL, o primeiro elemento do array é 1 (um). Logo, nós mantemos toda a lógica do programa inicial, inclusive as variáveis como se o array fosse base 0 (zero), porém na hora de endereçar os elementos do array, somamos uma unidade. Logo:

if c[i] < i

foi transformado para

if c[i+1] < i

A função swap() tem como objetivo trocar os elementos do array, um pelo outro. Como em AdvPL os arrays são passados por referência, podemos implementar a função de troca guardando o valor do elemento informado em uma variável local, depois atribuímos o conteúdo do segundo elemento informado sobre o primeiro, e então atribuímos o conteúdo salvo do primeiro elemento no segundo — vide função swap(). O diferencial dela em relação ao pseudocódigo é que eu passo para ela em AdvPL três parâmetros: O Array, e as duas posições a serem trocadas.

No pseudo-código, para verificar se um determinado numero é par (even em inglês), pode ser em AdvPL verificando  se o resto da divisão por dois é zero. Para isso, poderíamos usar a função mod(), ou de forma mais prática, o operador “%”.

if ( i % 2 ) == 0

Outro ponto de atenção é justamente a chamada da função swap() quando o número não for par. Veja no pseudocódigo:

swap(A[c[i]], A[i])

Agora, em AdvPL, a implementação ficou assim:

swap(A, c[i+1]+1, i+1)

Reparem que o array c[] guarda uma posição de um array. Como estamos trabalhando com array em base 1, eu devo somar 1 para recuperar o elemento do array c, e como o seu resultado será usado para indicar uma posição do array A[], eu também preciso somar 1.

Já a função output(), cujo entendimento óbvio é mostrar um dos conjuntos obtidos na permutação, implementamos simplesmente recebendo o Array , e criando uma string com o conteúdo de seus elementos separados por vírgula. Para testar o fonte acima, vamos usar a seguinte função:

User Function Permuta()
Generate( 4 , {'A' ,'B' ,'C' ,'D' } )
Return

Após salvarem, compilarem e executarem o programa acima, o resultado no log de console do Protheus Server deve ser:

A, B, C, D
B, A, C, D
C, A, B, D
A, C, B, D
B, C, A, D
C, B, A, D
D, B, A, C
B, D, A, C
A, D, B, C
D, A, B, C
B, A, D, C
A, B, D, C
A, C, D, B
C, A, D, B
D, A, C, B
A, D, C, B
C, D, A, B
D, C, A, B
D, C, B, A
C, D, B, A
B, D, C, A
D, B, C, A
C, B, D, A
B, C, D, A

Conclusão

Vendo o fonte assim, prontinho, parece fácil. Porém, eu comecei resolvendo trocar o nome das variáveis da implementação em AdvPL ao transcrever o pseudocódigo, e o programa não gerava os números corretamente. Joguei fora a primeira versão e parti do código original, mantendo o nome das variáveis, então funcionou.

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

Referências

Algoritmos – Parte 01 – Loterias

Introdução

Nos primeiros posts no Blog sobre programação — vide Desmistificando a análise de sistemas e Desmistificando a programação — foi colocada de forma simples a ideia de programar alguma coisa, como sendo apenas uma sequência de instruções e decisões para se realizar uma tarefa. E, realmente é simples assim, o que precisamos fazer é usar corretamente a gramática da linguagem para realizar as tarefas em pequenas etapas, criando tarefas maiores e reaproveitando tarefas menores.

Algoritmos

Uma sequência de instruções para realizar uma tarefa específica pode ser chamada de “Algoritmo” — verifique a definição na Wikipedia – Algoritmo. Não é algo aplicado apenas a programação de computadores.

Consultando também a Wikipédia – Lista de Algoritmos, podemos ver vários exemplos de problemas específicos resolvidos com sequências de operação. Como o algoritmo em si trata apenas da sequência de operações para obter um resultado, ele pode ser implementado em várias linguagens de programação.

Alguns dos mais conhecidos e utilizados são algoritmos de ordenação e busca de dados, compressão, sorteio de números (números randômicos), criptográficos (segurança da informação), entre outros. Na internet podemos obter não apenas vários modelos de algoritmos, mas também várias implementações dos mesmos em várias linguagens de programação.

Algoritmos em AdvPL

Vamos pegar um problema qualquer, e criar um algoritmo para resolvê-lo, um por tópico. Um problema bem interessante de ser resolvido usando um programa de computador, é por exemplo lidar com análise combinatória. Podemos usar um algoritmo de geração de combinações (ou Combinação Simples) para, por exemplo, criar massas de dados explorando as combinações de vários parâmetros para testes de funções, combinar números para cartões de loteria — adoro esse algoritmo — entre outras finalidades. Vamos ver como realizar uma análise combinatória eficiente para loterias em AdvPL.

Combinação para Loterias

Em um determinado cartão de loteria, podemos preencher no mínimo N números diferentes, dentre um total de X números disponíveis, para concorrer a um prêmio caso uma quantidade mínima de números sorteados estejam presentes entre os N números do cartão.

Quando queremos por exemplo, determinar todas as possibilidades de combinação de Y números em cartões de X números, utilizamos um recurso de análise combinatória chamado “Combinação Simples”. Atualmente, para alguns tipos de loterias,o próprio bilhete de apostas já permitem você fazer uma aposta com mais números do que a aposta mínima. Neste caso, a sua aposta equivale a todas as possibilidades de combinação simples dos Y números em um cartão de X números.

MEGA-SENA

Vamos tomar como exemplo a MEGA-SENA, onde a aposta mínima é um bilhete com seis números preenchidos, a um valor de R$ 3,50 . Podemos preencher de 6 a 15 números em um cartão ou bilhete, porém o preço da aposta sobe proporcionalmente ao número de combinações simples da quantidade de números preenchidos em blocos de seis números. Veja a tabela abaixo, de apostas e preços, e a quantidade de apostas de 6 números equivalente.

NUMEROS NO  | VALOR DA     | EQUIVALE A <n> 
BILHETE     | APOSTA       | APOSTAS de 6 números
------------+--------------+------------------------          
6 números   | R$ 3,50      |    1 aposta 
7 números   | R$ 24,50     |    7 apostas 
8 números   | R$ 98,00     |   28 apostas 
9 números   | R$ 294,00    |   84 apostas 
10 números  | R$ 735,00    |  210 apostas 
11 números  | R$ 1617,00   |  462 apostas 
12 números  | R$ 3234,00   |  924 apostas 
13 números  | R$ 6006,00   | 1716 apostas 
14 números  | R$ 10510,50  | 3003 apostas 
15 números  | R$ 17517,50  | 5005 apostas

Quantidade de combinações possíveis

A fórmula para determinar uma combinação simples C, de n números em conjuntos de k elementos é:

C(n,k) = n! / k! * (n-k)!

Onde “!” significa o valor FATORIAL do elemento, “*” é o operador de multiplicação, “/” é o operador de divisão.

Na matemática, o fatorial (AO 1945: factorial) de um número natural n, representado por n!, é o produto de todos os inteiros positivos menores ou iguais a n. A notação n! foi introduzida por Christian Kramp em 1808.

Vamos pegar por exemplo uma combinação de 12 elementos em conjuntos de 6 elementos — equivalente ao cartão de 12 números da MEGA-SENA.

C(12,6) = 12! / 6! * (12-6)!
C(12,6) = 12! / 6! * 6!
C(12,6) = 479001600 / 720 * 720 
C(12,6) = 479001600 / 518400 
C(12,6) = 924

Logo, antes mesmo de começar a fazer a combinação, podemos determinar o número de resultados possíveis.

Combinando elementos

A lógica para realizar a combinação dos elementos é basicamente a mesma, não importa a quantidade de elementos a combinar ou o tamanho do conjunto resultante. Porém, dependendo da forma que ela for implementada, ela tende a ficar mais pesada quanto maior a quantidade de elementos a combinar. Vamos pegar por exemplo a combinação de 7 elementos numéricos, de 1 a 7, em conjuntos de 6 elementos.

C(7,6) = 7! / 6! * (7-6)!
C(7,6) = 7! / 6! * (1)!
C(7,6) = 5040 / 720 * 1
C(7,6) = 7

Logo de antemão, sabemos que esta combinação resultará em 7 resultados. Agora, vamos fazer a combinação. Partindo dos números já ordenados, a primeira combinação é:

1 2 3 4 5 6

Para determinar o próximo resultado, pegamos o próximo numero do conjunto da última posição e incrementamos uma unidade:

1 2 3 4 5 7

Ao tentar fazer a mesma operação para pegar o terceiro resultado, o número 7 é o último elemento do conjunto. Então, precisamos incrementar o número na posição anterior — quinta posição. Quando fazemos isso, o número da posição que atingiu o limite deve ser o próximo elemento relativo a posição anterior. Logo, o próximo resultado será:

1 2 3 4 6 7

Repetindo novamente esta operação, começando da direita para a esquerda, na sexta posição, o elemento 7 já é o último do conjunto. Então vamos para a quinta posição. O próximo elemento desta posição seria o 7, porém ele é o último elemento da combinação, e eu não estou na última posição do conjunto de resultado, isto significa que ele já está em uso em uma posição posterior. Logo, vamos a posição anterior — quarta posição — e incrementamos o número 4 para 5, obtendo o resultado:

1 2 3 5 6 7

Repetindo os passos anteriores, os números 5, 6 e 7 não podem ser incrementados pois já são os elementos finais da combinação. Logo, incrementamos na terceira posição o 3 para 4, depois na segunda posição de 2 para 3, depois na primeira, de 1 para 2, e depois acabou, pois não há como incrementar mais nada.

1 2 4 5 6 7 
1 3 4 5 6 7 
2 3 4 5 6 7

Com isso obtivemos os 7 resultados possíveis. Agora, vamos colocar essa regra em um código fonte. Para facilitar o uso deste recurso, vamos criá-lo como uma Classe em AdvPL. Após construir e otimizar o algoritmo, a primeira versão ficou assim (fonte ACOMB.PRW):

#include "protheus.ch"

CLASS ACOMB FROM LONGNAMECLASS

  DATA nCols
  DATA aElements
  DATA nSize
  DATA aControl
  DATA aMaxVal
  DATA nPos

  METHOD NEW()
  METHOD GETCOMB()
  METHOD NEXTCOMB()
  METHOD GETTOTAL() 

ENDCLASS

METHOD NEW( nCols , aElements ) CLASS ACOMB
Local nI , nMax
::nCols := nCols
::aElements := aElements
::nSize := len(aElements)
::nPos := nCols
::aControl := {}
::aMaxVal := {}
nMax := ::nSize - ::nCols + 1
For nI := 1 to ::nCols
  aadd(::aControl,nI)
  aadd(::aMaxVal, nMax )
  nMax++
Next
Return self

METHOD GETCOMB() CLASS ACOMB
Local nI , aRet := array(::nCols)
For nI := 1 to ::nCols
  aRet[nI] := ::aElements[ ::aControl[nI] ] 
Next 
Return aRet

METHOD NEXTCOMB() CLASS ACOMB
If ::aControl[::nPos] + 1 > ::aMaxVal[::nPos]
  ::nPos := ::nPos - 1 
  If ::nPos < 1 
    Return .F. 
  Endif
  If ::NEXTCOMB()
    ::nPos := ::nPos + 1
    ::aControl[::nPos] := ::aControl[::nPos-1]+1
  Else
    Return .F. 
  Endif
Else
  ::aControl[::nPos]++
Endif
Return .T.

METHOD GETTOTAL() CLASS ACOMB
Local nFat1 := Fatorial( ::nSize )
Local nFat2 := fatorial( ::nCols )
Local nFat3 := Fatorial( ::nSize - ::nCols )
Local nTot := nFat1 / ( nFat2 * nFat3 ) 
Return nTot

STATIC Function Fatorial(nNum)
Local nI := nNum - 1
While nI > 1 
  nNum *= nI 
  nI--
Enddo
Return nNum

A propriedade aControl controla os contadores de cada posição do conjunto de retorno, e a propriedade aMaxVal eu já determino qual é o valor máximo de um determinado contador para a coluna ou posição atual. O Método GetComb() retorna um array com os elementos combinados, e o método NextComb() determina qual a próxima combinação da sequência, atualizando o array aControl. Quando o método NextComb() retornar .F., não há mais combinações possíveis. E, usando o método GetTotal(), eu determino quantas combinações são possíveis.

Para testar a classe acima, vamos usar o seguinte fonte:

User Function ACombTst()
Local nI
Local nResult := 1
Local lEcho := .F. 
Local nTimer
Local nCols , aData := {}

// Monta 35 dezenas para combinar 
For nI := 1 to 35
  aadd(aData,strzero(nI,2))
Next
// Combinação em conjuntos de 6 dezenas 
nCols := 6
oLoto := ACOMB():New( nCols , aData )
nTotal := oLoto:GetTotal()
conout("Elementos ...... "+cValToChar(len(aData)) )
conout("Conjunto ....... "+cValToChar(nCols) )
conout("Combinacoes .... "+cValToChar(nTotal) )
If lEcho
  aRet := oLoto:GETCOMB()
  conout("("+strzero(nResult,6)+") "+aRet[1]+" "+aRet[2]+;
         " "+aRet[3] +" "+aRet[4]+" "+aRet[5]+" "+aRet[6] )
endif
nTimer := seconds()
While oLoto:NEXTCOMB()
  nResult++
  If lEcho
    aRet := oLoto:GETCOMB()
    conout("("+strzero(nResult,6)+") "+aRet[1]+" "+aRet[2]+;
           " "+aRet[3] +" "+aRet[4]+" "+aRet[5]+" "+aRet[6] )
  Endif
Enddo
nTimer := seconds() - nTimer
conout("Resultados ..... "+cValToChar(nResult) )
conout("Tempo .......... "+str(nTimer,12,3)+" s.")
Return

No meu notebook, determinar as 1623160 combinações possíveis de 35 números em blocos de 6 demorou aproximadamente 3,5 segundos. Vejamos o log de console:

Elementos ...... 35
Conjunto ....... 6
Combinacoes .... 1623160
Resultados ..... 1623160
Tempo .......... 3.531 s.

Sim, neste teste eu não peguei os resultados, e não imprimi os resultados, eu apenas chamei em loop o método NextComb() até ele calcular todas as combinações. Para a aplicação imprimir os resultados em console, basta colocar .T. na variável lEcho e recompilar o fonte. Não é necessário dizer que, haverá uma boa diferença de desempenho quando você passar a resgatar cada uma das combinações e mostrar/gravar cada uma no log de console.

Dividindo o número total de combinações pelo tempo que a aplicação demorou, a rotina gerou aproximadamente 459688 resultados por segundo. Eu diria que isso é um tempo fantasticamente rápido. O tempo necessário para gerar estas combinações é simplesmente irrelevante perto do tempo que você deve gastar para, por exemplo, registrar estas combinações em uma tabela ou Banco de Dados.

Outras Loterias

Vamos pegar agora a Lotofácil. Cada cartão tem 25 dezenas, a aposta mínima é um bilhete com 15 dezenas preenchidas, e o prêmio máximo é você acertar os 15 números. As chances de você ganhar jogando um bilhete com a aposta mínima é 1 em 3.268.760. Afinal, se você fizer todas as combinações de 25 dezenas em grupos de 15, é exatamente esse o número de combinações possível. Vamos ver isso rodando o programa ? Basta alterar o programa de teste para criar 25 dezenas, e atribuir 15 para nCols.

Elementos ...... 25
Conjunto ....... 15
Combinacoes .... 3268760
Resultados ..... 3268760
Tempo .......... 12.002 s.

Pode fazer a mesma coisa para a MEGA-SENA, são 60 elementos em conjunto de 6. Garanto que vai demorar bem mais que 12 segundos, afinal a MEGA-SENA são 50.063.860 possibilidades ou combinações, mais de 15 vezes do que a Loto Fácil. Vamos rodar, apenas pra ver quanto tempo demora.

Elementos ...... 60
Conjunto ....... 6
Combinacoes .... 50063860
Resultados ..... 50063860
Tempo .......... 94.201 s.

Conclusão

Mesmo sabendo de tudo isso, até agora eu não ganhei na loteria, pois matematicamente as chances de um número qualquer ser sorteado é uma para o conjunto de números. As chances de acertar todos os números de uma loteria com uma aposta é uma para o conjunto de possibilidades / combinações dos números que eu posso colocar no cartão. Matematicamente, as chances de qualquer aposta — qualquer uma, inclusive por exemplo as dezenas “01 02 03 04 05 06” , ou “05 10 15 20 25 30”, são as mesmas. A única forma de aumentar matematicamente a sua probabilidade ou chance de acerto é jogar mais cartões diferentes. E, no final das contas, você pode jogar um cartão com 16 números e não acertar nenhum.

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

Referências

 

 

Protheus como Servidor de FTP

Introdução

Quando eu comentei um pouco sobre as capacidades do Servidor de Aplicação Protheus Server, em um post mais antigo, eu mencionei que ele não apenas servia a conexões do SmartClient para rodar aplicações AdvPL, mas também que ele poderia ser um servidor de HTTP, com páginas estáticas e dinâmicas — usando AdvPL ASP — bem como TELNET e FTP. No post de hoje, vamos explorar o que a gente puder sobre como usar um Protheus Server como servidor de FTP.

Configuração Mínima

Imagine que você quer usar um Protheus Server como um FTP Server, com acesso anônimo — sem criticar usuário e senha — e apenas disponibilizar uma estrutura de pastas para Download. Neste caso, a configuração mínima para este serviço, seria acrescentar no arquivo de configuração do Protheus (appserver.ini) a seção [FTP], com as seguinte chaves:

[ftp]
Enable=1
Port=21
Path=c:\Protheus12LG\EnvLight\ftp
CanAcceptAnonymous=1

Especificamos a porta padrão (21), o acesso anônimo habilitado, e o path raiz do FTP, a partir do qual as conexões terão acesso de Download. Dentro da pasta configurada em “path”, eu coloquei um arquivo chamado leiame.txt, vamos ver este acesso através de um cliente FTP nativo do Windows, usando o comando “ftp” em linha de comando.

C:\Users\siga0>ftp -A localhost
Connected to NOTE-JULIOW-SSD.
220 Connected to FTP server
331 Anonymous access allowed
502 Command not implemented
331 Anonymous access allowed, send email name as PASS
220 Logon successful
230 Welcome to Application Server FTP!
Anonymous login succeeded for siga0@NOTE-JULIOW-SSD
ftp> dir
250 PORT command successful
150 Opening ASCII mode data connection
-r-xr-x--- 1 owner group 39 Nov 04 20:10 leiame.txt
226 Transfer Complete
ftp: 74 bytes received in 0.00Seconds 37.00Kbytes/sec.
ftp> ls
250 PORT command successful
150 Opening ASCII mode data connection
leiame.txt
226 Transfer Complete
ftp: 15 bytes received in 0.00Seconds 15.00Kbytes/sec.
ftp>

Através do parâmetro “-A” na linha de comando, informamos ao cliente FTP que o Login deverá ser anônimo. Caso este parâmetro não seja especificado, você deve entrar manualmente com o usuário “anonymous“. Uma senha deve ser informada, mas não será validada — pode ser qualquer coisa, inclusive “anonymous”.

Após feito o login, executamos os comandos “ls” e “dir” para recuperar a lista de arquivos e pastas disponíveis para download. Vamos então fazer o download do arquivo “leiame.txt”:

ftp> get leiame.txt
250 PORT command successful
150 RETR command started
226 Transfer Complete
ftp: 39 bytes received in 0.00Seconds 39000.00Kbytes/sec.
ftp>

De dentro do FTP Client do Windows, podemos executar um comando do sistema operacional, prefixando ele com o sinal de exclamação. Por exemplo, para verificarmos  o conteúdo do arquivo na pasta local após o Download, vamos executar o comando “type”.

ftp> !type leiame.txt
Exemplo de Configuraτπo Mφnima de FTP
ftp>

No caso, o texto do arquivo justamente é “Exemplo de Configuração Mínima de FTP”. Porém, como a página de código do Prompt de Comando está com o CodePage 437 (CodePage original do IBM-PC, também conhecido por OEM-US, CP437 ou DOS Latin US), a acentuação é mostrada com outros caracteres. Para ver o arquivo da forma correta, ele pode ser aberto pelo NOTEPAD ou qualquer outro editor de textos, OU você deve digitar no Prompt de Comando a instrução abaixo, antes de abrir o cliente FTP:

mode con cp select=1252

Com isso, o seu Prompt de Comando vai usar o CodePage do Windows, CP1252, que também é o CodePage usado pelo Protheus. Para ver a lista de instruções implementadas na camada interna do FTP Server, use o comando remotehelp

ftp> remotehelp
214-The following commands are implemented
USER PASS ACCT QUIT PORT RETR
STOR DELE RNFR PWD CWD CDUP
MKD RMD NOOP TYPE MODE STRU
LIST HELP
214 HELP command successful
ftp>

Caso você tente fazer um upload no FTP nesta conexão, a operação será negada.

ftp> put upload.txt
250 PORT command successful
550 Access is denied
ftp>

Usando outros clientes FTP

Normalmente basta desligar o “Passive Mode” na configuração do programa que você usa como Cliente de FTP (SCP, WINSCP, etc.) que a conexão e operações são realizadas sem maiores problemas.

Implementando mais controles

Na configuração mínima, o FTP está totalmente aberto para download de qualquer arquivo colocado a partir da pasta configurada na chave PATH, para qualquer cliente que conecte usando a identificação “anonymous” — ou seja, sem autenticação alguma. No máximo, usando por exemplo um recurso externo, como um Firewall, você pode permitir por exemplo apenas receber conexões FTP na porta 21 a partir de um ou mais endereços de rede, e apenas isso.

Para atender a necessidade de permitir ou restringir operações por usuário, existe a necessidade de desligar o acesso anônimo, configurar algumas chaves adicionais na seção [FTP], e criar algumas funções AdvPL no repositório para serem acionadas por estas chaves. Vamos direto para o exemplo completo:

[ftp]
Enable=1
Port=21
Path=c:\Protheus12LG\EnvLight\ftp
RPCEnv=envlight
CheckPassword=U_FTPPASS
GetUserPath=U_FTPPATH
CheckUserOper=U_FTPOPER

Primeiramente, removemos o acesso anônimo. Então, criamos uma chave chamada RPCENV, onde colocamos o nome  do environment (ambiente) existente neste Protheus Server, responsável por executar as funções AdvPL que serão colocadas para validar algumas operações do FTP.

Configuração CHECKPASSWORD

Quando um usuário conectar no FTP e informar o usuário e senha, será chamada a função U_FTPPASS(), que receberá como parâmetros o usuário e senha informados pelo cliente de FTP. Se esta função retornar .T., o Protheus Server responde ao cliente de FTP que o login foi aceito, caso contrário responde uma mensagem de erro e nega o acesso. Vejamos o exemplo abaixo:

User Function FTPPass(cUser,cPass)
cUser := lower(cUser)
if ( cUser == "root" )
  if( cPass == "root" )
    Return .T.
  Endif
Endif
Return .F. 

Neste exemplo, permitimos apenas um usuário chamado “root”, com a senha “manager”  a entrar no FTP.

Configuração GETUSERPATH

Imagine que, eu quero fornecer, por exemplo, uma pasta raiz de FTP diferenciada para alguns usuários. Para isso, eu crio uma função AdvPL — no nosso exemplo, USER FUNCTION FTPPATH(), que recebe como parâmetros o usuário e senha informados no login. A função deve retornar um PATH completo no servidor onde está sendo executado o Protheus Server, e esta pasta será o diretório raiz de FTP. Vamos ao exemplo:

User Function FTPPath(cUser,cPass)
cUser := lower(cUser)
If cUser == "siga0984"
  return "C:\Protheus12LG\ftp"
Endif
Return "C:\Protheus12LG\ftp\anonymous"

Neste caso, quando o usuário de FTP for “siga0984“, ele têm acesso à pasta raiz do FTP, quando qualquer outro usuário somente terá acesso a partir da pasta “anonymous”.

Configuração CHECKUSEROPER

Caso você queira permitir UPLOAD de arquivos no FTP, ou outras operações que modifiquem conteúdo, como apagar arquivo, criar ou apagar uma pasta, é necessário implementar uma função AdvPL para ser chamada pelo Protheus Server para autorizar estas operações, usando a configuração CheckUserOper — no nosso exemplo, vamos implementar a função U_FTPOPER(). Ela recebe três parâmetros: O usuário de login no FTP Server, a senha utilizada, e o comando enviado pelo Cliente do FTP.

Apenas alguns comandos são desviados para esta função, por exemplo STOR <arquivo>, DELE <arquivo>, MKD <pasta>,  RMD <pasta>, e dois comandos não implementados para renomear arquivo (RNFR e RNTO).

  • STOR = Upload de arquivo
  • DELE = Apagar arquivo
  • MKD = Criar pasta 
  • RMD = Remover pasta
  • RNFR <arquivo1> e RNTO <arquivo2> — Renomar arquivo1 para arquivo2

Caso a função AdvPL retorne .T., a operação é autorizada. Caso contrário, negada. Vamos ao nosso exemplo:

User Function FTPOPER(cUser,cPass,cOper)
Local cCmd
cUser := lower(cUser)
If cUser == 'root'
  cCmd := left(cOper,4)
  If cCmd $ "STOR,DELE"
    Return .T.
  Endif
Endif
Return .F.

No exemplo acima, permitimos apenas ao usuário “root” a possibilidade de fazer upload ou mesmo de apagar um arquivo remotamente.

Resultados dos Testes

Os testes realizados mostraram que alguns clientes de FTP, por exemplo o WINSCP, usou uma sintaxe para a troca de pasta (comando CWD) que o FTP Server do Protheus não entendeu, mas funcionou adequadamente com o cliente FTP nativo do Windows em linha de comando, e um cliente de FTP do Altap(r) Salamander.

Mesmo com as implementações em AdvPL, o objetivo de ter um servidor nativo de FTP no servidor Protheus é atender a necessidade de integrações entre sistemas, normalmente em ambientes restritos — ou fechados. Ele não oferece logs de utilização nativos, não permite interceptar outros comandos para implementar por exemplo restrição de acesso de usuário para uma pasta ou arquivo, etc.

Devido a questões ligadas a implementação do FTP em múltiplas plataformas, é recomendado usar os nomes de arquivos sem espaços em branco, sem acentuação, e com letras minúsculas, e utilizar sub-pastas se e somente se realmente necessário. Qualquer demanda maior, que exija mais controles, como um FTP publicado na Internet para clientes e parceiros ter acesso a múltiplos arquivos, eu pessoalmente recomendo a utilização de uma aplicação especializada em ser Servidor de FTP, que vai lhe oferecer nativamente muito mais controles do que o Protheus Server como FTP Server.

Conclusão

Para cada tamanho de problema, existe uma solução adequada. O Protheus como servidor de FTP não foi criado para competir com um FTP Server de mercado, mas apenas para ter uma alternativa simples e nativa para integração entre sistemas, onde não são necessários níveis muito avançados de controle. Porém, para o que ele se propõe, ele dá conta do recado.

Em um próximo post, vamos explorar a classe client de FTP do Protheus Server —  chamada TFTPCLIENT() — para conectar e realizar operações de Cliente de FTP conectando-se em um FTP Server configurado também no Protheus.

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

Referências

 

 

Dicas valiosas de programação – Parte 04

Introdução

Continuando o assunto de dicas valiosas de programação, vamos abordar alguns assuntos relacionados a JOBS (Programas sem interface), pontos de atenção, alternativas de controle, etc.

Considerações sobre JOBS

Em tópicos anteriores, vimos que existem várias formas de subir um ou mais jobs em um serviço do Protheus Server. A maior dificuldade dos JOBS consiste em saber o que ele está fazendo, e como ele está fazendo. O fato do Job não ter nenhum tipo de interface torna esse trabalho um pouco diferente dos demais programas.

Quando usamos um pool de JOBS, como por exemplo os processos de WebServices ou de portais WEB (JOB TYPE=WEBEX), definimos o número inicial (mínimo) de processos, numero máximo, e opcionalmente mínimo livre e incremento. Logo, não precisamos nos preocupar se o servidor recebe mais uma requisição e todos os processos que estão no ar estão ocupados — se o número máximo de processos configurado não foi atingido, o próprio Protheus Server coloca um ou mais jobs no ar.

Gerando LOG de um JOB

Normalmente quando precisamos acompanhar o que um determinado JOB está fazendo, podemos usar o IDE e/ou TDS para depurar o Job, ou caso isto não seja uma alternativa para a questão, o programa pode ser alterado para emitir mensagens a cada etapa de processamento. Uma das alternativas normalmente usadas — e mais simples de usar — é usar a função AdvPL conout() nos programas envolvidos, para registar no log de console do Protheus Server mensagens sobre o que cada processo está fazendo. Para diferenciar os processos, podemos usar a função ThreadID() do AdvPL, para identificar o número da thread que gerou a mensagem.

Outra alternativa interessante, inclusive mais interessante que usar o log de console do servidor de aplicação, é fazer com que o job crie um arquivo de LOG dele próprio em disco, usando por exemplo a função fCreate(), criando o arquivo em uma pasta a partir do RootPath do ambiente, usando por exemplo um prefixo mais o numero da thread atual mais o horário de inicio do job como nome do arquivo — para ficar fácil saber quais logs são de quais JOBS — e gravar os dados de LOG dentro desse arquivo usando a função fWrite()  — lembrando de inclusive gravar os caracteres chr(13)+chr(10) ao final de cada linha — estes caracteres de controle indicam uma quebra de linha em um arquivo no padrão Windows. Para Linux, a quebra de linha padrão é apenas chr(10).

Acompanhando a execução de um JOB

Quando você cria um determinado JOB para alguma tarefa longa, pode ser interessante saber em que ponto ou etapa da tarefa o JOB está trabalhando em um determinado momento. A solução mais leve, é você criar um nome de uma variável global — aquelas que são acessadas através das funções PutGlbVars() e GetGlbVars() — e alimentar dentro do JOB a variável global com a etapa atual do processo, enquanto um outro programa (em outro processo, com interface por exemplo) consulta a variável para saber qual é a tarefa interna do Job em andamento.

Desta forma, um programa externo pode consultar — através de variáveis globais com nomes pré-definidos — o status de não apenas um, mas vários jobs sendo executados no servidor de aplicação atual. Basta criar identificadores únicos não repetidos antes de iniciar os processos.

Ocorrências de Erro Críticas

Mesmo que o seu JOB possua um tratamento de erro, cercado com BEGIN SEQUENCE … END SEQUENCE e afins, as ocorrências de erro de criticidade mais alta não são interceptadas ou tratadas. Desse modo, se você apenas consulta uma variável global para pegar o status de um Job, ele pode ter sido derrubado ou ter finalizado com uma ocorrência critica de erro, e o programa que está esperando ou dependendo de um retorno dele nem sabe que ele já não está mais sendo executado.

Não há contorno para tentar manter no ar um JOB que foi finalizado por uma ocorrência crítica, porém você pode descobrir se ele ainda está no ar ou não, usando alguns recursos, por exemplo:

  1. Além da variável global para troca de status, faça o JOB obter por exemplo um Lock Virtual no DBAccess ou um Lock em Disco — busque no blog por “MUTEX” e veja algumas alternativas. A ideia é usar um recurso nomeado em modo exclusivo, que é liberado automaticamente caso o JOB seja finalizado por qualquer razão. Se o seu programa que espera retorno do JOB está sem receber nenhuma atualização, verifique se o JOB está no ar tentando fazer um bloqueio do recurso que o JOB está usando. Se o seu processo conseguiu o bloqueio, o JOB foi pro vinagre…
  2. Verifique se o seu processo ainda está no ar usando — com moderação — por exemplo a função GetUserInfoArray() — ela retorna um array com as informações dos processos em execução na instância atual do Protheus Server. Para isso, pode ser necessário que o JOB que foi colocado em execução use uma variável global para o processo principal e de controle de jobs saber qual é o ThreadID deste processo, para ser possível um match com o retorno da GetUserInfoArray().

Seu processo principal pode não saber o que aconteceu com o processo filho, mas sabe que ele não está mais no ar, e saiu antes de dar um resultado. Isso muitas vezes é suficiente para você estudar uma forma de submeter o processo novamente, ou de encerrar o processo principal informando que houve um término anormal e os logs de erro do sistema devem ser verificados, ao invés de esperar para sempre um JOB que já não está mais lá.

Conclusão

Quanto mais nos aprofundamos em um tema, mais temas aparecem para nos aprofundarmos 🙂 E, é claro que veremos exemplos de uso práticos destes mecanismos, com fonte e tudo, nos próximos posts !!!

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

Referências

 

Dicas valiosas de programação – Parte 03

Introdução

Neste post, vamos a uma dica muito importante e específica do AdvPL: Como proteger a chamada de novas funções implementadas na linguagem AdvPL — e evitar o erro “function <xxx> has compilation problems. Rebuild RPO”

Funções do AdvPL

Ao escrevermos uma aplicação em AdvPL, os desenvolvedores podem usar funções básicas da linguagem AdvPL, que por serem nativas da linguagem, estão compiladas e publicadas dentro do Application Server (executável/dlls), e as funções de Framework e Produto, compiladas dentro do RPO (Repositório de funções e classes AdvPL).

Compilation Problems …

Quando é criada uma nova função básica da linguagem AdvPL, ela estará disponível para uso apenas quando você atualizar o seu Application Server para uma build igual ou superior a qual a função foi disponibilizada. O problema é que, uma vez que você implemente uma chamada desta nova função dentro de um código AdvPL, para que esta chamada funcione, você precisa compilar o seu fonte usando o binário mais novo — que já têm a função — e executar o seu fonte com ele.

Caso você compile seu fonte com um binário mais antigo — que não tem a função — e tente executar o fonte em um binário mais novo, ou faça o contrário — compile usando um binário novo e tente executar em um binário antigo, ocorre o erro “function <xxx> has compilation problems. Rebuild RPO”. E este erro não ocorre durante a execução do fonte, mas ocorre durante a carga da função na memória para ser executada.

Este comportamento é uma amarração de segurança, para evitar que inadvertidamente uma função AdvPL compilada no RPO possa conflitar — ou mesmo tentar sobrescrever — uma função básica da linguagem AdvPL.

Onde isso pode ser um problema ?

Imagine que você dá manutenção em um código já existente e que funciona, e em uma build mais nova do Application Server, uma função nova foi publicada, que pode tornar o seu processo mais rápido.  A mão “coça” pra implementar a chamada da função, porém você não sabe se esse fonte será compilado com um binário mais velho ou novo, nem como garantir que o seu cliente vá atualizar a Build do Application Server para rodar o código novo. Neste caso, você precisa que seu código continue funcionando em uma build antiga ou nova, e independentemente se ele foi compilado em uma build nova ou não.

Como fazer a implementação protegida ?

São apenas alguns passos simples, mas que precisam ser feitos desta forma.

  • Verifique se a nova função existe em tempo de execução, chamando a função FindFunction(), passando como parâmetro entre aspas o nome da função.  Caso a função exista na build em uso, ela retorna .T.
  • Coloque o nome da função nova da linguagem AdvPL que você quer chamar, dentro de uma variável local caractere — por exemplo cFuncName
  • Após verificar se a função existe, faça a chamada para esta função usando o operador de macro-execução, com a seguinte sintaxe:

[<retorno> := ] &cFuncName.([parm1][,parm2][,…])

Por exemplo, vamos imaginar que você quer usar uma nova função do AdvPL, chamada FindClass() — que serve para dizer se uma determinada classe existe no ambiente onde a função está sendo executada.

user function tstfindcls()
Local cFnName := 'findclass'
Local lExist := .F.
If FindFunction(cFnName)
   lExist := &cFnName.("TGET")
   If lExist
      MsgInfo("A Classe TGET existe")
   Else
      MsgStop("A classe TGET nao existe")
   Endif
Else
   MsgStop("Função "+cFnName+" não encontrada", ;
           "Atualize o Application Server")
Endif
Return

Dessa forma, mesmo que você compile esse fonte AdvPL em uma build antiga ou nova, ele vai rodar corretamente em uma build nova, e caso seja executado em uma build antiga — que ainda não tenha a função FindClass — o fonte não vai apresentar erro de carga, mas vai identificar que a função não existe e mostrar a mensagem desejada.

Conclusão

Espero que esta dica, mesmo que  “curtinha”, seja de grande valia para todos os desenvolvedores AdvPL. Desejo novamente a todos TERABYTES de sucesso !!! 😀

Referências

 

 

Acelerando o AdvPL – Parte 02 (ERRATA)

Pessoal, boa tarde,

Hoje eu estava lendo novamente o código do post anterior, referente ao exemplo de um cache em array, com tamanho limitado de elementos, e uma optimização para subir no array cada item pesquisado, para que os itens mais pesquisados sejam encontrados sequencialmente em primeiro lugar, e encontrei um erro de lógica em um ponto da rotina … vamos ver apenas este pedaço do código:

Function GetJurosPad( cTipoCtr )
Local nJuros := -1
Local cChave := xFilial('ZZ1')+cTipoCtr
Local nPos := ascan(aJurosPad,{ |x| x[1] == cChave })
Local aTmp
If nPos > 0 
 // Achou no cache. 
 If nPos > 1 
 // nao esta na primeira posição, sobe uma 
   aTmp := aJurosPad[nPos]
   aJurosPad[nPos] := aJurosPad[nPos-1]
   aJurosPad[nPos-1] := aTmp
 Endif
 // Incrementa cache HITS -- achei no cache, economia de I/O
 nJurosHit++
 Return aJurosPad[nPos][2]
Endif

Reparem que a busca foi feita na declaração da variável nPos, usando Ascan(). Porém, caso o item encontrado não seja o primeiro elemento do array, o elemento encontrado sobe uma posição (usando a variável aTmp para fazer o SWAP). E, no final das contas, o valor retornado é a segunda coluna do elemento após a troca. Neste caso, a função somente vai retornar o valor correto caso o valor buscado seja encontrado no primeiro elemento do array.

Para  corrigir isto, precisamos armazenar o resultado encontrado antes de fazer a troca no array, e retornar este valor, veja a correção abaixo:

Function GetJurosPad( cTipoCtr )
Local nJuros := -1
Local cChave := xFilial('ZZ1')+cTipoCtr
Local nPos := ascan(aJurosPad,{ |x| x[1] == cChave })
Local aTmp
If nPos > 0 
 // Achou no cache. 
 nJuros := aJuros[nPos][2]
 If nPos > 1 
   // nao esta na primeira posição, sobe uma 
   aTmp := aJurosPad[nPos]
   aJurosPad[nPos] := aJurosPad[nPos-1]
   aJurosPad[nPos-1] := aTmp
 Endif
 // Incrementa cache HITS -- achei no cache, economia de I/O
 nJurosHit++
 Return nJuros
Endif

Lembrem-se da importância de revisar e testar o código. Ser um bom programador e ter experiência não vai te livrar de cometer erros, uma revisão de código e um bom teste nunca é uma questão de desconfiança, mas sim procedimento.

Até a próxima, pessoal 😀

Pense fora da caixa e resolva problemas

Hoje não veremos nenhuma linha de código, separei este post para compartilhar um pouco das experiências diárias do profissional de TI em lidar com problemas, e contar um “causo” (pelo menos pra mim) interessante, onde pensar fora da caixa foi fundamental para chega a uma solução elegante.

Problemas existem, e podem acontecer

Na trajetória do desenvolvimento de Software, volta e meia aparecem problemas de diferentes magnitudes e complexidades. Normalmente existe solução para todos os problemas, porém algumas soluções podem ser mais caras do que conviver com o problema ou contorná-lo. A primeira coisa que aprendemos na informática é como utilizá-la para resolver problemas. A segunda coisa é como resolver o problema quando uma falha na implementação de uma solução informatizada torna-se o problema.

A primeira coisa que devemos entender sobre o problema é a sua gravidade. E, é claro que isto é uma medida totalmente relativa. Um cliente não conseguir emitir uma nota fiscal pode parecer algo não grave, porém se este cliente depende da emissão desta nota para liberar um caminhão com produtos perecíveis, com data e horário certos para entrega sob pena de multa ou rescisão do contrato, não emitir essa nota é gravíssimo.

Depois, precisamos pensar em uma solução para o problema. Se o problema é grave e urgente, precisamos de um contorno para o problema. Dá-se preferência a um contorno elegante, mas se o que você têm para o momento é um arame e um durepoxi, e isso vai contornar o problema, apodere-se do espírito McGuyver que existe em você, e contorne. Mas pelo amor de Deus, não venda a gambiarra que você fez como a solução do problema. Explique que você está botando uma fita isolante no problema para o cliente não ficar parado, e que você vai trabalhar na solução definitiva assim que você entender as causas do problema.

Se você já está na fase de trabalhar no problema, ou teve que ir pra ela direto pois nenhum contorno passa pela sua cabeça, pesquise o que você puder sobre o problema. Procure entender o problema e suas causas, a partir de que momento ele manifestou-se, sob que circunstâncias, quando e onde ocorre, com que frequência, se é generalizado ou localizado, se ocorre em outros pontos do sistema, em um servidor ou em todos, em um tipo de programa ou qualquer programa, quais foram os eventos recentes que antecederam o início do surgimento do problema. É quase uma atividade de detetive. Procure as pessoas certas para levantar estas informações, limpe eventuais “sujeiras” na comunicação, e não tenha medo de perguntar “o que você quer dizer com isso ?”, quando você tiver qualquer dúvida a respeito das informações que permeiam o assunto em foco.

Muitas vezes você encontra um contorno para o problema — e posteriormente a solução — durante a análise dos detalhes do problema, ou tomando banho, ou tomando um café depois do almoço — sim, essas coisas acontecem. Se você já avaliou as possíveis causas e tudo parece certo, procure alguém mais experiente para trocar uma ideia, ou divida o problema com seus colegas de equipe, às vezes um detalhe que você não viu nas informações fornecidas  é visto na hora por outro analista, e dá um novo norte na busca pela causa do problema.

Alguns tipos de problemas são mais difíceis de reproduzir em um ambiente controlado do que Tamanduá-Bandeira em cativeiro. A causa pode estar relacionada a alguma particularidade do ambiente ou da configuração do sistema, ou mesmo até um defeito físico em uma parte da infra-estrutura. Quando as causas prováveis foram analisadas sem sucesso, comece a analisar as improváveis. Olhe tudo com uma lupa, revalide as possibilidades que foram eliminadas durante a análise, mas não desista. Existem pequenos problemas que se escondem por trás de um problema maior, e grandes problemas por trás de pequenos problemas. Não está achando o problema grande, verifique se existem problemas menores, eles podem ser consequência ou parte da causa do problema grande.

O caso do CNAB

No final das contas, entrei tanto dentro do problema que quase fugi do foco principal do post, o pensamento “fora da caixa”. Um cliente do ERP estava com dificuldade de utilizar a integração CNAB (padrão Febraban para remessa e retorno de títulos para compensação bancária). Quando o arquivo de remessa era gerado e enviado ao banco, alguns títulos do arquivo eram rejeitados pelo banco, pois o código de verificação do título que era enviado no arquivo, segundo o banco, não estava com os valores corretos.

O código de verificação de cada título era gerado pelo ERP, na geração do arquivo de remessa, e este código era composto por uma série de operações aritméticas de soma e multiplicação a partir de determinadas informações do título, como valor, vencimento, código do cliente e afins. A conta realizada com os valores daquele título resultava em um número inteiro muito grande, que esbarrava em um limite aritmético da linguagem AdvPL. O cálculo feito pela calculadora do Windows batia com o valor esperado pelo banco, que estava correto, mas o cálculo feito dentro do programa usando a aritmética da linguagem não comporta um número com mais de 16 dígitos significativos, havendo perda de precisão e consequentemente valor incorreto do código.

Isto foi antes do uso do Protheus Server, hoje chamado de TOTVS Application Server, muito anos antes de serem implementadas funções no AdvPL para lidar com números decimais de ponto fixo, que possuem precisão de até 64 dígitos. No final das contas, a fórmula matemática de cálculo escrita em AdvPL da forma originalmente proposta somente seria executada corretamente se a linguagem usasse um tipo de dado numérico com precisão superior ao ponto flutuante.

Depois de sair da minha mesa e tomar um café, levei meu caderno de rabiscos para passear, e já estava pensando em fazer um programa em outra linguagem e chamar ele de dentro do ERP para fazer aquela conta, quando eu olhei para os números e percebi o óbvio: Eu consigo fazer essa conta em um pedaço de papel, usando um lápis e as mesmas regrinhas básicas de soma e multiplicação que aprendemos no colégio primário. Se dá pra fazer no papel, dá pra fazer no computador, exatamente da mesma forma.

Eu só precisava de soma e multiplicação, e não importa se a operação ficasse um pouco mais lenta do que um cálculo em ponto flutuante nativo da linguagem, afinal seriam apenas algumas chamadas para cada título do borderô. Então, em menos de uma hora eu fiz duas funções para somar e multiplicar números recebidos como String. Bastou re-escrever as fórmulas para o CNAB daquele banco para pegar os dados do título e passar para as novas funções, trabalhando com os números como String.

Conclusão

Análise, programação e desenvolvimento de sistemas é uma atividade que requer jogo de cintura e criatividade para lidar com o universo de desafios de resolução de problemas, é saber usar o conhecimento e as ferramentas que se têm na mão, ser capaz de apontar mais de uma alternativa para resolver um problema, e escolher a que melhor atende a necessidade.

Pensar fora da caixa é uma expressão onde a caixa normalmente significa o limite do seu pensamento criativo, ou os paradigmas assimilados e embutidos nos problemas que você normalmente lida no dia a dia. A busca por novas soluções é constante, e avaliar um problema por novos ângulos e abordagens pode fazer toda a diferença. Não desista, a resposta está lá fora, esperando que você a encontre !!!

Até o próximo post, pessoal 😉

Informações Adicionais

As funções desenvolvidas para cálculo numérico de soma e multiplicação com strings estão disponíveis no ERP Microsiga, e documentadas na TDN nos links http://tdn.totvs.com/display/public/mp/FUNCAO+GENERICA+-+SOMASTR
e http://tdn.totvs.com/pages/releaseview.action?pageId=6814818 , e foram mantidas por compatibilidade. As funções implementadas na linguagem AdvPL para cálculo com números que exigem precisão maior do que a suportada por variáveis numéricas com ponto flutuante lidam com o tipo numérico “F” do AdvPl (Fixed size decimals), e estão também documentadas na TDN, no link http://tdn.totvs.com/display/tec/Decimais+de+Ponto+Fixo