From 7717c675465dfaf78e78b05bdbe1e258bddb6bbf Mon Sep 17 00:00:00 2001 From: Philip Top Date: Thu, 6 Aug 2026 05:57:36 -0700 Subject: [PATCH 01/35] address some issues raised concerning half power exponents for meters which is a valid unit. allow more complete processing of valid units with half power and decimals in the exponent --- test/test_unit_ops.cpp | 27 ++++ test/test_unit_strings.cpp | 28 ++++ units/units.cpp | 235 ++++++++++++++++++++++---------- units/units_conversion_maps.hpp | 8 +- units/units_decl.hpp | 11 +- 5 files changed, 231 insertions(+), 78 deletions(-) diff --git a/test/test_unit_ops.cpp b/test/test_unit_ops.cpp index 2bb1d769..1cf13b18 100644 --- a/test/test_unit_ops.cpp +++ b/test/test_unit_ops.cpp @@ -483,6 +483,8 @@ TEST(preciseUnitOps, rootMeter) auto m1 = precise::m.pow(1); EXPECT_EQ(precise::m, root(m1, 1)); EXPECT_EQ(precise::m.inv(), root(m1, -1)); + EXPECT_EQ(precise::m, precise::special::rootMeter.pow(2)); + EXPECT_EQ(precise::m.inv(), precise::special::rootMeter.inv().pow(2)); auto m2 = pow(precise::m, 2); // use the alternate free function form EXPECT_EQ(precise::m, root(m2, 2)); EXPECT_EQ(precise::m, sqrt(m2)); @@ -831,6 +833,11 @@ TEST(specialOps, rootHertz) EXPECT_FALSE(is_error(precise::special::ASD)); auto rh = precise::special::rootHertz; + auto asd = precise::m / precise::s.pow(2) / rh; + + EXPECT_EQ(asd, precise::special::ASD); + EXPECT_EQ(unit_from_string("[m/s2/Hz^(1/2)]"), precise::special::ASD); + EXPECT_EQ(unit_from_string("m/s^2/Hz^0.5"), precise::special::ASD); EXPECT_EQ(rh.pow(1), rh); EXPECT_EQ(rh.pow(0), precise::one); @@ -851,6 +858,26 @@ TEST(specialOps, rootHertz) // EXPECT_EQ(rh.pow(-6), s.pow(3)); } +TEST(specialOps, rootMeterAndRootHertzDistinct) +{ + auto rm = precise::special::rootMeter; + auto rh = precise::special::rootHertz; + + EXPECT_NE(rm, rh); + EXPECT_EQ(rm, unit_from_string("m^0.5")); + EXPECT_EQ(rm, unit_from_string("sqrt(m)")); + EXPECT_EQ(rh, unit_from_string("Hz^0.5")); + EXPECT_EQ(rh.inv(), unit_from_string("sqrt(s)")); + + EXPECT_EQ(rm.pow(2), precise::m); + EXPECT_EQ(rh.pow(2), precise::Hz); + EXPECT_EQ(rm.pow(-2), precise::m.inv()); + EXPECT_EQ(rh.pow(-2), precise::s); + + EXPECT_NE(rm.pow(2), precise::Hz); + EXPECT_NE(rh.pow(2), precise::m); +} + TEST(specialOps, degC) { auto res = precise::degC.pow(2); diff --git a/test/test_unit_strings.cpp b/test/test_unit_strings.cpp index bbf59936..ace85b8c 100644 --- a/test/test_unit_strings.cpp +++ b/test/test_unit_strings.cpp @@ -557,6 +557,34 @@ TEST(stringToUnits, morePower) EXPECT_EQ(precise::us::mile.pow(2), unit_from_string("mi(USA)^(2)")); } +TEST(stringToUnits, decimalPowerExponents) +{ + EXPECT_EQ(precise::special::rootMeter, unit_from_string("m^0.5")); + EXPECT_EQ(precise::special::rootMeter, unit_from_string("m**0.5")); + EXPECT_EQ(precise::special::rootMeter, unit_from_string("m^(0.5)")); + EXPECT_EQ(precise::special::rootMeter.inv(), unit_from_string("m^-0.5")); + EXPECT_EQ(precise::special::rootMeter.inv(), unit_from_string("m^(-0.5)")); + EXPECT_EQ(precise::m.pow(2), unit_from_string("m^2.0")); + EXPECT_EQ(precise::m.pow(2), unit_from_string("m^2.00")); + EXPECT_EQ(precise::Hz, unit_from_string("s^-1.0")); + EXPECT_EQ(precise::m.pow(2), unit_from_string("m^2e0")); + EXPECT_EQ(precise::special::rootHertz, unit_from_string("Hz^(0.5)")); + + const std::vector invalidPowers{ + "m^0.25", + "m^(0.25)", + "m^1.5", + "m^2.25", + "m^2.5", + "m^-2.5", + "m^2e", + "m^2e2"}; + + for (const auto& powerString : invalidPowers) { + EXPECT_TRUE(is_error(unit_from_string(powerString))) << powerString; + } +} + TEST(stringToUnits, specialUnits) { EXPECT_EQ( diff --git a/units/units.cpp b/units/units.cpp index 980d1438..abc2fc86 100644 --- a/units/units.cpp +++ b/units/units.cpp @@ -3960,40 +3960,63 @@ static bool checkExponentOperations(const std::string& unit_string) while (cx != std::string::npos) { const bool ndigit = isDigitCharacter(unit_string[cx - 1]); ++cx; - const char c = unit_string[cx]; - if (!isDigitCharacter(c)) { - if (c == '-') { - if (!isDigitCharacter(unit_string[cx + 1])) { - return false; - } + const bool parenthesized = (unit_string[cx] == '('); + if (parenthesized) { + ++cx; + } + if (unit_string[cx] == '-' || unit_string[cx] == '+') { + ++cx; + } + const auto startDigit = cx; + bool dpoint_encountered = false; + while (cx < unit_string.size() && isDigitCharacter(unit_string[cx])) { + ++cx; + } + const auto wholeDigitCount = cx - startDigit; + if (cx < unit_string.size() && unit_string[cx] == '.') { + dpoint_encountered = true; + ++cx; + while (cx < unit_string.size() && + isDigitCharacter(unit_string[cx])) { ++cx; - } else if (c == '(') { + } + } + if (wholeDigitCount == 0 && !dpoint_encountered) { + return false; + } + if (cx < unit_string.size() && + (unit_string[cx] == 'e' || unit_string[cx] == 'E')) { + ++cx; + if (cx < unit_string.size() && + (unit_string[cx] == '-' || unit_string[cx] == '+')) { ++cx; - if (unit_string[cx] == '-') { - ++cx; - } - bool dpoint_encountered = false; - while (unit_string[cx] != ')') { - if (!isDigitCharacter(unit_string[cx])) { - if (unit_string[cx] == '.' && !dpoint_encountered) { - dpoint_encountered = true; - } else { - return false; - } - } - ++cx; - } - } else { + } + const auto exponentDigitStart = cx; + while (cx < unit_string.size() && + isDigitCharacter(unit_string[cx])) { + ++cx; + } + if (cx == exponentDigitStart) { return false; } } + if (parenthesized) { + if (cx >= unit_string.size() || unit_string[cx] != ')') { + return false; + } + } else if (cx < unit_string.size() && + unit_string[cx] != '*' && unit_string[cx] != '/' && + unit_string[cx] != '^') { + return false; + } else { + --cx; + } #ifdef UNITS_CONSTEXPR_IF_SUPPORTED if constexpr (detail::bitwidth::base_size == sizeof(std::uint32_t)) { #else if (detail::bitwidth::base_size == sizeof(std::uint32_t)) { #endif - if (unit_string.size() > cx + 1 && - isDigitCharacter(unit_string[cx + 1]) && !ndigit) { + if (wholeDigitCount > 1 && !ndigit) { // non representable unit power return false; } @@ -4774,7 +4797,7 @@ static bool cleanUnitString(std::string& unit_string, std::uint64_t match_flags) if (seq > 1) { auto c2 = unit_string[fnd + seq]; if (c2 != '\0' && c2 != '*' && c2 != '/' && c2 != '^' && - c2 != 'e' && c2 != 'E') { + c2 != 'e' && c2 != 'E' && c2 != '.') { unit_string.insert(fnd + seq, 1, '*'); } } @@ -5297,6 +5320,90 @@ static precise_unit unit_to_the_power_of( return precise::defunit; } +static bool string_power_to_twice_power( + const std::string& powerString, + int& twicePower) +{ + char* eptr{nullptr}; + const auto power = std::strtod(powerString.c_str(), &eptr); + if (eptr != powerString.c_str() + powerString.size() || + !std::isfinite(power)) { + return false; + } + const auto scaledPower = 2.0 * power; + const auto roundedPower = std::round(scaledPower); + if (std::abs(scaledPower - roundedPower) > 1e-12 || + roundedPower > static_cast(std::numeric_limits::max()) || + roundedPower < static_cast(std::numeric_limits::min())) { + return false; + } + twicePower = static_cast(roundedPower); + return true; +} + +static precise_unit root_with_special_units(const precise_unit& un) +{ + auto retunit = root(un, 2); + if (!is_error(retunit)) { + return retunit; + } + + const auto meterPower = un.base_units().meter(); + if (meterPower % 2 != 0) { + const auto adjusted = + (meterPower > 0) ? un / precise::m : un * precise::m; + retunit = root(adjusted, 2); + if (!is_error(retunit)) { + return (meterPower > 0) ? + retunit * precise::special::rootMeter : + retunit / precise::special::rootMeter; + } + } + + const auto secondPower = un.base_units().second(); + if (secondPower % 2 != 0) { + const auto adjusted = + (secondPower < 0) ? un / precise::Hz : un * precise::Hz; + retunit = root(adjusted, 2); + if (!is_error(retunit)) { + return (secondPower < 0) ? + retunit * precise::special::rootHertz : + retunit / precise::special::rootHertz; + } + } + + return precise::invalid; +} + +static precise_unit unit_to_the_half_power_of( + const std::string& unit_string, + int twicePower, + std::uint64_t match_flags) +{ + const auto wholePower = twicePower / 2; + auto retunit = (wholePower == 0) ? + precise::one : + unit_to_the_power_of(unit_string, wholePower, match_flags); + if (is_error(retunit)) { + return precise::invalid; + } + + auto rootUnit = unit_to_the_power_of( + unit_string, (twicePower > 0) ? 1 : -1, match_flags); + if (is_error(rootUnit)) { + return precise::invalid; + } + rootUnit = root_with_special_units(rootUnit); + if (is_error(rootUnit)) { + return precise::invalid; + } + if (wholePower != 0 && + (rootUnit.has_i_flag() || rootUnit.has_e_flag())) { + return precise::invalid; + } + return retunit * rootUnit; +} + static precise_unit checkSIprefix(const std::string& unit_string, std::uint64_t match_flags) { @@ -5632,60 +5739,40 @@ static precise_unit unit_from_string_internal( sep = findOperatorSep(unit_string, "^"); if (sep != std::string::npos) { auto pchar = sep - 1; - if (unit_string[sep + 1] == '(') { - ++sep; - } - const char c1 = unit_string[sep + 1]; - int power{+1}; - if (c1 == '-' || c1 == '+') { - ++sep; - if (unit_string.length() < sep + 2) { - // this should have been caught as an invalid sequence - // earlier - return precise::invalid; // LCOV_EXCL_LINE - } - // the - ',' is a +/- sign - power = -(c1 - ','); - } - if (isDigitCharacter(unit_string[sep + 1])) { + const bool parenthesizedPower = (unit_string[sep + 1] == '('); + const auto powerStart = sep + (parenthesizedPower ? 2 : 1); + auto powerLength = unit_string.size() - powerStart; + if (parenthesizedPower) { + if (unit_string.back() != ')' || powerLength < 2) { + return precise::invalid; + } + --powerLength; + } + int twicePower{0}; + if (!string_power_to_twice_power( + unit_string.substr(powerStart, powerLength), twicePower)) { + return precise::invalid; + } #ifdef UNITS_CONSTEXPR_IF_SUPPORTED - if constexpr (sizeof(UNITS_BASE_TYPE) == 8) { + if constexpr (sizeof(UNITS_BASE_TYPE) != 8) { #else - if (sizeof(UNITS_BASE_TYPE) == 8) { + if (sizeof(UNITS_BASE_TYPE) != 8) { #endif - size_t end = sep + 2; - for (; end < unit_string.size() && - isDigitCharacter(unit_string[end]); - ++end) { - } - auto powerStringLength = end - sep - 1; - if (powerStringLength > 1) { - auto pstring = - unit_string.substr(sep + 1, powerStringLength); - char* eptr{nullptr}; - auto mpower = strtoul(pstring.c_str(), &eptr, 10); - if (eptr - pstring.c_str() == - static_cast(powerStringLength)) { - power *= mpower; - } else { - return precise::invalid; // LCOV_EXCL_LINE - } - } else { - power *= (unit_string[sep + 1] - '0'); - } - - } else { - power *= (unit_string[sep + 1] - '0'); + if (!isDigitCharacter(unit_string[pchar]) && + (twicePower > 18 || twicePower < -18)) { + return precise::invalid; } + } + + const auto powerUnitString = + unit_string.substr(0, (pchar > 0) ? pchar + 1 : 1); + if (twicePower % 2 == 0) { + retunit = unit_to_the_power_of( + powerUnitString, twicePower / 2, match_flags); } else { - // the check functions should catch this but it would be - // problematic if not caught - return precise::invalid; // LCOV_EXCL_LINE - } - retunit = unit_to_the_power_of( - unit_string.substr(0, (pchar > 0) ? pchar + 1 : 1), - power, - match_flags); + retunit = unit_to_the_half_power_of( + powerUnitString, twicePower, match_flags); + } if (retunit != precise::defunit) { return retunit; } diff --git a/units/units_conversion_maps.hpp b/units/units_conversion_maps.hpp index beaea07d..5948f17f 100644 --- a/units/units_conversion_maps.hpp +++ b/units/units_conversion_maps.hpp @@ -13,7 +13,7 @@ SPDX-License-Identifier: BSD-3-Clause namespace UNITS_NAMESPACE { -UNITS_CPP14_CONSTEXPR_OBJECT std::array, 113> +UNITS_CPP14_CONSTEXPR_OBJECT std::array, 114> defined_unit_names_si{ {{m, "m"}, {m * m, "m^2"}, @@ -81,6 +81,7 @@ UNITS_CPP14_CONSTEXPR_OBJECT std::array, 113> {percent, "%"}, {unit_cast(precise::special::ASD), "ASD"}, {unit_cast(precise::special::rootHertz), "rootHertz"}, + {unit_cast(precise::special::rootMeter), "rootMeter"}, {currency, "$"}, {count, "count"}, {ratio, ""}, @@ -204,7 +205,7 @@ UNITS_CPP14_CONSTEXPR_OBJECT std::array, 55> /// definitions for the default units for specific types of measurmeents UNITS_CPP14_CONSTEXPR_OBJECT std::array< std::pair, - 1216> + 1219> defined_unit_strings_si{ {{"", precise::defunit}, {"[]", precise::defunit}, @@ -1409,6 +1410,8 @@ UNITS_CPP14_CONSTEXPR_OBJECT std::array< {"ASD", precise::special::ASD}, {"[m/s2/Hz^(1/2)]", precise::special::ASD}, {"[M/s2/HZ^(1/2)]", precise::special::ASD}, + {"sqrt(m)", precise::special::rootMeter}, + {"sqrt(s)", precise::special::rootHertz.inv()}, {"Hz^(1/2)", precise::special::rootHertz}, {"HZ^(1/2)", precise::special::rootHertz}, {"squarerootofhertz", precise::special::rootHertz}, @@ -1416,6 +1419,7 @@ UNITS_CPP14_CONSTEXPR_OBJECT std::array< {"roothertz", precise::special::rootHertz}, // capitalized version is needed since this is also a generated unit {"rootHertz", precise::special::rootHertz}, + {"rootmeter", precise::special::rootMeter}, {"B", precise::data::byte}, {"bel", precise::log::bel}, {"dB", precise::log::dB}, diff --git a/units/units_decl.hpp b/units/units_decl.hpp index c0167e3f..dc980c24 100644 --- a/units/units_decl.hpp +++ b/units/units_decl.hpp @@ -208,9 +208,9 @@ namespace detail { /// take a unit_data to some power constexpr unit_data pow(int power) const { // the modifier is to handle a few weird operations that operate on - // square_root Hz, + // square_root meter and Hz, return { - meter_ * power, + (meter_ * power) + rootMeterModifier(power), kilogram_ * power, (second_ * power) + rootHertzModifier(power), ampere_ * power, @@ -460,6 +460,13 @@ namespace detail { 0 : (power / 2) * ((second_ < 0) || (power < 0) ? 9 : -9); } + constexpr int rootMeterModifier(int power) const + { + return (meter_ * power == 0 || ((e_flag_ & i_flag_) == 0U) || + power % 2 != 0) ? + 0 : + (power / 2) * ((meter_ < 0) ? 11 : -11); + } // needs to be defined for the full 32 bits(or 64 bits) signed int meter_ : bitwidth::meter; From de26eb5abfbce7de34e36afab878a755ad808d34 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Thu, 6 Aug 2026 06:58:34 -0700 Subject: [PATCH 02/35] move tests to correct location --- test/test_unit_ops.cpp | 6 ------ test/test_unit_strings.cpp | 5 +++++ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/test/test_unit_ops.cpp b/test/test_unit_ops.cpp index 1cf13b18..0a8a4dc2 100644 --- a/test/test_unit_ops.cpp +++ b/test/test_unit_ops.cpp @@ -836,8 +836,6 @@ TEST(specialOps, rootHertz) auto asd = precise::m / precise::s.pow(2) / rh; EXPECT_EQ(asd, precise::special::ASD); - EXPECT_EQ(unit_from_string("[m/s2/Hz^(1/2)]"), precise::special::ASD); - EXPECT_EQ(unit_from_string("m/s^2/Hz^0.5"), precise::special::ASD); EXPECT_EQ(rh.pow(1), rh); EXPECT_EQ(rh.pow(0), precise::one); @@ -864,10 +862,6 @@ TEST(specialOps, rootMeterAndRootHertzDistinct) auto rh = precise::special::rootHertz; EXPECT_NE(rm, rh); - EXPECT_EQ(rm, unit_from_string("m^0.5")); - EXPECT_EQ(rm, unit_from_string("sqrt(m)")); - EXPECT_EQ(rh, unit_from_string("Hz^0.5")); - EXPECT_EQ(rh.inv(), unit_from_string("sqrt(s)")); EXPECT_EQ(rm.pow(2), precise::m); EXPECT_EQ(rh.pow(2), precise::Hz); diff --git a/test/test_unit_strings.cpp b/test/test_unit_strings.cpp index ace85b8c..872b84c0 100644 --- a/test/test_unit_strings.cpp +++ b/test/test_unit_strings.cpp @@ -562,13 +562,18 @@ TEST(stringToUnits, decimalPowerExponents) EXPECT_EQ(precise::special::rootMeter, unit_from_string("m^0.5")); EXPECT_EQ(precise::special::rootMeter, unit_from_string("m**0.5")); EXPECT_EQ(precise::special::rootMeter, unit_from_string("m^(0.5)")); + EXPECT_EQ(precise::special::rootMeter, unit_from_string("sqrt(m)")); EXPECT_EQ(precise::special::rootMeter.inv(), unit_from_string("m^-0.5")); EXPECT_EQ(precise::special::rootMeter.inv(), unit_from_string("m^(-0.5)")); EXPECT_EQ(precise::m.pow(2), unit_from_string("m^2.0")); EXPECT_EQ(precise::m.pow(2), unit_from_string("m^2.00")); EXPECT_EQ(precise::Hz, unit_from_string("s^-1.0")); EXPECT_EQ(precise::m.pow(2), unit_from_string("m^2e0")); + EXPECT_EQ(precise::special::rootHertz, unit_from_string("Hz^0.5")); EXPECT_EQ(precise::special::rootHertz, unit_from_string("Hz^(0.5)")); + EXPECT_EQ(precise::special::rootHertz.inv(), unit_from_string("sqrt(s)")); + EXPECT_EQ(unit_from_string("[m/s2/Hz^(1/2)]"), precise::special::ASD); + EXPECT_EQ(unit_from_string("m/s^2/Hz^0.5"), precise::special::ASD); const std::vector invalidPowers{ "m^0.25", From 1d259f6b0706a26ee672b75b3cf7c08a7233b6e2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:38:43 +0000 Subject: [PATCH 03/35] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- units/units.cpp | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/units/units.cpp b/units/units.cpp index abc2fc86..7750f04b 100644 --- a/units/units.cpp +++ b/units/units.cpp @@ -4004,9 +4004,9 @@ static bool checkExponentOperations(const std::string& unit_string) if (cx >= unit_string.size() || unit_string[cx] != ')') { return false; } - } else if (cx < unit_string.size() && - unit_string[cx] != '*' && unit_string[cx] != '/' && - unit_string[cx] != '^') { + } else if ( + cx < unit_string.size() && unit_string[cx] != '*' && + unit_string[cx] != '/' && unit_string[cx] != '^') { return false; } else { --cx; @@ -5320,9 +5320,8 @@ static precise_unit unit_to_the_power_of( return precise::defunit; } -static bool string_power_to_twice_power( - const std::string& powerString, - int& twicePower) +static bool + string_power_to_twice_power(const std::string& powerString, int& twicePower) { char* eptr{nullptr}; const auto power = std::strtod(powerString.c_str(), &eptr); @@ -5354,9 +5353,8 @@ static precise_unit root_with_special_units(const precise_unit& un) (meterPower > 0) ? un / precise::m : un * precise::m; retunit = root(adjusted, 2); if (!is_error(retunit)) { - return (meterPower > 0) ? - retunit * precise::special::rootMeter : - retunit / precise::special::rootMeter; + return (meterPower > 0) ? retunit * precise::special::rootMeter : + retunit / precise::special::rootMeter; } } @@ -5366,9 +5364,8 @@ static precise_unit root_with_special_units(const precise_unit& un) (secondPower < 0) ? un / precise::Hz : un * precise::Hz; retunit = root(adjusted, 2); if (!is_error(retunit)) { - return (secondPower < 0) ? - retunit * precise::special::rootHertz : - retunit / precise::special::rootHertz; + return (secondPower < 0) ? retunit * precise::special::rootHertz : + retunit / precise::special::rootHertz; } } @@ -5397,8 +5394,7 @@ static precise_unit unit_to_the_half_power_of( if (is_error(rootUnit)) { return precise::invalid; } - if (wholePower != 0 && - (rootUnit.has_i_flag() || rootUnit.has_e_flag())) { + if (wholePower != 0 && (rootUnit.has_i_flag() || rootUnit.has_e_flag())) { return precise::invalid; } return retunit * rootUnit; From b50339665f25fc98ac5b58ad409a1f341606237c Mon Sep 17 00:00:00 2001 From: Philip Top Date: Thu, 6 Aug 2026 07:21:58 -0700 Subject: [PATCH 04/35] fix some more failing test cases --- test/test_unit_strings.cpp | 4 ++++ units/units.cpp | 4 +++- units/units_decl.hpp | 8 ++++++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/test/test_unit_strings.cpp b/test/test_unit_strings.cpp index 872b84c0..07683076 100644 --- a/test/test_unit_strings.cpp +++ b/test/test_unit_strings.cpp @@ -567,6 +567,10 @@ TEST(stringToUnits, decimalPowerExponents) EXPECT_EQ(precise::special::rootMeter.inv(), unit_from_string("m^(-0.5)")); EXPECT_EQ(precise::m.pow(2), unit_from_string("m^2.0")); EXPECT_EQ(precise::m.pow(2), unit_from_string("m^2.00")); + EXPECT_EQ(precise::kg / precise::m.pow(2), unit_from_string("kg/(m^2)")); + EXPECT_EQ( + precise::kg / precise::m.pow(2), + unit_from_string("kg/(m2)")); EXPECT_EQ(precise::Hz, unit_from_string("s^-1.0")); EXPECT_EQ(precise::m.pow(2), unit_from_string("m^2e0")); EXPECT_EQ(precise::special::rootHertz, unit_from_string("Hz^0.5")); diff --git a/units/units.cpp b/units/units.cpp index 7750f04b..dcd972b4 100644 --- a/units/units.cpp +++ b/units/units.cpp @@ -4006,7 +4006,9 @@ static bool checkExponentOperations(const std::string& unit_string) } } else if ( cx < unit_string.size() && unit_string[cx] != '*' && - unit_string[cx] != '/' && unit_string[cx] != '^') { + unit_string[cx] != '/' && unit_string[cx] != '^' && + unit_string[cx] != ')' && unit_string[cx] != ']' && + unit_string[cx] != '}') { return false; } else { --cx; diff --git a/units/units_decl.hpp b/units/units_decl.hpp index dc980c24..1f8aefad 100644 --- a/units/units_decl.hpp +++ b/units/units_decl.hpp @@ -462,8 +462,12 @@ namespace detail { } constexpr int rootMeterModifier(int power) const { - return (meter_ * power == 0 || ((e_flag_ & i_flag_) == 0U) || - power % 2 != 0) ? + return ((meter_ != -5 && meter_ != 5) || + ((e_flag_ & i_flag_) == 0U) || power % 2 != 0 || + second_ != 0 || kilogram_ != 0 || ampere_ != 0 || + candela_ != 0 || kelvin_ != 0 || mole_ != 0 || + radians_ != 0 || currency_ != 0 || count_ != 0 || + equation_ != 0U) ? 0 : (power / 2) * ((meter_ < 0) ? 11 : -11); } From edf7f2d8da3aeead14b4e8d8eae96892a32c20ef Mon Sep 17 00:00:00 2001 From: Philip Top Date: Thu, 6 Aug 2026 09:12:03 -0700 Subject: [PATCH 05/35] remove the conditions to square rootMeter --- test/test_unit_ops.cpp | 5 ----- units/units_decl.hpp | 16 ++-------------- 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/test/test_unit_ops.cpp b/test/test_unit_ops.cpp index 0a8a4dc2..08d835aa 100644 --- a/test/test_unit_ops.cpp +++ b/test/test_unit_ops.cpp @@ -483,8 +483,6 @@ TEST(preciseUnitOps, rootMeter) auto m1 = precise::m.pow(1); EXPECT_EQ(precise::m, root(m1, 1)); EXPECT_EQ(precise::m.inv(), root(m1, -1)); - EXPECT_EQ(precise::m, precise::special::rootMeter.pow(2)); - EXPECT_EQ(precise::m.inv(), precise::special::rootMeter.inv().pow(2)); auto m2 = pow(precise::m, 2); // use the alternate free function form EXPECT_EQ(precise::m, root(m2, 2)); EXPECT_EQ(precise::m, sqrt(m2)); @@ -863,12 +861,9 @@ TEST(specialOps, rootMeterAndRootHertzDistinct) EXPECT_NE(rm, rh); - EXPECT_EQ(rm.pow(2), precise::m); EXPECT_EQ(rh.pow(2), precise::Hz); - EXPECT_EQ(rm.pow(-2), precise::m.inv()); EXPECT_EQ(rh.pow(-2), precise::s); - EXPECT_NE(rm.pow(2), precise::Hz); EXPECT_NE(rh.pow(2), precise::m); } diff --git a/units/units_decl.hpp b/units/units_decl.hpp index 1f8aefad..a41bf1ea 100644 --- a/units/units_decl.hpp +++ b/units/units_decl.hpp @@ -208,9 +208,9 @@ namespace detail { /// take a unit_data to some power constexpr unit_data pow(int power) const { // the modifier is to handle a few weird operations that operate on - // square_root meter and Hz, + // square_root Hz, return { - (meter_ * power) + rootMeterModifier(power), + meter_ * power, kilogram_ * power, (second_ * power) + rootHertzModifier(power), ampere_ * power, @@ -460,18 +460,6 @@ namespace detail { 0 : (power / 2) * ((second_ < 0) || (power < 0) ? 9 : -9); } - constexpr int rootMeterModifier(int power) const - { - return ((meter_ != -5 && meter_ != 5) || - ((e_flag_ & i_flag_) == 0U) || power % 2 != 0 || - second_ != 0 || kilogram_ != 0 || ampere_ != 0 || - candela_ != 0 || kelvin_ != 0 || mole_ != 0 || - radians_ != 0 || currency_ != 0 || count_ != 0 || - equation_ != 0U) ? - 0 : - (power / 2) * ((meter_ < 0) ? 11 : -11); - } - // needs to be defined for the full 32 bits(or 64 bits) signed int meter_ : bitwidth::meter; signed int second_ : bitwidth::second; // 8 From db7aa0445f9bfa515bb8518dd226d2d66e9b17de Mon Sep 17 00:00:00 2001 From: Philip Top Date: Thu, 6 Aug 2026 09:27:00 -0700 Subject: [PATCH 06/35] fix expectation on powers for 64 representation --- test/test_unit_strings.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test/test_unit_strings.cpp b/test/test_unit_strings.cpp index 07683076..68529931 100644 --- a/test/test_unit_strings.cpp +++ b/test/test_unit_strings.cpp @@ -573,6 +573,15 @@ TEST(stringToUnits, decimalPowerExponents) unit_from_string("kg/(m2)")); EXPECT_EQ(precise::Hz, unit_from_string("s^-1.0")); EXPECT_EQ(precise::m.pow(2), unit_from_string("m^2e0")); +#ifdef UNITS_CONSTEXPR_IF_SUPPORTED + if constexpr (detail::bitwidth::base_size == sizeof(std::uint64_t)) { +#else + if (detail::bitwidth::base_size == sizeof(std::uint64_t)) { +#endif + EXPECT_EQ(precise::m.pow(200), unit_from_string("m^2e2")); + } else { + EXPECT_TRUE(is_error(unit_from_string("m^2e2"))); + } EXPECT_EQ(precise::special::rootHertz, unit_from_string("Hz^0.5")); EXPECT_EQ(precise::special::rootHertz, unit_from_string("Hz^(0.5)")); EXPECT_EQ(precise::special::rootHertz.inv(), unit_from_string("sqrt(s)")); @@ -586,8 +595,7 @@ TEST(stringToUnits, decimalPowerExponents) "m^2.25", "m^2.5", "m^-2.5", - "m^2e", - "m^2e2"}; + "m^2e"}; for (const auto& powerString : invalidPowers) { EXPECT_TRUE(is_error(unit_from_string(powerString))) << powerString; From 2e2237daded5c27ddd91b45b0adce3df31dd6707 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:27:25 +0000 Subject: [PATCH 07/35] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- test/test_unit_strings.cpp | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/test/test_unit_strings.cpp b/test/test_unit_strings.cpp index 68529931..4d10d0bc 100644 --- a/test/test_unit_strings.cpp +++ b/test/test_unit_strings.cpp @@ -589,13 +589,7 @@ TEST(stringToUnits, decimalPowerExponents) EXPECT_EQ(unit_from_string("m/s^2/Hz^0.5"), precise::special::ASD); const std::vector invalidPowers{ - "m^0.25", - "m^(0.25)", - "m^1.5", - "m^2.25", - "m^2.5", - "m^-2.5", - "m^2e"}; + "m^0.25", "m^(0.25)", "m^1.5", "m^2.25", "m^2.5", "m^-2.5", "m^2e"}; for (const auto& powerString : invalidPowers) { EXPECT_TRUE(is_error(unit_from_string(powerString))) << powerString; From 7586f87d173ad492e32ca70848009ba1717e5167 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Thu, 6 Aug 2026 10:56:28 -0700 Subject: [PATCH 08/35] add some more checks and support other bracket types --- test/test_unit_strings.cpp | 9 ++++++++- units/units.cpp | 29 ++++++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/test/test_unit_strings.cpp b/test/test_unit_strings.cpp index 4d10d0bc..2599c21a 100644 --- a/test/test_unit_strings.cpp +++ b/test/test_unit_strings.cpp @@ -589,7 +589,14 @@ TEST(stringToUnits, decimalPowerExponents) EXPECT_EQ(unit_from_string("m/s^2/Hz^0.5"), precise::special::ASD); const std::vector invalidPowers{ - "m^0.25", "m^(0.25)", "m^1.5", "m^2.25", "m^2.5", "m^-2.5", "m^2e"}; + "m^0.25", + "m^(0.25)", + "m^1.5", + "m^2.25", + "m^2.5", + "m^-2.5", + "m^2e", + "[(0.5)]^34s"}; for (const auto& powerString : invalidPowers) { EXPECT_TRUE(is_error(unit_from_string(powerString))) << powerString; diff --git a/units/units.cpp b/units/units.cpp index dcd972b4..23fd59cf 100644 --- a/units/units.cpp +++ b/units/units.cpp @@ -3959,6 +3959,28 @@ static bool checkExponentOperations(const std::string& unit_string) auto cx = unit_string.find_first_of('^'); while (cx != std::string::npos) { const bool ndigit = isDigitCharacter(unit_string[cx - 1]); + if (unit_string[cx - 1] == ']') { + int index = static_cast(cx) - 2; + if (segmentcheckReverse(unit_string, '[', index)) { + const auto openBracket = static_cast(index + 1); + auto contentStart = openBracket + 1; + auto contentLength = cx - contentStart - 1; + if (contentLength >= 2 && + unit_string[contentStart] == '(' && + unit_string[contentStart + contentLength - 1] == ')') { + ++contentStart; + contentLength -= 2; + } + char* eptr{nullptr}; + const auto content = + unit_string.substr(contentStart, contentLength); + std::strtod(content.c_str(), &eptr); + if (!content.empty() && + eptr == content.c_str() + content.size()) { + return false; + } + } + } ++cx; const bool parenthesized = (unit_string[cx] == '('); if (parenthesized) { @@ -4110,9 +4132,10 @@ static bool checkValidUnitString( break; } } - if (!checkExponentOperations(unit_string)) { - return false; - } + } + + if (!checkExponentOperations(unit_string)) { + return false; } return true; From b7328f00df520c45cd7f35f8b1a636aad23e0893 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Thu, 6 Aug 2026 12:24:38 -0700 Subject: [PATCH 09/35] more updates and resolutions --- units/units.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/units/units.cpp b/units/units.cpp index 23fd59cf..e0c675bc 100644 --- a/units/units.cpp +++ b/units/units.cpp @@ -3965,9 +3965,11 @@ static bool checkExponentOperations(const std::string& unit_string) const auto openBracket = static_cast(index + 1); auto contentStart = openBracket + 1; auto contentLength = cx - contentStart - 1; + bool parenthesizedContent = false; if (contentLength >= 2 && unit_string[contentStart] == '(' && unit_string[contentStart + contentLength - 1] == ')') { + parenthesizedContent = true; ++contentStart; contentLength -= 2; } @@ -3975,7 +3977,7 @@ static bool checkExponentOperations(const std::string& unit_string) const auto content = unit_string.substr(contentStart, contentLength); std::strtod(content.c_str(), &eptr); - if (!content.empty() && + if (parenthesizedContent && !content.empty() && eptr == content.c_str() + content.size()) { return false; } From 582c95cf7c9930e2804dcef64fdfdf9f619b9c59 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 17:57:10 +0000 Subject: [PATCH 10/35] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci From 06b9bd7f85f0b42408a9c0b35e44fa58b03c3528 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 9 Aug 2026 23:28:48 +0000 Subject: [PATCH 11/35] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- units/units.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/units/units.cpp b/units/units.cpp index e0c675bc..35b80fb3 100644 --- a/units/units.cpp +++ b/units/units.cpp @@ -3966,8 +3966,7 @@ static bool checkExponentOperations(const std::string& unit_string) auto contentStart = openBracket + 1; auto contentLength = cx - contentStart - 1; bool parenthesizedContent = false; - if (contentLength >= 2 && - unit_string[contentStart] == '(' && + if (contentLength >= 2 && unit_string[contentStart] == '(' && unit_string[contentStart + contentLength - 1] == ')') { parenthesizedContent = true; ++contentStart; From 487d4697671921e066b9d0a3f125cf180d508fca Mon Sep 17 00:00:00 2001 From: Philip Top Date: Tue, 1 Sep 2026 08:00:45 -0700 Subject: [PATCH 12/35] fix a clang-tidy issue --- units/units.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/units/units.cpp b/units/units.cpp index 35b80fb3..67cb3b37 100644 --- a/units/units.cpp +++ b/units/units.cpp @@ -3962,7 +3962,7 @@ static bool checkExponentOperations(const std::string& unit_string) if (unit_string[cx - 1] == ']') { int index = static_cast(cx) - 2; if (segmentcheckReverse(unit_string, '[', index)) { - const auto openBracket = static_cast(index + 1); + const auto openBracket = static_cast(index) + 1; auto contentStart = openBracket + 1; auto contentLength = cx - contentStart - 1; bool parenthesizedContent = false; @@ -3975,7 +3975,8 @@ static bool checkExponentOperations(const std::string& unit_string) char* eptr{nullptr}; const auto content = unit_string.substr(contentStart, contentLength); - std::strtod(content.c_str(), &eptr); + const auto parsedValue = std::strtod(content.c_str(), &eptr); + (void)parsedValue; if (parenthesizedContent && !content.empty() && eptr == content.c_str() + content.size()) { return false; From 8a246257cf452f058b625ccc46c029f7f8e38355 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Wed, 2 Sep 2026 07:21:32 -0700 Subject: [PATCH 13/35] add more tests --- test/test_unit_strings.cpp | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/test_unit_strings.cpp b/test/test_unit_strings.cpp index 2599c21a..86d22c38 100644 --- a/test/test_unit_strings.cpp +++ b/test/test_unit_strings.cpp @@ -603,6 +603,43 @@ TEST(stringToUnits, decimalPowerExponents) } } +TEST(stringToUnits, bracketParenthesizedExponents) +{ + // The modified code specifically handles the case where: + // - There's a ']' before '^' + // - The content between brackets is parenthesized + // - That content parses as a pure number + // This pattern is invalid and should be rejected + + // Test that these invalid patterns are properly rejected + // (This exercises the strtod-based validation we modified) + EXPECT_TRUE(is_error(unit_from_string("[(0.5)]^34s"))); + EXPECT_TRUE(is_error(unit_from_string("[(2)]^3m"))); + EXPECT_TRUE(is_error(unit_from_string("[(1)]^5kg"))); + EXPECT_TRUE(is_error(unit_from_string("[(2.0)]^2m"))); + EXPECT_TRUE(is_error(unit_from_string("[(2e0)]^2m"))); + EXPECT_TRUE(is_error(unit_from_string("[(2e+1)]^1m"))); + EXPECT_TRUE(is_error(unit_from_string("[(2e-1)]^1m"))); + EXPECT_TRUE(is_error(unit_from_string("[(+2)]^2m"))); + EXPECT_TRUE(is_error(unit_from_string("[(-1)]^2m"))); + EXPECT_TRUE(is_error(unit_from_string("[(-0.5)]^3m"))); + + // These should be valid because the content is not purely numeric + // (the strtod parsing doesn't consume the entire string) + EXPECT_NE(unit_from_string("[(m)]"), precise::error); + EXPECT_NE(unit_from_string("[(m/s)]"), precise::error); + EXPECT_NE(unit_from_string("[(kg*m)]"), precise::error); + + // Partially numeric content - parsing stops before end + EXPECT_NE(unit_from_string("[(2m)]"), precise::error); + EXPECT_NE(unit_from_string("[(1.5kg)]"), precise::error); + EXPECT_NE(unit_from_string("[(0.5x)]"), precise::error); + + // Valid complex bracket notation that was already tested + EXPECT_EQ( + unit_from_string("[m/s2/Hz^(1/2)]"), precise::special::ASD); +} + TEST(stringToUnits, specialUnits) { EXPECT_EQ( From f5fb66c4ebce5e26e665839913a1c129c07fe7b5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 14:22:00 +0000 Subject: [PATCH 14/35] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- test/test_unit_strings.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/test_unit_strings.cpp b/test/test_unit_strings.cpp index 86d22c38..259bf2f8 100644 --- a/test/test_unit_strings.cpp +++ b/test/test_unit_strings.cpp @@ -636,8 +636,7 @@ TEST(stringToUnits, bracketParenthesizedExponents) EXPECT_NE(unit_from_string("[(0.5x)]"), precise::error); // Valid complex bracket notation that was already tested - EXPECT_EQ( - unit_from_string("[m/s2/Hz^(1/2)]"), precise::special::ASD); + EXPECT_EQ(unit_from_string("[m/s2/Hz^(1/2)]"), precise::special::ASD); } TEST(stringToUnits, specialUnits) From 54735fd6cf9a13edc3265af030c6de85339616ad Mon Sep 17 00:00:00 2001 From: Philip Top Date: Wed, 2 Sep 2026 09:07:47 -0700 Subject: [PATCH 15/35] add and fix fuzz test --- test/files/fuzz_issues/rtrip_fail38 | 1 + test/fuzz_issue_tests.cpp | 4 ++-- units/units.cpp | 6 ++++++ 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 test/files/fuzz_issues/rtrip_fail38 diff --git a/test/files/fuzz_issues/rtrip_fail38 b/test/files/fuzz_issues/rtrip_fail38 new file mode 100644 index 00000000..bfeefc0b --- /dev/null +++ b/test/files/fuzz_issues/rtrip_fail38 @@ -0,0 +1 @@ +ZSh^-3999h$/991(•úÿ) \ No newline at end of file diff --git a/test/fuzz_issue_tests.cpp b/test/fuzz_issue_tests.cpp index cfe68363..9b72de0c 100644 --- a/test/fuzz_issue_tests.cpp +++ b/test/fuzz_issue_tests.cpp @@ -255,11 +255,11 @@ TEST_P(rtripProblems, rtripFiles) } } -INSTANTIATE_TEST_SUITE_P(rtripFiles, rtripProblems, ::testing::Range(1, 38)); +INSTANTIATE_TEST_SUITE_P(rtripFiles, rtripProblems, ::testing::Range(1, 39)); TEST(fuzzFailures, rtripSingleProblems) { - auto cdata = loadFailureFile("rtrip_fail", 37); + auto cdata = loadFailureFile("rtrip_fail", 38); auto u1 = unit_from_string(cdata); if (!is_error(u1)) { auto str = to_string(u1); diff --git a/units/units.cpp b/units/units.cpp index 67cb3b37..46d5ea9f 100644 --- a/units/units.cpp +++ b/units/units.cpp @@ -3984,9 +3984,15 @@ static bool checkExponentOperations(const std::string& unit_string) } } ++cx; + if (cx >= unit_string.size()) { + return false; + } const bool parenthesized = (unit_string[cx] == '('); if (parenthesized) { ++cx; + if (cx >= unit_string.size()) { + return false; + } } if (unit_string[cx] == '-' || unit_string[cx] == '+') { ++cx; From e66191b30801741452cc41b89586a46ee07d3310 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Wed, 2 Sep 2026 18:52:49 -0700 Subject: [PATCH 16/35] resolve edge case --- units/units.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/units/units.cpp b/units/units.cpp index 46d5ea9f..cff84c6a 100644 --- a/units/units.cpp +++ b/units/units.cpp @@ -3984,17 +3984,17 @@ static bool checkExponentOperations(const std::string& unit_string) } } ++cx; + // Check bounds before accessing unit_string[cx] if (cx >= unit_string.size()) { return false; } const bool parenthesized = (unit_string[cx] == '('); if (parenthesized) { ++cx; - if (cx >= unit_string.size()) { - return false; - } + // Continue even if at bounds; digit validation will catch it } - if (unit_string[cx] == '-' || unit_string[cx] == '+') { + if (cx < unit_string.size() && + (unit_string[cx] == '-' || unit_string[cx] == '+')) { ++cx; } const auto startDigit = cx; From 75c63ac4e34a1e869dd6ac5ca0f4ef92aa464da2 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Wed, 2 Sep 2026 19:19:46 -0700 Subject: [PATCH 17/35] try alternative fix --- units/units.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/units/units.cpp b/units/units.cpp index cff84c6a..ea7f3959 100644 --- a/units/units.cpp +++ b/units/units.cpp @@ -3958,6 +3958,9 @@ static bool checkExponentOperations(const std::string& unit_string) // check all power operations auto cx = unit_string.find_first_of('^'); while (cx != std::string::npos) { + if (cx == 0) { + return false; + } const bool ndigit = isDigitCharacter(unit_string[cx - 1]); if (unit_string[cx - 1] == ']') { int index = static_cast(cx) - 2; @@ -3984,17 +3987,17 @@ static bool checkExponentOperations(const std::string& unit_string) } } ++cx; - // Check bounds before accessing unit_string[cx] if (cx >= unit_string.size()) { return false; } const bool parenthesized = (unit_string[cx] == '('); if (parenthesized) { ++cx; - // Continue even if at bounds; digit validation will catch it + if (cx >= unit_string.size()) { + return false; + } } - if (cx < unit_string.size() && - (unit_string[cx] == '-' || unit_string[cx] == '+')) { + if (unit_string[cx] == '-' || unit_string[cx] == '+') { ++cx; } const auto startDigit = cx; From 8f774a101eaec261f296a5e0d054800861f316b1 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Thu, 3 Sep 2026 05:24:53 -0700 Subject: [PATCH 18/35] add some test diagnostics --- test/fuzz_issue_tests.cpp | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/test/fuzz_issue_tests.cpp b/test/fuzz_issue_tests.cpp index 9b72de0c..2d9ce880 100644 --- a/test/fuzz_issue_tests.cpp +++ b/test/fuzz_issue_tests.cpp @@ -10,6 +10,7 @@ SPDX-License-Identifier: BSD-3-Clause #include #include +#include #include #include #include @@ -259,12 +260,44 @@ INSTANTIATE_TEST_SUITE_P(rtripFiles, rtripProblems, ::testing::Range(1, 39)); TEST(fuzzFailures, rtripSingleProblems) { - auto cdata = loadFailureFile("rtrip_fail", 38); + // This is rtrip_fail37; Google Test displays it as parameter 38 because + // the parametrized range starts at 1 and the displayed index is zero-based. + auto cdata = loadFailureFile("rtrip_fail", 37); auto u1 = unit_from_string(cdata); - if (!is_error(u1)) { + std::cerr << "rtrip input (hex): "; + for (const auto byte : cdata) { + std::cerr << std::hex << std::setw(2) << std::setfill('0') + << static_cast(static_cast(byte)); + } + std::cerr << std::dec << "\n"; + std::cerr << "u1 error: " << std::boolalpha << is_error(u1) + << ", multiplier: " << u1.multiplier() << "\n"; + if (is_error(u1)) { + std::cerr << "u1 was rejected; no round-trip values available\n"; + } else { auto str = to_string(u1); - std::cout << str << '\n'; auto u2 = unit_from_string(str); + auto preciseRoot1 = root(u1, 2); + auto preciseRoot2 = root(u2, 2); + auto root1 = root(unit_cast(u1), 2); + auto root2 = root(unit_cast(u2), 2); + std::cerr << "serialized: [" << str << "]\n"; + std::cerr << "u2 error: " << is_error(u2) + << ", multiplier: " << u2.multiplier() << "\n"; + std::cerr << "precise root1 error: " << is_error(preciseRoot1) + << ", multiplier: " << preciseRoot1.multiplier() + << ", e flag: " << preciseRoot1.base_units().has_e_flag() + << "\n"; + std::cerr << "precise root2 error: " << is_error(preciseRoot2) + << ", multiplier: " << preciseRoot2.multiplier() + << ", e flag: " << preciseRoot2.base_units().has_e_flag() + << "\n"; + std::cerr << "root1 error: " << is_error(root1) + << ", multiplier: " << root1.multiplier() + << ", e flag: " << root1.base_units().has_e_flag() << "\n"; + std::cerr << "root2 error: " << is_error(root2) + << ", multiplier: " << root2.multiplier() + << ", e flag: " << root2.base_units().has_e_flag() << "\n"; EXPECT_FALSE(is_error(u2)); if (u2 == u1) { EXPECT_EQ(u2, u1); From 8b445eb209dd0f58316acc513c3567852534a81a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 13:11:59 +0000 Subject: [PATCH 19/35] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- test/fuzz_issue_tests.cpp | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/test/fuzz_issue_tests.cpp b/test/fuzz_issue_tests.cpp index 2d9ce880..612b3800 100644 --- a/test/fuzz_issue_tests.cpp +++ b/test/fuzz_issue_tests.cpp @@ -266,8 +266,9 @@ TEST(fuzzFailures, rtripSingleProblems) auto u1 = unit_from_string(cdata); std::cerr << "rtrip input (hex): "; for (const auto byte : cdata) { - std::cerr << std::hex << std::setw(2) << std::setfill('0') - << static_cast(static_cast(byte)); + std::cerr + << std::hex << std::setw(2) << std::setfill('0') + << static_cast(static_cast(byte)); } std::cerr << std::dec << "\n"; std::cerr << "u1 error: " << std::boolalpha << is_error(u1) @@ -285,19 +286,19 @@ TEST(fuzzFailures, rtripSingleProblems) std::cerr << "u2 error: " << is_error(u2) << ", multiplier: " << u2.multiplier() << "\n"; std::cerr << "precise root1 error: " << is_error(preciseRoot1) - << ", multiplier: " << preciseRoot1.multiplier() - << ", e flag: " << preciseRoot1.base_units().has_e_flag() - << "\n"; + << ", multiplier: " << preciseRoot1.multiplier() + << ", e flag: " << preciseRoot1.base_units().has_e_flag() + << "\n"; std::cerr << "precise root2 error: " << is_error(preciseRoot2) - << ", multiplier: " << preciseRoot2.multiplier() - << ", e flag: " << preciseRoot2.base_units().has_e_flag() - << "\n"; + << ", multiplier: " << preciseRoot2.multiplier() + << ", e flag: " << preciseRoot2.base_units().has_e_flag() + << "\n"; std::cerr << "root1 error: " << is_error(root1) - << ", multiplier: " << root1.multiplier() - << ", e flag: " << root1.base_units().has_e_flag() << "\n"; + << ", multiplier: " << root1.multiplier() + << ", e flag: " << root1.base_units().has_e_flag() << "\n"; std::cerr << "root2 error: " << is_error(root2) - << ", multiplier: " << root2.multiplier() - << ", e flag: " << root2.base_units().has_e_flag() << "\n"; + << ", multiplier: " << root2.multiplier() + << ", e flag: " << root2.base_units().has_e_flag() << "\n"; EXPECT_FALSE(is_error(u2)); if (u2 == u1) { EXPECT_EQ(u2, u1); From 135e0d1586d00e6c8d8ecfad7e6de48bb35d1ab7 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Thu, 3 Sep 2026 06:34:48 -0700 Subject: [PATCH 20/35] try new test --- test/fuzz_issue_tests.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/fuzz_issue_tests.cpp b/test/fuzz_issue_tests.cpp index 2d9ce880..4a492af5 100644 --- a/test/fuzz_issue_tests.cpp +++ b/test/fuzz_issue_tests.cpp @@ -260,9 +260,7 @@ INSTANTIATE_TEST_SUITE_P(rtripFiles, rtripProblems, ::testing::Range(1, 39)); TEST(fuzzFailures, rtripSingleProblems) { - // This is rtrip_fail37; Google Test displays it as parameter 38 because - // the parametrized range starts at 1 and the displayed index is zero-based. - auto cdata = loadFailureFile("rtrip_fail", 37); + const std::string cdata("\x00\xBDTh+TTh\x00", 9); auto u1 = unit_from_string(cdata); std::cerr << "rtrip input (hex): "; for (const auto byte : cdata) { From 30d6f30df3aeb728bd38b3ff753467fdb2a30c88 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Thu, 3 Sep 2026 07:27:00 -0700 Subject: [PATCH 21/35] match the single failure and the test --- test/fuzz_issue_tests.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/fuzz_issue_tests.cpp b/test/fuzz_issue_tests.cpp index 7d685e10..bde066be 100644 --- a/test/fuzz_issue_tests.cpp +++ b/test/fuzz_issue_tests.cpp @@ -260,7 +260,8 @@ INSTANTIATE_TEST_SUITE_P(rtripFiles, rtripProblems, ::testing::Range(1, 39)); TEST(fuzzFailures, rtripSingleProblems) { - const std::string cdata("\x00\xBDTh+TTh\x00", 9); + auto cdata = loadFailureFile("rtrip_fail", 38); + ASSERT_FALSE(cdata.empty()); auto u1 = unit_from_string(cdata); std::cerr << "rtrip input (hex): "; for (const auto byte : cdata) { @@ -269,6 +270,7 @@ TEST(fuzzFailures, rtripSingleProblems) << static_cast(static_cast(byte)); } std::cerr << std::dec << "\n"; + std::cerr << "input length: " << cdata.size() << "\n"; std::cerr << "u1 error: " << std::boolalpha << is_error(u1) << ", multiplier: " << u1.multiplier() << "\n"; if (is_error(u1)) { @@ -303,14 +305,17 @@ TEST(fuzzFailures, rtripSingleProblems) EXPECT_EQ(unit_cast(u2), unit_cast(u1)); EXPECT_FALSE(units::unit_cast(u2) != units::unit_cast(u1)); } else if (!is_error(root(u2, 2))) { + std::cerr << "selected branch: square root\n"; EXPECT_EQ(root(unit_cast(u2), 2), root(unit_cast(u1), 2)); EXPECT_FALSE( root(units::unit_cast(u2), 2) != root(units::unit_cast(u1), 2)); } else if (!is_error(root(u2, 3))) { + std::cerr << "selected branch: cube root\n"; EXPECT_EQ(root(unit_cast(u2), 3), root(unit_cast(u1), 3)); EXPECT_FALSE( root(units::unit_cast(u2), 3) != root(units::unit_cast(u1), 3)); } else { + std::cerr << "selected branch: direct unit comparison\n"; auto uc1 = unit_cast(u1); auto uc2 = unit_cast(u2); EXPECT_EQ(uc2, uc1); From 3aaefd2d82f3210dbfb3ecda2cc802129176dae6 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Thu, 3 Sep 2026 19:47:25 -0700 Subject: [PATCH 22/35] more diagnostics --- test/fuzz_issue_tests.cpp | 44 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/test/fuzz_issue_tests.cpp b/test/fuzz_issue_tests.cpp index bde066be..265fdb03 100644 --- a/test/fuzz_issue_tests.cpp +++ b/test/fuzz_issue_tests.cpp @@ -233,9 +233,36 @@ TEST_P(rtripProblems, rtripFiles) { auto cdata = loadFailureFile("rtrip_fail", GetParam()); auto u1 = unit_from_string(cdata); + if (GetParam() == 38) { + std::cerr << "rtrip parametrized input length: " << cdata.size() + << "\n"; + std::cerr << "rtrip parametrized input (hex): "; + for (const auto byte : cdata) { + std::cerr << std::hex << std::setw(2) << std::setfill('0') + << static_cast( + static_cast(byte)); + } + std::cerr << std::dec << "\n"; + std::cerr << "rtrip parametrized u1 error: " << std::boolalpha + << is_error(u1) << ", multiplier: " << u1.multiplier() + << "\n"; + } if (!is_error(u1)) { auto str = to_string(u1); auto u2 = unit_from_string(str); + if (GetParam() == 38) { + std::cerr << "rtrip parametrized serialized: [" << str << "]\n"; + std::cerr << "rtrip parametrized u2 error: " << is_error(u2) + << ", multiplier: " << u2.multiplier() << "\n"; + std::cerr << "rtrip parametrized root1 error: " + << is_error(root(u1, 2)) + << ", e flag: " + << root(u1, 2).base_units().has_e_flag() << "\n"; + std::cerr << "rtrip parametrized root2 error: " + << is_error(root(u2, 2)) + << ", e flag: " + << root(u2, 2).base_units().has_e_flag() << "\n"; + } EXPECT_FALSE(is_error(u2)); if (u2 == u1) { EXPECT_EQ(u2, u1); @@ -282,9 +309,26 @@ TEST(fuzzFailures, rtripSingleProblems) auto preciseRoot2 = root(u2, 2); auto root1 = root(unit_cast(u1), 2); auto root2 = root(unit_cast(u2), 2); + const auto printUnitData = [](const char* name, const precise_unit& unit) { + const auto base = unit.base_units(); + std::cerr << name << " base: m=" << base.meter() + << ", kg=" << base.kg() << ", s=" << base.second() + << ", A=" << base.ampere() << ", K=" << base.kelvin() + << ", mol=" << base.mole() << ", cd=" << base.candela() + << ", rad=" << base.radian() + << ", currency=" << base.currency() + << ", count=" << base.count() + << ", i=" << base.has_i_flag() + << ", e=" << base.has_e_flag() + << ", equation=" << base.is_equation() << "\n"; + }; std::cerr << "serialized: [" << str << "]\n"; std::cerr << "u2 error: " << is_error(u2) << ", multiplier: " << u2.multiplier() << "\n"; + printUnitData("u1", u1); + printUnitData("u2", u2); + printUnitData("precise root1", preciseRoot1); + printUnitData("precise root2", preciseRoot2); std::cerr << "precise root1 error: " << is_error(preciseRoot1) << ", multiplier: " << preciseRoot1.multiplier() << ", e flag: " << preciseRoot1.base_units().has_e_flag() From 993203a1012a8d903e40a7e0cf0aa6ce408245ff Mon Sep 17 00:00:00 2001 From: Philip Top Date: Fri, 4 Sep 2026 05:35:19 -0700 Subject: [PATCH 23/35] more test updates --- test/fuzz_issue_tests.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/test/fuzz_issue_tests.cpp b/test/fuzz_issue_tests.cpp index 265fdb03..037d9e93 100644 --- a/test/fuzz_issue_tests.cpp +++ b/test/fuzz_issue_tests.cpp @@ -253,7 +253,14 @@ TEST_P(rtripProblems, rtripFiles) if (GetParam() == 38) { std::cerr << "rtrip parametrized serialized: [" << str << "]\n"; std::cerr << "rtrip parametrized u2 error: " << is_error(u2) - << ", multiplier: " << u2.multiplier() << "\n"; + << ", multiplier: " << u2.multiplier() + << ", e flag: " << u2.base_units().has_e_flag() + << ", equation: " << u2.base_units().is_equation() + << "\n"; + std::cerr << "rtrip parametrized u1 flags: e=" + << u1.base_units().has_e_flag() + << ", equation=" << u1.base_units().is_equation() + << "\n"; std::cerr << "rtrip parametrized root1 error: " << is_error(root(u1, 2)) << ", e flag: " @@ -324,7 +331,11 @@ TEST(fuzzFailures, rtripSingleProblems) }; std::cerr << "serialized: [" << str << "]\n"; std::cerr << "u2 error: " << is_error(u2) - << ", multiplier: " << u2.multiplier() << "\n"; + << ", multiplier: " << u2.multiplier() + << ", e flag: " << u2.base_units().has_e_flag() + << ", equation: " << u2.base_units().is_equation() << "\n"; + std::cerr << "u1 flags: e=" << u1.base_units().has_e_flag() + << ", equation=" << u1.base_units().is_equation() << "\n"; printUnitData("u1", u1); printUnitData("u2", u2); printUnitData("precise root1", preciseRoot1); From fcd8deeb975556d41b947c7d73aab34827c90537 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 12:36:13 +0000 Subject: [PATCH 24/35] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- test/fuzz_issue_tests.cpp | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/test/fuzz_issue_tests.cpp b/test/fuzz_issue_tests.cpp index 037d9e93..51dcf4b4 100644 --- a/test/fuzz_issue_tests.cpp +++ b/test/fuzz_issue_tests.cpp @@ -238,9 +238,9 @@ TEST_P(rtripProblems, rtripFiles) << "\n"; std::cerr << "rtrip parametrized input (hex): "; for (const auto byte : cdata) { - std::cerr << std::hex << std::setw(2) << std::setfill('0') - << static_cast( - static_cast(byte)); + std::cerr + << std::hex << std::setw(2) << std::setfill('0') + << static_cast(static_cast(byte)); } std::cerr << std::dec << "\n"; std::cerr << "rtrip parametrized u1 error: " << std::boolalpha @@ -253,22 +253,21 @@ TEST_P(rtripProblems, rtripFiles) if (GetParam() == 38) { std::cerr << "rtrip parametrized serialized: [" << str << "]\n"; std::cerr << "rtrip parametrized u2 error: " << is_error(u2) - << ", multiplier: " << u2.multiplier() - << ", e flag: " << u2.base_units().has_e_flag() - << ", equation: " << u2.base_units().is_equation() - << "\n"; - std::cerr << "rtrip parametrized u1 flags: e=" - << u1.base_units().has_e_flag() - << ", equation=" << u1.base_units().is_equation() - << "\n"; + << ", multiplier: " << u2.multiplier() + << ", e flag: " << u2.base_units().has_e_flag() + << ", equation: " << u2.base_units().is_equation() + << "\n"; + std::cerr << "rtrip parametrized u1 flags: e=" + << u1.base_units().has_e_flag() + << ", equation=" << u1.base_units().is_equation() << "\n"; std::cerr << "rtrip parametrized root1 error: " << is_error(root(u1, 2)) - << ", e flag: " - << root(u1, 2).base_units().has_e_flag() << "\n"; + << ", e flag: " << root(u1, 2).base_units().has_e_flag() + << "\n"; std::cerr << "rtrip parametrized root2 error: " << is_error(root(u2, 2)) - << ", e flag: " - << root(u2, 2).base_units().has_e_flag() << "\n"; + << ", e flag: " << root(u2, 2).base_units().has_e_flag() + << "\n"; } EXPECT_FALSE(is_error(u2)); if (u2 == u1) { @@ -316,7 +315,8 @@ TEST(fuzzFailures, rtripSingleProblems) auto preciseRoot2 = root(u2, 2); auto root1 = root(unit_cast(u1), 2); auto root2 = root(unit_cast(u2), 2); - const auto printUnitData = [](const char* name, const precise_unit& unit) { + const auto printUnitData = [](const char* name, + const precise_unit& unit) { const auto base = unit.base_units(); std::cerr << name << " base: m=" << base.meter() << ", kg=" << base.kg() << ", s=" << base.second() From aa7bc621ea267f82571ce693e4aa1e1dd3878cb2 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Fri, 4 Sep 2026 06:00:59 -0700 Subject: [PATCH 25/35] update the test cases to try to narrow down the issue --- test/fuzz_issue_tests.cpp | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/test/fuzz_issue_tests.cpp b/test/fuzz_issue_tests.cpp index 037d9e93..34dac401 100644 --- a/test/fuzz_issue_tests.cpp +++ b/test/fuzz_issue_tests.cpp @@ -336,6 +336,41 @@ TEST(fuzzFailures, rtripSingleProblems) << ", equation: " << u2.base_units().is_equation() << "\n"; std::cerr << "u1 flags: e=" << u1.base_units().has_e_flag() << ", equation=" << u1.base_units().is_equation() << "\n"; + const auto equationUnit = unit_from_string("EQXUN[8]"); + const auto zeroEquationUnit = unit_from_string("0*EQXUN[8]"); + std::cerr << "EQXUN[8]: error=" << is_error(equationUnit) + << ", equation=" << equationUnit.base_units().is_equation() + << ", e=" << equationUnit.base_units().has_e_flag() << "\n"; + std::cerr << "0*EQXUN[8]: error=" << is_error(zeroEquationUnit) + << ", equation=" + << zeroEquationUnit.base_units().is_equation() + << ", e=" << zeroEquationUnit.base_units().has_e_flag() + << "\n"; + const auto equationPos = str.rfind("EQXUN[8]"); + if (equationPos != std::string::npos) { + const auto serializedTail = str.substr(equationPos); + const auto tailUnit = unit_from_string(serializedTail); + std::cerr << "serialized tail: [" << serializedTail + << "], error=" << is_error(tailUnit) + << ", equation=" << tailUnit.base_units().is_equation() + << ", e=" << tailUnit.base_units().has_e_flag() << "\n"; + } + if (str.size() > 2 && str[0] == '0' && str[1] == '*') { + const auto zeroPrefixTail = str.substr(2); + const auto zeroPrefixTailUnit = unit_from_string(zeroPrefixTail); + const auto zeroPrefixUnit = unit_from_string("0*" + zeroPrefixTail); + std::cerr << "zero-prefix tail: [" << zeroPrefixTail + << "], error=" << is_error(zeroPrefixTailUnit) + << ", equation=" + << zeroPrefixTailUnit.base_units().is_equation() + << ", e=" << zeroPrefixTailUnit.base_units().has_e_flag() + << "\n"; + std::cerr << "zero-prefix parse: error=" << is_error(zeroPrefixUnit) + << ", equation=" + << zeroPrefixUnit.base_units().is_equation() + << ", e=" << zeroPrefixUnit.base_units().has_e_flag() + << "\n"; + } printUnitData("u1", u1); printUnitData("u2", u2); printUnitData("precise root1", preciseRoot1); From 49058058500e11e24ffd80770ba0a57c1b876e9c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 13:01:56 +0000 Subject: [PATCH 26/35] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- test/fuzz_issue_tests.cpp | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/test/fuzz_issue_tests.cpp b/test/fuzz_issue_tests.cpp index d473faf8..3689ed78 100644 --- a/test/fuzz_issue_tests.cpp +++ b/test/fuzz_issue_tests.cpp @@ -341,11 +341,10 @@ TEST(fuzzFailures, rtripSingleProblems) std::cerr << "EQXUN[8]: error=" << is_error(equationUnit) << ", equation=" << equationUnit.base_units().is_equation() << ", e=" << equationUnit.base_units().has_e_flag() << "\n"; - std::cerr << "0*EQXUN[8]: error=" << is_error(zeroEquationUnit) - << ", equation=" - << zeroEquationUnit.base_units().is_equation() - << ", e=" << zeroEquationUnit.base_units().has_e_flag() - << "\n"; + std::cerr + << "0*EQXUN[8]: error=" << is_error(zeroEquationUnit) + << ", equation=" << zeroEquationUnit.base_units().is_equation() + << ", e=" << zeroEquationUnit.base_units().has_e_flag() << "\n"; const auto equationPos = str.rfind("EQXUN[8]"); if (equationPos != std::string::npos) { const auto serializedTail = str.substr(equationPos); @@ -365,11 +364,10 @@ TEST(fuzzFailures, rtripSingleProblems) << zeroPrefixTailUnit.base_units().is_equation() << ", e=" << zeroPrefixTailUnit.base_units().has_e_flag() << "\n"; - std::cerr << "zero-prefix parse: error=" << is_error(zeroPrefixUnit) - << ", equation=" - << zeroPrefixUnit.base_units().is_equation() - << ", e=" << zeroPrefixUnit.base_units().has_e_flag() - << "\n"; + std::cerr + << "zero-prefix parse: error=" << is_error(zeroPrefixUnit) + << ", equation=" << zeroPrefixUnit.base_units().is_equation() + << ", e=" << zeroPrefixUnit.base_units().has_e_flag() << "\n"; } printUnitData("u1", u1); printUnitData("u2", u2); From 777480a2e1f465fc2f0e1e1895adf6b2d4f881de Mon Sep 17 00:00:00 2001 From: Philip Top Date: Fri, 4 Sep 2026 06:21:23 -0700 Subject: [PATCH 27/35] more tweaks --- units/commodities.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/units/commodities.cpp b/units/commodities.cpp index c9701146..4c408b0d 100644 --- a/units/commodities.cpp +++ b/units/commodities.cpp @@ -243,7 +243,13 @@ static void removeEscapeSequences(std::string& str) uint32_t getCommodity(std::string comm) { removeEscapeSequences(comm); - std::transform(comm.begin(), comm.end(), comm.begin(), ::tolower); + std::transform( + comm.begin(), + comm.end(), + comm.begin(), + [](unsigned char character) { + return static_cast(std::tolower(character)); + }); if (allowCustomCommodities.load(std::memory_order_acquire)) { if (!customCommodityCodes.empty()) { auto fnd2 = customCommodityCodes.find(comm); From 01fd0f2a8320347e99fc131712aee7c9a366f053 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 13:22:25 +0000 Subject: [PATCH 28/35] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- units/commodities.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/units/commodities.cpp b/units/commodities.cpp index 4c408b0d..2b1fabf6 100644 --- a/units/commodities.cpp +++ b/units/commodities.cpp @@ -244,10 +244,7 @@ uint32_t getCommodity(std::string comm) { removeEscapeSequences(comm); std::transform( - comm.begin(), - comm.end(), - comm.begin(), - [](unsigned char character) { + comm.begin(), comm.end(), comm.begin(), [](unsigned char character) { return static_cast(std::tolower(character)); }); if (allowCustomCommodities.load(std::memory_order_acquire)) { From a247975716ea845e17f5fa2ac3797d4a9e98f01c Mon Sep 17 00:00:00 2001 From: Philip Top Date: Fri, 4 Sep 2026 07:34:12 -0700 Subject: [PATCH 29/35] specify hashing types in commodity conversions --- units/commodities.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/units/commodities.cpp b/units/commodities.cpp index 2b1fabf6..b1bcdea1 100644 --- a/units/commodities.cpp +++ b/units/commodities.cpp @@ -199,7 +199,8 @@ static uint32_t stringHash(const std::string& str) std::uint32_t hash{hashcodes::firstH}; for (auto c : str) { hash = (hash * hashcodes::Ac) ^ - (static_cast(c) * hashcodes::Bc); + (static_cast(static_cast(c)) * + hashcodes::Bc); } return hash; // or return h % C; } From 6e43e7da1255b4b2e8b8be11aff40f89250a676a Mon Sep 17 00:00:00 2001 From: Philip Top Date: Fri, 4 Sep 2026 07:45:19 -0700 Subject: [PATCH 30/35] more character conversions --- units/commodities.cpp | 8 ++++++- units/units.cpp | 53 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 50 insertions(+), 11 deletions(-) diff --git a/units/commodities.cpp b/units/commodities.cpp index b1bcdea1..b491c86d 100644 --- a/units/commodities.cpp +++ b/units/commodities.cpp @@ -324,7 +324,13 @@ std::string getCommodityName(std::uint32_t commodity) void addCustomCommodity(std::string comm, std::uint32_t code) { if (allowCustomCommodities.load()) { - std::transform(comm.begin(), comm.end(), comm.begin(), ::tolower); + std::transform( + comm.begin(), + comm.end(), + comm.begin(), + [](unsigned char character) { + return static_cast(std::tolower(character)); + }); customCommodityNames.emplace(code, comm); customCommodityCodes.emplace(comm, code); } diff --git a/units/units.cpp b/units/units.cpp index ea7f3959..66408bd2 100644 --- a/units/units.cpp +++ b/units/units.cpp @@ -2251,7 +2251,12 @@ static double readNumericalWords(const std::string& ustring, size_t& index) std::string lcstring{ustring}; // make the string lower case for consistency std::transform( - lcstring.begin(), lcstring.end(), lcstring.begin(), ::tolower); + lcstring.begin(), + lcstring.end(), + lcstring.begin(), + [](unsigned char character) { + return static_cast(std::tolower(character)); + }); for (const auto& wp : groupNumericalWords) { auto loc = lcstring.find(std::get<0>(wp)); if (loc != std::string::npos) { @@ -2850,10 +2855,13 @@ static precise_unit continue; } } - if (unit[1] > 0 && (std::isupper(unit[1]) != 0) && - (std::toupper(unit[0]) == irep.first[0]) && + if (static_cast(unit[1]) > 0 && + (std::isupper(static_cast(unit[1])) != 0) && + (std::toupper(static_cast(unit[0])) == + irep.first[0]) && (unit[1] == irep.first[1])) { - unit[0] = std::toupper(unit[0]); + unit[0] = static_cast( + std::toupper(static_cast(unit[0]))); } } auto fnd = unit.find(irep.first); @@ -3923,7 +3931,12 @@ static void ciConversion(std::string& unit_string) }; // transform to upper case so we have a common starting point std::transform( - unit_string.begin(), unit_string.end(), unit_string.begin(), ::toupper); + unit_string.begin(), + unit_string.end(), + unit_string.begin(), + [](unsigned char character) { + return static_cast(std::toupper(character)); + }); auto fnd = ciConversions.find(unit_string); if (fnd != ciConversions.end()) { unit_string = fnd->second; @@ -4855,7 +4868,9 @@ static bool cleanUnitString(std::string& unit_string, std::uint64_t match_flags) unit_string.begin() + bloc + 1, unit_string.begin() + ind - 1, unit_string.begin() + bloc + 1, - ::tolower); + [](unsigned char character) { + return static_cast(std::tolower(character)); + }); bloc = unit_string.find_first_of('{', ind); } else { bloc = std::string::npos; @@ -5058,7 +5073,9 @@ static precise_unit checkNamedUnitCode( codeString.begin(), codeString.end(), codeString.begin(), - ::toupper); + [](unsigned char character) { + return static_cast(std::toupper(character)); + }); } if (codeString.compare(0, 4, "X12:") == 0) { return x12_unit(codeString.substr(4)); @@ -5255,7 +5272,13 @@ static precise_unit checkForCustomUnit(const std::string& unit_string) return {1.0, precise::generate_custom_count_unit(0), hcode}; } - std::transform(csub.begin(), csub.end(), csub.begin(), ::tolower); + std::transform( + csub.begin(), + csub.end(), + csub.begin(), + [](unsigned char character) { + return static_cast(std::tolower(character)); + }); auto custcode = std::hash{}(csub); return precise::generate_custom_unit(custcode & 0x3FU); } @@ -6064,7 +6087,12 @@ static precise_unit unit_from_string_internal( { ustring = unit_string; std::transform( - ustring.begin(), ustring.end(), ustring.begin(), ::tolower); + ustring.begin(), + ustring.end(), + ustring.begin(), + [](unsigned char character) { + return static_cast(std::tolower(character)); + }); if (ustring != unit_string) { retunit = unit_quick_match(ustring, match_flags); if (!is_error(retunit)) { @@ -6297,7 +6325,12 @@ precise_unit default_unit(std::string unit_type) } } std::transform( - unit_type.begin(), unit_type.end(), unit_type.begin(), ::tolower); + unit_type.begin(), + unit_type.end(), + unit_type.begin(), + [](unsigned char character) { + return static_cast(std::tolower(character)); + }); unit_type.erase( std::remove(unit_type.begin(), unit_type.end(), ' '), unit_type.end()); auto fnd = measurement_types.find(unit_type); From 253f5370a966fe48fb5e18f0e70121e7cea8704e Mon Sep 17 00:00:00 2001 From: Philip Top Date: Fri, 4 Sep 2026 09:11:43 -0700 Subject: [PATCH 31/35] try commodities tweak --- units/commodities.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/units/commodities.cpp b/units/commodities.cpp index b491c86d..d79dc16b 100644 --- a/units/commodities.cpp +++ b/units/commodities.cpp @@ -296,7 +296,18 @@ std::string getCommodityName(std::uint32_t commodity) if (!customCommodityNames.empty()) { auto fnd2 = customCommodityNames.find(commodity); if (fnd2 != customCommodityNames.end()) { - return fnd2->second; + const auto& name = fnd2->second; + const auto printable = std::all_of( + name.begin(), + name.end(), + [](unsigned char character) { + return character >= 0x20U && character <= 0x7EU; + }); + if (printable) { + return name; + } + return std::string("CXCOMM[") + std::to_string(commodity) + + "]"; } } } From fa54eb650c5cbf0b83a15589965a6e15d222ead6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:12:12 +0000 Subject: [PATCH 32/35] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- units/commodities.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/units/commodities.cpp b/units/commodities.cpp index d79dc16b..0ebd9dd8 100644 --- a/units/commodities.cpp +++ b/units/commodities.cpp @@ -298,16 +298,13 @@ std::string getCommodityName(std::uint32_t commodity) if (fnd2 != customCommodityNames.end()) { const auto& name = fnd2->second; const auto printable = std::all_of( - name.begin(), - name.end(), - [](unsigned char character) { + name.begin(), name.end(), [](unsigned char character) { return character >= 0x20U && character <= 0x7EU; }); if (printable) { return name; } - return std::string("CXCOMM[") + std::to_string(commodity) + - "]"; + return std::string("CXCOMM[") + std::to_string(commodity) + "]"; } } } From f02f3acaa195feb2909a87dcfd674df843fcb2d2 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Fri, 4 Sep 2026 10:57:15 -0700 Subject: [PATCH 33/35] more attempted fixes --- test/test_unit_strings.cpp | 27 +++++++++++++++++++++++++++ units/units.cpp | 36 ++++++++++++++++++++++++++++++------ 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/test/test_unit_strings.cpp b/test/test_unit_strings.cpp index 259bf2f8..d0ca7f65 100644 --- a/test/test_unit_strings.cpp +++ b/test/test_unit_strings.cpp @@ -2035,6 +2035,33 @@ TEST(stringCleanup, withCommodities) res = detail::testing::testCleanUpString("1/m^2", ~commodities::aluminum); EXPECT_EQ(res, "1/m{aluminum}^2"); + + res = detail::testing::testCleanUpString( + "$/item^(32)*EQXUN[8]", ~commodities::aluminum); + EXPECT_EQ(res, "$/item{aluminum}^(32)*EQXUN[8]"); +} + +TEST(stringToUnits, trailingCommodityAfterPower) +{ + if (sizeof(UNITS_BASE_TYPE) != 8) { + return; + } + + const auto canonical = + unit_from_string("0*$/item{aluminum}^(32)*EQXUN[8]"); + const auto legacy = + unit_from_string("0*$/item^(32){aluminum}*EQXUN[8]"); + ASSERT_FALSE(is_error(canonical)); + ASSERT_FALSE(is_error(legacy)); + EXPECT_EQ(legacy, canonical); + + const auto serialized = to_string(canonical); + EXPECT_EQ(serialized, "0*$/item{aluminum}^(32)*EQXUN[8]"); + const auto reparsed = unit_from_string(serialized); + ASSERT_FALSE(is_error(reparsed)); + EXPECT_EQ(reparsed, canonical); + + EXPECT_TRUE(is_error(unit_from_string("0*item^(not_an_exponent)"))); } TEST(stringGeneration, addPowerString) diff --git a/units/units.cpp b/units/units.cpp index 66408bd2..04b9abf4 100644 --- a/units/units.cpp +++ b/units/units.cpp @@ -1159,7 +1159,14 @@ static std::string propUnitString.push_back('/'); propUnitString.append(cString); } else { - auto locp = propUnitString.find_last_of("^*"); + // Stop at the next multiplication separator. In particular, + // an equation-unit suffix ("*EQXUN[...]") must not cause the + // commodity to be appended after the powered denominator. + auto end = propUnitString.find('*', loc + 1); + if (end != std::string::npos && end > 0) { + --end; + } + auto locp = propUnitString.find_last_of("^*", end); if (locp == std::string::npos || locp < loc) { propUnitString.append(cString); } else { @@ -5727,6 +5734,13 @@ static precise_unit unit_from_string_internal( if (is_valid(retunit)) { return front_unit * retunit; } + // A failed expression with unit operators is not an + // unbraced commodity. In particular, do not discard its + // dimensions when a zero multiplier precedes it. + if (unit_string.substr(index).find_first_of("*/^{}") != + std::string::npos) { + return precise::invalid; + } auto commodity = getCommodity(unit_string.substr(index)); front_unit.commodity(commodity); return front_unit; @@ -5791,6 +5805,15 @@ static precise_unit unit_from_string_internal( const bool containsPer = (findWordOperatorSep(unit_string, "per") != std::string::npos); + // A commodity following a power is accepted for backwards compatibility + // with strings emitted before commodities were placed before exponents. + // Handle it before parsing '^', otherwise the commodity text is mistaken + // for part of the exponent. + if ((match_flags & no_commodities) == 0 && unit_string.back() == '}' && + unit_string.find('{') != std::string::npos) { + return commoditizedUnit(unit_string, match_flags); + } + sep = findOperatorSep(unit_string, "^"); if (sep != std::string::npos) { auto pchar = sep - 1; @@ -5832,10 +5855,6 @@ static precise_unit unit_from_string_internal( return retunit; } } - if ((match_flags & no_commodities) == 0 && unit_string.back() == '}' && - unit_string.find('{') != std::string::npos) { - return commoditizedUnit(unit_string, match_flags); - } retunit = checkSIprefix(unit_string, match_flags); if (is_valid(retunit)) { return retunit; @@ -5901,7 +5920,12 @@ static precise_unit unit_from_string_internal( return retunit; } std::transform( - ustring.begin(), ustring.end(), ustring.begin(), ::toupper); + ustring.begin(), + ustring.end(), + ustring.begin(), + [](unsigned char character) { + return static_cast(std::toupper(character)); + }); retunit = get_unit(ustring, match_flags); if (is_valid(retunit)) { From b0757cff8e646f36342157f02bff148a5394163a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:58:50 +0000 Subject: [PATCH 34/35] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- test/test_unit_strings.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/test_unit_strings.cpp b/test/test_unit_strings.cpp index d0ca7f65..27751c47 100644 --- a/test/test_unit_strings.cpp +++ b/test/test_unit_strings.cpp @@ -2047,10 +2047,8 @@ TEST(stringToUnits, trailingCommodityAfterPower) return; } - const auto canonical = - unit_from_string("0*$/item{aluminum}^(32)*EQXUN[8]"); - const auto legacy = - unit_from_string("0*$/item^(32){aluminum}*EQXUN[8]"); + const auto canonical = unit_from_string("0*$/item{aluminum}^(32)*EQXUN[8]"); + const auto legacy = unit_from_string("0*$/item^(32){aluminum}*EQXUN[8]"); ASSERT_FALSE(is_error(canonical)); ASSERT_FALSE(is_error(legacy)); EXPECT_EQ(legacy, canonical); From cc26b5f758e9474cafcc04b332a46a1aab06a2cf Mon Sep 17 00:00:00 2001 From: Philip Top Date: Sat, 5 Sep 2026 07:28:10 -0700 Subject: [PATCH 35/35] adjust commodity interpretation --- units/units.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/units/units.cpp b/units/units.cpp index 04b9abf4..3aa07076 100644 --- a/units/units.cpp +++ b/units/units.cpp @@ -1141,6 +1141,14 @@ static std::string } } } else { // inverse commodity + const auto multloc = propUnitString.find('*', 2); + if (propUnitString.compare(0, 2, "1/") == 0 && + multloc != std::string::npos && + (propUnitString.compare(multloc + 1, 5, "CXUN[") == 0 || + propUnitString.compare(multloc + 1, 6, "CXCUN[") == 0)) { + return propUnitString.substr(multloc + 1) + "/" + + propUnitString.substr(2, multloc - 2) + cString; + } auto loc = propUnitString.find_last_of('/'); if (loc == std::string::npos) { auto rs = checkForCustomUnit(cString);