Skip to content

Commit

Permalink
feat: fair randomness calculation
Browse files Browse the repository at this point in the history
This implements a new algorithm for getting fortunes randomly, based on the
weighted sum of all fortunes, where the weight is the file size in bytes.

This prevents the problem of all files being uniformly distributed, leading to
small files being overrepresented, and thus often repeating fortunes from these
files, in a very unsatisfactory way.

Refs: #15
Signed-off-by: Christina Sørensen <christina@cafkafk.com>
  • Loading branch information
cafkafk committed Nov 8, 2023
1 parent 7c73d58 commit 8b3cd1e
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 103 deletions.
101 changes: 51 additions & 50 deletions src/file.rs
Original file line number Diff line number Diff line change
@@ -1,69 +1,88 @@
//! A module for file related actions.
use rand::prelude::SliceRandom;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};

/// Picks a random file from a given directory and returns its contents as a string.
/// Reads the contents of all files in a given directory and returns them as a vector of strings.
///
/// # Arguments
///
/// * `dir` - The path to the directory from which a random file will be chosen.
/// * `dir` - The path to the directory from which all files will be read.
///
/// # Returns
///
/// A `Result` containing the contents of the randomly chosen file as a `String`.
/// A `Result` containing a vector of strings, where each string represents the contents of a file in the directory.
///
/// # Errors
///
/// Returns an error if:
/// * The provided directory path is invalid or inaccessible.
/// * No files are found in the specified directory.
/// * There's an issue reading the chosen file.
///
/// # Note
///
/// This function does not account for the number of fortunes (or entries) within the files.
pub fn pick_file(dir: String) -> Result<String, Box<dyn std::error::Error>> {
let mut rng = rand::thread_rng();
let files: Vec<_> = fs::read_dir(dir)?.collect();
let file = files.choose(&mut rng).ok_or("No files found")?;
let path = file.as_ref().unwrap().path();
/// * There's an issue reading any of the files.
pub fn read_all_files(dir: &str) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let entries = fs::read_dir(dir)?;
let mut contents_vec = Vec::new();

let mut contents = String::new();
fs::File::open(path)?.read_to_string(&mut contents)?;
for entry in entries {
let path = entry?.path();
if path.is_file() {
let mut contents = String::new();
fs::File::open(path)?.read_to_string(&mut contents)?;
contents_vec.push(contents);
}
}

Ok(contents)
Ok(contents_vec)
}

/// Reads the contents of all files in a given directory and returns them as a vector of strings.
/// Retrieves the sizes of files in the specified directory.
///
/// This function will traverse the directory given by `path` and return a vector
/// of tuples. Each tuple contains the file size in bytes and the path to the file as `PathBuf`.
///
/// # Arguments
///
/// * `dir` - The path to the directory from which all files will be read.
/// * `path` - A generic parameter that implements `AsRef<Path>`, which is the path to the directory to read.
///
/// # Returns
///
/// A `Result` containing a vector of strings, where each string represents the contents of a file in the directory.
/// A `std::io::Result` containing a vector of tuples. Each tuple consists of a `u64` file size
/// and a `PathBuf` corresponding to a file's path. If an error occurs during directory traversal
/// or metadata retrieval, an `io::Error` is returned.
///
/// # Errors
///
/// Returns an error if:
/// * The provided directory path is invalid or inaccessible.
/// * There's an issue reading any of the files.
pub fn read_all_files(dir: &str) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let entries = fs::read_dir(dir)?;
let mut contents_vec = Vec::new();
/// This function will return an error in the following situations:
///
/// * The path does not exist.
/// * The current process lacks permissions to read the directory.
/// * The path points to a non-directory file.
/// * Any I/O error encountered when reading the directory contents or retrieving file metadata.
///
/// # Examples
///
/// ```
/// use std::path::Path;
///
/// let sizes = get_file_sizes(Path::new("./some/directory")).expect("Directory should be read");
/// for (size, path) in sizes {
/// println!("{} bytes - {:?}", size, path);
/// }
/// ```
pub fn get_file_sizes<P: AsRef<Path>>(path: P) -> std::io::Result<Vec<(u64, PathBuf)>> {
let entries = fs::read_dir(path)?;
let mut files: Vec<(u64, PathBuf)> = vec![];

for entry in entries {
let path = entry?.path();
let entry = entry?;
let path = entry.path();

if path.is_file() {
let mut contents = String::new();
fs::File::open(path)?.read_to_string(&mut contents)?;
contents_vec.push(contents);
let metadata = fs::metadata(&path)?;
files.push((metadata.len(), path));
}
}

Ok(contents_vec)
Ok(files)
}

#[cfg(test)]
Expand All @@ -87,17 +106,6 @@ mod tests {
tmp_dir
}

/// test_pick_file: Tests if the pick_file function can pick and read a file from a directory.
#[test]
fn test_pick_file() {
let tmp_dir = setup_test_directory();
let result = pick_file(tmp_dir.path().to_str().unwrap().to_string());

assert!(result.is_ok());
let content = result.unwrap();
assert!(content == "Content of file1\n" || content == "Content of file2\n");
}

