Be more liberal when parsing Denomination
There is no reason to force users to use one particular form when providing a denomination string. We can be liberal in what we accept with no loss of clarity. Allow `Denomination` strings to use a variety of forms, in particular lower case and uppercase. Note, we explicitly disallow various forms of `Msat` because it is ambiguous whether this means milli or mega sats. Co-developed-by: Martin Habovštiak <martin.habovstiak@gmail.com>
This commit is contained in:
parent
628168e493
commit
f690b8e362
|
@ -66,20 +66,61 @@ impl fmt::Display for Denomination {
|
||||||
impl FromStr for Denomination {
|
impl FromStr for Denomination {
|
||||||
type Err = ParseAmountError;
|
type Err = ParseAmountError;
|
||||||
|
|
||||||
|
/// Convert from a str to Denomination.
|
||||||
|
///
|
||||||
|
/// Any combination of upper and/or lower case, excluding uppercase 'M' is considered valid.
|
||||||
|
/// - Singular: BTC, mBTC, uBTC
|
||||||
|
/// - Plural or singular: sat, satoshi, bit, msat
|
||||||
|
///
|
||||||
|
/// Due to ambiguity between mega and milli we prohibit usage of leading capital 'M'.
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
match s {
|
use self::ParseAmountError::*;
|
||||||
"BTC" => Ok(Denomination::Bitcoin),
|
|
||||||
"mBTC" => Ok(Denomination::MilliBitcoin),
|
if s.starts_with('M') {
|
||||||
"uBTC" => Ok(Denomination::MicroBitcoin),
|
return Err(denomination_from_str(s).map_or_else(
|
||||||
"bits" => Ok(Denomination::Bit),
|
|| UnknownDenomination(s.to_owned()),
|
||||||
"satoshi" => Ok(Denomination::Satoshi),
|
|_| PossiblyConfusingDenomination(s.to_owned())
|
||||||
"sat" => Ok(Denomination::Satoshi),
|
));
|
||||||
"msat" => Ok(Denomination::MilliSatoshi),
|
|
||||||
d => Err(ParseAmountError::UnknownDenomination(d.to_owned())),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
denomination_from_str(s).ok_or_else(|| UnknownDenomination(s.to_owned()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn denomination_from_str(mut s: &str) -> Option<Denomination> {
|
||||||
|
if s.eq_ignore_ascii_case("BTC") {
|
||||||
|
return Some(Denomination::Bitcoin);
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.eq_ignore_ascii_case("mBTC") {
|
||||||
|
return Some(Denomination::MilliBitcoin);
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.eq_ignore_ascii_case("uBTC") {
|
||||||
|
return Some(Denomination::MicroBitcoin);
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.ends_with('s') || s.ends_with('S') {
|
||||||
|
s = &s[..(s.len() - 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.eq_ignore_ascii_case("bit") {
|
||||||
|
return Some(Denomination::Bit);
|
||||||
|
}
|
||||||
|
if s.eq_ignore_ascii_case("satoshi") {
|
||||||
|
return Some(Denomination::Satoshi);
|
||||||
|
}
|
||||||
|
if s.eq_ignore_ascii_case("sat") {
|
||||||
|
return Some(Denomination::Satoshi);
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.eq_ignore_ascii_case("msat") {
|
||||||
|
return Some(Denomination::MilliSatoshi);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// An error during amount parsing.
|
/// An error during amount parsing.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum ParseAmountError {
|
pub enum ParseAmountError {
|
||||||
|
@ -97,6 +138,8 @@ pub enum ParseAmountError {
|
||||||
InvalidCharacter(char),
|
InvalidCharacter(char),
|
||||||
/// The denomination was unknown.
|
/// The denomination was unknown.
|
||||||
UnknownDenomination(String),
|
UnknownDenomination(String),
|
||||||
|
/// The denomination has multiple possible interpretations.
|
||||||
|
PossiblyConfusingDenomination(String)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ParseAmountError {
|
impl fmt::Display for ParseAmountError {
|
||||||
|
@ -109,6 +152,9 @@ impl fmt::Display for ParseAmountError {
|
||||||
ParseAmountError::InputTooLarge => f.write_str("input string was too large"),
|
ParseAmountError::InputTooLarge => f.write_str("input string was too large"),
|
||||||
ParseAmountError::InvalidCharacter(c) => write!(f, "invalid character in input: {}", c),
|
ParseAmountError::InvalidCharacter(c) => write!(f, "invalid character in input: {}", c),
|
||||||
ParseAmountError::UnknownDenomination(ref d) => write!(f, "unknown denomination: {}", d),
|
ParseAmountError::UnknownDenomination(ref d) => write!(f, "unknown denomination: {}", d),
|
||||||
|
ParseAmountError::PossiblyConfusingDenomination(ref d) => {
|
||||||
|
write!(f, "the 'M' at the beginning of {} should technically mean 'Mega' but that denomination is uncommon and maybe 'milli' was intended", d)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1639,4 +1685,27 @@ mod tests {
|
||||||
let sum = amounts.into_iter().checked_sum();
|
let sum = amounts.into_iter().checked_sum();
|
||||||
assert_eq!(Some(SignedAmount::from_sat(3364)), sum);
|
assert_eq!(Some(SignedAmount::from_sat(3364)), sum);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn denomination_string_acceptable_forms() {
|
||||||
|
// Non-exhaustive list of valid forms.
|
||||||
|
let valid = vec!["BTC", "btc", "mBTC", "mbtc", "uBTC", "ubtc", "SATOSHI","Satoshi", "Satoshis", "satoshis", "SAT", "Sat", "sats", "bit", "bits"];
|
||||||
|
for denom in valid.iter() {
|
||||||
|
assert!(Denomination::from_str(denom).is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn disallow_confusing_forms() {
|
||||||
|
// Non-exhaustive list of confusing forms.
|
||||||
|
let confusing = vec!["Msat", "Msats", "MSAT", "MSATS", "MSat", "MSats", "MBTC", "Mbtc"];
|
||||||
|
for denom in confusing.iter() {
|
||||||
|
match Denomination::from_str(denom) {
|
||||||
|
Ok(_) => panic!("from_str should error for {}", denom),
|
||||||
|
Err(ParseAmountError::PossiblyConfusingDenomination(_)) => {},
|
||||||
|
Err(e) => panic!("unexpected error: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue