Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Minha solução para o desafio 06. #1177

Open
wants to merge 25 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions desafio-06/AlexPHorta/python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# README

## Desafio 6 (Linguagem: Python)

O desafio consiste em localizar anagramas de uma expressão, de no máximo 16 caracteres, fornecida pelo usuário na linha de comando. Esses anagramas deverão ser compostos por palavras obtidas no arquivo 'words.txt', fornecido pelo proponente do desafio.

Minha solução foi escrita usando a linguagem Python, nas versões 3.12 e 3.13, e não utiliza bibliotecas externas. Ela consiste em, primeiramente, reduzir o campo de busca ao mínimo usando uma série de funções (aquelas relacionadas em _shrink_search_field_). Após, utilizei um algoritmo obtido na web (Livre para uso, segundo o autor.) para encontrar todas as partições ([Função de Partição](https://pt.wikipedia.org/wiki/Fun%C3%A7%C3%A3o_de_parti%C3%A7%C3%A3o_(matem%C3%A1tica))) possíveis para o número de caracteres contidos na expressão fornecida pelo usuário e escrevi uma função (_shrink_partitions_) para eliminar as que não fossem adequadas para a obtenção dos anagramas.

Com as partições definidas, escrevi uma função (_find_in_partition_) que busca os anagramas no arquivo 'words.txt', utilizando um dicionário para mapeamento dos totais de caracteres contidos na expressão e nas diversas palavras a serem testadas.

## Modo de Usar

Abra um terminal e execute o programa, assim:

```bash
$ python desafio06.py VERMELHO
ELM HO REV
ELM OH REV
ELM HOVER
LEVER OHM
OHM REVEL
HELM OVER
HELM ROVE
HOLM VEER
```

Para ajuda sobre o uso do programa, digite no terminal `python desafio06.py --help`. Os testes podem ser executados com `python test_desafio06.py` (Optei por deixá-los em um arquivo separado.).
326 changes: 326 additions & 0 deletions desafio-06/AlexPHorta/python/desafio06.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,326 @@
import functools
import itertools
import operator
import string


WORDS = 'words.txt'


def print_anagrams(expression, words_file):
"""Print all anagrams from expression, found in words_file.

Positional arguments:
expression -- the expression to be used for the search of anagrams
words_file -- path to the file with words
"""
with open(words_file) as words:
_anagrams = list(m for m in find_matches(expression, words))
if len(_anagrams) == 1 and len(_anagrams[0]) == 0:
print(f"Nenhum anagrama encontrado para a expressão: {expression}")
for anagram in _anagrams:
for a in anagram:
print(' '.join(a))

def prep(expression):
"""Prepare user input to be used by the program.

Positional arguments:
expression -- the expression to be used for the search of anagrams
"""
if expression == "":
return expression

res = ""

for c in expression:
if c not in string.ascii_letters:
continue
res += c.upper()

return res

def find_matches(expression, words_file):
"""Find all anagram matches for, found in words_file.

Positional arguments:
expression -- the expression to be used for the search of anagrams
words_file -- path to the file with words
"""
candidates = shrink_search_field(expression, words_file)
res = []
grouped_by_len = group_by_len(candidates)
valid_partitions = shrink_partitions(expression, grouped_by_len)

for v in valid_partitions:
res.extend(find_in_partition(expression, v, grouped_by_len))

yield res

def find_in_partition(expression, partition, grouped):
"""Search possible matches in each partition group.

Positional arguments:
expression -- the expression to be used for the search of anagrams
partition -- the expression's partition to be used for the search
grouped -- dictionary with the word matches, grouped by length
"""
letters_in_expression = quant_letters(expression)
group_lengths = [range(len(grouped[p])) for p in partition]

group_letters = {}
for k, v in grouped.items():
letters = [quant_letters(w) for w in v]
group_letters.update({k:letters})

res = []

for g in itertools.product(*group_lengths):
letters_in_prod = dict(letters_in_expression)
words = []
fits = True
selected_words = sorted([grouped[partition[i]][j] for i, j in enumerate(g)])
if selected_words in words:
continue
for i, j in enumerate(g):
word = grouped[partition[i]][j]
word_sum = group_letters[partition[i]][j]
for k, v in word_sum.items():
letters_in_prod[k] -= v
if letters_in_prod[k] < 0:
fits = False
break
if not fits:
break
words.append(word)
if not fits:
continue
words = sorted(words)
if words not in res:
res.append(words)

return res

def group_by_len(candidates):
"""Group the words by length.

Positional arguments
candidates -- a list with the possible words to make the anagrams
"""
candidates = sorted(candidates, key=len)
res = {}
grouped = []
for _, g in itertools.groupby(candidates, len):
grouped.append(list(g))
lengths = [len(l[0]) for l in grouped]
for l, g in zip(lengths, grouped):
res[l] = g
return res

def shrink_partitions(expression, grouped):
"""Remove all partitions that dont't need to be checked.

Positional arguments
expression -- the expression to be used for the search of anagrams
grouped -- dictionary with the word matches, grouped by length
"""
partitions = list(accel_asc(len(expression)))
available = list(grouped.keys())
res = []

# If a partition includes words whose length is not in one of the available
# partitions, don't include it.
for partition in partitions:
for e in partition:
if e not in available:
break
else:
res.append(partition)

rem = []
no_solo = not_solo(expression, grouped)

for r in res:
if set(r) <= set(no_solo):
rem.append(r)

# If there's a group 1, remove all partitions that have more ones than the length of group 1.
ones = 1 in available
if ones:
gg = grouped.get('1')
len_ones = len(gg) if gg is not None else 1
for r in res:
if r.count(1) > len_ones:
rem.append(r)

for r in rem:
try:
remover = res.index(r)
del res[remover]
except ValueError:
continue

return res

def not_solo(expression, grouped):
"""Return length groups that don't include all the letters in expression.

Positional arguments
expression -- the expression to be used for the search of anagrams
grouped -- dictionary with the word matches, grouped by length
"""
in_expression = set(expression)
res = []
for k, g in grouped.items():
g = [list(i) for i in g]
available = set(functools.reduce(operator.iconcat, g, []))
remaining = in_expression - available
if len(remaining) > 0:
res.append(k)
return res

def shrink_search_field(expression, words_file):
"""Shrink the search field for possible anagrams, before considering
partitions of expression.

Positional arguments
expression -- the expression to be used for the search of anagrams
words_file -- path to the file with words
"""
res = []
for w in words_file:
w = w.strip()
le = sieve_less_or_equal(expression, w)
sw = sieve_starts_with(expression, w)
rm = sieve_remaining(expression, w)
ql = sieve_number_of_letters(expression, w)
if all((le, sw, rm, ql)):
res.append(w)
return res

def sieve_number_of_letters(expression, word):
"""Check if a word is contained in expression. Remove those that can't be.

Positional arguments
expression -- the expression to be used for the search of anagrams
word -- the word to be compared
"""
expression_quant = quant_letters(expression)
word_quant = quant_letters(word)
for k, v in word_quant.items():
if expression_quant.get(k) is not None and v > expression_quant[k]:
return False
return True

def quant_letters(a_word):
"""Return a mapping of the number of the different letters in a word.

Positional arguments
a_word -- the word to be mapped
"""
keys = set(a_word)
quanto = {k:a_word.count(k) for k in keys}
return quanto

def sieve_remaining(expression, word):
"""Remove any word that has letters that are not in expression.

Positional arguments
expression -- the expression to be used for the search of anagrams
word -- the word to be evaluated
"""
letters = set(expression)
uppercase = set(string.ascii_uppercase)
remaining = uppercase - letters
return not any((l in remaining) for l in word)

def sieve_starts_with(expression, word):
"""Return only words that begin with one of the letters in expression.

Positional arguments
expression -- the expression to be used for the search of anagrams
word -- the word to be evaluated
"""
letters = set(expression)
return any(word.startswith(l) for l in letters)

def sieve_less_or_equal(expression, word):
"""Exclude words that are lengthier than expression.

Positional arguments
expression -- the expression to be used for the search of anagrams
word -- the word to be evaluated
"""
return len(expression) >= len(word)

# Peguei de https://jeromekelleher.net/generating-integer-partitions.html
def accel_asc(n):
"""Yield all partitions of a given integer.
E.g.: The partitions of three are: 1,1,1; 1,2; 2,1; 3

Positional arguments
n -- the integer to be used
"""
a = [0 for i in range(n + 1)]
k = 1
y = n - 1
while k != 0:
x = a[k - 1] + 1
k -= 1
while 2 * x <= y:
a[k] = x
y -= x
k += 1
l = k + 1
while x <= y:
a[k] = x
a[l] = y
yield a[:k + 2]
x += 1
y -= 1
a[k] = x + y
y = x + y - 1
yield a[:k + 1]


if __name__ == "__main__":

import gettext
import sys

def translate(text):
text = text.replace("usage", "modo de usar")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dá erro se eu digito números como argumento.
Você tem que tratar isso:
➜ testes-desafios git:(master) ✗ python3 des6.py 902823
Traceback (most recent call last):
File "/home/marcelo/testes-desafios/des6.py", line 318, in
if len(args.termo) == 0:
^^^^^^^^^^

text = text.replace("positional arguments", "argumentos posicionais")
text = text.replace("show this help message and exit",
"Exibe esta mensagem de ajuda e termina a execução.")
text = text.replace("error", "erro")
text = text.replace("options", "opções")
text = text.replace("the following arguments are required",
"os seguintes argumentos são obrigatórios")
return text
gettext.gettext = translate

import argparse

parser = argparse.ArgumentParser(
description="Imprime todos os anagramas de TERMO encontrados no\
arquivo 'words.txt' (Que deverá estar na mesma pasta do programa.)."
)

parser.add_argument(
"TERMO",
nargs = 1,
help="O termo a ser usado para a busca de anagramas."
)

args = parser.parse_args()

if len(args.termo) == 0:
parser.print_help()

if args.termo:
try:
assert len(args.termo) == 1, "Número excessivo de argumentos."
print_anagrams(sys.argv[1], WORDS)
except Exception as exc:
raise ValueError(f"{exc}") from exc
Loading
Loading