commerce_units 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +1 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +94 -0
  7. data/Rakefile +1 -0
  8. data/commerce_units.gemspec +34 -0
  9. data/lib/commerce_units/converter.rb +56 -0
  10. data/lib/commerce_units/dimension.rb +102 -0
  11. data/lib/commerce_units/simplifier.rb +33 -0
  12. data/lib/commerce_units/terms_reducer.rb +63 -0
  13. data/lib/commerce_units/unit.rb +98 -0
  14. data/lib/commerce_units/unit_lexer.rb +67 -0
  15. data/lib/commerce_units/unit_parser.rb +68 -0
  16. data/lib/commerce_units/value.rb +62 -0
  17. data/lib/commerce_units/version.rb +3 -0
  18. data/lib/commerce_units.rb +27 -0
  19. data/lib/generators/commerce_units/USAGE +7 -0
  20. data/lib/generators/commerce_units/install_generator.rb +38 -0
  21. data/lib/generators/commerce_units/templates/migrations/create_commerce_units_dimensions.rb.erb +11 -0
  22. data/spec/commerce_units/converter_spec.rb +37 -0
  23. data/spec/commerce_units/dimension_spec.rb +40 -0
  24. data/spec/commerce_units/simplifier_spec.rb +22 -0
  25. data/spec/commerce_units/terms_reducer_spec.rb +18 -0
  26. data/spec/commerce_units/unit_lexer_spec.rb +15 -0
  27. data/spec/commerce_units/unit_parser_spec.rb +15 -0
  28. data/spec/commerce_units/unit_spec.rb +15 -0
  29. data/spec/commerce_units/value_spec.rb +71 -0
  30. data/spec/database.yml +3 -0
  31. data/spec/debug.log +2753 -0
  32. data/spec/factories/base_factory.rb +30 -0
  33. data/spec/factories/dimension_factory.rb +41 -0
  34. data/spec/factories/length_factory.rb +39 -0
  35. data/spec/factories/time_factory.rb +31 -0
  36. data/spec/fixtures/migration.rb +11 -0
  37. data/spec/spec_helper.rb +17 -0
  38. metadata +224 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1abedfde0d6bd48016d5daaae8b936b1f6cdb3e5
