You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

158 lines
4.0 KiB

use bit_vec::BitVec;
use std::fmt::Display;
use std::num::NonZeroU8;
use std::ops::RangeInclusive;
mod digit_decomposition;
/// Maximum supported BTC price in whole USD.
pub const MAX_PRICE_DEC: u64 = (BASE as u64).pow(MAX_DIGITS as u32) - 1;
/// Maximum number of binary digits for BTC price in whole USD.
const MAX_DIGITS: usize = 20;
const BASE: usize = 2;
/// Binary representation of a price interval.
#[derive(Clone, Debug, PartialEq)]
pub struct Digits(BitVec);
impl Digits {
pub fn new(range: RangeInclusive<u64>) -> Result<Vec<Self>, Error> {
let (start, end) = range.into_inner();
if start > MAX_PRICE_DEC || end > MAX_PRICE_DEC {
return Err(Error::RangeOverMax);
}
if start > end {
return Err(Error::DecreasingRange);
}
let digits = digit_decomposition::group_by_ignoring_digits(
start as usize,
end as usize,
BASE,
MAX_DIGITS,
)
.iter()
.map(|digits| {
let digits = digits.iter().map(|n| *n != 0).collect::<BitVec>();
Digits(digits)
})
.collect();
Ok(digits)
}
/// Calculate the range of prices expressed by these digits.
///
/// With the resulting range one can assess wether a particular
/// price corresponds to the described interval.
pub fn range(&self) -> RangeInclusive<u64> {
let missing_bits = MAX_DIGITS - self.0.len();
let mut bits = self.0.clone();
bits.append(&mut BitVec::from_elem(missing_bits, false));
let start = bits.as_u64();
let mut bits = self.0.clone();
bits.append(&mut BitVec::from_elem(missing_bits, true));
let end = bits.as_u64();
start..=end
}
/// Map each bit to its index in the set {0, 1}, starting at 1.
pub fn to_indices(&self) -> Vec<NonZeroU8> {
self.0
.iter()
.map(|bit| NonZeroU8::new(if bit { 2u8 } else { 1u8 }).expect("1 and 2 are non-zero"))
.collect()
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Interval would generate values over maximum price of {MAX_PRICE_DEC}.")]
RangeOverMax,
#[error("Invalid decreasing interval.")]
DecreasingRange,
}
impl Display for Digits {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0
.iter()
.try_for_each(|digit| write!(f, "{}", digit as u8))?;
Ok(())
}
}
trait BitVecExt {
fn as_u64(&self) -> u64;
}
impl BitVecExt for BitVec {
fn as_u64(&self) -> u64 {
let len = self.len();
self.iter().enumerate().fold(0, |acc, (i, x)| {
acc + ((x as u64) * (BASE.pow((len - i - 1) as u32) as u64))
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use anyhow::Result;
use proptest::prelude::*;
#[derive(Debug, Clone)]
struct Interval(RangeInclusive<u64>);
impl Interval {
fn new(range: RangeInclusive<u64>) -> Self {
Self(range)
}
fn to_digits(&self) -> Result<Vec<Digits>> {
let digits = Digits::new(self.0.clone())?;
Ok(digits)
}
}
impl PartialEq<Vec<Digits>> for Interval {
fn eq(&self, other: &Vec<Digits>) -> bool {
let sub_intervals = other.iter().flat_map(|i| i.range());
sub_intervals.eq(self.0.clone())
}
}
prop_compose! {
fn interval()(x in 0u64..=MAX_PRICE_DEC, y in 0u64..=MAX_PRICE_DEC) -> Interval {
let (start, end) = if x < y { (x, y) } else { (y, x) };
Interval::new(start..=end)
}
}
proptest! {
#[test]
fn interval_equal_to_sum_of_sub_intervals_described_by_digits(interval in interval()) {
prop_assert!(interval == interval.to_digits().unwrap())
}
}
}