use core::marker::PhantomData;
use score_set::*;
struct ConstMetric<T: Float, I> {
name: &'static str,
value: T,
_phantom: PhantomData<I>,
}
impl<T: Float, I> ConstMetric<T, I> {
fn new(name: &'static str, value: T) -> Self {
Self {
name,
value,
_phantom: PhantomData,
}
}
fn eval(&self, _input: &I) -> Witnessed<T, Value01> {
Value01::witness(self.value.min(T::one()).max(T::zero())).unwrap()
}
fn name(&self) -> &str {
self.name
}
}
impl<T: Float, I> Scorable<T, I> for ConstMetric<T, I> {
fn eval(&self, input: &I) -> Witnessed<T, Value01> {
ConstMetric::eval(self, input)
}
fn name(&self) -> &str {
ConstMetric::name(self)
}
}
struct Restaurant {
cleanliness: f64, food_quality: f64, price: f64, wait_minutes: f64, }
fn measure_cleanliness(r: &Restaurant) -> f64 {
r.cleanliness
}
fn map01_linear_100(raw: &f64, _: &Restaurant) -> Witnessed<f64, Value01> {
Value01::witness((raw / 100.0).min(1.0).max(0.0)).unwrap()
}
struct Cleanliness(
Metric<
f64,
Restaurant,
f64,
fn(&Restaurant) -> f64,
fn(&f64, &Restaurant) -> Witnessed<f64, Value01>,
>,
);
impl Cleanliness {
fn new() -> Self {
Self(
metric("cleanliness")
.measure()
.by(measure_cleanliness as fn(&Restaurant) -> f64)
.map01()
.by(map01_linear_100),
)
}
fn eval(&self, r: &Restaurant) -> Witnessed<f64, Value01> {
self.0.eval(r)
}
fn name(&self) -> &str {
self.0.name()
}
}
fn measure_quality(r: &Restaurant) -> f64 {
r.food_quality
}
fn map01_linear_5(raw: &f64, _: &Restaurant) -> Witnessed<f64, Value01> {
Value01::witness((raw / 5.0).min(1.0).max(0.0)).unwrap()
}
struct FoodQuality(
Metric<
f64,
Restaurant,
f64,
fn(&Restaurant) -> f64,
fn(&f64, &Restaurant) -> Witnessed<f64, Value01>,
>,
);
impl FoodQuality {
fn new() -> Self {
Self(
metric("food_quality")
.measure()
.by(measure_quality as fn(&Restaurant) -> f64)
.map01()
.by(map01_linear_5),
)
}
fn eval(&self, r: &Restaurant) -> Witnessed<f64, Value01> {
self.0.eval(r)
}
fn name(&self) -> &str {
self.0.name()
}
}
fn measure_price(r: &Restaurant) -> f64 {
r.price
}
fn map01_invert_50(raw: &f64, _: &Restaurant) -> Witnessed<f64, Value01> {
Value01::witness((1.0 - raw / 50.0).min(1.0).max(0.0)).unwrap()
}
struct PriceScore(
Metric<
f64,
Restaurant,
f64,
fn(&Restaurant) -> f64,
fn(&f64, &Restaurant) -> Witnessed<f64, Value01>,
>,
);
impl PriceScore {
fn new() -> Self {
Self(
metric("price")
.measure()
.by(measure_price as fn(&Restaurant) -> f64)
.map01()
.by(map01_invert_50),
)
}
fn eval(&self, r: &Restaurant) -> Witnessed<f64, Value01> {
self.0.eval(r)
}
fn name(&self) -> &str {
self.0.name()
}
}
finite_metric! {
metric => RestaurantMetric,
float => f64,
subject => Restaurant,
dimensions =>
Clean(Cleanliness),
Quality(FoodQuality),
Price(PriceScore),
}
#[test]
fn restaurant_scoring_layer2() -> Result<(), &'static str> {
let set = FiniteScoreSet::normalize(vec![
(3.0, RestaurantMetric::Clean(Cleanliness::new())),
(5.0, RestaurantMetric::Quality(FoodQuality::new())),
(2.0, RestaurantMetric::Price(PriceScore::new())),
])?;
let r = Restaurant {
cleanliness: 85.0,
food_quality: 4.2,
price: 18.0,
wait_minutes: 12.0,
};
let total = set.sum(&r);
let expected = 0.3 * 0.85 + 0.5 * 0.84 + 0.2 * 0.64;
assert!((total - expected).abs() < 1e-10);
Ok(())
}
#[test]
fn restaurant_scoring_layer2_inspect() -> Result<(), &'static str> {
let set = FiniteScoreSet::normalize(vec![
(1.0, RestaurantMetric::Clean(Cleanliness::new())),
(1.0, RestaurantMetric::Quality(FoodQuality::new())),
])?;
let r = Restaurant {
cleanliness: 90.0,
food_quality: 3.0,
price: 25.0,
wait_minutes: 5.0,
};
let mut results: Vec<(&str, f64, f64)> = vec![];
for m in set.iter() {
let name = m.metric().name();
let score = *m.metric().eval(&r);
let weight = m.weight.into_inner();
results.push((name, weight, score));
}
assert_eq!(results[0].0, "cleanliness");
assert_eq!(results[1].0, "food_quality");
assert!((results[0].1 - 0.5).abs() < 1e-10); assert!((results[1].1 - 0.5).abs() < 1e-10);
Ok(())
}
#[test]
fn restaurant_scoring_layer2_custom_escape_hatch() -> Result<(), &'static str> {
finite_metric! {
metric => DemoMetricWithCustom<T, I>,
dimensions =>
AlwaysZero(ConstMetric<T, I>),
AlwaysOne(ConstMetric<T, I>),
Custom(Box<dyn Scorable<T, I>>),
}
let wait_metric: Box<dyn Scorable<f64, Restaurant>> = metric("wait_time")
.measure()
.by(|r: &Restaurant| r.wait_minutes)
.map01()
.linear(30.0)
.boxed();
use DemoMetricWithCustom as D;
let set = FiniteScoreSet::normalize(vec![
(
1.0,
D::<f64, Restaurant>::AlwaysZero(ConstMetric::new("zero", 0.0)),
),
(
1.0,
D::<f64, Restaurant>::AlwaysOne(ConstMetric::new("one", 1.0)),
),
(1.0, D::<f64, Restaurant>::Custom(wait_metric)),
])?;
let r = Restaurant {
cleanliness: 100.0,
food_quality: 5.0,
price: 0.0,
wait_minutes: 15.0,
};
let total = set.sum(&r);
assert!((total - 0.5).abs() < 1e-10);
Ok(())
}
#[test]
fn restaurant_scoring_layer3() -> Result<(), &'static str> {
let set = DynamicScoreSet::normalize(vec![
(
3.0,
metric("cleanliness")
.measure()
.by(|r: &Restaurant| r.cleanliness)
.map01()
.linear(100.0)
.boxed(),
),
(
5.0,
metric("food_quality")
.measure()
.by(|r: &Restaurant| r.food_quality)
.map01()
.linear(5.0)
.boxed(),
),
(
2.0,
metric("price")
.measure()
.by(|r: &Restaurant| r.price)
.map01()
.by(|raw: &f64, _: &Restaurant| {
Value01::witness((1.0 - raw / 50.0).min(1.0).max(0.0)).unwrap()
})
.boxed(),
),
])?;
let r = Restaurant {
cleanliness: 85.0,
food_quality: 4.2,
price: 18.0,
wait_minutes: 12.0,
};
let total = set.sum(&r);
let expected = 0.3 * 0.85 + 0.5 * 0.84 + 0.2 * 0.64;
assert!((total - expected).abs() < 1e-10);
Ok(())
}
finite_metric! {
pub
metric => GenericKind<T, I>,
dimensions =>
Yes(ConstMetric<T, I>),
No(ConstMetric<T, I>),
Custom(Box<dyn Scorable<T, I>>),
}
#[test]
fn generic_form_works_across_types() -> Result<(), &'static str> {
let yes_f64: GenericKind<f64, &str> = GenericKind::Yes(ConstMetric::new("yes", 1.0_f64));
assert_eq!(yes_f64.name(), "yes");
assert!((*yes_f64.eval(&"ignored") - 1.0).abs() < 1e-10);
let no_f32: GenericKind<f32, i32> = GenericKind::No(ConstMetric::new("no", 0.0_f32));
assert_eq!(no_f32.name(), "no");
assert!((*no_f32.eval(&42) - 0.0).abs() < 1e-6);
Ok(())
}