From c0d24ce575825a2045a320ae02993ff29497b2ef Mon Sep 17 00:00:00 2001
From: Lukas Senionis <warliukz@gmail.com>
Date: Wed, 17 Jul 2024 19:06:47 +0300
Subject: [PATCH] feat: Use new FloatingPoint type (#62)

Co-authored-by: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
---
 .../displaydevice/detail/jsonserializer.h     |  1 +
 .../detail/jsonserializerdetails.h            | 58 ++++++++++++++++++-
 src/common/include/displaydevice/types.h      | 26 ++++++++-
 src/common/jsonserializer.cpp                 |  1 +
 src/common/types.cpp                          | 18 +++++-
 .../windows/detail/jsonserializer.h           |  1 -
 .../displaydevice/windows/settingsutils.h     |  2 +-
 .../include/displaydevice/windows/types.h     | 20 -------
 .../displaydevice/windows/winapilayer.h       |  2 +-
 .../windows/winapilayerinterface.h            |  2 +-
 src/windows/jsonserializer.cpp                |  1 -
 src/windows/settingsutils.cpp                 | 20 ++++++-
 src/windows/types.cpp                         | 18 ------
 src/windows/winapilayer.cpp                   |  5 +-
 src/windows/windisplaydevicegeneral.cpp       | 10 ++--
 .../include/fixtures/jsonconvertertest.h      |  5 +-
 tests/unit/general/test_comparison.cpp        | 22 +++++++
 tests/unit/general/test_json.cpp              | 26 +++++++++
 tests/unit/general/test_jsonconverter.cpp     | 14 ++---
 tests/unit/windows/test_comparison.cpp        |  6 --
 tests/unit/windows/test_settingsutils.cpp     | 20 ++++++-
 .../windows/test_windisplaydevicegeneral.cpp  | 10 ++--
 tests/unit/windows/utils/mockwinapilayer.h    |  2 +-
 23 files changed, 209 insertions(+), 81 deletions(-)

diff --git a/src/common/include/displaydevice/detail/jsonserializer.h b/src/common/include/displaydevice/detail/jsonserializer.h
index 23ee4f8..0db59f0 100644
--- a/src/common/include/displaydevice/detail/jsonserializer.h
+++ b/src/common/include/displaydevice/detail/jsonserializer.h
@@ -11,6 +11,7 @@ namespace display_device {
 
   // Structs
   DD_JSON_DECLARE_SERIALIZE_TYPE(Resolution)
+  DD_JSON_DECLARE_SERIALIZE_TYPE(Rational)
   DD_JSON_DECLARE_SERIALIZE_TYPE(Point)
   DD_JSON_DECLARE_SERIALIZE_TYPE(EnumeratedDevice::Info)
   DD_JSON_DECLARE_SERIALIZE_TYPE(EnumeratedDevice)
diff --git a/src/common/include/displaydevice/detail/jsonserializerdetails.h b/src/common/include/displaydevice/detail/jsonserializerdetails.h
index 7dadca2..c8e6763 100644
--- a/src/common/include/displaydevice/detail/jsonserializerdetails.h
+++ b/src/common/include/displaydevice/detail/jsonserializerdetails.h
@@ -43,9 +43,38 @@
     }
 
 namespace display_device {
+  /**
+   * @brief Holds information for serializing variants.
+   */
+  namespace detail {
+    template <class T>
+    struct JsonTypeName;
+
+    template <>
+    struct JsonTypeName<double> {
+      static constexpr std::string_view m_name { "double" };
+    };
+
+    template <>
+    struct JsonTypeName<Rational> {
+      static constexpr std::string_view m_name { "rational" };
+    };
+
+    template <class T, class... Ts>
+    bool
+    variantFromJson(const nlohmann::json &nlohmann_json_j, std::variant<Ts...> &value) {
+      if (nlohmann_json_j.at("type").get<std::string_view>() != JsonTypeName<T>::m_name) {
+        return false;
+      }
+
+      value = nlohmann_json_j.at("value").get<T>();
+      return true;
+    }
+  }  // namespace detail
+
   // A shared function for enums to find values in the map. Extracted here for UTs + coverage
   template <class T, class Predicate>
-  std::map<T, nlohmann::json>::const_iterator
+  typename std::map<T, nlohmann::json>::const_iterator
   findInEnumMap(const char *error_msg, Predicate predicate) {
     const auto &map { getEnumMap(T {}) };
     auto it { std::find_if(std::begin(map), std::end(map), predicate) };
@@ -58,7 +87,7 @@ namespace display_device {
 
 namespace nlohmann {
   // Specialization for optional types until they actually implement it.
-  template <typename T>
+  template <class T>
   struct adl_serializer<std::optional<T>> {
     static void
     to_json(json &nlohmann_json_j, const std::optional<T> &nlohmann_json_t) {
@@ -80,5 +109,30 @@ namespace nlohmann {
       }
     }
   };
+
+  // Specialization for variant type.
+  // See https://github.com/nlohmann/json/issues/1261#issuecomment-2048770747
+  template <typename... Ts>
+  struct adl_serializer<std::variant<Ts...>> {
+    static void
+    to_json(json &nlohmann_json_j, const std::variant<Ts...> &nlohmann_json_t) {
+      std::visit(
+        [&nlohmann_json_j]<class T>(const T &value) {
+          nlohmann_json_j["type"] = display_device::detail::JsonTypeName<std::decay_t<T>>::m_name;
+          nlohmann_json_j["value"] = value;
+        },
+        nlohmann_json_t);
+    }
+
+    static void
+    from_json(const json &nlohmann_json_j, std::variant<Ts...> &nlohmann_json_t) {
+      // Call variant_from_json for all types, only one will succeed
+      const bool found { (display_device::detail::variantFromJson<Ts>(nlohmann_json_j, nlohmann_json_t) || ...) };
+      if (!found) {
+        const std::string error { "Could not parse variant from type " + nlohmann_json_j.at("type").get<std::string>() + "!" };
+        throw std::runtime_error(error);
+      }
+    }
+  };
 }  // namespace nlohmann
 #endif
diff --git a/src/common/include/displaydevice/types.h b/src/common/include/displaydevice/types.h
index eb29fb3..850b0c4 100644
--- a/src/common/include/displaydevice/types.h
+++ b/src/common/include/displaydevice/types.h
@@ -3,6 +3,7 @@
 // system includes
 #include <optional>
 #include <string>
+#include <variant>
 #include <vector>
 
 namespace display_device {
@@ -42,6 +43,25 @@ namespace display_device {
     operator==(const Point &lhs, const Point &rhs);
   };
 
+  /**
+   * @brief Floating point stored in a "numerator/denominator" form.
+   */
+  struct Rational {
+    unsigned int m_numerator {};
+    unsigned int m_denominator {};
+
+    /**
+     * @brief Comparator for strict equality.
+     */
+    friend bool
+    operator==(const Rational &lhs, const Rational &rhs);
+  };
+
+  /**
+   * @brief Floating point type.
+   */
+  using FloatingPoint = std::variant<double, Rational>;
+
   /**
    * @brief Enumerated display device information.
    */
@@ -51,8 +71,8 @@ namespace display_device {
      */
     struct Info {
       Resolution m_resolution {}; /**< Resolution of an active device. */
-      double m_resolution_scale {}; /**< Resolution scaling of an active device. */
-      double m_refresh_rate {}; /**< Refresh rate of an active device. */
+      FloatingPoint m_resolution_scale {}; /**< Resolution scaling of an active device. */
+      FloatingPoint m_refresh_rate {}; /**< Refresh rate of an active device. */
       bool m_primary {}; /**< Indicates whether the device is a primary display. */
       Point m_origin_point {}; /**< A starting point of the display. */
       std::optional<HdrState> m_hdr_state {}; /**< HDR of an active device. */
@@ -101,7 +121,7 @@ namespace display_device {
     std::string m_device_id {}; /**< Device to perform configuration for (can be empty if primary device should be used). */
     DevicePreparation m_device_prep {}; /**< Instruction on how to prepare device. */
     std::optional<Resolution> m_resolution {}; /**< Resolution to configure. */
-    std::optional<double> m_refresh_rate {}; /**< Refresh rate to configure. */
+    std::optional<FloatingPoint> m_refresh_rate {}; /**< Refresh rate to configure. */
     std::optional<HdrState> m_hdr_state {}; /**< HDR state to configure (if supported by the display). */
 
     /**
diff --git a/src/common/jsonserializer.cpp b/src/common/jsonserializer.cpp
index 97b4302..424d2c5 100644
--- a/src/common/jsonserializer.cpp
+++ b/src/common/jsonserializer.cpp
@@ -16,6 +16,7 @@ namespace display_device {
 
   // Structs
   DD_JSON_DEFINE_SERIALIZE_STRUCT(Resolution, width, height)
+  DD_JSON_DEFINE_SERIALIZE_STRUCT(Rational, numerator, denominator)
   DD_JSON_DEFINE_SERIALIZE_STRUCT(Point, x, y)
   DD_JSON_DEFINE_SERIALIZE_STRUCT(EnumeratedDevice::Info, resolution, resolution_scale, refresh_rate, primary, origin_point, hdr_state)
   DD_JSON_DEFINE_SERIALIZE_STRUCT(EnumeratedDevice, device_id, display_name, friendly_name, info)
diff --git a/src/common/types.cpp b/src/common/types.cpp
index f6901c8..1ea065a 100644
--- a/src/common/types.cpp
+++ b/src/common/types.cpp
@@ -1,4 +1,4 @@
-// local includes
+// header include
 #include "displaydevice/types.h"
 
 namespace {
@@ -6,9 +6,25 @@ namespace {
   fuzzyCompare(const double lhs, const double rhs) {
     return std::abs(lhs - rhs) * 1000000000000. <= std::min(std::abs(lhs), std::abs(rhs));
   }
+
+  bool
+  fuzzyCompare(const display_device::FloatingPoint &lhs, const display_device::FloatingPoint &rhs) {
+    if (lhs.index() == rhs.index()) {
+      if (std::holds_alternative<double>(lhs)) {
+        return fuzzyCompare(std::get<double>(lhs), std::get<double>(rhs));
+      }
+      return lhs == rhs;
+    }
+    return false;
+  }
 }  // namespace
 
 namespace display_device {
+  bool
+  operator==(const Rational &lhs, const Rational &rhs) {
+    return lhs.m_numerator == rhs.m_numerator && lhs.m_denominator == rhs.m_denominator;
+  }
+
   bool
   operator==(const Point &lhs, const Point &rhs) {
     return lhs.m_x == rhs.m_x && lhs.m_y == rhs.m_y;
diff --git a/src/windows/include/displaydevice/windows/detail/jsonserializer.h b/src/windows/include/displaydevice/windows/detail/jsonserializer.h
index d1574ef..407f92e 100644
--- a/src/windows/include/displaydevice/windows/detail/jsonserializer.h
+++ b/src/windows/include/displaydevice/windows/detail/jsonserializer.h
@@ -6,7 +6,6 @@
 #ifdef DD_JSON_DETAIL
 namespace display_device {
   // Structs
-  DD_JSON_DECLARE_SERIALIZE_TYPE(Rational)
   DD_JSON_DECLARE_SERIALIZE_TYPE(DisplayMode)
   DD_JSON_DECLARE_SERIALIZE_TYPE(SingleDisplayConfigState::Initial)
   DD_JSON_DECLARE_SERIALIZE_TYPE(SingleDisplayConfigState::Modified)
diff --git a/src/windows/include/displaydevice/windows/settingsutils.h b/src/windows/include/displaydevice/windows/settingsutils.h
index 53e017e..8fef529 100644
--- a/src/windows/include/displaydevice/windows/settingsutils.h
+++ b/src/windows/include/displaydevice/windows/settingsutils.h
@@ -112,7 +112,7 @@ namespace display_device::win_utils {
    */
   DeviceDisplayModeMap
   computeNewDisplayModes(const std::optional<Resolution> &resolution,
-    const std::optional<double> &refresh_rate,
+    const std::optional<FloatingPoint> &refresh_rate,
     bool configuring_primary_devices,
     const std::string &device_to_configure,
     const std::set<std::string> &additional_devices_to_configure,
diff --git a/src/windows/include/displaydevice/windows/types.h b/src/windows/include/displaydevice/windows/types.h
index 477360e..ee6ea78 100644
--- a/src/windows/include/displaydevice/windows/types.h
+++ b/src/windows/include/displaydevice/windows/types.h
@@ -77,26 +77,6 @@ namespace display_device {
    */
   using ActiveTopology = std::vector<std::vector<std::string>>;
 
-  /**
-   * @brief Floating point stored in a "numerator/denominator" form.
-   */
-  struct Rational {
-    unsigned int m_numerator {};
-    unsigned int m_denominator {};
-
-    /**
-     * @brief Contruct rational struct from double precission floating point.
-     */
-    static Rational
-    fromFloatingPoint(double value);
-
-    /**
-     * @brief Comparator for strict equality.
-     */
-    friend bool
-    operator==(const Rational &lhs, const Rational &rhs);
-  };
-
   /**
    * @brief Display's mode (resolution + refresh rate).
    */
diff --git a/src/windows/include/displaydevice/windows/winapilayer.h b/src/windows/include/displaydevice/windows/winapilayer.h
index 2b3e071..1cfa626 100644
--- a/src/windows/include/displaydevice/windows/winapilayer.h
+++ b/src/windows/include/displaydevice/windows/winapilayer.h
@@ -46,7 +46,7 @@ namespace display_device {
     setHdrState(const DISPLAYCONFIG_PATH_INFO &path, HdrState state) override;
 
     /** For details @see WinApiLayerInterface::getDisplayScale */
-    [[nodiscard]] std::optional<double>
+    [[nodiscard]] std::optional<Rational>
     getDisplayScale(const std::string &display_name, const DISPLAYCONFIG_SOURCE_MODE &source_mode) const override;
   };
 }  // namespace display_device
diff --git a/src/windows/include/displaydevice/windows/winapilayerinterface.h b/src/windows/include/displaydevice/windows/winapilayerinterface.h
index 94d6aad..46acbb0 100644
--- a/src/windows/include/displaydevice/windows/winapilayerinterface.h
+++ b/src/windows/include/displaydevice/windows/winapilayerinterface.h
@@ -212,7 +212,7 @@ namespace display_device {
      * const auto scale = iface->getDisplayScale(iface->getDisplayName(path), source_mode);
      * ```
      */
-    [[nodiscard]] virtual std::optional<double>
+    [[nodiscard]] virtual std::optional<Rational>
     getDisplayScale(const std::string &display_name, const DISPLAYCONFIG_SOURCE_MODE &source_mode) const = 0;
   };
 }  // namespace display_device
diff --git a/src/windows/jsonserializer.cpp b/src/windows/jsonserializer.cpp
index ee9c9e3..019eaa8 100644
--- a/src/windows/jsonserializer.cpp
+++ b/src/windows/jsonserializer.cpp
@@ -7,7 +7,6 @@
 
 namespace display_device {
   // Structs
-  DD_JSON_DEFINE_SERIALIZE_STRUCT(Rational, numerator, denominator)
   DD_JSON_DEFINE_SERIALIZE_STRUCT(DisplayMode, resolution, refresh_rate)
   DD_JSON_DEFINE_SERIALIZE_STRUCT(SingleDisplayConfigState::Initial, topology, primary_devices)
   DD_JSON_DEFINE_SERIALIZE_STRUCT(SingleDisplayConfigState::Modified, topology, original_modes, original_hdr_states, original_primary_device)
diff --git a/src/windows/settingsutils.cpp b/src/windows/settingsutils.cpp
index af089ca..cbd6c88 100644
--- a/src/windows/settingsutils.cpp
+++ b/src/windows/settingsutils.cpp
@@ -3,6 +3,7 @@
 
 // system includes
 #include <algorithm>
+#include <cmath>
 #include <thread>
 
 // local includes
@@ -264,7 +265,7 @@ namespace display_device::win_utils {
   }
 
   DeviceDisplayModeMap
-  computeNewDisplayModes(const std::optional<Resolution> &resolution, const std::optional<double> &refresh_rate, const bool configuring_primary_devices, const std::string &device_to_configure, const std::set<std::string> &additional_devices_to_configure, const DeviceDisplayModeMap &original_modes) {
+  computeNewDisplayModes(const std::optional<Resolution> &resolution, const std::optional<FloatingPoint> &refresh_rate, const bool configuring_primary_devices, const std::string &device_to_configure, const std::set<std::string> &additional_devices_to_configure, const DeviceDisplayModeMap &original_modes) {
     DeviceDisplayModeMap new_modes { original_modes };
 
     if (resolution) {
@@ -278,19 +279,32 @@ namespace display_device::win_utils {
     }
 
     if (refresh_rate) {
+      const auto from_floating_point { [](const FloatingPoint &value) {
+        if (const auto *rational_value { std::get_if<Rational>(&value) }; rational_value) {
+          return *rational_value;
+        }
+
+        // It's hard to deal with floating values, so we just multiply it
+        // to keep 4 decimal places (if any) and let Windows deal with it!
+        // Genius idea if I'm being honest.
+        constexpr auto multiplier { static_cast<unsigned int>(std::pow(10, 4)) };
+        const double transformed_value { std::round(std::get<double>(value) * multiplier) };
+        return Rational { static_cast<unsigned int>(transformed_value), multiplier };
+      } };
+
       if (configuring_primary_devices) {
         // No device has been specified, so if they're all are primary devices
         // we need to apply the refresh rate change to all duplicates.
         const auto devices { joinConfigurableDevices(device_to_configure, additional_devices_to_configure) };
         for (const auto &device_id : devices) {
-          new_modes[device_id].m_refresh_rate = Rational::fromFloatingPoint(*refresh_rate);
+          new_modes[device_id].m_refresh_rate = from_floating_point(*refresh_rate);
         }
       }
       else {
         // Even if we have duplicate devices, their refresh rate may differ
         // and since the device was specified, let's apply the refresh
         // rate only to the specified device.
-        new_modes[device_to_configure].m_refresh_rate = Rational::fromFloatingPoint(*refresh_rate);
+        new_modes[device_to_configure].m_refresh_rate = from_floating_point(*refresh_rate);
       }
     }
 
diff --git a/src/windows/types.cpp b/src/windows/types.cpp
index 61b83c0..7ed9808 100644
--- a/src/windows/types.cpp
+++ b/src/windows/types.cpp
@@ -1,25 +1,7 @@
 // header include
 #include "displaydevice/windows/types.h"
 
-// system includes
-#include <cmath>
-
 namespace display_device {
-  Rational
-  Rational::fromFloatingPoint(const double value) {
-    // It's hard to deal with floating values, so we just multiply it
-    // to keep 4 decimal places (if any) and let Windows deal with it!
-    // Genius idea if I'm being honest.
-    constexpr auto multiplier { static_cast<unsigned int>(std::pow(10, 4)) };
-    const double transformed_value { std::round(value * multiplier) };
-    return Rational { static_cast<unsigned int>(transformed_value), multiplier };
-  }
-
-  bool
-  operator==(const Rational &lhs, const Rational &rhs) {
-    return lhs.m_numerator == rhs.m_numerator && lhs.m_denominator == rhs.m_denominator;
-  }
-
   bool
   operator==(const DisplayMode &lhs, const DisplayMode &rhs) {
     return lhs.m_refresh_rate == rhs.m_refresh_rate && lhs.m_resolution == rhs.m_resolution;
diff --git a/src/windows/winapilayer.cpp b/src/windows/winapilayer.cpp
index 3d5b2f7..fcadf79 100644
--- a/src/windows/winapilayer.cpp
+++ b/src/windows/winapilayer.cpp
@@ -7,6 +7,7 @@
 #include <boost/uuid/name_generator_sha1.hpp>
 #include <boost/uuid/uuid.hpp>
 #include <boost/uuid/uuid_io.hpp>
+#include <cmath>
 #include <cstdint>
 #include <iomanip>
 
@@ -575,7 +576,7 @@ namespace display_device {
     return true;
   }
 
-  std::optional<double>
+  std::optional<Rational>
   WinApiLayer::getDisplayScale(const std::string &display_name, const DISPLAYCONFIG_SOURCE_MODE &source_mode) const {
     // Note: implementation based on https://stackoverflow.com/a/74046173
     struct EnumData {
@@ -617,6 +618,6 @@ namespace display_device {
     }
 
     const auto width { static_cast<double>(*enum_data.m_width) / static_cast<double>(source_mode.width) };
-    return static_cast<double>(GetDpiForSystem()) / 96. / width;
+    return Rational { static_cast<unsigned int>(std::round((static_cast<double>(GetDpiForSystem()) / 96. / width) * 100)), 100 };
   }
 }  // namespace display_device
diff --git a/src/windows/windisplaydevicegeneral.cpp b/src/windows/windisplaydevicegeneral.cpp
index 06c2a97..8b28c70 100644
--- a/src/windows/windisplaydevicegeneral.cpp
+++ b/src/windows/windisplaydevicegeneral.cpp
@@ -59,15 +59,15 @@ namespace display_device {
         }
 
         const auto display_name { m_w_api->getDisplayName(best_path) };
-        const double refresh_rate { best_path.targetInfo.refreshRate.Denominator > 0 ?
-                                      static_cast<double>(best_path.targetInfo.refreshRate.Numerator) / static_cast<double>(best_path.targetInfo.refreshRate.Denominator) :
-                                      0. };
+        const Rational refresh_rate { best_path.targetInfo.refreshRate.Denominator > 0 ?
+                                        Rational { best_path.targetInfo.refreshRate.Numerator, best_path.targetInfo.refreshRate.Denominator } :
+                                        Rational { 0, 1 } };
         const EnumeratedDevice::Info info {
           { source_mode->width, source_mode->height },
-          m_w_api->getDisplayScale(display_name, *source_mode).value_or(0.),
+          m_w_api->getDisplayScale(display_name, *source_mode).value_or(Rational { 0, 1 }),
           refresh_rate,
           win_utils::isPrimary(*source_mode),
-          { source_mode->position.x, source_mode->position.y },
+          { static_cast<int>(source_mode->position.x), static_cast<int>(source_mode->position.y) },
           m_w_api->getHdrState(best_path)
         };
 
diff --git a/tests/fixtures/include/fixtures/jsonconvertertest.h b/tests/fixtures/include/fixtures/jsonconvertertest.h
index 809a89a..e528781 100644
--- a/tests/fixtures/include/fixtures/jsonconvertertest.h
+++ b/tests/fixtures/include/fixtures/jsonconvertertest.h
@@ -14,8 +14,11 @@ class JsonConverterTest: public BaseTest {
     EXPECT_TRUE(success);
     EXPECT_EQ(json_string, expected_string);
 
+    std::string error_message {};
     T defaulted_input {};
-    EXPECT_TRUE(display_device::fromJson(json_string, defaulted_input));
+    if (!display_device::fromJson(json_string, defaulted_input, &error_message)) {
+      GTEST_FAIL() << error_message;
+    }
     EXPECT_EQ(input, defaulted_input);
   }
 };
diff --git a/tests/unit/general/test_comparison.cpp b/tests/unit/general/test_comparison.cpp
index 7a8a031..62d6ea7 100644
--- a/tests/unit/general/test_comparison.cpp
+++ b/tests/unit/general/test_comparison.cpp
@@ -13,6 +13,12 @@ TEST_S(Point) {
   EXPECT_NE(display_device::Point({ 1, 1 }), display_device::Point({ 1, 0 }));
 }
 
+TEST_S(Rational) {
+  EXPECT_EQ(display_device::Rational({ 1, 1 }), display_device::Rational({ 1, 1 }));
+  EXPECT_NE(display_device::Rational({ 1, 1 }), display_device::Rational({ 0, 1 }));
+  EXPECT_NE(display_device::Rational({ 1, 1 }), display_device::Rational({ 1, 0 }));
+}
+
 TEST_S(Resolution) {
   EXPECT_EQ(display_device::Resolution({ 1, 1 }), display_device::Resolution({ 1, 1 }));
   EXPECT_NE(display_device::Resolution({ 1, 1 }), display_device::Resolution({ 0, 1 }));
@@ -20,8 +26,19 @@ TEST_S(Resolution) {
 }
 
 TEST_S(EnumeratedDevice, Info) {
+  using Rat = display_device::Rational;
   EXPECT_EQ(display_device::EnumeratedDevice::Info({ { 1, 1 }, 1., 1., true, { 1, 1 }, std::nullopt }),
     display_device::EnumeratedDevice::Info({ { 1, 1 }, 1., 1., true, { 1, 1 }, std::nullopt }));
+  EXPECT_EQ(display_device::EnumeratedDevice::Info({ { 1, 1 }, Rat { 1, 1 }, Rat { 1, 1 }, true, { 1, 1 }, std::nullopt }),
+    display_device::EnumeratedDevice::Info({ { 1, 1 }, Rat { 1, 1 }, Rat { 1, 1 }, true, { 1, 1 }, std::nullopt }));
+  EXPECT_EQ(display_device::EnumeratedDevice::Info({ { 1, 1 }, 1., Rat { 1, 1 }, true, { 1, 1 }, std::nullopt }),
+    display_device::EnumeratedDevice::Info({ { 1, 1 }, 1., Rat { 1, 1 }, true, { 1, 1 }, std::nullopt }));
+  EXPECT_EQ(display_device::EnumeratedDevice::Info({ { 1, 1 }, Rat { 1, 1 }, 1., true, { 1, 1 }, std::nullopt }),
+    display_device::EnumeratedDevice::Info({ { 1, 1 }, Rat { 1, 1 }, 1., true, { 1, 1 }, std::nullopt }));
+  EXPECT_NE(display_device::EnumeratedDevice::Info({ { 1, 1 }, 1., Rat { 1, 1 }, true, { 1, 1 }, std::nullopt }),
+    display_device::EnumeratedDevice::Info({ { 1, 1 }, Rat { 1, 1 }, Rat { 1, 1 }, true, { 1, 1 }, std::nullopt }));
+  EXPECT_NE(display_device::EnumeratedDevice::Info({ { 1, 1 }, Rat { 1, 1 }, 1., true, { 1, 1 }, std::nullopt }),
+    display_device::EnumeratedDevice::Info({ { 1, 1 }, Rat { 1, 1 }, Rat { 1, 1 }, true, { 1, 1 }, std::nullopt }));
   EXPECT_NE(display_device::EnumeratedDevice::Info({ { 1, 1 }, 1., 1., true, { 1, 1 }, std::nullopt }),
     display_device::EnumeratedDevice::Info({ { 1, 0 }, 1., 1., true, { 1, 1 }, std::nullopt }));
   EXPECT_NE(display_device::EnumeratedDevice::Info({ { 1, 1 }, 1., 1., true, { 1, 1 }, std::nullopt }),
@@ -51,8 +68,13 @@ TEST_S(EnumeratedDevice) {
 
 TEST_S(SingleDisplayConfiguration) {
   using DevicePrep = display_device::SingleDisplayConfiguration::DevicePreparation;
+  using Rat = display_device::Rational;
   EXPECT_EQ(display_device::SingleDisplayConfiguration({ "1", DevicePrep::EnsureActive, { { 1, 1 } }, 1., display_device::HdrState::Disabled }),
     display_device::SingleDisplayConfiguration({ "1", DevicePrep::EnsureActive, { { 1, 1 } }, 1., display_device::HdrState::Disabled }));
+  EXPECT_EQ(display_device::SingleDisplayConfiguration({ "1", DevicePrep::EnsureActive, { { 1, 1 } }, Rat { 1, 1 }, display_device::HdrState::Disabled }),
+    display_device::SingleDisplayConfiguration({ "1", DevicePrep::EnsureActive, { { 1, 1 } }, Rat { 1, 1 }, display_device::HdrState::Disabled }));
+  EXPECT_NE(display_device::SingleDisplayConfiguration({ "1", DevicePrep::EnsureActive, { { 1, 1 } }, 1., display_device::HdrState::Disabled }),
+    display_device::SingleDisplayConfiguration({ "1", DevicePrep::EnsureActive, { { 1, 1 } }, Rat { 1, 1 }, display_device::HdrState::Disabled }));
   EXPECT_NE(display_device::SingleDisplayConfiguration({ "1", DevicePrep::EnsureActive, { { 1, 1 } }, 1., display_device::HdrState::Disabled }),
     display_device::SingleDisplayConfiguration({ "0", DevicePrep::EnsureActive, { { 1, 1 } }, 1., display_device::HdrState::Disabled }));
   EXPECT_NE(display_device::SingleDisplayConfiguration({ "1", DevicePrep::EnsureActive, { { 1, 1 } }, 1., display_device::HdrState::Disabled }),
diff --git a/tests/unit/general/test_json.cpp b/tests/unit/general/test_json.cpp
index 3d90fa6..ce4706d 100644
--- a/tests/unit/general/test_json.cpp
+++ b/tests/unit/general/test_json.cpp
@@ -25,6 +25,8 @@ namespace display_device {
     Nested m_b {};
   };
 
+  using TestVariant = std::variant<double, Rational>;
+
   bool
   operator==(const TestStruct::Nested &lhs, const TestStruct::Nested &rhs) {
     return lhs.m_c == rhs.m_c;
@@ -41,6 +43,7 @@ namespace display_device {
 
   DD_JSON_DEFINE_CONVERTER(TestEnum)
   DD_JSON_DEFINE_CONVERTER(TestStruct)
+  DD_JSON_DEFINE_CONVERTER(TestVariant)
 }  // namespace display_device
 
 namespace {
@@ -166,3 +169,26 @@ TEST_S(FromJson, Enum, MissingMappingValue) {
   EXPECT_FALSE(display_device::fromJson(R"("OtherValue")", value, &error_message));
   EXPECT_EQ(error_message, "TestEnum is missing enum mapping!");
 }
+
+TEST_S(ToJson, TestVariant) {
+  EXPECT_EQ(toJson(display_device::TestVariant { 123. }, std::nullopt, nullptr), R"({"type":"double","value":123.0})");
+  EXPECT_EQ(toJson(display_device::TestVariant { display_device::Rational { 1, 2 } }, std::nullopt, nullptr), R"({"type":"rational","value":{"denominator":2,"numerator":1}})");
+}
+
+TEST_S(FromJson, TestVariant) {
+  display_device::TestVariant variant {};
+
+  EXPECT_TRUE(display_device::fromJson(R"({"type":"double","value":123.0})", variant, nullptr));
+  EXPECT_EQ(std::get<double>(variant), 123.0);  // Relying on GTest to properly compare floats
+
+  EXPECT_TRUE(display_device::fromJson(R"({"type":"rational","value":{"denominator":2,"numerator":1}})", variant, nullptr));
+  EXPECT_EQ(std::get<display_device::Rational>(variant), display_device::Rational({ 1, 2 }));
+}
+
+TEST_S(FromJson, TestVariant, UnknownVariantType) {
+  display_device::TestVariant variant {};
+  std::string error_message {};
+
+  EXPECT_FALSE(display_device::fromJson(R"({"type":"SomeUnknownType","value":123.0})", variant, &error_message));
+  EXPECT_EQ(error_message, "Could not parse variant from type SomeUnknownType!");
+}
diff --git a/tests/unit/general/test_jsonconverter.cpp b/tests/unit/general/test_jsonconverter.cpp
index 355ce50..d1a975c 100644
--- a/tests/unit/general/test_jsonconverter.cpp
+++ b/tests/unit/general/test_jsonconverter.cpp
@@ -13,7 +13,7 @@ TEST_F_S(EnumeratedDeviceList) {
     "FU_NAME_3",
     display_device::EnumeratedDevice::Info {
       { 1920, 1080 },
-      1.75,
+      display_device::Rational { 175, 100 },
       119.9554,
       false,
       { 1, 2 },
@@ -26,7 +26,7 @@ TEST_F_S(EnumeratedDeviceList) {
     display_device::EnumeratedDevice::Info {
       { 1920, 1080 },
       1.75,
-      119.9554,
+      display_device::Rational { 1199554, 10000 },
       true,
       { 0, 0 },
       display_device::HdrState::Disabled }
@@ -35,20 +35,20 @@ TEST_F_S(EnumeratedDeviceList) {
 
   executeTestCase(display_device::EnumeratedDeviceList {}, R"([])");
   executeTestCase(display_device::EnumeratedDeviceList { item_1, item_2, item_3 },
-    R"([{"device_id":"ID_1","display_name":"NAME_2","friendly_name":"FU_NAME_3","info":{"hdr_state":"Enabled","origin_point":{"x":1,"y":2},"primary":false,"refresh_rate":119.9554,"resolution":{"height":1080,"width":1920},"resolution_scale":1.75}},)"
-    R"({"device_id":"ID_2","display_name":"NAME_2","friendly_name":"FU_NAME_2","info":{"hdr_state":"Disabled","origin_point":{"x":0,"y":0},"primary":true,"refresh_rate":119.9554,"resolution":{"height":1080,"width":1920},"resolution_scale":1.75}},)"
+    R"([{"device_id":"ID_1","display_name":"NAME_2","friendly_name":"FU_NAME_3","info":{"hdr_state":"Enabled","origin_point":{"x":1,"y":2},"primary":false,"refresh_rate":{"type":"double","value":119.9554},"resolution":{"height":1080,"width":1920},"resolution_scale":{"type":"rational","value":{"denominator":100,"numerator":175}}}},)"
+    R"({"device_id":"ID_2","display_name":"NAME_2","friendly_name":"FU_NAME_2","info":{"hdr_state":"Disabled","origin_point":{"x":0,"y":0},"primary":true,"refresh_rate":{"type":"rational","value":{"denominator":10000,"numerator":1199554}},"resolution":{"height":1080,"width":1920},"resolution_scale":{"type":"double","value":1.75}}},)"
     R"({"device_id":"","display_name":"","friendly_name":"","info":null}])");
 }
 
 TEST_F_S(SingleDisplayConfiguration) {
   display_device::SingleDisplayConfiguration config_1 { "ID_1", display_device::SingleDisplayConfiguration::DevicePreparation::VerifyOnly, { { 156, 123 } }, 85., display_device::HdrState::Enabled };
-  display_device::SingleDisplayConfiguration config_2 { "ID_2", display_device::SingleDisplayConfiguration::DevicePreparation::EnsureActive, std::nullopt, 85., display_device::HdrState::Disabled };
+  display_device::SingleDisplayConfiguration config_2 { "ID_2", display_device::SingleDisplayConfiguration::DevicePreparation::EnsureActive, std::nullopt, display_device::Rational { 85, 1 }, display_device::HdrState::Disabled };
   display_device::SingleDisplayConfiguration config_3 { "ID_3", display_device::SingleDisplayConfiguration::DevicePreparation::EnsureOnlyDisplay, { { 156, 123 } }, std::nullopt, std::nullopt };
   display_device::SingleDisplayConfiguration config_4 { "ID_4", display_device::SingleDisplayConfiguration::DevicePreparation::EnsurePrimary, std::nullopt, std::nullopt, std::nullopt };
 
   executeTestCase(display_device::SingleDisplayConfiguration {}, R"({"device_id":"","device_prep":"VerifyOnly","hdr_state":null,"refresh_rate":null,"resolution":null})");
-  executeTestCase(config_1, R"({"device_id":"ID_1","device_prep":"VerifyOnly","hdr_state":"Enabled","refresh_rate":85.0,"resolution":{"height":123,"width":156}})");
-  executeTestCase(config_2, R"({"device_id":"ID_2","device_prep":"EnsureActive","hdr_state":"Disabled","refresh_rate":85.0,"resolution":null})");
+  executeTestCase(config_1, R"({"device_id":"ID_1","device_prep":"VerifyOnly","hdr_state":"Enabled","refresh_rate":{"type":"double","value":85.0},"resolution":{"height":123,"width":156}})");
+  executeTestCase(config_2, R"({"device_id":"ID_2","device_prep":"EnsureActive","hdr_state":"Disabled","refresh_rate":{"type":"rational","value":{"denominator":1,"numerator":85}},"resolution":null})");
   executeTestCase(config_3, R"({"device_id":"ID_3","device_prep":"EnsureOnlyDisplay","hdr_state":null,"refresh_rate":null,"resolution":{"height":123,"width":156}})");
   executeTestCase(config_4, R"({"device_id":"ID_4","device_prep":"EnsurePrimary","hdr_state":null,"refresh_rate":null,"resolution":null})");
 }
diff --git a/tests/unit/windows/test_comparison.cpp b/tests/unit/windows/test_comparison.cpp
index cd87154..0cdea8f 100644
--- a/tests/unit/windows/test_comparison.cpp
+++ b/tests/unit/windows/test_comparison.cpp
@@ -7,12 +7,6 @@ namespace {
 #define TEST_S(...) DD_MAKE_TEST(TEST, TypeComparison, __VA_ARGS__)
 }  // namespace
 
-TEST_S(Rational) {
-  EXPECT_EQ(display_device::Rational({ 1, 1 }), display_device::Rational({ 1, 1 }));
-  EXPECT_NE(display_device::Rational({ 1, 1 }), display_device::Rational({ 0, 1 }));
-  EXPECT_NE(display_device::Rational({ 1, 1 }), display_device::Rational({ 1, 0 }));
-}
-
 TEST_S(DisplayMode) {
   EXPECT_EQ(display_device::DisplayMode({ 1, 1 }, { 1, 1 }), display_device::DisplayMode({ 1, 1 }, { 1, 1 }));
   EXPECT_NE(display_device::DisplayMode({ 1, 1 }, { 1, 1 }), display_device::DisplayMode({ 1, 0 }, { 1, 1 }));
diff --git a/tests/unit/windows/test_settingsutils.cpp b/tests/unit/windows/test_settingsutils.cpp
index c170a8f..83966de 100644
--- a/tests/unit/windows/test_settingsutils.cpp
+++ b/tests/unit/windows/test_settingsutils.cpp
@@ -91,7 +91,7 @@ TEST_F_S_MOCKED(ComputeNewTopology, EnsurePrimary) {
     (display_device::ActiveTopology { { "DeviceId3" }, { "DeviceId4" } }));
 }
 
-TEST_F_S_MOCKED(ComputeNewDisplayModes, PrimaryDevices) {
+TEST_F_S_MOCKED(ComputeNewDisplayModes, PrimaryDevices, DoubleFloatType) {
   auto expected_modes { DEFAULT_CURRENT_MODES };
   expected_modes["DeviceId1"] = { { 1920, 1080 }, { 1200000, 10000 } };
   expected_modes["DeviceId2"] = { { 1920, 1080 }, { 1200000, 10000 } };
@@ -99,7 +99,7 @@ TEST_F_S_MOCKED(ComputeNewDisplayModes, PrimaryDevices) {
   EXPECT_EQ(display_device::win_utils::computeNewDisplayModes({ { 1920, 1080 } }, { 120. }, true, "DeviceId1", { "DeviceId2" }, DEFAULT_CURRENT_MODES), expected_modes);
 }
 
-TEST_F_S_MOCKED(ComputeNewDisplayModes, NonPrimaryDevices) {
+TEST_F_S_MOCKED(ComputeNewDisplayModes, NonPrimaryDevices, DoubleFloatType) {
   auto expected_modes { DEFAULT_CURRENT_MODES };
   expected_modes["DeviceId1"] = { { 1920, 1080 }, { 1200000, 10000 } };
   expected_modes["DeviceId2"] = { { 1920, 1080 }, expected_modes["DeviceId2"].m_refresh_rate };
@@ -107,6 +107,22 @@ TEST_F_S_MOCKED(ComputeNewDisplayModes, NonPrimaryDevices) {
   EXPECT_EQ(display_device::win_utils::computeNewDisplayModes({ { 1920, 1080 } }, { 120. }, false, "DeviceId1", { "DeviceId2" }, DEFAULT_CURRENT_MODES), expected_modes);
 }
 
+TEST_F_S_MOCKED(ComputeNewDisplayModes, PrimaryDevices, RationalFloatType) {
+  auto expected_modes { DEFAULT_CURRENT_MODES };
+  expected_modes["DeviceId1"] = { { 1920, 1080 }, { 120, 1 } };
+  expected_modes["DeviceId2"] = { { 1920, 1080 }, { 120, 1 } };
+
+  EXPECT_EQ(display_device::win_utils::computeNewDisplayModes({ { 1920, 1080 } }, { display_device::Rational { 120, 1 } }, true, "DeviceId1", { "DeviceId2" }, DEFAULT_CURRENT_MODES), expected_modes);
+}
+
+TEST_F_S_MOCKED(ComputeNewDisplayModes, NonPrimaryDevices, RationalFloatType) {
+  auto expected_modes { DEFAULT_CURRENT_MODES };
+  expected_modes["DeviceId1"] = { { 1920, 1080 }, { 120, 1 } };
+  expected_modes["DeviceId2"] = { { 1920, 1080 }, expected_modes["DeviceId2"].m_refresh_rate };
+
+  EXPECT_EQ(display_device::win_utils::computeNewDisplayModes({ { 1920, 1080 } }, { display_device::Rational { 120, 1 } }, false, "DeviceId1", { "DeviceId2" }, DEFAULT_CURRENT_MODES), expected_modes);
+}
+
 TEST_F_S_MOCKED(ComputeNewHdrStates, PrimaryDevices) {
   auto expected_states { DEFAULT_CURRENT_HDR_STATES };
   expected_states["DeviceId1"] = display_device::HdrState::Enabled;
diff --git a/tests/unit/windows/test_windisplaydevicegeneral.cpp b/tests/unit/windows/test_windisplaydevicegeneral.cpp
index 54278d3..20bec15 100644
--- a/tests/unit/windows/test_windisplaydevicegeneral.cpp
+++ b/tests/unit/windows/test_windisplaydevicegeneral.cpp
@@ -160,7 +160,7 @@ TEST_F_S_MOCKED(EnumAvailableDevices) {
     .RetiresOnSaturation();
   EXPECT_CALL(*m_layer, getDisplayScale(_, _))
     .Times(1)
-    .WillOnce(Return(1.75))
+    .WillOnce(Return(display_device::Rational { 175, 100 }))
     .RetiresOnSaturation();
   EXPECT_CALL(*m_layer, getHdrState(_))
     .Times(1)
@@ -178,8 +178,8 @@ TEST_F_S_MOCKED(EnumAvailableDevices) {
       "FriendlyName1",
       display_device::EnumeratedDevice::Info {
         { 1920, 1080 },
-        0.,
-        0.,
+        display_device::Rational { 0, 1 },
+        display_device::Rational { 0, 1 },
         true,
         { 0, 0 },
         std::nullopt } },
@@ -188,8 +188,8 @@ TEST_F_S_MOCKED(EnumAvailableDevices) {
       "FriendlyName2",
       display_device::EnumeratedDevice::Info {
         { 1920, 2160 },
-        1.75,
-        119.995,
+        display_device::Rational { 175, 100 },
+        display_device::Rational { 119995, 1000 },
         false,
         { 1921, 0 },
         display_device::HdrState::Enabled } },
diff --git a/tests/unit/windows/utils/mockwinapilayer.h b/tests/unit/windows/utils/mockwinapilayer.h
index b28f7a3..979861a 100644
--- a/tests/unit/windows/utils/mockwinapilayer.h
+++ b/tests/unit/windows/utils/mockwinapilayer.h
@@ -18,7 +18,7 @@ namespace display_device {
     MOCK_METHOD(LONG, setDisplayConfig, (std::vector<DISPLAYCONFIG_PATH_INFO>, std::vector<DISPLAYCONFIG_MODE_INFO>, UINT32), (override));
     MOCK_METHOD(std::optional<HdrState>, getHdrState, (const DISPLAYCONFIG_PATH_INFO &), (const, override));
     MOCK_METHOD(bool, setHdrState, (const DISPLAYCONFIG_PATH_INFO &, HdrState), (override));
-    MOCK_METHOD(std::optional<double>, getDisplayScale, (const std::string &, const DISPLAYCONFIG_SOURCE_MODE &), (const, override));
+    MOCK_METHOD(std::optional<Rational>, getDisplayScale, (const std::string &, const DISPLAYCONFIG_SOURCE_MODE &), (const, override));
   };
 }  // namespace display_device