1
\$\begingroup\$

I have written a Rust program that lets you input a citation's metadata in a Bibtex-like format and generates a bibliography entry formatted in Markdown that conforms (more or less) to the Chicago Author-Date style described here.

Currently, it supports citations for Books, JournalArticles, and NewspaperArticles with any number (>0) of authors. One of my goals was to write the code in a way that makes it easy to add a new resource type, say AcademicThesis, by creating a struct AcademicThesis containing the necessary fields and adding impl AcademicThesis { citation(&self) -> String { ... }} that generates the citation.

A secondary goal was to make it expandable to other citation styles, but of course this would require first renaming the citation() method to something more specific.

My motivation for writing this program is that almost all of the coding I do involves numerical schemes like this, and I almost never work with strings at all, but it seems like strings have a lot to teach us about how the Rust language works.

The following are non-features that I decided against implementing until I have a firmer grasp of the Rust language:

  • Page ranges such as (455, 468) are rendered naively as 455–468 instead of 455–68
  • Generates citations only, not a full bibliography (e.g. with line breaks, alphabetization, hanging indents...)

I would appreciate a general code review.

Here is the program:

const MONTHS: [&str; 12] = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];

pub struct Date {
    year: u16,
    month: usize,
    day: u16
}

impl Date {
    // Return a string such as "May 23" representing the date
    fn to_month_day(&self) -> String {
        format!(
            "{} {}",
            MONTHS[self.month],
            self.day
        )
    }
}

pub struct Name {
    first: String,
    last: String
}

/// Convert a vector of author names into a string listing them in Chicago style
fn list_authors(authors: &Vec<Name>) -> String {
    match authors.len() {
        0 => panic!("No author given!"),
        1 => format!("{}, {}", authors.first().unwrap().last, authors.first().unwrap().first),
        2 => format!(
            "{}, {} and {} {}",
            authors.first().unwrap().last,
            authors.first().unwrap().first,
            authors.last().unwrap().first,
            authors.last().unwrap().last,
        ),
        _ => {
            let mut s =  format!(
                "{}, {}, ",
                authors.first().unwrap().last,
                authors.first().unwrap().first
            );
            for i in 1..authors.len()-1 {
                s.push_str(&format!(
                    "{} {}, ",
                    authors[i].first,
                    authors[i].last
                ))
            };
            s.push_str(&format!(
                    "and {} {}",
                    authors.last().unwrap().first,
                    authors.last().unwrap().last
            ));
            s
        }
    }
}

/// Convert "str" to "[str](https://str)", which renders as a link in markdown
fn link_to_clickable(url: &String) -> String {
    return format!("[{}](https://{})", url, url);
}

pub struct JournalArticle {
    authors: Vec<Name>,
    year: u16,
    title: String,
    journal: String,
    volume: u16,
    issue: u16,
    pages: (u16, u16),
    url: String
}

impl JournalArticle{
    /// Generate a citation in Chicago style
    fn citation(&self) -> String {
        let author_string = list_authors(&self.authors);

        return format!(
            "{}. {}. &ldquo;{}.&rdquo; *{}* {} ({}): {}&ndash;{}. {}.",
            author_string,
            self.year,
            self.title, 
            self.journal, 
            self.volume,
            self.issue, 
            self.pages.0,
            self.pages.1,
            link_to_clickable(&self.url)
        );
    }
}

pub struct NewspaperArticle {
    authors: Vec<Name>,
    date: Date,
    title: String,
    newspaper: String,
    url: String
}

impl NewspaperArticle {
    /// Generate a citation in Chicago style
    fn citation(&self) -> String {
        let author_string = list_authors(&self.authors);
        let date_string = self.date.to_month_day();

        return format!(
            "{}. {}. &ldquo;{}.&rdquo; *{},* {}. {}.",
            author_string,
            self.date.year,
            self.title, 
            self.newspaper,
            date_string,
            link_to_clickable(&self.url)
        );
    }
}

pub struct Book {
    authors: Vec<Name>,
    year: u16,
    title: String,
    publisher: String,
    location: String
}

impl Book {
    /// Generate a citation in Chicago style
    fn citation(&self) -> String {
        let author_string = list_authors(&self.authors);

        return format!(
            "{}. {}. *{}.* {}: {}.",
            author_string,
            self.year,
            self.title, 
            self.location,
            self.publisher,
        );
    }
}

