Regex: Combine uma série igualitária

18

Introdução

Não vejo muitos desafios de regex aqui, portanto, gostaria de oferecer esse enganosamente simples que pode ser feito de várias maneiras, usando vários sabores de regex. Espero que ele ofereça aos entusiastas de regex um pouco de diversão no golfe.

Desafio

O desafio é igualar o que denominei muito livremente de uma série "igualitária": uma série de números iguais de personagens diferentes. Isso é melhor descrito com exemplos.

Combine:

aaabbbccc
xyz 
iillppddff
ggggggoooooollllllffffff
abc
banana

Não corresponde:

aabc
xxxyyzzz
iilllpppddff
ggggggoooooollllllfff
aaaaaabbbccc
aaabbbc
abbaa
aabbbc

Para generalizar, queremos corresponder a um assunto da forma ( para qualquer lista de caracteres para onde para todosc1)n(c2)n(c3)n...(ck)nc1ckci != ci+1i, k > 1, and n > 0.

Esclarecimentos:

  • A entrada não estará vazia.

  • Um personagem pode se repetir mais tarde na sequência (por exemplo, "banana")

  • k > 1, sempre haverá pelo menos 2 caracteres diferentes na string.

  • Você pode assumir que apenas caracteres ASCII serão passados ​​como entrada e nenhum caractere será um terminador de linha.

Regras

(Obrigado a Martin Ender por este bloco de regras excelentemente declarado)

Sua resposta deve consistir em um único regex, sem nenhum código adicional (exceto, opcionalmente, uma lista de modificadores de regex necessários para fazer sua solução funcionar). Você não deve usar recursos do tipo regex do seu idioma que permitam invocar código no idioma de hospedagem (por exemplo, o emodificador do Perl ).

Você pode usar qualquer sabor de regex que existia antes deste desafio, mas especifique o sabor.

Não assuma que o regex esteja ancorado implicitamente, por exemplo, se você estiver usando Python, assuma que seu regex seja usado com re.search e não com re.match. Seu regex deve corresponder a toda a cadeia de caracteres para seqüências igualitárias válidas e não produzir correspondências para cadeias de caracteres inválidas. Você pode usar quantos grupos de captura desejar.

Você pode assumir que a entrada sempre será uma sequência de dois ou mais caracteres ASCII que não contém nenhum terminador de linha.

Como o regex golf, ganha o menor regex em bytes. Se o seu idioma exigir delimitadores (geralmente /.../) para indicar expressões regulares, não conte os delimitadores. Se sua solução exigir modificadores, adicione um byte por modificador.

Critério

Este é um bom golfe à moda antiga, então esqueça a eficiência e tente obter o máximo de regex possível.

Mencione qual sabor de expressão regular você usou e, se possível, inclua um link mostrando uma demonstração online da sua expressão em ação.

Jaytea
fonte
Isso é especificamente um golfe regular? Você provavelmente deve esclarecer isso, juntamente com as regras para isso. A maioria dos desafios neste site são campos de linguagens de programação variadas.
precisa
@LyricLy Obrigado pelo conselho! Sim, eu gostaria que fosse puramente regex, ie. uma única expressão regular em um sabor regex de escolha do remetente. Existem outras regras que eu deveria ter em mente em incluir?
Jaytea #
Eu não entendo sua definição de "igualitário", tal que bananaé igualitário.
Msh210
@ msh210 Quando inventei o termo "igualitário" para descrever a série, não considerei que permitiria que os personagens fossem repetidos mais tarde na série (como em "banana" ou "aaabbbcccaaa" etc.) . Eu só queria um termo para representar a ideia de que cada pedaço de caracteres repetidos tem o mesmo tamanho. Como "banana" não possui caracteres repetidos, essa definição é verdadeira.
jaytea

Respostas:

11

Sabor .NET, 48 bytes

^(.)\1*((?<=(\5())*(.))(.)(?<-4>\6)*(?!\4|\6))+$

