Add median* condition

The median condition determines the median value from a configured group
of properties and checks that against a defined condition to determine
whether or not the callback is called.

*Note: When 2 properties are used with the median condition, the
property determined to be the max is used instead of the average of the
2 properties. This is for providing a "worst case" median value.

An example would be to create a group consisting of multiple ambient
sensors where the median value from these ambient sensors would be used
to shutdown the system if above a given temperature.

i.e.)

- name: median temps
  description: >
    'If this condition passes the median ambient temperature
    is too high(>= 45C). Shut the system down.'
  class: condition
  condition: median
  paths: ambient sensors
  properties: ambient temp
  callback: ambient log and shutdown
  op: '>='
  bound: 45000
  oneshot: true

Tested:
    A defined median condition is generated according to the
MedianCondition class
    The MedianCondition class produces a single median value from a
group of property values
    Median value used against the given operation to determine if
callback is called or not

Change-Id: Icd53e1a6e30a263b7706a935f040eea97dcc2414
Signed-off-by: Matthew Barth <msbarth@us.ibm.com>
diff --git a/src/median.hpp b/src/median.hpp
new file mode 100644
index 0000000..a3ffe67
--- /dev/null
+++ b/src/median.hpp
@@ -0,0 +1,123 @@
+#pragma once
+
+#include "callback.hpp"
+#include "data_types.hpp"
+
+#include <algorithm>
+#include <functional>
+
+namespace phosphor
+{
+namespace dbus
+{
+namespace monitoring
+{
+
+/** @class MedianCondition
+ *  @brief Determine a median from properties and apply a condition.
+ *
+ *  When invoked, a median class instance performs its condition
+ *  test against a median value that has been determined from a set
+ *  of configured properties.
+ *
+ *  Once the median value is determined, a C++ relational operator
+ *  is applied to it and a value provided by the configuration file,
+ *  which determines if the condition passes or not.
+ *
+ *  Where no property values configured are found to determine a median from,
+ *  the condition defaults to `true` and passes.
+ *
+ *  If the oneshot parameter is true, then this condition won't pass
+ *  again until it fails at least once.
+ */
+template <typename T>
+class MedianCondition : public IndexedConditional
+{
+  public:
+    MedianCondition() = delete;
+    MedianCondition(const MedianCondition&) = default;
+    MedianCondition(MedianCondition&&) = default;
+    MedianCondition& operator=(const MedianCondition&) = default;
+    MedianCondition& operator=(MedianCondition&&) = default;
+    ~MedianCondition() = default;
+
+    MedianCondition(const PropertyIndex& conditionIndex,
+                    const std::function<bool(T)>& _medianOp,
+                    bool oneshot = false) :
+        IndexedConditional(conditionIndex),
+        medianOp(_medianOp), oneshot(oneshot)
+    {
+    }
+
+    bool operator()() override
+    {
+        // Default the condition result to true
+        // if no property values are found to produce a median.
+        auto result = true;
+        std::vector<T> values;
+        for (const auto& item : index)
+        {
+            const auto& storage = std::get<storageIndex>(item.second);
+            // Don't count properties that don't exist.
+            if (std::get<valueIndex>(storage.get()).empty())
+            {
+                continue;
+            }
+            values.emplace_back(
+                any_ns::any_cast<T>(std::get<valueIndex>(storage.get())));
+        }
+
+        if (!values.empty())
+        {
+            auto median = values.front();
+            // Get the determined median value
+            if (values.size() == 2)
+            {
+                // For 2 values, use the highest instead of the average
+                // for a worst case median value
+                median = *std::max_element(values.begin(), values.end());
+            }
+            else if (values.size() > 2)
+            {
+                const auto oddIt = values.begin() + values.size() / 2;
+                std::nth_element(values.begin(), oddIt, values.end());
+                median = *oddIt;
+                // Determine median for even number of values
+                if (index.size() % 2 == 0)
+                {
+                    // Use average of middle 2 values for median
+                    const auto evenIt = values.begin() + values.size() / 2 - 1;
+                    std::nth_element(values.begin(), evenIt, values.end());
+                    median = (median + *evenIt) / 2;
+                }
+            }
+
+            // Now apply the condition to the median value.
+            result = medianOp(median);
+        }
+
+        // If this was a oneshot and the the condition has already
+        // passed, then don't let it pass again until the condition
+        // has gone back to false.
+        if (oneshot && result && lastResult)
+        {
+            return false;
+        }
+
+        lastResult = result;
+        return result;
+    }
+
+  private:
+    /** @brief The comparison to perform on the median value. */
+    std::function<bool(T)> medianOp;
+    /** @brief If the condition can be allowed to pass again
+               on subsequent checks that are also true. */
+    const bool oneshot;
+    /** @brief The result of the previous check. */
+    bool lastResult = false;
+};
+
+} // namespace monitoring
+} // namespace dbus
+} // namespace phosphor