From 41c29c84b16fa4096158df7b405d7f65a1f7b1c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Doramas=20Garc=C3=ADa=20Jorge?= Date: Mon, 8 Jun 2026 22:05:32 +0100 Subject: [PATCH] Solve "Little Sister's Vocabulary" exercise --- README.md | 5 +- python/little-sisters-vocab/HINTS.md | 39 +++ python/little-sisters-vocab/README.md | 368 ++++++++++++++++++++ python/little-sisters-vocab/strings.py | 87 +++++ python/little-sisters-vocab/strings_test.py | 126 +++++++ 5 files changed, 623 insertions(+), 2 deletions(-) create mode 100644 python/little-sisters-vocab/HINTS.md create mode 100644 python/little-sisters-vocab/README.md create mode 100644 python/little-sisters-vocab/strings.py create mode 100644 python/little-sisters-vocab/strings_test.py diff --git a/README.md b/README.md index 1aad16c..7a239b0 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,11 @@ Personal solutions to [**Python** track][python_track] exercises from [**Exercis 4. ["**Currency Exchange**" solution](python/currency-exchange/exchange.py). 5. ["**Meltdown Mitigation**" solution](python/meltdown-mitigation/conditionals.py). 6. ["**Black Jack**" solution](python/black-jack/black_jack.py). +8. ["**Little Sister's Vocabulary**" solution](python/little-sisters-vocab/strings.py). [python_version_badge]: https://img.shields.io/badge/Python%203.13-3776AB?logo=python&logoColor=FFD43B -[track_completion_badge]: https://img.shields.io/badge/Track%20completion-4.1%25-604fcd?logo=exercism&logoColor=604fcd&labelColor=e9ecef -[exercises_completed_badge]: https://img.shields.io/badge/Exercises%20completed-6%2F146-604fcd?logo=exercism&logoColor=604fcd&labelColor=e9ecef +[track_completion_badge]: https://img.shields.io/badge/Track%20completion-5.5%25-604fcd?logo=exercism&logoColor=604fcd&labelColor=e9ecef +[exercises_completed_badge]: https://img.shields.io/badge/Exercises%20completed-8%2F146-604fcd?logo=exercism&logoColor=604fcd&labelColor=e9ecef [python_track]: https://exercism.org/tracks/python [exercism]: https://exercism.org diff --git a/python/little-sisters-vocab/HINTS.md b/python/little-sisters-vocab/HINTS.md new file mode 100644 index 0000000..d6f16fb --- /dev/null +++ b/python/little-sisters-vocab/HINTS.md @@ -0,0 +1,39 @@ +# Hints + +## General + +- The Python Docs [Tutorial for strings][python-str-doc] has an overview of the Python `str` type. +- String methods [`str.join()`][str-join] and [`str.split()`][str-split] ar very helpful when processing strings. +- The Python Docs on [Sequence Types][common sequence operations] has a rundown of operations common to all sequences, including `strings`, `lists`, `tuples`, and `ranges`. + +There's four activities in the assignment, each with a set of text or words to work with. + +## 1. Add a prefix to a word + +- Small strings can be concatenated with the `+` operator. + +## 2. Add prefixes to word groups + +- Believe it or not, [`str.join()`][str-join] is all you need here. **A loop is not required**. +- The tests will be feeding your function a `list`. There will be no need to alter this `list` if you can figure out a good delimiter string. +- Remember that delimiter strings go between elements and "glue" them together into a single string. Delimiters are inserted _without_ space, although you can include space characters within them. +- Like [`str.split()`][str-split], `str.join()` can process an arbitrary-length string, made up of any unicode code points. _Unlike_ `str.split()`, it can also process arbitrary-length iterables like `list`, `tuple`, and `set`. + +## 3. Remove a suffix from a word + +- Strings can be indexed or sliced from either the left (starting at 0) or the right (starting at -1). +- If you want the last code point of an arbitrary-length string, you can use `[-1]`. +- The last three letters in a string can be "sliced off" using a negative index. e.g. `beautiful'[:-3] == 'beauti` + +## 4. Extract and transform a word + +- Using [`str.split()`][str-split] returns a `list` of strings broken on white space. +- `lists` are sequences, and can be indexed. +- [`str.split()`][str-split] can be directly indexed: `'Exercism rocks!'.split()[0] == 'Exercism'` +- Be careful of punctuation! Periods can be removed via slice: `'dark.'[:-1] == 'dark'` + + +[common sequence operations]: https://docs.python.org/3/library/stdtypes.html#text-sequence-type-str +[python-str-doc]: https://docs.python.org/3/tutorial/introduction.html#strings +[str-join]: https://docs.python.org/3/library/stdtypes.html#str.join +[str-split]: https://docs.python.org/3/library/stdtypes.html#str.split \ No newline at end of file diff --git a/python/little-sisters-vocab/README.md b/python/little-sisters-vocab/README.md new file mode 100644 index 0000000..28fcb6c --- /dev/null +++ b/python/little-sisters-vocab/README.md @@ -0,0 +1,368 @@ +# Little Sister's Vocabulary + +Welcome to Little Sister's Vocabulary on Exercism's Python Track. +If you get stuck on the exercise, check out `HINTS.md`, but try and solve it without using those first :) + +## Introduction + +A `str` in Python is an [immutable sequence][text sequence] of [Unicode code points][unicode code points]. +These could include letters, diacritical marks, positioning characters, numbers, currency symbols, emoji, punctuation, space and line break characters, and more. + Being immutable, a `str` object's value in memory doesn't change; methods that appear to modify a string return a new copy or instance of that `str` object. + + +A `str` literal can be declared via single `'` or double `"` quotes. The escape `\` character is available as needed. + + +```python + +>>> single_quoted = 'These allow "double quoting" without "escape" characters.' + +>>> double_quoted = "These allow embedded 'single quoting', so you don't have to use an 'escape' character." + +>>> escapes = 'If needed, a \'slash\' can be used as an escape character within a string when switching quote styles won\'t work.' +``` + +Multi-line strings are declared with `'''` or `"""`. + + +```python +>>> triple_quoted = '''Three single quotes or "double quotes" in a row allow for multi-line string literals. + Line break characters, tabs and other whitespace are fully supported. + + You\'ll most often encounter these as "doc strings" or "doc tests" written just below the first line of a function or class definition. + They\'re often used with auto documentation ✍ tools. + ''' +``` + +Strings can be concatenated using the `+` operator. + This method should be used sparingly, as it is not very performant or easily maintained. + + +```python +language = "Ukrainian" +number = "nine" +word = "дев'ять" + +sentence = word + " " + "means" + " " + number + " in " + language + "." + +>>> print(sentence) +... +"дев'ять means nine in Ukrainian." +``` + +If a `list`, `tuple`, `set` or other collection of individual strings needs to be combined into a single `str`, [`.join()`][str-join], is a better option: + + +```python +# str.join() makes a new string from the iterables elements. +>>> chickens = ["hen", "egg", "rooster"] # Lists are iterable. +>>> ' '.join(chickens) +'hen egg rooster' + +# Any string can be used as the joining element. +>>> ' :: '.join(chickens) +'hen :: egg :: rooster' + +>>> ' 🌿 '.join(chickens) +'hen 🌿 egg 🌿 rooster' + + +# Any iterable can be used as input. +>>> flowers = ("rose", "daisy", "carnation") # Tuples are iterable. +>>> '*-*'.join(flowers) +'rose*-*daisy*-*carnation' + +>>> flowers = {"rose", "daisy", "carnation"} # Sets are iterable, but output order is not guaranteed. +>>> '*-*'.join(flowers) +'rose*-*carnation*-*daisy' + +>>> phrase = "This is my string" # Strings are iterable, but be careful! +>>> '..'.join(phrase) +'T..h..i..s.. ..i..s.. ..m..y.. ..s..t..r..i..n..g' + + +# Separators are inserted **between** elements, but can be any string (including spaces). +# This can be exploited for interesting effects. +>>> under_words = ['under', 'current', 'sea', 'pin', 'dog', 'lay'] +>>> separator = ' ⤴️ under' +>>> separator.join(under_words) +'under ⤴️ undercurrent ⤴️ undersea ⤴️ underpin ⤴️ underdog ⤴️ underlay' + +# The separator can be composed different ways, as long as the result is a string. +>>> upper_words = ['upper', 'crust', 'case', 'classmen', 'most', 'cut'] +>>> separator = ' 🌟 ' + upper_words[0] +>>> separator.join(upper_words) + 'upper 🌟 uppercrust 🌟 uppercase 🌟 upperclassmen 🌟 uppermost 🌟 uppercut' +``` + +Code points within a `str` can be referenced by `0-based index` number from the left: + + +```python +creative = '창의적인' + +>>> creative[0] +'창' + +>>> creative[2] +'적' + +>>> creative[3] +'인' +``` + +Indexing also works from the right, starting with a `-1-based index`: + + +```python +creative = '창의적인' + +>>> creative[-4] +'창' + +>>> creative[-2] +'적' + +>>> creative[-1] +'인' + +``` + +There is no separate “character” or "rune" type in Python, so indexing a string produces a new `str` of length 1: + + +```python + +>>> website = "exercism" +>>> type(website[0]) + + +>>> len(website[0]) +1 + +>>> website[0] == website[0:1] == 'e' +True +``` + +Substrings can be selected via _slice notation_, using [`[:stop:]`][common sequence operations] to produce a new string. + Results exclude the `stop` index. + If no `start` is given, the starting index will be 0. + If no `stop` is given, the `stop` index will be the end of the string. + + +```python +moon_and_stars = '🌟🌟🌙🌟🌟⭐' +sun_and_moon = '🌞🌙🌞🌙🌞🌙🌞🌙🌞' + +>>> moon_and_stars[1:4] +'🌟🌙🌟' + +>>> moon_and_stars[:3] +'🌟🌟🌙' + +>>> moon_and_stars[3:] +'🌟🌟⭐' + +>>> moon_and_stars[:-1] +'🌟🌟🌙🌟🌟' + +>>> moon_and_stars[:-3] +'🌟🌟🌙' + +>>> sun_and_moon[::2] +'🌞🌞🌞🌞🌞' + +>>> sun_and_moon[:-2:2] +'🌞🌞🌞🌞' + +>>> sun_and_moon[1:-1:2] +'🌙🌙🌙🌙' +``` + +Strings can also be broken into smaller strings via [`.split()`][str-split], which will return a `list` of substrings. + The list can then be further indexed or split, if needed. + Using `.split()` without any arguments will split the string on whitespace. + + +```python +>>> cat_ipsum = "Destroy house in 5 seconds mock the hooman." +>>> cat_ipsum.split() +... +['Destroy', 'house', 'in', '5', 'seconds', 'mock', 'the', 'hooman.'] + + +>>> cat_ipsum.split()[-1] +'hooman.' + + +>>> cat_words = "feline, four-footed, ferocious, furry" +>>> cat_words.split(', ') +... +['feline', 'four-footed', 'ferocious', 'furry'] +``` + +Separators for `.split()` can be more than one character. +The **whole string** is used for split matching. + + +```python + +>>> colors = """red, +orange, +green, +purple, +yellow""" + +>>> colors.split(',\n') +['red', 'orange', 'green', 'purple', 'yellow'] +``` + +Strings support all [common sequence operations][common sequence operations]. + Individual code points can be iterated through in a loop via `for item in `. + Indexes _with_ items can be iterated through in a loop via `for index, item in enumerate()`. + + +```python + +>>> exercise = 'လေ့ကျင့်' + +# Note that there are more code points than perceived glyphs or characters +>>> for code_point in exercise: +... print(code_point) +... +လ +ေ +့ +က +ျ +င +် +့ + +# Using enumerate will give both the value and index position of each element. +>>> for index, code_point in enumerate(exercise): +... print(index, ": ", code_point) +... +0 : လ +1 : ေ +2 : ့ +3 : က +4 : ျ +5 : င +6 : ် +7 : ့ +``` + + +[common sequence operations]: https://docs.python.org/3/library/stdtypes.html#common-sequence-operations +[str-join]: https://docs.python.org/3/library/stdtypes.html#str.join +[str-split]: https://docs.python.org/3/library/stdtypes.html#str.split +[text sequence]: https://docs.python.org/3/library/stdtypes.html#text-sequence-type-str +[unicode code points]: https://stackoverflow.com/questions/27331819/whats-the-difference-between-a-character-a-code-point-a-glyph-and-a-grapheme + +## Instructions + +You are helping your younger sister with her English vocabulary homework, which she is finding very tedious. + Her class is learning to create new words by adding _prefixes_ and _suffixes_. + Given a set of words, the teacher is looking for correctly transformed words with correct spelling by adding the prefix to the beginning or the suffix to the ending. + +The assignment has four activities, each with a set of text or words to work with. + + +## 1. Add a prefix to a word + +One of the most common prefixes in English is `un`, meaning "not". + In this activity, your sister needs to make negative, or "not" words by adding `un` to them. + +Implement the `add_prefix_un()` function that takes `word` as a parameter and returns a new `un` prefixed word: + + +```python +>>> add_prefix_un("happy") +'unhappy' + +>>> add_prefix_un("manageable") +'unmanageable' +``` + + +## 2. Add prefixes to word groups + +There are four more common prefixes that your sister's class is studying: + `en` (_meaning to 'put into' or 'cover with'_), + `pre` (_meaning 'before' or 'forward'_), + `auto` (_meaning 'self' or 'same'_), + and `inter` (_meaning 'between' or 'among'_). + + In this exercise, the class is creating groups of vocabulary words using these prefixes, so they can be studied together. + Each prefix comes in a list with common words it's used with. + The students need to apply the prefix and produce a string that shows the prefix applied to all of the words. + +Implement the `make_word_groups()` function that takes a `vocab_words` as a parameter in the following form: + `[, , .... ]`, and returns a string with the prefix applied to each word that looks like: + `' :: :: :: '`. + +Creating a `for` or `while` loop to process the input is not needed here. +Think carefully about which string methods (and delimiters) you could use instead. + + +```python +>>> make_word_groups(['en', 'close', 'joy', 'lighten']) +'en :: enclose :: enjoy :: enlighten' + +>>> make_word_groups(['pre', 'serve', 'dispose', 'position']) +'pre :: preserve :: predispose :: preposition' + +>> make_word_groups(['auto', 'didactic', 'graph', 'mate']) +'auto :: autodidactic :: autograph :: automate' + +>>> make_word_groups(['inter', 'twine', 'connected', 'dependent']) +'inter :: intertwine :: interconnected :: interdependent' +``` + + +## 3. Remove a suffix from a word + +`ness` is a common suffix that means _'state of being'_. + In this activity, your sister needs to find the original root word by removing the `ness` suffix. + But of course there are pesky spelling rules: If the root word originally ended in a consonant followed by a 'y', then the 'y' was changed to 'i'. + Removing 'ness' needs to restore the 'y' in those root words. e.g. `happiness` --> `happi` --> `happy`. + +Implement the `remove_suffix_ness()` function that takes in a `word`, and returns the root word without the `ness` suffix. + + +```python +>>> remove_suffix_ness("heaviness") +'heavy' + +>>> remove_suffix_ness("sadness") +'sad' +``` + +## 4. Extract and transform a word + +Suffixes are often used to change the part of speech a word is assigned to. + A common practice in English is "verbing" or "verbifying" -- where an adjective _becomes_ a verb by adding an `en` suffix. + +In this task, your sister is going to practice "verbing" words by extracting an adjective from a sentence and turning it into a verb. + Fortunately, all the words that need to be transformed here are "regular" - they don't need spelling changes to add the suffix. + +Implement the `adjective_to_verb(, )` function that takes two parameters. + A `sentence` using the vocabulary word, and the `index` of the word, once that sentence is split apart. + The function should return the extracted adjective as a verb. + + +```python +>>> adjective_to_verb('I need to make that bright.', -1 ) +'brighten' + +>>> adjective_to_verb('It got dark as the sun set.', 2) +'darken' +``` + +## Source + +### Created by + +- @aldraco +- @BethanyG \ No newline at end of file diff --git a/python/little-sisters-vocab/strings.py b/python/little-sisters-vocab/strings.py new file mode 100644 index 0000000..40e6679 --- /dev/null +++ b/python/little-sisters-vocab/strings.py @@ -0,0 +1,87 @@ +"""Functions for creating, transforming, and adding prefixes to strings.""" + + +def add_prefix_un(word): + """Take the given word and add the 'un' prefix. + + Parameters: + word (str): The root word. + + Returns: + str: Root word prepended with 'un'. + """ + + return 'un' + word + + +def make_word_groups(vocab_words): + """Transform a list containing a prefix and words. + + Parameters: + vocab_words (list[str]): Vocabulary words with prefix at first index. + + Returns: + str: Prefix followed by vocabulary words with prefix applied. + + This function takes a `vocab_words` list of strings and returns a string + with the prefix and the words with prefix applied, separated by ' :: '. + + Examples: + >>> list('en', 'close', 'joy', 'lighten') + 'en :: enclose :: enjoy :: enlighten'. + + """ + + prefix = vocab_words.pop(0) + prefixed_words = [prefix + word for word in vocab_words] + prefixed_words.insert(0, prefix) + return ' :: '.join(prefixed_words) + + +def remove_suffix_ness(word): + """Remove the suffix from the word while keeping spelling in mind. + + Parameters: + word (str): Word to remove suffix from. + + Returns: + str: Word with suffix removed & spelling adjusted. + + Examples: + >>> remove_suffix_ness('heaviness') + 'heavy' + + >>> remove_suffix_ness('sadness') + 'sad' + + """ + + if word.endswith('ness'): + base = word[:-4] + if base.endswith('i'): + return base[:-1] + 'y' + return base + return word + + +def adjective_to_verb(sentence, index): + """Change the adjective within the sentence to a verb. + + Parameters: + sentence (str): The word used in a sentence as an adjective. + index (int): Index of the adjective to remove and transform. + + Returns: + str: The extracted adjective in verb form. + + Examples: + >>> adjective_to_verb('It got dark as the sun set.', 2) + 'darken' + + >>> adjective_to_verb('The ink stains her fingers black.', -1) + 'blacken' + + """ + + words = sentence.rstrip('.').split() + return words[index] + 'en' diff --git a/python/little-sisters-vocab/strings_test.py b/python/little-sisters-vocab/strings_test.py new file mode 100644 index 0000000..b13d4e9 --- /dev/null +++ b/python/little-sisters-vocab/strings_test.py @@ -0,0 +1,126 @@ +import unittest +import pytest +from strings import (add_prefix_un, + make_word_groups, + remove_suffix_ness, + adjective_to_verb) + + +class LittleSistersVocabTest(unittest.TestCase): + + @pytest.mark.task(taskno=1) + def test_add_prefix_un(self): + input_data = ['happy', 'manageable', 'fold', 'eaten', 'avoidable', 'usual'] + result_data = [f'un{item}' for item in input_data] + + for variant, (word, expected) in enumerate(zip(input_data, result_data), start=1): + with self.subTest(f'variation #{variant}', word=word, expected=expected): + + actual_result = add_prefix_un(word) + error_message = (f'Called add_prefix_un("{word}"). ' + f'The function returned "{actual_result}", but the ' + f'tests expected "{expected}" after adding "un" as a prefix.') + + self.assertEqual(actual_result, expected, msg=error_message) + + @pytest.mark.task(taskno=2) + def test_make_word_groups_en(self): + input_data = ['en', 'circle', 'fold', 'close', 'joy', 'lighten', 'tangle', 'able', 'code', 'culture'] + expected = ('en :: encircle :: enfold :: enclose :: enjoy :: enlighten ::' + ' entangle :: enable :: encode :: enculture') + + actual_result = make_word_groups(input_data) + error_message = (f'Called make_word_groups({input_data}). ' + f'The function returned "{actual_result}", ' + f'but the tests expected "{expected}" for the ' + 'word groups.') + + self.assertEqual(actual_result, expected, msg=error_message) + + @pytest.mark.task(taskno=2) + def test_make_word_groups_pre(self): + input_data = ['pre', 'serve', 'dispose', 'position', 'requisite', 'digest', + 'natal', 'addressed', 'adolescent', 'assumption', 'mature', 'compute'] + expected = ('pre :: preserve :: predispose :: preposition :: prerequisite :: ' + 'predigest :: prenatal :: preaddressed :: preadolescent :: preassumption :: ' + 'premature :: precompute') + + actual_result = make_word_groups(input_data) + error_message = (f'Called make_word_groups({input_data}). ' + f'The function returned "{actual_result}", ' + f'but the tests expected "{expected}" for the ' + 'word groups.') + + self.assertEqual(actual_result, expected, msg=error_message) + + @pytest.mark.task(taskno=2) + def test_make_word_groups_auto(self): + input_data = ['auto', 'didactic', 'graph', 'mate', 'chrome', 'centric', 'complete', + 'echolalia', 'encoder', 'biography'] + expected = ('auto :: autodidactic :: autograph :: automate :: autochrome :: ' + 'autocentric :: autocomplete :: autoecholalia :: autoencoder :: ' + 'autobiography') + + actual_result = make_word_groups(input_data) + error_message = (f'Called make_word_groups({input_data}). ' + f'The function returned "{actual_result}", ' + f'but the tests expected "{expected}" for the ' + 'word groups.') + + self.assertEqual(actual_result, expected, msg=error_message) + + @pytest.mark.task(taskno=2) + def test_make_words_groups_inter(self): + input_data = ['inter', 'twine', 'connected', 'dependent', 'galactic', 'action', + 'stellar', 'cellular', 'continental', 'axial', 'operative', 'disciplinary'] + expected = ('inter :: intertwine :: interconnected :: interdependent :: ' + 'intergalactic :: interaction :: interstellar :: intercellular :: ' + 'intercontinental :: interaxial :: interoperative :: interdisciplinary') + + actual_result = make_word_groups(input_data) + error_message = (f'Called make_word_groups({input_data}). ' + f'The function returned "{actual_result}", ' + f'but the tests expected "{expected}" for the ' + 'word groups.') + + self.assertEqual(actual_result, expected, msg=error_message) + + @pytest.mark.task(taskno=3) + def test_remove_suffix_ness(self): + input_data = ['heaviness', 'sadness', 'softness', 'crabbiness', 'lightness', 'artiness', 'edginess'] + result_data = ['heavy', 'sad', 'soft', 'crabby', 'light', 'arty', 'edgy'] + + for variant, (word, expected) in enumerate(zip(input_data, result_data), start=1): + with self.subTest(f'variation #{variant}', word=word, expected=expected): + actual_result = remove_suffix_ness(word) + error_message = (f'Called remove_suffix_ness("{word}"). ' + f'The function returned "{actual_result}", ' + f'but the tests expected "{expected}" after the ' + 'suffix was removed.') + + self.assertEqual(actual_result, expected, msg=error_message) + + @pytest.mark.task(taskno=4) + def test_adjective_to_verb(self): + input_data = ['Look at the bright sky.', + 'His expression went dark.', + 'The bread got hard after sitting out.', + 'The butter got soft in the sun.', + 'Her eyes were light blue.', + 'The morning fog made everything damp with mist.', + 'He cut the fence pickets short by mistake.', + 'Charles made weak crying noises.', + 'The black oil got on the white dog.'] + index_data = [-2, -1, 3, 3, -2, -3, 5, 2, 1] + result_data = ['brighten', 'darken', 'harden', 'soften', + 'lighten', 'dampen', 'shorten', 'weaken', 'blacken'] + + for variant, (sentence, index, expected) in enumerate(zip(input_data, index_data, result_data), start=1): + with self.subTest(f'variation #{variant}', sentence=sentence, index=index, expected=expected): + actual_result = adjective_to_verb(sentence, index) + error_message = (f'Called adjective_to_verb("{sentence}", {index}). ' + f'The function returned "{actual_result}", but the tests ' + f'expected "{expected}" as the verb for ' + f'the word at index {index}.') + + self.assertEqual(actual_result, expected, msg=error_message)