use kitsune_p2p_types::KOpHash;
use std::hash::Hash;
const GRAN: usize = 4096;
#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)]
#[cfg_attr(
feature = "fuzzing",
derive(arbitrary::Arbitrary, proptest_derive::Arbitrary)
)]
pub struct RoughInt(i16);
impl std::fmt::Debug for RoughInt {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.get())
}
}
impl RoughInt {
pub const MAX: usize = i16::MAX as usize * GRAN;
pub fn get(&self) -> usize {
if self.0 > 0 {
self.0 as usize
} else {
(-self.0) as usize * GRAN
}
}
pub fn set(&mut self, v: usize) -> Self {
if v <= i16::MAX as usize {
self.0 = v as i16
} else {
self.0 = -(std::cmp::min(i16::MAX as usize, v / GRAN) as i16);
}
*self
}
}
impl From<usize> for RoughInt {
fn from(v: usize) -> Self {
Self::default().set(v)
}
}
pub type OpHashSized = RoughSized<KOpHash>;
#[derive(
Clone,
Debug,
serde::Deserialize,
serde::Serialize,
derive_more::From,
derive_more::Into,
derive_more::Constructor,
derive_more::Deref,
)]
#[cfg_attr(feature = "fuzzing", derive(proptest_derive::Arbitrary))]
pub struct RoughSized<T> {
#[deref]
data: T,
size: Option<RoughInt>,
}
impl<T> RoughSized<T> {
pub fn into_inner(self) -> (T, Option<RoughInt>) {
(self.data, self.size)
}
pub fn data_ref(&self) -> &T {
&self.data
}
pub fn maybe_size(&self) -> Option<RoughInt> {
self.size
}
pub fn size(&self) -> RoughInt {
self.size.unwrap_or_default()
}
}
impl<T: Clone> RoughSized<T> {
pub fn data(&self) -> T {
self.data.clone()
}
}
impl<T: Hash> Hash for RoughSized<T> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.data.hash(state);
}
}
impl<T: PartialEq> PartialEq for RoughSized<T> {
fn eq(&self, other: &Self) -> bool {
self.data == other.data
}
}
impl<T: Eq> Eq for RoughSized<T> {}
#[cfg(feature = "fuzzing")]
impl<'a, T: arbitrary::Arbitrary<'a>> arbitrary::Arbitrary<'a> for RoughSized<T> {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
Ok(Self {
data: arbitrary::Arbitrary::arbitrary(u)?,
size: arbitrary::Arbitrary::arbitrary(u)?,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
#[test]
fn error_upper_bound() {
let m16 = i16::MAX as usize;
for m in [
m16,
m16 + 2,
m16 + GRAN - 1,
m16 + GRAN * 10 - 1,
RoughInt::MAX - 1,
RoughInt::MAX,
] {
let r = RoughInt::from(m).get();
let error = r.abs_diff(m) as f64 / m as f64;
dbg!(r, m, error);
assert!(error < 0.13);
}
}
proptest! {
#[test]
fn roughint_roundtrip(v: usize) {
let r = RoughInt::from(v);
let v = r.get();
assert_eq!(r, RoughInt::from(v));
}
#[test]
fn roughint_always_underestimates(actual: usize) {
let rough = RoughInt::from(actual);
assert!(rough.get() <= actual);
}
#[test]
fn roughint_sum_error_problematic_range(
real in proptest::collection::vec(i16::MAX as usize..i16::MAX as usize + GRAN, 1..10)
) {
let rough = real.iter().copied().map(RoughInt::from).map(|r| r.get());
let both: Vec<(usize, usize)> = real.iter().copied().zip(rough).collect();
let real_sum: usize = both.iter().map(|(r, _)| r).sum();
let rough_sum: usize = both.iter().map(|(_, r)| r).sum();
if real_sum == 0 || rough_sum == 0 {
unreachable!("zero sum");
}
let error = (rough_sum.abs_diff(real_sum)) as f64 / real_sum as f64;
dbg!(error);
assert!(error <= 0.13);
}
fn roughint_sum_error_full_range(
real in proptest::collection::vec(1..RoughInt::MAX, 1..10)
) {
let rough = real.iter().copied().map(RoughInt::from).map(|r| r.get());
let both: Vec<(usize, usize)> = real.iter().copied().zip(rough).collect();
let real_sum: usize = both.iter().map(|(r, _)| r).sum();
let rough_sum: usize = both.iter().map(|(_, r)| r).sum();
if real_sum == 0 || rough_sum == 0 {
unreachable!("zero sum");
}
let error = (rough_sum.abs_diff(real_sum)) as f64 / real_sum as f64;
dbg!(error);
assert!(error <= 0.13);
}
}
}