Skip to main content

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}