fn main() {
    // Journal article with one author
    let source = JournalArticle{
        authors: Vec::<Name>::from([Name{
            first: String::from("Aaron"),
            last: String::from("Bodoh&ndash;Creed")
        }]),
        year: 2020,
        title: String::from("Optimizing for Distributional Goals in School Choice Problems"),
        journal: String::from("Management Science"),
        volume: 66,
        issue: 8,
        pages: (3657, 3676),
        url: String::from("doi.org/10.1287/mnsc.2019.3376")
    };

    println!("{}\n", source.citation());

    // Journal article with two authors
    let source = JournalArticle{
        authors: Vec::<Name>::from([Name{
            first: String::from("Paul"),
            last: String::from("Milgrom")
        }, Name{
            first: String::from("John"),
            last: String::from("Roberts")
        }]),
        year: 1990,
        title: String::from("The Economics of Modern Manufacturing: Technology, Strategy, and Organization"),
        journal: String::from("The American Economic Review"),
        volume: 80,
        issue: 3,
        pages: (511, 528),
        url: String::from("jstor.org/stable/2006681")
    };

    println!("{}\n", source.citation());

    // Journal article with three authors
    let source = JournalArticle{
        authors: Vec::<Name>::from([Name{
            first: String::from("Chandra"),
            last: String::from("Chekuri")
        }, Name{
            first: String::from("Jan"),
            last: String::from("Vondrák")
        }, Name{
            first: String::from("Rico"),
            last: String::from("Zenklusen")
        }]),
        year: 2014,
        title: String::from("Submodular Function Maximization via the Multilinear Relaxation and Contention Resolution Schemes"),
        journal: String::from("SIAM Journal on Computing"),
        volume: 43,
        issue: 6,
        pages: (3657, 3676),
        url: String::from("doi.org/10.1137/110839655")
    };

    println!("{}\n", source.citation());

    // Newspaper article
    let source = NewspaperArticle{
        authors: Vec::<Name>::from([Name{
            first: String::from("Kathy Johnson"),  // Middle names can be treated as an extension of first names
            last: String::from("Bowles")
        }]),
        date: Date{day: 24, month: 5, year: 2022},
        title: String::from("Campus Landscapes: So Much More Than Marketing"),
        newspaper: String::from("Inside Higher Ed"),
        url: String::from("insidehighered.com/blogs/just-explain-it-me/campus-landscapes-so-much-more-marketing")
    };

    println!("{}\n", source.citation());

    // Book
    let source = Book{
        authors: Vec::<Name>::from([Name{
            first: String::from("Thomas"),
            last: String::from("Cormen")
        }, Name{
            first: String::from("Charles"),
            last: String::from("Leiserson")
        }, Name{
            first: String::from("Ronald"),
            last: String::from("Rivest")
        }]),
        year: 1996,
        title: String::from("Introduction to Algorithms"),
        publisher: String::from("The MIT Press"),
        location: String::from("Cambridge, Massachusetts")
    };

    println!("{}\n", source.citation());
}

It outputs

