probability_to_friendly_string/
lib.rs

1use std::fmt;
2#[macro_use]
3extern crate lazy_static;
4
5/// A probability that can be expressed in a user-friendly form.
6/// The `friendly_string` property is a textual representation of
7/// the probability (i.e. "5 out of 7")
8#[derive(PartialEq, Eq, Debug)]
9pub struct FriendlyProbability {
10    numerator: u8,
11    denominator: u8,
12    friendly_description: &'static str,
13    friendly_string: String
14}
15
16impl fmt::Display for FriendlyProbability {
17    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18        write!(f, "{}", self.friendly_string)
19    }
20}
21impl FriendlyProbability {
22    // http://xion.io/post/code/rust-optional-args.html
23    /// Create a new FriendlyProbability.  Most of the time you will want to use `from_probability()` instead.
24    pub fn new<T: Into<Option<String>>>(numerator: u8, denominator: u8, friendly_description: &'static str, friendly_string: T) -> FriendlyProbability {
25        let real_friendly_string = friendly_string.into().unwrap_or_else(|| format!("{} in {}", numerator, denominator));
26        FriendlyProbability {
27            numerator,
28            denominator,
29            friendly_description,
30            friendly_string: real_friendly_string
31        }
32    }
33    /// Gets the numerator of the FriendlyProbability.
34    pub fn numerator(self: &FriendlyProbability) -> u8 {
35        self.numerator
36    }
37    /// Gets the denominator of the FriendlyProbability.
38    pub fn denominator(self: &FriendlyProbability) -> u8 {
39        self.denominator
40    }
41    /// Gets the friendly description of the FriendlyProbability.
42    /// This is a qualitative description of the probability ("Still possible", "Flip a coin", "Good chance", etc.)
43    pub fn friendly_description(self: &FriendlyProbability) -> &str {
44        &self.friendly_description
45    }
46    /// Gets the friendly string of the FriendlyProbability.
47    /// Usually this is the same as "{numerator} in {denominator}",
48    /// but if the probability is very small it will instead be "<1 in 100",
49    /// and if it's very large it will be ">99 in 100"
50    pub fn friendly_string(self: &FriendlyProbability) -> &str {
51        &self.friendly_string
52    }
53    /// Create a FriendlyProbability from an f32.
54    /// # Examples
55    /// 
56    /// ```
57    /// use probability_to_friendly_string::FriendlyProbability;
58    /// 
59    /// let friendly = FriendlyProbability::from_probability(0.723);
60    /// assert_eq!(5, friendly.numerator());
61    /// assert_eq!(7, friendly.denominator());
62    /// assert_eq!("Good chance", friendly.friendly_description());
63    /// assert_eq!("5 in 7", friendly.friendly_string());
64    /// 
65    /// let friendly = FriendlyProbability::from_probability(0.999);
66    /// assert_eq!(">99 in 100", friendly.friendly_string());
67    /// 
68    /// let friendly = FriendlyProbability::from_probability(0.001);
69    /// assert_eq!("<1 in 100", friendly.friendly_string());
70    /// ```
71    /// # Panics
72    /// If probability is less than 0.0 or greater than 1.0.
73    pub fn from_probability(probability: f32) -> FriendlyProbability {
74        if probability < 0.0 || probability > 1.0 {
75            panic!("probability is less than 0 or greater than 1!")
76        }
77        // use slice::binary_search_by
78        // because f32's are not orderable
79        // https://stackoverflow.com/questions/28247990/how-to-do-a-binary-search-on-a-vec-of-floats
80        let friendly_description_location = FRIENDLY_DESCRIPTIONS.binary_search_by(|f| {
81            f.0.partial_cmp(&probability).expect("Couldn't compare floats?")
82        });
83        let friendly_description = match friendly_description_location {
84            Ok(i) => FRIENDLY_DESCRIPTIONS[i].1,
85            Err(i) => FRIENDLY_DESCRIPTIONS[i - 1].1
86        };
87        if probability == 0.0 {
88            return FriendlyProbability::new(0, 1, friendly_description, None)
89        }
90        if probability == 1.0 {
91            return FriendlyProbability::new(1, 1, friendly_description, None)
92        }
93        if probability > 0.99 {
94            return FriendlyProbability::new(99, 100, friendly_description, String::from(">99 in 100"))
95        }
96        if probability < 0.01 {
97            return FriendlyProbability::new(1, 100, friendly_description, String::from("<1 in 100"))
98        }
99        let data = &FRACTION_DATA;
100        let fraction_to_compare = Fraction::new_for_comparison(probability);
101        // use slice::binary_search_by since we can't derive Ord
102        // because f32's are not orderable
103        // https://stackoverflow.com/questions/28247990/how-to-do-a-binary-search-on-a-vec-of-floats
104        let location = data.binary_search_by(|f| {
105            f.partial_cmp(&fraction_to_compare).expect("Couldn't compare values?")
106        });
107        fn friendly_probability_from_fraction(fraction: &Fraction, friendly_description: &'static str) -> FriendlyProbability {
108            FriendlyProbability::new(fraction.numerator, fraction.denominator, friendly_description, None)
109        }
110        let data_len = data.len();
111        match location {
112            Ok(i) => friendly_probability_from_fraction(&data[i], friendly_description),
113            Err(i) => {
114                // This means it could be inserted at index i
115                if i == 0 {
116                    return friendly_probability_from_fraction(&data[0], friendly_description);
117                }
118                if i == data_len {
119                    return friendly_probability_from_fraction(&data[data_len - 1], friendly_description);
120                }
121                if probability - (&data[i - 1]).value < (&data[i]).value - probability {
122                    return friendly_probability_from_fraction(&data[i - 1], friendly_description);
123                }
124                else {
125                    return friendly_probability_from_fraction(&data[i], friendly_description);
126                }
127            }
128        }
129    }
130}
131
132#[derive(PartialOrd, PartialEq, Debug)]
133struct Fraction {
134    value: f32,
135    numerator: u8,
136    denominator: u8,
137}
138impl Fraction {
139    pub fn new(numerator: u8, denominator: u8) -> Fraction {
140        Fraction {
141            numerator: numerator,
142            denominator: denominator,
143            value: numerator as f32/denominator as f32
144        }
145    }
146    fn new_for_comparison(value: f32) -> Fraction {
147        Fraction {
148            numerator: 0,
149            denominator: 0,
150            value
151        }
152    }
153}
154const FRIENDLY_DESCRIPTIONS : [(f32, &str); 14] = [
155    (0.0, "Hard to imagine"),
156    (0.005, "Barely possible"),
157    (0.02, "Still possible"),
158    (0.08, "Some chance"),
159    (0.15, "Could happen"),
160    (0.2, "Perhaps"),
161    (0.45, "Flip a coin"),
162    (0.55, "Likelier than not"),
163    (0.7, "Good chance"),
164    (0.8, "Probably"),
165    (0.85, "Quite likely"),
166    (0.9, "Pretty likely"),
167    (0.95, "Very likely"),
168    (0.995, "Almost certainly"),
169];
170
171lazy_static! {
172    static ref FRACTION_DATA: Vec<Fraction> = {
173        let mut fractions : Vec<Fraction> = Vec::new();
174        fn gcd(x: u8, y: u8) -> u8 {
175            let mut x = x;
176            let mut y = y;
177            while y != 0 {
178                let t = y;
179                y = x % y;
180                x = t;
181            }
182            x
183        }
184        for d in 2..11 {
185            for n in 1..d {
186                if gcd(n, d) == 1 {
187                    fractions.push(Fraction::new(n, d));
188                }
189            }
190        }
191        for &d in [12, 15, 20, 30, 40, 50, 60, 80, 100].iter() {
192            fractions.push(Fraction::new(1, d));
193            fractions.push(Fraction::new(d - 1, d));
194        }
195        fractions.sort_unstable_by(|a,b| a.partial_cmp(b).unwrap());
196        fractions
197    };
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn friendly_probability_string_matches_numeric_inputs() {
206        let fp = FriendlyProbability::new(1, 2, "", None);
207        assert_eq!("1 in 2", fp.friendly_string);
208    }
209    #[test]
210    fn friendly_probability_string_matches_string_input() {
211        let s = String::from("something weird");
212        let fp = FriendlyProbability::new(1, 2, "", s.clone());
213        assert_eq!(s, fp.friendly_string);
214    }
215    #[test]
216    #[should_panic]
217    fn friendly_probability_from_fraction_less_than_0() {
218        FriendlyProbability::from_probability(-0.01);
219    }
220    #[test]
221    #[should_panic]
222    fn friendly_probability_from_fraction_greater_than_1() {
223        FriendlyProbability::from_probability(1.01);
224    }
225}