• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

geo-engine / geoengine / 12466077302

23 Dec 2024 11:27AM UTC coverage: 90.695% (+0.2%) from 90.509%
12466077302

push

github

web-flow
Merge pull request #1002 from geo-engine/update-deps-2024-12-06

update deps 2024-12-06

38 of 75 new or added lines in 14 files covered. (50.67%)

24 existing lines in 15 files now uncovered.

133182 of 146846 relevant lines covered (90.7%)

54823.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.19
/datatypes/src/primitives/datetime.rs
1
use crate::error::ErrorSource;
2
use crate::primitives::TimeInstance;
3
use chrono::{Datelike, NaiveDate, Timelike};
4
use postgres_types::{FromSql, ToSql};
5
use serde::{Deserialize, Serialize};
6
use snafu::Snafu;
7
use std::cmp::Ordering;
8
use std::collections::HashSet;
9
use std::fmt::Display;
10
use std::ops::{Add, Sub};
11
use std::str::FromStr;
12

13
/// An object that composes the date and a timestamp with time zone.
14
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15
pub struct DateTime {
16
    datetime: chrono::DateTime<chrono::Utc>,
17
}
18

19
impl DateTime {
20
    /// The minimum possible `DateTime`.
21
    pub const MIN: DateTime = DateTime {
22
        datetime: chrono::DateTime::<chrono::Utc>::MIN_UTC,
23
    };
24
    /// The maximum possible `DateTime`.
25
    pub const MAX: DateTime = DateTime {
26
        datetime: chrono::DateTime::<chrono::Utc>::MAX_UTC,
27
    };
28

29
    /// Creates a new `DateTime` from year, month day and hour, minute, second values.
30
    /// Assumes the time is in UTC.
31
    ///
32
    /// # Panics
33
    /// Panics if the values are out of range.
34
    ///
35
    pub fn new_utc(year: i32, month: u8, day: u8, hour: u8, minute: u8, second: u8) -> Self {
388✔
36
        let datetime = NaiveDate::from_ymd_opt(year, month.into(), day.into())
388✔
37
            .expect("should set valid date")
388✔
38
            .and_hms_opt(hour.into(), minute.into(), second.into())
388✔
39
            .expect("should set valid time");
388✔
40
        let datetime = chrono::DateTime::from_naive_utc_and_offset(datetime, chrono::Utc);
388✔
41

388✔
42
        Self { datetime }
388✔
43
    }
388✔
44

45
    /// Creates a new `DateTime` from year, month day and hour, minute, second values.
46
    /// Assumes the time is in UTC.
47
    ///
48
    /// If the date or the time would overflow, it returns `None`
49
    ///
50
    pub fn new_utc_checked(
×
51
        year: i32,
×
52
        month: u8,
×
53
        day: u8,
×
54
        hour: u8,
×
55
        minute: u8,
×
56
        second: u8,
×
57
    ) -> Option<Self> {
×
58
        let date = NaiveDate::from_ymd_opt(year, month.into(), day.into())?;
×
59
        let datetime = date.and_hms_opt(hour.into(), minute.into(), second.into())?;
×
60
        let datetime = chrono::DateTime::from_naive_utc_and_offset(datetime, chrono::Utc);
×
61

×
62
        Some(Self { datetime })
×
63
    }
×
64

65
    /// Creates a new `DateTime` from year, month day and hour, minute, second values.
66
    /// Assumes the time is in UTC.
67
    ///
68
    /// # Panics
69
    /// Panics if the values are out of range.
70
    ///
71
    pub fn new_utc_with_millis(
25✔
72
        year: i32,
25✔
73
        month: u8,
25✔
74
        day: u8,
25✔
75
        hour: u8,
25✔
76
        minute: u8,
25✔
77
        second: u8,
25✔
78
        millis: u16,
25✔
79
    ) -> Self {
25✔
80
        let datetime = NaiveDate::from_ymd_opt(year, month.into(), day.into())
25✔
81
            .expect("should set valid date")
25✔
82
            .and_hms_milli_opt(hour.into(), minute.into(), second.into(), millis.into())
25✔
83
            .expect("should set valid time");
25✔
84
        let datetime = chrono::DateTime::from_naive_utc_and_offset(datetime, chrono::Utc);
25✔
85

25✔
86
        Self { datetime }
25✔
87
    }
25✔
88

89
    /// Creates a new `DateTime` from year, month day and hour, minute, second values.
90
    /// Assumes the time is in UTC.
91
    ///
92
    /// If the date or the time would overflow, it returns `None`
93
    ///
94
    pub fn new_utc_checked_with_millis(
2,124✔
95
        year: i32,
2,124✔
96
        month: u8,
2,124✔
97
        day: u8,
2,124✔
98
        hour: u8,
2,124✔
99
        minute: u8,
2,124✔
100
        second: u8,
2,124✔
101
        millis: u16,
2,124✔
102
    ) -> Option<Self> {
2,124✔
103
        let date = NaiveDate::from_ymd_opt(year, month.into(), day.into())?;
2,124✔
104
        let datetime =
2,120✔
105
            date.and_hms_milli_opt(hour.into(), minute.into(), second.into(), millis.into())?;
2,120✔
106
        let datetime = chrono::DateTime::from_naive_utc_and_offset(datetime, chrono::Utc);
2,120✔
107

2,120✔
108
        Some(Self { datetime })
2,120✔
109
    }
2,124✔
110

111
    #[allow(clippy::missing_panics_doc)]
112
    pub fn parse_from_str(
253✔
113
        input: &str,
253✔
114
        format: &DateTimeParseFormat,
253✔
115
    ) -> Result<Self, DateTimeError> {
253✔
116
        match (format.has_time(), format.has_tz()) {
253✔
117
            (true, true) => {
118
                let datetime = chrono::DateTime::<chrono::FixedOffset>::parse_from_str(
11✔
119
                    input,
11✔
120
                    format.parse_format(),
11✔
121
                )
11✔
122
                .map_err(|e| DateTimeError::DateParse {
11✔
123
                    source: Box::new(e),
×
124
                })?;
11✔
125

126
                Ok(Self {
11✔
127
                    datetime: datetime.into(),
11✔
128
                })
11✔
129
            }
130
            (true, false) => {
131
                let datetime = chrono::NaiveDateTime::parse_from_str(input, format.parse_format())
241✔
132
                    .map_err(|e| DateTimeError::DateParse {
241✔
NEW
133
                        source: Box::new(e),
×
134
                    })?;
241✔
135

136
                Ok(Self {
241✔
137
                    datetime: chrono::DateTime::from_naive_utc_and_offset(datetime, chrono::Utc),
241✔
138
                })
241✔
139
            }
140
            (false, true) => Err(DateTimeError::CannotParseOnlyDateWithTimeZone),
×
141
            (false, false) => {
142
                let datetime = chrono::NaiveDate::parse_from_str(input, format.parse_format())
1✔
143
                    .map_err(|e| DateTimeError::DateParse {
1✔
144
                        source: Box::new(e),
×
145
                    })?
1✔
146
                    .and_hms_opt(0, 0, 0)
1✔
147
                    .expect("`00:00:00` should be a valid time");
1✔
148

1✔
149
                Ok(Self {
1✔
150
                    datetime: chrono::DateTime::from_naive_utc_and_offset(datetime, chrono::Utc),
1✔
151
                })
1✔
152
            }
153
        }
154
    }
253✔
155

156
    pub fn parse_from_rfc3339(input: &str) -> Result<Self, DateTimeError> {
2✔
157
        let date_time = chrono::DateTime::<chrono::FixedOffset>::parse_from_rfc3339(input)
2✔
158
            .map_err(|e| DateTimeError::DateParse {
2✔
159
                source: Box::new(e),
×
160
            })?;
2✔
161

162
        Ok(date_time.into())
2✔
163
    }
2✔
164

165
    pub fn format(&self, format: &DateTimeParseFormat) -> String {
159✔
166
        let chrono_date_time: chrono::DateTime<chrono::FixedOffset> = self.into();
159✔
167
        let parse_format = format.parse_format();
159✔
168

159✔
169
        chrono_date_time.format(parse_format).to_string()
159✔
170
    }
159✔
171

172
    /// Format the `DateTime` as a string similar to the RFC 3339 standard.
173
    ///
174
    /// Note: negative numbers will the padded to 6 digits.
175
    pub fn to_datetime_string(self) -> String {
107✔
176
        let chrono_date_time: chrono::DateTime<chrono::FixedOffset> = self.into();
107✔
177

107✔
178
        if chrono_date_time.year() < 0 {
107✔
179
            chrono_date_time
22✔
180
                .format(&format!(
22✔
181
                    "{year:07}-%m-%dT%H:%M:%S%.f+00:00",
22✔
182
                    year = chrono_date_time.year()
22✔
183
                ))
22✔
184
                .to_string()
22✔
185
        } else {
186
            chrono_date_time
85✔
187
                .format("%Y-%m-%dT%H:%M:%S%.f+00:00")
85✔
188
                .to_string()
85✔
189
        }
190
    }
107✔
191

192
    /// Format the `DateTime` as a string similar to the RFC 3339 standard.
193
    /// Format fractional seconds as milliseconds and use Z for timezone.
194
    ///
195
    /// Note: negative numbers will the padded to 6 digits.
196
    pub fn to_datetime_string_with_millis(self) -> String {
4,224✔
197
        let chrono_date_time: chrono::DateTime<chrono::FixedOffset> = self.into();
4,224✔
198

4,224✔
199
        if chrono_date_time.year() < 0 {
4,224✔
200
            chrono_date_time
1✔
201
                .format(&format!(
1✔
202
                    "{year:07}-%m-%dT%H:%M:%S%.3fZ",
1✔
203
                    year = chrono_date_time.year()
1✔
204
                ))
1✔
205
                .to_string()
1✔
206
        } else {
207
            chrono_date_time
4,223✔
208
                .format("%Y-%m-%dT%H:%M:%S%.3fZ")
4,223✔
209
                .to_string()
4,223✔
210
        }
211
    }
4,224✔
212

213
    /// Now in UTC.
214
    pub fn now() -> Self {
286,470✔
215
        chrono::offset::Utc::now().into()
286,470✔
216
    }
286,470✔
217

218
    pub fn day_of_year(&self) -> u16 {
78✔
219
        self.datetime.ordinal() as u16
78✔
220
    }
78✔
221

222
    pub fn year(&self) -> i32 {
3,004✔
223
        self.datetime.year()
3,004✔
224
    }
3,004✔
225
    pub fn month(&self) -> u8 {
3,052✔
226
        self.datetime.month() as u8
3,052✔
227
    }
3,052✔
228

229
    /// Zero-based month from 0-11.
230
    pub fn month0(&self) -> u8 {
1,784✔
231
        self.month() - 1
1,784✔
232
    }
1,784✔
233

234
    pub fn day(&self) -> u8 {
2,384✔
235
        self.datetime.day() as u8
2,384✔
236
    }
2,384✔
237
    pub fn hour(&self) -> u8 {
2,126✔
238
        self.datetime.hour() as u8
2,126✔
239
    }
2,126✔
240
    pub fn minute(&self) -> u8 {
2,126✔
241
        self.datetime.minute() as u8
2,126✔
242
    }
2,126✔
243
    pub fn second(&self) -> u8 {
2,126✔
244
        self.datetime.second() as u8
2,126✔
245
    }
2,126✔
246
    pub fn millisecond(&self) -> u16 {
2,124✔
247
        self.datetime.timestamp_subsec_millis() as u16
2,124✔
248
    }
2,124✔
249

250
    /// Creates a new `DateTime` from the original one by changing the year.
251
    ///
252
    /// Returns `None` if the year overflows the date.
253
    pub fn with_year(&self, year: i32) -> Option<Self> {
×
254
        Some(Self {
×
255
            datetime: self.datetime.with_year(year)?,
×
256
        })
257
    }
×
258
}
259

