From 990bc614a9adea0cc9319419d8462cf7354e6b08 Mon Sep 17 00:00:00 2001 From: "Joab Leite S. Neto" Date: Wed, 18 Sep 2024 21:21:02 -0300 Subject: [PATCH] v2.02.2 (#213) * improves: improve split_sequence and add map_values utils * feat: Create a function to calculate the distance to Manhattan (#212) * improves: improved datetime util * Create test to calculate distance from Manhattan (#214) * Fix Manhattan distance calculation * Create test for manhattan distance to function * chore: add singleton ensure only one instance in multithreaded environments. --------- Co-authored-by: Ana Ferreira --- cereja/__init__.py | 2 +- cereja/config/conf.py | 2 +- cereja/date/_datetime.py | 236 +++++++++++++++++++++++++++++++------- cereja/geolinear/point.py | 12 ++ cereja/utils/_utils.py | 117 +++++++++++++++---- tests/testsdatetime.py | 185 ++++++++++++++++++++++++++++++ tests/testsgeolinear.py | 12 ++ tests/testsutils.py | 6 +- 8 files changed, 506 insertions(+), 66 deletions(-) create mode 100644 tests/testsdatetime.py diff --git a/cereja/__init__.py b/cereja/__init__.py index fc86e33..19a859e 100644 --- a/cereja/__init__.py +++ b/cereja/__init__.py @@ -48,7 +48,7 @@ from ._requests import request from . import scraping -VERSION = "2.0.1.final.0" +VERSION = "2.0.2.final.0" __version__ = get_version_pep440_compliant(VERSION) diff --git a/cereja/config/conf.py b/cereja/config/conf.py index 224f4ca..89736ff 100644 --- a/cereja/config/conf.py +++ b/cereja/config/conf.py @@ -109,4 +109,4 @@ def get(self, item=None): ] console_logger = logging.StreamHandler(sys.stdout) -logging.basicConfig(handlers=(console_logger,), level=logging.WARNING) +logging.basicConfig(handlers=(console_logger,), level=logging.INFO) diff --git a/cereja/date/_datetime.py b/cereja/date/_datetime.py index 84ec8b3..5a1c82e 100644 --- a/cereja/date/_datetime.py +++ b/cereja/date/_datetime.py @@ -1,46 +1,78 @@ -""" -Copyright (c) 2019 The Cereja Project - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" import re from datetime import datetime -from typing import Union +from typing import Union, List -__all__ = ["DateTime"] +__all__ = ['DateTime', 'AmbiguousDateFormatError'] + + +class AmbiguousDateFormatError(ValueError): + def __init__(self, date_string, possible_formats): + self.date_string = date_string + self.possible_formats = possible_formats + message = ( + f"Ambiguous date format for '{date_string}'. " + f"Possible formats: {possible_formats}" + ) + super().__init__(message) + + +class UnrecognizedDateFormatError(ValueError): + pass class DateTime(datetime): - # Defining common date formats + """ + A subclass of the built-in datetime class with additional methods for parsing and comparing dates. + + This class extends the functionality of the standard datetime class by adding methods to: + - Parse date strings in various formats into DateTime objects. + - Compare dates for equality. + - Add or subtract time intervals from dates. + - Calculate the number of days between dates. + - Validate and disambiguate date formats. + + Attributes: + __DATE_FORMATS (dict): A dictionary mapping regex patterns to lists of date formats. + """ __DATE_FORMATS = { - r'\d{4}-\d{2}-\d{2}': '%Y-%m-%d', # Format ISO 8601 (YYYY-MM-DD) - r'\d{4}/\d{2}/\d{2}': '%Y/%m/%d', # Format (YYYY/MM/DD) - r'\d{2}/\d{2}/\d{4}': '%d/%m/%Y', # Format (DD/MM/YYYY) - r'\d{2}-\d{2}-\d{4}': '%d-%m-%Y', # Format (DD-MM-YYYY) - r'\d{1,2}/\d{1,2}/\d{2,4}': '%m/%d/%Y', # Format (MM/DD/YYYY) - r'\d{1,2}-\d{1,2}-\d{2,4}': '%m-%d-%Y', # Format (MM-DD-YYYY) - r'\d{8}': '%Y%m%d', # Format (YYYYMMDD) - r'\d{2}\d{2}\d{4}': '%d%m%Y', # Format (DDMMYYYY) - r'\d{4}\d{2}\d{2}': '%Y%m%d', # Format (YYYYMMDD) without separators + r'\d{1,2}/\d{1,2}/\d{2,4}': ['%d/%m/%Y', '%m/%d/%Y', '%d/%m/%y', '%m/%d/%y'], + r'\d{1,2}-\d{1,2}-\d{2,4}': ['%d-%m-%Y', '%m-%d-%Y', '%d-%m-%y', '%m-%d-%y'], + r'\d{2,4}/\d{1,2}/\d{1,2}': ['%Y/%m/%d', '%y/%m/%d', '%Y/%d/%m', '%y/%d/%m'], + r'\d{2,4}-\d{1,2}-\d{1,2}': ['%Y-%m-%d', '%y-%m-%d', '%Y-%d-%m', '%y-%d-%m'], + r'\d{8}': ['%Y%m%d', '%d%m%Y', '%Y%d%m'], + r'\d{6}': ['%y%m%d', '%d%m%y', '%y%d%m'], + r'\d{2}-\d{2}-\d{2}': ['%y-%m-%d', '%d-%m-%y'], + r'\d{2}\.\d{2}\.\d{4}': ['%d.%m.%Y'], # DD.MM.YYYY + r'\d{4}\.\d{2}\.\d{2}': ['%Y.%m.%d'], # YYYY.MM.DD + r'\d{2}\s[A-Za-z]{3}\s\d{4}': ['%d %b %Y'], # DD Mon YYYY + r'[A-Za-z]{3}\s\d{2},\s\d{4}': ['%b %d, %Y'], # Mon DD, YYYY + r'\d{2}-[A-Za-z]{3}-\d{4}': ['%d-%b-%Y'], # DD-Mon-YYYY + r'\d{2}\s[A-Za-z]+\s\d{4}': ['%d %B %Y'], # DD Month YYYY + r'[A-Za-z]+\s\d{2},\s\d{4}': ['%B %d, %Y'], # Month DD, YYYY + r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}': ['%Y-%m-%dT%H:%M:%S'], # ISO 8601 + r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}': ['%Y-%m-%d %H:%M:%S'], # Com tempo + r'\d{2}/\d{2}/\d{4} \d{2}:\d{2}:\d{2}': ['%d/%m/%Y %H:%M:%S'], # DD/MM/YYYY HH:MM:SS + r'\d{2}-\d{2}-\d{4} \d{2}:\d{2}:\d{2}': ['%d-%m-%Y %H:%M:%S'], # DD-MM-YYYY HH:MM:SS + r'\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}': ['%Y/%m/%d %H:%M:%S'], # YYYY/MM/DD HH:MM:SS + r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}': ['%Y-%m-%d %H:%M'], # YYYY-MM-DD HH:MM + r'\d{2}/\d{2}/\d{4} \d{2}:\d{2}': ['%d/%m/%Y %H:%M'], # DD/MM/YYYY HH:MM + r'\d{2}-\d{2}-\d{4} \d{2}:\d{2}': ['%d-%m-%Y %H:%M'], # DD-MM-YYYY HH:MM + r'\d{4}/\d{2}/\d{2} \d{2}:\d{2}': ['%Y/%m/%d %H:%M'], # YYYY/MM/DD HH:MM + r'\d{2}[A-Za-z]{3}\d{4}': ['%d%b%Y'], # DDMonYYYY + r'[A-Za-z]{3}\s\d{2}': ['%b %d'], # Mon DD (ano atual) + r'\d{2}\s[A-Za-z]{3}': ['%d %b'], # DD Mon (ano atual) + r'[A-Za-z]{3}\s\d{4}': ['%b %Y'], # Mon YYYY (dia = 1) + # Formats with timezone (manual parsing) + r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z': ['%Y-%m-%dT%H:%M:%SZ'], # ISO 8601 UTC + r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+': ['%Y-%m-%d %H:%M:%S.%f'], # With microseconds # Other formats can be added here } + def __new__(cls, *args, **kwargs): + if args and isinstance(args[0], str): + return cls.parse_from_string(args[0]) + return super().__new__(cls, *args, **kwargs) + @classmethod def _validate_timestamp(cls, value) -> Union[int, float]: assert isinstance(value, (int, float)), f"{value} is not valid." @@ -91,17 +123,139 @@ def compare(self, other): else 0 ) + @classmethod + def _parse_with_formats(cls, date_string: str, date_formats: List[str]): + """ + Parse the given date string using a list of possible date formats. + + This method attempts to parse the date string with each format in the provided list. + If the year or day is not specified in the format, it defaults to the current year or the first day of the month, respectively. + + Args: + date_string (str): The date string to be parsed. + date_formats (List[str]): A list of date formats to try for parsing the date string. + + Returns: + List[Tuple[datetime, str]]: A list of tuples where each tuple contains a parsed datetime object and the format used. + """ + parsed_dates = [] + for date_format in date_formats: + try: + parsed_date = cls.strptime(date_string, date_format) + if '%Y' not in date_format and '%y' not in date_format: + parsed_date = parsed_date.replace(year=datetime.now().year) + if '%d' not in date_format: + parsed_date = parsed_date.replace(day=1) + parsed_dates.append((parsed_date, date_format)) + except ValueError: + continue + return parsed_dates + + @classmethod + def _get_possible_dates(cls, date_string: str): + possible_dates = [] + for regex, date_formats in cls.__DATE_FORMATS.items(): + if re.fullmatch(regex, date_string): + dates = cls._parse_with_formats(date_string, date_formats) + possible_dates.extend(dates) + if not possible_dates: + raise UnrecognizedDateFormatError(f"Unrecognized date format: {date_string}") + return possible_dates + @classmethod def parse_from_string(cls, date_string: str): """ - Parses a date string in various formats and returns a DateTime object. + Parse a date string into a DateTime object. + + This method attempts to parse the given date string into a DateTime object. + It uses a set of predefined date formats to identify the correct format of the date string. + If multiple possible dates are found, it tries to disambiguate them to find a valid date. + If no valid date can be determined, it raises an AmbiguousDateFormatError. + + Args: + date_string (str): The date string to be parsed. + + Returns: + DateTime: The parsed DateTime object. + + Raises: + ValueError: If the date string format is unrecognized. + AmbiguousDateFormatError: If multiple possible dates are found and cannot be disambiguated. """ + possible_dates = cls._get_possible_dates(date_string) + + if len(possible_dates) == 1: + return possible_dates[0][0] + else: + selected_date = cls._disambiguate_date(possible_dates) + if selected_date: + return selected_date + else: + # If unable to disambiguate, throw an exception + raise AmbiguousDateFormatError(date_string, [fmt for _, fmt in possible_dates]) + + @classmethod + def are_dates_equal(cls, date_str1: Union[str, datetime], date_str2: Union[str, datetime]) -> bool: + """ + Check if two dates are equal. + + This method compares two dates, which can be either strings or datetime objects. + It parses the date strings into datetime objects if necessary and then compares them. + + Args: + date_str1 (Union[str, datetime]): The first date to compare, as a string or datetime object. + date_str2 (Union[str, datetime]): The second date to compare, as a string or datetime object. - for regex, date_format in cls.__DATE_FORMATS.items(): - if re.match(regex, date_string): - try: - return cls.strptime(date_string, date_format) - except ValueError: - pass + Returns: + bool: True if the dates are equal, False otherwise. + """ + if isinstance(date_str1, datetime) and isinstance(date_str2, datetime): + return date_str1 == date_str2 + if isinstance(date_str1, datetime): + possible_dates1 = [(date_str1, None)] + else: + possible_dates1 = cls._get_possible_dates(date_str1) + if isinstance(date_str2, datetime): + possible_dates2 = [(date_str2, None)] + else: + possible_dates2 = cls._get_possible_dates(date_str2) + for date1, _ in possible_dates1: + for date2, _ in possible_dates2: + if date1 == date2: + return True + return False + + @classmethod + def _disambiguate_date(cls, possible_dates): + """ + Disambiguate a list of possible dates to find a valid date. + + This method filters the possible dates to find a date within a valid range (1900-2100). + If there is only one valid date, it returns that date. + If there are multiple valid dates, it further filters them to ensure the day and month are within valid ranges. + If only one valid date remains after this filtering, it returns that date. + If no valid date can be determined, it returns None. + + Args: + possible_dates (List[Tuple[datetime, str]]): A list of tuples where each tuple contains a datetime object and the format used. + + Returns: + datetime or None: The disambiguated valid date or None if no valid date can be determined. + """ + dates_in_valid_range = [date for date, _ in possible_dates if 1900 <= date.year <= 2100] + if len(dates_in_valid_range) == 1: + return dates_in_valid_range[0] + elif len(dates_in_valid_range) > 1: + valid_dates = [ + date for date in dates_in_valid_range + if 1 <= date.day <= 31 and 1 <= date.month <= 12 + ] + if len(valid_dates) == 1: + return valid_dates[0] + else: + return None - raise ValueError(f"Unrecognized date format: {date_string}") + def __eq__(self, other): + if isinstance(other, str): + return self.are_dates_equal(self, other) + return super().__eq__(other) diff --git a/cereja/geolinear/point.py b/cereja/geolinear/point.py index d4a15bd..715d4af 100644 --- a/cereja/geolinear/point.py +++ b/cereja/geolinear/point.py @@ -230,6 +230,18 @@ def rotate(self, angle: float, center=(0, 0, 0), axis: str = 'z') -> 'Point': else: raise ValueError(f"Invalid axis: {axis}. Axis should be 'x', 'y', or 'z'.") + def manhattan_distance_to(self, other: 'Point') -> float: + """ + Computes the Manhattan distance between this point and another point. + + Args: + other (Point): The other point to which the Manhattan distance is calculated. + + Returns: + float: The Manhattan distance between the two points. + """ + return sum(abs(a - b) for a, b in zip(self, other)) + def __getitem__(self, item: Union[int, str]) -> Union[int, float]: """Allows indexing into the point to retrieve its coordinates.""" if isinstance(item, str): diff --git a/cereja/utils/_utils.py b/cereja/utils/_utils.py index 44b734a..c868bf3 100644 --- a/cereja/utils/_utils.py +++ b/cereja/utils/_utils.py @@ -22,6 +22,7 @@ import math import re import string +import threading import time from collections import OrderedDict, defaultdict from importlib import import_module @@ -29,7 +30,7 @@ import sys import types import random -from typing import Any, Union, List, Tuple, Sequence, Iterable, Dict, MappingView, Optional, Callable, AnyStr +from typing import Any, Union, List, Tuple, Sequence, Iterable, Dict, MappingView, Optional, Callable, AnyStr, Iterator import logging import itertools from copy import copy @@ -94,7 +95,11 @@ "value_from_memory", "str_gen", "set_interval", - "SourceCodeAnalyzer" + "SourceCodeAnalyzer", + "map_values", + 'decode_coordinates', + 'encode_coordinates', + 'SingletonMeta' ] logger = logging.getLogger(__name__) @@ -140,31 +145,51 @@ def split_sequence(seq: List[Any], is_break_fn: Callable) -> List[List[Any]]: @return: list of subsequences """ if not isinstance(seq, list) or not seq: - raise ValueError("A sequência deve ser uma lista não vazia.") + raise ValueError("The sequence must be a non-empty list.") if not callable(is_break_fn): - raise TypeError("is_break_fn deve ser uma função.") + raise TypeError("is_break_fn must be a function.") - # Inicializa com a primeira subsequência sub_seqs = [] start_idx = 0 - break_fn_arg_count = SourceCodeAnalyzer(is_break_fn).argument_count - for indx, val in enumerate(seq): + if len(seq) == 1: + return [seq] + for idx, is_break in enumerate(map_values(seq, is_break_fn)): + if is_break: + sub_seqs.append(seq[start_idx:idx + 1]) + start_idx = idx + 1 + if start_idx < len(seq): + sub_seqs.append(seq[start_idx:]) + return sub_seqs + + +def map_values(obj: Union[dict, list, tuple, Iterator], fn: Callable) -> Union[dict, list, tuple, Iterator]: + fn_arg_count = SourceCodeAnalyzer(fn).argument_count + is_dict = isinstance(obj, dict) + if isinstance(obj, dict): + obj = obj.items() + _iter = iter(obj) + last = next(_iter, '__stop__') + if last == '__stop__': + return map(fn, obj) + idx = 0 + while last != '__stop__': _args = None - if indx + 1 == len(seq): - sub_seqs.append(seq[start_idx:]) + _next = next(_iter, '__stop__') + + if fn_arg_count == 1: + _args = (last,) + elif fn_arg_count == 2: + _args = (last, None if _next == '__stop__' else _next) + elif fn_arg_count == 3: + _args = (last, None if _next == '__stop__' else _next, idx) + if _next == '__stop__' and fn_arg_count >= 2: + if idx == 0: + yield fn(*_args) break - if break_fn_arg_count == 1: - _args = (val,) - elif break_fn_arg_count == 2: - _args = (val, seq[indx + 1]) - elif break_fn_arg_count == 3: - _args = (val, seq[indx + 1], indx) - - if is_break_fn(*_args) if _args else is_break_fn(): - sub_seqs.append(seq[start_idx:indx+1]) - start_idx = indx+1 - return sub_seqs + yield fn(*_args) if _args else last + last = _next + idx += 1 def chunk( @@ -1518,7 +1543,7 @@ def get_zero_mask(number: int, max_len: int = 3) -> str: return f"%0.{max_len}d" % number -def get_batch_strides(data, kernel_size, strides=1, fill_=True, take_index=False): +def get_batch_strides(data, kernel_size, strides=1, fill_=False, take_index=False): """ Returns batches of fixed window size (kernel_size) with a given stride @param data: iterable @@ -1565,3 +1590,53 @@ def set_interval(func: Callable, sec: float): """ from .decorators import on_elapsed on_elapsed(sec, loop=True, use_threading=True)(func)() + + +def encode_coordinates(x: int, y: int): + """ + Encode the coordinates (x, y) into a single lParam value. + + The encoding is done by shifting the y-coordinate 16 bits to the left and + then performing a bitwise OR with the x-coordinate. + + Args: + x (int): The x-coordinate. + y (int): The y-coordinate. + + Returns: + int: The encoded lParam value. + """ + return (y << 16) | x + + +def decode_coordinates(lparam: int): + """ + Decode the lParam value back into the original coordinates (x, y). + + The decoding is done by extracting the lower 16 bits for the x-coordinate + and the upper 16 bits for the y-coordinate. + + Args: + lparam (int): The encoded lParam value. + + Returns: + tuple: A tuple containing the x and y coordinates. + """ + x = lparam & 0xFFFF + y = (lparam >> 16) & 0xFFFF + return x, y + + +class SingletonMeta(type): + """A thread-safe implementation of Singleton.""" + _instances = {} + _lock: threading.Lock = threading.Lock() # Class-level lock + + def __call__(cls, *args, **kwargs): + # First, check if an instance exists + if cls not in cls._instances: + with cls._lock: + # Double-check locking + if cls not in cls._instances: + cls._instances[cls] = super(SingletonMeta, cls).__call__(*args, **kwargs) + return cls._instances[cls] diff --git a/tests/testsdatetime.py b/tests/testsdatetime.py new file mode 100644 index 0000000..eae34f8 --- /dev/null +++ b/tests/testsdatetime.py @@ -0,0 +1,185 @@ +import unittest +from datetime import datetime + +from cereja import DateTime, AmbiguousDateFormatError # Replace 'date_time_module' with the actual module name + + +class TestDateTime(unittest.TestCase): + def test_validate_timestamp(self): + self.assertEqual(DateTime._validate_timestamp(1609459200), 1609459200) + self.assertEqual(DateTime._validate_timestamp(1609459200.0), 1609459200.0) + with self.assertRaises(AssertionError): + DateTime._validate_timestamp("1609459200") + + def test_validate_date(self): + now = datetime.now() + self.assertEqual(DateTime._validate_date(now), now) + with self.assertRaises(AssertionError): + DateTime._validate_date("2021-01-01") + + def test_days_from_timestamp(self): + self.assertEqual(DateTime.days_from_timestamp(86400), 1) + self.assertEqual(DateTime.days_from_timestamp(172800), 2) + + def test_into_timestamp(self): + self.assertEqual(DateTime.into_timestamp(days=1), 86400) + self.assertEqual(DateTime.into_timestamp(min_=1), 3600 * 60) + self.assertEqual(DateTime.into_timestamp(sec=60), 60) + self.assertEqual(DateTime.into_timestamp(days=1, min_=1, sec=60), 86400 + 3600 * 60 + 60) + + def test_add(self): + dt = DateTime(2021, 1, 1) + new_dt = dt.add(days=1) + self.assertEqual(new_dt, datetime(2021, 1, 2)) + + def test_sub(self): + dt = DateTime(2021, 1, 2) + new_dt = dt.sub(days=1) + self.assertEqual(new_dt, datetime(2021, 1, 1)) + + def test_days_between(self): + dt1 = DateTime(2021, 1, 1) + dt2 = DateTime(2021, 1, 3) + self.assertEqual(dt1.days_between(dt2), 2) + + def test_compare(self): + dt1 = DateTime(2021, 1, 1) + dt2 = DateTime(2021, 1, 2) + self.assertEqual(dt1.compare(dt2), -1) + self.assertEqual(dt2.compare(dt1), 1) + self.assertEqual(dt1.compare(dt1), 0) + + def test_parse_from_string(self): + # Test with unambiguous formats + self.assertEqual( + DateTime.parse_from_string("2021/01/01 12:00:00"), + datetime(2021, 1, 1, 12, 0, 0) + ) + + # Test with ambiguous formats + with self.assertRaises(AmbiguousDateFormatError): + DateTime.parse_from_string("01/02/03") + DateTime.parse_from_string("2021-01-01") + DateTime.parse_from_string("01/01/2021") + DateTime.parse_from_string("20210101") + + # Test with formats that require disambiguation + self.assertEqual( + DateTime.parse_from_string("20210113"), + datetime(2021, 1, 13) + ) + + def test_disambiguate_date(self): + # Internal method, but we can test via parse_from_string + self.assertEqual( + DateTime.parse_from_string("12-31-2021"), + datetime(2021, 12, 31) + ) + + def test_ambiguous_date_format_error(self): + try: + DateTime.parse_from_string("01/02/03") + except AmbiguousDateFormatError as e: + self.assertIn("Possible formats", str(e)) + self.assertEqual(e.date_string, "01/02/03") + self.assertIsInstance(e.possible_formats, list) + + def test_unrecognized_format(self): + with self.assertRaises(ValueError): + DateTime.parse_from_string("Not a date") + + def test_parse_with_formats(self): + # Testing the internal method indirectly via parse_from_string + self.assertEqual( + DateTime.parse_from_string("31 Dec 2021"), + datetime(2021, 12, 31) + ) + + def test_timezone_formats(self): + # Test parsing ISO 8601 UTC format + self.assertEqual( + DateTime.parse_from_string("2021-12-31T23:59:59Z"), + datetime(2021, 12, 31, 23, 59, 59) + ) + + # Test parsing with microseconds + self.assertEqual( + DateTime.parse_from_string("2021-12-31 23:59:59.123456"), + datetime(2021, 12, 31, 23, 59, 59, 123456) + ) + + def test_partial_dates(self): + # Test parsing dates without year (assumes current year) + date = DateTime.parse_from_string("Dec 31") + self.assertEqual(date.month, 12) + self.assertEqual(date.day, 31) + self.assertEqual(date.year, datetime.now().year) + + # Test parsing dates without day (assumes day=1) + date = DateTime.parse_from_string("Dec 2021") + self.assertEqual(date.month, 12) + self.assertEqual(date.day, 1) + self.assertEqual(date.year, 2021) + + def test_formats_with_textual_month(self): + self.assertEqual( + DateTime.parse_from_string("31 December 2021"), + datetime(2021, 12, 31) + ) + self.assertEqual( + DateTime.parse_from_string("December 31, 2021"), + datetime(2021, 12, 31) + ) + + def test_formats_with_different_separators(self): + self.assertEqual( + DateTime.parse_from_string("31-12-2021"), + datetime(2021, 12, 31) + ) + self.assertEqual( + DateTime.parse_from_string("31.12.2021"), + datetime(2021, 12, 31) + ) + + def test_instance_with_string(self): + dt = DateTime("2021-01-31") + self.assertEqual(dt, datetime(2021, 1, 31)) + + def test_dates_are_equal_when_both_are_datetime_objects(self): + date1 = DateTime(2023, 10, 5) + date2 = DateTime(2023, 10, 5) + self.assertTrue(DateTime.are_dates_equal(date1, date2)) + + def test_dates_are_not_equal_when_both_are_datetime_objects(self): + date1 = DateTime(2023, 10, 5) + date2 = DateTime(2023, 10, 6) + self.assertFalse(DateTime.are_dates_equal(date1, date2)) + + def test_dates_are_equal_when_one_is_string(self): + date1 = DateTime(2023, 10, 5) + date_str2 = "2023-10-05" + self.assertTrue(DateTime.are_dates_equal(date1, date_str2)) + + def test_dates_are_not_equal_when_one_is_string(self): + date1 = DateTime(2023, 10, 5) + date_str2 = "2023-10-06" + self.assertFalse(DateTime.are_dates_equal(date1, date_str2)) + + def test_dates_are_equal_when_both_are_strings(self): + date_str1 = "2023-10-05" + date_str2 = "2023-10-05" + self.assertTrue(DateTime.are_dates_equal(date_str1, date_str2)) + + def test_dates_are_not_equal_when_both_are_strings(self): + date_str1 = "2023-10-05" + date_str2 = "2023-10-06" + self.assertFalse(DateTime.are_dates_equal(date_str1, date_str2)) + + def test_dates_are_equal_with_different_formats(self): + date_str1 = "05/10/2023" + date_str2 = "2023-10-05" + self.assertTrue(DateTime.are_dates_equal(date_str1, date_str2)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/testsgeolinear.py b/tests/testsgeolinear.py index c24432c..e45d627 100644 --- a/tests/testsgeolinear.py +++ b/tests/testsgeolinear.py @@ -99,6 +99,18 @@ def test_invalid_position(self): with self.assertRaises(ValueError): self.point.bounding_box(4, 6, position="invalid_position") + def test_manhattan_distance(self): + # Testa a distância de Manhattan entre pontos iguais + result = self.p1.manhattan_distance_to(self.p2) + self.assertEqual(result, 0) + + # Testa a distância de Manhattan entre pontos diferentes + result = self.p1.manhattan_distance_to(self.p3) + self.assertEqual(result, abs(1 - 4) + abs(2 - 5) + abs(3 - 6)) + + # Testa a distância de Manhattan para uma tupla de coordenadas + result = Point((1, 4)).manhattan_distance_to((2, 5)) + self.assertEqual(result, abs(1 - 2) + abs(4 - 5)) if __name__ == '__main__': unittest.main() diff --git a/tests/testsutils.py b/tests/testsutils.py index 448e299..36f8b28 100644 --- a/tests/testsutils.py +++ b/tests/testsutils.py @@ -480,7 +480,7 @@ def test_get_batch_strides(self): ] for test_value, kernel_size, strides, expected in tests: self.assertEqual( - list(utils.get_batch_strides(test_value, kernel_size, strides)), + list(utils.get_batch_strides(test_value, kernel_size, strides, fill_=True)), expected, ) @@ -623,7 +623,9 @@ def __init__(self, x, y): self.y = y def __eq__(self, other): - return self.x == other.x and self.y == other.y + if isinstance(other, Point): + return self.x == other.x and self.y == other.y + return False seq = [Point(1, 2), Point(1, 3), Point(4, 4), Point(4, 8)] is_same_x = lambda p1, p2: p1.x == p2.x