1#[cfg(feature = "chumsky")]
4use chumsky::{IterParser as _, Parser, prelude::just, text::digits};
5
6#[derive(
8 Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
9)]
10pub struct LindenAmount(pub u64);
11
12impl std::fmt::Display for LindenAmount {
13 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
14 let Self(value) = self;
15 write!(f, "{value} L$")
16 }
17}
18
19impl std::ops::Add for LindenAmount {
20 type Output = Self;
21
22 fn add(self, rhs: Self) -> Self::Output {
23 let Self(lhs) = self;
24 let Self(rhs) = rhs;
25 #[expect(
26 clippy::arithmetic_side_effects,
27 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"
28 )]
29 Self(lhs + rhs)
30 }
31}
32
33impl std::ops::Sub for LindenAmount {
34 type Output = Self;
35
36 fn sub(self, rhs: Self) -> Self::Output {
37 let Self(lhs) = self;
38 let Self(rhs) = rhs;
39 #[expect(
40 clippy::arithmetic_side_effects,
41 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"
42 )]
43 Self(lhs - rhs)
44 }
45}
46
47impl std::ops::Mul<u8> for LindenAmount {
48 type Output = Self;
49
50 fn mul(self, rhs: u8) -> Self::Output {
51 let Self(lhs) = self;
52 #[expect(
53 clippy::arithmetic_side_effects,
54 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"
55 )]
56 Self(lhs * u64::from(rhs))
57 }
58}
59
60impl std::ops::Mul<u16> for LindenAmount {
61 type Output = Self;
62
63 fn mul(self, rhs: u16) -> Self::Output {
64 let Self(lhs) = self;
65 #[expect(
66 clippy::arithmetic_side_effects,
67 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"
68 )]
69 Self(lhs * u64::from(rhs))
70 }
71}
72
73impl std::ops::Mul<u32> for LindenAmount {
74 type Output = Self;
75
76 fn mul(self, rhs: u32) -> Self::Output {
77 let Self(lhs) = self;
78 #[expect(
79 clippy::arithmetic_side_effects,
80 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"
81 )]
82 Self(lhs * u64::from(rhs))
83 }
84}
85
86impl std::ops::Mul<u64> for LindenAmount {
87 type Output = Self;
88
89 fn mul(self, rhs: u64) -> Self::Output {
90 let Self(lhs) = self;
91 #[expect(
92 clippy::arithmetic_side_effects,
93 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"
94 )]
95 Self(lhs * rhs)
96 }
97}
98
99impl std::ops::Div<u8> for LindenAmount {
100 type Output = Self;
101
102 fn div(self, rhs: u8) -> Self::Output {
103 let Self(lhs) = self;
104 #[expect(
105 clippy::arithmetic_side_effects,
106 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"
107 )]
108 Self(lhs / u64::from(rhs))
109 }
110}
111
112impl std::ops::Div<u16> for LindenAmount {
113 type Output = Self;
114
115 fn div(self, rhs: u16) -> Self::Output {
116 let Self(lhs) = self;
117 #[expect(
118 clippy::arithmetic_side_effects,
119 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"
120 )]
121 Self(lhs / u64::from(rhs))
122 }
123}
124
125impl std::ops::Div<u32> for LindenAmount {
126 type Output = Self;
127
128 fn div(self, rhs: u32) -> Self::Output {
129 let Self(lhs) = self;
130 #[expect(
131 clippy::arithmetic_side_effects,
132 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"
133 )]
134 Self(lhs / u64::from(rhs))
135 }
136}
137
138impl std::ops::Div<u64> for LindenAmount {
139 type Output = Self;
140
141 fn div(self, rhs: u64) -> Self::Output {
142 let Self(lhs) = self;
143 #[expect(
144 clippy::arithmetic_side_effects,
145 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"
146 )]
147 Self(lhs / rhs)
148 }
149}
150
151impl std::ops::Rem<u8> for LindenAmount {
152 type Output = Self;
153
154 fn rem(self, rhs: u8) -> Self::Output {
155 let Self(lhs) = self;
156 #[expect(
157 clippy::arithmetic_side_effects,
158 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"
159 )]
160 Self(lhs % u64::from(rhs))
161 }
162}
163
164impl std::ops::Rem<u16> for LindenAmount {
165 type Output = Self;
166
167 fn rem(self, rhs: u16) -> Self::Output {
168 let Self(lhs) = self;
169 #[expect(
170 clippy::arithmetic_side_effects,
171 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"
172 )]
173 Self(lhs % u64::from(rhs))
174 }
175}
176
177impl std::ops::Rem<u32> for LindenAmount {
178 type Output = Self;
179
180 fn rem(self, rhs: u32) -> Self::Output {
181 let Self(lhs) = self;
182 #[expect(
183 clippy::arithmetic_side_effects,
184 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"
185 )]
186 Self(lhs % u64::from(rhs))
187 }
188}
189
190impl std::ops::Rem<u64> for LindenAmount {
191 type Output = Self;
192
193 fn rem(self, rhs: u64) -> Self::Output {
194 let Self(lhs) = self;
195 #[expect(
196 clippy::arithmetic_side_effects,
197 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"
198 )]
199 Self(lhs % rhs)
200 }
201}
202
203#[cfg(feature = "chumsky")]
211#[must_use]
212pub fn linden_amount_parser<'src>()
213-> impl Parser<'src, &'src str, LindenAmount, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
214{
215 just("L$")
216 .ignore_then(digits(10).collect::<String>())
217 .try_map(|x: String, span: chumsky::span::SimpleSpan| {
218 Ok(LindenAmount(x.parse().map_err(|e| {
219 chumsky::error::Rich::custom(span, format!("{e:?}"))
220 })?))
221 })
222}
223
224#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
238pub struct LindenBalance {
239 negative: bool,
242 magnitude: LindenAmount,
244}
245
246impl LindenBalance {
247 pub const ZERO: Self = Self {
249 negative: false,
250 magnitude: LindenAmount(0),
251 };
252
253 #[must_use]
256 pub const fn new(negative: bool, magnitude: LindenAmount) -> Self {
257 let LindenAmount(value) = magnitude;
258 Self {
259 negative: negative && value != 0,
260 magnitude,
261 }
262 }
263
264 #[must_use]
266 pub const fn is_negative(&self) -> bool {
267 self.negative
268 }
269
270 #[must_use]
272 pub const fn is_zero(&self) -> bool {
273 let LindenAmount(value) = self.magnitude;
274 value == 0
275 }
276
277 #[must_use]
279 pub fn magnitude(&self) -> LindenAmount {
280 self.magnitude.clone()
281 }
282
283 fn signed_i128(&self) -> i128 {
287 let LindenAmount(value) = self.magnitude;
288 let magnitude = i128::from(value);
289 if self.negative {
290 magnitude.wrapping_neg()
291 } else {
292 magnitude
293 }
294 }
295
296 fn from_signed_i128(value: i128) -> Self {
299 let magnitude = u64::try_from(value.unsigned_abs()).unwrap_or(u64::MAX);
300 Self::new(value.is_negative(), LindenAmount(magnitude))
301 }
302
303 fn signed_i64(&self) -> Option<i64> {
306 let LindenAmount(value) = self.magnitude;
307 let magnitude = i64::try_from(value).ok()?;
308 Some(if self.negative {
309 magnitude.wrapping_neg()
310 } else {
311 magnitude
312 })
313 }
314
315 #[must_use]
318 pub fn from_i32(value: i32) -> Self {
319 Self::new(
320 value.is_negative(),
321 LindenAmount(u64::from(value.unsigned_abs())),
322 )
323 }
324
325 #[must_use]
328 pub fn to_i32(&self) -> Option<i32> {
329 i32::try_from(self.signed_i64()?).ok()
330 }
331
332 #[must_use]
335 pub const fn from_i64(value: i64) -> Self {
336 Self::new(value.is_negative(), LindenAmount(value.unsigned_abs()))
337 }
338
339 #[must_use]
342 pub fn to_i64(&self) -> Option<i64> {
343 self.signed_i64()
344 }
345}
346
347impl std::fmt::Display for LindenBalance {
348 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
349 if self.negative {
350 write!(f, "-{}", self.magnitude)
351 } else {
352 write!(f, "{}", self.magnitude)
353 }
354 }
355}
356
357impl Ord for LindenBalance {
358 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
359 self.signed_i128().cmp(&other.signed_i128())
360 }
361}
362
363impl PartialOrd for LindenBalance {
364 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
365 Some(self.cmp(other))
366 }
367}
368
369impl From<LindenAmount> for LindenBalance {
370 fn from(amount: LindenAmount) -> Self {
371 Self::new(false, amount)
372 }
373}
374
375#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
378#[error("L$ balance {balance} is negative and has no LindenAmount representation")]
379#[non_exhaustive]
380pub struct NegativeBalanceError {
381 pub balance: LindenBalance,
383}
384
385impl TryFrom<LindenBalance> for LindenAmount {
386 type Error = NegativeBalanceError;
387
388 fn try_from(balance: LindenBalance) -> Result<Self, Self::Error> {
389 if balance.negative {
390 Err(NegativeBalanceError { balance })
391 } else {
392 Ok(balance.magnitude)
393 }
394 }
395}
396
397impl std::ops::Add<LindenAmount> for LindenBalance {
398 type Output = Self;
399
400 fn add(self, rhs: LindenAmount) -> Self::Output {
401 let LindenAmount(value) = rhs;
402 #[expect(
403 clippy::arithmetic_side_effects,
404 reason = "operands are bounded by u64::MAX, so the sum fits in i128 without overflow"
405 )]
406 Self::from_signed_i128(self.signed_i128() + i128::from(value))
407 }
408}
409
410impl std::ops::Sub<LindenAmount> for LindenBalance {
411 type Output = Self;
412
413 fn sub(self, rhs: LindenAmount) -> Self::Output {
414 let LindenAmount(value) = rhs;
415 #[expect(
416 clippy::arithmetic_side_effects,
417 reason = "operands are bounded by u64::MAX, so the difference fits in i128 without overflow"
418 )]
419 Self::from_signed_i128(self.signed_i128() - i128::from(value))
420 }
421}
422
423impl std::ops::Add for LindenBalance {
424 type Output = Self;
425
426 fn add(self, rhs: Self) -> Self::Output {
427 #[expect(
428 clippy::arithmetic_side_effects,
429 reason = "operands are bounded by u64::MAX in magnitude, so the sum fits in i128 without overflow"
430 )]
431 Self::from_signed_i128(self.signed_i128() + rhs.signed_i128())
432 }
433}
434
435impl std::ops::Sub for LindenBalance {
436 type Output = Self;
437
438 fn sub(self, rhs: Self) -> Self::Output {
439 #[expect(
440 clippy::arithmetic_side_effects,
441 reason = "operands are bounded by u64::MAX in magnitude, so the difference fits in i128 without overflow"
442 )]
443 Self::from_signed_i128(self.signed_i128() - rhs.signed_i128())
444 }
445}
446
447impl std::ops::Neg for LindenBalance {
448 type Output = Self;
449
450 fn neg(self) -> Self::Output {
451 Self::new(!self.negative, self.magnitude)
452 }
453}
454
455impl std::ops::AddAssign<LindenAmount> for LindenBalance {
456 fn add_assign(&mut self, rhs: LindenAmount) {
457 #[expect(
458 clippy::arithmetic_side_effects,
459 reason = "delegates to the annotated Add impl, which cannot overflow for u64-bounded operands"
460 )]
461 {
462 *self = self.clone() + rhs;
463 }
464 }
465}
466
467impl std::ops::SubAssign<LindenAmount> for LindenBalance {
468 fn sub_assign(&mut self, rhs: LindenAmount) {
469 #[expect(
470 clippy::arithmetic_side_effects,
471 reason = "delegates to the annotated Sub impl, which cannot overflow for u64-bounded operands"
472 )]
473 {
474 *self = self.clone() - rhs;
475 }
476 }
477}
478
479impl std::ops::AddAssign for LindenBalance {
480 fn add_assign(&mut self, rhs: Self) {
481 #[expect(
482 clippy::arithmetic_side_effects,
483 reason = "delegates to the annotated Add impl, which cannot overflow for u64-bounded operands"
484 )]
485 {
486 *self = self.clone() + rhs;
487 }
488 }
489}
490
491impl std::ops::SubAssign for LindenBalance {
492 fn sub_assign(&mut self, rhs: Self) {
493 #[expect(
494 clippy::arithmetic_side_effects,
495 reason = "delegates to the annotated Sub impl, which cannot overflow for u64-bounded operands"
496 )]
497 {
498 *self = self.clone() - rhs;
499 }
500 }
501}
502
503#[cfg(feature = "chumsky")]
511#[must_use]
512pub fn linden_balance_parser<'src>()
513-> impl Parser<'src, &'src str, LindenBalance, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
514{
515 just("-")
516 .or_not()
517 .then(linden_amount_parser())
518 .map(|(sign, magnitude)| LindenBalance::new(sign.is_some(), magnitude))
519}
520
521#[cfg(test)]
522mod tests {
523 use super::{LindenAmount, LindenBalance, NegativeBalanceError};
524 use pretty_assertions::assert_eq;
525
526 #[test]
527 fn i32_wire_round_trips_bit_identically() {
528 for wire in [0_i32, 1, -1, 250, -250, i32::MAX, i32::MIN] {
529 let balance = LindenBalance::from_i32(wire);
530 assert_eq!(balance.to_i32(), Some(wire));
531 }
532 }
533
534 #[test]
535 fn negative_zero_normalises_to_non_negative() {
536 let from_ctor = LindenBalance::new(true, LindenAmount(0));
537 assert!(!from_ctor.is_negative());
538 assert!(from_ctor.is_zero());
539 assert_eq!(from_ctor, LindenBalance::ZERO);
540 assert_eq!(from_ctor, LindenBalance::from_i32(0));
541 }
542
543 #[test]
544 fn ordering_is_by_signed_value() {
545 let neg = LindenBalance::from_i32(-100);
546 let zero = LindenBalance::ZERO;
547 let pos = LindenBalance::from_i32(100);
548 assert!(neg < zero);
549 assert!(zero < pos);
550 assert!(neg < pos);
551 assert!(LindenBalance::from_i32(-200) < LindenBalance::from_i32(-100));
553 }
554
555 #[test]
556 fn amount_and_balance_compose_by_type() {
557 let balance = LindenBalance::from_i32(-30);
558 assert_eq!(balance + LindenAmount(100), LindenBalance::from_i32(70));
559 assert_eq!(
560 LindenBalance::from_i32(50) - LindenAmount(80),
561 LindenBalance::from_i32(-30)
562 );
563 assert_eq!(
564 LindenBalance::from_i32(10) + LindenBalance::from_i32(-25),
565 LindenBalance::from_i32(-15)
566 );
567 assert_eq!(-LindenBalance::from_i32(42), LindenBalance::from_i32(-42));
568 }
569
570 #[test]
571 fn linden_amount_interconverts() {
572 assert_eq!(
573 LindenBalance::from(LindenAmount(500)),
574 LindenBalance::from_i32(500)
575 );
576 assert_eq!(
577 LindenAmount::try_from(LindenBalance::from_i32(500)),
578 Ok(LindenAmount(500))
579 );
580 let err = LindenAmount::try_from(LindenBalance::from_i32(-1));
581 assert!(matches!(err, Err(NegativeBalanceError { .. })));
582 }
583
584 #[test]
585 fn out_of_i32_range_does_not_encode() {
586 let too_big = LindenBalance::from_i64(i64::from(i32::MAX) + 1);
587 assert_eq!(too_big.to_i32(), None);
588 assert_eq!(too_big.to_i64(), Some(i64::from(i32::MAX) + 1));
589 }
590
591 #[cfg(feature = "chumsky")]
592 #[test]
593 fn balance_parser_round_trips() {
594 use chumsky::Parser as _;
595 assert_eq!(
596 super::linden_balance_parser().parse("L$1234").into_result(),
597 Ok(LindenBalance::from_i32(1234))
598 );
599 assert_eq!(
600 super::linden_balance_parser()
601 .parse("-L$1234")
602 .into_result(),
603 Ok(LindenBalance::from_i32(-1234))
604 );
605 }
606}