260
impl FromStr for DateTime {
261
    type Err = DateTimeError;
262

263
    fn from_str(input: &str) -> Result<Self, Self::Err> {
672✔
264
        let date_time = chrono::DateTime::<chrono::FixedOffset>::from_str(input).map_err(|e| {
672✔
265
            DateTimeError::DateParse {
×
266
                source: Box::new(e),
×
267
            }
×
268
        })?;
672✔
269

270
        Ok(date_time.into())
672✔
271
    }
672✔
272
}
273

274
impl Display for DateTime {
275
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2,612✔
276
        write!(f, "{}", self.to_datetime_string_with_millis())
2,612✔
277
    }
2,612✔
278
}
279

280
impl From<DateTime> for TimeInstance {
281
    fn from(datetime: DateTime) -> Self {
3,638✔
282
        Self::from(&datetime)
3,638✔
283
    }
3,638✔
284
}
285

286
impl From<&DateTime> for TimeInstance {
287
    fn from(datetime: &DateTime) -> Self {
3,644✔
288
        TimeInstance::from_millis_unchecked(datetime.datetime.timestamp_millis())
3,644✔
289
    }
3,644✔
290
}
291

292
impl TryFrom<TimeInstance> for DateTime {
293
    type Error = DateTimeError;
294

295
    fn try_from(time_instance: TimeInstance) -> Result<Self, Self::Error> {
4,965✔
296
        use chrono::TimeZone;
297

298
        if time_instance < TimeInstance::MIN || time_instance > TimeInstance::MAX {
4,965✔
299
            return Err(DateTimeError::OutOfBounds);
×
300
        }
4,965✔
301

4,965✔
302
        match chrono::Utc.timestamp_millis_opt(time_instance.inner()) {
4,965✔
303
            chrono::LocalResult::Single(datetime) => Ok(Self { datetime }),
4,965✔
304
            chrono::LocalResult::None | chrono::LocalResult::Ambiguous(_, _) => {
305
                Err(DateTimeError::OutOfBounds)
×
306
            }
307
        }
308
    }
4,965✔
309
}
310

