triblespace_core/value/schemas/
r256.rs1use 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
24pub struct R256LE;
33
34pub 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#[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}