winston 0.0.1 → 0.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.
@@ -0,0 +1,120 @@
1
+ require "winston"
2
+
3
+ describe "Sudoku Example" do
4
+ let(:csp) { Winston::CSP.new }
5
+
6
+ describe "Unique values for every row, column and quadrant" do
7
+ before do
8
+ domain = [1, 2, 3, 4, 5, 6, 7, 8, 9]
9
+
10
+ # Adding variables
11
+ 1.upto(9).each do |x|
12
+ 1.upto(9).each do |j|
13
+ csp.add_variable({ x: x, y: j }, domain: domain)
14
+ end
15
+ end
16
+
17
+ variables = csp.variables.keys
18
+
19
+ # add columns and rows constraints
20
+ 1.upto(9).each do |i|
21
+ column = variables.select { |name| name[:x] == i }
22
+ row = variables.select { |name| name[:y] == i }
23
+
24
+ csp.add_constraint constraint: Winston::Constraints::AllDifferent.new(variables: row, allow_nil: true)
25
+ csp.add_constraint constraint: Winston::Constraints::AllDifferent.new(variables: column, allow_nil: true)
26
+ end
27
+
28
+ # blocks
29
+ variables.each_slice(3).group_by { |i| (i[0][:y] -1) % 9 }.values.flatten.each_slice(9).each do |vars|
30
+ csp.add_constraint constraint: Winston::Constraints::AllDifferent.new(variables: vars, allow_nil: true)
31
+ end
32
+ end
33
+
34
+ it "should return a valid solution" do
35
+ expect(csp.solve).to eq({
36
+ {:x=>1, :y=>1}=>1,
37
+ {:x=>1, :y=>2}=>2,
38
+ {:x=>1, :y=>3}=>3,
39
+ {:x=>1, :y=>4}=>4,
40
+ {:x=>1, :y=>5}=>5,
41
+ {:x=>1, :y=>6}=>6,
42
+ {:x=>1, :y=>7}=>7,
43
+ {:x=>1, :y=>8}=>8,
44
+ {:x=>1, :y=>9}=>9,
45
+ {:x=>2, :y=>1}=>4,
46
+ {:x=>2, :y=>2}=>5,
47
+ {:x=>2, :y=>3}=>6,
48
+ {:x=>2, :y=>4}=>7,
49
+ {:x=>2, :y=>5}=>8,
50
+ {:x=>2, :y=>6}=>9,
51
+ {:x=>2, :y=>7}=>1,
52
+ {:x=>2, :y=>8}=>2,
53
+ {:x=>2, :y=>9}=>3,
54
+ {:x=>3, :y=>1}=>7,
55
+ {:x=>3, :y=>2}=>8,
56
+ {:x=>3, :y=>3}=>9,
57
+ {:x=>3, :y=>4}=>1,
58
+ {:x=>3, :y=>5}=>2,
59
+ {:x=>3, :y=>6}=>3,
60
+ {:x=>3, :y=>7}=>4,
61
+ {:x=>3, :y=>8}=>5,
62
+ {:x=>3, :y=>9}=>6,
63
+ {:x=>4, :y=>1}=>2,
64
+ {:x=>4, :y=>2}=>1,
65
+ {:x=>4, :y=>3}=>4,
66
+ {:x=>4, :y=>4}=>3,
67
+ {:x=>4, :y=>5}=>6,
68
+ {:x=>4, :y=>6}=>5,
69
+ {:x=>4, :y=>7}=>8,
70
+ {:x=>4, :y=>8}=>9,
71
+ {:x=>4, :y=>9}=>7,
72
+ {:x=>5, :y=>1}=>3,
73
+ {:x=>5, :y=>2}=>6,
74
+ {:x=>5, :y=>3}=>5,
75
+ {:x=>5, :y=>4}=>8,
76
+ {:x=>5, :y=>5}=>9,
77
+ {:x=>5, :y=>6}=>7,
78
+ {:x=>5, :y=>7}=>2,
79
+ {:x=>5, :y=>8}=>1,
80
+ {:x=>5, :y=>9}=>4,
81
+ {:x=>6, :y=>1}=>8,
82
+ {:x=>6, :y=>2}=>9,
83
+ {:x=>6, :y=>3}=>7,
84
+ {:x=>6, :y=>4}=>2,
85
+ {:x=>6, :y=>5}=>1,
86
+ {:x=>6, :y=>6}=>4,
87
+ {:x=>6, :y=>7}=>3,
88
+ {:x=>6, :y=>8}=>6,
89
+ {:x=>6, :y=>9}=>5,
90
+ {:x=>7, :y=>1}=>5,
91
+ {:x=>7, :y=>2}=>3,
92
+ {:x=>7, :y=>3}=>1,
93
+ {:x=>7, :y=>4}=>6,
94
+ {:x=>7, :y=>5}=>4,
95
+ {:x=>7, :y=>6}=>2,
96
+ {:x=>7, :y=>7}=>9,
97
+ {:x=>7, :y=>8}=>7,
98
+ {:x=>7, :y=>9}=>8,
99
+ {:x=>8, :y=>1}=>6,
100
+ {:x=>8, :y=>2}=>4,
101
+ {:x=>8, :y=>3}=>2,
102
+ {:x=>8, :y=>4}=>9,
103
+ {:x=>8, :y=>5}=>7,
104
+ {:x=>8, :y=>6}=>8,
105
+ {:x=>8, :y=>7}=>5,
106
+ {:x=>8, :y=>8}=>3,
107
+ {:x=>8, :y=>9}=>1,
108
+ {:x=>9, :y=>1}=>9,
109
+ {:x=>9, :y=>2}=>7,
110
+ {:x=>9, :y=>3}=>8,
111
+ {:x=>9, :y=>4}=>5,
112
+ {:x=>9, :y=>5}=>3,
113
+ {:x=>9, :y=>6}=>1,
114
+ {:x=>9, :y=>7}=>6,
115
+ {:x=>9, :y=>8}=>4,
116
+ {:x=>9, :y=>9}=>2
117
+ })
118
+ end
119
+ end
120
+ end
@@ -1,6 +1,6 @@
1
1
  require "winston"
