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        return None;
48    }
49    let digits: String = format!("{int_str}{frac_str}");
50    let mut mant = digits.parse::<i128>().ok()?;
51    if neg {
52        mant = -mant;
53    }
54    let mut scale = frac_str.len() as i32 - exp;
55    if scale < 0 {
56        mant = mant.checked_mul(10i128.checked_pow((-scale) as u32)?)?;
57        scale = 0;
58    }
59    Some((mant, scale as u32))
60}
61
62/// 十进制操作数对 `(mantissa, scale)`(target 已保证非零)。
63type DecParts = (i128, u32);
64
65/// `value / target` 的精确整数比 `(num, den)`(den 恒 > 0;target 符号已折入 num)。
66fn ratio_parts(value: DecParts, target: DecParts) -> Option<(i128, i128)> {
67    let (mut num, sv) = value;
68    let (mut den, st) = target;
69    debug_assert!(den != 0);
70    if den < 0 {
71        num = num.checked_neg()?;
72        den = den.checked_neg()?;
73    }
74    // (mv·10^-sv) / (mt·10^-st) = mv/mt · 10^(st-sv)
75    let p = st as i32 - sv as i32;
76    if p >= 0 {
77        num = num.checked_mul(10i128.checked_pow(p as u32)?)?;
78    } else {
79        den = den.checked_mul(10i128.checked_pow((-p) as u32)?)?;
80    }
81    Some((num, den))
82}
83
84/// `round_half_even(num / den)`(den > 0)。
85fn div_round_half_even(num: i128, den: i128) -> i128 {
86    let q = num.div_euclid(den);
87    let r = num.rem_euclid(den);
88    match (r * 2).cmp(&den) {
89        std::cmp::Ordering::Greater => q + 1,
90        std::cmp::Ordering::Less => q,
91        std::cmp::Ordering::Equal => {
92            if q % 2 == 0 {
93                q
94            } else {
95                q + 1
96            }
97        }
98    }
99}
100
101/// `floor(num / den)`(den > 0)。
102fn div_floor(num: i128, den: i128) -> i128 {
103    num.div_euclid(den)
104}
105
106/// `ceil(num / den)`(den > 0)。
107fn div_ceil(num: i128, den: i128) -> i128 {
108    -((-num).div_euclid(den))
109}
110
111/// `q × target` 还原为 f64(目标以 `(mantissa, scale)` 表达)。
112fn combine(q: i128, target: DecParts) -> Option<f64> {
113    let (mt, st) = target;
114    let prod = q.checked_mul(mt)?;
115    Some(prod as f64 / 10f64.powi(st as i32))
116}
117
118fn target_parts(target: f64) -> Option<DecParts> {
119    let parts = parse_decimal(&repr_of(target))?;
120    (parts.0 != 0).then_some(parts)
121}
122
123/// 取整到 `target` 的最近整数倍,平局取**偶**(Python `round_to`,utility.py:275)。
124///
125/// 例:`round_to(2.5, 1.0) == 2.0`(不是 3.0);`round_to(3.5, 1.0) == 4.0`。
126pub fn round_to(value: f64, target: f64) -> f64 {
127    try_round_to(value, target).unwrap_or(f64::NAN)
128}
129
130/// [`round_to`] 的可失败版本;输入非有限 / target 为 0 / 溢出时返回 `None`。
131pub fn try_round_to(value: f64, target: f64) -> Option<f64> {
132    let t = target_parts(target)?;
133    let v = parse_decimal(&repr_of(value))?;
134    let (num, den) = ratio_parts(v, t)?;
135    combine(div_round_half_even(num, den), t)
136}
137
138/// 向 `-∞` 取整到 `target` 的整数倍(Python `floor_to`,utility.py:285)。
139pub fn floor_to(value: f64, target: f64) -> f64 {
140    try_floor_to(value, target).unwrap_or(f64::NAN)
141}
142
143/// [`floor_to`] 的可失败版本。
144pub fn try_floor_to(value: f64, target: f64) -> Option<f64> {
145    let t = target_parts(target)?;
146    let v = parse_decimal(&repr_of(value))?;
147    let (num, den) = ratio_parts(v, t)?;
148    combine(div_floor(num, den), t)
149}
150
151/// 向 `+∞` 取整到 `target` 的整数倍(Python `ceil_to`,utility.py:295)。
152pub fn ceil_to(value: f64, target: f64) -> f64 {
153    try_ceil_to(value, target).unwrap_or(f64::NAN)
154}
155
156/// [`ceil_to`] 的可失败版本。
157pub fn try_ceil_to(value: f64, target: f64) -> Option<f64> {
158    let t = target_parts(target)?;
159    let v = parse_decimal(&repr_of(value))?;
160    let (num, den) = ratio_parts(v, t)?;
161    combine(div_ceil(num, den), t)
162}
163
164/// 小数位数(Python `get_digits`,utility.py:305)。
165///
166/// 语义按 Python `str(float)` 计:整数形浮点(`100.0`)算 1 位,
167/// 科学计数法(`1e-5`)取指数,`1e16` 之类正指数形算 0 位。
168pub fn get_digits(value: f64) -> i32 {
169    let s = repr_of(value);
170    if let Some((_, buf)) = s.split_once("e-") {
171        buf.parse().unwrap_or(0)
172    } else if let Some((_, buf)) = s.split_once('.') {
173        buf.len() as i32
174    } else {
175        0
176    }
177}