4
+ data.tar.gz: 1a9d0c12f33623e6b2065cb2c7f1003a1dc43e2d
5
+ SHA512:
6
+ metadata.gz: ac8f6d6df3a1cc8c33fc79d28be9af62bb0d816c543d825aa65a2010e5dcd9ebc622785c55f56826ba7436c8ad44d6d1491549608b4ec8434361bc480aa3cc3a
7
+ data.tar.gz: 379345b2ea3e703311bcf26c24b8608603a7b9834537bc40f618763375207e4ad21e75b105248b0a928fba03e2bf87f87e8a64b93c54fd4ae8424bcbec54b809
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in commerce_units.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Thomas Chen
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # CommerceUnits
2
+
3
+ Another units library for Ruby which helps with maintaining consist mathematics. But unlike scientific (SI) units, where we're all agreed to things like 1000g = 1kg, 1 hour = 60 minutes, and whatnot, commerce_units makes no assumptions on what kinds of units there.
4
+
5
+ Instead, commerical units allows you to define your own conversion rates between different units and operations such as multiply and divide will automatically handle conversions and reductions while addition and subtraction will throw errors if inappropriately dimensioned values are added together.
6
+
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ gem 'commerce_units'
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install commerce_units
21
+
22
+ ## Usage
23
+
24
+ ### Step 1: generate the migration to create the dimensions
25
+ ```shell
26
+ rails generate commerce_units
27
+ ```
28
+ What?! This is unit library, yet you demand a database migration? What sort of stupid shit...
29
+
30
+ Well, the justification is that this is a commerce units library, and, in business, I have no idea what sort of units you might be using to quantify your transactions. In my case, I built this library originally to use for money per weight, and the dimension of money has all sorts of units (dollars, cents, rupies, RMB, yen, pound-sterling, etc.) which even change with time.
31
+
32
+ If you think your business is measured in dollars per boxes, then it's up to you to declare dollars with a money dimension and boxes in some other root dimension.
33
+
34
+ ### Step 2: seed the CommerceUnits::Dimension record with the units you'll need
35
+ Declare your units, here's an example
36
+ ```ruby
37
+ CommerceUnits::Dimension.create! root_dimension: :money,
38
+ unit_name: "dollar",
39
+ multiply_constant: 1.0,
40
+ unitary_role: :primary
41
+
42
+ CommerceUnits::Dimension.create! root_dimension: :money,
43
+ unit_name: "cent",
44
+ multiply_constant: 100.0 # 100 cents go into 1 us dollar
45
+
46
+ CommerceUnits::Dimension.create! root_dimension: :money,
47
+ unit_name: "RMB",
48
+ multiply_constant: 6.654 # 6.654 RMB go into 1 us dollar
49
+
50
+ CommerceUnits::Dimension.create! root_dimension: :money,
51
+ unit_name: "euro",
52
+ multiply_constant: 12000 # 12000 euros go into 1 us dollar because Yuropoor is poor
53
+
54
+ CommerceUnits::Dimension.create! root_dimension: :mass,
55
+ unit_name: 'pound',
56
+ multiply_constant: 1.0,
57
+ unitary_role: :primary
58
+
59
+ CommerceUnits::Dimension.create! root_dimension: :mass,
60
+ unit_name: 'ton',
61
+ multiply_constant: 2000 # 2000 pounds go into 1 ton
62
+
63
+ CommerceUnits::Dimension.create! root_dimension: :mass,
64
+ unit_name: 'landwhale',
65
+ multiply_constant: 354 # 354 pounds go into 1 landwhale
66
+ ```
67
+
68
+ ### Step 3: Use
69
+ Here's an example
70
+ ```ruby
71
+ dollar_per_pound = CommerceUnit::Value.from_params number: 1234, units: "dollar / pound"
72
+ dollar_per_pound.to_s # 1234 dollar / pound
73
+
74
+ euro_per_ton = CommerceUnit::Value.from_params number: 34, units: "euro / ton"
75
+ dollar_per_pound + euro_per_ton # 1234.00000... dollar / pound
76
+
77
+ dollar_per_landwhale_squared = CommerceUnit::Value.from_params number: 1, units: "dollar / landwhale / landwhale"
78
+ euro_per_ton + dollar_per_landwhale_squared # throws unit mismatch error
79
+
80
+ (euro_per_ton / dollar_per_pound ).unitless? # true
81
+
82
+ ```
83
+ ## Assumptions
84
+ If "mango" and "chair" are both declared as commerce units of the same dimension, then any amount of mangos can be converted to chairs by a constant multiplication. This necessarily means 0 mangos == 0 chairs
85
+
86
+ If you specify 0 as the multiply_constant, you'll eat a face full of DivideByZero errors.
87
+
88
+ ## Contributing
89
+
90
+ 1. Fork it ( http://github.com/<my-github-username>/commerce_units/fork )
91
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
92
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
93
+ 4. Push to the branch (`git push origin my-new-feature`)
94
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,34 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'commerce_units/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "commerce_units"
8
+ spec.version = CommerceUnits::VERSION
9
+ spec.authors = ["Thomas Chen"]
10
+ spec.email = ["foxnewsnetwork@gmail.com"]
11
+ spec.summary = %q{Another ruby units library for doing dimensional math, this one to be used with rails, preferrably commerce-related apps}
12
+ spec.description = %q{Another ruby units library for doing dimensional math, this one to be used with rails, preferrably commerce-related apps}
13
+ spec.homepage = "http://github.com/foxnewsnetwork/commerce_units"
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.required_ruby_version = ">= 1.9.2"
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.5"
24
+ spec.add_development_dependency "rake"
25
+ spec.add_development_dependency "rspec", ">= 2.14"
26
+ spec.add_development_dependency "sqlite3"
27
+ spec.add_development_dependency "ffaker"
28
+
29
+ spec.add_dependency('activemodel', '>= 3.0.0')
30
+ spec.add_dependency('activerecord', '>= 3.0.0')
31
+ spec.add_dependency('activesupport', '>= 3.0.0')
32
+ spec.add_dependency "functional_support", '>= 0.0.6'
33
+
34
+ end
@@ -0,0 +1,56 @@
1
+ class CommerceUnits::Converter
2
+ class << self
3
+ def convert(value: value, unit: unit)
4
+ new.tap do |c|
5
+ c.target_unit = unit
6
+ c.origin_value = value
7
+ end.coerce
8
+ end
9
+ end
10
+ class MismatchDimension < StandardError; end
11
+ class WrongType < StandardError; end
12
+ attr_reader :target_unit, :origin_value
13
+
14
+ def coerce
15
+ return origin_value if target_unit == origin_unit
16
+ raise MismatchDimension, "Unable to convert #{origin_unit} to #{target_unit}" if _mismatch_dimension?
17
+ _base_to_target_transform _origin_to_base_transform origin_value
18
+ end
19
+
20
+ def origin_unit
21
+ origin_value.unit
22
+ end
23
+
24
+ def target_unit=(unit)
25
+ raise WrongType, "#{unit} should be an instance of CommerceUnits::Unit" unless unit.is_a? CommerceUnits::Unit
26
+ @target_unit = unit
27
+ end
28
+
29
+ def origin_value=(value)
30
+ raise WrongType, "#{value} should be an instance of CommerceUnits::Value" unless value.is_a? CommerceUnits::Value
31
+ @origin_value = value
32
+ end
33
+
34
+ private
35
+
36
+ def _origin_to_base_transform(value)
37
+ _conversion_constants(origin_unit).inject(value) { |v, c| v * c }
38
+ end
39
+
40
+ def _conversion_constants(unit)
41
+ CommerceUnits.dimensional_database.from_array_of_unit_names!(unit.numerator).map(&:to_converter_value) +
42
+ CommerceUnits.dimensional_database.from_array_of_unit_names!(unit.denominator).map(&:to_converter_value).map(&:flip)
43
+ end
44
+
45
+ def _base_to_target_transform(value)
46
+ _conversion_constants(target_unit).inject(value) { |v,c| v * c }
47
+ end
48
+
49
+ def _mismatch_dimension?
50
+ not _matching_dimension?
51
+ end
52
+
53
+ def _matching_dimension?
54
+ target_unit.to_root_dimension == origin_unit.to_root_dimension
55
+ end
56
+ end
@@ -0,0 +1,102 @@
1
+ # == Schema Information
2
+ #
3
+ # Table name: commerce_units_dimensions
4
+ #
5
+ # id :integer not null, primary key
6
+ # root_dimension :string(255) not null
7
+ # unit_name :string(255) not null
8
+ # multiply_constant :decimal(15, 5) default(1.0), not null
9
+ # unitary_role :string(255) default("tertiary"), not null
10
+ # created_at :datetime
11
+ # updated_at :datetime
12
+ #
13
+
14
+ class CommerceUnits::Dimension < ActiveRecord::Base
15
+ class NoPrimaryUnit < StandardError; end
16
+ KnownRoots = [:money, :mass]
17
+
18
+ scope :primary_roles,
19
+ -> { where "#{self.table_name}.unitary_role = ?", :primary }
20
+
21
+ scope :by_roots,
22
+ -> (root_name) { where "#{self.table_name}.root_dimension = ?", root_name }
23
+
24
+ scope :primary_unit_by_roots,
25
+ -> (root_name) { primary_roles.by_roots(root_name) }
26
+
27
+ class << self
28
+ def from_array_of_unit_names!(names)
29
+ result = where unit_name: names
30
+ unless result.count == names.count
31
+ unfound_names = names.map(&:to_s) - result.map(&:unit_name)
32
+ raise ActiveRecord::RecordNotFound, "I don't know the following units: #{unfound_names} out of #{names}"
33
+ end
34
+ result
35
+ end
36
+
37
+ def create_and_consider_making_primary!(param_hash)
38
+ dimension = create! param_hash
39
+ dimension.make_primary! if primary_unit_by_roots(dimension.root_dimension).blank?
40
+ dimension
41
+ end
42
+
43
+ def find_or_create_and_consider_making_primary!(param_hash)
44
+ where(param_hash).first || create_and_consider_making_primary!(param_hash)
45
+ end
46
+ end
47
+
48
+ def to_converter_unit
49
+ CommerceUnits::Unit.new.tap do |u|
50
+ u.numerator = [_primary_root_unit_name]
51
+ u.denominator = [unit_name]
52
+ end
53
+ end
54
+
55
+ def to_unit
56
+ CommerceUnits::Unit.new.tap do |u|
57
+ u.numerator = [unit_name]
58
+ end
59
+ end
60
+
61
+ def to_converter_value
62
+ CommerceUnits::Value.new.tap do |v|
63
+ v.number = multiply_constant
64
+ v.unit = to_converter_unit
65
+ end
66
+ end
67
+
68
+ def to_value
69
+ CommerceUnits::Value.new.tap do |v|
70
+ v.number = multiply_constant
71
+ v.unit = to_unit
72
+ end
73
+ end
74
+
75
+ def make_primary!
76
+ _shift_primary! if self.class.primary_unit_by_roots(root_dimension).present?
77
+ _make_primary!
78
+ self
79
+ end
80
+
81
+ private
82
+ def _make_primary!
83
+ update multiply_constant: 1.0,
84
+ unitary_role: :primary
85
+ end
86
+ def _shift_primary!
87
+ self.class.by_roots(root_dimension).reject do |dim|
88
+ dim.id == self.id
89
+ end.each do |dim|
90
+ dim.update multiply_constant: dim.multiply_constant / multiply_constant,
91
+ unitary_role: :secondary
92
+ end
93
+ end
94
+ def _primary_unit
95
+ self.class.primary_unit_by_roots(root_dimension).first
96
+ end
97
+ def _primary_root_unit_name
98
+ d = _primary_unit
99
+ raise NoPrimaryUnit, "#{root_dimension} does not have a primary unit" if d.blank?
100
+ d.unit_name
101
+ end
102
+ end
@@ -0,0 +1,33 @@
1
+ class CommerceUnits::Simplifier
2
+ class NotComparable < StandardError; end
3
+ def initialize(numerator: [], denominator: [])
4
+ @original_top = numerator.sort
5
+ @original_bot = denominator.sort
6
+ end
7
+
8
+ def numerator
9
+ @numerator ||= _reduced_numerator_from top: @original_top, bot: @original_bot
10
+ end
11
+
12
+ def denominator
13
+ @denominator ||= _reduced_denominator_from top: @original_top, bot: @original_bot
14
+ end
15
+
16
+ private
17
+ def _reduced_numerator_from(top: [], bot: [])
18
+ return top if top.blank? or bot.blank?
19
+ return _reduced_numerator_from(top: top.tail, bot: bot.tail) if top.first == bot.first
20
+ return _reduced_numerator_from(top: top, bot: bot.tail) if top.first > bot.first
21
+ return [top.first] + _reduced_numerator_from(top: top.tail, bot: bot) if top.first < bot.first
22
+ raise NotComparable, "Failed to compare #{top.first} with #{bot.first}"
23
+ end
24
+
25
+ def _reduced_denominator_from(top: [], bot: [])
26
+ return bot if top.blank? or bot.blank?
27
+ return _reduced_denominator_from(top: top.tail, bot: bot.tail) if top.first == bot.first
28
+ return [bot.first] + _reduced_denominator_from(top: top, bot: bot.tail) if bot.first < top.first
29
+ return _reduced_denominator_from(top: top.tail, bot: bot) if bot.first > top.first
30
+ raise NotComparable, "Failed to compare #{top.first} with #{bot.first}"
31
+ end
32
+
33
+ end
@@ -0,0 +1,63 @@
1
+ class CommerceUnits::TermsReducer
2
+ attr_accessor :value
3
+ delegate :number, :unit, to: :value
4
+ def initialize(value)
5
+ @value = value
6
+ end
7
+
8
+ def reduce_to_simplest_terms
9
+ _reducing_terms.reduce(value, &:*)
10
+ end
11
+
12
+ private
13
+ def _reducing_terms
14
+ _units_by_dimensions.flat_map do |root_dimension, numerators_and_denominators|
15
+ numerators_and_denominators[:numerators].zip numerators_and_denominators[:denominators]
16
+ end.reject do |top_and_bottom|
17
+ top_and_bottom.first.blank? || top_and_bottom.last.blank?
18
+ end.map do |top_and_bottom|
19
+ _value_from_ratio *top_and_bottom
20
+ end
21
+ end
22
+
23
+ def _units_by_dimensions
24
+ _special_merge _numerator_by_dimensions, _denominator_by_dimensions
25
+ end
26
+
27
+ def _special_merge(top_group, bot_group)
28
+ output = {}
29
+ top_group.each do |root_dimension, unit_names|
30
+ output[root_dimension] ||= {}
31
+ output[root_dimension][:numerators] = unit_names
32
+ end
33
+ bot_group.each do |root_dimension, unit_names|
34
+ output[root_dimension] ||= {}
35
+ output[root_dimension][:denominators] = unit_names
36
+ end
37
+ output
38
+ end
39
+
40
+ def _numerator_by_dimensions
41
+ unit.numerator.group_by { |unit_name| _get_root_dim unit_name }
42
+ end
43
+
44
+ def _denominator_by_dimensions
45
+ unit.denominator.group_by { |unit_name| _get_root_dim unit_name }
46
+ end
47
+
48
+ def _value_from_ratio(top, bottom)
49
+ _get_conversion_value(top) / _get_conversion_value(bottom)
50
+ end
51
+
52
+ def _get_conversion_value(unit_name)
53
+ _get_dim(unit_name).to_converter_value
54
+ end
55
+
56
+ def _get_dim(unit_name)
57
+ CommerceUnits.dimensional_database.find_by_unit_name! unit_name
58
+ end
59
+
60
+ def _get_root_dim(unit_name)
61
+ _get_dim(unit_name).root_dimension
62
+ end
63
+ end