Ir para conteúdo

MergeSort


felipe lopes

Posts Recomendados

16 horas atrás, fredericopissarra disse:

Não... Se espera ajuda num forum, deve ser público.

Bom dia, adicionei o arquivo em anexo, eu consigo ler os valores e salvar em um vetor, mas não descobrir como passar esse vetor pra chamada do merge e depois receber os valores de volta e imprimir na tela

main.c

Link para o comentário
Compartilhar em outros sites

Eu não verifiquei a lógica do merge sort. As modificações que eu fiz foram apenas na parte de leitura dos arquivos, e minha "investigação" do seu código passou por 4 etapas:

1. A formatação do arquivo CSV

Como eu não sabia se seu arquivo CSV estava com os dados dispostos em linhas ou colunas, criei um arquivo CSV com os dados dispostos em cada formato para ver o que acontecia com seu código:
- Se os dados estiverem dispostos em coluna, a primeira entrada será ignorada, e o programa processará o CSV a partir da segunda linha. Aí, o programa dá erro;

- Se os dados estiverem dispostos em linha, a primeira linha será ignorada, e o programa processará o CSV a partir da segunda linha (como no caso acima). Se os dados estiverem todos na mesma linha, essa linha será ignorada, o loop de leitura dos dados se encerrará, e com ele, o programa.

O que estava provocando isso tudo era esse trecho bem estranho:

while (fgets(buf, 1024, fp)) {
        row_count++;

        if (row_count == 1) {
            continue;
        }
		...
    }

Na leitura da primeira linha, como row_count sempre ficara igual a 1, o código entra nesse if e executa o comando continue, que encerra a iteração atual do loop e passa diretamente para a próxima. Se o CSV tiver apenas uma linha, o comando dentro do while já terá chegado no final do arquivo, o que encerra o loop, fechando o arquivo em seguida, e fechando o programa, sem fazer nada.

Mas esse não era o único problema do código, tampouco o mais crítico.

 

2. A leitura dos dados do arquivo para o programa

A lógica de leitura do arquivo parecia bastante complicada. Aparentemente, a intenção do código era encontrar onde estavam as aspas do CSV para passar o conteúdo entre aspas para uma outra função que deveria passar os dados para uma estrutura chamada "dataset". Ocorre que C possui umas ferramentas bem legais para manipulação de strings. Deixei o loop de leitura do arquivo assim:

while (fgets(buf, 1024, fp)) {
    field_count = 0;
    token = strtok(buf, ",");
    
    while (token != NULL) {
        remove_quotes(token_wq, token);
        process_field(field_count++, token_wq, i);
        token = strtok(NULL, sep);
    }
    
    i++;
    printf("\n");
     
    if (i > TAMANHO)
        break;
}

Primeiro, strtok (em string.h) procurará por ocorrências de "," em buf. Por exemplo, se a linha do csv for  "bla","ble","bli", a primeira ocorrência de strtok gerará a string "bla". As ocorrências seguintes, onde NULL aparece como argumento, irão, em sequência, gerar "ble" e "bli". Como não há "," depois do "bli", strtok retornará NULL no token, indicando que aquela linha não possui mais tokens.

Outra função que aparece é a remove_quotes. Essa função eu criei para eliminar as aspas das strings:

void remove_quotes(char* dst, char* src)
{
    char* quote;
    int first_quote;
    int last_quote;
    int len;
    
    quote = strchr(src, '"');
    first_quote = quote - src;
    
    while (quote != NULL) {
        quote = strchr(quote+1, '"');
        if (quote) {
            last_quote = quote - src;
        }
    }

    len = last_quote - first_quote - 1;

    strncpy(dst, src+first_quote+1, len);
    dst[len] = '\0';
}

Essa função utiliza a função strchr, que localiza a primeira ocorrência de um char (que nesse caso, é um char de aspas duplas) dentro da string iniciada pelo ponteiro indicado (que aqui eu chamei de src). Essa função encontrará as primeiras aspas da string, armazena sua posição relativa na string de origem, e vai encontrando, em seguida, aspas duplas até a função strchr retornar NULL. A posição relativa das últimas aspas duplas é armazenada.

Depois de encontrar a posição relativa das primeiras e últimas aspas duplas na string de origem (src), o tamanho da string entre aspas é calculada (len). No final, strncpy copia exatamente o tamanho necessário (len) a partir do primeiro caractere depois das primeiras aspas duplas da origem na string de destino (dst)

