answer-factory 0.0.1

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.
Files changed (38) hide show
  1. data/LICENSE.txt +21 -0
  2. data/Rakefile +29 -0
  3. data/Thorfile +79 -0
  4. data/VERSION +1 -0
  5. data/_spikes/old_vs_new_dominated_by?.rb +45 -0
  6. data/config/database.yml +9 -0
  7. data/lib/answer-factory.rb +14 -0
  8. data/lib/answers/answer.rb +126 -0
  9. data/lib/answers/batch.rb +49 -0
  10. data/lib/factories/factory.rb +53 -0
  11. data/lib/factories/workstation.rb +33 -0
  12. data/lib/operators/basic_operators.rb +240 -0
  13. data/lib/operators/evaluators.rb +113 -0
  14. data/lib/operators/samplers_and_selectors.rb +131 -0
  15. data/pkg/nudgegp-0.0.1.gem +0 -0
  16. data/readme.md +29 -0
  17. data/spec/answer_spec.rb +412 -0
  18. data/spec/batch_spec.rb +98 -0
  19. data/spec/config_spec.rb +94 -0
  20. data/spec/factories/factory_spec.rb +86 -0
  21. data/spec/factories/workstation_spec.rb +139 -0
  22. data/spec/operators/any_one_sampler_spec.rb +39 -0
  23. data/spec/operators/dominated_quantile_spec.rb +111 -0
  24. data/spec/operators/duplicate_genomes_spec.rb +35 -0
  25. data/spec/operators/evaluators/program_point_evaluator_spec.rb +43 -0
  26. data/spec/operators/evaluators/test_case_evaluator_spec.rb +129 -0
  27. data/spec/operators/infrastructure_spec.rb +45 -0
  28. data/spec/operators/most_dominated_subset_spec.rb +47 -0
  29. data/spec/operators/nondominated_subset_spec.rb +103 -0
  30. data/spec/operators/pointCrossover_spec.rb +60 -0
  31. data/spec/operators/pointDeletion_spec.rb +62 -0
  32. data/spec/operators/pointMutation_spec.rb +77 -0
  33. data/spec/operators/random_guess_spec.rb +77 -0
  34. data/spec/operators/resample_and_clone_spec.rb +60 -0
  35. data/spec/operators/resample_values_spec.rb +135 -0
  36. data/spec/operators/uniformBackboneCrossover_spec.rb +67 -0
  37. data/spec/spec_helper.rb +14 -0
  38. metadata +201 -0
