physical 0.4.8 → 0.5.0
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 +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
|