

[tool.coverage.run]
source_pkgs = ["radixhopper", "tests"]
branch = true
parallel = true
omit = [
  "src/radixhopper/__about__.py",
]

[tool.coverage.paths]
radixhopper = ["src/radixhopper", "*/radixhopper/src/radixhopper"]
tests = ["tests", "*/radixhopper/tests"]

[tool.coverage.report]
exclude_lines = [
  "no cov",
  "if __name__ == .__main__.:",
  "if TYPE_CHECKING:",
]

[project.optional-dependencies]
dev = [
    "pytest>=6.0",
    "pytest-cov>=2.0",
    "black>=21.0",
    "isort>=5.0",
    "flake8>=3.9",
]

[tool.black]
line-length = 88
target-version = ['py38']

[tool.isort]
profile = "black"
line_length = 88

[tool.flake8]
max-line-length = 88
extend-ignore = "E203"

[tool.pytest.ini_options]
addopts = "--cov=radixhopper"
testpaths = ["tests"]




class TheRadixNumber:
    """
    Initialize a RadixNumber with a value and base.

    Args:
        value: The number value as string, int, float or Fraction
        base: The base of the number (2-36)

    Raises:
        BaseRangeError: If base is outside the allowed range
        DigitError: If any digit in the value is invalid for the given base
        ParseError: If the value string format is invalid
    """

    @classmethod
    def _validate_digits(cls, value: str, base: int) -> None:
        valid_digits = cls.DIGITS[:base]
        if not all(d in valid_digits for d in value if d not in ".[]-"):
            raise DigitError(f"Invalid digit(s) for base {base}")

    def _parse_to_fraction(self, value: Union[str, int, float, Fraction]) -> Fraction:
        """Convert the input value to a Fraction for internal representation"""
        if isinstance(value, Fraction):
            return value

        if isinstance(value, (int, float)):
            return Fraction(value)

        # String processing
        # value = value.upper()
        self._validate_digits(value, self.base)

        int_part = ""
        frac_part = ""
        rep_part = ""

        # Parse the string into components
        if "." in value:
            int_part, frac_str = value.split(".")
            if "[" in frac_str and "]" in frac_str:
                match = re.search(r"\[(.*?)\]", frac_str)
                if not match:
                    raise ParseError("Invalid repeating decimal format")
                rep_part = match.group(1)
                frac_part = frac_str[: frac_str.index("[")]
            else:
                frac_part = frac_str
        else:
            int_part = value

        # Handle empty parts
        int_part = int_part or "0"

        # Convert to Fraction
        fraction = Fraction()

        # Integer part
        for index, digit in enumerate(int_part):
            if digit in "+-":  # Handle sign
                continue
            digit_value = self.DIGITS.index(digit)
            fraction += digit_value * (self.base ** (len(int_part) - index - 1))

        # Apply sign
        if int_part.startswith("-"):
            fraction = -fraction

        # Fractional part
        if frac_part:
            for index, digit in enumerate(frac_part):
                digit_value = self.DIGITS.index(digit)
                fraction += Fraction(digit_value, self.base ** (index + 1))

        # Repeating part
        if rep_part:
            repeating_value = Fraction(0)
            for index, digit in enumerate(rep_part):
                digit_value = self.DIGITS.index(digit)
                repeating_value += Fraction(digit_value, self.base ** (index + 1))

            # Formula for repeating decimal: x = n / (10^k * (10^p - 1))
            # where n is the repeating part, k is the position where repetition starts,
            # and p is the length of the repeating part
            denominator = (self.base ** len(rep_part)) - 1
            rep_fraction = Fraction(
                repeating_value.numerator, repeating_value.denominator * denominator
            )

            # Adjust for the position after the decimal point
            rep_fraction = Fraction(
                rep_fraction.numerator,
                rep_fraction.denominator * (self.base ** len(frac_part)),
            )

            fraction += rep_fraction

        return fraction

    def _int_and_frac_parts(self) -> Tuple[int, Fraction]:
        """Split the number into integer and fractional parts"""
        int_part = int(self._fraction)
        frac_part = self._fraction - int_part
        return int_part, frac_part

    def __str__(self) -> str:
        """
        Convert the number to its string representation in its base

        Returns:
            String representation of the number in its base
        """
        int_part, frac_part = self._int_and_frac_parts()

        int_str = self._convert_int_part(int_part, self.base)
        frac_str = self._convert_frac_part(frac_part, self.base)

        if frac_str:
            return f"{int_str}.{frac_str}"
        else:
            return int_str

    def __eq__(self, other) -> bool:
        if isinstance(other, RadixNumber):
            return self._fraction == other._fraction
        return self._fraction == other

    def __add__(self, other) -> "RadixNumber":
        if isinstance(other, RadixNumber):
            result = RadixNumber(self._fraction + other._fraction, self.base)
            return result
        return RadixNumber(self._fraction + other, self.base)

    def __sub__(self, other) -> "RadixNumber":
        if isinstance(other, RadixNumber):
            result = RadixNumber(self._fraction - other._fraction, self.base)
            return result
        return RadixNumber(self._fraction - other, self.base)

    def __mul__(self, other) -> "RadixNumber":
        if isinstance(other, RadixNumber):
            result = RadixNumber(self._fraction * other._fraction, self.base)
            return result
        return RadixNumber(self._fraction * other, self.base)

    def __truediv__(self, other) -> "RadixNumber":
        if isinstance(other, RadixNumber):
            result = RadixNumber(self._fraction / other._fraction, self.base)
            return result
        return RadixNumber(self._fraction / other, self.base)

    def __pow__(self, other) -> "RadixNumber":
        if isinstance(other, RadixNumber):
            result = RadixNumber(self._fraction**other._fraction, self.base)
            return result
        return RadixNumber(self._fraction**other, self.base)

    def __neg__(self) -> "RadixNumber":
        return RadixNumber(-self._fraction, self.base)

    def __abs__(self) -> "RadixNumber":
        return RadixNumber(abs(self._fraction), self.base)

    def __rshift__(self, n: int) -> "RadixNumber":
        """
        Right shift operator - shifts decimal point right by n positions
        Equivalent to multiplying by base^(-n)

        Args:
            n: Number of positions to shift right

        Returns:
            A new RadixNumber shifted right by n positions
        """
        if not isinstance(n, int):
            raise TypeError("Right shift amount must be an integer")

        shift_factor = Fraction(1, self.base**n)
        return RadixNumber(self._fraction * shift_factor, self.base)

    def __lshift__(self, n: int) -> "RadixNumber":
        """
        Left shift operator - shifts decimal point left by n positions
        Equivalent to multiplying by base^n

        Args:
            n: Number of positions to shift left

        Returns:
            A new RadixNumber shifted left by n positions
        """
        if not isinstance(n, int):
            raise TypeError("Left shift amount must be an integer")

        shift_factor = self.base**n
        return RadixNumber(self._fraction * shift_factor, self.base)










    # def __init_string__(
    #         self, 
    #         value: str,
    #         base: Optional[int] = None, # (#TODO: none for default behaviour, number for strict) base is used for understanding str inputs, ignored if zero_b_o_x_leading_implicit_based or scientific notation
    #         digits: str = _DEFAULT_DIGITS, # TODO: List or Tuple as well, also weird multi char should be handled here if wanna be handled
    #         is_scientific_notation_str = False, 
    #         zero_b_o_x_leading_implicit_based = False, # if true, if str has a format of 0b 0x 0o it would be converted to the respective base
    #         scientific_notation_char = "eE", # used for scientific notation conversion 
    #         case_sensitive = False, # if true, the digits and scientific notation char are case sensitive
    #         representation_base = None # None for default (using 10 for int, float, decimal, using `base` or b_o_x for str, using `"fraction"` for Fraction), int for base, "fraction" for fraction, this is used for showing output and cross operation propegation
    # ):

    # def num_to_num():

    # def _append_any_base_str_particles_to_unified_str(self):
    #     return self.i_str + ("." if (self.fp_rep_str or self.fp_str) else "") + self.fp_str + ("[" + self.fp_rep_str + "]" if self.fp_rep_str else "")

        # frac: fractional value
        # case_sensitive = case_sensitive
        # digits: string of digits used for representation and conversion of string
        # representation_base: int or "fraction"
        # representation_value

