Skip to main content

triblespace_core/value/schemas/
r256.rs

1use crate::id::ExclusiveId;
2use crate::id::Id;
3use crate::id_hex;
4use crate::macros::entity;
5use crate::metadata;
6use crate::metadata::ConstMetadata;
7use crate::repo::BlobStore;
8use crate::trible::TribleSet;
9use crate::value::schemas::hash::Blake3;
10use crate::value::FromValue;
11use crate::value::ToValue;
12use crate::value::TryFromValue;
13use crate::value::Value;
14use crate::value::ValueSchema;
15use std::convert::Infallible;
16
17use std::convert::TryInto;
18
19#[cfg(feature = "wasm")]
20use crate::blob::schemas::wasmcode::WasmCode;
21use num_rational::Ratio;
22
23/// A 256-bit ratio value.
24/// It is stored as two 128-bit signed integers, the numerator and the denominator.
25/// The ratio is always reduced to its canonical form, which mean that the numerator and the denominator
26/// are coprime and the denominator is positive.
27/// Both the numerator and the denominator are stored in little-endian byte order,
28/// with the numerator in the first 16 bytes and the denominator in the last 16 bytes.
29///
30/// For a big-endian version, see [R256BE].
31pub struct R256LE;
32
33/// A 256-bit ratio value.
34/// It is stored as two 128-bit signed integers, the numerator and the denominator.
35/// The ratio is always reduced to its canonical form, which mean that the numerator and the denominator
36/// are coprime and the denominator is positive.
37/// Both the numerator and the denominator are stored in big-endian byte order,
38/// with the numerator in the first 16 bytes and the denominator in the last 16 bytes.
39///
40/// For a little-endian version, see [R256LE].
41pub struct R256BE;
42
43pub type R256 = R256LE;
44
45impl ConstMetadata for R256LE {
46    fn id() -> Id {
47        id_hex!("0A9B43C5C2ECD45B257CDEFC16544358")
48    }
49
50    fn describe<B>(blobs: &mut B) -> Result<TribleSet, B::PutError>
51    where
52        B: BlobStore<Blake3>,
53    {
54        let id = Self::id();
55        let description = blobs.put(
56            "Exact ratio stored as two i128 values (numerator/denominator) in little-endian, normalized with a positive denominator. This keeps fractions canonical and comparable.\n\nUse for exact rates, proportions, or unit conversions where rounding is unacceptable. Prefer F64 or F256 when approximate floats are fine or when interfacing with floating-point APIs.\n\nDenominator zero is invalid; the schema expects canonicalized fractions. If you need intervals or ranges instead of ratios, use the range schemas.",
57        )?;
58        let tribles = entity! {
59            ExclusiveId::force_ref(&id) @
60                metadata::name: blobs.put("r256le".to_string())?,
61                metadata::description: description,
62                metadata::tag: metadata::KIND_VALUE_SCHEMA,
63        };
64
65        #[cfg(feature = "wasm")]
66        let tribles = {
67            let mut tribles = tribles;
68            tribles += entity! { ExclusiveId::force_ref(&id) @
69                metadata::value_formatter: blobs.put(wasm_formatter::R256_LE_WASM)?,
70            };
71            tribles
72        };
73        Ok(tribles)
74    }
75}
76impl ValueSchema for R256LE {
77    type ValidationError = Infallible;
78}
79impl ConstMetadata for R256BE {
80    fn id() -> Id {
81        id_hex!("CA5EAF567171772C1FFD776E9C7C02D1")
82    }
83
84    fn describe<B>(blobs: &mut B) -> Result<TribleSet, B::PutError>
85    where
86        B: BlobStore<Blake3>,
87    {
88        let id = Self::id();
89        let description = blobs.put(
90            "Exact ratio stored as two i128 values (numerator/denominator) in big-endian, normalized with a positive denominator. This is useful when bytewise ordering or protocol encoding matters.\n\nUse for exact fractions in ordered or interoperable formats. Prefer F64 or F256 when approximate floats are acceptable.\n\nAs with the little-endian variant, values are expected to be canonical and denominator must be non-zero.",
91        )?;
92        let tribles = entity! {
93            ExclusiveId::force_ref(&id) @
94                metadata::name: blobs.put("r256be".to_string())?,
95                metadata::description: description,
96                metadata::tag: metadata::KIND_VALUE_SCHEMA,
97        };
98
99        #[cfg(feature = "wasm")]
100        let tribles = {
101            let mut tribles = tribles;
102            tribles += entity! { ExclusiveId::force_ref(&id) @
103                metadata::value_formatter: blobs.put(wasm_formatter::R256_BE_WASM)?,
104            };
105            tribles
106        };
107        Ok(tribles)
108    }
109}
110
111#[cfg(feature = "wasm")]
112mod wasm_formatter {
113    use core::fmt::Write;
114
115    use triblespace_core_macros::value_formatter;
116
117    #[value_formatter]
118    pub(crate) fn r256_le(raw: &[u8; 32], out: &mut impl Write) -> Result<(), u32> {
119        let mut buf = [0u8; 16];
120        buf.copy_from_slice(&raw[..16]);
121        let numer = i128::from_le_bytes(buf);
122        buf.copy_from_slice(&raw[16..]);
123        let denom = i128::from_le_bytes(buf);
124
125        if denom == 0 {
126            return Err(2);
127        }
128
129        if denom == 1 {
130            write!(out, "{numer}").map_err(|_| 1u32)?;
131        } else {
132            write!(out, "{numer}/{denom}").map_err(|_| 1u32)?;
133        }
134        Ok(())
135    }
136
137    #[value_formatter]
138    pub(crate) fn r256_be(raw: &[u8; 32], out: &mut impl Write) -> Result<(), u32> {
139        let mut buf = [0u8; 16];
140        buf.copy_from_slice(&raw[..16]);
141        let numer = i128::from_be_bytes(buf);
142        buf.copy_from_slice(&raw[16..]);
143        let denom = i128::from_be_bytes(buf);
144
145        if denom == 0 {
146            return Err(2);
147        }
148
149        if denom == 1 {
150            write!(out, "{numer}").map_err(|_| 1u32)?;
151        } else {
152            write!(out, "{numer}/{denom}").map_err(|_| 1u32)?;
153        }
154        Ok(())
155    }
156}
157impl ValueSchema for R256BE {
158    type ValidationError = Infallible;
159}
160
161/// An error that can occur when converting a ratio value.
162///
163/// The error can be caused by a non-canonical ratio, where the numerator and the denominator are not coprime,
164/// or by a zero denominator.
165#[derive(Debug)]
166pub enum RatioError {
167    NonCanonical(i128, i128),
168    ZeroDenominator,
169}
170
171impl TryFromValue<'_, R256BE> for Ratio<i128> {
172    type Error = RatioError;
173
174    fn try_from_value(v: &Value<R256BE>) -> Result<Self, Self::Error> {
175        let n = i128::from_be_bytes(v.raw[0..16].try_into().unwrap());
176        let d = i128::from_be_bytes(v.raw[16..32].try_into().unwrap());
177
178        if d == 0 {
179            return Err(RatioError::ZeroDenominator);
180        }
181
182        let ratio = Ratio::new_raw(n, d);
183        let ratio = ratio.reduced();
184        let (reduced_n, reduced_d) = ratio.into_raw();
185
186        if reduced_n != n || reduced_d != d {
187            Err(RatioError::NonCanonical(n, d))
188        } else {
189            Ok(ratio)
190        }
191    }
192}
193
194impl FromValue<'_, R256BE> for Ratio<i128> {
195    fn from_value(v: &Value<R256BE>) -> Self {
196        match Ratio::try_from_value(v) {
197            Ok(ratio) => ratio,
198            Err(RatioError::NonCanonical(n, d)) => {
199                panic!("Non canonical ratio: {n}/{d}");
200            }
201            Err(RatioError::ZeroDenominator) => {
202                panic!("Zero denominator ratio");
203            }
204        }
205    }
206}
207
208impl ToValue<R256BE> for Ratio<i128> {
209    fn to_value(self) -> Value<R256BE> {
210        let ratio = self.reduced();
211
212        let mut bytes = [0; 32];
213        bytes[0..16].copy_from_slice(&ratio.numer().to_be_bytes());
214        bytes[16..32].copy_from_slice(&ratio.denom().to_be_bytes());
215
216        Value::new(bytes)
217    }
218}
219
220impl ToValue<R256BE> for i128 {
221    fn to_value(self) -> Value<R256BE> {
222        let mut bytes = [0; 32];
223        bytes[0..16].copy_from_slice(&self.to_be_bytes());
224        bytes[16..32].copy_from_slice(&1i128.to_be_bytes());
225
226        Value::new(bytes)
227    }
228}
229
230impl TryFromValue<'_, R256LE> for Ratio<i128> {
231    type Error = RatioError;
232
233    fn try_from_value(v: &Value<R256LE>) -> Result<Self, Self::Error> {
234        let n = i128::from_le_bytes(v.raw[0..16].try_into().unwrap());
235        let d = i128::from_le_bytes(v.raw[16..32].try_into().unwrap());
236
237        if d == 0 {
238            return Err(RatioError::ZeroDenominator);
239        }
240
241        let ratio = Ratio::new_raw(n, d);
242        let ratio = ratio.reduced();
243        let (reduced_n, reduced_d) = ratio.into_raw();
244
245        if reduced_n != n || reduced_d != d {
246            Err(RatioError::NonCanonical(n, d))
247        } else {
248            Ok(ratio)
249        }
250    }
251}
252
253impl FromValue<'_, R256LE> for Ratio<i128> {
254    fn from_value(v: &Value<R256LE>) -> Self {
255        match Ratio::try_from_value(v) {
256            Ok(ratio) => ratio,
257            Err(RatioError::NonCanonical(n, d)) => {
258                panic!("Non canonical ratio: {n}/{d}");
259            }
260            Err(RatioError::ZeroDenominator) => {
261                panic!("Zero denominator ratio");
262            }
263        }
264    }
265}
266
267impl ToValue<R256LE> for Ratio<i128> {
268    fn to_value(self) -> Value<R256LE> {
269        let mut bytes = [0; 32];
270        bytes[0..16].copy_from_slice(&self.numer().to_le_bytes());
271        bytes[16..32].copy_from_slice(&self.denom().to_le_bytes());
272
273        Value::new(bytes)
274    }
275}
276
277impl ToValue<R256LE> for i128 {
278    fn to_value(self) -> Value<R256LE> {
279        let mut bytes = [0; 32];
280        bytes[0..16].copy_from_slice(&self.to_le_bytes());
281        bytes[16..32].copy_from_slice(&1i128.to_le_bytes());
282
283        Value::new(bytes)
284    }
285}