Skip to main content

triblespace_core/value/schemas/
r256.rs

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