sexta-feira, 16 de fevereiro de 2018

Largura Máxima de Cada Coluna num CSV

Após tentar carregar um CSV cheio de inconsistências, resolvi buscar o tamanho máximo de cada coluna usando apenas a linha de comando no Linux.

O resultado é o comando que segue:

 head -1 arquivo.csv | \
 grep -Po ';' | \
 cat -n | \
 grep -Po '\d+' | \
 xargs -I'{}' bash -c "cut -d';' -f'{}' arquivo.csv | \
 awk 'length(\$0) > max { max=length(\$0) } END { print max }'"

Os passos são:
  1. Pegar a primeira linha (o cabeçalho);
  2. Elimina todos os caracteres exceto o separadores (para contar as colunas);
  3. Numera as colunas;
  4. Elimina os separadores para deixar apenas os números das colunas;
  5. Para cada coluna, executa um comando composto que retira a enésima coluna e imprime a largura do valor mais largo.
Então, para um cabeçalho do tipo COL1;COL2;COL3, os comandos de 1 a 4 produzem o seguinte:

 % head -1 arquivo.csv | grep -Po ';' |  cat -n | grep -Po '\d+'
 1
 2
 3


Depois, o xargs vai executar os seguintes comandos:

 bash -c cut -d';' -f'1' arquivo.csv | \
   awk 'length($0) > max { max=length($0) } END { print max }'
 bash -c cut -d';' -f'2' arquivo.csv | \
   awk 'length($0) > max { max=length($0) } END { print max }'
 bash -c cut -d';' -f'3' arquivo.csv | \
   awk 'length($0) > max { max=length($0) } END { print max }'

E o resultado final será uma lista de larguras:

 10
 25
 100

Para facilitar a leitura, dá para adicionar o número da coluna com um echo bem posicionado:

 head -1 arquivo.csv | \
 grep -Po ';' | \
 cat -n | \
 grep -Po '\d+' | \
 xargs -I'{}' bash -c "echo -n '{}: '; cut -d';' -f'{}' arquivo.csv | \
 awk 'length(\$0) > max { max=length(\$0) } END { print max }'"

E o resultado sairá assim:

 1: 10
 2: 25
 3: 100

Se faltar uma coluna, basta adicionar uma ao primeiro comando:

 bash -c "echo -n ';' &&  head -1 arquivo.csv"

Ou, sendo mais prático, basta usar o número de colunas e evitar a contagem:

 seq 1 3 | \
 xargs -I'{}' bash -c "cut -d';' -f'{}' arquivo.csv  | \
   awk 'length(\$0) > max { max=length(\$0) } END { print max }'"

O cabeçalho pode gerar problemas, quando suas colunas forem maiores que os dados propriamente ditos. A solução é usar o tail para pular a primeira linha.

 seq 1 3 | \
 xargs -I'{}' bash -c "tail -n +2 arquivo.csv | \
   cut -d';' -f'{}'  | \
   awk 'length(\$0) > max { max=length(\$0) } END { print max }'"

Isso vai falhar se o arquivo tiver campos com quebra de linha. Então, uma solução mais robusta pode ser obtida com um pouco de perl.

#!/usr/bin/perl
use Text::CSV_PP;
use List::Util qw(max);

my @max=();
my $csv=Text::CSV_PP->new({sep_char=>';',auto_diag=>1,binary=>1});
open(my $fh, '<:encoding(UTF-8)', $ARGV[0]) or die "Can't read file '$file' [$!]\n";
<$fh>; #Ignore header
while (my $line = $csv->getline($fh)) {
  my @fields=@$line;
  $max[$_]=max(length($fields[$_]),$max[$_]) for 0..$#fields;
};
print "@max\n";

Esse script recebe um único parâmetro: o nome de um arquivo. Ele percorre todas as linhas, exceto a primeira (ignorando o cabeçalho).

quinta-feira, 1 de fevereiro de 2018

Visitando os Departamentos do Uruguai

Um projeto que eu gostaria de colocar em marcha um dia é o de visitar todos os departamentos do Uruguai. Então, o primeiro passo é ter uma ideia da distância a ser percorrida. Assim, posso avaliar a viabilidade e quanto tempo seria necessário.



Eu comecei com uma tabela de todas as distâncias entre as capitais departamentais. Depois, uma função que percorre recursivamente todas as possibilidades partindo de uma cidade específica. Este não é o problema do caixeiro viajante: não é preciso terminar no mesmo lugar.

