quandl_cassandra_models 0.3.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +7 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +7 -0
  5. data/README.md +7 -0
  6. data/Rakefile +11 -0
  7. data/UPGRADE.md +34 -0
  8. data/config/cassandra.yml +41 -0
  9. data/lib/quandl/cassandra/models.rb +16 -0
  10. data/lib/quandl/cassandra/models/column.rb +42 -0
  11. data/lib/quandl/cassandra/models/column/read.rb +49 -0
  12. data/lib/quandl/cassandra/models/column/read/collapse.rb +41 -0
  13. data/lib/quandl/cassandra/models/column/read/column.rb +19 -0
  14. data/lib/quandl/cassandra/models/column/read/data.rb +59 -0
  15. data/lib/quandl/cassandra/models/column/read/offset.rb +104 -0
  16. data/lib/quandl/cassandra/models/column/read/row.rb +20 -0
  17. data/lib/quandl/cassandra/models/column/read/select_columns.rb +63 -0
  18. data/lib/quandl/cassandra/models/column/read/transform.rb +53 -0
  19. data/lib/quandl/cassandra/models/column/read/trim.rb +14 -0
  20. data/lib/quandl/cassandra/models/column/read/type.rb +25 -0
  21. data/lib/quandl/cassandra/models/column/write.rb +25 -0
  22. data/lib/quandl/cassandra/models/column/write/group_data_by_column.rb +36 -0
  23. data/lib/quandl/cassandra/models/column/write/group_data_by_frequency.rb +24 -0
  24. data/lib/quandl/cassandra/models/column/write/insert_column_attributes.rb +22 -0
  25. data/lib/quandl/cassandra/models/column/write/insert_columns.rb +9 -0
  26. data/lib/quandl/cassandra/models/column_attribute.rb +11 -0
  27. data/lib/quandl/cassandra/models/data.rb +18 -0
  28. data/lib/quandl/cassandra/models/data/search.rb +105 -0
  29. data/lib/quandl/cassandra/models/dataset.rb +87 -0
  30. data/lib/quandl/cassandra/models/dataset/columns.rb +63 -0
  31. data/lib/quandl/cassandra/models/dataset_attribute.rb +6 -0
  32. data/lib/quandl/cassandra/models/multiset.rb +55 -0
  33. data/lib/quandl/cassandra/models/version.rb +7 -0
  34. data/migrations/20131105204200_create_datasets.rb +18 -0
  35. data/migrations/20131105204201_create_columns.rb +18 -0
  36. data/migrations/20131105204202_create_dataset_attributes.rb +17 -0
  37. data/migrations/20131105204203_create_column_attributes.rb +17 -0
  38. data/quandl_cassandra_models.gemspec +28 -0
  39. data/spec/expectations/string.rb +5 -0
  40. data/spec/expectations/time.rb +5 -0
  41. data/spec/factories/dataset.rb +8 -0
  42. data/spec/lib/quandl/cassandra/models/column/read_spec.rb +27 -0
  43. data/spec/lib/quandl/cassandra/models/column/write/group_data_by_frequency_spec.rb +28 -0
  44. data/spec/lib/quandl/cassandra/models/column/write_spec.rb +23 -0
  45. data/spec/lib/quandl/cassandra/models/column_attribute_spec.rb +16 -0
  46. data/spec/lib/quandl/cassandra/models/column_spec.rb +17 -0
  47. data/spec/lib/quandl/cassandra/models/data_spec.rb +105 -0
  48. data/spec/lib/quandl/cassandra/models/dataset/collapse_spec.rb +44 -0
  49. data/spec/lib/quandl/cassandra/models/dataset/column_spec.rb +24 -0
  50. data/spec/lib/quandl/cassandra/models/dataset/persistence_spec.rb +25 -0
  51. data/spec/lib/quandl/cassandra/models/dataset/row_spec.rb +26 -0
  52. data/spec/lib/quandl/cassandra/models/dataset/transform_spec.rb +16 -0
  53. data/spec/lib/quandl/cassandra/models/dataset/trim_spec.rb +74 -0
  54. data/spec/lib/quandl/cassandra/models/dataset/update_spec.rb +37 -0
  55. data/spec/lib/quandl/cassandra/models/dataset_attribute_spec.rb +18 -0
  56. data/spec/lib/quandl/cassandra/models/dataset_spec.rb +117 -0
  57. data/spec/lib/quandl/cassandra/models/multiset/collapse_spec.rb +122 -0
  58. data/spec/lib/quandl/cassandra/models/multiset/columns_spec.rb +57 -0
  59. data/spec/lib/quandl/cassandra/models/multiset/data_spec.rb +25 -0
  60. data/spec/lib/quandl/cassandra/models/multiset/transform_spec.rb +69 -0
  61. data/spec/spec_helper.rb +40 -0
  62. data/tasks/migrations.rake +14 -0
  63. metadata +212 -0
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+
3
+ describe Quandl::Cassandra::Models::Column do
4
+
5
+ let(:id){ rand(10000*10000) + 10000*10000 }
6
+ let(:data){ Quandl::Fabricate::Data.rand( rows: 10, columns: 2, nils: false ) }
7
+ let(:dataset){ Quandl::Cassandra::Models::Dataset.create( id: id, data: data ) }
8
+
9
+ describe ".read" do
10
+ before(:each){ Quandl::Cassandra::Models::Column.write( id: id, data: data ); sleep(0.2) }
11
+
12
+ subject{ Quandl::Cassandra::Models::Column.read( id: id ) }
13
+ its(:count){ should eq 10 }
14
+ it{ should eq data }
15
+ end
16
+
17
+ end
@@ -0,0 +1,105 @@
1
+ require 'spec_helper'
2
+
3
+ describe Quandl::Cassandra::Models::Data do
4
+
5
+ let(:id){ rand(10000*10000) + 10000*10000 }
6
+ let(:data){ Quandl::Fabricate::Data.rand(rows: 10, columns: 2, nils: false) }
7
+ let(:dataset){ Quandl::Cassandra::Models::Dataset.create( id: id, data: data ) }
8
+ subject{ Quandl::Cassandra::Models::Data }
9
+
10
+ [:row, :id, :limit, :offset, :column, :accuracy, :frequency, :column_ids,
11
+ :collapse, :transform, :order, :trim_start, :trim_end ].each do |name|
12
+ it{ should respond_to name }
13
+ end
14
+
15
+ describe "#column" do
16
+ it "should handle column that is outside the range of available columns" do
17
+ dataset.data.scoped.column(4).to_table.should be_blank
18
+ end
19
+ end
20
+
21
+ describe "#collapse" do
22
+ it "should become annual" do
23
+ scope = subject.collapse(:annual)
24
+ scope.attributes[:collapse].should eq :annual
25
+ end
26
+ it "should become annual given string" do
27
+ scope = subject.collapse('annual')
28
+ scope.attributes[:collapse].should eq :annual
29
+ end
30
+ it "should become monthly" do
31
+ scope = subject.collapse(:monthly)
32
+ scope.attributes[:collapse].should eq :monthly
33
+ end
34
+ it "should become weekly" do
35
+ scope = subject.collapse('weekly')
36
+ scope.attributes[:collapse].should eq :weekly
37
+ end
38
+ end
39
+
40
+ describe "#trim_start & #trim_end" do
41
+ it "should handle trim_start > trim_end" do
42
+ dataset.data.scoped.trim_start(data[-1][0]).trim_end(data[0][0]).to_table.should be_a Quandl::Cassandra::Data
43
+ end
44
+ end
45
+
46
+ describe "#trim_start" do
47
+
48
+ subject{ dataset.data }
49
+
50
+ context "given invalid date" do
51
+
52
+ it "rejects string" do
53
+ subject.trim_start('invalid').attributes[:trim_start].should be_nil
54
+ end
55
+
56
+ end
57
+
58
+ context "given valid date" do
59
+
60
+ it "accepts string" do
61
+ subject.trim_start('1980-01-01').attributes[:trim_start].should eq Date.parse('1980-01-01').jd
62
+ end
63
+
64
+ it "accepts julian date" do
65
+ subject.trim_start( Date.today.jd ).attributes[:trim_start].should eq Date.today.jd
66
+ end
67
+
68
+ end
69
+
70
+ end
71
+
72
+ describe "#trim_end" do
73
+
74
+ subject{ dataset.data }
75
+
76
+ context "given invalid date" do
77
+ it "rejects string" do
78
+ subject.trim_end('invalid').attributes[:trim_end].should be_nil
79
+ end
80
+ end
81
+ context "given valid date" do
82
+ it "accepts date" do
83
+ subject.trim_end(Date.today).attributes[:trim_end].should eq Date.today.jd
84
+ end
85
+ it "accepts string" do
86
+ subject.trim_end('1980-01-01').attributes[:trim_end].should eq Date.parse('1980-01-01').jd
87
+ end
88
+ it "accepts julian date" do
89
+ subject.trim_end( Date.today.jd ).attributes[:trim_end].should eq Date.today.jd
90
+ end
91
+ end
92
+ end
93
+
94
+ context "data with empty columns" do
95
+ let(:data){ Quandl::Data.new("2012-12-31,21.0,11.0,12.0,0.0,,,,0.0,,4.0,,4.0,18.0,19.0\n2012-12-24,21.0,11.0,12.0,0.0,,,,0.0,,4.0,,4.0,18.0,19.0\n2012-12-18,21.0,11.0,12.0,0.0,,,,0.0,,4.0,,4.0,18.0,19.0\n") }
96
+ let(:dataset){ Quandl::Cassandra::Models::Dataset.create( id: rand(100000), data: data ) }
97
+ subject{ Dataset.find(dataset.id) }
98
+
99
+ it "should eq data" do
100
+ subject.data.to_table.should eq data
101
+ end
102
+
103
+ end
104
+
105
+ end
@@ -0,0 +1,44 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe Quandl::Cassandra::Models::Dataset do
5
+
6
+ let(:dataset){ create(:dataset) }
7
+ subject { dataset }
8
+
9
+ it "should have columns" do
10
+ subject.columns.count.should eq 4
11
+ end
12
+
13
+ describe "#data" do
14
+
15
+ subject{ dataset.data }
16
+
17
+ its(:class){ should eq Quandl::Cassandra::Models::Data::ScopeScope }
18
+
19
+ its(:to_date){ should be_present }
20
+ its(:to_h){ should be_present }
21
+
22
+ it "should have data" do
23
+ subject.count.should eq 60
24
+ end
25
+
26
+ it "should collapse to daily" do
27
+ subject.collapse(:daily).count.should eq 60
28
+ end
29
+
30
+ it "should collapse to weekly" do
31
+ subject.collapse(:weekly).count.should be < 12
32
+ end
33
+
34
+ it "should collapse to monthly" do
35
+ subject.collapse(:monthly).count.should be < 4
36
+ end
37
+
38
+ it "should collapse to annual" do
39
+ subject.collapse(:annual).count.should eq 1
40
+ end
41
+
42
+ end
43
+
44
+ end
@@ -0,0 +1,24 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe Quandl::Cassandra::Models::Dataset do
5
+
6
+ context "column" do
7
+
8
+ subject { create(:dataset) }
9
+
10
+ it "should return first column" do
11
+ subject.data.column(1)[0][1].should eq subject.data.scoped.to_table[0][1]
12
+ end
13
+
14
+ it "should return second column" do
15
+ subject.data.column(2)[0][1].should eq subject.data.scoped.to_table[0][2]
16
+ end
17
+
18
+ it "should return third column" do
19
+ subject.data.column(2)[0][1].should eq subject.data.scoped.to_table[0][2]
20
+ end
21
+
22
+ end
23
+
24
+ end
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe Quandl::Cassandra::Models::Dataset do
5
+
6
+ let(:data){ Quandl::Fabricate::Data.rand( rows: 4, columns: 4, nils: false ) }
7
+ let(:dataset){ create( :dataset, data: data ) }
8
+
9
+ subject{ dataset }
10
+
11
+ its(:dataset_attribute){ should be_a DatasetAttribute }
12
+ its(:trim_start){ should eq Date.jd(Dataset.find(dataset.id).data[-1][0]) }
13
+ its(:trim_end){ should eq Date.jd(Dataset.find(dataset.id).data[0][0]) }
14
+ its(:updated_at){ should_not be_nil }
15
+
16
+ context "after save" do
17
+ before(:each){
18
+ @previously_updated_at = subject.updated_at
19
+ subject.data = Quandl::Fabricate::Data.rand( rows: 4, columns: 4, nils: false )
20
+ subject.save
21
+ }
22
+ its(:updated_at){ should_not eq @previously_updated_at }
23
+ end
24
+
25
+ end
@@ -0,0 +1,26 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe Quandl::Cassandra::Models::Dataset do
5
+
6
+ let(:data){ Quandl::Fabricate::Data.rand( rows: 216, columns: 2, frequency: :weekly, nils: false ).to_csv }
7
+ let(:dataset){ create( :dataset, data: data ) }
8
+ subject { dataset }
9
+
10
+ it "should be monthly" do
11
+ subject.frequency.should eq 'weekly'
12
+ end
13
+
14
+ it "should pluck the second row" do
15
+ subject.data.scoped.collapse(:monthly).row(1).to_a.should eq [subject.data.scoped.collapse(:monthly)[1]]
16
+ end
17
+
18
+ it "should collapse and pluck the second year" do
19
+ subject.data.scoped.collapse(:annual).row(2).to_a.should eq [subject.data.scoped.collapse(:annual)[2]]
20
+ end
21
+
22
+ it "should collapse and pluck the second year with a transformation" do
23
+ subject.data.scoped.collapse(:annual).transform(:rdiff).row(2).to_a.should eq [subject.data.scoped.collapse(:annual).transform(:rdiff)[2]]
24
+ end
25
+
26
+ end
@@ -0,0 +1,16 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe Dataset do
5
+
6
+ let(:data){ Quandl::Data.new([ [1002, 10], [1001, 20], [1000, 30] ]) }
7
+ let(:dataset){ build(:dataset) }
8
+
9
+ it "should cumul data" do
10
+ dataset.data = data
11
+ dataset.save!
12
+ cumul_data = Dataset.find(dataset.id).data.transform(:cumul).to_table.data_array
13
+ cumul_data.should eq [ [1002, 60.0], [1001, 50.0], [1000, 30.0] ]
14
+ end
15
+
16
+ end
@@ -0,0 +1,74 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe Quandl::Cassandra::Models::Dataset do
5
+
6
+ describe ".trim_start,.trim_end" do
7
+ { weekly: 7, monthly: 30, quarterly: 92, annual: 365 }.each do | collapse_to, occurences |
8
+ context "when collapsed to #{collapse_to}" do
9
+
10
+ subject(:dataset){ create(:dataset, data: Quandl::Fabricate::Data.rand( rows: occurences * 3, columns: 1, nils: false ).to_csv ) }
11
+
12
+ it "should include trim_start" do
13
+ # raw data
14
+ source_data = subject.data.scoped.collapse(collapse_to).to_table
15
+ trim_start = source_data[-1][0]
16
+ # trim and check
17
+ trim_data = subject.data.trim_start( trim_start ).collapse(collapse_to).to_table
18
+ trim_data.last[0].should eq trim_start
19
+ end
20
+ it "should include trim_end" do
21
+ # raw data
22
+ source_data = subject.data.scoped.collapse(collapse_to).to_table
23
+ trim_end = source_data[1][0]
24
+ # trim and check
25
+ trim_data = subject.data.trim_end( trim_end ).collapse(collapse_to).to_table
26
+ trim_data.first[0].should eq trim_end
27
+ end
28
+ it "should include trim_start and trim_end" do
29
+ # raw data
30
+ source_data = subject.data.scoped.collapse(collapse_to).to_table
31
+ trim_end = source_data[1][0]
32
+ trim_start = source_data[-1][0]
33
+ # trim and check
34
+ trim_data = subject.data.trim_start( trim_start ).trim_end( trim_end ).collapse(collapse_to).to_table
35
+ trim_data.first[0].should eq trim_end
36
+ trim_data.last[0].should eq trim_start
37
+ end
38
+
39
+ [:diff, :rdiff, :cumul].each do |transformed_to|
40
+ context "when transformed to #{transformed_to}" do
41
+ it "should include trim_start" do
42
+ # raw data
43
+ source_data = subject.data.scoped.collapse(collapse_to).transform(transformed_to).to_table
44
+ trim_start = source_data[-1][0]
45
+ # trim and check
46
+ trim_data = subject.data.trim_start( trim_start ).transform(transformed_to).collapse(collapse_to).to_table
47
+ trim_data.last[0].should eq trim_start
48
+ end
49
+ it "should include trim_end" do
50
+ # raw data
51
+ source_data = subject.data.scoped.collapse(collapse_to).transform(transformed_to).to_table
52
+ trim_end = source_data[-1][0]
53
+ # trim and check
54
+ trim_data = subject.data.trim_end( trim_end ).transform(transformed_to).collapse(collapse_to).to_table
55
+ trim_data.first[0].should eq trim_end
56
+ end
57
+ it "should include trim_start and trim_end" do
58
+ source_data = subject.data.scoped.collapse(collapse_to).transform(transformed_to).to_table
59
+ trim_end = source_data[1][0]
60
+ trim_start = source_data[-1][0]
61
+ # trim and check
62
+ trim_data = subject.data.trim_start( trim_start ).trim_end( trim_end ).transform(transformed_to).collapse(collapse_to).to_table
63
+ trim_data.first[0].should eq trim_end
64
+ trim_data.last[0].should eq trim_start
65
+ end
66
+ end # each transform
67
+
68
+ end
69
+
70
+ end # each collapse
71
+ end
72
+ end
73
+
74
+ end
@@ -0,0 +1,37 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe Dataset do
5
+
6
+ context "update data with newer data" do
7
+
8
+ subject{ create(:dataset, data: Quandl::Fabricate::Data.rand( rows: 730, columns: 2, nils: false ).to_csv ) }
9
+
10
+ it "should update the collapse data" do
11
+ old_row = subject.data.scoped[0]
12
+ old_row_month = subject.data.scoped.collapse(:monthly)[0]
13
+ # update
14
+ dataset = Dataset.find(subject.id)
15
+ # advance data dates by 60 days
16
+ new_data = subject.data.collect{|r|
17
+ date = r[0] + 60
18
+ values = r[1..-1].collect{ rand(9102841).to_f / 1000 }
19
+ [date, values].flatten
20
+ }
21
+ # assign new data
22
+ dataset.data = new_data.to_a.collect{|r| r.to_csv }.join
23
+ dataset.save!
24
+
25
+ new_row = dataset.data.scoped[0]
26
+ new_row_month = dataset.data.scoped.collapse(:monthly)[0]
27
+
28
+ new_row[0].should_not eq new_row_month[0]
29
+ new_row[1].should eq new_row_month[1]
30
+
31
+ old_row[0].should_not eq new_row[0]
32
+ old_row_month[1].should_not eq new_row_month[1]
33
+ end
34
+
35
+ end
36
+
37
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ describe DatasetAttribute do
4
+ let(:id){ rand(10000*10000) + 10000*10000 }
5
+ let(:dataset_attribute){ DatasetAttribute.create( id: id ) }
6
+ subject{ dataset_attribute }
7
+
8
+ its(:updated_at){ should be_present }
9
+ its(:created_at){ should be_present }
10
+
11
+ describe ".find" do
12
+ subject{ DatasetAttribute.find( dataset_attribute.id ) }
13
+ it{ should be_a DatasetAttribute }
14
+ its(:updated_at){ should be_same_second_as dataset_attribute.updated_at }
15
+ its(:created_at){ should be_same_second_as dataset_attribute.created_at }
16
+ end
17
+
18
+ end
@@ -0,0 +1,117 @@
1
+ require 'spec_helper'
2
+
3
+ describe Quandl::Cassandra::Models::Dataset do
4
+
5
+ let(:id){ rand(10000*10000) + 10000*10000 }
6
+ let(:dataset){ Quandl::Cassandra::Models::Dataset.new( id: id ) }
7
+ subject{ dataset }
8
+
9
+ it{ should respond_to :id }
10
+ it{ should respond_to :data }
11
+ it{ should respond_to :column_ids }
12
+
13
+ its(:id){ should eq id }
14
+ its(:changes){ should eq( { "id" => [nil, id] }) }
15
+
16
+ describe ".new" do
17
+ let(:dataset){ Quandl::Cassandra::Models::Dataset.new( id: 0 ) }
18
+ subject{ dataset }
19
+ its(:data){ should eq [] }
20
+ its(:column_ids){ should eq [] }
21
+ its(:columns){ should eq [] }
22
+ describe "#data" do
23
+ subject{ dataset.data }
24
+ its(:count){ should eq 0 }
25
+ its(:to_a){ should eq Quandl::Cassandra::Models::Data.new }
26
+ context "given filters" do
27
+ subject{ dataset.data.row(0).column(1) }
28
+ its(:to_a){ should eq Quandl::Cassandra::Models::Data.new }
29
+ end
30
+ end
31
+ end
32
+
33
+ describe "#data=" do
34
+ it "should accept data with dates" do
35
+ subject.data = Quandl::Fabricate::Data.rand(rows: 10, columns: 2, nils: false).to_date.to_a
36
+ subject.save.should be_true
37
+ end
38
+ end
39
+
40
+ describe "#save" do
41
+ subject{ Quandl::Cassandra::Models::Dataset.new }
42
+ before(:each){ subject.save }
43
+ end
44
+
45
+ describe "#class" do
46
+ subject{ dataset.class }
47
+ its(:table_name){ should eq 'datasets' }
48
+ end
49
+
50
+ context "given data" do
51
+ before(:each){ dataset.data = Quandl::Fabricate::Data.rand(rows: 10, columns: 2, nils: false) }
52
+
53
+ describe "#save" do
54
+ before(:each){ dataset.save }
55
+
56
+ subject{ dataset }
57
+
58
+ its(:changes){ should be_blank }
59
+ its(:frequency){ should eq 'daily' }
60
+
61
+ describe ".find" do
62
+ subject{ Quandl::Cassandra::Models::Dataset.find(id) }
63
+ its(:data){ should eq dataset.data.to_table }
64
+ its(:frequency){ should eq 'daily' }
65
+
66
+ describe "#columns" do
67
+ subject{ Quandl::Cassandra::Models::Dataset.find(id).columns }
68
+ its(:first){ should be_a Quandl::Cassandra::Models::ColumnAttribute }
69
+ end
70
+
71
+ it "data should count and return data" do
72
+ subject.data.count.should eq 10
73
+ subject.data.to_table.should be_a Quandl::Cassandra::Models::Data
74
+ end
75
+ it "columns should eq dataset.columns" do
76
+ subject.columns.collect{|c| c.id.to_s }.should eq dataset.columns.collect{|c| c.id.to_s }
77
+ end
78
+ it "column_ids should eq dataset.column_ids" do
79
+ subject.column_ids.collect(&:to_s).should eq dataset.column_ids.collect(&:to_s)
80
+ end
81
+ end
82
+
83
+ describe "#reload" do
84
+ before(:each){
85
+ dataset.data.limit(5).to_a
86
+ dataset.reload
87
+ }
88
+
89
+ describe "#attributes" do
90
+ subject{ dataset.attributes }
91
+ its([:data]){ should eq nil }
92
+ end
93
+
94
+ end
95
+
96
+ describe "#data" do
97
+ subject{ dataset.data }
98
+ its(:count){ should eq 10 }
99
+ end
100
+
101
+ describe "#column_ids" do
102
+ subject{ dataset.column_ids }
103
+ its(:count){ should eq 2 }
104
+ its(:first){ should be_a Cql::Uuid }
105
+ end
106
+
107
+ describe "#columns" do
108
+ subject{ dataset.columns }
109
+ its(:count){ should eq 2 }
110
+ its(:first){ should be_a Quandl::Cassandra::Models::ColumnAttribute }
111
+ end
112
+
113
+ end
114
+
115
+ end
116
+
117
+ end