base-conversion 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,49 @@
1
+ """base-conversion — 任意进制(2-62)精确转换库。
2
+
3
+ 核心能力:
4
+
5
+ - 大整数无精度损失(Python 原生 ``int``);
6
+ - 小数用 ``Fraction`` 精确算术,循环小数用括号循环节表示并可无损往返
7
+ (``1/3`` ↔ ``"0.(3)"``);
8
+ - 2-36 进制大小写不敏感,37-62 大小写敏感(``0-9A-Za-z``);
9
+ - 提供 ``baseconv`` 命令行入口(``pip install`` 后可用)。
10
+
11
+ 快速上手::
12
+
13
+ >>> from base_conversion import to_base, to_decimal
14
+ >>> to_base(255, 16)
15
+ 'FF'
16
+ >>> to_decimal("0.1", 2)
17
+ Fraction(1, 2)
18
+ """
19
+
20
+ from base_conversion.converter import (
21
+ BASE_MAX,
22
+ BASE_MIN,
23
+ DIGITS,
24
+ base_to_decimal,
25
+ binary_to_decimal,
26
+ decimal_to_base,
27
+ decimal_to_binary,
28
+ decimal_to_hex,
29
+ hex_to_decimal,
30
+ to_base,
31
+ to_decimal,
32
+ )
33
+
34
+ __version__ = "1.0.0"
35
+
36
+ __all__ = [
37
+ "BASE_MAX",
38
+ "BASE_MIN",
39
+ "DIGITS",
40
+ "__version__",
41
+ "base_to_decimal",
42
+ "binary_to_decimal",
43
+ "decimal_to_base",
44
+ "decimal_to_binary",
45
+ "decimal_to_hex",
46
+ "hex_to_decimal",
47
+ "to_base",
48
+ "to_decimal",
49
+ ]
base_conversion/cli.py ADDED
@@ -0,0 +1,129 @@
1
+ """``baseconv`` 命令行入口。
2
+
3
+ 用法::
4
+
5
+ baseconv NUMBER [FROM_BASE] [TO_BASE] # 位置参数式(基数默认 10)
6
+ baseconv -n NUMBER -f FROM -t TO # 选项式(负数输入推荐)
7
+ baseconv -- -123 10 2 # ``--`` 分隔符式
8
+
9
+ 输出即目标进制表示:整数为数字串,精确小数为有限位或括号循环节
10
+ (``0.(3)`` 表示 1/3 的十进制循环展开)。
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import re
17
+ import sys
18
+
19
+ from base_conversion import __version__
20
+ from base_conversion.converter import to_base, to_decimal
21
+
22
+ # 带符号输入可能以数字、字母、小数点或循环括号开头,如 -123、-FF、-.5。
23
+ _SIGNED_NUMBER = re.compile(r"^[-+](?=[0-9A-Za-z.(])")
24
+ _OPTION_FLAGS = frozenset(
25
+ {"-h", "--help", "-V", "--version", "-n", "--number", "-f", "--from-base", "-t", "--to-base"}
26
+ )
27
+
28
+
29
+ def _is_signed_number(value: str) -> bool:
30
+ """判断参数是否形如带符号数字,而非 CLI 自身的选项。"""
31
+ return value not in _OPTION_FLAGS and _SIGNED_NUMBER.match(value) is not None
32
+
33
+
34
+ def _normalize_argv(argv: list[str]) -> list[str]:
35
+ """避免 argparse 将带符号数字误判为选项。
36
+
37
+ ``-n``/``--number`` 后的带符号值改写为 ``--number=VALUE``;首个位置参数
38
+ 为带符号值时保留原有兼容行为,自动在其前插入 ``--``。
39
+ """
40
+ normalized: list[str] = []
41
+ index = 0
42
+ while index < len(argv):
43
+ arg = argv[index]
44
+ if arg == "--":
45
+ normalized.extend(argv[index:])
46
+ break
47
+ if (
48
+ arg in ("-n", "--number")
49
+ and index + 1 < len(argv)
50
+ and _is_signed_number(argv[index + 1])
51
+ ):
52
+ normalized.append(f"--number={argv[index + 1]}")
53
+ index += 2
54
+ continue
55
+ normalized.append(arg)
56
+ index += 1
57
+
58
+ if normalized and _is_signed_number(normalized[0]):
59
+ return ["--", *normalized]
60
+ return normalized
61
+
62
+
63
+ def build_parser() -> argparse.ArgumentParser:
64
+ """构建命令行解析器。"""
65
+ parser = argparse.ArgumentParser(
66
+ prog="baseconv",
67
+ description=(
68
+ "任意进制(2-62)精确转换:大整数无损、小数用分数精确表示、循环小数输出括号循环节。"
69
+ ),
70
+ epilog=(
71
+ "示例:\n"
72
+ " baseconv 255 10 16 # → FF\n"
73
+ " baseconv -- -FF 16 10 # → -255(位置参数负数用 -- 分隔)\n"
74
+ " baseconv -n -FF -f 16 -t 10 # → -255(-n 可直接接收负数值)\n"
75
+ " baseconv -n 0.1 -f 2 -t 10 # → 0.5(二进制小数精确转十进制)"
76
+ ),
77
+ formatter_class=argparse.RawDescriptionHelpFormatter,
78
+ )
79
+ parser.add_argument(
80
+ "-V",
81
+ "--version",
82
+ action="version",
83
+ version=f"base-conversion {__version__}",
84
+ help="显示版本并退出",
85
+ )
86
+ parser.add_argument(
87
+ "number",
88
+ nargs="?",
89
+ help="待转换的数字(源进制字符串;负数可用 -- 分隔或用 -n 提供)",
90
+ )
91
+ parser.add_argument("from_base", nargs="?", type=int, default=10, help="源进制(默认 10)")
92
+ parser.add_argument("to_base_pos", nargs="?", type=int, default=10, help="目标进制(默认 10)")
93
+ parser.add_argument(
94
+ "-n", "--number", dest="number_opt", help="数字(可直接接收 -FF、-0.1 等负数值)"
95
+ )
96
+ parser.add_argument(
97
+ "-f", "--from-base", dest="from_base_opt", type=int, help="源进制(选项式,优先于位置)"
98
+ )
99
+ parser.add_argument(
100
+ "-t", "--to-base", dest="to_base_opt", type=int, help="目标进制(选项式,优先于位置)"
101
+ )
102
+ return parser
103
+
104
+
105
+ def main(argv: list[str] | None = None) -> int:
106
+ """CLI 入口:返回 0 成功 / 1 转换错误 / 2 用法错误(argparse 自带)。"""
107
+ args_list = sys.argv[1:] if argv is None else list(argv)
108
+ args_list = _normalize_argv(args_list)
109
+ parser = build_parser()
110
+ ns = parser.parse_args(args_list)
111
+
112
+ number = ns.number_opt if ns.number_opt is not None else ns.number
113
+ from_base = ns.from_base_opt if ns.from_base_opt is not None else ns.from_base
114
+ target_base = ns.to_base_opt if ns.to_base_opt is not None else ns.to_base_pos
115
+
116
+ if number is None:
117
+ parser.error("必须提供待转换的数字(位置参数或 -n)")
118
+
119
+ try:
120
+ value = to_decimal(number, from_base)
121
+ print(to_base(value, target_base))
122
+ except (ValueError, TypeError) as exc:
123
+ print(f"baseconv: 错误: {exc}", file=sys.stderr)
124
+ return 1
125
+ return 0
126
+
127
+
128
+ if __name__ == "__main__":
129
+ raise SystemExit(main())
@@ -0,0 +1,271 @@
1
+ """Core conversion routines for the base-conversion library.
2
+
3
+ Convert numbers between arbitrary bases from 2 to 62 with exact
4
+ integer/Fraction arithmetic, including repeating-digit notation such as
5
+ ``"0.(3)"`` for 1/3 in base 10.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from fractions import Fraction
11
+
12
+ __all__ = [
13
+ "BASE_MAX",
14
+ "BASE_MIN",
15
+ "DIGITS",
16
+ "base_to_decimal",
17
+ "binary_to_decimal",
18
+ "decimal_to_base",
19
+ "decimal_to_binary",
20
+ "decimal_to_hex",
21
+ "hex_to_decimal",
22
+ "to_base",
23
+ "to_decimal",
24
+ ]
25
+
26
+ DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
27
+
28
+ BASE_MIN = 2
29
+ BASE_MAX = 62
30
+
31
+ # Digit value lookup: character -> value (0..61).
32
+ _DIGIT_VALUES = {ch: i for i, ch in enumerate(DIGITS)}
33
+
34
+
35
+ def _validate_base(base: int) -> None:
36
+ """Validate that *base* is a usable radix.
37
+
38
+ Args:
39
+ base: Candidate radix.
40
+
41
+ Raises:
42
+ TypeError: If *base* is a ``bool`` or not an ``int``.
43
+ ValueError: If *base* is outside ``[BASE_MIN, BASE_MAX]``.
44
+ """
45
+ if isinstance(base, bool) or not isinstance(base, int):
46
+ raise TypeError(f"base must be an int, got {type(base).__name__}")
47
+ if not BASE_MIN <= base <= BASE_MAX:
48
+ raise ValueError(f"base must be {BASE_MIN}-{BASE_MAX}, got {base}")
49
+
50
+
51
+ def _digit_value(ch: str, base: int) -> int:
52
+ """Return the numeric value of a single digit character.
53
+
54
+ For bases 2-36 upper and lower case are both accepted; for bases
55
+ 37-62 the alphabetic digits are case sensitive (``'A'`` is 10 and
56
+ ``'a'`` is 36).
57
+
58
+ Raises:
59
+ ValueError: If *ch* is not a valid digit in *base*.
60
+ """
61
+ lookup = ch.upper() if base <= 36 else ch
62
+ value = _DIGIT_VALUES.get(lookup)
63
+ if value is None or value >= base:
64
+ raise ValueError(f"invalid character {ch!r} for base {base}")
65
+ return value
66
+
67
+
68
+ def _parse_digit_run(text: str, base: int) -> int:
69
+ """Convert a non-empty run of digits (no sign, no dot) to an int."""
70
+ value = 0
71
+ for ch in text:
72
+ value = value * base + _digit_value(ch, base)
73
+ return value
74
+
75
+
76
+ def to_decimal(s: str, base: int) -> int | Fraction:
77
+ """Parse a string in *base* and return its exact numeric value.
78
+
79
+ Accepted grammar (whitespace is not allowed)::
80
+
81
+ [sign] digits [ "." digits ] [ "(" digits ")" ]
82
+
83
+ * Bases 2-36 are case insensitive; bases 37-62 are case sensitive
84
+ (``'a'`` is worth 36 and only reachable for base >= 37).
85
+ * Either the integer or the fractional digits may be empty, but not
86
+ both (unless a repeating part is present); a bare ``"."`` is an
87
+ error.
88
+ * Repeating digits use parentheses: ``"0.(3)"`` is 1/3 in base 10
89
+ and ``"0.1(6)"`` is 1/6. Empty, nested, or repeated parenthesized
90
+ groups are rejected.
91
+
92
+ Args:
93
+ s: The string to parse.
94
+ base: The radix to interpret *s* with (2-62).
95
+
96
+ Returns:
97
+ An ``int`` when *s* has no fractional part, otherwise an exact
98
+ ``fractions.Fraction``.
99
+
100
+ Raises:
101
+ TypeError: If *s* is not a ``str`` or *base* is not an ``int``.
102
+ ValueError: If *s* is malformed for *base*.
103
+ """
104
+ _validate_base(base)
105
+ if not isinstance(s, str):
106
+ raise TypeError(f"number must be a str, got {type(s).__name__}")
107
+
108
+ sign = 1
109
+ body = s
110
+ if body[:1] in ("+", "-"):
111
+ if body[0] == "-":
112
+ sign = -1
113
+ body = body[1:]
114
+
115
+ # Extract the repeating (parenthesized) part, if any.
116
+ repeating = ""
117
+ open_count = body.count("(")
118
+ if open_count or body.count(")"):
119
+ if open_count != 1 or body.count(")") != 1:
120
+ raise ValueError(f"multiple or misplaced parentheses in {s!r}")
121
+ open_idx = body.index("(")
122
+ close_idx = body.index(")")
123
+ if close_idx != len(body) - 1 or close_idx < open_idx:
124
+ raise ValueError(f"misplaced parenthesis in {s!r}")
125
+ repeating = body[open_idx + 1 : close_idx]
126
+ if not repeating:
127
+ raise ValueError(f"empty repeating part in {s!r}")
128
+ body = body[:open_idx]
129
+
130
+ if body.count(".") > 1:
131
+ raise ValueError(f"more than one decimal point in {s!r}")
132
+ if "." in body:
133
+ int_part, frac_part = body.split(".", 1)
134
+ else:
135
+ int_part, frac_part = body, ""
136
+ if not int_part and not frac_part and not repeating:
137
+ raise ValueError(f"empty or invalid number string {s!r}")
138
+
139
+ int_value = _parse_digit_run(int_part, base) if int_part else 0
140
+
141
+ if not frac_part and not repeating:
142
+ return sign * int_value
143
+
144
+ result = Fraction(int_value)
145
+ if frac_part:
146
+ frac_value = _parse_digit_run(frac_part, base)
147
+ result += Fraction(frac_value, base ** len(frac_part))
148
+ if repeating:
149
+ rep_value = _parse_digit_run(repeating, base)
150
+ n = len(frac_part)
151
+ m = len(repeating)
152
+ # value = int + prefix/base^n + rep/(base^n * (base^m - 1))
153
+ result += Fraction(rep_value, base**n * (base**m - 1))
154
+ return sign * result
155
+
156
+
157
+ def _int_to_digits(n: int, base: int) -> str:
158
+ """Render a non-negative ``int`` as digits in *base* (no sign)."""
159
+ if n == 0:
160
+ return DIGITS[0]
161
+ digits: list[str] = []
162
+ while n:
163
+ n, digit = divmod(n, base)
164
+ digits.append(DIGITS[digit])
165
+ return "".join(reversed(digits))
166
+
167
+
168
+ def _fraction_to_digits(frac: Fraction, base: int) -> str:
169
+ """Render a fractional part (0 <= frac < 1) as digits in *base*.
170
+
171
+ Terminating expansions are returned plain (``"5"``); repeating ones
172
+ use parenthesized notation (``"(3)"`` or ``1(6)``), detected by
173
+ memoizing the remainder after each digit.
174
+ """
175
+ digits: list[str] = []
176
+ seen: dict[Fraction, int] = {}
177
+ while frac != 0:
178
+ if frac in seen:
179
+ start = seen[frac]
180
+ prefix = "".join(digits[:start])
181
+ repeating = "".join(digits[start:])
182
+ return f"{prefix}({repeating})"
183
+ seen[frac] = len(digits)
184
+ scaled = frac * base
185
+ digit = scaled.numerator // scaled.denominator
186
+ digits.append(DIGITS[digit])
187
+ frac = scaled - digit
188
+ return "".join(digits)
189
+
190
+
191
+ def to_base(value: int | Fraction | float, base: int) -> str:
192
+ """Render *value* as a string in *base*.
193
+
194
+ Args:
195
+ value: An ``int``, an exact ``fractions.Fraction``, or a float
196
+ with an integral value (e.g. ``255.0``). Booleans and
197
+ non-integral floats are rejected.
198
+ base: The target radix (2-62).
199
+
200
+ Returns:
201
+ The canonical string for *value* in *base*:
202
+
203
+ * uppercase letters for digit values 10-35, lowercase for
204
+ 36-61 (so output round-trips case-sensitively in bases 37-62);
205
+ * a leading ``-`` for negative values;
206
+ * repeating notation for non-terminating expansions, e.g.
207
+ ``"0.(3)"`` for 1/3 in base 10.
208
+
209
+ Raises:
210
+ TypeError: If *value* is a ``bool`` or of an unsupported type.
211
+ ValueError: If *value* is a non-integral float or *base* is out
212
+ of range.
213
+ """
214
+ _validate_base(base)
215
+ if isinstance(value, bool):
216
+ raise TypeError("value must be an int, Fraction, or integral float, not bool")
217
+ if isinstance(value, float):
218
+ if not value.is_integer():
219
+ raise ValueError(f"non-integral float {value!r} is lossy; pass a Fraction instead")
220
+ value = int(value)
221
+
222
+ if isinstance(value, Fraction):
223
+ sign = "-" if value < 0 else ""
224
+ magnitude = abs(value)
225
+ int_part = magnitude.numerator // magnitude.denominator
226
+ int_str = _int_to_digits(int_part, base)
227
+ frac = magnitude - int_part
228
+ if frac == 0:
229
+ return f"{sign}{int_str}"
230
+ return f"{sign}{int_str}.{_fraction_to_digits(frac, base)}"
231
+
232
+ if isinstance(value, int):
233
+ sign = "-" if value < 0 else ""
234
+ return f"{sign}{_int_to_digits(abs(value), base)}"
235
+
236
+ raise TypeError(
237
+ f"value must be an int, Fraction, or integral float, got {type(value).__name__}"
238
+ )
239
+
240
+
241
+ # --- Backwards-compatible convenience aliases ---------------------------
242
+
243
+
244
+ def decimal_to_base(value: int | Fraction | float, base: int) -> str:
245
+ """Alias of :func:`to_base`: render a decimal value in *base*."""
246
+ return to_base(value, base)
247
+
248
+
249
+ def base_to_decimal(s: str, base: int) -> int | Fraction:
250
+ """Alias of :func:`to_decimal`: parse a *base* string to decimal."""
251
+ return to_decimal(s, base)
252
+
253
+
254
+ def decimal_to_binary(value: int | Fraction | float) -> str:
255
+ """Alias of :func:`to_base` with ``base=2``."""
256
+ return to_base(value, 2)
257
+
258
+
259
+ def binary_to_decimal(s: str) -> int | Fraction:
260
+ """Alias of :func:`to_decimal` with ``base=2``."""
261
+ return to_decimal(s, 2)
262
+
263
+
264
+ def decimal_to_hex(value: int | Fraction | float) -> str:
265
+ """Alias of :func:`to_base` with ``base=16``."""
266
+ return to_base(value, 16)
267
+
268
+
269
+ def hex_to_decimal(s: str) -> int | Fraction:
270
+ """Alias of :func:`to_decimal` with ``base=16``."""
271
+ return to_decimal(s, 16)
File without changes
@@ -0,0 +1,156 @@
1
+ Metadata-Version: 2.4
2
+ Name: base-conversion
3
+ Version: 1.0.1
4
+ Summary: Exact arbitrary-base (2-62) number conversion: lossless big ints, Fraction-precise decimals, repeating-digit notation
5
+ Author: xiaomaozjj666
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/xiaomaozjj666/base-conversion
8
+ Project-URL: Issues, https://github.com/xiaomaozjj666/base-conversion/issues
9
+ Project-URL: Changelog, https://github.com/xiaomaozjj666/base-conversion/blob/main/CHANGELOG.md
10
+ Keywords: base,radix,conversion,numbers,fractions,cli
11
+ Classifier: Development Status :: 6 - Mature
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Topic :: Utilities
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=7; extra == "dev"
28
+ Requires-Dist: ruff>=0.5; extra == "dev"
29
+ Requires-Dist: mypy>=1.8; extra == "dev"
30
+ Requires-Dist: build>=1.0; extra == "dev"
31
+ Requires-Dist: setuptools>=69; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # base-conversion
35
+
36
+ > **⚠️ 本仓库已归档**:进制转换已迁入 [`programming-exercises`](https://github.com/xiaomaozjj666/programming-exercises)(`python|js|cpp/base_converter.*` 三语实现),算法基准并入其 `benchmark/`。本仓库仅保留库源码与基准的历史快照,不再演进——测试与 CI 持续可跑,但不再接受功能变更。
37
+
38
+ <p align="center">
39
+ <a href="https://github.com/xiaomaozjj666/base-conversion/actions/workflows/ci.yml"><img src="https://github.com/xiaomaozjj666/base-conversion/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>
40
+ <img src="https://img.shields.io/badge/python-3.9%2B-blue" alt="Python 3.9+" />
41
+ <img src="https://img.shields.io/badge/ruff-linted-261230" alt="Ruff" />
42
+ <img src="https://img.shields.io/badge/tests-76%20passed-brightgreen" alt="Tests" />
43
+ <img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT License" />
44
+ </p>
45
+
46
+ **任意进制(2-62)精确转换库** — 大整数无精度损失、小数用分数精确算术、循环小数用括号循环节表示并可无损往返。
47
+
48
+ ## 为什么用它
49
+
50
+ | 能力 | 内置 `int(s, base)` | 本库 |
51
+ |------|---------------------|------|
52
+ | 进制范围 | 2-36 | **2-62**(`0-9A-Za-z`,37 起大小写敏感) |
53
+ | 小数 | ✗ | ✓ **Fraction 精确**(二进制 `"0.1"` ≡ 1/2) |
54
+ | 循环小数 | ✗ | ✓ `1/3` ↔ `"0.(3)"` 无损往返 |
55
+ | 大整数 | ✓ | ✓(同一量级性能,见下) |
56
+ | CLI | ✗ | ✓ `baseconv` 一条命令 |
57
+
58
+ 浮点实现的转换在 `0.1` 这类值上必然引入二进制近似误差;本库全程使用 `int` 与 `fractions.Fraction`,**任何输入的转换结果都是数学上精确的**。
59
+
60
+ ## 安装
61
+
62
+ ```bash
63
+ git clone https://github.com/xiaomaozjj666/base-conversion.git
64
+ cd base-conversion
65
+ python -m pip install .
66
+ ```
67
+
68
+ > 尚未发布到 PyPI;从源码安装(零运行时依赖,仅标准库)。
69
+
70
+ ## 快速开始
71
+
72
+ ```python
73
+ >>> from fractions import Fraction
74
+ >>> from base_conversion import to_base, to_decimal
75
+ >>> to_base(255, 16)
76
+ 'FF'
77
+ >>> to_decimal("ff", 16) # 2-36 进制大小写不敏感
78
+ 255
79
+ >>> to_decimal("0.1", 2) # 精确:不是 0.5 的浮点近似
80
+ Fraction(1, 2)
81
+ >>> to_base(Fraction(1, 3), 10) # 循环小数 → 括号循环节
82
+ '0.(3)'
83
+ >>> to_decimal("0.(3)", 10) # 且可无损解析回来
84
+ Fraction(1, 3)
85
+ >>> to_base(10**100 + 7, 62) # 大整数无损
86
+ 'QCyvrY2MJnQFGlUHTCA95Xz8AHOrLuoIO0fuPkHHCcyXy9ytM5N1lqsh'
87
+ ```
88
+
89
+ 命令行:
90
+
91
+ ```bash
92
+ baseconv 255 10 16 # → FF
93
+ baseconv -- -FF 16 10 # → -255(位置参数负数用 -- 分隔)
94
+ baseconv -n -FF -f 16 -t 10 # → -255(-n 可直接接收负数值)
95
+ baseconv -n 0.1 -f 2 -t 10 # → 0.5(二进制小数精确转十进制)
96
+ ```
97
+
98
+ ## API
99
+
100
+ | 函数 | 签名 | 说明 |
101
+ |------|------|------|
102
+ | `to_decimal` | `(s: str, base: int) → int \| Fraction` | 解析 `base` 进制字符串;支持 `±`、小数点、括号循环节 |
103
+ | `to_base` | `(value: int \| Fraction \| float, base: int) → str` | 渲染为目标进制;非整数 float 拒绝(精度陷阱) |
104
+ | `decimal_to_base` / `base_to_decimal` | 同上 | 旧 API 兼容别名 |
105
+ | `decimal_to_binary` / `binary_to_decimal` | — | 二进制快捷方式 |
106
+ | `decimal_to_hex` / `hex_to_decimal` | — | 十六进制快捷方式(输出大写) |
107
+
108
+ 常量:`BASE_MIN=2`、`BASE_MAX=62`、`DIGITS`(62 字符表)。
109
+
110
+ ### 文法(`to_decimal` 接受的输入)
111
+
112
+ ```
113
+ [sign] digits [ "." digits ] [ "(" digits ")" ]
114
+ ```
115
+
116
+ - 2-36 进制大小写不敏感;37-62 大小写敏感(`'A'`=10、`'a'`=36);
117
+ - `".5"`、`"5."` 合法,单独的 `"."` 非法;
118
+ - 循环节括号必须收尾、非空、不嵌套:`"0.(3)"`、`"0.1(6)"` 合法,`"0.()"`、`"0.(3"` 非法。
119
+
120
+ ### 错误处理
121
+
122
+ - 非法字符 / 文法错误 → `ValueError`(信息含具体字符与进制);
123
+ - 进制越界(非 2-62)→ `ValueError`;非 int 进制或 `bool` → `TypeError`;
124
+ - `to_base` 收到非整数 `float` → `ValueError`(提示改用 `Fraction`;`to_base` 不接受数字字符串)。
125
+
126
+ ## 与内置的性能关系
127
+
128
+ 核心整数路径就是「除基取余 / 乘基累加」,与内置 `int(s, base)`、`format(n, 'x')` 同一数量级——测试含 500 例随机大整数交叉验证(对照内置 `int(s, base)`)与 300 例随机 Fraction 往返性质测试,10^60 量级毫秒级完成。本库的定位差异在**能力**(62 进制、精确小数、循环节)而非速度;极限吞吐场景请直接用内置函数。
129
+
130
+ ## 历史与归档
131
+
132
+ 本仓库早期是三语言(C++ / Python / JS)进制转换 + 查找排序基准的对比实现,2026-09 归档整理后:
133
+
134
+ - Python 部分重写为本库(2-62 进制、Fraction 精确小数、CLI、76 个测试);
135
+ - 三语言基准脚本保留在 [`archive/`](archive/)(历史快照,不再维护);
136
+ - 查找/排序算法练习与多语言题解迁移至 [programming-exercises](https://github.com/xiaomaozjj666/programming-exercises)。
137
+
138
+ 历史 benchmark 实测数据(2026-08-21 本机,三语言算法对比)见 [`docs/images/benchmark.svg`](docs/images/benchmark.svg)。
139
+
140
+ ## 开发
141
+
142
+ ```bash
143
+ python -m pip install -e ".[dev]"
144
+ pytest # 76 个测试(含随机交叉验证与往返性质测试)
145
+ ruff check src tests # 代码检查
146
+ mypy src # 类型检查
147
+ python -m build # 构建分发包
148
+ ```
149
+
150
+ CI 在 Python 3.9-3.13 矩阵上运行测试,并执行 lint、类型检查与构建验证。
151
+
152
+ 维护者发布流程见 [docs/RELEASING.md](docs/RELEASING.md),版本变更记录见 [CHANGELOG.md](CHANGELOG.md)。
153
+
154
+ ## License
155
+
156
+ [MIT](LICENSE)
@@ -0,0 +1,10 @@
1
+ base_conversion/__init__.py,sha256=D-liyHzNxMEBvm9pnocDWz2f7SaIli0jqxh8srWDFJ0,1094
2
+ base_conversion/cli.py,sha256=EyToV9URR_UF3jKWT7kjb0Y3e4imxorwQOHiosSo7wI,4720
3
+ base_conversion/converter.py,sha256=TMlJ9sqTh4Xz6eCtpMUOlM-s9UzYFj8QEwwRDIM27N0,8898
4
+ base_conversion/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ base_conversion-1.0.1.dist-info/licenses/LICENSE,sha256=tlEuX4--E18mJ6Ijcm9vwSIc7zMZJZFrT72JOq4cTIY,1070
6
+ base_conversion-1.0.1.dist-info/METADATA,sha256=0Z_MCEwAsaTox3Gm8QW2puqC4mruvnkWtTzTKgYIhE8,7143
7
+ base_conversion-1.0.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
8
+ base_conversion-1.0.1.dist-info/entry_points.txt,sha256=olksxALzkhyCxzumMLqQhaQbCelHFub7mo_Zvrm8Bdo,54
9
+ base_conversion-1.0.1.dist-info/top_level.txt,sha256=V2KYDEOWw4a_nNsTIbhjPf2_sGx4dcasGHj1uRmdvrQ,16
10
+ base_conversion-1.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ baseconv = base_conversion.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 xiaomaozjj666
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ base_conversion