dates_str/lib.rs
1//! dates_str - A date parser
2//!
3//! This crate, as it's name implies, it's not a "date & time" crate, but rather one to provide fast methods for handling datestrings:
4//! from formatting to more advanced features (TBI) as addition, subtraction or checking if a date is valid, to name a few.
5//!
6//! There's a lot of assumptions in this crate, such as when adding or substracting months have 30 days.
7//! Probably this coul be solved easily using a time crate, but I won't be checking that short-term.
8//!
9//! For full fledged date & time experiences, see:
10//! - [chrono](https://crates.io/crates/chrono)
11//! - [time](https://crates.io/crates/time)
12
13#![deny(missing_docs)]
14
15use std::fmt::Display;
16use std::vec::Vec;
17
18/// Tests
19#[cfg(test)]
20pub mod tests;
21
22/// Error module
23pub mod errors;
24
25/// Traits and implementations module
26pub mod impls;
27
28/// Allowed formatter options
29const FORMATTER_OPTIONS: [&str; 3] = ["YYYY", "MM", "DD"];
30
31// #[allow(dead_code)]
32// const EPOCH_DATE: &str = "1970-01-01";
33
34/// Max number for february month
35const MAX_DAY_FEBR: u8 = 29 as u8;
36
37/// The date struct
38///
39/// Months and years are *1-indexed*, meaning they start at ONE (1). So January would be 1, as
40/// written normally, and December is 12.
41///
42/// Called DateStr because it comes from a String
43#[derive(Debug, PartialEq, Eq)]
44pub struct DateStr {
45 /// An unsigned 64-bit integer to hold the year
46 year: Year,
47 /// An unsigned 8-bit integer to hold the month
48 month: Month,
49 /// An unsigned 8-bit integer to hold the day
50 day: Day,
51}
52
53impl DateStr {
54 /// Creates a new DateStr from the given parts
55 pub fn new(year: Year, month: Month, day: Day) -> Result<Self, errors::DateErrors> {
56 if month.0 != 2 && day.0 > 29 {
57 let err = errors::DateErrors::InvalidDay { day: day.0 };
58 return Err(err);
59 };
60 Ok(Self { year, month, day })
61 }
62}
63
64/// The `Day` struct. Holds a u8 because there's no 255 days.
65///
66/// On substractions it's value is casted to a i16 to allow for an ample range of negatives,
67/// and then casted to u8 again on construction.
68#[derive(Debug, Eq, PartialEq)]
69pub struct Day(u8);
70
71impl Day {
72 /// Returns a new `Day` struct, or an [Err] of [`DateErrors`](crate::errors::DateErrors) if it exceeds 31.
73 pub fn new(value: u8) -> Result<Self, errors::DateErrors> {
74 if !(1..=31).contains(&value) {
75 let err = errors::DateErrors::InvalidDay { day: value };
76 return Err(err);
77 };
78 Ok(Self(value))
79 }
80
81 #[allow(dead_code)]
82 fn new_unchecked(value: u8) -> Self {
83 Self(value)
84 }
85}
86
87impl Display for Day {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 write!(f, "{}", &self.0)
90 }
91}
92
93impl std::ops::Add for Day {
94 type Output = (Self, Month);
95 fn add(self, rhs: Self) -> Self::Output {
96 let mut sum = self.0 + rhs.0;
97 let mut mo = 0;
98 while sum > 30 {
99 mo = mo + 1;
100 sum = sum - 30;
101 }
102 (Self(sum), Month::new_unchecked(mo))
103 }
104}
105
106impl std::ops::Sub for Day {
107 type Output = (Self, Month);
108
109 fn sub(self, rhs: Self) -> Self::Output {
110 let mut sub = self.0 as i16 - rhs.0 as i16;
111 let mut mos = 0;
112
113 if sub > 0 {
114 return (Self(sub as u8), Month::new_unchecked(mos));
115 }
116
117 while sub * -1 > 30 {
118 mos = mos + 1;
119 sub = sub + 30;
120 }
121 (Self(sub as u8), Month::new_unchecked(mos))
122 }
123}
124
125/// The `Month` struct. Holds a u8 because there's just 12 months.
126#[derive(Debug, Eq, PartialEq)]
127pub struct Month(u8);
128
129impl Month {
130 /// Returns a new `Month` from a `u8`, or an error containing [`DateErrors`](crate::errors::DateErrors).
131 pub fn new(value: u8) -> Result<Self, errors::DateErrors> {
132 if !(1..=12).contains(&value) {
133 return Err(errors::DateErrors::InvalidMonth { month: value });
134 }
135 Ok(Self(value))
136 }
137
138 fn new_unchecked(value: u8) -> Self {
139 Self(value)
140 }
141}
142
143impl Display for Month {
144 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145 write!(f, "{}", &self.0)
146 }
147}
148
149impl std::ops::Add for Month {
150 type Output = (Self, Year);
151 fn add(self, rhs: Self) -> Self::Output {
152 let mut sum = self.0 + rhs.0;
153 let mut y2a: u64 = 0;
154 while sum > 12 {
155 y2a = y2a + 1;
156 sum = sum - 12;
157 }
158 (Self(sum), Year::new(y2a))
159 }
160}
161
162impl std::ops::Sub for Month {
163 type Output = (Self, Year);
164 fn sub(self, rhs: Self) -> Self::Output {
165 let mut sub = self.0 as i16 - rhs.0 as i16;
166 let mut yrs = 0;
167 if sub > 0 {
168 return (Self(sub as u8), Year::new(yrs));
169 }
170 sub = sub * (-1);
171 while sub > 12 {
172 yrs = yrs + 1;
173 sub = sub - 12;
174 }
175 (Self(sub as u8), Year::new(yrs))
176 }
177}
178
179/// The year struct. Holds a u64
180#[derive(Debug, Eq, PartialEq)]
181pub struct Year(u64);
182
183impl Year {
184 /// Creates a new `Year` from a number
185 pub fn new(value: u64) -> Self {
186 Self(value)
187 }
188}
189
190impl Display for Year {
191 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192 write!(f, "{}", &self.0)
193 }
194}
195
196impl std::ops::Add for Year {
197 type Output = Self;
198
199 fn add(self, rhs: Self) -> Self::Output {
200 Self(self.0 + rhs.0)
201 }
202}
203
204impl std::ops::Sub for Year {
205 type Output = Self;
206
207 fn sub(self, rhs: Self) -> Self::Output {
208 Self(self.0 - rhs.0)
209 }
210}
211
212/// The format a [DateStr] will be printed
213#[derive(Debug)]
214pub struct DateFormat {
215 /// The format to be used
216 pub formatter: String,
217}
218
219impl DateFormat {
220 /// Creates a DateFormat from String or a &str
221 ///
222 /// This method will try to create a [DateFormat] from any type that implements the ToString
223 /// type, although is mainly oriented to String and string slices.
224 ///
225 /// # Example:
226 /// ```rust
227 /// # use dates_str::DateFormat;
228 /// let format: DateFormat = DateFormat::from_string("YYYY-MM-DD", None).unwrap();
229 /// assert_eq!(format.formatter, "YYYY-MM-DD");
230 /// ```
231 /// Above code will create a new DateFormat object. If none is passed as separator, it defaults
232 /// to a dash ('-').
233 ///
234 /// # Example returning error:
235 /// ```rust
236 /// # use dates_str::{DateStr, DateFormat, errors::DateErrors};
237 /// let format: Result<DateFormat, DateErrors> = DateFormat::from_string("2020_10_20", Some('/'));
238 /// assert!(format.is_err());
239 /// ```
240 ///
241 /// When the separator is not explicitly specified, it will give an error if it's not a dash.
242 pub fn from_string<T: ToString>(
243 format: T,
244 separator: Option<char>,
245 ) -> Result<DateFormat, errors::DateErrors> {
246 let separator: char = separator.unwrap_or('-');
247 for fmt_opt in FORMATTER_OPTIONS {
248 if !format
249 .to_string()
250 .split(separator)
251 .any(|e| *e.to_uppercase() == *fmt_opt.to_string())
252 {
253 return Err(errors::DateErrors::FormatDateError);
254 }
255 }
256 Ok(DateFormat {
257 formatter: format.to_string().to_uppercase(),
258 })
259 }
260}
261
262impl DateStr {
263 /// Parse a string to a DateStr struct
264 ///
265 /// Parses a string (or any type implementing the [ToString] trait) to a DateStr struct.
266 ///
267 /// The given date must be in ISO-8601 format, that is: YYYY-MM-DD.
268 ///
269 /// I'd recommend using [crate::DateStr::try_from_iso_str] when unsure what the input string will be, since it
270 /// returns a Result with understandable errors.
271 ///
272 /// # Examples
273 /// ```rust
274 /// # use dates_str::DateStr;
275 /// let date_string: String = String::from("2022-12-31");
276 /// let new_date_from_string: DateStr = DateStr::from_iso_str(date_string);
277 /// let new_date_from_str: DateStr = DateStr::from_iso_str("2022-12-31");
278 /// assert_eq!(new_date_from_str, new_date_from_string);
279 /// ```
280 pub fn from_iso_str<T: ToString>(string: T) -> DateStr {
281 let sep_date: Vec<String> = string
282 .to_string()
283 .split('-')
284 .into_iter()
285 .map(|split| split.to_string())
286 .collect();
287 let year: Year = Year::new(sep_date[0].parse::<u64>().unwrap_or_default());
288 let month: Month = Month::new(sep_date[1].parse::<u8>().unwrap_or_default()).unwrap();
289 if !(1..=12).contains(&month.0) {
290 panic!("Month is out of bounds");
291 }
292 let day: Day = Day::new(sep_date[2].parse::<u8>().unwrap_or_default()).unwrap();
293 let (month_ok, day_ok): (bool, bool) = DateStr::check_date_constraints(month.0, day.0);
294 if !month_ok {
295 panic!("Month {} is out of bounds", month);
296 }
297 if !day_ok {
298 panic!("Day {} is out of bounds for month {}", day, month);
299 }
300 DateStr { year, month, day }
301 }
302
303 /// Checks if month and day are inside allowed range. Checks if day is within the months day
304 /// too.
305 ///
306 /// Checks if month is within 1 and 12. Depending on month checks day is within that month's
307 /// days. Returns a tuple with two bools: first is for the month, and second for the day.
308 fn check_date_constraints(month: u8, day: u8) -> (bool, bool) {
309 // TODO: improve this if .. else hell
310 if !(1..=12).contains(&month) {
311 return (false, false);
312 }
313 if month == 2 {
314 if !(1..=MAX_DAY_FEBR).contains(&day) {
315 (true, false)
316 } else {
317 (true, true)
318 }
319 } else if [1, 3, 5, 7, 8, 10, 12].contains(&month) {
320 if !(1..=31).contains(&day) {
321 (true, false)
322 } else {
323 (true, true)
324 }
325 } else if [4, 6, 9, 11].contains(&month) {
326 if !(1..31).contains(&day) {
327 (true, false)
328 } else {
329 (true, true)
330 }
331 } else {
332 (false, false)
333 }
334 }
335
336 /// Parse a string to a DateStr struct
337 ///
338 /// Parses a string (or any type implementing the [ToString] trait) to a DateStr struct. This
339 /// function returns a Result enum.
340 ///
341 /// The given date must be in ISO-8601 format, that is: YYYY-MM-DD.
342 ///
343 /// # Examples
344 /// ```rust
345 /// # use dates_str::DateStr;
346 /// # use dates_str::errors;
347 /// let date_string: String = String::from("2022-12-31");
348 /// let date_from_string: Result<DateStr, errors::DateErrors> = DateStr::try_from_iso_str(date_string);
349 /// assert!(date_from_string.is_ok());;
350 /// ```
351 ///
352 /// # Errors
353 /// Since it checks for month first, it will return a DateErrors::InvalidMonth even if the day
354 /// is wrong too, in wich it would return a DateErrors::InvalidDay.
355 pub fn try_from_iso_str<T: ToString>(string: T) -> Result<DateStr, errors::DateErrors> {
356 let sep_date: Vec<String> = string
357 .to_string()
358 .split('-')
359 .into_iter()
360 .map(|split| split.to_string())
361 .collect();
362 let year: u64 = sep_date[0].parse::<u64>().unwrap_or_default();
363 let month: u8 = sep_date[1].parse::<u8>().unwrap_or_default();
364 if !(1..=12).contains(&month) {
365 return Err(errors::DateErrors::InvalidMonth { month });
366 };
367 let day: u8 = sep_date[2].parse::<u8>().unwrap_or_default();
368 if !(1..=31).contains(&day) {
369 return Err(errors::DateErrors::InvalidDay { day });
370 };
371 Ok(DateStr {
372 year: Year::new(year),
373 month: Month::new(month).unwrap(),
374 day: Day::new(day).unwrap(),
375 })
376 }
377}
378
379/// Display trait implementation for DateStr
380///
381/// Prints the date in ISO-8601 format (YYYY-MM-DD)
382impl Display for DateStr {
383 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
384 write!(f, "{}-{:02}-{:02}", self.year, self.month, self.day)
385 }
386}
387
388impl DateStr {
389 /// Format the date with a [DateFormat]
390 ///
391 /// Pass a [DateFormat]. Will output a String with the date formatted how you wanted.
392 ///
393 /// Use [crate::DateStr::try_format] for easy error handling
394 ///
395 /// # Example
396 /// ```rust
397 /// # use dates_str::{DateStr, DateFormat};
398 /// let a_date: DateStr = DateStr::from_iso_str("2022-12-29");
399 /// let a_fmtr: DateFormat = DateFormat::from_string("dd_mm_yyyy", Some('_')).unwrap();
400 /// let formatted_date: String = a_date.format(a_fmtr);
401 /// println!("{}", formatted_date);
402 /// ```
403 /// Above code will output 29-12-2022.
404 ///
405 /// # Panics
406 /// This function will panic when an invalid [DateFormat] is passed.
407 ///
408 /// To use errors see [crate::DateStr::try_format()]
409 pub fn format(&self, fmt: DateFormat) -> String {
410 let self_fmtd: String = fmt
411 .formatter
412 .replace("YYYY", &self.year.to_string())
413 .replace("MM", &self.month.to_string())
414 .replace("DD", &self.day.to_string());
415 self_fmtd
416 }
417
418 /// Try to format the date with a custom formatter
419 ///
420 /// Safe function using the Result enum.
421 /// Receives a [DateFormat] struct.
422 ///
423 /// # Example:
424 /// ```rust
425 /// # use dates_str::{DateStr, DateFormat};
426 /// let a_date: DateStr = DateStr::from_iso_str("2022-12-29");
427 /// let some_formatter: DateFormat = DateFormat::from_string("dd-mm-yyyy", None).unwrap();
428 /// let formatted_date: String = a_date.try_format(some_formatter).unwrap();
429 /// println!("{}", formatted_date);
430 /// ```
431 /// Will output 29-12-2022
432 pub fn try_format(&self, fmt: DateFormat) -> Result<String, errors::DateErrors> {
433 let self_fmtd: String = fmt
434 .formatter
435 .replace("YYYY", &self.year.to_string())
436 .replace("MM", &self.month.to_string())
437 .replace("DD", &self.day.to_string());
438 Ok(self_fmtd)
439 }
440}