triblespace_core/inline/encodings/
r256.rs1use crate::inline::Encodes;
2use crate::id::ExclusiveId;
3use crate::id::Id;
4use crate::id_hex;
5use crate::macros::entity;
6use crate::metadata;
7use crate::metadata::MetaDescribe;
8use crate::trible::Fragment;
9use crate::inline::TryFromInline;
10use crate::inline::Inline;
11use crate::inline::InlineEncoding;
12use std::convert::Infallible;
13
14use std::convert::TryInto;
15
16use num_rational::Ratio;
17
18pub struct R256LE;
27
28pub struct R256BE;
37
38pub type R256 = R256LE;
40
41impl MetaDescribe for R256LE {
42 fn describe() -> Fragment {
43 let id: Id = id_hex!("0A9B43C5C2ECD45B257CDEFC16544358");
44 #[allow(unused_mut)]
45 let mut tribles = entity! {
46 ExclusiveId::force_ref(&id) @
47 metadata::name: "r256le",
48 metadata::description: "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.",
49 metadata::tag: metadata::KIND_INLINE_ENCODING,
50 };
51
52 #[cfg(feature = "wasm")]
53 {
54 tribles += entity! { ExclusiveId::force_ref(&id) @
55 metadata::value_formatter: wasm_formatter::R256_LE_WASM,
56 };
57 }
58 tribles
59 }
60}
61impl InlineEncoding for R256LE {
62 type ValidationError = Infallible;
63 type Encoding = Self;
64}
65impl MetaDescribe for R256BE {
66 fn describe() -> Fragment {
67 let id: Id = id_hex!("CA5EAF567171772C1FFD776E9C7C02D1");
68 #[allow(unused_mut)]
69 let mut tribles = entity! {
70 ExclusiveId::force_ref(&id) @
71 metadata::name: "r256be",
72 metadata::description: "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.",
73 metadata::tag: metadata::KIND_INLINE_ENCODING,
74 };
75
76 #[cfg(feature = "wasm")]
77 {
78 tribles += entity! { ExclusiveId::force_ref(&id) @
79 metadata::value_formatter: wasm_formatter::R256_BE_WASM,
80 };
81 }
82 tribles
83 }
84}
85
86#[cfg(feature = "wasm")]
87mod wasm_formatter {
88 use core::fmt::Write;
89
90 use triblespace_core_macros::value_formatter;
91
92 #[value_formatter]
93 pub(crate) fn r256_le(raw: &[u8; 32], out: &mut impl Write) -> Result<(), u32> {
94 let mut buf = [0u8; 16];
95 buf.copy_from_slice(&raw[..16]);
96 let numer = i128::from_le_bytes(buf);
97 buf.copy_from_slice(&raw[16..]);
98 let denom = i128::from_le_bytes(buf);
99
100 if denom == 0 {
101 return Err(2);
102 }
103
104 if denom == 1 {
105 write!(out, "{numer}").map_err(|_| 1u32)?;
106 } else {
107 write!(out, "{numer}/{denom}").map_err(|_| 1u32)?;
108 }
109 Ok(())
110 }
111
112 #[value_formatter]
113 pub(crate) fn r256_be(raw: &[u8; 32], out: &mut impl Write) -> Result<(), u32> {
114 let mut buf = [0u8; 16];
115 buf.copy_from_slice(&raw[..16]);
116 let numer = i128::from_be_bytes(buf);
117 buf.copy_from_slice(&raw[16..]);
118 let denom = i128::from_be_bytes(buf);
119
120 if denom == 0 {
121 return Err(2);
122 }
123
124 if denom == 1 {
125 write!(out, "{numer}").map_err(|_| 1u32)?;
126 } else {
127 write!(out, "{numer}/{denom}").map_err(|_| 1u32)?;
128 }
129 Ok(())
130 }
131}
132impl InlineEncoding for R256BE {
133 type ValidationError = Infallible;
134 type Encoding = Self;
135}
136
137#[derive(Debug)]
142pub enum RatioError {
143 NonCanonical(i128, i128),
145 ZeroDenominator,
147}
148
149impl TryFromInline<'_, R256BE> for Ratio<i128> {
150 type Error = RatioError;
151
152 fn try_from_inline(v: &Inline<R256BE>) -> Result<Self, Self::Error> {
153 let n = i128::from_be_bytes(v.raw[0..16].try_into().unwrap());
154 let d = i128::from_be_bytes(v.raw[16..32].try_into().unwrap());
155
156 if d == 0 {
157 return Err(RatioError::ZeroDenominator);
158 }
159
160 let ratio = Ratio::new_raw(n, d);
161 let ratio = ratio.reduced();
162 let (reduced_n, reduced_d) = ratio.into_raw();
163
164 if reduced_n != n || reduced_d != d {
165 Err(RatioError::NonCanonical(n, d))
166 } else {
167 Ok(ratio)
168 }
169 }
170}
171
172impl Encodes<Ratio<i128>> for R256BE
173{
174 type Output = Inline<R256BE>;
175 fn encode(source: Ratio<i128>) -> Inline<R256BE> {
176 let ratio = source.reduced();
177
178 let mut bytes = [0; 32];
179 bytes[0..16].copy_from_slice(&ratio.numer().to_be_bytes());
180 bytes[16..32].copy_from_slice(&ratio.denom().to_be_bytes());
181
182 Inline::new(bytes)
183 }
184}
185
186impl Encodes<i128> for R256BE
187{
188 type Output = Inline<R256BE>;
189 fn encode(source: i128) -> Inline<R256BE> {
190 let mut bytes = [0; 32];
191 bytes[0..16].copy_from_slice(&source.to_be_bytes());
192 bytes[16..32].copy_from_slice(&1i128.to_be_bytes());
193
194 Inline::new(bytes)
195 }
196}
197
198impl TryFromInline<'_, R256LE> for Ratio<i128> {
199 type Error = RatioError;
200
201 fn try_from_inline(v: &Inline<R256LE>) -> Result<Self, Self::Error> {
202 let n = i128::from_le_bytes(v.raw[0..16].try_into().unwrap());
203 let d = i128::from_le_bytes(v.raw[16..32].try_into().unwrap());
204
205 if d == 0 {
206 return Err(RatioError::ZeroDenominator);
207 }
208
209 let ratio = Ratio::new_raw(n, d);
210 let ratio = ratio.reduced();
211 let (reduced_n, reduced_d) = ratio.into_raw();
212
213 if reduced_n != n || reduced_d != d {
214 Err(RatioError::NonCanonical(n, d))
215 } else {
216 Ok(ratio)
217 }
218 }
219}
220
221impl Encodes<Ratio<i128>> for R256LE
222{
223 type Output = Inline<R256LE>;
224 fn encode(source: Ratio<i128>) -> Inline<R256LE> {
225 let mut bytes = [0; 32];
226 bytes[0..16].copy_from_slice(&source.numer().to_le_bytes());
227 bytes[16..32].copy_from_slice(&source.denom().to_le_bytes());
228
229 Inline::new(bytes)
230 }
231}
232
233impl Encodes<i128> for R256LE
234{
235 type Output = Inline<R256LE>;
236 fn encode(source: i128) -> Inline<R256LE> {
237 let mut bytes = [0; 32];
238 bytes[0..16].copy_from_slice(&source.to_le_bytes());
239 bytes[16..32].copy_from_slice(&1i128.to_le_bytes());
240
241 Inline::new(bytes)
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248 use crate::inline::{IntoInline, TryFromInline};
249 use num_rational::Ratio;
250 use proptest::prelude::*;
251
252 fn arb_ratio() -> impl Strategy<Value = Ratio<i128>> {
253 (
254 any::<i128>(),
255 any::<i128>().prop_filter("non-zero", |d| *d != 0),
256 )
257 .prop_map(|(n, d)| Ratio::new(n, d))
258 }
259
260 proptest! {
261 #[test]
264 fn r256be_ratio_roundtrip(input in arb_ratio()) {
265 let value: Inline<R256BE> = input.to_inline();
266 let output = Ratio::<i128>::try_from_inline(&value).expect("valid ratio");
267 prop_assert_eq!(input, output);
268 }
269
270 #[test]
271 fn r256be_canonicalization(n: i128, d in any::<i128>().prop_filter("non-zero", |d| *d != 0)) {
272 let ratio = Ratio::new(n, d);
273 let value: Inline<R256BE> = ratio.to_inline();
274 let output = Ratio::<i128>::try_from_inline(&value).expect("valid ratio");
275 prop_assert_eq!(output, output.reduced());
277 }
278
279 #[test]
280 fn r256be_i128_roundtrip(input: i128) {
281 let value: Inline<R256BE> = input.to_inline();
282 let output = Ratio::<i128>::try_from_inline(&value).expect("valid ratio");
283 prop_assert_eq!(*output.numer(), input);
284 prop_assert_eq!(*output.denom(), 1i128);
285 }
286
287 #[test]
288 fn r256be_validates(input in arb_ratio()) {
289 let value: Inline<R256BE> = input.to_inline();
290 prop_assert!(R256BE::validate(value).is_ok());
291 }
292
293 #[test]
296 fn r256le_ratio_roundtrip(input in arb_ratio()) {
297 let value: Inline<R256LE> = input.to_inline();
298 let output = Ratio::<i128>::try_from_inline(&value).expect("valid ratio");
299 prop_assert_eq!(input, output);
300 }
301
302 #[test]
303 fn r256le_canonicalization(n: i128, d in any::<i128>().prop_filter("non-zero", |d| *d != 0)) {
304 let ratio = Ratio::new(n, d);
305 let value: Inline<R256LE> = ratio.to_inline();
306 let output = Ratio::<i128>::try_from_inline(&value).expect("valid ratio");
307 prop_assert_eq!(output, output.reduced());
308 }
309
310 #[test]
311 fn r256le_i128_roundtrip(input: i128) {
312 let value: Inline<R256LE> = input.to_inline();
313 let output = Ratio::<i128>::try_from_inline(&value).expect("valid ratio");
314 prop_assert_eq!(*output.numer(), input);
315 prop_assert_eq!(*output.denom(), 1i128);
316 }
317
318 #[test]
319 fn r256le_validates(input in arb_ratio()) {
320 let value: Inline<R256LE> = input.to_inline();
321 prop_assert!(R256LE::validate(value).is_ok());
322 }
323
324 #[test]
325 fn r256_le_and_be_differ(input in arb_ratio().prop_filter("non-trivial", |r| *r.numer() != 0)) {
326 let le_val: Inline<R256LE> = input.to_inline();
327 let be_val: Inline<R256BE> = input.to_inline();
328 prop_assert_ne!(le_val.raw, be_val.raw);
329 }
330 }
331
332 #[test]
335 fn r256be_non_canonical_error() {
336 let mut bytes = [0u8; 32];
337 bytes[0..16].copy_from_slice(&2i128.to_be_bytes());
338 bytes[16..32].copy_from_slice(&4i128.to_be_bytes());
339 let value = Inline::<R256BE>::new(bytes);
340 assert!(Ratio::<i128>::try_from_inline(&value).is_err());
341 }
342
343 #[test]
344 fn r256be_zero_denominator_error() {
345 let mut bytes = [0u8; 32];
346 bytes[0..16].copy_from_slice(&1i128.to_be_bytes());
347 bytes[16..32].copy_from_slice(&0i128.to_be_bytes());
348 let value = Inline::<R256BE>::new(bytes);
349 assert!(matches!(
350 Ratio::<i128>::try_from_inline(&value),
351 Err(RatioError::ZeroDenominator)
352 ));
353 }
354
355 #[test]
356 fn r256le_non_canonical_error() {
357 let mut bytes = [0u8; 32];
358 bytes[0..16].copy_from_slice(&6i128.to_le_bytes());
359 bytes[16..32].copy_from_slice(&4i128.to_le_bytes());
360 let value = Inline::<R256LE>::new(bytes);
361 assert!(Ratio::<i128>::try_from_inline(&value).is_err());
362 }
363
364 #[test]
365 fn r256le_zero_denominator_error() {
366 let mut bytes = [0u8; 32];
367 bytes[0..16].copy_from_slice(&1i128.to_le_bytes());
368 bytes[16..32].copy_from_slice(&0i128.to_le_bytes());
369 let value = Inline::<R256LE>::new(bytes);
370 assert!(matches!(
371 Ratio::<i128>::try_from_inline(&value),
372 Err(RatioError::ZeroDenominator)
373 ));
374 }
375}