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

geo-engine / geoengine / 12417045631

19 Dec 2024 04:45PM UTC coverage: 90.354% (-0.2%) from 90.512%
12417045631

Pull #998

github

web-flow
Merge 9e7b54661 into 34e12969f
Pull Request #998: quota logging wip

834 of 1211 new or added lines in 66 files covered. (68.87%)

227 existing lines in 20 files now uncovered.

133835 of 148123 relevant lines covered (90.35%)

54353.34 hits per line

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

86.8
/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._to_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 =
241✔
132
                    chrono::NaiveDateTime::parse_from_str(input, format._to_parse_format())
241✔
133
                        .map_err(|e| DateTimeError::DateParse {
241✔
134
                            source: Box::new(e),
×
135
                        })?;
241✔
136

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

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

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

163
        Ok(date_time.into())
4✔
164
    }
4✔
165

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

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

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

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

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

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

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

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

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

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

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

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

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

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

271
        Ok(date_time.into())
678✔
272
    }
678✔
273
}
274

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

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

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

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

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

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

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

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

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

326
impl From<DateTime> for chrono::DateTime<chrono::Utc> {
NEW
327
    fn from(datetime: DateTime) -> Self {
×
NEW
328
        datetime.datetime
×
NEW
329
    }
×
330
}
331

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

338
impl From<&DateTime> for chrono::DateTime<chrono::FixedOffset> {
339
    fn from(datetime: &DateTime) -> Self {
4,999✔
340
        datetime.datetime.into()
4,999✔
341
    }
4,999✔
342
}
343

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

393
enum FormatStrLoopState {
394
    Normal,
395
    Percent(String),
396
}
397

398
impl DateTimeParseFormat {
399
    pub fn custom(fmt: String) -> Self {
244✔
400
        let (has_tz, has_time) = {
244✔
401
            let mut has_tz = false;
244✔
402
            let mut has_time = false;
244✔
403

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

244✔
408
            let mut state = FormatStrLoopState::Normal;
244✔
409

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

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

431
                            state = FormatStrLoopState::Normal;
1,246✔
432
                        }
229✔
433
                    }
434
                }
435
            }
436

437
            (has_tz, has_time)
244✔
438
        };
244✔
439

244✔
440
        DateTimeParseFormat {
244✔
441
            fmt,
244✔
442
            has_tz,
244✔
443
            has_time,
244✔
444
        }
244✔
445
    }
244✔
446

447
    pub fn unix() -> Self {
4✔
448
        let fmt = "%s".to_owned();
4✔
449
        Self {
4✔
450
            fmt,
4✔
451
            has_tz: false,
4✔
452
            has_time: true,
4✔
453
        }
4✔
454
    }
4✔
455

456
    pub fn ymd() -> Self {
×
457
        let fmt = "%Y-%m-%d".to_owned();
×
458
        Self {
×
459
            fmt,
×
460
            has_tz: false,
×
461
            has_time: false,
×
462
        }
×
463
    }
×
464

465
    pub fn has_tz(&self) -> bool {
263✔
466
        self.has_tz
263✔
467
    }
263✔
468

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

473
    pub fn is_empty(&self) -> bool {
102✔
474
        self.fmt.is_empty()
102✔
475
    }
102✔
476

477
    pub fn _to_parse_format(&self) -> &str {
420✔
478
        &self.fmt
420✔
479
    }
420✔
480
}
481

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

492
impl Serialize for DateTimeParseFormat {
493
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1✔
494
    where
1✔
495
        S: serde::Serializer,
1✔
496
    {
1✔
497
        serializer.serialize_str(&self.fmt)
1✔
498
    }
1✔
499
}
500

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

513
impl<'de> Deserialize<'de> for DateTime {
514
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
239✔
515
    where
239✔
516
        D: serde::Deserializer<'de>,
239✔
517
    {
239✔
518
        let s = String::deserialize(deserializer)?;
239✔
519
        <Self as FromStr>::from_str(&s).map_err(serde::de::Error::custom)
239✔
520
    }
239✔
521
}
522

