Allow fuzzy string comparisons in $filter expr

Filter allows comparing certain strings as numeric greater than or less
than operators.  The most obvious example of this is something like

\$filter=Created gt <timestamp>

Because internally timestamps are treated as strings, this requires
including and parsing out the timestamps again, which we have utilities
for.

In addition, "fuzzy" string comparisons, like

GPU_2 gt GPU_1

Should also be supported.

Tested: Unit tests pass

Change-Id: I39fc543921ed8cc93664d9cf297dad8ee902b68f
Signed-off-by: Ed Tanous <ed@tanous.net>
diff --git a/redfish-core/src/filter_expr_executor.cpp b/redfish-core/src/filter_expr_executor.cpp
index 291b1e4..70046f0 100644
--- a/redfish-core/src/filter_expr_executor.cpp
+++ b/redfish-core/src/filter_expr_executor.cpp
@@ -1,7 +1,9 @@
 #include "filter_expr_executor.hpp"
 
 #include "filter_expr_parser_ast.hpp"
+#include "human_sort.hpp"
 #include "logging.hpp"
+#include "utils/time_utils.hpp"
 
 namespace redfish
 {
@@ -9,12 +11,83 @@
 namespace
 {
 
+// A value that has been parsed as a time string per Edm.DateTimeOffset
+struct DateTimeString
+{
+    time_utils::usSinceEpoch value = time_utils::usSinceEpoch::zero();
+
+    // The following is created by dumping all key names of type
+    // Edm.DateTimeOffset.  While imperfect that it's a hardcoded list, these
+    // keys don't change that often
+    static constexpr auto timeKeys =
+        std::to_array<std::string_view>({"AccountExpiration",
+                                         "CalibrationTime",
+                                         "CoefficientUpdateTime",
+                                         "Created",
+                                         "CreatedDate",
+                                         "CreatedTime",
+                                         "CreateTime",
+                                         "DateTime",
+                                         "EndDateTime",
+                                         "EndTime",
+                                         "EventTimestamp",
+                                         "ExpirationDate",
+                                         "FirstOverflowTimestamp",
+                                         "InitialStartTime",
+                                         "InstallDate",
+                                         "LastOverflowTimestamp",
+                                         "LastResetTime",
+                                         "LastStateTime",
+                                         "LastUpdated",
+                                         "LifetimeStartDateTime",
+                                         "LowestReadingTime",
+                                         "MaintenanceWindowStartTime",
+                                         "Modified",
+                                         "PasswordExpiration",
+                                         "PeakReadingTime",
+                                         "PresentedPublicHostKeyTimestamp",
+                                         "ProductionDate",
+                                         "ReadingTime",
+                                         "ReleaseDate",
+                                         "ReservationTime",
+                                         "SensorResetTime",
+                                         "ServicedDate",
+                                         "SetPointUpdateTime",
+                                         "StartDateTime",
+                                         "StartTime",
+                                         "Time",
+                                         "Timestamp",
+                                         "ValidNotAfter",
+                                         "ValidNotBefore"});
+
+    explicit DateTimeString(std::string_view strvalue)
+    {
+        std::optional<time_utils::usSinceEpoch> out =
+            time_utils::dateStringToEpoch(strvalue);
+        if (!out)
+        {
+            BMCWEB_LOG_ERROR(
+                "Internal datetime value didn't parse as datetime?");
+        }
+        else
+        {
+            value = *out;
+        }
+    }
+
+    static bool isDateTimeKey(std::string_view key)
+    {
+        auto out = std::equal_range(timeKeys.begin(), timeKeys.end(), key);
+        return out.first != out.second;
+    }
+};
+
 // Class that can convert an arbitrary AST type into a structured value
 // Pulling from the json pointer when required
 struct ValueVisitor
 {
-    using result_type =
-        std::variant<std::monostate, double, int64_t, std::string>;
+    using result_type = std::variant<std::monostate, double, int64_t,
+                                     std::string, DateTimeString>;
     nlohmann::json& body;
     result_type operator()(double n);
     result_type operator()(int64_t x);
@@ -63,6 +136,10 @@
     const std::string* strValue = entry->get_ptr<const std::string*>();
     if (strValue != nullptr)
     {
+        if (DateTimeString::isDateTimeKey(x))
+        {
+            return DateTimeString(*strValue);
+        }
         return {*strValue};
     }
 
@@ -166,6 +243,14 @@
             return left == right;
         case filter_ast::ComparisonOpEnum::NotEquals:
             return left != right;
+        case filter_ast::ComparisonOpEnum::GreaterThan:
+            return alphanumComp(left, right) > 0;
+        case filter_ast::ComparisonOpEnum::GreaterThanOrEqual:
+            return alphanumComp(left, right) >= 0;
+        case filter_ast::ComparisonOpEnum::LessThan:
+            return alphanumComp(left, right) < 0;
+        case filter_ast::ComparisonOpEnum::LessThanOrEqual:
+            return alphanumComp(left, right) <= 0;
         default:
             BMCWEB_LOG_ERROR(
                 "Got comparator that should never happen.  Attempt to do numeric comparison on string {}",
@@ -177,10 +262,10 @@
 bool ApplyFilter::operator()(const filter_ast::Comparison& x)
 {
     ValueVisitor numeric(body);
-    std::variant<std::monostate, double, int64_t, std::string> left =
-        boost::apply_visitor(numeric, x.left);
-    std::variant<std::monostate, double, int64_t, std::string> right =
-        boost::apply_visitor(numeric, x.right);
+    std::variant<std::monostate, double, int64_t, std::string, DateTimeString>
+        left = boost::apply_visitor(numeric, x.left);
+    std::variant<std::monostate, double, int64_t, std::string, DateTimeString>
+        right = boost::apply_visitor(numeric, x.right);
 
     // Numeric comparisons
     const double* lDoubleValue = std::get_if<double>(&left);
@@ -221,6 +306,27 @@
     // String comparisons
     const std::string* lStrValue = std::get_if<std::string>(&left);
     const std::string* rStrValue = std::get_if<std::string>(&right);
+
+    const DateTimeString* lDateValue = std::get_if<DateTimeString>(&left);
+    const DateTimeString* rDateValue = std::get_if<DateTimeString>(&right);
+
+    // If we're trying to compare a date string to a string, construct a
+    // datestring from the string
+    if (lDateValue != nullptr && rStrValue != nullptr)
+    {
+        rDateValue = &right.emplace<DateTimeString>(std::string(*rStrValue));
+    }
+    if (lStrValue != nullptr && rDateValue != nullptr)
+    {
+        lDateValue = &left.emplace<DateTimeString>(std::string(*lStrValue));
+    }
+
+    if (lDateValue != nullptr && rDateValue != nullptr)
+    {
+        return doIntComparison(lDateValue->value.count(), x.token,
+                               rDateValue->value.count());
+    }
+
     if (lStrValue != nullptr && rStrValue != nullptr)
     {
         return doStringComparison(*lStrValue, x.token, *rStrValue);
diff --git a/test/redfish-core/include/filter_expr_executor_test.cpp b/test/redfish-core/include/filter_expr_executor_test.cpp
index adc5748..0761668 100644
--- a/test/redfish-core/include/filter_expr_executor_test.cpp
+++ b/test/redfish-core/include/filter_expr_executor_test.cpp
@@ -147,22 +147,104 @@
 TEST(FilterParser, String)
 {
     const nlohmann::json members =
-        R"({"Members": [{"SerialNumber": "Foo"}]})"_json;
+        R"({"Members": [{"SerialNumber": "1234"}]})"_json;
     // Forward true conditions
-    filterTrue("SerialNumber eq 'Foo'", members);
+    filterTrue("SerialNumber eq '1234'", members);
     filterTrue("SerialNumber ne 'NotFoo'", members);
+    filterTrue("SerialNumber gt '1233'", members);
+    filterTrue("SerialNumber ge '1234'", members);
+    filterTrue("SerialNumber lt '1235'", members);
+    filterTrue("SerialNumber le '1234'", members);
 
     // Reverse true conditions
-    filterTrue("'Foo' eq SerialNumber", members);
+    filterTrue("'1234' eq SerialNumber", members);
     filterTrue("'NotFoo' ne SerialNumber", members);
+    filterTrue("'1235' gt SerialNumber", members);
+    filterTrue("'1234' ge SerialNumber", members);
+    filterTrue("'1233' lt SerialNumber", members);
+    filterTrue("'1234' le SerialNumber", members);
 
     // Forward false conditions
     filterFalse("SerialNumber eq 'NotFoo'", members);
-    filterFalse("SerialNumber ne 'Foo'", members);
+    filterFalse("SerialNumber ne '1234'", members);
+    filterFalse("SerialNumber gt '1234'", members);
+    filterFalse("SerialNumber ge '1235'", members);
+    filterFalse("SerialNumber lt '1234'", members);
+    filterFalse("SerialNumber le '1233'", members);
 
     // Reverse false conditions
     filterFalse("'NotFoo' eq SerialNumber", members);
-    filterFalse("'Foo' ne SerialNumber", members);
+    filterFalse("'1234' ne SerialNumber", members);
+    filterFalse("'1234' gt SerialNumber", members);
+    filterFalse("'1233' ge SerialNumber", members);
+    filterFalse("'1234' lt SerialNumber", members);
+    filterFalse("'1235' le SerialNumber", members);
+}
+
+TEST(FilterParser, StringHuman)
+{
+    // Ensure that we're sorting based on human facing numbers, not
+    // lexicographic comparison
+
+    const nlohmann::json members = R"({"Members": [{}]})"_json;
+    // Forward true conditions
+    filterFalse("'20' eq '3'", members);
+    filterTrue("'20' ne '3'", members);
+    filterTrue("'20' gt '3'", members);
+    filterTrue("'20' ge '3'", members);
+    filterFalse("'20' lt '3'", members);
+    filterFalse("'20' le '3'", members);
+}
+
+TEST(FilterParser, StringSemver)
+{
+    const nlohmann::json members =
+        R"({"Members": [{"Version": "20.0.2"}]})"_json;
+    // Forward true conditions
+    filterTrue("Version eq '20.0.2'", members);
+    filterTrue("Version ne '20.2.0'", members);
+    filterTrue("Version gt '20.0.1'", members);
+    filterTrue("Version gt '1.9.9'", members);
+    filterTrue("Version gt '10.9.9'", members);
+}
+
+TEST(FilterParser, Dates)
+{
+    const nlohmann::json members =
+        R"({"Members": [{"Created": "2021-11-30T22:41:35.123+00:00"}]})"_json;
+
+    // Note, all comparisons below differ by a single millisecond
+    // Forward true conditions
+    filterTrue("Created eq '2021-11-30T22:41:35.123+00:00'", members);
+    filterTrue("Created ne '2021-11-30T22:41:35.122+00:00'", members);
+    filterTrue("Created gt '2021-11-30T22:41:35.122+00:00'", members);
+    filterTrue("Created ge '2021-11-30T22:41:35.123+00:00'", members);
+    filterTrue("Created lt '2021-11-30T22:41:35.124+00:00'", members);
+    filterTrue("Created le '2021-11-30T22:41:35.123+00:00'", members);
+
+    // Reverse true conditions
+    filterTrue("'2021-11-30T22:41:35.123+00:00' eq Created", members);
+    filterTrue("'2021-11-30T22:41:35.122+00:00' ne Created", members);
+    filterTrue("'2021-11-30T22:41:35.124+00:00' gt Created", members);
+    filterTrue("'2021-11-30T22:41:35.123+00:00' ge Created", members);
+    filterTrue("'2021-11-30T22:41:35.122+00:00' lt Created", members);
+    filterTrue("'2021-11-30T22:41:35.123+00:00' le Created", members);
+
+    // Forward false conditions
+    filterFalse("Created eq '2021-11-30T22:41:35.122+00:00'", members);
+    filterFalse("Created ne '2021-11-30T22:41:35.123+00:00'", members);
+    filterFalse("Created gt '2021-11-30T22:41:35.123+00:00'", members);
+    filterFalse("Created ge '2021-11-30T22:41:35.124+00:00'", members);
+    filterFalse("Created lt '2021-11-30T22:41:35.123+00:00'", members);
+    filterFalse("Created le '2021-11-30T22:41:35.122+00:00'", members);
+
+    // Reverse false conditions
+    filterFalse("'2021-11-30T22:41:35.122+00:00' eq Created", members);
+    filterFalse("'2021-11-30T22:41:35.123+00:00' ne Created", members);
+    filterFalse("'2021-11-30T22:41:35.123+00:00' gt Created", members);
+    filterFalse("'2021-11-30T22:41:35.122+00:00' ge Created", members);
+    filterFalse("'2021-11-30T22:41:35.123+00:00' lt Created", members);
+    filterFalse("'2021-11-30T22:41:35.124+00:00' le Created", members);
 }
 
 } // namespace redfish