Dividir string em determinadas posições

8

Como faço para dividir uma sequência de caracteres bem / idioma em uma lista de posições?

O que eu tenho:

.say for split-at( "0019ABX26002", (3, 4, 8) ); 

sub split-at( $s, @positions )
{
  my $done = 0;

  gather 
  {
    for @positions -> $p
    {
      take $s.substr($done, $p - $done );
      $done = $p;
    }
    take $s.substr( $done, * );
  }
}

o que é razoável. Estou intrigado com a falta de suporte ao idioma para isso. Se "dividir em" é uma coisa, por que não "dividir em" também? Eu acho que isso deve ser uma operação central. Eu deveria ser capaz de escrever

.say for "0019ABX26002".split( :at(3, 4, 8) );

Ou talvez eu esteja negligenciando alguma coisa?

Edit: Um pequeno benchmark do que temos até agora

O------------O---------O------------O--------O-------O-------O
|            | Rate    | array-push | holli  | raiph | simon |
O============O=========O============O========O=======O=======O
| array-push | 15907/s | --         | -59%   | -100% | -91%  |
| holli      | 9858/s  | 142%       | --     | -100% | -79%  |
| raiph      | 72.8/s  | 50185%     | 20720% | --    | 4335% |
| simon      | 2901/s  | 1034%      | 369%   | -98%  | --    |
O------------O---------O------------O--------O-------O-------O

Código:

use Bench;

my $s = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbccccddddddddddddddddddddddddddddddddddddefggggggggggggggggggg";
my @p = 29, 65, 69, 105, 106, 107;

Bench.new.cmpthese(1000, {
  holli  => sub { my @ = holli($s, @p); },
  simon => sub { my @ = simon($s, @p); },
  raiph => sub { my @ = raiph($s, @p); },
  array-push => sub { my @ = array-push($s, @p); },
});

#say user($s, @p);


sub simon($str, *@idxs ) {
    my @rotors = @idxs.map( { state $l = 0; my $o = $_ - $l; $l = $_; $o } );
    $str.comb("").rotor( |@rotors,* ).map(*.join(""));
}

sub raiph($s, @p) {
    $s.split( / <?{$/.pos == any(@p)}> / )
}

sub holli( $s, @positions )
{
  my $done = 0;

  gather
  {
    for @positions -> $p
    {
      take $s.substr($done, $p - $done );
      $done = $p;
    }
    take $s.substr( $done, * );
  }
}

sub array-push( $s, @positions )
{
  my $done = 0;
  my @result;

  for @positions -> $p
  {
    @result.push: $s.substr($done, $p - $done );
    $done = $p;
  }
  @result.push: $s.substr( $done, * );

  @result;
}
Holli
fonte
Então, para isso você está esperando ("001", "9", "ABX2", "6002"):?
Scimon Proctor 22/01
Nesse caso, essa seria a saída sim.
Holli
Se você está procurando velocidade bruta, criar um array de retorno explícito é um pouco mais rápido: recebo ~ 15k com coleta / recebimento, quase 19k com Array / push, mas isso pressupõe que cada item seja necessário.
User0721090601 22/01
Oh uau, eu não esperava isso. Medo uma diferença de velocidade de quase 100% entre o meu código inicial e o eqiv. código com uma matriz explícita e empurra. Alguma idéia de por que reunir é muito mais lento?
Holli
2
À luz desta pergunta, adicionei um módulo: String :: Fields . Sua interface é um pouco diferente, mas acho que também é mais flexível e mais útil em outras situações.
Elizabeth Mattijsen 24/01

Respostas:

9

Pessoalmente, eu o dividiria em uma lista, use rotorpara dividir a lista e juntar o resultado:

"0019ABX26002".comb().rotor(3,1,4,*).map(*.join)

Se você deseja uma divisão na função (usando os índices fornecidos):

sub split-at( $str, *@idxs ) { 
    my @rotors = @idxs.map( { state $l = 0; my $o = $_ - $l; $l = $_; $o } );
    $str.comb("").rotor( |@rotors,* ).map(*.join("")); 
}

Basicamente, se eu quiser fazer coisas do tipo lista, eu uso uma lista.

Eu vim com outra versão que eu realmente gosto de um sentido de programação funcional:

