utils: support negative lexnums

We use "lexnums" to efficiently/sortably store numbers >= 0; let's
support negative numbers as well (albeit not very optimal in size yet)
This commit is contained in:
Dirk-Jan C. Binnema
2026-07-23 11:05:08 +03:00
committed by Seth Ladygo
parent 51a2bc5f60
commit a03fd48551
3 changed files with 52 additions and 9 deletions

View File

@ -1,5 +1,5 @@
/*
** Copyright (C) 2017-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
** Copyright (C) 2017-2026 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
**
** This library is free software; you can redistribute it and/or
** modify it under the terms of the GNU Lesser General Public License
@ -581,17 +581,30 @@ std::string
Mu::to_lexnum(int64_t val)
{
char buf[18]; /* 1 byte prefix + hex + \0 */
buf[0] = 'f' + ::snprintf(buf + 1, sizeof(buf) - 1, "%" PRIx64, val);
const auto len = ::snprintf(buf + 1, sizeof(buf) - 1, "%" PRIx64,
static_cast<uint64_t>(val));
/* an uppercase prefix marks a negative value (see mu-utils.hh);
* uppercase sorts before lowercase, so negative values sort before
* the positive ones. */
buf[0] = (val < 0 ? 'F' : 'f') + len;
return buf;
}
int64_t
Mu::from_lexnum(const std::string& str)
{
int64_t val{};
std::from_chars(str.c_str() + 1, str.c_str() + str.size(), val, 16);
uint64_t val{};
if (str.empty())
return 0;
return val;
const auto res = std::from_chars(str.c_str() + 1,
str.c_str() + str.size(), val, 16);
if (res.ec != std::errc{})
return 0;
/* negative values are the two's-complement representation, so
* simply cast back. */
return static_cast<int64_t>(val);
}
bool

View File

@ -677,16 +677,27 @@ inline std::string shell_quote(const std::string& str) {
/*
* Lexnums are lexicographically sortable string representations of non-negative
* integers. Start with 'f' + length of hex-representation number, followed by
* the hex representation itself. So,
* Lexnums are lexicographically sortable string representations of integers.
*
* Non-negative values start with 'f' + length of the hex-representation,
* followed by the hex representation itself. So,
*
* 0 -> 'g0'
* 1 -> 'g1'
* 10 -> 'ga'
* 16 -> 'h10'
*
* etc.
* etc. Negative values use the corresponding uppercase prefix, followed by the
* hex of their two's-complement representation (always 16 digits, hence always
* 'V'). Since uppercase sorts before lowercase in ASCII, and the
* two's-complement hex of negative values increases with the value, all lexnums
* sort in numerical order. So,
*
* -1 -> 'Vffffffffffffffff'
* -2 -> 'Vfffffffffffffffe'
*
* So, this is not quite optimimal (length-wise) for negative values; but at least
* they are supported now (they are rare).
*/
std::string to_lexnum(int64_t val);
int64_t from_lexnum(const std::string& str);

View File

@ -18,6 +18,7 @@
*/
#include <vector>
#include <limits>
#include <glib.h>
#include <iostream>
@ -302,6 +303,24 @@ test_to_from_lexnum()
g_assert_cmpuint(from_lexnum(to_lexnum(0)), ==, 0);
g_assert_cmpuint(from_lexnum(to_lexnum(7777)), ==, 7777);
g_assert_cmpuint(from_lexnum(to_lexnum(9876543)), ==, 9876543);
/* negative values have an uppercase prefix and thus sort before all
* positive values, in numerical order */
assert_equal(to_lexnum(-1), "Vffffffffffffffff");
g_assert_cmpint(from_lexnum(to_lexnum(-1)), ==, -1);
g_assert_cmpint(from_lexnum(to_lexnum(-12345)), ==, -12345);
g_assert_true(to_lexnum(-2) < to_lexnum(-1));
g_assert_true(to_lexnum(-1) < to_lexnum(0));
g_assert_true(to_lexnum(-12345) < to_lexnum(12345));
constexpr auto int64_min = std::numeric_limits<int64_t>::min();
constexpr auto int64_max = std::numeric_limits<int64_t>::max();
g_assert_cmpint(from_lexnum(to_lexnum(int64_min)), ==, int64_min);
g_assert_cmpint(from_lexnum(to_lexnum(int64_max)), ==, int64_max);
g_assert_true(to_lexnum(int64_min) < to_lexnum(-1));
g_assert_true(to_lexnum(0) < to_lexnum(int64_max));
g_assert_cmpint(from_lexnum(""), ==, 0);
}
static void