repo_description: Command-line Git information tool
repo_topics: cli command-line command-line-interface git rust tool
code: use super::git::sig::Sig;
  use crate::{
      cli::NumberSeparator,
      info::utils::{format_number, info_field::InfoField},
  };
  use serde::Serialize;
  use std::{collections::HashMap, fmt::Write};
  
  #[derive(Serialize, Clone, Debug, PartialEq)]
  #[serde(rename_all = "camelCase")]
  pub struct Author {
      pub name: String,
      email: Option<String>,
      nbr_of_commits: usize,
      contribution: usize,
      #[serde(skip_serializing)]
      number_separator: NumberSeparator,
  }
  
  impl Author {
      pub fn new(
          name: String,
          email: Option<String>,
          nbr_of_commits: usize,
          total_nbr_of_commits: usize,
          number_separator: NumberSeparator,
      ) -> Self {
          let contribution =
              (nbr_of_commits as f32 * 100. / total_nbr_of_commits as f32).round() as usize;
          Self {
              name,
              email,
              nbr_of_commits,
              contribution,
              number_separator,
          }
      }
  }
  
  impl std::fmt::Display for Author {
      fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
          if let Some(email) = &self.email {
              write!(
                  f,
                  "{}% {} <{}> {}",
                  self.contribution,
                  self.name,
                  email,
                  format_number(&self.nbr_of_commits, self.number_separator)
              )
          } else {
              write!(
                  f,
                  "{}% {} {}",
                  self.contribution,
                  self.name,
                  format_number(&self.nbr_of_commits, self.number_separator)
              )
          }
      }
  }
  
  #[derive(Serialize)]
  pub struct AuthorsInfo {
      pub authors: Vec<Author>,
  }
  
  impl AuthorsInfo {
      pub fn new(
          number_of_commits_by_signature: &HashMap<Sig, usize>,
          total_number_of_commits: usize,
          number_of_authors_to_display: usize,
          show_email: bool,
          number_separator: NumberSeparator,
      ) -> Self {
          let authors = compute_authors(
              number_of_commits_by_signature,
              total_number_of_commits,
              number_of_authors_to_display,
              show_email,
              number_separator,
          );
          Self { authors }
      }
  
      fn top_contribution(&self) -> usize {
          if let Some(top_contributor) = self.authors.first() {
              return top_contributor.contribution;
          }
          0
      }
  }
  
  fn compute_authors(
      number_of_commits_by_signature: &HashMap<Sig, usize>,
      total_number_of_commits: usize,
      number_of_authors_to_display: usize,
      show_email: bool,
      number_separator: NumberSeparator,
  ) -> Vec<Author> {
      let mut signature_with_number_of_commits_sorted: Vec<(&Sig, &usize)> =
          Vec::from_iter(number_of_commits_by_signature);
  
      signature_with_number_of_commits_sorted.sort_by(|(sa, a_count), (sb, b_count)| {
          b_count.cmp(a_count).then_with(|| sa.name.cmp(&sb.name))
      });
  
      let authors: Vec<Author> = signature_with_number_of_commits_sorted
          .into_iter()
          .map(|(author, author_nbr_of_commits)| {
              Author::new(
                  author.name.to_string(),
                  if show_email {
                      Some(author.email.to_string())
                  } else {
                      None
                  },
                  *author_nbr_of_commits,
                  total_number_of_commits,
                  number_separator,
              )
          })
          .take(number_of_authors_to_display)
          .collect();
      authors
  }
  
  fn digit_difference(num1: usize, num2: usize) -> usize {
      let count_digits = |num: usize| (num.checked_ilog10().unwrap_or(0) + 1) as usize;
      count_digits(num1).abs_diff(count_digits(num2))
  }
  
  impl std::fmt::Display for AuthorsInfo {
      fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
          let mut authors_info = String::new();
  
          let pad = self.title().len() + 2;
          for (i, author) in self.authors.iter().enumerate() {
              if i == 0 {
                  write!(authors_info, "{author}")?;
              } else {
                  write!(
                      authors_info,
                      "\n{:<width$}{}",
                      "",
                      author,
                      width = pad + digit_difference(self.top_contribution(), author.contribution)
                  )?;
              }
          }
  
          write!(f, "{authors_info}")
      }
  }
  
  #[typetag::serialize]
  impl InfoField for AuthorsInfo {
      fn value(&self) -> String {
          self.to_string()
      }
  
      fn title(&self) -> String {
          let mut title: String = "Author".into();
          if self.authors.len() > 1 {
              title.push('s');
          }
          title
      }
  }
  
  #[cfg(test)]
  mod test {
      use super::*;
      use crate::ui::text_colors::TextColors;
      use insta::assert_snapshot;
      use owo_colors::DynColors;
      use rstest::rstest;
  
      #[test]
      fn test_display_author() {
          let author = Author::new(
              "John Doe".into(),
              Some("[REDACTED_EMAIL]".into()),
              1500,
              2000,
              NumberSeparator::Plain,
          );
  
          assert_eq!(author.to_string(), "75% John Doe <[REDACTED_EMAIL]> 1500");
      }
  
      #[test]
      fn test_display_author_with_no_email() {
          let author = Author::new("John Doe".into(), None, 1500, 2000, NumberSeparator::Plain);
  
          assert_eq!(author.to_string(), "75% John Doe 1500");
      }
  
      #[test]
      fn test_authors_info_title_with_one_author() {
          let author = Author::new(
              "John Doe".into(),
              Some("[REDACTED_EMAIL]".into()),
              1500,
              2000,
              NumberSeparator::Plain,
          );
  
          let authors_info = AuthorsInfo {
              authors: vec![author],
          };
  
          assert_eq!(authors_info.title(), "Author");
      }
  
      #[test]
      fn test_authors_info_title_with_two_authors() {
          let author = Author::new(
              "John Doe".into(),
              Some("[REDACTED_EMAIL]".into()),
              1500,
              2000,
              NumberSeparator::Plain,
          );
  
          let author_2 = Author::new(
              "Roberto Berto".into(),
              None,
              240,
              300,
              NumberSeparator::Plain,
          );
  
          let authors_info = AuthorsInfo {
              authors: vec![author, author_2],
          };
  
          assert_eq!(authors_info.title(), "Authors");
      }
  
      #[test]
      fn test_author_info_with_one_author() {
          let author = Author::new(
              "John Doe".into(),
              Some("[REDACTED_EMAIL]".into()),
              1500,
              2000,
              NumberSeparator::Plain,
          );
  
          let authors_info = AuthorsInfo {
              authors: vec![author],
          };
          let colors = TextColors::new(&[], DynColors::Rgb(0xFF, 0xFF, 0xFF));
          let mut buffer = String::new();
          authors_info
              .write_styled(&mut buffer, false, &colors)
              .unwrap();
  
          assert_snapshot!(buffer);
      }
  
      #[test]
      fn test_author_info_with_two_authors() {
          let author = Author::new(
              "John Doe".into(),
              Some("[REDACTED_EMAIL]".into()),
              1500,
              2000,
              NumberSeparator::Plain,
          );
  
          let author_2 = Author::new(
              "Roberto Berto".into(),
              None,
              240,
              300,
              NumberSeparator::Plain,
          );
  
          let authors_info = AuthorsInfo {
              authors: vec![author, author_2],
          };
  
          let colors = TextColors::new(&[], DynColors::Rgb(0xFF, 0xFF, 0xFF));
          let mut buffer = String::new();
          authors_info
              .write_styled(&mut buffer, false, &colors)
              .unwrap();
  
          assert_snapshot!(buffer);
      }
      #[test]
      fn test_author_info_alignment_with_three_authors() {
          let author = Author::new(
              "John Doe".into(),
              Some("[REDACTED_EMAIL]".into()),
              1500,
              2000,
              NumberSeparator::Plain,
          );
  
          let author_2 = Author::new(
              "Roberto Berto".into(),
              None,
              240,
              300,
              NumberSeparator::Plain,
          );
  
          let author_3 = Author::new("Jane Doe".into(), None, 1, 100, NumberSeparator::Plain);
  
          let authors_info = AuthorsInfo {
              authors: vec![author, author_2, author_3],
          };
  
          let colors = TextColors::new(&[], DynColors::Rgb(0xFF, 0xFF, 0xFF));
          let mut buffer = String::new();
          authors_info
              .write_styled(&mut buffer, false, &colors)
              .unwrap();
  
          assert_snapshot!(buffer);
      }
  
      #[rstest]
      #[case(456, 123, 0)]
      #[case(456789, 123, 3)]
      #[case(1, 12, 1)]
      fn test_digit_difference(#[case] num1: usize, #[case] num2: usize, #[case] expected: usize) {
          let result = digit_difference(num1, num2);
          assert_eq!(result, expected);
      }
  
      #[test]
      fn test_compute_authors() {
          let mut number_of_commits_by_signature: HashMap<Sig, usize> = HashMap::new();
          number_of_commits_by_signature.insert(
              Sig {
                  name: "John Doe".into(),
                  email: "johndoe@example.com".into(),
              },
              30,
          );
          number_of_commits_by_signature.insert(
              Sig {
                  name: "Jane Doe".into(),
                  email: "janedoe@example.com".into(),
              },
              20,
          );
          number_of_commits_by_signature.insert(
              Sig {
                  name: "Ellen Smith".into(),
                  email: "ellensmith@example.com".into(),
              },
              50,
          );
          let total_number_of_commits = 100;
          let number_of_authors_to_display = 2;
          let show_email = false;
          let number_separator = NumberSeparator::Comma;
  
          let actual = compute_authors(
              &number_of_commits_by_signature,
              total_number_of_commits,
              number_of_authors_to_display,
              show_email,
              number_separator,
          );
  
          let expected = vec![
              Author::new(String::from("Ellen Smith"), None, 50, 100, number_separator),
              Author::new(String::from("John Doe"), None, 30, 100, number_separator),
          ];
          assert_eq!(actual, expected);
      }
  }
  