Merge rust-bitcoin/rust-bitcoin#1521: Make space optional in amount with denomination

16c49df688 Accept amounts with denominations with and without spaces (Casey Rodarmor)

Pull request description:

  I didn't add tests for parsing with no space, but wanted to get a PR up to show the approach.

  Fixes #1519.

ACKs for top commit:
  apoelstra:
    ACK 16c49df688
  Kixunil:
    ACK 16c49df688

Tree-SHA512: 651f12974a23b711a421005cc5905cb613bfdb092b03f7b0a0e2c02b3f9351f81eb985f848d313a17506e4960df7c01b40f673a100e4230a7045364acc4865de
This commit is contained in:
Andrew Poelstra 2023-01-04 21:53:49 +00:00
commit dd2091ed93
No known key found for this signature in database
GPG Key ID: C588D63CE41B97C1
1 changed files with 62 additions and 46 deletions

View File

@ -288,6 +288,16 @@ fn parse_signed_to_satoshi(
Ok((is_negative, value)) 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` /// Options given by `fmt::Formatter`
struct FormatOptions { struct FormatOptions {
fill: char, fill: char,
@ -523,14 +533,8 @@ impl Amount {
/// If you want to parse only the amount without the denomination, /// If you want to parse only the amount without the denomination,
/// use [Self::from_str_in]. /// use [Self::from_str_in].
pub fn from_str_with_denomination(s: &str) -> Result<Amount, ParseAmountError> { pub fn from_str_with_denomination(s: &str) -> Result<Amount, ParseAmountError> {
let mut split = s.splitn(3, ' '); let (amt, denom) = split_amount_and_denomination(s)?;
let amt_str = split.next().unwrap(); Amount::from_str_in(amt, denom)
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()?)
} }
/// Express this [Amount] as a floating-point value in the given denomination. /// 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, /// If you want to parse only the amount without the denomination,
/// use [Self::from_str_in]. /// use [Self::from_str_in].
pub fn from_str_with_denomination(s: &str) -> Result<SignedAmount, ParseAmountError> { pub fn from_str_with_denomination(s: &str) -> Result<SignedAmount, ParseAmountError> {
let mut split = s.splitn(3, ' '); let (amt, denom) = split_amount_and_denomination(s)?;
let amt_str = split.next().unwrap(); SignedAmount::from_str_in(amt, denom)
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()?)
} }
/// Express this [SignedAmount] as a floating-point value in the given denomination. /// 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. #[allow(clippy::inconsistent_digit_grouping)] // Group to show 100,000,000 sats per bitcoin.
fn from_str() { fn from_str() {
use super::ParseAmountError as E; 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!(Amount::from_str("x BTC"), Err(E::InvalidCharacter('x')));
assert_eq!(p("5 BTC BTC"), Err(E::InvalidFormat)); assert_eq!(Amount::from_str("xBTC"), Err(E::UnknownDenomination("xBTC".into())));
assert_eq!(p("5 5 BTC"), Err(E::InvalidFormat)); 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<Amount, ParseAmountError>) {
assert_eq!(Amount::from_str(s), expected);
assert_eq!(Amount::from_str(&s.replace(' ', "")), expected);
}
assert_eq!(p("-1 BTC"), Err(E::Negative)); #[cfg_attr(rust_v_1_46, track_caller)]
assert_eq!(p("-0.0 BTC"), Err(E::Negative)); fn scase(s: &str, expected: Result<SignedAmount, ParseAmountError>) {
assert_eq!(p("0.123456789 BTC"), Err(E::TooPrecise)); assert_eq!(SignedAmount::from_str(s), expected);
assert_eq!(sp("-0.1 satoshi"), Err(E::TooPrecise)); assert_eq!(SignedAmount::from_str(&s.replace(' ', "")), expected);
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));
assert_eq!(sp("0 msat"), Err(E::TooPrecise)); case("5 BCH", Err(E::UnknownDenomination("BCH".to_owned())));
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));
assert_eq!(p(".5 bits"), Ok(Amount::from_sat(50))); case("-1 BTC", Err(E::Negative));
assert_eq!(sp("-.5 bits"), Ok(SignedAmount::from_sat(-50))); case("-0.0 BTC", Err(E::Negative));
assert_eq!(p("0.00253583 BTC"), Ok(Amount::from_sat(253583))); case("0.123456789 BTC", Err(E::TooPrecise));
assert_eq!(sp("-5 satoshi"), Ok(SignedAmount::from_sat(-5))); scase("-0.1 satoshi", Err(E::TooPrecise));
assert_eq!(p("0.10000000 BTC"), Ok(Amount::from_sat(100_000_00))); case("0.123456 mBTC", Err(E::TooPrecise));
assert_eq!(sp("-100 bits"), Ok(SignedAmount::from_sat(-10_000))); 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] #[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::MilliSatoshi)), Ok(amt));
assert_eq!(Amount::from_str(&denom(amt, D::PicoBitcoin)), 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!(
assert_eq!(SignedAmount::from_str("-42 satoshi BTC"), Err(ParseAmountError::InvalidFormat)); 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")] #[cfg(feature = "serde")]