1use crate::{EpochDays, MILLIS_PER_DAY};
2
3#[inline]
4fn truncate_millis(ts: i64, truncate: i64) -> i64 {
5 ts / truncate * truncate
6}
7
8#[inline]
9fn truncate_millis_float(ts: f64, truncate: i64) -> f64 {
10 let truncate = truncate as f64;
11 (ts / truncate).floor() * truncate
12}
13
14#[inline]
15pub fn date_trunc_day_timestamp_millis(ts: i64) -> i64 {
16 truncate_millis(ts, MILLIS_PER_DAY)
17}
18
19#[inline]
20pub fn date_trunc_day_timestamp_millis_float(ts: f64) -> f64 {
21 truncate_millis_float(ts, MILLIS_PER_DAY)
22}
23
24#[inline]
25pub fn date_trunc_week_timestamp_millis(ts: i64) -> i64 {
26 let offset = 4 * MILLIS_PER_DAY;
28 truncate_millis(ts - offset, 7 * MILLIS_PER_DAY) + offset
29}
30
31#[inline]
32pub fn date_trunc_week_timestamp_millis_float(ts: f64) -> f64 {
33 let offset = (4 * MILLIS_PER_DAY) as f64;
34 truncate_millis_float(ts - offset, 7 * MILLIS_PER_DAY) + offset
35}
36
37#[inline]
38pub fn date_trunc_month_timestamp_millis(ts: i64) -> i64 {
39 let epoch_days = EpochDays::from_timestamp_millis(ts);
40 let truncated = epoch_days.date_trunc_month();
41 truncated.to_timestamp_millis()
42}
43
44#[inline]
45pub fn date_trunc_month_timestamp_millis_float(ts: f64) -> f64 {
46 let epoch_days = EpochDays::from_timestamp_millis_float(ts);
47 let truncated = epoch_days.date_trunc_month();
48 truncated.to_timestamp_millis_float()
49}
50
51#[inline]
52pub fn date_trunc_year_timestamp_millis(ts: i64) -> i64 {
53 let epoch_days = EpochDays::from_timestamp_millis(ts);
54 let truncated = epoch_days.date_trunc_year();
55 truncated.to_timestamp_millis()
56}
57
58#[inline]
59pub fn date_trunc_year_timestamp_millis_float(ts: f64) -> f64 {
60 let epoch_days = EpochDays::from_timestamp_millis_float(ts);
61 let truncated = epoch_days.date_trunc_year();
62 truncated.to_timestamp_millis_float()
63}
64
65#[inline]
66pub fn date_trunc_quarter_timestamp_millis(ts: i64) -> i64 {
67 let epoch_days = EpochDays::from_timestamp_millis(ts);
68 let truncated = epoch_days.date_trunc_quarter();
69 truncated.to_timestamp_millis()
70}
71
72#[inline]
73pub fn date_trunc_quarter_timestamp_millis_float(ts: f64) -> f64 {
74 let epoch_days = EpochDays::from_timestamp_millis_float(ts);
75 let truncated = epoch_days.date_trunc_quarter();
76 truncated.to_timestamp_millis_float()
77}
78
79#[inline]
80pub fn date_part_year_timestamp_millis(ts: i64) -> i32 {
81 let epoch_days = EpochDays::from_timestamp_millis(ts);
82 epoch_days.extract_year()
83}
84
85#[inline]
86pub fn date_part_month_timestamp_millis(ts: i64) -> i32 {
87 let epoch_days = EpochDays::from_timestamp_millis(ts);
88 epoch_days.extract_month()
89}
90
91#[inline]
92fn timestamp_to_epoch_days_and_remainder(ts: i64) -> (EpochDays, i64) {
93 let (days, millis) = (ts.div_euclid(MILLIS_PER_DAY), ts.rem_euclid(MILLIS_PER_DAY));
94 (EpochDays::new(days as i32), millis)
95}
96
97#[inline]
98fn timestamp_to_epoch_days_and_remainder_float(ts: f64) -> (EpochDays, f64) {
99 let days = (ts * (1.0 / MILLIS_PER_DAY as f64)).floor();
100 let millis = ts - days * (MILLIS_PER_DAY as f64);
101 (EpochDays::new(unsafe { days.to_int_unchecked() }), millis)
102}
103
104#[inline]
105pub fn date_add_month_timestamp_millis(ts: i64, months: i32) -> i64 {
106 let (epoch_days, millis) = timestamp_to_epoch_days_and_remainder(ts);
107 let new_epoch_days = epoch_days.add_months(months);
108 new_epoch_days.to_timestamp_millis() + millis
109}
110
111#[inline]
112pub fn date_add_month_timestamp_millis_float(ts: f64, months: i32) -> f64 {
113 let (epoch_days, millis) = timestamp_to_epoch_days_and_remainder_float(ts);
114 let new_epoch_days = epoch_days.add_months(months);
115 new_epoch_days.to_timestamp_millis_float() + millis
116}
117
118#[inline]
119fn timestamp_to_year_month_millis_of_month(ts: i64) -> (i32, i32, i64) {
120 let (ed, millis) = timestamp_to_epoch_days_and_remainder(ts);
121 let (year, month, day) = ed.to_ymd();
122 let millis_of_month = (day as i64) * MILLIS_PER_DAY + millis;
123 (year, month, millis_of_month)
124}
125
126#[inline]
127fn timestamp_to_year_month_millis_of_month_float(ts: f64) -> (i32, i32, f64) {
128 let (ed, millis) = timestamp_to_epoch_days_and_remainder_float(ts);
129 let (year, month, day) = ed.to_ymd();
130 let millis_of_month = (day as f64) * (MILLIS_PER_DAY as f64) + millis;
131 (year, month, millis_of_month)
132}
133
134#[inline]
135pub fn date_diff_month_timestamp_millis(t0: i64, t1: i64) -> i32 {
136 let (y0, m0, ms0) = timestamp_to_year_month_millis_of_month(t0);
137 let (y1, m1, ms1) = timestamp_to_year_month_millis_of_month(t1);
138 (y1 * 12 + m1) - (y0 * 12 + m0) - ((ms1 < ms0) as i32)
139}
140
141#[inline]
142pub fn date_diff_month_timestamp_millis_float(t0: f64, t1: f64) -> i32 {
143 let (y0, m0, ms0) = timestamp_to_year_month_millis_of_month_float(t0);
144 let (y1, m1, ms1) = timestamp_to_year_month_millis_of_month_float(t1);
145 (y1 * 12 + m1) - (y0 * 12 + m0) - ((ms1 < ms0) as i32)
146}
147
148#[inline]
149pub fn date_diff_year_timestamp_millis(t0: i64, t1: i64) -> i32 {
150 let (y0, m0, ms0) = timestamp_to_year_month_millis_of_month(t0);
151 let (y1, m1, ms1) = timestamp_to_year_month_millis_of_month(t1);
152 y1 - y0 - (((m1, ms1) < (m0, ms0)) as i32)
153}
154
155#[inline]
156pub fn date_diff_year_timestamp_millis_float(t0: f64, t1: f64) -> i32 {
157 let (y0, m0, ms0) = timestamp_to_year_month_millis_of_month_float(t0);
158 let (y1, m1, ms1) = timestamp_to_year_month_millis_of_month_float(t1);
159 y1 - y0 - (((m1, ms1) < (m0, ms0)) as i32)
160}
161
162#[inline]
163pub fn days_in_month_timestamp_millis(ts: i64) -> i32 {
164 let epoch_days = EpochDays::from_timestamp_millis(ts);
165 epoch_days.days_in_month()
166}
167
168#[inline]
169pub fn days_in_month_timestamp_millis_float(ts: f64) -> i32 {
170 let epoch_days = EpochDays::from_timestamp_millis_float(ts);
171 epoch_days.days_in_month()
172}
173
174#[cfg(test)]
175mod tests {
176 use crate::epoch_days::EpochDays;
177 use crate::{
178 date_add_month_timestamp_millis, date_diff_month_timestamp_millis, date_diff_year_timestamp_millis,
179 date_trunc_month_timestamp_millis, date_trunc_quarter_timestamp_millis, date_trunc_year_timestamp_millis,
180 };
181 use chrono::{Datelike, NaiveDate, NaiveDateTime, NaiveTime};
182 use std::ops::Add;
183
184 fn timestamp_to_naive_date_time(ts: i64) -> NaiveDateTime {
185 NaiveDateTime::from_timestamp(ts / 1000, 0).add(chrono::Duration::milliseconds(ts % 1000))
186 }
187
188 fn date_trunc_year_chrono(ts: i64) -> i64 {
189 let ndt = timestamp_to_naive_date_time(ts);
190 let truncated = NaiveDateTime::new(NaiveDate::from_ymd(ndt.year(), 1, 1), NaiveTime::from_hms(0, 0, 0));
191 truncated.timestamp_millis()
192 }
193
194 fn date_trunc_month_chrono(ts: i64) -> i64 {
195 let ndt = timestamp_to_naive_date_time(ts);
196 let truncated = NaiveDateTime::new(NaiveDate::from_ymd(ndt.year(), ndt.month(), 1), NaiveTime::from_hms(0, 0, 0));
197 truncated.timestamp_millis()
198 }
199
200 #[test]
201 fn test_date_trunc_year_millis() {
202 assert_eq!(1640995200_000, date_trunc_year_timestamp_millis(1640995200_000));
203 assert_eq!(1640995200_000, date_trunc_year_timestamp_millis(1658765238_000));
204 }
205
206 #[test]
207 fn test_date_trunc_quarter_millis() {
208 assert_eq!(1640995200_000, date_trunc_quarter_timestamp_millis(1640995200_000));
209 assert_eq!(1656633600_000, date_trunc_quarter_timestamp_millis(1658766592_000));
210 }
211
212 #[test]
213 fn test_date_trunc_month_millis() {
214 assert_eq!(1640995200_000, date_trunc_month_timestamp_millis(1640995200_000));
215 assert_eq!(1656633600_000, date_trunc_month_timestamp_millis(1658765238_000));
216 }
217
218 #[test]
219 fn test_date_add_months() {
220 let epoch_day = EpochDays::from_ymd(2022, 7, 31);
221 assert_eq!(epoch_day.add_months(1), EpochDays::from_ymd(2022, 8, 31));
222 assert_eq!(epoch_day.add_months(2), EpochDays::from_ymd(2022, 9, 30));
223 assert_eq!(epoch_day.add_months(3), EpochDays::from_ymd(2022, 10, 31));
224 assert_eq!(epoch_day.add_months(4), EpochDays::from_ymd(2022, 11, 30));
225 assert_eq!(epoch_day.add_months(5), EpochDays::from_ymd(2022, 12, 31));
226 }
227
228 #[test]
229 fn test_date_add_months_year_boundary() {
230 let epoch_day = EpochDays::from_ymd(2022, 7, 31);
231 assert_eq!(epoch_day.add_months(6), EpochDays::from_ymd(2023, 1, 31));
232 assert_eq!(epoch_day.add_months(7), EpochDays::from_ymd(2023, 2, 28));
233 assert_eq!(epoch_day.add_months(8), EpochDays::from_ymd(2023, 3, 31));
234 }
235
236 #[test]
237 fn test_date_add_months_leap_year() {
238 let epoch_day = EpochDays::from_ymd(2022, 7, 31);
239 assert_eq!(epoch_day.add_months(19), EpochDays::from_ymd(2024, 2, 29));
240
241 let epoch_day = EpochDays::from_ymd(2022, 2, 28);
242 assert_eq!(epoch_day.add_months(24), EpochDays::from_ymd(2024, 2, 28));
243
244 let epoch_day = EpochDays::from_ymd(2024, 2, 29);
245 assert_eq!(epoch_day.add_months(12), EpochDays::from_ymd(2025, 2, 28));
246 }
247
248 #[test]
249 fn test_date_add_months_negative() {
250 let epoch_day = EpochDays::from_ymd(2022, 7, 31);
251 assert_eq!(epoch_day.add_months(-1), EpochDays::from_ymd(2022, 6, 30));
252 assert_eq!(epoch_day.add_months(-2), EpochDays::from_ymd(2022, 5, 31));
253 assert_eq!(epoch_day.add_months(-3), EpochDays::from_ymd(2022, 4, 30));
254 assert_eq!(epoch_day.add_months(-4), EpochDays::from_ymd(2022, 3, 31));
255 assert_eq!(epoch_day.add_months(-5), EpochDays::from_ymd(2022, 2, 28));
256 assert_eq!(epoch_day.add_months(-6), EpochDays::from_ymd(2022, 1, 31));
257 assert_eq!(epoch_day.add_months(-7), EpochDays::from_ymd(2021, 12, 31));
258 }
259
260 #[test]
261 fn test_date_add_months_negative_year_boundary() {
262 let epoch_day = EpochDays::from_ymd(2022, 7, 31);
263 assert_eq!(epoch_day.add_months(-7), EpochDays::from_ymd(2021, 12, 31));
264 }
265
266 #[test]
267 fn test_date_add_months_timestamp_millis() {
268 assert_eq!(date_add_month_timestamp_millis(1661102969_000, 1), 1663781369000);
269 assert_eq!(date_add_month_timestamp_millis(1661102969_000, 12), 1692638969000);
270 }
271
272 #[test]
273 fn test_date_diff_months() {
274 assert_eq!(
275 date_diff_month_timestamp_millis(
276 EpochDays::from_ymd(2023, 10, 1).to_timestamp_millis(),
277 EpochDays::from_ymd(2023, 10, 1).to_timestamp_millis()
278 ),
279 0
280 );
281 assert_eq!(
282 date_diff_month_timestamp_millis(
283 EpochDays::from_ymd(2023, 10, 1).to_timestamp_millis(),
284 EpochDays::from_ymd(2023, 11, 1).to_timestamp_millis()
285 ),
286 1
287 );
288 assert_eq!(
289 date_diff_month_timestamp_millis(
290 EpochDays::from_ymd(2023, 10, 15).to_timestamp_millis(),
291 EpochDays::from_ymd(2023, 11, 14).to_timestamp_millis()
292 ),
293 0
294 );
295 assert_eq!(
296 date_diff_month_timestamp_millis(
297 EpochDays::from_ymd(2023, 10, 15).to_timestamp_millis(),
298 EpochDays::from_ymd(2023, 11, 15).to_timestamp_millis()
299 ),
300 1
301 );
302 assert_eq!(
303 date_diff_month_timestamp_millis(
304 EpochDays::from_ymd(2023, 10, 15).to_timestamp_millis(),
305 EpochDays::from_ymd(2023, 11, 16).to_timestamp_millis()
306 ),
307 1
308 );
309 }
310
311 #[test]
312 fn test_date_diff_years() {
313 assert_eq!(
314 date_diff_year_timestamp_millis(
315 EpochDays::from_ymd(2023, 10, 1).to_timestamp_millis(),
316 EpochDays::from_ymd(2023, 10, 1).to_timestamp_millis()
317 ),
318 0
319 );
320 assert_eq!(
321 date_diff_year_timestamp_millis(
322 EpochDays::from_ymd(2023, 10, 1).to_timestamp_millis(),
323 EpochDays::from_ymd(2023, 11, 1).to_timestamp_millis()
324 ),
325 0
326 );
327 assert_eq!(
328 date_diff_year_timestamp_millis(
329 EpochDays::from_ymd(2023, 10, 15).to_timestamp_millis(),
330 EpochDays::from_ymd(2024, 10, 14).to_timestamp_millis()
331 ),
332 0
333 );
334 assert_eq!(
335 date_diff_year_timestamp_millis(
336 EpochDays::from_ymd(2023, 10, 15).to_timestamp_millis(),
337 EpochDays::from_ymd(2024, 10, 15).to_timestamp_millis()
338 ),
339 1
340 );
341 assert_eq!(
342 date_diff_year_timestamp_millis(
343 EpochDays::from_ymd(2023, 10, 15).to_timestamp_millis(),
344 EpochDays::from_ymd(2024, 10, 16).to_timestamp_millis()
345 ),
346 1
347 );
348 assert_eq!(
349 date_diff_year_timestamp_millis(
350 EpochDays::from_ymd(2024, 2, 29).to_timestamp_millis(),
351 EpochDays::from_ymd(2025, 2, 28).to_timestamp_millis()
352 ),
353 0
354 );
355 assert_eq!(
356 date_diff_year_timestamp_millis(
357 EpochDays::from_ymd(2024, 2, 29).to_timestamp_millis(),
358 EpochDays::from_ymd(2025, 3, 1).to_timestamp_millis()
359 ),
360 1
361 );
362 }
363
364 #[test]
365 #[cfg_attr(any(miri, not(feature = "expensive_tests")), ignore)]
366 fn test_date_trunc_year_exhaustive() {
367 let start = chrono::NaiveDate::from_ymd(1700, 1, 1).and_hms(0, 0, 0).timestamp_millis();
368 let end = chrono::NaiveDate::from_ymd(2500, 1, 1).and_hms(0, 0, 0).timestamp_millis();
369
370 for ts in (start..end).step_by(60_000) {
371 let trunc_chrono = date_trunc_year_chrono(ts);
372 let trunc_packed = date_trunc_year_timestamp_millis(ts);
373 assert_eq!(trunc_chrono, trunc_packed, "{} != {} for {}", trunc_chrono, trunc_packed, ts);
374
375 let ts = ts + 59_999;
376 let trunc_chrono = date_trunc_year_chrono(ts);
377 let trunc_packed = date_trunc_year_timestamp_millis(ts);
378 assert_eq!(trunc_chrono, trunc_packed, "{} != {} for {}", trunc_chrono, trunc_packed, ts);
379 }
380 }
381
382 #[test]
383 #[cfg_attr(any(miri, not(feature = "expensive_tests")), ignore)]
384 fn test_date_trunc_month_exhaustive() {
385 let start = chrono::NaiveDate::from_ymd(1700, 1, 1).and_hms(0, 0, 0).timestamp_millis();
386 let end = chrono::NaiveDate::from_ymd(2500, 1, 1).and_hms(0, 0, 0).timestamp_millis();
387
388 for ts in (start..end).step_by(60_000) {
389 let trunc_chrono = date_trunc_month_chrono(ts);
390 let trunc_packed = date_trunc_month_timestamp_millis(ts);
391 assert_eq!(trunc_packed, trunc_chrono, "{} != {} for {}", trunc_packed, trunc_chrono, ts);
392
393 let ts = ts + 59_999;
394 let trunc_chrono = date_trunc_month_chrono(ts);
395 let trunc_packed = date_trunc_month_timestamp_millis(ts);
396 assert_eq!(trunc_chrono, trunc_packed, "{} != {} for {}", trunc_chrono, trunc_packed, ts);
397 }
398 }
399}