use chrono::{Datelike, Local, NaiveDate};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fs;
use std::path::PathBuf;
use crate::error::{Error, Result};
use crate::reference::{get_bible_book_by_number, get_number_of_chapters};
use crate::storage::config_dir;
const SCHEMA_VERSION: u32 = 1;
pub const PLAN_ID: &str = "bible-in-a-year";
#[derive(Debug, Clone)]
pub struct ReadingPlan {
pub plan_id: String,
pub year: i32,
pub days: Vec<DailyReading>,
}
#[derive(Debug, Clone)]
pub struct DailyReading {
pub day: u16,
pub date: NaiveDate,
pub refs: Vec<ChapterRange>,
}
#[derive(Debug, Clone)]
pub struct ChapterRange {
pub book_number: u8,
pub chapter_start: u16,
pub chapter_end: u16,
}
impl ChapterRange {
pub fn short(&self) -> String {
let book = short_book_name(self.book_number);
if self.chapter_start == self.chapter_end {
format!("{} {}", book, self.chapter_start)
} else {
format!("{} {}-{}", book, self.chapter_start, self.chapter_end)
}
}
}
impl DailyReading {
pub fn short(&self) -> String {
self.refs
.iter()
.map(|r| r.short())
.collect::<Vec<_>>()
.join(", ")
}
}
fn short_book_name(num: u8) -> &'static str {
match num {
1 => "Gen",
2 => "Exo",
3 => "Lev",
4 => "Num",
5 => "Deu",
6 => "Jos",
7 => "Jdg",
8 => "Rut",
9 => "1Sa",
10 => "2Sa",
11 => "1Ki",
12 => "2Ki",
13 => "1Ch",
14 => "2Ch",
15 => "Ezr",
16 => "Neh",
17 => "Est",
18 => "Job",
19 => "Psa",
20 => "Pro",
21 => "Ecc",
22 => "Sng",
23 => "Isa",
24 => "Jer",
25 => "Lam",
26 => "Eze",
27 => "Dan",
28 => "Hos",
29 => "Joe",
30 => "Amo",
31 => "Oba",
32 => "Jon",
33 => "Mic",
34 => "Nah",
35 => "Hab",
36 => "Zep",
37 => "Hag",
38 => "Zec",
39 => "Mal",
40 => "Mat",
41 => "Mar",
42 => "Luk",
43 => "Jhn",
44 => "Act",
45 => "Rom",
46 => "1Co",
47 => "2Co",
48 => "Gal",
49 => "Eph",
50 => "Phi",
51 => "Col",
52 => "1Th",
53 => "2Th",
54 => "1Ti",
55 => "2Ti",
56 => "Tit",
57 => "Phm",
58 => "Heb",
59 => "Jas",
60 => "1Pe",
61 => "2Pe",
62 => "1Jn",
63 => "2Jn",
64 => "3Jn",
65 => "Jud",
66 => "Rev",
_ => "?",
}
}
pub fn current_year() -> i32 {
Local::now().year()
}
pub fn today_day_of_year() -> u16 {
Local::now().date_naive().ordinal() as u16
}
fn is_leap(year: i32) -> bool {
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}
pub fn generate_bible_in_a_year(year: i32) -> ReadingPlan {
let mut all: Vec<(u8, u16)> = Vec::with_capacity(1200);
for book_num in 1..=66u8 {
if let Some(book) = get_bible_book_by_number(book_num) {
let nch: u32 = get_number_of_chapters(&book).into();
for c in 1..=nch as u16 {
all.push((book_num, c));
}
}
}
let total_days: u16 = if is_leap(year) { 366 } else { 365 };
let total = all.len();
let base = total / total_days as usize;
let extra = total % total_days as usize;
let mut days = Vec::with_capacity(total_days as usize);
let mut idx = 0usize;
for d in 1..=total_days {
let n = base + if (d as usize) <= extra { 1 } else { 0 };
let slice = &all[idx..idx + n];
idx += n;
let date = NaiveDate::from_yo_opt(year, d as u32)
.expect("valid day-of-year");
days.push(DailyReading {
day: d,
date,
refs: chunk_to_ranges(slice),
});
}
ReadingPlan {
plan_id: PLAN_ID.to_string(),
year,
days,
}
}
fn chunk_to_ranges(chs: &[(u8, u16)]) -> Vec<ChapterRange> {
let mut out = Vec::new();
if chs.is_empty() {
return out;
}
let mut cur = ChapterRange {
book_number: chs[0].0,
chapter_start: chs[0].1,
chapter_end: chs[0].1,
};
for &(b, c) in &chs[1..] {
if b == cur.book_number && c == cur.chapter_end + 1 {
cur.chapter_end = c;
} else {
out.push(cur);
cur = ChapterRange {
book_number: b,
chapter_start: c,
chapter_end: c,
};
}
}
out.push(cur);
out
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PlanFile {
pub schema_version: u32,
pub year: i32,
pub plan_id: String,
#[serde(default)]
pub completed_days: Vec<u16>,
}
fn plan_path() -> Result<PathBuf> {
Ok(config_dir()?.join("plan.toml"))
}
pub fn load_progress() -> Option<PlanFile> {
let path = plan_path().ok()?;
let s = fs::read_to_string(&path).ok()?;
let parsed: PlanFile = toml::from_str(&s).ok()?;
if parsed.schema_version != SCHEMA_VERSION {
return None;
}
Some(parsed)
}
pub fn save_progress(file: &PlanFile) -> Result<()> {
let path = plan_path()?;
let s = toml::to_string_pretty(file).map_err(|e| Error::Toml(e.to_string()))?;
fs::write(&path, s).map_err(Error::Io)?;
Ok(())
}
pub fn load_completed_for(year: i32, plan_id: &str) -> HashSet<u16> {
match load_progress() {
Some(f) if f.year == year && f.plan_id == plan_id => {
f.completed_days.into_iter().collect()
}
_ => HashSet::new(),
}
}