bitcoin_units/amount/signed.rs
1// SPDX-License-Identifier: CC0-1.0
2
3//! A signed bitcoin amount.
4
5#[cfg(feature = "alloc")]
6use alloc::string::{String, ToString};
7use core::str::FromStr;
8use core::{default, fmt};
9
10#[cfg(feature = "arbitrary")]
11use arbitrary::{Arbitrary, Unstructured};
12
13use super::error::ParseErrorInner;
14use super::{
15 parse_signed_to_satoshi, split_amount_and_denomination, Amount, Denomination, Display,
16 DisplayStyle, OutOfRangeError, ParseAmountError, ParseError,
17};
18
19mod encapsulate {
20 use super::OutOfRangeError;
21
22 /// A signed amount.
23 ///
24 /// The [`SignedAmount`] type can be used to express Bitcoin amounts that support arithmetic and
25 /// conversion to various denominations. The [`SignedAmount`] type does not implement [`serde`]
26 /// traits but we do provide modules for serializing as satoshis or bitcoin.
27 ///
28 /// **Warning!**
29 ///
30 /// This type implements several arithmetic operations from [`core::ops`].
31 /// To prevent errors due to an overflow when using these operations,
32 /// it is advised to instead use the checked arithmetic methods whose names
33 /// start with `checked_`. The operations from [`core::ops`] that [`SignedAmount`]
34 /// implements will panic when an overflow occurs.
35 ///
36 /// # Examples
37 ///
38 /// ```
39 /// # #[cfg(feature = "serde")] {
40 /// use serde::{Serialize, Deserialize};
41 /// use bitcoin_units::SignedAmount;
42 ///
43 /// #[derive(Serialize, Deserialize)]
44 /// struct Foo {
45 /// // If you are using `rust-bitcoin` then `bitcoin::amount::serde::as_sat` also works.
46 /// #[serde(with = "bitcoin_units::amount::serde::as_sat")] // Also `serde::as_btc`.
47 /// amount: SignedAmount,
48 /// }
49 /// # }
50 /// ```
51 #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
52 pub struct SignedAmount(i64);
53
54 impl SignedAmount {
55 /// The maximum value of an amount.
56 pub const MAX: Self = Self(21_000_000 * 100_000_000);
57 /// The minimum value of an amount.
58 pub const MIN: Self = Self(-21_000_000 * 100_000_000);
59
60 /// Gets the number of satoshis in this [`SignedAmount`].
61 ///
62 /// # Examples
63 ///
64 /// ```
65 /// # use bitcoin_units::SignedAmount;
66 /// assert_eq!(SignedAmount::ONE_BTC.to_sat(), 100_000_000);
67 /// ```
68 pub const fn to_sat(self) -> i64 { self.0 }
69
70 /// Constructs a new [`SignedAmount`] from the given number of satoshis.
71 ///
72 /// # Errors
73 ///
74 /// If `satoshi` is outside of valid range (see [`Self::MAX_MONEY`]).
75 ///
76 /// # Examples
77 ///
78 /// ```
79 /// # use bitcoin_units::{amount, SignedAmount};
80 /// # let sat = -100_000;
81 /// let amount = SignedAmount::from_sat(sat)?;
82 /// assert_eq!(amount.to_sat(), sat);
83 /// # Ok::<_, amount::OutOfRangeError>(())
84 /// ```
85 pub const fn from_sat(satoshi: i64) -> Result<Self, OutOfRangeError> {
86 if satoshi < Self::MIN.to_sat() {
87 Err(OutOfRangeError { is_signed: true, is_greater_than_max: false })
88 } else if satoshi > Self::MAX_MONEY.to_sat() {
89 Err(OutOfRangeError { is_signed: true, is_greater_than_max: true })
90 } else {
91 Ok(Self(satoshi))
92 }
93 }
94 }
95}
96#[doc(inline)]
97pub use encapsulate::SignedAmount;
98use internals::const_casts;
99
100impl SignedAmount {
101 /// The zero amount.
102 pub const ZERO: Self = Self::from_sat_i32(0);
103 /// Exactly one satoshi.
104 pub const ONE_SAT: Self = Self::from_sat_i32(1);
105 /// Exactly one bitcoin.
106 pub const ONE_BTC: Self = Self::from_btc_i16(1);
107 /// Exactly fifty bitcoin.
108 pub const FIFTY_BTC: Self = Self::from_btc_i16(50);
109 /// The maximum value allowed as an amount. Useful for sanity checking.
110 pub const MAX_MONEY: Self = Self::MAX;
111
112 /// Constructs a new [`SignedAmount`] with satoshi precision and the given number of satoshis.
113 ///
114 /// Accepts an `i32` which is guaranteed to be in range for the type, but which can only
115 /// represent roughly -21.47 to 21.47 BTC.
116 #[allow(clippy::missing_panics_doc)]
117 pub const fn from_sat_i32(satoshi: i32) -> Self {
118 let sats = satoshi as i64; // cannot use i64::from in a constfn
119 match Self::from_sat(sats) {
120 Ok(amount) => amount,
121 Err(_) => panic!("unreachable - i32 input [-2,147,483,648 to 2,147,483,647 satoshis] is within range"),
122 }
123 }
124
125 /// Converts from a value expressing a decimal number of bitcoin to a [`SignedAmount`].
126 ///
127 /// # Errors
128 ///
129 /// If the amount is too big (positive or negative) or too precise.
130 ///
131 /// Please be aware of the risk of using floating-point numbers.
132 ///
133 /// # Examples
134 ///
135 /// ```
136 /// # use bitcoin_units::{amount, SignedAmount};
137 /// let amount = SignedAmount::from_btc(-0.01)?;
138 /// assert_eq!(amount.to_sat(), -1_000_000);
139 /// # Ok::<_, amount::ParseAmountError>(())
140 /// ```
141 #[cfg(feature = "alloc")]
142 pub fn from_btc(btc: f64) -> Result<Self, ParseAmountError> {
143 Self::from_float_in(btc, Denomination::Bitcoin)
144 }
145
146 /// Converts from a value expressing a whole number of bitcoin to a [`SignedAmount`].
147 #[allow(clippy::missing_panics_doc)]
148 pub fn from_int_btc<T: Into<i16>>(whole_bitcoin: T) -> Self {
149 Self::from_btc_i16(whole_bitcoin.into())
150 }
151
152 /// Converts from a value expressing a whole number of bitcoin to a [`SignedAmount`]
153 /// in const context.
154 #[allow(clippy::missing_panics_doc)]
155 pub const fn from_btc_i16(whole_bitcoin: i16) -> Self {
156 let btc = const_casts::i16_to_i64(whole_bitcoin);
157 let sats = btc * 100_000_000;
158
159 match Self::from_sat(sats) {
160 Ok(amount) => amount,
161 Err(_) => panic!("unreachable - 32,767 BTC is within range"),
162 }
163 }
164
165 /// Parses a decimal string as a value in the given [`Denomination`].
166 ///
167 /// Note: This only parses the value string. If you want to parse a string
168 /// containing the value with denomination, use [`FromStr`].
169 ///
170 /// # Errors
171 ///
172 /// If the amount is too big (positive or negative) or too precise.
173 pub fn from_str_in(s: &str, denom: Denomination) -> Result<Self, ParseAmountError> {
174 parse_signed_to_satoshi(s, denom)
175 .map(|(_, amount)| amount)
176 .map_err(|error| error.convert(true))
177 }
178
179 /// Parses amounts with denomination suffix as produced by [`Self::to_string_with_denomination`]
180 /// or with [`fmt::Display`].
181 ///
182 /// If you want to parse only the amount without the denomination, use [`Self::from_str_in`].
183 ///
184 /// # Errors
185 ///
186 /// If the amount is too big (positive or negative) or too precise.
187 ///
188 /// # Examples
189 ///
190 /// ```
191 /// # use bitcoin_units::{amount, SignedAmount};
192 /// let amount = SignedAmount::from_str_with_denomination("0.1 BTC")?;
193 /// assert_eq!(amount, SignedAmount::from_sat(10_000_000)?);
194 /// # Ok::<_, amount::ParseError>(())
195 /// ```
196 pub fn from_str_with_denomination(s: &str) -> Result<Self, ParseError> {
197 let (amt, denom) = split_amount_and_denomination(s)?;
198 Self::from_str_in(amt, denom).map_err(Into::into)
199 }
200
201 /// Expresses this [`SignedAmount`] as a floating-point value in the given [`Denomination`].
202 ///
203 /// Please be aware of the risk of using floating-point numbers.
204 ///
205 /// # Examples
206 ///
207 /// ```
208 /// # use bitcoin_units::amount::{self, SignedAmount, Denomination};
209 /// let amount = SignedAmount::from_sat(100_000)?;
210 /// assert_eq!(amount.to_float_in(Denomination::Bitcoin), 0.001);
211 /// # Ok::<_, amount::OutOfRangeError>(())
212 /// ```
213 #[cfg(feature = "alloc")]
214 #[allow(clippy::missing_panics_doc)]
215 pub fn to_float_in(self, denom: Denomination) -> f64 {
216 self.to_string_in(denom).parse::<f64>().unwrap()
217 }
218
219 /// Expresses this [`SignedAmount`] as a floating-point value in Bitcoin.
220 ///
221 /// Please be aware of the risk of using floating-point numbers.
222 ///
223 /// # Examples
224 ///
225 /// ```
226 /// # use bitcoin_units::amount::{self, SignedAmount, Denomination};
227 /// let amount = SignedAmount::from_sat(100_000)?;
228 /// assert_eq!(amount.to_btc(), amount.to_float_in(Denomination::Bitcoin));
229 /// # Ok::<_, amount::OutOfRangeError>(())
230 /// ```
231 #[cfg(feature = "alloc")]
232 pub fn to_btc(self) -> f64 { self.to_float_in(Denomination::Bitcoin) }
233
234 /// Converts this [`SignedAmount`] in floating-point notation in the given [`Denomination`].
235 ///
236 /// # Errors
237 ///
238 /// If the amount is too big (positive or negative) or too precise.
239 ///
240 /// Please be aware of the risk of using floating-point numbers.
241 #[cfg(feature = "alloc")]
242 pub fn from_float_in(value: f64, denom: Denomination) -> Result<Self, ParseAmountError> {
243 // This is inefficient, but the safest way to deal with this. The parsing logic is safe.
244 // Any performance-critical application should not be dealing with floats.
245 Self::from_str_in(&value.to_string(), denom)
246 }
247
248 /// Constructs a new object that implements [`fmt::Display`] in the given [`Denomination`].
249 ///
250 /// This function is useful if you do not wish to allocate. See also [`Self::to_string_in`].
251 ///
252 /// # Examples
253 ///
254 /// ```
255 /// # use bitcoin_units::amount::{self, SignedAmount, Denomination};
256 /// # use std::fmt::Write;
257 /// let amount = SignedAmount::from_sat(10_000_000)?;
258 /// let mut output = String::new();
259 /// let _ = write!(&mut output, "{}", amount.display_in(Denomination::Bitcoin));
260 /// assert_eq!(output, "0.1");
261 /// # Ok::<_, amount::OutOfRangeError>(())
262 /// ```
263 #[must_use]
264 pub fn display_in(self, denomination: Denomination) -> Display {
265 Display {
266 sats_abs: self.unsigned_abs().to_sat(),
267 is_negative: self.is_negative(),
268 style: DisplayStyle::FixedDenomination { denomination, show_denomination: false },
269 }
270 }
271
272 /// Constructs a new object that implements [`fmt::Display`] dynamically selecting
273 /// [`Denomination`].
274 ///
275 /// This will use BTC for values greater than or equal to 1 BTC and satoshis otherwise. To
276 /// avoid confusion the denomination is always shown.
277 #[must_use]
278 pub fn display_dynamic(self) -> Display {
279 Display {
280 sats_abs: self.unsigned_abs().to_sat(),
281 is_negative: self.is_negative(),
282 style: DisplayStyle::DynamicDenomination,
283 }
284 }
285
286 /// Returns a formatted string representing this [`SignedAmount`] in the given [`Denomination`].
287 ///
288 /// Returned string does not include the denomination.
289 ///
290 /// # Examples
291 ///
292 /// ```
293 /// # use bitcoin_units::amount::{self, SignedAmount, Denomination};
294 /// let amount = SignedAmount::from_sat(10_000_000)?;
295 /// assert_eq!(amount.to_string_in(Denomination::Bitcoin), "0.1");
296 /// # Ok::<_, amount::OutOfRangeError>(())
297 /// ```
298 #[cfg(feature = "alloc")]
299 pub fn to_string_in(self, denom: Denomination) -> String { self.display_in(denom).to_string() }
300
301 /// Returns a formatted string representing this [`SignedAmount`] in the given [`Denomination`],
302 /// suffixed with the abbreviation for the denomination.
303 ///
304 /// # Examples
305 ///
306 /// ```
307 /// # use bitcoin_units::amount::{self, SignedAmount, Denomination};
308 /// let amount = SignedAmount::from_sat(10_000_000)?;
309 /// assert_eq!(amount.to_string_with_denomination(Denomination::Bitcoin), "0.1 BTC");
310 /// # Ok::<_, amount::OutOfRangeError>(())
311 /// ```
312 #[cfg(feature = "alloc")]
313 pub fn to_string_with_denomination(self, denom: Denomination) -> String {
314 self.display_in(denom).show_denomination().to_string()
315 }
316
317 /// Gets the absolute value of this [`SignedAmount`].
318 ///
319 /// This function never overflows or panics, unlike `i64::abs()`.
320 #[must_use]
321 #[allow(clippy::missing_panics_doc)]
322 pub const fn abs(self) -> Self {
323 // `i64::abs()` can never overflow because SignedAmount::MIN == -MAX_MONEY.
324 match Self::from_sat(self.to_sat().abs()) {
325 Ok(amount) => amount,
326 Err(_) => panic!("a positive signed amount is always valid"),
327 }
328 }
329
330 /// Gets the absolute value of this [`SignedAmount`] returning [`Amount`].
331 #[must_use]
332 #[allow(clippy::missing_panics_doc)]
333 pub fn unsigned_abs(self) -> Amount {
334 self.abs().to_unsigned().expect("a positive signed amount is always valid")
335 }
336
337 /// Returns a number representing sign of this [`SignedAmount`].
338 ///
339 /// - `0` if the amount is zero
340 /// - `1` if the amount is positive
341 /// - `-1` if the amount is negative
342 #[must_use]
343 pub fn signum(self) -> i64 { self.to_sat().signum() }
344
345 /// Checks if this [`SignedAmount`] is positive.
346 ///
347 /// Returns `true` if this [`SignedAmount`] is positive and `false` if
348 /// this [`SignedAmount`] is zero or negative.
349 pub fn is_positive(self) -> bool { self.to_sat().is_positive() }
350
351 /// Checks if this [`SignedAmount`] is negative.
352 ///
353 /// Returns `true` if this [`SignedAmount`] is negative and `false` if
354 /// this [`SignedAmount`] is zero or positive.
355 pub fn is_negative(self) -> bool { self.to_sat().is_negative() }
356
357 /// Returns the absolute value of this [`SignedAmount`].
358 ///
359 /// Consider using `unsigned_abs` which is often more practical.
360 ///
361 /// Returns [`None`] if overflow occurred. (`self == i64::MIN`)
362 #[must_use]
363 #[deprecated(since = "1.0.0-rc.0", note = "Never returns none, use `abs()` instead")]
364 #[allow(clippy::unnecessary_wraps)] // To match stdlib function definition.
365 pub const fn checked_abs(self) -> Option<Self> { Some(self.abs()) }
366
367 /// Checked addition.
368 ///
369 /// Returns [`None`] if the sum is above [`SignedAmount::MAX`] or below [`SignedAmount::MIN`].
370 #[must_use]
371 pub const fn checked_add(self, rhs: Self) -> Option<Self> {
372 // No `map()` in const context.
373 match self.to_sat().checked_add(rhs.to_sat()) {
374 Some(res) => match Self::from_sat(res) {
375 Ok(amount) => Some(amount),
376 Err(_) => None,
377 },
378 None => None,
379 }
380 }
381
382 /// Checked subtraction.
383 ///
384 /// Returns [`None`] if the difference is above [`SignedAmount::MAX`] or below
385 /// [`SignedAmount::MIN`].
386 #[must_use]
387 pub const fn checked_sub(self, rhs: Self) -> Option<Self> {
388 // No `map()` in const context.
389 match self.to_sat().checked_sub(rhs.to_sat()) {
390 Some(res) => match Self::from_sat(res) {
391 Ok(amount) => Some(amount),
392 Err(_) => None,
393 },
394 None => None,
395 }
396 }
397
398 /// Checked multiplication.
399 ///
400 /// Returns [`None`] if the product is above [`SignedAmount::MAX`] or below
401 /// [`SignedAmount::MIN`].
402 #[must_use]
403 pub const fn checked_mul(self, rhs: i64) -> Option<Self> {
404 // No `map()` in const context.
405 match self.to_sat().checked_mul(rhs) {
406 Some(res) => match Self::from_sat(res) {
407 Ok(amount) => Some(amount),
408 Err(_) => None,
409 },
410 None => None,
411 }
412 }
413
414 /// Checked integer division.
415 ///
416 /// Be aware that integer division loses the remainder if no exact division can be made.
417 ///
418 /// Returns [`None`] if overflow occurred.
419 #[must_use]
420 pub const fn checked_div(self, rhs: i64) -> Option<Self> {
421 // No `map()` in const context.
422 match self.to_sat().checked_div(rhs) {
423 Some(res) => match Self::from_sat(res) {
424 Ok(amount) => Some(amount),
425 Err(_) => None, // Unreachable because of checked_div above.
426 },
427 None => None,
428 }
429 }
430
431 /// Checked remainder.
432 ///
433 /// Returns [`None`] if overflow occurred.
434 #[must_use]
435 pub const fn checked_rem(self, rhs: i64) -> Option<Self> {
436 // No `map()` in const context.
437 match self.to_sat().checked_rem(rhs) {
438 Some(res) => match Self::from_sat(res) {
439 Ok(amount) => Some(amount),
440 Err(_) => None, // Unreachable because of checked_rem above.
441 },
442 None => None,
443 }
444 }
445
446 /// Subtraction that doesn't allow negative [`SignedAmount`]s.
447 ///
448 /// Returns [`None`] if either `self`, `rhs` or the result is strictly negative.
449 #[must_use]
450 pub fn positive_sub(self, rhs: Self) -> Option<Self> {
451 if self.is_negative() || rhs.is_negative() || rhs > self {
452 None
453 } else {
454 self.checked_sub(rhs)
455 }
456 }
457
458 /// Converts to an unsigned amount.
459 ///
460 /// # Errors
461 ///
462 /// If the amount is negative.
463 #[allow(clippy::missing_panics_doc)]
464 pub fn to_unsigned(self) -> Result<Amount, OutOfRangeError> {
465 if self.is_negative() {
466 Err(OutOfRangeError::negative())
467 } else {
468 // Cast ok, checked not negative above.
469 Ok(Amount::from_sat(self.to_sat() as u64)
470 .expect("a positive signed amount is always valid"))
471 }
472 }
473}
474
475crate::internal_macros::impl_fmt_traits_for_u32_wrapper!(SignedAmount, to_sat);
476
477impl default::Default for SignedAmount {
478 fn default() -> Self { Self::ZERO }
479}
480
481impl fmt::Debug for SignedAmount {
482 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
483 write!(f, "SignedAmount({} SAT)", self.to_sat())
484 }
485}
486
487// No one should depend on a binding contract for Display for this type.
488// Just using Bitcoin denominated string.
489impl fmt::Display for SignedAmount {
490 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
491 fmt::Display::fmt(&self.display_in(Denomination::Bitcoin).show_denomination(), f)
492 }
493}
494
495impl FromStr for SignedAmount {
496 type Err = ParseError;
497
498 /// Parses a string slice where the slice includes a denomination.
499 ///
500 /// If the returned value would be zero or negative zero, then no denomination is required.
501 fn from_str(s: &str) -> Result<Self, Self::Err> {
502 let result = Self::from_str_with_denomination(s);
503
504 match result {
505 Err(ParseError(ParseErrorInner::MissingDenomination(_))) => {
506 let d = Self::from_str_in(s, Denomination::Satoshi);
507
508 if d == Ok(Self::ZERO) {
509 Ok(Self::ZERO)
510 } else {
511 result
512 }
513 }
514 _ => result,
515 }
516 }
517}
518
519impl From<Amount> for SignedAmount {
520 fn from(value: Amount) -> Self {
521 let v = value.to_sat() as i64; // Cast ok, signed amount and amount share positive range.
522 Self::from_sat(v).expect("all amounts are valid signed amounts")
523 }
524}
525
526#[cfg(feature = "arbitrary")]
527impl<'a> Arbitrary<'a> for SignedAmount {
528 fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
529 let sats = u.int_in_range(Self::MIN.to_sat()..=Self::MAX.to_sat())?;
530 Ok(Self::from_sat(sats).expect("range is valid"))
531 }
532}