diff --git a/include/CMakeLists.txt b/include/CMakeLists.txt index c36fb2e4..b9d699b2 100644 --- a/include/CMakeLists.txt +++ b/include/CMakeLists.txt @@ -27,12 +27,18 @@ set(Oasis_HEADERS Oasis/Variable.hpp) # Adds a header-only library target called "OasisHeaders" -add_library(OasisHeaders INTERFACE) +add_library(OasisHeaders INTERFACE + Oasis/UnitGraph.hpp + Oasis/GlobalUnitGraph.hpp) add_library(Oasis::Headers ALIAS OasisHeaders) target_compile_features(OasisHeaders INTERFACE cxx_std_20) target_include_directories( OasisHeaders INTERFACE $ - $) + $ + /opt/homebrew/include +) + +target_compile_options(OasisHeaders INTERFACE -Wno-error=language-extension-token) source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${Oasis_HEADERS}) diff --git a/include/Oasis/GlobalUnitGraph.hpp b/include/Oasis/GlobalUnitGraph.hpp new file mode 100644 index 00000000..056fcee0 --- /dev/null +++ b/include/Oasis/GlobalUnitGraph.hpp @@ -0,0 +1,14 @@ +// +// Created by Jary Chen on 3/10/25. +// + +#ifndef GLOBALUNITGRAPH_HPP +#define GLOBALUNITGRAPH_HPP +#include "UnitGraph.hpp" + +namespace Oasis { + // Reference to singleton UnitGraph + UnitGraph& getGlobalUnitGraph(); +} + +#endif //GLOBALUNITGRAPH_HPP diff --git a/include/Oasis/Real.hpp b/include/Oasis/Real.hpp index be8e3e36..4b28feb6 100644 --- a/include/Oasis/Real.hpp +++ b/include/Oasis/Real.hpp @@ -6,6 +6,15 @@ #define OASIS_REAL_HPP #include "LeafExpression.hpp" +#include "Unit.hpp" +#include "GlobalUnitGraph.hpp" +#include +#include +#include +#include +#include +#include + namespace Oasis { @@ -17,19 +26,22 @@ class Real : public LeafExpression { Real() = default; Real(const Real& other) = default; - explicit Real(double value); + explicit Real(double value, Unit unit = Unit::None); [[nodiscard]] auto Equals(const Expression& other) const -> bool final; EXPRESSION_TYPE(Real) EXPRESSION_CATEGORY(UnExp) + auto ConvertTo(Unit targetUnit) const -> Real; /** * Gets the value of the real number. * @return The value of the real number. */ [[nodiscard]] auto GetValue() const -> double; + [[nodiscard]] auto GetUnit() const -> Unit; + [[nodiscard]] auto Integrate(const Expression& integrationVariable) const -> std::unique_ptr final; [[nodiscard]] auto Differentiate(const Expression&) const -> std::unique_ptr final; @@ -38,6 +50,8 @@ class Real : public LeafExpression { private: double value {}; + Unit unit = Unit::None; + }; } // Oasis diff --git a/include/Oasis/Unit.hpp b/include/Oasis/Unit.hpp new file mode 100644 index 00000000..af15b810 --- /dev/null +++ b/include/Oasis/Unit.hpp @@ -0,0 +1,22 @@ +// Unit.hpp +#ifndef OASIS_UNIT_HPP +#define OASIS_UNIT_HPP + +namespace Oasis { + +enum class Unit { + None, + Meter, + Kilometer, + Gram, + Kilogram, + Second, + Ampere, + Kelvin, + Mole, + Candela +}; + +} // Oasis + +#endif // OASIS_UNIT_HPP diff --git a/include/Oasis/UnitGraph.hpp b/include/Oasis/UnitGraph.hpp new file mode 100644 index 00000000..f70201e9 --- /dev/null +++ b/include/Oasis/UnitGraph.hpp @@ -0,0 +1,44 @@ +// +// Created by Jary Chen on 3/4/25. +// + +#ifndef OASIS_UNIT_GRAPH_HPP +#define OASIS_UNIT_GRAPH_HPP + +#include "Unit.hpp" +#include +#include +#include +#include +#include +#include + +namespace Oasis { + +struct EdgeProperties { + double factor; +}; + +using Graph = boost::adjacency_list; + +class UnitGraph { +public: + UnitGraph(); + + void addConversion(Unit from, Unit to, double factor); + + std::optional findConversionFactor(Unit from, Unit to) const; + +private: + Graph graph_; + std::unordered_map unitToVertex_; + int nextVertex_; + + int getOrAddVertex(Unit unit); +}; + +} // namespace Oasis + +#endif // OASIS_UNIT_GRAPH_HPP + diff --git a/include/Oasis/Variable.hpp b/include/Oasis/Variable.hpp index 580268ae..b43f2ccf 100644 --- a/include/Oasis/Variable.hpp +++ b/include/Oasis/Variable.hpp @@ -9,6 +9,7 @@ #include #include "LeafExpression.hpp" +#include "Unit.hpp" namespace Oasis { @@ -23,7 +24,7 @@ class Variable : public LeafExpression { Variable() = default; Variable(const Variable& other) = default; - explicit Variable(std::string name); + explicit Variable(std::string name, Unit unit = Unit::None); [[nodiscard]] virtual auto Equals(const Expression& other) const -> bool final; @@ -37,6 +38,8 @@ class Variable : public LeafExpression { */ [[nodiscard]] auto GetName() const -> std::string; + [[nodiscard]] auto GetUnit() const -> Unit; + [[nodiscard]] auto Differentiate(const Expression& differentiationVariable) const -> std::unique_ptr final; [[nodiscard]] auto Integrate(const Expression& integrationVariable) const -> std::unique_ptr final; @@ -47,6 +50,7 @@ class Variable : public LeafExpression { private: std::string name {}; + Unit unit = Unit::None; }; } // Oasis diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index aa6add6b..176084cf 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -27,7 +27,9 @@ set(Oasis_SOURCES Subtract.cpp # Summation.cpp Undefined.cpp - Variable.cpp) + Variable.cpp + UnitGraph.cpp + GlobalUnitGraph.cpp) # Adds a library target called "Oasis" to be built from source files. add_library(Oasis ${Oasis_SOURCES}) diff --git a/src/GlobalUnitGraph.cpp b/src/GlobalUnitGraph.cpp new file mode 100644 index 00000000..af7e0864 --- /dev/null +++ b/src/GlobalUnitGraph.cpp @@ -0,0 +1,21 @@ +// +// Created by Jary Chen on 3/10/25. +// + +#include "../include/Oasis/GlobalUnitGraph.hpp" + +namespace Oasis { + +UnitGraph& getGlobalUnitGraph() { + static UnitGraph globalGraph; + static bool initialized = false; + if (!initialized) { + initialized = true; + + // Mapping Here + globalGraph.addConversion(Unit::Meter, Unit::Kilometer, 0.001); + } + return globalGraph; +} + +} // namespace Oasis diff --git a/src/Real.cpp b/src/Real.cpp index 8eff8f56..71703d09 100644 --- a/src/Real.cpp +++ b/src/Real.cpp @@ -13,8 +13,8 @@ namespace Oasis { -Real::Real(double value) - : value(value) +Real::Real(double value, Unit unit) + : value(value), unit(unit) { } @@ -28,11 +28,31 @@ auto Real::Equals(const Expression& other) const -> bool return other.Is() && value == dynamic_cast(other).value; } +auto Real::ConvertTo(Unit targetUnit) const -> Real +{ + if (unit == targetUnit) { + return *this; + } + + const auto& globalGraph = getGlobalUnitGraph(); + auto factorOpt = globalGraph.findConversionFactor(unit, targetUnit); + if (!factorOpt.has_value()) { + throw std::runtime_error("Real::ConvertTo: Conversion to unit not found"); + } + + return Real(value * factorOpt.value(), targetUnit); +} + auto Real::GetValue() const -> double { return value; } +auto Real::GetUnit() const -> Unit +{ + return unit; +} + auto Real::Integrate(const Expression& integrationVariable) const -> std::unique_ptr { if (auto variable = RecursiveCast(integrationVariable); variable != nullptr) { diff --git a/src/UnitGraph.cpp b/src/UnitGraph.cpp new file mode 100644 index 00000000..a58d0ed4 --- /dev/null +++ b/src/UnitGraph.cpp @@ -0,0 +1,73 @@ +// +// Created by Jary Chen on 3/4/25. +// + +#include "../include/Oasis/UnitGraph.hpp" + +namespace Oasis { + +UnitGraph::UnitGraph() : nextVertex_(0) {} + +int UnitGraph::getOrAddVertex(Unit unit) { + auto it = unitToVertex_.find(unit); + if (it != unitToVertex_.end()) + return it->second; + int vertex = nextVertex_; + unitToVertex_[unit] = vertex; + ++nextVertex_; + + // Check if the graph has enough vertices + if (boost::num_vertices(graph_) <= vertex) + boost::add_vertex(graph_); + return vertex; +} + +void UnitGraph::addConversion(Unit from, Unit to, double factor) { + //Build Adj Edges + int vFrom = getOrAddVertex(from); + int vTo = getOrAddVertex(to); + boost::add_edge(vFrom, vTo, EdgeProperties{factor}, graph_); + boost::add_edge(vTo, vFrom, EdgeProperties{1.0 / factor}, graph_); +} + +std::optional UnitGraph::findConversionFactor(Unit from, Unit to) const { + if (from == to) + return 1.0; + auto itFrom = unitToVertex_.find(from); + auto itTo = unitToVertex_.find(to); + if (itFrom == unitToVertex_.end() || itTo == unitToVertex_.end()) + return std::nullopt; + + int start = itFrom->second; + int target = itTo->second; + + //Standard BFS + std::queue queue; + std::unordered_map cumulative; + cumulative[start] = 1.0; + queue.push(start); + + while (!queue.empty()) { + int current = queue.front(); + queue.pop(); + + if (current == target) + return cumulative[current]; + + auto edges = boost::out_edges(current, graph_); + for (auto edgeIter = edges.first; edgeIter != edges.second; ++edgeIter) { + int neighbor = boost::target(*edgeIter, graph_); + double factor = graph_[*edgeIter].factor; + double newFactor = cumulative[current] * factor; + + //cycle detection + if (cumulative.find(neighbor) == cumulative.end()) { + cumulative[neighbor] = newFactor; + queue.push(neighbor); + } + } + } + return std::nullopt; +} + +} // namespace Oasis diff --git a/src/Variable.cpp b/src/Variable.cpp index b68981ab..49ae6a6c 100644 --- a/src/Variable.cpp +++ b/src/Variable.cpp @@ -13,8 +13,8 @@ namespace Oasis { -Variable::Variable(std::string name) - : name(std::move(name)) +Variable::Variable(std::string name, Unit unit) + : name(std::move(name)), unit(unit) { } @@ -28,6 +28,11 @@ auto Variable::GetName() const -> std::string return name; } +auto Variable::GetUnit() const -> Unit +{ + return unit; +} + auto Variable::Integrate(const Expression& integrationVariable) const -> std::unique_ptr { if (auto variable = RecursiveCast(integrationVariable); variable != nullptr) { diff --git a/tests/AddTests.cpp b/tests/AddTests.cpp index 9e805911..7f8e7404 100644 --- a/tests/AddTests.cpp +++ b/tests/AddTests.cpp @@ -11,8 +11,33 @@ #include "Oasis/RecursiveCast.hpp" #include "Oasis/Variable.hpp" +#include #include +TEST_CASE("Conversion: Meter to Kilometer", "[Real][UnitConversion]") { + Oasis::Real meterValue{1000.0, Oasis::Unit::Meter}; + auto kmValue = meterValue.ConvertTo(Oasis::Unit::Kilometer); + + REQUIRE(kmValue.GetValue() == Catch::Approx(1.0)); + REQUIRE(kmValue.GetUnit() == Oasis::Unit::Kilometer); +} + +TEST_CASE("Conversion: Kilometer to Meter", "[Real][UnitConversion]") { + Oasis::Real kmValue{2.0, Oasis::Unit::Kilometer}; + auto meterValue = kmValue.ConvertTo(Oasis::Unit::Meter); + + REQUIRE(meterValue.GetValue() == Catch::Approx(2000.0)); + REQUIRE(meterValue.GetUnit() == Oasis::Unit::Meter); +} + +TEST_CASE("Conversion: No Conversion Needed", "[Real][UnitConversion]") { + Oasis::Real value{123.45, Oasis::Unit::Meter}; + auto converted = value.ConvertTo(Oasis::Unit::Meter); + + REQUIRE(converted.GetValue() == Catch::Approx(123.45)); + REQUIRE(converted.GetUnit() == Oasis::Unit::Meter); +} + TEST_CASE("Addition", "[Add]") { Oasis::Add add {