1use super::format::extract_year;
36use crate::db::zotero::{Creator, ZoteroItem};
37use std::collections::HashMap;
38
39pub fn is_native_style(style: &str) -> bool {
41 let s = style.to_lowercase();
42 s.contains("apa") || s.contains("ieee")
43}
44
45pub fn format_bibliography(
47 item: &ZoteroItem,
48 metadata: &HashMap<String, String>,
49 style: &str,
50) -> String {
51 let s = style.to_lowercase();
52 if s.contains("ieee") {
53 format_ieee(item, metadata)
54 } else {
55 format_apa(item, metadata)
57 }
58}
59
60pub fn format_bibliography_list(
62 items: &[(ZoteroItem, HashMap<String, String>)],
63 style: &str,
64) -> String {
65 items
66 .iter()
67 .enumerate()
68 .map(|(i, (item, metadata))| {
69 let entry = format_bibliography(item, metadata, style);
70 if style.to_lowercase().contains("ieee") {
71 format!("[{}] {}", i + 1, entry)
72 } else {
73 entry
74 }
75 })
76 .collect::<Vec<_>>()
77 .join("\n\n")
78}
79
80fn format_apa(item: &ZoteroItem, metadata: &HashMap<String, String>) -> String {
98 let mut parts: Vec<String> = Vec::new();
99
100 let authors = format_apa_authors(&item.creators);
102 if !authors.is_empty() {
103 parts.push(authors);
104 }
105
106 if let Some(year) = item.date.as_ref().and_then(|d| extract_year(d)) {
108 parts.push(format!("({year})."));
109 }
110
111 match item.item_type.as_str() {
113 "book" => parts.push(format!("*{}*.", item.title)),
114 _ => parts.push(format!("{}.", item.title)),
115 }
116
117 if let Some(journal) = metadata.get("publicationTitle") {
119 let volume = metadata.get("volume");
120 let issue = metadata.get("issue");
121 let pages = metadata.get("pages");
122
123 let mut journal_part = format!("*{journal}*");
124 if let Some(vol) = volume {
125 journal_part.push_str(&format!(", *{vol}*"));
126 if let Some(iss) = issue {
127 journal_part.push_str(&format!("({iss})"));
128 }
129 }
130 if let Some(pp) = pages {
131 journal_part.push_str(&format!(", {pp}"));
132 }
133 journal_part.push('.');
134 parts.push(journal_part);
135 } else if let Some(conf) = metadata.get("conferenceName") {
136 let mut conf_part = format!("In *{conf}*");
137 if let Some(pp) = metadata.get("pages") {
138 conf_part.push_str(&format!(" (pp. {pp})"));
139 }
140 conf_part.push('.');
141 parts.push(conf_part);
142 } else if let Some(publisher) = metadata.get("publisher") {
143 parts.push(format!("{publisher}."));
144 }
145
146 if let Some(doi) = &item.doi {
148 parts.push(format!("https://doi.org/{doi}"));
149 }
150
151 parts.join(" ")
152}
153
154fn format_apa_authors(creators: &[Creator]) -> String {
156 let authors: Vec<&Creator> = creators
157 .iter()
158 .filter(|c| c.creator_type == "author")
159 .collect();
160
161 if authors.is_empty() {
162 return String::new();
163 }
164
165 let formatted: Vec<String> = authors
166 .iter()
167 .map(|c| {
168 match &c.first_name {
169 Some(first) if !first.is_empty() => {
170 let initials: String = first
172 .split_whitespace()
173 .map(|w| format!("{}.", w.chars().next().unwrap_or(' ')))
174 .collect::<Vec<_>>()
175 .join(" ");
176 format!("{}, {}", c.last_name, initials)
177 }
178 _ => c.last_name.clone(),
179 }
180 })
181 .collect();
182
183 match formatted.len() {
184 1 => formatted[0].clone(),
185 2 => format!("{} & {}", formatted[0], formatted[1]),
186 _ => {
187 let last = formatted.last().unwrap();
189 let rest = &formatted[..formatted.len() - 1];
190 format!("{}, & {}", rest.join(", "), last)
191 }
192 }
193}
194
195fn format_ieee(item: &ZoteroItem, metadata: &HashMap<String, String>) -> String {
209 let mut parts: Vec<String> = Vec::new();
210
211 let authors = format_ieee_authors(&item.creators);
213 if !authors.is_empty() {
214 parts.push(format!("{authors},"));
215 }
216
217 parts.push(format!("\"{}\"", item.title));
219
220 if let Some(journal) = metadata.get("publicationTitle") {
222 let mut journal_part = format!("*{journal}*");
223 if let Some(vol) = metadata.get("volume") {
224 journal_part.push_str(&format!(", vol. {vol}"));
225 }
226 if let Some(iss) = metadata.get("issue") {
227 journal_part.push_str(&format!(", no. {iss}"));
228 }
229 if let Some(pp) = metadata.get("pages") {
230 journal_part.push_str(&format!(", pp. {pp}"));
231 }
232 if let Some(year) = item.date.as_ref().and_then(|d| extract_year(d)) {
233 journal_part.push_str(&format!(", {year}"));
234 }
235 journal_part.push('.');
236 parts.push(journal_part);
237 } else if let Some(conf) = metadata.get("conferenceName") {
238 let mut conf_part = format!("in *{conf}*");
239 if let Some(year) = item.date.as_ref().and_then(|d| extract_year(d)) {
240 conf_part.push_str(&format!(", {year}"));
241 }
242 if let Some(pp) = metadata.get("pages") {
243 conf_part.push_str(&format!(", pp. {pp}"));
244 }
245 conf_part.push('.');
246 parts.push(conf_part);
247 } else if let Some(year) = item.date.as_ref().and_then(|d| extract_year(d)) {
248 parts.push(format!("{year}."));
249 }
250
251 if let Some(doi) = &item.doi {
253 parts.push(format!("doi: {doi}."));
254 }
255
256 parts.join(" ")
257}
258
259fn format_ieee_authors(creators: &[Creator]) -> String {
261 let authors: Vec<&Creator> = creators
262 .iter()
263 .filter(|c| c.creator_type == "author")
264 .collect();
265
266 if authors.is_empty() {
267 return String::new();
268 }
269
270 let formatted: Vec<String> = authors
271 .iter()
272 .map(|c| match &c.first_name {
273 Some(first) if !first.is_empty() => {
274 let initials: String = first
275 .split_whitespace()
276 .map(|w| format!("{}.", w.chars().next().unwrap_or(' ')))
277 .collect::<Vec<_>>()
278 .join(" ");
279 format!("{} {}", initials, c.last_name)
280 }
281 _ => c.last_name.clone(),
282 })
283 .collect();
284
285 match formatted.len() {
286 1 => formatted[0].clone(),
287 2 => format!("{} and {}", formatted[0], formatted[1]),
288 _ => {
289 let last = formatted.last().unwrap();
290 let rest = &formatted[..formatted.len() - 1];
291 format!("{}, and {}", rest.join(", "), last)
292 }
293 }
294}
295
296#[cfg(test)]
299mod tests {
300 use super::*;
301 use crate::db::zotero::Creator;
302
303 fn demillo_item() -> ZoteroItem {
304 ZoteroItem {
305 item_id: 1,
306 item_key: "ABC12345".into(),
307 item_type: "journalArticle".into(),
308 title: "Hints on Test Data Selection: Help for the Practicing Programmer".into(),
309 date: Some("1978".into()),
310 doi: Some("10.1109/C-M.1978.218136".into()),
311 url: None,
312 abstract_note: None,
313 creators: vec![
314 Creator {
315 creator_type: "author".into(),
316 first_name: Some("Richard A.".into()),
317 last_name: "DeMillo".into(),
318 order: 0,
319 },
320 Creator {
321 creator_type: "author".into(),
322 first_name: Some("Richard J.".into()),
323 last_name: "Lipton".into(),
324 order: 1,
325 },
326 Creator {
327 creator_type: "author".into(),
328 first_name: Some("Fred G.".into()),
329 last_name: "Sayward".into(),
330 order: 2,
331 },
332 ],
333 tags: vec![],
334 date_added: "2024-01-01".into(),
335 date_modified: "2024-06-15".into(),
336 }
337 }
338
339 fn demillo_metadata() -> HashMap<String, String> {
340 let mut m = HashMap::new();
341 m.insert("publicationTitle".into(), "Computer".into());
342 m.insert("volume".into(), "11".into());
343 m.insert("issue".into(), "4".into());
344 m.insert("pages".into(), "34-41".into());
345 m
346 }
347
348 #[test]
351 fn apa_journal_article() {
352 let bib = format_apa(&demillo_item(), &demillo_metadata());
353 assert!(
355 bib.contains("DeMillo, R. A., Lipton, R. J., & Sayward, F. G."),
356 "Got: {bib}"
357 );
358 assert!(bib.contains("(1978)."));
359 assert!(bib.contains("Hints on Test Data Selection"));
360 assert!(bib.contains("*Computer*, *11*(4), 34-41."));
361 assert!(bib.contains("https://doi.org/10.1109/C-M.1978.218136"));
362 }
363
364 #[test]
365 fn apa_two_authors() {
366 let mut item = demillo_item();
367 item.creators = vec![
368 Creator {
369 creator_type: "author".into(),
370 first_name: Some("Yue".into()),
371 last_name: "Jia".into(),
372 order: 0,
373 },
374 Creator {
375 creator_type: "author".into(),
376 first_name: Some("Mark".into()),
377 last_name: "Harman".into(),
378 order: 1,
379 },
380 ];
381 let bib = format_apa(&item, &HashMap::new());
382 assert!(bib.contains("Jia, Y. & Harman, M."), "Got: {bib}");
383 }
384
385 #[test]
386 fn apa_single_author() {
387 let mut item = demillo_item();
388 item.creators = vec![Creator {
389 creator_type: "author".into(),
390 first_name: Some("Yue".into()),
391 last_name: "Jia".into(),
392 order: 0,
393 }];
394 let bib = format_apa(&item, &HashMap::new());
395 assert!(bib.starts_with("Jia, Y."), "Got: {bib}");
396 }
397
398 #[test]
399 fn apa_book() {
400 let mut item = demillo_item();
401 item.item_type = "book".into();
402 item.title = "The Art of Software Testing".into();
403 let mut meta = HashMap::new();
404 meta.insert("publisher".into(), "Wiley".into());
405 let bib = format_apa(&item, &meta);
406 assert!(bib.contains("*The Art of Software Testing*."), "Got: {bib}");
407 assert!(bib.contains("Wiley."));
408 }
409
410 #[test]
413 fn ieee_journal_article() {
414 let bib = format_ieee(&demillo_item(), &demillo_metadata());
415 assert!(
417 bib.contains("R. A. DeMillo, R. J. Lipton, and F. G. Sayward"),
418 "Got: {bib}"
419 );
420 assert!(bib.contains("\"Hints on Test Data Selection"));
421 assert!(bib.contains("*Computer*"));
422 assert!(bib.contains("vol. 11"));
423 assert!(bib.contains("no. 4"));
424 assert!(bib.contains("pp. 34-41"));
425 assert!(bib.contains("doi: 10.1109/C-M.1978.218136"));
426 }
427
428 #[test]
429 fn ieee_two_authors() {
430 let mut item = demillo_item();
431 item.creators = vec![
432 Creator {
433 creator_type: "author".into(),
434 first_name: Some("Goran".into()),
435 last_name: "Petrovic".into(),
436 order: 0,
437 },
438 Creator {
439 creator_type: "author".into(),
440 first_name: Some("Marko".into()),
441 last_name: "Ivankovic".into(),
442 order: 1,
443 },
444 ];
445 let bib = format_ieee(&item, &HashMap::new());
446 assert!(bib.contains("G. Petrovic and M. Ivankovic"), "Got: {bib}");
447 }
448
449 #[test]
452 fn native_style_detection() {
453 assert!(is_native_style("http://www.zotero.org/styles/apa"));
454 assert!(is_native_style("apa"));
455 assert!(is_native_style("APA"));
456 assert!(is_native_style("http://www.zotero.org/styles/ieee"));
457 assert!(is_native_style("IEEE"));
458 assert!(!is_native_style(
459 "http://www.zotero.org/styles/chicago-author-date"
460 ));
461 assert!(!is_native_style("vancouver"));
462 }
463
464 #[test]
467 fn ieee_list_is_numbered() {
468 let items = vec![(demillo_item(), demillo_metadata())];
469 let list = format_bibliography_list(&items, "ieee");
470 assert!(list.starts_with("[1]"), "Got: {list}");
471 }
472
473 #[test]
474 fn apa_list_is_not_numbered() {
475 let items = vec![(demillo_item(), demillo_metadata())];
476 let list = format_bibliography_list(&items, "apa");
477 assert!(!list.starts_with("[1]"));
478 assert!(list.starts_with("DeMillo"), "Got: {list}");
479 }
480}