master
c 392 lines 13.6 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 #include "duration.h"
4
5 #ifdef NSEC_PER_USEC
6 #undef NSEC_PER_USEC
7 #endif
8 #define NSEC_PER_USEC (1000ULL)
9
10 #ifdef USEC_PER_MS
11 #undef USEC_PER_MS
12 #endif
13 #define USEC_PER_MS (1000ULL)
14
15 #ifdef NSEC_PER_SEC
16 #undef NSEC_PER_SEC
17 #endif
18 #define NSEC_PER_SEC (1000000000ULL)
19
20 #define NSEC_PER_MS (USEC_PER_MS * NSEC_PER_USEC)
21 #define NSEC_PER_MIN (NSEC_PER_SEC * 60ULL)
22 #define NSEC_PER_HOUR (NSEC_PER_MIN * 60ULL)
23 #define NSEC_PER_DAY (NSEC_PER_HOUR * 24ULL)
24 #define NSEC_PER_WEEK (NSEC_PER_DAY * 7ULL)
25 #define NSEC_PER_MONTH (NSEC_PER_DAY * 30ULL)
26 #define NSEC_PER_QUARTER (NSEC_PER_MONTH * 3ULL)
27
28 // more accurate, but not an integer multiple of days, weeks, months
29 #define NSEC_PER_YEAR (NSEC_PER_DAY * 365ULL)
30
31 // Define a structure to map time units to their multipliers
32 static const struct duration_unit {
33 const char *unit;
34 const bool formatter; // true when this unit should be used when formatting to string
35 const snsec_t multiplier;
36 } units[] = {
37
38 // IMPORTANT: the order of this array is crucial!
39 // The array should be sorted from the smaller unit to the biggest unit.
40 // For each multiplier value, the first entry with formatter=true is used for generation.
41
42 { .unit = "ns", .formatter = true, .multiplier = 1 }, // UCUM
43 { .unit = "nanosecond", .formatter = false, .multiplier = 1 },
44 { .unit = "nanoseconds", .formatter = false, .multiplier = 1 },
45
46 { .unit = "us", .formatter = true, .multiplier = NSEC_PER_USEC }, // UCUM
47 { .unit = "microsecond", .formatter = false, .multiplier = NSEC_PER_USEC },
48 { .unit = "microseconds", .formatter = false, .multiplier = NSEC_PER_USEC },
49
50 { .unit = "ms", .formatter = true, .multiplier = NSEC_PER_MS }, // UCUM
51 { .unit = "millisecond", .formatter = false, .multiplier = NSEC_PER_MS },
52 { .unit = "milliseconds", .formatter = false, .multiplier = NSEC_PER_MS },
53
54 { .unit = "s", .formatter = true, .multiplier = NSEC_PER_SEC }, // UCUM
55 { .unit = "sec", .formatter = false, .multiplier = NSEC_PER_SEC },
56 { .unit = "secs", .formatter = false, .multiplier = NSEC_PER_SEC },
57 { .unit = "second", .formatter = false, .multiplier = NSEC_PER_SEC },
58 { .unit = "seconds", .formatter = false, .multiplier = NSEC_PER_SEC },
59
60 { .unit = "m", .formatter = true, .multiplier = NSEC_PER_MIN }, // -
61 { .unit = "min", .formatter = false, .multiplier = NSEC_PER_MIN }, // UCUM
62 { .unit = "minute", .formatter = false, .multiplier = NSEC_PER_MIN },
63 { .unit = "minutes", .formatter = false, .multiplier = NSEC_PER_MIN },
64
65 { .unit = "h", .formatter = true, .multiplier = NSEC_PER_HOUR }, // UCUM
66 { .unit = "hr", .formatter = false, .multiplier = NSEC_PER_HOUR },
67 { .unit = "hrs", .formatter = false, .multiplier = NSEC_PER_HOUR },
68 { .unit = "hour", .formatter = false, .multiplier = NSEC_PER_HOUR },
69 { .unit = "hours", .formatter = false, .multiplier = NSEC_PER_HOUR },
70
71 { .unit = "d", .formatter = true, .multiplier = NSEC_PER_DAY }, // UCUM
72 { .unit = "day", .formatter = false, .multiplier = NSEC_PER_DAY },
73 { .unit = "days", .formatter = false, .multiplier = NSEC_PER_DAY },
74
75 { .unit = "w", .formatter = false, .multiplier = NSEC_PER_WEEK }, // -
76 { .unit = "wk", .formatter = false, .multiplier = NSEC_PER_WEEK }, // UCUM
77 { .unit = "week", .formatter = false, .multiplier = NSEC_PER_WEEK },
78 { .unit = "weeks", .formatter = false, .multiplier = NSEC_PER_WEEK },
79
80 { .unit = "mo", .formatter = true, .multiplier = NSEC_PER_MONTH }, // UCUM
81 { .unit = "M", .formatter = false, .multiplier = NSEC_PER_MONTH }, // compatibility
82 { .unit = "month", .formatter = false, .multiplier = NSEC_PER_MONTH },
83 { .unit = "months", .formatter = false, .multiplier = NSEC_PER_MONTH },
84
85 { .unit = "q", .formatter = false, .multiplier = NSEC_PER_QUARTER }, // -
86 { .unit = "quarter", .formatter = false, .multiplier = NSEC_PER_QUARTER },
87 { .unit = "quarters", .formatter = false, .multiplier = NSEC_PER_QUARTER },
88
89 { .unit = "y", .formatter = true, .multiplier = NSEC_PER_YEAR }, // -
90 { .unit = "Y", .formatter = false, .multiplier = NSEC_PER_YEAR }, // compatibility
91 { .unit = "a", .formatter = false, .multiplier = NSEC_PER_YEAR }, // UCUM
92 { .unit = "year", .formatter = false, .multiplier = NSEC_PER_YEAR },
93 { .unit = "years", .formatter = false, .multiplier = NSEC_PER_YEAR }
94 };
95
96 static inline const struct duration_unit *duration_find_unit(const char *unit) {
97 if(!unit || !*unit)
98 unit = "ns";
99
100 for (size_t i = 0; i < sizeof(units) / sizeof(units[0]); i++) {
101 const struct duration_unit *du = &units[i];
102 if (strcasecmp(unit, du->unit) == 0)
103 return du;
104 }
105
106 return NULL;
107 }
108
109 inline int64_t duration_round_to_resolution(int64_t value, int64_t resolution) {
110 if(value > 0)
111 return (value + ((resolution - 1) / 2)) / resolution;
112
113 if(value < 0)
114 return (value - ((resolution - 1) / 2)) / resolution;
115
116 return 0;
117 }
118
119 // -------------------------------------------------------------------------------------------------------------------
120 // parse a duration string
121
122 bool duration_parse(const char *duration, int64_t *result, const char *default_unit, const char *output_unit) {
123 if (!duration || !*duration) {
124 *result = 0;
125 return false;
126 }
127
128 const struct duration_unit *du_def = duration_find_unit(default_unit);
129 if(!du_def) {
130 *result = 0;
131 return false;
132 }
133
134 const struct duration_unit *du_out = duration_find_unit(output_unit);
135 if(!du_out) {
136 *result = 0;
137 return false;
138 }
139
140 int64_t sign = 1;
141 const char *s = duration;
142 while (isspace((uint8_t)*s)) s++;
143 if(*s == '-') {
144 s++;
145 sign = -1;
146 }
147
148 int64_t v = 0;
149 bool found_ago = false;
150 bool parsed_any_duration = false;
151
152 while (*s) {
153 // Skip leading spaces
154 while (isspace((uint8_t)*s)) s++;
155
156 // If no more content, break out of the loop
157 if (!*s) break;
158
159 // compatibility - case insensitive
160 if(*s == 'n' || *s == 'N') {
161 if(strcasecmp(s, "never") == 0) {
162 *result = 0;
163 return true;
164 }
165 }
166
167 if(*s == 'o' || *s == 'O') {
168 if(strcasecmp(s, "off") == 0) {
169 *result = 0;
170 return true;
171 }
172 }
173
174 // Parse the number
175 const char *number_start = s;
176 NETDATA_DOUBLE value = str2ndd(s, (char **)&s);
177
178 // If no valid number found, check if it's "ago"
179 if (s == number_start) {
180 // Maybe it's the "ago" suffix
181 if (strcasecmp(s, "ago") == 0) {
182 found_ago = true;
183 s += 3; // Skip "ago"
184 break; // Exit the loop
185 }
186 *result = 0;
187 return false;
188 }
189
190 // Skip spaces between number and unit
191 while (isspace((uint8_t)*s)) s++;
192
193 const char *unit_start = s;
194 while (isalpha((uint8_t)*s)) s++;
195
196 char unit[16]; // Increased to handle "microseconds" (12 chars)
197 size_t unit_len = s - unit_start;
198 const struct duration_unit *du = NULL;
199
200 if (unit_len == 0) {
201 du = du_def;
202 }
203 else {
204 // First check if we have "ago" at the end of the alphabetic sequence
205 if (unit_len >= 3 && strncasecmp(s - 3, "ago", 3) == 0) {
206 // We might have something like "daysago"
207 // Try to parse the unit without "ago"
208 unit_len -= 3;
209 if (unit_len > 0 && unit_len < sizeof(unit)) {
210 strncpyz(unit, unit_start, unit_len);
211 du = duration_find_unit(unit);
212 if (du) {
213 // Successfully found the unit, mark that we found "ago"
214 found_ago = true;
215 s -= 3; // Back up to just after the unit, before "ago"
216 }
217 }
218 }
219
220 // If we didn't find a unit with "ago" suffix, try the whole thing
221 if (!du) {
222 unit_len = s - unit_start;
223 if (unit_len >= sizeof(unit)) unit_len = sizeof(unit) - 1;
224 strncpyz(unit, unit_start, unit_len);
225
226 // Check if this might be "ago" by itself
227 if (strcasecmp(unit, "ago") == 0) {
228 // Found "ago" - this ends the duration parsing
229 found_ago = true;
230 break;
231 }
232
233 du = duration_find_unit(unit);
234 if(!du) {
235 *result = 0;
236 return false;
237 }
238 }
239 }
240
241 v += (int64_t)round(value * (NETDATA_DOUBLE)du->multiplier);
242 parsed_any_duration = true;
243 }
244
245 v *= sign;
246
247 // Check for "ago" suffix to negate the result if not already found
248 if (!found_ago) {
249 // Skip any trailing whitespace
250 while (isspace((uint8_t)*s)) s++;
251
252 // Check if the remaining string is "ago" (case-insensitive)
253 if (*s) {
254 if (strcasecmp(s, "ago") == 0) {
255 found_ago = true;
256 s += 3; // Skip past "ago"
257 }
258 else {
259 // If there's any other trailing text, it's an error
260 *result = 0;
261 return false;
262 }
263 }
264 }
265
266 // Apply "ago" negation if found
267 if (found_ago) {
268 // But only if we actually parsed some duration
269 if (!parsed_any_duration) {
270 // "ago" without any duration is an error
271 *result = 0;
272 return false;
273 }
274
275 // If the original sign was negative, "ago" is redundant
276 // For example: "-7 days ago" means the same as "-7 days"
277 // We keep it negative (don't apply double negative)
278 if (sign > 0) {
279 v = -v; // Only negate if originally positive
280 }
281 // If sign < 0, v is already negative, so we keep it that way
282
283 // Check for any trailing content after "ago"
284 while (isspace((uint8_t)*s)) s++;
285 if (*s) {
286 // Extra text after "ago" is an error
287 *result = 0;
288 return false;
289 }
290 }
291
292 // Convert the final value from nanoseconds to the desired output unit
293 // and apply appropriate rounding
294 if(du_out->multiplier == 1)
295 *result = v;
296 else {
297 // First convert to the output unit
298 NETDATA_DOUBLE converted = (NETDATA_DOUBLE)v / (NETDATA_DOUBLE)du_out->multiplier;
299 // Then round to nearest integer in the output unit
300 *result = (int64_t)round(converted);
301 }
302
303 return true;
304 }
305
306 // --------------------------------------------------------------------------------------------------------------------
307 // generate a string to represent a duration
308
309 ssize_t duration_snprintf(char *dst, size_t dst_size, int64_t value, const char *unit, bool add_spaces) {
310 if (!dst || dst_size == 0) return -1;
311 if (dst_size == 1) {
312 dst[0] = '\0';
313 return -2;
314 }
315
316 if(value == 0)
317 return snprintfz(dst, dst_size, "off");
318
319 const char *sign = "";
320 if(value < 0) {
321 sign = "-";
322 value = -value;
323 }
324
325 const struct duration_unit *du_min = duration_find_unit(unit);
326 size_t offset = 0;
327
328 int64_t nsec = value * du_min->multiplier;
329
330 // Iterate through units from largest to smallest
331 for (ssize_t i = (ssize_t)(sizeof(units) / sizeof(units[0])) - 1; i >= 0 && nsec > 0; i--) {
332 const struct duration_unit *du = &units[i];
333 if(!units[i].formatter && du != du_min)
334 continue;
335
336 // IMPORTANT:
337 // The week (7 days) is not aligned to the quarter (~91 days) or the year (365.25 days).
338 // To make sure that the value returned can be parsed back without loss,
339 // we have to round the value per unit (inside this loop), not globally.
340 // Otherwise, we have to make sure that all larger units are integer multiples of the smaller ones.
341
342 int64_t multiplier = units[i].multiplier;
343 int64_t rounded = (du == du_min) ? (duration_round_to_resolution(nsec, multiplier) * multiplier) : nsec;
344
345 int64_t unit_count = rounded / multiplier;
346 if (unit_count > 0) {
347 const char *space = (add_spaces && offset) ? " " : "";
348 int written = snprintfz(dst + offset, dst_size - offset,
349 "%s%s%" PRIi64 "%s", space, sign, unit_count, units[i].unit);
350
351 if (written < 0)
352 return -3;
353
354 sign = "";
355 offset += written;
356
357 if (offset >= dst_size) {
358 // buffer overflow
359 return (ssize_t)offset;
360 }
361
362 if(unit_count * multiplier >= nsec)
363 break;
364 else
365 nsec -= unit_count * multiplier;
366 }
367
368 if(du == du_min)
369 // we should not go to smaller units
370 break;
371 }
372
373 if (offset == 0)
374 // nothing has been written
375 offset = snprintfz(dst, dst_size, "off");
376
377 return (ssize_t)offset;
378 }
379
380 // --------------------------------------------------------------------------------------------------------------------
381 // compatibility for parsing seconds in int.
382
383 bool duration_parse_seconds(const char *str, int *result) {
384 int64_t v;
385
386 if(duration_parse_time_t(str, &v)) {
387 *result = (int)v;
388 return true;
389 }
390
391 return false;
392 }