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

geo-engine / geoengine / 8158689789

05 Mar 2024 03:19PM UTC coverage: 90.58% (+0.2%) from 90.424%
8158689789

push

github

web-flow
Merge pull request #931 from geo-engine/netcdf-overviews

NetCDF Overview Metadata in Database

2393 of 2553 new or added lines in 21 files covered. (93.73%)

37 existing lines in 12 files now uncovered.

128580 of 141952 relevant lines covered (90.58%)

54451.48 hits per line

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

86.43
/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)]
490✔
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 {
382✔
36
        let datetime = NaiveDate::from_ymd_opt(year, month.into(), day.into())
382✔
37
            .expect("should set valid date")
382✔
38
            .and_hms_opt(hour.into(), minute.into(), second.into())
382✔
39
            .expect("should set valid time");
382✔
40
        let datetime = chrono::DateTime::from_naive_utc_and_offset(datetime, chrono::Utc);
382✔
41

382✔
42
        Self { datetime }
382✔
43
    }
382✔
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
    ///
UNCOV
50
    pub fn new_utc_checked(
×
UNCOV
51
        year: i32,
×
UNCOV
52
        month: u8,
×
UNCOV
53
        day: u8,
×
UNCOV
54
        hour: u8,
×
UNCOV
55
        minute: u8,
×
UNCOV
56
        second: u8,
×
UNCOV
57
    ) -> Option<Self> {
×
UNCOV
58
        let date = NaiveDate::from_ymd_opt(year, month.into(), day.into())?;
×
UNCOV
59
        let datetime = date.and_hms_opt(hour.into(), minute.into(), second.into())?;
×
UNCOV
60
        let datetime = chrono::DateTime::from_naive_utc_and_offset(datetime, chrono::Utc);
×
UNCOV
61

×
UNCOV
62
        Some(Self { datetime })
×
UNCOV
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(
780✔
95
        year: i32,
780✔
96
        month: u8,
780✔
97
        day: u8,
780✔
98
        hour: u8,
780✔
99
        minute: u8,
780✔
100
        second: u8,
780✔
101
        millis: u16,
780✔
102
    ) -> Option<Self> {
780✔
103
        let date = NaiveDate::from_ymd_opt(year, month.into(), day.into())?;
780✔
104
        let datetime =
776✔
105
            date.and_hms_milli_opt(hour.into(), minute.into(), second.into(), millis.into())?;
776✔
106
        let datetime = chrono::DateTime::from_naive_utc_and_offset(datetime, chrono::Utc);
776✔
107

776✔
108
        Some(Self { datetime })
776✔
109
    }
780✔
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> {
×
158
        let date_time = chrono::DateTime::<chrono::FixedOffset>::parse_from_rfc3339(input)
×
159
            .map_err(|e| DateTimeError::DateParse {
×
160
                source: Box::new(e),
×
161
            })?;
×
162

163
        Ok(date_time.into())
×
164
    }
×
165

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

156✔
170
        chrono_date_time.format(parse_format).to_string()
156✔
171
    }
156✔
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 {
105✔
177
        let chrono_date_time: chrono::DateTime<chrono::FixedOffset> = self.into();
105✔
178

105✔
179
        if chrono_date_time.year() < 0 {
105✔
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
83✔
188
                .format("%Y-%m-%dT%H:%M:%S%.f+00:00")
83✔
189
                .to_string()
83✔
190
        }
191
    }
105✔
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 {
3,796✔
198
        let chrono_date_time: chrono::DateTime<chrono::FixedOffset> = self.into();
3,796✔
199

3,796✔
200
        if chrono_date_time.year() < 0 {
3,796✔
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
3,795✔
209
                .format("%Y-%m-%dT%H:%M:%S%.3fZ")
3,795✔
210
                .to_string()
3,795✔
211
        }
212
    }
3,796✔
213

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

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

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

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

235
    pub fn day(&self) -> u8 {
1,040✔
236
        self.datetime.day() as u8
1,040✔
237
    }
1,040✔
238
    pub fn hour(&self) -> u8 {
782✔
239
        self.datetime.hour() as u8
782✔
240
    }
782✔
241
    pub fn minute(&self) -> u8 {
782✔
242
        self.datetime.minute() as u8
782✔
243
    }
782✔
244
    pub fn second(&self) -> u8 {
782✔
245
        self.datetime.second() as u8
782✔
246
    }
782✔
247
    pub fn millisecond(&self) -> u16 {
780✔
248
        self.datetime.timestamp_subsec_millis() as u16
780✔
249
    }
