diff --git a/CHANGELOG.md b/CHANGELOG.md index e98e93e..f8426fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.8.0] - 2024-10-15 +### Added +- Date element + ## [1.7.0] - 2022-12-12 ### Added - Array element diff --git a/README.md b/README.md index 3219a09..82f7a9e 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,11 @@ Our DSL consists of these components: 43.56% -20% +* **date**, a sequence of [0-9]{1,2}\/[0-9]{1,2}\/[0-9]{4}. Examples: + + 9/2/2024 + 31/12/2024 + * **array**, a comma separated list of integer, decimal or string elements wrapped by [] (square brackets) [1,2,3] @@ -81,7 +86,7 @@ Our DSL consists of these components: !Apple_Tree !what_i5_YOUR_name? -* **element** is one of a **string**, **percentage**, **decimal**, **integer**, **array** or **attribute**. +* **element** is one of a **string**, **date**, **percentage**, **decimal**, **integer**, **array** or **attribute**. * **comparison** consists of an **element**, followed by one of ==, !=, <, >, <=, >=, includes, excludes (the operator) followed by another **element**. When a comparison is evaluated, the 2 elements are compared using the operater @@ -92,6 +97,7 @@ Our DSL consists of these components: alpha != delta 8 >= 1 [1,2,3] includes 3 + start_date > 1/9/2024 * **boolean** expression consists of a left side, followed by one of _and_, _or_ (the operator), followed by a right side. diff --git a/lib/boolean_dsl/evaluator.rb b/lib/boolean_dsl/evaluator.rb index 49745ef..16759a4 100644 --- a/lib/boolean_dsl/evaluator.rb +++ b/lib/boolean_dsl/evaluator.rb @@ -26,7 +26,7 @@ def evaluate(tree) if context.key?(tree[:attribute].to_s) context[tree[:attribute].to_s] else - raise BooleanDsl::EvaluationFailed.new("Context does not have key #{tree[:attribute]}") + raise BooleanDsl::EvaluationFailed, "Context does not have key #{tree[:attribute]}" end elsif tree.key?(:negation) !evaluate(tree[:negation]) @@ -40,6 +40,12 @@ def evaluate(tree) BigDecimal(tree[:decimal]) elsif tree.key?(:integer) Integer(tree[:integer], 10) + elsif tree.key?(:date) + begin + Date.parse(tree[:date]) + rescue Date::Error => e + raise BooleanDsl::EvaluationFailed, "#{e.message.upcase_first} #{tree[:date]}" + end end end @@ -68,6 +74,8 @@ def evaluate_comparison(left, operator, right) when 'excludes' !left.include?(right) end + rescue StandardError + raise BooleanDsl::EvaluationFailed, "Invalid comparison" end # Given a left and right expression and an operator, compares left and right using the operator. diff --git a/lib/boolean_dsl/parser.rb b/lib/boolean_dsl/parser.rb index 1d6b7e7..730ccc0 100644 --- a/lib/boolean_dsl/parser.rb +++ b/lib/boolean_dsl/parser.rb @@ -5,13 +5,16 @@ class BooleanDsl::Parser < Parslet::Parser # Literals rule(:sign) { match('[+-]') } - rule(:digits) { match('[0-9]').repeat(1) } + rule(:digit) { match('[0-9]') } + rule(:digits) { digit.repeat(1) } rule(:decimal_fragment) { digits >> str(".") >> digits } rule(:integer) { (sign.maybe >> digits).as(:integer) >> space? } rule(:decimal) { (sign.maybe >> decimal_fragment).as(:decimal) >> space? } rule(:percentage) { (sign.maybe >> (decimal_fragment | digits) >> str("%")).as(:percentage) >> space? } + rule(:date) { (digit.repeat(1,2) >> str("/") >> digit.repeat(1,2) >> str("/") >> digit.repeat(4,4)).as(:date) } + rule(:string_content) { (str("'").absent? >> any).repeat } rule(:string) { str("'") >> string_content.as(:string) >> str("'") >> space? } @@ -27,7 +30,7 @@ class BooleanDsl::Parser < Parslet::Parser rule(:negation) { str('!') >> attribute.as(:negation) } # Elements - rule(:element) { negation | percentage | decimal | integer | string | array | attribute } + rule(:element) { negation | date | percentage | decimal | integer | string | array | attribute } # Booleans are rules that will evaluate to a true or false result rule(:boolean) { value_comparison | negation | attribute } diff --git a/spec/evaluator/evaluate_spec.rb b/spec/evaluator/evaluate_spec.rb index c7a59ba..788042c 100644 --- a/spec/evaluator/evaluate_spec.rb +++ b/spec/evaluator/evaluate_spec.rb @@ -8,6 +8,34 @@ specify { expect(evaluator.evaluate(percentage: '89%')).to eq(0.89) } specify { expect(evaluator.evaluate(string: 'alpha5')).to eq('alpha5') } + describe 'dates' do + subject { evaluator.evaluate(tree) } + + let(:tree) { { date: date } } + + context 'where date has leading zeros' do + let(:date) { '02/03/2024' } + specify { expect(subject).to eq(Date.new(2024,3,2)) } + end + + context 'where date does not have leading zeros' do + let(:date) { '2/3/2024' } + specify { expect(subject).to eq(Date.new(2024,3,2)) } + end + + context 'where date has mix of leading zeros' do + let(:date) { '2/03/2024' } + specify { expect(subject).to eq(Date.new(2024,3,2)) } + end + + context 'where date is invalid' do + let(:date) { '32/6/2024' } + specify do + expect { subject }.to raise_error(BooleanDsl::EvaluationFailed, 'Invalid date 32/6/2024') + end + end + end + describe 'evaluate array' do subject { evaluator.evaluate(tree) } diff --git a/spec/evaluator_spec.rb b/spec/evaluator_spec.rb index f43988d..4fd3a18 100644 --- a/spec/evaluator_spec.rb +++ b/spec/evaluator_spec.rb @@ -30,6 +30,17 @@ specify { expect(evaluator.evaluate_comparison([1, 'alpha'], 'excludes', 0)).to be_truthy } specify { expect(evaluator.evaluate_comparison([1, 'alpha'], 'excludes', 'alpha')).to be_falsey } specify { expect(evaluator.evaluate_comparison([1, 'alpha'], 'excludes', 'beta')).to be_truthy } + + specify { expect(evaluator.evaluate_comparison(Date.new(2024,9,1), '==', Date.new(2024,9,1))).to be_truthy } + specify { expect(evaluator.evaluate_comparison(Date.new(2024,9,1), '<', Date.new(2024,9,2))).to be_truthy } + specify { expect(evaluator.evaluate_comparison(Date.new(2024,9,1), '>', Date.new(2024,8,31))).to be_truthy } + specify { expect(evaluator.evaluate_comparison(Date.new(2024,9,1), '<=', Date.new(2024,9,1))).to be_truthy } + specify { expect(evaluator.evaluate_comparison(Date.new(2024,9,1), '>=', Date.new(2024,9,1))).to be_truthy } + + specify do + expect { evaluator.evaluate_comparison(Date.new(2024,9,1), '>', 'a string') } + .to raise_error(BooleanDsl::EvaluationFailed, 'Invalid comparison') + end end context '#evaluate_boolean' do @@ -71,6 +82,7 @@ def outcome_for(expression, context_hash = {}) specify { expect(outcome_for("['alpha','beta'] excludes 'beta'")).to be_falsey } specify { expect(outcome_for("[] excludes 'beta'")).to be_truthy } specify { expect(outcome_for("attribute_array excludes 'beta'", { "attribute_array" => ["alpha", "beta"]})).to be_falsey } + specify { expect(outcome_for("start_date > 1/9/2024", { 'start_date' => Date.new(2024,9,2) })).to be_truthy } end end diff --git a/spec/parser_spec.rb b/spec/parser_spec.rb index 5940add..4e7353b 100644 --- a/spec/parser_spec.rb +++ b/spec/parser_spec.rb @@ -44,6 +44,12 @@ specify { expect(parser.parse_with_debug("'I am, 12345, \"you\" are' ")).to eq(string: 'I am, 12345, "you" are') } end + context 'date literals' do + specify { expect(parser.parse_with_debug("31/12/2024")).to eq(date: "31/12/2024") } + specify { expect(parser.parse_with_debug("1/1/2024")).to eq(date: "1/1/2024") } + specify { expect(parser.parse_with_debug("02/03/2024")).to eq(date: "02/03/2024") } + end + context 'arrays' do specify { expect(parser.parse_with_debug("[]")).to eq(array: []) } specify { expect(parser.parse_with_debug("[1]")).to eq(array: [{ integer: "1" }]) }