311
impl From<chrono::DateTime<chrono::FixedOffset>> for DateTime {
312
    fn from(datetime: chrono::DateTime<chrono::FixedOffset>) -> Self {
1,033✔
313
        Self {
1,033✔
314
            datetime: datetime.into(),
1,033✔
315
        }
1,033✔
316
    }
1,033✔
317
}
318

319
impl From<chrono::DateTime<chrono::Utc>> for DateTime {
320
    fn from(datetime: chrono::DateTime<chrono::Utc>) -> Self {
288,357✔
321
        Self { datetime }
288,357✔
322
    }
288,357✔
323
}
324

325
impl From<DateTime> for chrono::DateTime<chrono::FixedOffset> {
326
    fn from(datetime: DateTime) -> Self {
4,794✔
327
        Self::from(&datetime)
4,794✔
328
    }
4,794✔
329
}
330

331
impl From<&DateTime> for chrono::DateTime<chrono::FixedOffset> {
332
    fn from(datetime: &DateTime) -> Self {
4,953✔
333
        datetime.datetime.into()
4,953✔
334
    }
4,953✔
335
}
336

337
impl From<DateTime> for chrono::DateTime<chrono::Utc> {
NEW
338
    fn from(datetime: DateTime) -> Self {
×
NEW
339
        Self::from(&datetime)
×
NEW
340
    }
×
341
}
342

