#[cfg(feature = "chumsky")]
use chumsky::{IterParser as _, Parser, prelude::just, text::digits};
#[derive(
Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
pub struct LindenAmount(pub u64);
impl std::fmt::Display for LindenAmount {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self(value) = self;
write!(f, "{value} L$")
}
}
impl std::ops::Add for LindenAmount {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
let Self(lhs) = self;
let Self(rhs) = rhs;
#[expect(
clippy::arithmetic_side_effects,
reason = "this results in the exact same arithmetic side-effects as the same operation on integers which is probably the most expected result for the user"
)]
Self(lhs + rhs)
}
}
impl std::ops::Sub for LindenAmount {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
let Self(lhs) = self;
let Self(rhs) = rhs;
#[expect(
clippy::arithmetic_side_effects,
reason = "this results in the exact same arithmetic side-effects as the same operation on integers which is probably the most expected result for the user"
)]
Self(lhs - rhs)
}
}
impl std::ops::Mul<u8> for LindenAmount {
type Output = Self;
fn mul(self, rhs: u8) -> Self::Output {
let Self(lhs) = self;
#[expect(
clippy::arithmetic_side_effects,
reason = "this results in the exact same arithmetic side-effects as the same operation on integers which is probably the most expected result for the user"
)]
Self(lhs * u64::from(rhs))
}
}
impl std::ops::Mul<u16> for LindenAmount {
type Output = Self;
fn mul(self, rhs: u16) -> Self::Output {
let Self(lhs) = self;
#[expect(
clippy::arithmetic_side_effects,
reason = "this results in the exact same arithmetic side-effects as the same operation on integers which is probably the most expected result for the user"
)]
Self(lhs * u64::from(rhs))
}
}
impl std::ops::Mul<u32> for LindenAmount {
type Output = Self;
fn mul(self, rhs: u32) -> Self::Output {
let Self(lhs) = self;
#[expect(
clippy::arithmetic_side_effects,
reason = "this results in the exact same arithmetic side-effects as the same operation on integers which is probably the most expected result for the user"
)]
Self(lhs * u64::from(rhs))
}
}
impl std::ops::Mul<u64> for LindenAmount {
type Output = Self;
fn mul(self, rhs: u64) -> Self::Output {
let Self(lhs) = self;
#[expect(
clippy::arithmetic_side_effects,
reason = "this results in the exact same arithmetic side-effects as the same operation on integers which is probably the most expected result for the user"
)]
Self(lhs * rhs)
}
}
impl std::ops::Div<u8> for LindenAmount {
type Output = Self;
fn div(self, rhs: u8) -> Self::Output {
let Self(lhs) = self;
#[expect(
clippy::arithmetic_side_effects,
reason = "this results in the exact same arithmetic side-effects as the same operation on integers which is probably the most expected result for the user"
)]
Self(lhs / u64::from(rhs))
}
}
impl std::ops::Div<u16> for LindenAmount {
type Output = Self;
fn div(self, rhs: u16) -> Self::Output {
let Self(lhs) = self;
#[expect(
clippy::arithmetic_side_effects,
reason = "this results in the exact same arithmetic side-effects as the same operation on integers which is probably the most expected result for the user"
)]
Self(lhs / u64::from(rhs))
}
}
impl std::ops::Div<u32> for LindenAmount {
type Output = Self;
fn div(self, rhs: u32) -> Self::Output {
let Self(lhs) = self;
#[expect(
clippy::arithmetic_side_effects,
reason = "this results in the exact same arithmetic side-effects as the same operation on integers which is probably the most expected result for the user"
)]
Self(lhs / u64::from(rhs))
}
}
impl std::ops::Div<u64> for LindenAmount {
type Output = Self;
fn div(self, rhs: u64) -> Self::Output {
let Self(lhs) = self;
#[expect(
clippy::arithmetic_side_effects,
reason = "this results in the exact same arithmetic side-effects as the same operation on integers which is probably the most expected result for the user"
)]
Self(lhs / rhs)
}
}
impl std::ops::Rem<u8> for LindenAmount {
type Output = Self;
fn rem(self, rhs: u8) -> Self::Output {
let Self(lhs) = self;
#[expect(
clippy::arithmetic_side_effects,
reason = "this results in the exact same arithmetic side-effects as the same operation on integers which is probably the most expected result for the user"
)]
Self(lhs % u64::from(rhs))
}
}
impl std::ops::Rem<u16> for LindenAmount {
type Output = Self;
fn rem(self, rhs: u16) -> Self::Output {
let Self(lhs) = self;
#[expect(
clippy::arithmetic_side_effects,
reason = "this results in the exact same arithmetic side-effects as the same operation on integers which is probably the most expected result for the user"
)]
Self(lhs % u64::from(rhs))
}
}
impl std::ops::Rem<u32> for LindenAmount {
type Output = Self;
fn rem(self, rhs: u32) -> Self::Output {
let Self(lhs) = self;
#[expect(
clippy::arithmetic_side_effects,
reason = "this results in the exact same arithmetic side-effects as the same operation on integers which is probably the most expected result for the user"
)]
Self(lhs % u64::from(rhs))
}
}
impl std::ops::Rem<u64> for LindenAmount {
type Output = Self;
fn rem(self, rhs: u64) -> Self::Output {
let Self(lhs) = self;
#[expect(
clippy::arithmetic_side_effects,
reason = "this results in the exact same arithmetic side-effects as the same operation on integers which is probably the most expected result for the user"
)]
Self(lhs % rhs)
}
}
#[cfg(feature = "chumsky")]
#[must_use]
pub fn linden_amount_parser<'src>()
-> impl Parser<'src, &'src str, LindenAmount, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
{
just("L$")
.ignore_then(digits(10).collect::<String>())
.try_map(|x: String, span: chumsky::span::SimpleSpan| {
Ok(LindenAmount(x.parse().map_err(|e| {
chumsky::error::Rich::custom(span, format!("{e:?}"))
})?))
})
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct LindenBalance {
negative: bool,
magnitude: LindenAmount,
}
impl LindenBalance {
pub const ZERO: Self = Self {
negative: false,
magnitude: LindenAmount(0),
};
#[must_use]
pub const fn new(negative: bool, magnitude: LindenAmount) -> Self {
let LindenAmount(value) = magnitude;
Self {
negative: negative && value != 0,
magnitude,
}
}
#[must_use]
pub const fn is_negative(&self) -> bool {
self.negative
}
#[must_use]
pub const fn is_zero(&self) -> bool {
let LindenAmount(value) = self.magnitude;
value == 0
}
#[must_use]
pub fn magnitude(&self) -> LindenAmount {
self.magnitude.clone()
}
fn signed_i128(&self) -> i128 {
let LindenAmount(value) = self.magnitude;
let magnitude = i128::from(value);
if self.negative {
magnitude.wrapping_neg()
} else {
magnitude
}
}
fn from_signed_i128(value: i128) -> Self {
let magnitude = u64::try_from(value.unsigned_abs()).unwrap_or(u64::MAX);
Self::new(value.is_negative(), LindenAmount(magnitude))
}
fn signed_i64(&self) -> Option<i64> {
let LindenAmount(value) = self.magnitude;
let magnitude = i64::try_from(value).ok()?;
Some(if self.negative {
magnitude.wrapping_neg()
} else {
magnitude
})
}
#[must_use]
pub fn from_i32(value: i32) -> Self {
Self::new(
value.is_negative(),
LindenAmount(u64::from(value.unsigned_abs())),
)
}
#[must_use]
pub fn to_i32(&self) -> Option<i32> {
i32::try_from(self.signed_i64()?).ok()
}
#[must_use]
pub const fn from_i64(value: i64) -> Self {
Self::new(value.is_negative(), LindenAmount(value.unsigned_abs()))
}
#[must_use]
pub fn to_i64(&self) -> Option<i64> {
self.signed_i64()
}
}
impl std::fmt::Display for LindenBalance {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.negative {
write!(f, "-{}", self.magnitude)
} else {
write!(f, "{}", self.magnitude)
}
}
}
impl Ord for LindenBalance {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.signed_i128().cmp(&other.signed_i128())
}
}
impl PartialOrd for LindenBalance {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl From<LindenAmount> for LindenBalance {
fn from(amount: LindenAmount) -> Self {
Self::new(false, amount)
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("L$ balance {balance} is negative and has no LindenAmount representation")]
#[non_exhaustive]
pub struct NegativeBalanceError {
pub balance: LindenBalance,
}
impl TryFrom<LindenBalance> for LindenAmount {
type Error = NegativeBalanceError;
fn try_from(balance: LindenBalance) -> Result<Self, Self::Error> {
if balance.negative {
Err(NegativeBalanceError { balance })
} else {
Ok(balance.magnitude)
}
}
}
impl std::ops::Add<LindenAmount> for LindenBalance {
type Output = Self;
fn add(self, rhs: LindenAmount) -> Self::Output {
let LindenAmount(value) = rhs;
#[expect(
clippy::arithmetic_side_effects,
reason = "operands are bounded by u64::MAX, so the sum fits in i128 without overflow"
)]
Self::from_signed_i128(self.signed_i128() + i128::from(value))
}
}
impl std::ops::Sub<LindenAmount> for LindenBalance {
type Output = Self;
fn sub(self, rhs: LindenAmount) -> Self::Output {
let LindenAmount(value) = rhs;
#[expect(
clippy::arithmetic_side_effects,
reason = "operands are bounded by u64::MAX, so the difference fits in i128 without overflow"
)]
Self::from_signed_i128(self.signed_i128() - i128::from(value))
}
}
impl std::ops::Add for LindenBalance {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
#[expect(
clippy::arithmetic_side_effects,
reason = "operands are bounded by u64::MAX in magnitude, so the sum fits in i128 without overflow"
)]
Self::from_signed_i128(self.signed_i128() + rhs.signed_i128())
}
}
impl std::ops::Sub for LindenBalance {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
#[expect(
clippy::arithmetic_side_effects,
reason = "operands are bounded by u64::MAX in magnitude, so the difference fits in i128 without overflow"
)]
Self::from_signed_i128(self.signed_i128() - rhs.signed_i128())
}
}
impl std::ops::Neg for LindenBalance {
type Output = Self;
fn neg(self) -> Self::Output {
Self::new(!self.negative, self.magnitude)
}
}
impl std::ops::AddAssign<LindenAmount> for LindenBalance {
fn add_assign(&mut self, rhs: LindenAmount) {
#[expect(
clippy::arithmetic_side_effects,
reason = "delegates to the annotated Add impl, which cannot overflow for u64-bounded operands"
)]
{
*self = self.clone() + rhs;
}
}
}
impl std::ops::SubAssign<LindenAmount> for LindenBalance {
fn sub_assign(&mut self, rhs: LindenAmount) {
#[expect(
clippy::arithmetic_side_effects,
reason = "delegates to the annotated Sub impl, which cannot overflow for u64-bounded operands"
)]
{
*self = self.clone() - rhs;
}
}
}
impl std::ops::AddAssign for LindenBalance {
fn add_assign(&mut self, rhs: Self) {
#[expect(
clippy::arithmetic_side_effects,
reason = "delegates to the annotated Add impl, which cannot overflow for u64-bounded operands"
)]
{
*self = self.clone() + rhs;
}
}
}
impl std::ops::SubAssign for LindenBalance {
fn sub_assign(&mut self, rhs: Self) {
#[expect(
clippy::arithmetic_side_effects,
reason = "delegates to the annotated Sub impl, which cannot overflow for u64-bounded operands"
)]
{
*self = self.clone() - rhs;
}
}
}
#[cfg(feature = "chumsky")]
#[must_use]
pub fn linden_balance_parser<'src>()
-> impl Parser<'src, &'src str, LindenBalance, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
{
just("-")
.or_not()
.then(linden_amount_parser())
.map(|(sign, magnitude)| LindenBalance::new(sign.is_some(), magnitude))
}
#[cfg(test)]
mod tests {
use super::{LindenAmount, LindenBalance, NegativeBalanceError};
use pretty_assertions::assert_eq;
#[test]
fn i32_wire_round_trips_bit_identically() {
for wire in [0_i32, 1, -1, 250, -250, i32::MAX, i32::MIN] {
let balance = LindenBalance::from_i32(wire);
assert_eq!(balance.to_i32(), Some(wire));
}
}
#[test]
fn negative_zero_normalises_to_non_negative() {
let from_ctor = LindenBalance::new(true, LindenAmount(0));
assert!(!from_ctor.is_negative());
assert!(from_ctor.is_zero());
assert_eq!(from_ctor, LindenBalance::ZERO);
assert_eq!(from_ctor, LindenBalance::from_i32(0));
}
#[test]
fn ordering_is_by_signed_value() {
let neg = LindenBalance::from_i32(-100);
let zero = LindenBalance::ZERO;
let pos = LindenBalance::from_i32(100);
assert!(neg < zero);
assert!(zero < pos);
assert!(neg < pos);
assert!(LindenBalance::from_i32(-200) < LindenBalance::from_i32(-100));
}
#[test]
fn amount_and_balance_compose_by_type() {
let balance = LindenBalance::from_i32(-30);
assert_eq!(balance + LindenAmount(100), LindenBalance::from_i32(70));
assert_eq!(
LindenBalance::from_i32(50) - LindenAmount(80),
LindenBalance::from_i32(-30)
);
assert_eq!(
LindenBalance::from_i32(10) + LindenBalance::from_i32(-25),
LindenBalance::from_i32(-15)
);
assert_eq!(-LindenBalance::from_i32(42), LindenBalance::from_i32(-42));
}
#[test]
fn linden_amount_interconverts() {
assert_eq!(
LindenBalance::from(LindenAmount(500)),
LindenBalance::from_i32(500)
);
assert_eq!(
LindenAmount::try_from(LindenBalance::from_i32(500)),
Ok(LindenAmount(500))
);
let err = LindenAmount::try_from(LindenBalance::from_i32(-1));
assert!(matches!(err, Err(NegativeBalanceError { .. })));
}
#[test]
fn out_of_i32_range_does_not_encode() {
let too_big = LindenBalance::from_i64(i64::from(i32::MAX) + 1);
assert_eq!(too_big.to_i32(), None);
assert_eq!(too_big.to_i64(), Some(i64::from(i32::MAX) + 1));
}
#[cfg(feature = "chumsky")]
#[test]
fn balance_parser_round_trips() {
use chumsky::Parser as _;
assert_eq!(
super::linden_balance_parser().parse("L$1234").into_result(),
Ok(LindenBalance::from_i32(1234))
);
assert_eq!(
super::linden_balance_parser()
.parse("-L$1234")
.into_result(),
Ok(LindenBalance::from_i32(-1234))
);
}
}