diff --git a/.github/workflows/ci-checks.yml b/.github/workflows/ci-checks.yml index b3d7b0052..74de21293 100644 --- a/.github/workflows/ci-checks.yml +++ b/.github/workflows/ci-checks.yml @@ -4,19 +4,19 @@ on: [push, pull_request, workflow_dispatch] jobs: ls_linting: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: ls-lint/action@v2.2.3 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ls-lint/action@v2.2.3 md_formatting: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: nosborn/github-action-markdown-cli@v3.3.0 - with: - files: . - config_file: .markdownlint.yml + - uses: actions/checkout@v4 + - uses: nosborn/github-action-markdown-cli@v3.3.0 + with: + files: . + config_file: .markdownlint.yml py_formatting: runs-on: ubuntu-latest @@ -44,11 +44,11 @@ jobs: if: always() - name: install pylint run: | - python - m pip install --upgrade pip + python -m pip install --upgrade pip pip install pylint shell: bash - name: Python - Check Linting - pylint - run: "pylint solutions tests || echo '::warning title=Pylint Error(s)::Discuss solutions and trade-offs in code review.'" + run: "pylint solutions tests || echo '::warning title=Pylint Error(s)::Discuss solutions and trade-offs in code review.'" shell: bash py_tests: @@ -59,22 +59,12 @@ jobs: - name: python version run: python --version shell: bash - + - name: Check for test files id: check_tests run: | - test_files=$(find ./solutions/tests -type f -name "test_*.py") - if [ -n "$test_files" ]; then - echo "Found test files:" - echo "$test_files" - echo "has_tests=true" >> $GITHUB_OUTPUT - else - echo "No test files found matching pattern ./solutions/tests/test_*.py" - echo "has_tests=false" >> $GITHUB_OUTPUT - fi - shell: bash - + - name: Python - Run Tests if: steps.check_tests.outputs.has_tests == 'true' - run: python -m unittest + run: echo "PYTHONPATH=$GITHUB_WORKSPACE" >> $GITHUB_ENV shell: bash diff --git a/.markdownlint.yml b/.markdownlint.yml index ee6205f5c..8cc0eea94 100644 --- a/.markdownlint.yml +++ b/.markdownlint.yml @@ -1,3 +1,9 @@ ignore: - venv - .github + + +MD013: + line_length: 180 + ignore_urls: true + diff --git a/.vscode/settings.json b/.vscode/settings.json index 252022b48..683a92b16 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -119,8 +119,6 @@ "editor.defaultFormatter": "charliermarsh.ruff", "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.fixAll.ruff": "explicit", - "source.organizeImports.ruff": "explicit" } } } diff --git a/README.md b/README.md index e69de29bb..edc238599 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,48 @@ +# Collab Coders + +> Welcome to the **Collab Coders** repository! 🚀 +We are a team of passionate Junior developers From MET. +> dedicated to building impactful solutions through collaboration and innovation. + +--- + +## 🌟 About Us + +> At **Collab Coders**, we believe in the power of teamwork: + +- to tackle complex challenges and create meaningful projects. +- This repository serves as a hub for our collaborative efforts. +- where ideas turn into reality. + +--- + +## 📂 Repository Structure + +Here’s how the repository is organized: + +- `main`: The primary branch containing stable and reviewed code. +- `feature-branches`: Each team member has their own branch +- for working on features or tasks. +- Branches are named after it is functionality for clarity +- (e.g., `challenges/factorial-function`, `challenges/sort-list`). + +--- + +## 🛠️ Technologies Used + +> Our team use stack of tools and technologies +> based on the program specifications and requirements: + +- **Languages**: Python, Markdown +- **Frameworks**: N/A +- **Tools**: Github,Git, VS Code +- **Communication_channels**: Slack, Google Meet, Zoom, Whatsapp + +--- + +## 🌱 How to Contribute + +1. Clone the repository: + + ```bash + git clone diff --git a/collaboration/README.md b/collaboration/README.md index 20889b951..cbd6a9e75 100644 --- a/collaboration/README.md +++ b/collaboration/README.md @@ -2,4 +2,156 @@ +## Group norms for ET6-Foundation-group-09's project + +**These group norms will serve as the guidelines and directives, everyone is expected to comply with, +yet no repercussions will follow if they are disregarded.** + +## Communication + +### Primary communication channel + +Slack + +WhatsAPP + +GitHub + +Zoom Meeting + +Google meet + +#### Response Time + +Everyone is expected to response to the messages on all the communication channels within 24 hours. + +#### Language of communication + +The official language of communication is English, and we strongly recommend using clear, concise language, avoid using jargon. + +#### Official Meeting platforms + +Zoom Meeting + +Google Meet + +The group meeting will be conducted on the official platforms at least once in the week which can be adjusted based on group demands. + +The time will be discussed and agreed by all the participants on slack and WhatsAPP. + +All the meetings are expected to last for one hour. + +The recording will be made available after the meeting and uploaded to the group channels, it will be helpful to the participants who missed the meeting. + +All the participants who missed the meeting are advised to watch the recording before sharing their opinions. + +## Group Organization + +**The team organization will ensure effective communication among the group members, making informed decisions +and ensuring that everyone is working on the same goal.** + +### Organizational Structure + +### Project lead + +### Responsibilities + +Oversees the project, ensuring timely completion and making sure no member is left behind. + +The project lead has no absolute authority over the group members. + +Coordinates the resolution of key issues and pull requests + +### Head of the project Communication + +### Key Responsibilities + +Discussion management on Github and WhatsAPP + +Schedule and control the group meeting + +### Deputy Head of the project Communication + +Assist the Head of communication in implementing communication plan + +Provides supports to group members, helping in resolving issues and answering questions. + +### Technical lead + +Assist in code review and conflict resolution + +### Collaboration Norms + +### collaboration Tools + +Github + +Git + +Visual Code Studio (VS Code) + +### Task Assignment + +Each team member will assign him/her self to the task using Github issues + +Everyone is expected to work individually and push it to the branch upon completion + +### Code Review + +Team members will review and provide feedback on each others's code within 48 hours + +we anticipate everyone bear in mind that we are reviewing codes not individuals. + +### Branching and Merging + +we will use feature branches and pull request to manage different version of codes + +## Conflict Resolution + +### Open Communication + +All the members are expected to communicate their concerns and challenges + +### Constructive Feedback + +We encourage the culture of constructive feedback and respectful criticism + +Remember we are reviewing codes not people + +### Conflict resolution process + +All sorts of conflict and misunderstanding shall be resolved through open discussion and mutual understanding + +## Respect and Inclusion + +### Equality + +All the group members have equal rights and their opinion must be respected. + +We equal participation from all the group members. + +### Respectful Communication + +Group members are expected to communicate respectful and professionally by avoiding offensive language. + +### Decision Making + +All the team members will have their voices in decision making. + +### Diversity + +We endeavor to create an inclusive environment that fosters diversity. + + +## Groups Norms List + +Communication Norms + +Organization norms + +Collaboration Norms + +Conflict Resolution Norms + +Respect and Inclusion Norms diff --git a/collaboration/communication.md b/collaboration/communication.md index 484652e0f..cc1936106 100644 --- a/collaboration/communication.md +++ b/collaboration/communication.md @@ -7,41 +7,44 @@ you can share the rest in confidence with you group by another channel --> -# Communication +# Communication 📬 ______________________________________________________________________ ## Communication Schedule -| Day | How | The topic of discussion | | --- | :-: | ----------------------- | +| Day & Time | How | The topic of discussion | +| --- | :-: | ----------------------- | +|Every Friday, 9 EST| Google Meet| Activity Updates| | | | | ## Communication Channels how often will we get in touch on each channel, and what we will discuss there: -- **Issues**: -- **Pull Requests**: -- **Slack/Discord**: -- **Video Calls**: +- **Issues**: To track team progress, update as needed, and assign tasks where needed +- **Pull Requests**: Reviewed within 48 hours for feedback, approving the change and merging it to the main branch + +- **Slack**: For daily task updates and resolving small problems from team members +- **Video Calls**: Zoom/Google meet preferably for general group meetings and weekly updates ______________________________________________________________________ ## Availability -### Availability for calling/messaging - -| Day | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday | | ------- | :----: | :-----: | :-------: | :------: | :----: | :------: | :----: | -| _name_ | | | | | | | | - -### How many hours everyone has per day - -- name: _5h_; -- name: _6h_; -- name: _5h_; -- name: _4h_; -- name: _3h_; +### Availability for calling/messaging(+how many hours each day in EST) + +| Day | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday | +|:------ | :----: | :-----: | :-------: | :------: | :----: | :------: | ----: | +| Hiba | 9-12 | 9-12 | 9-12 | 9-12 | 9-12 | 9-12 | 9-12 | +| Jeffery | 9-12 | 9-12 | 9-12 | 9-12 | 9-12 | 9-12 | 9-12 | +| Kelvin | | | | | | | | +| Mahdia | 9-12 | 9-12 | 10-12 | 10-12 | 9-12 | 9-12 | 10-12 | +| Martha | 9-12 | 9-12 | 9-12 | 9-12 | 9-12 | 9-12 | 9-12 | +| Osei | 10-13 |11-15 | 9-12 | 9-12 | 9-15 | 13-15 | 9-15| +| Rina | | 7-15 | | 7-15 | 7-15 | 9-15 | 9-15 | +| Terry | 9-11 | 9-11 | 9-11 | 6-9 | 9-11 | 9-12 | 9-11 | +| Yool | 9-12 | 9-12 | 10-13 | 8-12 | 9-12 | 10-13 | 10-13 | ## Asking for Help diff --git a/collaboration/constraints.md b/collaboration/constraints.md index 24079505c..92d48196e 100644 --- a/collaboration/constraints.md +++ b/collaboration/constraints.md @@ -2,7 +2,8 @@ # Constraints -Some boundaries around our project. +In any collaborative project, understanding and addressing constraints is essential for success. +A survey was conducted within the group to know the constraint of each member of our group. Below are the internal and external constraint of the group. ## External @@ -15,6 +16,13 @@ Some boundaries around our project. - ... --> +- **Primary Constraint:** + - Technology limitations (e.g., Git, GitHub, and communication tools) were the most frequently cited + external constraint, affecting 75% of respondents. +- Time zone differences and internet connectivity are also notable external constraints, each affecting 50% of respondents. +- **Other Concerns:** + - Project deadlines, inconsistent power supply, and access to a personal computer were mentioned by 25% of respondents each. + ## Internal: Involuntary -## Internal: Voluntary +- **Primary Constraint:** + - The individual skill levels of group members were cited by 62.5% of respondents, making it the most significant internal constraint. +- **Other notable constraints (each at 37.5%) include:** + - The amount of time available to work on the project. + - Individual work schedules. + - Inability to respond quickly to messages. +- **Least Concern:** + - The number of hours members want to spend working (12.5%). +## Internal: Voluntary + +- **Primary Constraint**: + - Technology limitations (e.g., Git, GitHub, and communication tools) were the most frequently cited external constraint, affecting 75% of respondents. +- **Time Zone Differences and Connectivity**: + - Time zone differences and internet connectivity are notable external constraints, each affecting 50% of respondents. +- **Other Concerns**: + - Project deadlines, inconsistent power supply, and access to a personal computer were mentioned by 25% of respondents each. diff --git a/collaboration/learning_goals.md b/collaboration/learning_goals.md index 11c583d2b..bf41a3eba 100644 --- a/collaboration/learning_goals.md +++ b/collaboration/learning_goals.md @@ -1,5 +1,23 @@ -# Learning Goals +# Learning Goals 🧑🏽‍💻 -## Collective +## Collective Goals -## Individual +- Collaboration and productivity between team members in GitHub and Slack. +- Get used to Git and GitHub. +- Communicate effectively and have an inclusive and friendly environment. +- Learn more about documentation and testing in programming. +- Learn more about Python language features. + +## Individual Goals + +| Name | Goals | +| ------- | ------------------------------------------------------------------------------------------------------------------------------ | +| Mahdia | Enhance skills in Python for data analysis and automation scripts. | +| Jeffery | Master foundational data analysis skills, build a portfolio with real-world projects, earn certifications to kickstart my career in data analysis.| +| Hiba | Enhance my collaboration skills and develop good programming habits like documenting. | +| Rina | Learn how to use GitHub and the basics of coding. | +| Yool | Gain a solid understanding of Python, Git, GitHub, and teamwork to advance my computer science career, build projects, gain collaborative experience.| +| Osei | Learn the basics of Git and GitHub, how to send a pull request, and the basics of Python, which include documenting and testing. | +| Martha | Master skills in version control for tracking changes, collaborating on projects, and improve my knowledge in Python for data analysis. | +| Kelvin | | +| Terry | Deepen Python and Git/GitHub skills, collaborate effectively with teammates, and create a program that helps others in the foundation for growth.| diff --git a/collaboration/retrospective.md b/collaboration/retrospective.md index 74e18813b..69e9e9ee0 100644 --- a/collaboration/retrospective.md +++ b/collaboration/retrospective.md @@ -1,23 +1,47 @@ - + # Retrospective ## Stop Doing +- Skipping regular informal check-ins between scheduled meetings. Introducing occasional touchpoints could help identify and address potential blockers earlier. +- Assuming that everyone was fully comfortable with the tools and workflows. While most team members adapted well, a little extra guidance or troubleshooting sessions could have eased the learning curve for some. + ## Continue Doing +- Regular Friday meetings to track progress and provide updates. These were effective in aligning team members and ensuring transparency. +- Using GitHub pull requests for code reviews and collaboration. The process of reviewing changes within 48 hours worked well. +- Maintaining an inclusive and open communication culture where team members feel free to ask for help, as outlined in the "Asking for Help" section. + ## Start Doing +- Implementing a clearer method for prioritizing tasks to ensure everyone knows the critical priorities for the week. +- Holding occasional training or Q&A sessions to address gaps in technical skills, especially in Git and Python. +- Creating a shared calendar or reminder system to avoid any potential conflicts in availability for meetings or task deadlines. + ## Lessons Learned -______________________________________________________________________ +- **Time Zone Challenges**: The group realized that time zone differences, while manageable, required better planning for synchronous communication. +- **Skill Gaps**: Variations in team members' proficiency with tools highlighted the need for tailored support to help everyone contribute effectively. +- **Flexibility and Adaptability**: The team showed great adaptability in overcoming technology limitations and communication barriers. However, this can be improved further by having contingency plans for unexpected constraints. ## Strategy vs. Board ### What parts of your plan went as expected? +- The team successfully conducted weekly meetings, reviewed pull requests on time, and collaborated effectively on Slack. + ### What parts of your plan did not work out? +- The reliance on Slack or GitHub only for resolving complex issues sometimes caused delays. More face-to-face virtual calls could have streamlined problem-solving. + ### Did you need to add things that weren't in your strategy? +- Introduce a weekly summary of constraints and priorities to keep everyone updated. +- Create clearer guidelines for documenting issues and resolutions in GitHub. + ### Or remove extra steps? + +- Reduce redundant follow-ups in Slack by consolidating updates during meetings. +- Modify Group norms accordingly to the team’s needs. + diff --git a/solutions/add_numbers.py b/solutions/add_numbers.py new file mode 100644 index 000000000..e8e742581 --- /dev/null +++ b/solutions/add_numbers.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +# -- coding: utf-8 -- + +""" +A module that takes two numbers and return their sum. + +Module contents: Takes two numbers and return their sum. + +Created on 2025-01-08 + +@author: Martha Yelademe Nyekanga +""" + + +def add_numbers(first_number: int, second_number: int) -> int: + """This function takes two numbers and returns their sum. + Parameters: + + first_number (int): The first number for the function + second_number (int): The second number for the function + + Returns: + int: The sum of the two numbers + + Raises: + TypeError: If the input is not a number + + Examples: + >>> add_numbers(2, 3) + 5 + + >>> add_numbers(-2, 3) + 1 + + >>> add_numbers(0, 0) + 0 + + >>> add_numbers(2, -3) + -1 + """ + + assert isinstance(first_number, int), "Input must be a number." + assert isinstance(second_number, int), "Input must be a number." + + # return the sum of the two numbers + return first_number + second_number + + +print(add_numbers(0, 0)) diff --git a/solutions/even_numbers.py b/solutions/even_numbers.py new file mode 100644 index 000000000..b2e75379e --- /dev/null +++ b/solutions/even_numbers.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# -- coding: utf-8 -- + +""" +This module checks if a given number is even and returns True for even numbers or False otherwise. +Created on 2025-01-11 + +@author: Martha Yelademe Nyekanga +""" + + +def is_even(number: int) -> bool: + """This function checks if a given number is even. + + Parameters: + number (int): The number to check + + Returns: + bool: True if the number is even, False otherwise + + Raises: + AssertionError: If the input is not an integer + + Examples: + >>> is_even(2) + True + + >>> is_even(3) + False + + >>> is_even(0) + True + + >>> is_even(-4) + True + """ + assert isinstance(number, int) and not isinstance(number, bool), ( + "the input number must be an integer and not a boolean" + ) + # Return True if the number is even, False otherwise + return number % 2 == 0 + + +# Example: +if __name__ == "__main__": + print(is_even(0)) diff --git a/solutions/find_longest_palindrome.py b/solutions/find_longest_palindrome.py new file mode 100644 index 000000000..8297df846 --- /dev/null +++ b/solutions/find_longest_palindrome.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +A module for finding the longest palindromic substring in a string. + +Module contents: + - find_longest_palindrome: finds the longest palindrome within a string. + - is_palindrome: helper function to check if a string is a palindrome. + +Created on 28 12 2024 +@author: Mahdia Ahmadi +""" + + +def find_longest_palindrome(input_string: str) -> str: + """ + Finds the longest palindromic substring within the input string. + + A palindrome reads the same forwards and backwards. This function returns + the first occurrence of the longest palindrome if multiple exist. + + Parameters: + text: str, the input string to search for palindromes + + Returns: + str: the longest palindromic substring found + + Raises: + AssertionError: if the input is not a string + + Examples: + >>> find_longest_palindrome("babad") + 'bab' + >>> find_longest_palindrome("racecar") + 'racecar' + >>> find_longest_palindrome("hello") + 'll' + >>> find_longest_palindrome("python") + 'p' + >>> find_longest_palindrome("") + '' + """ + # Input validation + assert isinstance(input_string, str), "Input must be a string" + + # Handle empty string and single characters + if len(input_string) <= 1: + return input_string + + # Initialize variables to track the longest palindrome + longest_start = 0 + longest_length = 1 + + # Check all possible substrings + for start in range(len(input_string)): + for end in range(start + longest_length, len(input_string) + 1): + substring = input_string[start:end] + # Only check if this could be a longer palindrome + if is_palindrome(substring) and len(substring) > longest_length: + longest_start = start + longest_length = len(substring) + + # Return the longest palindrome found + return input_string[longest_start : longest_start + longest_length] + + +def is_palindrome(input_string: str) -> bool: + """ + Checks if the input string is a palindrome. + + Parameters: + text: str, string to check + + Returns: + bool: True if the string is a palindrome, False otherwise + + Examples: + >>> is_palindrome("racecar") + True + >>> is_palindrome("hello") + False + >>> is_palindrome("a") + True + >>> is_palindrome("") + True + """ + # Compare string with its reverse + return input_string == input_string[::-1] diff --git a/solutions/find_two_sum_indices.py b/solutions/find_two_sum_indices.py new file mode 100644 index 000000000..81529f8ac --- /dev/null +++ b/solutions/find_two_sum_indices.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +A module for providing the indices of two numbers in a list that add up to a target sum. + +Module contents: Takes a list of numbers (integers or floats) and a target sum as input and returns the indices of the two numbers that add up to the target sum. + +Created on 2025-01-04 + +@author: Jeffery Offei Darko +""" + + +def find_two_sum_indices(nums: list[float], target: float) -> list[int]: + """ + Find two indices in the list such that their values add up to the target. + + Parameters: + nums (list[float]): The list of numbers (integers or floats). + target (float): The target sum. + + Returns: + list[int]: The indices of the two numbers that add up to the target. + + Raises: + AssertionError: If the input is not valid or no solution is found. + + Examples: + >>> find_two_sum_indices([2, 7, 11, 15], 9) + [0, 1] + + >>> find_two_sum_indices([3, 2.5, 4], 6.5) + [1, 2] + + >>> find_two_sum_indices([3.0, 3], 6.0) + [0, 1] + """ + import math + + # Validate inputs + assert isinstance(nums, list), "Input must be a list." + assert all(isinstance(num, (int, float)) for num in nums), ( + "List must contain numbers." + ) + assert isinstance(target, (int, float)), "Target must be a number." + assert len(nums) > 1, "List must contain at least two numbers." + assert not any(math.isnan(x) for x in nums), ( + "Input list must not contain NaN values." + ) + assert not math.isnan(target), "Target must not be NaN." + + # Main logic with floating-point precision handling + epsilon = 1e-7 + num_map = {} + for i, num in enumerate(nums): + complement = target - num + for seen_num, seen_index in num_map.items(): + if abs(seen_num - complement) < epsilon: + return [seen_index, i] + num_map[num] = i + + assert False, "No two numbers add up to the target sum." diff --git a/solutions/longest_substring.py b/solutions/longest_substring.py new file mode 100644 index 000000000..86e7bfa84 --- /dev/null +++ b/solutions/longest_substring.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +A module for generating the longest substring + +Module contents: + longest_substring: generates the longest substring without + repetition for a given string. + +Created on 2025-01-05 +@author: Hiba Daffallah +""" + + +def longest_substring(input_string: str) -> str: + """ + Generates a string containing the longest substring without repitition + from the input_string. + + Parameters: + input_string: str + + Returns : + substring: the longest substring + + Raises: + AssertionError: if the argument is not a string + + >>> longest_substring('') + '' + >>> longest_substring('bbbbbb') + 'b' + >>> longest_substring('abcdef') + 'abcdef' + >>> longest_substring('pwwkew') + 'wke' + >>> longest_substring('celelocceli') + 'eloc' + """ + # Ensure the input is a string + assert isinstance(input_string, str) + + n = len(input_string) + # If the string is empty, return an empty string as the result + if n == 0: + return "" + # Initialize a set to keep track of unique characters in the current substring + sub = set() + # Initialize the longest substring variable + longest_sub = "" + # Initialize the left pointer of the sliding window + left = 0 + for right in range(n): + # If the character at the right pointer is already in the set, + # slide the left pointer to remove characters until the duplicate is removed + while input_string[right] in sub: + sub.remove(input_string[left]) + left += 1 + + # Add the current character at the right pointer to the set + sub.add(input_string[right]) + current_len = right - left + 1 + + # If the current substring is longer than the previously recorded longest substring, + # update the longest substring + if current_len > len(longest_sub): + longest_sub = input_string[left : right + 1] + + # Return the longest substring found + return longest_sub diff --git a/solutions/number_to_word.py b/solutions/number_to_word.py new file mode 100644 index 000000000..b23a3171c --- /dev/null +++ b/solutions/number_to_word.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +A module for converting single-digit number into its word representation. + +Module contents: + - number_to_word: converts a number ranging from (0-9) to it's word equivalent. + +Created on 04 01 2024 +@author: Terry Aziaba +""" + + +def number_to_word(number: int) -> str: + """ + Converts a single-digit number into its word representation. + + Parameters: + number (int): A single-digit number (0–9). + + Returns: + str: The word representation of the number. + + Raises: + ValueError: If the number is not a single digit. + + Examples: + >>> number_to_word(0) + 'zero' + + >>> number_to_word(7) + 'seven' + + >>> number_to_word(10) + Traceback (most recent call last): + ... + ValueError: Number must be a single digit (0–9). + """ + # A dictionary mapping digits to their word representation + digit_to_word = { + 0: "zero", + 1: "one", + 2: "two", + 3: "three", + 4: "four", + 5: "five", + 6: "six", + 7: "seven", + 8: "eight", + 9: "nine", + } + + # Checks if the input is a valid single-digit number + if number not in digit_to_word: + raise ValueError("Number must be a single digit (0–9).") + + # Returns the word representation + return digit_to_word[number] diff --git a/solutions/parse_email_alias.py b/solutions/parse_email_alias.py new file mode 100644 index 000000000..89b17b61a --- /dev/null +++ b/solutions/parse_email_alias.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +A module for parsing and validating email aliases. + +This module provides functionality to validate email addresses and parse their components, +including support for Gmail-style plus (+) aliases and subdomain addresses. The implementation +combines specific validation checks with regex pattern matching to provide detailed error +messages while maintaining RFC 5322 compliance. + +Module contents: + - parse_email_alias: validates and extracts components of an email alias + +Features: + - Full RFC 5322 compliant email validation + - Support for Gmail-style + aliases + - Support for subdomains + - Detailed error reporting for specific validation failures + - Comprehensive component extraction + +Created on 4 01 2025 +@author: Mahdia Ahamdi +""" + +import re + + +def parse_email_alias(email: str) -> dict: + """ + Parse and validate an email address with optional alias tags. + + This function performs comprehensive validation of email addresses, providing + specific error messages for different types of validation failures. It supports + Gmail-style plus (+) aliases and subdomains while validating against RFC 5322 + standards for email format. + + Parameters: + email: str + The email address to parse. Must be a string containing the email address + to be validated and parsed. + + Returns: + dict + A dictionary containing the following keys: + - valid (bool): Whether the email is valid + - local_part (str | None): Part before the @ (None if invalid) + - domain (str | None): Part after the @ (None if invalid) + - tags (list[str]): Any alias tags found (empty if none) + - errors (list[str]): Any validation errors found + + Raises: + AssertionError: If the input is not a string + + Examples: + >>> # Test basic valid email + >>> result = parse_email_alias("user@example.com") + >>> result['valid'] + True + >>> result['local_part'] + 'user' + >>> result['domain'] + 'example.com' + >>> result['tags'] + [] + >>> result['errors'] + [] + + >>> # Test email with single alias tag + >>> result = parse_email_alias("user+newsletter@example.com") + >>> result['valid'] + True + >>> result['local_part'] + 'user' + >>> result['domain'] + 'example.com' + >>> result['tags'] + ['newsletter'] + + >>> # Test email with multiple alias tags + >>> result = parse_email_alias("user+tag1+tag2@example.com") + >>> result['valid'] + True + >>> result['local_part'] + 'user' + >>> result['tags'] + ['tag1', 'tag2'] + + >>> # Test email with subdomain + >>> result = parse_email_alias("user@sub.example.com") + >>> result['valid'] + True + >>> result['domain'] + 'sub.example.com' + + >>> # Test missing @ symbol + >>> result = parse_email_alias("invalid.email") + >>> result['valid'] + False + >>> result['errors'] + ['Missing @ symbol'] + + >>> # Test empty local part + >>> result = parse_email_alias("@example.com") + >>> result['valid'] + False + >>> result['errors'] + ['Empty local part'] + + >>> # Test empty domain + >>> result = parse_email_alias("user@") + >>> result['valid'] + False + >>> result['errors'] + ['Empty domain'] + + >>> # Test invalid characters in local part + >>> result = parse_email_alias("user<>@example.com") + >>> result['valid'] + False + >>> result['errors'] + ['Invalid characters in local part'] + + >>> # Test missing TLD + >>> result = parse_email_alias("user@domain") + >>> result['valid'] + False + >>> result['errors'] + ['Invalid domain (missing TLD)'] + + >>> # Test invalid domain format + >>> result = parse_email_alias("user@[192.168.1.1]") + >>> result['valid'] + False + >>> result['errors'] + ['Invalid domain format'] + + >>> # Test empty email + >>> result = parse_email_alias("") + >>> result['valid'] + False + >>> result['errors'] + ['Email cannot be empty'] + + >>> # Test non-string input + >>> try: + ... parse_email_alias(123) + ... except AssertionError as e: + ... print(str(e)) + Input must be a string + """ + assert isinstance(email, str), "Input must be a string" + + result = { + "valid": False, + "local_part": None, + "domain": None, + "tags": [], + "errors": [], + } + + if not email: + result["errors"].append("Email cannot be empty") + return result + + # Check for @ symbol first + if "@" not in email: + result["errors"].append("Missing @ symbol") + return result + + # Split into local part and domain + local_part, domain = email.split("@", 1) + + # Check empty local part + if not local_part: + result["errors"].append("Empty local part") + return result + + # Check empty domain + if not domain: + result["errors"].append("Empty domain") + return result + + # Validate local part characters using regular expression(regx) + allowed_chars = set( + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.!#$%&'*+-/=?^_`{|}~" + ) + if not all(c in allowed_chars for c in local_part.split("+")[0]): + result["errors"].append("Invalid characters in local part") + return result + + # Validate domain + if not domain: + result["errors"].append("Empty domain") + return result + + if ".." in domain: + result["errors"].append("Invalid domain format") + return result + + # Check for valid domain format + domain_parts = domain.split(".") + + # Check for missing or invalid TLD + if len(domain_parts) == 1 or not domain_parts[-1]: + result["errors"].append("Invalid domain (missing TLD)") + return result + + # Validate domain format using regx + domain_pattern = r"^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$" + if not re.match(domain_pattern, domain): + result["errors"].append("Invalid domain format") + return result + + # Extract tags if present + base_local = local_part + tags = [] + if "+" in local_part: + base_local, *tags = local_part.split("+") + + result["valid"] = True + result["local_part"] = base_local + result["domain"] = domain + result["tags"] = tags + + return result diff --git a/solutions/phone_letter_combinations.py b/solutions/phone_letter_combinations.py new file mode 100644 index 000000000..3730d54b2 --- /dev/null +++ b/solutions/phone_letter_combinations.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +A module for all the possible letter combinations for a given number + +Module contents: + phone_letter_combinations: Creates a new list with possible + letter combinations in phone keyboard for the numbers given + for simplification I made it maximum of 2 digits + +Created on 2025-01-07 +Author: Hiba Daffallah +""" + + +def phone_letter_combinations(digits: str) -> list[str]: + """ + phone_letter_combinations function creates a new list with possible + letter combinations in phone keyboard for the numbers given + + + Parameters: + digits: string of numbers it should be from 2 to 9 + + Returns: + possible_combinations: list of characters + + Raises: + AssertionError if the input was not a number + + Example: + >>> phone_letter_combinations("23") + ['ad', 'ae', 'af', 'bd', 'be', 'bf', 'cd', 'ce', 'cf'] + >>> phone_letter_combinations("") + [] + >>> phone_letter_combinations("234") + ['adg', 'adh', 'adi', 'aeg', 'aeh', 'aei', 'afg', 'afh', + 'afi', 'bdg', 'bdh', 'bdi', 'beg', 'beh', 'bei', 'bfg', + 'bfh', 'bfi', 'cdg', 'cdh', 'cdi', 'ceg', 'ceh', 'cei', 'cfg', 'cfh', 'cfi'] + >>> phone_letter_combinations("1") + Input must only contain digits from 2 to 9. + """ + if not all(char in "23456789" for char in digits): + raise AssertionError("Input must only contain digits from 2 to 9.") + if not digits: + return [] + digit_to_letters = { + "2": "abc", + "3": "def", + "4": "ghi", + "5": "jkl", + "6": "mno", + "7": "pqrs", + "8": "tuv", + "9": "wxyz", + } + + def possibility(i, temp): + if i == len(digits): + combination.append(temp[:]) + return + for letter in digit_to_letters[digits[i]]: + possibility(i + 1, temp + letter) + + combination = [] + possibility(0, "") + + return combination diff --git a/solutions/reverse_string.py b/solutions/reverse_string.py new file mode 100644 index 000000000..735097482 --- /dev/null +++ b/solutions/reverse_string.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +A module for generating the reversing strings + +Module contents: Takes a string as input and returns its reversed version + +Created on 2025-01-01 + +@author: Jeffery Offei Darko +""" + + +def reverse_string(input_string: str): + """This function takes a string and returns the reverse form of the string + + Parameters: + string (str): The input to be reversed + + Returns: + str: The reversed version of the input string + + Raises: + AssertionError: If the input is not a string + + Examples: + >>> reverse_string('hello') + 'olleh' + + >>> reverse_string('Python programming') + 'gnimmargorp nohtyP' + + >>> reverse_string('12345') + '54321' + + >>> reverse_string('Madam') + 'madaM' + + >>> reverse_string('racecar') + 'racecar' + + >>> reverse_string('!@#$%^&*()') + ')(*&^%$#@!' + """ + # Check if input is a string + assert isinstance(input_string, str), "Input must be a string" + + return input_string[::-1] + + +print(reverse_string("Evan Cole")) diff --git a/solutions/tests/test_add_numbers.py b/solutions/tests/test_add_numbers.py new file mode 100644 index 000000000..cb6dd8e53 --- /dev/null +++ b/solutions/tests/test_add_numbers.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +# -- coding: utf-8 -- +""" +Test module for adding two numbers. + +Created on 2025-01-10 + +Author: Martha Yelademe Nyekanga +""" + +import unittest +from ..add_numbers import add_numbers + + +class TestAddNumbers(unittest.TestCase): + """This class contains tests for the add_numbers function.""" + + def test_add_two_positive_numbers(self): + """Test adding two positive numbers.""" + self.assertEqual(add_numbers(2, 3), 5) + + def test_add_positive_and_negative_numbers(self): + """Test adding a positive and a negative number.""" + self.assertEqual(add_numbers(-2, 3), 1) + + def test_add_zeros(self): + """Test adding two zeros.""" + self.assertEqual(add_numbers(0, 0), 0) + + def test_add_two_negative_numbers(self): + """Test adding two negative numbers.""" + self.assertEqual(add_numbers(-2, -3), -5) + + def test_add_two_float_numbers(self): + """ "Test adding two float numbers.""" + with self.assertRaises(AssertionError): + add_numbers(2.5, 1.3) + + def test_add_numbers_string(self): + """Test adding a string and a number.""" + with self.assertRaises(AssertionError): + add_numbers("2", 3) diff --git a/solutions/tests/test_even_numbers.py b/solutions/tests/test_even_numbers.py new file mode 100644 index 000000000..1c0ce00d7 --- /dev/null +++ b/solutions/tests/test_even_numbers.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +# -- coding: utf-8 -- +""" +Test module to check is a number is even + +Created on 2025-01-11 + +Author:Martha Yelademe Nyekanga +""" + +import unittest +from ..even_numbers import is_even + + +class TestIsEven(unittest.TestCase): + # Test case for a positive even number + def test_positive_even(self): + self.assertTrue(is_even(2)) + + # Test case for a positive odd number + def test_positive_odd(self): + self.assertFalse(is_even(3)) + + # Test case for zero (edge case) + def test_zero(self): + self.assertTrue(is_even(0)) + + # Test case for a negative even number + def test_negative_even(self): + self.assertTrue(is_even(-4)) + + # Test case for a negative odd number + def test_negative_odd(self): + self.assertFalse(is_even(-7)) + + # Test case for a large positive even number + def test_large_even(self): + self.assertTrue(is_even(1000000)) + + # Test case for a large positive odd number + def test_large_odd(self): + self.assertFalse(is_even(1000001)) + + # Test case for a small positive even number + def test_small_even(self): + self.assertTrue(is_even(2)) + + # Test case for a small positive odd number + def test_small_odd(self): + self.assertFalse(is_even(1)) + + # Test case to check if a float raises a TypeError + def test_type_error_with_float(self): + with self.assertRaises(AssertionError): + is_even(4.5) + + # Test case to check if a string raises a TypeError + def test_type_error_with_string(self): + with self.assertRaises(AssertionError): + is_even("10") + + # Test case to check if a list raises a TypeError + def test_type_error_with_list(self): + with self.assertRaises(AssertionError): + is_even([2]) + + # Test case to check if a dictionary raises a TypeError + def test_type_error_with_dict(self): + with self.assertRaises(AssertionError): + is_even({"number": 4}) + + # Test case to check if None raises a TypeError + def test_type_error_with_none(self): + with self.assertRaises(AssertionError): + is_even(None) + + # Test case to check if a boolean raises a TypeError + def test_type_error_with_bool(self): + with self.assertRaises(AssertionError): + is_even(True) diff --git a/solutions/tests/test_find_longest_palindrome.py b/solutions/tests/test_find_longest_palindrome.py new file mode 100644 index 000000000..543f3901c --- /dev/null +++ b/solutions/tests/test_find_longest_palindrome.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Test module for the longest palindrome finder functionality. + +This module contains unittest test cases for the find_longest_palindrome and +is_palindrome functions, including edge cases and invalid inputs. + +Created on 28 12 2024 +@author: Mahdia Ahmadi +""" + +import unittest + +from solutions.find_longest_palindrome import find_longest_palindrome, is_palindrome + + +class TestPalindromeFinder(unittest.TestCase): + """ + Test suite for palindrome finding functions. + + This class contains unit tests for both the main palindrome finding function + and its helper function, covering normal cases, edge cases, and error conditions. + """ + + def test_empty_string(self): + """ + Test that an empty string returns an empty string. + """ + self.assertEqual(find_longest_palindrome(""), "") + + def test_single_character(self): + """ + Test that a single character string returns that character. + """ + self.assertEqual(find_longest_palindrome("a"), "a") + + def test_two_same_characters(self): + """ + Test string with two identical characters. + """ + self.assertEqual(find_longest_palindrome("aa"), "aa") + + def test_two_different_characters(self): + """ + Test string with two different characters returns first character. + """ + self.assertEqual(find_longest_palindrome("ab"), "a") + + def test_simple_odd_palindrome(self): + """ + Test finding a simple odd-length palindrome. + """ + self.assertEqual(find_longest_palindrome("racecar"), "racecar") + + def test_simple_even_palindrome(self): + """ + Test finding a simple even-length palindrome. + """ + self.assertEqual(find_longest_palindrome("abba"), "abba") + + def test_multiple_palindromes(self): + """ + Test string containing multiple palindromes returns the longest. + """ + self.assertEqual(find_longest_palindrome("abbaracecarxy"), "racecar") + + def test_overlapping_palindromes(self): + """ + Test string with overlapping palindromes. + """ + self.assertEqual(find_longest_palindrome("abababa"), "abababa") + + def test_no_long_palindrome(self): + """ + Test string with no palindromes longer than one character. + """ + self.assertEqual(find_longest_palindrome("python"), "p") + + def test_case_sensitive(self): + """ + Test that the function is case-sensitive. + """ + self.assertNotEqual(find_longest_palindrome("Racecar"), "Racecar") + + def test_with_spaces(self): + """ + Test string containing spaces. + """ + self.assertEqual(find_longest_palindrome("race car"), "r") + + def test_with_special_characters(self): + """ + Test string containing special characters. + """ + self.assertEqual(find_longest_palindrome("a!@#a"), "a") + + # defensive testcase + def test_invalid_input_type(self): + """ + Test that non-string input raises an AssertionError. + """ + with self.assertRaises(AssertionError): + find_longest_palindrome(123) + + +class TestIsPalindrome(unittest.TestCase): + """ + Test suite for the is_palindrome helper function. + """ + + def test_empty_string_palindrome(self): + """ + Test that an empty string is considered a palindrome. + """ + self.assertEqual(is_palindrome(""), True, "Empty string should be a palindrome") + + def test_single_char_palindrome(self): + """ + Test that a single character is a palindrome. + """ + self.assertEqual( + is_palindrome("x"), True, "Single character should be a palindrome" + ) + + def test_simple_palindrome(self): + """ + Test a simple palindrome string. + """ + self.assertEqual( + is_palindrome("level"), True, "The word 'level' should be a palindrome" + ) + + def test_non_palindrome(self): + """ + Test a non-palindrome string. + """ + self.assertEqual( + is_palindrome("python"), + False, + "The word 'python' should not be a palindrome", + ) + + def test_palindrome_with_spaces(self): + """ + Test that spaces affect palindrome detection. + """ + self.assertEqual( + is_palindrome("race car"), + False, + "String with spaces should not be considered a palindrome", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/solutions/tests/test_find_two_sum_indices.py b/solutions/tests/test_find_two_sum_indices.py new file mode 100644 index 000000000..ff1fccf14 --- /dev/null +++ b/solutions/tests/test_find_two_sum_indices.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Test module for find_two_sum_indices function. + +Created on 2025-01-05 + +Author: Jeffery Offei Darko +""" + +import unittest +from solutions.find_two_sum_indices import find_two_sum_indices + + +class TestFindTwoSumIndices(unittest.TestCase): + # Basic tests + def test_with_simple_target_sum(self): + """Test basic case with a simple target sum.""" + self.assertEqual(find_two_sum_indices([2, 7, 11, 15], 9), [0, 1]) + + def test_with_different_array_and_target(self): + """Test basic case with a different array and target.""" + self.assertEqual(find_two_sum_indices([3, 2, 4], 6), [1, 2]) + + def test_with_duplicates(self): + """Test basic case with duplicate values.""" + self.assertEqual(find_two_sum_indices([3, 3], 6), [0, 1]) + + # Edge cases with integers + def test_with_negative_numbers(self): + """Test case with negative numbers.""" + self.assertEqual(find_two_sum_indices([-3, 4, 3, 90], 0), [0, 2]) + + def test_with_zeroes(self): + """Test case where all numbers are zero.""" + self.assertEqual(find_two_sum_indices([0, 0, 0], 0), [0, 1]) + + def test_with_large_numbers(self): + """Test case with large integers.""" + self.assertEqual( + find_two_sum_indices([1000000, 2500000, 1500000], 3500000), [0, 1] + ) + + # Cases where no solution exists + def test_no_solution(self): + """Test case where no solution exists.""" + with self.assertRaises(AssertionError): + find_two_sum_indices([1, 2, 3], 7) + + def test_empty_list(self): + """Test case with an empty list.""" + with self.assertRaises(AssertionError): + find_two_sum_indices([], 5) + + def test_single_element(self): + """Test case with a single element.""" + with self.assertRaises(AssertionError): + find_two_sum_indices([1], 1) + + def test_repeated_elements(self): + """Test case with repeated elements where no solution exists.""" + with self.assertRaises(AssertionError): + find_two_sum_indices([1, 1, 1, 1], 3) + + # Tests with floats + def test_with_floats_basic(self): + """Test with floats that add up to the target.""" + self.assertEqual(find_two_sum_indices([2.5, 7.1, 11.3], 9.6), [0, 1]) + + def test_with_floats_and_integers(self): + """Test with a mix of floats and integers.""" + self.assertEqual(find_two_sum_indices([1, 2.5, 4.5], 7), [1, 2]) + + def test_with_floats_no_solution(self): + """Test with floats where no solution exists.""" + with self.assertRaises(AssertionError): + find_two_sum_indices([1.2, 3.4, 5.6], 10) + + def test_with_float_target(self): + """Test with a float target.""" + self.assertEqual(find_two_sum_indices([2, 3.5, 5], 8.5), [1, 2]) + + def test_with_float_and_negative(self): + """Test with floats and negative numbers.""" + self.assertEqual(find_two_sum_indices([-1.5, 3.0, 2.5], 1.0), [0, 2]) + + # Invalid input tests + def test_invalid_input_not_list(self): + """Test with an invalid input that is not a list.""" + with self.assertRaises(AssertionError): + find_two_sum_indices("123", 5) + + def test_invalid_input_with_non_number_elements(self): + """Test with invalid input containing non-number elements.""" + with self.assertRaises(AssertionError): + find_two_sum_indices([1, "two", 3], 5) + + def test_invalid_target_not_number(self): + """Test with an invalid target that is not a number.""" + with self.assertRaises(AssertionError): + find_two_sum_indices([1, 2, 3], "five") + + def test_invalid_input_none(self): + """Test with an invalid input of None.""" + with self.assertRaises(AssertionError): + find_two_sum_indices(None, 5) + + def test_invalid_target_none(self): + """Test with an invalid target of None.""" + with self.assertRaises(AssertionError): + find_two_sum_indices([1, 2, 3], None) + + # Edge cases for precision and NaN + def test_with_precision(self): + """Test edge case with float precision.""" + self.assertEqual(find_two_sum_indices([1.0000001, 2.9999999], 4.0), [0, 1]) + + def test_with_nan(self): + """Test with NaN input or target.""" + with self.assertRaises(AssertionError): + find_two_sum_indices([float("nan"), 2.5], float("nan")) + + def test_with_zero_target(self): + """Test when the target is zero.""" + self.assertEqual(find_two_sum_indices([-1, 1, 2], 0), [0, 1]) + + +if __name__ == "__main__": + unittest.main() diff --git a/solutions/tests/test_longest_substring.py b/solutions/tests/test_longest_substring.py new file mode 100644 index 000000000..230e4cbb6 --- /dev/null +++ b/solutions/tests/test_longest_substring.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on 2024-01-01 + +@author: Hiba Daffallah +""" + +import unittest + +from ..longest_substring import longest_substring + + +class TestLongestSubstring(unittest.TestCase): + """Test the longest_substring function""" + + def test_empty_string(self): + """Checks what will happen if the input is an empty + string""" + self.assertEqual(longest_substring(""), "") + + def test_one_char_repeated(self): + """Checks what will happen if the input is a repeated + character""" + self.assertEqual(longest_substring("bbbbbb"), "b") + + def test_no_repitition(self): + """Checks what will happen if the input didn't have + repeated characters""" + self.assertEqual(longest_substring("abcdef"), "abcdef") + + def test_repeated_substring(self): + """Checks what will happen if the input conatins a + repeated substring""" + self.assertEqual(longest_substring("pwwkew"), "wke") + + def test_multiple_substring(self): + """Checks what will happen if the input contains multiple + substrings""" + self.assertEqual(longest_substring("celelocceli"), "eloc") + + def test_input_numbers(self): + """Checks what will happen if the input contains numbers""" + self.assertEqual(longest_substring("991215888"), "2158") + + def test_input_conatins_numbers(self): + """Checks what will happen if the input contains numbers""" + self.assertEqual(longest_substring("9cort9kl9m"), "cort9kl") + + def test_input_non_string(self): + """Checks what will happen if the input is not a string""" + with self.assertRaises(AssertionError): + longest_substring(["abc"]) diff --git a/solutions/tests/test_number_to_word.py b/solutions/tests/test_number_to_word.py new file mode 100644 index 000000000..803e38bc1 --- /dev/null +++ b/solutions/tests/test_number_to_word.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Test module for conversion from number to word. + +This module contains unittest test and edge cases for the number_to_word. + +Created on 03 01 2025 +@author: Terry Aziaba +""" + +import unittest + +from ..number_to_word import number_to_word + + +class TestNumberToWord(unittest.TestCase): + """Unit tests for the number_to_word function.""" + + def test_zero(self): + """Test the input 0.""" + self.assertEqual(number_to_word(0), "zero") + + def test_one(self): + """Test the input 1.""" + self.assertEqual(number_to_word(1), "one") + + def test_two(self): + """Test the input 2.""" + self.assertEqual(number_to_word(2), "two") + + def test_three(self): + """Test the input 3.""" + self.assertEqual(number_to_word(3), "three") + + def test_four(self): + """Test the input 4.""" + self.assertEqual(number_to_word(4), "four") + + def test_five(self): + """Test the input 5.""" + self.assertEqual(number_to_word(5), "five") + + def test_six(self): + """Test the input 6.""" + self.assertEqual(number_to_word(6), "six") + + def test_seven(self): + """Test the input 7.""" + self.assertEqual(number_to_word(7), "seven") + + def test_eight(self): + """Test the input 8.""" + self.assertEqual(number_to_word(8), "eight") + + def test_nine(self): + """Test the input 9.""" + self.assertEqual(number_to_word(9), "nine") + + def test_invalid_input_ten(self): + """Test the input 10 (two-digit number).""" + with self.assertRaises(ValueError): + number_to_word(10) + + def test_invalid_input_negative(self): + """Test the input -1 (negative number).""" + with self.assertRaises(ValueError): + number_to_word(-1) + + def test_invalid_input_large_number(self): + """Test the input 100 (large number).""" + with self.assertRaises(ValueError): + number_to_word(100) + + def test_invalid_input_float(self): + """Test the input 3.5 (float).""" + with self.assertRaises(ValueError): + number_to_word(3.5) + + def test_invalid_input_string(self): + """Test the input "three" (string).""" + with self.assertRaises(ValueError): + number_to_word("three") + + def test_invalid_input_none(self): + """Test the input None.""" + with self.assertRaises(ValueError): + number_to_word(None) + + def test_edge_case_zero(self): + """Test the lower boundary input 0.""" + self.assertEqual(number_to_word(0), "zero") + + def test_edge_case_nine(self): + """Test the upper boundary input 9.""" + self.assertEqual(number_to_word(9), "nine") diff --git a/solutions/tests/test_parse_email_alias.py b/solutions/tests/test_parse_email_alias.py new file mode 100644 index 000000000..1bb3081d3 --- /dev/null +++ b/solutions/tests/test_parse_email_alias.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Tests for the parse_email_alias module. + +This module contains unit tests for the parse_email_alias function, +including normal cases, edge cases, and error conditions. + +Created on 4 01 2025 +@author: Mahdia Ahmadi +""" + +import unittest + +from solutions.parse_email_alias import parse_email_alias + + +class TestParseEmailAlias(unittest.TestCase): + """ + Test suite for the parse_email_alias function. + + This class contains unit tests covering various email parsing scenarios, + including valid emails, invalid formats, and edge cases. + """ + + def test_valid_email(self): + """ + Test a basic valid email address. + + Verifies that the function correctly parses a standard email address. + """ + self.assertEqual(parse_email_alias("user@example.com")["valid"], True) + + def test_email_with_tags(self): + """ + Test an email with alias tags. + + Checks if the function properly handles Gmail-style + aliases. + """ + self.assertEqual(parse_email_alias("user+tag@example.com")["tags"], ["tag"]) + + def test_multiple_tags(self): + """ + Test an email with multiple alias tags. + + Ensures the function can handle multiple + separated tags. + """ + self.assertEqual( + parse_email_alias("user+tag1+tag2@example.com")["tags"], ["tag1", "tag2"] + ) + + def test_subdomain(self): + """ + Test an email with a subdomain. + + Verifies correct handling of domain names with subdomains. + """ + self.assertEqual( + parse_email_alias("user@sub.example.com")["domain"], "sub.example.com" + ) + + def test_invalid_email_format(self): + """ + Test an email without @ symbol. + + Checks if the function correctly identifies missing @ as an error. + """ + self.assertIn("Missing @ symbol", parse_email_alias("invalid.email")["errors"]) + + def test_empty_local_part(self): + """ + Test an email with empty local part. + + Ensures the function recognizes an empty local part as an error. + """ + self.assertIn("Empty local part", parse_email_alias("@example.com")["errors"]) + + def test_invalid_characters_in_local_part(self): + """ + Test an email with invalid characters in the local part. + + Verifies that the function detects non-standard characters in the local part. + """ + self.assertIn( + "Invalid characters in local part", + parse_email_alias("user<>@example.com")["errors"], + ) + + def test_empty_domain(self): + """ + Test an email with empty domain. + + Checks if the function correctly identifies an empty domain as an error. + """ + self.assertIn("Empty domain", parse_email_alias("user@")["errors"]) + + def test_missing_top_level_domain(self): + """ + Test an email with missing top-level domain. + + Verifies that the function detects domains without TLDs as invalid. + """ + self.assertIn( + "Invalid domain (missing TLD)", parse_email_alias("user@sub.")["errors"] + ) + + def test_invalid_domain_format(self): + """ + Test an email with an invalid domain format. + + Checks if the function correctly identifies non-standard domain formats. + """ + self.assertIn( + "Invalid domain format", parse_email_alias("user@[192.168.1.1]")["errors"] + ) + + def test_empty_email(self): + """ + Test an empty email string. + + Ensures the function handles empty inputs correctly. + """ + self.assertIn("Email cannot be empty", parse_email_alias("")["errors"]) + + def test_non_string_input(self): + """ + Test non-string input to check defensive assertion. + + Verifies that the function raises an AssertionError for non-string inputs. + """ + with self.assertRaises(AssertionError): + parse_email_alias(123) + + def test_long_local_part(self): + """ + Test an email with a very long local part. + + Checks if the function can handle unusually long local parts. + """ + long_local = "a" * 1000 + self.assertTrue(parse_email_alias(f"{long_local}@example.com")["valid"]) + + def test_long_domain(self): + """ + Test an email with a very long domain. + + Ensures the function can handle unusually long domain names. + """ + long_domain = "example.com" * 100 + self.assertTrue(parse_email_alias(f"user@{long_domain}")["valid"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/solutions/tests/test_phone_letter_combinations.py b/solutions/tests/test_phone_letter_combinations.py new file mode 100644 index 000000000..ecd93f1df --- /dev/null +++ b/solutions/tests/test_phone_letter_combinations.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Test module for phone_letter_combinations function. + +Created on 2025-01-07 +Author: Hiba Daffallah +""" + +import unittest + +from solutions.phone_letter_combinations import phone_letter_combinations + + +class TestPhoneLetterCombinations(unittest.TestCase): + """Test phone_letter_combinations function""" + + def test_numbers_within_range(self): + """It should return all the possible letter combinations for 2,3""" + self.assertEqual( + phone_letter_combinations("23"), + ["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"], + ) + + def test_empty(self): + """It should return empty list""" + self.assertEqual(phone_letter_combinations(""), []) + + def test_single_number(self): + """It should return all the letters for 2""" + self.assertEqual(phone_letter_combinations("2"), ["a", "b", "c"]) + + def test_numbers_out_of_range(self): + """It should return a message""" + with self.assertRaises(AssertionError) as context: + phone_letter_combinations("1") + self.assertEqual( + str(context.exception), "Input must only contain digits from 2 to 9." + ) + + def test_contains_numbers_out_of_range(self): + """It should return a message""" + with self.assertRaises(AssertionError) as context: + phone_letter_combinations("12") + self.assertEqual( + str(context.exception), "Input must only contain digits from 2 to 9." + ) + + def test_contains_multiple_numbers(self): + "Tests if it can handle multiple numbers" + self.assertEqual( + phone_letter_combinations("234"), + [ + "adg", + "adh", + "adi", + "aeg", + "aeh", + "aei", + "afg", + "afh", + "afi", + "bdg", + "bdh", + "bdi", + "beg", + "beh", + "bei", + "bfg", + "bfh", + "bfi", + "cdg", + "cdh", + "cdi", + "ceg", + "ceh", + "cei", + "cfg", + "cfh", + "cfi", + ], + ) + + # Defensive tests + def test_none_input(self): + """It should raise AssertionError for non-integer input""" + with self.assertRaises(AssertionError): + phone_letter_combinations("b") + + +if __name__ == "__main__": + unittest.main() diff --git a/solutions/tests/test_reverse_string.py b/solutions/tests/test_reverse_string.py new file mode 100644 index 000000000..01b8ba2b3 --- /dev/null +++ b/solutions/tests/test_reverse_string.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +# -- coding: utf-8 -- +""" +Created on 2025-01-01 + +@author: Jeffery Offei Darko +""" + +import unittest + +from ..reverse_string import reverse_string + + +class TestReverseString(unittest.TestCase): + """Test the reverse_string function""" + + def test_empty_string(self): + """It should return an empty string for an empty string""" + self.assertEqual(reverse_string(""), "") + + def test_single_character(self): + """It should return the same character for a single character""" + self.assertEqual(reverse_string("a"), "a") + + def test_two_characters(self): + """It should reverse two characters""" + self.assertEqual(reverse_string("ab"), "ba") + + def test_palindrome(self): + """It should return the same string for a palindrome""" + self.assertEqual(reverse_string("racecar"), "racecar") + + def test_spaces_only(self): + """It should return the same string for spaces only""" + self.assertEqual(reverse_string(" "), " ") + + def test_string_with_spaces(self): + """It should reverse a string containing spaces""" + self.assertEqual(reverse_string("hello world"), "dlrow olleh") + + def test_numeric_string(self): + """It should reverse a numeric string""" + self.assertEqual(reverse_string("12345"), "54321") + + def test_mixed_case(self): + """It should reverse a string with mixed case""" + self.assertEqual(reverse_string("Python"), "nohtyP") + + def test_special_characters(self): + """It should reverse a string with special characters""" + self.assertEqual(reverse_string("!@#$%"), "%$#@!") + + def test_alphanumeric_string(self): + """It should reverse an alphanumeric string""" + self.assertEqual(reverse_string("abc123"), "321cba") + + def test_unicode_characters(self): + """It should reverse a string with Unicode characters""" + self.assertEqual( + reverse_string("\u3053\u3093\u306b\u3061\u306f"), + "\u306f\u3061\u306b\u3093\u3053", + ) + + def test_leading_and_trailing_spaces(self): + """It should reverse a string with leading and trailing spaces""" + self.assertEqual(reverse_string(" hello "), " olleh ") + + def test_string_with_numbers_and_spaces(self): + """It should reverse a string containing numbers and spaces""" + self.assertEqual(reverse_string("12 34 56"), "65 43 21") + + def test_long_string(self): + """It should reverse a very long string""" + long_string = "a" * 1000 + self.assertEqual(reverse_string(long_string), "a" * 1000) + + def test_string_with_whitespaces_in_front_of_string(self): + """It should reverse a string with whitespaces in front of the string""" + self.assertEqual(reverse_string(" hello"), "olleh ") + + def test_string_with_whitespaces_in_middle_of_string(self): + """It should reverse a string with whitespaces in the middle of the string""" + self.assertEqual(reverse_string("hello world"), "dlrow olleh") + + def test_string_with_whitespaces_at_the_end_of_string(self): + """It should reverse a string with whitespaces at the end of the string""" + self.assertEqual(reverse_string(" hello "), " olleh ") + + def test_string_with_special_characters(self): + """It should reverse a string with special characters""" + self.assertEqual(reverse_string("a!b@c#d$e^f&g*j(h)"), ")h(j*g&f^e$d#c@b!a") + + def test_string_with_unicode_characters(self): + """It should reverse a string with Unicode characters""" + self.assertEqual(reverse_string("こんにちは"), "はちにんこ") + + def test_assertion_error_on_non_string(self): + """It should raise an AssertionError for non-string inputs""" + with self.assertRaises(AssertionError): + reverse_string(12345) diff --git a/solutions/tests/test_weather_conditions.py b/solutions/tests/test_weather_conditions.py new file mode 100644 index 000000000..870f60066 --- /dev/null +++ b/solutions/tests/test_weather_conditions.py @@ -0,0 +1,61 @@ +"""Test module for input temperature +This unittest contains test +and edge cases for the weather condition + +Created on Sat Jan 10 2025 + +@author: Yool Malaak +""" + +import unittest + +from ..weather_conditions import get_weather_condition + + +class TestGetWeatherCondition(unittest.TestCase): + """ + Unit tests for the `get_weather_condition` function, which determines + weather conditions based on temperature in Celsius. + """ + + def test_extremely_hot(self): + """ + Test for temperatures in the 'Extremely Hot' range (45-50°C). + """ + self.assertEqual(get_weather_condition(45), "Extremely Hot") + self.assertEqual(get_weather_condition(50), "Extremely Hot") + + def test_hot(self): + """ + Test for temperatures in the 'Hot' range (31-44°C). + """ + self.assertEqual(get_weather_condition(40), "Hot") + self.assertEqual(get_weather_condition(31), "Hot") + + def test_sunny(self): + """ + Test for temperatures in the 'Sunny' range (20-30°C). + """ + self.assertEqual(get_weather_condition(25), "Sunny") + self.assertEqual(get_weather_condition(20), "Sunny") + + def test_cloudy(self): + """ + Test for temperatures in the 'Cloudy' range (10-19°C). + """ + self.assertEqual(get_weather_condition(15), "Cloudy") + self.assertEqual(get_weather_condition(10), "Cloudy") + + def test_rainy(self): + """ + Test for temperatures in the 'Rainy' range (0-9°C). + """ + self.assertEqual(get_weather_condition(5), "Rainy") + self.assertEqual(get_weather_condition(0), "Rainy") + + def test_freezing(self): + """ + Test for temperatures in the 'Freezing' range (below 0°C). + """ + self.assertEqual(get_weather_condition(-5), "Freezing") + self.assertEqual(get_weather_condition(-10), "Freezing") diff --git a/solutions/weather_conditions.py b/solutions/weather_conditions.py new file mode 100644 index 000000000..f7f594d76 --- /dev/null +++ b/solutions/weather_conditions.py @@ -0,0 +1,53 @@ +""" +The function takes in temperature in degree celsius and +return weather condition based on temperature + +Created on Fri Jan 10 2025 + +@author: Yool Malaak +""" + + +def get_weather_condition(temperature): + """ + A module for getting the weather condition + Module: takes temperature as input and returns weather condition + + parameters: + temperature (float): The temperature in degrees Celsius. + + Returns: + str: The weather condition: + - 'Extremely Hot': 45 <= Temperature <= 50 + - 'Hot': 31 <= Temperature <= 44 + - 'Sunny': 20 <= Temperature <= 30 + - 'Cloudy': 10 <= Temperature <= 19 + - 'Rainy': 0 <= Temperature <= 9 + - 'Freezing': Temperature < 0 + + Examples: + >>> get_weather_condition(47) + 'Extremely Hot' + >>> get_weather_condition(40) + 'Hot' + >>> get_weather_condition(25) + 'Sunny' + >>> get_weather_condition(15) + 'Cloudy' + >>> get_weather_condition(5) + 'Rainy' + >>> get_weather_condition(-5) + 'Freezing' + """ + if 45 <= temperature <= 50: + return "Extremely Hot" + elif 31 <= temperature <= 44: + return "Hot" + elif 20 <= temperature <= 30: + return "Sunny" + elif 10 <= temperature <= 19: + return "Cloudy" + elif 0 <= temperature <= 9: + return "Rainy" + else: + return "Freezing"