bookdata/
gender.rs

1//! Code for working with genders.
2use std::fmt;
3use std::str::FromStr;
4
5use thiserror::Error;
6
7#[derive(Error, Debug)]
8pub enum GenderError {
9    #[error("could not parse gender from string")]
10    #[allow(dead_code)]
11    ParseError,
12}
13
14/// A gender representation.
15#[derive(Debug, PartialEq, Eq, Hash, Clone)]
16pub enum Gender {
17    Unknown,
18    Ambiguous,
19    Female,
20    Male,
21    Open(String),
22}
23
24/// A collection of genders.
25#[derive(Default, Debug)]
26pub struct GenderBag {
27    size: usize,
28    mask: u32,
29    resolved: Gender,
30}
31
32impl Default for Gender {
33    fn default() -> Self {
34        Gender::Unknown
35    }
36}
37
38impl FromStr for Gender {
39    type Err = GenderError;
40
41    fn from_str(s: &str) -> Result<Gender, GenderError> {
42        Ok(s.into())
43    }
44}
45
46impl From<&str> for Gender {
47    fn from(s: &str) -> Gender {
48        let sg = s.trim().to_lowercase();
49        match sg.as_str() {
50            "unknown" => Gender::Unknown,
51            "ambiguous" => Gender::Ambiguous,
52            "female" => Gender::Female,
53            "male" => Gender::Male,
54            _ => Gender::Open(sg),
55        }
56    }
57}
58
59impl From<String> for Gender {
60    fn from(s: String) -> Gender {
61        s.as_str().into()
62    }
63}
64
65impl<T> From<&T> for Gender
66where
67    T: Into<Gender> + Clone,
68{
69    fn from(obj: &T) -> Gender {
70        obj.clone().into()
71    }
72}
73
74impl fmt::Display for Gender {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        match self {
77            Gender::Unknown => f.write_str("unknown"),
78            Gender::Ambiguous => f.write_str("ambiguous"),
79            Gender::Female => f.write_str("female"),
80            Gender::Male => f.write_str("male"),
81            Gender::Open(s) => f.write_str(s.as_str()),
82        }
83    }
84}
85
86impl Gender {
87    /// Merge a gender record with another.  If the two records
88    /// disagree, the result is [Gender::Ambiguous].
89    pub fn merge(&self, other: &Gender) -> Gender {
90        match (self, other) {
91            (Gender::Unknown, g) => g.clone(),
92            (g, Gender::Unknown) => g.clone(),
93            (g1, g2) if g1 == g2 => g2.clone(),
94            _ => Gender::Ambiguous,
95        }
96    }
97
98    /// Get an integer usable as a mask for this gender.
99    fn mask_val(&self) -> u32 {
100        match self {
101            Gender::Unknown => 1,
102            Gender::Ambiguous => 2,
103            Gender::Female => 4,
104            Gender::Male => 8,
105            Gender::Open(_) => 0x8000_0000,
106        }
107    }
108}
109
110impl GenderBag {
111    /// Add a gender to this bag.
112    pub fn add(&mut self, gender: Gender) {
113        self.size += 1;
114        self.mask |= gender.mask_val();
115        self.resolved = self.resolved.merge(&gender);
116    }
117
118    /// Merge another gender bag into this one.
119    pub fn merge_from(&mut self, bag: &GenderBag) {
120        self.size += bag.size;
121        self.mask |= bag.mask;
122        self.resolved = self.resolved.merge(&bag.resolved);
123    }
124
125    /// Get the number of gender entries recorded to make this gender record.
126    #[allow(dead_code)]
127    pub fn len(&self) -> usize {
128        self.size
129    }
130
131    /// Check whether the gender bag is empty.
132    pub fn is_empty(&self) -> bool {
133        self.size == 0
134    }
135
136    /// Get the gender, returning [None] if no genders are seen.
137    #[allow(dead_code)]
138    pub fn maybe_gender(&self) -> Option<&Gender> {
139        if self.is_empty() {
140            None
141        } else {
142            Some(&self.resolved)
143        }
144    }
145
146    /// Get the gender, returning [Gender::Unknown] if no genders are seen.
147    pub fn to_gender(&self) -> &Gender {
148        &self.resolved
149    }
150}
151
152#[test]
153pub fn test_bag_empty() {
154    let bag = GenderBag::default();
155    assert_eq!(bag.to_gender(), &Gender::Unknown);
156}
157
158#[test]
159pub fn test_bag_female() {
160    let mut bag = GenderBag::default();
161    bag.add("female".into());
162    assert_eq!(bag.to_gender(), &Gender::Female);
163}
164
165#[test]
166pub fn test_bag_male() {
167    let mut bag = GenderBag::default();
168    bag.add("male".into());
169    assert_eq!(bag.to_gender(), &Gender::Male);
170}
171
172#[test]
173pub fn test_bag_mf() {
174    let mut bag = GenderBag::default();
175    bag.add("male".into());
176    bag.add("female".into());
177    assert_eq!(bag.to_gender(), &Gender::Ambiguous);
178}
179
180#[test]
181pub fn test_bag_ff() {
182    let mut bag = GenderBag::default();
183    bag.add("female".into());
184    bag.add("female".into());
185    assert_eq!(bag.to_gender(), &Gender::Female);
186}