triblespace_core/value/schemas/
r256.rs1use 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
20pub struct R256LE;
29
30impl ConstId for R256LE {
31 const ID: Id = id_hex!("0A9B43C5C2ECD45B257CDEFC16544358");
32}
33
34pub 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#[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}