measured 2.4.0 → 2.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.
Potentially problematic release.
This version of measured might be problematic. Click here for more details.
- checksums.yaml +5 -5
- data/.travis.yml +11 -10
- data/README.md +9 -8
- 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.0.gemfile} +1 -1
- data/gemfiles/activesupport-5.1.gemfile +5 -0
- data/gemfiles/activesupport-6.0.gemfile +5 -0
- data/lib/measured/base.rb +4 -0
- data/lib/measured/cache/json.rb +48 -0
- data/lib/measured/cache/json_writer.rb +8 -0
- data/lib/measured/cache/null.rb +15 -0
- data/lib/measured/conversion_table_builder.rb +23 -12
- data/lib/measured/unit_system.rb +10 -2
- data/lib/measured/unit_system_builder.rb +7 -1
- data/lib/measured/units/length.rb +2 -0
- data/lib/measured/units/volume.rb +3 -1
- data/lib/measured/units/weight.rb +2 -0
- data/lib/measured/version.rb +1 -1
- data/measured.gemspec +1 -1
- data/test/cache/json_test.rb +42 -0
- data/test/cache/json_writer_test.rb +22 -0
- data/test/cache/null_test.rb +19 -0
- data/test/cache_consistency_test.rb +18 -0
- data/test/conversion_table_builder_test.rb +18 -0
- data/test/measurable_test.rb +4 -4
- data/test/support/always_true_cache.rb +13 -0
- data/test/support/subclasses.rb +6 -0
- data/test/test_helper.rb +3 -2
- data/test/unit_system_builder_test.rb +21 -1
- data/test/unit_system_test.rb +14 -0
- metadata +27 -6
data/dev.yml
CHANGED
data/lib/measured/base.rb
CHANGED
@@ -2,6 +2,7 @@ require "forwardable"
|
|
2
2
|
require "measured/version"
|
3
3
|
require "active_support/all"
|
4
4
|
require "bigdecimal"
|
5
|
+
require "json"
|
5
6
|
|
6
7
|
module Measured
|
7
8
|
class UnitError < StandardError ; end
|
@@ -44,4 +45,7 @@ require "measured/unit"
|
|
44
45
|
require "measured/unit_system"
|
45
46
|
require "measured/unit_system_builder"
|
46
47
|
require "measured/conversion_table_builder"
|
48
|
+
require "measured/cache/null"
|
49
|
+
require "measured/cache/json_writer"
|
50
|
+
require "measured/cache/json"
|
47
51
|
require "measured/measurable"
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Measured::Cache
|
2
|
+
class Json
|
3
|
+
attr_reader :filename, :path
|
4
|
+
|
5
|
+
def initialize(filename)
|
6
|
+
@filename = filename
|
7
|
+
@path = Pathname.new(File.join(File.dirname(__FILE__), "../../../cache", @filename)).cleanpath
|
8
|
+
end
|
9
|
+
|
10
|
+
def exist?
|
11
|
+
File.exist?(@path)
|
12
|
+
end
|
13
|
+
|
14
|
+
def read
|
15
|
+
return unless exist?
|
16
|
+
decode(JSON.load(File.read(@path)))
|
17
|
+
end
|
18
|
+
|
19
|
+
def write(table)
|
20
|
+
raise ArgumentError, "Cannot overwrite file cache at runtime."
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
# JSON dump and load of Rational objects exists, but it changes the behaviour of JSON globally if required.
|
26
|
+
# Instead, the same marshalling technique is rewritten here to prevent changing this behaviour project wide.
|
27
|
+
# https://github.com/ruby/ruby/blob/trunk/ext/json/lib/json/add/rational.rb
|
28
|
+
def encode(table)
|
29
|
+
table.each_with_object(table.dup) do |(k1, v1), accu|
|
30
|
+
v1.each do |k2, v2|
|
31
|
+
if v2.is_a?(Rational)
|
32
|
+
accu[k1][k2] = { "numerator" => v2.numerator, "denominator" => v2.denominator }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def decode(table)
|
39
|
+
table.each_with_object(table.dup) do |(k1, v1), accu|
|
40
|
+
v1.each do |k2, v2|
|
41
|
+
if v2.is_a?(Hash)
|
42
|
+
accu[k1][k2] = Rational(v2["numerator"], v2["denominator"])
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -1,14 +1,29 @@
|
|
1
1
|
class Measured::ConversionTableBuilder
|
2
2
|
attr_reader :units
|
3
3
|
|
4
|
-
def initialize(units)
|
4
|
+
def initialize(units, cache: nil)
|
5
5
|
@units = units
|
6
|
+
cache ||= { class: Measured::Cache::Null }
|
7
|
+
@cache = cache[:class].new(*cache[:args])
|
6
8
|
end
|
7
9
|
|
8
10
|
def to_h
|
9
|
-
|
11
|
+
return @cache.read if cached?
|
12
|
+
generate_table
|
13
|
+
end
|
14
|
+
|
15
|
+
def update_cache
|
16
|
+
@cache.write(generate_table)
|
17
|
+
end
|
18
|
+
|
19
|
+
def cached?
|
20
|
+
@cache.exist?
|
21
|
+
end
|
10
22
|
|
11
|
-
|
23
|
+
private
|
24
|
+
|
25
|
+
def generate_table
|
26
|
+
units.map(&:name).each_with_object({}) do |to_unit, table|
|
12
27
|
to_table = {to_unit => Rational(1, 1)}
|
13
28
|
|
14
29
|
table.each do |from_unit, from_table|
|
@@ -19,12 +34,8 @@ class Measured::ConversionTableBuilder
|
|
19
34
|
|
20
35
|
table[to_unit] = to_table
|
21
36
|
end
|
22
|
-
|
23
|
-
table
|
24
37
|
end
|
25
38
|
|
26
|
-
private
|
27
|
-
|
28
39
|
def find_conversion(to:, from:)
|
29
40
|
conversion = find_direct_conversion_cached(to: to, from: from) || find_tree_traversal_conversion(to: to, from: from)
|
30
41
|
|
@@ -34,13 +45,13 @@ class Measured::ConversionTableBuilder
|
|
34
45
|
end
|
35
46
|
|
36
47
|
def find_direct_conversion_cached(to:, from:)
|
37
|
-
@
|
38
|
-
@
|
48
|
+
@direct_conversion_cache ||= {}
|
49
|
+
@direct_conversion_cache[to] ||= {}
|
39
50
|
|
40
|
-
if @
|
41
|
-
@
|
51
|
+
if @direct_conversion_cache[to].key?(from)
|
52
|
+
@direct_conversion_cache[to][from]
|
42
53
|
else
|
43
|
-
@
|
54
|
+
@direct_conversion_cache[to][from] = find_direct_conversion(to: to, from: from)
|
44
55
|
end
|
45
56
|
end
|
46
57
|
|
data/lib/measured/unit_system.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
class Measured::UnitSystem
|
2
2
|
attr_reader :units
|
3
3
|
|
4
|
-
def initialize(units)
|
4
|
+
def initialize(units, cache: nil)
|
5
5
|
@units = units.map { |unit| unit.with_unit_system(self) }
|
6
|
-
@conversion_table_builder = Measured::ConversionTableBuilder.new(@units)
|
6
|
+
@conversion_table_builder = Measured::ConversionTableBuilder.new(@units, cache: cache)
|
7
7
|
end
|
8
8
|
|
9
9
|
def unit_names_with_aliases
|
@@ -41,6 +41,14 @@ class Measured::UnitSystem
|
|
41
41
|
value.to_r * conversion
|
42
42
|
end
|
43
43
|
|
44
|
+
def update_cache
|
45
|
+
@conversion_table_builder.update_cache
|
46
|
+
end
|
47
|
+
|
48
|
+
def cached?
|
49
|
+
@conversion_table_builder.cached?
|
50
|
+
end
|
51
|
+
|
44
52
|
protected
|
45
53
|
|
46
54
|
def conversion_table
|
@@ -1,6 +1,7 @@
|
|
1
1
|
class Measured::UnitSystemBuilder
|
2
2
|
def initialize
|
3
3
|
@units = []
|
4
|
+
@cache = nil
|
4
5
|
end
|
5
6
|
|
6
7
|
def unit(unit_name, aliases: [], value: nil)
|
@@ -13,8 +14,13 @@ class Measured::UnitSystemBuilder
|
|
13
14
|
nil
|
14
15
|
end
|
15
16
|
|
17
|
+
def cache(cache_class, *args)
|
18
|
+
@cache = {class: cache_class, args: args}
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
|
16
22
|
def build
|
17
|
-
Measured::UnitSystem.new(@units)
|
23
|
+
Measured::UnitSystem.new(@units, cache: @cache)
|
18
24
|
end
|
19
25
|
|
20
26
|
private
|
@@ -12,4 +12,6 @@ Measured::Volume = Measured.build do
|
|
12
12
|
unit :us_pt, value: "0.125 us_gal", aliases: [:us_pint, :us_pints]
|
13
13
|
unit :oz, value: "0.00625 gal", aliases: [:fl_oz, :imp_fl_oz, :imperial_fluid_ounce, :imperial_fluid_ounces]
|
14
14
|
unit :us_oz, value: "0.0078125 us_gal", aliases: [:us_fl_oz, :us_fluid_ounce, :us_fluid_ounces]
|
15
|
-
|
15
|
+
|
16
|
+
cache Measured::Cache::Json, "volume.json"
|
17
|
+
end
|
@@ -8,4 +8,6 @@ Measured::Weight = Measured.build do
|
|
8
8
|
unit :short_ton, value: "2000 lb", aliases: [:short_tons]
|
9
9
|
unit :lb, value: "0.45359237 kg", aliases: [:lbs, :pound, :pounds]
|
10
10
|
unit :oz, value: "1/16 lb", aliases: [:ounce, :ounces]
|
11
|
+
|
12
|
+
cache Measured::Cache::Json, "weight.json"
|
11
13
|
end
|
data/lib/measured/version.rb
CHANGED
data/measured.gemspec
CHANGED
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
|
-
spec.add_runtime_dependency "activesupport", ">=
|
21
|
+
spec.add_runtime_dependency "activesupport", ">= 5.0"
|
22
22
|
|
23
23
|
spec.add_development_dependency "rake", "> 10.0"
|
24
24
|
spec.add_development_dependency "minitest", "> 5.5.1"
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class Measured::Cache::JsonTest < ActiveSupport::TestCase
|
4
|
+
setup do
|
5
|
+
@cache = Measured::Cache::Json.new("test.json")
|
6
|
+
@table_json = { "a" => { "b" => { "numerator" => 2, "denominator" => 3 } } }.to_json
|
7
|
+
@table_hash = { "a" => { "b" => Rational(2, 3) } }
|
8
|
+
end
|
9
|
+
|
10
|
+
test "#initialize sets the filename and path" do
|
11
|
+
assert_equal "test.json", @cache.filename
|
12
|
+
assert_match(/.+\/cache\/test\.json$/, @cache.path.to_s)
|
13
|
+
refute_match "../", @cache.path.to_s
|
14
|
+
end
|
15
|
+
|
16
|
+
test "#exist? returns false if the file does not exist" do
|
17
|
+
File.expects(:exist?).with(@cache.path).returns(false)
|
18
|
+
refute_predicate @cache, :exist?
|
19
|
+
end
|
20
|
+
|
21
|
+
test "#exist? returns true if the file exists" do
|
22
|
+
File.expects(:exist?).with(@cache.path).returns(true)
|
23
|
+
assert_predicate @cache, :exist?
|
24
|
+
end
|
25
|
+
|
26
|
+
test "#read returns nil if the file does not exist" do
|
27
|
+
File.expects(:exist?).with(@cache.path).returns(false)
|
28
|
+
assert_nil @cache.read
|
29
|
+
end
|
30
|
+
|
31
|
+
test "#read loads the file if it exists" do
|
32
|
+
File.expects(:exist?).with(@cache.path).returns(true)
|
33
|
+
File.expects(:read).with(@cache.path).returns(@table_json)
|
34
|
+
assert_equal @table_hash, @cache.read
|
35
|
+
end
|
36
|
+
|
37
|
+
test "#write raises not implemented" do
|
38
|
+
assert_raises(ArgumentError) do
|
39
|
+
@cache.write({})
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class Measured::Cache::JsonWriterTest < ActiveSupport::TestCase
|
4
|
+
class JsonTestWithWriter < Measured::Cache::Json
|
5
|
+
prepend Measured::Cache::JsonWriter
|
6
|
+
end
|
7
|
+
|
8
|
+
setup do
|
9
|
+
@cache = JsonTestWithWriter.new("test.json")
|
10
|
+
@table_json = JSON.pretty_generate({ "a" => { "b" => { "numerator" => 2, "denominator" => 3 } } })
|
11
|
+
@table_hash = { "a" => { "b" => Rational(2, 3) } }
|
12
|
+
end
|
13
|
+
|
14
|
+
test "#write writes the file" do
|
15
|
+
f = stub
|
16
|
+
f.expects(:write).with("// Do not modify this file directly. Regenerate it with 'rake cache:write'.\n")
|
17
|
+
f.expects(:write).with(@table_json)
|
18
|
+
|
19
|
+
File.expects(:open).with(@cache.path, "w").returns(123).yields(f)
|
20
|
+
assert_equal 123, @cache.write(@table_hash)
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class Measured::Cache::NullTest < ActiveSupport::TestCase
|
4
|
+
setup do
|
5
|
+
@cache = Measured::Cache::Null.new
|
6
|
+
end
|
7
|
+
|
8
|
+
test "#exist? false" do
|
9
|
+
assert_equal false, @cache.exist?
|
10
|
+
end
|
11
|
+
|
12
|
+
test "#read returns nil" do
|
13
|
+
assert_nil @cache.read
|
14
|
+
end
|
15
|
+
|
16
|
+
test "#write returns nil" do
|
17
|
+
assert_nil @cache.write({})
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
# In general we do not want to expose the inner workings of the caching and unit systems as part of the API.
|
4
|
+
# 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
|
5
|
+
# in the unit systems. So we compromise by reaching into implementation to force compare them.
|
6
|
+
class Measured::CacheConsistencyTest < ActiveSupport::TestCase
|
7
|
+
measurable_subclasses.select { |m| m.unit_system.cached? }.each do |measurable|
|
8
|
+
test "cached measurable #{ measurable } is not out of sync with the definition" do
|
9
|
+
builder = measurable.unit_system.instance_variable_get("@conversion_table_builder")
|
10
|
+
cache = builder.instance_variable_get("@cache")
|
11
|
+
|
12
|
+
expected = builder.send(:generate_table)
|
13
|
+
actual = cache.read
|
14
|
+
|
15
|
+
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."
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -115,4 +115,22 @@ class Measured::ConversionTableBuilderTest < ActiveSupport::TestCase
|
|
115
115
|
assert_instance_of Rational, value
|
116
116
|
end
|
117
117
|
end
|
118
|
+
|
119
|
+
test "#cached? returns true if there's a cache" do
|
120
|
+
builder = Measured::ConversionTableBuilder.new([Measured::Unit.new(:test)], cache: { class: AlwaysTrueCache })
|
121
|
+
assert_predicate builder, :cached?
|
122
|
+
end
|
123
|
+
|
124
|
+
test "#cached? returns false if there is not a cache" do
|
125
|
+
builder = Measured::ConversionTableBuilder.new([Measured::Unit.new(:test)])
|
126
|
+
refute_predicate builder, :cached?
|
127
|
+
end
|
128
|
+
|
129
|
+
test "#write_cache pushes the generated table into the cache and writes it" do
|
130
|
+
builder = Measured::ConversionTableBuilder.new([Measured::Unit.new(:test)], cache: { class: AlwaysTrueCache })
|
131
|
+
AlwaysTrueCache.any_instance.expects(:exist?).returns(false)
|
132
|
+
table = builder.to_h
|
133
|
+
AlwaysTrueCache.any_instance.expects(:write).with(table).returns(123)
|
134
|
+
assert_equal 123, builder.update_cache
|
135
|
+
end
|
118
136
|
end
|
data/test/measurable_test.rb
CHANGED
@@ -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
|