measured 2.4.0 → 2.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|