use serde::Deserialize;
const TECH_YAML: &str = include_str!("../data/fortunes/tech.yaml");
const SCIENCE_YAML: &str = include_str!("../data/fortunes/science.yaml");
const HUMOR_YAML: &str = include_str!("../data/fortunes/humor.yaml");
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Category {
Tech,
Science,
Humor,
All,
}
#[derive(Debug, Clone, Deserialize)]
struct FortuneFile {
entries: Vec<FortuneEntry>,
}
#[derive(Debug, Clone, Deserialize)]
struct FortuneEntry {
text: String,
#[serde(default)]
author: Option<String>,
}
pub fn fortune(category: Option<&Category>, probability: f32) -> Option<String> {
if probability <= 0.0 {
return None;
}
if probability < 1.0 {
let roll: f32 = simple_random_f32();
if roll > probability {
return None;
}
}
let cat = category.unwrap_or(&Category::All);
let entries = load_entries(cat);
if entries.is_empty() {
return None;
}
let idx = simple_random_usize(entries.len());
let entry = &entries[idx];
Some(match &entry.author {
Some(author) => format!("{} -- {}", entry.text, author),
None => entry.text.clone(),
})
}
fn load_entries(category: &Category) -> Vec<FortuneEntry> {
match category {
Category::Tech => parse_entries(TECH_YAML),
Category::Science => parse_entries(SCIENCE_YAML),
Category::Humor => parse_entries(HUMOR_YAML),
Category::All => {
let mut all = parse_entries(TECH_YAML);
all.extend(parse_entries(SCIENCE_YAML));
all.extend(parse_entries(HUMOR_YAML));
all
}
}
}
fn parse_entries(yaml: &str) -> Vec<FortuneEntry> {
serde_yaml_ng::from_str::<FortuneFile>(yaml)
.map(|f| f.entries)
.unwrap_or_default()
}
fn simple_random_f32() -> f32 {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.subsec_nanos();
let mut x = nanos;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
(x as f32) / (u32::MAX as f32)
}
fn simple_random_usize(max: usize) -> usize {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.subsec_nanos();
let mut x = nanos;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
(x as usize) % max
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fortune_returns_some_at_full_probability() {
let result = fortune(Some(&Category::Tech), 1.0);
assert!(result.is_some());
}
#[test]
fn fortune_returns_none_at_zero_probability() {
let result = fortune(Some(&Category::Tech), 0.0);
assert!(result.is_none());
}
#[test]
fn fortune_all_category_has_entries() {
let entries = load_entries(&Category::All);
assert!(entries.len() > 150); }
#[test]
fn fortune_each_category_has_entries() {
assert!(load_entries(&Category::Tech).len() >= 50);
assert!(load_entries(&Category::Science).len() >= 50);
assert!(load_entries(&Category::Humor).len() >= 50);
}
#[test]
fn fortune_format_with_author() {
let entry = FortuneEntry {
text: "Test quote".into(),
author: Some("Test Author".into()),
};
let formatted = match &entry.author {
Some(author) => format!("{} -- {}", entry.text, author),
None => entry.text.clone(),
};
assert_eq!(formatted, "Test quote -- Test Author");
}
#[test]
fn fortune_format_without_author() {
let entry = FortuneEntry {
text: "Anonymous quote".into(),
author: None,
};
let formatted = match &entry.author {
Some(author) => format!("{} -- {}", entry.text, author),
None => entry.text.clone(),
};
assert_eq!(formatted, "Anonymous quote");
}
}