343
impl From<&DateTime> for chrono::DateTime<chrono::Utc> {
NEW
344
    fn from(datetime: &DateTime) -> Self {
×
NEW
345
        datetime.datetime
×
NEW
346
    }
×
347
}
348

349
/// We allow C's strftime parameters:
350
///
351
/// | Specifier |                               Replaced By                              |          Example         |
352
/// |:---------:|:----------------------------------------------------------------------:|:------------------------:|
353
/// | %a        | Abbreviated weekday name                                               | Sun                      |
354
/// | %A        | Full weekday name                                                      | Sunday                   |
355
/// | %b        | Abbreviated month name                                                 | Mar                      |
356
/// | %B        | Full month name                                                        | March                    |
357
/// | %c        | Date and time representation                                           | Sun Aug 19 02:56:02 2012 |
358
/// | %d        | Day of the month (01-31)                                               | 19                       |
359
/// | %H        | Hour in 24h format (00-23)                                             | 14                       |
360
/// | %I        | Hour in 12h format (01-12)                                             | 05                       |
361
/// | %j        | Day of the year (001-366)                                              | 231                      |
362
/// | %m        | Month as a decimal number (01-12)                                      | 08                       |
363
/// | %M        | Minute (00-59)                                                         | 55                       |
364
/// | %p        | AM or PM designation                                                   | PM                       |
365
/// | %S        | Second (00-61)                                                         | 02                       |
366
/// | %U        | Week number with the first Sunday as the first day of week one (00-53) | 33                       |
367
/// | %w        | Weekday as a decimal number with Sunday as 0 (0-6)                     | 4                        |
368
/// | %W        | Week number with the first Monday as the first day of week one (00-53) | 34                       |
369
/// | %x        | Date representation                                                    | 08/19/12                 |
370
/// | %X        | Time representation                                                    | 02:50:06                 |
371
/// | %y        | Year, last two digits (00-99)                                          | 01                       |
372
/// | %Y        | Year                                                                   | 2012                     |
373
/// | %Z        | Timezone name or abbreviation                                          | CDT                      |
374
/// | %z        | Timezone offset                                                        | +0930                    |
375
/// | %:z       | Timezone offset with colon                                             | +09:30                   |
376
/// | %%        | A % sign                                                               | %                        |
377
///
378
/// Additionally, we allow some specifiers for subseconds:
379
///
380
/// | Specifier | Replaced By                                                            | Example    |
381
/// |-----------|------------------------------------------------------------------------|------------|
382
/// | %f        | The fractional seconds (in nanoseconds) since last whole second. 6     | 026490000  |
383
/// | %.f       | Similar to .%f but left-aligned. These all consume the leading dot. 6  | .026490    |
384
/// | %.3f      | Similar to .%f but left-aligned but fixed to a length of 3. 6          | .026       |
385
/// | %.6f      | Similar to .%f but left-aligned but fixed to a length of 6. 6          | .026490    |
386
/// | %.9f      | Similar to .%f but left-aligned but fixed to a length of 9. 6          | .026490000 |
387
/// | %3f       | Similar to %.3f but without the leading dot. 6                         | 026        |
388
/// | %6f       | Similar to %.6f but without the leading dot. 6                         | 026490     |
389
/// | %9f       | Similar to %.9f but without the leading dot. 6                         | 026490000  |
390
///
391
#[derive(Debug, Clone, PartialEq, Eq, FromSql, ToSql)]
6,622✔
392
pub struct DateTimeParseFormat {
393
    fmt: String,
394
    has_tz: bool,
395
    has_time: bool,
396
}
397

