1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
#![forbid(unsafe_code)]
pub mod date_component {
use chrono::prelude::*;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct DateComponent {
/// Number of years.
pub year: isize,
/// Number of months.
pub month: isize,
/// Number of weeks.
pub week: isize,
/// Number of days remaining when using weeks.
pub modulo_days: isize,
/// Number of days.
pub day: isize,
/// Number of hours.
pub hour: isize,
/// Number of minutes.
pub minute: isize,
/// Number of seconds.
pub second: isize,
/// total number of seconds between the start and end dates.
pub interval_seconds: isize,
/// total number of minutes between the start and end dates.
pub interval_minutes: isize,
/// total number of hours between the start and end dates.
pub interval_hours: isize,
/// total number of days between the start and end dates
pub interval_days: isize,
/// Is true if the interval represents a negative time period and false otherwise
pub invert: bool,
}
/// Returns a DateComponent object that represents the difference between the from and to datetime.
pub fn calculate<T: chrono::TimeZone>(
from_datetime: &DateTime<T>,
to_datetime: &DateTime<T>,
) -> DateComponent {
let timezone = from_datetime.timezone();
let to_datetime_in_from_tz = to_datetime.with_timezone(&timezone);
let duration = from_datetime
.clone()
.signed_duration_since(to_datetime.clone());
let seconds = duration.num_seconds();
let (start, end, invert) = match seconds {
x if x <= 0 => (from_datetime.clone(), to_datetime_in_from_tz, false),
_ => (to_datetime_in_from_tz, from_datetime.clone(), true),
};
// Use mutable variables for interval components
let mut year = end.year() as i64 - start.year() as i64;
let mut month = end.month() as i64 - start.month() as i64;
let mut day = end.day() as i64 - start.day() as i64;
// For DST handling, we need to use duration for time components
// instead of calculating differences directly
let duration_hours = duration.num_hours().abs() % 24;
let duration_minutes = duration.num_minutes().abs() % 60;
let duration_seconds = duration.num_seconds().abs() % 60;
// Now handle date borrowing (days -> months -> years)
let (previous_year, previous_month) = if end.month() == 1 {
(end.year() - 1, 12)
} else {
(end.year(), end.month() - 1)
};
if day < 0 {
month -= 1;
// Add days in the month *before* the end date's month.
// Use get_nearest_day_before to find the last day of that month.
let last_day_of_prev_month = get_nearest_day_before(
previous_year,
previous_month,
31, // Try 31, it will be adjusted down correctly
0,
0,
0, // Time doesn't matter for finding the last day
&timezone,
);
day += last_day_of_prev_month.day() as i64;
}
if month < 0 {
month += 12;
year -= 1;
};
// Calculate week and modulo_days based on the final adjusted day value
let final_year = year;
let final_month = month;
let mut final_day = day;
let mut final_hour = duration_hours;
let mut final_minute = duration_minutes;
let mut final_second = duration_seconds;
// Consistency check: fix cases where small time differences are incorrectly
// calculated as larger units due to crossing date boundaries
let total_seconds = duration.num_seconds().abs();
// If total time is less than 1 day but we calculated days, adjust
if total_seconds < 86400 && final_day > 0 {
// For small time differences that cross boundaries,
// use the total duration directly instead of calculated day components
final_day = 0;
final_hour = total_seconds / 3600;
final_minute = (total_seconds % 3600) / 60;
final_second = total_seconds % 60;
}
// Similar check for hours when total time is less than 1 hour
if total_seconds < 3600 && final_hour > 0 {
let hour_seconds = final_hour * 3600;
let remaining_seconds = hour_seconds + final_minute * 60 + final_second;
final_hour = 0;
final_minute = remaining_seconds / 60;
final_second = remaining_seconds % 60;
}
// Similar check for minutes when total time is less than 1 minute
if total_seconds < 60 && final_minute > 0 {
let minute_seconds = final_minute * 60;
final_second += minute_seconds;
final_minute = 0;
}
let final_week = final_day / 7;
let final_modulo_days = final_day % 7;
// Return the final DateComponent
DateComponent {
year: final_year as isize,
month: final_month as isize,
week: final_week as isize,
modulo_days: final_modulo_days as isize,
day: final_day as isize,
hour: final_hour as isize,
minute: final_minute as isize,
second: final_second as isize,
interval_seconds: duration.num_seconds().abs() as isize,
interval_minutes: duration.num_minutes().abs() as isize,
interval_hours: duration.num_hours().abs() as isize,
interval_days: duration.num_days().abs() as isize,
invert,
}
}
/// Given date specified by year / month / day where the `day` may be invalid,
/// (e.g. 2021-02-30), return the nearest valid day before it
/// (e.g. 2021-02-28).
pub(crate) fn get_nearest_day_before<T: TimeZone>(
year: i32,
month: u32,
day: u32,
hour: u32,
min: u32,
sec: u32,
timezone: &T,
) -> DateTime<T> {
let mut subtract = 0;
loop {
match timezone.with_ymd_and_hms(year, month, day - subtract, hour, min, sec) {
chrono::LocalResult::None => subtract += 1,
chrono::LocalResult::Single(d) => {
return d;
}
chrono::LocalResult::Ambiguous(d, _) => {
return d;
}
}
}
}
}
#[cfg(test)]
mod internal_tests {
use chrono::prelude::*;
use crate::date_component::get_nearest_day_before;
#[test]
fn test_get_nearest_day_before_regular() {
let dt = get_nearest_day_before(2023, 2, 30, 0, 0, 0, &Utc);
assert_eq!(dt.day(), 28);
}
#[test]
fn test_get_nearest_day_before_leap() {
let dt = get_nearest_day_before(2024, 2, 30, 0, 0, 0, &Utc);
assert_eq!(dt.day(), 29);
}
#[test]
fn test_get_nearest_day_before_big_month() {
let dt = get_nearest_day_before(2023, 1, 32, 0, 0, 0, &Utc);
assert_eq!(dt.day(), 31);
}
#[test]
fn test_get_nearest_day_before_edge_case() {
// Test with day = 1, should return valid date
let dt = get_nearest_day_before(2023, 2, 1, 0, 0, 0, &Utc);
assert_eq!(dt.day(), 1);
}
#[test]
fn test_potential_infinite_loop_prevention() {
// This tests if the function would handle extremely large day values gracefully
// It should not cause infinite loop even with very large subtract values
let dt = get_nearest_day_before(2023, 2, 100, 0, 0, 0, &Utc);
assert_eq!(dt.day(), 28); // February 2023 has 28 days
}
}