2
2
 
3
- describe Winston::Backtrack do
3
+ describe Winston::Solvers::Backtrack do
4
4
  let(:csp) { Winston::CSP.new }
5
5
  subject { described_class.new(csp) }
6
6
 
@@ -44,5 +44,38 @@ describe Winston::Backtrack do
44
44
  end
45
45
  end
46
46
  end
47
+
48
+ context "with heuristics" do
49
+ it "uses MRV to pick the smallest remaining domain first" do
50
+ csp.add_variable :a, domain: [1, 2, 3]
51
+ csp.add_variable :b, domain: [1]
52
+ csp.add_variable :c, domain: [1, 2]
53
+
54
+ solver = described_class.new(csp, variable_strategy: :mrv)
55
+ result = solver.search
56
+
57
+ expect(result.keys).to eq([:b, :c, :a])
58
+ end
59
+
60
+ it "uses LCV to prefer values that leave more options" do
61
+ csp.add_variable :a, domain: [2, 1]
62
+ csp.add_variable :b, domain: [2, 3]
63
+ csp.add_constraint(:a, :b) { |a, b| b > a }
64
+
65
+ solver = described_class.new(csp, value_strategy: :lcv)
66
+ result = solver.search
67
+
68
+ expect(result[:a]).to eq(1)
69
+ end
70
+
71
+ it "supports forward checking" do
72
+ csp.add_variable :a, domain: [1]
73
+ csp.add_variable :b, domain: [1]
74
+ csp.add_constraint(:a, :b) { |a, b| a != b }
75
+
76
+ solver = described_class.new(csp, forward_checking: true)
77
+ expect(solver.search).to be(false)
78
+ end
79
+ end
47
80
  end
48
81
  end
@@ -4,7 +4,8 @@ describe Winston::Constraint do
4
4
 
5
5
  let(:variables) { nil }
6
6
  let(:predicate) { nil }
7
- subject { described_class.new(variables, predicate) }
7
+ let(:allow_nil) { false }
8
+ subject { described_class.new(variables: variables, predicate: predicate, allow_nil: allow_nil) }
8
9
 
9
10
  describe "#elegible_for?" do
10
11
  context "global" do
@@ -49,6 +50,50 @@ describe Winston::Constraint do
49
50
  expect(subject.elegible_for?(:a, { b: 2 })).to be(false)
50
51
  end
51
52
  end
