Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 16 additions & 1 deletion include/behaviortree_cpp/utils/safe_any.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

#include <memory>
#include <string>
#include <string_view>
#include <type_traits>
#include <typeindex>

Expand All @@ -33,6 +34,12 @@ namespace BT

static std::type_index UndefinedAnyType = typeid(nullptr);

// Declared here (defined in basic_types.cpp, documented in basic_types.h) so that
// Any can share the locale-independent double parser without including
// basic_types.h, which itself includes this header.
[[nodiscard]] bool parseDouble(std::string_view str, double& out,
bool require_full_consumption);

// Trait to detect std::shared_ptr types (used for polymorphic port support)
template <typename T>
struct is_shared_ptr : std::false_type
Expand Down Expand Up @@ -458,7 +465,15 @@ inline nonstd::expected<T, std::string> Any::stringToNumber() const
}
if constexpr(std::is_floating_point_v<T>)
{
return std::stod(str.toStdString());
// std::stod honors LC_NUMERIC, so under a locale that uses ',' as decimal
// separator "3.5" would silently parse as 3. parseDouble reproduces the
// std::from_chars semantics of the branch above, on every platform.
double value = 0.0;
if(parseDouble(str.toStdStringView(), value, /*require_full_consumption=*/false))
{
return static_cast<T>(value);
}
return nonstd::make_unexpected("Any failed string to number conversion");
}
}
catch(...)
Expand Down
12 changes: 2 additions & 10 deletions src/controls/switch_node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,8 @@ bool CheckStringEquality(const std::string& v1, const std::string& v2,
auto [ptr, ec] = std::from_chars(str.data(), end, result);
return ec == std::errc() && ptr == end;
#else
try
{
std::size_t pos = 0;
result = std::stod(str, &pos);
return pos == str.size();
}
catch(...)
{
return false;
}
// locale-independent, unlike std::stod (see parseDouble)
return parseDouble(str, result, /*require_full_consumption=*/true);
#endif
};
double v1_real = 0;
Expand Down
19 changes: 19 additions & 0 deletions tests/gtest_any.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

#include "test_helper.hpp"

#include <charconv> // std::{from_chars,from_chars_result},
#include <string>
#include <system_error> // std::errc.
Expand Down Expand Up @@ -250,3 +252,20 @@ TEST(Any, Cast)
EXPECT_EQ(a.cast<std::vector<int>>(), v);
}
}

TEST(Any, StringToNumberIgnoresLocale)
{
// The string -> number conversion must use '.' as decimal separator
// regardless of the locale selected by the host application.
const ScopedCommaDecimalLocale comma_locale;
if(!comma_locale.active())
{
GTEST_SKIP() << "no locale with ',' as decimal separator on this machine";
}

EXPECT_DOUBLE_EQ(Any(std::string("3.5")).cast<double>(), 3.5);
EXPECT_DOUBLE_EQ(Any(std::string("-0.25")).cast<double>(), -0.25);
EXPECT_FLOAT_EQ(Any(std::string("1.5e2")).cast<float>(), 150.0f);
EXPECT_EQ(Any(std::string("42")).cast<int>(), 42);
EXPECT_ANY_THROW(auto res = Any(std::string("fifty")).cast<double>());
}
17 changes: 17 additions & 0 deletions tests/gtest_switch.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "action_test_node.h"
#include "condition_test_node.h"
#include "test_helper.hpp"

#include "behaviortree_cpp/behavior_tree.h"
#include "behaviortree_cpp/bt_factory.h"
Expand Down Expand Up @@ -264,3 +265,19 @@ TEST(SwitchStringEquality, RejectsTrailingCharacters)
EXPECT_FALSE(CheckStringEquality("5 ", "5", nullptr));
EXPECT_FALSE(CheckStringEquality("none", "1", nullptr));
}

TEST(SwitchStringEquality, RealComparisonIgnoresLocale)
{
using BT::details::CheckStringEquality;

const ScopedCommaDecimalLocale comma_locale;
if(!comma_locale.active())
{
GTEST_SKIP() << "no locale with ',' as decimal separator on this machine";
}

EXPECT_TRUE(CheckStringEquality("3.50", "3.5", nullptr));
EXPECT_TRUE(CheckStringEquality("5", "5.0", nullptr));
EXPECT_FALSE(CheckStringEquality("3.5", "3", nullptr));
EXPECT_FALSE(CheckStringEquality("0.5", "1", nullptr));
}
59 changes: 59 additions & 0 deletions tests/test_helper.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,65 @@
#include "behaviortree_cpp/bt_factory.h"

#include <cstdio>
#include <cstdlib>
#include <string>

#if !defined(_WIN32)
#include <locale.h>
#if defined(__APPLE__)
#include <xlocale.h>
#endif
#endif

/**
* Switch the calling thread (and only this thread) to a locale whose decimal
* separator is ',' for the duration of the scope. Use active() to skip the test
* when no such locale is available on the machine.
*/
class ScopedCommaDecimalLocale
{
public:
ScopedCommaDecimalLocale()
{
#if !defined(_WIN32)
locale_ = ::newlocale(LC_NUMERIC_MASK, "de_DE.UTF-8", static_cast<::locale_t>(0));
if(locale_ != static_cast<::locale_t>(0))
{
previous_ = ::uselocale(locale_);
}
#endif
}

~ScopedCommaDecimalLocale()
{
#if !defined(_WIN32)
if(locale_ != static_cast<::locale_t>(0))
{
::uselocale(previous_);
::freelocale(locale_);
}
#endif
}

ScopedCommaDecimalLocale(const ScopedCommaDecimalLocale&) = delete;
ScopedCommaDecimalLocale& operator=(const ScopedCommaDecimalLocale&) = delete;

/// true if the C library now parses "0,5" as one half on this thread
[[nodiscard]] bool active() const
{
#if !defined(_WIN32)
return locale_ != static_cast<::locale_t>(0) && std::strtod("0,5", nullptr) == 0.5;
#else
return false;
#endif
}

private:
#if !defined(_WIN32)
::locale_t locale_ = static_cast<::locale_t>(0);
::locale_t previous_ = static_cast<::locale_t>(0);
#endif
};

inline BT::NodeStatus TestTick(int* tick_counter)
{
Expand Down
Loading