From 621d62e9ab8b4e30f54d2d2c32df515a81d49e48 Mon Sep 17 00:00:00 2001
From: Li Yi Yu
Date: Tue, 9 Jan 2024 07:47:27 -0500
Subject: [PATCH] feat(flags): add relative date operators and fix numeric ops
(#34)
Co-authored-by: Neil Kakkar
---
CHANGELOG.md | 5 +
Gemfile | 1 +
lib/posthog/feature_flags.rb | 110 ++++++++++---
spec/posthog/feature_flag_spec.rb | 261 +++++++++++++++++++++++++++++-
4 files changed, 354 insertions(+), 23 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 12cf0b9..d32a69e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+# 2.4.0 - 2024-01-09
+
+1. Numeric property handling for feature flags now does the expected: When passed in a number, we do a numeric comparison. When passed in a string, we do a string comparison. Previously, we always did a string comparison.
+2. Add support for relative date operators for local evaluation.
+
# 2.3.1 - 2023-08-14
1. Update option doc string to show personal API Key as an option
diff --git a/Gemfile b/Gemfile
index c18bfdf..554e927 100644
--- a/Gemfile
+++ b/Gemfile
@@ -7,4 +7,5 @@ gem 'concurrent-ruby', require: 'concurrent'
group :development, :test do
gem 'webmock'
gem 'prettier'
+ gem 'timecop'
end
diff --git a/lib/posthog/feature_flags.rb b/lib/posthog/feature_flags.rb
index 3415cf6..359cc7e 100644
--- a/lib/posthog/feature_flags.rb
+++ b/lib/posthog/feature_flags.rb
@@ -205,6 +205,52 @@ def shutdown_poller()
# Class methods
+ def self.compare(lhs, rhs, operator)
+ if operator == "gt"
+ return lhs > rhs
+ elsif operator == "gte"
+ return lhs >= rhs
+ elsif operator == "lt"
+ return lhs < rhs
+ elsif operator == "lte"
+ return lhs <= rhs
+ else
+ raise "Invalid operator: #{operator}"
+ end
+ end
+
+ def self.relative_date_parse_for_feature_flag_matching(value)
+ match = /^([0-9]+)([a-z])$/.match(value)
+ parsed_dt = DateTime.now.new_offset(0)
+ if match
+ number = match[1].to_i
+
+ if number >= 10000
+ # Guard against overflow, disallow numbers greater than 10_000
+ return nil
+ end
+
+ interval = match[2]
+ if interval == "h"
+ parsed_dt = parsed_dt - (number/24r)
+ elsif interval == "d"
+ parsed_dt = parsed_dt.prev_day(number)
+ elsif interval == "w"
+ parsed_dt = parsed_dt.prev_day(number*7)
+ elsif interval == "m"
+ parsed_dt = parsed_dt.prev_month(number)
+ elsif interval == "y"
+ parsed_dt = parsed_dt.prev_year(number)
+ else
+ return nil
+ end
+ parsed_dt
+ else
+ nil
+ end
+ end
+
+
def self.match_property(property, property_values)
# only looks for matches where key exists in property_values
# doesn't support operator is_not_set
@@ -225,11 +271,21 @@ def self.match_property(property, property_values)
override_value = property_values[key]
case operator
- when 'exact'
- value.is_a?(Array) ? value.include?(override_value) : value == override_value
- when 'is_not'
- value.is_a?(Array) ? !value.include?(override_value) : value != override_value
- when'is_set'
+ when 'exact', 'is_not'
+ if value.is_a?(Array)
+ values_stringified = value.map { |val| val.to_s.downcase }
+ if operator == 'exact'
+ return values_stringified.any?(override_value.to_s.downcase)
+ else
+ return !values_stringified.any?(override_value.to_s.downcase)
+ end
+ end
+ if operator == 'exact'
+ value.to_s.downcase == override_value.to_s.downcase
+ else
+ value.to_s.downcase != override_value.to_s.downcase
+ end
+ when 'is_set'
property_values.key?(key)
when 'icontains'
override_value.to_s.downcase.include?(value.to_s.downcase)
@@ -239,25 +295,39 @@ def self.match_property(property, property_values)
PostHog::Utils.is_valid_regex(value.to_s) && !Regexp.new(value.to_s).match(override_value.to_s).nil?
when 'not_regex'
PostHog::Utils.is_valid_regex(value.to_s) && Regexp.new(value.to_s).match(override_value.to_s).nil?
- when 'gt'
- override_value.class == value.class && override_value > value
- when 'gte'
- override_value.class == value.class && override_value >= value
- when 'lt'
- override_value.class == value.class && override_value < value
- when 'lte'
- override_value.class == value.class && override_value <= value
- when 'is_date_before', 'is_date_after'
- parsed_date = PostHog::Utils::convert_to_datetime(value)
- override_date = PostHog::Utils::convert_to_datetime(override_value)
- if operator == 'is_date_before'
- return override_date < parsed_date
+ when 'gt', 'gte', 'lt', 'lte'
+ parsed_value = nil
+ begin
+ parsed_value = Float(value)
+ rescue StandardError => e
+ end
+ if !parsed_value.nil? && !override_value.nil?
+ if override_value.is_a?(String)
+ self.compare(override_value, value.to_s, operator)
+ else
+ self.compare(override_value, parsed_value, operator)
+ end
+ else
+ self.compare(override_value.to_s, value.to_s, operator)
+ end
+ when 'is_date_before', 'is_date_after', 'is_relative_date_before', 'is_relative_date_after'
+ if operator == 'is_relative_date_before' || operator == 'is_relative_date_after'
+ parsed_date = self.relative_date_parse_for_feature_flag_matching(value.to_s)
+ override_date = PostHog::Utils.convert_to_datetime(override_value.to_s)
else
+ parsed_date = PostHog::Utils.convert_to_datetime(value.to_s)
+ override_date = PostHog::Utils.convert_to_datetime(override_value.to_s)
+ end
+ if !parsed_date
+ raise InconclusiveMatchError.new("Invalid date format")
+ end
+ if operator == 'is_date_before' or operator == 'is_relative_date_before'
+ return override_date < parsed_date
+ elsif operator == 'is_date_after' or operator == 'is_relative_date_after'
return override_date > parsed_date
end
else
- logger.error "Unknown operator: #{operator}"
- false
+ raise InconclusiveMatchError.new("Unknown operator: #{operator}")
end
end
diff --git a/spec/posthog/feature_flag_spec.rb b/spec/posthog/feature_flag_spec.rb
index 8881503..c4506a7 100644
--- a/spec/posthog/feature_flag_spec.rb
+++ b/spec/posthog/feature_flag_spec.rb
@@ -1,5 +1,5 @@
require 'spec_helper'
-
+require 'timecop'
class PostHog
@@ -1153,7 +1153,7 @@ class PostHog
expect(FeatureFlagsPoller.match_property(property_a, { 'key' => 0 })).to be false
expect(FeatureFlagsPoller.match_property(property_a, { 'key' => -1 })).to be false
- expect(FeatureFlagsPoller.match_property(property_a, { 'key' => "23" })).to be false
+ expect(FeatureFlagsPoller.match_property(property_a, { 'key' => "23" })).to be true
property_b = { 'key' => 'key', 'value' => 1, 'operator' => 'lt' }
expect(FeatureFlagsPoller.match_property(property_b, { 'key' => 0 })).to be true
@@ -1171,7 +1171,7 @@ class PostHog
expect(FeatureFlagsPoller.match_property(property_c, { 'key' => 0 })).to be false
expect(FeatureFlagsPoller.match_property(property_c, { 'key' => -1 })).to be false
expect(FeatureFlagsPoller.match_property(property_c, { 'key' => -3 })).to be false
- expect(FeatureFlagsPoller.match_property(property_c, { 'key' => "3" })).to be false
+ expect(FeatureFlagsPoller.match_property(property_c, { 'key' => "3" })).to be true
property_d = { 'key' => 'key', 'value' => '43', 'operator' => 'lte' }
expect(FeatureFlagsPoller.match_property(property_d, { 'key' => '43' })).to be true
@@ -1179,7 +1179,21 @@ class PostHog
expect(FeatureFlagsPoller.match_property(property_d, { 'key' => '44' })).to be false
expect(FeatureFlagsPoller.match_property(property_d, { 'key' => 44 })).to be false
+ expect(FeatureFlagsPoller.match_property(property_d, { 'key' => 42 })).to be true
+
+ property_e = { 'key' => 'key', 'value' => "30", 'operator' => 'lt' }
+ expect(FeatureFlagsPoller.match_property(property_e, { 'key' => '29' })).to be true
+
+ # depending on the type of override, we adjust type comparison
+ expect(FeatureFlagsPoller.match_property(property_e, { 'key' => '100' })).to be true
+ expect(FeatureFlagsPoller.match_property(property_e, { 'key' => 100 })).to be false
+ property_f = { 'key' => 'key', 'value' => "123aloha", 'operator' => 'gt' }
+ expect(FeatureFlagsPoller.match_property(property_f, { 'key' => '123' })).to be false
+ expect(FeatureFlagsPoller.match_property(property_f, { 'key' => 122 })).to be false
+
+ # this turns into a string comparison
+ expect(FeatureFlagsPoller.match_property(property_f, { 'key' => 129 })).to be true
end
it 'with date operators' do
@@ -1220,6 +1234,247 @@ class PostHog
expect(FeatureFlagsPoller.match_property(property_d, { 'key' => '2022-04-05 11:34:13 +00:00' })).to be false
end
+
+ it 'with relative date operators - is_relative_date_before' do
+ Timecop.freeze(Time.gm(2022, 5, 1)) do
+ property_a = { 'key' => 'key', 'value' => '6h', 'operator' => 'is_relative_date_before'}
+ expect(FeatureFlagsPoller.match_property(property_a, { 'key' => '2022-03-01' })).to be true
+ expect(FeatureFlagsPoller.match_property(property_a, { 'key' => '2022-04-30' })).to be true
+ expect(FeatureFlagsPoller.match_property(property_a, { 'key' => DateTime.new(2022, 4, 30, 1, 2, 3) })).to be true
+ # false because date comparison, instead of datetime, so reduces to same date
+ # we converts this to datetime for appropriate comparison anyway
+ expect(FeatureFlagsPoller.match_property(property_a, { 'key' => DateTime.new(2022, 4, 30, 19, 2, 3) })).to be false
+
+ expect(FeatureFlagsPoller.match_property(property_a, { 'key' => DateTime.new(2022, 4, 30, 1, 2, 3, '+2') })).to be true
+ expect(FeatureFlagsPoller.match_property(property_a, { 'key' => DateTime.new(2022, 4, 30, 20, 2, 3, '+2') })).to be false
+ expect(FeatureFlagsPoller.match_property(property_a, { 'key' => DateTime.new(2022, 4, 30, 19, 59, 3, '+2') })).to be true
+ expect(FeatureFlagsPoller.match_property(property_a, { 'key' => DateTime.parse('2022-04-30') })).to be true
+ expect(FeatureFlagsPoller.match_property(property_a, { 'key' => '2022-05-30' })).to be false
+
+ # can't be a number
+ expect{ FeatureFlagsPoller.match_property(property_a, { 'key' => 1}) }.to raise_error(InconclusiveMatchError)
+
+ # can't be invalid string
+ expect{ FeatureFlagsPoller.match_property(property_a, { 'key' => 'abcdef'}) }.to raise_error(InconclusiveMatchError)
+ end
+ end
+
+ it 'with relative date operators - is_relative_date_after' do
+ Timecop.freeze(Time.gm(2022, 5, 1)) do
+ property_b = { 'key' => 'key', 'value' => '1h', 'operator' => 'is_relative_date_after'}
+ expect(FeatureFlagsPoller.match_property(property_b, { 'key' => '2022-05-02' })).to be true
+ expect(FeatureFlagsPoller.match_property(property_b, { 'key' => '2022-05-30' })).to be true
+ expect(FeatureFlagsPoller.match_property(property_b, { 'key' => DateTime.new(2022, 5, 30) })).to be true
+ expect(FeatureFlagsPoller.match_property(property_b, { 'key' => DateTime.parse('2022-05-30') })).to be true
+ expect(FeatureFlagsPoller.match_property(property_b, { 'key' => '2022-04-30' })).to be false
+
+ # can't be invalid string
+ expect{ FeatureFlagsPoller.match_property(property_b, { 'key' => 'abcdef'}) }.to raise_error(InconclusiveMatchError)
+ # invalid flag property
+ property_c = { 'key' => 'key', 'value' => 1234, 'operator' => 'is_relative_date_after'}
+ expect{ FeatureFlagsPoller.match_property(property_c, { 'key' => 1}) }.to raise_error(InconclusiveMatchError)
+ expect{ FeatureFlagsPoller.match_property(property_c, { 'key' => '2022-05-30'}) }.to raise_error(InconclusiveMatchError)
+ end
+ end
+
+ it 'with relative date operators - all possible relative dates' do
+ Timecop.freeze(Time.gm(2022, 5, 1)) do
+ property_d = { 'key' => 'key', 'value' => '12d', 'operator' => 'is_relative_date_before'}
+ expect(FeatureFlagsPoller.match_property(property_d, { 'key' => '2022-05-30' })).to be false
+
+ expect(FeatureFlagsPoller.match_property(property_d, { 'key' => '2022-03-30' })).to be true
+ expect(FeatureFlagsPoller.match_property(property_d, { 'key' => '2022-04-05 12:34:11+01:00' })).to be true
+ expect(FeatureFlagsPoller.match_property(property_d, { 'key' => '2022-04-19 01:34:11+02:00' })).to be true
+ expect(FeatureFlagsPoller.match_property(property_d, { 'key' => '2022-04-19 02:00:01+02:00' })).to be false
+
+ # Try all possible relative dates
+ property_e = { 'key' => 'key', 'value' => '1h', 'operator' => 'is_relative_date_before'}
+ expect(FeatureFlagsPoller.match_property(property_e, { 'key' => '2022-05-01 00:00:00' })).to be false
+ expect(FeatureFlagsPoller.match_property(property_e, { 'key' => '2022-04-30 22:00:00' })).to be true
+
+ property_f = { 'key' => 'key', 'value' => '1d', 'operator' => 'is_relative_date_before'}
+ expect(FeatureFlagsPoller.match_property(property_f, { 'key' => '2022-04-29 23:59:00' })).to be true
+ expect(FeatureFlagsPoller.match_property(property_f, { 'key' => '2022-04-30 00:00:01' })).to be false
+
+ property_g = { 'key' => 'key', 'value' => '1w', 'operator' => 'is_relative_date_before'}
+ expect(FeatureFlagsPoller.match_property(property_g, { 'key' => '2022-04-23 00:00:00' })).to be true
+ expect(FeatureFlagsPoller.match_property(property_g, { 'key' => '2022-04-24 00:00:00' })).to be false
+ expect(FeatureFlagsPoller.match_property(property_g, { 'key' => '2022-04-24 00:00:01' })).to be false
+
+ property_h = { 'key' => 'key', 'value' => '1m', 'operator' => 'is_relative_date_before'}
+ expect(FeatureFlagsPoller.match_property(property_h, { 'key' => '2022-03-01 00:00:00' })).to be true
+ expect(FeatureFlagsPoller.match_property(property_h, { 'key' => '2022-04-05 00:00:00' })).to be false
+
+ property_i = { 'key' => 'key', 'value' => '1y', 'operator' => 'is_relative_date_before'}
+ expect(FeatureFlagsPoller.match_property(property_i, { 'key' => '2021-04-28 00:00:00' })).to be true
+ expect(FeatureFlagsPoller.match_property(property_i, { 'key' => '2021-05-01 00:00:01' })).to be false
+
+ property_j = { 'key' => 'key', 'value' => '122h', 'operator' => 'is_relative_date_after'}
+ expect(FeatureFlagsPoller.match_property(property_j, { 'key' => '2022-05-01 00:00:00' })).to be true
+ expect(FeatureFlagsPoller.match_property(property_j, { 'key' => '2022-04-23 01:00:00' })).to be false
+
+ property_k = { 'key' => 'key', 'value' => '2d', 'operator' => 'is_relative_date_after'}
+ expect(FeatureFlagsPoller.match_property(property_k, { 'key' => '2022-05-01 00:00:00' })).to be true
+ expect(FeatureFlagsPoller.match_property(property_k, { 'key' => '2022-04-29 00:00:01' })).to be true
+ expect(FeatureFlagsPoller.match_property(property_k, { 'key' => '2022-04-29 00:00:00' })).to be false
+
+ property_l = { 'key' => 'key', 'value' => '02w', 'operator' => 'is_relative_date_after'}
+ expect(FeatureFlagsPoller.match_property(property_l, { 'key' => '2022-05-01 00:00:00' })).to be true
+ expect(FeatureFlagsPoller.match_property(property_l, { 'key' => '2022-04-16 00:00:00' })).to be false
+
+ property_m = { 'key' => 'key', 'value' => '1m', 'operator' => 'is_relative_date_after'}
+ expect(FeatureFlagsPoller.match_property(property_m, { 'key' => '2022-04-01 00:00:01' })).to be true
+ expect(FeatureFlagsPoller.match_property(property_m, { 'key' => '2022-04-01 00:00:00' })).to be false
+
+ property_n = { 'key' => 'key', 'value' => '1y', 'operator' => 'is_relative_date_after'}
+ expect(FeatureFlagsPoller.match_property(property_n, { 'key' => '2022-05-01 00:00:00' })).to be true
+ expect(FeatureFlagsPoller.match_property(property_n, { 'key' => '2021-05-01 00:00:01' })).to be true
+ expect(FeatureFlagsPoller.match_property(property_n, { 'key' => '2021-05-01 00:00:00' })).to be false
+ expect(FeatureFlagsPoller.match_property(property_n, { 'key' => '2021-04-30 00:00:00' })).to be false
+ expect(FeatureFlagsPoller.match_property(property_n, { 'key' => '2021-03-01 12:13:00' })).to be false
+
+ end
+ end
+
+ it 'with none property value with all operators' do
+ property_a = { 'key' => 'key', 'value' => 'nil', 'operator' => 'is_not' }
+ expect(FeatureFlagsPoller.match_property(property_a, { 'key' => nil })).to be true
+ # nil to string here is an empty string, not "nil" so it's true because it doesn't match
+ expect(FeatureFlagsPoller.match_property(property_a, { 'key' => 'ni' })).to be true
+
+ property_b = { 'key' => 'key', 'value' => nil, 'operator' => 'is_set' }
+ expect(FeatureFlagsPoller.match_property(property_b, { 'key' => nil })).to be true
+ expect{ FeatureFlagsPoller.match_property(property_b, { })}.to raise_error(InconclusiveMatchError)
+
+ property_c = { 'key' => 'key', 'value' => 'ni', 'operator' => 'icontains' }
+ expect(FeatureFlagsPoller.match_property(property_c, { 'key' => nil })).to be false
+ expect(FeatureFlagsPoller.match_property(property_c, { 'key' => 'smh' })).to be false
+
+ property_d = { 'key' => 'key', 'value' => 'Ni', 'operator' => 'regex' }
+ expect(FeatureFlagsPoller.match_property(property_d, { 'key' => nil })).to be false
+
+ property_d_lower_case = { 'key' => 'key', 'value' => 'ni', 'operator' => 'regex' }
+ expect(FeatureFlagsPoller.match_property(property_d_lower_case, { 'key' => nil })).to be false
+
+ property_e = { 'key' => 'key', 'value' => 1, 'operator' => 'gt' }
+ expect(FeatureFlagsPoller.match_property(property_e, { 'key' => nil })).to be false
+
+ property_f = { 'key' => 'key', 'value' => 1, 'operator' => 'lt' }
+ expect(FeatureFlagsPoller.match_property(property_f, { 'key' => nil })).to be true
+
+ property_g = { 'key' => 'key', 'value' => 'xyz', 'operator' => 'gte' }
+ expect(FeatureFlagsPoller.match_property(property_g, { 'key' => nil })).to be false
+
+ property_j = { 'key' => 'key', 'value' => '2022-05-01', 'operator' => 'is_date_after' }
+ expect{ FeatureFlagsPoller.match_property(property_j, { 'key' => nil }) }.to raise_error(InconclusiveMatchError)
+
+ property_k = { 'key' => 'key', 'value' => '2022-05-01', 'operator' => 'is_date_before' }
+ expect{ FeatureFlagsPoller.match_property(property_k, { 'key' => 'random' }) }.to raise_error(InconclusiveMatchError)
+ end
+
+ it 'with invalid operator' do
+ property_a = { 'key' => 'key', 'value'=>'2022-05-01', 'operator' => 'is_unknown'}
+ begin
+ expect { FeatureFlagsPoller.match_property(property_a, {"key" => "random"})}.to raise_error(InconclusiveMatchError)
+ FeatureFlagsPoller.match_property(property_a, {"key" => "random"})
+ rescue InconclusiveMatchError => e
+ expect(e.message).to eq("Unknown operator: is_unknown")
+ end
+
+ end
+ end
+
+ describe 'relative date parsing' do
+ it 'invalid input' do
+ Timecop.freeze(Time.gm(2022, 5, 1)) do
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("1")).to be_nil
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("1x")).to be_nil
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("1.2y")).to be_nil
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("1z")).to be_nil
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("1s")).to be_nil
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("123344000.134m")).to be_nil
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("bazinga")).to be_nil
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("000bello")).to be_nil
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("000hello")).to be_nil
+
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("000h")).not_to be_nil
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("1000h")).not_to be_nil
+ end
+ end
+
+ it 'test overflow' do
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("1000000h")).to be_nil
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("100000000000000000y")).to be_nil
+ end
+
+ it 'test hour parsing' do
+ Timecop.freeze(Time.gm(2020, 1, 1, 12, 1, 20, 134000, 0)) do
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("1h")).to eq(DateTime.new(2020, 1, 1, 11, 1, 20, Rational(0, 24)))
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("2h")).to eq(DateTime.new(2020, 1, 1, 10, 1, 20, Rational(0, 24)))
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("24h")).to eq(DateTime.new(2019, 12, 31, 12, 1, 20, Rational(0, 24)))
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("30h")).to eq(DateTime.new(2019, 12, 31, 6, 1, 20, Rational(0, 24)))
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("48h")).to eq(DateTime.new(2019, 12, 30, 12, 1, 20, Rational(0, 24)))
+
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("24h")).to eq(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("1d"))
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("48h")).to eq(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("2d"))
+ end
+ end
+
+ it 'test day parsing' do
+ Timecop.freeze(Time.gm(2020, 1, 1, 12, 1, 20, 134000, 0)) do
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("1d")).to eq(DateTime.new(2019, 12, 31, 12, 1, 20, Rational(0, 24)))
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("2d")).to eq(DateTime.new(2019, 12, 30, 12, 1, 20, Rational(0, 24)))
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("7d")).to eq(DateTime.new(2019, 12, 25, 12, 1, 20, Rational(0, 24)))
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("14d")).to eq(DateTime.new(2019, 12, 18, 12, 1, 20, Rational(0, 24)))
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("30d")).to eq(DateTime.new(2019, 12, 2, 12, 1, 20, Rational(0, 24)))
+
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("7d")).to eq(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("1w"))
+ end
+ end
+
+ it 'test week parsing' do
+ Timecop.freeze(Time.gm(2020, 1, 1, 12, 1, 20, 134000, 0)) do
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("1w")).to eq(DateTime.new(2019, 12, 25, 12, 1, 20, Rational(0, 24)))
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("2w")).to eq(DateTime.new(2019, 12, 18, 12, 1, 20, Rational(0, 24)))
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("4w")).to eq(DateTime.new(2019, 12, 4, 12, 1, 20, Rational(0, 24)))
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("8w")).to eq(DateTime.new(2019, 11, 6, 12, 1, 20, Rational(0, 24)))
+
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("1m")).to eq(DateTime.new(2019, 12, 1, 12, 1, 20, Rational(0, 24)))
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("4w")).not_to eq(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("1m"))
+ end
+ end
+
+ it 'test month parsing' do
+ Timecop.freeze(Time.gm(2020, 1, 1, 12, 1, 20, 134000, 0)) do
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("1m")).to eq(DateTime.new(2019, 12, 1, 12, 1, 20, Rational(0, 24)))
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("2m")).to eq(DateTime.new(2019, 11, 1, 12, 1, 20, Rational(0, 24)))
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("4m")).to eq(DateTime.new(2019, 9, 1, 12, 1, 20, Rational(0, 24)))
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("8m")).to eq(DateTime.new(2019, 5, 1, 12, 1, 20, Rational(0, 24)))
+
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("1y")).to eq(DateTime.new(2019, 1, 1, 12, 1, 20, Rational(0, 24)))
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("12m")).to eq(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("1y"))
+ end
+
+ Timecop.freeze(Time.gm(2020, 4, 3, 0, 0, 0, 0, 0)) do
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("1m")).to eq(DateTime.new(2020, 3, 3, 0, 0, 0, Rational(0, 24)))
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("2m")).to eq(DateTime.new(2020, 2, 3, 0, 0, 0, Rational(0, 24)))
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("4m")).to eq(DateTime.new(2019, 12, 3, 0, 0, 0, Rational(0, 24)))
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("8m")).to eq(DateTime.new(2019, 8, 3, 0, 0, 0, Rational(0, 24)))
+
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("1y")).to eq(DateTime.new(2019, 4, 3, 0, 0, 0, Rational(0, 24)))
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("12m")).to eq(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("1y"))
+ end
+ end
+
+ it 'test year parsing' do
+ Timecop.freeze(Time.gm(2020, 1, 1, 12, 1, 20, 134000, 0)) do
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("1y")).to eq(DateTime.new(2019, 1, 1, 12, 1, 20, Rational(0, 24)))
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("2y")).to eq(DateTime.new(2018, 1, 1, 12, 1, 20, Rational(0, 24)))
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("4y")).to eq(DateTime.new(2016, 1, 1, 12, 1, 20, Rational(0, 24)))
+ expect(FeatureFlagsPoller.relative_date_parse_for_feature_flag_matching("8y")).to eq(DateTime.new(2012, 1, 1, 12, 1, 20, Rational(0, 24)))
+ end
+ end
+
end