53
+
54
+ context "allowing nil" do
55
+ let(:allow_nil) { true }
56
+
57
+ context "for specific variable" do
58
+ let(:variables) { [:a] }
59
+
60
+ it "should return true when that variable is changed" do
61
+ expect(subject.elegible_for?(:a, { a: nil })).to be(true)
62
+ end
63
+
64
+ it "should return false when that variable isn't the one changed" do
65
+ expect(subject.elegible_for?(:b, { a: 1, b: 2 })).to be(false)
66
+ end
67
+
68
+ it "should return true when that variable is changed but doesn't have a value" do
69
+ expect(subject.elegible_for?(:a, { b: 2 })).to be(true)
70
+ end
71
+ end
72
+
73
+ context "for multiple variables" do
74
+ let(:variables) { [:a, :b] }
75
+
76
+ it "should return true when one of those variables is changed" do
77
+ expect(subject.elegible_for?(:a, { a: 1, b: nil })).to be(true)
78
+ end
79
+
80
+ it "should return false when one of those variables isn't the one changed" do
81
+ expect(subject.elegible_for?(:c, { a: 1, b: 2 })).to be(false)
82
+ end
83
+
84
+ it "should return true when one of those variables is changed but doesn't have a value for every one of them" do
85
+ expect(subject.elegible_for?(:b, { b: 2 })).to be(true)
86
+ end
87
+
88
+ it "should return true when one of those variables is changed but doesn't have a value for every one of them" do
89
+ expect(subject.elegible_for?(:a, { b: 2 })).to be(true)
90
+ end
91
+
92
+ it "should return true when one of those variables is changed but doesn't have a value for any of them" do
93
+ expect(subject.elegible_for?(:a, {})).to be(true)
94
+ end
95
+ end
96
+ end
52
97
  end
53
98
 
54
99
  describe "#validate" do
@@ -3,13 +3,6 @@ require "winston"
3
3
  describe Winston::Constraints::AllDifferent do
4
4
  subject { described_class.new }
5
5
 
6
- describe "#elegible_for?" do
7
- it "should be elegible for everything" do
8
- expect(subject.elegible_for?(:a, {})).to be(true)
9
- expect(subject.elegible_for?(:b, { a: 1 })).to be(true)
10
- end
11
- end
12
-
13
6
  describe "#validate" do
14
7
  it "should return 'true' when all values are unique" do
15
8
  expect(subject.validate(a: 1, b: 2, c: 3, d: 4)).to be(true)
@@ -18,5 +11,17 @@ describe Winston::Constraints::AllDifferent do
18
11
  it "should return 'false' when not all values are unique" do
19
12
  expect(subject.validate(a: 1, b: 2, c: 2, d: 4)).to be(false)
20
13
  end
14
+
15
+ context "for specific variables" do
16
+ subject { described_class.new(variables: [:a, :b, :c]) }
17
+
18
+ it "should return 'true' when all values are unique" do
19
+ expect(subject.validate(a: 1, b: 2, c: 3, d: 3)).to be(true)
20
+ end
21
+
22
+ it "should return 'false' when not all values are unique" do
23
+ expect(subject.validate(a: 1, b: 2, c: 2, d: 4)).to be(false)
24
+ end
25
+ end
21
26
  end
22
27
  end
@@ -0,0 +1,35 @@
1
+ require "winston"
2
+
3
+ describe Winston::Constraints::NotInList do
4
+ subject { described_class.new(list: [ 4, 5, 6 ]) }
5
+
6
+ describe "#validate" do
7
+ it "should return 'true' none of the values are on the list" do
8
+ expect(subject.validate(a: 1, b: 2, c: 3, d: 3)).to be(true)
9
+ end
10
+
11
+ it "should return 'false' when any of the values are on the list" do
12
+ expect(subject.validate(a: 1, b: 2, c: 2, d: 4)).to be(false)
13
+ end
14
+
15
+ it "should return 'false' when all the values are on the list" do
16
+ expect(subject.validate(a: 4, b: 6, c: 5, d: 4)).to be(false)
17
+ end
18
+
19
+ context "for specific variables" do
20
+ subject { described_class.new(variables: [:a, :b, :c], list: [ 4, 5, 6 ]) }
21
+
22
+ it "should return 'true' when none of the values are on the list" do
23
+ expect(subject.validate(a: 1, b: 2, c: 3, d: 6)).to be(true)
24
+ end
25
+
26
+ it "should return 'false' when any of the values are on the list" do
27
+ expect(subject.validate(a: 1, b: 2, c: 5, d: 3)).to be(false)
28
+ end
29
+
30
+ it "should return 'false' when all the values are on the list" do
31
+ expect(subject.validate(a: 4, b: 6, c: 5, d: 3)).to be(false)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -28,7 +28,7 @@ describe Winston::CSP do
28
28
  describe "#add_constraint" do
