#![doc(html_root_url = "https://docs.rs/thousands/0.1.3")]
use std::fmt::Display;
pub trait Separable {
fn separate_with_commas(&self) -> String {
self.separate_by_policy(policies::COMMA_SEPARATOR)
}
fn separate_with_spaces(&self) -> String {
self.separate_by_policy(policies::SPACE_SEPARATOR)
}
fn separate_with_dots(&self) -> String {
self.separate_by_policy(policies::DOT_SEPARATOR)
}
fn separate_with_underscores(&self) -> String {
self.separate_by_policy(policies::UNDERSCORE_SEPARATOR)
}
fn separate_by_policy(&self, policy: SeparatorPolicy) -> String;
}
impl<T: Display> Separable for T {
fn separate_by_policy(&self, policy: SeparatorPolicy) -> String {
let original = self.to_string();
let (before, number, after) = find_span(&original, |c| policy.digits.contains(&c));
let formatted = insert_separator_rev(number, policy.separator, policy.groups);
let mut result = String::with_capacity(before.len() + formatted.len() + after.len());
result.push_str(before);
result.extend(formatted.chars().rev());
result.push_str(after);
result
}
}
fn insert_separator_rev(number: &str, sep: char, mut groups: &[u8]) -> String {
let mut buffer = String::with_capacity(2 * number.len());
let mut counter = 0;
for c in number.chars().rev() {
if Some(&counter) == groups.get(0) {
buffer.push(sep);
counter = 0;
if groups.len() > 1 {
groups = &groups[1 ..];
}
}
counter += 1;
buffer.push(c);
}
buffer
}
fn find_span<F>(s: &str, is_digit: F) -> (&str, &str, &str) where F: Fn(char) -> bool {
let mut chars = s.chars().enumerate().skip_while(|&(_, c)| !is_digit(c));
let start = if let Some((i, _)) = chars.next() {
i
} else {
return (s, "", "");
};
let stop = if let Some((i, _)) = chars.skip_while(|&(_, c)| is_digit(c)).next() {
i
} else {
s.len()
};
(&s[.. start], &s[start .. stop], &s[stop ..])
}
#[derive(Debug, Clone, Copy)]
pub struct SeparatorPolicy<'a> {
pub separator: char,
pub groups: &'a [u8],
pub digits: &'a [char],
}
pub mod digits {
pub const ASCII_DECIMAL: &[char] = &[
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
];
pub const ASCII_HEXADECIMAL: &[char] = &[
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F',
];
}
pub mod policies {
use super::*;
use super::digits::*;
pub const COMMA_SEPARATOR: SeparatorPolicy = SeparatorPolicy {
separator: ',',
groups: &[3],
digits: ASCII_DECIMAL,
};
pub const SPACE_SEPARATOR: SeparatorPolicy = SeparatorPolicy {
separator: ' ',
groups: &[3],
digits: ASCII_DECIMAL,
};
pub const DOT_SEPARATOR: SeparatorPolicy = SeparatorPolicy {
separator: '.',
groups: &[3],
digits: ASCII_DECIMAL,
};
pub const UNDERSCORE_SEPARATOR: SeparatorPolicy = SeparatorPolicy {
separator: '_',
groups: &[3],
digits: ASCII_DECIMAL,
};
pub const HEX_FOUR: SeparatorPolicy = SeparatorPolicy {
separator: ' ',
groups: &[4],
digits: ASCII_HEXADECIMAL,
};
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn integer_thousands_commas() {
assert_eq!( 12345.separate_with_commas(),
"12,345" );
}
#[test]
fn three_two_two_two() {
let policy = SeparatorPolicy {
separator: ',',
groups: &[3, 2],
digits: &digits::ASCII_DECIMAL,
};
assert_eq!( 1234567890.separate_by_policy(policy),
"1,23,45,67,890" );
}
#[test]
fn minus_sign_and_decimal_point() {
assert_eq!( (-1234.5).separate_with_commas(),
"-1,234.5" );
}
#[test]
fn hex_four() {
assert_eq!( "deadbeef".separate_by_policy(policies::HEX_FOUR),
"dead beef" );
}
}