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,19 @@
1
+ # encoding: utf-8
2
+ require 'gorilla/unit'
3
+
4
+ module Gorilla
5
+ class Temperature < Unit
6
+ unit :celsius, lambda { |t| (t * Rational(9, 5)) + 32 }, :fahrenheit
7
+ unit :fahrenheit, lambda { |t| (t - 32) * Rational(5, 9) }, :celsius
8
+
9
+ self.pluralize = false
10
+
11
+ def humanized_amount
12
+ "#{super}°"
13
+ end
14
+
15
+ def humanized_unit
16
+ super.capitalize if unit
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,51 @@
1
+ require 'gorilla/unit'
2
+
3
+ module Gorilla
4
+ class Time < Unit
5
+ base :second, :metric => true
6
+
7
+ unit :minute, 60, :second
8
+ unit :hour, 60, :minute
9
+ unit :day, 24, :hour
10
+ unit :week, 7, :day
11
+ unit :month, 30, :day
12
+ unit :year, 52, :week
13
+ unit :decade, 10, :year
14
+ unit :century, 10, :decade
15
+ unit :millennium, 10, :century
16
+
17
+ # Expands in favor of non-metric units first. This behavior can be
18
+ # overridden by providing a block, and negated if the block returns +true+.
19
+ #
20
+ # ==== Example
21
+ #
22
+ # time = Gorilla::Time.new 1000, :second
23
+ # time.expand
24
+ # # => [(16 minutes), (40 seconds)]
25
+ #
26
+ # time.expand(:metric => true) { true }
27
+ # # => [(1 kilosecond)]
28
+ def expand options = {}, &block
29
+ block ||= lambda { |t| !t.metric? || t.unit == :second }
30
+ super options, &block
31
+ end
32
+
33
+ # Returns a string which represents the duration as defined by ISO 8601.
34
+ #
35
+ # time = 1.year + 2.weeks + 3.days + 4.hours + 5.minutes + 6.5.seconds
36
+ # time.iso8601
37
+ # # => "P1Y2W3DT4H5M6.5S"
38
+ def iso8601
39
+ string = 'P'
40
+ day = self.class.new 1, :day
41
+
42
+ expand.each do |measured|
43
+ string << 'T' if !string.include?('T') && measured < day
44
+ string << measured.humanized_amount
45
+ string << measured.unit.to_s[0, 1].capitalize
46
+ end
47
+
48
+ string
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,453 @@
1
+ # require 'bigdecimal'
2
+
3
+ module Gorilla
4
+ # The base unit class from which all inherit.
5
+ class Unit
6
+ # Pluralize by default.
7
+ @pluralize = true
8
+
9
+ # Maps metric prefixes to scale.
10
+ METRIC_MAP = {
11
+ :yotta => 1_000_000_000_000_000_000_000_000,
12
+ :zetta => 1_000_000_000_000_000_000_000,
13
+ :exa => 1_000_000_000_000_000_000,
14
+ :peta => 1_000_000_000_000_000,
15
+ :tera => 1_000_000_000_000,
16
+ :giga => 1_000_000_000,
17
+ :mega => 1_000_000,
18
+ :kilo => 1_000,
19
+ :hecto => 100,
20
+ :deca => 10,
21
+ :deci => Rational(1, 10),
22
+ :centi => Rational(1, 100),
23
+ :milli => Rational(1, 1_000),
24
+ :micro => Rational(1, 1_000_000),
25
+ :nano => Rational(1, 1_000_000_000),
26
+ :pico => Rational(1, 1_000_000_000_000),
27
+ :femto => Rational(1, 1_000_000_000_000_000),
28
+ :atto => Rational(1, 1_000_000_000_000_000_000),
29
+ :zepto => Rational(1, 1_000_000_000_000_000_000_000),
30
+ :yocto => Rational(1, 1_000_000_000_000_000_000_000_000)
31
+ }
32
+
33
+ class << self
34
+ # The base unit of the class.
35
+ attr_accessor :base_unit
36
+
37
+ # Whether or not to pluralize a unit.
38
+ attr_accessor :pluralize
39
+
40
+ # Defines the base unit of the class.
41
+ #
42
+ # ==== Example
43
+ #
44
+ # class Coolness < Gorilla::Unit
45
+ # base :Fonzie, :metric => true
46
+ # end
47
+ def base name, options = {}
48
+ self.base_unit = name
49
+ unit name, Rational(1), options
50
+ end
51
+
52
+ # Defines a unit of the class.
53
+ #
54
+ # The rule can be a Numeric factor or Proc relative to another unit. An
55
+ # optional hash of data can be appended to the rule and will be
56
+ # accessible wherever that rule is yielded to a block.
57
+ #
58
+ # ==== Example
59
+ #
60
+ # Gorilla::Weight.class_eval do
61
+ # unit :sun, 2 * (10 ** 30), :kilogram
62
+ # end
63
+ #
64
+ # Gorilla::Temperature.class_eval do
65
+ # unit :Q, lambda { |t| t + 57 }, :celsius, :source => 'Zork'
66
+ # end
67
+ def unit *args
68
+ options = args.last.is_a?(Hash) ? args.pop : {}
69
+ name, conversion, other = args
70
+
71
+ if conversion.respond_to? :call
72
+ (options[:rules] ||= {})[other] = conversion
73
+ elsif other
74
+ options[:factor] = Rational rules[other][:factor], conversion
75
+ else
76
+ options[:factor] = Rational conversion
77
+ end
78
+
79
+ rules[name] = options
80
+
81
+ if options[:metric]
82
+ METRIC_MAP.each_pair do |prefix, factor|
83
+ subname = :"#{prefix}#{name}"
84
+ unit subname, factor, name
85
+ rules[subname][:metric] = true
86
+ end
87
+ end
88
+ end
89
+
90
+ # Returns the hash of rules for the current class.
91
+ def rules
92
+ Gorilla.units[name] ||= {}
93
+ end
94
+
95
+ private
96
+
97
+ def inherited klass
98
+ klass.pluralize = pluralize
99
+ end
100
+ end
101
+
102
+ # The unit amount (can be +nil+).
103
+ attr_reader :amount
104
+
105
+ # The unit name.
106
+ attr_reader :unit
107
+
108
+ # Instantiates a new unit for the class. Assumes the base unit if one is
109
+ # defined.
110
+ #
111
+ # ==== Example
112
+ #
113
+ # Gorilla::Unit.new 1 # => (1)
114
+ # Gorilla::Time.new 1 # => (1 second)
115
+ # Gorilla::Time.new 1, :minute # => (1 minute)
116
+ def initialize amount, unit = self.class.base_unit
117
+ if unit && self.class.rules[unit].nil?
118
+ raise TypeError, "no such unit #{self.class.name}:#{unit}"
119
+ elsif unit.nil? && !instance_of?(Unit)
120
+ raise ArgumentError, "unit can't be nil for #{self.class.name}"
121
+ end
122
+
123
+ @amount, @unit = (amount.to_r if amount), unit
124
+ end
125
+
126
+ # Converts an instance to a new unit.
127
+ #
128
+ # ==== Example
129
+ #
130
+ # Gorilla::Weight.new(1, :pound).convert_to(:ounce) # => (16 ounces)
131
+ def convert_to other_unit
132
+ return dup if unit == other_unit
133
+
134
+ unless self.class.rules.key? other_unit
135
+ raise TypeError, "no such unit #{self.class.name}:#{other_unit}"
136
+ end
137
+
138
+ if unit and rules = self.class.rules[unit][:rules]
139
+ unless rules.key? other_unit
140
+ raise TypeError, "can't convert to #{self.class.name}:#{other_unit}"
141
+ end
142
+
143
+ amount = rules[other_unit].call normalized_amount
144
+ return self.class.new amount, other_unit
145
+ else
146
+ amount = normalized_amount
147
+ end
148
+
149
+ new = self.class.new amount
150
+ new.unit = other_unit
151
+ new
152
+ end
153
+
154
+ # Returns whether a unit was defined as metric.
155
+ #
156
+ # ==== Example
157
+ #
158
+ # class Coolness < Gorilla::Unit
159
+ # base :Fonzie, :metric => true
160
+ # end
161
+ # Coolness.new(1, :megaFonzie).metric? # => true
162
+ def metric?
163
+ unit and self.class.rules[unit][:metric] || false
164
+ end
165
+
166
+ # Normalizes and expands a unit into an array of units. Filters rules based
167
+ # on provided options, and yields each unit to an optional block. If the
168
+ # block returns +false+, the unit will be omitted.
169
+ #
170
+ # ==== Example
171
+ #
172
+ # Gorilla::Weight.new(24, :ounce).expand
173
+ # # => [(1 pound), (8 ounces)]
174
+ #
175
+ # # The block provided here is also the default for Gorilla::Time.
176
+ # Gorilla::Time.new(1000, :second).expand { |t|
177
+ # !t.metric? && t.unit != :minute || t.unit == :second
178
+ # }
179
+ # # => [(16 minutes), (40 seconds)]
180
+ def expand options = {}
181
+ rules = rules_for_options options
182
+ return [self] if rules.empty?
183
+
184
+ clone = self
185
+ units = []
186
+ rules.sort_by { |_, r| r[:factor] }.each do |rule|
187
+ clone = clone.convert_to rule[0]
188
+ next unless yield clone if block_given?
189
+ amount = clone.truncate
190
+ units << clone and break if clone.metric? && amount > 0
191
+ units << self.class.new(amount, clone.unit) if amount > 0
192
+ clone %= 1
193
+ end
194
+
195
+ units
196
+ end
197
+
198
+ # Normalizes a unit to the nearest whole number. Filters rules based on
199
+ # provided options, and yields each unit to an optional block. If the block
200
+ # returns +false+, the unit will be omitted.
201
+ #
202
+ # ==== Example
203
+ #
204
+ # weight = Gorilla::Weight.new 0.021, :kilogram # => (0.021 kilograms)
205
+ # weight.normalize
206
+ # # => (21 grams)
207
+ #
208
+ # weight.normalize { |w| w.metric? && w.amount >= 1 }
209
+ # # => (2.1 decagrams)
210
+ def normalize options = {}
211
+ rules = rules_for_options options
212
+ return if rules.empty?
213
+
214
+ rules.sort_by { |_, r| r[:factor] }.each do |rules|
215
+ clone = convert_to rules[0]
216
+ block_given? and case yield clone
217
+ when true then return clone when false then next
218
+ end
219
+ return clone if clone.amount >= 1 && (clone % 1).round(10).zero?
220
+ end
221
+
222
+ self
223
+ end
224
+
225
+ def normalize! options = {}, &block
226
+ normalized = normalize options, &block
227
+ return if eql? normalized
228
+ @amount, @unit = normalized.amount, normalized.unit
229
+ self
230
+ end
231
+
232
+ def coerced_amount
233
+ return unless self.amount
234
+ amount = metric? ? self.amount.to_f : self.amount.to_r
235
+ amount = amount.to_f if amount.denominator > 100
236
+ amount = amount.to_i if amount.denominator == 1
237
+ amount
238
+ end
239
+
240
+ def humanized_amount
241
+ return unless amount = coerced_amount
242
+
243
+ if amount.is_a?(Rational) && amount.numerator > amount.denominator
244
+ amount = "#{amount.floor} #{amount % 1}"
245
+ else
246
+ amount = "#{amount}"
247
+ end
248
+
249
+ amount = amount.split '.'
250
+ amount[0].gsub! /(?!\.)(\d)(?=(\d{3})+(?!\d))/, '\1,'
251
+ amount.join '.'
252
+ end
253
+
254
+ def humanized_unit
255
+ return unless unit
256
+ humanized = unit.to_s.gsub '_', ' '
257
+ humanized << (humanized.end_with?('s') ? 'es' : 's') if pluralize?
258
+ humanized
259
+ end
260
+
261
+ def to_s
262
+ [humanized_amount, humanized_unit].compact.join(' ')
263
+ end
264
+
265
+ def inspect
266
+ "(#{to_s})"
267
+ end
268
+
269
+ include Comparable
270
+
271
+ def <=> other
272
+ return unless self.class == other.class
273
+ normalized_amount <=> other.normalized_amount
274
+ end
275
+
276
+ def == other
277
+ return amount == other if instance_of? Unit
278
+ self.class == other.class && normalized_amount == other.normalized_amount
279
+ end
280
+
281
+ def + other
282
+ self.class.new amount + other.convert_to(unit).amount, unit
283
+ end
284
+
285
+ def - other
286
+ self.class.new amount - other.convert_to(unit).amount, unit
287
+ end
288
+
289
+ def * other
290
+ self.class.new amount * other, unit
291
+ end
292
+
293
+ def / other
294
+ self.class.new amount / other, unit
295
+ end
296
+
297
+ def % other
298
+ self.class.new amount % other, unit
299
+ end
300
+ alias modulo %
301
+
302
+ def ** other
303
+ self.class.new amount ** other, unit
304
+ end
305
+ alias power! **
306
+
307
+ def abs
308
+ self.class.new amount.abs, unit
309
+ end
310
+ alias magnitude abs
311
+
312
+ def abs2
313
+ self.class.new amount.abs2, unit
314
+ end
315
+
316
+ def ceil
317
+ amount.ceil
318
+ end
319
+
320
+ def coerce other
321
+ case other
322
+ when Unit
323
+ [other, convert_to(other.unit)]
324
+ when Numeric
325
+ [self.class.new(other, unit), self]
326
+ else
327
+ raise TypeError, "#{self.class} can't be coerced into #{other.class}"
328
+ end
329
+ end
330
+
331
+ def denominator
332
+ amount.denominator
333
+ end
334
+
335
+ def div n
336
+ to_i.div n
337
+ end
338
+
339
+ def eql? other
340
+ unit.eql?(other.unit) && normalized_amount.eql?(other.normalized_amount)
341
+ end
342
+
343
+ def even?
344
+ amount.even?
345
+ end
346
+
347
+ def floor
348
+ amount.floor
349
+ end
350
+
351
+ def finite?
352
+ amount.finite?
353
+ end
354
+
355
+ def infinite?
356
+ amount.infinite?
357
+ end
358
+
359
+ def integer?
360
+ amount && coerced_amount.integer?
361
+ end
362
+
363
+ def nonzero?
364
+ amount.zero?
365
+ end
366
+
367
+ def numerator
368
+ amount.numerator
369
+ end
370
+
371
+ def odd?
372
+ amount && amount.odd?
373
+ end
374
+
375
+ def real?
376
+ amount && amount.real?
377
+ end
378
+
379
+ def round digits
380
+ amount.round digits
381
+ end
382
+
383
+ def to_f
384
+ amount.to_f
385
+ end
386
+
387
+ def to_i
388
+ amount.to_i
389
+ end
390
+ alias to_int to_i
391
+ alias truncate to_i
392
+
393
+ def to_r
394
+ amount.to_r
395
+ end
396
+
397
+ def zero?
398
+ amount.zero?
399
+ end
400
+
401
+ protected
402
+
403
+ def unit= unit
404
+ @unit = unit and @amount *= factor || 1
405
+ end
406
+
407
+ def normalized_amount
408
+ factor ? Rational(amount, factor) : amount
409
+ end
410
+
411
+ private
412
+
413
+ def factor
414
+ return if instance_of? Unit
415
+ self.class.rules[unit][:factor] if self.class.rules.key? unit
416
+ end
417
+
418
+ def rules_for_options options
419
+ rules = self.class.rules.reject { |_, r| r[:factor].nil? }
420
+
421
+ unless options.empty?
422
+ rules.reject! { |_, r|
423
+ options.none? { |k, v| r[k] == v || r[k].nil? && v == false }
424
+ }
425
+ end
426
+
427
+ rules
428
+ end
429
+
430
+ def pluralize?
431
+ return false unless self.class.pluralize
432
+ return true unless coerced_amount
433
+
434
+ case abs = coerced_amount.abs
435
+ when Rational then abs <= 0 || abs > 1
436
+ when Numeric then abs != 1
437
+ else false
438
+ end
439
+ end
440
+
441
+ def method_missing method_name, *args, &block
442
+ if args.empty? && unit = method_name.to_s.sub!(/^to_/, '')
443
+ if Gorilla.units.key? unit
444
+ return convert_to unit
445
+ elsif Gorilla.const_defined? :CoreExt
446
+ return convert_to 1.send(unit).unit rescue super
447
+ end
448
+ end
449
+
450
+ super
451
+ end
452
+ end
453
+ end