decision_agent 0.3.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +234 -14
- data/lib/decision_agent/ab_testing/ab_test.rb +5 -1
- data/lib/decision_agent/ab_testing/ab_test_assignment.rb +2 -0
- data/lib/decision_agent/ab_testing/ab_test_manager.rb +2 -0
- data/lib/decision_agent/ab_testing/ab_testing_agent.rb +2 -0
- data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +2 -13
- data/lib/decision_agent/ab_testing/storage/adapter.rb +2 -0
- data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +2 -0
- data/lib/decision_agent/agent.rb +78 -9
- data/lib/decision_agent/audit/adapter.rb +2 -0
- data/lib/decision_agent/audit/logger_adapter.rb +2 -0
- data/lib/decision_agent/audit/null_adapter.rb +2 -0
- data/lib/decision_agent/auth/access_audit_logger.rb +2 -0
- data/lib/decision_agent/auth/authenticator.rb +2 -0
- data/lib/decision_agent/auth/password_reset_manager.rb +2 -0
- data/lib/decision_agent/auth/password_reset_token.rb +2 -0
- data/lib/decision_agent/auth/permission.rb +2 -0
- data/lib/decision_agent/auth/permission_checker.rb +2 -0
- data/lib/decision_agent/auth/rbac_adapter.rb +2 -0
- data/lib/decision_agent/auth/rbac_config.rb +2 -0
- data/lib/decision_agent/auth/role.rb +2 -0
- data/lib/decision_agent/auth/session.rb +2 -0
- data/lib/decision_agent/auth/session_manager.rb +2 -0
- data/lib/decision_agent/auth/user.rb +2 -0
- data/lib/decision_agent/context.rb +14 -0
- data/lib/decision_agent/decision.rb +113 -4
- data/lib/decision_agent/dmn/adapter.rb +2 -0
- data/lib/decision_agent/dmn/cache.rb +2 -2
- data/lib/decision_agent/dmn/decision_graph.rb +7 -7
- data/lib/decision_agent/dmn/decision_tree.rb +16 -8
- data/lib/decision_agent/dmn/errors.rb +2 -0
- data/lib/decision_agent/dmn/exporter.rb +2 -0
- data/lib/decision_agent/dmn/feel/evaluator.rb +130 -114
- data/lib/decision_agent/dmn/feel/functions.rb +2 -0
- data/lib/decision_agent/dmn/feel/parser.rb +2 -0
- data/lib/decision_agent/dmn/feel/simple_parser.rb +98 -77
- data/lib/decision_agent/dmn/feel/transformer.rb +56 -102
- data/lib/decision_agent/dmn/feel/types.rb +2 -0
- data/lib/decision_agent/dmn/importer.rb +2 -0
- data/lib/decision_agent/dmn/model.rb +2 -4
- data/lib/decision_agent/dmn/parser.rb +2 -0
- data/lib/decision_agent/dmn/testing.rb +3 -2
- data/lib/decision_agent/dmn/validator.rb +5 -3
- data/lib/decision_agent/dmn/visualizer.rb +7 -6
- data/lib/decision_agent/dsl/condition_evaluator.rb +242 -1375
- data/lib/decision_agent/dsl/helpers/cache_helpers.rb +82 -0
- data/lib/decision_agent/dsl/helpers/comparison_helpers.rb +98 -0
- data/lib/decision_agent/dsl/helpers/date_helpers.rb +91 -0
- data/lib/decision_agent/dsl/helpers/geospatial_helpers.rb +85 -0
- data/lib/decision_agent/dsl/helpers/operator_evaluation_helpers.rb +160 -0
- data/lib/decision_agent/dsl/helpers/parameter_parsing_helpers.rb +206 -0
- data/lib/decision_agent/dsl/helpers/template_helpers.rb +39 -0
- data/lib/decision_agent/dsl/helpers/utility_helpers.rb +45 -0
- data/lib/decision_agent/dsl/operators/base.rb +70 -0
- data/lib/decision_agent/dsl/operators/basic_comparison_operators.rb +80 -0
- data/lib/decision_agent/dsl/operators/collection_operators.rb +60 -0
- data/lib/decision_agent/dsl/operators/date_arithmetic_operators.rb +206 -0
- data/lib/decision_agent/dsl/operators/date_time_operators.rb +47 -0
- data/lib/decision_agent/dsl/operators/duration_operators.rb +149 -0
- data/lib/decision_agent/dsl/operators/financial_operators.rb +237 -0
- data/lib/decision_agent/dsl/operators/geospatial_operators.rb +106 -0
- data/lib/decision_agent/dsl/operators/mathematical_operators.rb +234 -0
- data/lib/decision_agent/dsl/operators/moving_window_operators.rb +135 -0
- data/lib/decision_agent/dsl/operators/numeric_operators.rb +120 -0
- data/lib/decision_agent/dsl/operators/rate_operators.rb +65 -0
- data/lib/decision_agent/dsl/operators/statistical_aggregations.rb +187 -0
- data/lib/decision_agent/dsl/operators/string_aggregations.rb +84 -0
- data/lib/decision_agent/dsl/operators/string_operators.rb +72 -0
- data/lib/decision_agent/dsl/operators/time_component_operators.rb +72 -0
- data/lib/decision_agent/dsl/rule_parser.rb +2 -0
- data/lib/decision_agent/dsl/schema_validator.rb +37 -14
- data/lib/decision_agent/errors.rb +2 -0
- data/lib/decision_agent/evaluation.rb +14 -2
- data/lib/decision_agent/evaluators/base.rb +2 -0
- data/lib/decision_agent/evaluators/dmn_evaluator.rb +108 -19
- data/lib/decision_agent/evaluators/json_rule_evaluator.rb +56 -11
- data/lib/decision_agent/evaluators/static_evaluator.rb +2 -0
- data/lib/decision_agent/explainability/condition_trace.rb +85 -0
- data/lib/decision_agent/explainability/explainability_result.rb +50 -0
- data/lib/decision_agent/explainability/rule_trace.rb +41 -0
- data/lib/decision_agent/explainability/trace_collector.rb +26 -0
- data/lib/decision_agent/monitoring/alert_manager.rb +7 -16
- data/lib/decision_agent/monitoring/dashboard_server.rb +383 -250
- data/lib/decision_agent/monitoring/metrics_collector.rb +2 -0
- data/lib/decision_agent/monitoring/monitored_agent.rb +2 -0
- data/lib/decision_agent/monitoring/prometheus_exporter.rb +3 -1
- data/lib/decision_agent/replay/replay.rb +4 -1
- data/lib/decision_agent/scoring/base.rb +2 -0
- data/lib/decision_agent/scoring/consensus.rb +2 -0
- data/lib/decision_agent/scoring/max_weight.rb +2 -0
- data/lib/decision_agent/scoring/threshold.rb +2 -0
- data/lib/decision_agent/scoring/weighted_average.rb +2 -0
- data/lib/decision_agent/simulation/errors.rb +20 -0
- data/lib/decision_agent/simulation/impact_analyzer.rb +500 -0
- data/lib/decision_agent/simulation/monte_carlo_simulator.rb +638 -0
- data/lib/decision_agent/simulation/replay_engine.rb +488 -0
- data/lib/decision_agent/simulation/scenario_engine.rb +320 -0
- data/lib/decision_agent/simulation/scenario_library.rb +165 -0
- data/lib/decision_agent/simulation/shadow_test_engine.rb +274 -0
- data/lib/decision_agent/simulation/what_if_analyzer.rb +1008 -0
- data/lib/decision_agent/simulation.rb +19 -0
- data/lib/decision_agent/testing/batch_test_importer.rb +6 -2
- data/lib/decision_agent/testing/batch_test_runner.rb +5 -2
- data/lib/decision_agent/testing/test_coverage_analyzer.rb +2 -0
- data/lib/decision_agent/testing/test_result_comparator.rb +2 -0
- data/lib/decision_agent/testing/test_scenario.rb +2 -0
- data/lib/decision_agent/version.rb +3 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +108 -43
- data/lib/decision_agent/versioning/adapter.rb +9 -0
- data/lib/decision_agent/versioning/file_storage_adapter.rb +19 -6
- data/lib/decision_agent/versioning/version_manager.rb +9 -0
- data/lib/decision_agent/web/dmn_editor/serialization.rb +74 -0
- data/lib/decision_agent/web/dmn_editor/xml_builder.rb +107 -0
- data/lib/decision_agent/web/dmn_editor.rb +8 -67
- data/lib/decision_agent/web/middleware/auth_middleware.rb +2 -0
- data/lib/decision_agent/web/middleware/permission_middleware.rb +3 -1
- data/lib/decision_agent/web/public/app.js +186 -26
- data/lib/decision_agent/web/public/batch_testing.html +80 -6
- data/lib/decision_agent/web/public/dmn-editor.html +2 -2
- data/lib/decision_agent/web/public/dmn-editor.js +74 -8
- data/lib/decision_agent/web/public/index.html +69 -3
- data/lib/decision_agent/web/public/login.html +1 -1
- data/lib/decision_agent/web/public/sample_batch.csv +11 -0
- data/lib/decision_agent/web/public/sample_impact.csv +11 -0
- data/lib/decision_agent/web/public/sample_replay.csv +11 -0
- data/lib/decision_agent/web/public/sample_rules.json +118 -0
- data/lib/decision_agent/web/public/sample_shadow.csv +11 -0
- data/lib/decision_agent/web/public/sample_whatif.csv +11 -0
- data/lib/decision_agent/web/public/simulation.html +146 -0
- data/lib/decision_agent/web/public/simulation_impact.html +495 -0
- data/lib/decision_agent/web/public/simulation_replay.html +547 -0
- data/lib/decision_agent/web/public/simulation_shadow.html +561 -0
- data/lib/decision_agent/web/public/simulation_whatif.html +549 -0
- data/lib/decision_agent/web/public/styles.css +65 -0
- data/lib/decision_agent/web/public/users.html +1 -1
- data/lib/decision_agent/web/rack_helpers.rb +106 -0
- data/lib/decision_agent/web/rack_request_helpers.rb +196 -0
- data/lib/decision_agent/web/server.rb +2126 -1374
- data/lib/decision_agent.rb +19 -1
- data/lib/generators/decision_agent/install/install_generator.rb +2 -0
- data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +2 -0
- data/lib/generators/decision_agent/install/templates/ab_test_model.rb +2 -0
- data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +2 -0
- data/lib/generators/decision_agent/install/templates/migration.rb +2 -0
- data/lib/generators/decision_agent/install/templates/rule.rb +2 -0
- data/lib/generators/decision_agent/install/templates/rule_version.rb +2 -0
- metadata +103 -89
- data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
- data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
- data/spec/ab_testing/ab_test_spec.rb +0 -270
- data/spec/ab_testing/ab_testing_agent_spec.rb +0 -655
- data/spec/ab_testing/storage/adapter_spec.rb +0 -64
- data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
- data/spec/activerecord_thread_safety_spec.rb +0 -553
- data/spec/advanced_operators_spec.rb +0 -3150
- data/spec/agent_spec.rb +0 -289
- data/spec/api_contract_spec.rb +0 -430
- data/spec/audit_adapters_spec.rb +0 -92
- data/spec/auth/access_audit_logger_spec.rb +0 -394
- data/spec/auth/authenticator_spec.rb +0 -112
- data/spec/auth/password_reset_spec.rb +0 -294
- data/spec/auth/permission_checker_spec.rb +0 -207
- data/spec/auth/permission_spec.rb +0 -73
- data/spec/auth/rbac_adapter_spec.rb +0 -778
- data/spec/auth/rbac_config_spec.rb +0 -82
- data/spec/auth/role_spec.rb +0 -51
- data/spec/auth/session_manager_spec.rb +0 -172
- data/spec/auth/session_spec.rb +0 -112
- data/spec/auth/user_spec.rb +0 -130
- data/spec/comprehensive_edge_cases_spec.rb +0 -1777
- data/spec/context_spec.rb +0 -127
- data/spec/decision_agent_spec.rb +0 -96
- data/spec/decision_spec.rb +0 -423
- data/spec/dmn/decision_graph_spec.rb +0 -282
- data/spec/dmn/decision_tree_spec.rb +0 -203
- data/spec/dmn/feel/errors_spec.rb +0 -18
- data/spec/dmn/feel/functions_spec.rb +0 -400
- data/spec/dmn/feel/simple_parser_spec.rb +0 -274
- data/spec/dmn/feel/types_spec.rb +0 -176
- data/spec/dmn/feel_parser_spec.rb +0 -489
- data/spec/dmn/hit_policy_spec.rb +0 -202
- data/spec/dmn/integration_spec.rb +0 -226
- data/spec/dsl/condition_evaluator_spec.rb +0 -774
- data/spec/dsl_validation_spec.rb +0 -648
- data/spec/edge_cases_spec.rb +0 -353
- data/spec/evaluation_spec.rb +0 -364
- data/spec/evaluation_validator_spec.rb +0 -165
- data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
- data/spec/examples.txt +0 -1909
- data/spec/fixtures/dmn/complex_decision.dmn +0 -81
- data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
- data/spec/fixtures/dmn/simple_decision.dmn +0 -40
- data/spec/issue_verification_spec.rb +0 -759
- data/spec/json_rule_evaluator_spec.rb +0 -587
- data/spec/monitoring/alert_manager_spec.rb +0 -378
- data/spec/monitoring/metrics_collector_spec.rb +0 -501
- data/spec/monitoring/monitored_agent_spec.rb +0 -225
- data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
- data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
- data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
- data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
- data/spec/performance_optimizations_spec.rb +0 -493
- data/spec/replay_edge_cases_spec.rb +0 -699
- data/spec/replay_spec.rb +0 -210
- data/spec/rfc8785_canonicalization_spec.rb +0 -215
- data/spec/scoring_spec.rb +0 -225
- data/spec/spec_helper.rb +0 -60
- data/spec/testing/batch_test_importer_spec.rb +0 -693
- data/spec/testing/batch_test_runner_spec.rb +0 -307
- data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
- data/spec/testing/test_result_comparator_spec.rb +0 -392
- data/spec/testing/test_scenario_spec.rb +0 -113
- data/spec/thread_safety_spec.rb +0 -490
- data/spec/thread_safety_spec.rb.broken +0 -878
- data/spec/versioning/adapter_spec.rb +0 -156
- data/spec/versioning_spec.rb +0 -1030
- data/spec/web/middleware/auth_middleware_spec.rb +0 -133
- data/spec/web/middleware/permission_middleware_spec.rb +0 -247
- data/spec/web_ui_rack_spec.rb +0 -2134
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DecisionAgent
|
|
4
|
+
module Dsl
|
|
5
|
+
module Operators
|
|
6
|
+
# Handles geospatial operators: within_radius, in_polygon
|
|
7
|
+
module GeospatialOperators
|
|
8
|
+
def self.handle(op, actual_value, expected_value, geospatial_cache: nil, geospatial_cache_mutex: nil)
|
|
9
|
+
case op
|
|
10
|
+
when "within_radius"
|
|
11
|
+
# Checks if point is within radius of center point
|
|
12
|
+
point = parse_coordinates(actual_value)
|
|
13
|
+
return false unless point
|
|
14
|
+
|
|
15
|
+
params = parse_radius_params(expected_value)
|
|
16
|
+
return false unless params
|
|
17
|
+
|
|
18
|
+
# Cache geospatial distance calculations
|
|
19
|
+
distance = get_cached_distance(point, params[:center], geospatial_cache: geospatial_cache,
|
|
20
|
+
geospatial_cache_mutex: geospatial_cache_mutex)
|
|
21
|
+
distance <= params[:radius]
|
|
22
|
+
|
|
23
|
+
when "in_polygon"
|
|
24
|
+
# Checks if point is inside a polygon using ray casting algorithm
|
|
25
|
+
point = parse_coordinates(actual_value)
|
|
26
|
+
return false unless point
|
|
27
|
+
|
|
28
|
+
polygon = parse_polygon(expected_value)
|
|
29
|
+
return false unless polygon
|
|
30
|
+
return false if polygon.size < 3 # Need at least 3 vertices
|
|
31
|
+
|
|
32
|
+
point_in_polygon?(point, polygon)
|
|
33
|
+
end
|
|
34
|
+
# Returns nil if not handled by this module
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Parse coordinates from various formats
|
|
38
|
+
def self.parse_coordinates(value)
|
|
39
|
+
case value
|
|
40
|
+
when Hash
|
|
41
|
+
lat = value[:lat] || value["lat"]
|
|
42
|
+
lon = value[:lon] || value["lon"]
|
|
43
|
+
return nil unless lat && lon
|
|
44
|
+
|
|
45
|
+
{ lat: lat.to_f, lon: lon.to_f }
|
|
46
|
+
when Array
|
|
47
|
+
return nil unless value.size >= 2
|
|
48
|
+
|
|
49
|
+
{ lat: value[0].to_f, lon: value[1].to_f }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Parse radius parameters
|
|
54
|
+
def self.parse_radius_params(value)
|
|
55
|
+
return nil unless value.is_a?(Hash)
|
|
56
|
+
|
|
57
|
+
center = value[:center] || value["center"]
|
|
58
|
+
radius = value[:radius] || value["radius"]
|
|
59
|
+
return nil unless center && radius
|
|
60
|
+
|
|
61
|
+
center_coords = parse_coordinates(center)
|
|
62
|
+
return nil unless center_coords
|
|
63
|
+
|
|
64
|
+
{ center: center_coords, radius: radius.to_f }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Parse polygon from various formats
|
|
68
|
+
def self.parse_polygon(value)
|
|
69
|
+
return nil unless value.is_a?(Array)
|
|
70
|
+
return nil if value.empty?
|
|
71
|
+
|
|
72
|
+
value.map { |v| parse_coordinates(v) }.compact
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Get cached distance between two points
|
|
76
|
+
def self.get_cached_distance(point1, point2, geospatial_cache: nil, geospatial_cache_mutex: nil)
|
|
77
|
+
cache = geospatial_cache
|
|
78
|
+
mutex = geospatial_cache_mutex
|
|
79
|
+
if cache.nil? || mutex.nil?
|
|
80
|
+
cache = ConditionEvaluator.instance_variable_get(:@geospatial_cache)
|
|
81
|
+
mutex = ConditionEvaluator.instance_variable_get(:@geospatial_cache_mutex)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Create cache key from sorted coordinates
|
|
85
|
+
key = [[point1[:lat], point1[:lon]], [point2[:lat], point2[:lon]]].sort.hash
|
|
86
|
+
cached = cache[key]
|
|
87
|
+
return cached if cached
|
|
88
|
+
|
|
89
|
+
mutex.synchronize do
|
|
90
|
+
cache[key] ||= calculate_distance(point1, point2)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Calculate distance between two points using Haversine formula
|
|
95
|
+
def self.calculate_distance(point1, point2)
|
|
96
|
+
Helpers::GeospatialHelpers.haversine_distance(point1, point2)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Check if point is inside polygon using ray casting algorithm
|
|
100
|
+
def self.point_in_polygon?(point, polygon)
|
|
101
|
+
Helpers::GeospatialHelpers.point_in_polygon?(point, polygon)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DecisionAgent
|
|
4
|
+
module Dsl
|
|
5
|
+
module Operators
|
|
6
|
+
# Handles mathematical operators: trigonometric, exponential, logarithmic, rounding, etc.
|
|
7
|
+
module MathematicalOperators
|
|
8
|
+
def self.handle(op, actual_value, expected_value, param_cache: nil, param_cache_mutex: nil)
|
|
9
|
+
case op
|
|
10
|
+
# Trigonometric functions
|
|
11
|
+
when "sin"
|
|
12
|
+
return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
|
|
13
|
+
|
|
14
|
+
Base.epsilon_equal?(Math.sin(actual_value), expected_value)
|
|
15
|
+
when "cos"
|
|
16
|
+
return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
|
|
17
|
+
|
|
18
|
+
Base.epsilon_equal?(Math.cos(actual_value), expected_value)
|
|
19
|
+
when "tan"
|
|
20
|
+
return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
|
|
21
|
+
|
|
22
|
+
Base.epsilon_equal?(Math.tan(actual_value), expected_value)
|
|
23
|
+
when "asin"
|
|
24
|
+
return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
|
|
25
|
+
return false if actual_value < -1 || actual_value > 1
|
|
26
|
+
|
|
27
|
+
Base.epsilon_equal?(Math.asin(actual_value), expected_value)
|
|
28
|
+
when "acos"
|
|
29
|
+
return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
|
|
30
|
+
return false if actual_value < -1 || actual_value > 1
|
|
31
|
+
|
|
32
|
+
Base.epsilon_equal?(Math.acos(actual_value), expected_value)
|
|
33
|
+
when "atan"
|
|
34
|
+
return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
|
|
35
|
+
|
|
36
|
+
Base.epsilon_equal?(Math.atan(actual_value), expected_value)
|
|
37
|
+
when "atan2"
|
|
38
|
+
return false unless actual_value.is_a?(Numeric)
|
|
39
|
+
|
|
40
|
+
params = parse_atan2_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
|
|
41
|
+
return false unless params
|
|
42
|
+
|
|
43
|
+
Base.epsilon_equal?(Math.atan2(actual_value, params[:y]), params[:result])
|
|
44
|
+
when "sinh"
|
|
45
|
+
return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
|
|
46
|
+
|
|
47
|
+
Base.epsilon_equal?(Math.sinh(actual_value), expected_value)
|
|
48
|
+
when "cosh"
|
|
49
|
+
return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
|
|
50
|
+
|
|
51
|
+
Base.epsilon_equal?(Math.cosh(actual_value), expected_value)
|
|
52
|
+
when "tanh"
|
|
53
|
+
return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
|
|
54
|
+
|
|
55
|
+
Base.epsilon_equal?(Math.tanh(actual_value), expected_value)
|
|
56
|
+
|
|
57
|
+
# Exponential and logarithmic functions
|
|
58
|
+
when "sqrt"
|
|
59
|
+
return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
|
|
60
|
+
return false if actual_value.negative?
|
|
61
|
+
|
|
62
|
+
Base.epsilon_equal?(Math.sqrt(actual_value), expected_value)
|
|
63
|
+
when "cbrt"
|
|
64
|
+
return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
|
|
65
|
+
|
|
66
|
+
result = if actual_value.negative?
|
|
67
|
+
-((-actual_value)**(1.0 / 3))
|
|
68
|
+
else
|
|
69
|
+
actual_value**(1.0 / 3)
|
|
70
|
+
end
|
|
71
|
+
Base.epsilon_equal?(result, expected_value)
|
|
72
|
+
when "power"
|
|
73
|
+
return false unless actual_value.is_a?(Numeric)
|
|
74
|
+
|
|
75
|
+
params = parse_power_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
|
|
76
|
+
return false unless params
|
|
77
|
+
|
|
78
|
+
Base.epsilon_equal?(actual_value**params[:exponent], params[:result])
|
|
79
|
+
when "exp"
|
|
80
|
+
return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
|
|
81
|
+
|
|
82
|
+
Base.epsilon_equal?(Math.exp(actual_value), expected_value)
|
|
83
|
+
when "log"
|
|
84
|
+
return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
|
|
85
|
+
return false if actual_value <= 0
|
|
86
|
+
|
|
87
|
+
Base.epsilon_equal?(Math.log(actual_value), expected_value)
|
|
88
|
+
when "log10"
|
|
89
|
+
return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
|
|
90
|
+
return false if actual_value <= 0
|
|
91
|
+
|
|
92
|
+
Base.epsilon_equal?(Math.log10(actual_value), expected_value)
|
|
93
|
+
when "log2"
|
|
94
|
+
return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
|
|
95
|
+
return false if actual_value <= 0
|
|
96
|
+
|
|
97
|
+
Base.epsilon_equal?(Math.log(actual_value) / Math.log(2), expected_value)
|
|
98
|
+
|
|
99
|
+
# Rounding and absolute value functions
|
|
100
|
+
when "round"
|
|
101
|
+
return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
|
|
102
|
+
|
|
103
|
+
actual_value.round == expected_value
|
|
104
|
+
when "floor"
|
|
105
|
+
return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
|
|
106
|
+
|
|
107
|
+
actual_value.floor == expected_value
|
|
108
|
+
when "ceil"
|
|
109
|
+
return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
|
|
110
|
+
|
|
111
|
+
actual_value.ceil == expected_value
|
|
112
|
+
when "abs"
|
|
113
|
+
return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
|
|
114
|
+
|
|
115
|
+
actual_value.abs == expected_value
|
|
116
|
+
when "truncate"
|
|
117
|
+
return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
|
|
118
|
+
|
|
119
|
+
actual_value.truncate == expected_value
|
|
120
|
+
|
|
121
|
+
# Advanced mathematical functions
|
|
122
|
+
when "factorial"
|
|
123
|
+
return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
|
|
124
|
+
return false if actual_value.negative? || !actual_value.integer?
|
|
125
|
+
|
|
126
|
+
(1..actual_value.to_i).reduce(1, :*) == expected_value
|
|
127
|
+
when "gcd"
|
|
128
|
+
return false unless actual_value.is_a?(Numeric) && actual_value.integer?
|
|
129
|
+
|
|
130
|
+
params = parse_gcd_lcm_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
|
|
131
|
+
return false unless params && params[:other].integer?
|
|
132
|
+
|
|
133
|
+
actual_value.to_i.gcd(params[:other].to_i) == params[:result]
|
|
134
|
+
when "lcm"
|
|
135
|
+
return false unless actual_value.is_a?(Numeric) && actual_value.integer?
|
|
136
|
+
|
|
137
|
+
params = parse_gcd_lcm_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
|
|
138
|
+
return false unless params && params[:other].integer?
|
|
139
|
+
|
|
140
|
+
actual_value.to_i.lcm(params[:other].to_i) == params[:result]
|
|
141
|
+
end
|
|
142
|
+
# Returns nil if not handled by this module
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Parse atan2 parameters
|
|
146
|
+
def self.parse_atan2_params(value, param_cache: nil, param_cache_mutex: nil)
|
|
147
|
+
normalized = Base.normalize_params_to_hash(value, %i[y result])
|
|
148
|
+
return nil unless normalized.is_a?(Hash)
|
|
149
|
+
|
|
150
|
+
cache = param_cache
|
|
151
|
+
mutex = param_cache_mutex
|
|
152
|
+
if cache.nil? || mutex.nil?
|
|
153
|
+
cache = ConditionEvaluator.instance_variable_get(:@param_cache)
|
|
154
|
+
mutex = ConditionEvaluator.instance_variable_get(:@param_cache_mutex)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
cache_key = Base.normalize_param_cache_key(normalized, "atan2")
|
|
158
|
+
cached = cache[cache_key]
|
|
159
|
+
return cached if cached
|
|
160
|
+
|
|
161
|
+
mutex.synchronize do
|
|
162
|
+
cache[cache_key] ||= parse_atan2_params_impl(normalized)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def self.parse_atan2_params_impl(value)
|
|
167
|
+
y = value[:y] || value["y"]
|
|
168
|
+
result = value[:result] || value["result"]
|
|
169
|
+
return nil unless y && !result.nil?
|
|
170
|
+
|
|
171
|
+
{ y: y, result: result }
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Parse power parameters
|
|
175
|
+
def self.parse_power_params(value, param_cache: nil, param_cache_mutex: nil)
|
|
176
|
+
normalized = Base.normalize_params_to_hash(value, %i[exponent result])
|
|
177
|
+
return nil unless normalized.is_a?(Hash)
|
|
178
|
+
|
|
179
|
+
cache = param_cache
|
|
180
|
+
mutex = param_cache_mutex
|
|
181
|
+
if cache.nil? || mutex.nil?
|
|
182
|
+
cache = ConditionEvaluator.instance_variable_get(:@param_cache)
|
|
183
|
+
mutex = ConditionEvaluator.instance_variable_get(:@param_cache_mutex)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
cache_key = Base.normalize_param_cache_key(normalized, "power")
|
|
187
|
+
cached = cache[cache_key]
|
|
188
|
+
return cached if cached
|
|
189
|
+
|
|
190
|
+
mutex.synchronize do
|
|
191
|
+
cache[cache_key] ||= parse_power_params_impl(normalized)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def self.parse_power_params_impl(value)
|
|
196
|
+
exponent = value[:exponent] || value["exponent"]
|
|
197
|
+
result = value[:result] || value["result"]
|
|
198
|
+
return nil unless exponent && !result.nil?
|
|
199
|
+
|
|
200
|
+
{ exponent: exponent, result: result }
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Parse gcd/lcm parameters
|
|
204
|
+
def self.parse_gcd_lcm_params(value, param_cache: nil, param_cache_mutex: nil)
|
|
205
|
+
normalized = Base.normalize_params_to_hash(value, %i[other result])
|
|
206
|
+
return nil unless normalized.is_a?(Hash)
|
|
207
|
+
|
|
208
|
+
cache = param_cache
|
|
209
|
+
mutex = param_cache_mutex
|
|
210
|
+
if cache.nil? || mutex.nil?
|
|
211
|
+
cache = ConditionEvaluator.instance_variable_get(:@param_cache)
|
|
212
|
+
mutex = ConditionEvaluator.instance_variable_get(:@param_cache_mutex)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
cache_key = Base.normalize_param_cache_key(normalized, "gcd_lcm")
|
|
216
|
+
cached = cache[cache_key]
|
|
217
|
+
return cached if cached
|
|
218
|
+
|
|
219
|
+
mutex.synchronize do
|
|
220
|
+
cache[cache_key] ||= parse_gcd_lcm_params_impl(normalized)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def self.parse_gcd_lcm_params_impl(value)
|
|
225
|
+
other = value[:other] || value["other"]
|
|
226
|
+
result = value[:result] || value["result"]
|
|
227
|
+
return nil unless other && !result.nil?
|
|
228
|
+
|
|
229
|
+
{ other: other, result: result }
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DecisionAgent
|
|
4
|
+
module Dsl
|
|
5
|
+
module Operators
|
|
6
|
+
# Handles moving window calculation operators: moving_average, moving_sum, moving_max, moving_min
|
|
7
|
+
module MovingWindowOperators
|
|
8
|
+
def self.handle(op, actual_value, expected_value, param_cache: nil, param_cache_mutex: nil)
|
|
9
|
+
case op
|
|
10
|
+
when "moving_average"
|
|
11
|
+
# Calculates moving average over window
|
|
12
|
+
return false unless actual_value.is_a?(Array)
|
|
13
|
+
return false if actual_value.empty?
|
|
14
|
+
|
|
15
|
+
numeric_array = actual_value.select { |v| v.is_a?(Numeric) }
|
|
16
|
+
return false if numeric_array.empty?
|
|
17
|
+
|
|
18
|
+
params = parse_moving_window_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
|
|
19
|
+
return false unless params
|
|
20
|
+
|
|
21
|
+
window = [params[:window], numeric_array.size].min
|
|
22
|
+
return false if window < 1
|
|
23
|
+
|
|
24
|
+
window_array = numeric_array.slice(-window, window)
|
|
25
|
+
moving_avg = window_array.sum.to_f / window
|
|
26
|
+
compare_moving_window_result(moving_avg, params)
|
|
27
|
+
|
|
28
|
+
when "moving_sum"
|
|
29
|
+
# Calculates moving sum over window
|
|
30
|
+
return false unless actual_value.is_a?(Array)
|
|
31
|
+
return false if actual_value.empty?
|
|
32
|
+
|
|
33
|
+
numeric_array = actual_value.select { |v| v.is_a?(Numeric) }
|
|
34
|
+
return false if numeric_array.empty?
|
|
35
|
+
|
|
36
|
+
params = parse_moving_window_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
|
|
37
|
+
return false unless params
|
|
38
|
+
|
|
39
|
+
window = [params[:window], numeric_array.size].min
|
|
40
|
+
return false if window < 1
|
|
41
|
+
|
|
42
|
+
window_array = numeric_array.slice(-window, window)
|
|
43
|
+
moving_sum = window_array.sum
|
|
44
|
+
compare_moving_window_result(moving_sum, params)
|
|
45
|
+
|
|
46
|
+
when "moving_max"
|
|
47
|
+
# Calculates moving max over window
|
|
48
|
+
return false unless actual_value.is_a?(Array)
|
|
49
|
+
return false if actual_value.empty?
|
|
50
|
+
|
|
51
|
+
numeric_array = actual_value.select { |v| v.is_a?(Numeric) }
|
|
52
|
+
return false if numeric_array.empty?
|
|
53
|
+
|
|
54
|
+
params = parse_moving_window_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
|
|
55
|
+
return false unless params
|
|
56
|
+
|
|
57
|
+
window = [params[:window], numeric_array.size].min
|
|
58
|
+
return false if window < 1
|
|
59
|
+
|
|
60
|
+
window_array = numeric_array.slice(-window, window)
|
|
61
|
+
moving_max = window_array.max
|
|
62
|
+
compare_moving_window_result(moving_max, params)
|
|
63
|
+
|
|
64
|
+
when "moving_min"
|
|
65
|
+
# Calculates moving min over window
|
|
66
|
+
return false unless actual_value.is_a?(Array)
|
|
67
|
+
return false if actual_value.empty?
|
|
68
|
+
|
|
69
|
+
numeric_array = actual_value.select { |v| v.is_a?(Numeric) }
|
|
70
|
+
return false if numeric_array.empty?
|
|
71
|
+
|
|
72
|
+
params = parse_moving_window_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
|
|
73
|
+
return false unless params
|
|
74
|
+
|
|
75
|
+
window = [params[:window], numeric_array.size].min
|
|
76
|
+
return false if window < 1
|
|
77
|
+
|
|
78
|
+
window_array = numeric_array.slice(-window, window)
|
|
79
|
+
moving_min = window_array.min
|
|
80
|
+
compare_moving_window_result(moving_min, params)
|
|
81
|
+
end
|
|
82
|
+
# Returns nil if not handled by this module
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Parse moving window parameters
|
|
86
|
+
def self.parse_moving_window_params(value, param_cache: nil, param_cache_mutex: nil)
|
|
87
|
+
return nil unless value.is_a?(Hash)
|
|
88
|
+
|
|
89
|
+
# Normalize to hash (already a hash, but normalize keys)
|
|
90
|
+
normalized = Base.normalize_params_to_hash(value, [])
|
|
91
|
+
|
|
92
|
+
cache = param_cache
|
|
93
|
+
mutex = param_cache_mutex
|
|
94
|
+
if cache.nil? || mutex.nil?
|
|
95
|
+
cache = ConditionEvaluator.instance_variable_get(:@param_cache)
|
|
96
|
+
mutex = ConditionEvaluator.instance_variable_get(:@param_cache_mutex)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
cache_key = Base.normalize_param_cache_key(normalized, "moving_window")
|
|
100
|
+
cached = cache[cache_key]
|
|
101
|
+
return cached if cached
|
|
102
|
+
|
|
103
|
+
mutex.synchronize do
|
|
104
|
+
cache[cache_key] ||= parse_moving_window_params_impl(normalized)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def self.parse_moving_window_params_impl(value)
|
|
109
|
+
window = value[:window] || value["window"]
|
|
110
|
+
return nil unless window.is_a?(Numeric) && window.positive?
|
|
111
|
+
|
|
112
|
+
{
|
|
113
|
+
window: window.to_i,
|
|
114
|
+
threshold: value[:threshold] || value["threshold"],
|
|
115
|
+
gt: value[:gt] || value["gt"],
|
|
116
|
+
lt: value[:lt] || value["lt"],
|
|
117
|
+
gte: value[:gte] || value["gte"],
|
|
118
|
+
lte: value[:lte] || value["lte"]
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Compare moving window result
|
|
123
|
+
def self.compare_moving_window_result(actual, params)
|
|
124
|
+
result = true
|
|
125
|
+
result &&= (actual >= params[:threshold]) if params[:threshold]
|
|
126
|
+
result &&= (actual > params[:gt]) if params[:gt]
|
|
127
|
+
result &&= (actual < params[:lt]) if params[:lt]
|
|
128
|
+
result &&= (actual >= params[:gte]) if params[:gte]
|
|
129
|
+
result &&= (actual <= params[:lte]) if params[:lte]
|
|
130
|
+
result
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DecisionAgent
|
|
4
|
+
module Dsl
|
|
5
|
+
module Operators
|
|
6
|
+
# Handles numeric operators: between, modulo
|
|
7
|
+
module NumericOperators
|
|
8
|
+
def self.handle(op, actual_value, expected_value, param_cache: nil, param_cache_mutex: nil)
|
|
9
|
+
case op
|
|
10
|
+
when "between"
|
|
11
|
+
# Checks if numeric value is between min and max (inclusive)
|
|
12
|
+
# expected_value should be [min, max] or {min: x, max: y}
|
|
13
|
+
if actual_value.is_a?(Numeric)
|
|
14
|
+
range = parse_range(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
|
|
15
|
+
range ? actual_value.between?(range[:min], range[:max]) : false
|
|
16
|
+
else
|
|
17
|
+
false
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
when "modulo"
|
|
21
|
+
# Checks if value modulo divisor equals remainder
|
|
22
|
+
# expected_value should be [divisor, remainder] or {divisor: x, remainder: y}
|
|
23
|
+
if actual_value.is_a?(Numeric)
|
|
24
|
+
params = parse_modulo_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
|
|
25
|
+
params ? (actual_value % params[:divisor]) == params[:remainder] : false
|
|
26
|
+
else
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
# Returns nil if not handled by this module
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Parse range for 'between' operator
|
|
34
|
+
# Accepts [min, max] or {min: x, max: y}
|
|
35
|
+
# Converts arrays to hash for consistency and better performance
|
|
36
|
+
def self.parse_range(value, param_cache: nil, param_cache_mutex: nil)
|
|
37
|
+
# Normalize to hash if array (for large params, hash is more efficient)
|
|
38
|
+
normalized_value = normalize_params_to_hash(value, %i[min max])
|
|
39
|
+
|
|
40
|
+
# Use provided caches or access ConditionEvaluator class variables
|
|
41
|
+
cache = param_cache
|
|
42
|
+
mutex = param_cache_mutex
|
|
43
|
+
|
|
44
|
+
if cache.nil? || mutex.nil?
|
|
45
|
+
cache = ConditionEvaluator.instance_variable_get(:@param_cache)
|
|
46
|
+
mutex = ConditionEvaluator.instance_variable_get(:@param_cache_mutex)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
cache_key = normalize_param_cache_key(normalized_value, "range")
|
|
50
|
+
|
|
51
|
+
# Fast path: check cache without lock
|
|
52
|
+
cached = cache[cache_key]
|
|
53
|
+
return cached if cached
|
|
54
|
+
|
|
55
|
+
# Slow path: parse and cache
|
|
56
|
+
mutex.synchronize do
|
|
57
|
+
cache[cache_key] ||= parse_range_impl(normalized_value)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.parse_range_impl(value)
|
|
62
|
+
return nil unless value.is_a?(Hash)
|
|
63
|
+
|
|
64
|
+
min = value[:min] || value["min"]
|
|
65
|
+
max = value[:max] || value["max"]
|
|
66
|
+
return nil unless min && max
|
|
67
|
+
|
|
68
|
+
{ min: min, max: max }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Parse modulo parameters
|
|
72
|
+
# Accepts [divisor, remainder] or {divisor: x, remainder: y}
|
|
73
|
+
# Converts arrays to hash for consistency and better performance
|
|
74
|
+
def self.parse_modulo_params(value, param_cache: nil, param_cache_mutex: nil)
|
|
75
|
+
# Normalize to hash if array (for large params, hash is more efficient)
|
|
76
|
+
normalized_value = normalize_params_to_hash(value, %i[divisor remainder])
|
|
77
|
+
|
|
78
|
+
# Use provided caches or access ConditionEvaluator class variables
|
|
79
|
+
cache = param_cache
|
|
80
|
+
mutex = param_cache_mutex
|
|
81
|
+
|
|
82
|
+
if cache.nil? || mutex.nil?
|
|
83
|
+
cache = ConditionEvaluator.instance_variable_get(:@param_cache)
|
|
84
|
+
mutex = ConditionEvaluator.instance_variable_get(:@param_cache_mutex)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
cache_key = normalize_param_cache_key(normalized_value, "modulo")
|
|
88
|
+
|
|
89
|
+
# Fast path: check cache without lock
|
|
90
|
+
cached = cache[cache_key]
|
|
91
|
+
return cached if cached
|
|
92
|
+
|
|
93
|
+
# Slow path: parse and cache
|
|
94
|
+
mutex.synchronize do
|
|
95
|
+
cache[cache_key] ||= parse_modulo_params_impl(normalized_value)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def self.parse_modulo_params_impl(value)
|
|
100
|
+
return nil unless value.is_a?(Hash)
|
|
101
|
+
|
|
102
|
+
divisor = value[:divisor] || value["divisor"]
|
|
103
|
+
remainder = value[:remainder] || value["remainder"]
|
|
104
|
+
return nil unless divisor && !remainder.nil?
|
|
105
|
+
|
|
106
|
+
{ divisor: divisor, remainder: remainder }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Use Base utilities
|
|
110
|
+
def self.normalize_params_to_hash(value, keys)
|
|
111
|
+
Base.normalize_params_to_hash(value, keys)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def self.normalize_param_cache_key(value, prefix)
|
|
115
|
+
Base.normalize_param_cache_key(value, prefix)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DecisionAgent
|
|
4
|
+
module Dsl
|
|
5
|
+
module Operators
|
|
6
|
+
# Handles rate calculation operators: rate_per_second, rate_per_minute, rate_per_hour
|
|
7
|
+
module RateOperators
|
|
8
|
+
def self.handle(op, actual_value, expected_value)
|
|
9
|
+
case op
|
|
10
|
+
when "rate_per_second"
|
|
11
|
+
# Calculates rate per second from array of timestamps
|
|
12
|
+
return false unless actual_value.is_a?(Array)
|
|
13
|
+
return false if actual_value.empty?
|
|
14
|
+
|
|
15
|
+
timestamps = actual_value.map { |ts| ConditionEvaluator.parse_date(ts) }.compact
|
|
16
|
+
return false if timestamps.size < 2
|
|
17
|
+
|
|
18
|
+
sorted_timestamps = timestamps.sort
|
|
19
|
+
time_span = sorted_timestamps.last - sorted_timestamps.first
|
|
20
|
+
return false if time_span <= 0
|
|
21
|
+
|
|
22
|
+
rate = timestamps.size.to_f / time_span
|
|
23
|
+
compare_rate_result(rate, expected_value)
|
|
24
|
+
|
|
25
|
+
when "rate_per_minute"
|
|
26
|
+
# Calculates rate per minute from array of timestamps
|
|
27
|
+
return false unless actual_value.is_a?(Array)
|
|
28
|
+
return false if actual_value.empty?
|
|
29
|
+
|
|
30
|
+
timestamps = actual_value.map { |ts| ConditionEvaluator.parse_date(ts) }.compact
|
|
31
|
+
return false if timestamps.size < 2
|
|
32
|
+
|
|
33
|
+
sorted_timestamps = timestamps.sort
|
|
34
|
+
time_span = sorted_timestamps.last - sorted_timestamps.first
|
|
35
|
+
return false if time_span <= 0
|
|
36
|
+
|
|
37
|
+
rate = (timestamps.size.to_f / time_span) * 60.0
|
|
38
|
+
compare_rate_result(rate, expected_value)
|
|
39
|
+
|
|
40
|
+
when "rate_per_hour"
|
|
41
|
+
# Calculates rate per hour from array of timestamps
|
|
42
|
+
return false unless actual_value.is_a?(Array)
|
|
43
|
+
return false if actual_value.empty?
|
|
44
|
+
|
|
45
|
+
timestamps = actual_value.map { |ts| ConditionEvaluator.parse_date(ts) }.compact
|
|
46
|
+
return false if timestamps.size < 2
|
|
47
|
+
|
|
48
|
+
sorted_timestamps = timestamps.sort
|
|
49
|
+
time_span = sorted_timestamps.last - sorted_timestamps.first
|
|
50
|
+
return false if time_span <= 0
|
|
51
|
+
|
|
52
|
+
rate = (timestamps.size.to_f / time_span) * 3600.0
|
|
53
|
+
compare_rate_result(rate, expected_value)
|
|
54
|
+
end
|
|
55
|
+
# Returns nil if not handled by this module
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Compare rate result
|
|
59
|
+
def self.compare_rate_result(actual, expected)
|
|
60
|
+
ConditionEvaluator.compare_rate_result(actual, expected)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|