398
enum FormatStrLoopState {
399
    Normal,
400
    Percent(String),
401
}
402

403
impl DateTimeParseFormat {
404
    pub fn custom(fmt: String) -> Self {
244✔
405
        let (has_tz, has_time) = {
244✔
406
            let mut has_tz = false;
244✔
407
            let mut has_time = false;
244✔
408

244✔
409
            let has_tz_values: HashSet<&str> = ["%Z", "%z", "%:z"].into();
244✔
410
            let has_time_values: HashSet<&str> =
244✔
411
                ["%a", "%A", "%M", "%p", "%S", "%X", "%H", "%I"].into();
244✔
412

244✔
413
            let mut state = FormatStrLoopState::Normal;
244✔
414

415
            for c in fmt.chars() {
3,622✔
416
                match state {
3,622✔
417
                    FormatStrLoopState::Normal => {
418
                        if c == '%' {
2,147✔
419
                            state = FormatStrLoopState::Percent("%".to_string());
1,246✔
420
                        }
1,246✔
421
                    }
422
                    FormatStrLoopState::Percent(ref mut s) => {
1,475✔
423
                        s.push(c);
1,475✔
424

1,475✔
425
                        if c == '%' {
1,475✔
426
                            // was escaped percentage
×
427
                            state = FormatStrLoopState::Normal;
×
428
                        } else if c.is_ascii_alphabetic() {
1,475✔
429
                            if has_tz_values.contains(s.as_str()) {
1,246✔
430
                                has_tz = true;
5✔
431
                            }
1,241✔
432
                            if has_time_values.contains(s.as_str()) {
1,246✔
433
                                has_time = true;
399✔
434
                            }
847✔
435

436
                            state = FormatStrLoopState::Normal;
1,246✔
437
                        }
229✔
438
                    }
439
                }
440
            }
441

442
            (has_tz, has_time)
244✔
443
        };
244✔
444

244✔
445
        DateTimeParseFormat {
244✔
446
            fmt,
244✔
447
            has_tz,
244✔
448
            has_time,
244✔
449
        }
244✔
450
    }
244✔
451

452
    pub fn unix() -> Self {
4✔
453
        let fmt = "%s".to_owned();
4✔
454
        Self {
4✔
455
            fmt,
4✔
456
            has_tz: false,
4✔
457
            has_time: true,
4✔
458
        }
4✔
459
    }
4✔
460

461
    pub fn ymd() -> Self {
×
462
        let fmt = "%Y-%m-%d".to_owned();
×
463
        Self {
×
464
            fmt,
×
465
            has_tz: false,
×
466
            has_time: false,
×
467
        }
×
468
    }
×
469

470
    pub fn has_tz(&self) -> bool {
263✔
471
        self.has_tz
263✔
472
    }
263✔
473

474
    pub fn has_time(&self) -> bool {
263✔
475
        self.has_time
263✔
476
    }
263✔
477

478
    pub fn is_empty(&self) -> bool {
102✔
479
        self.fmt.is_empty()
102✔
480
    }
102✔
481

482
    pub fn parse_format(&self) -> &str {
420✔
483
        &self.fmt
420✔
484
    }
420✔
485
}
486

