Skip to content

Commit 2b3f6bc

Browse files
Merge pull request #190 from nyx-space/182-support-day-of-year-initialization-and-formatting
Support strftime and strptime equivalent for formatting and parsing
2 parents 09bfcf3 + b1071b9 commit 2b3f6bc

22 files changed

+2497
-182
lines changed

Cargo.toml

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,17 @@ crate-type = ["cdylib", "rlib"]
1818
name = "hifitime"
1919

2020
[dependencies]
21-
serde = {version = "1.0.145", optional = true}
22-
serde_derive = {version = "1.0.145", optional = true}
23-
der = {version = "0.6.0", features = ["derive", "real"], optional = true}
24-
pyo3 = { version = "0.17.2", features = ["extension-module"], optional = true}
21+
serde = {version = "1.0.152", optional = true}
22+
serde_derive = {version = "1.0.152", optional = true}
23+
der = {version = "0.6.1", features = ["derive", "real"], optional = true}
24+
pyo3 = { version = "0.17.3", features = ["extension-module"], optional = true}
2525
num-traits = {version = "0.2.15", default-features = false, features = ["libm"]}
2626
lexical-core = {version = "0.8.5", default-features = false, features = ["parse-integers", "parse-floats"]}
2727

2828
[dev-dependencies]
29-
serde_json = "1"
29+
serde_json = "1.0.91"
3030
criterion = "0.4.0"
31+
iai = "0.1"
3132