sub split-at( $str, *@idxs ) {
    (|@idxs, $str.codes)
    ==> map( { state $s = 0;my $e = $_ - $s;my $o = [$s,$e]; $s = $_; $o } )
    ==> map( { $str.substr(|$_) } );
}

Funciona para ser um pouco mais lento que o outro.

Scimon Proctor
fonte
2
Eh bem. Nós pensamos da mesma forma, e quase ao mesmo tempo :-)
jjmerelo 22/01
Obrigado por me lembrar da existência de rotor. Nesse caso, porém. Você está trabalhando muito para o que deveria ser uma operação simples.
Holli
4

Mão única:

.say for "0019ABX26002" .split: / <?{ $/.pos ∈ (3,4,8) }> /

exibe:

001
9
ABX2
6002
raiph
fonte
1
Arrumado. Mas bastante complicado.
Holli
1
Também é muito lento. Confira o benchmark
Holli
1
Oi Holli. Votei seus comentários como sinal de concordância com todos eles, incluindo o fato de ser lento. /// Como prêmio de consolação pela minha abordagem regex, pode-se substituir o original == 3|4|8por ∈ @pospara melhorar a velocidade. (E alguns podem preferir a forma como se lê também.)
raiph
3

Como cada substring não depende do outro, hyper se torna uma opção.

method split-at(\p) {
  do hyper for (0,|p) Z (|p,self.chars) {
    self.substr: .head, .tail - .head
  }
}

Ou na forma secundária:

sub split-at(\s, \p) {
  do hyper for (0,|p) Z (|p,s.chars) {
    s.substr: .head, .tail - .head
  }
}

Mas a sobrecarga envolvida não vale a pena, a menos que o número de elementos solicitados seja extremo - nos meus testes é cerca de dez vezes mais lento que a forma ingênua.

user0721090601
fonte
2

Aqui está a solução que eu usaria:

my method break (Str \s: *@i where .all ~~ Int) {
  gather for @i Z [\+] 0,|@i -> ($length, $start) {
    take s.substr: $start, $length
  }
}

say "abcdefghi".&break(2,3,4)   # "ab","cde","fghi"

O gather/ takepermite que seja preguiçoso se você não precisar usar todos eles. O loop pega @i( 2,3,4no exemplo) e o fecha com o redutor de adição em cascata [\+], que normalmente produziria 2,5,9, mas inserimos um 0 0,2,5,9para marcar os índices iniciais de cada um. Isso permite que a tomada real seja uma substroperação simples .

Ao torná-lo um em methodvez de um sub, você pode usá-lo como faria (você pode até nomeá-lo, splitse quiser, a adição do &sigil significa que Raku não ficará confuso se você deseja o embutido ou personalizado.

Você pode, inclusive, adicioná-lo diretamente ao Str:

use MONKEY-TYPING;   # enable augment
augment class Str {
  multi method split (Str \s: *@i where .all ~~ Int) {
    gather for @i Z [\+] 0,|@i -> ($length, $start) {
      take s.substr: $start, $length
    }
  }
}

say "abcdefghi".split(2,3,4)

Nesse caso, ele precisa ser definido, pois multi methodjá existem vários splitmétodos. O bom é que, como nenhum deles é definido apenas por Intargumentos, é fácil garantir que o nosso aumentado seja usado.

Dito isto, chamá-lo usando a versão sigilada em um léxico methodé definitivamente o melhor.

user0721090601
fonte
Aceite, acabei de perceber que você preferiria um :atparâmetro nomeado, atualizarei para fazer isso.
user0721090601 22/01
Eu não prefiro por si só. Eu gostaria que fosse no idioma. É bastante comum. Já temos meia dúzia de variantes de split. Tal seria uma adição razoável, imho.
Holli
Holli: Na verdade, acho que faria mais sentido combdo que em split, pois o pente já foi projetado para funcionar com números inteiros. Que tal agora? tio.run/##VVLJasMwFLz7KwYTgk1dZyn0kODQaw@FQo4lLbIs26LygiTThCy/…
user0721090601 22/01
Além disso, adições básicas podem ser feitas e propostas em github.com/Raku/problem-solving . Nesse caso, eu acho que uma proposta para comb () pode ser bastante fácil de ser aprovada, embora possa não torná-la essencial até o 6.f (não tenho certeza se o 6.e ainda está aberto para coisas)
user0721090601
Sua solução está esperando comprimentos como entrada, não posições.
Holli