pragmatic_segmenter 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +1 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +730 -0
  7. data/Rakefile +4 -0
  8. data/lib/pragmatic_segmenter.rb +2 -0
  9. data/lib/pragmatic_segmenter/abbreviation.rb +22 -0
  10. data/lib/pragmatic_segmenter/abbreviation_replacer.rb +149 -0
  11. data/lib/pragmatic_segmenter/between_punctuation.rb +78 -0
  12. data/lib/pragmatic_segmenter/cleaner.rb +141 -0
  13. data/lib/pragmatic_segmenter/ellipsis.rb +36 -0
  14. data/lib/pragmatic_segmenter/exclamation_words.rb +19 -0
  15. data/lib/pragmatic_segmenter/languages/amharic.rb +33 -0
  16. data/lib/pragmatic_segmenter/languages/arabic.rb +83 -0
  17. data/lib/pragmatic_segmenter/languages/armenian.rb +33 -0
  18. data/lib/pragmatic_segmenter/languages/burmese.rb +33 -0
  19. data/lib/pragmatic_segmenter/languages/deutsch.rb +132 -0
  20. data/lib/pragmatic_segmenter/languages/english.rb +44 -0
  21. data/lib/pragmatic_segmenter/languages/french.rb +29 -0
  22. data/lib/pragmatic_segmenter/languages/greek.rb +29 -0
  23. data/lib/pragmatic_segmenter/languages/hindi.rb +33 -0
  24. data/lib/pragmatic_segmenter/languages/italian.rb +39 -0
  25. data/lib/pragmatic_segmenter/languages/japanese.rb +58 -0
  26. data/lib/pragmatic_segmenter/languages/persian.rb +56 -0
  27. data/lib/pragmatic_segmenter/languages/russian.rb +60 -0
  28. data/lib/pragmatic_segmenter/languages/spanish.rb +39 -0
  29. data/lib/pragmatic_segmenter/languages/urdu.rb +33 -0
  30. data/lib/pragmatic_segmenter/list.rb +169 -0
  31. data/lib/pragmatic_segmenter/number.rb +35 -0
  32. data/lib/pragmatic_segmenter/process.rb +126 -0
  33. data/lib/pragmatic_segmenter/punctuation.rb +12 -0
  34. data/lib/pragmatic_segmenter/punctuation_replacer.rb +62 -0
  35. data/lib/pragmatic_segmenter/rules.rb +38 -0
  36. data/lib/pragmatic_segmenter/segmenter.rb +81 -0
  37. data/lib/pragmatic_segmenter/sentence_boundary_punctuation.rb +17 -0
  38. data/lib/pragmatic_segmenter/single_letter_abbreviation.rb +37 -0
  39. data/lib/pragmatic_segmenter/types.rb +12 -0
  40. data/lib/pragmatic_segmenter/version.rb +3 -0
  41. data/pragmatic_segmenter.gemspec +25 -0
  42. data/spec/performance_spec.rb +24 -0
  43. data/spec/pragmatic_segmenter_spec.rb +1906 -0
  44. data/spec/spec_helper.rb +1 -0
  45. metadata +150 -0
