physical 0.4.8 → 0.4.9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 45adbe9fa036b31165e380d114a9742467b551368e2a2dfc720452f2fba97e4c
4
- data.tar.gz: 351f94658f866fe313b7499bcacef7de4a00c8370587dbbde9d81b093f6dd04d
3
+ metadata.gz: 8198a4417d9f1dd923a4bfee9bf8695ecb1c270853182a92835e64c1b9f4e10c
4
+ data.tar.gz: 8a007792624c95421bce9a09abb978de1142b8a2bfbfe6cef049c860ca101416
5
5
  SHA512:
6
- metadata.gz: 7fd937c2f74862b1d28a0f819622e8d7a29b141bca24b175707adea171ef1a7e95f86494757fe32d9d24088356ebc31967688a0b1c6b895171f0e5a746fd4e35
7
- data.tar.gz: df819d350b08f636b8afd2d56294982564f2662205b091b429fd58cc1d1c7db507a385a04966b2c153c336ae5eb46887ae1f9f226267ae227db21a69ef2ecb36
6
+ metadata.gz: b39b9310b28cd5bd9cfe3ea0511f6c615f3c130060693296e6aa01b18d3c893c9cd1704954133e9655c6a47fbe66f636eabc95c4a012f1e34ae4a0e634f6c0b3
7
+ data.tar.gz: 4881137e7cd922d5a9e56bcaec2fd5701b1e21f3e4ed53528585c14a2c24e0f2b90d93b2b36c7327160dae03cdc9c3e0f3a6171abb898d17843de862bf1f02a1
data/CHANGELOG.md CHANGED
@@ -6,6 +6,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## [0.4.9] - 2023-08-02
10
+
11
+ ### Added
12
+ - Extract cuboid property handling into mixin [#25]
13
+ - Add properties to `Physical::Location` [#26]
14
+ - Add pallets to `Physical::Shipment` [#29]
15
+
16
+ ### Changed
17
+ - Faster package weight and volume calculations [#23]
18
+
9
19
  ## [0.4.8] - 2023-03-21
10
20
 
11
21
  ### Added
@@ -4,6 +4,8 @@ require 'measured'
4
4
 
5
5
  module Physical
6
6
  class Cuboid
7
+ include PropertyReaders
8
+
7
9
  attr_reader :dimensions, :length, :width, :height, :weight, :id, :properties
8
10
 
9
11
  def initialize(id: nil, dimensions: [], weight: Measured::Weight(0, :g), properties: {})
@@ -33,27 +35,6 @@ module Physical
33
35
 
34
36
  private
35
37
 
36
- NORMALIZED_METHOD_REGEX = /(\w+)\??$/.freeze
37
-
38
- def method_missing(method)
39
- symbolized_properties = properties.symbolize_keys
40
- method_name = normalize_method_name(method)
41
- if symbolized_properties.key?(method_name)
42
- symbolized_properties[method_name]
43
- else
44
- super
45
- end
46
- end
47
-
48
- def respond_to_missing?(method, *args)
49
- method_name = normalize_method_name(method)
50
- properties.symbolize_keys.key?(method_name) || super
51
- end
52
-
53
- def normalize_method_name(method)
54
- method.to_s.sub(NORMALIZED_METHOD_REGEX, '\1').to_sym
55
- end
56
-
57
38
  def fill_dimensions(dimensions)
58
39
  dimensions.fill(dimensions.length..2) do |index|
59
40
  @dimensions[index] || Measured::Length(self.class::DEFAULT_LENGTH, :cm)
@@ -4,6 +4,8 @@ require 'carmen'
4
4
 
5
5
  module Physical
6
6
  class Location
7
+ include PropertyReaders
8
+
7
9
  ADDRESS_TYPES = %w(residential commercial po_box).freeze
8
10
 
9
11
  attr_reader :country,
@@ -20,7 +22,8 @@ module Physical
20
22
  :address_type,
21
23
  :company_name,
22
24
  :latitude,
23
- :longitude
25
+ :longitude,
26
+ :properties
24
27
 
25
28
  def initialize(
26
29
  name: nil,
@@ -37,7 +40,8 @@ module Physical
37
40
  email: nil,
38
41
  address_type: nil,
39
42
  latitude: nil,
40
- longitude: nil
43
+ longitude: nil,
44
+ properties: {}
41
45
  )
42
46
 
43
47
  @country = if country.is_a?(Carmen::Country)
@@ -65,6 +69,7 @@ module Physical
65
69
  @address_type = address_type
66
70
  @latitude = latitude
67
71
  @longitude = longitude
72
+ @properties = properties
68
73
  end
69
74
 
70
75
  def residential?
@@ -3,24 +3,31 @@
3
3
  module Physical
4
4
  class Package
5
5
  extend Forwardable
6
- attr_reader :container, :items, :void_fill_density, :id
6
+ attr_reader :id, :container, :items, :void_fill_density, :items_weight, :used_volume
7
7
 
8
8
  def initialize(id: nil, container: nil, items: [], void_fill_density: Measured::Density(0, :g_ml), dimensions: nil, weight: nil, properties: {})
9
9
  @id = id || SecureRandom.uuid
10
10
  @void_fill_density = Types::Density[void_fill_density]
11
11
  @container = container || Physical::Box.new(dimensions: dimensions || [], weight: weight || Measured::Weight(0, :g), properties: properties)
12
+
12
13
  @items = Set[*items]
14
+ @items_weight = @items.map(&:weight).reduce(Measured::Weight(0, :g), &:+)
15
+ @used_volume = @items.map(&:volume).reduce(Measured::Volume(0, :ml), &:+)
13
16
  end
14
17
 
15
18
  delegate [:dimensions, :width, :length, :height, :properties, :volume] => :container
16
19
 
17
20
  def <<(other)
18
21
  @items.add(other)
22
+ @items_weight += other.weight
23
+ @used_volume += other.volume
19
24
  end
20
25
  alias_method :add, :<<
21
26
 
22
27
  def >>(other)
23
28
  @items.delete(other)
29
+ @items_weight -= other.weight
30
+ @used_volume -= other.volume
24
31
  end
25
32
  alias_method :delete, :>>
26
33
 
@@ -29,29 +36,19 @@ module Physical
29
36
  end
30
37
 
31
38
  # Cost is optional. We will only return an aggregate if all items
32
- # have cost defined. Otherwise we will retun nil.
39
+ # have cost defined. Otherwise we will return nil.
33
40
  # @return Money
34
41
  def items_value
35
42
  items_cost = items.map(&:cost)
36
43
  items_cost.reduce(&:+) if items_cost.compact.size == items_cost.size
37
44
  end
38
45
 
39
- # @return [Measured::Weight]
40
- def items_weight
41
- items.map(&:weight).reduce(Measured::Weight(0, :g), &:+)
42
- end
43
-
44
46
  def void_fill_weight
45
47
  return Measured::Weight(0, :g) if container.volume.value.infinite?
46
48
 
47
49
  Measured::Weight(void_fill_density.convert_to(:g_ml).value * remaining_volume.convert_to(:ml).value, :g)
48
50
  end
49
51
 
50
- # @return [Measured::Volume]
51
- def used_volume
52
- items.map(&:volume).reduce(Measured::Volume(0, :ml), &:+)
53
- end
54
-
55
52
  def remaining_volume
56
53
  container.inner_volume - used_volume
57
54
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Physical
4
+ module PropertyReaders
5
+ private
6
+
7
+ NORMALIZED_METHOD_REGEX = /(\w+)\??$/.freeze
8
+
9
+ def method_missing(method)
10
+ symbolized_properties = properties.symbolize_keys
11
+ method_name = normalize_method_name(method)
12
+ if symbolized_properties.key?(method_name)
13
+ symbolized_properties[method_name]
14
+ else
15
+ super
16
+ end
17
+ end
18
+
19
+ def respond_to_missing?(method, *args)
20
+ method_name = normalize_method_name(method)
21
+ properties.symbolize_keys.key?(method_name) || super
22
+ end
23
+
24
+ def normalize_method_name(method)
25
+ method.to_s.sub(NORMALIZED_METHOD_REGEX, '\1').to_sym
26
+ end
27
+ end
28
+ end
@@ -6,14 +6,16 @@ module Physical
6
6
  :origin,
7
7
  :destination,
8
8
  :service_code,
9
+ :pallets,
9
10
  :packages,
10
11
  :options
11
12
 
12
- def initialize(id: nil, origin: nil, destination: nil, service_code: nil, packages: [], options: {})
13
+ def initialize(id: nil, origin: nil, destination: nil, service_code: nil, pallets: [], packages: [], options: {})
13
14
  @id = id || SecureRandom.uuid
14
15
  @origin = origin
15
16
  @destination = destination
16
17
  @service_code = service_code
18
+ @pallets = pallets
17
19
  @packages = packages
18
20
  @options = options
19
21
  end
@@ -14,6 +14,7 @@ FactoryBot.define do
14
14
  city { 'Herndon' }
15
15
  sequence(:zip, 10_001, &:to_s)
16
16
  phone { '555-555-0199' }
17
+ email { 'jane@company.com' }
17
18
  region { country.subregions.coded(region_code) }
18
19
  country { Carmen::Country.coded(country_code) }
19
20
  initialize_with { new(**attributes) }
@@ -4,6 +4,7 @@ FactoryBot.define do
4
4
  factory :physical_shipment, class: "Physical::Shipment" do
5
5
  origin { FactoryBot.build(:physical_location) }
6
6
  destination { FactoryBot.build(:physical_location) }
7
+ pallets { build_list(:physical_pallet, 1) }
7
8
  packages { build_list(:physical_package, 2) }
8
9
  service_code { "usps_priority_mail" }
9
10
  initialize_with { new(**attributes) }
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples "a cuboid" do
4
+ subject(:cuboid) { described_class.new(**args) }
5
+
6
+ it_behaves_like "has property readers"
7
+
8
+ let(:args) do
9
+ {
10
+ dimensions: [
11
+ Measured::Length.new(1.1, :cm),
12
+ Measured::Length.new(3.3, :cm),
13
+ Measured::Length.new(2.2, :cm)
14
+ ]
15
+ }
16
+ end
17
+
18
+ it { is_expected.to be_a(Physical::Cuboid) }
19
+ it { is_expected.to respond_to(:id) }
20
+
21
+ describe "#dimensions" do
22
+ subject(:dimensions) { cuboid.dimensions }
23
+
24
+ it "has dimensions as Measured::Length objects with rational values" do
25
+ expect(dimensions).to eq(
26
+ [
27
+ Measured::Length.new(1.1, :cm),
28
+ Measured::Length.new(3.3, :cm),
29
+ Measured::Length.new(2.2, :cm)
30
+ ]
31
+ )
32
+ end
33
+
34
+ context "when given a one-element dimensions array" do
35
+ let(:args) do
36
+ {
37
+ dimensions: [Measured::Length(2, :cm)]
38
+ }
39
+ end
40
+
41
+ specify "the other dimensions are filled up with default length" do
42
+ expect(dimensions).to eq(
43
+ [
44
+ Measured::Length.new(2, :cm),
45
+ Measured::Length.new(default_length, :cm),
46
+ Measured::Length.new(default_length, :cm)
47
+ ]
48
+ )
49
+ end
50
+ end
51
+
52
+ context "when given a two-element dimensions array" do
53
+ let(:args) do
54
+ {
55
+ dimensions: [1, 2].map { |d| Measured::Length(d, :cm) }
56
+ }
57
+ end
58
+
59
+ it "the last dimension is filled up with default length" do
60
+ expect(dimensions).to eq(
61
+ [
62
+ Measured::Length.new(1, :cm),
63
+ Measured::Length.new(2, :cm),
64
+ Measured::Length.new(default_length, :cm)
65
+ ]
66
+ )
67
+ end
68
+ end
69
+
70
+ context "when given no arguments" do
71
+ let(:args) { {} }
72
+
73
+ it "assumes cm as the dimension_unit and the default length as value" do
74
+ expect(dimensions).to eq(
75
+ [
76
+ Measured::Length.new(default_length, :cm),
77
+ Measured::Length.new(default_length, :cm),
78
+ Measured::Length.new(default_length, :cm)
79
+ ]
80
+ )
81
+ end
82
+ end
83
+ end
84
+
85
+ describe "dimension methods" do
86
+ it "has getter methods for each dimension as Measured::Length object" do
87
+ expect(cuboid.length).to eq(Measured::Length.new(1.1, :cm))
88
+ expect(cuboid.width).to eq(Measured::Length.new(3.3, :cm))
89
+ expect(cuboid.height).to eq(Measured::Length.new(2.2, :cm))
90
+ end
91
+ end
92
+
93
+ describe "#weight" do
94
+ subject(:weight) { cuboid.weight }
95
+
96
+ context "with no weight given" do
97
+ let(:args) { {} }
98
+ it { is_expected.to eq(Measured::Weight(0, :g)) }
99
+ end
100
+
101
+ context "with a weight" do
102
+ let(:args) { { weight: Measured::Weight(1, :lb) } }
103
+ it { is_expected.to eq(Measured::Weight(453.59237, :g)) }
104
+ end
105
+ end
106
+
107
+ describe "#volume" do
108
+ subject(:volume) { cuboid.volume }
109
+
110
+ context "if all three dimensions are given" do
111
+ let(:args) do
112
+ {
113
+ dimensions: [1.1, 2.1, 3.2].map { |d| Measured::Length(d, :cm) }
114
+ }
115
+ end
116
+
117
+ it { is_expected.to eq(Measured::Volume(7.392, :ml)) }
118
+ end
119
+
120
+ context "if a dimension is missing" do
121
+ let(:args) do
122
+ {
123
+ dimensions: [1.1, 2.1].map { |d| Measured::Length(d, :cm) }
124
+ }
125
+ end
126
+
127
+ it { is_expected.to eq(Measured::Volume(default_length, :ml)) }
128
+ end
129
+ end
130
+
131
+ describe "#density" do
132
+ subject(:density) { cuboid.density.value.to_f }
133
+
134
+ let(:args) do
135
+ {
136
+ dimensions: dimensions,
137
+ weight: weight
138
+ }
139
+ end
140
+
141
+ context "if volume is larger than 0" do
142
+ let(:dimensions) do
143
+ [1.1, 2.1, 3.2].map { |d| Measured::Length(d, :in) }
144
+ end
145
+
146
+ context "if weight is 1" do
147
+ let(:weight) { Measured::Weight(1, :pound) }
148
+
149
+ it "returns the density in gramms per cubiq centimeter (ml)" do
150
+ is_expected.to eq(3.7445758536530196)
151
+ end
152
+ end
153
+
154
+ context "if weight is 0" do
155
+ let(:weight) { Measured::Weight(0, :pound) }
156
+
157
+ it { is_expected.to eq(0.0) }
158
+ end
159
+ end
160
+
161
+ context "if volume is 0" do
162
+ let(:dimensions) do
163
+ [1.1, 2.1].map { |d| Measured::Length(d, :in) }
164
+ end
165
+
166
+ let(:weight) { Measured::Weight(1, :pound) }
167
+ let(:expected_volume) { default_length.zero? ? BigDecimal::INFINITY : 0 }
168
+
169
+ it { is_expected.to eq(expected_volume) }
170
+ end
171
+
172
+ context "if volume is infinite" do
173
+ let(:dimensions) do
174
+ [1.1, 2.1].map { |d| Measured::Length(d, :in) }
175
+ end
176
+
177
+ let(:weight) { Measured::Weight(1, :pound) }
178
+ let(:expected_volume) { default_length.zero? ? BigDecimal::INFINITY : 0 }
179
+
180
+ it { is_expected.to eq(expected_volume) }
181
+ end
182
+ end
183
+
184
+ describe "#==" do
185
+ let(:args) { Hash[id: 123] }
186
+ let(:other_cuboid) { described_class.new(**args) }
187
+ let(:non_cuboid) { double(id: 123) }
188
+
189
+ it "compares cuboids" do
190
+ aggregate_failures do
191
+ expect(cuboid == other_cuboid).to be(true)
192
+ expect(cuboid == non_cuboid).to be(false)
193
+ expect(cuboid.nil?).to be(false)
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples 'has property readers' do
4
+ subject(:instance) { described_class.new(**args) }
5
+
6
+ describe "#properties" do
7
+ subject(:properties) { instance.properties }
8
+
9
+ let(:args) do
10
+ {
11
+ properties: { flammable: true }
12
+ }
13
+ end
14
+
15
+ it { is_expected.to eq({ flammable: true }) }
16
+ end
17
+
18
+ describe "properties methods" do
19
+ context "if method is a property" do
20
+ let(:args) do
21
+ {
22
+ properties: { already_packaged: true }
23
+ }
24
+ end
25
+
26
+ it "returns its value" do
27
+ expect(instance.already_packaged).to be(true)
28
+ end
29
+
30
+ it { is_expected.to respond_to(:already_packaged?) }
31
+ end
32
+
33
+ context "if method is a string property" do
34
+ let(:args) do
35
+ {
36
+ properties: { "already_packaged" => true }
37
+ }
38
+ end
39
+
40
+ it "returns its value" do
41
+ expect(instance.already_packaged).to be(true)
42
+ end
43
+
44
+ it { is_expected.to respond_to(:already_packaged?) }
45
+ end
46
+
47
+ context "if method is a boolean property" do
48
+ let(:args) do
49
+ {
50
+ properties: { already_packaged: true }
51
+ }
52
+ end
53
+
54
+ it "it is also accessible by its predicate method" do
55
+ expect(instance.already_packaged?).to be(true)
56
+ end
57
+
58
+ it { is_expected.to respond_to(:already_packaged?) }
59
+
60
+ context "with a falsey value" do
61
+ let(:args) do
62
+ {
63
+ properties: { already_packaged: false }
64
+ }
65
+ end
66
+
67
+ it "returns its value" do
68
+ expect(instance.already_packaged).to be(false)
69
+ end
70
+ end
71
+ end
72
+
73
+ context "if method is not a property" do
74
+ let(:args) do
75
+ {
76
+ properties: {}
77
+ }
78
+ end
79
+
80
+ it "raises method missing" do
81
+ expect { instance.already_packaged? }.to raise_error(NoMethodError)
82
+ end
83
+
84
+ it { is_expected.not_to respond_to(:already_packaged?) }
85
+ end
86
+ end
87
+ end
@@ -1,45 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- RSpec.shared_examples 'a cuboid' do
4
- let(:args) do
5
- {
6
- dimensions: [
7
- Measured::Length.new(1.1, :cm),
8
- Measured::Length.new(3.3, :cm),
9
- Measured::Length.new(2.2, :cm)
10
- ]
11
- }
12
- end
13
-
14
- it { is_expected.to be_a(Physical::Cuboid) }
15
-
16
- it "has dimensions as Measured::Length objects with rational values" do
17
- expect(subject.dimensions).to eq(
18
- [
19
- Measured::Length.new(1.1, :cm),
20
- Measured::Length.new(3.3, :cm),
21
- Measured::Length.new(2.2, :cm)
22
- ]
23
- )
24
- end
25
-
26
- it "has getter methods for each dimension as Measured::Length object" do
27
- expect(subject.length).to eq(Measured::Length.new(1.1, :cm))
28
- expect(subject.width).to eq(Measured::Length.new(3.3, :cm))
29
- expect(subject.height).to eq(Measured::Length.new(2.2, :cm))
30
- end
31
-
32
- describe "#==" do
33
- let(:args) { Hash[id: 123] }
34
- let(:other_cuboid) { described_class.new(**args) }
35
- let(:non_cuboid) { double(id: 123) }
36
-
37
- it "compares cuboids" do
38
- aggregate_failures do
39
- expect(subject == other_cuboid).to be(true)
40
- expect(subject == non_cuboid).to be(false)
41
- expect(subject.nil?).to be(false)
42
- end
43
- end
44
- end
45
- end
3
+ require "physical/spec_support/shared_examples/a_cuboid"
4
+ require "physical/spec_support/shared_examples/has_property_readers"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Physical
4
- VERSION = "0.4.8"
4
+ VERSION = "0.4.9"
5
5
  end
data/lib/physical.rb CHANGED
@@ -4,6 +4,7 @@ require "money"
4
4
  require "measured/density"
5
5
  require "physical/types"
6
6
  require "physical/version"
7
+ require "physical/property_readers"
7
8
  require "physical/cuboid"
8
9
  require "physical/box"
9
10
  require "physical/package"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: physical
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.8
4
+ version: 0.4.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Martin Meyerhoff
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-03-21 00:00:00.000000000 Z
11
+ date: 2023-08-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: carmen
@@ -199,6 +199,7 @@ files:
199
199
  - lib/physical/location.rb
200
200
  - lib/physical/package.rb
201
201
  - lib/physical/pallet.rb
202
+ - lib/physical/property_readers.rb
202
203
  - lib/physical/shipment.rb
203
204
  - lib/physical/spec_support/factories/box_factory.rb
204
205
  - lib/physical/spec_support/factories/item_factory.rb
@@ -207,6 +208,8 @@ files:
207
208
  - lib/physical/spec_support/factories/pallet_factory.rb
208
209
  - lib/physical/spec_support/factories/shipment_factory.rb
209
210
  - lib/physical/spec_support/shared_examples.rb
211
+ - lib/physical/spec_support/shared_examples/a_cuboid.rb
212
+ - lib/physical/spec_support/shared_examples/has_property_readers.rb
210
213
  - lib/physical/test_support.rb
211
214
  - lib/physical/types.rb
212
215
  - lib/physical/version.rb
@@ -230,7 +233,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
230
233
  - !ruby/object:Gem::Version
231
234
  version: '0'
232
235
  requirements: []
233
- rubygems_version: 3.3.26
236
+ rubygems_version: 3.4.10
234
237
  signing_key:
235
238
  specification_version: 4
236
239
  summary: A facade to deal with physical packages