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