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;
50
51impl ConstDescribe for R256LE {
52 fn describe<B>(blobs: &mut B) -> Result<Fragment, B::PutError>
53 where
54 B: BlobStore<Blake3>,
55 {
56 let id = Self::ID;
57 let description = blobs.put(
58 "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.",
59 )?;
60 let tribles = entity! {
61 ExclusiveId::force_ref(&id) @
62 metadata::name: blobs.put("r256le")?,
63 metadata::description: description,
64 metadata::tag: metadata::KIND_VALUE_SCHEMA,
65 };
66
67 #[cfg(feature = "wasm")]
68 let tribles = {
69 let mut tribles = tribles;
70 tribles += entity! { ExclusiveId::force_ref(&id) @
71 metadata::value_formatter: blobs.put(wasm_formatter::R256_LE_WASM)?,
72 };
73 tribles
74 };
75 Ok(tribles)
76 }
77}
78impl ValueSchema for R256LE {
79 type ValidationError = Infallible;
80}
81impl ConstDescribe for R256BE {
82 fn describe<B>(blobs: &mut B) -> Result<Fragment, B::PutError>
83 where
84 B: BlobStore<Blake3>,
85 {
86 let id = Self::ID;
87 let description = blobs.put(
88 "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.",
89 )?;
90 let tribles = entity! {
91 ExclusiveId::force_ref(&id) @
92 metadata::name: blobs.put("r256be")?,
93 metadata::description: description,
94 metadata::tag: metadata::KIND_VALUE_SCHEMA,
95 };
96
97 #[cfg(feature = "wasm")]
98 let tribles = {
99 let mut tribles = tribles;
100 tribles += entity! { ExclusiveId::force_ref(&id) @
101 metadata::value_formatter: blobs.put(wasm_formatter::R256_BE_WASM)?,
102 };
103 tribles
104 };
105 Ok(tribles)
106 }
107}
108
109#[cfg(feature = "wasm")]
110mod wasm_formatter {
111 use core::fmt::Write;
112
113 use triblespace_core_macros::value_formatter;
114
115 #[value_formatter]
116 pub(crate) fn r256_le(raw: &[u8; 32], out: &mut impl Write) -> Result<(), u32> {
117 let mut buf = [0u8; 16];
118 buf.copy_from_slice(&raw[..16]);
119 let numer = i128::from_le_bytes(buf);
120 buf.copy_from_slice(&raw[16..]);
121 let denom = i128::from_le_bytes(buf);
122
123 if denom == 0 {
124 return Err(2);
125 }
126
127 if denom == 1 {
128 write!(out, "{numer}").map_err(|_| 1u32)?;
129 } else {
130 write!(out, "{numer}/{denom}").map_err(|_| 1u32)?;
131 }
132 Ok(())
133 }
134
135 #[value_formatter]
136 pub(crate) fn r256_be(raw: &[u8; 32], out: &mut impl Write) -> Result<(), u32> {
137 let mut buf = [0u8; 16];
138 buf.copy_from_slice(&raw[..16]);
139 let numer = i128::from_be_bytes(buf);
140 buf.copy_from_slice(&raw[16..]);
141 let denom = i128::from_be_bytes(buf);
142
143 if denom == 0 {
144 return Err(2);
145 }
146
147 if denom == 1 {
148 write!(out, "{numer}").map_err(|_| 1u32)?;
149 } else {
150 write!(out, "{numer}/{denom}").map_err(|_| 1u32)?;
151 }
152 Ok(())
153 }
154}
155impl ValueSchema for R256BE {
156 type ValidationError = Infallible;
157}
158
159#[derive(Debug)]
164pub enum RatioError {
165 NonCanonical(i128, i128),
167 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
194
195impl ToValue<R256BE> for Ratio<i128> {
196 fn to_value(self) -> Value<R256BE> {
197 let ratio = self.reduced();
198
199 let mut bytes = [0; 32];
200 bytes[0..16].copy_from_slice(&ratio.numer().to_be_bytes());
201 bytes[16..32].copy_from_slice(&ratio.denom().to_be_bytes());
202
203 Value::new(bytes)
204 }
205}
206
207impl ToValue<R256BE> for i128 {
208 fn to_value(self) -> Value<R256BE> {
209 let mut bytes = [0; 32];
210 bytes[0..16].copy_from_slice(&self.to_be_bytes());
211 bytes[16..32].copy_from_slice(&1i128.to_be_bytes());
212
213 Value::new(bytes)
214 }
215}
216
217impl TryFromValue<'_, R256LE> for Ratio<i128> {
218 type Error = RatioError;
219
220 fn try_from_value(v: &Value<R256LE>) -> Result<Self, Self::Error> {
221 let n = i128::from_le_bytes(v.raw[0..16].try_into().unwrap());
222 let d = i128::from_le_bytes(v.raw[16..32].try_into().unwrap());
223
224 if d == 0 {
225 return Err(RatioError::ZeroDenominator);
226 }
227
228 let ratio = Ratio::new_raw(n, d);
229 let ratio = ratio.reduced();
230 let (reduced_n, reduced_d) = ratio.into_raw();
231
232 if reduced_n != n || reduced_d != d {
233 Err(RatioError::NonCanonical(n, d))
234 } else {
235 Ok(ratio)
236 }
237 }
238}
239
240
241impl ToValue<R256LE> for Ratio<i128> {
242 fn to_value(self) -> Value<R256LE> {
243 let mut bytes = [0; 32];
244 bytes[0..16].copy_from_slice(&self.numer().to_le_bytes());
245 bytes[16..32].copy_from_slice(&self.denom().to_le_bytes());
246
247 Value::new(bytes)
248 }
249}
250
251impl ToValue<R256LE> for i128 {
252 fn to_value(self) -> Value<R256LE> {
253 let mut bytes = [0; 32];
254 bytes[0..16].copy_from_slice(&self.to_le_bytes());
255 bytes[16..32].copy_from_slice(&1i128.to_le_bytes());
256
257 Value::new(bytes)
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264 use crate::value::{ToValue, TryFromValue};
265 use num_rational::Ratio;
266 use proptest::prelude::*;
267
268 fn arb_ratio() -> impl Strategy<Value = Ratio<i128>> {
269 (any::<i128>(), any::<i128>().prop_filter("non-zero", |d| *d != 0))
270 .prop_map(|(n, d)| Ratio::new(n, d))
271 }
272
273 proptest! {
274 #[test]
277 fn r256be_ratio_roundtrip(input in arb_ratio()) {
278 let value: Value<R256BE> = input.to_value();
279 let output = Ratio::<i128>::try_from_value(&value).expect("valid ratio");
280 prop_assert_eq!(input, output);
281 }
282
283 #[test]
284 fn r256be_canonicalization(n: i128, d in any::<i128>().prop_filter("non-zero", |d| *d != 0)) {
285 let ratio = Ratio::new(n, d);
286 let value: Value<R256BE> = ratio.to_value();
287 let output = Ratio::<i128>::try_from_value(&value).expect("valid ratio");
288 prop_assert_eq!(output, output.reduced());
290 }
291
292 #[test]
293 fn r256be_i128_roundtrip(input: i128) {
294 let value: Value<R256BE> = input.to_value();
295 let output = Ratio::<i128>::try_from_value(&value).expect("valid ratio");
296 prop_assert_eq!(*output.numer(), input);
297 prop_assert_eq!(*output.denom(), 1i128);
298 }
299
300 #[test]
301 fn r256be_validates(input in arb_ratio()) {
302 let value: Value<R256BE> = input.to_value();
303 prop_assert!(R256BE::validate(value).is_ok());
304 }
305
306 #[test]
309 fn r256le_ratio_roundtrip(input in arb_ratio()) {
310 let value: Value<R256LE> = input.to_value();
311 let output = Ratio::<i128>::try_from_value(&value).expect("valid ratio");
312 prop_assert_eq!(input, output);
313 }
314
315 #[test]
316 fn r256le_canonicalization(n: i128, d in any::<i128>().prop_filter("non-zero", |d| *d != 0)) {
317 let ratio = Ratio::new(n, d);
318 let value: Value<R256LE> = ratio.to_value();
319 let output = Ratio::<i128>::try_from_value(&value).expect("valid ratio");
320 prop_assert_eq!(output, output.reduced());
321 }
322
323 #[test]
324 fn r256le_i128_roundtrip(input: i128) {
325 let value: Value<R256LE> = input.to_value();
326 let output = Ratio::<i128>::try_from_value(&value).expect("valid ratio");
327 prop_assert_eq!(*output.numer(), input);
328 prop_assert_eq!(*output.denom(), 1i128);
329 }
330
331 #[test]
332 fn r256le_validates(input in arb_ratio()) {
333 let value: Value<R256LE> = input.to_value();
334 prop_assert!(R256LE::validate(value).is_ok());
335 }
336
337 #[test]
338 fn r256_le_and_be_differ(input in arb_ratio().prop_filter("non-trivial", |r| *r.numer() != 0)) {
339 let le_val: Value<R256LE> = input.to_value();
340 let be_val: Value<R256BE> = input.to_value();
341 prop_assert_ne!(le_val.raw, be_val.raw);
342 }
343 }
344
345 #[test]
348 fn r256be_non_canonical_error() {
349 let mut bytes = [0u8; 32];
350 bytes[0..16].copy_from_slice(&2i128.to_be_bytes());
351 bytes[16..32].copy_from_slice(&4i128.to_be_bytes());
352 let value = Value::<R256BE>::new(bytes);
353 assert!(Ratio::<i128>::try_from_value(&value).is_err());
354 }
355
356 #[test]
357 fn r256be_zero_denominator_error() {
358 let mut bytes = [0u8; 32];
359 bytes[0..16].copy_from_slice(&1i128.to_be_bytes());
360 bytes[16..32].copy_from_slice(&0i128.to_be_bytes());
361 let value = Value::<R256BE>::new(bytes);
362 assert!(matches!(
363 Ratio::<i128>::try_from_value(&value),
364 Err(RatioError::ZeroDenominator)
365 ));
366 }
367
368 #[test]
369 fn r256le_non_canonical_error() {
370 let mut bytes = [0u8; 32];
371 bytes[0..16].copy_from_slice(&6i128.to_le_bytes());
372 bytes[16..32].copy_from_slice(&4i128.to_le_bytes());
373 let value = Value::<R256LE>::new(bytes);
374 assert!(Ratio::<i128>::try_from_value(&value).is_err());
375 }
376
377 #[test]
378 fn r256le_zero_denominator_error() {
379 let mut bytes = [0u8; 32];
380 bytes[0..16].copy_from_slice(&1i128.to_le_bytes());
381 bytes[16..32].copy_from_slice(&0i128.to_le_bytes());
382 let value = Value::<R256LE>::new(bytes);
383 assert!(matches!(
384 Ratio::<i128>::try_from_value(&value),
385 Err(RatioError::ZeroDenominator)
386 ));
387 }
388}