División de cadena en las posiciones dadas

8

¿Cómo divido agradable / idiomáticamente una cadena en una lista de posiciones?

Lo que tengo:

.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, * );
  }
}

Lo cual es razonable. Sin embargo, estoy desconcertado por la falta de soporte de idiomas para esto. Si "dividir en" es una cosa, ¿por qué no "dividir en" también? Creo que esto debería ser una operación central. Debería poder escribir

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

¿O tal vez estoy pasando por alto algo?

Editar: un pequeño punto de referencia de lo que tenemos hasta ahora

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
fuente
Entonces, ¿para esto estás esperando ("001", "9", "ABX2", "6002")?
Scimon Proctor
En este caso, ese sería el resultado sí.
Holli
Si está buscando una velocidad bruta, hacer una matriz de retorno explícita es un poco más rápido: obtengo ~ 15k con recolección / toma, frente a casi 19k con Array / push, pero eso supone que cada elemento sea finalmente necesario.
user0721090601
Oh wow, no esperaba eso. Mido una diferencia de velocidad cercana al 100% entre mi código inicial y el eqiv. código con una matriz explícita y empuja. ¿Alguna idea de por qué reunirse es mucho más lento?
Holli
2
A la luz de esta pregunta, he agregado un módulo: String :: Fields . Su interfaz es ligeramente diferente, pero creo que también es más flexible y más útil en otras situaciones.
Elizabeth Mattijsen

Respuestas:

9

Personalmente lo dividiría en una lista, utilizo rotorpara dividir la lista y unir el resultado:

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

Si desea una división en la función (usando los índices dados):

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

Básicamente, si quiero hacer cosas de tipo de lista, uso una lista.

Se me ocurrió otra versión que realmente me gusta por un sentido de programación 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(|$_) } );
}

Resulta ser un poco más lento que el otro.

Scimon Proctor
fuente
2
Eh bien Pensamos igual, y casi al mismo tiempo :-)
jjmerelo
Gracias por recordarme la existencia de rotor. En este caso, sin embargo. Estás haciendo mucho trabajo para lo que debería ser una operación simple.
Holli
4

De una sola mano:

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

muestra:

001
9
ABX2
6002
raiph
fuente
1
Ordenado. Pero bastante complicado.
Holli
1
También muy lento. Mira el punto de referencia
Holli
1
Hola hola He votado sus comentarios para indicar que están de acuerdo con todos ellos, incluido que es lento. /// Como premio de consolación para mi enfoque de expresiones regulares, se puede reemplazar el original == 3|4|8con el ∈ @posde mejorar la velocidad. (Y algunos podrían preferir la forma en que se lee también.)
raiph
3

Debido a que cada subcadena no depende de la otra, hiper se convierte en una opción.

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

O en forma secundaria:

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

Pero la sobrecarga involucrada no vale la pena a menos que la cantidad de elementos solicitados sea extrema: en mis pruebas es aproximadamente diez veces más lenta que la forma ingenua.

usuario0721090601
fuente
2

Aquí está la solución que usaría:

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"

El gather/ takepermite que sea flojo si finalmente no necesita usarlos todos. El bucle toma @i( 2,3,4en el ejemplo) y lo cierra con el reductor de adición en cascada [\+], que normalmente produciría 2,5,9, pero insertamos un 0 0,2,5,9para marcar los índices iniciales de cada uno. Esto permite que la toma real sea simplesubstr operación .

Al convertirlo methoden un sub en lugar de un sub, puede usarlo como lo haría (incluso podría nombrarlo splitsi lo desea, la adición de& sigilo significa que Raku no se confundirá si desea el incorporado o personalizado.

Incluso podría agregarlo directamente a 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)

En este caso, debe definirse como multi methodya existen varios splitmétodos. Lo bueno es que, dado que ninguno de esos se define solo por Intargumentos, es fácil asegurarse de que se use nuestro aumentado.

Dicho esto, llamarlo usando la versión sigulada en un léxico methodes definitivamente la mejor.

usuario0721090601
fuente
De acuerdo, me acabo de dar cuenta de que preferirías un :atparámetro con nombre, lo actualizaré para hacerlo.
user0721090601
No lo prefiero per se. Me gustaría que fuera en el idioma. Es bastante común. Ya tenemos media docena de variantes de split. Tal sería una adición razonable, en mi humilde opinión.
Holli
Holli: En realidad, creo que tendría más sentido en comblugar de en split, ya que el peine ya está diseñado para trabajar en números enteros. ¿Qué tal esto? tio.run/##VVLJasMwFLz7KwYTgk1dZyn0kODQaw@FQo4lLbIs26LygiTThCy/…
user0721090601
Además, se pueden realizar adiciones centrales y se pueden proponer en github.com/Raku/problem-solving . En este caso, creo que una propuesta para peinar () podría ser bastante fácil de aprobar, aunque podría no convertirse en núcleo hasta 6.f (no estoy seguro de si 6.e todavía está abierto para cosas)
user0721090601
Su solución espera longitudes como entrada, no posiciones.
Holli