Experimente online! (usando Retina )

Bem, acontece que não negar a lógica é mais simples, afinal. Estou fazendo disso uma resposta separada, porque as duas abordagens são completamente diferentes.

Explicação

^            # Anchor the match to the beginning of the string.
(.)\1*       # Match the first run of identical characters. In principle, 
             # it's possible that this matches only half, a quarter, an 
             # eighth etc of of the first run, but that won't affect the 
             # result of the match (in other words, if the match fails with 
             # matching this as the entire first run, then backtracking into
             # only matching half of it won't cause the rest of the regex to
             # match either).
(            # Match this part one or more times. Each instance matches one
             # run of identical letters.
  (?<=       #   We start with a lookbehind to record the length
             #   of the preceding run. Remember that the lookbehind
             #   should be read from the bottom up (and so should
             #   my comments).
    (\5())*  #     And then we match all of its adjacent copies, pushing an
             #     empty capture onto stack 4 each time. That means at the
             #     end of the lookbehind, we will have n-1 captures stack 4, 
             #     where n is the length of the preceding run. Due to the 
             #     atomic nature of lookbehinds, we don't have to worry 
             #     about backtracking matching less than n-1 copies here.
    (.)      #     We capture the character that makes up the preceding
             #     run in group 5.
  )
  (.)        #   Capture the character that makes up the next run in group 6.
  (?<-4>\6)* #   Match copies of that character while depleting stack 4.
             #   If the runs are the same length that means we need to be
             #   able to get to the end of the run at the same time we
             #   empty stack 4 completely.
  (?!\4|\6)  #   This lookahead ensures that. If stack 4 is not empty yet,
             #   \4 will match, because the captures are all empty, so the
             #   the backreference can't fail. If the stack is empty though,
             #   then the backreference will always fail. Similarly, if we
             #   are not at the end of the run yet, then \6 will match 
             #   another copy of the run. So we ensure that neither \4 nor
             #   \6 are possible at this position to assert that this run
             #   has the same length das the previous one.
)+
$            # Finally, we make sure that we can cover the entire string
             # by going through runs of identical lengths like this.
Martin Ender
fonte
Eu amo que você gangorra entre os dois métodos! Eu também pensei que a abordagem negativa deveria ter sido mais curta até que eu realmente tentei e a achei muito mais estranha (embora pareça que deveria ser mais simples). Eu tenho 48b em PCRE e 49b em Perl com um método completamente diferente, e com o seu terceiro método em .NET em torno do mesmo tamanho, eu diria que este é um grande desafio legal regex: D
jaytea
@ Jaytea Eu adoraria vê-los. Se ninguém aparecer com alguma coisa durante uma semana ou mais, espero que você os publique. :) E sim, concordou, é bom que as abordagens sejam tão próximas na contagem de bytes.
Martin Ender
Eu só poderia! Além disso, um Perl foi golfed até 46b;)
jaytea
Então achei que você pode querer vê-las agora! Aqui está 48b no PCRE: ((^.|\2(?=.*\4\3)|\4(?!\3))(?=\2*+((.)\3?)))+\3$Eu estava experimentando \3*no lugar de (?!\3)fazê-lo 45b, mas isso falha em "aabbbc" :( A versão Perl é mais fácil de entender, e agora é 45b: ^((?=(.)\2*(.))(?=(\2(?4)?\3)(?!\3))\2+)+\3+$- a razão pela qual eu chamo Perl, mesmo que seja parece ser válido PCRE é que PCRE pensa (\2(?4)?\3)poderia recurse indefinidamente enquanto Perl é um pouco mais esperto / perdoando!
jaytea
@ Jaytea Ah, essas são soluções realmente legais. Você realmente deve publicá-las em uma resposta separada. :)
Martin Ender
9

Sabor .NET, 54 bytes

^(?!.*(?<=(\2)*(.))(?!\2)(?>(.)(?<-1>\3)*)(?(1)|\3)).+

