measured 2.4.0 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/dev.yml CHANGED
@@ -2,7 +2,7 @@ name: measured
2
2
 
3
3
  up:
4
4
  - ruby:
5
- version: 2.3.1
5
+ version: 2.6.3
6
6
  - bundler
7
7
 
8
8
  commands:
@@ -2,4 +2,4 @@ source 'https://rubygems.org'
2
2
 
3
3
  gemspec path: '..'
4
4
 
5
- gem 'activesupport', '~> 4.2'
5
+ gem 'activesupport', '~> 5.0'
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec path: '..'
4
+
5
+ gem 'activesupport', '~> 5.1'
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec path: '..'
4
+
5
+ gem 'activesupport', '~> 6.0.0.rc1'
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
@@ -0,0 +1,8 @@
1
+ module Measured::Cache::JsonWriter
2
+ def write(table)
3
+ File.open(@path, "w") do |f|
4
+ f.write("// Do not modify this file directly. Regenerate it with 'rake cache:write'.\n")
5
+ f.write(JSON.pretty_generate(encode(table)))
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,15 @@
1
+ module Measured::Cache
2
+ class Null
3
+ def exist?
4
+ false
5
+ end
6
+
7
+ def read
8
+ nil
9
+ end
10
+
11
+ def write(table)
12
+ nil
13
+ end
14
+ end
15
+ 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
- table = {}
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
- units.map{|u| u.name}.each do |to_unit|
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
- @cache ||= {}
38
- @cache[to] ||= {}
48
+ @direct_conversion_cache ||= {}
49
+ @direct_conversion_cache[to] ||= {}
39
50
 
40
- if @cache[to].key?(from)
41
- @cache[to][from]
51
+ if @direct_conversion_cache[to].key?(from)
52
+ @direct_conversion_cache[to][from]
42
53
  else
43
- @cache[to][from] = find_direct_conversion(to: to, from: from)
54
+ @direct_conversion_cache[to][from] = find_direct_conversion(to: to, from: from)
44
55
  end
45
56
  end
46
57
 
@@ -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
@@ -5,4 +5,6 @@ Measured::Length = Measured.build do
5
5
  unit :ft, value: "12 in", aliases: [:foot, :feet]
6
6
  unit :yd, value: "3 ft", aliases: [:yard, :yards]
7
7
  unit :mi, value: "5280 ft", aliases: [:mile, :miles]
8
+
9
+ cache Measured::Cache::Json, "length.json"
8
10
  end
@@ -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
- end
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
@@ -1,3 +1,3 @@
1
1
  module Measured
2
- VERSION = "2.4.0"
2
+ VERSION = "2.5.0"
3
3
  end
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", ">= 4.2"
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
@@ -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.new(0)
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.new(0)
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.new(0) }
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.new(0) }
301
+ assert_raises(ArgumentError) { @magic < BigDecimal(0) }
302
302
  assert_raises(ArgumentError) { @magic < 0.00 }
303
303
  end
304
304
  end