Skip to main content

argumentation_weighted/
types.rs

1//! Foundational types for weighted argumentation.
2//!
3//! - [`AttackWeight`] — validated non-negative finite f64 wrapper.
4//! - [`Budget`] — validated non-negative finite f64 wrapper for
5//!   inconsistency-budget values.
6//! - [`WeightedAttack`] — a directed attack edge carrying a weight.
7
8use crate::error::Error;
9
10/// A non-negative finite attack weight. Constructed via [`Self::new`],
11/// which rejects NaN, infinity, and negative values.
12///
13/// Implements `Copy`, `Clone`, `Debug`, `PartialEq`, and `PartialOrd`
14/// but NOT `Eq` or `Hash` — `f64` does not satisfy those by default.
15#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
16pub struct AttackWeight(f64);
17
18impl AttackWeight {
19    /// Construct a weight, rejecting NaN, infinity, and negative values.
20    pub fn new(value: f64) -> Result<Self, Error> {
21        if !value.is_finite() || value < 0.0 {
22            return Err(Error::InvalidWeight { weight: value });
23        }
24        Ok(Self(value))
25    }
26
27    /// The underlying `f64` value. Always non-negative and finite by
28    /// construction.
29    #[must_use]
30    pub fn value(self) -> f64 {
31        self.0
32    }
33}
34
35/// A non-negative finite inconsistency budget. Semantics: attacks whose
36/// cumulative weight is at most this value may be tolerated for the
37/// purposes of Dung semantics.
38#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
39pub struct Budget(f64);
40
41impl Budget {
42    /// Construct a budget, rejecting NaN, infinity, and negative values.
43    pub fn new(value: f64) -> Result<Self, Error> {
44        if !value.is_finite() || value < 0.0 {
45            return Err(Error::InvalidBudget { budget: value });
46        }
47        Ok(Self(value))
48    }
49
50    /// The underlying `f64` value.
51    #[must_use]
52    pub fn value(self) -> f64 {
53        self.0
54    }
55
56    /// A zero budget — equivalent to running standard Dung semantics
57    /// (no attacks are tolerated).
58    #[must_use]
59    pub fn zero() -> Self {
60        Self(0.0)
61    }
62}
63
64/// A weighted directed attack edge: `attacker` attacks `target` with
65/// the given `weight`.
66///
67/// Generic over argument type `A` to match the core crate's convention.
68// Eq is not derived because AttackWeight wraps f64, which violates
69// Eq's reflexivity requirement (NaN ≠ NaN). All constructed weights
70// are finite non-NaN by AttackWeight::new validation, but the trait
71// bound is unavailable.
72#[derive(Debug, Clone, PartialEq)]
73pub struct WeightedAttack<A: Clone + Eq> {
74    /// The attacking argument.
75    pub attacker: A,
76    /// The target argument.
77    pub target: A,
78    /// The attack weight. Higher weights are harder to tolerate.
79    pub weight: AttackWeight,
80}
81
82impl<A: Clone + Eq> WeightedAttack<A> {
83    /// Convenience constructor.
84    pub fn new(attacker: A, target: A, weight: f64) -> Result<Self, Error> {
85        Ok(Self {
86            attacker,
87            target,
88            weight: AttackWeight::new(weight)?,
89        })
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn attack_weight_accepts_valid_values() {
99        assert!(AttackWeight::new(0.0).is_ok());
100        assert!(AttackWeight::new(0.5).is_ok());
101        assert!(AttackWeight::new(100.0).is_ok());
102    }
103
104    #[test]
105    fn attack_weight_rejects_nan() {
106        let err = AttackWeight::new(f64::NAN).unwrap_err();
107        assert!(matches!(err, Error::InvalidWeight { .. }));
108    }
109
110    #[test]
111    fn attack_weight_rejects_infinity() {
112        assert!(AttackWeight::new(f64::INFINITY).is_err());
113        assert!(AttackWeight::new(f64::NEG_INFINITY).is_err());
114    }
115
116    #[test]
117    fn attack_weight_rejects_negative() {
118        assert!(AttackWeight::new(-0.1).is_err());
119    }
120
121    #[test]
122    fn budget_zero_is_valid() {
123        assert_eq!(Budget::zero().value(), 0.0);
124        assert!(Budget::new(0.0).is_ok());
125    }
126
127    #[test]
128    fn budget_rejects_invalid_values() {
129        assert!(Budget::new(-1.0).is_err());
130        assert!(Budget::new(f64::NAN).is_err());
131        assert!(Budget::new(f64::INFINITY).is_err());
132    }
133
134    #[test]
135    fn weighted_attack_new_validates_weight() {
136        assert!(WeightedAttack::new("a", "b", 0.5).is_ok());
137        assert!(WeightedAttack::new("a", "b", -0.5).is_err());
138    }
139}