1use anyhow::{Result, anyhow};
8use jiff::civil::DateTime;
9use nom::{
10 IResult, Parser, character::{char, complete::{digit1, multispace1}}, combinator::map_res,
11 sequence::preceded,
12};
13use std::{
14 fmt,
15 fs::File,
16 io::{BufRead, BufReader, Read, Write},
17 path::{Path, PathBuf},
18 sync::Arc,
19};
20use crate::{aim::{Writer, WriterLike, parse_version}, literals::parse_date};
21
22#[derive(Debug, PartialEq, Copy, Clone)]
24pub struct Journal {
25 version: u32,
26 position: u64,
27 date: DateTime
28}
29
30impl Journal {
31 pub fn new(version: u32, position: u64, date: DateTime) -> Self {
35 Self { version, position, date }
36 }
37
38 pub fn version(&self) -> u32 {
40 self.version
41 }
42
43 pub fn position(&self) -> u64 {
45 self.position
46 }
47
48 pub fn date(&self) -> DateTime {
49 self.date
50 }
51
52 pub fn print(&self, writer: &mut Writer) {
53 writer.write_unsigned(self.version);
54 writer.write_char(',');
55 writer.write_u64(self.position);
56 writer.write_char(' ');
57 writer.write_date(&self.date);
58 }
59
60 pub fn to_formula(&self) -> String {
62 let mut writer = Writer::formulizer();
63 self.print(&mut writer);
64 writer.finish()
65 }
66}
67
68impl WriterLike for Journal {
69 fn write(&self, writer: &mut Writer) {
70 self.print(writer);
71 }
72}
73
74impl fmt::Display for Journal {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 write!(f, "{}", self.to_stringized())
77 }
78}
79
80pub fn build_journal(journals: &mut Vec<Arc<Journal>>, aim_path: &Path) -> Result<()> {
85 if !aim_path.exists() {
87 return Err(anyhow!("Invalid filename: {}", aim_path.to_string_lossy()));
88 }
89
90 let file = File::open(aim_path)?;
92
93 journals.clear();
95
96 let mut reader = BufReader::new(file);
98
99 let mut position: u64 = 0;
101 let mut line = String::new();
102
103 loop {
105 line.clear();
106 let bytes_read = reader.read_line(&mut line)?;
107
108 if bytes_read == 0 {
110 break;
111 }
112
113 let trimmed_line = line.trim_start();
115 if trimmed_line.starts_with('[') {
116 match parse_version(trimmed_line) {
118 Ok((_, version)) => {
119 if version.partial() == 0 {
121 journals.push(Arc::new(Journal {
122 version: version.epoch(),
123 position: position,
124 date: version.date(),
125 }));
126 }
127 }
128 Err(_) => {}
130 }
131 }
132
133 position += bytes_read as u64;
135 }
136
137 save_journal(aim_path, journals)?;
139
140 Ok(())
141}
142
143pub fn save_journal(aim_path: &Path, journals: &Vec<Arc<Journal>>) -> Result<()> {
147 let jnl_path = to_journal_path(aim_path)?;
148
149 let mut file = File::create(&jnl_path)?;
151
152 for journal in journals {
153 writeln!(file, "{}", journal)?;
154 }
155
156 Ok(())
157}
158
159pub fn to_journal_path(aim_path: &Path) -> Result<PathBuf> {
163 let stem = aim_path
164 .file_stem()
165 .ok_or_else(|| anyhow!("Invalid filename: {}", aim_path.to_string_lossy()))?;
166 let parent = aim_path.parent().unwrap_or_else(|| Path::new(""));
167
168 let mut jnl_path = parent.to_path_buf();
169 jnl_path.push(format!("{}.jnl", stem.to_string_lossy()));
170
171 Ok(jnl_path)
172}
173
174fn parse_u32(input: &str) -> IResult<&str, u32> {
176 map_res(digit1, |s: &str| s.parse::<u32>()).parse(input)
177}
178
179fn parse_u64(input: &str) -> IResult<&str, u64> {
181 map_res(digit1, |s: &str| s.parse::<u64>()).parse(input)
182}
183
184fn parse_journal_entry(input: &str) -> IResult<&str, Journal> {
186 let (input, (version, position, date)) = (
187 parse_u32,
188 preceded(
189 char(','),
190 parse_u64
191 ),
192 preceded(
193 multispace1,
194 parse_date
195 )
196 ).parse(input)?;
197 Ok((input, Journal { version, position, date }))
198}
199
200fn parse_journal(journals: &mut Vec<Arc<Journal>>, input: &str) -> Result<()> {
202 let lines: Vec<&str> = input.lines().collect();
203 journals.clear();
205 for line in lines {
206 match parse_journal_entry(line) {
207 Ok((_, journal)) => journals.push(Arc::new(journal)),
208 Err(nom::Err::Error(e)) | Err(nom::Err::Failure(e)) => {
209 return Err(anyhow!("Parse error: {}", e));
210 }
211 Err(nom::Err::Incomplete(_)) => return Err(anyhow!("Incomplete input")),
212 }
213 }
214 Ok(())
215}
216
217pub fn load_journal(journals: &mut Vec<Arc<Journal>>, aim_path: &Path) -> Result<()> {
222 let jnl_path = to_journal_path(aim_path)?;
223 if !Path::new(&jnl_path).exists() {
225 return build_journal(journals, aim_path);
226 }
227
228 let mut file = File::open(jnl_path)?;
230 let mut contents = String::new();
231 file.read_to_string(&mut contents)?;
232
233 parse_journal(journals, &contents)
235}