Para acelerar a busca, o código corta os ramos da árvore de busca que forem maiores que a menor distância já encontrada. Então, o programa vai acelerando com o tempo. Mesmo assim, ele levou um dia inteiro para encontrar o melhor caminho a partir de Artigas.

A solução encontrada tem 1927km. Ou seja, o projeto é viável, mesmo para ser executado em uma semana.

A tabela abaixo tem todos os passos e as distâncias percorridas em cada um.

Artigas
Rivera 183
Tacuarembó 111
Melo 204
Treinta y Tres 113
Rocha 172
Maldonado 85
Minas 75
Montevideo 122
Canelones 46
Florida 52
Durazno 85
Trindad 41
San José 95
Colonia 108
Mercedes 176
Fray Bentos 31
Paysandú 110
Salto 118



#!/usr/bin/perl
use strict;
use experimental 'smartmatch';

$|=1;

my $capitals={
0=>'Artigas',
1=>'Canelones',
2=>'Colonia',
3=>'Durazno',
4=>'Florida',
5=>'Fray Bentos',
6=>'Maldonado',
7=>'Melo',
8=>'Mercedes',
9=>'Minas',
10=>'Montevideo',
11=>'Paysandú',
12=>'Rivera',
13=>'Rocha',
14=>'Salto',
15=>'San José',
16=>'Tacuarembó',
17=>'Treinta y Tres',
18=>'Trinidad'
};

my $distances=[
  [0,555,611,418,503,435,748,392,435,711,601,325,183,671,207,580,211,503,459],
  [555,0,145,137,52,268,155,378,237,131,46,332,455,220,450,47,344,282,151],
  [611,145,0,218,178,207,301,507,176,276,177,286,549,360,404,108,429,427,176],
  [418,137,218,0,85,201,298,418,170,273,183,229,318,363,348,136,207,424,41],
  [503,52,178,85,0,286,209,329,255,184,98,318,403,274,437,88,292,355,126],
  [435,268,207,201,286,0,422,622,31,395,309,110,452,483,228,220,341,546,160],
  [748,155,301,298,209,422,0,325,391,75,134,487,572,85,605,202,509,212,301],
  [392,378,507,418,329,622,325,0,590,276,387,435,262,285,428,407,204,113,460],
  [435,237,176,170,255,31,391,590,0,363,278,110,452,452,228,189,341,415,129],
  [711,131,276,273,184,395,75,276,363,0,122,463,604,132,582,182,484,164,276],
  [601,46,177,183,98,309,134,387,278,122,0,378,501,210,496,93,390,286,188],
  [325,332,286,229,318,110,487,435,110,463,378,0,342,553,118,285,231,614,190],
  [183,455,549,318,403,452,572,262,452,604,501,342,0,541,335,473,111,373,359],
  [671,220,360,363,274,483,85,285,452,132,210,553,541,0,672,272,490,172,366],
  [207,450,404,348,437,228,605,428,228,582,496,118,335,672,0,403,224,534,309],
  [580,47,108,136,88,220,202,407,189,182,93,285,473,272,403,0,353,327,95],
  [211,344,429,207,292,341,509,204,341,484,390,231,111,490,224,353,0,320,248],
  [503,282,427,424,335,546,212,113,514,164,286,614,373,172,534,327,320,0,427],
  [459,151,176,41,126,160,301,460,129,276,188,190,359,366,309,95,248,427,0]
];

sub print_path {
  my $distance=shift;
  my $marker=shift;
  my @path=@_;
  my @names=map { $capitals->{$_} } @path;
  print "$distance km $marker "; 
  print join '->', @names;
  print "\n";
}

my $limit=3_000;

sub run {
  my $capital=shift;
  my $distance=shift||0;
  my @path=@_;

  if(scalar(@path)==18) { 
    $limit=$distance if $distance<$limit;
    print_path($distance, '*', @path, $capital);
  } else {
    for my $c (0..18) {
      if (!($c~~@path) && $c!=$capital) {
        my $d=$distances->[$capital]->[$c];
        run($c, $distance+$d, @path, $capital) if ($d+$distance)<$limit;
      }  
    }
  }   
}

run(0);


A função run() recebe o índice de uma cidade e recursivamento adiciona todas as outras, uma a uma, até ter um caminho completo. Mas ela aborta qualquer caminho que ultrapasse o valor de $limit, além de atualizar $limit quando encontra um caminho menor.