29
29
  let(:constraint) { double("Constraint") }
30
30
  it "should build a constraint for the given block" do
31
- expect(Winston::Constraint).to receive(:new).with([:a, :b], an_instance_of(Proc)).once.and_return(constraint)
31
+ expect(Winston::Constraint).to receive(:new).with(variables: [:a, :b], allow_nil: false, predicate: an_instance_of(Proc)).once.and_return(constraint)
32
32
  subject.add_constraint(:a, :b) { true }
33
33
  expect(subject.constraints).to include(constraint)
34
34
  end
@@ -60,6 +60,25 @@ describe Winston::CSP do
60
60
  end
61
61
  end
62
62
 
63
+ describe "#domain_for" do
64
+ before do
65
+ subject.add_variable :a, value: 1
66
+ subject.add_variable :b, domain: [1, 2]
67
+ end
68
+
69
+ it "returns domain values for a given variable" do
70
+ expect(subject.domain_for(:b)).to eq([1, 2])
71
+ end
72
+
73
+ it "returns an empty list when a variable doesn't exist in the problem" do
74
+ expect(subject.domain_for(:c)).to be_empty
75
+ end
76
+
77
+ it "return an empty list when a variable doesn't have a domain" do
78
+ expect(subject.domain_for(:a)).to be_empty
79
+ end
80
+ end
81
+
63
82
  describe "#solve" do
64
83
  let(:solver) { double("Solver") }
65
84
  before do
@@ -68,8 +87,32 @@ describe Winston::CSP do
68
87
  end
69
88
 
70
89
  it "should pass a collection of preset variables to the solver" do
71
- expect(solver).to receive(:search).with(a: 1)
90
+ expect(solver).to receive(:search).with({ a: 1 })
72
91
  subject.solve(solver)
73
92
  end
93
+
94
+ it "builds a solver by name with options" do
95
+ subject.add_constraint(:a, :b) { |a, b| a < b }
96
+ result = subject.solve(:backtrack, variable_strategy: :mrv)
97
+
98
+ expect(result).to eq({ a: 1, b: 2 })
99
+ end
100
+
101
+ it "raises for an unknown solver name" do
102
+ expect { subject.solve(:unknown) }.to raise_error(ArgumentError, /Unknown solver/)
103
+ end
104
+
105
+ it "returns false when preset assignments violate a constraint" do
106
+ subject.add_constraint(:a) { |a| a > 2 }
107
+ expect(solver).to_not receive(:search)
108
+ expect(subject.solve(solver)).to be(false)
109
+ end
110
+
111
+ it "returns false when a constraint among preset variables is violated" do
112
+ subject.add_variable :c, value: 2
113
+ subject.add_constraint(:a, :c) { |a, c| a == c }
114
+ expect(solver).to_not receive(:search)
115
+ expect(subject.solve(solver)).to be(false)
116
+ end
74
117
  end
75
118
  end
