1use rust_decimal::Decimal;
10use rust_decimal::prelude::*;
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 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
102pub 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
131pub 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
149pub 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 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
256fn 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
271fn 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
297fn 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
324fn 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
349fn 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
360fn 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 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}