523
impl Serialize for DateTime {
524
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2,635✔
525
    where
2,635✔
526
        S: serde::Serializer,
2,635✔
527
    {
2,635✔
528
        serializer.serialize_str(&self.to_string())
2,635✔
529
    }
2,635✔
530
}
531

532
impl PartialOrd for DateTime {
533
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
345✔
534
        Some(self.cmp(other))
345✔
535
    }
345✔
536
}
537

538
impl Ord for DateTime {
539
    fn cmp(&self, other: &Self) -> Ordering {
1,582✔
540
        self.datetime.cmp(&other.datetime)
1,582✔
541
    }
1,582✔
542
}
543

544
impl Add<Duration> for DateTime {
545
    type Output = Self;
546

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

309✔
550
        chrono::DateTime::<chrono::FixedOffset>::from(self)
309✔
551
            // TODO: do we ignore the overflow?
309✔
552
            .add(duration)
309✔
553
            .into()
309✔
554
    }
309✔
555
}
556

557
impl Sub<DateTime> for DateTime {
558
    type Output = Duration;
559

560
    fn sub(self, rhs: DateTime) -> Self::Output {
59✔
561
        let duration = chrono::DateTime::<chrono::FixedOffset>::from(self)
59✔
562
            .signed_duration_since(chrono::DateTime::<chrono::FixedOffset>::from(rhs));
59✔
563

59✔
564
        Duration::milliseconds(duration.num_milliseconds())
59✔
565
    }
59✔
566
}
567

568
impl Sub<Duration> for DateTime {
569
    type Output = Self;
570

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

36✔
574
        chrono::DateTime::<chrono::FixedOffset>::from(self)
36✔
575
            // TODO: do we ignore the overflow?
36✔
576
            .sub(duration)
36✔
577
            .into()
36✔
578
    }
36✔
579
}
580

581
mod sql {
582
    use super::*;
583
    use postgres_types::{
584
        accepts, private::BytesMut, to_sql_checked, FromSql, IsNull, ToSql, Type,
585
    };
586
    use std::error::Error;
587

588
    impl<'a> FromSql<'a> for DateTime {
589
        fn from_sql(ty: &Type, raw: &'a [u8]) -> Result<Self, Box<dyn Error + Sync + Send>> {
433✔
590
            let naive = chrono::NaiveDateTime::from_sql(ty, raw)?;
433✔
591
            let datetime = chrono::DateTime::from_naive_utc_and_offset(naive, chrono::Utc);
433✔
592
            Ok(DateTime { datetime })
433✔
593
        }
433✔
594

595
        postgres_types::accepts!(TIMESTAMPTZ);
596
    }
597

598
    impl ToSql for DateTime {
599
        fn to_sql(
×
600
            &self,
×
601
            type_: &Type,
×
602
            w: &mut BytesMut,
×
603
        ) -> Result<IsNull, Box<dyn Error + Sync + Send>> {
×
604
            self.datetime.naive_utc().to_sql(type_, w)
×
605
        }
×
606

607
        accepts!(TIMESTAMPTZ);
608

609
        to_sql_checked!();
610
    }
611
}
612

613
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
614
pub struct Duration {
615
    milliseconds: i64,
616
}
617