# keep these for same base merging, @ and []

# operation stuff
# [] @ 
# digits, base and other stuff how to keep and propegate cross operations

# @Base() @Digits() @SciNot() @CaseSensitive
# .to(base="")
# [10] to base 10


















        """
        Initialize a RadixNumber from various input types with flexible base conversion options.

        This     constru?ctor accepts a value that can be a string, integer, float, Decimal, Fraction, or another
                xNumber. It converts the input into an internal Fraction representation, handling different numeric
                ats such as standard, scientific, and many more. The provided base, digit set, and notation
                ons customize both parsing and output. The key feature is the ability to handle repeating decimals in
                ts and outputs, and first class support for numbers in any base with custom digits and arbitrary
                ision.
                constructor also allows for automatic detection of base prefixes (0b, 0x, 0o) in string inputs, and
                ides options for case sensitivity and scientific notation handling.

        Parameters:
            value (Union[str, int, float, Decimal, Fraction, RadixNumber]):
                The number to be converted. If a RadixNumber is provided, it is returned as is.
                Floats are treated as scientific notation strings.
            base (int, optional):
                The base used to interpret string inputs. This parameter is ignored if the input includes an
                implicit base prefix (e.g. 0b, 0x, 0o) or when scientific notation is detected. Defaults to 10.
            digits (str, optional):
                String containing the characters representing the digits. Must not overlap with special characters
                (e.g. '.', '_', '[', ']'). Defaults to the module’s _DEFAULT_DIGITS.
            is_scientific_notation_str (bool, optional):
                Indicates whether to treat the input string as a scientific notation string, triggering a conversion
                to its full decimal representation. Defaults to False.
            zero_b_o_x_leading_implicit_based (bool, optional):
                If True, automatically detects prefixes (0b, 0x, 0o) in the input string and adjusts the base accordingly.
                Defaults to False.
            scientific_notation_char (str, optional):
                Characters used to denote scientific notation. Must not overlap with digits in their active range.
                Defaults to "eE".
            case_sensitive (bool, optional):
                Determines if the digit set and scientific notation characters are treated as case sensitive.
                If False, they are normalized to uppercase. Defaults to False.
            representation_base (int or str, optional):
                Specifies the base for output and operation propagation. Use an integer for a fixed base or
                "fraction" to preserve the Fraction representation. Defaults to None, inferring the base from the input.

        Raises:
            ValueError:
                If the provided digit set contains any reserved special characters or if the input value contains
                characters not found within the allowed digit set.

        Notes:
            - When the provided value is already a RadixNumber, it is directly assigned.
            - Scientific notation strings are fully expanded using a dedicated conversion mechanism without
            relying on floating-point arithmetic.
        """




    # def __next__(self):
    # def __reversed__(self):
    # def __contains__(self):
    # __index__

    # def __enter__(self):
    # def __exit__(self):

    # Not sure Dunders 

    # and, rand
    # xor, rxor
    # or, ror

    # not sure how to implement this
    # should it be shift in base or base 2?
    # how to handle sub 1 values?
    # denom * base for r and nom * base for l?
    # then what about info loss of real shift?
    # more useful overload?

    # IDEA: call objects handle for some actions maybe