use std::borrow::Cow;
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct FormatOptions {
pub number: NumberFormatOption,
pub custom: Option<Cow<'static, str>>,
}
impl FormatOptions {
pub fn builder() -> Builder {
Builder {
radix: 10,
prefix: NumberPrefixPolicy::Forbidden,
custom: None,
}
}
}
#[derive(Clone, Debug)]
pub struct Builder {
radix: u32,
prefix: NumberPrefixPolicy,
custom: Option<Cow<'static, str>>,
}
impl Builder {
pub fn binary(mut self) -> Self {
self.radix = 2;
self
}
pub fn octal(mut self) -> Self {
self.radix = 8;
self
}
pub fn decimal(mut self) -> Self {
self.radix = 10;
self
}
pub fn hex(mut self) -> Self {
self.radix = 16;
self
}
#[track_caller]
pub fn custom_radix(mut self, radix: u32) -> Self {
if !(2..=36).contains(&radix) {
panic!("Radix must be in the range 2..=36, got {radix}");
}
self.radix = radix;
self
}
pub fn with_optional_prefix(mut self) -> Self {
self.prefix = NumberPrefixPolicy::Optional;
self
}
pub fn with_prefix(mut self) -> Self {
self.prefix = NumberPrefixPolicy::Required;
self
}
pub fn with_custom_string(mut self, custom: impl Into<Cow<'static, str>>) -> Self {
self.custom = Some(custom.into());
self
}
#[track_caller]
pub fn build(self) -> FormatOptions {
use NumberFormatOption::*;
use NumberPrefixPolicy::*;
FormatOptions {
number: match (self.radix, self.prefix) {
(2, policy) => Binary(policy),
(8, policy) => Octal(policy),
(10, Forbidden) => Decimal,
(10, _) => panic!("Decimal format (the default) does not allow a prefix"),
(16, policy) => Hexadecimal(policy),
(radix, Forbidden) => Other(CustomRadix::new(radix).unwrap()), (radix, _) => panic!("Custom radix {radix} does not allow a prefix"),
},
custom: self.custom,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum NumberFormatOption {
Binary(NumberPrefixPolicy),
Octal(NumberPrefixPolicy),
#[default]
Decimal,
Hexadecimal(NumberPrefixPolicy),
Other(CustomRadix),
}
impl NumberFormatOption {
pub fn to_number(self) -> u32 {
match self {
Self::Binary(_) => 2,
Self::Octal(_) => 8,
Self::Decimal => 10,
Self::Hexadecimal(_) => 16,
Self::Other(base) => base.0,
}
}
pub fn prefix_policy(self) -> NumberPrefixPolicy {
match self {
Self::Binary(policy) | Self::Octal(policy) | Self::Hexadecimal(policy) => policy,
Self::Decimal | Self::Other(_) => NumberPrefixPolicy::Forbidden,
}
}
pub fn prefix(self) -> Option<&'static str> {
use NumberPrefixPolicy::*;
match self {
Self::Binary(Optional | Required) => Some("0b"),
Self::Octal(Optional | Required) => Some("0o"),
Self::Hexadecimal(Optional | Required) => Some("0x"),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CustomRadix(u32);
impl CustomRadix {
pub const fn new(radix: u32) -> Result<Self, &'static str> {
if radix < 2 || radix > 36 {
return Err("Radix must be in the range 2..=36");
}
if matches!(radix, 2 | 8 | 10 | 16) {
return Err("Radix 2, 8, 10, and 16 are covered by other variants");
}
Ok(Self(radix))
}
pub fn value(self) -> u32 {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NumberPrefixPolicy {
Forbidden,
Optional,
Required,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_custom_radix_new() {
assert!(CustomRadix::new(1).is_err());
assert!(CustomRadix::new(3).is_ok());
assert!(CustomRadix::new(36).is_ok());
assert!(CustomRadix::new(37).is_err());
assert!(CustomRadix::new(2).is_err());
assert!(CustomRadix::new(8).is_err());
assert!(CustomRadix::new(10).is_err());
assert!(CustomRadix::new(16).is_err());
let valid_ranges = [3..=7, 9..=9, 11..=15, 17..=36];
for range in valid_ranges {
for radix in range {
assert!(
CustomRadix::new(radix).is_ok(),
"CustomRadix::new({radix}) should be valid"
);
}
}
}
#[test]
fn test_to_number() {
let options = NumberFormatOption::Binary(NumberPrefixPolicy::Forbidden);
assert_eq!(options.to_number(), 2);
let options = NumberFormatOption::Octal(NumberPrefixPolicy::Forbidden);
assert_eq!(options.to_number(), 8);
let options = NumberFormatOption::Decimal;
assert_eq!(options.to_number(), 10);
let options = NumberFormatOption::Hexadecimal(NumberPrefixPolicy::Forbidden);
assert_eq!(options.to_number(), 16);
let radix = CustomRadix::new(3).unwrap();
let options = NumberFormatOption::Other(radix);
assert_eq!(options.to_number(), 3);
}
#[test]
fn test_builder_mapping() {
let opts = FormatOptions::builder().custom_radix(2).build();
assert!(matches!(opts.number, NumberFormatOption::Binary(_)));
let opts = FormatOptions::builder().custom_radix(8).build();
assert!(matches!(opts.number, NumberFormatOption::Octal(_)));
let opts = FormatOptions::builder().custom_radix(10).build();
assert!(matches!(opts.number, NumberFormatOption::Decimal));
let opts = FormatOptions::builder().custom_radix(16).build();
assert!(matches!(opts.number, NumberFormatOption::Hexadecimal(_)));
}
#[test]
fn test_builder_custom() {
let opts = FormatOptions::builder().custom_radix(3).build();
assert_eq!(opts.number, NumberFormatOption::Other(CustomRadix(3)));
}
#[test]
fn test_prefix_policies() {
let opts = FormatOptions::builder()
.binary()
.with_optional_prefix()
.build();
assert_eq!(opts.number.prefix_policy(), NumberPrefixPolicy::Optional);
assert_eq!(opts.number.prefix(), Some("0b"));
let opts = FormatOptions::builder().octal().with_prefix().build();
assert_eq!(opts.number.prefix_policy(), NumberPrefixPolicy::Required);
assert_eq!(opts.number.prefix(), Some("0o"));
let opts = FormatOptions::builder().hex().build();
assert_eq!(opts.number.prefix_policy(), NumberPrefixPolicy::Forbidden);
assert_eq!(opts.number.prefix(), None);
}
#[test]
#[should_panic(expected = "Custom radix 3 does not allow a prefix")]
fn test_builder_custom_prefix_panic() {
FormatOptions::builder()
.custom_radix(3)
.with_optional_prefix()
.build();
}
#[test]
#[should_panic(expected = "Decimal format (the default) does not allow a prefix")]
fn test_builder_decimal_prefix_panic() {
FormatOptions::builder()
.decimal()
.with_optional_prefix()
.build();
}
}