base_converter/
lib.rs

1//! Convert numbers from base to base.
2
3#[cfg(feature = "wasm")]
4#[allow(non_snake_case)]
5pub mod wasm;
6
7use anyhow::{ensure, Result};
8use thiserror::Error;
9
10/// 01
11pub static BASE2: &str = "01";
12/// 01234567
13pub static BASE8: &str = "01234567";
14/// 0123456789
15pub static BASE10: &str = "0123456789";
16/// 0123456789ABCDEF
17pub static BASE16: &str = "0123456789ABCDEF";
18
19/// Errors that can be returned by [check_base] and any function that takes
20/// a base as argument.
21#[derive(Error, Debug)]
22pub enum CheckBaseError {
23  /// Error occurring when a base length is inferior at 2.
24  #[error("length of base '{0}' must be at least 2")]
25  BaseLenTooShort(String),
26  /// Error occurring when a character appears more than once in a base.
27  #[error("base '{base}' has at least 2 occurrences of char '{c}'")]
28  DuplicateCharInBase { base: String, c: char },
29}
30
31/// Checks if a base is valid.
32///
33/// # Errors
34/// - [CheckBaseError]
35///
36/// # Examples
37/// ```
38/// use base_converter::check_base;
39/// check_base("0123456789").expect("valid hardcoded base");
40/// ```
41/// ```
42/// use base_converter::{check_base, CheckBaseError};
43/// let err = check_base("0").unwrap_err(); // Base must have at least 2 characters
44/// assert_eq!(format!("{}", err), format!("{}", CheckBaseError::BaseLenTooShort("0".into())));
45/// ```
46pub fn check_base(base: &str) -> Result<()> {
47  ensure!(
48    base.chars().count() >= 2,
49    CheckBaseError::BaseLenTooShort(base.into())
50  );
51  for c in base.chars() {
52    ensure!(
53      base.chars().filter(|c2| &c == c2).count() == 1,
54      CheckBaseError::DuplicateCharInBase {
55        base: base.into(),
56        c,
57      }
58    )
59  }
60  Ok(())
61}
62
63/// Errors that can be returned when converting a number between bases.
64#[derive(Error, Debug)]
65pub enum ConversionError {
66  /// Error occurring when a char in a number is not found from the base it should be in.
67  #[error("char '{c}' not found in base '{base}'")]
68  CharNotFoundInBase { base: String, c: char },
69  /// Error occurring when a number encoded in a base cannot be represented in an [usize].
70  #[error("base '{base}' of length {base_length} ** {power} overflowed")]
71  ConversionOverflow {
72    base: String,
73    base_length: usize,
74    power: u32,
75  },
76}
77
78/// Converts a number from any base to an [usize].
79///
80/// # Errors
81/// - [CheckBaseError]
82/// - [ConversionError]
83///
84/// # Examples
85/// ```
86/// use base_converter::base_to_decimal;
87/// let nbr = base_to_decimal("101010", "01").unwrap();
88/// assert_eq!(nbr, 42);
89/// ```
90pub fn base_to_decimal(nbr: &str, from_base: &str) -> Result<usize> {
91  check_base(from_base)?;
92  let base_length = from_base.chars().count();
93  let mut result: usize = 0;
94  for (c, i) in nbr.chars().zip((0..nbr.chars().count() as u32).rev()) {
95    let x = from_base.chars().position(|x| x == c).ok_or_else(|| {
96      ConversionError::CharNotFoundInBase {
97        base: from_base.into(),
98        c,
99      }
100    })?;
101    result += x
102      * base_length
103        .checked_pow(i)
104        .ok_or_else(|| ConversionError::ConversionOverflow {
105          base: from_base.into(),
106          base_length,
107          power: i,
108        })?;
109  }
110  Ok(result)
111}
112
113/// Converts an [usize] to another base.
114///
115/// # Errors
116/// - [CheckBaseError]
117///
118/// # Examples
119/// ```
120/// use base_converter::decimal_to_base;
121/// let nbr = decimal_to_base(51966, "0123456789ABCDEF").unwrap();
122/// assert_eq!(nbr, "CAFE");
123/// ```
124pub fn decimal_to_base(mut nbr: usize, to_base: &str) -> Result<String> {
125  check_base(to_base)?;
126  if nbr == 0 {
127    return Ok(to_base.chars().next().unwrap().into());
128  }
129  let base_length = to_base.chars().count();
130  let mut result = String::new();
131  while nbr > 0 {
132    result.push(to_base.chars().nth(nbr % base_length).unwrap());
133    nbr /= base_length;
134  }
135  Ok(result.chars().rev().collect())
136}
137
138/// Converts a number from any base to any other base.
139///
140/// # Errors
141/// - [CheckBaseError]
142/// - [ConversionError]
143///
144/// # Examples
145/// ```
146/// use base_converter::base_to_base;
147/// let nbr = base_to_base("🚀🚀🚀🦀🚀🦀🦀🚀🚀🦀🚀🚀🦀🦀🦀🚀🚀🦀🦀🦀🚀🦀🚀🦀🚀🚀🦀🦀🦀🦀🚀🚀🚀🚀🦀🚀🚀🦀🚀🚀🦀🦀🦀🚀🦀🦀🦀🦀", "🦀🚀", "abcdefghijklmnopqrstuvwxyz !🦀").unwrap();
148/// assert_eq!(nbr, "rust ftw 🦀")
149/// ```
150pub fn base_to_base(nbr: &str, from_base: &str, to_base: &str) -> Result<String> {
151  let nbr = base_to_decimal(nbr, from_base)?;
152  let nbr = decimal_to_base(nbr, to_base)?;
153  Ok(nbr)
154}
155
156#[cfg(test)]
157mod tests {
158  use super::*;
159
160  #[test]
161  fn test_check_base() {
162    struct TestCase {
163      base: &'static str,
164      err: CheckBaseError,
165    }
166    let test_cases = vec![
167      TestCase {
168        base: "",
169        err: CheckBaseError::BaseLenTooShort("".into()),
170      },
171      TestCase {
172        base: "x",
173        err: CheckBaseError::BaseLenTooShort("x".into()),
174      },
175      TestCase {
176        base: "xx",
177        err: CheckBaseError::DuplicateCharInBase {
178          base: "xx".into(),
179          c: 'x',
180        },
181      },
182    ];
183    for test_case in &test_cases {
184      assert_eq!(
185        format!("{}", check_base(test_case.base).unwrap_err()),
186        format!("{}", test_case.err)
187      );
188    }
189  }
190
191  #[test]
192  fn test_base_to_decimal() {
193    assert_eq!(51966, base_to_decimal("CAFE", BASE16).unwrap());
194    assert_eq!(42, base_to_decimal("101010", BASE2).unwrap());
195    assert_eq!(0, base_to_decimal("0", BASE8).unwrap());
196    assert_eq!(0, base_to_decimal("", BASE8).unwrap());
197  }
198
199  #[test]
200  fn test_decimal_to_base() {
201    assert_eq!("CAFE", decimal_to_base(51966, BASE16).unwrap());
202    assert_eq!("0", decimal_to_base(0, BASE8).unwrap());
203    assert_eq!("x", decimal_to_base(0, "xyz").unwrap());
204  }
205
206  #[test]
207  fn test_base_to_base() {
208    let err = base_to_base("25", "01234", "1123").unwrap_err();
209    assert_eq!(
210      format!("{}", err),
211      format!(
212        "{}",
213        ConversionError::CharNotFoundInBase {
214          base: "01234".into(),
215          c: '5'
216        }
217      )
218    );
219  }
220}