measured 2.4.0 → 2.7.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 +5 -5
- data/.github/workflows/ci.yml +31 -0
- data/CHANGELOG.md +32 -0
- data/README.md +12 -10
- data/Rakefile +18 -0
- data/cache/.keep +0 -0
- data/cache/length.json +2553 -0
- data/cache/volume.json +4163 -0
- data/cache/weight.json +3195 -0
- data/dev.yml +1 -1
- data/gemfiles/{activesupport-4.2.gemfile → activesupport-5.2.gemfile} +1 -1
- data/gemfiles/activesupport-6.0.gemfile +5 -0
- data/gemfiles/activesupport-6.1.gemfile +5 -0
- data/lib/measured.rb +1 -0
- data/lib/measured/arithmetic.rb +1 -0
- data/lib/measured/base.rb +11 -5
- data/lib/measured/cache/json.rb +49 -0
- data/lib/measured/cache/json_writer.rb +9 -0
- data/lib/measured/cache/null.rb +16 -0
- data/lib/measured/conversion_table_builder.rb +42 -12
- data/lib/measured/cycle_detected.rb +11 -0
- data/lib/measured/measurable.rb +18 -21
- data/lib/measured/missing_conversion_path.rb +12 -0
- data/lib/measured/parser.rb +2 -1
- data/lib/measured/unit.rb +16 -26
- data/lib/measured/unit_already_added.rb +11 -0
- data/lib/measured/unit_error.rb +4 -0
- data/lib/measured/unit_system.rb +26 -22
- data/lib/measured/unit_system_builder.rb +11 -4
- data/lib/measured/units/length.rb +3 -0
- data/lib/measured/units/volume.rb +4 -1
- data/lib/measured/units/weight.rb +3 -0
- data/lib/measured/version.rb +2 -1
- data/measured.gemspec +10 -2
- data/test/arithmetic_test.rb +1 -0
- data/test/cache/json_test.rb +43 -0
- data/test/cache/json_writer_test.rb +23 -0
- data/test/cache/null_test.rb +20 -0
- data/test/cache_consistency_test.rb +19 -0
- data/test/conversion_table_builder_test.rb +29 -0
- data/test/measurable_test.rb +5 -5
- data/test/parser_test.rb +1 -0
- data/test/support/always_true_cache.rb +14 -0
- data/test/support/fake_system.rb +1 -0
- data/test/support/subclasses.rb +7 -0
- data/test/test_helper.rb +5 -3
- data/test/unit_error_test.rb +1 -0
- data/test/unit_system_builder_test.rb +49 -3
- data/test/unit_system_test.rb +15 -0
- data/test/unit_test.rb +1 -0
- data/test/units/length_test.rb +5 -4
- data/test/units/volume_test.rb +6 -5
- data/test/units/weight_test.rb +1 -0
- metadata +39 -13
- data/.travis.yml +0 -20
data/lib/measured/unit_system.rb
CHANGED
@@ -1,17 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
class Measured::UnitSystem
|
2
|
-
attr_reader :units
|
3
|
-
|
4
|
-
def initialize(units)
|
5
|
-
@units = units.map { |unit| unit.
|
6
|
-
@
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
@
|
3
|
+
attr_reader :units, :unit_names, :unit_names_with_aliases
|
4
|
+
|
5
|
+
def initialize(units, cache: nil)
|
6
|
+
@units = units.map { |unit| unit.with(unit_system: self) }
|
7
|
+
@units = @units.map do |unit|
|
8
|
+
next unit unless unit.conversion_unit
|
9
|
+
conversion_unit = @units.find { |u| u.names.include?(unit.conversion_unit) }
|
10
|
+
next unit unless conversion_unit
|
11
|
+
unit.with(value: [unit.conversion_amount, conversion_unit.name])
|
12
|
+
end
|
13
|
+
@unit_names = @units.map(&:name).sort.freeze
|
14
|
+
@unit_names_with_aliases = @units.flat_map(&:names).sort.freeze
|
15
|
+
@unit_name_to_unit = @units.each_with_object({}) do |unit, hash|
|
16
|
+
unit.names.each { |name| hash[name.to_s] = unit }
|
17
|
+
end
|
18
|
+
@conversion_table_builder = Measured::ConversionTableBuilder.new(@units, cache: cache)
|
19
|
+
@conversion_table = @conversion_table_builder.to_h.freeze
|
15
20
|
end
|
16
21
|
|
17
22
|
def unit_or_alias?(name)
|
@@ -41,16 +46,15 @@ class Measured::UnitSystem
|
|
41
46
|
value.to_r * conversion
|
42
47
|
end
|
43
48
|
|
44
|
-
|
45
|
-
|
46
|
-
def conversion_table
|
47
|
-
@conversion_table ||= @conversion_table_builder.to_h
|
49
|
+
def update_cache
|
50
|
+
@conversion_table_builder.update_cache
|
48
51
|
end
|
49
52
|
|
50
|
-
def
|
51
|
-
@
|
52
|
-
unit.names.each { |name| hash[name.to_s] = unit }
|
53
|
-
hash
|
54
|
-
end
|
53
|
+
def cached?
|
54
|
+
@conversion_table_builder.cached?
|
55
55
|
end
|
56
|
+
|
57
|
+
protected
|
58
|
+
|
59
|
+
attr_reader :unit_name_to_unit, :conversion_table
|
56
60
|
end
|
@@ -1,6 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
class Measured::UnitSystemBuilder
|
2
3
|
def initialize
|
3
4
|
@units = []
|
5
|
+
@cache = nil
|
4
6
|
end
|
5
7
|
|
6
8
|
def unit(unit_name, aliases: [], value: nil)
|
@@ -13,8 +15,13 @@ class Measured::UnitSystemBuilder
|
|
13
15
|
nil
|
14
16
|
end
|
15
17
|
|
18
|
+
def cache(cache_class, *args)
|
19
|
+
@cache = {class: cache_class, args: args}
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
|
16
23
|
def build
|
17
|
-
Measured::UnitSystem.new(@units)
|
24
|
+
Measured::UnitSystem.new(@units, cache: @cache)
|
18
25
|
end
|
19
26
|
|
20
27
|
private
|
@@ -39,8 +46,8 @@ class Measured::UnitSystemBuilder
|
|
39
46
|
["P", "peta", 15],
|
40
47
|
["E", "exa", 18],
|
41
48
|
["Z", "zetta", 21],
|
42
|
-
["Y", "yotta", 24]
|
43
|
-
]
|
49
|
+
["Y", "yotta", 24],
|
50
|
+
].map(&:freeze).freeze
|
44
51
|
|
45
52
|
def build_si_units(name, aliases: [], value: nil)
|
46
53
|
si_units = [build_unit(name, aliases: aliases, value: value)]
|
@@ -60,7 +67,7 @@ class Measured::UnitSystemBuilder
|
|
60
67
|
def check_for_duplicate_unit_names!(unit)
|
61
68
|
names = @units.flat_map(&:names)
|
62
69
|
if names.any? { |name| unit.names.include?(name) }
|
63
|
-
raise Measured::
|
70
|
+
raise Measured::UnitAlreadyAdded.new(unit.name)
|
64
71
|
end
|
65
72
|
end
|
66
73
|
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
Measured::Length = Measured.build do
|
2
3
|
si_unit :m, aliases: [:meter, :metre, :meters, :metres]
|
3
4
|
|
@@ -5,4 +6,6 @@ Measured::Length = Measured.build do
|
|
5
6
|
unit :ft, value: "12 in", aliases: [:foot, :feet]
|
6
7
|
unit :yd, value: "3 ft", aliases: [:yard, :yards]
|
7
8
|
unit :mi, value: "5280 ft", aliases: [:mile, :miles]
|
9
|
+
|
10
|
+
cache Measured::Cache::Json, "length.json"
|
8
11
|
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
Measured::Volume = Measured.build do
|
2
3
|
si_unit :l, aliases: [:liter, :litre, :liters, :litres]
|
3
4
|
|
@@ -12,4 +13,6 @@ Measured::Volume = Measured.build do
|
|
12
13
|
unit :us_pt, value: "0.125 us_gal", aliases: [:us_pint, :us_pints]
|
13
14
|
unit :oz, value: "0.00625 gal", aliases: [:fl_oz, :imp_fl_oz, :imperial_fluid_ounce, :imperial_fluid_ounces]
|
14
15
|
unit :us_oz, value: "0.0078125 us_gal", aliases: [:us_fl_oz, :us_fluid_ounce, :us_fluid_ounces]
|
15
|
-
|
16
|
+
|
17
|
+
cache Measured::Cache::Json, "volume.json"
|
18
|
+
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
Measured::Weight = Measured.build do
|
2
3
|
si_unit :g, aliases: [:gram, :grams]
|
3
4
|
|
@@ -8,4 +9,6 @@ Measured::Weight = Measured.build do
|
|
8
9
|
unit :short_ton, value: "2000 lb", aliases: [:short_tons]
|
9
10
|
unit :lb, value: "0.45359237 kg", aliases: [:lbs, :pound, :pounds]
|
10
11
|
unit :oz, value: "1/16 lb", aliases: [:ounce, :ounces]
|
12
|
+
|
13
|
+
cache Measured::Cache::Json, "weight.json"
|
11
14
|
end
|
data/lib/measured/version.rb
CHANGED
data/measured.gemspec
CHANGED
@@ -13,16 +13,24 @@ Gem::Specification.new do |spec|
|
|
13
13
|
spec.homepage = "https://github.com/Shopify/measured"
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
16
|
+
# Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
|
17
|
+
# delete this section to allow pushing this gem to any host.
|
18
|
+
if spec.respond_to?(:metadata)
|
19
|
+
spec.metadata['allowed_push_host'] = "https://rubygems.org"
|
20
|
+
else
|
21
|
+
raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
|
22
|
+
end
|
23
|
+
|
16
24
|
spec.files = `git ls-files -z`.split("\x0")
|
17
25
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
26
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
27
|
spec.require_paths = ["lib"]
|
20
28
|
|
21
|
-
spec.add_runtime_dependency "activesupport", ">=
|
29
|
+
spec.add_runtime_dependency "activesupport", ">= 5.2"
|
22
30
|
|
23
31
|
spec.add_development_dependency "rake", "> 10.0"
|
24
32
|
spec.add_development_dependency "minitest", "> 5.5.1"
|
25
33
|
spec.add_development_dependency "minitest-reporters"
|
26
|
-
spec.add_development_dependency "mocha", "
|
34
|
+
spec.add_development_dependency "mocha", ">= 1.4.0"
|
27
35
|
spec.add_development_dependency "pry"
|
28
36
|
end
|
data/test/arithmetic_test.rb
CHANGED
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "test_helper"
|
3
|
+
|
4
|
+
class Measured::Cache::JsonTest < ActiveSupport::TestCase
|
5
|
+
setup do
|
6
|
+
@cache = Measured::Cache::Json.new("test.json")
|
7
|
+
@table_json = { "a" => { "b" => { "numerator" => 2, "denominator" => 3 } } }.to_json
|
8
|
+
@table_hash = { "a" => { "b" => Rational(2, 3) } }
|
9
|
+
end
|
10
|
+
|
11
|
+
test "#initialize sets the filename and path" do
|
12
|
+
assert_equal "test.json", @cache.filename
|
13
|
+
assert_match(/.+\/cache\/test\.json$/, @cache.path.to_s)
|
14
|
+
refute_match "../", @cache.path.to_s
|
15
|
+
end
|
16
|
+
|
17
|
+
test "#exist? returns false if the file does not exist" do
|
18
|
+
File.expects(:exist?).with(@cache.path).returns(false)
|
19
|
+
refute_predicate @cache, :exist?
|
20
|
+
end
|
21
|
+
|
22
|
+
test "#exist? returns true if the file exists" do
|
23
|
+
File.expects(:exist?).with(@cache.path).returns(true)
|
24
|
+
assert_predicate @cache, :exist?
|
25
|
+
end
|
26
|
+
|
27
|
+
test "#read returns nil if the file does not exist" do
|
28
|
+
File.expects(:exist?).with(@cache.path).returns(false)
|
29
|
+
assert_nil @cache.read
|
30
|
+
end
|
31
|
+
|
32
|
+
test "#read loads the file if it exists" do
|
33
|
+
File.expects(:exist?).with(@cache.path).returns(true)
|
34
|
+
File.expects(:read).with(@cache.path).returns(@table_json)
|
35
|
+
assert_equal @table_hash, @cache.read
|
36
|
+
end
|
37
|
+
|
38
|
+
test "#write raises not implemented" do
|
39
|
+
assert_raises(ArgumentError) do
|
40
|
+
@cache.write({})
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "test_helper"
|
3
|
+
|
4
|
+
class Measured::Cache::JsonWriterTest < ActiveSupport::TestCase
|
5
|
+
class JsonTestWithWriter < Measured::Cache::Json
|
6
|
+
prepend Measured::Cache::JsonWriter
|
7
|
+
end
|
8
|
+
|
9
|
+
setup do
|
10
|
+
@cache = JsonTestWithWriter.new("test.json")
|
11
|
+
@table_json = JSON.pretty_generate({ "a" => { "b" => { "numerator" => 2, "denominator" => 3 } } })
|
12
|
+
@table_hash = { "a" => { "b" => Rational(2, 3) } }
|
13
|
+
end
|
14
|
+
|
15
|
+
test "#write writes the file" do
|
16
|
+
f = stub
|
17
|
+
f.expects(:write).with("// Do not modify this file directly. Regenerate it with 'rake cache:write'.\n")
|
18
|
+
f.expects(:write).with(@table_json)
|
19
|
+
|
20
|
+
File.expects(:open).with(@cache.path, "w").returns(123).yields(f)
|
21
|
+
assert_equal 123, @cache.write(@table_hash)
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "test_helper"
|
3
|
+
|
4
|
+
class Measured::Cache::NullTest < ActiveSupport::TestCase
|
5
|
+
setup do
|
6
|
+
@cache = Measured::Cache::Null.new
|
7
|
+
end
|
8
|
+
|
9
|
+
test "#exist? false" do
|
10
|
+
assert_equal false, @cache.exist?
|
11
|
+
end
|
12
|
+
|
13
|
+
test "#read returns nil" do
|
14
|
+
assert_nil @cache.read
|
15
|
+
end
|
16
|
+
|
17
|
+
test "#write returns nil" do
|
18
|
+
assert_nil @cache.write({})
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "test_helper"
|
3
|
+
|
4
|
+
# In general we do not want to expose the inner workings of the caching and unit systems as part of the API.
|
5
|
+
# But we do want to be able to test that there is no conflict between what's in the cache file and what is defined
|
6
|
+
# in the unit systems. So we compromise by reaching into implementation to force compare them.
|
7
|
+
class Measured::CacheConsistencyTest < ActiveSupport::TestCase
|
8
|
+
measurable_subclasses.select { |m| m.unit_system.cached? }.each do |measurable|
|
9
|
+
test "cached measurable #{ measurable } is not out of sync with the definition" do
|
10
|
+
builder = measurable.unit_system.instance_variable_get("@conversion_table_builder")
|
11
|
+
cache = builder.instance_variable_get("@cache")
|
12
|
+
|
13
|
+
expected = builder.send(:generate_table)
|
14
|
+
actual = cache.read
|
15
|
+
|
16
|
+
assert expected == actual, "The contents of the file cache for `#{ measurable }` does not match what the unit system generated.\nTry running `rake cache:write` to update the caches."
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require "test_helper"
|
2
3
|
|
3
4
|
class Measured::ConversionTableBuilderTest < ActiveSupport::TestCase
|
@@ -115,4 +116,32 @@ class Measured::ConversionTableBuilderTest < ActiveSupport::TestCase
|
|
115
116
|
assert_instance_of Rational, value
|
116
117
|
end
|
117
118
|
end
|
119
|
+
|
120
|
+
test "#to_h raises exception when there are cycles" do
|
121
|
+
unit1 = Measured::Unit.new(:pallets, value: "1 liters")
|
122
|
+
unit2 = Measured::Unit.new(:liters, value: "0.1 cases")
|
123
|
+
unit3 = Measured::Unit.new(:cases, value: "0.1 pallets")
|
124
|
+
|
125
|
+
assert_raises(Measured::CycleDetected) do
|
126
|
+
Measured::ConversionTableBuilder.new([unit1, unit2, unit3]).to_h
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
test "#cached? returns true if there's a cache" do
|
131
|
+
builder = Measured::ConversionTableBuilder.new([Measured::Unit.new(:test)], cache: { class: AlwaysTrueCache })
|
132
|
+
assert_predicate builder, :cached?
|
133
|
+
end
|
134
|
+
|
135
|
+
test "#cached? returns false if there is not a cache" do
|
136
|
+
builder = Measured::ConversionTableBuilder.new([Measured::Unit.new(:test)])
|
137
|
+
refute_predicate builder, :cached?
|
138
|
+
end
|
139
|
+
|
140
|
+
test "#write_cache pushes the generated table into the cache and writes it" do
|
141
|
+
builder = Measured::ConversionTableBuilder.new([Measured::Unit.new(:test)], cache: { class: AlwaysTrueCache })
|
142
|
+
AlwaysTrueCache.any_instance.expects(:exist?).returns(false)
|
143
|
+
table = builder.to_h
|
144
|
+
AlwaysTrueCache.any_instance.expects(:write).with(table).returns(123)
|
145
|
+
assert_equal 123, builder.update_cache
|
146
|
+
end
|
118
147
|
end
|
data/test/measurable_test.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require "test_helper"
|
2
3
|
|
3
4
|
class Measured::MeasurableTest < ActiveSupport::TestCase
|
4
|
-
|
5
5
|
setup do
|
6
6
|
@arcane = Magic.unit_system.unit_for!(:arcane)
|
7
7
|
@fireball = Magic.unit_system.unit_for!(:fireball)
|
@@ -261,7 +261,7 @@ class Measured::MeasurableTest < ActiveSupport::TestCase
|
|
261
261
|
|
262
262
|
test "#<=> doesn't compare against zero" do
|
263
263
|
assert_nil @magic <=> 0
|
264
|
-
assert_nil @magic <=> BigDecimal
|
264
|
+
assert_nil @magic <=> BigDecimal(0)
|
265
265
|
assert_nil @magic <=> 0.00
|
266
266
|
end
|
267
267
|
|
@@ -279,7 +279,7 @@ class Measured::MeasurableTest < ActiveSupport::TestCase
|
|
279
279
|
test "#== doesn't compare against zero" do
|
280
280
|
arcane_zero = Magic.new(0, :arcane)
|
281
281
|
refute_equal arcane_zero, 0
|
282
|
-
refute_equal arcane_zero, BigDecimal
|
282
|
+
refute_equal arcane_zero, BigDecimal(0)
|
283
283
|
refute_equal arcane_zero, 0.0
|
284
284
|
end
|
285
285
|
|
@@ -295,10 +295,10 @@ class Measured::MeasurableTest < ActiveSupport::TestCase
|
|
295
295
|
|
296
296
|
test "#> and #< should not compare against zero" do
|
297
297
|
assert_raises(ArgumentError) { @magic > 0 }
|
298
|
-
assert_raises(ArgumentError) { @magic > BigDecimal
|
298
|
+
assert_raises(ArgumentError) { @magic > BigDecimal(0) }
|
299
299
|
assert_raises(ArgumentError) { @magic > 0.00 }
|
300
300
|
assert_raises(ArgumentError) { @magic < 0 }
|
301
|
-
assert_raises(ArgumentError) { @magic < BigDecimal
|
301
|
+
assert_raises(ArgumentError) { @magic < BigDecimal(0) }
|
302
302
|
assert_raises(ArgumentError) { @magic < 0.00 }
|
303
303
|
end
|
304
304
|
end
|
data/test/parser_test.rb
CHANGED
data/test/support/fake_system.rb
CHANGED
@@ -0,0 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# Extract the subclasses that exist early on because other classes will get added by tests
|
3
|
+
# later on in execution and in an unpredictable order.
|
4
|
+
class ActiveSupport::TestCase
|
5
|
+
cattr_accessor :measurable_subclasses
|
6
|
+
self.measurable_subclasses = Measured::Measurable.subclasses
|
7
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -1,17 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "pry" unless ENV["CI"]
|
1
3
|
require "measured"
|
2
4
|
require "minitest/reporters"
|
3
5
|
require "minitest/autorun"
|
4
|
-
require "mocha/
|
5
|
-
require "pry" unless ENV["CI"]
|
6
|
+
require "mocha/minitest"
|
6
7
|
|
7
8
|
ActiveSupport.test_order = :random
|
8
9
|
|
9
10
|
Minitest::Reporters.use! [Minitest::Reporters::ProgressReporter.new(color: true)]
|
10
11
|
|
12
|
+
require "support/subclasses"
|
11
13
|
require "support/fake_system"
|
14
|
+
require "support/always_true_cache"
|
12
15
|
|
13
16
|
class ActiveSupport::TestCase
|
14
|
-
|
15
17
|
protected
|
16
18
|
|
17
19
|
def assert_close_bigdecimal exp, act, delta = BigDecimal('0.000001')
|
data/test/unit_error_test.rb
CHANGED