487
impl<'de> Deserialize<'de> for DateTimeParseFormat {
488
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
4✔
489
    where
4✔
490
        D: serde::Deserializer<'de>,
4✔
491
    {
4✔
492
        let s = String::deserialize(deserializer)?;
4✔
493
        Ok(Self::custom(s))
4✔
494
    }
4✔
495
}
496

497
impl Serialize for DateTimeParseFormat {
498
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1✔
499
    where
1✔
500
        S: serde::Serializer,
1✔
501
    {
1✔
502
        serializer.serialize_str(&self.fmt)
1✔
503
    }
1✔
504
}
505

506
#[derive(Debug, Snafu)]
×
507
#[snafu(visibility(pub(crate)))]
508
#[snafu(context(suffix(false)), module(error))] // disables default `Snafu` suffix
509
pub enum DateTimeError {
510
    #[snafu(display("Failed to parse date: {}", source))]
511
    DateParse {
512
        source: Box<dyn ErrorSource>,
513
    },
514
    CannotParseOnlyDateWithTimeZone,
515
    OutOfBounds,
516
}
517

518
impl<'de> Deserialize<'de> for DateTime {
519
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
233✔
520
    where
233✔
521
        D: serde::Deserializer<'de>,
233✔
522
    {
233✔
523
        let s = String::deserialize(deserializer)?;
233✔
524
        <Self as FromStr>::from_str(&s).map_err(serde::de::Error::custom)
233✔
525
    }
233✔
526
}
527

528
impl Serialize for DateTime {
529
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2,589✔
530
    where
2,589✔
531
        S: serde::Serializer,
2,589✔
532
    {
2,589✔
533
        serializer.serialize_str(&self.to_string())
2,589✔
534
    }
2,589✔
535
}
536

537
impl PartialOrd for DateTime {
538
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
345✔
539
        Some(self.cmp(other))
345✔
540
    }
345✔
541
}
542

543
impl Ord for DateTime {
544
    fn cmp(&self, other: &Self) -> Ordering {
1,582✔
545
        self.datetime.cmp(&other.datetime)
1,582✔
546
    }
1,582✔
547
}
548

549
impl Add<Duration> for DateTime {
550
    type Output = Self;
551

552
    fn add(self, rhs: Duration) -> Self::Output {
309✔
553
        let duration = chrono::Duration::milliseconds(rhs.num_milliseconds());
309✔
554

309✔
555
        chrono::DateTime::<chrono::FixedOffset>::from(self)
309✔
556
            // TODO: do we ignore the overflow?
309✔
557
            .add(duration)
309✔
558
            .into()
309✔
559
    }
309✔
560
}
561

562
impl Sub<DateTime> for DateTime {
563
    type Output = Duration;
564

565
    fn sub(self, rhs: DateTime) -> Self::Output {
59✔
566
        let duration = chrono::DateTime::<chrono::FixedOffset>::from(self)
59✔
567
            .signed_duration_since(chrono::DateTime::<chrono::FixedOffset>::from(rhs));
59✔
568

59✔
569
        Duration::milliseconds(duration.num_milliseconds())
59✔
570
    }
59✔
571
}
572