780✔
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> {
610✔
265
        let date_time = chrono::DateTime::<chrono::FixedOffset>::from_str(input).map_err(|e| {
610✔
266
            DateTimeError::DateParse {
×
267
                source: Box::new(e),
×
268
            }
×
269
        })?;
610✔
270

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

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

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

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

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

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

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

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

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

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

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

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

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

387
enum FormatStrLoopState {
388
    Normal,
389
    Percent(String),
390
}
391

392
impl DateTimeParseFormat {
393
    pub fn custom(fmt: String) -> Self {
239✔
394
        let (has_tz, has_time) = {
239✔
395
            let mut has_tz = false;
239✔
396
            let mut has_time = false;
239✔
397

239✔
398
            let has_tz_values: HashSet<&str> = ["%Z", "%z", "%:z"].into();
239✔
399
            let has_time_values: HashSet<&str> =
239✔
400
                ["%a", "%A", "%M", "%p", "%S", "%X", "%H", "%I"].into();
239✔
401

239✔
402
            let mut state = FormatStrLoopState::Normal;
239✔
403

404
            for c in fmt.chars() {
3,582✔
405
                match state {
3,582✔
406
                    FormatStrLoopState::Normal => {
407
                        if c == '%' {
2,122✔
408
                            state = FormatStrLoopState::Percent("%".to_string());
1,231✔
409
                        }
1,231✔
410
                    }
411
                    FormatStrLoopState::Percent(ref mut s) => {
1,460✔
412
                        s.push(c);
1,460✔
413

1,460✔
414
                        if c == '%' {
1,460✔
415
                            // was escaped percentage
×
416
                            state = FormatStrLoopState::Normal;
×
417
                        } else if c.is_ascii_alphabetic() {
1,460✔
418
                            if has_tz_values.contains(s.as_str()) {
1,231✔
419
                                has_tz = true;
5✔
420
                            }
1,226✔
421
                            if has_time_values.contains(s.as_str()) {
1,231✔
422
                                has_time = true;
399✔
423
                            }
832✔
424

425
                            state = FormatStrLoopState::Normal;
1,231✔
426
                        }
229✔
427
                    }
428
                }
429
            }
430

431
            (has_tz, has_time)
239✔
432
        };
239✔
433

239✔
434
        DateTimeParseFormat {
239✔
435
            fmt,
239✔
436
            has_tz,
239✔
437
            has_time,
239✔
438
        }
239✔
439
    }
239✔
440

441
    pub fn unix() -> Self {
4✔
442
        let fmt = "%s".to_owned();
4✔
443
        Self {
4✔
444
            fmt,
4✔
445
            has_tz: false,
4✔
446
            has_time: true,
4✔
447
        }
4✔
448
    }
4✔
449

UNCOV
450
    pub fn ymd() -> Self {
×
UNCOV
451
        let fmt = "%Y-%m-%d".to_owned();
×
UNCOV
452
        Self {
×
UNCOV
453
            fmt,
×
UNCOV
454
            has_tz: false,
×
UNCOV
455
            has_time: false,
×
UNCOV
456
        }
×
UNCOV
457
    }
×
458

459
    pub fn has_tz(&self) -> bool {
259✔
460
        self.has_tz
259✔
461
    }
259✔
462

463
    pub fn has_time(&self) -> bool {
259✔
464
        self.has_time
259✔
465
    }
259✔
466

467
    pub fn is_empty(&self) -> bool {
102✔
468
        self.fmt.is_empty()
102✔
469
    }
102✔
470

471
    pub fn _to_parse_format(&self) -> &str {
413✔
472
        &self.fmt
413✔
473
    }
413✔
474
}
475

476
impl<'de> Deserialize<'de> for DateTimeParseFormat {
477
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
4✔
478
    where
4✔
479
        D: serde::Deserializer<'de>,
4✔
480
    {
4✔
481
        let s = String::deserialize(deserializer)?;
4✔
482
        Ok(Self::custom(s))
4✔
483
    }
4✔
484
}
485

486
impl Serialize for DateTimeParseFormat {
487
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1✔
488
    where
1✔
489
        S: serde::Serializer,
1✔
490
    {
1✔
491
        serializer.serialize_str(&self.fmt)
1✔
492
    }
1✔
493
}
494

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

507
impl<'de> Deserialize<'de> for DateTime {
508
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
189✔
509
    where
189✔
510
        D: serde::Deserializer<'de>,
189✔
511
    {
189✔
512
        let s = String::deserialize(deserializer)?;
189✔
513
        <Self as FromStr>::from_str(&s).map_err(serde::de::Error::custom)
189✔
514
    }
189✔
515
}
516

517
impl Serialize for DateTime {
518
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2,565✔
519
    where
2,565✔
520
        S: serde::Serializer,
2,565✔
521
    {
2,565✔
522
        serializer.serialize_str(&self.to_string())
2,565✔
523
    }
2,565✔
524
}
525

526
impl PartialOrd for DateTime {
527
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
94✔
528
        Some(self.cmp(other))
94✔
529
    }
94✔
530
}
531