@@ -0,0 +1,62 @@
1
+ require File.join(File.dirname(__FILE__), "./../spec_helper")
2
+
3
+
4
+ describe "PointDeleteOperator search operator" do
5
+ describe "generate" do
6
+ before(:each) do
7
+ @zapper = PointDeleteOperator.new()
8
+ @dude1 = Answer.new("block { do thing1 \n do thing2 \n do thing3}")
9
+ end
10
+
11
+ it "should accept a Batch as a param" do
12
+ lambda{@zapper.generate()}.should raise_error(ArgumentError)
13
+ lambda{@zapper.generate(812)}.should raise_error(ArgumentError)
14
+ lambda{@zapper.generate(Batch.new)}.should_not raise_error(ArgumentError)
15
+ lambda{@zapper.generate(Batch[@dude1])}.should_not raise_error(ArgumentError)
16
+ end
17
+
18
+ it "should raise an Argument error if all contents of the crowd aren't Answers" do
19
+ lambda{@zapper.generate([])}.should_not raise_error(ArgumentError)
20
+ lambda{@zapper.generate([ 77 ])}.should raise_error(ArgumentError)
21
+ lambda{@zapper.generate([ @dude1, 77 ])}.should raise_error(ArgumentError)
22
+ end
23
+
24
+ it "should return a Batch as a result" do
25
+ @zapper.generate([@dude1]).should be_a_kind_of(Batch)
26
+ end
27
+
28
+ it "should use Answer#delete_point to produce the variants" do
29
+ @dude1.should_receive(:delete_point_or_clone).and_return("do parseable")
30
+ @zapper.generate([@dude1])
31
+ end
32
+
33
+ it "should produce one result per individual in the wildtype crowd as a default" do
34
+ @zapper.generate([@dude1]).length.should == 1
35
+ @zapper.generate([@dude1, @dude1]).length.should == 2
36
+ end
37
+
38
+ it "should produce more if passed the optional howManyCopies parameter > 1" do
39
+ @zapper.generate([@dude1],2).length.should == 2
40
+ @zapper.generate([@dude1, @dude1],3).length.should == 6
41
+ end
42
+
43
+ it "should produce individuals from which a random point (and its subpoints) is deleted" do
44
+ @zapper.generate([@dude1],5).each {|baby| baby.points.should < @dude1.points}
45
+ end
46
+
47
+ it "should produce 'block {}' whenever a root is deleted" do
48
+ @zapper.should_receive(:rand).with(4).and_return(0)
49
+ @zapper.generate([@dude1])[0].blueprint.should == "block {}"
50
+ end
51
+
52
+ it "should increment the progress of the offspring" do
53
+ @dude1.stub(:progress).and_return(195)
54
+ @zapper.generate([@dude1])[0].progress.should == 196
55
+ end
56
+
57
+ it "should handle moving the footnotes correctly"
58
+
59
+ it "should maintain unused footnotes correctly"
60
+
61
+ end
62
+ end
@@ -0,0 +1,77 @@
1
+ require File.join(File.dirname(__FILE__), "./../spec_helper")
2
+
3
+
4
+ describe "PointMutationOperator" do
5
+ describe "initialization" do
6
+ it "should have a params attribute when created that sets basic values for code generation" do
7
+ PointMutationOperator.new.incoming_options.should == {}
8
+ mutator = PointMutationOperator.new(:points => 3, :blocks => 1)
9
+ mutator.incoming_options.should_not == {}
10
+ mutator.incoming_options[:points].should == 3
11
+ end
12
+ end
13
+
14
+ describe "generate" do
15
+ before(:each) do
16
+ @gammaray = PointMutationOperator.new(target_size_in_points: 3, types_name: ["int"])
17
+ @dude1 = Answer.new("block { do x1 \n do x2 \n do x3}")
18
+ end
19
+
20
+ it "should accept an Array of one or more Answers as a param" do
21
+ lambda{@gammaray.generate()}.should raise_error(ArgumentError)
22
+ lambda{@gammaray.generate(99)}.should raise_error(ArgumentError)
23
+ lambda{@gammaray.generate([])}.should raise_error(ArgumentError)
24
+ lambda{@gammaray.generate([99])}.should raise_error(ArgumentError)
25
+
26
+ lambda{@gammaray.generate([@dude1])}.should_not raise_error(ArgumentError)
27
+ end
28
+
29
+ it "should return a Batch as a result" do
30
+ @gammaray.generate([@dude1]).should be_a_kind_of(Batch)
31
+ end
32
+
33
+ it "should use Answer#replace_point to produce the variants" do
34
+ @dude1.should_receive(:replace_point_or_clone).and_return("do anything")
35
+ @gammaray.generate([@dude1])
36
+ end
37
+
38
+ it "should produce one result per individual in the wildtype crowd as a default" do
39
+ @gammaray.generate([@dude1]).length.should == 1
40
+ @gammaray.generate([@dude1,@dude1]).length.should == 2
41
+ end
42
+
43
+ it "should produce more if passed the optional howManyCopies parameter > 1" do
44
+ @gammaray.generate([@dude1],3).length.should == 3
45
+ @gammaray.generate([@dude1,@dude1],2).length.should == 4
46
+ end
47
+
48
+ it "should produce individuals from which a random point (and all subpoints) is replaced" do
49
+ @gammaray.should_receive(:rand).and_return(0)
50
+ @gammaray.generate([@dude1])[0].points.should == 3 # totally replaced with 3-pt code
51
+ @gammaray.should_receive(:rand).and_return(1)
52
+ @gammaray.generate([@dude1])[0].points.should == 6 #replace point 2 with 3-pt code
53
+ @gammaray.should_receive(:rand).and_return(2)
54
+ @gammaray.generate([@dude1])[0].points.should == 6 #replace point 3 with 3-pt code
55
+ @gammaray.should_receive(:rand).and_return(3)
56
+ @gammaray.generate([@dude1])[0].points.should == 6 #replace point 4 with 3-pt code
57
+ end
58
+
59
+ it "should accept temporarily overriding params to pass into CodeType.random_value" do
60
+ @gammaray.should_receive(:rand).and_return(0)
61
+ @gammaray.generate([@dude1])[0].points.should == 3 # totally replaced with 3-pt code
62
+ @gammaray.should_receive(:rand).and_return(0)
63
+ @gammaray.generate([@dude1],1,target_size_in_points: 10)[0].points.should == 10
64
+ end
65
+
66
+ it "should increment the #progress of the offspring" do
67
+ @dude1.stub(:progress).and_return(888)
68
+ @gammaray.generate([@dude1],13).each {|baby| baby.progress.should == 889}
69
+ end
70
+
71
+ it "should handle moving the footnotes correctly"
72
+
73
+ it "should maintain unused footnotes correctly"
74
+
75
+ it "should introduce new footnotes smoothly, if a ValuePoint is added"
76
+ end
77
+ end
@@ -0,0 +1,77 @@
1
+ #encoding:utf-8
2
+ require File.join(File.dirname(__FILE__), "./../spec_helper")
3
+
4
+
5
+ describe "random_guess operator" do
6
+ before(:each) do
7
+ @myGuesser = RandomGuessOperator.new
8
+ end
9
+
10
+ it "should be a kind of SearchOperator" do
11
+ @myGuesser.should be_a_kind_of(SearchOperator)
12
+ end
13
+
14
+ it "should have a params attribute when created that sets basic values for code generation" do
15
+ RandomGuessOperator.new.incoming_options.should == {}
16
+ thisGuesser = RandomGuessOperator.new(foo:99, bar:101)
17
+ thisGuesser.incoming_options.should_not == {}
18
+ thisGuesser.incoming_options[:foo].should == 99
19
+ end
20
+
21
+
22
+ it "should produce a Batch of Answers when it receives #generate" do
23
+ newDudes = @myGuesser.generate
24
+ newDudes.should be_a_kind_of(Batch)
25
+ newDudes[0].should be_a_kind_of(Answer)
26
+ newDudes[0].blueprint.should_not == nil
27
+ newDudes[0].program.should_not == nil
28
+ end
29
+
30
+ it "should produce one as a default, more if a higher number is passed in" do
31
+ @myGuesser.generate.length.should == 1
32
+ @myGuesser.generate(4).length.should == 4
33
+ end
34
+
35
+ it "should have a parsed blueprint as its #program attribute" do
36
+ newDudes = @myGuesser.generate
37
+ newDudes[0].program.should be_a_kind_of(NudgeProgram)
38
+ end
39
+
40
+ it "should accept temporarily overriding options to pass into CodeType.any_value" do
41
+ @myNewGuesser = RandomGuessOperator.new(
42
+ target_size_in_points: 7,
43
+ instruction_names: ["int_add", "int_subtract"],
44
+ reference_names: ["x1", "x2", "x3"])
45
+
46
+ lambda{@myNewGuesser.generate(
47
+ 3,
48
+ target_size_in_points: 12,
49
+ reference_names: ["y1"])}.should_not raise_error
50
+
51
+ @myNewGuesser.generate(
52
+ 3,
53
+ target_size_in_points: 12)[0].program.points.should_not == 7
54
+
55
+ @myNewGuesser.generate(
56
+ 3,
57
+ target_size_in_points: 12)[0].program.points.should == 12
58
+
59
+ @myNewGuesser.generate(
60
+ 1,
61
+ target_size_in_points: 16,
62
+ probabilities:{b:0,r:1,v:0,i:0},
63
+ reference_names: ["y1"])[0].blueprint.should include("y1")
64
+ end
65
+
66
+ it "should produce a Batch that contains Answers with progress=0 only" do
67
+ @myGuesser.generate(12).each {|dude| dude.progress.should == 0}
68
+ end
69
+
70
+ it "should handle generated footnotes correctly" do
71
+ @myGuesser.generate(1,
72
+ target_size_in_points: 52,
73
+ type_names:["int"],
74
+ probabilities: {b:0,v:1,i:0,r:0})[0].program.footnote_section.scan(/«int»/).length.should == 51
75
+ end
76
+
77
+ end
@@ -0,0 +1,60 @@
1
+ require File.join(File.dirname(__FILE__), "./../spec_helper")
2
+
3
+
4
+ describe "resample_and_clone operator" do
5
+ before(:each) do
6
+ @myGuesser = RandomGuessOperator.new(type_names: ["int"], instruction_names: ["int_add"])
7
+ @mySampler = ResampleAndCloneOperator.new
8
+ end
9
+
10
+ it "should be a kind of SearchOperator" do
11
+ @mySampler.should be_a_kind_of(SearchOperator)
12
+ end
13
+
14
+ it "should produce a list of Answers when it receives #generate" do
15
+ newDudes = @mySampler.generate(@myGuesser.generate(3))
16
+ newDudes.should be_a_kind_of(Batch)
17
+ newDudes[0].should be_a_kind_of(Answer)
18
+ newDudes[0].blueprint.should_not == nil
19
+ newDudes[0].program.should_not == nil
20
+ end
21
+
22
+ it "should produce one Answer with a blueprint identical to one of the passed in crowd's" do
23
+ pop = @myGuesser.generate(1)
24
+ newDudes = @mySampler.generate(pop)
25
+ newDudes[0].blueprint.should == pop[0].blueprint
26
+ end
27
+
28
+ it "should return more than one individual when asked to, resampling as needed" do
29
+ newDudes = @mySampler.generate(@myGuesser.generate(10))
30
+ newDudes.length.should == 1
31
+ newDudes = @mySampler.generate(@myGuesser.generate(3),2)
32
+ newDudes.length.should == 2
33
+ newDudes = @mySampler.generate(@myGuesser.generate(1),2)
34
+ newDudes.length.should == 2
35
+ newDudes[0].blueprint.should == newDudes[1].blueprint
36
+ end
37
+
38
+ it "should not return links to the original program copies in the new clones" do
39
+ pop = @myGuesser.generate(3)
40
+ newDudes = @mySampler.generate(pop)
41
+ pop.collect {|old_dude| old_dude.program.object_id}.should_not include(newDudes[0].program.object_id)
42
+ end
43
+
44
+ it "should have a parsed blueprint as its #program attribute" do
45
+ pop = @myGuesser.generate(3)
46
+ newDudes = @mySampler.generate(pop)
47
+ newDudes[0].program.should be_a_kind_of(NudgeProgram)
48
+ end
49
+
50
+ it "should increment the #progress of each clone" do
51
+ pop = @myGuesser.generate(3)
52
+ pop.each {|donor| donor.stub(:progress).and_return(12)}
53
+ newDudes = @mySampler.generate(pop)
54
+ newDudes.each {|kid| kid.progress.should == 13}
55
+ end
56
+
57
+ it "should handle footnotes correctly"
58
+
59
+ it "should use a deep copy to clone new guys, not just clone"
60
+ end
@@ -0,0 +1,135 @@
1
+ #encoding: utf-8
2
+ require File.join(File.dirname(__FILE__), "./../spec_helper")
3
+
4
+
5
+ describe "ResampleValuesOperator search operator" do
6
+
7
+ it "should not need any initial parameters" do
8
+ lambda{ResampleValuesOperator.new()}.should_not raise_error
9
+ end
10
+
11
+ it "should be possible to pass in stored parameters" do
12
+ lambda{ResampleValuesOperator.new(randomIntegerLowerBound: 12)}.should_not raise_error
13
+ rs = ResampleValuesOperator.new({boolTrueProbability: 0.2})
14
+ rs.incoming_options.should include(:boolTrueProbability)
15
+ end
16
+
17
+ describe "generate" do
18
+ before(:each) do
19
+ @rs = ResampleValuesOperator.new({boolTrueProbability: 0.2})
20
+ @intDude = Answer.new("block {value «int»}\n«int» 3")
21
+ @boolDude = Answer.new("block {value «bool»}\n«bool» false")
22
+ @floatDude = Answer.new("block {value «float»}\n«float» -991.2213")
23
+ @complicatedDude = Answer.new(
24
+ "block {value «int» value «bool» block {value «float»}}\n«int» 3\n«bool» false\n«float» 0.0")
25
+ end
26
+
27
+ it "should require a Batch as a first parameter" do
28
+ lambda{@rs.generate()}.should raise_error(ArgumentError)
29
+ lambda{@rs.generate(Batch.new)}.should_not raise_error(ArgumentError)
30
+ end
31
+
32
+ it "should raise an ArgumentError if the array parameter isn't all Answers" do
33
+ lambda{@rs.generate([88])}.should raise_error(ArgumentError)
34
+ lambda{@rs.generate([@intDude])}.should_not raise_error(ArgumentError)
35
+ end
36
+
37
+ it "should by default produce one (1) resampled mutant for each input" do
38
+ @rs.generate([@intDude]).length.should == 1
39
+ @rs.generate([@intDude, @intDude]).length.should == 2
40
+ end
41
+
42
+ it "should call #any_value for each line in each individual that is a 'sample'" do
43
+ IntType.should_receive(:any_value).and_return(777)
44
+ newGuys = @rs.generate([@intDude])
45
+
46
+ BoolType.should_receive(:any_value).and_return(false)
47
+ newGuys = @rs.generate([@boolDude])
48
+
49
+ FloatType.should_receive(:any_value).and_return(9.999)
50
+ newGuys = @rs.generate([@floatDude])
51
+
52
+ IntType.should_receive(:any_value).and_return(777)
53
+ BoolType.should_receive(:any_value).and_return(false)
54
+ FloatType.should_receive(:any_value).and_return(9.999)
55
+ newGuys = @rs.generate([@complicatedDude])
56
+
57
+ IntType.should_receive(:any_value).and_return(1,2)
58
+ BoolType.should_receive(:any_value).and_return(false,true)
59
+ FloatType.should_receive(:any_value).and_return(1.0,2.0)
60
+ oldGuys = [@intDude,@boolDude,@floatDude,@complicatedDude]
61
+ newGuys = @rs.generate(oldGuys,1)
62
+ end
63
+
64
+ it "should be possible to pass in a higher integer, and get that many variants for each input" do
65
+ IntType.should_receive(:any_value).and_return(11,22,33)
66
+ newGuys = @rs.generate([@intDude],3)
67
+ newGuys[0].blueprint.should include("11")
68
+ newGuys[1].blueprint.should include("22")
69
+ newGuys[2].blueprint.should include("33")
70
+
71
+ IntType.should_receive(:any_value).and_return(4,5,6,7)
72
+ newGuys = @rs.generate([@intDude, @intDude],2)
73
+ end
74
+
75
+ it "should be using the Operator's saved parameters as a default behavior" do
76
+ wholeLottaParams = {
77
+ :randomIntegerLowerBound => 1000,
78
+ :randomIntegerUpperBound => 1005,
79
+ :randomBooleanTruthProb => 0.2,
80
+ :randomFloatLowerBound => 112.0,
81
+ :randomFloatUpperBound => 112.5}
82
+
83
+ resampleLimited = ResampleValuesOperator.new(wholeLottaParams)
84
+
85
+ IntType.should_receive(:any_value).with(hash_including(wholeLottaParams))
86
+ newGuys = resampleLimited.generate([@intDude])
87
+
88
+ BoolType.should_receive(:any_value).with(hash_including(wholeLottaParams))
89
+ newGuys = resampleLimited.generate([@boolDude])
90
+
91
+ FloatType.should_receive(:any_value).with(hash_including(wholeLottaParams))
92
+ newGuys = resampleLimited.generate([@floatDude])
93
+ end
94
+
95
+ it "should return Answers who (probably) differ from the originals passed in" do
96
+ outOfRangeParams = {
97
+ :randomIntegerLowerBound => 1000,
98
+ :randomIntegerUpperBound => 1005,
99
+ :randomBooleanTruthProb => 1.0,
100
+ :randomFloatLowerBound => 112.0,
101
+ :randomFloatUpperBound => 112.5}
102
+ resampleFarAway = ResampleValuesOperator.new(outOfRangeParams)
103
+ newGuys = resampleFarAway.generate([@intDude])
104
+ newGuys[0].blueprint.should =~ /100[0-5]/
105
+ newGuys = resampleFarAway.generate([@boolDude])
106
+ newGuys[0].blueprint.should =~ /true/
107
+ newGuys = resampleFarAway.generate([@floatDude])
108
+ newGuys[0].blueprint.should =~ /112\.[0-5]/
109
+ end
110
+
111
+ it "should be possible to temporarily override some or all of the preset @params" do
112
+ bigInt = {:randomIntegerLowerBound => 90000,:randomIntegerUpperBound => 91000}
113
+ toBeOverridden = ResampleValuesOperator.new(bigInt)
114
+ defaults = toBeOverridden.generate([@intDude],5)
115
+ defaults.each {|dude| dude.blueprint.should =~ /9\d\d\d\d/}
116
+
117
+ littler = toBeOverridden.generate([@intDude],5,
118
+ :randomIntegerLowerBound => -19, :randomIntegerUpperBound => -10)
119
+ littler.each {|dude| dude.blueprint.should =~ /-1\d/}
120
+ end
121
+
122
+ it "should increment the #progress of every clone" do
123
+ @complicatedDude.stub!(:progress).and_return(192)
124
+ @rs.generate([@complicatedDude],5).each {|clone| clone.progress.should == 193}
125
+ end
126
+
127
+ it "should leave unrecognized types with footnote values in place" do
128
+ hunh = Answer.new("value «foo»\n«foo» bar")
129
+ ResampleValuesOperator.new.generate([hunh])[0].blueprint.should include("bar")
130
+ end
131
+
132
+ it "should work with CodeType footnotes, and reduce the number of points in each successive call"
133
+ end
134
+
135
+ end
@@ -0,0 +1,67 @@
1
+ require File.join(File.dirname(__FILE__), "./../spec_helper")
2
+
3
+
4
+ describe "UniformBackboneCrossoverOperator" do
5
+ before(:each) do
6
+ @newDudes = []
7
+ @options = {target_size_in_points: 6, instruction_names: ["a", "b", "c"], type_names: ["int"]}
8
+ @myXover = UniformBackboneCrossoverOperator.new
9
+ @myGuesser = RandomGuessOperator.new(@options)
10
+ end
11
+
12
+ it "should be a kind of SearchOperator" do
13
+ @myXover.should be_a_kind_of(SearchOperator)
14
+ end
15
+
16
+ it "should produce a Batch of Answers when it receives #generate" do
17
+ @newDudes = @myXover.generate(@myGuesser.generate(2))
18
+ @newDudes.should be_a_kind_of(Batch)
19
+ @newDudes.each {|dude| dude.should be_a_kind_of(Answer)}
20
+ end
21
+
22
+ it "should produce the same number of Answers it gets as a default" do
23
+ @newDudes = @myXover.generate(@myGuesser.generate(6))
24
+ @newDudes.length.should == 6
25
+ end
26
+
27
+ it "should have an optional parameter that specifies the number of offspring to produce" do
28
+ @newDudes = @myXover.generate(@myGuesser.generate(2),5)
29
+ @newDudes.length.should == 5
30
+ end
31
+
32
+ it "should only include backbone points from one of the parents in the offsprings' genomes" do
33
+ rents = @myGuesser.generate(2)
34
+ @newDudes = @myXover.generate(rents,1)
35
+ @newDudes.length.should == 1
36
+ allParentalPoints = rents[0].program[1].contents + rents[1].program[1].contents
37
+ allTidied = allParentalPoints.collect {|pt| "#{pt.tidy}<br />"}
38
+ end
39
+
40
+ it "should return an identical individual if given only one parent" do
41
+ rent = @myGuesser.generate(1)
42
+ @newDudes = @myXover.generate(rent,3)
43
+ @newDudes.each {|kid| kid.program.tidy.should == rent[0].program.tidy}
44
+ end
45
+
46
+
47
+ it "should not affect the original parents set in any way" do
48
+ rents = @myGuesser.generate(2)
49
+ originalMom = rents[0].object_id
50
+ @newDudes = @myXover.generate(rents,1)
51
+ rents[0].object_id.should == originalMom
52
+ end
53
+
54
+ it "should return offspring with #progress values incremented from the largest parent value" do
55
+ rents = @myGuesser.generate(2)
56
+ rents[0].should_receive(:progress).at_least(1).times.and_return(12)
57
+ rents[1].should_receive(:progress).at_least(1).times.and_return(33)
58
+ @newDudes = @myXover.generate(rents,20)
59
+ @newDudes.each {|baby| [13, 34].should include(baby.progress)}
60
+ end
61
+
62
+ it "should handle moving the footnotes correctly"
63
+
64
+ it "should maintain unused footnotes correctly"
65
+
66
+
67
+ end