probability_to_friendly_string/
lib.rs1use std::fmt;
2#[macro_use]
3extern crate lazy_static;
4
5#[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 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 pub fn numerator(self: &FriendlyProbability) -> u8 {
35 self.numerator
36 }
37 pub fn denominator(self: &FriendlyProbability) -> u8 {
39 self.denominator
40 }
41 pub fn friendly_description(self: &FriendlyProbability) -> &str {
44 &self.friendly_description
45 }
46 pub fn friendly_string(self: &FriendlyProbability) -> &str {
51 &self.friendly_string
52 }
53 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 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 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 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}