Skip to main content

scientific_method/
method.rs

1// Copyright © 2026 James Hunt. All rights reserved.
2
3use crate::{
4    Conclusion, Control, Criterion, CriterionError, CriterionResult, DependentVariable, Hypothesis,
5    IndependentVariable, Observations, Outcome, Prediction, Question,
6};
7
8/// A complete statement of the scientific method for one experiment.
9#[derive(Debug, Clone, PartialEq)]
10#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11pub struct ScientificMethod {
12    pub question: Option<Question>,
13    pub hypothesis: Hypothesis,
14    pub predictions: Vec<Prediction>,
15    pub independent_variables: Vec<IndependentVariable>,
16    pub dependent_variables: Vec<DependentVariable>,
17    pub controls: Vec<Control>,
18    pub criteria: Vec<Criterion>,
19}
20
21impl ScientificMethod {
22    pub fn new(hypothesis: Hypothesis) -> Self {
23        Self {
24            question: None,
25            hypothesis,
26            predictions: Vec::new(),
27            independent_variables: Vec::new(),
28            dependent_variables: Vec::new(),
29            controls: Vec::new(),
30            criteria: Vec::new(),
31        }
32    }
33
34    pub fn question(mut self, question: Question) -> Self {
35        self.question = Some(question);
36        self
37    }
38
39    pub fn prediction(mut self, prediction: Prediction) -> Self {
40        self.predictions.push(prediction);
41        self
42    }
43
44    pub fn independent(mut self, variable: IndependentVariable) -> Self {
45        self.independent_variables.push(variable);
46        self
47    }
48
49    pub fn dependent(mut self, variable: DependentVariable) -> Self {
50        self.dependent_variables.push(variable);
51        self
52    }
53
54    pub fn control(mut self, control: Control) -> Self {
55        self.controls.push(control);
56        self
57    }
58
59    pub fn criterion(mut self, criterion: Criterion) -> Self {
60        self.criteria.push(criterion);
61        self
62    }
63
64    /// Evaluate every declared criterion against the supplied observations.
65    pub fn evaluate(
66        &self,
67        observations: &Observations,
68    ) -> Result<MethodEvaluation, CriterionError> {
69        if self.criteria.is_empty() {
70            return Ok(MethodEvaluation {
71                criteria: Vec::new(),
72                conclusion: Conclusion::new(Outcome::Inconclusive),
73            });
74        }
75
76        let criteria = self
77            .criteria
78            .iter()
79            .map(|criterion| criterion.evaluate(observations))
80            .collect::<Result<Vec<_>, _>>()?;
81
82        let outcome = if criteria.iter().all(|result| result.passed) {
83            Outcome::Supported
84        } else {
85            Outcome::NotSupported
86        };
87
88        Ok(MethodEvaluation {
89            criteria,
90            conclusion: Conclusion::new(outcome),
91        })
92    }
93}
94
95/// The result of applying a scientific method to observations.
96#[derive(Debug, Clone, PartialEq, Eq)]
97#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
98pub struct MethodEvaluation {
99    pub criteria: Vec<CriterionResult>,
100    pub conclusion: Conclusion,
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use crate::{Comparator, Measurement, Observation, Operand};
107
108    #[test]
109    fn no_criteria_is_inconclusive() {
110        let method = ScientificMethod::new(Hypothesis::new("something changes"));
111        assert_eq!(
112            method
113                .evaluate(&Observations::new())
114                .unwrap()
115                .conclusion
116                .outcome,
117            Outcome::Inconclusive
118        );
119    }
120
121    #[test]
122    fn all_declared_criteria_must_pass() {
123        let method = ScientificMethod::new(Hypothesis::new("candidate is valid"))
124            .criterion(Criterion::compare(
125                Operand::observation("candidate", "tests_pass"),
126                Comparator::Equal,
127                Operand::literal(true),
128            ))
129            .criterion(Criterion::compare(
130                Operand::observation("candidate", "checksum_matches"),
131                Comparator::Equal,
132                Operand::literal(true),
133            ));
134
135        let observations = Observations::new()
136            .with(Observation::new(
137                "candidate",
138                Measurement::new("tests_pass", true),
139            ))
140            .with(Observation::new(
141                "candidate",
142                Measurement::new("checksum_matches", false),
143            ));
144
145        assert_eq!(
146            method.evaluate(&observations).unwrap().conclusion.outcome,
147            Outcome::NotSupported
148        );
149    }
150}