Added `amount::Display` - configurable formatting

This significatnly refactors the amount formatting code to make
formatting more configurable. The main addition is the
`amount::Display` type which is a builder that can configure
denomination or other things (possibly more in the future).

Further, this makes all representations of numbers minimal by default,
so should be documented as a possibly-breaking change.

Because of the effort to support all other `fmt::Formatter` options this
required practically complete rewrite of `fmt_satoshi_in`. As a
byproduct I took the opportunity of removing one allocation from there.

Closes #709
This commit is contained in:
Martin Habovstiak 2021-11-23 17:40:28 +01:00
parent cb35766979
commit 4f1200d629
1 changed files with 468 additions and 46 deletions

View File

@ -42,7 +42,7 @@ pub enum Denomination {
impl Denomination { impl Denomination {
/// The number of decimal places more than a satoshi. /// The number of decimal places more than a satoshi.
fn precision(self) -> i32 { fn precision(self) -> i8 {
match self { match self {
Denomination::Bitcoin => -8, Denomination::Bitcoin => -8,
Denomination::MilliBitcoin => -5, Denomination::MilliBitcoin => -5,
@ -54,11 +54,10 @@ impl Denomination {
Denomination::MilliSatoshi => 3, Denomination::MilliSatoshi => 3,
} }
} }
}
impl fmt::Display for Denomination { /// Returns stringly representation of this
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn as_str(self) -> &'static str {
f.write_str(match *self { match self {
Denomination::Bitcoin => "BTC", Denomination::Bitcoin => "BTC",
Denomination::MilliBitcoin => "mBTC", Denomination::MilliBitcoin => "mBTC",
Denomination::MicroBitcoin => "uBTC", Denomination::MicroBitcoin => "uBTC",
@ -67,7 +66,13 @@ impl fmt::Display for Denomination {
Denomination::Bit => "bits", Denomination::Bit => "bits",
Denomination::Satoshi => "satoshi", Denomination::Satoshi => "satoshi",
Denomination::MilliSatoshi => "msat", Denomination::MilliSatoshi => "msat",
}) }
}
}
impl fmt::Display for Denomination {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(self.as_str())
} }
} }
@ -223,7 +228,7 @@ fn parse_signed_to_satoshi(
// into a less precise amount. That is not allowed unless // into a less precise amount. That is not allowed unless
// there are no decimals and the last digits are zeroes as // there are no decimals and the last digits are zeroes as
// many as the difference in precision. // many as the difference in precision.
let last_n = precision_diff.abs() as usize; let last_n = unsigned_abs(precision_diff).into();
if is_too_precise(s, last_n) { if is_too_precise(s, last_n) {
return Err(ParseAmountError::TooPrecise); return Err(ParseAmountError::TooPrecise);
} }
@ -275,43 +280,171 @@ fn parse_signed_to_satoshi(
Ok((is_negative, value)) Ok((is_negative, value))
} }
/// Options given by `fmt::Formatter`
struct FormatOptions {
fill: char,
align: Option<fmt::Alignment>,
width: Option<usize>,
precision: Option<usize>,
sign_plus: bool,
sign_aware_zero_pad: bool,
}
impl FormatOptions {
fn from_formatter(f: &fmt::Formatter) -> Self {
FormatOptions {
fill: f.fill(),
align: f.align(),
width: f.width(),
precision: f.precision(),
sign_plus: f.sign_plus(),
sign_aware_zero_pad: f.sign_aware_zero_pad(),
}
}
}
impl Default for FormatOptions {
fn default() -> Self {
FormatOptions {
fill: ' ',
align: None,
width: None,
precision: None,
sign_plus: false,
sign_aware_zero_pad: false,
}
}
}
fn dec_width(mut num: u64) -> usize {
let mut width = 1;
loop {
num /= 10;
if num == 0 {
break;
}
width += 1;
}
width
}
// NIH due to MSRV, impl copied from `core`
fn unsigned_abs(x: i8) -> u8 {
x.wrapping_abs() as u8
}
fn repeat_char(f: &mut dyn fmt::Write, c: char, count: usize) -> fmt::Result {
for _ in 0..count {
f.write_char(c)?;
}
Ok(())
}
/// Format the given satoshi amount in the given denomination. /// Format the given satoshi amount in the given denomination.
///
/// Does not include the denomination.
fn fmt_satoshi_in( fn fmt_satoshi_in(
satoshi: u64, satoshi: u64,
negative: bool, negative: bool,
f: &mut dyn fmt::Write, f: &mut dyn fmt::Write,
denom: Denomination, denom: Denomination,
show_denom: bool,
options: FormatOptions,
) -> fmt::Result { ) -> fmt::Result {
if negative { let precision = denom.precision();
f.write_str("-")?; // First we normalize the number:
// {num_before_decimal_point}{:0exp}{"." if nb_decimals > 0}{:0nb_decimals}{num_after_decimal_point}{:0trailing_decimal_zeros}
let mut num_after_decimal_point = 0;
let mut norm_nb_decimals = 0;
let mut num_before_decimal_point = satoshi;
let trailing_decimal_zeros;
let mut exp = 0;
match precision.cmp(&0) {
// We add the number of zeroes to the end
Ordering::Greater => {
if satoshi > 0 {
exp = precision as usize;
}
trailing_decimal_zeros = options.precision.unwrap_or(0);
},
Ordering::Less => {
let precision = unsigned_abs(precision);
let divisor = 10u64.pow(precision.into());
num_before_decimal_point = satoshi / divisor;
num_after_decimal_point = satoshi % divisor;
// normalize by stripping trailing zeros
if num_after_decimal_point == 0 {
norm_nb_decimals = 0;
} else {
norm_nb_decimals = usize::from(precision);
while num_after_decimal_point % 10 == 0 {
norm_nb_decimals -= 1;
num_after_decimal_point /= 10
}
}
// compute requested precision
let opt_precision = options.precision.unwrap_or(0);
trailing_decimal_zeros = opt_precision.saturating_sub(norm_nb_decimals);
},
Ordering::Equal => trailing_decimal_zeros = options.precision.unwrap_or(0),
}
let total_decimals = norm_nb_decimals + trailing_decimal_zeros;
// Compute expected width of the number
let mut num_width = if total_decimals > 0 {
// 1 for decimal point
1 + total_decimals
} else {
0
};
num_width += dec_width(num_before_decimal_point) + exp;
if options.sign_plus || negative {
num_width += 1;
} }
let precision = denom.precision(); if show_denom {
match precision.cmp(&0) { // + 1 for space
Ordering::Greater => { num_width += denom.as_str().len() + 1;
// add zeroes in the end
let width = precision as usize;
write!(f, "{}{:0width$}", satoshi, 0, width = width)?;
} }
Ordering::Less => {
// need to inject a comma in the number let width = options.width.unwrap_or(0);
let nb_decimals = precision.abs() as usize; let (left_pad, pad_right) = match (num_width < width, options.sign_aware_zero_pad, options.align.unwrap_or(fmt::Alignment::Right)) {
let real = format!("{:0width$}", satoshi, width = nb_decimals); (false, _, _) => (0, 0),
if real.len() == nb_decimals { // Alignment is always right (ignored) when zero-padding
write!(f, "0.{}", &real[real.len() - nb_decimals..])?; (true, true, _) | (true, false, fmt::Alignment::Right) => (width - num_width, 0),
} else { (true, false, fmt::Alignment::Left) => (0, width - num_width),
write!( // If the required padding is odd it needs to be skewed to the left
f, (true, false, fmt::Alignment::Center) => ((width - num_width) / 2, (width - num_width + 1) / 2),
"{}.{}", };
&real[0..(real.len() - nb_decimals)],
&real[real.len() - nb_decimals..] if !options.sign_aware_zero_pad {
)?; repeat_char(f, options.fill, left_pad)?;
} }
if negative {
write!(f, "-")?;
} else if options.sign_plus {
write!(f, "+")?;
} }
Ordering::Equal => write!(f, "{}", satoshi)?,
if options.sign_aware_zero_pad {
repeat_char(f, '0', left_pad)?;
} }
write!(f, "{}", num_before_decimal_point)?;
repeat_char(f, '0', exp)?;
if total_decimals > 0 {
write!(f, ".")?;
}
if norm_nb_decimals > 0 {
write!(f, "{:0width$}", num_after_decimal_point, width = norm_nb_decimals)?;
}
repeat_char(f, '0', trailing_decimal_zeros)?;
if show_denom {
write!(f, " {}", denom.as_str())?;
}
repeat_char(f, options.fill, pad_right)?;
Ok(()) Ok(())
} }
@ -430,11 +563,32 @@ impl Amount {
Amount::from_str_in(&value.to_string(), denom) Amount::from_str_in(&value.to_string(), denom)
} }
/// Create an object that implements [`fmt::Display`] using specified denomination.
pub fn display_in(self, denomination: Denomination) -> Display {
Display {
sats_abs: self.as_sat(),
is_negative: false,
style: DisplayStyle::FixedDenomination { denomination, show_denomination: false, },
}
}
/// Create an object that implements [`fmt::Display`] dynamically selecting denomination.
///
/// This will use BTC for values greater than or equal to 1 BTC and satoshis otherwise. To
/// avoid confusion the denomination is always shown.
pub fn display_dynamic(self) -> Display {
Display {
sats_abs: self.as_sat(),
is_negative: false,
style: DisplayStyle::DynamicDenomination,
}
}
/// Format the value of this [Amount] in the given denomination. /// Format the value of this [Amount] in the given denomination.
/// ///
/// Does not include the denomination. /// Does not include the denomination.
pub fn fmt_value_in(self, f: &mut dyn fmt::Write, denom: Denomination) -> fmt::Result { pub fn fmt_value_in(self, f: &mut dyn fmt::Write, denom: Denomination) -> fmt::Result {
fmt_satoshi_in(self.as_sat(), false, f, denom) fmt_satoshi_in(self.as_sat(), false, f, denom, false, FormatOptions::default())
} }
/// Get a string number of this [Amount] in the given denomination. /// Get a string number of this [Amount] in the given denomination.
@ -605,6 +759,62 @@ impl ::core::iter::Sum for Amount {
} }
} }
/// A helper/builder that displays amount with specified settings.
///
/// This provides richer interface than `fmt::Formatter`:
///
/// * Ability to select denomination
/// * Show or hide denomination
/// * Dynamically-selected denomination - show in sats if less than 1 BTC.
///
/// However this can still be combined with `fmt::Formatter` options to precisely control zeros,
/// padding, alignment... The formatting works like floats from `core` but note that precision will
/// **never** be lossy - that means no rounding.
///
/// See [`Amount::display_in`] and [`Amount::display_dynamic`] on how to construct this.
#[derive(Debug, Clone)]
pub struct Display {
/// Absolute value of satoshis to display (sign is below)
sats_abs: u64,
/// The sign
is_negative: bool,
/// How to display the value
style: DisplayStyle,
}
impl Display {
/// Makes subsequent calls to `Display::fmt` display denomination.
pub fn show_denomination(mut self) -> Self {
match &mut self.style {
DisplayStyle::FixedDenomination { show_denomination, .. } => *show_denomination = true,
// No-op because dynamic denomination is always shown
DisplayStyle::DynamicDenomination => (),
}
self
}
}
impl fmt::Display for Display {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let format_options = FormatOptions::from_formatter(f);
match &self.style {
DisplayStyle::FixedDenomination { show_denomination, denomination } => fmt_satoshi_in(self.sats_abs, self.is_negative, f, *denomination, *show_denomination, format_options),
DisplayStyle::DynamicDenomination if self.sats_abs >= Amount::ONE_BTC.as_sat() => {
fmt_satoshi_in(self.sats_abs, self.is_negative, f, Denomination::Bitcoin, true, format_options)
},
DisplayStyle::DynamicDenomination => {
fmt_satoshi_in(self.sats_abs, self.is_negative, f, Denomination::Satoshi, true, format_options)
},
}
}
}
#[derive(Clone, Debug)]
enum DisplayStyle {
FixedDenomination { denomination: Denomination, show_denomination: bool, },
DynamicDenomination,
}
/// SignedAmount /// SignedAmount
/// ///
/// The [SignedAmount] type can be used to express Bitcoin amounts that supports /// The [SignedAmount] type can be used to express Bitcoin amounts that supports
@ -717,15 +927,40 @@ impl SignedAmount {
SignedAmount::from_str_in(&value.to_string(), denom) SignedAmount::from_str_in(&value.to_string(), denom)
} }
/// Returns the absolute value as satoshis.
///
/// This is the implementation of `unsigned_abs()` copied from `core` to support older MSRV.
fn to_sat_abs(self) -> u64 {
self.as_sat().wrapping_abs() as u64
}
/// Create an object that implements [`fmt::Display`] using specified denomination.
pub fn display_in(self, denomination: Denomination) -> Display {
Display {
sats_abs: self.to_sat_abs(),
is_negative: self.is_negative(),
style: DisplayStyle::FixedDenomination { denomination, show_denomination: false, },
}
}
/// Create an object that implements [`fmt::Display`] dynamically selecting denomination.
///
/// This will use BTC for values greater than or equal to 1 BTC and satoshis otherwise. To
/// avoid confusion the denomination is always shown.
pub fn display_dynamic(self) -> Display {
Display {
sats_abs: self.to_sat_abs(),
is_negative: self.is_negative(),
style: DisplayStyle::DynamicDenomination,
}
}
/// Format the value of this [SignedAmount] in the given denomination. /// Format the value of this [SignedAmount] in the given denomination.
/// ///
/// Does not include the denomination. /// Does not include the denomination.
pub fn fmt_value_in(self, f: &mut dyn fmt::Write, denom: Denomination) -> fmt::Result { pub fn fmt_value_in(self, f: &mut dyn fmt::Write, denom: Denomination) -> fmt::Result {
let sats = self.as_sat().checked_abs().map(|a: i64| a as u64).unwrap_or_else(|| {
// We could also hard code this into `9223372036854775808` fmt_satoshi_in(self.to_sat_abs(), self.is_negative(), f, denom, false, FormatOptions::default())
u64::max_value() - self.as_sat() as u64 +1
});
fmt_satoshi_in(sats, self.is_negative(), f, denom)
} }
/// Get a string number of this [SignedAmount] in the given denomination. /// Get a string number of this [SignedAmount] in the given denomination.
@ -1374,12 +1609,13 @@ mod tests {
fn to_string() { fn to_string() {
use super::Denomination as D; use super::Denomination as D;
assert_eq!(Amount::ONE_BTC.to_string_in(D::Bitcoin), "1.00000000"); assert_eq!(Amount::ONE_BTC.to_string_in(D::Bitcoin), "1");
assert_eq!(format!("{:.8}", Amount::ONE_BTC.display_in(D::Bitcoin)), "1.00000000");
assert_eq!(Amount::ONE_BTC.to_string_in(D::Satoshi), "100000000"); assert_eq!(Amount::ONE_BTC.to_string_in(D::Satoshi), "100000000");
assert_eq!(Amount::ONE_SAT.to_string_in(D::Bitcoin), "0.00000001"); assert_eq!(Amount::ONE_SAT.to_string_in(D::Bitcoin), "0.00000001");
assert_eq!(SignedAmount::from_sat(-42).to_string_in(D::Bitcoin), "-0.00000042"); assert_eq!(SignedAmount::from_sat(-42).to_string_in(D::Bitcoin), "-0.00000042");
assert_eq!(Amount::ONE_BTC.to_string_with_denomination(D::Bitcoin), "1.00000000 BTC"); assert_eq!(Amount::ONE_BTC.to_string_with_denomination(D::Bitcoin), "1 BTC");
assert_eq!(Amount::ONE_SAT.to_string_with_denomination(D::MilliSatoshi), "1000 msat"); assert_eq!(Amount::ONE_SAT.to_string_with_denomination(D::MilliSatoshi), "1000 msat");
assert_eq!( assert_eq!(
SignedAmount::ONE_BTC.to_string_with_denomination(D::Satoshi), SignedAmount::ONE_BTC.to_string_with_denomination(D::Satoshi),
@ -1392,6 +1628,188 @@ mod tests {
); );
} }
// May help identify a problem sooner
#[test]
fn test_repeat_char() {
let mut buf = String::new();
repeat_char(&mut buf, '0', 0).unwrap();
assert_eq!(buf.len(), 0);
repeat_char(&mut buf, '0', 42).unwrap();
assert_eq!(buf.len(), 42);
assert!(buf.chars().all(|c| c == '0'));
}
// Creates individual test functions to make it easier to find which check failed.
macro_rules! check_format_non_negative {
($denom:ident; $($test_name:ident, $val:expr, $format_string:expr, $expected:expr);* $(;)*) => {
$(
#[test]
fn $test_name() {
assert_eq!(format!($format_string, Amount::from_sat($val).display_in(Denomination::$denom)), $expected);
assert_eq!(format!($format_string, SignedAmount::from_sat($val as i64).display_in(Denomination::$denom)), $expected);
}
)*
}
}
macro_rules! check_format_non_negative_show_denom {
($denom:ident, $denom_suffix:expr; $($test_name:ident, $val:expr, $format_string:expr, $expected:expr);* $(;)*) => {
$(
#[test]
fn $test_name() {
assert_eq!(format!($format_string, Amount::from_sat($val).display_in(Denomination::$denom).show_denomination()), concat!($expected, $denom_suffix));
assert_eq!(format!($format_string, SignedAmount::from_sat($val as i64).display_in(Denomination::$denom).show_denomination()), concat!($expected, $denom_suffix));
}
)*
}
}
check_format_non_negative! {
Satoshi;
sat_check_fmt_non_negative_0, 0, "{}", "0";
sat_check_fmt_non_negative_1, 0, "{:2}", " 0";
sat_check_fmt_non_negative_2, 0, "{:02}", "00";
sat_check_fmt_non_negative_3, 0, "{:.1}", "0.0";
sat_check_fmt_non_negative_4, 0, "{:4.1}", " 0.0";
sat_check_fmt_non_negative_5, 0, "{:04.1}", "00.0";
sat_check_fmt_non_negative_6, 1, "{}", "1";
sat_check_fmt_non_negative_7, 1, "{:2}", " 1";
sat_check_fmt_non_negative_8, 1, "{:02}", "01";
sat_check_fmt_non_negative_9, 1, "{:.1}", "1.0";
sat_check_fmt_non_negative_10, 1, "{:4.1}", " 1.0";
sat_check_fmt_non_negative_11, 1, "{:04.1}", "01.0";
sat_check_fmt_non_negative_12, 10, "{}", "10";
sat_check_fmt_non_negative_13, 10, "{:2}", "10";
sat_check_fmt_non_negative_14, 10, "{:02}", "10";
sat_check_fmt_non_negative_15, 10, "{:3}", " 10";
sat_check_fmt_non_negative_16, 10, "{:03}", "010";
sat_check_fmt_non_negative_17, 10, "{:.1}", "10.0";
sat_check_fmt_non_negative_18, 10, "{:5.1}", " 10.0";
sat_check_fmt_non_negative_19, 10, "{:05.1}", "010.0";
sat_check_fmt_non_negative_20, 1, "{:<2}", "1 ";
sat_check_fmt_non_negative_21, 1, "{:<02}", "01";
sat_check_fmt_non_negative_22, 1, "{:<3.1}", "1.0";
sat_check_fmt_non_negative_23, 1, "{:<4.1}", "1.0 ";
}
check_format_non_negative_show_denom! {
Satoshi, " satoshi";
sat_check_fmt_non_negative_show_denom_0, 0, "{}", "0";
sat_check_fmt_non_negative_show_denom_1, 0, "{:2}", "0";
sat_check_fmt_non_negative_show_denom_2, 0, "{:02}", "0";
sat_check_fmt_non_negative_show_denom_3, 0, "{:9}", "0";
sat_check_fmt_non_negative_show_denom_4, 0, "{:09}", "0";
sat_check_fmt_non_negative_show_denom_5, 0, "{:10}", " 0";
sat_check_fmt_non_negative_show_denom_6, 0, "{:010}", "00";
sat_check_fmt_non_negative_show_denom_7, 0, "{:.1}", "0.0";
sat_check_fmt_non_negative_show_denom_8, 0, "{:11.1}", "0.0";
sat_check_fmt_non_negative_show_denom_9, 0, "{:011.1}", "0.0";
sat_check_fmt_non_negative_show_denom_10, 0, "{:12.1}", " 0.0";
sat_check_fmt_non_negative_show_denom_11, 0, "{:012.1}", "00.0";
sat_check_fmt_non_negative_show_denom_12, 1, "{}", "1";
sat_check_fmt_non_negative_show_denom_13, 1, "{:10}", " 1";
sat_check_fmt_non_negative_show_denom_14, 1, "{:010}", "01";
sat_check_fmt_non_negative_show_denom_15, 1, "{:.1}", "1.0";
sat_check_fmt_non_negative_show_denom_16, 1, "{:12.1}", " 1.0";
sat_check_fmt_non_negative_show_denom_17, 1, "{:012.1}", "01.0";
sat_check_fmt_non_negative_show_denom_18, 10, "{}", "10";
sat_check_fmt_non_negative_show_denom_19, 10, "{:10}", "10";
sat_check_fmt_non_negative_show_denom_20, 10, "{:010}", "10";
sat_check_fmt_non_negative_show_denom_21, 10, "{:11}", " 10";
sat_check_fmt_non_negative_show_denom_22, 10, "{:011}", "010";
}
check_format_non_negative! {
Bitcoin;
btc_check_fmt_non_negative_0, 0, "{}", "0";
btc_check_fmt_non_negative_1, 0, "{:2}", " 0";
btc_check_fmt_non_negative_2, 0, "{:02}", "00";
btc_check_fmt_non_negative_3, 0, "{:.1}", "0.0";
btc_check_fmt_non_negative_4, 0, "{:4.1}", " 0.0";
btc_check_fmt_non_negative_5, 0, "{:04.1}", "00.0";
btc_check_fmt_non_negative_6, 1, "{}", "0.00000001";
btc_check_fmt_non_negative_7, 1, "{:2}", "0.00000001";
btc_check_fmt_non_negative_8, 1, "{:02}", "0.00000001";
btc_check_fmt_non_negative_9, 1, "{:.1}", "0.00000001";
btc_check_fmt_non_negative_10, 1, "{:11}", " 0.00000001";
btc_check_fmt_non_negative_11, 1, "{:11.1}", " 0.00000001";
btc_check_fmt_non_negative_12, 1, "{:011.1}", "00.00000001";
btc_check_fmt_non_negative_13, 1, "{:.9}", "0.000000010";
btc_check_fmt_non_negative_14, 1, "{:11.9}", "0.000000010";
btc_check_fmt_non_negative_15, 1, "{:011.9}", "0.000000010";
btc_check_fmt_non_negative_16, 1, "{:12.9}", " 0.000000010";
btc_check_fmt_non_negative_17, 1, "{:012.9}", "00.000000010";
btc_check_fmt_non_negative_18, 100_000_000, "{}", "1";
btc_check_fmt_non_negative_19, 100_000_000, "{:2}", " 1";
btc_check_fmt_non_negative_20, 100_000_000, "{:02}", "01";
btc_check_fmt_non_negative_21, 100_000_000, "{:.1}", "1.0";
btc_check_fmt_non_negative_22, 100_000_000, "{:4.1}", " 1.0";
btc_check_fmt_non_negative_23, 100_000_000, "{:04.1}", "01.0";
btc_check_fmt_non_negative_24, 110_000_000, "{}", "1.1";
btc_check_fmt_non_negative_25, 100_000_001, "{}", "1.00000001";
btc_check_fmt_non_negative_26, 100_000_001, "{:1}", "1.00000001";
btc_check_fmt_non_negative_27, 100_000_001, "{:.1}", "1.00000001";
btc_check_fmt_non_negative_28, 100_000_001, "{:10}", "1.00000001";
btc_check_fmt_non_negative_29, 100_000_001, "{:11}", " 1.00000001";
btc_check_fmt_non_negative_30, 100_000_001, "{:011}", "01.00000001";
btc_check_fmt_non_negative_31, 100_000_001, "{:.8}", "1.00000001";
btc_check_fmt_non_negative_32, 100_000_001, "{:.9}", "1.000000010";
btc_check_fmt_non_negative_33, 100_000_001, "{:11.9}", "1.000000010";
btc_check_fmt_non_negative_34, 100_000_001, "{:12.9}", " 1.000000010";
btc_check_fmt_non_negative_35, 100_000_001, "{:012.9}", "01.000000010";
btc_check_fmt_non_negative_36, 100_000_001, "{:+011.8}", "+1.00000001";
btc_check_fmt_non_negative_37, 100_000_001, "{:+12.8}", " +1.00000001";
btc_check_fmt_non_negative_38, 100_000_001, "{:+012.8}", "+01.00000001";
btc_check_fmt_non_negative_39, 100_000_001, "{:+12.9}", "+1.000000010";
btc_check_fmt_non_negative_40, 100_000_001, "{:+012.9}", "+1.000000010";
btc_check_fmt_non_negative_41, 100_000_001, "{:+13.9}", " +1.000000010";
btc_check_fmt_non_negative_42, 100_000_001, "{:+013.9}", "+01.000000010";
btc_check_fmt_non_negative_43, 100_000_001, "{:<10}", "1.00000001";
btc_check_fmt_non_negative_44, 100_000_001, "{:<11}", "1.00000001 ";
btc_check_fmt_non_negative_45, 100_000_001, "{:<011}", "01.00000001";
btc_check_fmt_non_negative_46, 100_000_001, "{:<11.9}", "1.000000010";
btc_check_fmt_non_negative_47, 100_000_001, "{:<12.9}", "1.000000010 ";
btc_check_fmt_non_negative_48, 100_000_001, "{:<12}", "1.00000001 ";
btc_check_fmt_non_negative_49, 100_000_001, "{:^11}", "1.00000001 ";
btc_check_fmt_non_negative_50, 100_000_001, "{:^11.9}", "1.000000010";
btc_check_fmt_non_negative_51, 100_000_001, "{:^12.9}", "1.000000010 ";
btc_check_fmt_non_negative_52, 100_000_001, "{:^12}", " 1.00000001 ";
btc_check_fmt_non_negative_53, 100_000_001, "{:^12.9}", "1.000000010 ";
btc_check_fmt_non_negative_54, 100_000_001, "{:^13.9}", " 1.000000010 ";
}
check_format_non_negative_show_denom! {
Bitcoin, " BTC";
btc_check_fmt_non_negative_show_denom_0, 1, "{:14.1}", "0.00000001";
btc_check_fmt_non_negative_show_denom_1, 1, "{:14.8}", "0.00000001";
btc_check_fmt_non_negative_show_denom_2, 1, "{:15}", " 0.00000001";
btc_check_fmt_non_negative_show_denom_3, 1, "{:015}", "00.00000001";
btc_check_fmt_non_negative_show_denom_4, 1, "{:.9}", "0.000000010";
btc_check_fmt_non_negative_show_denom_5, 1, "{:15.9}", "0.000000010";
btc_check_fmt_non_negative_show_denom_6, 1, "{:16.9}", " 0.000000010";
btc_check_fmt_non_negative_show_denom_7, 1, "{:016.9}", "00.000000010";
}
check_format_non_negative_show_denom! {
Bitcoin, " BTC ";
btc_check_fmt_non_negative_show_denom_align_0, 1, "{:<15}", "0.00000001";
btc_check_fmt_non_negative_show_denom_align_1, 1, "{:^15}", "0.00000001";
btc_check_fmt_non_negative_show_denom_align_2, 1, "{:^16}", " 0.00000001";
}
check_format_non_negative! {
MilliSatoshi;
msat_check_fmt_non_negative_0, 0, "{}", "0";
msat_check_fmt_non_negative_1, 1, "{}", "1000";
msat_check_fmt_non_negative_2, 1, "{:5}", " 1000";
msat_check_fmt_non_negative_3, 1, "{:05}", "01000";
msat_check_fmt_non_negative_4, 1, "{:.1}", "1000.0";
msat_check_fmt_non_negative_5, 1, "{:6.1}", "1000.0";
msat_check_fmt_non_negative_6, 1, "{:06.1}", "1000.0";
msat_check_fmt_non_negative_7, 1, "{:7.1}", " 1000.0";
msat_check_fmt_non_negative_8, 1, "{:07.1}", "01000.0";
}
#[test] #[test]
fn test_unsigned_signed_conversion() { fn test_unsigned_signed_conversion() {
use super::ParseAmountError as E; use super::ParseAmountError as E;
@ -1459,18 +1877,22 @@ mod tests {
let sa_str = SignedAmount::from_str_in; let sa_str = SignedAmount::from_str_in;
let sa_sat = SignedAmount::from_sat; let sa_sat = SignedAmount::from_sat;
assert_eq!("0.50", Amount::from_sat(50).to_string_in(D::Bit)); assert_eq!("0.5", Amount::from_sat(50).to_string_in(D::Bit));
assert_eq!("-0.50", SignedAmount::from_sat(-50).to_string_in(D::Bit)); assert_eq!("-0.5", SignedAmount::from_sat(-50).to_string_in(D::Bit));
assert_eq!("0.00253583", Amount::from_sat(253583).to_string_in(D::Bitcoin)); assert_eq!("0.00253583", Amount::from_sat(253583).to_string_in(D::Bitcoin));
assert_eq!("-5", SignedAmount::from_sat(-5).to_string_in(D::Satoshi)); assert_eq!("-5", SignedAmount::from_sat(-5).to_string_in(D::Satoshi));
assert_eq!("0.10000000", Amount::from_sat(100_000_00).to_string_in(D::Bitcoin)); assert_eq!("0.1", Amount::from_sat(100_000_00).to_string_in(D::Bitcoin));
assert_eq!("-100.00", SignedAmount::from_sat(-10_000).to_string_in(D::Bit)); assert_eq!("-100", SignedAmount::from_sat(-10_000).to_string_in(D::Bit));
assert_eq!("2535830", Amount::from_sat(253583).to_string_in(D::NanoBitcoin)); assert_eq!("2535830", Amount::from_sat(253583).to_string_in(D::NanoBitcoin));
assert_eq!("-100000", SignedAmount::from_sat(-10_000).to_string_in(D::NanoBitcoin)); assert_eq!("-100000", SignedAmount::from_sat(-10_000).to_string_in(D::NanoBitcoin));
assert_eq!("2535830000", Amount::from_sat(253583).to_string_in(D::PicoBitcoin)); assert_eq!("2535830000", Amount::from_sat(253583).to_string_in(D::PicoBitcoin));
assert_eq!("-100000000", SignedAmount::from_sat(-10_000).to_string_in(D::PicoBitcoin)); assert_eq!("-100000000", SignedAmount::from_sat(-10_000).to_string_in(D::PicoBitcoin));
assert_eq!("0.50", format!("{:.2}", Amount::from_sat(50).display_in(D::Bit)));
assert_eq!("-0.50", format!("{:.2}", SignedAmount::from_sat(-50).display_in(D::Bit)));
assert_eq!("0.10000000", format!("{:.8}", Amount::from_sat(100_000_00).display_in(D::Bitcoin)));
assert_eq!("-100.00", format!("{:.2}", SignedAmount::from_sat(-10_000).display_in(D::Bit)));
assert_eq!(ua_str(&ua_sat(0).to_string_in(D::Satoshi), D::Satoshi), Ok(ua_sat(0))); assert_eq!(ua_str(&ua_sat(0).to_string_in(D::Satoshi), D::Satoshi), Ok(ua_sat(0)));
assert_eq!(ua_str(&ua_sat(500).to_string_in(D::Bitcoin), D::Bitcoin), Ok(ua_sat(500))); assert_eq!(ua_str(&ua_sat(500).to_string_in(D::Bitcoin), D::Bitcoin), Ok(ua_sat(500)));