Experimente online! (usando Retina )

Tenho certeza de que isso é subótimo, mas é o melhor que estou apresentando para equilibrar grupos no momento. Eu tenho uma alternativa na mesma contagem de bytes, que é basicamente a mesma:

^(?!.*(?<=(\3())*(.))(?!\3)(?>(.)(?<-2>\4)*)(\2|\4)).+

Explicação

A idéia principal é inverter o problema, combinar cordas não igualitárias e colocar tudo em uma situação negativa para negar o resultado. O benefício é que não precisamos rastrear n em toda a cadeia (porque, devido à natureza dos grupos de balanceamento, você geralmente consome n ao verificá-la), para verificar se todas as execuções têm o mesmo comprimento. Em vez disso, procuramos apenas um único par de execuções adjacentes que não tenham o mesmo comprimento. Dessa forma, eu só preciso usar n uma vez.

Aqui está um detalhamento do regex.

^(?!.*         # This negative lookahead means that we will match
               # all strings where the pattern inside the lookahead
               # would fail if it were used as a regex on its own.
               # Due to the .* that inner regex can match from any
               # position inside the string. The particular position
               # we're looking for is between two runs (and this
               # will be ensured later).

  (?<=         #   We start with a lookbehind to record the length
               #   of the preceding run. Remember that the lookbehind
               #   should be read from the bottom up (and so should
               #   my comments).
    (\2)*      #     And then we match all of its adjacent copies, capturing
               #     them separately in group 1. That means at the
               #     end of the lookbehind, we will have n-1 captures
               #     on stack 1, where n is the length of the preceding
               #     run. Due to the atomic nature of lookbehinds, we
               #     don't have to worry about backtracking matching
               #     less than n-1 copies here.
    (.)        #     We capture the character that makes up the preceding
               #     run in group 2.
  )
  (?!\2)       #   Make sure the next character isn't the same as the one
               #   we used for the preceding run. This ensures we're at a
               #   boundary between runs.
  (?>          #   Match the next stuff with an atomic group to avoid
               #   backtracking.
    (.)        #     Capture the character that makes up the next run
               #     in group 3.
    (?<-1>\3)* #     Match as many of these characters as possible while
               #     depleting the captures on stack 1.
  )
               #   Due to the atomic group, there are three two possible
               #   situations that cause the previous quantifier to stopp
               #   matching. 
               #   Either the run has ended, or stack 1 has been depleted.
               #   If both of those are true, the runs are the same length,
               #   and we don't actually want a match here. But if the runs
               #   are of different lengths than either the run ended but
               #   the stack isn't empty yet, or the stack was depleted but
               #   the run hasn't ended yet.
  (?(1)|\3)    #   This conditional matches these last two cases. If there's
               #   still a capture on stack 1, we don't match anything,
               #   because we know this run was shorter than the previous
               #   one. But if stack 1, we want to match another copy of 
               #   the character in this run to ensure that this run is 
               #   longer than the previous one.
)
.+             # Finally we just match the entire string to comply with the
               # challenge spec.
Martin Ender
fonte
Tentei fazê-lo falhar em: banana, aba, bbbaaannnaaannnaaa, bbbaaannnaaannnaaaaaa, The Nineteenth Byte, 11, 110, ^(?!.*(?<=(\2)*(.))(?!\2)(?>(.)(?<-1>\3)*)(?(1)|\3)).+, bababa. Fui eu quem falhou. :( +1
Erik the Outgolfer
1
Naquele momento em que você termina sua explicação e descobre que pode salvar 1 byte usando a abordagem exatamente oposta ... Acho que darei outra resposta daqui a pouco ...: |
Martin Ender
@MartinEnder ... E então perceba que você poderia jogar este por 2 bytes haha: P
Sr. Xcoder
@ Mr.Xcoder Teria que ter 7 bytes agora, então espero estar seguro. ;)
Martin Enders