618
/// TODO: out of bounds checks
619
impl Duration {
620
    pub const SECOND: Self = Self::milliseconds(1_000);
621
    pub const MINUTE: Self = Self::seconds(60);
622
    pub const HOUR: Self = Self::minutes(60);
623
    pub const DAY: Self = Self::hours(24);
624

625
    pub const fn days(days: i64) -> Self {
13✔
626
        Self::milliseconds(days * Self::DAY.milliseconds)
13✔
627
    }
13✔
628

629
    pub const fn hours(hours: i64) -> Self {
19✔
630
        Self::milliseconds(hours * Self::HOUR.milliseconds)
19✔
631
    }
19✔
632

633
    pub const fn minutes(minutes: i64) -> Self {
18✔
634
        Self::milliseconds(minutes * Self::MINUTE.milliseconds)
18✔
635
    }
18✔
636

637
    pub const fn seconds(seconds: i64) -> Self {
42✔
638
        Self::milliseconds(seconds * Self::SECOND.milliseconds)
42✔
639
    }
42✔
640

641
    pub const fn milliseconds(milliseconds: i64) -> Self {
787✔
642
        Self { milliseconds }
787✔
643
    }
787✔
644

645
    pub const fn num_milliseconds(self) -> i64 {
461✔
646
        self.milliseconds
461✔
647
    }
461✔
648

649
    pub const fn num_seconds(self) -> i64 {
88✔
650
        self.milliseconds / Self::SECOND.milliseconds
88✔
651
    }
88✔
652

653
    pub const fn num_minutes(self) -> i64 {
9✔
654
        self.milliseconds / Self::MINUTE.milliseconds
9✔
655
    }
9✔
656

657
    pub const fn num_hours(self) -> i64 {
7✔
658
        self.milliseconds / Self::HOUR.milliseconds
7✔
659
    }
7✔
660

661
    pub const fn num_days(self) -> i64 {
6✔
662
        self.milliseconds / Self::DAY.milliseconds
6✔
663
    }
6✔
664

665
    pub const fn is_zero(self) -> bool {
259✔
666
        self.milliseconds == 0
259✔
667
    }
259✔
668
}
669

670
// TODO: must this be checked?
671
impl Add<Duration> for Duration {
672
    type Output = Self;
673

674
    fn add(self, rhs: Duration) -> Self::Output {
×
675
        Self {
×
676
            milliseconds: self.milliseconds + rhs.milliseconds,
×
677
        }
×
678
    }
×
679
}
680

681
// TODO: must this be checked?
682
impl Sub<Duration> for Duration {
683
    type Output = Self;
684

685
    fn sub(self, rhs: Duration) -> Self::Output {
×
686
        Self {
×
687
            milliseconds: self.milliseconds - rhs.milliseconds,
×
688
        }
×
689
    }
×
690
}
691

692
#[cfg(test)]
693
mod tests {
694
    use super::*;
695

696
    #[test]
697
    fn parse() {
1✔
698
        let format = DateTimeParseFormat::custom("%Y-%m-%dT%H:%M:%S%.3f".to_string());
1✔
699

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

1✔
703
        assert_eq!(result, expected);
1✔
704
    }
1✔
705

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

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

730
    #[test]
731
    fn has_tz() {
1✔
732
        assert!(!DateTimeParseFormat::custom("%Y-%m-%dT%H:%M:%S%.3f".to_string()).has_tz());
1✔
733
        assert!(DateTimeParseFormat::custom("%Y-%m-%dT%H:%M:%S%z".to_string()).has_tz());
1✔
734
    }
1✔
735

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

742
    #[test]
743
    fn test_serialize() {
1✔
744
        assert_eq!(
1✔
745
            serde_json::to_string(&DateTime::from_str("2010-01-02T03:04:05.000Z").unwrap())
1✔
746
                .unwrap(),
1✔
747
            "\"2010-01-02T03:04:05.000Z\""
1✔
748
        );
1✔
749
    }
1✔
750

751
    #[test]
752
    fn test_as_datetime_string() {
1✔
753
        assert_eq!(
1✔
754
            DateTime::new_utc(2010, 1, 2, 3, 4, 5).to_datetime_string_with_millis(),
1✔
755
            "2010-01-02T03:04:05.000Z"
1✔
756
        );
1✔
757
    }
1✔
758

759
    #[test]
760
    fn test_datetime_negative_years() {
1✔
761
        assert_eq!(
1✔
762
            DateTime::new_utc(-2010, 1, 2, 3, 4, 5).to_datetime_string(),
1✔
763
            "-002010-01-02T03:04:05+00:00"
1✔
764
        );
1✔
765
    }
1✔
766

767
    #[test]
768
    fn test_sub_datetime_datetime() {
1✔
769
        let a = DateTime::new_utc(2010, 1, 2, 3, 4, 5);
1✔
770
        let b = DateTime::new_utc(2010, 1, 2, 3, 4, 5);
1✔
771
        assert_eq!(a - b, Duration::milliseconds(0));
1✔
772
    }
1✔
773
}
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