Skip to main content

ek_data/
price.rs

1//! 价格与小数工具(`utility.py:275-318` 的忠实移植)。
2//!
3//! Python 版用 `Decimal(str(value))` 做**精确十进制**运算,不能用朴素 f64
4//! 替代:经典反例 `floor(0.3 / 0.1)` 在 f64 下是 `2`(0.3/0.1=2.9999…),
5//! 而 Python 版给出 `0.3`。本模块以「十进制重表 → i128 整数比」复刻该语义。
6//!
7//! 十进制来源:Python `str(float)` 与 Rust `Debug for f64` 都是**最短往返**
8//! 表示(`0.1` → `"0.1"`,`100.0` → `"100.0"`,`1e-5` → `"1e-5"`),故可从
9//! 两者的字符串形态拿到同一组十进制数字。
10//!
11//! 舍入规则对齐 Python:
12//! - [`round_to`] 用 **half-even**(Python `round(Decimal)` 默认 `ROUND_HALF_EVEN`)
13//! - [`floor_to`] 向 `-∞`、[`ceil_to`] 向 `+∞`(Python `math.floor/ceil`)
14//!
15//! value 与 target 各自只解析一次( Mantissa×10^-scale 形式直进整数比);
16//! target 的符号折叠进比值分子,保证分母恒正,平局/方向语义统一。
17
18/// Python `str(float)` 的等价形态:最短往返十进制文本。
19fn repr_of(value: f64) -> String {
20    format!("{value:?}")
21}
22
23/// 十进制文本 → `(mantissa, scale)`,表示 `mantissa × 10^-scale`(符号折入 mantissa)。
24///
25/// 支持 `"123.45"` / `"-0.5"` / `"1e-5"` / `"100.0"`;拒绝非有限值与超范围。
26fn parse_decimal(s: &str) -> Option<(i128, u32)> {
27    let s = s.trim();
28    if s.is_empty() || s.eq_ignore_ascii_case("nan") || s.contains("inf") {
29        return None;
30    }
31    let (neg, body) = match s.strip_prefix('-') {
32        Some(rest) => (true, rest),
33        None => (false, s.strip_prefix('+').unwrap_or(s)),
34    };
35    let (mantissa_str, exp) = match body.split_once(['e', 'E']) {
36        Some((m, e)) => (m, e.parse::<i32>().ok()?),
37        None => (body, 0),
38    };
39    let (int_str, frac_str) = match mantissa_str.split_once('.') {
40        Some((i, f)) => (i, f),
41        None => (mantissa_str, ""),
42    };
43    if int_str.is_empty() && frac_str.is_empty() {
44        return None;
45    }
46    if !int_str.bytes().all(|b| b.is_ascii_digit()) || !frac_str.bytes().all(|b| b.is_ascii_digit())
47    {
48        return None;
49    }
50    let digits: String = format!("{int_str}{frac_str}");
51    let mut mant = digits.parse::<i128>().ok()?;
52    if neg {
53        mant = -mant;
54    }
55    let mut scale = frac_str.len() as i32 - exp;
56    if scale < 0 {
57        mant = mant.checked_mul(10i128.checked_pow((-scale) as u32)?)?;
58        scale = 0;
59    }
60    Some((mant, scale as u32))
61}
62
63/// 十进制操作数对 `(mantissa, scale)`(target 已保证非零)。
64type DecParts = (i128, u32);
65
66/// `value / target` 的精确整数比 `(num, den)`(den 恒 > 0;target 符号已折入 num)。
67fn ratio_parts(value: DecParts, target: DecParts) -> Option<(i128, i128)> {
68    let (mut num, sv) = value;
69    let (mut den, st) = target;
70    debug_assert!(den != 0);
71    if den < 0 {
72        num = num.checked_neg()?;
73        den = den.checked_neg()?;
74    }
75    // (mv·10^-sv) / (mt·10^-st) = mv/mt · 10^(st-sv)
76    let p = st as i32 - sv as i32;
77    if p >= 0 {
78        num = num.checked_mul(10i128.checked_pow(p as u32)?)?;
79    } else {
80        den = den.checked_mul(10i128.checked_pow((-p) as u32)?)?;
81    }
82    Some((num, den))
83}
84
85/// `round_half_even(num / den)`(den > 0)。
86fn div_round_half_even(num: i128, den: i128) -> i128 {
87    let q = num.div_euclid(den);
88    let r = num.rem_euclid(den);
89    match (r * 2).cmp(&den) {
90        std::cmp::Ordering::Greater => q + 1,
91        std::cmp::Ordering::Less => q,
92        std::cmp::Ordering::Equal => {
93            if q % 2 == 0 {
94                q
95            } else {
96                q + 1
97            }
98        }
99    }
100}
101
102/// `floor(num / den)`(den > 0)。
103fn div_floor(num: i128, den: i128) -> i128 {
104    num.div_euclid(den)
105}
106
107/// `ceil(num / den)`(den > 0)。
108fn div_ceil(num: i128, den: i128) -> i128 {
109    -((-num).div_euclid(den))
110}
111
112/// `q × target` 还原为 f64(目标以 `(mantissa, scale)` 表达)。
113fn combine(q: i128, target: DecParts) -> Option<f64> {
114    let (mt, st) = target;
115    let prod = q.checked_mul(mt)?;
116    Some(prod as f64 / 10f64.powi(st as i32))
117}
118
119fn target_parts(target: f64) -> Option<DecParts> {
120    let parts = parse_decimal(&repr_of(target))?;
121    (parts.0 != 0).then_some(parts)
122}
123
124/// 取整到 `target` 的最近整数倍,平局取**偶**(Python `round_to`,utility.py:275)。
125///
126/// 例:`round_to(2.5, 1.0) == 2.0`(不是 3.0);`round_to(3.5, 1.0) == 4.0`。
127pub fn round_to(value: f64, target: f64) -> f64 {
128    try_round_to(value, target).unwrap_or(f64::NAN)
129}
130
131/// [`round_to`] 的可失败版本;输入非有限 / target 为 0 / 溢出时返回 `None`。
132pub fn try_round_to(value: f64, target: f64) -> Option<f64> {
133    let t = target_parts(target)?;
134    let v = parse_decimal(&repr_of(value))?;
135    let (num, den) = ratio_parts(v, t)?;
136    combine(div_round_half_even(num, den), t)
137}
138
139/// 向 `-∞` 取整到 `target` 的整数倍(Python `floor_to`,utility.py:285)。
140pub fn floor_to(value: f64, target: f64) -> f64 {
141    try_floor_to(value, target).unwrap_or(f64::NAN)
142}
143
144/// [`floor_to`] 的可失败版本。
145pub fn try_floor_to(value: f64, target: f64) -> Option<f64> {
146    let t = target_parts(target)?;
147    let v = parse_decimal(&repr_of(value))?;
148    let (num, den) = ratio_parts(v, t)?;
149    combine(div_floor(num, den), t)
150}
151
152/// 向 `+∞` 取整到 `target` 的整数倍(Python `ceil_to`,utility.py:295)。
153pub fn ceil_to(value: f64, target: f64) -> f64 {
154    try_ceil_to(value, target).unwrap_or(f64::NAN)
155}
156
157/// [`ceil_to`] 的可失败版本。
158pub fn try_ceil_to(value: f64, target: f64) -> Option<f64> {
159    let t = target_parts(target)?;
160    let v = parse_decimal(&repr_of(value))?;
161    let (num, den) = ratio_parts(v, t)?;
162    combine(div_ceil(num, den), t)
163}
164
165/// 小数位数(Python `get_digits`,utility.py:305)。
166///
167/// 语义按 Python `str(float)` 计:整数形浮点(`100.0`)算 1 位,
168/// 科学计数法(`1e-5`)取指数,`1e16` 之类正指数形算 0 位。
169pub fn get_digits(value: f64) -> i32 {
170    let s = repr_of(value);
171    if let Some((_, buf)) = s.split_once("e-") {
172        buf.parse().unwrap_or(0)
173    } else if let Some((_, buf)) = s.split_once('.') {
174        buf.len() as i32
175    } else {
176        0
177    }
178}