532
impl Ord for DateTime {
533
    fn cmp(&self, other: &Self) -> Ordering {
1,321✔
534
        self.datetime.cmp(&other.datetime)
1,321✔
535
    }
1,321✔
536
}
537

538
impl Add<Duration> for DateTime {
539
    type Output = Self;
540

541
    fn add(self, rhs: Duration) -> Self::Output {
254✔
542
        let duration = chrono::Duration::milliseconds(rhs.num_milliseconds());
254✔
543

254✔
544
        chrono::DateTime::<chrono::FixedOffset>::from(self)
254✔
545
            // TODO: do we ignore the overflow?
254✔
546
            .add(duration)
254✔
547
            .into()
254✔
548
    }
254✔
549
}
550

551
impl Sub<DateTime> for DateTime {
552
    type Output = Duration;
553

554
    fn sub(self, rhs: DateTime) -> Self::Output {
59✔
555
        let duration = chrono::DateTime::<chrono::FixedOffset>::from(self)
59✔
556
            .signed_duration_since(chrono::DateTime::<chrono::FixedOffset>::from(rhs));
59✔
557

59✔
558
        Duration::milliseconds(duration.num_milliseconds())
59✔
559
    }
59✔
560
}
561

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

565
    fn sub(self, rhs: Duration) -> Self::Output {
30✔
566
        let duration = chrono::Duration::milliseconds(rhs.num_milliseconds());
30✔
567

30✔
568
        chrono::DateTime::<chrono::FixedOffset>::from(self)
30✔
569
            // TODO: do we ignore the overflow?
30✔
570
            .sub(duration)
30✔
571
            .into()
30✔
572
    }
30✔
573
}
574

575
mod sql {
576
    use super::*;
577
    use postgres_types::{
578
        accepts, private::BytesMut, to_sql_checked, FromSql, IsNull, ToSql, Type,
579
    };
580
    use std::error::Error;
581

582
    impl<'a> FromSql<'a> for DateTime {
583
        fn from_sql(ty: &Type, raw: &'a [u8]) -> Result<Self, Box<dyn Error + Sync + Send>> {
393✔
584
            let naive = chrono::NaiveDateTime::from_sql(ty, raw)?;
393✔
585
            let datetime = chrono::DateTime::from_naive_utc_and_offset(naive, chrono::Utc);
393✔
586
            Ok(DateTime { datetime })
393✔
587
        }
393✔
588

589
        postgres_types::accepts!(TIMESTAMPTZ);
590
    }
591

592
    impl ToSql for DateTime {
593
        fn to_sql(
×
594
            &self,
×
595
            type_: &Type,
×
596
            w: &mut BytesMut,
×
597
        ) -> Result<IsNull, Box<dyn Error + Sync + Send>> {
×
598
            self.datetime.naive_utc().to_sql(type_, w)
×
599
        }
×
600

601
        accepts!(TIMESTAMPTZ);
602

603
        to_sql_checked!();
604
    }
605
}
606

607
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1✔
608
pub struct Duration {
609
    milliseconds: i64,
610
}
611

