#decimal #single-line #leverage #fixed-point #traits #generate #generics

safe_decimal_core

这是一个针对区块链开发定制的Rust定点数字库。最初是作为Invariant协议的一部分创建并使用的。当前版本利用宏、特性和泛型,将数十行易于出错的代码替换为单行,并生成其余部分。

2个版本

0.1.1 2023年6月3日
0.1.0 2023年6月3日

#483过程宏

24 每月下载

MIT 许可证

47KB
843

十进制库

Decimal macro crate

这是一个针对区块链的Rust定点数字库。它完全是为了实际原因而创建的,作为一种快速简单的方式来使用带有指定十进制精度的检查数学。

它经过多次迭代达到现在的形式,最初是在 Synthetify协议 中实现。当前版本利用宏、特性和泛型,将数十行易于出错的代码替换为单行,并生成其余部分。这种形式在 Invariant协议 中使用,并作为其一部分进行了审计。

它允许定义具有不同精度和原始类型的多个类型,以及它们之间的计算,下面是一个快速示例。

快速入门

使用该库需要添加一个宏 #[decimal(k)],其中 k 是所需的十进制精度(小数点后的数字位数)。

此宏为每个调用的结构生成几个泛型特性的实现,允许它们之间进行基本操作。

基本示例

导入库后,可以声明如下类型的变量

#[decimal(2)]
#[derive(Default, PartialEq, Debug, Clone, Copy)]
struct Percentage(u32);
  • #[decimal(3, u128)] - 调用宏生成十进制代码
  • #[derive(Default, Debug, Clone, Copy, PartialEq)] - 导出一些常用的内置特质(这五个是必需的)
  • struct R(u32); - 结构体本身的声明

反序列化

带名称的结构体可以像这样无问题地进行反序列化

#[decimal(6)]
#[zero_copy]
#[derive(AnchorSerialize, AnchorDeserialize, ...)]
pub struct Price {
    pub v: u128,
}

基本操作

宏生成的所有方法都使用检查数学,并在溢出时恐慌。尽可能地对运算符进行重载以方便使用。

使用上面定义的类型的基本示例看起来如下

let price = Price::from_integer(10); // this corresponds with 10 * 10^k so 10^7 in this case

let discount = Percentage::new(10); // using new doesn't account for decimal places so 0.10 here

// addition expects being called for left and right values being of the same type
// multiplication doesn't so you can be used like this:
let price = price * (Percentage::from_integer(1) - discount); // the resulting type is always the type of the left value

更多示例请参阅walkthrough.rs

宏的参数

如前所述,宏的第一个参数控制小数点后的位数。它可以介于0到38之间。它可以通过Price::scale()读取。第二个是可选的,可能更难理解

大类型

所接受的第二个参数有一个奇特的名称,即大类型。它设置在调用带有 _big_ 名称的方法时使用的类型。其目的是避免临时溢出,即使值适合给定的类型,计算S返回值时也会发生溢出。考虑以下示例

#[decimal(2)]
#[derive(Default, Debug, Clone, Copy, PartialEq)]
struct Percentage(u8, u128);

let p = Percentage(110); // 110% or 1.1

// assert_eq!(p * p, Percentage(121));      <- this would panic
assert_eq!(p.big_mul(p), Percentage(121));  <- this will work fine

要理解为什么它会这样工作,请查看decimal在底层是如何进行乘法的

内部发生的事情(在数学方面)

这个库的大部分使用的是非常基础的数学,下面列出了一些可能不明显的事情

保持比例

使用值(比例2)进行两个百分比的乘法看起来像这样

(x/ 10^2) / (y/ 10^2) =x/y

(上帝,我恨gh不允许LaTeX)

使用数字会是这样

10% / 10% = 10 / 10 = 1

这显然是错误的。我们需要做的是将每个值都乘以10^scale。所以它应该看起来像这样

(x/ 10^scale) / (y/ 10^scale)×10^scale=x/y ×10^scale

这与上面的例子相符

一般来说,每次值的乘法都需要进行除法,反之亦然。这是这个库的第一个目的——抽象它以减少代码、错误和浪费时间。

这里重要的是,乘法必须在除法之前进行,以保持精度,但这也被抽象掉了。

舍入误差

默认情况下,每个方法都向下舍入,但有以up结尾的对应方法会以相反的方式舍入。

舍入是通过将denominator - 1添加到分子来实现的,所以mul_up看起来像这样

(x × y+ 10^scale- 1) / 10^scale

例如,对于10% × 1%

(10×1 + (10^2 - 1)) / (10^2) = 109 / 100 = 1%

内部发生的事情(在代码层面)

如您所知,整个库都采用宏的形式。在其内部,以通用形式实现了一些特质的实现,以便在实现之间调用方法。

  • Decimal - 所有其他特质都依赖于它,通过实现它,您可以使用您的实现与任何其他特质一起使用。一个用例可能是将其实现为2的基数

    • 类型 U: Debug + 默认值; - 一个相关的类型,是值存储的原始类型(或非原始类型),宏被调用的结构体中第一个字段的类型
    • fn get(&self) -> Self::U; - 十进制数的值
    • fn new(value: Self::U) -> Self; - 构造函数
    • fn max_value() -> Self::U - 基底类型的最大值
    • fn max_instance() -> Self - 将 max_value() 用十进制包裹
    • fn here<Y: TryFrom<Self::U>>(&self) -> Y; - 与get相同,但还尝试转换成所需值
    • fn scale() -> u8; - 小数点后的位数(由宏给出)
    • fn one<T: TryFrom<u128>>() -> T; - 基本上是 10^scale,在编译时评估
    • fn almost_one<T: TryFrom<u128>>() -> T; - 与上面相同,但 -1,也适用于编译时
  • std::ops - 加、减、乘、除以及相应的赋值运算符(+=)

  • pub trait BigOps<T> - 与上面相同,但使用了之前提到的用于计算的大类型

  • pub trait Others<T> - 供将来使用的方法特性,目前只有两个方法

    • fn mul_up(self, rhs: T) -> Self; - 乘法,向上取整
    • fn div_up(self, rhs: T) -> Self; - 除法,向上取整
  • pub trait Factories<T> - 作为构造函数使用的方法(不包括 new)

    • fn from_integer(integer: T) -> Self; - 创建具有值 integer × 10^scale 的 self
    • fn from_scale(integer: T, scale: u8) -> Self; - 创建具有值 integer × 10^(scale - given\_scale) 的 self
    • fn checked_from_scale(val: T, scale: u8) -> <Self, String> - from_scale 的检查版本
    • fn from_scale_up(integer: T, scale: u8) -> Self; - 与上面相同,但向上取整
  • pub trait BetweenDecimals<T> - 用于在具有不同比例的不同类型之间进行转换

  • pub trait ToValue<T, B>pub trait ByNumber<B> - 可以一起使用,将溢出的值移出类型,然后再放入,通常不需要经常使用

依赖项

~3.5–4.5MB
~88K SLoC