1use rust_decimal::Decimal;
10use rust_decimal::prelude::ToPrimitive;
11use std::str::FromStr;
12
13use crate::error::{Error, ParseError, Result};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum RoundingMode {
18 Round = 1,
20 RoundUp = 2,
22 RoundDown = 3,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum CountingMode {
29 DecimalPlaces = 2,
31 SignificantDigits = 3,
33 TickSize = 4,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum PaddingMode {
40 NoPadding = 5,
42 PadWithZero = 6,
44}
45
46pub 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
71pub fn precision_from_string(s: &str) -> i32 {
85 if (s.contains('e') || s.contains('E'))
86 && let Some(e_pos) = s.find(['e', 'E'])
87 {
88 let exp_str = &s[e_pos + 1..];
89 if let Ok(exp) = exp_str.parse::<i32>() {
90 return -exp;
91 }
92 }
93
94 let trimmed = s.trim_end_matches('0');
95 if let Some(dot_pos) = trimmed.find('.') {
96 #[allow(clippy::cast_possible_truncation)]
97 let res = (trimmed.len() - dot_pos - 1) as i32;
98 res
99 } else {
100 0
101 }
102}
103
104pub fn truncate_to_string(value: Decimal, precision: i32) -> String {
119 if precision <= 0 {
120 return value.trunc().to_string();
121 }
122
123 let s = value.to_string();
124
125 if let Some(dot_pos) = s.find('.') {
126 let end_pos = std::cmp::min(dot_pos + 1 + precision as usize, s.len());
127 s[..end_pos].to_string()
128 } else {
129 s
130 }
131}
132
133pub fn truncate(value: Decimal, precision: i32) -> Decimal {
147 let s = truncate_to_string(value, precision);
148 Decimal::from_str(&s).unwrap_or(value)
149}
150
151pub fn decimal_to_precision(
193 value: Decimal,
194 rounding_mode: RoundingMode,
195 num_precision_digits: i32,
196 counting_mode: Option<CountingMode>,
197 padding_mode: Option<PaddingMode>,
198) -> Result<String> {
199 let counting_mode = counting_mode.unwrap_or(CountingMode::DecimalPlaces);
200 let padding_mode = padding_mode.unwrap_or(PaddingMode::NoPadding);
201
202 if num_precision_digits < 0 {
204 let to_nearest =
205 Decimal::from_i128_with_scale(10_i128.pow(num_precision_digits.unsigned_abs()), 0);
206
207 match rounding_mode {
208 RoundingMode::Round => {
209 let divided = value / to_nearest;
210 let rounded = round_decimal(divided, 0, RoundingMode::Round);
211 let result = to_nearest * rounded;
212 return Ok(format_decimal(result, padding_mode));
213 }
214 RoundingMode::RoundDown => {
215 let modulo = value % to_nearest;
216 let result = value - modulo;
217 return Ok(format_decimal(result, padding_mode));
218 }
219 RoundingMode::RoundUp => {
220 let modulo = value % to_nearest;
221 let result = if modulo.is_zero() {
222 value
223 } else {
224 value - modulo + to_nearest
225 };
226 return Ok(format_decimal(result, padding_mode));
227 }
228 }
229 }
230
231 match counting_mode {
232 CountingMode::DecimalPlaces => Ok(decimal_to_precision_decimal_places(
233 value,
234 rounding_mode,
235 num_precision_digits,
236 padding_mode,
237 )),
238 CountingMode::SignificantDigits => Ok(decimal_to_precision_significant_digits(
239 value,
240 rounding_mode,
241 num_precision_digits,
242 padding_mode,
243 )),
244 CountingMode::TickSize => {
245 let tick_size = Decimal::from_str(&format!(
246 "0.{}",
247 "0".repeat((num_precision_digits - 1) as usize) + "1"
248 ))
249 .map_err(|e| {
250 ParseError::invalid_format("tick_size", format!("Invalid tick size: {e}"))
251 })?;
252
253 decimal_to_precision_tick_size(value, rounding_mode, tick_size, padding_mode)
254 }
255 }
256}
257
258fn decimal_to_precision_decimal_places(
260 value: Decimal,
261 rounding_mode: RoundingMode,
262 decimal_places: i32,
263 padding_mode: PaddingMode,
264) -> String {
265 let rounded = round_decimal(value, decimal_places, rounding_mode);
266 format_decimal_with_places(rounded, decimal_places, padding_mode)
267}
268
269fn decimal_to_precision_significant_digits(
271 value: Decimal,
272 rounding_mode: RoundingMode,
273 sig_digits: i32,
274 padding_mode: PaddingMode,
275) -> String {
276 if value.is_zero() {
277 return "0".to_string();
278 }
279
280 let abs_value = value.abs();
281 let value_f64 = abs_value.to_f64().unwrap_or(0.0);
282 let log10 = value_f64.log10();
283 #[allow(clippy::cast_possible_truncation)]
284 let magnitude = log10.floor() as i32;
285
286 let decimal_places = sig_digits - magnitude - 1;
287
288 let rounded = round_decimal(value, decimal_places, rounding_mode);
289 format_decimal_with_places(rounded, decimal_places, padding_mode)
290}
291
292fn decimal_to_precision_tick_size(
294 value: Decimal,
295 rounding_mode: RoundingMode,
296 tick_size: Decimal,
297 padding_mode: PaddingMode,
298) -> Result<String> {
299 if tick_size <= Decimal::ZERO {
300 return Err(Error::invalid_request("Tick size must be positive"));
301 }
302
303 let ticks = match rounding_mode {
304 RoundingMode::Round => (value / tick_size).round(),
305 RoundingMode::RoundDown => (value / tick_size).floor(),
306 RoundingMode::RoundUp => (value / tick_size).ceil(),
307 };
308
309 let result = ticks * tick_size;
310
311 let tick_precision = precision_from_string(&tick_size.to_string());
312 Ok(format_decimal_with_places(
313 result,
314 tick_precision,
315 padding_mode,
316 ))
317}
318
319fn round_decimal(value: Decimal, decimal_places: i32, mode: RoundingMode) -> Decimal {
321 if decimal_places < 0 {
322 let scale = Decimal::from_i128_with_scale(10_i128.pow(decimal_places.unsigned_abs()), 0);
323 let divided = value / scale;
324 let rounded = match mode {
325 RoundingMode::Round => divided.round(),
326 RoundingMode::RoundDown => divided.floor(),
327 RoundingMode::RoundUp => divided.ceil(),
328 };
329 return rounded * scale;
330 }
331
332 let scale = Decimal::from_i128_with_scale(10_i128.pow(decimal_places as u32), 0);
333 let scaled = value * scale;
334
335 let rounded = match mode {
336 RoundingMode::Round => scaled.round(),
337 RoundingMode::RoundDown => scaled.floor(),
338 RoundingMode::RoundUp => scaled.ceil(),
339 };
340
341 rounded / scale
342}
343
344fn format_decimal(value: Decimal, _padding_mode: PaddingMode) -> String {
346 let s = value.to_string();
347
348 if s.contains('.') {
349 s.trim_end_matches('0').trim_end_matches('.').to_string()
350 } else {
351 s
352 }
353}
354
355fn format_decimal_with_places(
357 value: Decimal,
358 decimal_places: i32,
359 padding_mode: PaddingMode,
360) -> String {
361 match padding_mode {
362 PaddingMode::NoPadding => format_decimal(value, padding_mode),
363 PaddingMode::PadWithZero => {
364 if decimal_places > 0 {
365 format!("{:.prec$}", value, prec = decimal_places as usize)
366 } else {
367 value.trunc().to_string()
368 }
369 }
370 }
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376
377 #[test]
378 fn test_number_to_string() {
379 let num = Decimal::from_str("0.00000123").unwrap();
380 assert_eq!(number_to_string(num), "0.00000123");
381
382 let large = Decimal::from_str("1234567890").unwrap();
383 assert_eq!(number_to_string(large), "1234567890");
384
385 let with_trailing = Decimal::from_str("123.4500").unwrap();
386 assert_eq!(number_to_string(with_trailing), "123.45");
387 }
388
389 #[test]
390 fn test_precision_from_string() {
391 assert_eq!(precision_from_string("0.001"), 3);
392 assert_eq!(precision_from_string("0.01"), 2);
393 assert_eq!(precision_from_string("1.2345"), 4);
394 assert_eq!(precision_from_string("100"), 0);
395 assert_eq!(precision_from_string("1.0000"), 0);
396 }
397
398 #[test]
399 fn test_truncate() {
400 let num = Decimal::from_str("123.456789").unwrap();
401
402 assert_eq!(truncate_to_string(num, 2), "123.45");
403 assert_eq!(truncate_to_string(num, 4), "123.4567");
404 assert_eq!(truncate_to_string(num, 0), "123");
405
406 let truncated = truncate(num, 2);
407 assert_eq!(truncated.to_string(), "123.45");
408 }
409
410 #[test]
411 fn test_decimal_to_precision_round() {
412 let num = Decimal::from_str("123.456").unwrap();
413
414 let result = decimal_to_precision(
415 num,
416 RoundingMode::Round,
417 2,
418 Some(CountingMode::DecimalPlaces),
419 Some(PaddingMode::NoPadding),
420 )
421 .unwrap();
422 assert_eq!(result, "123.46");
423 }
424
425 #[test]
426 fn test_decimal_to_precision_round_down() {
427 let num = Decimal::from_str("123.456").unwrap();
428
429 let result = decimal_to_precision(
430 num,
431 RoundingMode::RoundDown,
432 2,
433 Some(CountingMode::DecimalPlaces),
434 Some(PaddingMode::NoPadding),
435 )
436 .unwrap();
437 assert_eq!(result, "123.45");
438 }
439
440 #[test]
441 fn test_decimal_to_precision_round_up() {
442 let num = Decimal::from_str("123.451").unwrap();
443
444 let result = decimal_to_precision(
445 num,
446 RoundingMode::RoundUp,
447 2,
448 Some(CountingMode::DecimalPlaces),
449 Some(PaddingMode::NoPadding),
450 )
451 .unwrap();
452 assert_eq!(result, "123.46");
453 }
454
455 #[test]
456 fn test_decimal_to_precision_with_padding() {
457 let num = Decimal::from_str("123.4").unwrap();
458
459 let result = decimal_to_precision(
460 num,
461 RoundingMode::Round,
462 3,
463 Some(CountingMode::DecimalPlaces),
464 Some(PaddingMode::PadWithZero),
465 )
466 .unwrap();
467 assert_eq!(result, "123.400");
468 }
469
470 #[test]
471 fn test_decimal_to_precision_negative_precision() {
472 let num = Decimal::from_str("123.456").unwrap();
473
474 let result = decimal_to_precision(
475 num,
476 RoundingMode::Round,
477 -1,
478 Some(CountingMode::DecimalPlaces),
479 Some(PaddingMode::NoPadding),
480 )
481 .unwrap();
482 assert_eq!(result, "120");
483 }
484
485 #[test]
486 fn test_decimal_to_precision_tick_size() {
487 let num = Decimal::from_str("123.456").unwrap();
488 let tick = Decimal::from_str("0.05").unwrap();
489
490 let result =
491 decimal_to_precision_tick_size(num, RoundingMode::Round, tick, PaddingMode::NoPadding)
492 .unwrap();
493
494 assert_eq!(result, "123.45");
496 }
497
498 #[test]
499 fn test_round_decimal() {
500 let num = Decimal::from_str("123.456").unwrap();
501
502 let rounded = round_decimal(num, 2, RoundingMode::Round);
503 assert_eq!(rounded.to_string(), "123.46");
504
505 let truncated = round_decimal(num, 2, RoundingMode::RoundDown);
506 assert_eq!(truncated.to_string(), "123.45");
507 }
508
509 #[test]
510 fn test_format_decimal() {
511 let num = Decimal::from_str("123.4500").unwrap();
512 let formatted = format_decimal(num, PaddingMode::NoPadding);
513 assert_eq!(formatted, "123.45");
514
515 let formatted_padded = format_decimal_with_places(num, 4, PaddingMode::PadWithZero);
516 assert_eq!(formatted_padded, "123.4500");
517 }
518}