koyomi/
date.rs

1//! # 日付
2//!
3//! カレンダーとして必要な情報を持つ。
4//!
5//! - 年
6//! - 月
7//! - 日
8//! - 曜日
9//! - 和暦
10//! - 祝祭日
11use std::cmp::Ordering;
12use std::fmt;
13
14use chrono::{Datelike, NaiveDate, Weekday as ChronoWeekday};
15
16use self::Weekday::*;
17use super::{KoyomiError, KoyomiResult};
18use crate::era;
19use crate::holiday;
20
21/// 曜日
22///
23/// 月曜を週の始まりとする。
24/// [`chrono::Weekday`]から生成することもできる。
25///
26/// [chrono::Weekday]: https://docs.rs/chrono/0.4.0/chrono/enum.Weekday.html
27#[derive(Clone, Debug, Eq, PartialEq)]
28pub enum Weekday {
29    Monday,
30    Tuesday,
31    Wednesday,
32    Thursday,
33    Friday,
34    Saturday,
35    Sunday,
36}
37
38impl Weekday {
39    /// 曜日の日本語表現を返す
40    ///
41    /// # Examples
42    ///
43    /// ```rust
44    /// use koyomi::Weekday;
45    ///
46    /// let w = Weekday::Monday;
47    /// assert_eq!(w.japanese(), '月');
48    /// ```
49    pub fn japanese(&self) -> char {
50        match *self {
51            Monday => '月',
52            Tuesday => '火',
53            Wednesday => '水',
54            Thursday => '木',
55            Friday => '金',
56            Saturday => '土',
57            Sunday => '日',
58        }
59    }
60}
61
62impl From<ChronoWeekday> for Weekday {
63    /// [`chrono::Weekday`]から対応する曜日を生成する
64    ///
65    /// [`chrono::Weekday`]: https://docs.rs/chrono/0.4.0/chrono/enum.Weekday.html
66    fn from(weekday: ChronoWeekday) -> Self {
67        match weekday {
68            ChronoWeekday::Mon => Monday,
69            ChronoWeekday::Tue => Tuesday,
70            ChronoWeekday::Wed => Wednesday,
71            ChronoWeekday::Thu => Thursday,
72            ChronoWeekday::Fri => Friday,
73            ChronoWeekday::Sat => Saturday,
74            ChronoWeekday::Sun => Sunday,
75        }
76    }
77}
78
79/// 日付
80///
81/// カレンダーのベースとなる構造体。
82/// 文字列または(年, 月, 日)から生成する。
83#[derive(Clone, Debug, Eq, PartialEq)]
84pub struct Date {
85    year: i32,
86    month: u32,
87    day: u32,
88    weekday: Weekday,
89}
90
91impl Date {
92    /// 文字列からオブジェクトを生成する
93    /// 文字列は `Y-m-d` か `Y/m/d` 形式のみ受け付ける
94    ///
95    /// # Examples
96    ///
97    /// ```rust
98    /// use koyomi::Date;
99    ///
100    /// let date = Date::parse("2018-01-01");
101    /// assert!(date.is_ok());
102    ///
103    /// let date = Date::parse("2018/01/01");
104    /// assert!(date.is_ok());
105    ///
106    /// let date = Date::parse("2018-01-32");
107    /// assert!(date.is_err());
108    /// ```
109    pub fn parse(fmt: &str) -> KoyomiResult<Self> {
110        NaiveDate::parse_from_str(fmt, "%Y-%m-%d")
111            .or(NaiveDate::parse_from_str(fmt, "%Y/%m/%d"))
112            .map_err(|_| KoyomiError::InvalidFormat(fmt.into()))
113            .map(|d| Date::from(d))
114    }
115
116    /// 年月日からオブジェクトを生成する
117    ///
118    /// # Examples
119    ///
120    /// ```rust
121    /// use koyomi::Date;
122    ///
123    /// let date = Date::from_ymd(2018, 1, 1);
124    /// assert!(date.is_ok());
125    ///
126    /// let date = Date::from_ymd(2018, 13, 1);
127    /// assert!(date.is_err());
128    /// ```
129    pub fn from_ymd(year: i32, month: u32, day: u32) -> KoyomiResult<Self> {
130        let ymd = format!("{:<4}-{:<02}-{:<02}", year, month, day);
131        Date::parse(&ymd)
132    }
133
134    /// 「日」を返す
135    ///
136    /// # Examples
137    ///
138    /// ```rust
139    /// use koyomi::Date;
140    ///
141    /// let date = Date::from_ymd(2018, 1, 31).unwrap();
142    /// assert_eq!(date.day(), 31);
143    /// ```
144    pub fn day(&self) -> u32 {
145        self.day
146    }
147
148    /// 「元号」を返す
149    ///
150    /// # Examples
151    ///
152    /// ```rust
153    /// use koyomi::Date;
154    ///
155    /// let date = Date::from_ymd(2018, 1, 1).unwrap();
156    /// assert_eq!(date.era().unwrap().name(), "平成");
157    /// ```
158    pub fn era(&self) -> Option<era::Era> {
159        era::era(self)
160    }
161
162    /// 「祝祭日」を返す
163    ///
164    /// # Examples
165    ///
166    /// ```rust
167    /// use koyomi::Date;
168    ///
169    /// let date = Date::from_ymd(2018, 1, 1).unwrap();
170    /// assert_eq!(date.holiday().unwrap(), "元日");
171    ///
172    /// let date = Date::from_ymd(2018, 1, 2).unwrap();
173    /// assert_eq!(date.holiday(), None);
174    /// ```
175    pub fn holiday(&self) -> Option<String> {
176        holiday::holiday(self)
177    }
178
179    /// 「月」を返す
180    ///
181    /// # Examples
182    ///
183    /// ```rust
184    /// use koyomi::Date;
185    ///
186    /// let date = Date::from_ymd(2018, 12, 1).unwrap();
187    /// assert_eq!(date.month(), 12);
188    /// ```
189    pub fn month(&self) -> u32 {
190        self.month
191    }
192
193    /// 日付間の期間が何日あるかを返す
194    ///
195    /// # Examples
196    ///
197    /// ```rust
198    /// use koyomi::Date;
199    ///
200    /// let from = Date::from_ymd(2018, 1, 1).unwrap();
201    /// let until = Date::from_ymd(2018, 12, 31).unwrap();
202    /// assert_eq!(until.num_days(&from), 364);
203    ///
204    /// // うるう年
205    /// let from = Date::from_ymd(2016, 1, 1).unwrap();
206    /// let until = Date::from_ymd(2016, 12, 31).unwrap();
207    /// assert_eq!(until.num_days(&from), 365);
208    /// ```
209    pub fn num_days(&self, date: &Date) -> i64 {
210        let min = NaiveDate::from_ymd(self.year, self.month, self.day);
211        let sub = NaiveDate::from_ymd(date.year(), date.month(), date.day());
212        min.signed_duration_since(sub).num_days()
213    }
214
215    /// 日付の文字列表現を返す
216    /// フォーマットは `Y-m-d` 形式となる
217    ///
218    /// # Examples
219    ///
220    /// ```rust
221    /// use koyomi::Date;
222    ///
223    /// let date = Date::from_ymd(2018, 1, 1).unwrap();
224    /// assert_eq!(date.to_string(), "2018-01-01");
225    /// ```
226    pub fn to_string(&self) -> String {
227        format!("{:<4}-{:<02}-{:<02}", self.year, self.month, self.day)
228    }
229
230    /// 翌日の日付を返す
231    ///
232    /// # Examples
233    ///
234    /// ```rust
235    /// use koyomi::Date;
236    ///
237    /// let date = Date::from_ymd(2018, 1, 1).unwrap();
238    /// let tomorrow = date.tomorrow();
239    /// assert_eq!(tomorrow.unwrap().to_string(), "2018-01-02");
240    /// ```
241    pub fn tomorrow(&self) -> KoyomiResult<Self> {
242        NaiveDate::from_ymd(self.year, self.month, self.day)
243            .succ_opt()
244            .ok_or(KoyomiError::NoTomorrow(self.year, self.month, self.day))
245            .map(|d| Date::from(d))
246    }
247
248    /// 「曜日」を返す
249    ///
250    /// # Examples
251    ///
252    /// ```rust
253    /// use koyomi::{Date, Weekday};
254    ///
255    /// let date = Date::from_ymd(2018, 1, 1).unwrap();
256    /// assert_eq!(date.weekday(), &Weekday::Monday);
257    /// ```
258    pub fn weekday(&self) -> &Weekday {
259        &self.weekday
260    }
261
262    /// 「年」を返す
263    ///
264    /// # Examples
265    ///
266    /// ```rust
267    /// use koyomi::Date;
268    ///
269    /// let date = Date::from_ymd(2018, 1, 1).unwrap();
270    /// assert_eq!(date.year(), 2018);
271    /// ```
272    pub fn year(&self) -> i32 {
273        self.year
274    }
275
276    /// 前日の日付を返す
277    ///
278    /// # Examples
279    ///
280    /// ```rust
281    /// use koyomi::Date;
282    ///
283    /// let date = Date::from_ymd(2018, 1, 1).unwrap();
284    /// let yesterday = date.yesterday();
285    /// assert_eq!(yesterday.unwrap().to_string(), "2017-12-31");
286    /// ```
287    pub fn yesterday(&self) -> KoyomiResult<Self> {
288        NaiveDate::from_ymd(self.year, self.month, self.day)
289            .pred_opt()
290            .ok_or(KoyomiError::NoYesterday(self.year, self.month, self.day))
291            .map(|d| Date::from(d))
292    }
293}
294
295impl From<NaiveDate> for Date {
296    fn from(date: NaiveDate) -> Self {
297        Date {
298            year: date.year(),
299            month: date.month(),
300            day: date.day(),
301            weekday: Weekday::from(date.weekday()),
302        }
303    }
304}
305
306impl fmt::Display for Date {
307    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
308        write!(f, "{}", self.to_string())
309    }
310}
311
312impl Ord for Date {
313    /// 日付オブジェクト同士を比較可能にする
314    fn cmp(&self, other: &Date) -> Ordering {
315        match self.year.cmp(&other.year) {
316            Ordering::Equal => match self.month.cmp(&other.month) {
317                Ordering::Equal => self.day.cmp(&other.day),
318                ord => ord,
319            },
320            ord => ord,
321        }
322    }
323}
324
325impl PartialOrd for Date {
326    /// 日付オブジェクト同士を比較可能にする
327    fn partial_cmp(&self, other: &Date) -> Option<Ordering> {
328        Some(self.cmp(&other))
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335    use chrono::naive::{MAX_DATE, MIN_DATE};
336
337    #[test]
338    fn parse_hyphen_format() {
339        assert!(Date::parse("2018-01-01").is_ok());
340    }
341
342    #[test]
343    fn parse_slash_format() {
344        assert!(Date::parse("2018/01/01").is_ok());
345    }
346
347    #[test]
348    fn parse_invalid_format() {
349        assert!(Date::parse("2018 01 01").is_err());
350    }
351
352    #[test]
353    fn valid_ymd() {
354        assert!(Date::from_ymd(2018, 1, 1).is_ok());
355    }
356
357    #[test]
358    fn invalid_ymd() {
359        assert!(Date::from_ymd(2018, 13, 1).is_err());
360    }
361
362    #[test]
363    fn ymd_of_date() {
364        let date = Date::parse("2018-12-24").unwrap();
365        assert_eq!(date.year(), 2018);
366        assert_eq!(date.month(), 12);
367        assert_eq!(date.day(), 24);
368    }
369
370    #[test]
371    fn era_of_date() {
372        let date = Date::parse("2018-12-24").unwrap();
373        assert_eq!(date.era().unwrap().name(), "平成");
374    }
375
376    #[test]
377    fn holiday_of_date() {
378        let date = Date::parse("2018-12-23").unwrap();
379        assert_eq!(date.holiday().unwrap(), "天皇誕生日");
380    }
381
382    #[test]
383    fn monday_of_weekday() {
384        let weekday = Weekday::from(ChronoWeekday::Mon);
385        assert_eq!(weekday, Monday);
386        assert_eq!(weekday.japanese(), '月');
387    }
388
389    #[test]
390    fn tuesday_of_weekday() {
391        let weekday = Weekday::from(ChronoWeekday::Tue);
392        assert_eq!(weekday, Tuesday);
393        assert_eq!(weekday.japanese(), '火');
394    }
395
396    #[test]
397    fn wednesday_of_weekday() {
398        let weekday = Weekday::from(ChronoWeekday::Wed);
399        assert_eq!(weekday, Wednesday);
400        assert_eq!(weekday.japanese(), '水');
401    }
402
403    #[test]
404    fn thursday_of_weekday() {
405        let weekday = Weekday::from(ChronoWeekday::Thu);
406        assert_eq!(weekday, Thursday);
407        assert_eq!(weekday.japanese(), '木');
408    }
409
410    #[test]
411    fn friday_of_weekday() {
412        let weekday = Weekday::from(ChronoWeekday::Fri);
413        assert_eq!(weekday, Friday);
414        assert_eq!(weekday.japanese(), '金');
415    }
416
417    #[test]
418    fn saturday_of_weekday() {
419        let weekday = Weekday::from(ChronoWeekday::Sat);
420        assert_eq!(weekday, Saturday);
421        assert_eq!(weekday.japanese(), '土');
422    }
423
424    #[test]
425    fn sunday_of_weekday() {
426        let weekday = Weekday::from(ChronoWeekday::Sun);
427        assert_eq!(weekday, Sunday);
428        assert_eq!(weekday.japanese(), '日');
429    }
430
431    #[test]
432    fn valid_tomorrow() {
433        let date = Date::parse("2018-12-24").unwrap();
434        assert!(date.tomorrow().is_ok());
435    }
436
437    #[test]
438    fn invalid_tomorrow() {
439        let date = Date::parse(&MAX_DATE.format("%Y-%m-%d").to_string()).unwrap();
440        assert!(date.tomorrow().is_err());
441    }
442
443    #[test]
444    fn valid_yesterday() {
445        let date = Date::parse("2018-12-24").unwrap();
446        assert!(date.yesterday().is_ok());
447    }
448
449    #[test]
450    fn invalid_yesterday() {
451        let date = Date::parse(&MIN_DATE.format("%Y-%m-%d").to_string()).unwrap();
452        assert!(date.yesterday().is_err());
453    }
454
455    #[test]
456    fn num_days() {
457        let date = Date::parse("2018-04-01").unwrap();
458
459        let sub = Date::parse("2018-03-01").unwrap();
460        assert_eq!(date.num_days(&sub), 31);
461
462        let sub = Date::parse("2018-05-01").unwrap();
463        assert_eq!(date.num_days(&sub), -30);
464
465        let sub = Date::parse("2018-04-01").unwrap();
466        assert_eq!(date.num_days(&sub), 0);
467    }
468
469    #[test]
470    fn date_to_string() {
471        let format = "2018-01-01";
472        let date = Date::parse(format).unwrap();
473        assert_eq!(date.to_string(), format);
474    }
475
476    #[test]
477    fn date_display() {
478        let format = "2018-01-01";
479        let date = Date::parse(format).unwrap();
480        assert_eq!(format!("{}", date), format);
481    }
482
483    #[test]
484    fn date_equal() {
485        let d1 = Date::parse("2018-04-01").unwrap();
486        let d2 = Date::parse("2018-04-01").unwrap();
487        assert!(d1 == d2);
488    }
489
490    #[test]
491    fn date_greater() {
492        let d1 = Date::parse("2018-04-10").unwrap();
493        let d2 = Date::parse("2017-04-10").unwrap();
494        assert!(d1 > d2);
495
496        let d2 = Date::parse("2018-03-20").unwrap();
497        assert!(d1 > d2);
498
499        let d2 = Date::parse("2018-04-01").unwrap();
500        assert!(d1 > d2);
501    }
502
503    #[test]
504    fn date_greater_than() {
505        let d1 = Date::parse("2018-04-10").unwrap();
506        let d2 = Date::parse("2018-04-10").unwrap();
507        assert!(d1 >= d2);
508
509        let d2 = Date::parse("2018-04-01").unwrap();
510        assert!(d1 >= d2);
511    }
512
513    #[test]
514    fn date_less() {
515        let d1 = Date::parse("2018-04-10").unwrap();
516        let d2 = Date::parse("2019-04-10").unwrap();
517        assert!(d1 < d2);
518
519        let d2 = Date::parse("2018-05-10").unwrap();
520        assert!(d1 < d2);
521
522        let d2 = Date::parse("2018-04-20").unwrap();
523        assert!(d1 < d2);
524    }
525
526    #[test]
527    fn date_less_than() {
528        let d1 = Date::parse("2018-04-10").unwrap();
529        let d2 = Date::parse("2018-04-10").unwrap();
530        assert!(d1 <= d2);
531
532        let d2 = Date::parse("2018-04-20").unwrap();
533        assert!(d1 <= d2);
534    }
535}