@@ -0,0 +1,71 @@
1
+ require "winston"
2
+
3
+ describe Winston::DSL do
4
+ describe ".define" do
5
+ it "builds a CSP with variables and constraints" do
6
+ csp = Winston.define do
7
+ var :a, domain: [1, 2, 3]
8
+ var :b, domain: [1, 2, 3]
9
+ constraint(:a, :b) { |a, b| a > b }
10
+ end
11
+
12
+ expect(csp.solve).to eq({ a: 2, b: 1 })
13
+ end
14
+
15
+ it "supports named domains" do
16
+ csp = Winston.define do
17
+ domain :digits, [1, 2, 3]
18
+ var :a, domain: :digits
19
+ var :b, domain: :digits
20
+ constraint(:a, :b) { |a, b| a != b }
21
+ end
22
+
23
+ result = csp.solve
24
+ expect([1, 2, 3]).to include(result[:a])
25
+ expect([1, 2, 3]).to include(result[:b])
26
+ expect(result[:a]).to_not eq(result[:b])
27
+ end
28
+
29
+ it "uses named constraints via use_constraint" do
30
+ csp = Winston.define do
31
+ var :a, domain: [1, 2]
32
+ var :b, domain: [1, 2]
33
+ use_constraint :all_different, :a, :b
34
+ end
35
+
36
+ expect(csp.solve).to eq({ a: 1, b: 2 })
37
+ end
38
+
39
+ it "passes options to named constraints" do
40
+ csp = Winston.define do
41
+ var :a, domain: [1, 2, 3]
42
+ use_constraint :not_in_list, :a, list: [1, 2]
43
+ end
44
+
45
+ expect(csp.solve).to eq({ a: 3 })
46
+ end
47
+ end
48
+
49
+ describe ".register_constraint" do
50
+ it "registers and uses a custom constraint by name" do
51
+ custom = Class.new(Winston::Constraint) do
52
+ def validate(assignments)
53
+ values = values_at(assignments)
54
+ values.all? { |v| v == 2 }
55
+ end
56
+ end
57
+
58
+ Winston.register_constraint(:all_twos) do |variables, allow_nil, **_options|
59
+ custom.new(variables: variables, allow_nil: allow_nil)
60
+ end
61
+
62
+ csp = Winston.define do
63
+ var :a, domain: [1, 2]
64
+ var :b, domain: [1, 2]
65
+ use_constraint :all_twos, :a, :b
66
+ end
67
+
68
+ expect(csp.solve).to eq({ a: 2, b: 2 })
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,47 @@
1
+ require "winston"
2
+
3
+ describe Winston::Heuristics do
4
+ let(:csp) { Winston::CSP.new }
5
+
6
+ describe ".mrv" do
7
+ it "picks the variable with the fewest remaining values" do
8
+ csp.add_variable :a, domain: [1, 2, 3]
9
+ csp.add_variable :b, domain: [1, 2]
10
+ csp.add_variable :c, domain: [1, 2, 3]
11
+ csp.add_constraint(:a) { |a| a == 1 }
12
+
13
+ vars = csp.variables.each_value.to_a
14
+ chosen = described_class.mrv.call(vars, {}, csp)
15
+
16
+ expect(chosen.name).to eq(:a)
17
+ end
18
+ end
19
+
20
+ describe ".lcv" do
21
+ it "orders values to leave more options for other variables" do
22
+ csp.add_variable :a, domain: [2, 1]
23
+ csp.add_variable :b, domain: [2, 3]
24
+ csp.add_constraint(:a, :b) { |a, b| b > a }
25
+
26
+ values = csp.domain_for(:a)
27
+ ordered = described_class.lcv.call(values, csp.variables[:a], {}, csp)
28
+
29
+ expect(ordered).to eq([1, 2])
30
+ end
31
+ end
32
+
33
+ describe ".in_order" do
34
+ it "returns values as-is" do
35
+ values = [3, 1, 2]
36
+ ordered = described_class.in_order.call(values, nil, {}, csp)
37
+
38
+ expect(ordered).to eq(values)
39
+ end
40
+ end
41
+
42
+ describe ".forward_checking" do
43
+ it "returns true to enable forward checking" do
44
+ expect(described_class.forward_checking).to be(true)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,44 @@
1
+ require "winston"
2
+
3
+ describe Winston::Solvers::MAC do
4
+ let(:csp) { Winston::CSP.new }
5
+
6
+ def valid_solution?(csp, assignments)
7
+ csp.constraints.all? { |constraint| constraint.validate(assignments) }
8
+ end
9
+
10
+ it "solves a simple constraint problem" do
11
+ csp.add_variable :a, domain: [1, 2, 3]
12
+ csp.add_variable :b, domain: [1, 2, 3]
13
+ csp.add_variable :c, domain: [1, 2, 3]
14
+ csp.add_constraint(:a, :b) { |a, b| a > b }
15
+ csp.add_constraint(:b, :c) { |b, c| b > c }
16
+
17
+ solver = described_class.new(csp)
18
+ result = solver.search
19
+
20
+ expect(result).to be_a(Hash)
21
+ expect(valid_solution?(csp, result)).to be(true)
22
+ end
23
+
24
+ it "respects preset assignments" do
25
+ csp.add_variable :a, value: 2
26
+ csp.add_variable :b, domain: [1, 2, 3]
27
+ csp.add_constraint(:a, :b) { |a, b| a > b }
28
+
29
+ solver = described_class.new(csp)
30
+ result = solver.search
31
+
32
+ expect(result[:a]).to eq(2)
33
+ expect(valid_solution?(csp, result)).to be(true)
34
+ end
35
+
36
+ it "returns false when constraints are impossible" do
37
+ csp.add_variable :a, domain: [1]
38
+ csp.add_variable :b, domain: [1]
39
+ csp.add_constraint(:a, :b) { |a, b| a != b }
40
+
41
+ solver = described_class.new(csp)
42
+ expect(solver.search).to be(false)
43
+ end
44
+ end
@@ -0,0 +1,44 @@
1
+ require "winston"
2
+
3
+ describe Winston::Solvers::MinConflicts do
4
+ let(:csp) { Winston::CSP.new }
5
+
6
+ def valid_solution?(csp, assignments)
7
+ csp.constraints.all? { |constraint| constraint.validate(assignments) }
8
+ end
9
+
10
+ it "finds a valid solution for a simple problem" do
11
+ csp.add_variable :a, domain: [1, 2, 3]
12
+ csp.add_variable :b, domain: [1, 2, 3]
13
+ csp.add_variable :c, domain: [1, 2, 3]
14
+ csp.add_constraint(:a, :b) { |a, b| a > b }
15
+ csp.add_constraint(:b, :c) { |b, c| b > c }
16
+
17
+ solver = described_class.new(csp, max_steps: 1_000, random: Random.new(1))
18
+ result = solver.search
19
+
20
+ expect(result).to be_a(Hash)
21
+ expect(valid_solution?(csp, result)).to be(true)
22
+ end
23
+
24
+ it "respects preset assignments" do
25
+ csp.add_variable :a, value: 2
26
+ csp.add_variable :b, domain: [1, 2, 3]
27
+ csp.add_constraint(:a, :b) { |a, b| a > b }
28
+
29
+ solver = described_class.new(csp, max_steps: 1_000, random: Random.new(1))
30
+ result = solver.search
31
+
32
+ expect(result[:a]).to eq(2)
33
+ expect(valid_solution?(csp, result)).to be(true)
34
+ end
35
+
36
+ it "returns false when no solution is found within max_steps" do
37
+ csp.add_variable :a, domain: [1]
38
+ csp.add_variable :b, domain: [1]
39
+ csp.add_constraint(:a, :b) { |a, b| a != b }
40
+
41
+ solver = described_class.new(csp, max_steps: 10, random: Random.new(1))
42
+ expect(solver.search).to be(false)
43
+ end
44
+ end
data/winston.gemspec CHANGED
@@ -1,19 +1,20 @@
1
1
  Gem::Specification.new do |gem|
