pubmed_client/pubmed/query/
date.rs

1//! Date types and utilities for PubMed query date filtering
2
3/// Represents a date for PubMed searches with varying precision
4#[derive(Debug, Clone, PartialEq)]
5pub struct PubDate {
6    year: u32,
7    month: Option<u32>,
8    day: Option<u32>,
9}
10
11impl PubDate {
12    /// Create a new PubDate with year only
13    pub fn new(year: u32) -> Self {
14        Self {
15            year,
16            month: None,
17            day: None,
18        }
19    }
20
21    /// Create a new PubDate with year and month
22    pub fn with_month(year: u32, month: u32) -> Self {
23        Self {
24            year,
25            month: Some(month),
26            day: None,
27        }
28    }
29
30    /// Create a new PubDate with year, month, and day
31    pub fn with_day(year: u32, month: u32, day: u32) -> Self {
32        Self {
33            year,
34            month: Some(month),
35            day: Some(day),
36        }
37    }
38
39    /// Format as PubMed date string
40    pub fn to_pubmed_string(&self) -> String {
41        match (self.month, self.day) {
42            (Some(month), Some(day)) => format!("{}/{:02}/{:02}", self.year, month, day),
43            (Some(month), None) => format!("{}/{:02}", self.year, month),
44            _ => self.year.to_string(),
45        }
46    }
47}
48
49impl From<u32> for PubDate {
50    fn from(year: u32) -> Self {
51        Self::new(year)
52    }
53}
54
55impl From<(u32, u32)> for PubDate {
56    fn from((year, month): (u32, u32)) -> Self {
57        Self::with_month(year, month)
58    }
59}
60
61impl From<(u32, u32, u32)> for PubDate {
62    fn from((year, month, day): (u32, u32, u32)) -> Self {
63        Self::with_day(year, month, day)
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn test_pubdate_new() {
73        let date = PubDate::new(2023);
74        assert_eq!(date.to_pubmed_string(), "2023");
75    }
76
77    #[test]
78    fn test_pubdate_with_month() {
79        let date = PubDate::with_month(2023, 6);
80        assert_eq!(date.to_pubmed_string(), "2023/06");
81    }
82
83    #[test]
84    fn test_pubdate_with_day() {
85        let date = PubDate::with_day(2023, 6, 15);
86        assert_eq!(date.to_pubmed_string(), "2023/06/15");
87    }
88
89    #[test]
90    fn test_pubdate_from_u32() {
91        let date: PubDate = 2023.into();
92        assert_eq!(date.to_pubmed_string(), "2023");
93    }
94
95    #[test]
96    fn test_pubdate_from_tuple_month() {
97        let date: PubDate = (2023, 6).into();
98        assert_eq!(date.to_pubmed_string(), "2023/06");
99    }
100
101    #[test]
102    fn test_pubdate_from_tuple_day() {
103        let date: PubDate = (2023, 6, 15).into();
104        assert_eq!(date.to_pubmed_string(), "2023/06/15");
105    }
106
107    #[test]
108    fn test_pubdate_equality() {
109        let date1 = PubDate::new(2023);
110        let date2 = PubDate::new(2023);
111        let date3 = PubDate::new(2024);
112
113        assert_eq!(date1, date2);
114        assert_ne!(date1, date3);
115
116        let date_month1 = PubDate::with_month(2023, 6);
117        let date_month2 = PubDate::with_month(2023, 6);
118        let date_month3 = PubDate::with_month(2023, 7);
119
120        assert_eq!(date_month1, date_month2);
121        assert_ne!(date_month1, date_month3);
122        assert_ne!(date1, date_month1); // Different precision
123    }
124
125    #[test]
126    fn test_pubdate_clone() {
127        let original = PubDate::with_day(2023, 12, 25);
128        let cloned = original.clone();
129
130        assert_eq!(original, cloned);
131        assert_eq!(original.to_pubmed_string(), cloned.to_pubmed_string());
132    }
133
134    #[test]
135    fn test_pubdate_debug_format() {
136        let date = PubDate::with_month(2023, 6);
137        let debug_str = format!("{:?}", date);
138        assert!(debug_str.contains("2023"));
139        assert!(debug_str.contains("6"));
140    }
141
142    #[test]
143    fn test_month_padding() {
144        let date = PubDate::with_month(2023, 1);
145        assert_eq!(date.to_pubmed_string(), "2023/01");
146
147        let date = PubDate::with_month(2023, 12);
148        assert_eq!(date.to_pubmed_string(), "2023/12");
149    }
150
151    #[test]
152    fn test_day_padding() {
153        let date = PubDate::with_day(2023, 1, 5);
154        assert_eq!(date.to_pubmed_string(), "2023/01/05");
155
156        let date = PubDate::with_day(2023, 12, 25);
157        assert_eq!(date.to_pubmed_string(), "2023/12/25");
158    }
159
160    #[test]
161    fn test_edge_case_dates() {
162        // Test minimum values
163        let min_date = PubDate::with_day(1, 1, 1);
164        assert_eq!(min_date.to_pubmed_string(), "1/01/01");
165
166        // Test typical boundary dates
167        let leap_year = PubDate::with_day(2024, 2, 29);
168        assert_eq!(leap_year.to_pubmed_string(), "2024/02/29");
169
170        // Test far future date (used internally)
171        let future_date = PubDate::new(3000);
172        assert_eq!(future_date.to_pubmed_string(), "3000");
173    }
174
175    #[test]
176    fn test_date_precision_consistency() {
177        // Year only should not have month/day
178        let year_only = PubDate::new(2023);
179        assert_eq!(year_only.year, 2023);
180        assert_eq!(year_only.month, None);
181        assert_eq!(year_only.day, None);
182
183        // Month precision should not have day
184        let month_precision = PubDate::with_month(2023, 6);
185        assert_eq!(month_precision.year, 2023);
186        assert_eq!(month_precision.month, Some(6));
187        assert_eq!(month_precision.day, None);
188
189        // Day precision should have all fields
190        let day_precision = PubDate::with_day(2023, 6, 15);
191        assert_eq!(day_precision.year, 2023);
192        assert_eq!(day_precision.month, Some(6));
193        assert_eq!(day_precision.day, Some(15));
194    }
195
196    mod proptest_suite {
197        use super::*;
198        use proptest::prelude::*;
199
200        proptest! {
201            #[test]
202            fn year_only_format_is_all_digits(year in 1u32..=9999u32) {
203                let s = PubDate::new(year).to_pubmed_string();
204                prop_assert!(s.chars().all(|c| c.is_ascii_digit()), "year-only should be all digits: {}", s);
205                prop_assert!(!s.contains('/'), "year-only should not contain '/': {}", s);
206            }
207
208            #[test]
209            fn year_month_zero_pads_month(year in 1u32..=9999u32, month in 1u32..=12u32) {
210                let s = PubDate::with_month(year, month).to_pubmed_string();
211                let parts: Vec<&str> = s.split('/').collect();
212                prop_assert_eq!(parts.len(), 2, "expected YYYY/MM format: {}", s);
213                prop_assert_eq!(parts[1].len(), 2, "month should be zero-padded: {}", s);
214            }
215
216            #[test]
217            fn full_date_zero_pads_month_and_day(
218                year in 1u32..=9999u32,
219                month in 1u32..=12u32,
220                day in 1u32..=31u32,
221            ) {
222                let s = PubDate::with_day(year, month, day).to_pubmed_string();
223                let parts: Vec<&str> = s.split('/').collect();
224                prop_assert_eq!(parts.len(), 3, "expected YYYY/MM/DD format: {}", s);
225                prop_assert_eq!(parts[1].len(), 2, "month should be zero-padded: {}", s);
226                prop_assert_eq!(parts[2].len(), 2, "day should be zero-padded: {}", s);
227            }
228        }
229    }
230}