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}