commerce_units 0.0.5

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.
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
@@ -0,0 +1,98 @@
1
+ class CommerceUnits::Unit
2
+ class RequireOrdInstance < StandardError; end
3
+ class << self
4
+ def multiply(ua, ub)
5
+ new.tap do |u|
6
+ u.numerator = ua.numerator + ub.numerator
7
+ u.denominator = ua.denominator + ub.denominator
8
+ end.simplify
9
+ end
10
+ def divide(ua, ub)
11
+ multiply ua, ub.flip
12
+ end
13
+ def equals?(ua, ub)
14
+ divide(ua, ub).unitless?
15
+ end
16
+ def parse(string)
17
+ CommerceUnits::UnitParser.parse(string)
18
+ end
19
+ end
20
+
21
+ attr_reader :numerator, :denominator
22
+ def initialize
23
+ @numerator = []
24
+ @denominator = []
25
+ end
26
+
27
+ def *(other)
28
+ self.class.multiply self, other
29
+ end
30
+
31
+ def /(other)
32
+ self.class.divide self, other
33
+ end
34
+
35
+ def simplify
36
+ CommerceUnits::Unit.new.tap do |u|
37
+ u.numerator = _simplifier.numerator
38
+ u.denominator = _simplifier.denominator
39
+ end
40
+ end
41
+
42
+ def unitless?
43
+ numerator.blank? and denominator.blank?
44
+ end
45
+
46
+ def flip
47
+ CommerceUnits::Unit.new.tap do |u|
48
+ u.numerator = denominator
49
+ u.denominator = numerator
50
+ end
51
+ end
52
+
53
+ def to_root_dimension
54
+ CommerceUnits::Unit.new.tap do |u|
55
+ u.numerator = CommerceUnits.dimensional_database.from_array_of_unit_names!(numerator).map(&:root_dimension)
56
+ u.denominator = CommerceUnits.dimensional_database.from_array_of_unit_names!(denominator).map(&:root_dimension)
57
+ end
58
+ end
59
+
60
+ def numerator=(xs)
61
+ _assert_all_orderable! xs
62
+ @numerator = xs
63
+ end
64
+
65
+ def denominator=(xs)
66
+ _assert_all_orderable! xs
67
+ @denominator = xs
68
+ end
69
+
70
+ def ==(unit)
71
+ s = self.simplify
72
+ u = unit.simplify
73
+ s.numerator == u.numerator && s.denominator == u.denominator
74
+ end
75
+
76
+ def to_s
77
+ '#<CommerceUnits::Unit:' + self.object_id.to_s + " @numerator=#{numerator}, @denominator=#{denominator}>"
78
+ end
79
+
80
+ def pretty_inspect
81
+ n = numerator.join(" * ")
82
+ d = denominator.join(" * ")
83
+ n = n.blank? ? "(1)" : n
84
+ [n,d].reject(&:blank?).join(" / ")
85
+ end
86
+
87
+ private
88
+ def _assert_all_orderable!(xs)
89
+ xs.map do |x|
90
+ unless x.respond_to?(:<) && x.respond_to?(:>)
91
+ raise RequireOrdInstance, "You tried to use #{x} as a unit, but I can't do it because it's not comparable"
92
+ end
93
+ end
94
+ end
95
+ def _simplifier
96
+ @simplifier ||= CommerceUnits::Simplifier.new numerator: numerator, denominator: denominator
97
+ end
98
+ end
@@ -0,0 +1,67 @@
1
+ class CommerceUnits::UnitLexer
2
+ class UnexpectedCharacter < StandardError; end
3
+ class << self
4
+ def tokenize(string)
5
+ string.split("").inject(new) do |lexer, character|
6
+ case character
7
+ when "*"
8
+ lexer.multiply_token
9
+ when "/"
10
+ lexer.divide_token
11
+ when " "
12
+ lexer.white_space
13
+ when /[a-zA-Z0-9]/
14
+ lexer.character character
15
+ else
16
+ raise UnexpectedCharacter, character
17
+ end
18
+ end.tokens
19
+ end
20
+ end
21
+ attr_accessor :tokens
22
+ def initialize
23
+ @tokens = []
24
+ @mode = :new_character
25
+ end
26
+ def multiply_token
27
+ _operator_token "*"
28
+ end
29
+ def divide_token
30
+ _operator_token "/"
31
+ end
32
+ def white_space
33
+ if :character == @mode
34
+ @tokens[-1] += " "
35
+ @mode = :space
36
+ end
37
+ self
38
+ end
39
+ def character(character)
40
+ case @mode
41
+ when :new_character
42
+ @tokens << character
43
+ @mode = :character
44
+ when :character, :space
45
+ @tokens[-1] += character
46
+ @mode = :character
47
+ else
48
+ raise UnexpectedCharacter, "character #{character} doesn't belong here"
49
+ end
50
+ self
51
+ end
52
+ private
53
+ def _operator_token(operator)
54
+ case @mode
55
+ when :character
56
+ @tokens << operator
57
+ @mode = :new_character
58
+ when :space
59
+ @tokens[-1].strip!
60
+ @tokens << operator
61
+ @mode = :new_character
62
+ else
63
+ raise UnexpectedCharacter, operator + ' should come after only units'
64
+ end
65
+ self
66
+ end
67
+ end
@@ -0,0 +1,68 @@
1
+ class CommerceUnits::UnitParser
2
+ class UnexpectedToken < StandardError; end
3
+ class << self
4
+ def parse(string)
5
+ _tokens(string).reduce_with_lookahead(new) do |parser, token, lookahead|
6
+ case token
7
+ when "*"
8
+ parser.multiply_token
9
+ when "/"
10
+ parser.divide_token
11
+ when nil
12
+ parser.eof_token
13
+ else
14
+ parser.units_token token
15
+ end
16
+ end.unit
17
+ end
18
+
19
+ private
20
+ def _tokens(string)
21
+ tokens = CommerceUnits::UnitLexer.tokenize(string)
22
+ tokens += [nil] if tokens.count.odd?
23
+ tokens
24
+ end
25
+ end
26
+ attr_accessor :unit
27
+ def initialize
28
+ @mode = :multiply
29
+ @unit = CommerceUnits::Unit.new
30
+ end
31
+ def multiply_token
32
+ if :units == @mode
33
+ @mode = :multiply
34
+ end
35
+ self
36
+ end
37
+ def divide_token
38
+ if :units == @mode
39
+ @mode = :divide
40
+ end
41
+ self
42
+ end
43
+ def eof_token
44
+ case @mode
45
+ when :multiply
46
+ raise UnexpectedToken, "You need to multiply by a term, not just end the file"
47
+ when :divide
48
+ raise UnexpectedToken, "You need to divide by a term, not just end the file"
49
+ else
50
+ @mode = :eof
51
+ end
52
+ self
53
+ end
54
+ def units_token(token)
55
+ case @mode
56
+ when :multiply
57
+ @unit *= CommerceUnits::Unit.new.tap { |u| u.numerator = [token] }
58
+ @mode = :units
59
+ when :divide
60
+ @unit *= CommerceUnits::Unit.new.tap { |u| u.denominator = [token] }
61
+ @mode = :units
62
+ else
63
+ raise UnexpectedToken, "You didn't tell me to either divide or multiply by your token: #{token}"
64
+ end
65
+ self
66
+ end
67
+ end
68
+
@@ -0,0 +1,62 @@
1
+ class CommerceUnits::Value
2
+ class << self
3
+ def from_params(number: number, units: units)
4
+ new number, CommerceUnits::UnitParser.parse(units)
5
+ end
6
+ end
7
+ delegate :unitless?,
8
+ to: :unit
9
+ attr_accessor :number, :unit
10
+ def initialize(number=nil, unit=nil)
11
+ @number = number
12
+ @unit = unit
13
+ end
14
+
15
+ def +(value)
16
+ v = _convert_units_to_match value
17
+ self.class.new v.number + number, unit
18
+ end
19
+
20
+ def -(value)
21
+ v = _convert_units_to_match value
22
+ self.class.new number - v.number, unit
23
+ end
24
+
25
+ def *(value)
26
+ self.class.new number * value.number, CommerceUnits::Unit.multiply(unit, value.unit)
27
+ end
28
+
29
+ def /(value)
30
+ self.class.new number / value.number, CommerceUnits::Unit.divide(unit, value.unit)
31
+ end
32
+
33
+ def flip
34
+ self.class.new.tap do |v|
35
+ v.number = 1.0 / number
36
+ v.unit = unit.flip
37
+ end
38
+ end
39
+
40
+ def simplify
41
+ CommerceUnits::TermsReducer.new(self).reduce_to_simplest_terms
42
+ end
43
+
44
+ def ==(value)
45
+ v = _convert_units_to_match value
46
+ number == v.number && unit == v.unit
47
+ end
48
+
49
+ def to_s
50
+ "#{number} #{unit.pretty_inspect}"
51
+ end
52
+
53
+ def constant?; unitless; end
54
+
55
+ private
56
+ def _convert_units_to_match(value)
57
+ CommerceUnits::Converter.new.tap do |c|
58
+ c.target_unit = self.unit
59
+ c.origin_value = value
60
+ end.coerce
61
+ end
62
+ end
@@ -0,0 +1,3 @@
1
+ module CommerceUnits
2
+ VERSION = "0.0.5"
3
+ end
@@ -0,0 +1,27 @@
1
+ require "functional_support"
2
+ require "commerce_units/version"
3
+ require "commerce_units/converter"
4
+ require "commerce_units/dimension"
5
+ require "commerce_units/simplifier"
6
+ require "commerce_units/terms_reducer"
7
+ require "commerce_units/unit"
8
+ require "commerce_units/unit_lexer"
9
+ require "commerce_units/unit_parser"
10
+ require "commerce_units/value"
11
+ module CommerceUnits
12
+ def self.table_name_prefix
13
+ "commerce_units_"
14
+ end
15
+ def self.dimensional_database
16
+ @dimensional_database ||= CommerceUnits::Dimension
17
+ end
18
+ def self.dimensional_database=(some_sort_of_class)
19
+ @dimensional_database = some_sort_of_class
20
+ end
21
+ def self.nt(number, unit)
22
+ CommerceUnits::Value.from_params number: number, unit: unit
23
+ end
24
+ end
25
+
26
+ # A helpful alias, but if and only if it isn't already used
27
+ CU = CommerceUnits unless defined?(CU)
@@ -0,0 +1,7 @@
1
+ Description:
2
+ Generates the necessary files to get you up and running with Sorcery gem
3
+
4
+ Examples:
5
+ rails generate commerce_units:install
6
+
7
+ This will move the migration to create the commerce_units_dimensions table
@@ -0,0 +1,38 @@
1
+ require 'rails/generators/migration'
2
+ module CommerceUnits
3
+ module Generators
4
+ class InstallGenerator < Rails::Generators::Base
5
+ include Rails::Generators::Migration
6
+ desc "Create a migration for the dimensional storage"
7
+
8
+ source_root File.expand_path('../templates/migrations', __FILE__)
9
+
10
+ # Define the next_migration_number method (necessary for the migration_template method to work)
11
+ # Stolen shamelessly from socery gem's generators
12
+ def self.next_migration_number(dirname)
13
+ if ActiveRecord::Base.timestamped_migrations
14
+ sleep 1 # make sure each time we get a different timestamp
15
+ Time.new.utc.strftime("%Y%m%d%H%M%S")
16
+ else
17
+ "%.3d" % (current_migration_number(dirname) + 1)
18
+ end
19
+ end
20
+
21
+ def generate_migration
22
+ migration_template "create_commerce_units_dimensions.rb.erb", "db/migrate/#{migration_file_name}"
23
+ end
24
+
25
+ def migration_name
26
+ "create_commerce_units_dimensions"
27
+ end
28
+
29
+ def migration_file_name
30
+ "#{migration_name}.rb"
31
+ end
32
+
33
+ def migration_class_name
34
+ migration_name.camelize
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,11 @@
1
+ class CreateCommerceUnitsDimensions < ActiveRecord::Migration
2
+ def change
3
+ create_table :commerce_units_dimensions do |t|
4
+ t.string :root_dimension, null: false
5
+ t.string :unit_name, null: false
6
+ t.string :unitary_role, null: false, default: "tertiary"
7
+ t.decimal :multiply_constant, precision: 17, scale: 5, null: false, default: 1.0
8
+ t.timestamps
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+
3
+ describe CommerceUnits::Converter do
4
+ before { @lengths = CommerceUnits::LengthFactory.everything }
5
+ let(:centimeter) { CommerceUnits::LengthFactory.centimeter }
6
+ let(:meter) { CommerceUnits::LengthFactory.meter }
7
+ context 'factory' do
8
+ subject { CommerceUnits::Dimension.find_by_unit_name "centimeter" }
9
+ specify { should eq centimeter }
10
+ end
11
+ context '#coerce' do
12
+ let(:target_unit) { centimeter.to_unit }
13
+ let(:origin_unit) { meter.to_unit }
14
+ let(:origin_value) { CommerceUnits::Value.new 987, origin_unit }
15
+ let(:expected_value) { CommerceUnits::Value.new 98700, target_unit }
16
+ let(:converter) do
17
+ described_class.new.tap do |c|
18
+ c.target_unit = target_unit
19
+ c.origin_value = origin_value
20
+ end
21
+ end
22
+ subject { converter.coerce }
23
+ specify { should be_a CommerceUnits::Value }
24
+ specify { should eq expected_value }
25
+ context '#number' do
26
+ subject { converter.coerce.number }
27
+ specify { should be_a Numeric }
28
+ specify { should eq expected_value.number }
29
+ end
30
+
31
+ context '#unit' do
32
+ subject { converter.coerce.unit }
33
+ specify { should eq expected_value.unit }
34
+ end
35
+ end
36
+
37
+ end
@@ -0,0 +1,40 @@
1
+ # == Schema Information
2
+ #
3
+ # Table name: scientific_dimensions
4
+ #
5
+ # id :integer not null, primary key
6
+ # root_dimension :string(255) not null
7
+ # unit_name :string(255) not null
8
+ # unitary_role :string(255) default("tertiary"), not null
9
+ # multiply_constant :decimal(15, 5) default(1.0), not null
10
+ # created_at :datetime
11
+ # updated_at :datetime
12
+ #
13
+
14
+ require 'spec_helper'
15
+
16
+ describe CommerceUnits::Dimension do
17
+ let(:time1) { CommerceUnits::DimensionFactory.time_mock.make_primary! }
18
+ let(:time2) { CommerceUnits::DimensionFactory.time_mock }
19
+ let(:time3) { CommerceUnits::DimensionFactory.time_mock }
20
+ let(:time4) { CommerceUnits::DimensionFactory.time_mock }
21
+ let(:times) { [time1, time2, time3, time4] }
22
+ context '#root_dimension' do
23
+ specify { expect(time1.root_dimension).to eq time2.root_dimension }
24
+ specify { expect(time2.root_dimension).to eq time3.root_dimension }
25
+ specify { expect(time3.root_dimension).to eq time4.root_dimension }
26
+ end
27
+ context ':by_roots' do
28
+ subject { described_class.by_roots(:time) }
29
+ specify { should include times.first }
30
+ specify { should include times.second }
31
+ specify { should include times.third }
32
+ specify { should include times.last }
33
+ end
34
+ context ':primary_unit_by_roots' do
35
+ before { times }
36
+ subject { described_class.primary_unit_by_roots(:time).first }
37
+ specify { should eq time1 }
38
+ end
39
+
40
+ end