physical 0.4.8 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/lib/physical/cuboid.rb +2 -21
- data/lib/physical/location.rb +7 -2
- data/lib/physical/package.rb +11 -13
- data/lib/physical/property_readers.rb +28 -0
- data/lib/physical/shipment.rb +9 -1
- data/lib/physical/spec_support/factories/location_factory.rb +4 -3
- data/lib/physical/spec_support/factories/shipment_factory.rb +6 -0
- data/lib/physical/spec_support/factories/structure_factory.rb +9 -0
- data/lib/physical/spec_support/shared_examples/a_cuboid.rb +197 -0
- data/lib/physical/spec_support/shared_examples/has_property_readers.rb +87 -0
- data/lib/physical/spec_support/shared_examples.rb +2 -43
- data/lib/physical/structure.rb +56 -0
- data/lib/physical/version.rb +1 -1
- data/lib/physical.rb +2 -0
- metadata +8 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 03d9eb10ac73675e3aa2bac745e7a6d8a30f4b9c0c6cb810d4dac0235d0cc55f
|
4
|
+
data.tar.gz: ed902ddd5a9349009a43d0a883a5526f7097d759e742c6ff43b8baf904765f63
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 52d08252ebda560e9abf682eb011c5e2281afbf0c3eead4acc650b9af8714555f465ffc9c613318eb2bed9e86caf72edddde1ad8edb5f26aea2233b7ebc38b96
|
7
|
+
data.tar.gz: cfdb971e4bdf9d1ce7c830b8f6da93614e630251ca7307f91baddf05353f43e1c1ef51aa84dc3db505ffb8221bf7fde68ee1e6348462e4fd9cacc9c820c2efb8
|
data/CHANGELOG.md
CHANGED
@@ -6,6 +6,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
6
6
|
|
7
7
|
## Unreleased
|
8
8
|
|
9
|
+
## [0.5.0] - 2023-12-19
|
10
|
+
|
11
|
+
### Added
|
12
|
+
- Introduce `Physical::Structure` class [#31]
|
13
|
+
|
14
|
+
### Changed
|
15
|
+
- Use legitimate state/zip in location factory [#30]
|
16
|
+
|
17
|
+
## [0.4.9] - 2023-08-02
|
18
|
+
|
19
|
+
### Added
|
20
|
+
- Extract cuboid property handling into mixin [#25]
|
21
|
+
- Add properties to `Physical::Location` [#26]
|
22
|
+
- Add pallets to `Physical::Shipment` [#29]
|
23
|
+
|
24
|
+
### Changed
|
25
|
+
- Faster package weight and volume calculations [#23]
|
26
|
+
|
9
27
|
## [0.4.8] - 2023-03-21
|
10
28
|
|
11
29
|
### Added
|
data/lib/physical/cuboid.rb
CHANGED
@@ -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)
|
data/lib/physical/location.rb
CHANGED
@@ -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?
|
data/lib/physical/package.rb
CHANGED
@@ -3,24 +3,32 @@
|
|
3
3
|
module Physical
|
4
4
|
class Package
|
5
5
|
extend Forwardable
|
6
|
-
attr_reader :container, :items, :void_fill_density, :
|
6
|
+
attr_reader :id, :container, :items, :void_fill_density, :items_weight, :used_volume, :description
|
7
7
|
|
8
|
-
def initialize(id: nil, container: nil, items: [], void_fill_density: Measured::Density(0, :g_ml), dimensions: nil, weight: nil, properties: {})
|
8
|
+
def initialize(id: nil, container: nil, items: [], void_fill_density: Measured::Density(0, :g_ml), dimensions: nil, weight: nil, description: 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
|
+
@description = description
|
13
|
+
|
12
14
|
@items = Set[*items]
|
15
|
+
@items_weight = @items.map(&:weight).reduce(Measured::Weight(0, :g), &:+)
|
16
|
+
@used_volume = @items.map(&:volume).reduce(Measured::Volume(0, :ml), &:+)
|
13
17
|
end
|
14
18
|
|
15
19
|
delegate [:dimensions, :width, :length, :height, :properties, :volume] => :container
|
16
20
|
|
17
21
|
def <<(other)
|
18
22
|
@items.add(other)
|
23
|
+
@items_weight += other.weight
|
24
|
+
@used_volume += other.volume
|
19
25
|
end
|
20
26
|
alias_method :add, :<<
|
21
27
|
|
22
28
|
def >>(other)
|
23
29
|
@items.delete(other)
|
30
|
+
@items_weight -= other.weight
|
31
|
+
@used_volume -= other.volume
|
24
32
|
end
|
25
33
|
alias_method :delete, :>>
|
26
34
|
|
@@ -29,29 +37,19 @@ module Physical
|
|
29
37
|
end
|
30
38
|
|
31
39
|
# Cost is optional. We will only return an aggregate if all items
|
32
|
-
# have cost defined. Otherwise we will
|
40
|
+
# have cost defined. Otherwise we will return nil.
|
33
41
|
# @return Money
|
34
42
|
def items_value
|
35
43
|
items_cost = items.map(&:cost)
|
36
44
|
items_cost.reduce(&:+) if items_cost.compact.size == items_cost.size
|
37
45
|
end
|
38
46
|
|
39
|
-
# @return [Measured::Weight]
|
40
|
-
def items_weight
|
41
|
-
items.map(&:weight).reduce(Measured::Weight(0, :g), &:+)
|
42
|
-
end
|
43
|
-
|
44
47
|
def void_fill_weight
|
45
48
|
return Measured::Weight(0, :g) if container.volume.value.infinite?
|
46
49
|
|
47
50
|
Measured::Weight(void_fill_density.convert_to(:g_ml).value * remaining_volume.convert_to(:ml).value, :g)
|
48
51
|
end
|
49
52
|
|
50
|
-
# @return [Measured::Volume]
|
51
|
-
def used_volume
|
52
|
-
items.map(&:volume).reduce(Measured::Volume(0, :ml), &:+)
|
53
|
-
end
|
54
|
-
|
55
53
|
def remaining_volume
|
56
54
|
container.inner_volume - used_volume
|
57
55
|
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
|
data/lib/physical/shipment.rb
CHANGED
@@ -6,16 +6,24 @@ module Physical
|
|
6
6
|
:origin,
|
7
7
|
:destination,
|
8
8
|
:service_code,
|
9
|
+
:pallets,
|
10
|
+
:structures,
|
9
11
|
:packages,
|
10
12
|
:options
|
11
13
|
|
12
|
-
def initialize(id: nil, origin: nil, destination: nil, service_code: nil, packages: [], options: {})
|
14
|
+
def initialize(id: nil, origin: nil, destination: nil, service_code: nil, pallets: [], structures: [], packages: [], options: {})
|
13
15
|
@id = id || SecureRandom.uuid
|
14
16
|
@origin = origin
|
15
17
|
@destination = destination
|
16
18
|
@service_code = service_code
|
19
|
+
@structures = structures
|
17
20
|
@packages = packages
|
18
21
|
@options = options
|
22
|
+
|
23
|
+
return unless pallets.any?
|
24
|
+
|
25
|
+
warn "[DEPRECATION] `pallets` is deprecated. Please use `structures` instead."
|
26
|
+
@pallets = pallets
|
19
27
|
end
|
20
28
|
end
|
21
29
|
end
|
@@ -4,16 +4,17 @@ FactoryBot.define do
|
|
4
4
|
factory :physical_location, class: 'Physical::Location' do
|
5
5
|
transient do
|
6
6
|
country_code { 'US' }
|
7
|
-
region_code { '
|
7
|
+
region_code { 'VA' }
|
8
8
|
end
|
9
9
|
|
10
10
|
name { 'Jane Doe' }
|
11
11
|
company_name { 'Company' }
|
12
12
|
address1 { '11 Lovely Street' }
|
13
|
-
address2 { '
|
13
|
+
address2 { 'Suite 100' }
|
14
14
|
city { 'Herndon' }
|
15
|
-
|
15
|
+
zip { '20170' }
|
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,8 +4,14 @@ 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) } # deprecated, will be removed
|
7
8
|
packages { build_list(:physical_package, 2) }
|
8
9
|
service_code { "usps_priority_mail" }
|
9
10
|
initialize_with { new(**attributes) }
|
11
|
+
|
12
|
+
trait :freight do
|
13
|
+
structures { build_list(:physical_structure, 1) }
|
14
|
+
service_code { "tforce_freight" }
|
15
|
+
end
|
10
16
|
end
|
11
17
|
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
FactoryBot.define do
|
4
|
+
factory :physical_structure, class: "Physical::Structure" do
|
5
|
+
container { FactoryBot.build(:physical_pallet) }
|
6
|
+
packages { build_list(:physical_package, 2) }
|
7
|
+
initialize_with { new(**attributes) }
|
8
|
+
end
|
9
|
+
end
|
@@ -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
|
-
|
4
|
-
|
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"
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Physical
|
4
|
+
class Structure
|
5
|
+
extend Forwardable
|
6
|
+
attr_reader :id, :container, :packages, :packages_weight, :used_volume
|
7
|
+
|
8
|
+
def initialize(id: nil, container: nil, packages: [], dimensions: nil, weight: nil, properties: {})
|
9
|
+
@id = id || SecureRandom.uuid
|
10
|
+
@container = container || Physical::Pallet.new(dimensions: dimensions || [], weight: weight || Measured::Weight(0, :g), properties: properties)
|
11
|
+
|
12
|
+
@packages = Set[*packages]
|
13
|
+
@packages_weight = @packages.map(&:weight).reduce(Measured::Weight(0, :g), &:+)
|
14
|
+
@used_volume = @packages.map(&:volume).reduce(Measured::Volume(0, :ml), &:+)
|
15
|
+
end
|
16
|
+
|
17
|
+
delegate [:dimensions, :width, :length, :height, :properties, :volume] => :container
|
18
|
+
|
19
|
+
def <<(other)
|
20
|
+
@packages.add(other)
|
21
|
+
@packages_weight += other.weight
|
22
|
+
@used_volume += other.volume
|
23
|
+
end
|
24
|
+
alias_method :add, :<<
|
25
|
+
|
26
|
+
def >>(other)
|
27
|
+
@packages.delete(other)
|
28
|
+
@packages_weight -= other.weight
|
29
|
+
@used_volume -= other.volume
|
30
|
+
end
|
31
|
+
alias_method :delete, :>>
|
32
|
+
|
33
|
+
def weight
|
34
|
+
container.weight + packages_weight
|
35
|
+
end
|
36
|
+
|
37
|
+
# Cost is optional. We will only return an aggregate if all packages
|
38
|
+
# have items value defined. Otherwise we will return nil.
|
39
|
+
# @return Money
|
40
|
+
def packages_value
|
41
|
+
packages_cost = packages.map(&:items_value)
|
42
|
+
packages_cost.reduce(&:+) if packages_cost.compact.size == packages_cost.size
|
43
|
+
end
|
44
|
+
|
45
|
+
def remaining_volume
|
46
|
+
container.inner_volume - used_volume
|
47
|
+
end
|
48
|
+
|
49
|
+
def density
|
50
|
+
return Measured::Density(Float::INFINITY, :g_ml) if container.volume.value.zero?
|
51
|
+
return Measured::Density(0.0, :g_ml) if container.volume.value.infinite?
|
52
|
+
|
53
|
+
Measured::Density(weight.convert_to(:g).value / container.volume.convert_to(:ml).value, :g_ml)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/physical/version.rb
CHANGED
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"
|
@@ -11,6 +12,7 @@ require "physical/pallet"
|
|
11
12
|
require "physical/item"
|
12
13
|
require "physical/location"
|
13
14
|
require "physical/shipment"
|
15
|
+
require "physical/structure"
|
14
16
|
|
15
17
|
module Physical
|
16
18
|
# Your code goes here...
|
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
|
+
version: 0.5.0
|
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-
|
11
|
+
date: 2023-12-19 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
|
@@ -206,7 +207,11 @@ files:
|
|
206
207
|
- lib/physical/spec_support/factories/package_factory.rb
|
207
208
|
- lib/physical/spec_support/factories/pallet_factory.rb
|
208
209
|
- lib/physical/spec_support/factories/shipment_factory.rb
|
210
|
+
- lib/physical/spec_support/factories/structure_factory.rb
|
209
211
|
- lib/physical/spec_support/shared_examples.rb
|
212
|
+
- lib/physical/spec_support/shared_examples/a_cuboid.rb
|
213
|
+
- lib/physical/spec_support/shared_examples/has_property_readers.rb
|
214
|
+
- lib/physical/structure.rb
|
210
215
|
- lib/physical/test_support.rb
|
211
216
|
- lib/physical/types.rb
|
212
217
|
- lib/physical/version.rb
|
@@ -230,7 +235,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
230
235
|
- !ruby/object:Gem::Version
|
231
236
|
version: '0'
|
232
237
|
requirements: []
|
233
|
-
rubygems_version: 3.
|
238
|
+
rubygems_version: 3.4.22
|
234
239
|
signing_key:
|
235
240
|
specification_version: 4
|
236
241
|
summary: A facade to deal with physical packages
|