3233
[features]
3334
default = ["std"]
@@ -42,3 +43,11 @@ harness = false
4243
[[bench]]
4344
name = "bench_duration"
4445
harness = false
46+
47+
[[bench]]
48+
name = "iai_duration"
49+
harness = false
50+
51+
[[bench]]
52+
name = "iai_epoch"
53+
harness = false

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ Scientifically accurate date and time handling with guaranteed nanosecond precis
44
Formally verified to not crash on operations on epochs and durations using the [`Kani`](https://model-checking.github.io/kani/) model checking.
55

66
[![hifitime on crates.io][cratesio-image]][cratesio]
7-
[![Build Status](https://github.com/nyx-space/hifitime/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/nyx-space/hifitime/actions)
87
[![hifitime on docs.rs][docsrs-image]][docsrs]
98
[![minimum rustc: 1.58](https://img.shields.io/badge/minimum%20rustc-1.58-yellowgreen?logo=rust)](https://www.whatrustisit.com)
9+
[![Build Status](https://github.com/nyx-space/hifitime/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/nyx-space/hifitime/actions)
10+
[![Build Status](https://github.com/nyx-space/hifitime/actions/workflows/formal_verification.yml/badge.svg?branch=master)](https://github.com/nyx-space/hifitime/actions)
11+
[![codecov](https://codecov.io/gh/nyx-space/hifitime/branch/master/graph/badge.svg?token=l7zU57rUGs)](https://codecov.io/gh/nyx-space/hifitime)
1012

1113
[cratesio-image]: https://img.shields.io/crates/v/hifitime.svg
1214
[cratesio]: https://crates.io/crates/hifitime

benches/iai_duration.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
use core::str::FromStr;
2+
use hifitime::{Duration, Unit};
3+
use iai::black_box;
4+
5+
fn duration_from_f64_seconds() {
6+
let value: f64 = 6311433599.999999;
7+
black_box(Duration::from_seconds(value));
8+
}
9+
10+
fn duration_from_f64_seconds_via_units() {
11+
let value: f64 = 6311433599.999999;
12+
black_box(value * Unit::Second);
13+
}
14+
15+
fn duration_from_i64_nanoseconds() {
16+
let value: i64 = 6311433599;
17+
black_box(Duration::from_truncated_nanoseconds(value));
18+
}
19+
20+
fn duration_from_i64_nanoseconds_via_units() {
21+
let value: i64 = 6311433599;
22+
black_box(value * Unit::Nanosecond);
23+
}
24+
25+
fn duration_parse_days_only() {
26+
black_box(Duration::from_str("15 d").unwrap());
27+
}
28+
29+
fn duration_parse_complex() {
30+
black_box(Duration::from_str("1 d 15.5 hours 25 ns").unwrap());
31+
}
32+
33+
fn large_duration_to_seconds() {
34+
let d: Duration = 50.15978 * Unit::Century;
35+
black_box(d.to_seconds());
36+
}
37+
38+
fn duration_to_seconds() {
39+
let d: Duration = 50.15978 * Unit::Second;
40+
black_box(d.to_seconds());
41+
}
42+
43+
fn small_duration_to_seconds() {
44+
let d: Duration = 50.159 * Unit::Microsecond;
45+
black_box(d.to_seconds());
46+
}
47+
48+
iai::main!(
49+
duration_from_f64_seconds,
50+
duration_from_i64_nanoseconds,
51+
duration_from_f64_seconds_via_units,
52+
duration_from_i64_nanoseconds_via_units,
53+
duration_parse_days_only,
54+
duration_parse_complex,
55+
large_duration_to_seconds,
56+
duration_to_seconds,
57+
small_duration_to_seconds,
58+
);

benches/iai_epoch.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
use hifitime::efmt::consts::RFC3339;
2+
use hifitime::{Epoch, Unit};
3+
use iai::black_box;
4+
5+
fn epoch_from_gregorian_utc() {
6+
black_box(Epoch::from_gregorian_utc_hms(2015, 2, 7, 11, 22, 33));
7+
}
8+
9+
fn epoch_from_gregorian_tai() {
10+
black_box(Epoch::from_gregorian_tai_hms(2015, 2, 7, 11, 22, 33));
11+
}
12+
13+
fn epoch_from_tdb_seconds() {
14+
black_box(Epoch::from_tdb_seconds(725603206.3990843));
15+
}
16+
17+
fn epoch_jde_tdb_seconds() {
18+
black_box(Epoch::from_jde_tdb(2459943.1861358252));
19+
}
20+
21+
fn epoch_from_et_seconds() {
22+
black_box(Epoch::from_et_seconds(725603337.1330023));
23+
}
24+
25+
fn epoch_jde_et_seconds() {
26+
black_box(Epoch::from_jde_tdb(2459943.186081989));
27+
}
28+
29+
fn epoch_add() {
30+
let e: Epoch = Epoch::from_gregorian_tai_hms(2015, 2, 7, 11, 22, 33);
31+
black_box(e + 50 * Unit::Second);
32+
}
33+
34+
fn epoch_sub() {
35+
let e: Epoch = Epoch::from_gregorian_tai_hms(2015, 2, 7, 11, 22, 33);
36+
black_box(e - 50 * Unit::Second);
37+
}
38+
39+
fn parse_rfc3339_with_seconds() {
40+
black_box(Epoch::from_gregorian_str("2018-02-13T23:08:32Z").unwrap());
41+
}
42+
43+
fn parse_rfc3339_with_milliseconds() {
44+
black_box(Epoch::from_gregorian_str("2018-02-13T23:08:32.123Z").unwrap());
45+
}
46+
47+
fn parse_rfc3339_with_nanoseconds() {
48+
black_box(Epoch::from_gregorian_str("2018-02-13T23:08:32.123456983Z").unwrap());
49+
}
50+
51+
fn fmt_parse_rfc3339_with_seconds() {
52+
black_box(RFC3339.parse("2018-02-13T23:08:32Z").unwrap());
53+
}
54+
55+
fn fmt_parse_rfc3339_with_milliseconds() {
56+
black_box(RFC3339.parse("2018-02-13T23:08:32.123Z").unwrap());
57+
}
58+
59+
fn fmt_parse_rfc3339_with_nanoseconds() {
60+
black_box(RFC3339.parse("2018-02-13T23:08:32.123456983Z").unwrap());
61+
}
62+
63+
iai::main!(
64+
epoch_from_gregorian_utc,
65+
epoch_from_gregorian_tai,
66+
epoch_from_tdb_seconds,
67+
epoch_jde_tdb_seconds,
68+
epoch_from_et_seconds,
69+
epoch_jde_et_seconds,
70+
epoch_add,
71+
epoch_sub,
72+
parse_rfc3339_with_seconds,
73+
parse_rfc3339_with_milliseconds,
74+
parse_rfc3339_with_nanoseconds,
75+
fmt_parse_rfc3339_with_seconds,
76+
fmt_parse_rfc3339_with_milliseconds,
77+
fmt_parse_rfc3339_with_nanoseconds
78+
);

src/deprecated.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -590,7 +590,7 @@ impl Epoch {
590590
since = "3.5.0"
591591
)]
592592
pub fn as_gregorian_utc_str(&self) -> String {
593-
self.to_gregorian_utc_str()
593+
format!("{}", self)
594594
}
595595

596596
#[must_use]
@@ -599,7 +599,7 @@ impl Epoch {
599599
since = "3.5.0"
600600
)]
601601
pub fn as_gregorian_tai_str(&self) -> String {
602-
self.to_gregorian_tai_str()
602+
format!("{:x}", self)
603603
}
604604

605605
#[must_use]

src/duration.rs

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,17 @@ impl Duration {
269269
me
270270
}
271271
}
272+
273+
/// Initializes a Duration from a timezone offset
274+
#[must_use]
275+
pub fn from_tz_offset(sign: i8, hours: i64, minutes: i64) -> Self {
276+
let dur = hours * Unit::Hour + minutes * Unit::Minute;
277+
if sign < 0 {
278+
-dur
279+
} else {
280+
dur
281+
}
282+
}
272283
}
273284

274285
#[cfg_attr(feature = "python", pymethods)]
@@ -550,6 +561,47 @@ impl Duration {
550561
}
551562
}
552563

564+
/// Rounds this duration to the largest units represented in this duration.
565+
///
566+
/// This is useful to provide an approximate human duration. Under the hood, this function uses `round`,
567+
/// so the "tipping point" of the rounding is half way to the next increment of the greatest unit.
568+
/// As shown below, one example is that 35 hours and 59 minutes rounds to 1 day, but 36 hours and 1 minute rounds
569+
/// to 2 days because 2 days is closer to 36h 1 min than 36h 1 min is to 1 day.
570+
///
571+
/// # Example
572+
///
573+
/// ```
574+
/// use hifitime::{Duration, TimeUnits};
575+
///
576+
/// assert_eq!((2.hours() + 3.minutes()).approx(), 2.hours());
577+
/// assert_eq!((24.hours() + 3.minutes()).approx(), 1.days());
578+
/// assert_eq!((35.hours() + 59.minutes()).approx(), 1.days());
579+
/// assert_eq!((36.hours() + 1.minutes()).approx(), 2.days());
580+
/// assert_eq!((47.hours() + 3.minutes()).approx(), 2.days());
581+
/// assert_eq!((49.hours() + 3.minutes()).approx(), 2.days());
582+
/// ```
583+
pub fn approx(&self) -> Self {
584+
let (_, days, hours, minutes, seconds, milli, us, _) = self.decompose();
585+
586+
let round_to = if days > 0 {
587+
1 * Unit::Day
588+
} else if hours > 0 {
589+
1 * Unit::Hour
590+
} else if minutes > 0 {
591+
1 * Unit::Minute
592+
} else if seconds > 0 {
593+
1 * Unit::Second
594+
} else if milli > 0 {
595+
1 * Unit::Millisecond
596+
} else if us > 0 {
597+
1 * Unit::Microsecond
598+
} else {
599+
1 * Unit::Nanosecond
600+
};
601+
602+
self.round(round_to)
603+
}
604+
553605
/// Returns the minimum of the two durations.
554606
///
555607
/// ```
@@ -1220,6 +1272,7 @@ impl FromStr for Duration {
12201272
/// + ms, millisecond, milliseconds
12211273
/// + us, microsecond, microseconds
12221274
/// + ns, nanosecond, nanoseconds
1275+
/// + `+` or `-` indicates a timezone offset
12231276
///
12241277
/// # Example
12251278
/// ```
@@ -1233,6 +1286,8 @@ impl FromStr for Duration {
12331286
/// assert_eq!(Duration::from_str("10.598 seconds").unwrap(), Unit::Second * 10.598);
12341287
/// assert_eq!(Duration::from_str("10.598 nanosecond").unwrap(), Unit::Nanosecond * 10.598);
12351288
/// assert_eq!(Duration::from_str("5 h 256 ms 1 ns").unwrap(), 5 * Unit::Hour + 256 * Unit::Millisecond + Unit::Nanosecond);
1289+
/// assert_eq!(Duration::from_str("-01:15:30").unwrap(), -(1 * Unit::Hour + 15 * Unit::Minute + 30 * Unit::Second));
1290+
/// assert_eq!(Duration::from_str("+3615").unwrap(), 36 * Unit::Hour + 15 * Unit::Minute);
12361291
/// ```
12371292
fn from_str(s_in: &str) -> Result<Self, Self::Err> {
12381293
// Each part of a duration as days, hours, minutes, seconds, millisecond, microseconds, and nanoseconds
@@ -1244,6 +1299,86 @@ impl FromStr for Duration {
12441299

12451300
let s = s_in.trim();
12461301

1302+
if s.is_empty() {
1303+
return Err(Errors::ParseError(ParsingErrors::ValueError));
1304+
}
1305+
1306+
// There is at least one character, so we can unwrap this.
1307+
if let Some(char) = s.chars().next() {
1308+
if char == '+' || char == '-' {
1309+
// This is a timezone offset.
1310+
let offset_sign = if char == '-' { -1 } else { 1 };
1311+
1312+
let indexes: (usize, usize, usize) = (1, 3, 5);
1313+
let colon = if s.len() == 3 || s.len() == 5 || s.len() == 7 {
1314+
// There is a zero or even number of separators between the hours, minutes, and seconds.
1315+
// Only zero (or one) characters separator is supported. This will return a ValueError later if there is
1316+
// an even but greater than one character separator.
1317+
0
1318+
} else if s.len() == 4 || s.len() == 6 || s.len() == 9 {
1319+
// There is an odd number of characters as a separator between the hours, minutes, and seconds.
1320+
// Only one character separator is supported. This will return a ValueError later if there is
1321+
// an odd but greater than one character separator.
1322+
1
1323+
} else {
1324+
// This invalid
1325+
return Err(Errors::ParseError(ParsingErrors::ValueError));
1326+
};
1327+
1328+
// Fetch the hours
1329+
let hours: i64 = match lexical_core::parse(s[indexes.0..indexes.1].as_bytes()) {
1330+
Ok(val) => val,
1331+
Err(_) => return Err(Errors::ParseError(ParsingErrors::ValueError)),
1332+
};
1333+
1334+
let mut minutes: i64 = 0;
1335+
let mut seconds: i64 = 0;
1336+
1337+
match s.get(indexes.1 + colon..indexes.2 + colon) {
1338+
None => {
1339+
//Do nothing, we've reached the end of the useful data.
1340+
}
1341+
Some(subs) => {
1342+
// Fetch the minutes
1343+
match lexical_core::parse(subs.as_bytes()) {
1344+
Ok(val) => minutes = val,
1345+
Err(_) => return Err(Errors::ParseError(ParsingErrors::ValueError)),
1346+
}
1347+
1348+
match s.get(indexes.2 + 2 * colon..) {
1349+
None => {
1350+
// Do nothing, there are no seconds inthis offset
1351+
}
1352+
Some(subs) => {
1353+
if !subs.is_empty() {
1354+
// Fetch the seconds
1355+
match lexical_core::parse(subs.as_bytes()) {
1356+
Ok(val) => seconds = val,
1357+
Err(_) => {
1358+
return Err(Errors::ParseError(
1359+
ParsingErrors::ValueError,
1360+
))
1361+
}
1362+
}
1363+
}
1364+
}
1365+
}
1366+
}
1367+
}
1368+
1369+
// Return the constructed offset
1370+
if offset_sign == -1 {
1371+
return Ok(-(hours * Unit::Hour
1372+
+ minutes * Unit::Minute
1373+
+ seconds * Unit::Second));
1374+
} else {
1375+
return Ok(hours * Unit::Hour
1376+
+ minutes * Unit::Minute
1377+
+ seconds * Unit::Second);
1378+
}
1379+
}
1380+
};
1381+
12471382
for (idx, char) in s.chars().enumerate() {
12481383
if char == ' ' || idx == s.len() - 1 {
12491384
if seeking_number {

0 commit comments

Comments
 (0)