Files
esphome/tests/components/core/test_value_accuracy.cpp
T
J. Nick Koston d8e586609f [core] Optimize value_accuracy_to_buf to avoid snprintf
Replace snprintf("%.*f") with integer-based formatting for finite
float values with accuracy_decimals 0-3 (covers virtually all sensor
usage). Falls back to snprintf for higher accuracy or NaN/Inf.

Uses lrint() with double cast for the multiply to match snprintf's
rounding behavior exactly. The fast path avoids snprintf's heavy
float formatting machinery entirely.

Also optimizes value_accuracy_with_uom_to_buf to append the UOM
string directly instead of going through snprintf.

Adds C++ unit tests that verify output matches snprintf for a range
of values including edge cases.

Benchmark: 92,961ns -> 6,484ns (14.3x faster, 2000 iterations).
2026-04-08 20:46:16 -10:00

152 lines
4.4 KiB
C++

#include <gtest/gtest.h>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <span>
#include "esphome/core/helpers.h"
namespace esphome::testing {
// Helper to call value_accuracy_to_buf and return as string
static std::string va_to_string(float value, int8_t accuracy_decimals) {
char buf[VALUE_ACCURACY_MAX_LEN];
std::span<char, VALUE_ACCURACY_MAX_LEN> sp(buf);
size_t len = value_accuracy_to_buf(sp, value, accuracy_decimals);
return std::string(buf, len);
}
// Helper: reference implementation using snprintf for comparison
static std::string va_reference(float value, int8_t accuracy_decimals) {
// Replicate normalize_accuracy_decimals logic
if (accuracy_decimals < 0) {
float divisor;
if (accuracy_decimals == -1) {
divisor = 10.0f;
} else if (accuracy_decimals == -2) {
divisor = 100.0f;
} else {
divisor = pow10_int(-accuracy_decimals);
}
value = roundf(value / divisor) * divisor;
accuracy_decimals = 0;
}
char buf[VALUE_ACCURACY_MAX_LEN];
snprintf(buf, sizeof(buf), "%.*f", accuracy_decimals, value);
return std::string(buf);
}
// --- Basic formatting ---
TEST(ValueAccuracyToBuf, ZeroDecimals) {
EXPECT_EQ(va_to_string(23.456f, 0), "23");
EXPECT_EQ(va_to_string(0.0f, 0), "0");
EXPECT_EQ(va_to_string(100.0f, 0), "100");
EXPECT_EQ(va_to_string(1.0f, 0), "1");
}
TEST(ValueAccuracyToBuf, OneDecimal) {
EXPECT_EQ(va_to_string(23.456f, 1), "23.5");
EXPECT_EQ(va_to_string(0.0f, 1), "0.0");
EXPECT_EQ(va_to_string(1.05f, 1), va_reference(1.05f, 1));
}
TEST(ValueAccuracyToBuf, TwoDecimals) {
EXPECT_EQ(va_to_string(23.456f, 2), "23.46");
EXPECT_EQ(va_to_string(0.0f, 2), "0.00");
EXPECT_EQ(va_to_string(1.005f, 2), va_reference(1.005f, 2));
}
TEST(ValueAccuracyToBuf, ThreeDecimals) {
EXPECT_EQ(va_to_string(23.456f, 3), "23.456");
EXPECT_EQ(va_to_string(0.0f, 3), "0.000");
}
// --- Negative values ---
TEST(ValueAccuracyToBuf, NegativeValues) {
EXPECT_EQ(va_to_string(-23.456f, 2), "-23.46");
EXPECT_EQ(va_to_string(-0.5f, 1), "-0.5");
EXPECT_EQ(va_to_string(-100.0f, 0), "-100");
}
// --- Negative accuracy_decimals (rounding to tens/hundreds) ---
TEST(ValueAccuracyToBuf, NegativeAccuracy) {
EXPECT_EQ(va_to_string(1234.0f, -1), va_reference(1234.0f, -1));
EXPECT_EQ(va_to_string(1234.0f, -2), va_reference(1234.0f, -2));
EXPECT_EQ(va_to_string(56.0f, -1), va_reference(56.0f, -1));
}
// --- Special float values ---
TEST(ValueAccuracyToBuf, NaN) {
std::string result = va_to_string(NAN, 2);
EXPECT_EQ(result, va_reference(NAN, 2));
}
TEST(ValueAccuracyToBuf, Infinity) {
std::string result = va_to_string(INFINITY, 2);
EXPECT_EQ(result, va_reference(INFINITY, 2));
}
TEST(ValueAccuracyToBuf, NegativeInfinity) {
std::string result = va_to_string(-INFINITY, 2);
EXPECT_EQ(result, va_reference(-INFINITY, 2));
}
// --- Edge cases ---
TEST(ValueAccuracyToBuf, VerySmallValues) {
EXPECT_EQ(va_to_string(0.001f, 3), "0.001");
EXPECT_EQ(va_to_string(0.001f, 2), "0.00");
EXPECT_EQ(va_to_string(0.009f, 2), "0.01");
}
TEST(ValueAccuracyToBuf, LargeValues) {
EXPECT_EQ(va_to_string(999999.0f, 0), va_reference(999999.0f, 0));
EXPECT_EQ(va_to_string(1013.25f, 2), "1013.25");
}
TEST(ValueAccuracyToBuf, Rounding) {
// 0.5 rounds up
EXPECT_EQ(va_to_string(23.5f, 0), "24");
EXPECT_EQ(va_to_string(23.45f, 1), "23.5"); // float: 23.45 -> 23.4 or 23.5
EXPECT_EQ(va_to_string(23.45f, 1), va_reference(23.45f, 1));
}
// --- Match snprintf for a range of typical sensor values ---
TEST(ValueAccuracyToBuf, MatchesSnprintf) {
float test_values[] = {0.0f, 1.0f, -1.0f, 23.456f, -23.456f, 100.0f, 0.1f, 0.01f, 99.99f, 1013.25f, -40.0f};
int8_t test_accuracies[] = {0, 1, 2, 3};
for (float value : test_values) {
for (int8_t acc : test_accuracies) {
EXPECT_EQ(va_to_string(value, acc), va_reference(value, acc))
<< "Mismatch for value=" << value << " accuracy=" << static_cast<int>(acc);
}
}
}
// --- Return value (length) ---
TEST(ValueAccuracyToBuf, ReturnsCorrectLength) {
char buf[VALUE_ACCURACY_MAX_LEN];
std::span<char, VALUE_ACCURACY_MAX_LEN> sp(buf);
size_t len = value_accuracy_to_buf(sp, 23.456f, 2);
EXPECT_EQ(len, 5u); // "23.46"
EXPECT_EQ(strlen(buf), len);
len = value_accuracy_to_buf(sp, 0.0f, 0);
EXPECT_EQ(len, 1u); // "0"
EXPECT_EQ(strlen(buf), len);
len = value_accuracy_to_buf(sp, -100.0f, 1);
EXPECT_EQ(len, 6u); // "-100.0"
EXPECT_EQ(strlen(buf), len);
}
} // namespace esphome::testing