1use crate::day_of_week::DayOfWeek;
2use crate::error::parse_error;
3use crate::month_of_year::Month;
4use crate::DayOfWeek::{Fri, Sun, Thu};
5use crate::TimeWarpError;
6use std::cmp::Ordering;
7use std::convert::TryFrom;
8use std::fmt::{Debug, Display, Formatter};
9use std::num::ParseIntError;
10use std::ops::{Add, Sub};
11use std::time::SystemTime;
12
13#[must_use]
15#[derive(Eq, PartialEq, Copy, Clone, Debug)]
16pub struct Doy {
17 pub year: i32,
18 pub doy: i32,
19}
20
21impl Doy {
22 pub const SECOND: u128 = 1000;
23 pub const MINUTE: u128 = Self::SECOND * 60;
24 pub const HOUR: u128 = Self::MINUTE * 60;
25 pub const DAY: u128 = Self::HOUR * 24;
26 pub const YEAR: u128 = Self::DAY * 365 + Self::HOUR * 6;
27
28 pub fn today() -> Self {
30 let millis = SystemTime::now()
31 .duration_since(SystemTime::UNIX_EPOCH)
32 .unwrap()
33 .as_millis();
34 Self::from_millis(millis)
35 }
36
37 pub fn from_millis(millis: u128) -> Self {
39 let offset = millis % Self::YEAR;
40 let year = 1970 + ((millis - offset) / Self::YEAR) as i32;
41 let doy_offset = offset % Self::DAY;
42 let doy = 1 + ((offset - doy_offset) / Self::DAY) as i32;
43 Self { year, doy }
44 }
45
46 pub fn new(doy: i32, year: i32) -> Self {
49 if doy < 1 {
50 return Self::new(365 + i32::from(Self::is_leapyear(year - 1)) + doy, year - 1);
51 }
52 let max_doy = 365 + i32::from(Self::is_leapyear(year));
53 if doy > max_doy {
54 Self::new(doy - max_doy, year + 1)
55 } else {
56 Self { year, doy }
57 }
58 }
59
60 #[inline]
61 fn day_per_month(year: i32) -> Vec<i32> {
62 let leap = Self::is_leapyear(year) as i32;
63 vec![31, 28 + leap, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
64 }
65
66 pub fn from_ymd(year: i32, month: i32, day: i32) -> Self {
71 assert!(month > 0 && month < 13, "Month has to be in 1..12");
72 let day_of_year = Self::day_per_month(year)
73 .iter()
74 .take(month as usize - 1)
75 .sum::<i32>()
76 + day;
77 Self::new(day_of_year, year)
78 }
79
80 pub fn from_week(year: i32, week: i32) -> Self {
85 assert!(week > 0 && week < 54, "Week has to be in 1..53");
86 let weekday = Self::new(4, year).day_of_week();
88 let day_of_year = (week - 1) * 7
89 + match weekday {
90 Sun => -2,
91 _ => Fri as i32 - weekday as i32,
92 };
93 Self::new(day_of_year, year)
94 }
95
96 #[inline]
98 pub fn is_leapyear(year: i32) -> bool {
99 year % 4 == 0 && year % 100 != 0
100 }
101
102 pub fn leapyear(self) -> bool {
104 Self::is_leapyear(self.year)
105 }
106
107 fn as_date(self) -> (i32, i32) {
109 let mut doy = self.doy;
110 let mut m = 1;
111 for ds in Self::day_per_month(self.year) {
112 if doy <= ds {
113 return (m, doy);
114 }
115 m += 1;
116 doy -= ds;
117 }
118 (-1, -1)
119 }
120
121 pub fn as_iso_date(self) -> String {
123 format!("{self:#}")
124 }
125
126 #[inline]
128 pub fn day_of_week(self) -> DayOfWeek {
129 let y = self.year % 100;
130 let y_off = y + (y / 4) + 6 - self.leapyear() as i32;
131 DayOfWeek::from(y_off + self.doy)
132 }
133
134 pub fn iso8601week(self) -> String {
139 let dow = self.day_of_week();
140 let thursday = match dow {
141 Sun => self + Thu - 7, _ => self + Thu - dow,
143 };
144 let kw = (thursday.doy + 6) / 7;
145 format!("{}-W{kw:02}", thursday.year)
146 }
147
148 pub fn day_of_month(self) -> i32 {
150 self.as_date().1
151 }
152
153 pub fn month(self) -> Month {
155 Month::from(self.as_date().0)
156 }
157}
158
159impl From<Doy> for String {
160 fn from(doy: Doy) -> Self {
161 doy.to_string()
162 }
163}
164
165impl From<u128> for Doy {
166 fn from(value: u128) -> Self {
167 Doy::from_millis(value)
168 }
169}
170
171impl Display for Doy {
172 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
173 let (month, day) = self.as_date();
174 let year = self.year;
175 if f.alternate() {
176 write!(f, "{year:04}-{month:02}-{day:02}")
177 } else {
178 write!(f, "{year:04}{month:02}{day:02}")
179 }
180 }
181}
182
183macro_rules! gen_calcs {
184 ($($key:ident),+) => {
185 $(
186 impl Add<$key> for Doy {
187 type Output = Doy;
188
189 fn add(self, rhs: $key) -> Self::Output {
190 Doy::new(self.doy + rhs as i32, self.year)
191 }
192 }
193
194 impl Sub<$key> for Doy {
195 type Output = Doy;
196
197 fn sub(self, rhs: $key) -> Self::Output {
198 Doy::new(self.doy - rhs as i32, self.year)
199 }
200 }
201 )+
202 }
203}
204
205gen_calcs!(i8, i16, i32, i64, u8, u16, u32, u64, DayOfWeek);
206
207impl PartialOrd for Doy {
208 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
209 let p = if self.lt(other) {
210 Ordering::Less
211 } else if self.gt(other) {
212 Ordering::Greater
213 } else {
214 Ordering::Equal
215 };
216 Some(p)
217 }
218
219 fn lt(&self, other: &Self) -> bool {
220 self.year < other.year || (self.year == other.year && self.doy < other.doy)
221 }
222
223 fn le(&self, other: &Self) -> bool {
224 self.year < other.year || (self.year == other.year && self.doy <= other.doy)
225 }
226
227 fn gt(&self, other: &Self) -> bool {
228 self.year > other.year || (self.year == other.year && self.doy > other.doy)
229 }
230
231 fn ge(&self, other: &Self) -> bool {
232 self.year > other.year || (self.year == other.year && self.doy >= other.doy)
233 }
234}
235
236impl TryFrom<&str> for Doy {
237 type Error = TimeWarpError;
238
239 fn try_from(value: &str) -> Result<Self, Self::Error> {
240 use std::str::FromStr;
241 let err = |e: ParseIntError| -> Result<i32, TimeWarpError> {
242 parse_error(format!("Error converting into number: '{value}'\n{e}",))
243 };
244 let y = i32::from_str(&value[0..4]).map_err(err)?;
245 let (m, d) = if value.len() == 10 && &value[4..5] == "-" && &value[7..8] == "-" {
246 (
247 i32::from_str(&value[5..7]).map_err(err)?,
248 i32::from_str(&value[8..10]).map_err(err)?,
249 )
250 } else if value.len() == 8 {
251 (
252 i32::from_str(&value[4..6]).map_err(err)?,
253 i32::from_str(&value[6..8]).map_err(err)?,
254 )
255 } else {
256 return parse_error(format!("Wrong date-format: '{value}'"));
257 };
258 if m < 1 || m > 12 {
259 return parse_error(format!("Month out of range 0..12: '{m}'"));
260 }
261 let days_in_month = Self::day_per_month(y).as_slice()[(m - 1) as usize];
262 if d < 1 || d > days_in_month {
263 return parse_error(format!(
264 "Days exceeded in month {m} '{d}' ({days_in_month})"
265 ));
266 }
267 Ok(Self::from_ymd(y, m, d))
268 }
269}
270
271#[derive(Debug, Eq, PartialEq)]
274pub enum Tempus {
275 Moment(Doy),
276 Interval(Doy, Doy),
277}
278
279impl Tempus {
280 pub fn start(&self) -> Doy {
282 match *self {
283 Tempus::Moment(d) | Tempus::Interval(d, _) => d,
284 }
285 }
286
287 pub fn end(&self) -> Doy {
289 match *self {
290 Tempus::Moment(d) => d + 1,
291 Tempus::Interval(_, e) => e,
292 }
293 }
294}
295
296#[cfg(test)]
297mod should {
298 use crate::day_of_week::DayOfWeek::*;
299 use crate::doy::Doy;
300 use crate::month_of_year::Month;
301 use std::convert::TryFrom;
302
303 #[test]
304 fn try_from() {
305 assert_eq!(
306 "2018-01-01",
307 Doy::try_from("2018-01-01").unwrap().as_iso_date()
308 );
309 assert!(Doy::try_from("2018-13-01").is_err());
310 assert!(Doy::try_from("2018-02-29").is_err());
311 assert!(Doy::try_from("20180431").is_err());
312 assert!(Doy::try_from("2018/04/15").is_err());
313 }
314
315 #[test]
316 fn from_week_of_year() {
317 assert_eq!("2018-01-01", Doy::from_week(2018, 1).as_iso_date());
318 assert_eq!("2018-12-31", Doy::from_week(2019, 1).as_iso_date());
319 assert_eq!("2019-12-30", Doy::from_week(2020, 1).as_iso_date());
320 assert_eq!("2021-01-04", Doy::from_week(2021, 1).as_iso_date());
321 assert_eq!("2022-01-03", Doy::from_week(2022, 1).as_iso_date());
322 }
323
324 #[test]
325 fn into_week_of_year() {
326 assert_eq!("2018-W01", Doy::from_ymd(2018, 1, 4).iso8601week());
328 assert_eq!("2019-W01", Doy::from_ymd(2019, 1, 4).iso8601week());
329 assert_eq!("2020-W01", Doy::from_ymd(2020, 1, 4).iso8601week());
330 assert_eq!("2021-W01", Doy::from_ymd(2021, 1, 4).iso8601week());
331 assert_eq!("2022-W01", Doy::from_ymd(2022, 1, 4).iso8601week());
332 assert_eq!("2023-W01", Doy::from_ymd(2023, 1, 4).iso8601week());
333 assert_eq!("2026-W01", Doy::from_ymd(2026, 1, 4).iso8601week());
334
335 assert_eq!("2018-W01", Doy::from_ymd(2018, 1, 1).iso8601week());
336 assert_eq!("2019-W01", Doy::from_ymd(2019, 1, 1).iso8601week());
337 assert_eq!("2020-W53", Doy::from_ymd(2021, 1, 1).iso8601week());
338 assert_eq!("2021-W52", Doy::from_ymd(2022, 1, 1).iso8601week());
339
340 assert_eq!("2018-W26", Doy::from_ymd(2018, 7, 1).iso8601week());
341 assert_eq!("2019-W27", Doy::from_ymd(2019, 7, 1).iso8601week());
342 assert_eq!("2020-W27", Doy::from_ymd(2020, 7, 1).iso8601week());
343 assert_eq!("2021-W26", Doy::from_ymd(2021, 7, 1).iso8601week());
344 }
345
346 #[test]
347 fn day_of_month() {
348 let test = Doy::from_ymd(2018, 4, 13);
349 assert_eq!(test.as_iso_date(), "2018-04-13");
350 assert_eq!(test.month(), Month::Apr);
351
352 let test = Doy::from_ymd(2018, 3, 6);
353 assert_eq!(test.as_iso_date(), "2018-03-06");
354 assert_eq!(test.month(), Month::Mar);
355 }
356
357 #[test]
358 fn create_by_doy_year() {
359 let proof = Doy::new(-7, 2020);
360 let test = Doy::new(358, 2019);
361 assert_eq!(test, proof);
362 let proof = Doy::new(-1, 2020);
363 assert_eq!("20191230", proof.to_string());
364 let proof = Doy::new(-1, 2021);
365 assert_eq!("20201230", proof.to_string());
366 }
367
368 #[test]
369 fn return_leapyear() {
370 assert!(Doy::new(1, 2020).leapyear());
371 assert!(!Doy::new(1, 2018).leapyear());
372 assert!(!Doy::new(1, 2000).leapyear());
373 }
374
375 #[test]
376 fn convert_to_string() {
377 assert_eq!("20201225", Doy::new(360, 2020).to_string());
378 assert_eq!("20181225", Doy::new(359, 2018).to_string());
379 }
380
381 #[test]
382 fn calc_day_of_week() {
383 assert_eq!(Wed, Doy::new(31, 2018).day_of_week());
384 assert_eq!(Thu, Doy::new(31, 2019).day_of_week());
385 assert_eq!(Fri, Doy::new(31, 2020).day_of_week());
386 assert_eq!(Tue, Doy::new(359, 2018).day_of_week());
388 assert_eq!(Fri, Doy::new(360, 2020).day_of_week());
389 assert_eq!(Sat, Doy::new(359, 2021).day_of_week());
390 }
391
392 #[test]
393 fn create_via_try_from() {
394 assert_eq!("20200229", Doy::from_ymd(2020, 2, 29).to_string());
395 assert_eq!("19990814", Doy::from_ymd(1999, 8, 14).to_string());
396 let d = "20240721";
397 assert_eq!(d, &Doy::try_from(d).unwrap().to_string());
398 }
399
400 #[test]
401 fn order_gt_or_lt() {
402 let a = Doy::new(112, 2020);
403 let b = Doy::new(225, 2020);
404 let c = Doy::new(85, 2021);
405
406 assert!(a < b);
407 assert!(c > a);
408 assert!(b < c);
409 assert!(a >= a);
410 assert!(b <= c);
411 }
412
413 #[test]
414 fn add_i32() {
415 let d = Doy::new(15, 2020) + 2;
416 assert_eq!(Doy::new(17, 2020), d);
417 }
418
419 #[test]
420 fn from_millis() {
421 assert_eq!("20230317", Doy::from_millis(1679086777511).to_string());
422 assert_eq!("20230101", Doy::from_millis(1672570315000).to_string());
423 assert_eq!("20181231", Doy::from_millis(1546253515000).to_string());
424 }
425}