Takes a strbuf as its first argument and appends the human-readable form of 'value', the second argument, to that buffer. e.g. strbuf_append_human_readable(sb, 1012, 0, 0, HR_SPACE) produces "0.9 Ki". Documented in strbuf.h; units can be directly appended to the strbuf to produce "0.9 KiB/s", "0.9 KiB", or other unit. Supports SI magnitude prefixes as well: e.g. strbuf_append_human_readable(sb, 2024, 0, 0, HR_USE_SI) produces "2.0k", to which a unit can be appended, such as " objects" to produce "2.0k objects". Also, add in test cases to ensure it produces the expected output and to demonstrate what different arguments do. Signed-off-by: Marcus Griep <marcus@xxxxxxxx> --- .gitignore | 1 + Makefile | 2 +- strbuf.c | 92 +++++++++++++++++++++++++++++++++++++++++++++ strbuf.h | 30 +++++++++++++++ t/t0031-human-readable.sh | 9 ++++ test-human-read.c | 68 +++++++++++++++++++++++++++++++++ 6 files changed, 201 insertions(+), 1 deletions(-) create mode 100755 t/t0031-human-readable.sh create mode 100644 test-human-read.c diff --git a/.gitignore b/.gitignore index a213e8e..d65fa75 100644 --- a/.gitignore +++ b/.gitignore @@ -146,6 +146,7 @@ test-date test-delta test-dump-cache-tree test-genrandom +test-human-read test-match-trees test-parse-options test-path-utils diff --git a/Makefile b/Makefile index 90c5a13..f17ab76 100644 --- a/Makefile +++ b/Makefile @@ -1297,7 +1297,7 @@ endif ### Testing rules -TEST_PROGRAMS = test-chmtime$X test-genrandom$X test-date$X test-delta$X test-sha1$X test-match-trees$X test-parse-options$X test-path-utils$X +TEST_PROGRAMS = test-chmtime$X test-genrandom$X test-date$X test-delta$X test-sha1$X test-match-trees$X test-parse-options$X test-path-utils$X test-human-read$X all:: $(TEST_PROGRAMS) diff --git a/strbuf.c b/strbuf.c index 720737d..d9888fb 100644 --- a/strbuf.c +++ b/strbuf.c @@ -308,3 +308,95 @@ int strbuf_read_file(struct strbuf *sb, const char *path, size_t hint) return len; } + +int strbuf_append_human_readable(struct strbuf *sb, + double val, + int maxlen, int scale, + int flags) +{ + const int maxscale = 7; + + char *hr_prefixes[] = { + "", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi", "Yi", NULL + }; + char *hr_si_prefixes[] = { + "", "k", "M", "G", "T", "P", "E", "Z", "Y", NULL + }; + char **prefix = &hr_prefixes[0]; + int period = 1024; + int sign = val < 0 ? -1 : 1; + int retval = 0; + + val *= sign; + + if (flags & HR_PAD_UNIT) { + hr_prefixes[0] = " "; + hr_si_prefixes[0] = " "; + } + + if (flags & HR_USE_SI) { + period = 1000; + prefix = &hr_si_prefixes[0]; + } + + if (scale == 0) { + if (maxlen == 0) { + scale = 1000; + maxlen = 3; + } + else { + int space = maxlen; + scale = 10; + while (--space > 0) { + scale *= 10; + } + } + } + else { + int space = 1; + int check = 10; + int setscale = scale; + while (check < scale) { + check *= 10; + ++space; + if (maxlen - space == 0) + setscale = check; + } + if (!maxlen) + maxlen = space; + scale = setscale; + } + + while (val >= scale && *prefix++) + val /= period; + + if (val >= scale) { + int needed = 0; + while (val >= scale) { + val /= period; + --needed; + } + if (needed < retval) + retval = needed; + } + + strbuf_addf(sb, "%f", sign * val); + + if (maxlen) { + int signlen = sign == -1 ? 1 : 0; + maxlen -= (sb->buf[maxlen-1+signlen] == '.' ? 1 : 0); + if (maxlen <= 0) { + strbuf_setlen(sb, 0); + retval = maxlen - 1; + } else { + strbuf_setlen(sb, maxlen + signlen); + } + } + + strbuf_addf(sb, "%s%s", + flags & HR_SPACE ? " " : "", + *prefix + ); + + return retval; +} diff --git a/strbuf.h b/strbuf.h index eba7ba4..305ef55 100644 --- a/strbuf.h +++ b/strbuf.h @@ -125,4 +125,34 @@ extern int strbuf_getline(struct strbuf *, FILE *, int); extern void stripspace(struct strbuf *buf, int skip_comments); extern int launch_editor(const char *path, struct strbuf *buffer, const char *const *env); +/* + * strbuf_append_human_readable + * + * 'val': value to be metrically-reduced to a human-readable number + * 'maxlen': maximum number of characters to be taken up by the reduced 'val' + * not including the sign or magnitude (i.e. 'Ki') characters; + * when 'maxlen' is 0 length is controled by 'scale' + * 'scale': when 'val' is greater than 'scale', 'val' is reduced by the + * period (default 1024, see 'flags') until it is less than 'scale'; + * when 'scale' is 0, 'val' is reduced until it fits in 'maxlen'; + * when 'scale' and 'maxlen' are both zero, 'scale' defaults to 1000 + * 'flags': HR_USE_SI: uses a period of 1000 and uses SI magnitude prefixes + * HR_SPACE: inserts a space between the reduced 'val' and the units + * HR_PAD_UNIT: instead of an empty string for singles, pads with + * spaces to the length of the magnitude prefixes + * + * Returns 0 if 'val' is successfully reduced and fits in 'maxlen', otherwise + * returns -n where n is the number of additional characters necessary to + * fully fit the reduced value. + */ + +#define HR_USE_SI 0x01 +#define HR_SPACE 0x02 +#define HR_PAD_UNIT 0x04 + +extern int strbuf_append_human_readable(struct strbuf *, + double val, + int maxlen, int scale, + int flags); + #endif /* STRBUF_H */ diff --git a/t/t0031-human-readable.sh b/t/t0031-human-readable.sh new file mode 100755 index 0000000..ce2fd77 --- /dev/null +++ b/t/t0031-human-readable.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +test_description="Test human-readable formatting" + +. ./test-lib.sh + +test_expect_success 'human-readable formatting tests' 'test-human-read' + +test_done diff --git a/test-human-read.c b/test-human-read.c new file mode 100644 index 0000000..855296c --- /dev/null +++ b/test-human-read.c @@ -0,0 +1,68 @@ +#include "builtin.h" +#include "strbuf.h" + +struct test { + double val; + int len; + int scale; + int flags; + + char *out; + int retval; +}; + +int main(int argc, char **argv) { + int failures = 0, i = 0, retval; + struct test tests[] = { + { 1012, 0, 0, HR_SPACE, "0.9 Ki", 0 }, + { 1012, 0, 0, HR_SPACE, "0.9 Ki", 0 }, + { 1012, 0, 0, HR_SPACE | HR_USE_SI, "1.0 k", 0 }, + { -1012, 0, 0, HR_SPACE | HR_USE_SI, "-1.0 k", 0 }, + { 1012, 5, 0, HR_SPACE | HR_PAD_UNIT, "1012 ", 0 }, + { 1012, 5, 0, HR_SPACE, "1012 ", 0 }, + { 1012, 5, 0, HR_USE_SI | HR_SPACE | HR_PAD_UNIT, "1012 ", 0 }, + { 1012, 5, 0, HR_USE_SI | HR_SPACE, "1012 ", 0 }, + { 1012, 4, 0, 0, "1012", 0 }, + { 1012, 3, 0, 0, "0.9Ki", 0 }, + { 1012, 2, 0, 0, "0Ki", 0 }, + { 1012, 1, 0, 0, "0Ki", 0 }, + { 1012, 0, 0, 0, "0.9Ki", 0 }, + { -1012, 3, 0, 0, "-0.9Ki", 0 }, + { -1012, 2, 0, 0, "-0Ki", 0 }, + { -1012, 1, 0, 0, "-0Ki", 0 }, + { -1012, 0, 0, 0, "-0.9Ki", 0 }, + { 2024, 4, 0, 0, "2024", 0 }, + { 20240, 4, 0, 0, "19.7Ki", 0 }, + { 1012, 0, 1000, 0, "0.9Ki", 0 }, + { 506.0 * 1024 * 1024, 0, 1000000, 0, "518144Ki", 0 }, + { 506.0 * 1024 * 1024, 0, 1000000, 0, "518144Ki", 0 }, + { 506.0 * 1024 * 1024, 7, 1000000, 0, "518144Ki", 0 }, + { 506.0 * 1024 * 1024, 6, 1000000, 0, "518144Ki", 0 }, + { 506.0 * 1024 * 1024, 5, 1000000, 0, "506.0Mi", 0 }, + { 506.0 * 1024 * 1024, 4, 1000000, 0, "506Mi", 0 }, + { 506.0 * 1024 * 1024, 3, 1000000, 0, "506Mi", 0 }, + { 506.0 * 1024 * 1024, 2, 1000000, 0, "0Gi", 0 }, + { 506.0 * 1024 * 1024 * 1024, 0, 1000000, 0, "518144Mi", 0 }, + { 506.0 * 1024 * 1024 * 1024, 0, 1000000, HR_USE_SI, "543313M", 0 }, + { 0, 0, 0, 0, NULL, 0 } + }; + struct strbuf sb; + struct test *current = &tests[0]; + strbuf_init(&sb, 0); + + while(current->out) { + printf("Test %2d - ", ++i); + strbuf_setlen(&sb, 0); + retval = strbuf_append_human_readable(&sb, current->val, current->len, + current->scale, current->flags); + if(strcmp(sb.buf, current->out) || retval != current->retval) { + printf("Failure: Act '%s' (%d); Exp '%s' (%d)\n", + sb.buf, retval, current->out, current->retval); + ++failures; + } else + printf("Success: '%s' (%d)\n", sb.buf, retval); + ++current; + } + + return failures; +} -- 1.6.0.rc2.6.g8eda3 -- To unsubscribe from this list: send the line "unsubscribe git" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html