gui: refactor amount formatting

This commit is contained in:
Aaron Carlucci 2024-07-16 15:47:38 +02:00
parent c6b8133286
commit 59035060e9

View File

@ -3,117 +3,124 @@ pub use bitcoin::Amount;
use crate::{color, component::text::*, widget::*};
pub fn amount<'a, T: 'a>(a: &Amount) -> Row<'a, T> {
amount_with_size(a, P1_SIZE)
render_amount(amount_as_string(*a), P1_SIZE)
}
pub fn amount_with_size<'a, T: 'a>(a: &Amount, size: u16) -> Row<'a, T> {
let spacing = if size > P1_SIZE { 10 } else { 5 };
let sats = format!("{:.8}", a.to_btc());
assert!(sats.len() >= 9);
let row = Row::new()
.spacing(spacing)
.push(split_digits(sats[0..sats.len() - 6].to_string(), size, true).into())
.push(if a.to_sat() < 1_000_000 {
split_digits(sats[sats.len() - 6..sats.len() - 3].to_string(), size, true).into()
} else {
Row::new()
.push(
text(sats[sats.len() - 6..sats.len() - 3].to_string())
.bold()
.size(size),
)
.into()
})
.push(if a.to_sat() < 1000 {
split_digits(sats[sats.len() - 3..sats.len()].to_string(), size, true).into()
} else {
Row::new()
.push(
text(sats[sats.len() - 3..sats.len()].to_string())
.bold()
.size(size),
)
.into()
});
Row::with_children(vec![
row.into(),
text("BTC").size(size).style(color::GREY_3).into(),
])
.spacing(spacing)
.align_items(iced::Alignment::Center)
render_amount(amount_as_string(*a), size)
}
pub fn unconfirmed_amount_with_size<'a, T: 'a>(a: &Amount, size: u16) -> Row<'a, T> {
let spacing = if size > P1_SIZE { 10 } else { 5 };
let sats = format!("{:.8}", a.to_btc());
assert!(sats.len() >= 9);
let row = Row::new()
.spacing(spacing)
.push(split_digits(sats[0..sats.len() - 6].to_string(), size, false).into())
.push(if a.to_sat() < 1_000_000 {
split_digits(
sats[sats.len() - 6..sats.len() - 3].to_string(),
size,
false,
)
.into()
} else {
Row::new()
.push(text(sats[sats.len() - 6..sats.len() - 3].to_string()).size(size))
.into()
})
.push(if a.to_sat() < 1000 {
split_digits(sats[sats.len() - 3..sats.len()].to_string(), size, false).into()
} else {
Row::new()
.push(text(sats[sats.len() - 3..sats.len()].to_string()).size(size))
.into()
});
Row::with_children(vec![
row.into(),
text("BTC").size(size).style(color::GREY_3).into(),
])
.spacing(spacing)
.align_items(iced::Alignment::Center)
render_unconfirmed_amount(amount_as_string(*a), size)
}
fn split_digits<'a, T: 'a>(mut s: String, size: u16, bold: bool) -> impl Into<Element<'a, T>> {
let prefixes = vec!["0.00", "0.0", "0.", "000", "00", "0"];
for prefix in prefixes {
if s.starts_with(prefix) {
let right = s.split_off(prefix.len());
return Row::new()
.push(text(s).size(size).style(color::GREY_3))
.push_maybe(if right.is_empty() {
None
} else if bold {
Some(text(right).bold().size(size))
} else {
Some(text(right).size(size))
});
}
}
//
// Helpers
//
// Format a BTC amount as a string for display.
fn amount_as_string(a: Amount) -> String {
let amount = a.to_btc().to_string();
// Reformat the integer portion of the amount with space separation.
let (integer, fraction) = match s.split_once('.') {
let (integer, fraction) = match amount.split_once('.') {
Some((i, f)) => (i, f),
None => (s.as_str(), "00 000 000"),
None => (amount.as_str(), "00000000"),
};
let mut integer = integer
let integer = format_amount_number_part(integer);
let fraction = format_amount_number_part(&format!("{:0<8}", fraction));
format!("{integer}.{fraction}")
}
// Format a "part" of a number string with spaces to fit display requirements.
// Currently using French formatting rules so digits are space-separated in groups
// of three, starting from the right side. Incidentally, this works for both the
// integer portion of the number as well as the fraction part.
// Ex:
// 1000 => 1 000
// 100000 => 100 000
fn format_amount_number_part(s: &str) -> String {
let mut part = s
.chars()
.collect::<Vec<_>>()
.rchunks(3)
.map(|c| c.iter().collect::<String>())
.collect::<Vec<_>>();
integer.reverse();
let integer = integer.join(" ");
s = format!("{integer}.{fraction}");
part.reverse();
if bold {
Row::new().push(text(s).bold().size(size))
} else {
Row::new().push(text(s).size(size))
part.join(" ")
}
// Helper functions split a string at the first occurence of a non-zero integer (where
// the amount starts).
fn split_at_first_non_zero(s: String) -> Option<(String, String)> {
for (index, c) in s.char_indices() {
if c.is_ascii_digit() && c != '0' {
let (before, after) = s.split_at(index);
return Some((before.to_string(), after.to_string()));
}
}
None
}
// Build the rendering elements for displaying a Bitcoin amount.
// The text should be bolded beginning where the BTC amount is non-zero.
fn render_amount<'a, T: 'a>(amount: String, size: u16) -> Row<'a, T> {
let spacing = if size > P1_SIZE { 10 } else { 5 };
let (before, after) = match split_at_first_non_zero(amount) {
Some((b, a)) => (b, a),
None => (String::from(""), String::from("")),
};
let row = Row::new()
.spacing(spacing)
.push(text(before).size(size).style(color::GREY_3))
.push(text(after).size(size).bold());
Row::with_children(vec![
row.into(),
text("BTC").size(size).style(color::GREY_3).into(),
])
.spacing(spacing)
.align_items(iced::Alignment::Center)
}
// Build the rendering elements for displaying a Bitcoin amount.
fn render_unconfirmed_amount<'a, T: 'a>(amount: String, size: u16) -> Row<'a, T> {
let spacing = if size > P1_SIZE { 10 } else { 5 };
let row = Row::new()
.spacing(spacing)
.push(text(amount).size(size).style(color::GREY_3));
Row::with_children(vec![
row.into(),
text("BTC").size(size).style(color::GREY_3).into(),
])
.spacing(spacing)
.align_items(iced::Alignment::Center)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_amount_as_str() {
assert_eq!(
"0.00 799 800",
amount_as_string(bitcoin::Amount::from_btc(0.00799800).unwrap())
);
assert_eq!(
"1 000.00 799 800",
amount_as_string(bitcoin::Amount::from_btc(1000.00799800).unwrap())
);
assert_eq!(
"1 000.00 000 000",
amount_as_string(bitcoin::Amount::from_btc(1000.0).unwrap())
);
}
}