612
/// TODO: out of bounds checks
613
impl Duration {
614
    pub const SECOND: Self = Self::milliseconds(1_000);
615
    pub const MINUTE: Self = Self::seconds(60);
616
    pub const HOUR: Self = Self::minutes(60);
617
    pub const DAY: Self = Self::hours(24);
618

619
    pub const fn days(days: i64) -> Self {
13✔
620
        Self::milliseconds(days * Self::DAY.milliseconds)
13✔
621
    }
13✔
622

623
    pub const fn hours(hours: i64) -> Self {
19✔
624
        Self::milliseconds(hours * Self::HOUR.milliseconds)
19✔
625
    }
19✔
626

627
    pub const fn minutes(minutes: i64) -> Self {
21✔
628
        Self::milliseconds(minutes * Self::MINUTE.milliseconds)
21✔
629
    }
21✔
630

631
    pub const fn seconds(seconds: i64) -> Self {
28✔
632
        Self::milliseconds(seconds * Self::SECOND.milliseconds)
28✔
633
    }
28✔
634

635
    pub const fn milliseconds(milliseconds: i64) -> Self {
608✔
636
        Self { milliseconds }
608✔
637
    }
608✔
638

639
    pub const fn num_milliseconds(self) -> i64 {
383✔
640
        self.milliseconds
383✔
641
    }
383✔
642

643
    pub const fn num_seconds(self) -> i64 {
70✔
644
        self.milliseconds / Self::SECOND.milliseconds
70✔
645
    }
70✔
646

647
    pub const fn num_minutes(self) -> i64 {
9✔
648
        self.milliseconds / Self::MINUTE.milliseconds
9✔
649
    }
9✔
650

651
    pub const fn num_hours(self) -> i64 {
7✔
652
        self.milliseconds / Self::HOUR.milliseconds
7✔
653
    }
7✔
654

655
    pub const fn num_days(self) -> i64 {
6✔
656
        self.milliseconds / Self::DAY.milliseconds
6✔
657
    }
6✔
658

659
    pub const fn is_zero(self) -> bool {
157✔
660
        self.milliseconds == 0
157✔
661
    }
157✔
662
}
663

664
// TODO: must this be checked?
665
impl Add<Duration> for Duration {
666
    type Output = Self;
667

668
    fn add(self, rhs: Duration) -> Self::Output {
×
669
        Self {
×
670
            milliseconds: self.milliseconds + rhs.milliseconds,
×
671
        }
×
672
    }
×
673
}
674

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

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

686
#[cfg(test)]
687
mod tests {
688
    use super::*;
689

690
    #[test]
1✔
691
    fn parse() {
1✔
692
        let format = DateTimeParseFormat::custom("%Y-%m-%dT%H:%M:%S%.3f".to_string());
1✔
693

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

1✔
697
        assert_eq!(result, expected);
1✔
698
    }
1✔
699

700
    #[test]
1✔
701
    fn to_string() {
1✔
702
        assert_eq!(
1✔
703
            DateTime::new_utc(2010, 1, 2, 3, 4, 5).to_string(),
1✔
704
            "2010-01-02T03:04:05.000Z"
1✔
705
        );
1✔
706
        assert_eq!(
1✔
707
            DateTime::new_utc(-2010, 1, 2, 3, 4, 5).to_string(),
1✔
708
            "-002010-01-02T03:04:05.000Z"
1✔
709
        );
1✔
710
    }
1✔
711

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

724
    #[test]
1✔
725
    fn has_tz() {
1✔
726
        assert!(!DateTimeParseFormat::custom("%Y-%m-%dT%H:%M:%S%.3f".to_string()).has_tz());
1✔
727
        assert!(DateTimeParseFormat::custom("%Y-%m-%dT%H:%M:%S%z".to_string()).has_tz());
1✔
728
    }
1✔
729

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

736
    #[test]
1✔
737
    fn test_serialize() {
1✔
738
        assert_eq!(
1✔
739
            serde_json::to_string(&DateTime::from_str("2010-01-02T03:04:05.000Z").unwrap())
1✔
740
                .unwrap(),
1✔
741
            "\"2010-01-02T03:04:05.000Z\""
1✔
742
        );
1✔
743
    }
1✔
744

745
    #[test]
1✔
746
    fn test_as_datetime_string() {
1✔
747
        assert_eq!(
1✔
748
            DateTime::new_utc(2010, 1, 2, 3, 4, 5).to_datetime_string_with_millis(),
1✔
749
            "2010-01-02T03:04:05.000Z"
1✔
750
        );
1✔
751
    }
1✔
752

753
    #[test]
1✔
754
    fn test_datetime_negative_years() {
1✔
755
        assert_eq!(
1✔
756
            DateTime::new_utc(-2010, 1, 2, 3, 4, 5).to_datetime_string(),
1✔
757
            "-002010-01-02T03:04:05+00:00"
1✔
758
        );
1✔
759
    }
1✔
760

761
    #[test]
1✔
762
    fn test_sub_datetime_datetime() {
1✔
763
        let a = DateTime::new_utc(2010, 1, 2, 3, 4, 5);
1✔
764
        let b = DateTime::new_utc(2010, 1, 2, 3, 4, 5);
1✔
765
        assert_eq!(a - b, Duration::milliseconds(0));
1✔
766
    }
1✔
767
}
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