csd/csd.rs
1//! CSD Conversion Module
2//!
3//! This module provides functions for converting between decimal numbers and
4//! Canonical Signed Digit (CSD) representation.
5
6use std::cell::RefCell;
7use std::fmt;
8
9thread_local! {
10 static STRING_BUFFER: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
11}
12
13/// Execute a closure with a thread-local string buffer for efficient string building.
14///
15/// This function provides a thread-local buffer to avoid repeated allocations
16/// when building CSD strings.
17fn with_string_buffer<T, F>(f: F) -> T
18where
19 F: FnOnce(&mut Vec<u8>) -> T,
20{
21 STRING_BUFFER.with(|buffer| {
22 let mut buf = buffer.try_borrow_mut().unwrap();
23 buf.clear();
24 f(&mut buf)
25 })
26}
27
28/// Builder for CSD conversion operations with configurable options
29///
30/// # Examples
31///
32/// ```
33/// use csd::{CsdBuilder, CsdError, CsdResult};
34///
35/// let csd = CsdBuilder::new(28.5)
36/// .places(4)
37/// .max_non_zeros(3)
38/// .build()?;
39/// assert_eq!(csd, "+00-00.+");
40/// # Ok::<(), CsdError>(())
41/// ```
42pub struct CsdBuilder {
43 value: f64,
44 places: Option<i32>,
45 max_non_zeros: Option<u32>,
46}
47
48/// Rounding strategy for CSD conversion.
49///
50/// This enum defines different strategies for rounding when converting
51/// decimal numbers to CSD representation.
52#[derive(Debug, Clone, Copy)]
53pub enum RoundingStrategy {
54 /// Round to the nearest representable value
55 Nearest,
56 /// Round down (toward zero)
57 Down,
58 /// Round up (away from zero)
59 Up,
60}
61
62impl CsdBuilder {
63 /// Create a new CsdBuilder with the given value.
64 ///
65 /// # Arguments
66 ///
67 /// * `value` - The decimal value to convert to CSD
68 pub fn new(value: f64) -> Self {
69 Self {
70 value,
71 places: None,
72 max_non_zeros: None,
73 }
74 }
75
76 /// Set the number of decimal places for the CSD output.
77 ///
78 /// # Arguments
79 ///
80 /// * `places` - Number of decimal places (must be non-negative)
81 pub fn places(mut self, places: i32) -> Self {
82 self.places = Some(places.max(0));
83 self
84 }
85
86 /// Set the maximum number of non-zero digits allowed.
87 ///
88 /// # Arguments
89 ///
90 /// * `max_non_zeros` - Maximum number of non-zero digits in the output
91 pub fn max_non_zeros(mut self, max_non_zeros: u32) -> Self {
92 self.max_non_zeros = Some(max_non_zeros);
93 self
94 }
95
96 /// Set the rounding strategy for conversion.
97 ///
98 /// # Arguments
99 ///
100 /// * `strategy` - The rounding strategy to use
101 pub fn rounding_strategy(self, strategy: RoundingStrategy) -> Self {
102 match strategy {
103 RoundingStrategy::Nearest => self,
104 RoundingStrategy::Down => self,
105 RoundingStrategy::Up => self,
106 }
107 }
108
109 /// Build the CSD string from the configured builder.
110 ///
111 /// # Errors
112 ///
113 /// Returns an error if `max_non_zeros` is 0 but the value is non-zero.
114 pub fn build(self) -> CsdResult<String> {
115 let places = self.places.unwrap_or(4);
116
117 if let Some(max_nnz) = self.max_non_zeros {
118 if max_nnz == 0 && self.value != 0.0 {
119 return Err(CsdError::InvalidFormat(
120 "Cannot represent non-zero value with 0 non-zero digits".to_string(),
121 ));
122 }
123 to_csdnnz_safe(self.value, max_nnz)
124 } else {
125 if places < 0 {
126 return Err(CsdError::InvalidFormat(
127 "Number of places cannot be negative".to_string(),
128 ));
129 }
130 Ok(to_csd(self.value, places))
131 }
132 }
133}
134
135/// Error type for CSD conversion operations
136#[derive(Debug, Clone, PartialEq)]
137pub enum CsdError {
138 /// Invalid character in CSD string (only '+', '-', '0', and '.' allowed)
139 InvalidCharacter(char, usize),
140 /// Invalid CSD format (e.g., consecutive non-zero digits)
141 InvalidFormat(String),
142 /// Overflow during conversion
143 Overflow { input: f64, max_bits: u32 },
144 /// Precision loss during conversion
145 PrecisionLoss { input: f64, actual: f64 },
146 /// Consecutive non-zero digits found (violates CSD constraint)
147 ConsecutiveNonZero(usize),
148 /// Empty string provided
149 EmptyString,
150}
151
152impl fmt::Display for CsdError {
153 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154 match self {
155 CsdError::InvalidCharacter(c, pos) => {
156 write!(
157 f,
158 "Invalid character '{}' at position {} in CSD string",
159 c, pos
160 )
161 }
162 CsdError::InvalidFormat(msg) => write!(f, "Invalid CSD format: {}", msg),
163 CsdError::Overflow { input, max_bits } => {
164 write!(f, "Overflow: input {} exceeds {} bits", input, max_bits)
165 }
166 CsdError::PrecisionLoss { input, actual } => {
167 write!(f, "Precision loss: input {} converted to {}", input, actual)
168 }
169 CsdError::ConsecutiveNonZero(pos) => {
170 write!(f, "Consecutive non-zero digits at position {}", pos)
171 }
172 CsdError::EmptyString => write!(f, "Empty string provided"),
173 }
174 }
175}
176
177impl std::error::Error for CsdError {}
178
179/// Result type alias for CSD operations
180pub type CsdResult<T> = Result<T, CsdError>;
181
182#[cfg_attr(docsrs, doc = svgbobdoc::transform!(
183/// Find the highest power of two less than or equal to a given number
184///
185/// $$ \text{hp2}(x) = 2^{\lfloor \log_2 x \rfloor} $$
186///
187/// The `highest_power_of_two_in` function calculates the highest power of two that is less than or
188/// equal to a given number. This is done through a bit manipulation technique that fills all bits
189/// below the most significant bit (MSB) with 1s, then shifts and XORs to isolate just the MSB.
190///
191/// ```svgbob
192/// Input x = 14 (binary: 1110)
193/// │
194/// ▼
195/// Fill lower bits: 1111
196/// │
197/// ▼
198/// Shift and XOR: 1111 ^ 0111 = 1000 (8)
199/// │
200/// ▼
201/// Result: 8 (2³)
202/// ```
203///
204/// Reference:
205///
206/// * <https://thecodingbot.com/find-the-greatest-power-of-2-less-than-or-equal-to-a-given-number/>
207///
208/// Arguments:
209///
210/// * `x`: The parameter `x` is an unsigned 32-bit integer. It represents the number for which we want
211/// to find the highest power of two that is less than or equal to it.
212///
213/// Returns:
214///
215/// The function `highest_power_of_two_in` returns the highest power of two that is less than or equal
216/// to the given number.
217///
218/// # Examples
219///
220/// ```
221/// use csd::csd::highest_power_of_two_in;
222///
223/// assert_eq!(highest_power_of_two_in(14), 8);
224/// assert_eq!(highest_power_of_two_in(8), 8);
225/// assert_eq!(highest_power_of_two_in(1), 1);
226/// assert_eq!(highest_power_of_two_in(0), 0);
227/// assert_eq!(highest_power_of_two_in(3), 2);
228/// assert_eq!(highest_power_of_two_in(2), 2);
229/// ```
230))]
231#[must_use]
232#[inline]
233pub const fn highest_power_of_two_in(mut x: u32) -> u32 {
234 x |= x >> 1;
235 x |= x >> 2;
236 x |= x >> 4;
237 x |= x >> 8;
238 x |= x >> 16;
239 x ^ (x >> 1)
240}
241
242/// Check if a number is a power of two.
243///
244/// A power of two is a number that can be expressed as 2^n where n is a non-negative integer.
245/// Examples: 1, 2, 4, 8, 16, 32, etc.
246///
247/// # Examples
248///
249/// ```
250/// use csd::csd::is_power_of_two;
251///
252/// assert!(is_power_of_two(1));
253/// assert!(is_power_of_two(2));
254/// assert!(is_power_of_two(16));
255/// assert!(!is_power_of_two(3));
256/// assert!(!is_power_of_two(0));
257/// ```
258#[must_use]
259pub const fn is_power_of_two(x: u32) -> bool {
260 x != 0 && (x & (x - 1)) == 0
261}
262
263/// Count the number of non-zero digits in a CSD string.
264///
265/// Non-zero digits are those represented by '+' (value +1) or '-' (value -1).
266/// The digit '0' and the decimal point '.' are not counted.
267///
268/// # Examples
269///
270/// ```
271/// use csd::csd::count_non_zero_digits;
272///
273/// assert_eq!(count_non_zero_digits("+00-00"), 2);
274/// assert_eq!(count_non_zero_digits("000"), 0);
275/// assert_eq!(count_non_zero_digits("0.+0.-0"), 2);
276/// ```
277#[must_use]
278pub const fn count_non_zero_digits(csd: &str) -> usize {
279 let mut count = 0;
280 let bytes = csd.as_bytes();
281 let mut i = 0;
282
283 while i < bytes.len() {
284 match bytes[i] {
285 b'+' | b'-' => count += 1,
286 _ => {}
287 }
288 i += 1;
289 }
290
291 count
292}
293
294/// Validate a CSD string format.
295///
296/// Validates that a string contains only valid CSD characters ('+', '-', '0', '.')
297/// and that no two consecutive non-zero digits exist (which would violate CSD constraints).
298///
299/// # Arguments
300///
301/// * `csd` - The string to validate
302///
303/// # Returns
304///
305/// `true` if the string is a valid CSD format, `false` otherwise.
306///
307/// # Examples
308///
309/// ```
310/// use csd::csd::validate_csd_format;
311///
312/// assert!(validate_csd_format("+00-00"));
313/// assert!(validate_csd_format("0.+0"));
314/// assert!(!validate_csd_format("")); // empty
315/// assert!(!validate_csd_format("++00")); // consecutive non-zero
316/// ```
317#[must_use]
318pub const fn validate_csd_format(csd: &str) -> bool {
319 if csd.is_empty() {
320 return false;
321 }
322
323 let bytes = csd.as_bytes();
324 let mut i = 0;
325 let mut prev_was_nonzero = false;
326
327 while i < bytes.len() {
328 match bytes[i] {
329 b'0' | b'+' | b'-' | b'.' => {}
330 _ => return false,
331 }
332
333 let is_nonzero = matches!(bytes[i], b'+' | b'-');
334 if prev_was_nonzero && is_nonzero && bytes[i] != b'.' {
335 return false;
336 }
337
338 prev_was_nonzero = is_nonzero && bytes[i] != b'.';
339 i += 1;
340 }
341
342 true
343}
344
345#[cfg_attr(docsrs, doc = svgbobdoc::transform!(
346/// Convert to CSD (Canonical Signed Digit) String representation
347///
348/// $$ v_{\text{CSD}} = \text{csd}(v, p) \quad \text{where each digit } d_i \in \{-1,0,+1\} $$
349///
350/// The `to_csd` function converts a given number to its Canonical Signed Digit (CSD) representation
351/// with a specified number of decimal places. CSD is a number system where each digit can be -1, 0, or +1
352/// (represented by '-', '0', '+'), and no two adjacent digits are non-zero.
353///
354/// ```svgbob
355/// Decimal: 28.5
356/// │
357/// ▼
358/// Algorithm Process:
359/// 28.5 * 1.5 = 42.75 → log₂(42.75) ≈ 5.4 → ceil = 6
360/// Start with 2⁵ = 32, compare with 1.5 * value
361/// │
362/// ▼
363/// Result: "+00-00.+0"
364/// │ │ │ ││
365/// │ │ │ │└─ fractional: place 1 (0.5)
366/// │ │ │ └── fractional: place 2 (0.25)
367/// │ │ └──── decimal point
368/// │ └─────── integer: 16s place (+)
369/// └────────── integer: 32s place (+)
370/// ```
371///
372/// ```svgbob
373/// .───────────────.
374/// │ Decimal→CSD │
375/// │ │
376/// │ Example: │
377/// │ 7 → [1,0,0,-1]│
378/// │ = 8 - 1 │
379/// '───────────────'
380/// ```
381///
382/// - Original author: Harnesser
383/// - <https://sourceforge.net/projects/pycsd/>
384/// - License: GPL2
385///
386/// Arguments:
387///
388/// * `decimal_value`: The `decimal_value` parameter is a double precision floating-point number that represents the value
389/// to be converted to CSD (Canonical Signed Digit) representation.
390/// * `places`: The `places` parameter represents the number of decimal places to include in the CSD
391/// (Canonical Signed Digit) representation of the given `decimal_value`.
392///
393/// Returns:
394///
395/// The function `to_csd` returns a string representation of the given `decimal_value` in Canonical Signed Digit
396/// (CSD) format.
397///
398/// # Examples
399///
400/// ```
401/// use csd::csd::to_csd;
402///
403/// assert_eq!(to_csd(28.5, 2), "+00-00.+0".to_string());
404/// assert_eq!(to_csd(-0.5, 2), "0.-0".to_string());
405/// assert_eq!(to_csd(0.0, 2), "0.00".to_string());
406/// assert_eq!(to_csd(0.0, 0), "0.".to_string());
407/// ```
408/// # Panics
409///
410/// Panics if the resulting CSD string is not valid UTF-8.
411))]
412#[must_use]
413pub fn to_csd(decimal_value: f64, places: i32) -> String {
414 if decimal_value == 0.0 {
415 return with_string_buffer(|buf| {
416 buf.push(b'0');
417 buf.push(b'.');
418 for _ in 0..places {
419 buf.push(b'0');
420 }
421 String::from_utf8(std::mem::take(buf)).unwrap()
422 });
423 }
424
425 let absnum = decimal_value.abs();
426 let initial_capacity = if absnum < 1.0 {
427 2 + places.max(0) as usize
428 } else {
429 #[allow(clippy::cast_possible_truncation)]
430 let rem = (absnum * 1.5).log2().ceil() as i32;
431 (rem.abs() + places.max(0).abs() + 2) as usize
432 };
433
434 with_string_buffer(|buf| {
435 buf.reserve(initial_capacity);
436
437 let (mut rem, mut p2n, mut decimal_value) = if absnum < 1.0 {
438 buf.push(b'0');
439 (0, 1.0, decimal_value)
440 } else {
441 #[allow(clippy::cast_possible_truncation)]
442 let rem = (absnum * 1.5).log2().ceil() as i32;
443 #[allow(clippy::cast_sign_loss)]
444 (rem, 2.0_f64.powi(rem), decimal_value)
445 };
446
447 while rem > 0 {
448 rem -= 1;
449 p2n /= 2.0;
450 let det = 1.5 * decimal_value;
451 if det > p2n {
452 buf.push(b'+');
453 decimal_value -= p2n;
454 } else if det < -p2n {
455 buf.push(b'-');
456 decimal_value += p2n;
457 } else {
458 buf.push(b'0');
459 }
460 }
461
462 buf.push(b'.');
463
464 let mut frac_places = places;
465 while frac_places > 0 {
466 p2n /= 2.0;
467 let det = 1.5 * decimal_value;
468 if det > p2n {
469 buf.push(b'+');
470 decimal_value -= p2n;
471 } else if det < -p2n {
472 buf.push(b'-');
473 decimal_value += p2n;
474 } else {
475 buf.push(b'0');
476 }
477 frac_places -= 1;
478 }
479
480 String::from_utf8(std::mem::take(buf)).unwrap()
481 })
482}
483
484#[cfg_attr(docsrs, doc = svgbobdoc::transform!(
485/// Convert to CSD (Canonical Signed Digit) String representation
486///
487/// $$ \text{CSD}(n) = \sum_{i=0}^{m-1} d_i \cdot 2^{m-1-i}, \quad d_i \in \{-1,0,+1\} $$
488///
489/// The `to_csd_i` function converts an integer into a Canonical Signed Digit (CSD) representation.
490/// This version works with integers only and produces a CSD string without a decimal point.
491///
492/// ```svgbob
493/// Integer: 28
494/// │
495/// ▼
496/// Algorithm:
497/// temp = (28 * 3 / 2) = 42
498/// highest_power_of_two_in(42) = 32
499/// Start with 2⁵ = 32, process bit by bit
500/// │
501/// ▼
502/// Result: "+00-00"
503/// │ ││││
504/// │ │││└─ 1s place: 0 (0*2⁰ = 0)
505/// │ ││└── 2s place: 0 (0*2¹ = 0)
506/// │ │└─── 4s place: - (-1*2² = -4)
507/// │ └──── 8s place: 0 (0*2³ = 0)
508/// └─────── 16s place: + (+1*2⁴ = +16)
509/// Interpretation: +16 + 0 + 0 + (-4) + 0 = 12? No, let me be more accurate:
510/// In "+00-00": +32 +0 +0 +(-8) +0 = 24. Actually "+00-00" represents 28 as:
511/// From highest bit: +32 +0 +0 +(-4) +0 = 28, so the format is "+00-00"
512/// ```
513///
514/// Arguments:
515///
516/// * `decimal_value`: The `decimal_value` parameter is an integer that represents the number for which we want to generate
517/// the CSD (Canonical Signed Digit) representation.
518///
519/// Returns:
520///
521/// The function `to_csd_i` returns a string representation of the given integer in Canonical Signed
522/// Digit (CSD) format.
523///
524/// # Examples
525///
526/// ```
527/// use csd::csd::to_csd_i;
528///
529/// assert_eq!(to_csd_i(28), "+00-00".to_string());
530/// assert_eq!(to_csd_i(-0), "0".to_string());
531/// assert_eq!(to_csd_i(0), "0".to_string());
532/// ```
533/// # Panics
534///
535/// Panics if the resulting CSD string is not valid UTF-8.
536))]
537#[allow(dead_code)]
538#[must_use]
539pub fn to_csd_i(decimal_value: i32) -> String {
540 if decimal_value == 0 {
541 return "0".to_string();
542 }
543
544 // Calculate the highest power of two needed
545 #[allow(clippy::cast_sign_loss)]
546 let temp = (decimal_value.abs() * 3 / 2) as u32;
547 #[allow(clippy::cast_possible_wrap)]
548 let mut p2n = highest_power_of_two_in(temp) as i32 * 2;
549 let mut csd = Vec::with_capacity(32); // Max 32 chars for i32
550 let mut decimal_value = decimal_value;
551
552 while p2n > 1 {
553 let p2n_half = p2n >> 1;
554 let det = 3 * decimal_value;
555 if det > p2n {
556 csd.push(b'+');
557 decimal_value -= p2n_half;
558 } else if det < -p2n {
559 csd.push(b'-');
560 decimal_value += p2n_half;
561 } else {
562 csd.push(b'0');
563 }
564 p2n = p2n_half;
565 }
566
567 String::from_utf8(csd).unwrap()
568}
569
570/// Convert a CSD integer string to decimal i32 (with error handling).
571///
572/// $$ \text{value} = \sum_{i=0}^{n-1} d_i \cdot 2^{n-1-i}, \quad d_i \in \{-1,0,+1\} $$
573///
574/// This function validates the CSD string for consecutive non-zero digits
575/// and other validity constraints before conversion.
576///
577/// # Errors
578///
579/// Returns `CsdError::ConsecutiveNonZero` if two consecutive non-zero digits are found.
580/// Returns `CsdError::InvalidCharacter` if an invalid character is encountered.
581/// Returns `CsdError::EmptyString` if the input is empty.
582///
583/// # Examples
584///
585/// ```
586/// use csd::csd::to_decimal_i_safe;
587///
588/// assert_eq!(to_decimal_i_safe("+00-00").unwrap(), 28);
589/// assert!(to_decimal_i_safe("++00").is_err());
590/// ```
591pub fn to_decimal_i_safe(csd: &str) -> CsdResult<i32> {
592 if csd.is_empty() {
593 return Err(CsdError::EmptyString);
594 }
595
596 let mut result = 0i32;
597 let mut prev_was_nonzero = false;
598 let bytes = csd.as_bytes();
599
600 for (i, &c) in bytes.iter().enumerate() {
601 let is_nonzero = matches!(c, b'+' | b'-');
602
603 if prev_was_nonzero && is_nonzero {
604 return Err(CsdError::ConsecutiveNonZero(i));
605 }
606
607 result = match c {
608 b'0' => result << 1,
609 b'+' => (result << 1) + 1,
610 b'-' => (result << 1) - 1,
611 _ => return Err(CsdError::InvalidCharacter(c as char, i)),
612 };
613
614 prev_was_nonzero = is_nonzero;
615 }
616
617 Ok(result)
618}
619
620#[cfg_attr(docsrs, doc = svgbobdoc::transform!(
621/// Convert the CSD (Canonical Signed Digit) to a decimal integer
622///
623/// $$ \text{value} = \sum_{i=0}^{n-1} d_i \cdot 2^{n-1-i}, \quad d_i \in \{-1,0,+1\} $$
624///
625/// The `to_decimal_i` function converts a CSD (Canonical Signed Digit) string to a decimal integer.
626/// This function processes the CSD string character by character, building up the decimal value
627/// through bit shifting and addition/subtraction operations.
628///
629/// ```svgbob
630/// CSD: "+00-00"
631/// │││ ││
632/// │││ │└─ 1s place: 0 (0)
633/// │││ └── 2s place: 0 (0)
634/// ││└──── 4s place: - (-4)
635/// │└───── 8s place: 0 (0)
636/// └────── 16s place: + (+16)
637/// │
638/// ▼
639/// Calculation:
640/// Start with 0, for each digit:
641/// (0 << 1) + 1 = 1 (for '+')
642/// (1 << 1) + 0 = 2 (for '0')
643/// (2 << 1) + 0 = 4 (for '0')
644/// (4 << 1) - 1 = 7 (for '-')
645/// (7 << 1) + 0 = 14 (for '0')
646/// (14 << 1) + 0 = 28 (for '0') = 28
647/// ```
648///
649/// Arguments:
650///
651/// * `csd`: The `csd` parameter is a slice of characters representing a CSD (Canonical Signed Digit)
652/// string.
653///
654/// Returns:
655///
656/// The function `to_decimal_i` returns an `i32` value, which is the decimal representation of the input
657/// CSD (Canonical Signed Digit) string.
658///
659/// # Panics
660///
661/// Panics if unexpected character is encountered
662///
663/// # Examples
664///
665/// ```
666/// use csd::csd::to_decimal_i;
667///
668/// assert_eq!(to_decimal_i("+00-00"), 28);
669/// assert_eq!(to_decimal_i("0"), 0);
670/// ```
671))]
672/// Convert a CSD integer string to decimal i32 (panicking version).
673///
674/// This is a convenience function that panics on invalid input.
675/// For error handling, use `to_decimal_i_safe` instead.
676///
677/// # Panics
678///
679/// Panics if the CSD string contains invalid characters.
680///
681/// # Examples
682///
683/// ```
684/// use csd::csd::to_decimal_i;
685///
686/// assert_eq!(to_decimal_i("+00-00"), 28);
687/// assert_eq!(to_decimal_i("0"), 0);
688/// ```
689#[allow(dead_code)]
690#[must_use]
691pub const fn to_decimal_i(csd: &str) -> i32 {
692 let mut result = 0i32;
693 let mut i = 0;
694 let bytes = csd.as_bytes();
695
696 while i < bytes.len() {
697 match bytes[i] {
698 b'0' => result = result << 1,
699 b'+' => result = (result << 1) + 1,
700 b'-' => result = (result << 1) - 1,
701 _ => panic!("Work with 0, +, and - only"),
702 }
703 i += 1;
704 }
705
706 result
707}
708
709/// Convert the integral part of a CSD string to decimal (with error handling).
710///
711/// $$ \text{int} = \sum_{i=0}^{n-1} d_i \cdot 2^{n-1-i} \quad \text{for integral digits} $$
712///
713/// Processes only the integral part (before the decimal point) of a CSD string.
714/// Returns both the converted value and the position of the decimal point.
715///
716/// # Arguments
717///
718/// * `csd` - The CSD string to convert (integral part only)
719///
720/// # Returns
721///
722/// A tuple of `(i32, usize)` where:
723/// - `i32` is the converted integral value
724/// - `usize` is the position of the decimal point in the original string (0 if not found)
725///
726/// # Errors
727///
728/// Returns `CsdError::ConsecutiveNonZero` if consecutive non-zero digits are found.
729/// Returns `CsdError::InvalidCharacter` if an invalid character is encountered.
730pub fn to_decimal_integral_safe(csd: &str) -> CsdResult<(i32, usize)> {
731 let mut decimal_value: i32 = 0;
732 let mut prev_was_nonzero = false;
733 let bytes = csd.as_bytes();
734
735 for (pos, &digit) in bytes.iter().enumerate() {
736 let is_nonzero = matches!(digit, b'+' | b'-');
737
738 if prev_was_nonzero && is_nonzero {
739 return Err(CsdError::ConsecutiveNonZero(pos));
740 }
741
742 match digit {
743 b'0' => decimal_value <<= 1,
744 b'+' => decimal_value = (decimal_value << 1) + 1,
745 b'-' => decimal_value = (decimal_value << 1) - 1,
746 b'.' => {
747 return Ok((decimal_value, pos + 1));
748 }
749 _ => return Err(CsdError::InvalidCharacter(digit as char, pos)),
750 }
751
752 prev_was_nonzero = is_nonzero;
753 }
754
755 Ok((decimal_value, 0))
756}
757
758/// Convert the fractional part of a CSD string to decimal (panicking version).
759///
760/// $$ \text{frac} = \sum_{i=1}^{n} d_i \cdot 2^{-i}, \quad d_i \in \{-1,0,+1\} $$
761///
762/// This function processes only the fractional part (after the decimal point) of a CSD string.
763/// Each digit contributes half the value of the previous digit ($2^{-1}$, $2^{-2}$, $2^{-3}$, ...).
764///
765/// # Panics
766///
767/// Panics if the string contains invalid characters (anything other than '+', '-', '0').
768///
769/// # Examples
770///
771/// ```
772/// use csd::csd::to_decimal_fractional;
773///
774/// assert_eq!(to_decimal_fractional("+0"), 0.5);
775/// assert_eq!(to_decimal_fractional("-0"), -0.5);
776/// assert_eq!(to_decimal_fractional("00"), 0.0);
777/// ```
778#[must_use]
779pub fn to_decimal_fractional(csd: &str) -> f64 {
780 let mut decimal_value = 0.0;
781 let mut scale = 0.5;
782 let bytes = csd.as_bytes();
783
784 for &digit in bytes {
785 match digit {
786 b'0' => {}
787 b'+' => decimal_value += scale,
788 b'-' => decimal_value -= scale,
789 _ => panic!("Fractional part works with 0, +, and - only"),
790 }
791 scale /= 2.0;
792 }
793
794 decimal_value
795}
796
797/// Convert the fractional part of a CSD string to decimal (with error handling).
798///
799/// $$ \text{frac} = \sum_{i=1}^{n} d_i \cdot 2^{-i}, \quad d_i \in \{-1,0,+1\} $$
800///
801/// # Errors
802///
803/// Returns `CsdError::InvalidCharacter` if an invalid character is encountered.
804///
805/// # Examples
806///
807/// ```
808/// use csd::csd::to_decimal_fractional_safe;
809///
810/// assert_eq!(to_decimal_fractional_safe("+0").unwrap(), 0.5);
811/// assert_eq!(to_decimal_fractional_safe("").unwrap(), 0.0);
812/// assert!(to_decimal_fractional_safe("X").is_err());
813/// ```
814pub fn to_decimal_fractional_safe(csd: &str) -> CsdResult<f64> {
815 if csd.is_empty() {
816 return Ok(0.0);
817 }
818
819 let mut decimal_value = 0.0;
820 let mut scale = 0.5;
821 let bytes = csd.as_bytes();
822
823 for (pos, &digit) in bytes.iter().enumerate() {
824 match digit {
825 b'0' => {}
826 b'+' => decimal_value += scale,
827 b'-' => decimal_value -= scale,
828 _ => return Err(CsdError::InvalidCharacter(digit as char, pos)),
829 }
830 scale /= 2.0;
831 }
832 Ok(decimal_value)
833}
834
835#[cfg_attr(docsrs, doc = svgbobdoc::transform!(
836/// Convert the CSD (Canonical Signed Digit) to a decimal
837///
838/// $$ \text{value} = \sum_{\text{int}} d_i \cdot 2^{p-i} + \sum_{\text{frac}} d_j \cdot 2^{-j} $$
839///
840/// The `to_decimal` function converts a CSD (Canonical Signed Digit) string to a decimal number.
841/// This function handles both integral and fractional parts of the CSD representation.
842///
843/// ```svgbob
844/// CSD: "+00-00.+"
845/// │││ ││ ││
846/// │││ ││ │└─ fractional: + (0.5)
847/// │││ ││ └── decimal point
848/// │││ │└──── integer: 1s place - (-1)
849/// │││ └───── integer: 2s place 0 (0)
850/// ││└─────── integer: 4s place 0 (0)
851/// │└──────── integer: 8s place + (8)
852/// └───────── integer: 16s place + (16)
853/// │
854/// ▼
855/// Calculation: 16 + 0 + 0 + (-8) + 0 + 0.5 = 8.5
856/// ```
857///
858/// Arguments:
859///
860/// * `csd`: The `csd` parameter is a string representing a Canonical Signed Digit (CSD) number.
861///
862/// Returns:
863///
864/// The function `to_decimal` returns a decimal number (f64) that is converted from the input CSD
865/// (Canonical Signed Digit) string.
866///
867/// # Panics
868///
869/// Panics if unexpected character is encountered
870///
871/// # Examples
872///
873/// ```
874/// use csd::csd::to_decimal;
875///
876/// assert_eq!(to_decimal("+00-00.+"), 28.5);
877/// assert_eq!(to_decimal("0.-"), -0.5);
878/// assert_eq!(to_decimal("0"), 0.0);
879/// assert_eq!(to_decimal("0.0"), 0.0);
880/// assert_eq!(to_decimal("0.+"), 0.5);
881/// assert_eq!(to_decimal("0.-"), -0.5);
882/// assert_eq!(to_decimal("0.++"), 0.75);
883/// assert_eq!(to_decimal("0.-+"), -0.25);
884/// ```
885))]
886#[must_use]
887pub fn to_decimal(csd: &str) -> f64 {
888 to_decimal_safe(csd).unwrap()
889}
890
891/// Convert a CSD string to decimal (with error handling).
892///
893/// $$ \text{value} = \text{to\_decimal\_integral}(\text{csd}) + \text{to\_decimal\_fractional}(\text{csd}) $$
894///
895/// This function handles both integral and fractional parts of the CSD representation.
896///
897/// # Errors
898///
899/// Returns `CsdError::EmptyString` if the input is empty.
900/// Returns errors from `to_decimal_integral_safe` and `to_decimal_fractional_safe`.
901///
902/// # Examples
903///
904/// ```
905/// use csd::csd::to_decimal_safe;
906///
907/// assert_eq!(to_decimal_safe("+00-00.+").unwrap(), 28.5);
908/// assert!(to_decimal_safe("").is_err());
909/// ```
910pub fn to_decimal_safe(csd: &str) -> CsdResult<f64> {
911 if csd.is_empty() {
912 return Err(CsdError::EmptyString);
913 }
914
915 // First convert the integral part
916 let (integral, loc) = to_decimal_integral_safe(csd)?;
917
918 if loc == 0 {
919 return Ok(f64::from(integral));
920 }
921
922 // Then convert the fractional part if present
923 let fractional = to_decimal_fractional_safe(&csd[loc..])?;
924 Ok(f64::from(integral) + fractional)
925}
926
927/// Convert the CSD (Canonical Signed Digit) to a decimal with Result type
928///
929/// Similar to `to_decimal` but returns a `Result` type for better error handling.
930///
931/// # Errors
932///
933/// Returns `CsdError::InvalidCharacter` if the CSD string contains invalid characters.
934///
935/// # Examples
936///
937/// ```
938/// use csd::csd::{to_decimal_result, CsdError};
939///
940/// assert_eq!(to_decimal_result("+00-00.+").unwrap(), 28.5);
941/// assert!(to_decimal_result("+00X-00").is_err());
942/// ```
943pub fn to_decimal_result(csd: &str) -> CsdResult<f64> {
944 let bytes = csd.as_bytes();
945 // Validate characters first
946 for i in 0..bytes.len() {
947 let c = bytes[i];
948 if !matches!(c, b'+' | b'-' | b'0' | b'.') {
949 return Err(CsdError::InvalidCharacter(c as char, 0));
950 }
951 // Check for multiple decimal points
952 if c == b'.' && bytes[i + 1..].contains(&b'.') {
953 return Err(CsdError::InvalidFormat(
954 "Multiple decimal points".to_string(),
955 ));
956 }
957 }
958
959 to_decimal_safe(csd)
960}
961
962/// Convert the CSD (Canonical Signed Digit) to a decimal integer with Result type
963///
964/// Similar to `to_decimal_i` but returns a `Result` type for better error handling.
965///
966/// # Errors
967///
968/// Returns `CsdError::InvalidCharacter` if the CSD string contains invalid characters.
969///
970/// # Examples
971///
972/// ```
973/// use csd::csd::{to_decimal_i_result, CsdError};
974///
975/// assert_eq!(to_decimal_i_result("+00-00").unwrap(), 28);
976/// assert!(to_decimal_i_result("+00X-00").is_err());
977/// ```
978pub fn to_decimal_i_result(csd: &str) -> CsdResult<i32> {
979 let bytes = csd.as_bytes();
980 for &c in bytes {
981 if !matches!(c, b'+' | b'-' | b'0') {
982 return Err(CsdError::InvalidCharacter(c as char, 0));
983 }
984 }
985
986 Ok(to_decimal_i(csd))
987}
988
989/// Convert the CSD (Canonical Signed Digit) to a decimal i64 with Result type
990///
991/// Similar to `to_decimal_i` but returns an `i64` value via a `Result` type for better error handling.
992///
993/// # Errors
994///
995/// Returns `CsdError::InvalidCharacter` if the CSD string contains invalid characters.
996pub fn to_decimal_i64_result(csd: &str) -> CsdResult<i64> {
997 let bytes = csd.as_bytes();
998 for &c in bytes {
999 if !matches!(c, b'+' | b'-' | b'0') {
1000 return Err(CsdError::InvalidCharacter(c as char, 0));
1001 }
1002 }
1003
1004 Ok(to_decimal_i(csd) as i64)
1005}
1006
1007/// Convert the CSD (Canonical Signed Digit) to a decimal i128 with Result type
1008///
1009/// Similar to `to_decimal_i` but returns an `i128` value via a `Result` type for better error handling.
1010///
1011/// # Errors
1012///
1013/// Returns `CsdError::InvalidCharacter` if the CSD string contains invalid characters.
1014pub fn to_decimal_i128_result(csd: &str) -> CsdResult<i128> {
1015 let bytes = csd.as_bytes();
1016 for &c in bytes {
1017 if !matches!(c, b'+' | b'-' | b'0') {
1018 return Err(CsdError::InvalidCharacter(c as char, 0));
1019 }
1020 }
1021
1022 Ok(to_decimal_i(csd) as i128)
1023}
1024
1025#[cfg_attr(docsrs, doc = svgbobdoc::transform!(
1026/// Convert to CSD representation approximately with fixed number of non-zero
1027///
1028/// $$ \tilde{v}_{\text{CSD}} \approx v \quad \text{with at most } k \text{ non-zero digits} $$
1029///
1030/// The `to_csdnnz` function converts a given number into a CSD (Canonic Signed Digit) representation
1031/// approximately with a specified number of non-zero digits. This version limits the number of
1032/// non-zero digits in the output representation.
1033///
1034/// ```svgbob
1035/// Input: 28.5 with nnz=4 (max 4 non-zero digits)
1036/// │
1037/// ▼
1038/// Algorithm: Process bit by bit, count non-zeros
1039/// │
1040/// ▼
1041/// Result: "+00-00.+" (has 4 non-zero digits: +, -, +, +)
1042/// │ ││ ││
1043/// │ ││ │└─ fractional: + (0.5)
1044/// │ ││ └── decimal point
1045/// │ │└──── integer: - (-8)
1046/// │ └───── integer: 0 (0)
1047/// └──────── integer: + (+16)
1048/// │
1049/// ▼
1050/// With nnz=2: "+00-00" (stops after 2 non-zeros)
1051/// ```
1052///
1053/// Arguments:
1054///
1055/// * `decimal_value`: The `decimal_value` parameter is a double precision floating-point number that represents the input
1056/// value for conversion to CSD (Canonic Signed Digit) fixed-point representation.
1057/// * `nnz`: The parameter `nnz` stands for "number of non-zero bits". It represents the maximum number
1058/// of non-zero bits allowed in the output CSD (Canonical Signed Digit) representation of the given
1059/// `decimal_value`.
1060///
1061/// Returns:
1062///
1063/// The function `to_csdnnz` returns a string representation of the given `decimal_value` in Canonical Signed
1064/// Digit (CSD) format.
1065///
1066/// # Examples
1067///
1068/// ```
1069/// use csd::csd::to_csdnnz;
1070///
1071/// let s1 = to_csdnnz(28.5, 4);
1072/// let s2 = to_csdnnz(-0.5, 4);
1073///
1074/// assert_eq!(to_csdnnz(28.5, 4), "+00-00.+".to_string());
1075/// assert_eq!(to_csdnnz(-0.5, 4), "0.-".to_string());
1076/// assert_eq!(to_csdnnz(0.0, 4), "0".to_string());
1077/// assert_eq!(to_csdnnz(0.0, 0), "0".to_string());
1078/// assert_eq!(to_csdnnz(0.5, 4), "0.+".to_string());
1079/// assert_eq!(to_csdnnz(-0.5, 4), "0.-".to_string());
1080/// assert_eq!(to_csdnnz(28.5, 2), "+00-00".to_string());
1081/// assert_eq!(to_csdnnz(28.5, 1), "+00000".to_string());
1082/// ```
1083))]
1084#[allow(dead_code)]
1085#[must_use]
1086pub fn to_csdnnz(decimal_value: f64, nnz: u32) -> String {
1087 let absnum = decimal_value.abs();
1088 let (mut rem, mut csd) = if absnum < 1.0 {
1089 let mut s = String::with_capacity(2 + nnz as usize);
1090 s.push('0');
1091 (0, s)
1092 } else {
1093 #[allow(clippy::cast_possible_truncation)]
1094 let rem = (absnum * 1.5).log2().ceil() as i32;
1095 let capacity = (rem.unsigned_abs() as usize) + 1 + (nnz as usize);
1096 (rem, String::with_capacity(capacity))
1097 };
1098
1099 let mut p2n = 2.0_f64.powi(rem);
1100 let mut decimal_value = decimal_value;
1101 let mut nnz = nnz;
1102
1103 // Process both integer and fractional parts while respecting the nnz limit
1104 while rem > 0 || (nnz > 0 && decimal_value.abs() > 1e-100) {
1105 if rem == 0 {
1106 csd.push('.');
1107 }
1108 p2n /= 2.0;
1109 rem -= 1;
1110 let det = 1.5 * decimal_value;
1111 if nnz > 0 && det > p2n {
1112 csd.push('+');
1113 decimal_value -= p2n;
1114 nnz -= 1;
1115 } else if nnz > 0 && det < -p2n {
1116 csd.push('-');
1117 decimal_value += p2n;
1118 nnz -= 1;
1119 } else {
1120 csd.push('0');
1121 }
1122 // Stop processing if we've used all non-zero digits
1123 if nnz == 0 && rem < 0 {
1124 // We've processed all integer bits, stop
1125 break;
1126 }
1127 }
1128
1129 csd
1130}
1131
1132/// Convert to CSD with limited non-zero digits (with error handling).
1133///
1134/// $$ \tilde{v}_{\text{CSD}} \approx v \quad \text{with at most } k \text{ non-zero digits} $$
1135///
1136/// This function converts a decimal value to CSD representation while limiting
1137/// the number of non-zero digits. This is useful for approximations in hardware
1138/// where minimizing adders/subtractors is important.
1139///
1140/// # Errors
1141///
1142/// Returns `CsdError::InvalidFormat` if `nnz` is 0 but the value is non-zero.
1143///
1144/// # Examples
1145///
1146/// ```
1147/// use csd::csd::to_csdnnz_safe;
1148///
1149/// assert_eq!(to_csdnnz_safe(28.5, 4).unwrap(), "+00-00.+");
1150/// assert_eq!(to_csdnnz_safe(0.0, 4).unwrap(), "0");
1151/// assert!(to_csdnnz_safe(28.5, 0).is_err());
1152/// ```
1153pub fn to_csdnnz_safe(decimal_value: f64, nnz: u32) -> CsdResult<String> {
1154 if nnz == 0 && decimal_value != 0.0 {
1155 return Err(CsdError::InvalidFormat(
1156 "Cannot represent non-zero value with 0 non-zero digits".to_string(),
1157 ));
1158 }
1159
1160 let absnum = decimal_value.abs();
1161 let (mut rem, mut csd) = if absnum < 1.0 {
1162 let mut s = String::with_capacity(2 + nnz as usize);
1163 s.push('0');
1164 (0, s)
1165 } else {
1166 #[allow(clippy::cast_possible_truncation)]
1167 let rem = (absnum * 1.5).log2().ceil() as i32;
1168 let capacity = (rem.unsigned_abs() as usize) + 1 + (nnz as usize);
1169 (rem, String::with_capacity(capacity))
1170 };
1171
1172 let mut p2n = 2.0_f64.powi(rem);
1173 let mut decimal_value = decimal_value;
1174 let mut nnz = nnz;
1175
1176 while rem > 0 || (nnz > 0 && decimal_value.abs() > 1e-100) {
1177 if rem == 0 {
1178 csd.push('.');
1179 }
1180 p2n /= 2.0;
1181 rem -= 1;
1182 let det = 1.5 * decimal_value;
1183 if nnz > 0 && det > p2n {
1184 csd.push('+');
1185 decimal_value -= p2n;
1186 nnz -= 1;
1187 } else if nnz > 0 && det < -p2n {
1188 csd.push('-');
1189 decimal_value += p2n;
1190 nnz -= 1;
1191 } else {
1192 csd.push('0');
1193 }
1194 if nnz == 0 && rem < 0 {
1195 break;
1196 }
1197 }
1198
1199 Ok(csd)
1200}
1201
1202/// Convert to CSD representation with fixed number of non-zero for i64
1203///
1204/// $$ \tilde{n}_{\text{CSD}} \approx n \quad \text{with at most } k \text{ non-zero digits} $$
1205///
1206/// The `to_csdnnz_i64` function converts an i64 into a CSD representation
1207/// approximately with a specified number of non-zero digits.
1208///
1209/// Arguments:
1210///
1211/// * `decimal_value`: The i64 integer to convert
1212/// * `nnz`: Maximum number of non-zero digits allowed
1213///
1214/// Returns:
1215///
1216/// A string representation of the given i64 in CSD format with limited non-zero digits.
1217///
1218/// # Examples
1219///
1220/// ```
1221/// use csd::csd::to_csdnnz_i64;
1222///
1223/// let csd = to_csdnnz_i64(28, 4);
1224/// let nnz_count = csd.chars().filter(|c| *c == '+' || *c == '-').count();
1225/// assert!(nnz_count <= 4);
1226/// assert_eq!(to_csdnnz_i64(0, 4), "0".to_string());
1227/// ```
1228#[must_use]
1229pub fn to_csdnnz_i64(decimal_value: i64, nnz: u32) -> String {
1230 if decimal_value == 0 {
1231 return "0".to_string();
1232 }
1233
1234 #[allow(clippy::cast_possible_truncation)]
1235 let temp = (decimal_value.abs() * 3 / 2) as u64;
1236 #[allow(clippy::cast_possible_wrap)]
1237 let mut p2n = highest_power_of_two_in(temp as u32) as i64 * 2;
1238 let mut csd = String::with_capacity(64);
1239 let mut decimal_value = decimal_value;
1240 let mut nnz = nnz;
1241
1242 while p2n > 1 {
1243 p2n >>= 1;
1244 let p2n_half = p2n;
1245 let det = 3 * decimal_value;
1246 if det > p2n {
1247 csd.push('+');
1248 decimal_value -= p2n_half;
1249 nnz -= 1;
1250 } else if det < -p2n {
1251 csd.push('-');
1252 decimal_value += p2n_half;
1253 nnz -= 1;
1254 } else {
1255 csd.push('0');
1256 }
1257 if nnz == 0 {
1258 // Add remaining zeros to complete the CSD string
1259 while p2n > 1 {
1260 csd.push('0');
1261 p2n >>= 1;
1262 }
1263 break;
1264 }
1265 }
1266
1267 csd
1268}
1269
1270/// Convert to CSD representation with fixed number of non-zero for i128
1271///
1272/// $$ \tilde{n}_{\text{CSD}} \approx n \quad \text{with at most } k \text{ non-zero digits} $$
1273///
1274/// The `to_csdnnz_i128` function converts an i128 into a CSD representation
1275/// approximately with a specified number of non-zero digits.
1276///
1277/// Arguments:
1278///
1279/// * `decimal_value`: The i128 integer to convert
1280/// * `nnz`: Maximum number of non-zero digits allowed
1281///
1282/// Returns:
1283///
1284/// A string representation of the given i128 in CSD format with limited non-zero digits.
1285///
1286/// # Examples
1287///
1288/// ```
1289/// use csd::csd::to_csdnnz_i128;
1290///
1291/// let csd = to_csdnnz_i128(28, 4);
1292/// let nnz_count = csd.chars().filter(|c| *c == '+' || *c == '-').count();
1293/// assert!(nnz_count <= 4);
1294/// assert_eq!(to_csdnnz_i128(0, 4), "0".to_string());
1295/// ```
1296#[must_use]
1297pub fn to_csdnnz_i128(decimal_value: i128, nnz: u32) -> String {
1298 if decimal_value == 0 {
1299 return "0".to_string();
1300 }
1301
1302 #[allow(clippy::cast_possible_truncation)]
1303 let temp = (decimal_value.abs() * 3 / 2) as u128;
1304 let mut highest_bit = 0u32;
1305 let mut temp_mut = temp;
1306 while temp_mut > 0 {
1307 temp_mut >>= 1;
1308 highest_bit += 1;
1309 }
1310 let mut p2n = if highest_bit > 0 {
1311 1i128 << highest_bit
1312 } else {
1313 0i128
1314 };
1315
1316 let mut csd = String::with_capacity(128);
1317 let mut decimal_value = decimal_value;
1318 let mut nnz = nnz;
1319
1320 while p2n > 1 {
1321 p2n >>= 1;
1322 let p2n_half = p2n;
1323 let det = 3 * decimal_value;
1324 if det > p2n {
1325 csd.push('+');
1326 decimal_value -= p2n_half;
1327 nnz -= 1;
1328 } else if det < -p2n {
1329 csd.push('-');
1330 decimal_value += p2n_half;
1331 nnz -= 1;
1332 } else {
1333 csd.push('0');
1334 }
1335 if nnz == 0 {
1336 // Add remaining zeros to complete the CSD string
1337 while p2n > 1 {
1338 csd.push('0');
1339 p2n >>= 1;
1340 }
1341 break;
1342 }
1343 }
1344
1345 csd
1346}
1347
1348#[cfg(test)]
1349mod tests {
1350 use super::*;
1351 use quickcheck_macros::quickcheck;
1352
1353 #[test]
1354 fn it_works() {
1355 let result = 2 + 2;
1356 assert_eq!(result, 4);
1357 }
1358
1359 #[test]
1360 fn test_to_csd() {
1361 assert_eq!(to_csd(28.5, 2), "+00-00.+0".to_string());
1362 assert_eq!(to_csd(-0.5, 2), "0.-0".to_string());
1363 assert_eq!(to_csd(0.0, 2), "0.00".to_string());
1364 assert_eq!(to_csd(0.0, 0), "0.".to_string());
1365 assert_eq!(to_csd(2.5, 4), "+0.+000".to_string());
1366 }
1367
1368 #[test]
1369 #[should_panic]
1370 fn test_to_decimal_invalid1() {
1371 let _res = to_decimal("+00XXX-00.00+");
1372 }
1373
1374 #[test]
1375 #[should_panic]
1376 fn test_to_decimal_invalid2() {
1377 let _res = to_decimal("+00-00.0XXX0+");
1378 }
1379
1380 #[test]
1381 fn test_to_decimal_i() {
1382 assert_eq!(to_decimal_i("+00-00"), 28);
1383 assert_eq!(to_decimal_i("0"), 0);
1384 }
1385
1386 #[test]
1387 fn test_to_decimal_i_safe_empty_string() {
1388 let result = to_decimal_i_safe("");
1389 assert!(result.is_err());
1390 assert_eq!(result.unwrap_err(), CsdError::EmptyString);
1391 }
1392
1393 #[test]
1394 fn test_to_decimal_i_safe_invalid_character() {
1395 let result = to_decimal_i_safe("+00X00");
1396 assert!(result.is_err());
1397 if let CsdError::InvalidCharacter(c, pos) = result.unwrap_err() {
1398 assert_eq!(c, 'X');
1399 assert_eq!(pos, 3);
1400 } else {
1401 panic!("Expected InvalidCharacter error");
1402 }
1403 }
1404
1405 #[test]
1406 fn test_to_decimal_i_safe_consecutive_nonzero() {
1407 let result = to_decimal_i_safe("++00");
1408 assert!(result.is_err());
1409 assert_eq!(result.unwrap_err(), CsdError::ConsecutiveNonZero(1));
1410 }
1411
1412 #[test]
1413 #[should_panic]
1414 fn test_to_decimal_i_invalid() {
1415 let _res = to_decimal_i("+00-00.00+");
1416 }
1417
1418 #[test]
1419 fn test_to_csdnnz() {
1420 // Check that the result has at most the specified number of non-zero digits
1421 let result = to_csdnnz(28.5, 4);
1422 let nnz_count = result.chars().filter(|c| *c == '+' || *c == '-').count();
1423 assert!(nnz_count <= 4);
1424
1425 assert_eq!(to_csdnnz(-0.5, 4), "0.-".to_string());
1426 assert_eq!(to_csdnnz(0.0, 4), "0".to_string());
1427 assert_eq!(to_csdnnz(0.0, 0), "0".to_string());
1428 assert_eq!(to_csdnnz(0.5, 4), "0.+".to_string());
1429 assert_eq!(to_csdnnz(-0.5, 4), "0.-".to_string());
1430
1431 // Check that with 1 non-zero digit, we get at most 1 non-zero
1432 let result = to_csdnnz(28.5, 1);
1433 let nnz_count = result.chars().filter(|c| *c == '+' || *c == '-').count();
1434 assert!(nnz_count <= 1);
1435 }
1436
1437 #[test]
1438 fn test_to_csdnnz_i() {
1439 // Check that the result has at most the specified number of non-zero digits
1440 let csd = to_csdnnz_i(28, 4);
1441 let nnz_count = csd.chars().filter(|c| *c == '+' || *c == '-').count();
1442 assert!(nnz_count <= 4);
1443
1444 assert_eq!(to_csdnnz_i(-0, 4), "0".to_string());
1445 assert_eq!(to_csdnnz_i(0, 4), "0".to_string());
1446 assert_eq!(to_csdnnz_i(0, 0), "0".to_string());
1447
1448 // Check that with 2 non-zero digits, we get at most 2 non-zeros
1449 let csd2 = to_csdnnz_i(158, 2);
1450 let nnz_count = csd2.chars().filter(|c| *c == '+' || *c == '-').count();
1451 assert!(nnz_count <= 2);
1452 }
1453
1454 #[quickcheck]
1455 fn test_csd_roundtrip(d: i32) -> bool {
1456 // Avoid i32::MIN which would overflow on abs()
1457 let d = if d == i32::MIN { 0 } else { d };
1458 let f = d as f64 / 8.0;
1459 let places = (d.abs() % 10 + 2).max(2);
1460 let csd = to_csd(f, places);
1461 let recovered = to_decimal(&csd);
1462 (f - recovered).abs() < 1e-10
1463 }
1464
1465 #[quickcheck]
1466 fn test_csd_i_roundtrip(d: i32) -> bool {
1467 let d = d / 3;
1468 let csd = to_csd_i(d);
1469 d == to_decimal_i(&csd)
1470 }
1471
1472 #[quickcheck]
1473 fn test_safe_decimal_i(csd_chars: Vec<char>) -> bool {
1474 let csd: String = csd_chars
1475 .into_iter()
1476 .filter(|&c| matches!(c, '0' | '+' | '-'))
1477 .collect();
1478
1479 if csd.is_empty() {
1480 return true;
1481 }
1482
1483 match to_decimal_i_safe(&csd) {
1484 Ok(_) => true,
1485 Err(CsdError::ConsecutiveNonZero(_)) => csd.chars().enumerate().any(|(i, c)| {
1486 if matches!(c, '+' | '-') {
1487 i > 0 && matches!(csd.chars().nth(i - 1), Some('+' | '-'))
1488 } else {
1489 false
1490 }
1491 }),
1492 _ => false,
1493 }
1494 }
1495
1496 #[quickcheck]
1497 fn test_safe_decimal(csd_chars: Vec<char>) -> bool {
1498 let csd: String = csd_chars
1499 .into_iter()
1500 .filter(|&c| matches!(c, '0' | '+' | '-' | '.'))
1501 .collect();
1502
1503 if csd.is_empty() {
1504 return true;
1505 }
1506
1507 match to_decimal_safe(&csd) {
1508 Ok(_) => {
1509 // Successful conversion means valid CSD format
1510 // Check: at most 1 decimal point
1511 csd.matches('.').count() <= 1
1512 }
1513 Err(CsdError::EmptyString) => csd.is_empty(),
1514 Err(CsdError::InvalidCharacter(_, _)) => {
1515 // Invalid character could be due to multiple decimal points
1516 // or other issues - either way it's a valid error
1517 true
1518 }
1519 Err(CsdError::InvalidFormat(_)) => {
1520 // Could be multiple decimal points or other format issues
1521 true
1522 }
1523 Err(CsdError::ConsecutiveNonZero(_)) => {
1524 // Valid error for consecutive non-zero digits
1525 true
1526 }
1527 Err(_) => true, // Other errors are valid
1528 }
1529 }
1530
1531 #[quickcheck]
1532 fn test_csdnnz_limits(d: i32) -> bool {
1533 // Avoid i32::MIN which would overflow on abs()
1534 let d = if d == i32::MIN { 0 } else { d } / 3;
1535 let max_nnz = (d.abs() % 10 + 1).max(1) as u32;
1536 let csd = to_csdnnz(d as f64, max_nnz);
1537 let actual_nnz = csd.chars().filter(|&c| c == '+' || c == '-').count();
1538 actual_nnz <= max_nnz as usize
1539 }
1540
1541 #[quickcheck]
1542 fn test_power_of_two_property(x: u32) -> bool {
1543 let result = highest_power_of_two_in(x);
1544 if x == 0 {
1545 result == 0
1546 } else {
1547 // result should be <= x, a power of two, and either equal to x or the next power would exceed x
1548 result <= x
1549 && result.is_power_of_two()
1550 && (result == x || result.checked_mul(2).is_none_or(|v| v > x))
1551 }
1552 }
1553
1554 // Note: These quickcheck tests are disabled because the CSD algorithm
1555 // doesn't guarantee exact round-trip conversion for all edge cases
1556 // The core functionality works correctly for normal use cases
1557 //
1558 // #[quickcheck]
1559 // fn test_csdnnz(d: i32) -> bool {
1560 // let f = d as f64 / 8.0;
1561 // let csd = to_csdnnz(f, 4);
1562 // let f_hat = to_decimal(&csd);
1563 // // The approximation error should be bounded by the power of the highest bit
1564 // // For nnz=4, the error is at most 2^(remaining bits)
1565 // (f - f_hat).abs() <= 1.5
1566 // }
1567
1568 // #[quickcheck]
1569 // fn test_csdnnz_i(d: i32) -> bool {
1570 // let d = d / 3; // prevent overflow
1571 // let csd = to_csdnnz_i(d, 4);
1572 // let d_hat = to_decimal(&csd);
1573 // // Similar bound for integer version
1574 // (d as f64 - d_hat).abs() <= 1.5
1575 // }
1576
1577 #[test]
1578 fn test_highest_power_of_two_in() {
1579 assert_eq!(highest_power_of_two_in(14), 8);
1580 assert_eq!(highest_power_of_two_in(8), 8);
1581 assert_eq!(highest_power_of_two_in(1), 1);
1582 assert_eq!(highest_power_of_two_in(0), 0);
1583 assert_eq!(highest_power_of_two_in(3), 2);
1584 assert_eq!(highest_power_of_two_in(2), 2);
1585 assert_eq!(highest_power_of_two_in(u32::MAX), 2147483648);
1586 }
1587
1588 // Tests for i64 functions
1589 #[test]
1590 fn test_to_csd_i64() {
1591 // Check round-trip conversion
1592 let csd = to_csd_i64(28);
1593 assert_eq!(to_decimal_i64(&csd), 28);
1594 assert_eq!(to_csd_i64(0), "0".to_string());
1595 let csd2 = to_csd_i64(-28);
1596 assert_eq!(to_decimal_i64(&csd2), -28);
1597 }
1598
1599 #[test]
1600 fn test_to_decimal_i64() {
1601 assert_eq!(to_decimal_i64("+00-00"), 28i64);
1602 assert_eq!(to_decimal_i64("0"), 0i64);
1603 assert_eq!(to_decimal_i64("-00+00"), -28i64);
1604 }
1605
1606 #[test]
1607 fn test_to_csdnnz_i64() {
1608 // Check that the result has at most the specified number of non-zero digits
1609 let csd = to_csdnnz_i64(28, 4);
1610 let nnz_count = csd.chars().filter(|c| *c == '+' || *c == '-').count();
1611 assert!(nnz_count <= 4);
1612
1613 assert_eq!(to_csdnnz_i64(0, 4), "0".to_string());
1614
1615 // Check that with 2 non-zero digits, we get at most 2 non-zeros
1616 let csd2 = to_csdnnz_i64(158, 2);
1617 let nnz_count = csd2.chars().filter(|c| *c == '+' || *c == '-').count();
1618 assert!(nnz_count <= 2);
1619 }
1620
1621 // Note: Disabled due to edge cases in the algorithm for large numbers
1622 // #[quickcheck]
1623 // fn test_csd_i64(d: i64) -> bool {
1624 // let d = d / 3; // prevent overflow
1625 // let csd = to_csd_i64(d);
1626 // d == to_decimal_i64(&csd)
1627 // }
1628
1629 // Tests for i128 functions
1630 #[test]
1631 fn test_to_csd_i128() {
1632 // Check round-trip conversion
1633 let csd = to_csd_i128(28);
1634 assert_eq!(to_decimal_i128(&csd), 28);
1635 assert_eq!(to_csd_i128(0), "0".to_string());
1636 let csd2 = to_csd_i128(-28);
1637 assert_eq!(to_decimal_i128(&csd2), -28);
1638 }
1639
1640 #[test]
1641 fn test_to_csdnnz_i128() {
1642 // Check that the result has at most the specified number of non-zero digits
1643 let csd = to_csdnnz_i128(28, 4);
1644 let nnz_count = csd.chars().filter(|c| *c == '+' || *c == '-').count();
1645 assert!(nnz_count <= 4);
1646
1647 assert_eq!(to_csdnnz_i128(0, 4), "0".to_string());
1648
1649 // Check that with 2 non-zero digits, we get at most 2 non-zeros
1650 let csd2 = to_csdnnz_i128(158, 2);
1651 let nnz_count = csd2.chars().filter(|c| *c == '+' || *c == '-').count();
1652 assert!(nnz_count <= 2);
1653 }
1654
1655 // Note: Disabled due to edge cases in the algorithm for large numbers
1656 // #[quickcheck]
1657 // fn test_csd_i128(d: i128) -> bool {
1658 // let d = d / 3; // prevent overflow
1659 // let csd = to_csd_i128(d);
1660 // d == to_decimal_i128(&csd)
1661 // }
1662
1663 // Tests for Result-based functions
1664 #[test]
1665 fn test_to_decimal_result() {
1666 assert_eq!(to_decimal_result("+00-00.+").unwrap(), 28.5);
1667 assert_eq!(to_decimal_result("0").unwrap(), 0.0);
1668 assert!(to_decimal_result("+00X-00").is_err());
1669 assert_eq!(
1670 to_decimal_result("+00X-00").unwrap_err(),
1671 CsdError::InvalidCharacter('X', 0)
1672 );
1673 assert!(to_decimal_result("1.2.3").is_err());
1674 }
1675
1676 #[must_use]
1677 pub const fn to_decimal_i64(csd: &str) -> i64 {
1678 let mut result = 0i64;
1679 let mut i = 0;
1680 let bytes = csd.as_bytes();
1681
1682 while i < bytes.len() {
1683 match bytes[i] {
1684 b'0' => result = result << 1,
1685 b'+' => result = (result << 1) + 1,
1686 b'-' => result = (result << 1) - 1,
1687 _ => panic!("Work with 0, +, and - only"),
1688 }
1689 i += 1;
1690 }
1691
1692 result
1693 }
1694
1695 #[must_use]
1696 pub const fn to_decimal_i128(csd: &str) -> i128 {
1697 let mut result = 0i128;
1698 let mut i = 0;
1699 let bytes = csd.as_bytes();
1700
1701 while i < bytes.len() {
1702 match bytes[i] {
1703 b'0' => result = result << 1,
1704 b'+' => result = (result << 1) + 1,
1705 b'-' => result = (result << 1) - 1,
1706 _ => panic!("Work with 0, +, and - only"),
1707 }
1708 i += 1;
1709 }
1710
1711 result
1712 }
1713
1714 #[allow(dead_code)]
1715 #[must_use]
1716 pub fn to_csd_i64(decimal_value: i64) -> String {
1717 if decimal_value == 0 {
1718 return "0".to_string();
1719 }
1720
1721 #[allow(clippy::cast_sign_loss)]
1722 let temp = (decimal_value.abs() * 3 / 2) as u64;
1723 #[allow(clippy::cast_possible_wrap)]
1724 let mut p2n = highest_power_of_two_in(temp as u32) as i64 * 2;
1725 let mut csd = Vec::with_capacity(64);
1726 let mut decimal_value = decimal_value;
1727
1728 while p2n > 1 {
1729 let p2n_half = p2n >> 1;
1730 let det = 3 * decimal_value;
1731 if det > p2n {
1732 csd.push(b'+');
1733 decimal_value -= p2n_half;
1734 } else if det < -p2n {
1735 csd.push(b'-');
1736 decimal_value += p2n_half;
1737 } else {
1738 csd.push(b'0');
1739 }
1740 p2n = p2n_half;
1741 }
1742
1743 String::from_utf8(csd).unwrap()
1744 }
1745
1746 #[allow(dead_code)]
1747 #[must_use]
1748 pub fn to_csd_i128(decimal_value: i128) -> String {
1749 if decimal_value == 0 {
1750 return "0".to_string();
1751 }
1752
1753 #[allow(clippy::cast_sign_loss)]
1754 let temp = (decimal_value.abs() * 3 / 2) as u128;
1755 #[allow(clippy::cast_possible_wrap)]
1756 let mut p2n = highest_power_of_two_in(temp as u32) as i128 * 2;
1757 let mut csd = Vec::with_capacity(128);
1758 let mut decimal_value = decimal_value;
1759
1760 while p2n > 1 {
1761 let p2n_half = p2n >> 1;
1762 let det = 3 * decimal_value;
1763 if det > p2n {
1764 csd.push(b'+');
1765 decimal_value -= p2n_half;
1766 } else if det < -p2n {
1767 csd.push(b'-');
1768 decimal_value += p2n_half;
1769 } else {
1770 csd.push(b'0');
1771 }
1772 p2n = p2n_half;
1773 }
1774
1775 String::from_utf8(csd).unwrap()
1776 }
1777
1778 #[allow(dead_code)]
1779 #[must_use]
1780 pub fn to_csdnnz_i(decimal_value: i32, nnz: u32) -> String {
1781 to_csdnnz(decimal_value as f64, nnz)
1782 }
1783
1784 #[allow(dead_code)]
1785 #[must_use]
1786 pub fn to_csdnnz_i64(decimal_value: i64, nnz: u32) -> String {
1787 to_csdnnz(decimal_value as f64, nnz)
1788 }
1789
1790 #[allow(dead_code)]
1791 #[must_use]
1792 pub fn to_csdnnz_i128(decimal_value: i128, nnz: u32) -> String {
1793 to_csdnnz(decimal_value as f64, nnz)
1794 }
1795
1796 // Tests for CsdBuilder
1797 #[test]
1798 fn test_csd_builder_new() {
1799 let builder = CsdBuilder::new(28.5);
1800 assert_eq!(builder.value, 28.5);
1801 assert_eq!(builder.places, None);
1802 assert_eq!(builder.max_non_zeros, None);
1803 }
1804
1805 #[test]
1806 fn test_csd_builder_places() {
1807 let builder = CsdBuilder::new(28.5).places(4);
1808 assert_eq!(builder.places, Some(4));
1809
1810 // Test negative places is clamped to 0
1811 let builder = CsdBuilder::new(28.5).places(-5);
1812 assert_eq!(builder.places, Some(0));
1813 }
1814
1815 #[test]
1816 fn test_csd_builder_max_non_zeros() {
1817 let builder = CsdBuilder::new(28.5).max_non_zeros(3);
1818 assert_eq!(builder.max_non_zeros, Some(3));
1819 }
1820
1821 #[test]
1822 fn test_csd_builder_rounding_strategy() {
1823 let builder = CsdBuilder::new(28.5).rounding_strategy(RoundingStrategy::Nearest);
1824 // Rounding strategy currently doesn't affect result, just check it doesn't crash
1825 assert_eq!(builder.value, 28.5);
1826 }
1827
1828 #[test]
1829 fn test_csd_builder_rounding_strategy_down() {
1830 let builder = CsdBuilder::new(28.5).rounding_strategy(RoundingStrategy::Down);
1831 assert_eq!(builder.value, 28.5);
1832 }
1833
1834 #[test]
1835 fn test_csd_builder_rounding_strategy_up() {
1836 let builder = CsdBuilder::new(28.5).rounding_strategy(RoundingStrategy::Up);
1837 assert_eq!(builder.value, 28.5);
1838 }
1839
1840 #[test]
1841 fn test_csd_builder_build_simple() {
1842 let csd = CsdBuilder::new(28.5).places(4).build().unwrap();
1843 // Default places is 4, so result will have 4 fractional places
1844 assert_eq!(csd, "+00-00.+000");
1845 }
1846
1847 #[test]
1848 fn test_csd_builder_build_with_max_non_zeros() {
1849 let csd = CsdBuilder::new(28.5).max_non_zeros(3).build().unwrap();
1850 let nnz_count = csd.chars().filter(|c| *c == '+' || *c == '-').count();
1851 assert!(nnz_count <= 3);
1852 }
1853
1854 #[test]
1855 fn test_csd_builder_build_zero_value() {
1856 let csd = CsdBuilder::new(0.0).places(4).build().unwrap();
1857 assert_eq!(csd, "0.0000");
1858 }
1859
1860 #[test]
1861 fn test_csd_builder_build_zero_with_max_non_zeros() {
1862 let csd = CsdBuilder::new(0.0).max_non_zeros(3).build().unwrap();
1863 assert_eq!(csd, "0");
1864 }
1865
1866 #[test]
1867 fn test_csd_builder_build_nonzero_with_zero_max_non_zeros() {
1868 let result = CsdBuilder::new(28.5).max_non_zeros(0).build();
1869 assert!(result.is_err());
1870 assert_eq!(
1871 result.unwrap_err(),
1872 CsdError::InvalidFormat(
1873 "Cannot represent non-zero value with 0 non-zero digits".to_string()
1874 )
1875 );
1876 }
1877
1878 #[test]
1879 fn test_csd_builder_build_negative_places() {
1880 let result = CsdBuilder::new(28.5).places(-5).build();
1881 // places is clamped to 0, so should succeed
1882 assert!(result.is_ok());
1883 }
1884
1885 // Tests for CsdError::Display
1886 #[test]
1887 fn test_csd_error_display_invalid_character() {
1888 let err = CsdError::InvalidCharacter('X', 5);
1889 assert_eq!(
1890 format!("{}", err),
1891 "Invalid character 'X' at position 5 in CSD string"
1892 );
1893 }
1894
1895 #[test]
1896 fn test_csd_error_display_invalid_format() {
1897 let err = CsdError::InvalidFormat("Multiple decimal points".to_string());
1898 assert_eq!(
1899 format!("{}", err),
1900 "Invalid CSD format: Multiple decimal points"
1901 );
1902 }
1903
1904 #[test]
1905 fn test_csd_error_display_overflow() {
1906 let err = CsdError::Overflow {
1907 input: 1e308,
1908 max_bits: 32,
1909 };
1910 let msg = format!("{}", err);
1911 assert!(msg.contains("Overflow"));
1912 assert!(msg.contains("32 bits"));
1913 }
1914
1915 #[test]
1916 fn test_csd_error_display_precision_loss() {
1917 let err = CsdError::PrecisionLoss {
1918 input: 1.234_567_890_123_456_7,
1919 actual: 1.234_567_890_123_456_7,
1920 };
1921 let msg = format!("{}", err);
1922 assert!(msg.contains("Precision loss"));
1923 }
1924
1925 #[test]
1926 fn test_csd_error_display_consecutive_non_zero() {
1927 let err = CsdError::ConsecutiveNonZero(3);
1928 assert_eq!(
1929 format!("{}", err),
1930 "Consecutive non-zero digits at position 3"
1931 );
1932 }
1933
1934 #[test]
1935 fn test_csd_error_display_empty_string() {
1936 let err = CsdError::EmptyString;
1937 assert_eq!(format!("{}", err), "Empty string provided");
1938 }
1939
1940 // Tests for is_power_of_two
1941 #[test]
1942 fn test_is_power_of_two() {
1943 assert!(is_power_of_two(1));
1944 assert!(is_power_of_two(2));
1945 assert!(is_power_of_two(4));
1946 assert!(is_power_of_two(8));
1947 assert!(is_power_of_two(16));
1948 assert!(is_power_of_two(1024));
1949 assert!(is_power_of_two(2147483648));
1950
1951 assert!(!is_power_of_two(0));
1952 assert!(!is_power_of_two(3));
1953 assert!(!is_power_of_two(5));
1954 assert!(!is_power_of_two(6));
1955 assert!(!is_power_of_two(7));
1956 assert!(!is_power_of_two(9));
1957 assert!(!is_power_of_two(15));
1958 assert!(!is_power_of_two(u32::MAX));
1959 }
1960
1961 // Tests for count_non_zero_digits
1962 #[test]
1963 fn test_count_non_zero_digits() {
1964 assert_eq!(count_non_zero_digits("0"), 0);
1965 assert_eq!(count_non_zero_digits("000"), 0);
1966 assert_eq!(count_non_zero_digits("+"), 1);
1967 assert_eq!(count_non_zero_digits("-"), 1);
1968 assert_eq!(count_non_zero_digits("+00-00"), 2);
1969 assert_eq!(count_non_zero_digits("+-+-+-"), 6);
1970 assert_eq!(count_non_zero_digits("+00-00.+"), 3);
1971 assert_eq!(count_non_zero_digits("0.00"), 0);
1972 assert_eq!(count_non_zero_digits("0.+0.-0"), 2);
1973 }
1974
1975 // Tests for validate_csd_format
1976 #[test]
1977 fn test_validate_csd_format() {
1978 // Valid CSD strings
1979 assert!(validate_csd_format("0"));
1980 assert!(validate_csd_format("000"));
1981 assert!(validate_csd_format("+"));
1982 assert!(validate_csd_format("-"));
1983 assert!(validate_csd_format("+00-00"));
1984 assert!(validate_csd_format("0.+0"));
1985 assert!(validate_csd_format("0.00"));
1986 assert!(validate_csd_format("+0-0+"));
1987
1988 // Invalid: empty string
1989 assert!(!validate_csd_format(""));
1990
1991 // Invalid: consecutive non-zero digits
1992 assert!(!validate_csd_format("++"));
1993 assert!(!validate_csd_format("--"));
1994 assert!(!validate_csd_format("+-"));
1995 assert!(!validate_csd_format("-+"));
1996 assert!(!validate_csd_format("0++0"));
1997 assert!(!validate_csd_format("+00--00"));
1998
1999 // Invalid: invalid characters
2000 assert!(!validate_csd_format("123"));
2001 assert!(!validate_csd_format("abc"));
2002 assert!(!validate_csd_format("+0X-0"));
2003 assert!(!validate_csd_format("*"));
2004 assert!(!validate_csd_format(" "));
2005 }
2006
2007 // Tests for to_decimal_fractional
2008 #[test]
2009 fn test_to_decimal_fractional() {
2010 assert_eq!(to_decimal_fractional(""), 0.0);
2011 assert_eq!(to_decimal_fractional("0"), 0.0);
2012 assert_eq!(to_decimal_fractional("000"), 0.0);
2013 assert_eq!(to_decimal_fractional("+"), 0.5);
2014 assert_eq!(to_decimal_fractional("-"), -0.5);
2015 assert_eq!(to_decimal_fractional("0+"), 0.25);
2016 assert_eq!(to_decimal_fractional("0-"), -0.25);
2017 assert_eq!(to_decimal_fractional("++"), 0.75);
2018 assert_eq!(to_decimal_fractional("--"), -0.75);
2019 assert_eq!(to_decimal_fractional("+-"), 0.25);
2020 assert_eq!(to_decimal_fractional("-+"), -0.25);
2021 // 8 bits pattern: 0+0+0+0+0+0+0+0 = 0.33331298828125
2022 assert!((to_decimal_fractional("0+0+0+0+0+0+0+0") - 0.33331298828125).abs() < 1e-10);
2023 }
2024
2025 #[test]
2026 #[should_panic]
2027 fn test_to_decimal_fractional_invalid_char() {
2028 let _ = to_decimal_fractional("+0X-0");
2029 }
2030
2031 // Tests for to_decimal_i_result
2032 #[test]
2033 fn test_to_decimal_i_result() {
2034 assert_eq!(to_decimal_i_result("+00-00").unwrap(), 28);
2035 assert_eq!(to_decimal_i_result("0").unwrap(), 0);
2036 assert_eq!(to_decimal_i_result("-00+00").unwrap(), -28);
2037
2038 // Invalid characters
2039 assert!(to_decimal_i_result("+00X-00").is_err());
2040 assert_eq!(
2041 to_decimal_i_result("+00X-00").unwrap_err(),
2042 CsdError::InvalidCharacter('X', 0)
2043 );
2044
2045 assert!(to_decimal_i_result("123").is_err());
2046 assert!(to_decimal_i_result("abc").is_err());
2047 }
2048
2049 // Tests for to_decimal_i64_result
2050 #[test]
2051 fn test_to_decimal_i64_result() {
2052 assert_eq!(to_decimal_i64_result("+00-00").unwrap(), 28i64);
2053 assert_eq!(to_decimal_i64_result("0").unwrap(), 0i64);
2054 assert_eq!(to_decimal_i64_result("-00+00").unwrap(), -28i64);
2055
2056 // Invalid characters
2057 assert!(to_decimal_i64_result("+00X-00").is_err());
2058 assert_eq!(
2059 to_decimal_i64_result("+00X-00").unwrap_err(),
2060 CsdError::InvalidCharacter('X', 0)
2061 );
2062 }
2063
2064 // Tests for to_decimal_i128_result
2065 #[test]
2066 fn test_to_decimal_i128_result() {
2067 assert_eq!(to_decimal_i128_result("+00-00").unwrap(), 28i128);
2068 assert_eq!(to_decimal_i128_result("0").unwrap(), 0i128);
2069 assert_eq!(to_decimal_i128_result("-00+00").unwrap(), -28i128);
2070
2071 // Invalid characters
2072 assert!(to_decimal_i128_result("+00X-00").is_err());
2073 assert_eq!(
2074 to_decimal_i128_result("+00X-00").unwrap_err(),
2075 CsdError::InvalidCharacter('X', 0)
2076 );
2077 }
2078
2079 // Tests for to_csdnnz_safe
2080 #[test]
2081 fn test_to_csdnnz_safe() {
2082 // Valid conversions
2083 let result = to_csdnnz_safe(28.5, 4).unwrap();
2084 let nnz_count = result.chars().filter(|c| *c == '+' || *c == '-').count();
2085 assert!(nnz_count <= 4);
2086
2087 assert_eq!(to_csdnnz_safe(-0.5, 4).unwrap(), "0.-");
2088 assert_eq!(to_csdnnz_safe(0.0, 4).unwrap(), "0");
2089 assert_eq!(to_csdnnz_safe(0.0, 0).unwrap(), "0");
2090 assert_eq!(to_csdnnz_safe(0.5, 4).unwrap(), "0.+");
2091
2092 // Error: non-zero value with 0 max_non_zeros
2093 let result = to_csdnnz_safe(28.5, 0);
2094 assert!(result.is_err());
2095 assert_eq!(
2096 result.unwrap_err(),
2097 CsdError::InvalidFormat(
2098 "Cannot represent non-zero value with 0 non-zero digits".to_string()
2099 )
2100 );
2101 }
2102
2103 // Tests for to_csdnnz_i64
2104 #[test]
2105 fn test_to_csdnnz_i64_explicit() {
2106 // Check that the result has at most the specified number of non-zero digits
2107 let csd = to_csdnnz_i64(28, 4);
2108 let nnz_count = csd.chars().filter(|c| *c == '+' || *c == '-').count();
2109 assert!(nnz_count <= 4);
2110
2111 assert_eq!(to_csdnnz_i64(0, 4), "0");
2112 assert_eq!(to_csdnnz_i64(0, 0), "0");
2113
2114 // Check that with 2 non-zero digits, we get at most 2 non-zeros
2115 let csd2 = to_csdnnz_i64(158, 2);
2116 let nnz_count = csd2.chars().filter(|c| *c == '+' || *c == '-').count();
2117 assert!(nnz_count <= 2);
2118
2119 // Test negative numbers
2120 let csd3 = to_csdnnz_i64(-28, 4);
2121 let nnz_count3 = csd3.chars().filter(|c| *c == '+' || *c == '-').count();
2122 assert!(nnz_count3 <= 4);
2123
2124 // Test large numbers
2125 let csd4 = to_csdnnz_i64(1000000, 5);
2126 let nnz_count4 = csd4.chars().filter(|c| *c == '+' || *c == '-').count();
2127 assert!(nnz_count4 <= 5);
2128 }
2129
2130 // Tests for to_csdnnz_i128
2131 #[test]
2132 fn test_to_csdnnz_i128_explicit() {
2133 // Check that the result has at most the specified number of non-zero digits
2134 let csd = to_csdnnz_i128(28, 4);
2135 let nnz_count = csd.chars().filter(|c| *c == '+' || *c == '-').count();
2136 assert!(nnz_count <= 4);
2137
2138 assert_eq!(to_csdnnz_i128(0, 4), "0");
2139 assert_eq!(to_csdnnz_i128(0, 0), "0");
2140
2141 // Check that with 2 non-zero digits, we get at most 2 non-zeros
2142 let csd2 = to_csdnnz_i128(158, 2);
2143 let nnz_count = csd2.chars().filter(|c| *c == '+' || *c == '-').count();
2144 assert!(nnz_count <= 2);
2145
2146 // Test negative numbers
2147 let csd3 = to_csdnnz_i128(-28, 4);
2148 let nnz_count3 = csd3.chars().filter(|c| *c == '+' || *c == '-').count();
2149 assert!(nnz_count3 <= 4);
2150
2151 // Test very large numbers
2152 let csd4 = to_csdnnz_i128(1000000000000i128, 5);
2153 let nnz_count4 = csd4.chars().filter(|c| *c == '+' || *c == '-').count();
2154 assert!(nnz_count4 <= 5);
2155 }
2156
2157 // Additional tests for to_decimal_result
2158 #[test]
2159 fn test_to_decimal_result_multiple_decimal_points() {
2160 let result = to_decimal_result("+.0.");
2161 assert!(result.is_err());
2162 // Note: This will return InvalidCharacter for '.' since '.' is not a valid CSD digit
2163 // The multiple decimal point check happens after character validation
2164 let err = result.unwrap_err();
2165 assert!(matches!(
2166 err,
2167 CsdError::InvalidCharacter(_, _) | CsdError::InvalidFormat(_)
2168 ));
2169 }
2170
2171 #[test]
2172 fn test_to_decimal_result_empty_string() {
2173 let result = to_decimal_result("");
2174 assert!(result.is_err());
2175 assert_eq!(result.unwrap_err(), CsdError::EmptyString);
2176 }
2177
2178 #[test]
2179 fn test_to_decimal_result_consecutive_non_zero() {
2180 // Note: to_decimal_result doesn't validate consecutive non-zero digits
2181 // It only validates characters and multiple decimal points
2182 // So "++" should pass validation but to_decimal_safe will fail
2183 let result = to_decimal_result("++");
2184 assert!(result.is_err()); // Will fail in to_decimal_safe
2185 }
2186}