savgol 0.2.1 → 0.3.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.
- checksums.yaml +4 -4
- data/README.md +33 -2
- data/lib/savgol.rb +172 -9
- data/lib/savgol/array.rb +5 -54
- data/lib/savgol/version.rb +1 -1
- data/savgol.gemspec +1 -0
- data/spec/savgol_shared_example.rb +1 -8
- data/spec/savgol_spec.rb +83 -0
- metadata +18 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bc7a6227404449143b6b264e85308bda89e5e2fa
|
4
|
+
data.tar.gz: 5e4089173d7d618e2bfe372a5758e78c6be22f36
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6552c4e22998e3f8fb8b594f89684c42fe296496e1481b7f03aef6d43d68032ba23a8341a8288e2695b73b7e5527fc382f92dba9a02ff84764395949735c1836
|
7
|
+
data.tar.gz: dab124a5685cd5c3215d9b3ab628d626ec44590e883c1d340d4bbcad278d7a74f426520abdd19a8cbc16ee2dddd652014ecc1001a6ff7dcf9279c65ce91ce763
|
data/README.md
CHANGED
@@ -2,17 +2,48 @@
|
|
2
2
|
|
3
3
|
Provides implementations of Savitzky-Golay smoothing (filtering).
|
4
4
|
|
5
|
-
The gem is based on the [scipy implementation](http://www.scipy.org/Cookbook/SavitzkyGolay). A good explanation of the process may be found [here on stackexchange](http://dsp.stackexchange.com/a/9494).
|
5
|
+
The gem is based on the [scipy implementation](http://www.scipy.org/Cookbook/SavitzkyGolay) (gives exactly the same result). A good explanation of the process may be found [here on stackexchange](http://dsp.stackexchange.com/a/9494).
|
6
6
|
|
7
7
|
# Examples
|
8
8
|
|
9
|
-
##
|
9
|
+
## Evenly spaced data
|
10
|
+
|
11
|
+
### Array implementation (object oriented)
|
10
12
|
|
11
13
|
require 'savgol/array'
|
12
14
|
data = [1, 2, 3, 4, 3.5, 5, 3, 2.2, 3, 0, -1, 2, 0, -2, -5, -8, -7, -2, 0, 1, 1]
|
13
15
|
# window size = 5, polynomial order = 3
|
14
16
|
data.savgol(5,3)
|
15
17
|
|
18
|
+
### Module implementation
|
19
|
+
|
20
|
+
require 'savgol'
|
21
|
+
data = [1, 2, 3, 4, 3.5, 5, 3, 2.2, 3, 0, -1, 2, 0, -2, -5, -8, -7, -2, 0, 1, 1]
|
22
|
+
# window size = 5, polynomial order = 3
|
23
|
+
Savgol.savgol(data, 5,3)
|
24
|
+
|
25
|
+
## Uneven data
|
26
|
+
|
27
|
+
The speed gain of the Savitzky-Golay filter is lost when interpolating
|
28
|
+
unevenly spaced data and devolves into simple polynomial linear regression [At
|
29
|
+
least I believe they are equivalent in complexity since both require a matrix
|
30
|
+
pseudo-inverse calculation as the most complex operation]. Even though it may
|
31
|
+
be much slower, a filter that handles unevenly spaced data may be useful in
|
32
|
+
some cases.
|
33
|
+
|
34
|
+
require 'savgol'
|
35
|
+
xvals = %w(-1 0 2 3 4 7 8 10 11 12 13 14 17 18).map &:to_f
|
36
|
+
yvals = %w(-2 1 0 1 1 3 4 7 8 9 7 4 1 2).map {|v| v.to_f + 30 }
|
37
|
+
|
38
|
+
### Interpolate at the given x-values
|
39
|
+
|
40
|
+
Savgol.savgol_uneven(xvals, yvals, 5, 2)
|
41
|
+
|
42
|
+
### Interpolate at a new set of x values
|
43
|
+
|
44
|
+
new_xvals = xvals.map {|v| v + 0.5 }
|
45
|
+
Savgol.savgol_uneven(xvals, yvals, 5, 2, new_xvals: new_xvals)
|
46
|
+
|
16
47
|
# Installation
|
17
48
|
|
18
49
|
gem install savgol
|
data/lib/savgol.rb
CHANGED
@@ -1,13 +1,176 @@
|
|
1
1
|
require 'savgol/version'
|
2
|
-
require '
|
2
|
+
require 'matrix'
|
3
3
|
|
4
|
-
|
4
|
+
class NilEnumerator < Enumerator
|
5
|
+
def initialize(enum)
|
6
|
+
@enum = enum
|
7
|
+
end
|
8
|
+
|
9
|
+
def next
|
10
|
+
begin
|
11
|
+
@enum.next
|
12
|
+
rescue StopIteration
|
13
|
+
nil
|
14
|
+
end
|
15
|
+
end
|
5
16
|
end
|
17
|
+
|
18
|
+
module Savgol
|
19
|
+
class << self
|
20
|
+
# Does simple least squares to fit a polynomial based on the given x
|
21
|
+
# values. The major speed-boost of doing savgol is lost for uneven time points.
|
22
|
+
# TODO: implement for different derivatives; implement with a window that
|
23
|
+
# is of fixed size (not based on the number of points)
|
24
|
+
def savgol_uneven(xvals, yvals, window_points=11, order=4, check_args: false, new_xvals: nil)
|
25
|
+
sg_check_arguments(window_points, order) if check_args
|
26
|
+
|
27
|
+
half_window = (window_points -1) / 2
|
28
|
+
yvals_padded = sg_pad_ends(yvals, half_window)
|
29
|
+
xvals_padded = sg_pad_xvals(xvals, half_window)
|
30
|
+
xvals_size = xvals.size
|
31
|
+
|
32
|
+
if new_xvals
|
33
|
+
nearest_xval_indices = sg_nearest_index(xvals, new_xvals)
|
34
|
+
new_xvals.zip(nearest_xval_indices).map do |new_xval, index|
|
35
|
+
sg_regress_and_find(
|
36
|
+
xvals_padded[index,window_points],
|
37
|
+
yvals_padded[index,window_points],
|
38
|
+
order,
|
39
|
+
new_xval
|
40
|
+
)
|
41
|
+
end
|
42
|
+
else
|
43
|
+
xs_iter = xvals_padded.each_cons(window_points)
|
44
|
+
yvals_padded.each_cons(window_points).map do |ys|
|
45
|
+
xs = xs_iter.next
|
46
|
+
sg_regress_and_find(xs, ys, order, xs[half_window])
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# returns the nearest index in original_vals for each value in new_vals
|
52
|
+
# (assumes both are sorted arrays). complexity: O(n + m)
|
53
|
+
def sg_nearest_index(original_vals, new_vals)
|
54
|
+
newval_iter = NilEnumerator.new(new_vals.each)
|
55
|
+
indices = []
|
56
|
+
|
57
|
+
index_iter = NilEnumerator.new((0...original_vals.size).each)
|
58
|
+
index = index_iter.next
|
59
|
+
newval=newval_iter.next
|
60
|
+
last_index = original_vals.size-1
|
61
|
+
|
62
|
+
until newval >= original_vals[index]
|
63
|
+
indices << index
|
64
|
+
break unless newval = newval_iter.next
|
65
|
+
end
|
66
|
+
|
67
|
+
loop do
|
68
|
+
break unless newval
|
69
|
+
|
70
|
+
if index.nil?
|
71
|
+
indices << last_index
|
72
|
+
else
|
73
|
+
until newval <= original_vals[index]
|
74
|
+
index = index_iter.next
|
75
|
+
if !index
|
76
|
+
indices << last_index
|
77
|
+
break
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
if index
|
82
|
+
if newval < original_vals[index]
|
83
|
+
indices << ((newval - original_vals[index-1]) <= (original_vals[index] - newval) ? index-1 : index)
|
84
|
+
else
|
85
|
+
indices << index
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
newval = newval_iter.next
|
90
|
+
end
|
91
|
+
|
92
|
+
indices
|
93
|
+
end
|
6
94
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
95
|
+
def sg_regress_and_find(xdata, ydata, order, xval)
|
96
|
+
xdata_matrix = xdata.map { |xi| (0..order).map { |pow| (xi**pow).to_f } }
|
97
|
+
mx = Matrix[*xdata_matrix]
|
98
|
+
my = Matrix.column_vector(ydata)
|
99
|
+
((mx.t * mx).inv * mx.t * my).transpose.to_a[0].
|
100
|
+
zip((0..order).to_a).
|
101
|
+
map {|coeff, pow| coeff * (xval**pow) }.
|
102
|
+
reduce(:+)
|
103
|
+
end
|
104
|
+
|
105
|
+
def savgol(array, window_points=11, order=4, deriv: 0, check_args: false)
|
106
|
+
sg_check_arguments(window_points, order) if check_args
|
107
|
+
half_window = (window_points -1) / 2
|
108
|
+
weights = sg_weights(half_window, order, deriv)
|
109
|
+
padded_array = sg_pad_ends(array, half_window)
|
110
|
+
sg_convolve(padded_array, weights)
|
111
|
+
end
|
112
|
+
|
113
|
+
def sg_check_arguments(window_points, order)
|
114
|
+
if !window_points.is_a?(Integer) || window_points.abs != window_points || window_points % 2 != 1 || window_points < 1
|
115
|
+
raise ArgumentError, "window_points size must be a positive odd integer"
|
116
|
+
end
|
117
|
+
if !order.is_a?(Integer) || order < 0
|
118
|
+
raise ArgumentError, "order must be an integer >= 0"
|
119
|
+
end
|
120
|
+
if window_points < order + 2
|
121
|
+
raise ArgumentError, "window_points is too small for the polynomials order"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def sg_convolve(data, weights, mode=:valid)
|
126
|
+
data.each_cons(weights.size).map do |ar|
|
127
|
+
ar.zip(weights).map {|pair| pair[0] * pair[1] }.reduce(:+)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# pads the ends with the reverse, geometric inverse sequence
|
132
|
+
def sg_pad_ends(array, half_window)
|
133
|
+
start = array[1..half_window]
|
134
|
+
start.reverse!
|
135
|
+
start.map! {|v| array[0] - (v - array[0]).abs }
|
136
|
+
|
137
|
+
fin = array[(-half_window-1)...-1]
|
138
|
+
fin.reverse!
|
139
|
+
fin.map! {|v| array[-1] + (v - array[-1]).abs }
|
140
|
+
start.push(*array, *fin)
|
141
|
+
end
|
142
|
+
|
143
|
+
# pads the ends of x vals
|
144
|
+
def sg_pad_xvals(array, half_window)
|
145
|
+
deltas = array[0..half_window].each_cons(2).map {|a,b| b-a }
|
146
|
+
start = array[0]
|
147
|
+
prevals = deltas.map do |delta|
|
148
|
+
newval = start - delta
|
149
|
+
start = newval
|
150
|
+
newval
|
151
|
+
end
|
152
|
+
prevals.reverse!
|
153
|
+
|
154
|
+
deltas = array[(-half_window-1)..-1].each_cons(2).map {|a,b| b-a }
|
155
|
+
start = array[-1]
|
156
|
+
postvals = deltas.reverse.map do |delta|
|
157
|
+
newval = start + delta
|
158
|
+
start = newval
|
159
|
+
newval
|
160
|
+
end
|
161
|
+
|
162
|
+
prevals.push(*array, *postvals)
|
163
|
+
end
|
164
|
+
|
165
|
+
# returns an object that will convolve with the padded array
|
166
|
+
def sg_weights(half_window, order, deriv=0)
|
167
|
+
# byebug
|
168
|
+
mat = Matrix[ *(-half_window..half_window).map {|k| (0..order).map {|i| k**i }} ]
|
169
|
+
# Moore-Penrose psuedo-inverse without SVD (not so precize)
|
170
|
+
# A' = (A.t * A)^-1 * A.t
|
171
|
+
pinv_matrix = Matrix[*(mat.transpose*mat).to_a].inverse * Matrix[*mat.to_a].transpose
|
172
|
+
pinv = Matrix[*pinv_matrix.to_a]
|
173
|
+
pinv.row(deriv).to_a
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
data/lib/savgol/array.rb
CHANGED
@@ -1,57 +1,8 @@
|
|
1
|
-
require
|
2
|
-
|
3
|
-
module Savgol
|
4
|
-
|
5
|
-
def savgol(window_size, order, deriv=0, check_args=false)
|
6
|
-
|
7
|
-
# check arguments
|
8
|
-
if !window_size.is_a?(Integer) || window_size.abs != window_size || window_size % 2 != 1 || window_size < 1
|
9
|
-
raise ArgumentError, "window_size size must be a positive odd integer"
|
10
|
-
end
|
11
|
-
if !order.is_a?(Integer) || order < 0
|
12
|
-
raise ArgumentError, "order must be an integer >= 0"
|
13
|
-
end
|
14
|
-
if window_size < order + 2
|
15
|
-
raise ArgumentError, "window_size is too small for the polynomials order"
|
16
|
-
end
|
17
|
-
|
18
|
-
half_window = (window_size -1) / 2
|
19
|
-
weights = sg_weights(half_window, order, deriv)
|
20
|
-
ar = sg_pad_ends(half_window)
|
21
|
-
sg_convolve(ar, weights)
|
22
|
-
end
|
23
|
-
|
24
|
-
def sg_convolve(data, weights, mode=:valid)
|
25
|
-
data.each_cons(weights.size).map do |ar|
|
26
|
-
ar.zip(weights).map {|pair| pair[0] * pair[1] }.reduce(:+)
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
# pads the ends with the reverse, geometric inverse sequence
|
31
|
-
def sg_pad_ends(half_window)
|
32
|
-
start = self[1..half_window]
|
33
|
-
start.reverse!
|
34
|
-
start.map! {|v| self[0] - (v - self[0]).abs }
|
35
|
-
|
36
|
-
fin = self[(-half_window-1)...-1]
|
37
|
-
fin.reverse!
|
38
|
-
fin.map! {|v| self[-1] + (v - self[-1]).abs }
|
39
|
-
start.push(*self, *fin)
|
40
|
-
end
|
41
|
-
|
42
|
-
# returns an object that will convolve with the padded array
|
43
|
-
def sg_weights(half_window, order, deriv=0)
|
44
|
-
# byebug
|
45
|
-
mat = Matrix[ *(-half_window..half_window).map {|k| (0..order).map {|i| k**i }} ]
|
46
|
-
# Moore-Penrose psuedo-inverse without SVD (not so precize)
|
47
|
-
# A' = (A.t * A)^-1 * A.t
|
48
|
-
pinv_matrix = Matrix[*(mat.transpose*mat).to_a].inverse * Matrix[*mat.to_a].transpose
|
49
|
-
pinv = Matrix[*pinv_matrix.to_a]
|
50
|
-
pinv.row(deriv).to_a
|
51
|
-
end
|
52
|
-
|
53
|
-
end
|
1
|
+
require 'savgol'
|
54
2
|
|
55
3
|
class Array
|
56
|
-
|
4
|
+
# see Savgol#savgol
|
5
|
+
def savgol(*args)
|
6
|
+
Savgol.savgol(self, *args)
|
7
|
+
end
|
57
8
|
end
|
data/lib/savgol/version.rb
CHANGED
data/savgol.gemspec
CHANGED
@@ -5,16 +5,9 @@ shared_examples "a savgol smoother" do
|
|
5
5
|
object
|
6
6
|
end
|
7
7
|
|
8
|
-
it 'pads with the reverse geometrically inverted sequence' do
|
9
|
-
expect(smoother.sg_pad_ends(2)).to eq [-1, 0, 1, 2, 3, 4, -7, -2, 0, 1, 1, 1, 2]
|
10
|
-
expect(smoother.sg_pad_ends(3)).to eq [-2, -1, 0, 1, 2, 3, 4, -7, -2, 0, 1, 1, 1, 2, 4]
|
11
|
-
end
|
12
|
-
|
13
8
|
describe 'smoothing a signal' do
|
14
9
|
let(:smoother) do
|
15
|
-
|
16
|
-
object.extend(Savgol)
|
17
|
-
object
|
10
|
+
described_class[1, 2, 3, 4, 3.5, 5, 3, 2.2, 3, 0, -1, 2, 0, -2, -5, -8, -7, -2, 0, 1, 1]
|
18
11
|
end
|
19
12
|
|
20
13
|
it "works for the simple case" do
|
data/spec/savgol_spec.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Savgol do
|
4
|
+
describe 'padding' do
|
5
|
+
let(:array) { [1, 2, 3, 4, -7, -2, 0, 1, 1] }
|
6
|
+
it 'pads with the reverse geometrically inverted sequence' do
|
7
|
+
expect(Savgol.sg_pad_ends(array, 2)).to eq [-1, 0, 1, 2, 3, 4, -7, -2, 0, 1, 1, 1, 2]
|
8
|
+
expect(Savgol.sg_pad_ends(array, 3)).to eq [-2, -1, 0, 1, 2, 3, 4, -7, -2, 0, 1, 1, 1, 2, 4]
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'pads unevenly spaced x vals' do
|
12
|
+
ar = %w(-1 0 2 3 4 7 8 10 11 12 13 14 17 18).map &:to_f
|
13
|
+
expect(Savgol.sg_pad_xvals(ar, 2)).to eq %w(-4 -2 -1 0 2 3 4 7 8 10 11 12 13 14 17 18 19 22]).map(&:to_f)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe 'finding nearest index' do
|
18
|
+
let(:original) { [0,1,2,3,4] }
|
19
|
+
|
20
|
+
it 'finds nearest index to the left' do
|
21
|
+
newvals = [-2, -1]
|
22
|
+
expect(Savgol.sg_nearest_index(original, newvals)).to eq([0,0])
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'finds nearest indices in the middle' do
|
26
|
+
newvals = [1,1.4,1.5,1.6,2,2.1]
|
27
|
+
expect(Savgol.sg_nearest_index(original, newvals)).to eq([1,1,1,2,2,2])
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'finds nearest index to the right' do
|
31
|
+
newvals = [5,6]
|
32
|
+
expect(Savgol.sg_nearest_index(original, newvals)).to eq([4,4])
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'efficiently finds the nearest index' do
|
36
|
+
newvals = [-2,-1,1,2.4,2.5,4,4.5,5]
|
37
|
+
expect(Savgol.sg_nearest_index(original, newvals)).to eq([0,0,1,2,2,4,4,4])
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe 'smoothing unevenly spaced data' do
|
42
|
+
xvals = %w(-1 0 2 3 4 7 8 10 11 12 13 14 17 18).map &:to_f
|
43
|
+
new_xvals = xvals.map {|v| v + 0.5 }
|
44
|
+
yvals = %w(-2 1 0 1 1 3 4 7 8 9 7 4 1 2).map {|v| v.to_f + 30 }
|
45
|
+
|
46
|
+
yvals_on = Savgol.savgol_uneven(xvals, yvals, 5, 2)
|
47
|
+
|
48
|
+
yvals_off = Savgol.savgol_uneven(xvals, yvals, 5, 2, new_xvals: new_xvals)
|
49
|
+
|
50
|
+
yvals_pretend_even = Savgol.savgol(yvals, 5, 2)
|
51
|
+
|
52
|
+
if false
|
53
|
+
require 'gnuplot'
|
54
|
+
Gnuplot.open do |gp|
|
55
|
+
Gnuplot::Plot.new(gp) do |plot|
|
56
|
+
|
57
|
+
plot.data << Gnuplot::DataSet.new([new_xvals, yvals_off]) do |ds|
|
58
|
+
ds.title = "x interpolated"
|
59
|
+
ds.with = "linespoints"
|
60
|
+
end
|
61
|
+
|
62
|
+
plot.data << Gnuplot::DataSet.new([xvals, yvals_pretend_even]) do |ds|
|
63
|
+
ds.title = "pretend evenly spaced"
|
64
|
+
ds.with = "linespoints"
|
65
|
+
end
|
66
|
+
|
67
|
+
plot.data << Gnuplot::DataSet.new([xvals, yvals_on]) do |ds|
|
68
|
+
ds.title = "smoothed"
|
69
|
+
ds.with = "linespoints"
|
70
|
+
end
|
71
|
+
|
72
|
+
plot.data << Gnuplot::DataSet.new([xvals, yvals]) do |ds|
|
73
|
+
ds.title = "original"
|
74
|
+
ds.with = "points"
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: savgol
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- John T. Prince
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-
|
12
|
+
date: 2014-04-24 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
@@ -81,6 +81,20 @@ dependencies:
|
|
81
81
|
- - ~>
|
82
82
|
- !ruby/object:Gem::Version
|
83
83
|
version: 0.8.2
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: gnuplot
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - '>='
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
type: :development
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - '>='
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
84
98
|
description: Extends Array class with method which calculates applies Savitzky-Golay
|
85
99
|
filter used for smoothing the data
|
86
100
|
email:
|
@@ -102,6 +116,7 @@ files:
|
|
102
116
|
- savgol.gemspec
|
103
117
|
- spec/savgol/array_spec.rb
|
104
118
|
- spec/savgol_shared_example.rb
|
119
|
+
- spec/savgol_spec.rb
|
105
120
|
- spec/spec_helper.rb
|
106
121
|
homepage: http://github.com/princelab/savgol
|
107
122
|
licenses:
|
@@ -130,4 +145,5 @@ summary: performs Savitzky-Golay smoothing
|
|
130
145
|
test_files:
|
131
146
|
- spec/savgol/array_spec.rb
|
132
147
|
- spec/savgol_shared_example.rb
|
148
|
+
- spec/savgol_spec.rb
|
133
149
|
- spec/spec_helper.rb
|