573
impl Sub<Duration> for DateTime {
574
    type Output = Self;
575

576
    fn sub(self, rhs: Duration) -> Self::Output {
36✔
577
        let duration = chrono::Duration::milliseconds(rhs.num_milliseconds());
36✔
578

36✔
579
        chrono::DateTime::<chrono::FixedOffset>::from(self)
36✔
580
            // TODO: do we ignore the overflow?
36✔
581
            .sub(duration)
36✔
582
            .into()
36✔
583
    }
36✔
584
}
585

586
mod sql {
587
    use super::*;
588
    use postgres_types::{
589
        accepts, private::BytesMut, to_sql_checked, FromSql, IsNull, ToSql, Type,
590
    };
591
    use std::error::Error;
592

593
    impl<'a> FromSql<'a> for DateTime {
594
        fn from_sql(ty: &Type, raw: &'a [u8]) -> Result<Self, Box<dyn Error + Sync + Send>> {
411✔
595
            let naive = chrono::NaiveDateTime::from_sql(ty, raw)?;
411✔
596
            let datetime = chrono::DateTime::from_naive_utc_and_offset(naive, chrono::Utc);
411✔
597
            Ok(DateTime { datetime })
411✔
598
        }
411✔
599

600
        postgres_types::accepts!(TIMESTAMPTZ);
601
    }
602

603
    impl ToSql for DateTime {
604
        fn to_sql(
×
605
            &self,
×
606
            type_: &Type,
×
607
            w: &mut BytesMut,
×
608
        ) -> Result<IsNull, Box<dyn Error + Sync + Send>> {
×
609
            self.datetime.naive_utc().to_sql(type_, w)
×
610
        }
×
611

612
        accepts!(TIMESTAMPTZ);
613

614
        to_sql_checked!();
615
    }
616
}
617

618
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
619
pub struct Duration {
620
    milliseconds: i64,
621
}
622

623
/// TODO: out of bounds checks
624
impl Duration {
625
    pub const SECOND: Self = Self::milliseconds(1_000);
626
    pub const MINUTE: Self = Self::seconds(60);
627
    pub const HOUR: Self = Self::minutes(60);
628
    pub const DAY: Self = Self::hours(24);
629

630
    pub const fn days(days: i64) -> Self {
13✔
631
        Self::milliseconds(days * Self::DAY.milliseconds)
13✔
632
    }
13✔
633

634
    pub const fn hours(hours: i64) -> Self {
19✔
635
        Self::milliseconds(hours * Self::HOUR.milliseconds)
19✔
636
    }
19✔
637

638
    pub const fn minutes(minutes: i64) -> Self {
18✔
639
        Self::milliseconds(minutes * Self::MINUTE.milliseconds)
18✔
640
    }
18✔
641

642
    pub const fn seconds(seconds: i64) -> Self {
42✔
643
        Self::milliseconds(seconds * Self::SECOND.milliseconds)
42✔
644
    }
42✔
645

646
    pub const fn milliseconds(milliseconds: i64) -> Self {
787✔
647
        Self { milliseconds }
787✔
648
    }
787✔
649

650
    pub const fn num_milliseconds(self) -> i64 {
461✔
651
        self.milliseconds
461✔
652
    }
461✔
653

654
    pub const fn num_seconds(self) -> i64 {
88✔
655
        self.milliseconds / Self::SECOND.milliseconds
88✔
656
    }
88✔
657

658
    pub const fn num_minutes(self) -> i64 {
9✔
659
        self.milliseconds / Self::MINUTE.milliseconds
9✔
660
    }
9✔
661

662
    pub const fn num_hours(self) -> i64 {
7✔
663
        self.milliseconds / Self::HOUR.milliseconds
7✔
664
    }
7✔
665

666
    pub const fn num_days(self) -> i64 {
6✔
667
        self.milliseconds / Self::DAY.milliseconds
6✔
668
    }
6✔
669

670
    pub const fn is_zero(self) -> bool {
259✔
671
        self.milliseconds == 0
259✔
672
    }
259✔
673
}
674

675
// TODO: must this be checked?
676
impl Add<Duration> for Duration {
677
    type Output = Self;
678

679
    fn add(self, rhs: Duration) -> Self::Output {
×
680
        Self {
×
681
            milliseconds: self.milliseconds + rhs.milliseconds,
×
682
        }
×
683
    }
×
684
}
685