/// test_read_all_files: Tests if the read_all_files function can read all files from a directory.
#[test]
fn test_read_all_files() {
Expand All @@ -111,13 +119,6 @@ mod tests {
assert!(contents.contains(&"Content of file2\n".to_string()));
}

/// test_pick_file_invalid_dir: Tests the error handling of pick_file when given an invalid directory.
#[test]
fn test_pick_file_invalid_dir() {
let result = pick_file("invalid_directory".to_string());
assert!(result.is_err());
}

/// test_read_all_files_invalid_dir: Tests the error handling of read_all_files when given an invalid directory.
#[test]
fn test_read_all_files_invalid_dir() {
Expand Down
83 changes: 30 additions & 53 deletions src/fortune.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::file;
use crate::random;

use std::env;
use std::path::PathBuf;
use std::process::exit;

/// The default maximum length for a short quote.
Expand Down Expand Up @@ -30,30 +31,6 @@ fn get_fortune_off_dir() -> String {
}
}

// TODO: refactor
fn handle_file_errors(
input: String,
f: &dyn Fn(String) -> Result<String, Box<dyn std::error::Error>>,
) -> String {
use std::io::ErrorKind;
match f(input.clone()) {
Ok(val) => val,
Err(e) => {
if let Some(io_err) = e.downcast_ref::<std::io::Error>() {
match io_err {
err if io_err.kind() == ErrorKind::NotFound => {
eprintln!("{err}");
println!("Couldn't find \"{input}\", make sure you set FORTUNE_DIR correctly, or verify that you're in a directory with a folder named \"{input}\".",);
std::process::exit(1);
}
&_ => panic!("{e:?}"),
}
}
panic!("{e:?}")
}
}
}

pub fn search_fortunes(pattern: &str) {
let fortune_dir = get_fortune_dir();

Expand Down Expand Up @@ -94,9 +71,8 @@ pub fn search_fortunes(pattern: &str) {
/// get_quote(&255); // Prints a humorous message and exits.
/// ```
pub fn get_quote(quote_size: &u8) {
let fortune_dir = get_fortune_dir();

let file = handle_file_errors(fortune_dir, &file::pick_file);
//let file = handle_file_errors(fortune_dir, &file::pick_file);
let file = &random::get_random_file_weighted(PathBuf::from(get_fortune_dir())).unwrap();

let quotes: Vec<&str> = file.split("\n%\n").collect();

Expand Down Expand Up @@ -131,29 +107,30 @@ pub fn get_quote(quote_size: &u8) {
}
}

#[cfg(test)]
mod tests {
use super::*;
use assert_cmd::Command;

// /// Tests the behavior of `get_quote` when the default size (1) is provided.
// /// It ensures that the output quote is within the expected length.
/* Doesn't work in CI
#[test]
fn test_get_quote_default_size() {
let mut cmd = Command::cargo_bin("fortune-kind").unwrap();
cmd.arg("-s");
let output = cmd.output().unwrap();
assert!(output.stdout.len() <= SHORT as usize);
}
/// Tests the behavior of `get_quote` when the humorous message trigger (255) is provided.
/// It ensures that the output matches the expected humorous message.
#[test]
fn test_get_quote_humorous_message() {
let mut cmd = Command::cargo_bin("fortune-kind").unwrap();
cmd.arg(format!("-{}", String::from("s").repeat(255)));
let output = cmd.output().unwrap();
assert_eq!(output.stdout, b"WE GET IT, YOU WANT A SHORT FORTUNE\n");
}*/
}
// TODO: yes, should be used or removed
// #[cfg(test)]
// mod tests {
// use super::*;
// use assert_cmd::Command;
//
// // /// Tests the behavior of `get_quote` when the default size (1) is provided.
// // /// It ensures that the output quote is within the expected length.
// /* Doesn't work in CI
// #[test]
// fn test_get_quote_default_size() {
// let mut cmd = Command::cargo_bin("fortune-kind").unwrap();
// cmd.arg("-s");
// let output = cmd.output().unwrap();
// assert!(output.stdout.len() <= SHORT as usize);
// }
//
// /// Tests the behavior of `get_quote` when the humorous message trigger (255) is provided.
// /// It ensures that the output matches the expected humorous message.
// #[test]
// fn test_get_quote_humorous_message() {
// let mut cmd = Command::cargo_bin("fortune-kind").unwrap();
// cmd.arg(format!("-{}", String::from("s").repeat(255)));
// let output = cmd.output().unwrap();
// assert_eq!(output.stdout, b"WE GET IT, YOU WANT A SHORT FORTUNE\n");
// }*/
// }
28 changes: 28 additions & 0 deletions src/random.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
//! A module for generating random numbers.
use std::path::PathBuf;

use rand::prelude::SliceRandom;
use rand::thread_rng;
use rand::Rng;
use std::io::Read;

use crate::file::get_file_sizes;

/// Generates a random number between 0 (inclusive) and the given upper bound (exclusive).
///
Expand All @@ -25,6 +31,28 @@ pub fn random(i: usize) -> usize {
rng.gen_range(0..i)
}

pub fn get_random_file_weighted(path: PathBuf) -> std::io::Result<String> {
use std::io::ErrorKind;
let mut rng = thread_rng();
match get_file_sizes(&path) {
Ok(mut files) => {
files.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
let mut contents = String::new();
std::fs::File::open(&files.choose_weighted(&mut rng, |item| item.0).unwrap().1)?
.read_to_string(&mut contents)?;
Ok(contents)
}
Err(e) => match e.kind() {
ErrorKind::NotFound => {
eprintln!("{e}");
println!("Couldn't find \"{path:?}\", make sure you set FORTUNE_DIR correctly, or verify that you're in a directory with a folder named \"{path:?}\".",);
std::process::exit(1);
}
_ => panic!("Error: {}", e),
},
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down

0 comments on commit 8b3cd1e

Please sign in to comment.