Como a função strncpy não termina a string de destino com um caractere nulo, ele é adicionado. (dst[len] = '\0).

Caso precise, é bem fácil modificar essa função caso a origem (src) já não possua aspas duplas.

 

3. Passando os dados para a estrutura dataset

Essa parte lida com a função process_field. Mais coisas foram retiradas de lá do que colocadas, mas as alterações feitas nela são importantes para entender como funcionam os tipos de dados em C. A função process_field ficou bem enxuta:

void process_field(int field_count, char *value, int i) {    
    if (field_count == 0) {
        vet[i].ano = atoi(value);
        printf("Ano:\t%d", vet[i].ano);
    }
}

A função que interpreta o valor da string em value é a atoi. Ela "transforma" uma string em um número inteiro. Essa função se faz necessária pois uma string é um vetor de chars, e não são podem ser imediatamente convertidas para o número que é lido ou informado pelo usuário humano. Sem levar isso em consideração, algumas linhas do código original estavam tentando fazer essa conversão direta, como por exemplo:

vet[i].ano = value;

vet.ano é um número inteiro, e value é um ponteiro de char. Quando se realiza esse comando, o computador entenderá que você quer armazenar o valor de endereço de memória armazenado em value no campo ano de vet. Ainda que value aponte para uma string do tipo "1234", o valor de value, em si, nada tem a ver com o número que está sendo representado naquela string. Outro caso onde houve essa confusão foi nessa linha:

printf("vetor[i] %s", vet[i].ano);

Nesse comando, você especificouum formato %s para algo do tipo int, quando se espera que o tipo seja char*. Essa inconsistência pode gerar vários tipos de comportamentos indefinidos, como tentar ler vet.ano como um endereço de memória de uma sequência de chars terminadas em \0, ou outras coisas que podem travar o programa ou sua máquina, a depender do quão bem seu sistema lida com esse tipo de exceção.

Outras linhas que eu removi por razões práticas, e que não fazem sentido foram essas:

int tam = sizeof(vet[i].ano)/sizeof(int);
merge(vet[i].ano,0,tam-1);

tam sempre será igual a 1, pois vet.ano é inteiro, e sizeof(int)/sizeof(int) = 1. A linha que chama a função merge também não faz sentido, pois assim vcê estará fazendo vários merge sort de um único inteiro. Caso a intenção fosse inserir o ano na estrutura de modo a já fazer o sort e ter um vetor já arrumado logo após a inserção, até poderia ser feito durante a leitura dos dados, porém, o merge sort não funcionará eficientemente. Caso essa seja uma das necessidades da sua solução, procure por heap sort.

 

4. Corrigindo a tipagem dos parâmetros das funções do merge sort

Por fim, alterei os tipos dos parâmetros das funções de sort. Como você está lidando com um vetor de dataset, onde cada dataset contém um int, as funções devem recever ponteiros para dataset, e não ponteiros para inteiro.

//void intercala(int *x, int inicio, int fim, int meio) {
void intercala(dataset* x, int inicio, int fim, int meio) {
    ...
}

//void merge(int *x, int inicio, int fim) {
void merge(dataset* x, int inicio, int fim) {
    ...
}

As linhas que se referiam aos valores de x, agora fazem para x.ano.

No fim, após a leitura dos dados, chamei a função merge e imprimi na tela o resultado:

#define TAMANHO 103

...

int main(int argc, char** argv) {
    ...
    
    // executa o sort
    merge(vet, 0, TAMANHO-1);
    
    // print na tela
    for (j = 0; j < TAMANHO; ++j) {
        printf("%d\n", vet[j].ano);
    }

    return 0;
}

O código com todas as modificações estão em anexo

main.c

Link para o comentário
Compartilhar em outros sites

19 horas atrás, Vinicius Antunes Osti disse:

Eu não verifiquei a lógica do merge sort. As modificações que eu fiz foram apenas na parte de leitura dos arquivos, e minha "investigação" do seu código passou por 4 etapas:

1. A formatação do arquivo CSV

Como eu não sabia se seu arquivo CSV estava com os dados dispostos em linhas ou colunas, criei um arquivo CSV com os dados dispostos em cada formato para ver o que acontecia com seu código:
- Se os dados estiverem dispostos em coluna, a primeira entrada será ignorada, e o programa processará o CSV a partir da segunda linha. Aí, o programa dá erro;

- Se os dados estiverem dispostos em linha, a primeira linha será ignorada, e o programa processará o CSV a partir da segunda linha (como no caso acima). Se os dados estiverem todos na mesma linha, essa linha será ignorada, o loop de leitura dos dados se encerrará, e com ele, o programa.

O que estava provocando isso tudo era esse trecho bem estranho:


while (fgets(buf, 1024, fp)) {
        row_count++;

        if (row_count == 1) {
            continue;
        }
		...
    }

Na leitura da primeira linha, como row_count sempre ficara igual a 1, o código entra nesse if e executa o comando continue, que encerra a iteração atual do loop e passa diretamente para a próxima. Se o CSV tiver apenas uma linha, o comando dentro do while já terá chegado no final do arquivo, o que encerra o loop, fechando o arquivo em seguida, e fechando o programa, sem fazer nada.

Mas esse não era o único problema do código, tampouco o mais crítico.

 

2. A leitura dos dados do arquivo para o programa

A lógica de leitura do arquivo parecia bastante complicada. Aparentemente, a intenção do código era encontrar onde estavam as aspas do CSV para passar o conteúdo entre aspas para uma outra função que deveria passar os dados para uma estrutura chamada "dataset". Ocorre que C possui umas ferramentas bem legais para manipulação de strings. Deixei o loop de leitura do arquivo assim:


while (fgets(buf, 1024, fp)) {
    field_count = 0;
    token = strtok(buf, ",");
    
    while (token != NULL) {
        remove_quotes(token_wq, token);
        process_field(field_count++, token_wq, i);
        token = strtok(NULL, sep);
    }
    
    i++;
    printf("\n");
     
    if (i > TAMANHO)
        break;
}

Primeiro, strtok (em string.h) procurará por ocorrências de "," em buf. Por exemplo, se a linha do csv for  "bla","ble","bli", a primeira ocorrência de strtok gerará a string "bla". As ocorrências seguintes, onde NULL aparece como argumento, irão, em sequência, gerar "ble" e "bli". Como não há "," depois do "bli", strtok retornará NULL no token, indicando que aquela linha não possui mais tokens.

Outra função que aparece é a remove_quotes. Essa função eu criei para eliminar as aspas das strings:


void remove_quotes(char* dst, char* src)
{
    char* quote;
    int first_quote;
    int last_quote;
    int len;
    
    quote = strchr(src, '"');
    first_quote = quote - src;
    
    while (quote != NULL) {
        quote = strchr(quote+1, '"');
        if (quote) {
            last_quote = quote - src;
        }
    }

    len = last_quote - first_quote - 1;

    strncpy(dst, src+first_quote+1, len);
    dst[len] = '\0';
}

Essa função utiliza a função strchr, que localiza a primeira ocorrência de um char (que nesse caso, é um char de aspas duplas) dentro da string iniciada pelo ponteiro indicado (que aqui eu chamei de src). Essa função encontrará as primeiras aspas da string, armazena sua posição relativa na string de origem, e vai encontrando, em seguida, aspas duplas até a função strchr retornar NULL. A posição relativa das últimas aspas duplas é armazenada.

Depois de encontrar a posição relativa das primeiras e últimas aspas duplas na string de origem (src), o tamanho da string entre aspas é calculada (len). No final, strncpy copia exatamente o tamanho necessário (len) a partir do primeiro caractere depois das primeiras aspas duplas da origem na string de destino (dst)

Como a função strncpy não termina a string de destino com um caractere nulo, ele é adicionado. (dst[len] = '\0).

Caso precise, é bem fácil modificar essa função caso a origem (src) já não possua aspas duplas.

 

3. Passando os dados para a estrutura dataset

Essa parte lida com a função process_field. Mais coisas foram retiradas de lá do que colocadas, mas as alterações feitas nela são importantes para entender como funcionam os tipos de dados em C. A função process_field ficou bem enxuta:


void process_field(int field_count, char *value, int i) {    
    if (field_count == 0) {
        vet[i].ano = atoi(value);
        printf("Ano:\t%d", vet[i].ano);
    }
}

A função que interpreta o valor da string em value é a atoi. Ela "transforma" uma string em um número inteiro. Essa função se faz necessária pois uma string é um vetor de chars, e não são podem ser imediatamente convertidas para o número que é lido ou informado pelo usuário humano. Sem levar isso em consideração, algumas linhas do código original estavam tentando fazer essa conversão direta, como por exemplo:


vet[i].ano = value;

vet.ano é um número inteiro, e value é um ponteiro de char. Quando se realiza esse comando, o computador entenderá que você quer armazenar o valor de endereço de memória armazenado em value no campo ano de vet. Ainda que value aponte para uma string do tipo "1234", o valor de value, em si, nada tem a ver com o número que está sendo representado naquela string. Outro caso onde houve essa confusão foi nessa linha:


printf("vetor[i] %s", vet[i].ano);

Nesse comando, você especificouum formato %s para algo do tipo int, quando se espera que o tipo seja char*. Essa inconsistência pode gerar vários tipos de comportamentos indefinidos, como tentar ler vet.ano como um endereço de memória de uma sequência de chars terminadas em \0, ou outras coisas que podem travar o programa ou sua máquina, a depender do quão bem seu sistema lida com esse tipo de exceção.

Outras linhas que eu removi por razões práticas, e que não fazem sentido foram essas:


int tam = sizeof(vet[i].ano)/sizeof(int);
merge(vet[i].ano,0,tam-1);

tam sempre será igual a 1, pois vet.ano é inteiro, e sizeof(int)/sizeof(int) = 1. A linha que chama a função merge também não faz sentido, pois assim vcê estará fazendo vários merge sort de um único inteiro. Caso a intenção fosse inserir o ano na estrutura de modo a já fazer o sort e ter um vetor já arrumado logo após a inserção, até poderia ser feito durante a leitura dos dados, porém, o merge sort não funcionará eficientemente. Caso essa seja uma das necessidades da sua solução, procure por heap sort.

 

4. Corrigindo a tipagem dos parâmetros das funções do merge sort

Por fim, alterei os tipos dos parâmetros das funções de sort. Como você está lidando com um vetor de dataset, onde cada dataset contém um int, as funções devem recever ponteiros para dataset, e não ponteiros para inteiro.


//void intercala(int *x, int inicio, int fim, int meio) {
void intercala(dataset* x, int inicio, int fim, int meio) {
    ...
}

//void merge(int *x, int inicio, int fim) {
void merge(dataset* x, int inicio, int fim) {
    ...
}

As linhas que se referiam aos valores de x, agora fazem para x.ano.

No fim, após a leitura dos dados, chamei a função merge e imprimi na tela o resultado:


#define TAMANHO 103

...

int main(int argc, char** argv) {
    ...
    
    // executa o sort
    merge(vet, 0, TAMANHO-1);
    
    // print na tela
    for (j = 0; j < TAMANHO; ++j) {
        printf("%d\n", vet[j].ano);
    }

    return 0;
}

O código com todas as modificações estão em anexo

main.c 2 kB · 0 downloads

Cara vlwww mesmo, de verdade

eu não percebi o quanto de erro meu programa continha, to a quase 1 mes tentando fazer ele funcionar, mas eu fiquei com uma duvida.

Eu testei na minha maquina com o meu .csv, mas ele não imprime os valores na tela, e um detalhe o .csv só tem uma coluna, que contem anos, e eles não possuem aspas.

Sabe me dizer onde o programa está se perdendo ou porque ele não imprime na tela esses valores ?

Link para o comentário
Compartilhar em outros sites

Se o seu CSV terá todos os campos sem aspas, basta modificar algumas linhas no loop while em main:

antes:
remove_quotes(token_wq, token);
process_field(field_count++, token_wq, i);

depois:
process_field(field_count++, token, i);

Se vocÊ quer lidar com dados que podem ou não ter aspas, eu fiz um comentário no post original:

Quote

Caso precise, é bem fácil modificar essa função caso a origem (src) já não possua aspas duplas.

 

Ah, claro, erro meu. Eu fiz uma alteração que não expliquei. Alterei a função main para que ela receba argc e argv, para que eu pudesse testar com diferentes arquivos especificados na linha de comando. Para voltar à funcionalidade original, basta alterar a linha que lê o arquivo. Em vez de ler o arquivo argv[1], leia o arquivo "DT.csv". Pode-se também retirar o argc e argv dos argumentos de main como era antes.

Link para o comentário
Compartilhar em outros sites

2 horas atrás, Vinicius Antunes Osti disse:

Se o seu CSV terá todos os campos sem aspas, basta modificar algumas linhas no loop while em main:


antes:
remove_quotes(token_wq, token);
process_field(field_count++, token_wq, i);

depois:
process_field(field_count++, token, i);

Se vocÊ quer lidar com dados que podem ou não ter aspas, eu fiz um comentário no post original:

 

Ah, claro, erro meu. Eu fiz uma alteração que não expliquei. Alterei a função main para que ela receba argc e argv, para que eu pudesse testar com diferentes arquivos especificados na linha de comando. Para voltar à funcionalidade original, basta alterar a linha que lê o arquivo. Em vez de ler o arquivo argv[1], leia o arquivo "DT.csv". Pode-se também retirar o argc e argv dos argumentos de main como era antes.

Ahhh certo, certo, então eu realizei a troca mesmo para o DT.csv, e removi o "Remove_quotes(token_wq, token);" de dentro do while.

Na função Int main, eu removi os argv e argv, deixando como void, ele até abre o arquivo e imprime os valores, mas ele imprime somente valores 0 e NULL.

 

Coloquei em Anexo a programação modifica como você falou e o arquivo .csv que estou usando

ED2.rar

Link para o comentário
Compartilhar em outros sites

Ahhh, agora eu entendi o motivo do continue no código original! A primeira linha do CSV possui o descritor do campo que será preenchido. Nesse caso, basta adicionar uma linha de fgets antes do while para descartar a primeira linha.

Algumas coisas ainda não estavam funcionando pois:

- você não observou atentamente qual flag de formato você usa para um int: é %d ou %s?

- você não modificou o loop conforme meu post anterior.

Dá uma olhada no código agora. Provavelmente você verá alguns zeros depois que o merge sort for feito  por inconsistência entre a quantidade de entradas no arquivo e na definição do tamanho do vetor de dataset, mas esse é um problema bem simples de resolver.

main.c

Link para o comentário
Compartilhar em outros sites

Sinto dizer-lhes, mas esse parser está completamente errado...

Por exemplo, a linha:

fred, o bom",10,3.14

Mesmo sem o " inicial, é perfeitamente válida e com apenas 3 itens (uma string e dois valores numéricos)... E ainda existem outras construções que o código não contempla... Recomendo estudar o que deve ser feito antes de tentar fazer...

Referência sobre CSV: https://tools.ietf.org/html/rfc4180

Link para o comentário
Compartilhar em outros sites

12 horas atrás, Vinicius Antunes Osti disse:

Ahhh, agora eu entendi o motivo do continue no código original! A primeira linha do CSV possui o descritor do campo que será preenchido. Nesse caso, basta adicionar uma linha de fgets antes do while para descartar a primeira linha.

Algumas coisas ainda não estavam funcionando pois:

- você não observou atentamente qual flag de formato você usa para um int: é %d ou %s?

- você não modificou o loop conforme meu post anterior.

Dá uma olhada no código agora. Provavelmente você verá alguns zeros depois que o merge sort for feito  por inconsistência entre a quantidade de entradas no arquivo e na definição do tamanho do vetor de dataset, mas esse é um problema bem simples de resolver.

main.c 3 kB · 0 downloads

Mano só agradece a força que tu me deu, serio mesmo.

O programa está funcionando da maneira que eu idealizei, ele está pegando os valores, aplicando o merge e exibindo na tela certinho.

Vlww mesmo pela ajuda

Link para o comentário
Compartilhar em outros sites

3 hours ago, fredericopissarra said:

Sinto dizer-lhes, mas esse parser está completamente errado...

Por exemplo, a linha:


fred, o bom",10,3.14

Mesmo sem o " inicial, é perfeitamente válida e com apenas 3 itens (uma string e dois valores numéricos)... E ainda existem outras construções que o código não contempla... Recomendo estudar o que deve ser feito antes de tentar fazer...

Referência sobre CSV: https://tools.ietf.org/html/rfc4180

Obrigado pelo toque, Frederico! Meu parser está bastante ad hoc, pensei em resolver de imediato o problema do Felipe e não me ative em resolver o problema da leitura do CSV de uma maneira mais abrangente. Apenas para registro, o que vi que está errado no meu parser:

  • Meu parser sempre vai procurar por duas aspas, ignorando tudo o que vem antes e depois delas;
  • Não é uma boa ideia usar o strtok com "," como separador se um campo possuir uma vírgula.
  • Meu parser não está lidando com caracteres especiais no contexto do CSV, como quebras de linha e as próprias aspas duplas. Usar fgets para ler um registro confiando que nenhum campo conterá quebras de linha pode ser problemático.

Se eu errei também na minha retratação, pode me descer o cascuco, Frederico.

Mas no exemplo que você deu são 2 strings e 2 números, não? "fred", " o bom"",  10 e 3.14

 

Link para o comentário
Compartilhar em outros sites

2 horas atrás, Vinicius Antunes Osti disse:

Obrigado pelo toque, Frederico! Meu parser está bastante ad hoc, pensei em resolver de imediato o problema do Felipe e não me ative em resolver o problema da leitura do CSV de uma maneira mais abrangente. Apenas para registro, o que vi que está errado no meu parser:

  • Meu parser sempre vai procurar por duas aspas, ignorando tudo o que vem antes e depois delas;
  • Não é uma boa ideia usar o strtok com "," como separador se um campo possuir uma vírgula.
  • Meu parser não está lidando com caracteres especiais no contexto do CSV, como quebras de linha e as próprias aspas duplas. Usar fgets para ler um registro confiando que nenhum campo conterá quebras de linha pode ser problemático.

Se eu errei também na minha retratação, pode me descer o cascuco, Frederico.

Mas no exemplo que você deu são 2 strings e 2 números, não? "fred", " o bom"",  10 e 3.14

 

Nope... UMA string, como há o termino com " o início é assumido como "...

Eu entendi que a rotina é ad hoc... Estou sendo apenas chato aqui! hehehe...

Concordo que o uso de strtok() pode ser problemático. Mas, normalmente, não gosto muito de usar fgets() porque ele exige um buffer de tamanho conhecido... Suponha que você defina um buffer de 256 characteres e sua linha tenha 512, como fica? Para isso, prefiro a função (POSIX) getline(), que permite alocação dinâmica:
 

char *buffer, *p;
size_t size;

// Prepara-se para a primeira alocação na leitura...
buffer = NULL;
size = 0;

while ( getline( &buffer, &size, fp ) >= 0 )
{
  // Estirpa '\n' final.
  if ( p = strchr( buffer, '\n' ) )
    *p = '\0';

  /* ... */

  // Livra-se do buffer, preparando para uma nova alocação.
  free( buffer );
  buffer = NULL;
  size = 0;
}

Existem outras "micro-otimizações" que eu faria no código como, por exemplo, livrar-se da abundância de operadores [], substituíndo-os por uso de ponteiros, mas é só chatice minha mesmo...

[]s
Fred

Link para o comentário
Compartilhar em outros sites

Ahhhh... sim... outro ERRO (embora a Microsoft pareça flexível com relação a isso) é usar "Portuguese" no setlocale()... A própria documentação da MS diz que o correto é obedecer as ISO 639 e 3166. Ou seja:

setlocale( LC_ALL, "pt_BR.UTF-8" );  // linux
setlocale( LC_ALL, "pt_BR.1252" ); // windows (code-page: WINDOWS-1252).

Referência: https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/setlocale-wsetlocale?view=vs-2019

https://docs.microsoft.com/en-us/cpp/c-runtime-library/locale-names-languages-and-country-region-strings?view=vs-2019

PS: Isso parece ser irrelevante aqui, já que o código não usa nenhuma função da libc que necessite de locale...

Link para o comentário
Compartilhar em outros sites

Caro Frederico, tudo bem. Gostaria de lhe pergunta se e possivel me direcionar na construcao de programa que lesse um arquivo csv   com mais de colunas e que ordene como vc fez a primeira. Tentei em vao em seu programa implementa lo em um arquivo  csv que vc desenvolveu lendo mais de uma coluna e ordenando somente a primeira. Ja fiz diversas tentativas por dias a fio. Seria de grande valia se pudesse me direcionar neste conhecimento. Grato. Tentei acrescentar neste seu programa mais campos. Porem ele ai ja para de ordenar a 1 coluna. E somente le o arquivo. Ok 

Link para o comentário
Compartilhar em outros sites

Arquivado

Este tópico foi arquivado e está fechado para novas respostas.

  • Quem Está Navegando   0 membros estão online

    • Nenhum usuário registrado visualizando esta página.
×
×
  • Criar Novo...