686
// TODO: must this be checked?
687
impl Sub<Duration> for Duration {
688
    type Output = Self;
689

690
    fn sub(self, rhs: Duration) -> Self::Output {
×
691
        Self {
×
692
            milliseconds: self.milliseconds - rhs.milliseconds,
×
693
        }
×
694
    }
×
695
}
696

697
#[cfg(test)]
698
mod tests {
699
    use super::*;
700

701
    #[test]
702
    fn parse() {
1✔
703
        let format = DateTimeParseFormat::custom("%Y-%m-%dT%H:%M:%S%.3f".to_string());
1✔
704

1✔
705
        let result = DateTime::parse_from_str("2020-01-02T03:04:05.006", &format).unwrap();
1✔
706
        let expected = DateTime::new_utc_with_millis(2020, 1, 2, 3, 4, 5, 6);
1✔
707

1✔
708
        assert_eq!(result, expected);
1✔
709
    }
1✔
710

711
    #[test]
712
    fn to_string() {
1✔
713
        assert_eq!(
1✔
714
            DateTime::new_utc(2010, 1, 2, 3, 4, 5).to_string(),
1✔
715
            "2010-01-02T03:04:05.000Z"
1✔
716
        );
1✔
717
        assert_eq!(
1✔
718
            DateTime::new_utc(-2010, 1, 2, 3, 4, 5).to_string(),
1✔
719
            "-002010-01-02T03:04:05.000Z"
1✔
720
        );
1✔
721
    }
1✔
722

723
    #[test]
724
    fn from_string() {
1✔
725
        assert_eq!(
1✔
726
            DateTime::new_utc(2010, 1, 2, 3, 4, 5),
1✔
727
            DateTime::from_str("2010-01-02T03:04:05.000Z").unwrap()
1✔
728
        );
1✔
729
        assert_eq!(
1✔
730
            DateTime::new_utc(-2010, 1, 2, 3, 4, 5),
1✔
731
            DateTime::from_str("-002010-01-02T03:04:05.000Z").unwrap()
1✔
732
        );
1✔
733
    }
1✔
734

735
    #[test]
736
    fn has_tz() {
1✔
737
        assert!(!DateTimeParseFormat::custom("%Y-%m-%dT%H:%M:%S%.3f".to_string()).has_tz());
1✔
738
        assert!(DateTimeParseFormat::custom("%Y-%m-%dT%H:%M:%S%z".to_string()).has_tz());
1✔
739
    }
1✔
740

741
    #[test]
742
    fn has_time() {
1✔
743
        assert!(!DateTimeParseFormat::custom("%Y-%m-%d".to_string()).has_time());
1✔
744
        assert!(DateTimeParseFormat::custom("%Y-%m-%dT%H:%M:%S%z".to_string()).has_time());
1✔
745
    }
1✔
746

747
    #[test]
748
    fn test_serialize() {
1✔
749
        assert_eq!(
1✔
750
            serde_json::to_string(&DateTime::from_str("2010-01-02T03:04:05.000Z").unwrap())
1✔
751
                .unwrap(),
1✔
752
            "\"2010-01-02T03:04:05.000Z\""
1✔
753
        );
1✔
754
    }
1✔
755

756
    #[test]
757
    fn test_as_datetime_string() {
1✔
758
        assert_eq!(
1✔
759
            DateTime::new_utc(2010, 1, 2, 3, 4, 5).to_datetime_string_with_millis(),
1✔
760
            "2010-01-02T03:04:05.000Z"
1✔
761
        );
1✔
762
    }
1✔
763

764
    #[test]
765
    fn test_datetime_negative_years() {
1✔
766
        assert_eq!(
1✔
767
            DateTime::new_utc(-2010, 1, 2, 3, 4, 5).to_datetime_string(),
1✔
768
            "-002010-01-02T03:04:05+00:00"
1✔
769
        );
1✔
770
    }
1✔
771

772
    #[test]
773
    fn test_sub_datetime_datetime() {
1✔
774
        let a = DateTime::new_utc(2010, 1, 2, 3, 4, 5);
1✔
775
        let b = DateTime::new_utc(2010, 1, 2, 3, 4, 5);
1✔
776
        assert_eq!(a - b, Duration::milliseconds(0));
1✔
777
    }
1✔
778
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc