gorilla 0.0.1.beta

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.
@@ -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