Bodoh&ndash;Creed, Aaron. 2020. &ldquo;Optimizing for Distributional Goals in School Choice Problems.&rdquo; *Management Science* 66 (8): 3657&ndash;3676. [doi.org/10.1287/mnsc.2019.3376](https://doi.org/10.1287/mnsc.2019.3376).

Milgrom, Paul and John Roberts. 1990. &ldquo;The Economics of Modern Manufacturing: Technology, Strategy, and Organization.&rdquo; *The American Economic Review* 80 (3): 511&ndash;528. [jstor.org/stable/2006681](https://jstor.org/stable/2006681).

Chekuri, Chandra, Jan Vondrák, and Rico Zenklusen. 2014. &ldquo;Submodular Function Maximization via the Multilinear Relaxation and Contention Resolution Schemes.&rdquo; *SIAM Journal on Computing* 43 (6): 3657&ndash;3676. [doi.org/10.1137/110839655](https://doi.org/10.1137/110839655).

Bowles, Kathy Johnson. 2022. &ldquo;Campus Landscapes: So Much More Than Marketing.&rdquo; *Inside Higher Ed,* June 24. [insidehighered.com/blogs/just-explain-it-me/campus-landscapes-so-much-more-marketing](https://insidehighered.com/blogs/just-explain-it-me/campus-landscapes-so-much-more-marketing).

Cormen, Thomas, Charles Leiserson, and Ronald Rivest. 1996. *Introduction to Algorithms.* Cambridge, Massachusetts: The MIT Press.

Which renders in Markdown as:

Bodoh–Creed, Aaron. 2020. “Optimizing for Distributional Goals in School Choice Problems.” Management Science 66 (8): 3657–3676. doi.org/10.1287/mnsc.2019.3376.

Milgrom, Paul and John Roberts. 1990. “The Economics of Modern Manufacturing: Technology, Strategy, and Organization.” The American Economic Review 80 (3): 511–528. jstor.org/stable/2006681.

Chekuri, Chandra, Jan Vondrák, and Rico Zenklusen. 2014. “Submodular Function Maximization via the Multilinear Relaxation and Contention Resolution Schemes.” SIAM Journal on Computing 43 (6): 3657–3676. doi.org/10.1137/110839655.

Bowles, Kathy Johnson. 2022. “Campus Landscapes: So Much More Than Marketing.” Inside Higher Ed, June 24. insidehighered.com/blogs/just-explain-it-me/campus-landscapes-so-much-more-marketing.

Cormen, Thomas, Charles Leiserson, and Ronald Rivest. 1996. Introduction to Algorithms. Cambridge, Massachusetts: The MIT Press.

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

Nice project! From previous experience writing citation formatting in Rust (just a small side project) I would advise you to not try to follow the standards to the letter, that is just a extreme pile of work that is not worth it. Otherwise really nice work. I attached the code with inline comments below, I prefixed the comments with Note: to make it easy to find them back. If you have questions about any of the comments feel free to reach out.

// Note: I always use clippy to warn me when code can be written in a nicer way.
// See: https://github.com/rust-lang/rust-clippy
#![warn(clippy::all, clippy::pedantic, clippy::nursery)] 

const MONTHS: [&str; 12] = [
    "January",
    "February",
    "March",
    "April",
    "May",
    "June",
    "July",
    "August",
    "September",
    "October",
    "November",
    "December",
];

pub struct Date {
    year: u16,
    month: usize,
    day: u16,
}

impl Date {
    /// Return a string such as "May 23" representing the date
    fn to_month_day(&self) -> String {
        format!("{} {}", MONTHS[self.month], self.day)
    }
}

pub struct Name {
    first: String, // Note: some cultures have a slightly different scheme with middle parts, this is commonly added to the last name so it will likely not be a problem, eg Dutch/Flemish: Erik van Huffelen (mind the casing!).
    last: String,
}

impl Name {
    // Note: I personally really like this pattern where you have a function/constructor that
    // takes a `impl into<String>` and calls `.into()`. In this way I very rarely have to think
    // about the exact string type any more as it will automatically do all the stuff. And
    // because it is Rust this will be just as fast as doing it 'by hand' (I assume, I have no
    // reason to believe otherwise).
    // This can of course be done just as easily for the Citation types. And `Into` is a very
    // prevalent trait that can be used in very many places.
    pub fn new(first: impl Into<String>, last: impl Into<String>) -> Self {
        Self {
            first: first.into(),
            last: last.into(),
        }
    }
}

/// Convert a vector of author names into a string listing them in Chicago style
fn list_authors(authors: &[Name]) -> String {
    // Note: I followed clippy here and replaced `&Vec<Name>` with `&[Name]` same explanation as `link_to_clickable` (line 80).
    match authors.len() {
        0 => panic!("No author given!"),
        1 => format!("{}, {}", authors[0].last, authors[0].first), // Note; I used direct indexing here into the array as the index is known and it takes less space visually, it is definitely not wrong to use `first` or `last`. I just want to show you some other ways ;-).
        2 => format!(
            "{}, {} and {} {}",
            authors[0].last, authors[0].first, authors[1].first, authors[1].last,
        ),
        _ => {
            let mut output = format!("{}, {}, ", authors[0].last, authors[0].first);
            for author in authors.iter().take(authors.len() - 1).skip(1) {
                // Note: clippy advised this, but I am not sure if I agree, make your own choice here.
                output.push_str(&format!("{} {}, ", author.first, author.last));
            }
            output.push_str(&format!(
                "and {} {}",
                authors.last().unwrap().first,
                authors.last().unwrap().last
            ));
            output
        }
    }
}

/// Convert "str" to "[str](https://str)", which renders as a link in markdown
fn link_to_clickable(url: &str) -> String {
    // Note: this way it is more general, any `&String` can automatically be dereferenced into `&str`.
    // Using `&String` is essentially the same from the borrow standpoint, it gives you read access to
    // a piece of text. Specifying `&String` makes it so that you cannot use this functions with a
    // `&'static str` or a slice/simple operation of a string, eg [`String::trim`] returns a `&str`.
    return format!("[{url}](https://{url})"); // Note: You can directly include variables in format strings.
}

// Note: As you a creating a shared behaviour for a couple of structs introducing a `trait` seems to me
// like a logical choice. You could also create a single `enum` with all options, but that gives a slightly
// different code architecture you might not like in certain cases. (Like not having the option for downstream
// users to implement the trait on their own structs.)
pub trait Citation {
    /// Generate a citation in Chicago style
    fn citation(&self) -> String;

    // Note: You could write an extra method which prepares the citation output for markdown.
    fn md_citation(&self) -> String {
        // Note: this is an example of course you would add more cases for the other special chars. Each
        // call to `replace` copies the whole string, if this is a problem you could look into a loop over
        // all chars concatenating everything with the special encoding in one round.
        self.citation().replace('-', "&ndash");

        // Note: here is an example of the single copy replacement.
        let citation = self.citation();
        let mut output = String::with_capacity(citation.len());
        for c in citation.chars() {
            match c {
                '-' => output += "&ndash;",
                n => output.push(n),
            }
        }
        output
    }
}

pub struct JournalArticle {
    authors: Vec<Name>,
    year: u16,
    title: String,
    journal: String,
    volume: u16,
    issue: u16,
    pages: (u16, u16),
    url: String,
}

impl Citation for JournalArticle {
    fn citation(&self) -> String {
        // Note: I tend to like 'one-liners' above introduced variables that are used only once. If needed the name of the variable being used as documentation can be replaced by a real comment ;-).
        format!(
            "{}. {}. &ldquo;{}.&rdquo; *{}* {} ({}): {}&ndash;{}. {}.",
            list_authors(&self.authors),
            self.year,
            self.title,
            self.journal,
            self.volume,
            self.issue,
            self.pages.0,
            self.pages.1,
            link_to_clickable(&self.url)
        ) // Note: here the return is not necessary
    }
}

pub struct NewspaperArticle {
    authors: Vec<Name>,
    date: Date,
    title: String,
    newspaper: String,
    url: String,
}

impl Citation for NewspaperArticle {
    fn citation(&self) -> String {
        format!(
            "{}. {}. &ldquo;{}.&rdquo; *{},* {}. {}.",
            list_authors(&self.authors),
            self.date.year,
            self.title,
            self.newspaper,
            self.date.to_month_day(),
            link_to_clickable(&self.url)
        )
    }
}

pub struct Book {
    authors: Vec<Name>,
    year: u16,
    title: String,
    publisher: String,
    location: String,
}

impl Book {
    pub fn new(
        authors: impl Into<Vec<Name>>,
        year: u16,
        title: impl Into<String>,
        publisher: impl Into<String>,
        location: impl Into<String>,
    ) -> Self {
        Self {
            authors: authors.into(),
            year,
            title: title.into(),
            publisher: publisher.into(),
            location: location.into(),
        }
    }
}

impl Citation for Book {
    fn citation(&self) -> String {
        format!(
            "{}. {}. *{}.* {}: {}.",
            list_authors(&self.authors),
            self.year,
            self.title,
            self.location,
            self.publisher,
        )
    }
}

fn main() {
    // Note: it is best to write these tests as unit tests so that reviewers (and you!) can be certain about
    // their refactorings. See below for how this is done with the book example.

    // Journal article with one author
    let source = JournalArticle {
        // Note: here you can see the impact of the `new` function with `impl Into<String>`. Additionally I
        // have an IDE which shows the names of parameters in-line once the definition becomes large, this
        // makes it so that the explicit names of the fields are not really necessary any more.
        authors: vec![Name::new("Aaron", "Bodoh&ndash;Creed")],
        year: 2020,
        title: String::from("Optimizing for Distributional Goals in School Choice Problems"),
        journal: String::from("Management Science"),
        volume: 66,
        issue: 8,
        pages: (3657, 3676),
        url: String::from("doi.org/10.1287/mnsc.2019.3376"),
    };

    println!("{}\n", source.citation());

    // Journal article with two authors
    let source = JournalArticle {
        authors: vec![
            Name {
                first: String::from("Paul"),
                last: String::from("Milgrom"),
            },
            Name {
                first: String::from("John"),
                last: String::from("Roberts"),
            },
        ],
        year: 1990,
        title: String::from(
            "The Economics of Modern Manufacturing: Technology, Strategy, and Organization",
        ),
        journal: String::from("The American Economic Review"),
        volume: 80,
        issue: 3,
        pages: (511, 528),
        url: String::from("jstor.org/stable/2006681"),
    };

    println!("{}\n", source.citation());

    // Journal article with three authors
    let source = JournalArticle{
        authors: vec![Name{
            first: String::from("Chandra"),
            last: String::from("Chekuri")
        }, Name{
            first: String::from("Jan"),
            last: String::from("Vondrák")
        }, Name{
            first: String::from("Rico"),
            last: String::from("Zenklusen")
        }],
        year: 2014,
        title: String::from("Submodular Function Maximization via the Multilinear Relaxation and Contention Resolution Schemes"),
        journal: String::from("SIAM Journal on Computing"),
        volume: 43,
        issue: 6,
        pages: (3657, 3676),
        url: String::from("doi.org/10.1137/110839655")
    };

    println!("{}\n", source.citation());

    // Newspaper article
    let source = NewspaperArticle {
        authors: vec![Name {
            first: String::from("Kathy Johnson"), // Middle names can be treated as an extension of first names
            last: String::from("Bowles"),
        }],
        date: Date {
            day: 24,
            month: 5,
            year: 2022,
        },
        title: String::from("Campus Landscapes: So Much More Than Marketing"),
        newspaper: String::from("Inside Higher Ed"),
        url: String::from(
            "insidehighered.com/blogs/just-explain-it-me/campus-landscapes-so-much-more-marketing",
        ),
    };

    println!("{}\n", source.citation());
}

#[cfg(test)] // Note: cfg is conditional compilation, so the compiler will not even look at the code in
             // this module when compiling in a normal mode, this improves the compiling performance somewhat.
mod tests {
    use super::*; // Note: this imports everything of the outer module (the one you are testing).

    #[test]
    fn book() {
        // Book
        // Note: arrays can be cast `Into` vecs!
        let source = Book::new(
            [
                Name::new("Thomas", "Cormen"),
                Name::new("Charles", "Leiserson"),
                Name::new("Ronald", "Rivest"),
            ],
            1996,
            "Introduction to Algorithms",
            "The MIT Press",
            "Cambridge, Massachusetts",
        );

        assert_eq!(
            source.citation(), 
            "Cormen, Thomas, Charles Leiserson, and Ronald Rivest. 1996. *Introduction to Algorithms.* Cambridge, Massachusetts: The MIT Press."
        );
    }
}
```
\$\endgroup\$
3
  • \$\begingroup\$ (Viewer discretion advised: assembly) I have tried the generated assembly for the impl Into<String> and String parameters and from what I can see (not very accustomed to reading/working with assembly) the generated code is exactly equal: rust.godbolt.org/z/eP7KEf8G9. So there should not be any performance reason to not go with this 'style'. \$\endgroup\$
    – Nonno
    May 26, 2022 at 0:29
  • 1
    \$\begingroup\$ Thank you so much! This was a really, really helpful review. I like replacing the vector of Names with a slices: it makes the input much more readable. \$\endgroup\$
    – Max
    May 28, 2022 at 5:18
  • \$\begingroup\$ s.push_str(&format!(/* ... */)) causes an additional allocation in format!. write!(&mut s, /* ... */) is more efficient (in theory). \$\endgroup\$
    – L. F.
    Jun 18, 2022 at 16:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.