ccxt_core/
precision.rs

1//! Financial-grade precision calculation utilities.
2//!
3//! Provides high-precision numerical operations for financial calculations:
4//! - Rounding modes (Round, RoundUp, RoundDown)
5//! - Precision counting modes (DecimalPlaces, SignificantDigits, TickSize)
6//! - Number formatting and parsing
7//! - `Decimal` type helper functions
8
9use rust_decimal::Decimal;
10use rust_decimal::prelude::*;
11use std::str::FromStr;
12
13use crate::error::{Error, ParseError, Result};
14
15/// Rounding mode for precision calculations.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum RoundingMode {
18    /// Standard rounding (rounds to nearest, ties away from zero)
19    Round = 1,
20    /// Round up (away from zero)
21    RoundUp = 2,
22    /// Round down (toward zero, truncate)
23    RoundDown = 3,
24}
25
26/// Precision counting mode.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum CountingMode {
29    /// Count decimal places
30    DecimalPlaces = 2,
31    /// Count significant digits
32    SignificantDigits = 3,
33    /// Use minimum price tick size
34    TickSize = 4,
35}
36
37/// Output padding mode.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum PaddingMode {
40    /// No padding (remove trailing zeros)
41    NoPadding = 5,
42    /// Pad with zeros to precision
43    PadWithZero = 6,
44}
45
46/// Converts a decimal number to string, handling scientific notation.
47///
48/// # Examples
49///
50/// ```
51/// use ccxt_core::precision::number_to_string;
52/// use rust_decimal::Decimal;
53/// use std::str::FromStr;
54///
55/// let num = Decimal::from_str("0.00000123").unwrap();
56/// assert_eq!(number_to_string(num), "0.00000123");
57///
58/// let large = Decimal::from_str("1234567890").unwrap();
59/// assert_eq!(number_to_string(large), "1234567890");
60/// ```
61pub fn number_to_string(value: Decimal) -> String {
62    let s = value.to_string();
63
64    if s.contains('.') {
65        s.trim_end_matches('0').trim_end_matches('.').to_string()
66    } else {
67        s
68    }
69}
70
71/// Extracts precision (number of decimal places) from a string representation.
72///
73/// # Examples
74///
75/// ```
76/// use ccxt_core::precision::precision_from_string;
77///
78/// assert_eq!(precision_from_string("0.001"), 3);
79/// assert_eq!(precision_from_string("0.01"), 2);
80/// assert_eq!(precision_from_string("1.2345"), 4);
81/// assert_eq!(precision_from_string("100"), 0);
82/// assert_eq!(precision_from_string("1e-8"), 8);
83/// ```
84pub fn precision_from_string(s: &str) -> i32 {
85    if s.contains('e') || s.contains('E') {
86        if let Some(e_pos) = s.find(|c| c == 'e' || c == 'E') {
87            let exp_str = &s[e_pos + 1..];
88            if let Ok(exp) = exp_str.parse::<i32>() {
89                return -exp;
90            }
91        }
92    }
93
94    let trimmed = s.trim_end_matches('0');
95    if let Some(dot_pos) = trimmed.find('.') {
96        (trimmed.len() - dot_pos - 1) as i32
97    } else {
98        0
99    }
100}
101
102/// Truncates a decimal to the specified number of decimal places without rounding.
103///
104/// # Examples
105///
106/// ```
107/// use ccxt_core::precision::truncate_to_string;
108/// use rust_decimal::Decimal;
109/// use std::str::FromStr;
110///
111/// let num = Decimal::from_str("123.456789").unwrap();
112/// assert_eq!(truncate_to_string(num, 2), "123.45");
113/// assert_eq!(truncate_to_string(num, 4), "123.4567");
114/// assert_eq!(truncate_to_string(num, 0), "123");
115/// ```
116pub fn truncate_to_string(value: Decimal, precision: i32) -> String {
117    if precision <= 0 {
118        return value.trunc().to_string();
119    }
120
121    let s = value.to_string();
122
123    if let Some(dot_pos) = s.find('.') {
124        let end_pos = std::cmp::min(dot_pos + 1 + precision as usize, s.len());
125        s[..end_pos].to_string()
126    } else {
127        s
128    }
129}
130
131/// Truncates a decimal to the specified number of decimal places, returning a `Decimal`.
132///
133/// # Examples
134///
135/// ```
136/// use ccxt_core::precision::truncate;
137/// use rust_decimal::Decimal;
138/// use std::str::FromStr;
139///
140/// let num = Decimal::from_str("123.456789").unwrap();
141/// let truncated = truncate(num, 2);
142/// assert_eq!(truncated.to_string(), "123.45");
143/// ```
144pub fn truncate(value: Decimal, precision: i32) -> Decimal {
145    let s = truncate_to_string(value, precision);
146    Decimal::from_str(&s).unwrap_or(value)
147}
148
149/// Formats a decimal to a specific precision with configurable rounding and counting modes.
150///
151/// Core precision formatting function equivalent to Go's `DecimalToPrecision` method.
152///
153/// # Arguments
154///
155/// * `value` - The decimal value to format
156/// * `rounding_mode` - Rounding behavior to apply
157/// * `num_precision_digits` - Number of precision digits
158/// * `counting_mode` - How to count precision (default: `DecimalPlaces`)
159/// * `padding_mode` - Output padding behavior (default: `NoPadding`)
160///
161/// # Examples
162///
163/// ```
164/// use ccxt_core::precision::{decimal_to_precision, RoundingMode, CountingMode, PaddingMode};
165/// use rust_decimal::Decimal;
166/// use std::str::FromStr;
167///
168/// let num = Decimal::from_str("123.456789").unwrap();
169///
170/// // 保留2位小数,四舍五入
171/// let result = decimal_to_precision(
172///     num,
173///     RoundingMode::Round,
174///     2,
175///     Some(CountingMode::DecimalPlaces),
176///     Some(PaddingMode::NoPadding),
177/// ).unwrap();
178/// assert_eq!(result, "123.46");
179///
180/// // 截断到2位小数
181/// let result = decimal_to_precision(
182///     num,
183///     RoundingMode::RoundDown,
184///     2,
185///     Some(CountingMode::DecimalPlaces),
186///     Some(PaddingMode::NoPadding),
187/// ).unwrap();
188/// assert_eq!(result, "123.45");
189/// ```
190pub fn decimal_to_precision(
191    value: Decimal,
192    rounding_mode: RoundingMode,
193    num_precision_digits: i32,
194    counting_mode: Option<CountingMode>,
195    padding_mode: Option<PaddingMode>,
196) -> Result<String> {
197    let counting_mode = counting_mode.unwrap_or(CountingMode::DecimalPlaces);
198    let padding_mode = padding_mode.unwrap_or(PaddingMode::NoPadding);
199
200    // Handle negative precision (round to powers of 10)
201    if num_precision_digits < 0 {
202        let to_nearest =
203            Decimal::from_i128_with_scale(10_i128.pow(num_precision_digits.unsigned_abs()), 0);
204
205        match rounding_mode {
206            RoundingMode::Round => {
207                let divided = value / to_nearest;
208                let rounded = round_decimal(divided, 0, RoundingMode::Round)?;
209                let result = to_nearest * rounded;
210                return Ok(format_decimal(result, padding_mode));
211            }
212            RoundingMode::RoundDown => {
213                let modulo = value % to_nearest;
214                let result = value - modulo;
215                return Ok(format_decimal(result, padding_mode));
216            }
217            RoundingMode::RoundUp => {
218                let modulo = value % to_nearest;
219                let result = if modulo.is_zero() {
220                    value
221                } else {
222                    value - modulo + to_nearest
223                };
224                return Ok(format_decimal(result, padding_mode));
225            }
226        }
227    }
228
229    match counting_mode {
230        CountingMode::DecimalPlaces => decimal_to_precision_decimal_places(
231            value,
232            rounding_mode,
233            num_precision_digits,
234            padding_mode,
235        ),
236        CountingMode::SignificantDigits => decimal_to_precision_significant_digits(
237            value,
238            rounding_mode,
239            num_precision_digits,
240            padding_mode,
241        ),
242        CountingMode::TickSize => {
243            let tick_size = Decimal::from_str(&format!(
244                "0.{}",
245                "0".repeat((num_precision_digits - 1) as usize) + "1"
246            ))
247            .map_err(|e| {
248                ParseError::invalid_format("tick_size", format!("Invalid tick size: {}", e))
249            })?;
250
251            decimal_to_precision_tick_size(value, rounding_mode, tick_size, padding_mode)
252        }
253    }
254}
255
256/// Formats decimal by counting decimal places.
257fn decimal_to_precision_decimal_places(
258    value: Decimal,
259    rounding_mode: RoundingMode,
260    decimal_places: i32,
261    padding_mode: PaddingMode,
262) -> Result<String> {
263    let rounded = round_decimal(value, decimal_places, rounding_mode)?;
264    Ok(format_decimal_with_places(
265        rounded,
266        decimal_places,
267        padding_mode,
268    ))
269}
270
271/// Formats decimal by counting significant digits.
272fn decimal_to_precision_significant_digits(
273    value: Decimal,
274    rounding_mode: RoundingMode,
275    sig_digits: i32,
276    padding_mode: PaddingMode,
277) -> Result<String> {
278    if value.is_zero() {
279        return Ok("0".to_string());
280    }
281
282    let abs_value = value.abs();
283    let value_f64 = abs_value.to_f64().unwrap_or(0.0);
284    let log10 = value_f64.log10();
285    let magnitude = log10.floor() as i32;
286
287    let decimal_places = sig_digits - magnitude - 1;
288
289    let rounded = round_decimal(value, decimal_places, rounding_mode)?;
290    Ok(format_decimal_with_places(
291        rounded,
292        decimal_places,
293        padding_mode,
294    ))
295}
296
297/// Formats decimal to align with tick size increments.
298fn decimal_to_precision_tick_size(
299    value: Decimal,
300    rounding_mode: RoundingMode,
301    tick_size: Decimal,
302    padding_mode: PaddingMode,
303) -> Result<String> {
304    if tick_size <= Decimal::ZERO {
305        return Err(Error::invalid_request("Tick size must be positive"));
306    }
307
308    let ticks = match rounding_mode {
309        RoundingMode::Round => (value / tick_size).round(),
310        RoundingMode::RoundDown => (value / tick_size).floor(),
311        RoundingMode::RoundUp => (value / tick_size).ceil(),
312    };
313
314    let result = ticks * tick_size;
315
316    let tick_precision = precision_from_string(&tick_size.to_string());
317    Ok(format_decimal_with_places(
318        result,
319        tick_precision,
320        padding_mode,
321    ))
322}
323
324/// Rounds a decimal value to the specified number of decimal places.
325fn round_decimal(value: Decimal, decimal_places: i32, mode: RoundingMode) -> Result<Decimal> {
326    if decimal_places < 0 {
327        let scale = Decimal::from_i128_with_scale(10_i128.pow(decimal_places.unsigned_abs()), 0);
328        let divided = value / scale;
329        let rounded = match mode {
330            RoundingMode::Round => divided.round(),
331            RoundingMode::RoundDown => divided.floor(),
332            RoundingMode::RoundUp => divided.ceil(),
333        };
334        return Ok(rounded * scale);
335    }
336
337    let scale = Decimal::from_i128_with_scale(10_i128.pow(decimal_places as u32), 0);
338    let scaled = value * scale;
339
340    let rounded = match mode {
341        RoundingMode::Round => scaled.round(),
342        RoundingMode::RoundDown => scaled.floor(),
343        RoundingMode::RoundUp => scaled.ceil(),
344    };
345
346    Ok(rounded / scale)
347}
348
349/// Formats decimal by removing trailing zeros.
350fn format_decimal(value: Decimal, _padding_mode: PaddingMode) -> String {
351    let s = value.to_string();
352
353    if s.contains('.') {
354        s.trim_end_matches('0').trim_end_matches('.').to_string()
355    } else {
356        s
357    }
358}
359
360/// Formats decimal to the specified number of decimal places.
361fn format_decimal_with_places(
362    value: Decimal,
363    decimal_places: i32,
364    padding_mode: PaddingMode,
365) -> String {
366    match padding_mode {
367        PaddingMode::NoPadding => format_decimal(value, padding_mode),
368        PaddingMode::PadWithZero => {
369            if decimal_places > 0 {
370                format!("{:.prec$}", value, prec = decimal_places as usize)
371            } else {
372                value.trunc().to_string()
373            }
374        }
375    }
376}
377
378#[cfg(test)]
379mod tests {
380    use super::*;
381
382    #[test]
383    fn test_number_to_string() {
384        let num = Decimal::from_str("0.00000123").unwrap();
385        assert_eq!(number_to_string(num), "0.00000123");
386
387        let large = Decimal::from_str("1234567890").unwrap();
388        assert_eq!(number_to_string(large), "1234567890");
389
390        let with_trailing = Decimal::from_str("123.4500").unwrap();
391        assert_eq!(number_to_string(with_trailing), "123.45");
392    }
393
394    #[test]
395    fn test_precision_from_string() {
396        assert_eq!(precision_from_string("0.001"), 3);
397        assert_eq!(precision_from_string("0.01"), 2);
398        assert_eq!(precision_from_string("1.2345"), 4);
399        assert_eq!(precision_from_string("100"), 0);
400        assert_eq!(precision_from_string("1.0000"), 0);
401    }
402
403    #[test]
404    fn test_truncate() {
405        let num = Decimal::from_str("123.456789").unwrap();
406
407        assert_eq!(truncate_to_string(num, 2), "123.45");
408        assert_eq!(truncate_to_string(num, 4), "123.4567");
409        assert_eq!(truncate_to_string(num, 0), "123");
410
411        let truncated = truncate(num, 2);
412        assert_eq!(truncated.to_string(), "123.45");
413    }
414
415    #[test]
416    fn test_decimal_to_precision_round() {
417        let num = Decimal::from_str("123.456").unwrap();
418
419        let result = decimal_to_precision(
420            num,
421            RoundingMode::Round,
422            2,
423            Some(CountingMode::DecimalPlaces),
424            Some(PaddingMode::NoPadding),
425        )
426        .unwrap();
427        assert_eq!(result, "123.46");
428    }
429
430    #[test]
431    fn test_decimal_to_precision_round_down() {
432        let num = Decimal::from_str("123.456").unwrap();
433
434        let result = decimal_to_precision(
435            num,
436            RoundingMode::RoundDown,
437            2,
438            Some(CountingMode::DecimalPlaces),
439            Some(PaddingMode::NoPadding),
440        )
441        .unwrap();
442        assert_eq!(result, "123.45");
443    }
444
445    #[test]
446    fn test_decimal_to_precision_round_up() {
447        let num = Decimal::from_str("123.451").unwrap();
448
449        let result = decimal_to_precision(
450            num,
451            RoundingMode::RoundUp,
452            2,
453            Some(CountingMode::DecimalPlaces),
454            Some(PaddingMode::NoPadding),
455        )
456        .unwrap();
457        assert_eq!(result, "123.46");
458    }
459
460    #[test]
461    fn test_decimal_to_precision_with_padding() {
462        let num = Decimal::from_str("123.4").unwrap();
463
464        let result = decimal_to_precision(
465            num,
466            RoundingMode::Round,
467            3,
468            Some(CountingMode::DecimalPlaces),
469            Some(PaddingMode::PadWithZero),
470        )
471        .unwrap();
472        assert_eq!(result, "123.400");
473    }
474
475    #[test]
476    fn test_decimal_to_precision_negative_precision() {
477        let num = Decimal::from_str("123.456").unwrap();
478
479        let result = decimal_to_precision(
480            num,
481            RoundingMode::Round,
482            -1,
483            Some(CountingMode::DecimalPlaces),
484            Some(PaddingMode::NoPadding),
485        )
486        .unwrap();
487        assert_eq!(result, "120");
488    }
489
490    #[test]
491    fn test_decimal_to_precision_tick_size() {
492        let num = Decimal::from_str("123.456").unwrap();
493        let tick = Decimal::from_str("0.05").unwrap();
494
495        let result =
496            decimal_to_precision_tick_size(num, RoundingMode::Round, tick, PaddingMode::NoPadding)
497                .unwrap();
498
499        // 123.456 / 0.05 = 2469.12, round = 2469, * 0.05 = 123.45
500        assert_eq!(result, "123.45");
501    }
502
503    #[test]
504    fn test_round_decimal() {
505        let num = Decimal::from_str("123.456").unwrap();
506
507        let rounded = round_decimal(num, 2, RoundingMode::Round).unwrap();
508        assert_eq!(rounded.to_string(), "123.46");
509
510        let truncated = round_decimal(num, 2, RoundingMode::RoundDown).unwrap();
511        assert_eq!(truncated.to_string(), "123.45");
512    }
513
514    #[test]
515    fn test_format_decimal() {
516        let num = Decimal::from_str("123.4500").unwrap();
517        let formatted = format_decimal(num, PaddingMode::NoPadding);
518        assert_eq!(formatted, "123.45");
519
520        let formatted_padded = format_decimal_with_places(num, 4, PaddingMode::PadWithZero);
521        assert_eq!(formatted_padded, "123.4500");
522    }
523}