Skip to content

Commit 0c3e368

Browse files
committed
fable: Add fable/utility/chrono.hpp utility functions
Add serialization to and deserialization from strings in a manner as exact as possible.
1 parent 3bc7a96 commit 0c3e368

File tree

4 files changed

+414
-0
lines changed

4 files changed

+414
-0
lines changed

fable/CMakeLists.txt

+2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ add_library(fable
2929
src/fable/schema/path.cpp
3030
src/fable/environment.cpp
3131
src/fable/utility.cpp
32+
src/fable/utility/chrono.cpp
3233
src/fable/utility/string.cpp
3334

3435
# For IDE integration
@@ -85,6 +86,7 @@ if(BUILD_TESTING)
8586
src/fable/schema/string_test.cpp
8687
src/fable/schema/struct_test.cpp
8788
src/fable/schema_test.cpp
89+
src/fable/utility/chrono_test.cpp
8890
)
8991
set_target_properties(test-fable PROPERTIES
9092
CXX_STANDARD 17
+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright 2023 Robert Bosch GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
*/
18+
/**
19+
* \file fable/utility/chrono.hpp
20+
*/
21+
22+
#pragma once
23+
24+
#include <chrono>
25+
#include <string>
26+
27+
#include <nlohmann/json.hpp>
28+
29+
namespace fable {
30+
31+
std::chrono::nanoseconds parse_duration_to_nanoseconds(const std::string& s);
32+
33+
/**
34+
* Convert a string containing a number and a unit to a duration.
35+
*
36+
* The following units are supported:
37+
*
38+
* - ns | nanosecond | nanoseconds
39+
* - us | microsecond | microseconds
40+
* - ms | millisecond | milliseconds
41+
* - s | second | seconds
42+
* - min | minute | minutes
43+
* - h | hour | hours
44+
*
45+
* Will throw an exception on malformed or out-of-range input:
46+
*
47+
* - std::invalid_argument if unit missing or unknown
48+
* - std::out_of_range if sub-nanosecond precision used (e.g. 0.5ns)
49+
*
50+
* Note: This parse function preserves precision even for floating
51+
* point numbers. For example, 0.1 is not exactly representable
52+
* as a floating point number, but together with a unit, we can
53+
* scale it so that it is represented exactly.
54+
*/
55+
template <typename Duration>
56+
Duration parse_duration(const std::string& s) {
57+
return std::chrono::duration_cast<Duration>(parse_duration_to_nanoseconds(s));
58+
}
59+
60+
template <typename Duration>
61+
std::string to_string(const Duration& d) {
62+
return to_string(std::chrono::duration_cast<std::chrono::nanoseconds>(d));
63+
}
64+
65+
template <>
66+
std::string to_string(const std::chrono::nanoseconds& d);
67+
68+
} // namespace fable
69+
70+
namespace nlohmann {
71+
72+
template <typename Rep, typename Period>
73+
struct adl_serializer<std::chrono::duration<Rep, Period>> {
74+
using Duration = std::chrono::duration<Rep, Period>;
75+
76+
static void to_json(json& j, const Duration& d) {
77+
j = ::fable::to_string(d);
78+
}
79+
80+
static void from_json(const json& j, Duration& d) {
81+
std::string s = j.get<std::string>();
82+
d = ::fable::parse_duration<Duration>(s);
83+
}
84+
};
85+
86+
} // namespace nlohmann

fable/src/fable/utility/chrono.cpp

+207
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
/*
2+
* Copyright 2023 Robert Bosch GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
*/
18+
19+
#include <fable/utility/chrono.hpp>
20+
21+
#include <chrono> // for duration_cast
22+
#include <cmath> // for pow
23+
#include <string> // for operator+, string, to_string
24+
25+
namespace fable {
26+
namespace {
27+
28+
enum class DurationUnit { Nanosecond, Microsecond, Millisecond, Second, Minute, Hour };
29+
30+
static std::map<std::string, DurationUnit> DURATION_UNITS{
31+
{"ns", DurationUnit::Nanosecond},
32+
{"nanosecond", DurationUnit::Nanosecond},
33+
{"nanoseconds", DurationUnit::Nanosecond},
34+
35+
{"us", DurationUnit::Microsecond},
36+
{"microsecond", DurationUnit::Microsecond},
37+
{"microseconds", DurationUnit::Microsecond},
38+
39+
{"ms", DurationUnit::Millisecond},
40+
{"millisecond", DurationUnit::Millisecond},
41+
{"milliseconds", DurationUnit::Millisecond},
42+
43+
{"s", DurationUnit::Second},
44+
{"second", DurationUnit::Second},
45+
{"seconds", DurationUnit::Second},
46+
47+
{"s", DurationUnit::Second},
48+
{"second", DurationUnit::Second},
49+
{"seconds", DurationUnit::Second},
50+
51+
{"min", DurationUnit::Minute},
52+
{"minute", DurationUnit::Minute},
53+
{"minutes", DurationUnit::Minute},
54+
55+
{"h", DurationUnit::Hour},
56+
{"hour", DurationUnit::Hour},
57+
{"hours", DurationUnit::Hour},
58+
};
59+
60+
/**
61+
* Parse a duration unit in an efficient way.
62+
*
63+
* This accepts the following unit specifications, where the 's' indicating
64+
* the plural is optional.
65+
*
66+
* ns | nanoseconds?
67+
* us | microseconds?
68+
* ms | milliseconds?
69+
* s | seconds?
70+
* min | minutes?
71+
* h | | hours?
72+
*/
73+
DurationUnit parse_duration_unit(const std::string& sv) { return DURATION_UNITS.at(sv); }
74+
75+
/**
76+
* Converts d to a (possibly) fractional string without precision loss.
77+
*
78+
* Note: The implementation does not use floating point numbers to avoid
79+
* any rounding errors that might otherwise occur.
80+
*/
81+
template <typename Unit, typename Duration>
82+
std::string to_string_with_unit(Duration d, std::string_view suffix) {
83+
std::string buf;
84+
buf.reserve(16); // This should be enough for most numbers we deal with.
85+
86+
// Write the whole component (e.g. "1" from "1.5s")
87+
Unit whole = std::chrono::duration_cast<Unit>(d);
88+
buf += std::to_string(whole.count());
89+
90+
// Write fraction, if non-zero (e.g. ".5" from "1.5s"):
91+
Duration fraction = d - whole;
92+
if (fraction.count() != 0) {
93+
buf += ".";
94+
95+
auto fraction_s = std::to_string(fraction.count());
96+
97+
// If fraction_s has less digits than the maximum the fraction can have,
98+
// then we need to left-pad it with zeros.
99+
auto max_digits = std::log10(Duration::period::den / Unit::period::den);
100+
buf += std::string("0", max_digits - fraction_s.size());
101+
102+
// Add the rest of the fraction, but chop off trailing zeros.
103+
buf += fraction_s.substr(0, fraction_s.find_last_not_of("0")+1);
104+
}
105+
106+
// Write suffix, if available (e.g. "s" from "1.5s")
107+
if (suffix.data()) {
108+
buf += suffix;
109+
}
110+
return buf;
111+
}
112+
113+
} // anonymous namespace
114+
115+
std::chrono::nanoseconds parse_duration_to_nanoseconds(const std::string& sv) {
116+
// Get whole component of the duration
117+
size_t idx = 0;
118+
uint64_t whole = std::stoull(sv, &idx);
119+
if (idx >= sv.size()) {
120+
throw std::invalid_argument("number requires unit to parse");
121+
}
122+
123+
// Get any fraction component of duration.
124+
// Note the number of digits this number has, so we can efficiently
125+
// add it the result without losing any precision.
126+
uint64_t fraction = 0;
127+
size_t fraction_digits = 0;
128+
if (sv[idx] == '.') {
129+
auto prev = ++idx;
130+
fraction = std::stoull(sv.substr(idx), &idx);
131+
fraction_digits = idx;
132+
idx += prev;
133+
}
134+
if (idx >= sv.size()) {
135+
throw std::invalid_argument("number requires unit to parse");
136+
}
137+
138+
// Read rest of sv as unit, skipping whitespace between number
139+
// and the unit.
140+
while (sv[idx] == ' ') {
141+
idx++;
142+
}
143+
DurationUnit unit;
144+
try {
145+
unit = parse_duration_unit(sv.substr(idx));
146+
} catch (std::out_of_range&) {
147+
throw std::invalid_argument("number requires valid unit to parse");
148+
}
149+
150+
std::chrono::nanoseconds result(0);
151+
int ns_exponent;
152+
int multiplier = 1;
153+
switch (unit) {
154+
case DurationUnit::Nanosecond:
155+
ns_exponent = 0;
156+
result += std::chrono::nanoseconds(whole);
157+
break;
158+
case DurationUnit::Microsecond:
159+
ns_exponent = 3;
160+
result += std::chrono::microseconds(whole);
161+
break;
162+
case DurationUnit::Millisecond:
163+
ns_exponent = 6;
164+
result += std::chrono::milliseconds(whole);
165+
break;
166+
case DurationUnit::Second:
167+
ns_exponent = 9;
168+
result += std::chrono::seconds(whole);
169+
break;
170+
case DurationUnit::Minute:
171+
ns_exponent = 10;
172+
multiplier = 6;
173+
result += std::chrono::minutes(whole);
174+
break;
175+
case DurationUnit::Hour:
176+
ns_exponent = 11;
177+
multiplier = 36;
178+
result += std::chrono::hours(whole);
179+
break;
180+
}
181+
182+
if (fraction) {
183+
int fraction_exponent = ns_exponent - fraction_digits;
184+
if (fraction_exponent < 0) {
185+
throw std::out_of_range("cannot represent sub-nanosecond precision: " + sv);
186+
}
187+
result += std::chrono::nanoseconds(fraction * multiplier) *
188+
static_cast<size_t>(std::pow(10, fraction_exponent));
189+
}
190+
return result;
191+
}
192+
193+
template <>
194+
std::string to_string<std::chrono::nanoseconds>(const std::chrono::nanoseconds& ns) {
195+
auto count = ns.count();
196+
if (count >= 1e9) {
197+
return to_string_with_unit<std::chrono::seconds>(ns, "s");
198+
} else if (count >= 1e6) {
199+
return to_string_with_unit<std::chrono::milliseconds>(ns, "ms");
200+
} else if (count >= 1e3) {
201+
return to_string_with_unit<std::chrono::microseconds>(ns, "us");
202+
} else {
203+
return std::to_string(count) + "ns";
204+
}
205+
}
206+
207+
} // namespace fable

0 commit comments

Comments
 (0)