use super::*;
use codec::{Decode, Encode, EncodeLike, MaxEncodedLen};
use frame_support::{
traits::{schedule::v3::Anon, Bounded},
Parameter,
};
use scale_info::TypeInfo;
use sp_arithmetic::{Rounding::*, SignedRounding::*};
use sp_runtime::{FixedI64, PerThing, RuntimeDebug};
use sp_std::fmt::Debug;
pub type BalanceOf<T, I = ()> =
<<T as Config<I>>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
pub type NegativeImbalanceOf<T, I> = <<T as Config<I>>::Currency as Currency<
<T as frame_system::Config>::AccountId,
>>::NegativeImbalance;
pub type CallOf<T, I> = <T as Config<I>>::RuntimeCall;
pub type BoundedCallOf<T, I> = Bounded<<T as Config<I>>::RuntimeCall>;
pub type VotesOf<T, I> = <T as Config<I>>::Votes;
pub type TallyOf<T, I> = <T as Config<I>>::Tally;
pub type PalletsOriginOf<T> =
<<T as frame_system::Config>::RuntimeOrigin as OriginTrait>::PalletsOrigin;
pub type ReferendumInfoOf<T, I> = ReferendumInfo<
TrackIdOf<T, I>,
PalletsOriginOf<T>,
BlockNumberFor<T>,
BoundedCallOf<T, I>,
BalanceOf<T, I>,
TallyOf<T, I>,
<T as frame_system::Config>::AccountId,
ScheduleAddressOf<T, I>,
>;
pub type ReferendumStatusOf<T, I> = ReferendumStatus<
TrackIdOf<T, I>,
PalletsOriginOf<T>,
BlockNumberFor<T>,
BoundedCallOf<T, I>,
BalanceOf<T, I>,
TallyOf<T, I>,
<T as frame_system::Config>::AccountId,
ScheduleAddressOf<T, I>,
>;
pub type DecidingStatusOf<T> = DecidingStatus<BlockNumberFor<T>>;
pub type TrackInfoOf<T, I = ()> = TrackInfo<BalanceOf<T, I>, BlockNumberFor<T>>;
pub type TrackIdOf<T, I> =
<<T as Config<I>>::Tracks as TracksInfo<BalanceOf<T, I>, BlockNumberFor<T>>>::Id;
pub type ScheduleAddressOf<T, I> = <<T as Config<I>>::Scheduler as Anon<
BlockNumberFor<T>,
CallOf<T, I>,
PalletsOriginOf<T>,
>>::Address;
pub type ReferendumIndex = u32;
pub trait InsertSorted<T> {
fn insert_sorted_by_key<F: FnMut(&T) -> K, K: PartialOrd<K> + Ord>(
&mut self,
t: T,
f: F,
) -> bool;
}
impl<T: Ord, S: Get<u32>> InsertSorted<T> for BoundedVec<T, S> {
fn insert_sorted_by_key<F: FnMut(&T) -> K, K: PartialOrd<K> + Ord>(
&mut self,
t: T,
mut f: F,
) -> bool {
let index = self.binary_search_by_key::<K, F>(&f(&t), f).unwrap_or_else(|x| x);
self.force_insert_keep_right(index, t).is_ok()
}
}
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub struct DecidingStatus<BlockNumber> {
pub since: BlockNumber,
pub confirming: Option<BlockNumber>,
}
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub struct Deposit<AccountId, Balance> {
pub who: AccountId,
pub amount: Balance,
}
#[derive(Clone, Encode, TypeInfo)]
pub struct TrackInfo<Balance, Moment> {
pub name: &'static str,
pub max_deciding: u32,
pub decision_deposit: Balance,
pub prepare_period: Moment,
pub decision_period: Moment,
pub confirm_period: Moment,
pub min_enactment_period: Moment,
pub min_approval: Curve,
pub min_support: Curve,
}
pub trait TracksInfo<Balance, Moment> {
type Id: Copy + Parameter + Ord + PartialOrd + Send + Sync + 'static + MaxEncodedLen;
type RuntimeOrigin;
fn tracks() -> &'static [(Self::Id, TrackInfo<Balance, Moment>)];
fn track_for(origin: &Self::RuntimeOrigin) -> Result<Self::Id, ()>;
fn info(id: Self::Id) -> Option<&'static TrackInfo<Balance, Moment>> {
Self::tracks().iter().find(|x| x.0 == id).map(|x| &x.1)
}
}
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub struct ReferendumStatus<
TrackId: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
RuntimeOrigin: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
Moment: Parameter + Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone + EncodeLike,
Call: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
Balance: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
Tally: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
AccountId: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
ScheduleAddress: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
> {
pub track: TrackId,
pub origin: RuntimeOrigin,
pub proposal: Call,
pub enactment: DispatchTime<Moment>,
pub submitted: Moment,
pub submission_deposit: Deposit<AccountId, Balance>,
pub decision_deposit: Option<Deposit<AccountId, Balance>>,
pub deciding: Option<DecidingStatus<Moment>>,
pub tally: Tally,
pub in_queue: bool,
pub alarm: Option<(Moment, ScheduleAddress)>,
}
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub enum ReferendumInfo<
TrackId: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
RuntimeOrigin: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
Moment: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone + EncodeLike,
Call: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
Balance: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
Tally: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
AccountId: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
ScheduleAddress: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
> {
Ongoing(
ReferendumStatus<
TrackId,
RuntimeOrigin,
Moment,
Call,
Balance,
Tally,
AccountId,
ScheduleAddress,
>,
),
Approved(Moment, Option<Deposit<AccountId, Balance>>, Option<Deposit<AccountId, Balance>>),
Rejected(Moment, Option<Deposit<AccountId, Balance>>, Option<Deposit<AccountId, Balance>>),
Cancelled(Moment, Option<Deposit<AccountId, Balance>>, Option<Deposit<AccountId, Balance>>),
TimedOut(Moment, Option<Deposit<AccountId, Balance>>, Option<Deposit<AccountId, Balance>>),
Killed(Moment),
}
impl<
TrackId: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
RuntimeOrigin: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
Moment: Parameter + Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone + EncodeLike,
Call: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
Balance: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
Tally: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
AccountId: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
ScheduleAddress: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
> ReferendumInfo<TrackId, RuntimeOrigin, Moment, Call, Balance, Tally, AccountId, ScheduleAddress>
{
pub fn take_decision_deposit(&mut self) -> Result<Option<Deposit<AccountId, Balance>>, ()> {
use ReferendumInfo::*;
match self {
Ongoing(x) if x.decision_deposit.is_none() => Ok(None),
Ongoing(_) => Err(()),
Approved(_, _, d) | Rejected(_, _, d) | TimedOut(_, _, d) | Cancelled(_, _, d) =>
Ok(d.take()),
Killed(_) => Ok(None),
}
}
pub fn take_submission_deposit(&mut self) -> Result<Option<Deposit<AccountId, Balance>>, ()> {
use ReferendumInfo::*;
match self {
Approved(_, s, _) | Cancelled(_, s, _) => Ok(s.take()),
Ongoing(..) | Rejected(..) | TimedOut(..) | Killed(..) => Err(()),
}
}
}
#[derive(Clone, Eq, PartialEq, Encode, Decode, TypeInfo, MaxEncodedLen)]
#[cfg_attr(not(feature = "std"), derive(RuntimeDebug))]
pub enum Curve {
LinearDecreasing { length: Perbill, floor: Perbill, ceil: Perbill },
SteppedDecreasing { begin: Perbill, end: Perbill, step: Perbill, period: Perbill },
Reciprocal { factor: FixedI64, x_offset: FixedI64, y_offset: FixedI64 },
}
const fn pos_quad_solution(a: FixedI64, b: FixedI64, c: FixedI64) -> FixedI64 {
const TWO: FixedI64 = FixedI64::from_u32(2);
const FOUR: FixedI64 = FixedI64::from_u32(4);
b.neg().add(b.mul(b).sub(FOUR.mul(a).mul(c)).sqrt()).div(TWO.mul(a))
}
impl Curve {
pub const fn make_linear(length: u128, period: u128, floor: FixedI64, ceil: FixedI64) -> Curve {
let length = FixedI64::from_rational(length, period).into_perbill();
let floor = floor.into_perbill();
let ceil = ceil.into_perbill();
Curve::LinearDecreasing { length, floor, ceil }
}
pub const fn make_reciprocal(
delay: u128,
period: u128,
level: FixedI64,
floor: FixedI64,
ceil: FixedI64,
) -> Curve {
let delay = FixedI64::from_rational(delay, period).into_perbill();
let mut bounds = (
(
FixedI64::from_u32(0),
Self::reciprocal_from_parts(FixedI64::from_u32(0), floor, ceil),
FixedI64::from_inner(i64::max_value()),
),
(
FixedI64::from_u32(1),
Self::reciprocal_from_parts(FixedI64::from_u32(1), floor, ceil),
FixedI64::from_inner(i64::max_value()),
),
);
const TWO: FixedI64 = FixedI64::from_u32(2);
while (bounds.1).0.sub((bounds.0).0).into_inner() > 1 {
let factor = (bounds.0).0.add((bounds.1).0).div(TWO);
let curve = Self::reciprocal_from_parts(factor, floor, ceil);
let curve_level = FixedI64::from_perbill(curve.const_threshold(delay));
if curve_level.into_inner() > level.into_inner() {
bounds = (bounds.0, (factor, curve, curve_level.sub(level)));
} else {
bounds = ((factor, curve, level.sub(curve_level)), bounds.1);
}
}
if (bounds.0).2.into_inner() < (bounds.1).2.into_inner() {
(bounds.0).1
} else {
(bounds.1).1
}
}
const fn reciprocal_from_parts(factor: FixedI64, floor: FixedI64, ceil: FixedI64) -> Self {
let delta = ceil.sub(floor);
let x_offset = pos_quad_solution(delta, delta, factor.neg());
let y_offset = floor.sub(factor.div(FixedI64::from_u32(1).add(x_offset)));
Curve::Reciprocal { factor, x_offset, y_offset }
}
#[cfg(feature = "std")]
pub fn info(&self, days: u32, name: impl std::fmt::Display) {
let hours = days * 24;
println!("Curve {} := {:?}:", name, self);
println!(" t + 0h: {:?}", self.threshold(Perbill::zero()));
println!(" t + 1h: {:?}", self.threshold(Perbill::from_rational(1, hours)));
println!(" t + 2h: {:?}", self.threshold(Perbill::from_rational(2, hours)));
println!(" t + 3h: {:?}", self.threshold(Perbill::from_rational(3, hours)));
println!(" t + 6h: {:?}", self.threshold(Perbill::from_rational(6, hours)));
println!(" t + 12h: {:?}", self.threshold(Perbill::from_rational(12, hours)));
println!(" t + 24h: {:?}", self.threshold(Perbill::from_rational(24, hours)));
let mut l = 0;
for &(n, d) in [(1, 12), (1, 8), (1, 4), (1, 2), (3, 4), (1, 1)].iter() {
let t = days * n / d;
if t != l {
println!(" t + {}d: {:?}", t, self.threshold(Perbill::from_rational(t, days)));
l = t;
}
}
let t = |p: Perbill| -> std::string::String {
if p.is_one() {
"never".into()
} else {
let minutes = p * (hours * 60);
if minutes < 60 {
format!("{} minutes", minutes)
} else if minutes < 8 * 60 && minutes % 60 != 0 {
format!("{} hours {} minutes", minutes / 60, minutes % 60)
} else if minutes < 72 * 60 {
format!("{} hours", minutes / 60)
} else if minutes / 60 % 24 == 0 {
format!("{} days", minutes / 60 / 24)
} else {
format!("{} days {} hours", minutes / 60 / 24, minutes / 60 % 24)
}
}
};
if self.delay(Perbill::from_percent(49)) < Perbill::one() {
println!(" 30% threshold: {}", t(self.delay(Perbill::from_percent(30))));
println!(" 10% threshold: {}", t(self.delay(Perbill::from_percent(10))));
println!(" 3% threshold: {}", t(self.delay(Perbill::from_percent(3))));
println!(" 1% threshold: {}", t(self.delay(Perbill::from_percent(1))));
println!(" 0.1% threshold: {}", t(self.delay(Perbill::from_rational(1u32, 1_000))));
println!(" 0.01% threshold: {}", t(self.delay(Perbill::from_rational(1u32, 10_000))));
} else {
println!(
" 99.9% threshold: {}",
t(self.delay(Perbill::from_rational(999u32, 1_000)))
);
println!(" 99% threshold: {}", t(self.delay(Perbill::from_percent(99))));
println!(" 95% threshold: {}", t(self.delay(Perbill::from_percent(95))));
println!(" 90% threshold: {}", t(self.delay(Perbill::from_percent(90))));
println!(" 75% threshold: {}", t(self.delay(Perbill::from_percent(75))));
println!(" 60% threshold: {}", t(self.delay(Perbill::from_percent(60))));
}
}
pub fn threshold(&self, x: Perbill) -> Perbill {
match self {
Self::LinearDecreasing { length, floor, ceil } =>
*ceil - (x.min(*length).saturating_div(*length, Down) * (*ceil - *floor)),
Self::SteppedDecreasing { begin, end, step, period } =>
(*begin - (step.int_mul(x.int_div(*period))).min(*begin)).max(*end),
Self::Reciprocal { factor, x_offset, y_offset } => factor
.checked_rounding_div(FixedI64::from(x) + *x_offset, Low)
.map(|yp| (yp + *y_offset).into_clamped_perthing())
.unwrap_or_else(Perbill::one),
}
}
const fn const_threshold(&self, x: Perbill) -> Perbill {
match self {
Self::Reciprocal { factor, x_offset, y_offset } => {
match factor.checked_rounding_div(FixedI64::from_perbill(x).add(*x_offset), Low) {
Some(yp) => (yp.add(*y_offset)).into_perbill(),
None => Perbill::one(),
}
},
_ => panic!("const_threshold cannot be used on this curve"),
}
}
pub fn delay(&self, y: Perbill) -> Perbill {
match self {
Self::LinearDecreasing { length, floor, ceil } =>
if y < *floor {
Perbill::one()
} else if y > *ceil {
Perbill::zero()
} else {
(*ceil - y).saturating_div(*ceil - *floor, Up).saturating_mul(*length)
},
Self::SteppedDecreasing { begin, end, step, period } =>
if y < *end {
Perbill::one()
} else {
period.int_mul((*begin - y.min(*begin) + step.less_epsilon()).int_div(*step))
},
Self::Reciprocal { factor, x_offset, y_offset } => {
let y = FixedI64::from(y);
let maybe_term = factor.checked_rounding_div(y - *y_offset, High);
maybe_term
.and_then(|term| (term - *x_offset).try_into_perthing().ok())
.unwrap_or_else(Perbill::one)
},
}
}
pub fn passing(&self, x: Perbill, y: Perbill) -> bool {
y >= self.threshold(x)
}
}
#[cfg(feature = "std")]
impl Debug for Curve {
fn fmt(&self, f: &mut sp_std::fmt::Formatter<'_>) -> sp_std::fmt::Result {
match self {
Self::LinearDecreasing { length, floor, ceil } => {
write!(
f,
"Linear[(0%, {:?}) -> ({:?}, {:?}) -> (100%, {:?})]",
ceil, length, floor, floor,
)
},
Self::SteppedDecreasing { begin, end, step, period } => {
write!(
f,
"Stepped[(0%, {:?}) -> (100%, {:?}) by ({:?}, {:?})]",
begin, end, period, step,
)
},
Self::Reciprocal { factor, x_offset, y_offset } => {
write!(
f,
"Reciprocal[factor of {:?}, x_offset of {:?}, y_offset of {:?}]",
factor, x_offset, y_offset,
)
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use frame_support::traits::ConstU32;
use sp_runtime::PerThing;
const fn percent(x: u128) -> FixedI64 {
FixedI64::from_rational(x, 100)
}
const TIP_APP: Curve = Curve::make_linear(10, 28, percent(50), percent(100));
const TIP_SUP: Curve = Curve::make_reciprocal(1, 28, percent(4), percent(0), percent(50));
const ROOT_APP: Curve = Curve::make_reciprocal(4, 28, percent(80), percent(50), percent(100));
const ROOT_SUP: Curve = Curve::make_linear(28, 28, percent(0), percent(50));
const WHITE_APP: Curve =
Curve::make_reciprocal(16, 28 * 24, percent(96), percent(50), percent(100));
const WHITE_SUP: Curve = Curve::make_reciprocal(1, 28, percent(20), percent(10), percent(50));
const SMALL_APP: Curve = Curve::make_linear(10, 28, percent(50), percent(100));
const SMALL_SUP: Curve = Curve::make_reciprocal(8, 28, percent(1), percent(0), percent(50));
const MID_APP: Curve = Curve::make_linear(17, 28, percent(50), percent(100));
const MID_SUP: Curve = Curve::make_reciprocal(12, 28, percent(1), percent(0), percent(50));
const BIG_APP: Curve = Curve::make_linear(23, 28, percent(50), percent(100));
const BIG_SUP: Curve = Curve::make_reciprocal(16, 28, percent(1), percent(0), percent(50));
const HUGE_APP: Curve = Curve::make_linear(28, 28, percent(50), percent(100));
const HUGE_SUP: Curve = Curve::make_reciprocal(20, 28, percent(1), percent(0), percent(50));
const PARAM_APP: Curve = Curve::make_reciprocal(4, 28, percent(80), percent(50), percent(100));
const PARAM_SUP: Curve = Curve::make_reciprocal(7, 28, percent(10), percent(0), percent(50));
const ADMIN_APP: Curve = Curve::make_linear(17, 28, percent(50), percent(100));
const ADMIN_SUP: Curve = Curve::make_reciprocal(12, 28, percent(1), percent(0), percent(50));
#[test]
#[should_panic]
fn check_curves() {
TIP_APP.info(28u32, "Tip Approval");
TIP_SUP.info(28u32, "Tip Support");
ROOT_APP.info(28u32, "Root Approval");
ROOT_SUP.info(28u32, "Root Support");
WHITE_APP.info(28u32, "Whitelist Approval");
WHITE_SUP.info(28u32, "Whitelist Support");
SMALL_APP.info(28u32, "Small Spend Approval");
SMALL_SUP.info(28u32, "Small Spend Support");
MID_APP.info(28u32, "Mid Spend Approval");
MID_SUP.info(28u32, "Mid Spend Support");
BIG_APP.info(28u32, "Big Spend Approval");
BIG_SUP.info(28u32, "Big Spend Support");
HUGE_APP.info(28u32, "Huge Spend Approval");
HUGE_SUP.info(28u32, "Huge Spend Support");
PARAM_APP.info(28u32, "Mid-tier Parameter Change Approval");
PARAM_SUP.info(28u32, "Mid-tier Parameter Change Support");
ADMIN_APP.info(28u32, "Admin (e.g. Cancel Slash) Approval");
ADMIN_SUP.info(28u32, "Admin (e.g. Cancel Slash) Support");
assert!(false);
}
#[test]
fn insert_sorted_works() {
let mut b: BoundedVec<u32, ConstU32<6>> = vec![20, 30, 40].try_into().unwrap();
assert!(b.insert_sorted_by_key(10, |&x| x));
assert_eq!(&b[..], &[10, 20, 30, 40][..]);
assert!(b.insert_sorted_by_key(60, |&x| x));
assert_eq!(&b[..], &[10, 20, 30, 40, 60][..]);
assert!(b.insert_sorted_by_key(50, |&x| x));
assert_eq!(&b[..], &[10, 20, 30, 40, 50, 60][..]);
assert!(!b.insert_sorted_by_key(9, |&x| x));
assert_eq!(&b[..], &[10, 20, 30, 40, 50, 60][..]);
assert!(b.insert_sorted_by_key(11, |&x| x));
assert_eq!(&b[..], &[11, 20, 30, 40, 50, 60][..]);
assert!(b.insert_sorted_by_key(21, |&x| x));
assert_eq!(&b[..], &[20, 21, 30, 40, 50, 60][..]);
assert!(b.insert_sorted_by_key(61, |&x| x));
assert_eq!(&b[..], &[21, 30, 40, 50, 60, 61][..]);
assert!(b.insert_sorted_by_key(51, |&x| x));
assert_eq!(&b[..], &[30, 40, 50, 51, 60, 61][..]);
}
#[test]
fn translated_reciprocal_works() {
let c: Curve = Curve::Reciprocal {
factor: FixedI64::from_float(0.03125),
x_offset: FixedI64::from_float(0.0363306838226),
y_offset: FixedI64::from_float(0.139845532427),
};
c.info(28u32, "Test");
for i in 0..9_696_969u32 {
let query = Perbill::from_rational(i, 9_696_969);
let delay_needed = c.delay(query);
assert!(delay_needed.is_one() || c.passing(delay_needed, query));
}
}
#[test]
fn stepped_decreasing_works() {
fn pc(x: u32) -> Perbill {
Perbill::from_percent(x)
}
let c =
Curve::SteppedDecreasing { begin: pc(80), end: pc(30), step: pc(10), period: pc(15) };
for i in 0..9_696_969u32 {
let query = Perbill::from_rational(i, 9_696_969);
let delay_needed = c.delay(query);
assert!(delay_needed.is_one() || c.passing(delay_needed, query));
}
assert_eq!(c.threshold(pc(0)), pc(80));
assert_eq!(c.threshold(pc(15).less_epsilon()), pc(80));
assert_eq!(c.threshold(pc(15)), pc(70));
assert_eq!(c.threshold(pc(30).less_epsilon()), pc(70));
assert_eq!(c.threshold(pc(30)), pc(60));
assert_eq!(c.threshold(pc(45).less_epsilon()), pc(60));
assert_eq!(c.threshold(pc(45)), pc(50));
assert_eq!(c.threshold(pc(60).less_epsilon()), pc(50));
assert_eq!(c.threshold(pc(60)), pc(40));
assert_eq!(c.threshold(pc(75).less_epsilon()), pc(40));
assert_eq!(c.threshold(pc(75)), pc(30));
assert_eq!(c.threshold(pc(100)), pc(30));
assert_eq!(c.delay(pc(100)), pc(0));
assert_eq!(c.delay(pc(80)), pc(0));
assert_eq!(c.delay(pc(80).less_epsilon()), pc(15));
assert_eq!(c.delay(pc(70)), pc(15));
assert_eq!(c.delay(pc(70).less_epsilon()), pc(30));
assert_eq!(c.delay(pc(60)), pc(30));
assert_eq!(c.delay(pc(60).less_epsilon()), pc(45));
assert_eq!(c.delay(pc(50)), pc(45));
assert_eq!(c.delay(pc(50).less_epsilon()), pc(60));
assert_eq!(c.delay(pc(40)), pc(60));
assert_eq!(c.delay(pc(40).less_epsilon()), pc(75));
assert_eq!(c.delay(pc(30)), pc(75));
assert_eq!(c.delay(pc(30).less_epsilon()), pc(100));
assert_eq!(c.delay(pc(0)), pc(100));
}
}