libdev
Advanced tools
| Metadata-Version: 2.1 | ||
| Name: libdev | ||
| Version: 0.93 | ||
| Version: 0.94 | ||
| Summary: Set of standard functions for development | ||
@@ -5,0 +5,0 @@ Home-page: https://github.com/chilleco/lib |
@@ -5,4 +5,4 @@ """ | ||
| __version__ = "0.93" | ||
| __version__ = "0.94" | ||
| __all__ = ("__version__",) |
+179
-72
@@ -7,17 +7,6 @@ """ | ||
| import math | ||
| from decimal import Decimal, ROUND_HALF_UP | ||
| from decimal import Decimal, InvalidOperation | ||
| _SUBSCRIPTS = { | ||
| "0": "₀", | ||
| "1": "₁", | ||
| "2": "₂", | ||
| "3": "₃", | ||
| "4": "₄", | ||
| "5": "₅", | ||
| "6": "₆", | ||
| "7": "₇", | ||
| "8": "₈", | ||
| "9": "₉", | ||
| } | ||
| _SUBSCRIPTS = str.maketrans("0123456789", "₀₁₂₃₄₅₆₇₈₉") | ||
@@ -133,3 +122,10 @@ | ||
| def pretty(value, decimals=None, sign=False, symbol="’"): | ||
| def pretty( | ||
| value, | ||
| decimals=None, | ||
| sign=False, | ||
| symbol="’", | ||
| zeros=4, | ||
| compress=None, | ||
| ): | ||
| """Decorate the number beautifully""" | ||
@@ -140,14 +136,41 @@ | ||
| data = str(float(value)) | ||
| # Handle decimals parameter first (takes precedence) | ||
| if decimals is not None: | ||
| cur = len(data.split(".", maxsplit=1)[0]) | ||
| data = str(round(value, max(0, decimals - cur))) | ||
| # Use the original decimals logic for backward compatibility | ||
| s = to_plain(abs(value)) | ||
| if "." in s: | ||
| int_part = s.split(".", 1)[0] | ||
| cur = len(int_part) | ||
| target_decimals = max(0, decimals - cur) | ||
| # Apply rounding first | ||
| rounded_value = round(float(value), target_decimals) | ||
| # If target_decimals is 0, convert to int for proper formatting | ||
| if target_decimals == 0: | ||
| rounded_value = int(rounded_value) | ||
| # Then use compress_zeros without round parameter | ||
| data = compress_zeros(rounded_value, zeros=zeros) | ||
| else: | ||
| data = compress_zeros(value, zeros=zeros) | ||
| elif zeros is None and compress is None: | ||
| # No compression or special formatting requested, use plain representation | ||
| data = to_plain(value) | ||
| else: | ||
| # Use compress_zeros with specified parameters | ||
| compress_zeros_args = {} | ||
| if data.rsplit(".", maxsplit=1)[-1] == "0": | ||
| data = data.split(".", maxsplit=1)[0] | ||
| if zeros is not None: | ||
| compress_zeros_args["zeros"] = zeros | ||
| if compress is not None: | ||
| compress_zeros_args["round"] = compress | ||
| data = compress_zeros(value, **compress_zeros_args) | ||
| if data == "0": | ||
| return "0" | ||
| # Remove trailing zeros after decimal point for cleaner formatting | ||
| if "." in data and data.rsplit(".", maxsplit=1)[-1] == "0": | ||
| data = data.split(".", maxsplit=1)[0] | ||
| if symbol: | ||
@@ -264,24 +287,38 @@ data = add_radix(data, symbol) | ||
| def _to_subscript(n: int) -> str: | ||
| """Convert an integer n to a string of subscript digits.""" | ||
| return "".join(_SUBSCRIPTS[d] for d in str(n)) | ||
| def to_plain(value) -> str: | ||
| if value is None: | ||
| return None | ||
| try: | ||
| if isinstance(value, str): | ||
| d = Decimal(value) | ||
| elif isinstance(value, float): | ||
| d = Decimal(str(value)) | ||
| else: | ||
| d = Decimal(value) | ||
| s = format(d.normalize(), "f") | ||
| def compress_zeros(x: int | float, round: int | None = None) -> str | None: | ||
| """ | ||
| Given a float or decimal‐string x, return a string where | ||
| runs of leading zeros in the fraction are shown as: | ||
| one '0' plus a subscript count of the zeros. | ||
| If `round` is provided, the remaining digits after the zeros | ||
| are rounded to that many places. | ||
| if "." in s: | ||
| s = s.rstrip("0").rstrip(".") | ||
| if s == "-0": | ||
| s = "0" | ||
| return s | ||
| except (InvalidOperation, ValueError, TypeError): | ||
| return str(value) | ||
| Examples: | ||
| compress_zeros(0.00012) -> '0.0₃12' | ||
| compress_zeros(0.00012, round=2) -> '0.0₃12' | ||
| compress_zeros(0.0123) -> '0.0123' (only one leading zero, so unchanged) | ||
| compress_zeros(0.0123456, round=3)-> '0.0123' | ||
| compress_zeros(1.000045) -> '1.0₄45' | ||
| compress_zeros(-0.0010959999999999997522, round=3) | ||
| -> '-0.0₂11' | ||
| def _round_to_decimals(x, decimals): | ||
| """Helper function to round to specified decimal places""" | ||
| if decimals <= 0: | ||
| return float(int(x)) | ||
| return round(float(x), decimals) | ||
| def compress_zeros(x, zeros=2, round=None) -> str: | ||
| """ | ||
| 0.000012 -> '0.0₄12' | ||
| 1.000045 -> '1.0₄45' | ||
| round: number of digits after the zero block (rounds). | ||
| zeros: minimum count of consecutive zeros to compress (default: 2). | ||
| """ | ||
@@ -291,45 +328,115 @@ if x is None: | ||
| dec_x = Decimal(str(x)) | ||
| s = format(dec_x, "f") | ||
| # Store original string representation for rounding calculations | ||
| original_str = None | ||
| if isinstance(x, str): | ||
| original_str = x.strip() | ||
| x = original_str | ||
| # Remove trailing zeros from string | ||
| if "." in x: | ||
| x = x.rstrip("0").rstrip(".") | ||
| # Convert to appropriate numeric type | ||
| try: | ||
| if "." in x: | ||
| x = float(x) | ||
| else: | ||
| x = int(x) | ||
| except ValueError: | ||
| return str(x) | ||
| # no fractional part | ||
| if "." not in s: | ||
| return s | ||
| # Determine if original was float or int to preserve format | ||
| is_float_type = isinstance(x, float) or (isinstance(x, str) and "." in str(x)) | ||
| int_part, frac = s.split(".", 1) | ||
| # Handle rounding if specified | ||
| if round is not None: | ||
| # For rounding, use original string if available, otherwise convert to plain string | ||
| if original_str and "." in original_str: | ||
| s = original_str.lstrip("-") | ||
| else: | ||
| # Use to_plain to avoid scientific notation | ||
| s = to_plain(abs(x)) | ||
| # count leading zeros in the fractional part | ||
| zero_run = len(frac) - len(frac.lstrip("0")) | ||
| if "." in s: | ||
| int_part, frac_part = s.split(".") | ||
| # Count leading zeros in fractional part | ||
| leading_zeros = 0 | ||
| for c in frac_part: | ||
| if c == "0": | ||
| leading_zeros += 1 | ||
| else: | ||
| break | ||
| # If rounding is requested, do it first | ||
| if round is not None: | ||
| # ensure at least one zero is counted for quantization | ||
| places = max(zero_run, 1) + round | ||
| quant = Decimal(f"1e-{places}") | ||
| dec_q = dec_x.quantize(quant, rounding=ROUND_HALF_UP) | ||
| s_q = format(dec_q, "f") | ||
| # Count trailing zeros in fractional part | ||
| trailing_zeros = 0 | ||
| for c in reversed(frac_part): | ||
| if c == "0": | ||
| trailing_zeros += 1 | ||
| else: | ||
| break | ||
| # if rounding eliminated fractional part | ||
| if "." not in s_q: | ||
| return s_q | ||
| # Apply rounding logic | ||
| if leading_zeros > 0: | ||
| # If there are leading zeros, round after them (regardless of compression) | ||
| total_decimals = leading_zeros + round | ||
| x = _round_to_decimals(x, total_decimals) | ||
| else: | ||
| # No leading zeros, apply normal rounding | ||
| x = _round_to_decimals(x, round) | ||
| int_part_q, frac_q = s_q.split(".", 1) | ||
| # for zero_run == 0 or 1, we just return the rounded string | ||
| if zero_run <= 1: | ||
| return s_q | ||
| # Convert to string representation | ||
| if isinstance(x, int) and not is_float_type: | ||
| s = str(x) | ||
| else: | ||
| # For floats, use format that preserves trailing decimals when needed | ||
| if x == int(x) and is_float_type: | ||
| s = f"{int(x)}.0" | ||
| else: | ||
| s = str(float(x)) | ||
| # Remove scientific notation if present | ||
| if "e" in s.lower(): | ||
| s = f"{float(x):.15f}".rstrip("0") | ||
| if s.endswith("."): | ||
| s += "0" | ||
| # strip any trailing zeros, then compress | ||
| frac_q = frac_q.rstrip("0") | ||
| if not frac_q: | ||
| return int_part_q | ||
| tail = frac_q[zero_run:] | ||
| return f"{int_part_q}.0{_to_subscript(zero_run)}{tail}" | ||
| # Handle negative sign | ||
| negative = s.startswith("-") | ||
| if negative: | ||
| s = s[1:] | ||
| # No rounding: only compress if more than one leading zero | ||
| if zero_run <= 1: | ||
| return s | ||
| # Process compression | ||
| if "." not in s: | ||
| result = s | ||
| else: | ||
| int_part, frac_part = s.split(".") | ||
| tail = frac[zero_run:] | ||
| # Compress leading zeros in fractional part | ||
| leading_zeros = 0 | ||
| for c in frac_part: | ||
| if c == "0": | ||
| leading_zeros += 1 | ||
| else: | ||
| break | ||
| # build compressed form | ||
| return f"{int_part}.0{_to_subscript(zero_run)}{tail}" | ||
| # Compress trailing zeros in fractional part | ||
| trailing_zeros = 0 | ||
| for c in reversed(frac_part): | ||
| if c == "0": | ||
| trailing_zeros += 1 | ||
| else: | ||
| break | ||
| # Apply compression | ||
| if leading_zeros >= zeros: | ||
| # Compress leading zeros | ||
| remaining_frac = frac_part[leading_zeros:] | ||
| result = f"{int_part}.0{str(leading_zeros).translate(_SUBSCRIPTS)}{remaining_frac}" | ||
| elif trailing_zeros >= zeros and leading_zeros == 0: | ||
| # Compress trailing zeros (but not if there are leading zeros) | ||
| remaining_frac = frac_part[:-trailing_zeros] | ||
| if remaining_frac: | ||
| result = f"{int_part}.{remaining_frac}0{str(trailing_zeros).translate(_SUBSCRIPTS)}" | ||
| else: | ||
| result = f"{int_part}.0{str(trailing_zeros).translate(_SUBSCRIPTS)}" | ||
| else: | ||
| result = s | ||
| return f"-{result}" if negative else result |
+21
-0
@@ -303,2 +303,23 @@ """ | ||
| def get_week_start(timestamp=None, tz=0): | ||
| """ | ||
| Get the start of the week (midnight on Monday) for a given timestamp in a specified timezone. | ||
| Args: | ||
| timestamp (float): The original timestamp (in seconds since epoch). Defaults to the current time if None. | ||
| tz (int): The timezone offset in hours (e.g., 3 for UTC+3). | ||
| Returns: | ||
| float: The timestamp for the start of the week (Monday at midnight) in the specified timezone. | ||
| """ | ||
| if timestamp is None: | ||
| timestamp = time.time() | ||
| dt_local = datetime.datetime.fromtimestamp(timestamp, tz=to_tz(tz)) | ||
| # Calculate days to subtract to get to Monday (weekday() returns 0 for Monday, 6 for Sunday) | ||
| days_since_monday = dt_local.weekday() | ||
| start_week = dt_local - datetime.timedelta(days=days_since_monday) | ||
| start_week = start_week.replace(hour=0, minute=0, second=0, microsecond=0) | ||
| return int(start_week.timestamp()) | ||
| def get_next_day(timestamp=None, tz=0): | ||
@@ -305,0 +326,0 @@ """ |
+1
-1
| Metadata-Version: 2.1 | ||
| Name: libdev | ||
| Version: 0.93 | ||
| Version: 0.94 | ||
| Summary: Set of standard functions for development | ||
@@ -5,0 +5,0 @@ Home-page: https://github.com/chilleco/lib |
+76
-0
@@ -14,2 +14,3 @@ from libdev.num import ( | ||
| pretty, | ||
| to_plain, | ||
| compress_zeros, | ||
@@ -160,4 +161,67 @@ ) | ||
| assert pretty(12345.6, 3, True) == "+12’346" | ||
| assert pretty(-0.000000235235, zeros=None, compress=None) == "-0.000000235235" | ||
| assert pretty(-0.000000235235, zeros=4, compress=2) == "-0.0₆24" | ||
| def test_to_plain(): | ||
| assert to_plain(None) == None | ||
| assert to_plain(0) == "0" | ||
| # assert to_plain(0.0) == "0.0" | ||
| # assert to_plain(1.0) == "1.0" | ||
| assert to_plain(1.1) == "1.1" | ||
| assert to_plain(0.000000235235) == "0.000000235235" | ||
| assert to_plain(-0.000000235235) == "-0.000000235235" | ||
| assert to_plain("0.000000235235") == "0.000000235235" | ||
| assert to_plain(2.35235e-07) == "0.000000235235" | ||
| assert to_plain("1e-12") == "0.000000000001" | ||
| assert to_plain("123.4500") == "123.45" | ||
| def test_pretty_with_compression(): | ||
| # Test pretty function with compression parameters | ||
| # Test zeros parameter (minimum zeros to compress) | ||
| assert pretty(0.00012, zeros=2) == "0.0₃12" # Default behavior, compress >=2 zeros | ||
| assert pretty(0.01, zeros=1) == "0.0₁1" # Compress single zero with zeros=1 | ||
| assert pretty(0.01, zeros=3) == "0.01" # Don't compress with zeros=3 (only 1 zero) | ||
| assert pretty(0.0001, zeros=3) == "0.0₃1" # Compress with zeros=3 (3 zeros) | ||
| # Test compress parameter (round after zero block) | ||
| assert ( | ||
| pretty(0.00012345, zeros=2, compress=2) == "0.0₃12" | ||
| ) # Round to 2 digits after zeros | ||
| assert ( | ||
| pretty(0.00012345, zeros=4, compress=4) == "0.0001234" | ||
| ) # No compression (3 < 4 zeros), 4 digits after 3 zeros | ||
| assert pretty(0.00012345, zeros=4, compress=8) == "0.00012345" | ||
| assert ( | ||
| pretty(0.00012345, zeros=2, compress=3) == "0.0₃123" | ||
| ) # Round to 3 digits after zeros | ||
| assert ( | ||
| pretty(-0.0010959999999999997522, compress=3) == "-0.0011" | ||
| ) # Negative with rounding | ||
| # Test both parameters together | ||
| assert ( | ||
| pretty("0.0000012345", zeros=4, compress=2) == "0.0₅12" | ||
| ) # 5 zeros, round to 2 digits | ||
| assert ( | ||
| pretty("0.00012345", zeros=4, compress=2) == "0.00012" | ||
| ) # Only 3 zeros, no compression, 2 digits after 3 zeros | ||
| # Test that other pretty parameters still work with compression | ||
| assert pretty(0.00012, zeros=2, sign=True) == "+0.0₃12" # With sign | ||
| assert pretty(12000.00012, zeros=2, symbol="'") == "12'000.0₃12" # With radix | ||
| assert ( | ||
| pretty(-0.00012, zeros=2, sign=True, symbol="'") == "-0.0₃12" | ||
| ) # Negative with sign and radix | ||
| # Test edge cases | ||
| assert pretty(0, zeros=1) == "0" # Zero value | ||
| assert pretty(None, zeros=2) == None # None value | ||
| assert ( | ||
| pretty(1.0, zeros=2) == "1" | ||
| ) # No fractional zeros to compress, trailing zero removed for clean formatting | ||
| def test_compress_zeros(): | ||
@@ -175,2 +239,14 @@ assert compress_zeros(None) == None | ||
| assert compress_zeros(1.000045) == "1.0₄45" | ||
| assert compress_zeros(0.0010859999999999997522, round=3) == "0.0₂109" | ||
| assert compress_zeros(-0.0010959999999999997522, round=3) == "-0.0₂11" | ||
| assert compress_zeros("-0.012300") == "-0.0123" | ||
| assert compress_zeros(0.01, zeros=1) == "0.0₁1" | ||
| assert compress_zeros(0.001, zeros=1) == "0.0₂1" | ||
| assert compress_zeros("1.10", zeros=1) == "1.1" | ||
| assert compress_zeros(0.01, zeros=3) == "0.01" | ||
| assert compress_zeros(0.001, zeros=3) == "0.001" | ||
| assert compress_zeros(0.0001, zeros=3) == "0.0₃1" | ||
| assert compress_zeros(0.00001, zeros=3) == "0.0₄1" | ||
| assert compress_zeros(0.0000012, zeros=1) == "0.0₅12" | ||
| assert compress_zeros(0.0000012, zeros=5) == "0.0₅12" | ||
| assert compress_zeros(0.0000012, zeros=6) == "0.0000012" |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
105994
8.18%2886
6.81%