gorilla 0.0.1.beta

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ = Gorilla
2
+
3
+ TODO
@@ -0,0 +1,13 @@
1
+ require 'rake/testtask'
2
+ require 'rdoctest/task'
3
+
4
+ Rdoctest::Task.new do |t|
5
+ t.ruby_opts << '-rgorilla/all -rgorilla/core_ext'
6
+ end
7
+
8
+ Rake::TestTask.new do |t|
9
+ t.libs << 'test'
10
+ t.pattern = 'test/**/*_test.rb'
11
+ end
12
+
13
+ task :default => [:doctest, :test]
@@ -0,0 +1,11 @@
1
+ require 'gorilla/unit'
2
+
3
+ module Gorilla
4
+ @units = {}
5
+
6
+ class << self
7
+ attr_reader :units
8
+ end
9
+ end
10
+
11
+ G = Gorilla
@@ -0,0 +1,5 @@
1
+ require 'gorilla'
2
+ require 'gorilla/temperature'
3
+ require 'gorilla/time'
4
+ require 'gorilla/volume'
5
+ require 'gorilla/weight'
@@ -0,0 +1,72 @@
1
+ module Gorilla::CoreExt
2
+ def unit
3
+ Gorilla::Unit.new self
4
+ end
5
+ alias units unit
6
+
7
+ Gorilla.units.each_pair do |klass_name, configs|
8
+ configs.each_key do |unit|
9
+ klass = Gorilla.const_get klass_name[/\w+$/]
10
+
11
+ define_method unit do
12
+ klass.new self, unit
13
+ end
14
+ alias_method "#{unit}s", unit if klass.pluralize
15
+ end
16
+ end
17
+
18
+ if Gorilla.const_defined? :Temperature
19
+ alias Celsius celsius
20
+ alias C celsius
21
+ alias Fahrenheit fahrenheit
22
+ alias F fahrenheit
23
+ end
24
+
25
+ if Gorilla.const_defined? :Temperature
26
+ alias s second
27
+ alias sec second
28
+ alias ms millisecond
29
+ alias min minute
30
+ alias h hour
31
+ alias hr hour
32
+ alias d day
33
+ end
34
+
35
+ if Gorilla.const_defined? :Volume
36
+ alias litre liter
37
+ alias l liter
38
+ alias L liter
39
+ alias millilitre milliliter
40
+ alias ml milliliter
41
+ alias mL milliliter
42
+ alias centilitre centiliter
43
+ alias cl centiliter
44
+ alias cL centiliter
45
+ alias t teaspoon
46
+ alias tsp teaspoon
47
+ alias T tablespoon
48
+ alias tbs tablespoon
49
+ alias tbsp tablespoon
50
+ alias fl_oz fluid_ounce
51
+ alias oz_fl fluid_ounce
52
+ alias c cup
53
+ alias cu cup
54
+ alias pt pint
55
+ alias qt quart
56
+ alias gal gallon
57
+ end
58
+
59
+ if Gorilla.const_defined? :Weight
60
+ alias g gram
61
+ alias kg kilogram
62
+ alias mg milligram
63
+ alias lb pound
64
+ alias lbs pounds
65
+ alias oz ounce
66
+ alias ozs ounces
67
+ end
68
+ end
69
+
70
+ class Numeric
71
+ include Gorilla::CoreExt
72
+ end
@@ -0,0 +1,222 @@
1
+ # encoding: utf-8
2
+ require 'gorilla/scantron_ext'
3
+
4
+ module Gorilla
5
+ # A {Scantron}[http://github.com/stephencelis/scantron] scanner class from
6
+ # which all Gorilla::Scanner classes inherit.
7
+ #
8
+ # Your own Gorilla::Scanners will inherit the data assigned their
9
+ # Gorilla::Unit definitions so that, for example, units defined as metric
10
+ # will have additional scanner rules created for each prefix.
11
+ #
12
+ # For more information, see Gorilla::Scanner.rule.
13
+ class Scanner < ::Scantron::Scanner
14
+ # Maps metric prefixes to regular expressions used for parsing.
15
+ METRIC_MAP = {
16
+ # :yotta => /Y/,
17
+ # :zetta => /Z/,
18
+ # :exa => /E/,
19
+ # :peta => /P/,
20
+ # :tera => /T/,
21
+ # :giga => /G/,
22
+ # :mega => /M/,
23
+ :kilo => /k(?:ilo)?/,
24
+ # :hecto => /H/,
25
+ # :deca => /da/,
26
+ # :deci => /d/,
27
+ :centi => /c(?:enti)?/,
28
+ :milli => /m(?:illi)?/
29
+ # :micro => /μ/,
30
+ # :nano => /n/,
31
+ # :pico => /p/,
32
+ # :femto => /f/,
33
+ # :atto => /a/,
34
+ # :zepto => /z/,
35
+ # :yocto => /y/
36
+ }
37
+
38
+ before_match do |r|
39
+ # Adjust for amounts before units.
40
+ pre_match = r.scanner.pre_match
41
+ if result = AmountScanner.new(pre_match).perform.last
42
+ between = r.scanner.string[
43
+ result.scanner.pos, pre_match.length - result.scanner.pos
44
+ ]
45
+
46
+ if between =~ /\S/
47
+ result = nil
48
+ else
49
+ r.length = r.length + (r.offset - result.offset)
50
+ r.offset = result.offset
51
+ end
52
+
53
+ amount = result.value if result
54
+ end
55
+
56
+ # Adjust for generic amounts.
57
+ unless amount
58
+ if match = pre_match =~ /\ban? *$/i
59
+ r.length = r.length + (r.offset - match)
60
+ r.offset = match
61
+ amount = 1
62
+ elsif match = pre_match =~ /\b(?:a )couple *$/i
63
+ r.length = r.length + (r.offset - match)
64
+ r.offset = match
65
+ amount = 2
66
+ end
67
+ end
68
+
69
+ # Adjust for trailing amounts ("...and a half").
70
+ if match = r.scanner.post_match.match(/^ and(?: an?)? (.+)/)
71
+ if r.scantron.class.parse(match[1]).nil?
72
+ if result = NumberScanner.new(match[1]).perform.first
73
+ if result.offset == 0
74
+ r.length = r.length + result.length + match.offset(1).first
75
+ amount += result.value
76
+ end
77
+ end
78
+ end
79
+ # Adjust for trailing ranges ("...or two").
80
+ elsif match = r.scanner.post_match.match(/^ or (.+)/)
81
+ if r.scantron.class.parse(match[1]).nil?
82
+ if result = NumberScanner.new(match[1]).perform.first
83
+ if result.offset == 0
84
+ r.length = r.length + result.length + match.offset(1).first
85
+ amount = amount..result.value
86
+ end
87
+ end
88
+ end
89
+ # Adjust for periods.
90
+ elsif r.scanner.post_match =~ /^\./
91
+ r.length += 1
92
+ end
93
+
94
+ r[:amount] = amount
95
+ r
96
+ end
97
+
98
+ after_match do |r|
99
+ case amount = r[:amount]
100
+ when Range
101
+ unit_class = constantize r.rule.data[:class_name]
102
+ unit_class.new(amount.min, r.name)..unit_class.new(amount.max, r.name)
103
+ else
104
+ constantize(r.rule.data[:class_name]).new amount, r.name
105
+ end
106
+ end
107
+
108
+ class << self
109
+ # ==== Options
110
+ #
111
+ # [<tt>:class_name</tt>] The Gorilla::Unit return class for the rule
112
+ # given. By default, it is inferred from the name
113
+ # of the scanner, so that a "BogosityScanner"
114
+ # would try instantiate matches as "Bogosity"
115
+ # units.
116
+ #
117
+ # [<tt>:metric</tt>] Whether or not additional rules should be
118
+ # generated for metric prefixes. By default, it
119
+ # is inferred from the unit's original
120
+ # definition.
121
+ #
122
+ # ==== Example
123
+ #
124
+ # Here we define a metric unit and scanner rule:
125
+ #
126
+ # class Beauty < Gorilla::Unit
127
+ # base :Helen, :metric => true
128
+ # end
129
+ #
130
+ # And here we define the scanner rule:
131
+ #
132
+ # class BeautyScanner < Gorilla::Scanner
133
+ # rule :Helen, /[Hh](?:elen)?s?/
134
+ # end
135
+ #
136
+ # BeautyScanner.scan '1 milliHelen is required to launch the ship.'
137
+ # # => [(1 milliHelen)]
138
+ #
139
+ # BeautyScanner.scan '2 kiloHelens'
140
+ # # => [(2 kiloHelens)]
141
+ #
142
+ # The return class (Beauty) is inferred from the scanner's class name
143
+ # (less "Scanner"), and the metric setting is taken from the matching
144
+ # rule on that class, but both can be overridden or made explicit.
145
+ #
146
+ # class BeautyScanner < Gorilla::Scanner
147
+ # rule :Helen, /[Hh](?:elen)?s?/, :class_name => 'Beauty',
148
+ # :metric => true
149
+ # end
150
+ def rule unit, regexp, data = {}, &block
151
+ if class_name = data.delete(:class_name)
152
+ klass = constantize class_name
153
+ elsif class_name = name.sub!(/Scanner$/, '')
154
+ klass = constantize class_name rescue nil
155
+ end
156
+
157
+ config = { :class_name => class_name || 'Unit' }
158
+ config.update klass.rules[unit] if klass && klass.rules[unit]
159
+ config.update data
160
+
161
+ super unit, /(?<=^|[\d ])#{regexp}(?=[\d ]|\b|$)/, config, &block
162
+
163
+ if config[:metric]
164
+ METRIC_MAP.each_pair do |pre, sub|
165
+ super :"#{pre}#{unit}", /(?<=^|[\d ])#{sub}#{regexp}(?= |\b|$)/,
166
+ config, &block
167
+ end
168
+ end
169
+ end
170
+
171
+ private
172
+
173
+ def constantize class_name
174
+ names = class_name.split '::'
175
+ names.shift if names.first && names.first.empty?
176
+
177
+ constant = Object
178
+ names.each do |name|
179
+ constant = if constant.const_defined?(name)
180
+ constant.const_get name
181
+ else
182
+ constant.const_missing name
183
+ end
184
+ end
185
+ constant
186
+ end
187
+ end
188
+
189
+ def scan
190
+ results = perform
191
+ array = []
192
+ range = false
193
+
194
+ results.each.with_index do |result, index|
195
+ if range
196
+ range = false
197
+ next
198
+ end
199
+
200
+ if !result.value.is_a?(Range) and next_result = results[index + 1]
201
+ substring = string[
202
+ result.scanner.pos, next_result.offset - result.scanner.pos
203
+ ]
204
+
205
+ case substring
206
+ when /^ *(and|or|to) *$/
207
+ if $1 == 'and' && result.pre_match !~ /between $/
208
+ array << (result.value + next_result.value)
209
+ else
210
+ array << (result.value..next_result.value)
211
+ end
212
+ range = true and next
213
+ end
214
+ end
215
+
216
+ array << result.value
217
+ end
218
+
219
+ array
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,9 @@
1
+ require 'scantron'
2
+ require 'number_scanner'
3
+ require 'range_scanner'
4
+ require 'gorilla/scanner'
5
+ require 'gorilla/scanners/temperature_scanner'
6
+ require 'gorilla/scanners/time_scanner'
7
+ require 'gorilla/scanners/volume_scanner'
8
+ require 'gorilla/scanners/weight_scanner'
9
+ require 'gorilla/scanners/omni_scanner'
@@ -0,0 +1,21 @@
1
+ # encoding: utf-8
2
+ require 'gorilla/scanner'
3
+ require 'gorilla/scanners/temperature_scanner'
4
+ require 'gorilla/scanners/time_scanner'
5
+ require 'gorilla/scanners/volume_scanner'
6
+ require 'gorilla/scanners/weight_scanner'
7
+
8
+ module Gorilla
9
+ # An all-purpose units scanner combining the rules of TemperatureScanner,
10
+ # TimeScanner, VolumeScanner, and WeightScanner.
11
+ #
12
+ # Gorilla::OmniScanner.
13
+ # scan "Add 1 cup flour (125g). Bake @ 350F for 25 min."
14
+ # # => [(1 cup), (125 grams), (350° Fahrenheit), (25 minutes)]
15
+ class OmniScanner < Scanner
16
+ rules.update TemperatureScanner.rules
17
+ rules.update TimeScanner.rules
18
+ rules.update VolumeScanner.rules
19
+ rules.update WeightScanner.rules
20
+ end
21
+ end
@@ -0,0 +1,11 @@
1
+ # encoding: utf-8
2
+ require 'gorilla/scanner'
3
+ require 'gorilla/temperature'
4
+
5
+ module Gorilla
6
+ class TemperatureScanner < Scanner
7
+ degrees = /°| ?deg(?:ree)?s? /
8
+ rule :celsius, /#{degrees}?(?:C|[Cc]elsius)|℃/
9
+ rule :fahrenheit, /#{degrees}?(?:F|[Ff]ahrenheit)|℉/
10
+ end
11
+ end
@@ -0,0 +1,34 @@
1
+ require 'gorilla/scanner'
2
+ require 'gorilla/time'
3
+
4
+ module Gorilla
5
+ class TimeScanner < Scanner
6
+ rule :second, /[Ss](?:ec(?:ond)?s?)?|S(?:EC(?:OND)?S?)/
7
+ rule :minute, /[Mm](?:in(?:ute)?s?)?|M(?:IN(?:UTE)?S?)/
8
+ rule :hour, /[Hh](?:(?:ou)?rs?)?|H(?:(?:OU)?RS?)?/
9
+ rule :day, /[Dd](?:ays?)?|D(?:AYS?)/
10
+ rule :week, /[Ww](?:ee)?ks?|W(?:EE)?KS?/
11
+ rule :month, /[Mm](?:o(?:n(?:th))?s?)|M(?:O(?:N(?:TH))?S?)/
12
+ rule :year, /[Yy](?:ea)?rs?|Y(?:EA)?RS?/
13
+ rule :decade, /[Dd]ecades?|DECADES?/
14
+ rule :century, /[Cc]entur(?:y|ies)|CENTUR(?:Y|IES)/
15
+ rule :millennium, /[Mm]illeni(?:um|a)|MILLENI(?:UM|A)/
16
+
17
+ rule :iso8601, /\bP(\d+Y)?(\d+W)?(\d+D)?T?(\d+H)?(\d+M)?([\d.]+S)?\b/ do |r|
18
+ y = Time.new r.scanner[1].to_i, :year
19
+ w = Time.new r.scanner[2].to_i, :week
20
+ d = Time.new r.scanner[3].to_i, :day
21
+ h = Time.new r.scanner[4].to_i, :hour
22
+ m = Time.new r.scanner[5].to_i, :minute
23
+ s = Time.new r.scanner[6].to_f, :second
24
+ y + w + d + h + m + s
25
+ end
26
+
27
+ rule :delimited, /\d{1,2}(?::\d{2}){1,2}/ do |r|
28
+ h, m, s = r.to_s.split ':'
29
+ time = Time.new(h, :hour) + Time.new(m, :minute)
30
+ time += Time.new(s, :second) if s
31
+ time
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,15 @@
1
+ require 'gorilla/scanner'
2
+ require 'gorilla/volume'
3
+
4
+ module Gorilla
5
+ class VolumeScanner < Scanner
6
+ rule :liter, /[Ll](?:i?t(?:er|re?)|t|r)?s?/
7
+ rule :teaspoon, /(?:t|(?:[Tt](?:ea)?s(?:p(?:oo)?n?)?))s?/
8
+ rule :tablespoon, /(?:T|(?:[Tt](?:bl?s?p?|a?b(?:le?)?(?:s(?:p(?:oo)?n?)))))s?/
9
+ rule :fluid_ounce, /(?:[Ff]l(?:uid|\.)? )?[Oo](?:unce|z)s?/
10
+ rule :cup, /[Cc]u?p?s?/
11
+ rule :pint, /[Pp](?:(?:i?n)?t)?s?/
12
+ rule :quart, /[Qq](?:(?:ua)?r)?ts?/
13
+ rule :gallon, /[Gg](?:a?l(?:(?:lo)?n)?)s?/
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ require 'gorilla/scanner'
2
+ require 'gorilla/weight'
3
+
4
+ module Gorilla
5
+ class WeightScanner < Scanner
6
+ rule :gram, /[Gg](?:r(?:am)?|m)?s?/
7
+ rule :ounce, /[Oo](?:unce|z)s?/
8
+ rule :pound, /(?:[Pp](?:ou)?n?d|[Ll]b|#)s?/
9
+ end
10
+ end
@@ -0,0 +1,83 @@
1
+ require 'scantron'
2
+ require 'amount_scanner'
3
+
4
+ class NumberScanner
5
+ words = WORD_MAP.keys.map { |v| v.sub /y$/, 'y-?' } * '|'
6
+ rules[:human].regexp = \
7
+ %r{(?:\b(?:\d+ (?:an?d? )*)?(?:#{words}))(?: ?\b(?:#{words}|an?d?|\d+)\b ?)*}i
8
+
9
+ def self.human_to_number words
10
+ numbers = words.split(/\W+/).map { |w|
11
+ WORD_MAP[w.downcase] || parse(w) || w
12
+ }
13
+
14
+ case numbers.count { |n| n.is_a? Numeric }
15
+ when 0 then false
16
+ when 1 then numbers[0]
17
+ else
18
+ array = []
19
+ total = 0
20
+ limit = 1
21
+ words = []
22
+ reset = true
23
+
24
+ numbers.each.with_index do |n, i|
25
+ words << n and next if n.is_a? String
26
+
27
+ if n == 1 && limit == 1
28
+ reset = false
29
+ next
30
+ end
31
+
32
+ if n >= 1_000
33
+ total += n * limit
34
+ limit = 1
35
+ reset = true
36
+ else
37
+ if n < 1
38
+ if words.join(' ') =~ /\band\b/
39
+ if total > 0 && total % 1_000
40
+ if total % (factor = 10 ** (total.to_i.to_s.size - 1)) == 0
41
+ limit = n * factor
42
+ else
43
+ limit = n
44
+ end
45
+ else
46
+ limit += n
47
+ end
48
+ else
49
+ limit *= n
50
+ end
51
+ elsif words.join(' ') =~ /\band\b/ && numbers[i + 1].to_i < 1
52
+ total += limit
53
+ limit = n
54
+ elsif !reset && limit >= 1 &&
55
+ m1 = (n > (m2 = numbers[i + 1].to_i) ? n + m2 : n) and
56
+ m = [limit, m1].sort and
57
+ !m[1].to_s[-(m0 = m[0].to_i.to_s.size), m0].to_i.zero?
58
+
59
+ array << total + limit
60
+ total = 0
61
+ limit = n
62
+ elsif !reset && limit == 1 && n > numbers[i + 1].to_i &&
63
+ m = [limit, n + numbers[i + 1].to_i].sort and
64
+ !m[1].to_s[-(m[0].to_i.to_s.size), m[0].to_i.to_s.size].to_i.zero?
65
+
66
+ array << total + limit
67
+ total = 0
68
+ limit = n
69
+ else
70
+ n > limit ? limit *= n : limit += n
71
+ end
72
+
73
+ total += limit if numbers[i + 1].nil?
74
+ reset = false
75
+ end
76
+
77
+ words.clear
78
+ end
79
+
80
+ array.empty? ? total : array << total
81
+ end
82
+ end
83
+ end