pub fn estimate_tokens(s: &str) -> usize {
s.len().div_ceil(4)
}
pub fn ui_amount(raw: u128, decimals: u8) -> String {
if decimals == 0 {
return group_thousands(&raw.to_string());
}
let scale = 10u128.pow(decimals as u32);
let whole = raw / scale;
let frac = raw % scale;
if frac == 0 {
return group_thousands(&whole.to_string());
}
let frac_str = format!("{frac:0width$}", width = decimals as usize);
let frac_str = frac_str.trim_end_matches('0');
format!("{}.{}", group_thousands(&whole.to_string()), frac_str)
}
pub fn compact_amount(raw: u128, decimals: u8) -> String {
let scale = 10u128.pow(decimals as u32);
let whole = raw / scale;
match whole {
0..=9_999 => ui_amount(raw, decimals),
10_000..=999_999 => format!("{:.1}K", whole as f64 / 1_000.0),
1_000_000..=999_999_999 => format!("{:.1}M", whole as f64 / 1_000_000.0),
_ => format!("{:.1}B", whole as f64 / 1_000_000_000.0),
}
}
pub fn parse_amount(input: &str, decimals: u8) -> Result<u128, String> {
let s = input.trim();
if s.is_empty() {
return Err("amount is empty".into());
}
if s.starts_with('-') {
return Err("amount must not be negative".into());
}
let (whole, frac) = match s.split_once('.') {
Some((w, f)) => (w, f),
None => (s, ""),
};
if whole.is_empty() && frac.is_empty() {
return Err("amount is empty".into());
}
if !whole.chars().all(|c| c.is_ascii_digit()) || !frac.chars().all(|c| c.is_ascii_digit()) {
return Err(format!("`{}` is not a decimal number", clip(s, 24)));
}
if frac.len() > decimals as usize {
return Err(format!(
"amount has {} decimal places but the mint has {decimals}",
frac.len()
));
}
let padded = format!("{whole}{frac}{}", "0".repeat(decimals as usize - frac.len()));
let padded = if padded.is_empty() { "0" } else { &padded };
padded
.parse::<u128>()
.map_err(|_| "amount is too large".to_string())
}
pub fn percent_of(part: u128, whole: u128) -> f64 {
if whole == 0 {
return 0.0;
}
(part as f64 / whole as f64) * 100.0
}
fn group_thousands(digits: &str) -> String {
let mut out = String::with_capacity(digits.len() + digits.len() / 3);
let n = digits.len();
for (i, c) in digits.chars().enumerate() {
if i > 0 && (n - i) % 3 == 0 {
out.push(',');
}
out.push(c);
}
out
}
pub fn clip(s: &str, max: usize) -> String {
if s.chars().count() <= max {
return s.to_string();
}
let mut out: String = s.chars().take(max).collect();
out.push('…');
out
}
pub struct Budget {
lines: Vec<String>,
used: usize,
max_chars: usize,
dropped: usize,
}
impl Budget {
pub fn new(max_chars: usize) -> Self {
Self {
lines: Vec::new(),
used: 0,
max_chars,
dropped: 0,
}
}
pub fn push(&mut self, line: impl Into<String>) {
let line = line.into();
let cost = line.len() + 1;
if self.used + cost > self.max_chars {
self.dropped += 1;
return;
}
self.used += cost;
self.lines.push(line);
}
pub fn push_always(&mut self, line: impl Into<String>) {
let line = line.into();
self.used += line.len() + 1;
self.lines.push(line);
}
pub fn dropped(&self) -> usize {
self.dropped
}
pub fn render(&self) -> String {
let mut out = self.lines.join("\n");
if self.dropped > 0 {
out.push_str(&format!("\n(+{} more, omitted for length)", self.dropped));
}
out
}
}