Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
7717c67
address some issues raised concerning half power exponents for meters…
phlptp Aug 6, 2026
de26eb5
move tests to correct location
phlptp Aug 6, 2026
1d259f6
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 6, 2026
b503396
fix some more failing test cases
phlptp Aug 6, 2026
edf7f2d
remove the conditions to square rootMeter
phlptp Aug 6, 2026
db7aa04
fix expectation on powers for 64 representation
phlptp Aug 6, 2026
2e2237d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 6, 2026
7586f87
add some more checks and support other bracket types
phlptp Aug 6, 2026
b7328f0
more updates and resolutions
phlptp Aug 6, 2026
582c95c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 6, 2026
06b9bd7
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 9, 2026
487d469
fix a clang-tidy issue
phlptp Sep 1, 2026
8a24625
add more tests
phlptp Sep 2, 2026
f5fb66c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 2, 2026
54735fd
add and fix fuzz test
phlptp Sep 2, 2026
df7ac19
Merge branch 'half_power_exponents' of https://github.com/LLNL/units …
phlptp Sep 3, 2026
e66191b
resolve edge case
phlptp Sep 3, 2026
75c63ac
try alternative fix
phlptp Sep 3, 2026
8f774a1
add some test diagnostics
phlptp Sep 3, 2026
8b445eb
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 3, 2026
135e0d1
try new test
phlptp Sep 3, 2026
89f9bc4
Merge branch 'half_power_exponents' of https://github.com/LLNL/units …
phlptp Sep 3, 2026
30d6f30
match the single failure and the test
phlptp Sep 3, 2026
3aaefd2
more diagnostics
phlptp Sep 4, 2026
993203a
more test updates
phlptp Sep 4, 2026
fcd8dee
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 4, 2026
aa7bc62
update the test cases to try to narrow down the issue
phlptp Sep 4, 2026
587d519
Merge branch 'half_power_exponents' of https://github.com/LLNL/units …
phlptp Sep 4, 2026
4905805
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 4, 2026
777480a
more tweaks
phlptp Sep 4, 2026
13e1a81
Merge branch 'half_power_exponents' of https://github.com/LLNL/units …
phlptp Sep 4, 2026
01fd0f2
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 4, 2026
a247975
specify hashing types in commodity conversions
phlptp Sep 4, 2026
6e43e7d
more character conversions
phlptp Sep 4, 2026
253f537
try commodities tweak
phlptp Sep 4, 2026
fa54eb6
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 4, 2026
f02f3ac
more attempted fixes
phlptp Sep 4, 2026
b0757cf
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 4, 2026
cc26b5f
adjust commodity interpretation
phlptp Sep 5, 2026
d268b15
Merge branch 'half_power_exponents' of https://github.com/LLNL/units …
phlptp Sep 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions test/files/fuzz_issues/rtrip_fail38
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ZSh^-3999h$/991(•úÿ)
133 changes: 129 additions & 4 deletions test/fuzz_issue_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ SPDX-License-Identifier: BSD-3-Clause

#include <cstring>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <string>
#include <vector>
Expand Down Expand Up @@ -232,9 +233,42 @@ 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<unsigned int>(static_cast<unsigned char>(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()
<< ", 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";
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);
Expand All @@ -255,30 +289,121 @@ 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);
ASSERT_FALSE(cdata.empty());
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<unsigned int>(static_cast<unsigned char>(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)) {
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);
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()
<< ", 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";
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);
printUnitData("precise root2", preciseRoot2);
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);
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);
Expand Down
16 changes: 16 additions & 0 deletions test/test_unit_ops.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,9 @@ 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(rh.pow(1), rh);
EXPECT_EQ(rh.pow(0), precise::one);
Expand All @@ -851,6 +854,19 @@ 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(rh.pow(2), precise::Hz);
EXPECT_EQ(rh.pow(-2), precise::s);

EXPECT_NE(rh.pow(2), precise::m);
}

TEST(specialOps, degC)
{
auto res = precise::degC.pow(2);
Expand Down
107 changes: 107 additions & 0 deletions test/test_unit_strings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,88 @@ 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, 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::kg / precise::m.pow(2), unit_from_string("kg/(m^2)"));
EXPECT_EQ(
precise::kg / precise::m.pow(2),
unit_from_string("kg/(m<sup>2</sup>)"));
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)"));
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<std::string> invalidPowers{
"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;
}
}

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(
Expand Down Expand Up @@ -1953,6 +2035,31 @@ 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)
Expand Down
26 changes: 22 additions & 4 deletions units/commodities.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::uint32_t>(c) * hashcodes::Bc);
(static_cast<std::uint32_t>(static_cast<unsigned char>(c)) *
hashcodes::Bc);
}
return hash; // or return h % C;
}
Expand Down Expand Up @@ -243,7 +244,10 @@ 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<char>(std::tolower(character));
});
if (allowCustomCommodities.load(std::memory_order_acquire)) {
if (!customCommodityCodes.empty()) {
auto fnd2 = customCommodityCodes.find(comm);
Expand Down Expand Up @@ -292,7 +296,15 @@ 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) + "]";
}
}
}
Expand Down Expand Up @@ -320,7 +332,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<char>(std::tolower(character));
});
customCommodityNames.emplace(code, comm);
customCommodityCodes.emplace(comm, code);
}
Expand Down
Loading
Loading