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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +94 -0
- data/Rakefile +1 -0
- data/commerce_units.gemspec +34 -0
- data/lib/commerce_units/converter.rb +56 -0
- data/lib/commerce_units/dimension.rb +102 -0
- data/lib/commerce_units/simplifier.rb +33 -0
- data/lib/commerce_units/terms_reducer.rb +63 -0
- data/lib/commerce_units/unit.rb +98 -0
- data/lib/commerce_units/unit_lexer.rb +67 -0
- data/lib/commerce_units/unit_parser.rb +68 -0
- data/lib/commerce_units/value.rb +62 -0
- data/lib/commerce_units/version.rb +3 -0
- data/lib/commerce_units.rb +27 -0
- data/lib/generators/commerce_units/USAGE +7 -0
- data/lib/generators/commerce_units/install_generator.rb +38 -0
- data/lib/generators/commerce_units/templates/migrations/create_commerce_units_dimensions.rb.erb +11 -0
- data/spec/commerce_units/converter_spec.rb +37 -0
- data/spec/commerce_units/dimension_spec.rb +40 -0
- data/spec/commerce_units/simplifier_spec.rb +22 -0
- data/spec/commerce_units/terms_reducer_spec.rb +18 -0
- data/spec/commerce_units/unit_lexer_spec.rb +15 -0
- data/spec/commerce_units/unit_parser_spec.rb +15 -0
- data/spec/commerce_units/unit_spec.rb +15 -0
- data/spec/commerce_units/value_spec.rb +71 -0
- data/spec/database.yml +3 -0
- data/spec/debug.log +2753 -0
- data/spec/factories/base_factory.rb +30 -0
- data/spec/factories/dimension_factory.rb +41 -0
- data/spec/factories/length_factory.rb +39 -0
- data/spec/factories/time_factory.rb +31 -0
- data/spec/fixtures/migration.rb +11 -0
- data/spec/spec_helper.rb +17 -0
- 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,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,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
|
data/lib/generators/commerce_units/templates/migrations/create_commerce_units_dimensions.rb.erb
ADDED
@@ -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
|