通用单位换算器
长度、重量、温度、数据量等常见单位互转,离线可用、实时更新。
在线演示
—换算结果
—换算系数
—换算方式
等待操作:选择类别与源 / 目标单位,输入数值后点击“换算”。
项目简介
日常开发与写作里最常见的需求之一:把 16 寸换算成厘米、把 1.5TB 换算成 GB。本页给出一个因子表驱动的单位换算器:每种物理量维护一张换算因子表,输入数值即可在任意单位间实时换算,逻辑清晰、易于扩展。
代码示例:因子表驱动换算
js
const CATEGORIES = {
length: { base: "m",
units: { mm: 0.001, cm: 0.01, m: 1, km: 1000,
inch: 0.0254, ft: 0.3048, yd: 0.9144, mi: 1609.344 } },
weight: { base: "kg",
units: { mg: 1e-6, g: 0.001, kg: 1, t: 1000,
oz: 0.028349523, lb: 0.45359237 } },
data: { base: "byte",
units: { b: 1, kb: 1024, mb: 1024 ** 2, gb: 1024 ** 3,
tb: 1024 ** 4, pb: 1024 ** 5 } }
};
function convert(category, value, from, to) {
const { units } = CATEGORIES[category];
if (!(from in units) || !(to in units)) return null;
return value * units[from] / units[to]; // 统一先换算到基准单位
}
function format(n) {
if (!isFinite(n)) return "∞";
return Math.abs(n) >= 10000 || (Math.abs(n) < 0.001 && n !== 0)
? n.toExponential(4)
: String(parseFloat(n.toFixed(6)));
}代码示例:温度换算(非均匀公式)
js
// 温度换算不是线性因子,单独处理
const TEMP = {
c: { f: v => v * 9 / 5 + 32, k: v => v + 273.15 },
f: { c: v => (v - 32) * 5 / 9, k: v => (v - 32) * 5 / 9 + 273.15 },
k: { c: v => v - 273.15, f: v => (v - 273.15) * 9 / 5 + 32 }
};
function convertTemp(value, from, to) {
if (from === to) return value;
return TEMP[from][to](value);
}操作步骤
- 选择物理量(长度 / 重量 / 数据量 / 温度),输入数值并选择源单位与目标单位。
- 结果实时更新:线性量走因子表统一换算,温度走公式分支。
- 新增单位只需在
units表里加一行因子,无需改动换算逻辑。
完整可运行工程
可直接运行零第三方依赖Python 3.8+
下面是完整工程(非片段):保存为同一目录后即可用 python convert.py --value 100 --from km --to mile 直接换算。单位表外置为 units.json,新增单位无需改代码。
unit-converter/
├── convert.py # 换算核心 + CLI
├── units.json # 单位因子 / 仿射参数
├── test_convert.py # 最小用例回归
├── requirements.txt
└── README.md
├── convert.py # 换算核心 + CLI
├── units.json # 单位因子 / 仿射参数
├── test_convert.py # 最小用例回归
├── requirements.txt
└── README.md
units.json
单位表:线性类别给因子,温度给 scale / offset。可自行增删单位。
json
{
"length": {
"label": "长度",
"base": "m",
"units": {"mm": 0.001, "cm": 0.01, "m": 1, "km": 1000, "in": 0.0254, "ft": 0.3048, "yd": 0.9144, "mile": 1609.344, "nmi": 1852}
},
"mass": {
"label": "质量",
"base": "kg",
"units": {"mg": 1e-06, "g": 0.001, "kg": 1, "t": 1000, "oz": 0.028349523125, "lb": 0.45359237}
},
"data": {
"label": "数据量",
"base": "B",
"units": {"bit": 0.125, "B": 1, "KiB": 1024, "MiB": 1048576, "GiB": 1073741824, "TiB": 1099511627776, "KB": 1000, "MB": 1000000, "GB": 1000000000, "TB": 1000000000000}
},
"time": {
"label": "时间",
"base": "s",
"units": {"ms": 0.001, "s": 1, "min": 60, "h": 3600, "day": 86400, "week": 604800}
},
"temperature": {
"label": "温度",
"base": "C",
"affine": {"C": {"scale": 1, "offset": 0}, "F": {"scale": 0.5555555555555556, "offset": -32}, "K": {"scale": 1, "offset": -273.15}}
}
}
convert.py
换算核心与命令行入口;--selftest 内含 8 个断言用例。
python
#!/usr/bin/env python3
"""多类别单位换算 CLI(零第三方依赖,仅标准库)
用法示例:
python convert.py --value 100 --from km --to mile
python convert.py --value 100 --from C --to F --json
python convert.py --list length
python convert.py --table length --unit m
python convert.py --selftest
"""
import argparse
import json
import sys
from decimal import Decimal, getcontext
from pathlib import Path
getcontext().prec = 28
UNITS_FILE = Path(__file__).with_name("units.json")
def load_units(path=None):
"""读取单位表;缺失文件时给出明确提示而不是静默失败。"""
p = Path(path) if path else UNITS_FILE
if not p.exists():
raise SystemExit("未找到单位表 %s,请与 convert.py 放在同一目录。" % p)
return json.loads(p.read_text(encoding="utf-8"))
def find_category(data, unit):
for name, cat in data.items():
pool = cat.get("units") or cat.get("affine") or {}
if unit in pool:
return name, cat
raise KeyError("未知单位:%s(可用 --list <类别> 查看)" % unit)
def to_base(value, unit, cat):
v = Decimal(str(value))
if "affine" in cat:
a = cat["affine"][unit]
return (v + Decimal(str(a["offset"]))) * Decimal(str(a["scale"]))
return v * Decimal(str(cat["units"][unit]))
def from_base(value, unit, cat):
v = Decimal(value)
if "affine" in cat:
a = cat["affine"][unit]
return v / Decimal(str(a["scale"])) - Decimal(str(a["offset"]))
return v / Decimal(str(cat["units"][unit]))
def convert(value, src, dst, data=None):
"""返回 (结果, 类别名, 中间基准量)。跨类别换算直接报错,避免静默给出错误数字。"""
data = data or load_units()
cs, scat = find_category(data, src)
cd, dcat = find_category(data, dst)
if cs != cd:
raise ValueError("跨类别换算不支持:%s(%s) -> %s(%s)" % (src, cs, dst, cd))
base = to_base(value, src, scat)
return from_base(base, dst, scat), cs, base
def fmt(x):
"""12 位有效数字后去尾零,避免 0.6213711922373341 这类浮点噪声。"""
d = Decimal(x)
if d == 0:
return "0"
q = d.quantize(Decimal(1).scaleb(d.adjusted() - 11))
s = format(q, "f")
if "." in s:
s = s.rstrip("0").rstrip(".")
return s or "0"
def selftest():
cases = [
(0, "C", "F", 32), (100, "C", "F", 212), (0, "C", "K", 273.15),
(-40, "C", "F", -40), (1, "km", "m", 1000), (1, "mile", "km", 1.609344),
(1024, "MiB", "GiB", 1), (1, "day", "h", 24),
]
for v, s, d, want in cases:
got = float(convert(v, s, d)[0])
assert abs(got - want) < 1e-9, (v, s, d, got, want)
try:
convert(1, "km", "kg")
except ValueError:
pass
else:
raise AssertionError("跨类别换算应当报错")
print("selftest OK:%d 个用例全部通过(含仿射温度与非均匀因子)" % len(cases))
def main(argv=None):
ap = argparse.ArgumentParser(description="多类别单位换算(因子表 + 仿射公式)")
ap.add_argument("--value", type=float, help="待换算数值")
ap.add_argument("--from", dest="src", help="源单位,如 km / C / MiB")
ap.add_argument("--to", dest="dst", help="目标单位")
ap.add_argument("--list", dest="list_cat", help="列出某类别全部单位")
ap.add_argument("--table", dest="table_cat", help="生成以 --unit 为基准的换算表")
ap.add_argument("--unit", default="m", help="--table 的基准单位")
ap.add_argument("--json", action="store_true", help="以 JSON 输出")
ap.add_argument("--selftest", action="store_true", help="运行内置用例")
a = ap.parse_args(argv)
if a.selftest:
selftest()
return 0
data = load_units()
if a.list_cat:
cat = data[a.list_cat]
pool = cat.get("units") or cat.get("affine")
print("%s(基准单位 %s,共 %d 个): %s" % (cat["label"], cat["base"], len(pool), ", ".join(pool)))
return 0
if a.table_cat:
cat = data[a.table_cat]
pool = cat.get("units") or cat.get("affine")
print("以 1 %s 为基准的 %s 换算表:" % (a.unit, cat["label"]))
for k in pool:
print(" 1 %-6s = %s %s" % (a.unit, fmt(convert(1, a.unit, k, data)[0]), k))
return 0
if a.value is None or not a.src or not a.dst:
ap.error("请给出 --value / --from / --to,或使用 --list / --table / --selftest")
out, cat_name, base = convert(a.value, a.src, a.dst, data)
if a.json:
print(json.dumps({"value": a.value, "from": a.src, "to": a.dst, "result": float(out),
"category": cat_name, "base_value": float(base), "base_unit": data[cat_name]["base"]},
ensure_ascii=False))
else:
print("%s %s = %s %s (%s 类别,中间量 %s %s)" % (fmt(a.value), a.src, fmt(out), a.dst,
data[cat_name]["label"], fmt(base), data[cat_name]["base"]))
return 0
if __name__ == "__main__":
sys.exit(main())
test_convert.py
unittest 回归:温度仿射、长度因子、二进制 / 十进制数据量、跨类别报错。
python
"""convert.py 的最小用例回归(标准库 unittest,零依赖)"""
import unittest
from convert import convert, fmt, load_units
class TestConvert(unittest.TestCase):
def test_temperature_affine(self):
self.assertAlmostEqual(float(convert(0, "C", "F")[0]), 32.0, places=9)
self.assertAlmostEqual(float(convert(100, "C", "F")[0]), 212.0, places=9)
self.assertAlmostEqual(float(convert(0, "C", "K")[0]), 273.15, places=9)
self.assertAlmostEqual(float(convert(-40, "C", "F")[0]), -40.0, places=9)
def test_length_factors(self):
self.assertAlmostEqual(float(convert(1, "km", "m")[0]), 1000.0, places=9)
self.assertAlmostEqual(float(convert(1, "mile", "km")[0]), 1.609344, places=9)
def test_data_binary_vs_decimal(self):
self.assertAlmostEqual(float(convert(1024, "MiB", "GiB")[0]), 1.0, places=9)
self.assertAlmostEqual(float(convert(1000, "MB", "GB")[0]), 1.0, places=9)
def test_cross_category_raises(self):
with self.assertRaises(ValueError):
convert(1, "km", "kg")
def test_units_file_has_all_categories(self):
data = load_units()
for name in ("length", "mass", "data", "time", "temperature"):
self.assertIn(name, data)
def test_fmt_trims_noise(self):
self.assertEqual(fmt(0.5), "0.5")
self.assertEqual(fmt(0), "0")
if __name__ == "__main__":
unittest.main(verbosity=2)
requirements.txt
依赖清单:本工程仅标准库,无需安装第三方包。
text
# 本工程仅使用 Python 标准库(argparse / decimal / json / pathlib / unittest),
# 不需要安装任何第三方包。此文件保留是为了让 `pip install -r requirements.txt` 流程统一。
README.md
安装、运行、扩展单位与精度策略说明。
markdown
# 单位换算 CLI(unit-converter)
因子表驱动的多类别单位换算:长度 / 质量 / 数据量 / 时间走「线性因子」,温度走「仿射公式」,
两类算法在 `convert.py` 中统一成 `to_base()` / `from_base()`,因此新增单位只需改 `units.json`。
## 文件说明
| 文件 | 作用 |
|---|---|
| `convert.py` | 换算核心 + 命令行入口(仅标准库) |
| `units.json` | 单位表:线性类别给因子,温度类别给 `scale` / `offset` |
| `test_convert.py` | unittest 最小用例回归 |
| `requirements.txt` | 依赖清单(本工程无第三方依赖) |
## 运行
```bash
python -m pip install -r requirements.txt # 无第三方依赖,可跳过
python convert.py --value 100 --from km --to mile
python convert.py --value 100 --from C --to F --json
python convert.py --list length
python convert.py --table length --unit m
python convert.py --selftest
python test_convert.py
```
## 扩展单位
在 `units.json` 对应类别里加一行即可,例如加「光年」到长度类别:
```json
"ly": 9460730472580800
```
因子含义是「1 个该单位等于多少个基准单位」。温度类单位则加 `{"scale": …, "offset": …}`,
含义为 `基准 = (值 + offset) × scale`。
## 精度策略
内部使用 `decimal.Decimal`(28 位精度)而不是二进制浮点,输出前统一压到 12 位有效数字并去掉尾零,
所以 `1 mile → km` 得到的是 `1.609344` 而不是 `1.6093440000000002`。
运行标注
- 可直接运行:把上面 5 个文件保存到同一目录,执行
python convert.py --value 100 --from km --to mile即可得到结果。 - 无需改任何配置项:默认单位表已覆盖长度 / 质量 / 数据量 / 时间 / 温度五类;只有要增删单位时才编辑
units.json。 - 自测:
python test_convert.py(等价于python convert.py --selftest),全部用例应通过。
来源参考
实现与部署所依据的官方文档 / 仓库(均可直接访问):