1use crate::{ArticleId, ArticleIdError, CategoryId};
2use jiff::civil::Date;
3use jiff::fmt::strtime::format as jiff_format;
4use jiff::fmt::strtime::parse as jiff_parse;
5use jiff::Error as JiffError;
6use std::error::Error;
7use std::fmt::{Display, Formatter, Result as FmtResult};
8
9pub type StampResult<'a> = Result<Stamp<'a>, StampError>;
11
12#[non_exhaustive]
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum StampError {
23 InvalidArxivId(ArticleIdError),
24 InvalidDate,
25 InvalidCategory,
26 NotEnoughComponents,
27}
28
29impl Error for StampError {}
30
31impl Display for StampError {
32 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
33 match self {
34 Self::InvalidArxivId(e) => write!(f, "Invalid arXiv ID: {e}"),
35 Self::InvalidDate => f.write_str("Invalid date"),
36 Self::InvalidCategory => f.write_str("Invalid category"),
37 Self::NotEnoughComponents => f.write_str("Not enough components"),
38 }
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
44pub struct Stamp<'a> {
45 pub id: ArticleId<'a>,
46 pub category: CategoryId<'a>,
47 pub submitted: Date,
48}
49
50impl<'a> Stamp<'a> {
51 #[inline]
65 pub const fn new(id: ArticleId<'a>, category: CategoryId<'a>, submitted: Date) -> Self {
66 Self {
67 id,
68 category,
69 submitted,
70 }
71 }
72}
73
74impl Display for Stamp<'_> {
75 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
76 write!(
77 f,
78 "{} [{}] {}",
79 self.id,
80 self.category,
81 jiff_format("%-e %b %Y", self.submitted).map_err(|_| core::fmt::Error)?
82 )
83 }
84}
85
86impl<'a> TryFrom<&'a str> for Stamp<'a> {
87 type Error = StampError;
88
89 fn try_from(s: &'a str) -> Result<Self, Self::Error> {
90 use StampError::*;
91
92 let wsp_indices: Vec<_> = s.match_indices(' ').collect();
93 if wsp_indices.len() < 2 {
94 return Err(NotEnoughComponents);
95 }
96
97 let space1 = wsp_indices[0].0;
99 let id = ArticleId::try_from(&s[0..space1]).map_err(InvalidArxivId)?;
100
101 let space2 = wsp_indices[1].0;
103 let cat_str = &s[space1 + 1..space2];
104 let category = CategoryId::parse_bracketed(cat_str).ok_or(InvalidCategory)?;
105
106 let date_str = &s[space2 + 1..];
108 let date = parse_date(date_str).map_err(|_| InvalidDate)?;
109
110 Ok(Self::new(id, category, date))
111 }
112}
113
114fn parse_date(date_str: &str) -> Result<Date, JiffError> {
119 jiff_parse("%e %b %Y", date_str)?.to_date()
120}
121
122#[cfg(test)]
123mod tests {
124 use crate::{Archive, ArticleId, CategoryId, Stamp};
125 use jiff::civil::date;
126
127 #[test]
128 fn display_stamp() {
129 let stamp = Stamp::new(
130 ArticleId::try_from("arXiv:2011.00001").unwrap(),
131 CategoryId::try_new(Archive::Cs, "LG").unwrap(),
132 date(2011, 1, 1),
133 );
134 assert_eq!(stamp.to_string(), "arXiv:2011.00001 [cs.LG] 1 Jan 2011");
135 }
136}
137
138#[cfg(test)]
139mod tests_parse_ok {
140 use crate::{Archive, ArticleId, CategoryId, Stamp};
141 use jiff::civil::date;
142
143 #[test]
144 fn parse_stamp() {
145 let stamp = "arXiv:2001.00001 [cs.LG] 1 Jan 2000";
146 let parsed = Stamp::try_from(stamp);
147 assert_eq!(
148 parsed,
149 Ok(Stamp::new(
150 ArticleId::try_from("arXiv:2001.00001").unwrap(),
151 CategoryId::try_new(Archive::Cs, "LG").unwrap(),
152 date(2000, 1, 1)
153 ))
154 );
155 }
156
157 #[test]
158 fn parse_stamp_readme() {
159 let stamp = "arXiv:0706.0001v1 [q-bio.CB] 1 Jun 2007";
160 let parsed = Stamp::try_from(stamp);
161
162 assert_eq!(
163 parsed,
164 Ok(Stamp::new(
165 ArticleId::try_from("arXiv:0706.0001v1").unwrap(),
166 CategoryId::try_new(Archive::QBio, "CB").unwrap(),
167 date(2007, 6, 1)
168 ))
169 )
170 }
171}
172
173#[cfg(test)]
174mod tests_parse_err {
175 use crate::{Stamp, StampError};
176
177 #[test]
178 fn is_empty() {
179 let stamp = "";
180 let parsed = Stamp::try_from(stamp);
181
182 assert_eq!(parsed, Err(StampError::NotEnoughComponents));
183 }
184
185 #[test]
186 fn not_enough_components() {
187 let stamp = "arXiv:2001.00001";
188 let parsed = Stamp::try_from(stamp);
189
190 assert_eq!(parsed, Err(StampError::NotEnoughComponents));
191 }
192
193 #[test]
194 fn invalid_category() {
195 let stamp = "arXiv:2001.00001 [cs.LG 1 Jan 2000";
196 let parsed = Stamp::try_from(stamp);
197
198 assert_eq!(parsed, Err(StampError::InvalidCategory));
199 }
200
201 #[test]
202 fn invalid_date_day() {
203 let stamp = "arXiv:2001.00001 [cs.LG] 32 Jan 2000";
204 let parsed = Stamp::try_from(stamp);
205
206 assert_eq!(parsed, Err(StampError::InvalidDate));
207 }
208
209 #[test]
210 fn invalid_date_month() {
211 let stamp = "arXiv:2001.00001 [cs.LG] 1 Zan 2000";
212 let parsed = Stamp::try_from(stamp);
213
214 assert_eq!(parsed, Err(StampError::InvalidDate));
215 }
216}