@@ -0,0 +1,35 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ module PragmaticSegmenter
4
+ # This class searches for numbers with periods within a string and
5
+ # replaces the periods.
6
+ class Number
7
+ # Rubular: http://rubular.com/r/oNyxBOqbyy
8
+ PeriodBeforeNumberRule = Rule.new(/\.(?=\d)/, '∯')
9
+
10
+ # Rubular: http://rubular.com/r/EMk5MpiUzt
11
+ NumberAfterPeriodBeforeLetterRule = Rule.new(/(?<=\d)\.(?=\S)/, '∯')
12
+
13
+ # Rubular: http://rubular.com/r/rf4l1HjtjG
14
+ NewLineNumberPeriodSpaceLetterRule = Rule.new(/(?<=\r\d)\.(?=(\s\S)|\))/, '∯')
15
+
16
+ # Rubular: http://rubular.com/r/HPa4sdc6b9
17
+ StartLineNumberPeriodRule = Rule.new(/(?<=^\d)\.(?=(\s\S)|\))/, '∯')
18
+
19
+ # Rubular: http://rubular.com/r/NuvWnKleFl
20
+ StartLineTwoDigitNumberPeriodRule = Rule.new(/(?<=^\d\d)\.(?=(\s\S)|\))/, '∯')
21
+
22
+ attr_reader :text
23
+ def initialize(text:)
24
+ @text = Text.new(text)
25
+ end
26
+
27
+ def replace
28
+ @formatted_text = @text.apply(PeriodBeforeNumberRule).
29
+ apply(NumberAfterPeriodBeforeLetterRule).
30
+ apply(NewLineNumberPeriodSpaceLetterRule).
31
+ apply(StartLineNumberPeriodRule).
32
+ apply(StartLineTwoDigitNumberPeriodRule)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,126 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'pragmatic_segmenter/list'
3
+ require 'pragmatic_segmenter/abbreviation_replacer'
4
+ require 'pragmatic_segmenter/number'
5
+ require 'pragmatic_segmenter/ellipsis'
6
+ require 'pragmatic_segmenter/exclamation_words'
7
+ require 'pragmatic_segmenter/punctuation_replacer'
8
+ require 'pragmatic_segmenter/between_punctuation'
9
+ require 'pragmatic_segmenter/sentence_boundary_punctuation'
10
+ require 'pragmatic_segmenter/punctuation'
11
+
12
+ module PragmaticSegmenter
13
+ # This class processing segmenting the text.
14
+ class Process
15
+ include Rules
16
+ # Rubular: http://rubular.com/r/NqCqv372Ix
17
+ QUOTATION_AT_END_OF_SENTENCE_REGEX = /[!?\.][\"\'\u{201d}\u{201c}]\s{1}[A-Z]/
18
+
19
+ # Rubular: http://rubular.com/r/JMjlZHAT4g
20
+ SPLIT_SPACE_QUOTATION_AT_END_OF_SENTENCE_REGEX = /(?<=[!?\.][\"\'\u{201d}\u{201c}])\s{1}(?=[A-Z])/
21
+
22
+ attr_reader :text, :doc_type
23
+ def initialize(text:, doc_type:)
24
+ @text = text
25
+ @doc_type = doc_type
26
+ end
27
+
28
+ def process
29
+ reformatted_text = PragmaticSegmenter::List.new(text: text).add_line_break
30
+ reformatted_text = replace_abbreviations(reformatted_text)
31
+ reformatted_text = replace_numbers(reformatted_text)
32
+ reformatted_text = reformatted_text.apply(GeoLocationRule)
33
+ split_lines(reformatted_text)
34
+ end
35
+
36
+ private
37
+
38
+ def split_lines(txt)
39
+ segments = []
40
+ lines = txt.split("\r")
41
+ lines.each do |l|
42
+ next if l.eql?('')
43
+ analyze_lines(l, segments, punctuation_array)
44
+ end
45
+ sentence_array = []
46
+ segments.each_with_index do |line|
47
+ next if line.gsub(/_{3,}/, '').length.eql?(0) || line.length < 2
48
+ line = reinsert_ellipsis(line)
49
+ line = line.apply(ExtraWhiteSpaceRule)
50
+ if line =~ QUOTATION_AT_END_OF_SENTENCE_REGEX
51
+ subline = line.split(SPLIT_SPACE_QUOTATION_AT_END_OF_SENTENCE_REGEX)
52
+ subline.each do |s|
53
+ sentence_array << s
54
+ end
55
+ else
56
+ sentence_array << line.tr("\n", '').strip
57
+ end
58
+ end
59
+ sentence_array.reject(&:empty?)
60
+ end
61
+
62
+ def analyze_lines(line, segments, punctuation)
63
+ line = line.apply(SingleNewLineRule, EllipsisRules::All, EmailRule)
64
+ clause_1 = false
65
+ end_punc_check = false
66
+ punctuation.each do |p|
67
+ end_punc_check = true if line[-1].include?(p)
68
+ clause_1 = true if line.include?(p)
69
+ end
70
+ if clause_1
71
+ segments = process_text(line, end_punc_check, segments)
72
+ else
73
+ line.gsub!(/ȹ/, "\n")
74
+ line.gsub!(/∯/, '.')
75
+ segments << line
76
+ end
77
+ end
78
+
79
+ def process_text(line, end_punc_check, segments)
80
+ line << 'ȸ' if !end_punc_check
81
+ PragmaticSegmenter::ExclamationWords.apply_rules(line)
82
+ between_punctutation(line)
83
+ line = line.apply(
84
+ DoublePuctationRules::All,
85
+ QuestionMarkInQuotationRule,
86
+ ExclamationPointRules::All
87
+ )
88
+ subline = sentence_boundary_punctuation(line)
89
+ subline.each_with_index do |s_l|
90
+ segments << sub_symbols(s_l)
91
+ end
92
+ end
93
+
94
+ def replace_numbers(txt)
95
+ PragmaticSegmenter::Number.new(text: txt).replace
96
+ end
97
+
98
+ def replace_abbreviations(txt)
99
+ PragmaticSegmenter::AbbreviationReplacer.new(text: txt).replace
100
+ end
101
+
102
+ def punctuation_array
103
+ PragmaticSegmenter::Punctuation.new.punct
104
+ end
105
+
106
+ def between_punctutation(txt)
107
+ PragmaticSegmenter::BetweenPunctuation.new(text: txt).replace
108
+ end
109
+
110
+ def sentence_boundary_punctuation(txt)
111
+ PragmaticSegmenter::SentenceBoundaryPunctuation.new(text: txt).split
112
+ end
113
+
114
+ def sub_symbols(txt)
115
+ txt.gsub(/∯/, '.').gsub(/♬/, '،').gsub(/♭/, ':').gsub(/ᓰ/, '。').gsub(/ᓱ/, '.')
116
+ .gsub(/ᓳ/, '!').gsub(/ᓴ/, '!').gsub(/ᓷ/, '?').gsub(/ᓸ/, '?').gsub(/☉/, '?!')
117
+ .gsub(/☈/, '!?').gsub(/☇/, '??').gsub(/☄/, '!!').delete('ȸ').gsub(/ȹ/, "\n")
118
+ end
119
+
120
+ def reinsert_ellipsis(line)
121
+ line.gsub(/ƪ/, '...').gsub(/♟/, ' . . . ')
122
+ .gsub(/♝/, '. . . .').gsub(/☏/, '..')
123
+ .gsub(/∮/, '.')
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,12 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ module PragmaticSegmenter
4
+ # This class holds the punctuation marks.
5
+ class Punctuation
6
+ PUNCT = ['。', '.', '.', '!', '!', '?', '?']
7
+
8
+ def punct
9
+ PUNCT
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,62 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ module PragmaticSegmenter
4
+ # This class replaces punctuation that is typically a sentence boundary
5
+ # but in this case is not a sentence boundary.
6
+ class PunctuationReplacer
7
+ attr_reader :matches_array, :text
8
+ def initialize(text:, matches_array:)
9
+ @text = text
10
+ @matches_array = matches_array
11
+ end
12
+
13
+ def replace
14
+ replace_punctuation(matches_array, text)
15
+ end
16
+
17
+ private
18
+
19
+ def replace_punctuation(array, txt)
20
+ return if !array || array.empty?
21
+ txt.gsub!('(', '\\(')
22
+ txt.gsub!(')', '\\)')
23
+ txt.gsub!(']', '\\]')
24
+ txt.gsub!('[', '\\[')
25
+ txt.gsub!('-', '\\-')
26
+ array.each do |a|
27
+ a.gsub!('(', '\\(')
28
+ a.gsub!(')', '\\)')
29
+ a.gsub!(']', '\\]')
30
+ a.gsub!('[', '\\[')
31
+ a.gsub!('-', '\\-')
32
+
33
+ sub = a.gsub('.', '∯')
34
+ txt.gsub!(/#{Regexp.escape(a)}/, "#{sub}")
35
+
36
+ sub_1 = sub.gsub('。', 'ᓰ')
37
+ txt.gsub!(/#{Regexp.escape(sub)}/, "#{sub_1}")
38
+
39
+ sub_2 = sub_1.gsub('.', 'ᓱ')
40
+ txt.gsub!(/#{Regexp.escape(sub_1)}/, "#{sub_2}")
41
+
42
+ sub_3 = sub_2.gsub('!', 'ᓳ')
43
+ txt.gsub!(/#{Regexp.escape(sub_2)}/, "#{sub_3}")
44
+
45
+ sub_4 = sub_3.gsub('!', 'ᓴ')
46
+ txt.gsub!(/#{Regexp.escape(sub_3)}/, "#{sub_4}")
47
+
48
+ sub_5 = sub_4.gsub('?', 'ᓷ')
49
+ txt.gsub!(/#{Regexp.escape(sub_4)}/, "#{sub_5}")
50
+
51
+ sub_6 = sub_5.gsub('?', 'ᓸ')
52
+ txt.gsub!(/#{Regexp.escape(sub_5)}/, "#{sub_6}")
53
+ end
54
+ txt.gsub!('\\(', '(')
55
+ txt.gsub!('\\)', ')')
56
+ txt.gsub!('\\[', '[')
57
+ txt.gsub!('\\]', ']')
58
+ txt.gsub!('\\-', '-')
59
+ txt
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,38 @@
1
+ module PragmaticSegmenter
2
+ module Rules
3
+ # Rubular: http://rubular.com/r/EUbZCNfgei
4
+ EmailRule = Rule.new(/(\w)(\.)(\w)/, '\1∮\3')
5
+
6
+ # Rubular: http://rubular.com/r/G2opjedIm9
7
+ GeoLocationRule = Rule.new(/(?<=[a-zA-z]°)\.(?=\s*\d+)/, '∯')
8
+
9
+ SingleNewLineRule = Rule.new(/\n/, 'ȹ')
10
+
11
+ ExtraWhiteSpaceRule = Rule.new(/\s{3,}/, ' ')
12
+
13
+ # Rubular: http://rubular.com/r/aXPUGm6fQh
14
+ QuestionMarkInQuotationRule = Rule.new(/\?(?=(\'|\"))/, 'ᓷ')
15
+
16
+ module ExclamationPointRules
17
+ # Rubular: http://rubular.com/r/XS1XXFRfM2
18
+ InQuotationRule = Rule.new(/\!(?=(\'|\"))/, 'ᓴ')
19
+
20
+ # Rubular: http://rubular.com/r/sl57YI8LkA
21
+ BeforeCommaMidSentenceRule = Rule.new(/\!(?=\,\s[a-z])/, 'ᓴ')
22
+
23
+ # Rubular: http://rubular.com/r/f9zTjmkIPb
24
+ MidSentenceRule = Rule.new(/\!(?=\s[a-z])/, 'ᓴ')
25
+
26
+ All = [ InQuotationRule, BeforeCommaMidSentenceRule, MidSentenceRule ]
27
+ end
28
+
29
+ module DoublePuctationRules
30
+ FirstRule = Rule.new(/\?!/, '☉')
31
+ SecondRule = Rule.new(/!\?/, '☈')
32
+ ThirdRule = Rule.new(/\?\?/, '☇')
33
+ ForthRule = Rule.new(/!!/, '☄')
34
+
35
+ All = [ FirstRule, SecondRule, ThirdRule, ForthRule ]
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,81 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'pragmatic_segmenter/types'
3
+ require 'pragmatic_segmenter/process'
4
+ require 'pragmatic_segmenter/cleaner'
5
+ require 'pragmatic_segmenter/languages/english'
6
+ require 'pragmatic_segmenter/languages/deutsch'
7
+ require 'pragmatic_segmenter/languages/hindi'
8
+ require 'pragmatic_segmenter/languages/persian'
9
+ require 'pragmatic_segmenter/languages/amharic'
10
+ require 'pragmatic_segmenter/languages/arabic'
11
+ require 'pragmatic_segmenter/languages/greek'
12
+ require 'pragmatic_segmenter/languages/armenian'
13
+ require 'pragmatic_segmenter/languages/burmese'
14
+ require 'pragmatic_segmenter/languages/urdu'
15
+ require 'pragmatic_segmenter/languages/french'
16
+ require 'pragmatic_segmenter/languages/italian'
17
+ require 'pragmatic_segmenter/languages/spanish'
18
+ require 'pragmatic_segmenter/languages/russian'
19
+ require 'pragmatic_segmenter/languages/japanese'
20
+ require 'pragmatic_segmenter/rules'
21
+
22
+ module PragmaticSegmenter
23
+ # This class segments a text into an array of sentences.
24
+ class Segmenter
25
+ include Rules
26
+ attr_reader :text, :language, :doc_type
27
+ def initialize(text:, **args)
28
+ return [] unless text
29
+ @language = args[:language] || 'en'
30
+ @doc_type = args[:doc_type]
31
+ if args[:clean].eql?(false)
32
+ @text = text.dup
33
+ else
34
+ case @language
35
+ when 'en'
36
+ @text = PragmaticSegmenter::Languages::English::Cleaner.new(text: text.dup, doc_type: args[:doc_type]).clean
37
+ when 'ja'
38
+ @text = PragmaticSegmenter::Languages::Japanese::Cleaner.new(text: text.dup, doc_type: args[:doc_type]).clean
39
+ else
40
+ @text = PragmaticSegmenter::Cleaner.new(text: text.dup, doc_type: args[:doc_type]).clean
41
+ end
42
+ end
43
+ end
44
+
45
+ def segment
46
+ return [] unless text
47
+ case language
48
+ when 'en'
49
+ PragmaticSegmenter::Process.new(text: text, doc_type: doc_type).process
50
+ when 'de'
51
+ PragmaticSegmenter::Languages::Deutsch::Process.new(text: text, doc_type: doc_type).process
52
+ when 'es'
53
+ PragmaticSegmenter::Languages::Spanish::Process.new(text: text, doc_type: doc_type).process
54
+ when 'it'
55
+ PragmaticSegmenter::Languages::Italian::Process.new(text: text, doc_type: doc_type).process
56
+ when 'ja'
57
+ PragmaticSegmenter::Languages::Japanese::Process.new(text: text, doc_type: doc_type).process
58
+ when 'el'
59
+ PragmaticSegmenter::Languages::Greek::Process.new(text: text, doc_type: doc_type).process
60
+ when 'ru'
61
+ PragmaticSegmenter::Languages::Russian::Process.new(text: text, doc_type: doc_type).process
62
+ when 'ar'
63
+ PragmaticSegmenter::Languages::Arabic::Process.new(text: text, doc_type: doc_type).process
64
+ when 'am'
65
+ PragmaticSegmenter::Languages::Amharic::Process.new(text: text, doc_type: doc_type).process
66
+ when 'hi'
67
+ PragmaticSegmenter::Languages::Hindi::Process.new(text: text, doc_type: doc_type).process
68
+ when 'hy'
69
+ PragmaticSegmenter::Languages::Armenian::Process.new(text: text, doc_type: doc_type).process
70
+ when 'fa'
71
+ PragmaticSegmenter::Languages::Persian::Process.new(text: text, doc_type: doc_type).process
72
+ when 'my'
73
+ PragmaticSegmenter::Languages::Burmese::Process.new(text: text, doc_type: doc_type).process
74
+ when 'ur'
75
+ PragmaticSegmenter::Languages::Urdu::Process.new(text: text, doc_type: doc_type).process
76
+ else
77
+ PragmaticSegmenter::Process.new(text: text, doc_type: doc_type).process
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,17 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ module PragmaticSegmenter
4
+ # This class splits text at sentence boundary punctuation marks
5
+ class SentenceBoundaryPunctuation
6
+ SENTENCE_BOUNDARY_REGEX = /\u{ff08}(?:[^\u{ff09}])*\u{ff09}(?=\s?[A-Z])|\u{300c}(?:[^\u{300d}])*\u{300d}(?=\s[A-Z])|\((?:[^\)])*\)(?=\s[A-Z])|'(?:[^'])*'(?=\s[A-Z])|"(?:[^"])*"(?=\s[A-Z])|“(?:[^”])*”(?=\s[A-Z])|\S.*?[。..!!??ȸȹ☉☈☇☄]/
7
+
8
+ attr_reader :text
9
+ def initialize(text:)
10
+ @text = text
11
+ end
12
+
13
+ def split
14
+ text.scan(SENTENCE_BOUNDARY_REGEX)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,37 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ module PragmaticSegmenter
4
+ # This class searches for periods within an abbreviation and
5
+ # replaces the periods.
6
+ class SingleLetterAbbreviation
7
+ # Rubular: http://rubular.com/r/e3H6kwnr6H
8
+ SingleUpperCaseLetterAtStartOfLineRule = Rule.new(/(?<=^[A-Z])\.(?=\s)/, '∯')
9
+
10
+ # Rubular: http://rubular.com/r/gitvf0YWH4
11
+ SingleUpperCaseLetterRule = Rule.new(/(?<=\s[A-Z])\.(?=\s)/, '∯')
12
+
13
+ attr_reader :text
14
+ def initialize(text:)
15
+ @text = text
16
+ end
17
+
18
+ def replace
19
+ @formatted_text = replace_single_letter_abbreviations(text)
20
+ end
21
+
22
+ private
23
+
24
+ def replace_single_letter_abbreviations(txt)
25
+ new_text = replace_single_uppercase_letter_abbreviation_at_start_of_line(txt)
26
+ replace_single_uppercase_letter_abbreviation(new_text)
27
+ end
28
+
29
+ def replace_single_uppercase_letter_abbreviation_at_start_of_line(txt)
30
+ txt.apply(SingleUpperCaseLetterAtStartOfLineRule)
31
+ end
32
+
33
+ def replace_single_uppercase_letter_abbreviation(txt)
34
+ txt.apply(SingleUpperCaseLetterRule)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,12 @@
1
+ module PragmaticSegmenter
2
+ Rule = Struct.new(:pattern, :replacement)
3
+
4
+ class Text < String
5
+ def apply(*rules)
6
+ rules.flatten.each do |rule|
7
+ self.gsub!(rule.pattern, rule.replacement)
8
+ end
9
+ self
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ module PragmaticSegmenter
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'pragmatic_segmenter/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "pragmatic_segmenter"
8
+ spec.version = PragmaticSegmenter::VERSION
9
+ spec.authors = ["Kevin S. Dias"]
10
+ spec.email = ["diasks2@gmail.com"]
11
+ spec.summary = %q{A rule-based sentence boundary detection gem that works out-of-the-box across many languages}
12
+ spec.description = %q{Pragmatic Segmenter is a sentence segmentation tool for Ruby. It allows you to split a text into an array of sentences. This gem provides 2 main benefits over other segmentation gems - 1) It works well even with ill-formatted text 2) It works for multiple languages }
13
+ spec.homepage = "https://github.com/diasks2/pragmatic_segmenter"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.7"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "rspec"
24
+ spec.add_development_dependency "rubocop"
25
+ end