1 // Copyright 2021 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "base/time/time_delta_from_string.h"
6
7 #include <limits>
8 #include <string_view>
9 #include <utility>
10
11 #include "base/strings/string_util.h"
12 #include "base/time/time.h"
13
14 namespace base {
15
16 namespace {
17
18 // Strips the |expected| prefix from the start of the given string, returning
19 // |true| if the strip operation succeeded or false otherwise.
20 //
21 // Example:
22 //
23 // std::string_view input("abc");
24 // EXPECT_TRUE(ConsumePrefix(input, "a"));
25 // EXPECT_EQ(input, "bc");
26 //
27 // Adapted from absl::ConsumePrefix():
28 // https://cs.chromium.org/chromium/src/third_party/abseil-cpp/absl/strings/strip.h?l=45&rcl=2c22e9135f107a4319582ae52e2e3e6b201b6b7c
ConsumePrefix(std::string_view & str,std::string_view expected)29 bool ConsumePrefix(std::string_view& str, std::string_view expected) {
30 if (!StartsWith(str, expected))
31 return false;
32 str.remove_prefix(expected.size());
33 return true;
34 }
35
36 // Utility struct used by ConsumeDurationNumber() to parse decimal numbers.
37 // A ParsedDecimal represents the number `int_part` + `frac_part`/`frac_scale`,
38 // where:
39 // (i) 0 <= `frac_part` < `frac_scale` (implies `frac_part`/`frac_scale` < 1)
40 // (ii) `frac_scale` is 10^[number of digits after the decimal point]
41 //
42 // Example:
43 // -42 => {.int_part = -42, .frac_part = 0, .frac_scale = 1}
44 // 1.23 => {.int_part = 1, .frac_part = 23, .frac_scale = 100}
45 struct ParsedDecimal {
46 int64_t int_part = 0;
47 int64_t frac_part = 0;
48 int64_t frac_scale = 1;
49 };
50
51 // A helper for FromString() that tries to parse a leading number from the given
52 // std::string_view. |number_string| is modified to start from the first
53 // unconsumed char.
54 //
55 // Adapted from absl:
56 // https://cs.chromium.org/chromium/src/third_party/abseil-cpp/absl/time/duration.cc?l=807&rcl=2c22e9135f107a4319582ae52e2e3e6b201b6b7c
ConsumeDurationNumber(std::string_view & number_string)57 constexpr std::optional<ParsedDecimal> ConsumeDurationNumber(
58 std::string_view& number_string) {
59 ParsedDecimal res;
60 StringPiece::const_iterator orig_start = number_string.begin();
61 // Parse contiguous digits.
62 for (; !number_string.empty(); number_string.remove_prefix(1)) {
63 const int d = number_string.front() - '0';
64 if (d < 0 || d >= 10)
65 break;
66
67 if (res.int_part > std::numeric_limits<int64_t>::max() / 10)
68 return std::nullopt;
69 res.int_part *= 10;
70 if (res.int_part > std::numeric_limits<int64_t>::max() - d)
71 return std::nullopt;
72 res.int_part += d;
73 }
74 const bool int_part_empty = number_string.begin() == orig_start;
75 if (number_string.empty() || number_string.front() != '.')
76 return int_part_empty ? std::nullopt : std::make_optional(res);
77
78 number_string.remove_prefix(1); // consume '.'
79 // Parse contiguous digits.
80 for (; !number_string.empty(); number_string.remove_prefix(1)) {
81 const int d = number_string.front() - '0';
82 if (d < 0 || d >= 10)
83 break;
84 DCHECK_LT(res.frac_part, res.frac_scale);
85 if (res.frac_scale <= std::numeric_limits<int64_t>::max() / 10) {
86 // |frac_part| will not overflow because it is always < |frac_scale|.
87 res.frac_part *= 10;
88 res.frac_part += d;
89 res.frac_scale *= 10;
90 }
91 }
92
93 return int_part_empty && res.frac_scale == 1 ? std::nullopt
94 : std::make_optional(res);
95 }
96
97 // A helper for FromString() that tries to parse a leading unit designator
98 // (e.g., ns, us, ms, s, m, h, d) from the given std::string_view. |unit_string|
99 // is modified to start from the first unconsumed char.
100 //
101 // Adapted from absl:
102 // https://cs.chromium.org/chromium/src/third_party/abseil-cpp/absl/time/duration.cc?l=841&rcl=2c22e9135f107a4319582ae52e2e3e6b201b6b7c
ConsumeDurationUnit(std::string_view & unit_string)103 std::optional<TimeDelta> ConsumeDurationUnit(std::string_view& unit_string) {
104 for (const auto& str_delta : {
105 std::make_pair("ns", Nanoseconds(1)),
106 std::make_pair("us", Microseconds(1)),
107 // Note: "ms" MUST be checked before "m" to ensure that milliseconds
108 // are not parsed as minutes.
109 std::make_pair("ms", Milliseconds(1)),
110 std::make_pair("s", Seconds(1)),
111 std::make_pair("m", Minutes(1)),
112 std::make_pair("h", Hours(1)),
113 std::make_pair("d", Days(1)),
114 }) {
115 if (ConsumePrefix(unit_string, str_delta.first))
116 return str_delta.second;
117 }
118
119 return std::nullopt;
120 }
121
122 } // namespace
123
TimeDeltaFromString(std::string_view duration_string)124 std::optional<TimeDelta> TimeDeltaFromString(std::string_view duration_string) {
125 int sign = 1;
126 if (ConsumePrefix(duration_string, "-"))
127 sign = -1;
128 else
129 ConsumePrefix(duration_string, "+");
130 if (duration_string.empty())
131 return std::nullopt;
132
133 // Handle special-case values that don't require units.
134 if (duration_string == "0")
135 return TimeDelta();
136 if (duration_string == "inf")
137 return sign == 1 ? TimeDelta::Max() : TimeDelta::Min();
138
139 TimeDelta delta;
140 while (!duration_string.empty()) {
141 std::optional<ParsedDecimal> number_opt =
142 ConsumeDurationNumber(duration_string);
143 if (!number_opt.has_value())
144 return std::nullopt;
145 std::optional<TimeDelta> unit_opt = ConsumeDurationUnit(duration_string);
146 if (!unit_opt.has_value())
147 return std::nullopt;
148
149 ParsedDecimal number = number_opt.value();
150 TimeDelta unit = unit_opt.value();
151 if (number.int_part != 0)
152 delta += sign * number.int_part * unit;
153 if (number.frac_part != 0)
154 delta +=
155 (static_cast<double>(sign) * number.frac_part / number.frac_scale) *
156 unit;
157 }
158 return delta;
159 }
160
161 } // namespace base
162