From 16c49df688b322cae31e5deb017ec9f5be519d44 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sat, 31 Dec 2022 14:19:56 -0800 Subject: [PATCH] Accept amounts with denominations with and without spaces --- bitcoin/src/amount.rs | 108 ++++++++++++++++++++++++------------------ 1 file changed, 62 insertions(+), 46 deletions(-) diff --git a/bitcoin/src/amount.rs b/bitcoin/src/amount.rs index d1c50744..3a7d2d0d 100644 --- a/bitcoin/src/amount.rs +++ b/bitcoin/src/amount.rs @@ -288,6 +288,16 @@ fn parse_signed_to_satoshi( Ok((is_negative, value)) } +fn split_amount_and_denomination(s: &str) -> Result<(&str, Denomination), ParseAmountError> { + let (i, j) = if let Some(i) = s.find(' ') { + (i, i + 1) + } else { + let i = s.find(|c: char| c.is_alphabetic()).ok_or(ParseAmountError::InvalidFormat)?; + (i, i) + }; + Ok((&s[..i], s[j..].parse()?)) +} + /// Options given by `fmt::Formatter` struct FormatOptions { fill: char, @@ -523,14 +533,8 @@ impl Amount { /// If you want to parse only the amount without the denomination, /// use [Self::from_str_in]. pub fn from_str_with_denomination(s: &str) -> Result { - let mut split = s.splitn(3, ' '); - let amt_str = split.next().unwrap(); - let denom_str = split.next().ok_or(ParseAmountError::InvalidFormat)?; - if split.next().is_some() { - return Err(ParseAmountError::InvalidFormat); - } - - Amount::from_str_in(amt_str, denom_str.parse()?) + let (amt, denom) = split_amount_and_denomination(s)?; + Amount::from_str_in(amt, denom) } /// Express this [Amount] as a floating-point value in the given denomination. @@ -864,14 +868,8 @@ impl SignedAmount { /// If you want to parse only the amount without the denomination, /// use [Self::from_str_in]. pub fn from_str_with_denomination(s: &str) -> Result { - let mut split = s.splitn(3, ' '); - let amt_str = split.next().unwrap(); - let denom_str = split.next().ok_or(ParseAmountError::InvalidFormat)?; - if split.next().is_some() { - return Err(ParseAmountError::InvalidFormat); - } - - SignedAmount::from_str_in(amt_str, denom_str.parse()?) + let (amt, denom) = split_amount_and_denomination(s)?; + SignedAmount::from_str_in(amt, denom) } /// Express this [SignedAmount] as a floating-point value in the given denomination. @@ -1896,39 +1894,51 @@ mod tests { #[allow(clippy::inconsistent_digit_grouping)] // Group to show 100,000,000 sats per bitcoin. fn from_str() { use super::ParseAmountError as E; - let p = Amount::from_str; - let sp = SignedAmount::from_str; - assert_eq!(p("x BTC"), Err(E::InvalidCharacter('x'))); - assert_eq!(p("5 BTC BTC"), Err(E::InvalidFormat)); - assert_eq!(p("5 5 BTC"), Err(E::InvalidFormat)); + assert_eq!(Amount::from_str("x BTC"), Err(E::InvalidCharacter('x'))); + assert_eq!(Amount::from_str("xBTC"), Err(E::UnknownDenomination("xBTC".into()))); + assert_eq!(Amount::from_str("5 BTC BTC"), Err(E::UnknownDenomination("BTC BTC".into()))); + assert_eq!(Amount::from_str("5BTC BTC"), Err(E::InvalidCharacter('B'))); + assert_eq!(Amount::from_str("5 5 BTC"), Err(E::UnknownDenomination("5 BTC".into()))); - assert_eq!(p("5 BCH"), Err(E::UnknownDenomination("BCH".to_owned()))); + #[cfg_attr(rust_v_1_46, track_caller)] + fn case(s: &str, expected: Result) { + assert_eq!(Amount::from_str(s), expected); + assert_eq!(Amount::from_str(&s.replace(' ', "")), expected); + } - assert_eq!(p("-1 BTC"), Err(E::Negative)); - assert_eq!(p("-0.0 BTC"), Err(E::Negative)); - assert_eq!(p("0.123456789 BTC"), Err(E::TooPrecise)); - assert_eq!(sp("-0.1 satoshi"), Err(E::TooPrecise)); - assert_eq!(p("0.123456 mBTC"), Err(E::TooPrecise)); - assert_eq!(sp("-1.001 bits"), Err(E::TooPrecise)); - assert_eq!(sp("-200000000000 BTC"), Err(E::TooBig)); - assert_eq!(p("18446744073709551616 sat"), Err(E::TooBig)); + #[cfg_attr(rust_v_1_46, track_caller)] + fn scase(s: &str, expected: Result) { + assert_eq!(SignedAmount::from_str(s), expected); + assert_eq!(SignedAmount::from_str(&s.replace(' ', "")), expected); + } - assert_eq!(sp("0 msat"), Err(E::TooPrecise)); - assert_eq!(sp("-0 msat"), Err(E::TooPrecise)); - assert_eq!(sp("000 msat"), Err(E::TooPrecise)); - assert_eq!(sp("-000 msat"), Err(E::TooPrecise)); - assert_eq!(p("0 msat"), Err(E::TooPrecise)); - assert_eq!(p("-0 msat"), Err(E::TooPrecise)); - assert_eq!(p("000 msat"), Err(E::TooPrecise)); - assert_eq!(p("-000 msat"), Err(E::TooPrecise)); + case("5 BCH", Err(E::UnknownDenomination("BCH".to_owned()))); - assert_eq!(p(".5 bits"), Ok(Amount::from_sat(50))); - assert_eq!(sp("-.5 bits"), Ok(SignedAmount::from_sat(-50))); - assert_eq!(p("0.00253583 BTC"), Ok(Amount::from_sat(253583))); - assert_eq!(sp("-5 satoshi"), Ok(SignedAmount::from_sat(-5))); - assert_eq!(p("0.10000000 BTC"), Ok(Amount::from_sat(100_000_00))); - assert_eq!(sp("-100 bits"), Ok(SignedAmount::from_sat(-10_000))); + case("-1 BTC", Err(E::Negative)); + case("-0.0 BTC", Err(E::Negative)); + case("0.123456789 BTC", Err(E::TooPrecise)); + scase("-0.1 satoshi", Err(E::TooPrecise)); + case("0.123456 mBTC", Err(E::TooPrecise)); + scase("-1.001 bits", Err(E::TooPrecise)); + scase("-200000000000 BTC", Err(E::TooBig)); + case("18446744073709551616 sat", Err(E::TooBig)); + + scase("0 msat", Err(E::TooPrecise)); + scase("-0 msat", Err(E::TooPrecise)); + scase("000 msat", Err(E::TooPrecise)); + scase("-000 msat", Err(E::TooPrecise)); + case("0 msat", Err(E::TooPrecise)); + case("-0 msat", Err(E::TooPrecise)); + case("000 msat", Err(E::TooPrecise)); + case("-000 msat", Err(E::TooPrecise)); + + case(".5 bits", Ok(Amount::from_sat(50))); + scase("-.5 bits", Ok(SignedAmount::from_sat(-50))); + case("0.00253583 BTC", Ok(Amount::from_sat(253583))); + scase("-5 satoshi", Ok(SignedAmount::from_sat(-5))); + case("0.10000000 BTC", Ok(Amount::from_sat(100_000_00))); + scase("-100 bits", Ok(SignedAmount::from_sat(-10_000))); } #[test] @@ -2034,8 +2044,14 @@ mod tests { assert_eq!(Amount::from_str(&denom(amt, D::MilliSatoshi)), Ok(amt)); assert_eq!(Amount::from_str(&denom(amt, D::PicoBitcoin)), Ok(amt)); - assert_eq!(Amount::from_str("42 satoshi BTC"), Err(ParseAmountError::InvalidFormat)); - assert_eq!(SignedAmount::from_str("-42 satoshi BTC"), Err(ParseAmountError::InvalidFormat)); + assert_eq!( + Amount::from_str("42 satoshi BTC"), + Err(ParseAmountError::UnknownDenomination("satoshi BTC".into())), + ); + assert_eq!( + SignedAmount::from_str("-42 satoshi BTC"), + Err(ParseAmountError::UnknownDenomination("satoshi BTC".into())), + ); } #[cfg(feature = "serde")]