]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
util: add some helpers for converting percent/permille/permyriad to parts of 2^32-1
authorLennart Poettering <lennart@poettering.net>
Wed, 17 Feb 2021 15:53:11 +0000 (16:53 +0100)
committerLennart Poettering <lennart@poettering.net>
Thu, 18 Feb 2021 21:36:34 +0000 (22:36 +0100)
At various places we accept values scaled to the range 0…2^32-1 which
are exposed to the user as percentages/permille/permyriad. Let's add
some helper macros (actually: typesafe macro-like functions) that help
with converting our internal encoding to the external encodings.

benefits: some of the previous code rounded up, some down. let's always
round to nearest, to ensure that our conversions are reversible. Also,
check for overflows correctly.

This also adds a test that makes sure that for the full
percent/permille/permyriad ranges we can convert forth and back without
loss of accuracy.

src/basic/percent-util.h
src/test/test-percent-util.c

index 26707e8703aca678c97889b2db833ca4790b3e82..d806ec007a76f3b25f6afee290ade217e6dc2ac3 100644 (file)
@@ -1,6 +1,11 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 #pragma once
 
+#include <errno.h>
+#include <inttypes.h>
+
+#include "macro.h"
+
 int parse_percent_unbounded(const char *p);
 int parse_percent(const char *p);
 
@@ -9,3 +14,48 @@ int parse_permille(const char *p);
 
 int parse_permyriad_unbounded(const char *p);
 int parse_permyriad(const char *p);
+
+/* Some macro-like helpers that convert a percent/permille/permyriad value (as parsed by parse_percent()) to
+ * a value relative to 100% == 2^32-1. Rounds to closest. */
+static inline uint32_t UINT32_SCALE_FROM_PERCENT(int percent) {
+        assert_cc(INT_MAX <= UINT32_MAX);
+        return (uint32_t) (((uint64_t) percent * UINT32_MAX + 50) / 100U);
+}
+
+static inline uint32_t UINT32_SCALE_FROM_PERMILLE(int permille) {
+        return (uint32_t) (((uint64_t) permille * UINT32_MAX + 500) / 1000U);
+}
+
+static inline uint32_t UINT32_SCALE_FROM_PERMYRIAD(int permyriad) {
+        return (uint32_t) (((uint64_t) permyriad * UINT32_MAX + 5000) / 10000U);
+}
+
+static inline int UINT32_SCALE_TO_PERCENT(uint32_t scale) {
+        uint32_t u;
+
+        u = (uint32_t) ((((uint64_t) scale) * 100U + UINT32_MAX/2) / UINT32_MAX);
+        if (u > INT_MAX)
+                return -ERANGE;
+
+        return (int) u;
+}
+
+static inline int UINT32_SCALE_TO_PERMILLE(uint32_t scale) {
+        uint32_t u;
+
+        u = (uint32_t) ((((uint64_t) scale) * 1000U + UINT32_MAX/2) / UINT32_MAX);
+        if (u > INT_MAX)
+                return -ERANGE;
+
+        return (int) u;
+}
+
+static inline int UINT32_SCALE_TO_PERMYRIAD(uint32_t scale) {
+        uint32_t u;
+
+        u = (uint32_t) ((((uint64_t) scale) * 10000U + UINT32_MAX/2) / UINT32_MAX);
+        if (u > INT_MAX)
+                return -ERANGE;
+
+        return (int) u;
+}
index 75f8a6c83d86603b84d71e28aed4b5aad7571b93..b8801438a7553c73ad62c309bed67532bc696c5f 100644 (file)
@@ -2,6 +2,7 @@
 
 #include "percent-util.h"
 #include "tests.h"
+#include "time-util.h"
 
 static void test_parse_percent(void) {
         assert_se(parse_percent("") == -EINVAL);
@@ -150,6 +151,51 @@ static void test_parse_permyriad_unbounded(void) {
         assert_se(parse_permyriad_unbounded("42949672.96%") == -ERANGE);
 }
 
+static void test_scale(void) {
+        /* Check some fixed values */
+        assert_se(UINT32_SCALE_FROM_PERCENT(0) == 0);
+        assert_se(UINT32_SCALE_FROM_PERCENT(50) == UINT32_MAX/2+1);
+        assert_se(UINT32_SCALE_FROM_PERCENT(100) == UINT32_MAX);
+
+        assert_se(UINT32_SCALE_FROM_PERMILLE(0) == 0);
+        assert_se(UINT32_SCALE_FROM_PERMILLE(500) == UINT32_MAX/2+1);
+        assert_se(UINT32_SCALE_FROM_PERMILLE(1000) == UINT32_MAX);
+
+        assert_se(UINT32_SCALE_FROM_PERMYRIAD(0) == 0);
+        assert_se(UINT32_SCALE_FROM_PERMYRIAD(5000) == UINT32_MAX/2+1);
+        assert_se(UINT32_SCALE_FROM_PERMYRIAD(10000) == UINT32_MAX);
+
+        /* Make sure there's no numeric noise on the 0%…100% scale when converting from percent and back. */
+        for (int percent = 0; percent <= 100; percent++) {
+                log_debug("%i%% → %" PRIu32 " → %i%%",
+                          percent,
+                          UINT32_SCALE_FROM_PERCENT(percent),
+                          UINT32_SCALE_TO_PERCENT(UINT32_SCALE_FROM_PERCENT(percent)));
+
+                assert_se(UINT32_SCALE_TO_PERCENT(UINT32_SCALE_FROM_PERCENT(percent)) == percent);
+        }
+
+        /* Make sure there's no numeric noise on the 0‰…1000‰ scale when converting from permille and back. */
+        for (int permille = 0; permille <= 1000; permille++) {
+                log_debug("%i‰ → %" PRIu32 " → %i‰",
+                          permille,
+                          UINT32_SCALE_FROM_PERMILLE(permille),
+                          UINT32_SCALE_TO_PERMILLE(UINT32_SCALE_FROM_PERMILLE(permille)));
+
+                assert_se(UINT32_SCALE_TO_PERMILLE(UINT32_SCALE_FROM_PERMILLE(permille)) == permille);
+        }
+
+        /* Make sure there's no numeric noise on the 0‱…10000‱ scale when converting from permyriad and back. */
+        for (int permyriad = 0; permyriad <= 10000; permyriad++) {
+                log_debug("%i‱ → %" PRIu32 " → %i‱",
+                          permyriad,
+                          UINT32_SCALE_FROM_PERMYRIAD(permyriad),
+                          UINT32_SCALE_TO_PERMYRIAD(UINT32_SCALE_FROM_PERMYRIAD(permyriad)));
+
+                assert_se(UINT32_SCALE_TO_PERMYRIAD(UINT32_SCALE_FROM_PERMYRIAD(permyriad)) == permyriad);
+        }
+}
+
 int main(int argc, char *argv[]) {
         test_setup_logging(LOG_DEBUG);
 
@@ -159,6 +205,7 @@ int main(int argc, char *argv[]) {
         test_parse_permille_unbounded();
         test_parse_permyriad();
         test_parse_permyriad_unbounded();
+        test_scale();
 
         return 0;
 }