Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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
Expand All @@ -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.
Expand Down
10 changes: 9 additions & 1 deletion lib/boolean_dsl/evaluator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand All @@ -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

Expand Down Expand Up @@ -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.
Expand Down
7 changes: 5 additions & 2 deletions lib/boolean_dsl/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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? }

Expand All @@ -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 }
Expand Down
28 changes: 28 additions & 0 deletions spec/evaluator/evaluate_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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) }

Expand Down
12 changes: 12 additions & 0 deletions spec/evaluator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

6 changes: 6 additions & 0 deletions spec/parser_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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" }]) }
Expand Down