5个版本
0.1.4 | 2022年6月1日 |
---|---|
0.1.3 | 2022年4月25日 |
0.1.2 | 2022年4月24日 |
0.1.1 | 2022年4月24日 |
0.1.0 | 2022年4月24日 |
#33 在 #checked
21 每月下载次数
在 checked_decimal_macro 中使用
33KB
714 行
十进制库
这是一个针对区块链的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_ 的方法时要使用的类型。它的目的是避免临时溢出,即使值适合给定的类型,但在计算返回值时发生溢出。考虑以下示例
#[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
要了解为什么它会这样工作,请看十进制乘法的底层实现
数学内部发生了什么
大多数这个库使用非常基础的数学,以下列出了几个可能不明显的事情
保持缩放
使用数值的两个百分比(缩放为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。type U: Debug + Default;
- 一个关联类型,原始(或非原始)存储值的类型,以及宏被调用的结构体中第一个字段的类型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;
- 创建具有值的self:integer × 10^scale
fn from_scale(integer: T, scale: u8) -> Self;
- 创建具有值的self:integer × 10^(scale - given\_scale)
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
~89K SLoC