1use serde::Deserialize;
10
11const TECH_YAML: &str = include_str!("../data/fortunes/tech.yaml");
12const SCIENCE_YAML: &str = include_str!("../data/fortunes/science.yaml");
13const HUMOR_YAML: &str = include_str!("../data/fortunes/humor.yaml");
14
15#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
16#[serde(rename_all = "lowercase")]
17pub enum Category {
18 Tech,
19 Science,
20 Humor,
21 All,
22}
23
24#[derive(Debug, Clone, Deserialize)]
25struct FortuneFile {
26 entries: Vec<FortuneEntry>,
27}
28
29#[derive(Debug, Clone, Deserialize)]
30struct FortuneEntry {
31 text: String,
32 #[serde(default)]
33 author: Option<String>,
34}
35
36pub fn fortune(category: Option<&Category>, probability: f32) -> Option<String> {
41 if probability <= 0.0 {
42 return None;
43 }
44 if probability < 1.0 {
45 let roll: f32 = simple_random_f32();
46 if roll > probability {
47 return None;
48 }
49 }
50
51 let cat = category.unwrap_or(&Category::All);
52 let entries = load_entries(cat);
53
54 if entries.is_empty() {
55 return None;
56 }
57
58 let idx = simple_random_usize(entries.len());
59 let entry = &entries[idx];
60
61 Some(match &entry.author {
62 Some(author) => format!("{} -- {}", entry.text, author),
63 None => entry.text.clone(),
64 })
65}
66
67fn load_entries(category: &Category) -> Vec<FortuneEntry> {
68 match category {
69 Category::Tech => parse_entries(TECH_YAML),
70 Category::Science => parse_entries(SCIENCE_YAML),
71 Category::Humor => parse_entries(HUMOR_YAML),
72 Category::All => {
73 let mut all = parse_entries(TECH_YAML);
74 all.extend(parse_entries(SCIENCE_YAML));
75 all.extend(parse_entries(HUMOR_YAML));
76 all
77 }
78 }
79}
80
81fn parse_entries(yaml: &str) -> Vec<FortuneEntry> {
82 serde_yaml_ng::from_str::<FortuneFile>(yaml)
83 .map(|f| f.entries)
84 .unwrap_or_default()
85}
86
87fn simple_random_f32() -> f32 {
90 let nanos = std::time::SystemTime::now()
91 .duration_since(std::time::UNIX_EPOCH)
92 .unwrap_or_default()
93 .subsec_nanos();
94 let mut x = nanos;
96 x ^= x << 13;
97 x ^= x >> 17;
98 x ^= x << 5;
99 (x as f32) / (u32::MAX as f32)
100}
101
102fn simple_random_usize(max: usize) -> usize {
104 let nanos = std::time::SystemTime::now()
105 .duration_since(std::time::UNIX_EPOCH)
106 .unwrap_or_default()
107 .subsec_nanos();
108 let mut x = nanos;
109 x ^= x << 13;
110 x ^= x >> 17;
111 x ^= x << 5;
112 (x as usize) % max
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn fortune_returns_some_at_full_probability() {
121 let result = fortune(Some(&Category::Tech), 1.0);
122 assert!(result.is_some());
123 }
124
125 #[test]
126 fn fortune_returns_none_at_zero_probability() {
127 let result = fortune(Some(&Category::Tech), 0.0);
128 assert!(result.is_none());
129 }
130
131 #[test]
132 fn fortune_all_category_has_entries() {
133 let entries = load_entries(&Category::All);
134 assert!(entries.len() > 150); }
136
137 #[test]
138 fn fortune_each_category_has_entries() {
139 assert!(load_entries(&Category::Tech).len() >= 50);
140 assert!(load_entries(&Category::Science).len() >= 50);
141 assert!(load_entries(&Category::Humor).len() >= 50);
142 }
143
144 #[test]
145 fn fortune_format_with_author() {
146 let entry = FortuneEntry {
147 text: "Test quote".into(),
148 author: Some("Test Author".into()),
149 };
150 let formatted = match &entry.author {
151 Some(author) => format!("{} -- {}", entry.text, author),
152 None => entry.text.clone(),
153 };
154 assert_eq!(formatted, "Test quote -- Test Author");
155 }
156
157 #[test]
158 fn fortune_format_without_author() {
159 let entry = FortuneEntry {
160 text: "Anonymous quote".into(),
161 author: None,
162 };
163 let formatted = match &entry.author {
164 Some(author) => format!("{} -- {}", entry.text, author),
165 None => entry.text.clone(),
166 };
167 assert_eq!(formatted, "Anonymous quote");
168 }
169}