diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index add7cebbc..b9fea6919 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -24,6 +24,9 @@ target_link_libraries(graph_example ignition-math${IGN_MATH_VER}::ignition-math$ add_executable(helpers_example helpers_example.cc) target_link_libraries(helpers_example ignition-math${IGN_MATH_VER}::ignition-math${IGN_MATH_VER}) +add_executable(interval_example interval_example.cc) +target_link_libraries(interval_example ignition-math${IGN_MATH_VER}::ignition-math${IGN_MATH_VER}) + add_executable(kmeans kmeans.cc) target_link_libraries(kmeans ignition-math${IGN_MATH_VER}::ignition-math${IGN_MATH_VER}) diff --git a/examples/interval_example.cc b/examples/interval_example.cc new file mode 100644 index 000000000..68623ca72 --- /dev/null +++ b/examples/interval_example.cc @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2022 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ +//! [complete] +#include +#include + +int main(int argc, char **argv) +{ + std::cout << std::boolalpha; + + const ignition::math::Intervald defaultInterval; + // A default constructed interval should be empty. + std::cout << "The " << defaultInterval << " interval is empty: " + << defaultInterval.Empty() << std::endl; + + const ignition::math::Intervald openInterval = + ignition::math::Intervald::Open(-1., 1.); + // An open interval should exclude its endpoints. + std::cout << "The " << openInterval << " interval contains its endpoints: " + << (openInterval.Contains(openInterval.LeftValue()) || + openInterval.Contains(openInterval.RightValue())) + << std::endl; + + const ignition::math::Intervald closedInterval = + ignition::math::Intervald::Closed(0., 1.); + + // A closed interval should include its endpoints. + std::cout << "The " << closedInterval << " interval contains its endpoints: " + << (closedInterval.Contains(closedInterval.LeftValue()) || + closedInterval.Contains(closedInterval.RightValue())) + << std::endl; + + // Closed and open intervals may intersect. + std::cout << "Intervals " << closedInterval << " and " << openInterval + << " intersect: " << closedInterval.Intersects(openInterval) + << std::endl; + + // The unbounded interval should include all non-empty intervals. + std::cout << "The " << ignition::math::Intervald::Unbounded + << " interval contains all previous non-empty intervals: " + << (ignition::math::Intervald::Unbounded.Contains(openInterval) || + ignition::math::Intervald::Unbounded.Contains(closedInterval)) + << std::endl; + +} +//! [complete] diff --git a/include/ignition/math/Interval.hh b/include/ignition/math/Interval.hh new file mode 100644 index 000000000..fb0229b10 --- /dev/null +++ b/include/ignition/math/Interval.hh @@ -0,0 +1,292 @@ +/* + * Copyright (C) 2022 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ +#ifndef IGNITION_MATH_INTERVAL_HH_ +#define IGNITION_MATH_INTERVAL_HH_ + +#include +#include +#include +#include +#include + +#include + +namespace ignition +{ + namespace math + { + // Inline bracket to help doxygen filtering. + inline namespace IGNITION_MATH_VERSION_NAMESPACE { + // + /// \class Interval Interval.hh ignition/math/Interval.hh + /// \brief The Interval class represents a range of real numbers. + /// Intervals may be open (a, b), left-closed [a, b), right-closed + /// (a, b], or fully closed [a, b]. + /// + /// ## Example + /// + /// \snippet examples/interval_example.cc complete + template + class Interval + { + /// \brief An unbounded interval (-∞, ∞) + public: static const Interval &Unbounded; + + /// \brief Constructor + public: Interval() = default; + + /// \brief Constructor + /// \param[in] _leftValue leftmost interval value + /// \param[in] _leftClosed whether the interval is left-closed or not + /// \param[in] _rightValue rightmost interval value + /// \param[in] _rightClosed whether the interval is right-closed or not + public: Interval(T _leftValue, bool _leftClosed, + T _rightValue, bool _rightClosed) + : leftValue(std::move(_leftValue)), + rightValue(std::move(_rightValue)), + leftClosed(_leftClosed), + rightClosed(_rightClosed) + { + } + + /// \brief Make an open interval (`_leftValue`, `_rightValue`) + /// \param[in] _leftValue leftmost interval value + /// \param[in] _rightValue rightmost interval value + /// \return the open interval + public: static Interval Open(T _leftValue, T _rightValue) + { + return Interval( + std::move(_leftValue), false, + std::move(_rightValue), false); + } + + /// \brief Make a left-closed interval [`_leftValue`, `_rightValue`) + /// \param[in] _leftValue leftmost interval value + /// \param[in] _rightValue rightmost interval value + /// \return the left-closed interval + public: static Interval LeftClosed(T _leftValue, T _rightValue) + { + return Interval( + std::move(_leftValue), true, + std::move(_rightValue), false); + } + + /// \brief Make a right-closed interval (`_leftValue`, `_rightValue`] + /// \param[in] _leftValue leftmost interval value + /// \param[in] _rightValue rightmost interval value + /// \return the left-closed interval + public: static Interval RightClosed(T _leftValue, T _rightValue) + { + return Interval( + std::move(_leftValue), false, + std::move(_rightValue), true); + } + + /// \brief Make a closed interval [`_leftValue`, `_rightValue`] + /// \param[in] _leftValue leftmost interval value + /// \param[in] _rightValue rightmost interval value + /// \return the closed interval + public: static Interval Closed(T _leftValue, T _rightValue) + { + return Interval{ + std::move(_leftValue), true, + std::move(_rightValue), true}; + } + + /// \brief Get the leftmost interval value + /// \return the leftmost interval value + public: const T &LeftValue() const { return this->leftValue; } + + /// \brief Check if the interval is left-closed + /// \return true if the interval is left-closed, false otherwise + public: bool IsLeftClosed() const { return this->leftClosed; } + + /// \brief Get the rightmost interval value + /// \return the rightmost interval value + public: const T &RightValue() const { return this->rightValue; } + + /// \brief Check if the interval is right-closed + /// \return true if the interval is right-closed, false otherwise + public: bool IsRightClosed() const { return this->rightClosed; } + + /// \brief Check if the interval is empty + /// Some examples of empty intervals include + /// (a, a), [a, a), and [a + 1, a]. + /// \return true if it is empty, false otherwise + public: bool Empty() const + { + if (this->leftClosed && this->rightClosed) + { + return this->rightValue < this->leftValue; + } + return this->rightValue <= this->leftValue; + } + + /// \brief Check if the interval contains `_value` + /// \param[in] _value value to check for membership + /// \return true if it is contained, false otherwise + public: bool Contains(const T &_value) const + { + if (this->leftClosed && this->rightClosed) + { + return this->leftValue <= _value && _value <= this->rightValue; + } + if (this->leftClosed) + { + return this->leftValue <= _value && _value < this->rightValue; + } + if (this->rightClosed) + { + return this->leftValue < _value && _value <= this->rightValue; + } + return this->leftValue < _value && _value < this->rightValue; + } + + /// \brief Check if the interval contains `_other` interval + /// \param[in] _other interval to check for membership + /// \return true if it is contained, false otherwise + public: bool Contains(const Interval &_other) const + { + if (this->Empty() || _other.Empty()) + { + return false; + } + if (!this->leftClosed && _other.leftClosed) + { + if (_other.leftValue <= this->leftValue) + { + return false; + } + } + else + { + if (_other.leftValue < this->leftValue) + { + return false; + } + } + if (!this->rightClosed && _other.rightClosed) + { + if (this->rightValue <= _other.rightValue) + { + return false; + } + } + else + { + if (this->rightValue < _other.rightValue) + { + return false; + } + } + return true; + } + + /// \brief Check if the interval intersects `_other` interval + /// \param[in] _other interval to check for intersection + /// \return true if both intervals intersect, false otherwise + public: bool Intersects(const Interval &_other) const + { + if (this->Empty() || _other.Empty()) + { + return false; + } + if (this->rightClosed && _other.leftClosed) + { + if (this->rightValue < _other.leftValue) + { + return false; + } + } + else + { + if (this->rightValue <= _other.leftValue) + { + return false; + } + } + if (_other.rightClosed && this->leftClosed) + { + if (_other.rightValue < this->leftValue) + { + return false; + } + } + else + { + if (_other.rightValue <= this->leftValue) + { + return false; + } + } + return true; + } + + /// \brief Equality test operator + /// \param _other interval to check for equality + /// \return true if intervals are equal, false otherwise + public: bool operator==(const Interval &_other) const + { + return this->Contains(_other) && _other.Contains(*this); + } + + /// \brief Inequality test operator + /// \param _other interval to check for inequality + /// \return true if intervals are unequal, false otherwise + public: bool operator!=(const Interval &_other) const + { + return !this->Contains(_other) || !_other.Contains(*this); + } + + /// \brief Stream insertion operator + /// \param _out output stream + /// \param _interval Interval to output + /// \return the stream + public: friend std::ostream &operator<<( + std::ostream &_out, const ignition::math::Interval &_interval) + { + return _out << (_interval.leftClosed ? "[" : "(") + << _interval.leftValue << ", " << _interval.rightValue + << (_interval.rightClosed ? "]" : ")"); + } + + /// \brief The leftmost interval value + private: T leftValue{0}; + /// \brief The righmost interval value + private: T rightValue{0}; + /// \brief Whether the interval is left-closed or not + private: bool leftClosed{false}; + /// \brief Whether the interval is right-closed or not + private: bool rightClosed{false}; + }; + + namespace detail { + template + const Interval gUnboundedInterval = + Interval::Open(-std::numeric_limits::infinity(), + std::numeric_limits::infinity()); + } // namespace detail + template + const Interval &Interval::Unbounded = detail::gUnboundedInterval; + + using Intervalf = Interval; + using Intervald = Interval; + } + } +} + +#endif diff --git a/src/Interval_TEST.cc b/src/Interval_TEST.cc new file mode 100644 index 000000000..ecd0c8ee1 --- /dev/null +++ b/src/Interval_TEST.cc @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2022 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ +#include +#include + +#include "ignition/math/Interval.hh" + +using namespace ignition; + +///////////////////////////////////////////////// +TEST(IntervalTest, DefaultConstructor) +{ + const math::Intervald interval; + EXPECT_DOUBLE_EQ( + interval.LeftValue(), + interval.RightValue()); +} + +///////////////////////////////////////////////// +TEST(IntervalTest, Constructor) +{ + constexpr bool kClosed = true; + + const math::Intervald interval( + 0., kClosed, 1., !kClosed); + EXPECT_DOUBLE_EQ(interval.LeftValue(), 0.); + EXPECT_TRUE(interval.IsLeftClosed()); + EXPECT_DOUBLE_EQ(interval.RightValue(), 1.); + EXPECT_FALSE(interval.IsRightClosed()); +} + +///////////////////////////////////////////////// +TEST(IntervalTest, ConstructionHelpers) +{ + const math::Intervald openInterval = + math::Intervald::Open(0., 1.); + EXPECT_DOUBLE_EQ(openInterval.LeftValue(), 0.); + EXPECT_FALSE(openInterval.IsLeftClosed()); + EXPECT_DOUBLE_EQ(openInterval.RightValue(), 1.); + EXPECT_FALSE(openInterval.IsRightClosed()); + + const math::Intervald leftClosedInterval = + math::Intervald::LeftClosed(0., 1.); + EXPECT_DOUBLE_EQ(leftClosedInterval.LeftValue(), 0.); + EXPECT_TRUE(leftClosedInterval.IsLeftClosed()); + EXPECT_DOUBLE_EQ(leftClosedInterval.RightValue(), 1.); + EXPECT_FALSE(leftClosedInterval.IsRightClosed()); + + const math::Intervald rightClosedInterval = + math::Intervald::RightClosed(0., 1.); + EXPECT_DOUBLE_EQ(rightClosedInterval.LeftValue(), 0.); + EXPECT_FALSE(rightClosedInterval.IsLeftClosed()); + EXPECT_DOUBLE_EQ(rightClosedInterval.RightValue(), 1.); + EXPECT_TRUE(rightClosedInterval.IsRightClosed()); + + const math::Intervald closedInterval = + math::Intervald::Closed(0., 1.); + EXPECT_DOUBLE_EQ(closedInterval.LeftValue(), 0.); + EXPECT_TRUE(closedInterval.IsLeftClosed()); + EXPECT_DOUBLE_EQ(closedInterval.RightValue(), 1.); + EXPECT_TRUE(closedInterval.IsRightClosed()); +} + +///////////////////////////////////////////////// +TEST(IntervalTest, UnboundedInterval) +{ + EXPECT_FALSE(math::Intervald::Unbounded.Empty()); + EXPECT_FALSE(math::Intervald::Unbounded.IsLeftClosed()); + EXPECT_FALSE(math::Intervald::Unbounded.IsRightClosed()); +} + +///////////////////////////////////////////////// +TEST(IntervalTest, EmptyInterval) +{ + EXPECT_FALSE(math::Intervald::Open(0., 1.).Empty()); + EXPECT_TRUE(math::Intervald::Open(0., 0.).Empty()); + EXPECT_TRUE(math::Intervald::LeftClosed(0., 0.).Empty()); + EXPECT_TRUE(math::Intervald::RightClosed(0., 0.).Empty()); + EXPECT_FALSE(math::Intervald::Closed(0., 0.).Empty()); + EXPECT_TRUE(math::Intervald::Closed(1., 0.).Empty()); +} + +///////////////////////////////////////////////// +TEST(IntervalTest, IntervalMembership) +{ + const math::Intervald emptyInterval = + math::Intervald::Open(0., 0.); + EXPECT_FALSE(emptyInterval.Contains(0.)); + + const math::Intervald openInterval = + math::Intervald::Open(0., 1.); + EXPECT_FALSE(openInterval.Contains(0.)); + EXPECT_TRUE(openInterval.Contains(0.5)); + EXPECT_FALSE(openInterval.Contains(1)); + + const math::Intervald leftClosedInterval = + math::Intervald::LeftClosed(0., 1.); + EXPECT_TRUE(leftClosedInterval.Contains(0.)); + EXPECT_TRUE(leftClosedInterval.Contains(0.5)); + EXPECT_FALSE(leftClosedInterval.Contains(1)); + + const math::Intervald rightClosedInterval = + math::Intervald::RightClosed(0., 1.); + EXPECT_FALSE(rightClosedInterval.Contains(0.)); + EXPECT_TRUE(rightClosedInterval.Contains(0.5)); + EXPECT_TRUE(rightClosedInterval.Contains(1)); + + const math::Intervald closedInterval = + math::Intervald::Closed(0., 1.); + EXPECT_TRUE(closedInterval.Contains(0.)); + EXPECT_TRUE(closedInterval.Contains(0.5)); + EXPECT_TRUE(closedInterval.Contains(1)); + + const math::Intervald degenerateInterval = + math::Intervald::Closed(0., 0.); + EXPECT_TRUE(degenerateInterval.Contains(0.)); +} + +///////////////////////////////////////////////// +TEST(IntervalTest, IntervalSubset) +{ + const math::Intervald openInterval = math::Intervald::Open(0., 1.); + EXPECT_FALSE(openInterval.Contains(math::Intervald::Open(0., 0.))); + EXPECT_FALSE(openInterval.Contains(math::Intervald::Closed(-1., 0.))); + EXPECT_FALSE(openInterval.Contains(math::Intervald::Closed(1., 2.))); + EXPECT_FALSE(openInterval.Contains(math::Intervald::Open(0.5, 1.5))); + EXPECT_FALSE(openInterval.Contains(math::Intervald::Open(-0.5, 0.5))); + EXPECT_TRUE(openInterval.Contains(math::Intervald::Open(0.25, 0.75))); + EXPECT_FALSE(openInterval.Contains(math::Intervald::Closed(0., 1.))); + + const math::Intervald closedInterval = math::Intervald::Closed(0., 1.); + EXPECT_FALSE(closedInterval.Contains(math::Intervald::Open(0., 0.))); + EXPECT_TRUE(closedInterval.Contains(math::Intervald::Closed(0., 0.))); + EXPECT_FALSE(closedInterval.Contains(math::Intervald::Closed(0.5, 1.5))); + EXPECT_FALSE(closedInterval.Contains(math::Intervald::Closed(-0.5, 0.5))); + EXPECT_TRUE(closedInterval.Contains(math::Intervald::Closed(0.25, 0.75))); + EXPECT_TRUE(closedInterval.Contains(math::Intervald::Open(0., 1.))); + + EXPECT_TRUE(math::Intervald::Unbounded.Contains(openInterval)); + EXPECT_TRUE(math::Intervald::Unbounded.Contains(closedInterval)); +} + +///////////////////////////////////////////////// +TEST(IntervalTest, IntervalEquality) +{ + EXPECT_NE(math::Intervald::Open(0., 0.), math::Intervald::Open(0., 0.)); + EXPECT_EQ(math::Intervald::Closed(0., 0.), math::Intervald::Closed(0., 0.)); + EXPECT_NE(math::Intervald::Open(0., 1.), math::Intervald::Closed(0., 1.)); + EXPECT_NE( + math::Intervald::Open(0., 1.), math::Intervald::LeftClosed(0., 1.)); + EXPECT_NE( + math::Intervald::Open(0., 1.), math::Intervald::RightClosed(0., 1.)); + EXPECT_NE( + math::Intervald::Closed(0., 1.), math::Intervald::LeftClosed(0., 1.)); + EXPECT_NE( + math::Intervald::Closed(0., 1.), math::Intervald::RightClosed(0., 1.)); +} + +///////////////////////////////////////////////// +TEST(IntervalTest, IntervalIntersection) +{ + const math::Intervald openInterval = math::Intervald::Open(0., 1.); + EXPECT_FALSE(openInterval.Intersects(math::Intervald::Open(0.5, 0.5))); + EXPECT_TRUE(openInterval.Intersects(math::Intervald::Open(0.5, 1.5))); + EXPECT_TRUE(openInterval.Intersects(math::Intervald::Open(-0.5, 0.5))); + EXPECT_FALSE(openInterval.Intersects(math::Intervald::Closed(1., 1.))); + EXPECT_TRUE(openInterval.Intersects(math::Intervald::Closed(0.5, 0.5))); + EXPECT_FALSE(openInterval.Intersects(math::Intervald::Open(1., 2.))); + EXPECT_FALSE(openInterval.Intersects(math::Intervald::Open(-1., 0.))); + EXPECT_FALSE(openInterval.Intersects(math::Intervald::LeftClosed(1., 2.))); + EXPECT_FALSE(openInterval.Intersects(math::Intervald::RightClosed(-1., 0.))); + + const math::Intervald closedInterval = math::Intervald::Closed(0., 1.); + EXPECT_FALSE(closedInterval.Intersects(math::Intervald::Open(1., 1.))); + EXPECT_TRUE(closedInterval.Intersects(math::Intervald::Closed(0.5, 1.5))); + EXPECT_TRUE(closedInterval.Intersects(math::Intervald::Closed(-0.5, 0.5))); + EXPECT_FALSE(closedInterval.Intersects(math::Intervald::Closed(1.5, 2.5))); + EXPECT_FALSE(closedInterval.Intersects(math::Intervald::Closed(-1.5, -0.5))); + EXPECT_FALSE(closedInterval.Intersects(math::Intervald::Open(1., 2.))); + EXPECT_FALSE(closedInterval.Intersects(math::Intervald::Open(-1., 0.))); + EXPECT_TRUE(closedInterval.Intersects(math::Intervald::LeftClosed(1., 2.))); + EXPECT_TRUE(closedInterval.Intersects(math::Intervald::RightClosed(-1., 0.))); +} + +///////////////////////////////////////////////// +TEST(IntervalTest, IntervalStreaming) +{ + { + std::ostringstream os; + os << math::Intervald::Open(0., 1.); + EXPECT_EQ(os.str(), "(0, 1)"); + } + { + std::ostringstream os; + os << math::Intervald::LeftClosed(0., 1.); + EXPECT_EQ(os.str(), "[0, 1)"); + } + { + std::ostringstream os; + os << math::Intervald::RightClosed(0., 1.); + EXPECT_EQ(os.str(), "(0, 1]"); + } + { + std::ostringstream os; + os << math::Intervald::Closed(0., 1.); + EXPECT_EQ(os.str(), "[0, 1]"); + } +}