2
2
  gem.name = 'winston'
3
- gem.version = '0.0.1'
3
+ gem.version = '0.1.0'
4
4
  gem.authors = ['David Michael Nelson']
5
5
  gem.homepage = 'http://github.com/dmnelson/winston'
6
6
 
7
7
  gem.summary = 'Constraint Satisfaction Problem (CSP) implementation for Ruby'
8
- gem.description = gem.summary
8
+ gem.description = 'A small, practical CSP library for Ruby with multiple solvers, heuristics, and a DSL.'
9
9
  gem.license = 'MIT'
10
+ gem.required_ruby_version = ">= 3.0"
10
11
 
11
12
  gem.files = `git ls-files`.split($/)
12
13
  gem.executables = gem.files.grep(%r{^bin/}) { |f| File.basename(f) }
13
14
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
15
  gem.require_paths = ["lib"]
15
16
 
16
- gem.add_development_dependency "bundler", "~> 1.3"
17
- gem.add_development_dependency "rake"
18
- gem.add_development_dependency 'rspec'
17
+ gem.add_development_dependency "bundler", "~> 2.4", ">= 2.4.22"
18
+ gem.add_development_dependency "rake", "~> 13.1"
19
+ gem.add_development_dependency "rspec", "~> 3.13"
19
20
  end